diff --git a/examples/cards/templates/todo-item-live/template.html b/examples/cards/templates/todo-item-live/template.html index 2859dcd..9b3ab52 100644 --- a/examples/cards/templates/todo-item-live/template.html +++ b/examples/cards/templates/todo-item-live/template.html @@ -87,57 +87,111 @@ .task-card__topline { display: flex; - align-items: flex-start; + position: relative; + z-index: 4; + align-items: center; justify-content: space-between; gap: 10px; } + .task-card__lane-button { + appearance: none; + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + padding: 0; + border: 0; + background: transparent; + font: inherit; + color: inherit; + cursor: pointer; + } + .task-card__lane { min-width: 0; - font-size: 0.69rem; + font-size: 0.64rem; line-height: 1.1; - letter-spacing: 0.14em; + letter-spacing: 0.11em; text-transform: uppercase; color: var(--task-muted); font-weight: 700; + white-space: nowrap; + } + + .task-card__lane-caret { + flex: 0 0 auto; + font-family: 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace); + font-size: 0.66rem; + line-height: 1; + color: var(--task-muted); + transform: translateY(-1px); + transition: transform 0.18s ease; + } + + .task-card__lane-button[data-open='1'] .task-card__lane-caret { + transform: translateY(-1px) rotate(180deg); + } + + .task-card__lane-button:disabled { + cursor: default; + opacity: 0.55; } .task-card__stamp { display: none; flex: 0 0 auto; align-items: center; - justify-content: center; + justify-content: flex-end; white-space: nowrap; - border-radius: 999px; - padding: 5px 8px; - font-size: 0.68rem; + padding: 0; + font-size: 0.72rem; line-height: 1; font-weight: 700; - border: 1px solid rgba(0, 0, 0, 0.04); + letter-spacing: 0.08em; + color: var(--task-muted); + background: transparent; + border: 0; } .task-card__title { font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif; - font-size: 1.08rem; - line-height: 1.1; + font-size: 0.96rem; + line-height: 1.06; font-weight: 700; - letter-spacing: -0.01em; + letter-spacing: -0.008em; color: var(--task-ink); text-wrap: balance; word-break: break-word; + cursor: pointer; + touch-action: manipulation; } .task-card__tags { display: none; - flex-wrap: wrap; + flex-wrap: nowrap; gap: 6px; + position: relative; + z-index: 1; + width: 100%; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; + -ms-overflow-style: none; + -webkit-overflow-scrolling: touch; + overscroll-behavior-x: contain; + } + + .task-card__tags::-webkit-scrollbar { + display: none; } .task-card__tag { + appearance: none; display: inline-flex; + flex: 0 0 auto; align-items: center; min-height: 24px; - max-width: 100%; border-radius: 999px; padding: 4px 9px; background: var(--task-accent-soft); @@ -148,6 +202,28 @@ border: 1px solid rgba(0, 0, 0, 0.035); overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; + cursor: default; + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + touch-action: manipulation; + } + + .task-card__tag:disabled { + cursor: default; + opacity: 0.6; + } + + .task-card__tag--action { + border-style: dashed; + background: rgba(255, 248, 239, 0.74); + cursor: pointer; + } + + .task-card__tag--holding { + background: rgba(165, 95, 75, 0.18); + color: #7b2f20; } .task-card__body { @@ -159,6 +235,13 @@ letter-spacing: 0.005em; color: #624d40; opacity: 0.95; + cursor: pointer; + touch-action: manipulation; + } + + .task-card__body--placeholder { + opacity: 0.62; + font-style: italic; } .task-card__meta { @@ -213,28 +296,221 @@ .task-card__move { display: none; - flex-wrap: wrap; + position: fixed; + top: 0; + left: 0; + z-index: 9999; + flex-direction: column; gap: 6px; + min-width: 150px; + padding: 6px; + border-radius: 14px; + background: rgba(255, 248, 239, 0.96); + box-shadow: + 0 10px 24px rgba(79, 56, 43, 0.12), + inset 0 1px 0 rgba(255, 255, 255, 0.72); } .task-card__move-button { appearance: none; border: 0; - border-radius: 999px; - padding: 7px 10px; + border-radius: 10px; + padding: 8px 10px; background: rgba(255, 248, 239, 0.78); color: var(--task-button-ink); font: 700 0.7rem/1 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace); cursor: pointer; border: 1px solid rgba(0, 0, 0, 0.05); + text-align: left; + } + + .task-card__editor-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 10001; + padding: + max(18px, env(safe-area-inset-top)) + max(16px, env(safe-area-inset-right)) + max(18px, env(safe-area-inset-bottom)) + max(16px, env(safe-area-inset-left)); + background: rgba(38, 27, 21, 0.42); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-sizing: border-box; + } + + .task-card__editor-sheet { + width: 100%; + max-width: 100%; + min-width: 0; + height: 100%; + display: grid; + grid-template-rows: auto 1fr auto; + gap: 14px; + border-radius: 22px; + border: 1px solid rgba(87, 65, 50, 0.16); + background: + radial-gradient(circle at top right, rgba(255, 255, 255, 0.74), transparent 28%), + linear-gradient(160deg, rgba(254, 246, 237, 0.985), rgba(240, 226, 210, 0.985)); + color: var(--task-ink); + box-shadow: + 0 22px 48px rgba(48, 32, 24, 0.24), + inset 0 1px 0 rgba(255, 255, 255, 0.7); + padding: 18px 16px 16px; + box-sizing: border-box; + overflow: hidden; + } + + .task-card__editor-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-width: 0; + } + + .task-card__editor-kicker { + font-size: 0.66rem; + line-height: 1.1; + letter-spacing: 0.12em; + text-transform: uppercase; + font-weight: 700; + color: var(--task-muted); + } + + .task-card__editor-title { + margin-top: 3px; + font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif; + font-size: 1.08rem; + line-height: 1.04; + font-weight: 700; + letter-spacing: -0.012em; + color: var(--task-ink); + min-width: 0; + overflow-wrap: anywhere; + } + + .task-card__editor-close { + appearance: none; + border: 0; + background: transparent; + color: var(--task-muted); + font: 700 0.86rem/1 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace); + letter-spacing: 0.02em; + padding: 6px 4px; + cursor: pointer; + flex: 0 0 auto; + } + + .task-card__editor-fields { + min-height: 0; + min-width: 0; + display: grid; + grid-template-rows: auto auto 1fr; + gap: 12px; + } + + .task-card__editor-group { + display: grid; + gap: 5px; + min-height: 0; + min-width: 0; + } + + .task-card__editor-label { + font-size: 0.66rem; + line-height: 1.1; + letter-spacing: 0.12em; + text-transform: uppercase; + font-weight: 700; + color: var(--task-muted); + } + + .task-card__editor-input, + .task-card__editor-textarea { + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; + border-radius: 16px; + border: 1px solid rgba(87, 65, 50, 0.14); + background: rgba(255, 251, 246, 0.92); + color: var(--task-ink); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78); + outline: none; + } + + .task-card__editor-input { + min-height: 52px; + padding: 12px 13px; + font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif; + font-size: 1.02rem; + line-height: 1.1; + font-weight: 700; + letter-spacing: -0.012em; + } + + .task-card__editor-textarea { + min-height: 0; + height: 100%; + max-height: 100%; + padding: 12px 13px; + resize: none; + font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif; + font-size: 0.94rem; + line-height: 1.36; + font-weight: 400; + letter-spacing: 0.004em; + } + + .task-card__editor-input:focus, + .task-card__editor-textarea:focus { + border-color: rgba(88, 112, 111, 0.48); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.82), + 0 0 0 3px rgba(88, 112, 111, 0.12); + } + + .task-card__editor-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + min-width: 0; + flex-wrap: wrap; + } + + .task-card__editor-action-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + flex-wrap: wrap; + justify-content: flex-end; + margin-left: auto; + } + + .task-card__editor-action-row > .task-card__button { + flex: 0 0 auto; }
-
Task
+ +
Loading...
@@ -243,15 +519,8 @@
-
-
- - -
- -
@@ -261,29 +530,27 @@ const root = script?.closest('[data-nanobot-card-root]'); const state = window.__nanobotGetCardState?.(script) || {}; if (!(root instanceof HTMLElement)) return; + const doc = root.ownerDocument || document; + const view = doc.defaultView || window; const cardEl = root.querySelector('[data-task-item-card]'); + const laneToggleEl = root.querySelector('[data-task-lane-toggle]'); const subtitleEl = root.querySelector('[data-task-subtitle]'); const statusEl = root.querySelector('[data-task-status]'); const summaryEl = root.querySelector('[data-task-summary]'); const tagsEl = root.querySelector('[data-task-tags]'); const dueEl = root.querySelector('[data-task-due]'); - const ageEl = root.querySelector('[data-task-age]'); const descriptionEl = root.querySelector('[data-task-description]'); - const primaryEl = root.querySelector('[data-task-primary]'); - const moveToggleEl = root.querySelector('[data-task-move-toggle]'); const moveMenuEl = root.querySelector('[data-task-move-menu]'); if ( !(cardEl instanceof HTMLElement) || + !(laneToggleEl instanceof HTMLButtonElement) || !(subtitleEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement) || !(summaryEl instanceof HTMLElement) || !(tagsEl instanceof HTMLElement) || !(dueEl instanceof HTMLElement) || - !(ageEl instanceof HTMLElement) || !(descriptionEl instanceof HTMLElement) || - !(primaryEl instanceof HTMLButtonElement) || - !(moveToggleEl instanceof HTMLButtonElement) || !(moveMenuEl instanceof HTMLElement) ) return; @@ -351,18 +618,9 @@ canceled: 'Cancel', }; - const primaryActionForLane = (value) => { - if (value === 'backlog') return { lane: 'in-progress', label: 'Start' }; - if (value === 'in-progress') return { lane: 'done', label: 'Done' }; - if (value === 'blocked') return { lane: 'in-progress', label: 'Resume' }; - return null; - }; - const moveOptionsForLane = (value) => { - const primary = primaryActionForLane(value); return ['backlog', 'in-progress', 'blocked', 'done', 'canceled'] .filter((targetLane) => targetLane !== value) - .filter((targetLane) => !primary || targetLane !== primary.lane) .map((targetLane) => ({ lane: targetLane, label: actionLabels[targetLane] || targetLane, @@ -379,8 +637,10 @@ const setStatus = (label, fg, bg) => { statusEl.textContent = label; - statusEl.style.color = fg; - statusEl.style.background = bg; + statusEl.style.color = fg || 'var(--task-muted)'; + statusEl.style.background = bg && bg !== 'transparent' ? bg : 'transparent'; + statusEl.style.border = bg && bg !== 'transparent' ? '1px solid rgba(0, 0, 0, 0.04)' : '0'; + statusEl.style.padding = bg && bg !== 'transparent' ? '5px 8px' : '0'; statusEl.style.display = label ? 'inline-flex' : 'none'; }; @@ -400,7 +660,7 @@ const computeScore = () => { const now = Date.now(); const dueMs = parseDueTimeMs(); - let score = 70; + let score = 54; if (Number.isFinite(dueMs)) { const hoursUntilDue = (dueMs - now) / (60 * 60 * 1000); if (hoursUntilDue <= 0) score = 100; @@ -413,12 +673,12 @@ const createdMs = parseCreatedTimeMs(); if (Number.isFinite(createdMs)) { const ageDays = Math.max(0, (now - createdMs) / (24 * 60 * 60 * 1000)); - if (ageDays >= 30) score = 94; - else if (ageDays >= 21) score = 90; - else if (ageDays >= 14) score = 86; - else if (ageDays >= 7) score = 82; - else if (ageDays >= 3) score = 78; - else if (ageDays >= 1) score = 74; + if (ageDays >= 30) score = 80; + else if (ageDays >= 21) score = 76; + else if (ageDays >= 14) score = 72; + else if (ageDays >= 7) score = 68; + else if (ageDays >= 3) score = 62; + else if (ageDays >= 1) score = 58; } } if (lane === 'blocked') return Math.min(100, score + 4); @@ -437,16 +697,6 @@ return `Due ${parsed.toLocaleDateString([], { month: 'short', day: 'numeric' })}`; }; - const formatAge = () => { - const createdMs = parseCreatedTimeMs(); - if (!Number.isFinite(createdMs)) return ''; - const days = Math.floor((Date.now() - createdMs) / (24 * 60 * 60 * 1000)); - if (days <= 0) return 'New today'; - if (days < 7) return `${days}d old`; - if (days < 30) return `${Math.floor(days / 7)}w old`; - return `${Math.floor(days / 30)}mo old`; - }; - const summarizeBody = () => { if (!body) return ''; const trimmed = body.trim(); @@ -496,21 +746,141 @@ } }; - const setBusy = (busy) => { - primaryEl.disabled = busy; - moveToggleEl.disabled = busy; - for (const button of moveMenuEl.querySelectorAll('button')) { - if (button instanceof HTMLButtonElement) button.disabled = busy; + const parseToolPayload = (result) => { + if (result && typeof result === 'object' && result.parsed && typeof result.parsed === 'object') { + return result.parsed; + } + const raw = typeof result?.content === 'string' ? result.content : ''; + if (!raw.trim()) return null; + try { + return JSON.parse(raw); + } catch { + return null; } }; - const refreshCards = () => { + const editorOverlayEl = doc.createElement('div'); + editorOverlayEl.className = 'task-card__editor-overlay'; + editorOverlayEl.innerHTML = ` + + `; + const editorFormEl = editorOverlayEl.querySelector('[data-task-editor-form]'); + const editorTitleInputEl = editorOverlayEl.querySelector('[data-task-editor-title-input]'); + const editorBodyInputEl = editorOverlayEl.querySelector('[data-task-editor-body-input]'); + const editorCloseEl = editorOverlayEl.querySelector('[data-task-editor-close]'); + const editorCancelEl = editorOverlayEl.querySelector('[data-task-editor-cancel]'); + const editorSaveEl = editorOverlayEl.querySelector('[data-task-editor-save]'); + if ( + !(editorFormEl instanceof HTMLFormElement) || + !(editorTitleInputEl instanceof HTMLInputElement) || + !(editorBodyInputEl instanceof HTMLTextAreaElement) || + !(editorCloseEl instanceof HTMLButtonElement) || + !(editorCancelEl instanceof HTMLButtonElement) || + !(editorSaveEl instanceof HTMLButtonElement) + ) return; + + const setBusy = (busy) => { + laneToggleEl.disabled = busy || !taskPath; + for (const button of moveMenuEl.querySelectorAll('button')) { + if (button instanceof HTMLButtonElement) button.disabled = busy; + } + for (const button of tagsEl.querySelectorAll('button')) { + if (button instanceof HTMLButtonElement) button.disabled = busy; + } + editorTitleInputEl.disabled = busy; + editorBodyInputEl.disabled = busy; + editorCloseEl.disabled = busy; + editorCancelEl.disabled = busy; + editorSaveEl.disabled = busy; + }; + + const closeMoveMenu = () => { moveMenuEl.style.display = 'none'; - moveToggleEl.textContent = 'Move'; - moveToggleEl.setAttribute('aria-expanded', 'false'); + laneToggleEl.setAttribute('aria-expanded', 'false'); + laneToggleEl.dataset.open = '0'; + }; + + const positionMoveMenu = () => { + if (moveMenuEl.style.display !== 'flex') return; + const rect = laneToggleEl.getBoundingClientRect(); + const menuRect = moveMenuEl.getBoundingClientRect(); + const gutter = 12; + let left = rect.left; + let top = rect.bottom + 6; + if (left + menuRect.width > view.innerWidth - gutter) { + left = Math.max(gutter, view.innerWidth - gutter - menuRect.width); + } + if (top + menuRect.height > view.innerHeight - gutter) { + top = Math.max(gutter, rect.top - menuRect.height - 6); + } + moveMenuEl.style.left = `${Math.round(left)}px`; + moveMenuEl.style.top = `${Math.round(top)}px`; + }; + + const openMoveMenu = () => { + if (laneToggleEl.disabled) return; + moveMenuEl.style.display = 'flex'; + moveMenuEl.style.visibility = 'hidden'; + moveMenuEl.style.left = '0px'; + moveMenuEl.style.top = '0px'; + positionMoveMenu(); + moveMenuEl.style.visibility = 'visible'; + laneToggleEl.setAttribute('aria-expanded', 'true'); + laneToggleEl.dataset.open = '1'; + }; + + const refreshCards = () => { + closeMoveMenu(); window.dispatchEvent(new Event('nanobot:cards-refresh')); }; + const closeEditor = () => { + editorOverlayEl.style.display = 'none'; + }; + + const openEditor = (focusField = 'title') => { + if (!taskPath) return; + closeMoveMenu(); + editorTitleInputEl.value = title; + editorBodyInputEl.value = body; + if (editorOverlayEl.parentElement !== doc.body) { + doc.body.appendChild(editorOverlayEl); + } + editorOverlayEl.style.display = 'block'; + view.requestAnimationFrame(() => { + const target = focusField === 'description' ? editorBodyInputEl : editorTitleInputEl; + target.focus(); + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + const end = target.value.length; + target.setSelectionRange(end, end); + } + }); + }; + const runTransition = async (targetLane) => { if (!taskPath) return; setBusy(true); @@ -549,25 +919,132 @@ } }; + const runTagMutation = async (action, tagValue) => { + if (!taskPath) return; + const cleanedTag = String(tagValue || '').trim(); + if (!cleanedTag) return; + setBusy(true); + setStatus('Saving', '#6e5b4d', 'rgba(255, 244, 227, 0.9)'); + try { + const result = await window.__nanobotCallTool?.('task_board', { + action, + task: taskPath, + tags: [cleanedTag], + }); + const payload = parseToolPayload(result); + if (payload && typeof payload === 'object' && payload.error) { + throw new Error(String(payload.error)); + } + refreshCards(); + } catch (error) { + console.error(`Task tag mutation failed (${action})`, error); + setBusy(false); + setStatus('Unavailable', '#8e3023', '#f3d3cc'); + publishLiveContent(lane, true, String(error)); + } + }; + + const runTaskEdit = async (changes) => { + if (!taskPath) return; + setBusy(true); + setStatus('Saving', '#6e5b4d', 'rgba(255, 244, 227, 0.9)'); + try { + const result = await window.__nanobotCallTool?.('task_board', { + action: 'edit', + task: taskPath, + ...changes, + }); + const payload = parseToolPayload(result); + if (payload && typeof payload === 'object' && payload.error) { + throw new Error(String(payload.error)); + } + closeEditor(); + refreshCards(); + } catch (error) { + console.error('Task edit failed', error); + setBusy(false); + setStatus('Unavailable', '#8e3023', '#f3d3cc'); + publishLiveContent(lane, true, String(error)); + } + }; + + const promptForTag = async () => { + if (!taskPath) return; + const value = window.prompt('Add tag to task', ''); + if (value == null) return; + const cleaned = value.trim(); + if (!cleaned) return; + await runTagMutation('add_tag', cleaned); + }; + + const bindTagRemoval = (button, tagValue) => { + let holdTimer = null; + let holdTriggered = false; + const clearHold = () => { + if (holdTimer !== null) { + window.clearTimeout(holdTimer); + holdTimer = null; + } + button.classList.remove('task-card__tag--holding'); + }; + + button.addEventListener('pointerdown', (event) => { + if (!taskPath || button.disabled) return; + if (event.pointerType === 'mouse' && event.button !== 0) return; + holdTriggered = false; + button.classList.add('task-card__tag--holding'); + holdTimer = window.setTimeout(async () => { + holdTimer = null; + holdTriggered = true; + button.classList.remove('task-card__tag--holding'); + const confirmed = window.confirm(`Remove ${tagValue} from this task?`); + if (!confirmed) return; + await runTagMutation('remove_tag', tagValue); + }, 650); + }); + + for (const eventName of ['pointerup', 'pointerleave', 'pointercancel']) { + button.addEventListener(eventName, clearHold); + } + + button.addEventListener('contextmenu', (event) => { + event.preventDefault(); + }); + + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + if (holdTriggered) { + holdTriggered = false; + } + }); + }; + const renderTags = () => { tagsEl.innerHTML = ''; - if (!tags.length) { - tagsEl.style.display = 'none'; - return; - } - const visibleTags = tags.slice(0, 4); - for (const tag of visibleTags) { - const chip = document.createElement('span'); + for (const tag of tags) { + const chip = document.createElement('button'); + chip.type = 'button'; chip.className = 'task-card__tag'; chip.textContent = tag; + chip.title = `Hold to remove ${tag}`; + bindTagRemoval(chip, tag); tagsEl.appendChild(chip); } - if (tags.length > visibleTags.length) { - const overflow = document.createElement('span'); - overflow.className = 'task-card__tag'; - overflow.textContent = `+${tags.length - visibleTags.length}`; - tagsEl.appendChild(overflow); - } + + const addTagButton = document.createElement('button'); + addTagButton.type = 'button'; + addTagButton.className = 'task-card__tag task-card__tag--action'; + addTagButton.textContent = '+'; + addTagButton.title = 'Add tag'; + addTagButton.disabled = !taskPath; + addTagButton.addEventListener('click', async (event) => { + event.preventDefault(); + event.stopPropagation(); + await promptForTag(); + }); + tagsEl.appendChild(addTagButton); + tagsEl.style.display = 'flex'; }; @@ -575,10 +1052,11 @@ moveMenuEl.innerHTML = ''; const options = moveOptionsForLane(lane); if (!options.length || !taskPath) { - moveToggleEl.style.display = 'none'; + laneToggleEl.disabled = true; + closeMoveMenu(); return; } - moveToggleEl.style.display = 'inline-flex'; + laneToggleEl.disabled = false; for (const option of options) { const button = document.createElement('button'); button.type = 'button'; @@ -593,56 +1071,94 @@ } }; - const renderPrimaryAction = () => { - const primary = primaryActionForLane(lane); - if (!primary || !taskPath) { - primaryEl.style.display = 'none'; - primaryEl.disabled = true; - return; - } - primaryEl.textContent = primary.label; - primaryEl.style.display = 'inline-flex'; - primaryEl.disabled = false; - primaryEl.onclick = (event) => { - event.preventDefault(); - event.stopPropagation(); - runTransition(primary.lane); - }; - }; - - moveToggleEl.addEventListener('click', (event) => { + laneToggleEl.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); const open = moveMenuEl.style.display === 'flex'; - moveMenuEl.style.display = open ? 'none' : 'flex'; - moveToggleEl.textContent = open ? 'Move' : 'Close'; - moveToggleEl.setAttribute('aria-expanded', open ? 'false' : 'true'); + if (open) closeMoveMenu(); + else openMoveMenu(); + }); + + doc.addEventListener('pointerdown', (event) => { + const target = event.target; + if (!(target instanceof Node)) return; + if (moveMenuEl.style.display !== 'flex') return; + if (moveMenuEl.contains(target) || laneToggleEl.contains(target)) return; + closeMoveMenu(); + }); + + view.addEventListener('resize', closeMoveMenu); + view.addEventListener('scroll', closeMoveMenu, true); + + if (moveMenuEl.parentElement !== doc.body) { + doc.body.appendChild(moveMenuEl); + } + + editorCloseEl.addEventListener('click', () => { + closeEditor(); + }); + + editorCancelEl.addEventListener('click', () => { + closeEditor(); + }); + + editorOverlayEl.addEventListener('pointerdown', (event) => { + if (event.target === editorOverlayEl) { + closeEditor(); + } + }); + + editorFormEl.addEventListener('submit', (event) => { + event.preventDefault(); + const nextTitle = editorTitleInputEl.value.trim(); + const nextDescription = editorBodyInputEl.value.trim(); + if (!nextTitle) { + editorTitleInputEl.focus(); + return; + } + if (nextTitle === title && nextDescription === body) { + closeEditor(); + return; + } + void runTaskEdit({ + title: nextTitle, + description: nextDescription, + }); + }); + + summaryEl.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + openEditor('title'); + }); + + descriptionEl.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + openEditor('description'); }); const render = () => { applyTheme(); subtitleEl.textContent = laneLabels[lane] || 'Task'; summaryEl.textContent = title || '(Untitled task)'; + summaryEl.title = taskPath ? 'Tap to edit title' : ''; const dueText = formatDue(); dueEl.textContent = dueText; dueEl.style.display = dueText ? 'inline-flex' : 'none'; - const ageText = formatAge(); - ageEl.textContent = ageText; - ageEl.style.display = ageText ? 'inline-flex' : 'none'; + setStatus('', 'var(--task-muted)', 'transparent'); const bodySummary = summarizeBody(); - descriptionEl.textContent = bodySummary; - descriptionEl.style.display = bodySummary ? 'block' : 'none'; + descriptionEl.textContent = bodySummary || 'Add description'; + descriptionEl.title = taskPath ? 'Tap to edit description' : ''; + descriptionEl.style.display = taskPath ? 'block' : 'none'; + descriptionEl.classList.toggle('task-card__body--placeholder', !bodySummary); renderTags(); - setStatus('', '', 'transparent'); - renderPrimaryAction(); renderMoveMenu(); - moveMenuEl.style.display = 'none'; - moveToggleEl.textContent = 'Move'; - moveToggleEl.setAttribute('aria-expanded', 'false'); + closeMoveMenu(); publishLiveContent(lane, true, ''); };