feat: apps view kanban + drag-and-drop across all columns; done cards no strikethrough
This commit is contained in:
parent
022a934785
commit
597d3dc997
154
index.html
154
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() {
|
|||
? `<button class="add-col-btn" data-add-status="${st.id}">
|
||||
<i data-lucide="plus" style="width:12px;height:12px;stroke-width:2.5"></i>追加
|
||||
</button>` : '';
|
||||
html += `<div class="col col-${st.id}">
|
||||
html += `<div class="col col-${st.id}" data-status="${st.id}">
|
||||
<div class="col-header">
|
||||
<i data-lucide="${st.icon}" style="width:13px;height:13px;stroke-width:2;opacity:.55"></i>
|
||||
<span class="col-label">${st.label}</span>
|
||||
<span class="col-count">${col.length}</span>
|
||||
</div>
|
||||
${col.length ? col.map(boardCard).join('') : '<div class="empty-col">なし</div>'}
|
||||
${col.length ? col.map(t => boardCard(t, true)).join('') : '<div class="empty-col">なし</div>'}
|
||||
${addBtn}
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -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)
|
||||
? `<div class="task-hold-note">${esc(t.note)}</div>` : '';
|
||||
const commit = t.commit
|
||||
? `<a class="task-commit" href="https://github.com/posimai/${t.appId}/commit/${t.commit}" target="_blank" rel="noopener">${t.commit.slice(0,7)}</a>` : '';
|
||||
const date = (done && t.done_at) ? `<span class="task-date">${t.done_at}</span>` : '';
|
||||
return `<div class="task-card${done?' task-done':''}" data-id="${t.id}" data-app="${t.appId}" role="button" tabindex="0">
|
||||
<div class="task-app-chip">${shortName(t.appId)}</div>
|
||||
const appChip = showApp ? `<div class="task-app-chip">${shortName(t.appId)}</div>` : '';
|
||||
return `<div class="task-card${done?' task-done':''}" data-id="${t.id}" data-app="${t.appId}" draggable="true" role="button" tabindex="0">
|
||||
${appChip}
|
||||
<div class="task-title">${esc(t.title)}</div>
|
||||
${note}
|
||||
<div class="task-meta">
|
||||
|
|
@ -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 = `<div class="app-view-header">
|
||||
<div class="app-view-icon">
|
||||
|
|
@ -1056,60 +1060,27 @@ function renderAppView(appId) {
|
|||
}
|
||||
html += '</div>';
|
||||
|
||||
let anyTask = false;
|
||||
html += '<div class="board">';
|
||||
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 += `<div class="status-group sg-${st.id}${collapsed}" data-sg="${st.id}">
|
||||
<div class="status-group-header">
|
||||
<i data-lucide="${st.icon}" style="width:13px;height:13px;stroke-width:2;opacity:.6"></i>
|
||||
<span class="status-group-label">${st.label}</span>
|
||||
<span class="status-group-count">${tasks.length}</span>
|
||||
<i data-lucide="chevron-down" style="width:13px;height:13px;stroke-width:2;opacity:.4" class="sg-chevron"></i>
|
||||
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'
|
||||
? `<button class="add-col-btn" data-add-status="${st.id}" data-add-app="${appId}">
|
||||
<i data-lucide="plus" style="width:12px;height:12px;stroke-width:2.5"></i>追加
|
||||
</button>` : '';
|
||||
html += `<div class="col col-${st.id}" data-status="${st.id}">
|
||||
<div class="col-header">
|
||||
<i data-lucide="${st.icon}" style="width:13px;height:13px;stroke-width:2;opacity:.55"></i>
|
||||
<span class="col-label">${st.label}</span>
|
||||
<span class="col-count">${col.length}</span>
|
||||
</div>
|
||||
<div class="status-group-body">`;
|
||||
|
||||
if (!tasks.length) {
|
||||
html += `<div style="padding:6px 0;font-size:12px;color:var(--text3)">なし</div>`;
|
||||
}
|
||||
for (const t of tasks) {
|
||||
const done = t.status === 'done';
|
||||
const note = t.note ? `<div class="app-task-note">${esc(t.note)}</div>` : '';
|
||||
const commit = t.commit
|
||||
? `<a class="task-commit" href="https://github.com/posimai/${appId}/commit/${t.commit}" target="_blank" rel="noopener">${t.commit.slice(0,7)}</a>` : '';
|
||||
html += `<div class="app-task-row${done?' app-task-done':''}" data-id="${t.id}" data-app="${appId}" role="button" tabindex="0">
|
||||
<div class="app-task-content">
|
||||
<div class="app-task-title">${esc(t.title)}</div>
|
||||
${note}
|
||||
<div class="task-meta" style="margin-top:3px">
|
||||
<span class="task-by">${esc(t.by||'mai')}</span>
|
||||
${t.done_at?`<span class="task-date">${t.done_at}</span>`:''}
|
||||
${commit}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (st.id !== 'done') {
|
||||
html += `<button class="add-task-row-btn" data-add-status="${st.id}" data-add-app="${appId}">
|
||||
<i data-lucide="plus" style="width:12px;height:12px;stroke-width:2.5"></i>タスクを追加
|
||||
</button>`;
|
||||
}
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
if (!anyTask) {
|
||||
html += `<div class="app-view-empty">
|
||||
<i data-lucide="inbox" style="width:32px;height:32px;stroke-width:1.25"></i>
|
||||
<div style="font-size:13px;color:var(--text2)">タスクなし</div>
|
||||
<button class="add-task-row-btn" data-add-status="next" data-add-app="${appId}" style="max-width:200px">
|
||||
<i data-lucide="plus" style="width:12px;height:12px;stroke-width:2.5"></i>最初のタスクを追加
|
||||
</button>
|
||||
${col.length ? col.map(t => boardCard(t, false)).join('') : '<div class="empty-col">なし</div>'}
|
||||
${addBtn}
|
||||
</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
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');
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue