From 597d3dc997031e20d1604c77fecaa42f4eae5886 Mon Sep 17 00:00:00 2001 From: posimai Date: Sun, 22 Mar 2026 22:47:23 +0900 Subject: [PATCH] feat: apps view kanban + drag-and-drop across all columns; done cards no strikethrough --- index.html | 154 ++++++++++++++++++++++++++++------------------------- 1 file changed, 81 insertions(+), 73 deletions(-) diff --git a/index.html b/index.html index 224b215..58c69d4 100644 --- a/index.html +++ b/index.html @@ -268,12 +268,16 @@ .task-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } .task-done { opacity: 0.45; } .task-done:hover { opacity: 0.7; } + .task-card[draggable] { cursor: grab; } + .task-card[draggable]:active { cursor: grabbing; } + .task-card.dragging { opacity: 0.3; transform: rotate(1.5deg); box-shadow: 0 8px 24px rgba(0,0,0,0.35); pointer-events: none; } + .col.drag-over { outline: 2px dashed color-mix(in srgb, var(--accent) 55%, transparent); outline-offset: -4px; background: color-mix(in srgb, var(--accent) 6%, transparent); border-radius: 10px; } .task-app-chip { font-size: 10px; font-weight: 700; color: var(--text3); text-transform: uppercase; letter-spacing: 0.07em; } .task-title { font-size: 13px; font-weight: 500; color: var(--text); line-height: 1.45; } - .task-done .task-title { text-decoration: line-through; } + .task-done .task-title { color: var(--text3); } .task-hold-note { font-size: 11px; color: var(--text3); line-height: 1.4; background: var(--surface2); border-radius: 6px; padding: 5px 8px; @@ -298,15 +302,15 @@ .empty-col { padding: 14px 8px; text-align: center; color: var(--text3); font-size: 12px; opacity: 0.7; } /* ── App task view ── */ - #view-app { max-width: 720px; margin: 0 auto; padding: 16px; } + #view-app.active { display: flex; flex-direction: column; } .app-view-header { display: flex; align-items: center; gap: 10px; - padding-bottom: 14px; + padding: 16px 16px 14px; border-bottom: 1px solid var(--border); - margin-bottom: 16px; + flex-shrink: 0; } .app-view-icon { width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0; @@ -1004,13 +1008,13 @@ function renderBoard() { ? `` : ''; - html += `
+ html += `
${st.label} ${col.length}
- ${col.length ? col.map(boardCard).join('') : '
なし
'} + ${col.length ? col.map(t => boardCard(t, true)).join('') : '
なし
'} ${addBtn}
`; } @@ -1018,15 +1022,16 @@ function renderBoard() { document.getElementById('view-board').innerHTML = html; } -function boardCard(t) { +function boardCard(t, showApp = true) { const done = t.status === 'done'; const note = (t.status === 'hold' && t.note) ? `
${esc(t.note)}
` : ''; const commit = t.commit ? `${t.commit.slice(0,7)}` : ''; const date = (done && t.done_at) ? `${t.done_at}` : ''; - return `
-
${shortName(t.appId)}
+ const appChip = showApp ? `
${shortName(t.appId)}
` : ''; + return `
+ ${appChip}
${esc(t.title)}
${note}
@@ -1043,8 +1048,7 @@ function renderAppView(appId) { : data.apps.find(a => a.id === appId); if (!appEntry) { document.getElementById('view-app').innerHTML = ''; return; } - const statusOrder = { next: 0, backlog: 1, hold: 2, done: 3 }; - const sorted = [...appEntry.tasks].sort((a, b) => (statusOrder[a.status]??9) - (statusOrder[b.status]??9)); + const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 30); let html = `
@@ -1056,60 +1060,27 @@ function renderAppView(appId) { } html += '
'; - let anyTask = false; + html += '
'; for (const st of STATUSES) { - const tasks = sorted.filter(t => t.status === st.id); - if (st.id !== 'done' && tasks.length === 0) continue; - anyTask = true; - - const collapsed = st.id === 'done' && tasks.length > 0 ? ' collapsed' : ''; - html += `
-
- - ${st.label} - ${tasks.length} - + let col = appEntry.tasks.filter(t => t.status === st.id); + if (st.id === 'done') { + col = col.filter(t => !t.done_at || new Date(t.done_at) >= cutoff).slice(0, 12); + } + const addBtn = st.id !== 'done' + ? `` : ''; + html += `
+
+ + ${st.label} + ${col.length}
-
`; - - if (!tasks.length) { - html += `
なし
`; - } - for (const t of tasks) { - const done = t.status === 'done'; - const note = t.note ? `
${esc(t.note)}
` : ''; - const commit = t.commit - ? `${t.commit.slice(0,7)}` : ''; - html += `
-
-
${esc(t.title)}
- ${note} -
- ${esc(t.by||'mai')} - ${t.done_at?`${t.done_at}`:''} - ${commit} -
-
-
`; - } - - if (st.id !== 'done') { - html += ``; - } - html += '
'; - } - - if (!anyTask) { - html += `
- -
タスクなし
- + ${col.length ? col.map(t => boardCard(t, false)).join('') : '
なし
'} + ${addBtn}
`; } + html += '
'; document.getElementById('view-app').innerHTML = html; } @@ -1363,6 +1334,52 @@ function closeSettings() { document.getElementById('settingsOpenBtn').setAttribute('aria-expanded', 'false'); } +// ── Drag & Drop ──────────────────────────────────────────────────────────────── +let dndId = null; +let dndApp = null; + +function attachDnd() { + const main = document.getElementById('main'); + main.addEventListener('dragstart', e => { + const card = e.target.closest('.task-card[draggable]'); + if (!card) return; + dndId = card.dataset.id; + dndApp = card.dataset.app; + e.dataTransfer.effectAllowed = 'move'; + setTimeout(() => card.classList.add('dragging'), 0); + }); + main.addEventListener('dragend', () => { + document.querySelectorAll('.task-card.dragging').forEach(el => el.classList.remove('dragging')); + document.querySelectorAll('.col.drag-over').forEach(el => el.classList.remove('drag-over')); + }); + main.addEventListener('dragover', e => { + const col = e.target.closest('.col[data-status]'); + if (!col || !dndId) return; + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + document.querySelectorAll('.col.drag-over').forEach(el => el !== col && el.classList.remove('drag-over')); + col.classList.add('drag-over'); + }); + main.addEventListener('dragleave', e => { + const col = e.target.closest('.col[data-status]'); + if (col && !col.contains(e.relatedTarget)) col.classList.remove('drag-over'); + }); + main.addEventListener('drop', e => { + const col = e.target.closest('.col[data-status]'); + if (!col || !dndId || !dndApp) return; + e.preventDefault(); + col.classList.remove('drag-over'); + const newStatus = col.dataset.status; + const t = findTask(dndId, dndApp); + if (!t || t.status === newStatus) { dndId = dndApp = null; return; } + t.status = newStatus; + if (newStatus === 'done' && !t.done_at) t.done_at = new Date().toISOString().slice(0, 10); + persist(); renderAll(); + showToast(newStatus === 'done' ? '完了にしました' : `→ ${newStatus}`); + dndId = dndApp = null; + }); +} + // ── Event delegation ─────────────────────────────────────────────────────────── document.addEventListener('click', e => { // Hamburger @@ -1399,17 +1416,7 @@ document.addEventListener('click', e => { openTaskModal({ taskId: card.dataset.id, appId: card.dataset.app }); return; } - // App view task row - const row = e.target.closest('.app-task-row[data-id]'); - if (row && !e.target.closest('a')) { - openTaskModal({ taskId: row.dataset.id, appId: row.dataset.app }); return; - } - - // Status group header toggle - const sgHeader = e.target.closest('.status-group-header'); - if (sgHeader) { sgHeader.closest('.status-group')?.classList.toggle('collapsed'); return; } - - // Add task buttons (board col + app view row) + // Add task buttons (board col + app view) const addBtn = e.target.closest('[data-add-status]'); if (addBtn) { openTaskModal({ defaultStatus: addBtn.dataset.addStatus, defaultApp: addBtn.dataset.addApp || null }); return; @@ -1477,7 +1484,7 @@ document.addEventListener('click', e => { document.addEventListener('keydown', e => { if (e.key === 'Escape') { closeTaskModal(); closeIdeaModal(); closeSettings(); closeSidebar(); } - if (e.key === 'Enter' && (e.target.matches('.task-card') || e.target.matches('.idea-card') || e.target.matches('.app-task-row'))) { + if (e.key === 'Enter' && (e.target.matches('.task-card') || e.target.matches('.idea-card'))) { e.target.click(); } }); @@ -1493,6 +1500,7 @@ document.getElementById('appSearch').addEventListener('input', e => { loadData().then(() => { initTheme(); renderAll(); + attachDnd(); if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js'); });