176 lines
7.8 KiB
HTML
176 lines
7.8 KiB
HTML
<div data-todo-item-card style="font-family: var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); background:#f7ecdf; color:#65483a; padding:12px 14px;">
|
|
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:10px;">
|
|
<div style="min-width:0; flex:1 1 auto;">
|
|
<div data-todo-subtitle style="font-size:0.78rem; line-height:1.25; color:#9a7b68; font-weight:700; text-transform:uppercase; letter-spacing:0.08em;">Todo</div>
|
|
<div data-todo-summary style="margin-top:6px; font-size:1.08rem; line-height:1.15; font-weight:800; letter-spacing:-0.02em; color:#65483a; word-break:break-word;">Loading...</div>
|
|
</div>
|
|
<span data-todo-status style="display:none; font-size:0.78rem; line-height:1.2; font-weight:800; color:#9a6a2f; background:#f4e2b8; white-space:nowrap; border-radius:999px; padding:4px 8px;"></span>
|
|
</div>
|
|
|
|
<div data-todo-due style="display:none; margin-top:8px; font-size:0.82rem; line-height:1.35; color:#947662; font-weight:700;"></div>
|
|
<div data-todo-description style="display:none; margin-top:8px; font-size:0.82rem; line-height:1.45; color:#7d5f4e;"></div>
|
|
|
|
<div style="margin-top:10px; display:flex; align-items:center; gap:10px;">
|
|
<button data-todo-complete type="button" style="display:none; border:none; border-radius:999px; padding:6px 10px; background:#dfe9d8; color:#35562c; font:800 0.76rem/1 var(--card-font, 'Iosevka', 'SF Mono', ui-monospace, Menlo, Consolas, monospace); cursor:pointer;">Done</button>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(() => {
|
|
const script = document.currentScript;
|
|
const root = script?.closest('[data-nanobot-card-root]');
|
|
const state = window.__nanobotGetCardState?.(script) || {};
|
|
if (!(root instanceof HTMLElement)) return;
|
|
|
|
const subtitleEl = root.querySelector('[data-todo-subtitle]');
|
|
const statusEl = root.querySelector('[data-todo-status]');
|
|
const summaryEl = root.querySelector('[data-todo-summary]');
|
|
const dueEl = root.querySelector('[data-todo-due]');
|
|
const descriptionEl = root.querySelector('[data-todo-description]');
|
|
const completeEl = root.querySelector('[data-todo-complete]');
|
|
if (!(subtitleEl instanceof HTMLElement) || !(statusEl instanceof HTMLElement) || !(summaryEl instanceof HTMLElement) || !(dueEl instanceof HTMLElement) || !(descriptionEl instanceof HTMLElement) || !(completeEl instanceof HTMLButtonElement)) return;
|
|
|
|
const sourceId = typeof state.source_id === 'string' ? state.source_id.trim() : '';
|
|
const entityId = typeof state.entity_id === 'string' ? state.entity_id.trim() : '';
|
|
const uid = typeof state.uid === 'string' ? state.uid.trim() : '';
|
|
const summary = typeof state.summary === 'string' ? state.summary.trim() : '';
|
|
const listName = typeof state.list_name === 'string' ? state.list_name.trim() : '';
|
|
const rawStatus = typeof state.status === 'string' ? state.status.trim() : 'needs_action';
|
|
const completed = Boolean(state.completed) || rawStatus === 'completed';
|
|
const due = typeof state.due === 'string' ? state.due.trim() : '';
|
|
const dueDateTime = typeof state.due_datetime === 'string' ? state.due_datetime.trim() : '';
|
|
const description = typeof state.description === 'string' ? state.description.trim() : '';
|
|
const configuredCompleteTool = typeof state.complete_tool_name === 'string' ? state.complete_tool_name.trim() : '';
|
|
const completeItem = typeof state.complete_item === 'string' ? state.complete_item.trim() : summary;
|
|
const canComplete = Boolean(state.can_complete) && Boolean(listName) && Boolean(completeItem) && !completed;
|
|
|
|
const setStatus = (label, fg, bg) => {
|
|
statusEl.textContent = label;
|
|
statusEl.style.color = fg;
|
|
statusEl.style.background = bg;
|
|
statusEl.style.display = label ? 'inline-flex' : 'none';
|
|
};
|
|
|
|
const formatDue = () => {
|
|
if (dueDateTime) {
|
|
const parsed = new Date(dueDateTime);
|
|
if (!Number.isNaN(parsed.getTime())) {
|
|
return `Due ${parsed.toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}`;
|
|
}
|
|
return `Due ${dueDateTime}`;
|
|
}
|
|
if (due) {
|
|
const parsed = new Date(`${due}T00:00:00`);
|
|
if (!Number.isNaN(parsed.getTime())) {
|
|
return `Due ${parsed.toLocaleDateString([], { month: 'short', day: 'numeric' })}`;
|
|
}
|
|
return `Due ${due}`;
|
|
}
|
|
return completed ? 'Completed' : '';
|
|
};
|
|
|
|
const publishLiveContent = (statusValue, exists = true, error = '') => {
|
|
window.__nanobotSetCardLiveContent?.(script, {
|
|
kind: 'ha_todo_item',
|
|
exists,
|
|
source_id: sourceId || null,
|
|
entity_id: entityId || null,
|
|
list_name: listName || null,
|
|
uid: uid || null,
|
|
status: statusValue,
|
|
summary: summary || null,
|
|
due: due || null,
|
|
due_datetime: dueDateTime || null,
|
|
description: description || null,
|
|
complete_tool_name: configuredCompleteTool || null,
|
|
can_complete: canComplete,
|
|
error: error || null,
|
|
});
|
|
};
|
|
|
|
const resolveCompleteToolName = async () => {
|
|
if (configuredCompleteTool) return configuredCompleteTool;
|
|
if (!window.__nanobotListTools) return 'mcp_home_assistant_HassListCompleteItem';
|
|
try {
|
|
const tools = await window.__nanobotListTools();
|
|
const completeTool = Array.isArray(tools)
|
|
? tools.find((tool) => /(^|_)HassListCompleteItem$/i.test(String(tool?.name || '')))
|
|
: null;
|
|
return completeTool?.name || 'mcp_home_assistant_HassListCompleteItem';
|
|
} catch {
|
|
return 'mcp_home_assistant_HassListCompleteItem';
|
|
}
|
|
};
|
|
|
|
const render = () => {
|
|
subtitleEl.textContent = listName || 'Todo';
|
|
summaryEl.textContent = summary || '(Untitled task)';
|
|
const dueText = formatDue();
|
|
dueEl.textContent = dueText;
|
|
dueEl.style.display = dueText ? 'block' : 'none';
|
|
descriptionEl.textContent = description;
|
|
descriptionEl.style.display = description ? 'block' : 'none';
|
|
|
|
if (completed) {
|
|
setStatus('Done', '#6c8b63', '#dfe9d8');
|
|
summaryEl.style.textDecoration = 'line-through';
|
|
summaryEl.style.color = '#7d5f4e';
|
|
completeEl.style.display = 'none';
|
|
completeEl.disabled = true;
|
|
} else {
|
|
setStatus('', '', 'transparent');
|
|
summaryEl.style.textDecoration = 'none';
|
|
summaryEl.style.color = '#65483a';
|
|
if (canComplete) {
|
|
completeEl.style.display = 'inline-flex';
|
|
completeEl.disabled = false;
|
|
} else {
|
|
completeEl.style.display = 'none';
|
|
completeEl.disabled = true;
|
|
}
|
|
}
|
|
|
|
|
|
publishLiveContent(completed ? 'completed' : rawStatus, true, '');
|
|
};
|
|
|
|
const requestCardResync = async () => {
|
|
if (!sourceId) return;
|
|
await fetch('/cards/sync', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ source_id: sourceId }),
|
|
});
|
|
window.dispatchEvent(new Event('nanobot:cards-refresh'));
|
|
};
|
|
|
|
completeEl.addEventListener('click', async (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (!canComplete) return;
|
|
completeEl.disabled = true;
|
|
setStatus('Saving', '#9a7b68', 'rgba(231, 220, 209, 0.92)');
|
|
try {
|
|
const completeToolName = await resolveCompleteToolName();
|
|
await window.__nanobotCallTool?.(completeToolName, {
|
|
name: listName,
|
|
item: completeItem,
|
|
});
|
|
setStatus('Done', '#6c8b63', '#dfe9d8');
|
|
summaryEl.style.textDecoration = 'line-through';
|
|
summaryEl.style.color = '#7d5f4e';
|
|
dueEl.textContent = 'Completed';
|
|
dueEl.style.display = 'block';
|
|
completeEl.style.display = 'none';
|
|
publishLiveContent('completed', true, '');
|
|
await requestCardResync();
|
|
} catch (error) {
|
|
setStatus('Unavailable', '#a14d43', '#f3d8d2');
|
|
console.error('Todo completion failed', error);
|
|
completeEl.disabled = false;
|
|
publishLiveContent(rawStatus, true, String(error));
|
|
}
|
|
});
|
|
|
|
render();
|
|
})();
|
|
</script>
|