nanobot-voice-interface/examples/cards/templates/todo-item-live/template.html

1168 lines
34 KiB
HTML
Raw Normal View History

2026-03-17 11:38:00 -04:00
<style>
@font-face {
font-family: 'M-1m Code';
src: url('/card-templates/todo-item-live/assets/mplus-1m-regular-sub.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'M-1m Code';
src: url('/card-templates/todo-item-live/assets/mplus-1m-bold-sub.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IBM Plex Sans Condensed';
src: url('/card-templates/todo-item-live/assets/ibm-plex-sans-condensed-400.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IBM Plex Sans Condensed';
src: url('/card-templates/todo-item-live/assets/ibm-plex-sans-condensed-600.ttf') format('truetype');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'IBM Plex Sans Condensed';
src: url('/card-templates/todo-item-live/assets/ibm-plex-sans-condensed-700.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
.task-card {
--task-accent: #58706f;
--task-accent-soft: rgba(88, 112, 111, 0.12);
--task-ink: #2f241e;
--task-muted: #7e6659;
--task-surface: rgba(255, 248, 239, 0.92);
--task-surface-strong: rgba(250, 238, 221, 0.95);
--task-border: rgba(87, 65, 50, 0.14);
--task-button-ink: #214240;
position: relative;
overflow: hidden;
border-radius: 20px;
border: 1px solid var(--task-border);
background:
radial-gradient(circle at top right, rgba(255, 255, 255, 0.72), transparent 32%),
linear-gradient(145deg, rgba(253, 245, 235, 0.98), rgba(242, 227, 211, 0.97));
color: var(--task-ink);
font-family: 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.68),
0 18px 36px rgba(79, 56, 43, 0.12);
}
.task-card::before {
content: '';
position: absolute;
inset: 0;
background:
repeating-linear-gradient(
135deg,
rgba(122, 97, 78, 0.035) 0,
rgba(122, 97, 78, 0.035) 2px,
transparent 2px,
transparent 10px
);
pointer-events: none;
opacity: 0.55;
}
.task-card__inner {
position: relative;
display: grid;
gap: 10px;
padding: 15px 14px 13px 14px;
}
.task-card__topline {
display: flex;
position: relative;
z-index: 4;
align-items: center;
2026-03-17 11:38:00 -04:00
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;
}
2026-03-17 11:38:00 -04:00
.task-card__lane {
min-width: 0;
font-size: 0.64rem;
2026-03-17 11:38:00 -04:00
line-height: 1.1;
letter-spacing: 0.11em;
2026-03-17 11:38:00 -04:00
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;
2026-03-17 11:38:00 -04:00
}
.task-card__stamp {
display: none;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
2026-03-17 11:38:00 -04:00
white-space: nowrap;
padding: 0;
font-size: 0.72rem;
2026-03-17 11:38:00 -04:00
line-height: 1;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--task-muted);
background: transparent;
border: 0;
2026-03-17 11:38:00 -04:00
}
.task-card__title {
font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
font-size: 0.96rem;
line-height: 1.06;
2026-03-17 11:38:00 -04:00
font-weight: 700;
letter-spacing: -0.008em;
2026-03-17 11:38:00 -04:00
color: var(--task-ink);
text-wrap: balance;
word-break: break-word;
cursor: pointer;
touch-action: manipulation;
2026-03-17 11:38:00 -04:00
}
.task-card__tags {
display: none;
flex-wrap: nowrap;
2026-03-17 11:38:00 -04:00
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;
2026-03-17 11:38:00 -04:00
}
.task-card__tag {
appearance: none;
2026-03-17 11:38:00 -04:00
display: inline-flex;
flex: 0 0 auto;
2026-03-17 11:38:00 -04:00
align-items: center;
min-height: 24px;
border-radius: 999px;
padding: 4px 9px;
background: var(--task-accent-soft);
color: var(--task-button-ink);
font-size: 0.71rem;
line-height: 1;
font-weight: 700;
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;
2026-03-17 11:38:00 -04:00
}
.task-card__body {
display: none;
font-family: 'IBM Plex Sans Condensed', 'Arial Narrow', sans-serif;
font-size: 0.86rem;
line-height: 1.34;
font-weight: 400;
letter-spacing: 0.005em;
color: #624d40;
opacity: 0.95;
cursor: pointer;
touch-action: manipulation;
}
.task-card__body--placeholder {
opacity: 0.62;
font-style: italic;
2026-03-17 11:38:00 -04:00
}
.task-card__meta {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.task-card__meta-chip {
display: none;
align-items: center;
min-height: 25px;
border-radius: 999px;
padding: 5px 9px;
background: var(--task-surface);
color: var(--task-muted);
font-size: 0.69rem;
line-height: 1;
font-weight: 700;
border: 1px solid rgba(0, 0, 0, 0.04);
}
.task-card__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 1px;
}
.task-card__button {
appearance: none;
border: 0;
border-radius: 999px;
padding: 8px 11px;
background: var(--task-accent);
color: #f9f4ed;
font: 700 0.74rem/1 'M-1m Code', var(--card-font, 'SF Mono', ui-monospace, Menlo, Consolas, monospace);
letter-spacing: 0.02em;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.task-card__button--secondary {
background: var(--task-surface-strong);
color: var(--task-button-ink);
}
.task-card__button:disabled {
cursor: default;
opacity: 0.55;
}
.task-card__move {
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
flex-direction: column;
2026-03-17 11:38:00 -04:00
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);
2026-03-17 11:38:00 -04:00
}
.task-card__move-button {
appearance: none;
border: 0;
border-radius: 10px;
padding: 8px 10px;
2026-03-17 11:38:00 -04:00
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;
2026-03-17 11:38:00 -04:00
}
</style>
<div data-task-item-card class="task-card">
<div class="task-card__inner">
<div class="task-card__topline">
<button
data-task-lane-toggle
type="button"
class="task-card__lane-button"
aria-expanded="false"
data-open="0"
>
<span data-task-subtitle class="task-card__lane">Task</span>
<span class="task-card__lane-caret"></span>
</button>
2026-03-17 11:38:00 -04:00
<span data-task-status class="task-card__stamp"></span>
<div data-task-move-menu class="task-card__move"></div>
</div>
2026-03-17 11:38:00 -04:00
<div data-task-summary class="task-card__title">Loading...</div>
<div data-task-tags class="task-card__tags"></div>
<div data-task-description class="task-card__body"></div>
2026-03-17 11:38:00 -04:00
<div class="task-card__meta">
<div data-task-due class="task-card__meta-chip"></div>
</div>
</div>
</div>
2026-03-17 11:38:00 -04:00
<script>
(() => {
const script = document.currentScript;
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;
2026-03-17 11:38:00 -04:00
const cardEl = root.querySelector('[data-task-item-card]');
const laneToggleEl = root.querySelector('[data-task-lane-toggle]');
2026-03-17 11:38:00 -04:00
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 descriptionEl = root.querySelector('[data-task-description]');
const moveMenuEl = root.querySelector('[data-task-move-menu]');
if (
!(cardEl instanceof HTMLElement) ||
!(laneToggleEl instanceof HTMLButtonElement) ||
2026-03-17 11:38:00 -04:00
!(subtitleEl instanceof HTMLElement) ||
!(statusEl instanceof HTMLElement) ||
!(summaryEl instanceof HTMLElement) ||
!(tagsEl instanceof HTMLElement) ||
!(dueEl instanceof HTMLElement) ||
!(descriptionEl instanceof HTMLElement) ||
!(moveMenuEl instanceof HTMLElement)
) return;
const taskPath = typeof state.task_path === 'string' ? state.task_path.trim() : '';
const taskKey = typeof state.task_key === 'string' ? state.task_key.trim() : '';
const title = typeof state.title === 'string' ? state.title.trim() : '';
const lane = typeof state.lane === 'string' ? state.lane.trim() : 'backlog';
const created = typeof state.created === 'string' ? state.created.trim() : '';
const updated = typeof state.updated === 'string' ? state.updated.trim() : '';
const due = typeof state.due === 'string' ? state.due.trim() : '';
2026-03-17 11:38:00 -04:00
const body = typeof state.body === 'string' ? state.body.trim() : '';
const tags = Array.isArray(state.tags)
? state.tags.map((value) => String(value || '').trim()).filter(Boolean)
: [];
const metadata = state && typeof state.metadata === 'object' && state.metadata && !Array.isArray(state.metadata)
? state.metadata
: {};
const laneLabels = {
backlog: 'Backlog',
'in-progress': 'In Progress',
blocked: 'Blocked',
done: 'Done',
canceled: 'Canceled',
};
const laneThemes = {
backlog: {
accent: '#5f7884',
accentSoft: 'rgba(95, 120, 132, 0.13)',
muted: '#6b7e87',
buttonInk: '#294a57',
},
'in-progress': {
accent: '#4f7862',
accentSoft: 'rgba(79, 120, 98, 0.13)',
muted: '#5e7768',
buttonInk: '#214437',
},
blocked: {
accent: '#a55f4b',
accentSoft: 'rgba(165, 95, 75, 0.13)',
muted: '#906659',
buttonInk: '#6c2f21',
},
done: {
accent: '#6d7f58',
accentSoft: 'rgba(109, 127, 88, 0.12)',
muted: '#6b755d',
buttonInk: '#304121',
},
canceled: {
accent: '#7b716a',
accentSoft: 'rgba(123, 113, 106, 0.12)',
muted: '#7b716a',
buttonInk: '#433932',
},
};
const actionLabels = {
backlog: 'Backlog',
'in-progress': 'Start',
blocked: 'Block',
done: 'Done',
canceled: 'Cancel',
};
const moveOptionsForLane = (value) => {
return ['backlog', 'in-progress', 'blocked', 'done', 'canceled']
.filter((targetLane) => targetLane !== value)
.map((targetLane) => ({
lane: targetLane,
label: actionLabels[targetLane] || targetLane,
}));
};
const applyTheme = () => {
const theme = laneThemes[lane] || laneThemes.backlog;
cardEl.style.setProperty('--task-accent', theme.accent);
cardEl.style.setProperty('--task-accent-soft', theme.accentSoft);
cardEl.style.setProperty('--task-muted', theme.muted);
cardEl.style.setProperty('--task-button-ink', theme.buttonInk);
};
const setStatus = (label, fg, bg) => {
statusEl.textContent = label;
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';
};
2026-03-17 11:38:00 -04:00
const parseDueTimeMs = () => {
if (!due) return null;
const raw = due.includes('T') ? due : `${due}T12:00:00`;
const parsed = new Date(raw).getTime();
return Number.isFinite(parsed) ? parsed : null;
};
const parseCreatedTimeMs = () => {
if (!created) return null;
const parsed = new Date(created).getTime();
return Number.isFinite(parsed) ? parsed : null;
};
const computeScore = () => {
const now = Date.now();
const dueMs = parseDueTimeMs();
let score = 54;
2026-03-17 11:38:00 -04:00
if (Number.isFinite(dueMs)) {
const hoursUntilDue = (dueMs - now) / (60 * 60 * 1000);
if (hoursUntilDue <= 0) score = 100;
else if (hoursUntilDue <= 6) score = 96;
else if (hoursUntilDue <= 24) score = 92;
else if (hoursUntilDue <= 72) score = 82;
else if (hoursUntilDue <= 168) score = 72;
else score = 62;
} else {
const createdMs = parseCreatedTimeMs();
if (Number.isFinite(createdMs)) {
const ageDays = Math.max(0, (now - createdMs) / (24 * 60 * 60 * 1000));
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;
}
}
2026-03-17 11:38:00 -04:00
if (lane === 'blocked') return Math.min(100, score + 4);
if (lane === 'in-progress') return Math.min(100, score + 2);
return score;
};
const formatDue = () => {
if (!due) return '';
const raw = due.includes('T') ? due : `${due}T00:00:00`;
const parsed = new Date(raw);
if (Number.isNaN(parsed.getTime())) return `Due ${due}`;
if (due.includes('T')) {
return `Due ${parsed.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}`;
}
2026-03-17 11:38:00 -04:00
return `Due ${parsed.toLocaleDateString([], { month: 'short', day: 'numeric' })}`;
};
const summarizeBody = () => {
if (!body) return '';
const trimmed = body.trim();
if (!trimmed || /^##\s+Imported\b/i.test(trimmed)) return '';
const [firstSection] = trimmed.split(/\n##\s+/);
const summary = firstSection
.split(/\n{2,}/)
.map((part) => part.trim())
.find(Boolean);
if (!summary) return '';
if (summary.toLowerCase() === title.toLowerCase()) return '';
return summary;
};
const publishLiveContent = (statusValue, exists = true, error = '') => {
window.__nanobotSetCardLiveContent?.(script, {
2026-03-17 11:38:00 -04:00
kind: 'file_task',
exists,
2026-03-17 11:38:00 -04:00
task_path: taskPath || null,
task_key: taskKey || null,
title: title || null,
lane,
created: created || null,
updated: updated || null,
due: due || null,
2026-03-17 11:38:00 -04:00
tags,
metadata,
score: computeScore(),
status: statusValue,
error: error || null,
});
};
2026-03-17 11:38:00 -04:00
const shellQuote = (value) => `'${String(value).replace(/'/g, `'\"'\"'`)}'`;
const parseExecPayload = (result) => {
if (result && typeof result === 'object' && result.parsed && typeof result.parsed === 'object') {
return result.parsed;
}
const raw = typeof result?.content === 'string' ? result.content : '';
const cleaned = raw.replace(/\nExit code:\s*\d+\s*$/i, '').trim();
if (!cleaned) return null;
try {
2026-03-17 11:38:00 -04:00
return JSON.parse(cleaned);
} catch {
2026-03-17 11:38:00 -04:00
return null;
}
};
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 editorOverlayEl = doc.createElement('div');
editorOverlayEl.className = 'task-card__editor-overlay';
editorOverlayEl.innerHTML = `
<div class="task-card__editor-sheet" role="dialog" aria-modal="true" aria-labelledby="task-editor-title">
<div class="task-card__editor-head">
<div>
<div class="task-card__editor-kicker">${laneLabels[lane] || 'Task'}</div>
<div id="task-editor-title" class="task-card__editor-title">Edit task</div>
</div>
<button type="button" class="task-card__editor-close" data-task-editor-close>Close</button>
</div>
<form class="task-card__editor-fields" data-task-editor-form>
<div class="task-card__editor-group">
<label class="task-card__editor-label" for="task-editor-title-input">Title</label>
<input id="task-editor-title-input" data-task-editor-title-input class="task-card__editor-input" type="text" maxlength="240" />
</div>
<div class="task-card__editor-group" style="min-height:0;">
<label class="task-card__editor-label" for="task-editor-body-input">Description</label>
<textarea id="task-editor-body-input" data-task-editor-body-input class="task-card__editor-textarea" placeholder="Add notes, context, or next steps"></textarea>
</div>
<div class="task-card__editor-actions">
<div class="task-card__editor-action-row">
<button type="button" class="task-card__button task-card__button--secondary" data-task-editor-cancel>Cancel</button>
<button type="submit" class="task-card__button" data-task-editor-save>Save</button>
</div>
</div>
</form>
</div>
`;
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;
2026-03-17 11:38:00 -04:00
const setBusy = (busy) => {
laneToggleEl.disabled = busy || !taskPath;
2026-03-17 11:38:00 -04:00
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;
2026-03-17 11:38:00 -04:00
};
const closeMoveMenu = () => {
2026-03-17 11:38:00 -04:00
moveMenuEl.style.display = 'none';
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();
2026-03-17 11:38:00 -04:00
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);
}
});
};
2026-03-17 11:38:00 -04:00
const runTransition = async (targetLane) => {
if (!taskPath) return;
setBusy(true);
setStatus('Saving', '#6e5b4d', 'rgba(255, 244, 227, 0.9)');
try {
const command = [
'python3',
'/home/kacper/nanobot/scripts/task_cards.py',
'move',
'--task',
shellQuote(taskPath),
'--lane',
shellQuote(targetLane),
].join(' ');
const result = await window.__nanobotCallToolAsync?.(
'exec',
{
command,
max_output_chars: 50000,
},
{
pollMs: 350,
timeoutMs: 30000,
},
);
const payload = parseExecPayload(result);
if (payload && typeof payload === 'object' && payload.sync && typeof payload.sync === 'object') {
publishLiveContent(targetLane, targetLane !== 'done' && targetLane !== 'canceled', '');
}
2026-03-17 11:38:00 -04:00
refreshCards();
} catch (error) {
console.error('Task transition failed', error);
setBusy(false);
setStatus('Unavailable', '#8e3023', '#f3d3cc');
publishLiveContent(lane, true, String(error));
}
2026-03-17 11:38:00 -04:00
};
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;
}
});
};
2026-03-17 11:38:00 -04:00
const renderTags = () => {
tagsEl.innerHTML = '';
for (const tag of tags) {
const chip = document.createElement('button');
chip.type = 'button';
2026-03-17 11:38:00 -04:00
chip.className = 'task-card__tag';
chip.textContent = tag;
chip.title = `Hold to remove ${tag}`;
bindTagRemoval(chip, tag);
2026-03-17 11:38:00 -04:00
tagsEl.appendChild(chip);
}
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);
2026-03-17 11:38:00 -04:00
tagsEl.style.display = 'flex';
};
2026-03-17 11:38:00 -04:00
const renderMoveMenu = () => {
moveMenuEl.innerHTML = '';
const options = moveOptionsForLane(lane);
if (!options.length || !taskPath) {
laneToggleEl.disabled = true;
closeMoveMenu();
2026-03-17 11:38:00 -04:00
return;
}
laneToggleEl.disabled = false;
2026-03-17 11:38:00 -04:00
for (const option of options) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'task-card__move-button';
button.textContent = option.label;
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
runTransition(option.lane);
});
moveMenuEl.appendChild(button);
}
};
laneToggleEl.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const open = moveMenuEl.style.display === 'flex';
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();
2026-03-17 11:38:00 -04:00
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');
});
2026-03-17 11:38:00 -04:00
const render = () => {
applyTheme();
subtitleEl.textContent = laneLabels[lane] || 'Task';
summaryEl.textContent = title || '(Untitled task)';
summaryEl.title = taskPath ? 'Tap to edit title' : '';
2026-03-17 11:38:00 -04:00
const dueText = formatDue();
dueEl.textContent = dueText;
dueEl.style.display = dueText ? 'inline-flex' : 'none';
setStatus('', 'var(--task-muted)', 'transparent');
2026-03-17 11:38:00 -04:00
const bodySummary = summarizeBody();
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);
2026-03-17 11:38:00 -04:00
renderTags();
renderMoveMenu();
closeMoveMenu();
2026-03-17 11:38:00 -04:00
publishLiveContent(lane, true, '');
};
render();
})();
</script>