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;
}
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 = `
+
+
+
+
${laneLabels[lane] || 'Task'}
+
Edit task
+
+
+
+
+
+ `;
+ 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, '');
};