feat: apps view kanban + drag-and-drop across all columns; done cards no strikethrough
This commit is contained in:
parent
022a934785
commit
597d3dc997
152
index.html
152
index.html
|
|
@ -268,12 +268,16 @@
|
||||||
.task-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
.task-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
.task-done { opacity: 0.45; }
|
.task-done { opacity: 0.45; }
|
||||||
.task-done:hover { opacity: 0.7; }
|
.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 {
|
.task-app-chip {
|
||||||
font-size: 10px; font-weight: 700;
|
font-size: 10px; font-weight: 700;
|
||||||
color: var(--text3); text-transform: uppercase; letter-spacing: 0.07em;
|
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-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 {
|
.task-hold-note {
|
||||||
font-size: 11px; color: var(--text3); line-height: 1.4;
|
font-size: 11px; color: var(--text3); line-height: 1.4;
|
||||||
background: var(--surface2); border-radius: 6px; padding: 5px 8px;
|
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; }
|
.empty-col { padding: 14px 8px; text-align: center; color: var(--text3); font-size: 12px; opacity: 0.7; }
|
||||||
|
|
||||||
/* ── App task view ── */
|
/* ── App task view ── */
|
||||||
#view-app { max-width: 720px; margin: 0 auto; padding: 16px; }
|
#view-app.active { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
.app-view-header {
|
.app-view-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding-bottom: 14px;
|
padding: 16px 16px 14px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
margin-bottom: 16px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.app-view-icon {
|
.app-view-icon {
|
||||||
width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0;
|
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}">
|
? `<button class="add-col-btn" data-add-status="${st.id}">
|
||||||
<i data-lucide="plus" style="width:12px;height:12px;stroke-width:2.5"></i>追加
|
<i data-lucide="plus" style="width:12px;height:12px;stroke-width:2.5"></i>追加
|
||||||
</button>` : '';
|
</button>` : '';
|
||||||
html += `<div class="col col-${st.id}">
|
html += `<div class="col col-${st.id}" data-status="${st.id}">
|
||||||
<div class="col-header">
|
<div class="col-header">
|
||||||
<i data-lucide="${st.icon}" style="width:13px;height:13px;stroke-width:2;opacity:.55"></i>
|
<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-label">${st.label}</span>
|
||||||
<span class="col-count">${col.length}</span>
|
<span class="col-count">${col.length}</span>
|
||||||
</div>
|
</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}
|
${addBtn}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -1018,15 +1022,16 @@ function renderBoard() {
|
||||||
document.getElementById('view-board').innerHTML = html;
|
document.getElementById('view-board').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function boardCard(t) {
|
function boardCard(t, showApp = true) {
|
||||||
const done = t.status === 'done';
|
const done = t.status === 'done';
|
||||||
const note = (t.status === 'hold' && t.note)
|
const note = (t.status === 'hold' && t.note)
|
||||||
? `<div class="task-hold-note">${esc(t.note)}</div>` : '';
|
? `<div class="task-hold-note">${esc(t.note)}</div>` : '';
|
||||||
const commit = t.commit
|
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>` : '';
|
? `<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>` : '';
|
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">
|
const appChip = showApp ? `<div class="task-app-chip">${shortName(t.appId)}</div>` : '';
|
||||||
<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>
|
<div class="task-title">${esc(t.title)}</div>
|
||||||
${note}
|
${note}
|
||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
|
|
@ -1043,8 +1048,7 @@ function renderAppView(appId) {
|
||||||
: data.apps.find(a => a.id === appId);
|
: data.apps.find(a => a.id === appId);
|
||||||
if (!appEntry) { document.getElementById('view-app').innerHTML = ''; return; }
|
if (!appEntry) { document.getElementById('view-app').innerHTML = ''; return; }
|
||||||
|
|
||||||
const statusOrder = { next: 0, backlog: 1, hold: 2, done: 3 };
|
const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 30);
|
||||||
const sorted = [...appEntry.tasks].sort((a, b) => (statusOrder[a.status]??9) - (statusOrder[b.status]??9));
|
|
||||||
|
|
||||||
let html = `<div class="app-view-header">
|
let html = `<div class="app-view-header">
|
||||||
<div class="app-view-icon">
|
<div class="app-view-icon">
|
||||||
|
|
@ -1056,60 +1060,27 @@ function renderAppView(appId) {
|
||||||
}
|
}
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
let anyTask = false;
|
html += '<div class="board">';
|
||||||
for (const st of STATUSES) {
|
for (const st of STATUSES) {
|
||||||
const tasks = sorted.filter(t => t.status === st.id);
|
let col = appEntry.tasks.filter(t => t.status === st.id);
|
||||||
if (st.id !== 'done' && tasks.length === 0) continue;
|
if (st.id === 'done') {
|
||||||
anyTask = true;
|
col = col.filter(t => !t.done_at || new Date(t.done_at) >= cutoff).slice(0, 12);
|
||||||
|
|
||||||
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>
|
|
||||||
</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 addBtn = st.id !== 'done'
|
||||||
const done = t.status === 'done';
|
? `<button class="add-col-btn" data-add-status="${st.id}" data-add-app="${appId}">
|
||||||
const note = t.note ? `<div class="app-task-note">${esc(t.note)}</div>` : '';
|
<i data-lucide="plus" style="width:12px;height:12px;stroke-width:2.5"></i>追加
|
||||||
const commit = t.commit
|
</button>` : '';
|
||||||
? `<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="col col-${st.id}" data-status="${st.id}">
|
||||||
html += `<div class="app-task-row${done?' app-task-done':''}" data-id="${t.id}" data-app="${appId}" role="button" tabindex="0">
|
<div class="col-header">
|
||||||
<div class="app-task-content">
|
<i data-lucide="${st.icon}" style="width:13px;height:13px;stroke-width:2;opacity:.55"></i>
|
||||||
<div class="app-task-title">${esc(t.title)}</div>
|
<span class="col-label">${st.label}</span>
|
||||||
${note}
|
<span class="col-count">${col.length}</span>
|
||||||
<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>
|
||||||
|
${col.length ? col.map(t => boardCard(t, false)).join('') : '<div class="empty-col">なし</div>'}
|
||||||
|
${addBtn}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
html += '</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>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('view-app').innerHTML = html;
|
document.getElementById('view-app').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
@ -1363,6 +1334,52 @@ function closeSettings() {
|
||||||
document.getElementById('settingsOpenBtn').setAttribute('aria-expanded', 'false');
|
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 ───────────────────────────────────────────────────────────
|
// ── Event delegation ───────────────────────────────────────────────────────────
|
||||||
document.addEventListener('click', e => {
|
document.addEventListener('click', e => {
|
||||||
// Hamburger
|
// Hamburger
|
||||||
|
|
@ -1399,17 +1416,7 @@ document.addEventListener('click', e => {
|
||||||
openTaskModal({ taskId: card.dataset.id, appId: card.dataset.app }); return;
|
openTaskModal({ taskId: card.dataset.id, appId: card.dataset.app }); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App view task row
|
// Add task buttons (board col + app view)
|
||||||
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)
|
|
||||||
const addBtn = e.target.closest('[data-add-status]');
|
const addBtn = e.target.closest('[data-add-status]');
|
||||||
if (addBtn) {
|
if (addBtn) {
|
||||||
openTaskModal({ defaultStatus: addBtn.dataset.addStatus, defaultApp: addBtn.dataset.addApp || null }); return;
|
openTaskModal({ defaultStatus: addBtn.dataset.addStatus, defaultApp: addBtn.dataset.addApp || null }); return;
|
||||||
|
|
@ -1477,7 +1484,7 @@ document.addEventListener('click', e => {
|
||||||
|
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
if (e.key === 'Escape') { closeTaskModal(); closeIdeaModal(); closeSettings(); closeSidebar(); }
|
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();
|
e.target.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1493,6 +1500,7 @@ document.getElementById('appSearch').addEventListener('input', e => {
|
||||||
loadData().then(() => {
|
loadData().then(() => {
|
||||||
initTheme();
|
initTheme();
|
||||||
renderAll();
|
renderAll();
|
||||||
|
attachDnd();
|
||||||
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js');
|
if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue