feat: integrate todo plugin into desktop workspace
This commit is contained in:
parent
8dafde4519
commit
2007b909d1
|
|
@ -78,7 +78,7 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
|
|
||||||
const health = page.locator('[data-plugin-manager-summary="health"]');
|
const health = page.locator('[data-plugin-manager-summary="health"]');
|
||||||
await expect(health).toBeVisible();
|
await expect(health).toBeVisible();
|
||||||
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('10');
|
await expect(health.locator('[data-plugin-status-summary="loaded"]')).toContainText('11');
|
||||||
await expect(health.locator('[data-plugin-status-summary="failed"]')).toContainText('0');
|
await expect(health.locator('[data-plugin-status-summary="failed"]')).toContainText('0');
|
||||||
await expect(health.locator('[data-plugin-status-summary="disabled"]')).toContainText('0');
|
await expect(health.locator('[data-plugin-status-summary="disabled"]')).toContainText('0');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
|
||||||
|
|
||||||
|
test.describe('Todo plugin workflow', () => {
|
||||||
|
let consoleCollector;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
consoleCollector = setupConsoleCollector(page);
|
||||||
|
await resetMockState(page);
|
||||||
|
await page.goto('/');
|
||||||
|
await waitForAppReady(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async () => {
|
||||||
|
consoleCollector.assertNoErrors();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('workspace Todo supports CRUD, reminders, and manual Journal conversion', async ({ page }) => {
|
||||||
|
await page.getByRole('tab', { name: 'Todos' }).click();
|
||||||
|
|
||||||
|
const todos = page.locator('.todo-root');
|
||||||
|
await expect(todos).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(todos).toContainText('Todos · Project');
|
||||||
|
|
||||||
|
await todos.locator('[data-todo-action="add"]').click();
|
||||||
|
await todos.locator('[data-todo-input="title"]').fill('Prepare project review');
|
||||||
|
await todos.locator('[data-todo-input="description"]').fill('Collect factual review notes.');
|
||||||
|
await todos.locator('[data-todo-input="priority"]').selectOption('high');
|
||||||
|
await todos.locator('[data-todo-input="dueAt"]').fill('2000-01-01');
|
||||||
|
await todos.locator('[data-todo-input="reminderAt"]').fill('2000-01-01T09:00');
|
||||||
|
await todos.locator('[data-todo-action="save"]').click();
|
||||||
|
|
||||||
|
await expect(todos).toContainText('Overdue');
|
||||||
|
await expect(todos).toContainText('Reminder due');
|
||||||
|
await expect.poll(async () => page.evaluate(async () => {
|
||||||
|
const result = await window.go.api.App.ReadPluginSettings('verstak.todo');
|
||||||
|
const settings = Array.isArray(result) ? result[0] : result;
|
||||||
|
const todo = settings['todos:global'].find((item) => item.title === 'Prepare project review');
|
||||||
|
return todo && [todo.workspaceRootPath, todo.priority, todo.dueAt, todo.reminderAt].join('|');
|
||||||
|
})).toBe('Project|high|2000-01-01|2000-01-01T09:00');
|
||||||
|
|
||||||
|
await todos.locator('[data-todo-action="edit"]').click();
|
||||||
|
await todos.locator('[data-todo-input="title"]').fill('Prepare project review updated');
|
||||||
|
await todos.locator('[data-todo-action="save"]').click();
|
||||||
|
await expect(todos).toContainText('Prepare project review updated');
|
||||||
|
|
||||||
|
await todos.locator('[data-todo-action="mark-done"]').click();
|
||||||
|
await expect(todos.locator('[data-todo-action="create-journal-entry"]')).toBeVisible();
|
||||||
|
await todos.locator('[data-todo-action="create-journal-entry"]').click();
|
||||||
|
|
||||||
|
const journal = page.locator('.journal-root');
|
||||||
|
await expect(page.getByRole('tab', { name: 'Journal' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await expect(journal).toBeVisible({ timeout: 10000 });
|
||||||
|
await expect(journal).toContainText('Create journal entry from completed todo');
|
||||||
|
await expect(journal.locator('[data-journal-input="title"]')).toHaveValue('Prepare project review updated');
|
||||||
|
await expect(journal.locator('[data-journal-input="summary"]')).toHaveValue('Collect factual review notes.');
|
||||||
|
await expect(journal.locator('[data-journal-input="minutes"]')).toHaveValue('0');
|
||||||
|
|
||||||
|
await journal.locator('[data-journal-input="title"]').fill('Prepare project review handoff');
|
||||||
|
await journal.locator('[data-journal-input="summary"]').fill('Reviewed factual project notes before handoff.');
|
||||||
|
await journal.locator('[data-journal-action="save-entry"]').click();
|
||||||
|
await expect.poll(async () => page.evaluate(async () => {
|
||||||
|
const todoResult = await window.go.api.App.ReadPluginSettings('verstak.todo');
|
||||||
|
const todoSettings = Array.isArray(todoResult) ? todoResult[0] : todoResult;
|
||||||
|
const todo = todoSettings['todos:global'].find((item) => item.title === 'Prepare project review updated');
|
||||||
|
const journalResult = await window.go.api.App.ReadPluginSettings('verstak.journal');
|
||||||
|
const journalSettings = Array.isArray(journalResult) ? journalResult[0] : journalResult;
|
||||||
|
const entry = journalSettings['worklog:workspace:Project'].find((item) => item.sourceTodoId === todo.id);
|
||||||
|
return entry && [entry.title, entry.summary, entry.minutes].join('|');
|
||||||
|
})).toBe('Prepare project review handoff|Reviewed factual project notes before handoff.|0');
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Todos' }).click();
|
||||||
|
await page.locator('.todo-root [data-todo-action="delete"]').click();
|
||||||
|
await expect.poll(async () => page.evaluate(async () => {
|
||||||
|
const result = await window.go.api.App.ReadPluginSettings('verstak.todo');
|
||||||
|
const settings = Array.isArray(result) ? result[0] : result;
|
||||||
|
return settings['todos:global'].some((item) => item.title === 'Prepare project review updated');
|
||||||
|
})).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('global Todos filter by workspace and Overview only exposes current workspace attention', async ({ page }) => {
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await window.go.api.App.WritePluginSettings('verstak.todo', {
|
||||||
|
'todos:global': [
|
||||||
|
{
|
||||||
|
id: 'todo-project-open',
|
||||||
|
title: 'Project deadline',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
status: 'open',
|
||||||
|
priority: 'high',
|
||||||
|
dueAt: '2000-01-01',
|
||||||
|
reminderAt: '2000-01-01T09:00',
|
||||||
|
createdAt: '2026-06-30T08:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-30T08:00:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'todo-project-done',
|
||||||
|
title: 'Project completed item',
|
||||||
|
workspaceRootPath: 'Project',
|
||||||
|
status: 'done',
|
||||||
|
priority: 'normal',
|
||||||
|
createdAt: '2026-06-30T08:01:00.000Z',
|
||||||
|
updatedAt: '2026-06-30T08:01:00.000Z',
|
||||||
|
completedAt: '2026-06-30T08:02:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'todo-test-open',
|
||||||
|
title: 'Test workspace item',
|
||||||
|
workspaceRootPath: 'Test',
|
||||||
|
status: 'open',
|
||||||
|
priority: 'normal',
|
||||||
|
dueAt: '2000-01-01',
|
||||||
|
createdAt: '2026-06-30T08:03:00.000Z',
|
||||||
|
updatedAt: '2026-06-30T08:03:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const overview = page.locator('[data-overview-root]');
|
||||||
|
await overview.locator('[data-overview-action="refresh"]').click();
|
||||||
|
const attention = overview.locator('[data-overview-section="attention"]');
|
||||||
|
await expect(attention).toContainText('Project deadline');
|
||||||
|
await expect(attention).not.toContainText('Test workspace item');
|
||||||
|
await attention.locator('.overview-attention-row', { hasText: 'Project deadline' }).getByRole('button', { name: 'Open Todos' }).click();
|
||||||
|
await expect(page.getByRole('tab', { name: 'Todos' })).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
|
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Todos' }).click();
|
||||||
|
const globalTodos = page.locator('.todo-root');
|
||||||
|
await expect(globalTodos).toContainText('Project deadline');
|
||||||
|
await expect(globalTodos).toContainText('Test workspace item');
|
||||||
|
|
||||||
|
await globalTodos.locator('[data-todo-filter="workspace"]').selectOption('Project');
|
||||||
|
await expect(globalTodos).toContainText('Project deadline');
|
||||||
|
await expect(globalTodos).toContainText('Project completed item');
|
||||||
|
await expect(globalTodos).not.toContainText('Test workspace item');
|
||||||
|
|
||||||
|
await globalTodos.locator('[data-todo-filter="status"]').selectOption('done');
|
||||||
|
await expect(globalTodos).toContainText('Project completed item');
|
||||||
|
await expect(globalTodos).not.toContainText('Project deadline');
|
||||||
|
|
||||||
|
await page.locator('.wt-label').filter({ hasText: 'Test' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Todos' }).click();
|
||||||
|
const workspaceTodos = page.locator('.todo-root');
|
||||||
|
await expect(workspaceTodos).toContainText('Test workspace item');
|
||||||
|
await expect(workspaceTodos).not.toContainText('Project deadline');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -31,13 +31,15 @@
|
||||||
let captures = [];
|
let captures = [];
|
||||||
let activityEvents = [];
|
let activityEvents = [];
|
||||||
let journalEntries = [];
|
let journalEntries = [];
|
||||||
|
let todos = [];
|
||||||
let workSessionCandidates = [];
|
let workSessionCandidates = [];
|
||||||
let keyResources = [];
|
let keyResources = [];
|
||||||
let totalNotes = 0;
|
let totalNotes = 0;
|
||||||
let loadedWorkspaceRoot = '';
|
let loadedWorkspaceRoot = '';
|
||||||
let toolProbe = 0;
|
let toolProbe = 0;
|
||||||
|
|
||||||
$: hasNotes = hasTool('notes');
|
$: hasNotes = hasTool('notes', availableTools);
|
||||||
|
$: hasTodos = hasTool('todo', availableTools);
|
||||||
$: recentChanges = buildRecentChanges(activityEvents, captures, journalEntries);
|
$: recentChanges = buildRecentChanges(activityEvents, captures, journalEntries);
|
||||||
$: filteredRecentChanges = activeFilter === 'all'
|
$: filteredRecentChanges = activeFilter === 'all'
|
||||||
? recentChanges
|
? recentChanges
|
||||||
|
|
@ -47,10 +49,11 @@
|
||||||
$: unprocessedCaptures = captures.filter(item => item?.processed !== true);
|
$: unprocessedCaptures = captures.filter(item => item?.processed !== true);
|
||||||
$: linkedCandidateIds = new Set(journalEntries.map(item => String(item?.sourceCandidateId || '')).filter(Boolean));
|
$: linkedCandidateIds = new Set(journalEntries.map(item => String(item?.sourceCandidateId || '')).filter(Boolean));
|
||||||
$: pendingWorkSessionCandidates = workSessionCandidates.filter(item => !linkedCandidateIds.has(String(item?.candidateId || '')));
|
$: pendingWorkSessionCandidates = workSessionCandidates.filter(item => !linkedCandidateIds.has(String(item?.candidateId || '')));
|
||||||
$: needsAttention = buildNeedsAttention(unprocessedCaptures, pendingWorkSessionCandidates);
|
$: urgentTodos = hasTodos ? todos.filter(item => todoAttentionState(item)) : [];
|
||||||
$: continueItems = buildContinueItems(activityEvents, unprocessedCaptures, journalEntries);
|
$: needsAttention = buildNeedsAttention(unprocessedCaptures, pendingWorkSessionCandidates, urgentTodos);
|
||||||
|
$: continueItems = buildContinueItems(activityEvents, unprocessedCaptures, journalEntries, urgentTodos);
|
||||||
$: attentionActionKind = needsAttention[0]?.actionKind || 'browser-inbox';
|
$: attentionActionKind = needsAttention[0]?.actionKind || 'browser-inbox';
|
||||||
$: lastActive = lastActiveDate([...recentChanges, ...continueItems], captures, journalEntries);
|
$: lastActive = lastActiveDate([...recentChanges, ...continueItems], captures, journalEntries, todos);
|
||||||
$: summaryItems = [
|
$: summaryItems = [
|
||||||
{ key: 'notes', label: 'Notes', count: totalNotes, detail: totalAndRecentLabel(totalNotes, noteRecentChanges), actionKind: 'notes', actionLabel: 'Open Notes' },
|
{ key: 'notes', label: 'Notes', count: totalNotes, detail: totalAndRecentLabel(totalNotes, noteRecentChanges), actionKind: 'notes', actionLabel: 'Open Notes' },
|
||||||
{ key: 'files', label: 'Files', count: fileRecentChanges, detail: countLabel(fileRecentChanges, 'recent change'), actionKind: 'files', actionLabel: 'Open Files' },
|
{ key: 'files', label: 'Files', count: fileRecentChanges, detail: countLabel(fileRecentChanges, 'recent change'), actionKind: 'files', actionLabel: 'Open Files' },
|
||||||
|
|
@ -68,10 +71,10 @@
|
||||||
loadOverview();
|
loadOverview();
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasTool(name) {
|
function hasTool(name, tools = availableTools) {
|
||||||
toolProbe;
|
toolProbe;
|
||||||
name = String(name || '').toLowerCase();
|
name = String(name || '').toLowerCase();
|
||||||
const fromProps = (availableTools || []).some(tool => {
|
const fromProps = (tools || []).some(tool => {
|
||||||
const label = `${tool?.title || ''} ${tool?.id || ''} ${tool?.pluginId || ''}`.toLowerCase();
|
const label = `${tool?.title || ''} ${tool?.id || ''} ${tool?.pluginId || ''}`.toLowerCase();
|
||||||
return label.includes(name);
|
return label.includes(name);
|
||||||
});
|
});
|
||||||
|
|
@ -129,6 +132,47 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function todoRowsForWorkspace(settings) {
|
||||||
|
const workspace = String(workspaceRootPath || '').trim();
|
||||||
|
if (!workspace) return [];
|
||||||
|
const seen = new Set();
|
||||||
|
return normalizeRows(settings?.['todos:global']).filter(item => {
|
||||||
|
const tagged = String(item.workspaceRootPath || item.workspaceName || item.workspaceNodeId || '').trim();
|
||||||
|
if (tagged !== workspace) return false;
|
||||||
|
const todoId = String(item.id || '');
|
||||||
|
if (!todoId || seen.has(todoId)) return !todoId;
|
||||||
|
seen.add(todoId);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoDateMs(value) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) return 0;
|
||||||
|
const normalized = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? `${raw}T00:00:00` : raw;
|
||||||
|
const date = new Date(normalized);
|
||||||
|
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoTitle(item) {
|
||||||
|
return String(item?.title || item?.name || 'Untitled todo').trim() || 'Untitled todo';
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoAttentionState(item) {
|
||||||
|
if (String(item?.status || 'open').toLowerCase() !== 'open') return '';
|
||||||
|
const now = Date.now();
|
||||||
|
const dueAt = todoDateMs(item?.dueAt);
|
||||||
|
const reminderAt = todoDateMs(item?.reminderAt);
|
||||||
|
if (reminderAt && reminderAt <= now) return 'Reminder due';
|
||||||
|
if (dueAt && dueAt < now) return 'Overdue';
|
||||||
|
if (dueAt && dueAt <= now + 3 * 24 * 60 * 60 * 1000) return 'Due soon';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoTimeValue(item) {
|
||||||
|
return item?.dueAt || item?.reminderAt || item?.updatedAt || item?.createdAt || '';
|
||||||
|
}
|
||||||
|
|
||||||
function timeValue(item) {
|
function timeValue(item) {
|
||||||
return item?.capturedAt || item?.endedAt || item?.startedAt || item?.occurredAt || item?.receivedAt || item?.updatedAt || item?.modifiedAt || item?.date || item?.time || '';
|
return item?.capturedAt || item?.endedAt || item?.startedAt || item?.occurredAt || item?.receivedAt || item?.updatedAt || item?.modifiedAt || item?.date || item?.time || '';
|
||||||
}
|
}
|
||||||
|
|
@ -321,7 +365,17 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildContinueItems(events, captureRows, journalRows) {
|
function buildContinueItems(events, captureRows, journalRows, todoRows) {
|
||||||
|
const todoCandidates = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).map(item => ({
|
||||||
|
id: item.id || `todo:${todoTitle(item)}`,
|
||||||
|
category: 'todo',
|
||||||
|
title: `Todo ${quoted(todoTitle(item))}`,
|
||||||
|
meta: `${todoAttentionState(item)}${item.dueAt ? ` · Due ${item.dueAt}` : ''}`,
|
||||||
|
time: todoTimeValue(item),
|
||||||
|
absolute: absoluteTime(todoTimeValue(item)),
|
||||||
|
actionKind: 'todo',
|
||||||
|
actionLabel: 'Open Todos',
|
||||||
|
}));
|
||||||
const captureCandidates = sortByTime(captureRows).map(item => ({
|
const captureCandidates = sortByTime(captureRows).map(item => ({
|
||||||
id: item.captureId || `capture:${timeValue(item)}`,
|
id: item.captureId || `capture:${timeValue(item)}`,
|
||||||
category: 'captures',
|
category: 'captures',
|
||||||
|
|
@ -348,10 +402,10 @@
|
||||||
actionKind: 'journal',
|
actionKind: 'journal',
|
||||||
actionLabel: 'Open Journal',
|
actionLabel: 'Open Journal',
|
||||||
}));
|
}));
|
||||||
return [...captureCandidates, ...noteCandidates, ...fileCandidates, ...journalCandidates].slice(0, 4);
|
return [...todoCandidates, ...captureCandidates, ...noteCandidates, ...fileCandidates, ...journalCandidates].slice(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNeedsAttention(captureRows, candidates) {
|
function buildNeedsAttention(captureRows, candidates, todoRows) {
|
||||||
const captureItems = sortByTime(captureRows).slice(0, 4).map(item => ({
|
const captureItems = sortByTime(captureRows).slice(0, 4).map(item => ({
|
||||||
id: item.captureId || `capture:${timeValue(item)}`,
|
id: item.captureId || `capture:${timeValue(item)}`,
|
||||||
title: captureTitle(item),
|
title: captureTitle(item),
|
||||||
|
|
@ -367,7 +421,14 @@
|
||||||
actionLabel: 'Review candidate',
|
actionLabel: 'Review candidate',
|
||||||
toolRequest: { type: 'work-session-candidate', candidate: item },
|
toolRequest: { type: 'work-session-candidate', candidate: item },
|
||||||
}));
|
}));
|
||||||
return [...captureItems, ...candidateItems].slice(0, 6);
|
const todoItems = [...todoRows].sort((a, b) => todoDateMs(todoTimeValue(a)) - todoDateMs(todoTimeValue(b))).slice(0, 4).map(item => ({
|
||||||
|
id: item.id || `todo:${todoTitle(item)}`,
|
||||||
|
title: todoTitle(item),
|
||||||
|
meta: `${todoAttentionState(item)}${item.dueAt ? ` · Due ${item.dueAt}` : ''}`,
|
||||||
|
actionKind: 'todo',
|
||||||
|
actionLabel: 'Open Todos',
|
||||||
|
}));
|
||||||
|
return [...todoItems, ...captureItems, ...candidateItems].slice(0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
function countCategory(items, category) {
|
function countCategory(items, category) {
|
||||||
|
|
@ -390,8 +451,8 @@
|
||||||
return `${count} journal entr${count === 1 ? 'y' : 'ies'}`;
|
return `${count} journal entr${count === 1 ? 'y' : 'ies'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastActiveDate(items, captureRows, journalRows) {
|
function lastActiveDate(items, captureRows, journalRows, todoRows) {
|
||||||
const source = sortByTime([...items, ...captureRows, ...journalRows])[0];
|
const source = sortByTime([...items, ...captureRows, ...journalRows, ...todoRows])[0];
|
||||||
return timeValue(source);
|
return timeValue(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,10 +490,11 @@
|
||||||
const workspaceAtStart = String(workspaceRootPath || '').trim();
|
const workspaceAtStart = String(workspaceRootPath || '').trim();
|
||||||
loadedWorkspaceRoot = workspaceAtStart;
|
loadedWorkspaceRoot = workspaceAtStart;
|
||||||
loading = true;
|
loading = true;
|
||||||
const [browserSettings, activitySettings, journalSettings, resources] = await Promise.all([
|
const [browserSettings, activitySettings, journalSettings, todoSettings, resources] = await Promise.all([
|
||||||
readPluginSettings('verstak.browser-inbox'),
|
readPluginSettings('verstak.browser-inbox'),
|
||||||
readPluginSettings('verstak.activity'),
|
readPluginSettings('verstak.activity'),
|
||||||
readPluginSettings('verstak.journal'),
|
readPluginSettings('verstak.journal'),
|
||||||
|
readPluginSettings('verstak.todo'),
|
||||||
loadWorkspaceResources(),
|
loadWorkspaceResources(),
|
||||||
]);
|
]);
|
||||||
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
|
if (workspaceAtStart !== String(workspaceRootPath || '').trim()) return;
|
||||||
|
|
@ -447,6 +509,7 @@
|
||||||
workspaceKey('worklog:workspace:'),
|
workspaceKey('worklog:workspace:'),
|
||||||
'worklog',
|
'worklog',
|
||||||
]);
|
]);
|
||||||
|
todos = todoRowsForWorkspace(todoSettings);
|
||||||
workSessionCandidates = rowsFor(activitySettings, [
|
workSessionCandidates = rowsFor(activitySettings, [
|
||||||
workspaceKey('work-session-candidates:workspace:'),
|
workspaceKey('work-session-candidates:workspace:'),
|
||||||
]);
|
]);
|
||||||
|
|
@ -583,7 +646,7 @@
|
||||||
<div class="today-panel-head overview-panel-head">
|
<div class="today-panel-head overview-panel-head">
|
||||||
<div>
|
<div>
|
||||||
<h3>Needs attention</h3>
|
<h3>Needs attention</h3>
|
||||||
<p>Pending captures and possible journal entries.</p>
|
<p>Pending captures, urgent todos, and possible journal entries.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
|
|
@ -599,7 +662,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="today-empty compact">No pending captures or possible journal entries.</p>
|
<p class="today-empty compact">No pending captures, urgent todos, or possible journal entries.</p>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
const toolOrder = new Map([
|
const toolOrder = new Map([
|
||||||
['notes', 10],
|
['notes', 10],
|
||||||
['files', 20],
|
['files', 20],
|
||||||
|
['todo', 30],
|
||||||
['activity', 40],
|
['activity', 40],
|
||||||
['browser', 50],
|
['browser', 50],
|
||||||
['inbox', 50],
|
['inbox', 50],
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,7 @@
|
||||||
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
||||||
error: ''
|
error: ''
|
||||||
},
|
},
|
||||||
|
'verstak.todo': makeTodoPluginState(),
|
||||||
'verstak.search': {
|
'verstak.search': {
|
||||||
status: 'loaded',
|
status: 'loaded',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -285,6 +286,8 @@
|
||||||
var vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
var vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
||||||
vaultPluginState.enabledPlugins.push('verstak.trash');
|
vaultPluginState.enabledPlugins.push('verstak.trash');
|
||||||
vaultPluginState.desiredPlugins.push({ id: 'verstak.trash', version: '0.1.0', source: 'official' });
|
vaultPluginState.desiredPlugins.push({ id: 'verstak.trash', version: '0.1.0', source: 'official' });
|
||||||
|
vaultPluginState.enabledPlugins.push('verstak.todo');
|
||||||
|
vaultPluginState.desiredPlugins.push({ id: 'verstak.todo', version: '0.1.0', source: 'official' });
|
||||||
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||||
var workbenchPreferences = {};
|
var workbenchPreferences = {};
|
||||||
var openedResources = [];
|
var openedResources = [];
|
||||||
|
|
@ -375,6 +378,33 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeTodoPluginState() {
|
||||||
|
return {
|
||||||
|
status: 'loaded',
|
||||||
|
enabled: true,
|
||||||
|
manifest: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
id: 'verstak.todo',
|
||||||
|
name: 'Todos',
|
||||||
|
version: '0.1.0',
|
||||||
|
apiVersion: '0.1.0',
|
||||||
|
description: 'Global and workspace todo tracking with due and reminder metadata.',
|
||||||
|
source: 'official',
|
||||||
|
icon: 'list-todo',
|
||||||
|
provides: ['todo.list', 'todo.workspace'],
|
||||||
|
permissions: ['files.read', 'storage.namespace', 'ui.register'],
|
||||||
|
frontend: { entry: 'frontend/dist/index.js' },
|
||||||
|
contributes: {
|
||||||
|
views: [{ id: 'verstak.todo.view', title: 'Todos', icon: 'list-todo', component: 'TodoView' }],
|
||||||
|
sidebarItems: [{ id: 'verstak.todo.sidebar', title: 'Todos', icon: 'list-todo', view: 'verstak.todo.view', position: 35 }],
|
||||||
|
workspaceItems: [{ id: 'verstak.todo.workspace', title: 'Todos', icon: 'list-todo', component: 'TodoView' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rootPath: '/tmp/verstak-test/plugins/todo',
|
||||||
|
error: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function makeDefaultSyncState() {
|
function makeDefaultSyncState() {
|
||||||
return {
|
return {
|
||||||
configured: false,
|
configured: false,
|
||||||
|
|
@ -1942,6 +1972,19 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function completedTodoFromProps(props, root) {
|
||||||
|
var request = props && props.toolRequest;
|
||||||
|
var todo = request && request.type === 'completed-todo' ? request.todo : null;
|
||||||
|
if (!todo || todo.workspaceRootPath !== root || !todo.id || !todo.title) return null;
|
||||||
|
return {
|
||||||
|
id: String(todo.id),
|
||||||
|
title: String(todo.title),
|
||||||
|
description: String(todo.description || todo.body || ''),
|
||||||
|
workspaceRootPath: root,
|
||||||
|
completedAt: String(todo.completedAt || '')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function candidateDate(value) {
|
function candidateDate(value) {
|
||||||
var date = new Date(value || '');
|
var date = new Date(value || '');
|
||||||
return isNaN(date.getTime()) ? new Date().toISOString().slice(0, 10) : date.toISOString().slice(0, 10);
|
return isNaN(date.getTime()) ? new Date().toISOString().slice(0, 10) : date.toISOString().slice(0, 10);
|
||||||
|
|
@ -1984,12 +2027,13 @@
|
||||||
return api.settings.write(key, entries);
|
return api.settings.write(key, entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEntryModal(candidate) {
|
function showEntryModal(candidate, completedTodo) {
|
||||||
var reviewing = !!candidate;
|
var reviewing = !!candidate;
|
||||||
var titleInput = el('input', { className: 'journal-input', type: 'text', value: '', 'data-journal-input': 'title' });
|
var reviewingTodo = !reviewing && !!completedTodo;
|
||||||
var summaryInput = el('textarea', { className: 'journal-input textarea', value: '', 'data-journal-input': 'summary' });
|
var titleInput = el('input', { className: 'journal-input', type: 'text', value: reviewingTodo ? completedTodo.title : '', 'data-journal-input': 'title' });
|
||||||
var minutesInput = el('input', { className: 'journal-input', type: 'number', value: reviewing ? String(candidate.estimatedMinutes) : '30', 'data-journal-input': 'minutes' });
|
var summaryInput = el('textarea', { className: 'journal-input textarea', value: reviewingTodo ? completedTodo.description : '', 'data-journal-input': 'summary' });
|
||||||
var dateInput = el('input', { className: 'journal-input', type: 'date', value: reviewing ? candidateDate(candidate.startedAt) : new Date().toISOString().slice(0, 10), 'data-journal-input': 'date' });
|
var minutesInput = el('input', { className: 'journal-input', type: 'number', value: reviewing ? String(candidate.estimatedMinutes) : (reviewingTodo ? '0' : '30'), 'data-journal-input': 'minutes' });
|
||||||
|
var dateInput = el('input', { className: 'journal-input', type: 'date', value: reviewing ? candidateDate(candidate.startedAt) : (reviewingTodo ? candidateDate(completedTodo.completedAt) : new Date().toISOString().slice(0, 10)), 'data-journal-input': 'date' });
|
||||||
var billableInput = el('input', { type: 'checkbox', checked: false, 'data-journal-input': 'billable' });
|
var billableInput = el('input', { type: 'checkbox', checked: false, 'data-journal-input': 'billable' });
|
||||||
var activityInputs = reviewing ? candidate.activities.map(function (activity) {
|
var activityInputs = reviewing ? candidate.activities.map(function (activity) {
|
||||||
return { activity: activity, input: el('input', { type: 'checkbox', checked: true, 'data-journal-candidate-activity': activity.activityId }) };
|
return { activity: activity, input: el('input', { type: 'checkbox', checked: true, 'data-journal-candidate-activity': activity.activityId }) };
|
||||||
|
|
@ -2001,6 +2045,11 @@
|
||||||
el('div', { textContent: 'Estimated duration: ' + candidate.estimatedMinutes + ' min' }),
|
el('div', { textContent: 'Estimated duration: ' + candidate.estimatedMinutes + ' min' }),
|
||||||
el('div', { textContent: 'Activities: ' + candidate.activityCount })
|
el('div', { textContent: 'Activities: ' + candidate.activityCount })
|
||||||
]) : null;
|
]) : null;
|
||||||
|
var todoContext = reviewingTodo ? el('div', { className: 'journal-candidate-context', 'data-journal-todo': completedTodo.id }, [
|
||||||
|
el('strong', { textContent: 'Completed todo' }),
|
||||||
|
el('div', { textContent: 'Workspace: ' + completedTodo.workspaceRootPath }),
|
||||||
|
completedTodo.completedAt ? el('div', { textContent: 'Completed: ' + candidateTime(completedTodo.completedAt) }) : null
|
||||||
|
]) : null;
|
||||||
var linked = reviewing ? el('fieldset', { className: 'journal-candidate-activities' }, [
|
var linked = reviewing ? el('fieldset', { className: 'journal-candidate-activities' }, [
|
||||||
el('legend', { textContent: 'Linked activities' })
|
el('legend', { textContent: 'Linked activities' })
|
||||||
].concat(activityInputs.map(function (item) {
|
].concat(activityInputs.map(function (item) {
|
||||||
|
|
@ -2010,6 +2059,7 @@
|
||||||
function save() {
|
function save() {
|
||||||
var title = String(titleInput.value || '').trim();
|
var title = String(titleInput.value || '').trim();
|
||||||
if (!title) return;
|
if (!title) return;
|
||||||
|
if (reviewingTodo && entries.some(function (entry) { return entry.sourceTodoId === completedTodo.id; })) return;
|
||||||
var entry = {
|
var entry = {
|
||||||
entryId: 'journal:' + Date.now(),
|
entryId: 'journal:' + Date.now(),
|
||||||
workspaceRootPath: rootPath,
|
workspaceRootPath: rootPath,
|
||||||
|
|
@ -2019,6 +2069,7 @@
|
||||||
minutes: Number(minutesInput.value || 0),
|
minutes: Number(minutesInput.value || 0),
|
||||||
billable: billableInput.checked === true,
|
billable: billableInput.checked === true,
|
||||||
sourceCandidateId: reviewing ? candidate.candidateId : '',
|
sourceCandidateId: reviewing ? candidate.candidateId : '',
|
||||||
|
sourceTodoId: reviewingTodo ? completedTodo.id : '',
|
||||||
activityIds: reviewing ? activityInputs.filter(function (item) { return item.input.checked; }).map(function (item) { return item.activity.activityId; }) : []
|
activityIds: reviewing ? activityInputs.filter(function (item) { return item.input.checked; }).map(function (item) { return item.activity.activityId; }) : []
|
||||||
};
|
};
|
||||||
entries = [entry].concat(entries);
|
entries = [entry].concat(entries);
|
||||||
|
|
@ -2030,8 +2081,9 @@
|
||||||
modalHost.hidden = false;
|
modalHost.hidden = false;
|
||||||
modalHost.appendChild(el('div', { className: 'journal-modal-overlay' }, [
|
modalHost.appendChild(el('div', { className: 'journal-modal-overlay' }, [
|
||||||
el('div', { className: 'journal-modal' }, [
|
el('div', { className: 'journal-modal' }, [
|
||||||
el('div', { className: 'journal-modal-title', textContent: reviewing ? 'Review possible journal entry' : 'Add journal entry' }),
|
el('div', { className: 'journal-modal-title', textContent: reviewing ? 'Review possible journal entry' : (reviewingTodo ? 'Create journal entry from completed todo' : 'Add journal entry') }),
|
||||||
context,
|
context,
|
||||||
|
todoContext,
|
||||||
el('div', { className: 'journal-modal-grid' }, [
|
el('div', { className: 'journal-modal-grid' }, [
|
||||||
el('label', { className: 'journal-field' }, ['Date', dateInput]),
|
el('label', { className: 'journal-field' }, ['Date', dateInput]),
|
||||||
el('label', { className: 'journal-field' }, ['Minutes', minutesInput]),
|
el('label', { className: 'journal-field' }, ['Minutes', minutesInput]),
|
||||||
|
|
@ -2063,7 +2115,7 @@
|
||||||
el('div', {}, [
|
el('div', {}, [
|
||||||
el('div', { className: 'journal-entry-title', textContent: entry.title }),
|
el('div', { className: 'journal-entry-title', textContent: entry.title }),
|
||||||
entry.summary ? el('div', { className: 'journal-summary', textContent: entry.summary }) : null,
|
entry.summary ? el('div', { className: 'journal-summary', textContent: entry.summary }) : null,
|
||||||
el('div', { className: 'journal-meta', textContent: entry.workspaceRootPath + (activityIds.length ? ' · ' + activityIds.length + ' linked activities' : '') })
|
el('div', { className: 'journal-meta', textContent: entry.workspaceRootPath + (activityIds.length ? ' · ' + activityIds.length + ' linked activities' : '') + (entry.sourceTodoId ? ' · linked todo' : '') })
|
||||||
]),
|
]),
|
||||||
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' })
|
el('div', { className: 'journal-minutes', textContent: entry.minutes + ' min' })
|
||||||
]));
|
]));
|
||||||
|
|
@ -2074,7 +2126,9 @@
|
||||||
entries = rows(stored);
|
entries = rows(stored);
|
||||||
render();
|
render();
|
||||||
var candidate = candidateFromProps(props || {}, rootPath);
|
var candidate = candidateFromProps(props || {}, rootPath);
|
||||||
|
var completedTodo = completedTodoFromProps(props || {}, rootPath);
|
||||||
if (candidate) showEntryModal(candidate);
|
if (candidate) showEntryModal(candidate);
|
||||||
|
else if (completedTodo) showEntryModal(null, completedTodo);
|
||||||
}).catch(function () { render(); });
|
}).catch(function () { render(); });
|
||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
|
|
@ -2525,6 +2579,357 @@
|
||||||
}.toString() + ')();';
|
}.toString() + ')();';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function todoBundle() {
|
||||||
|
return '(' + function () {
|
||||||
|
var PLUGIN_ID = 'verstak.todo';
|
||||||
|
var GLOBAL_KEY = 'todos:global';
|
||||||
|
|
||||||
|
function injectStyles() {
|
||||||
|
if (document.getElementById('mock-todo-style')) return;
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'mock-todo-style';
|
||||||
|
style.textContent = [
|
||||||
|
'.todo-root{height:100%;min-height:0;display:flex;flex-direction:column;background:#0d0d1a;color:#e0e0f0}',
|
||||||
|
'.todo-toolbar{display:flex;align-items:center;gap:.5rem;padding:.55rem .75rem;border-bottom:1px solid #16213e;background:#12122a;flex-wrap:wrap}.todo-title{font-size:.86rem;font-weight:600;color:#f0f0ff}.todo-count,.todo-status{font-size:.74rem;color:#8b8ba8}.todo-spacer{flex:1}.todo-filters{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}',
|
||||||
|
'.todo-input,.todo-select{min-height:1.85rem;box-sizing:border-box;border:1px solid #1a3a5c;border-radius:4px;background:#101020;color:#e0e0f0;padding:.28rem .42rem;font-size:.76rem}.todo-input.search{width:12rem}.todo-input.textarea{min-height:6rem;resize:vertical}',
|
||||||
|
'.todo-btn{min-height:1.85rem;padding:.3rem .65rem;border:1px solid #1a3a5c;border-radius:4px;background:#0f3460;color:#e0e0f0;font-size:.76rem;cursor:pointer}.todo-btn.primary{background:#4ecca3;border-color:#4ecca3;color:#102018}.todo-btn.danger{border-color:#633;color:#ffb0b0}',
|
||||||
|
'.todo-list{flex:1;min-height:0;overflow:auto;padding:.5rem .75rem}.todo-empty{padding:1.5rem;color:#8b8ba8}.todo-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.7rem;padding:.7rem 0;border-bottom:1px solid rgba(22,33,62,.75)}.todo-row.done .todo-row-title{text-decoration:line-through;color:#8b8ba8}.todo-row-title{font-weight:600}.todo-row-description,.todo-row-meta{margin-top:.25rem;font-size:.76rem;color:#aaa}.todo-row-actions{display:flex;justify-content:flex-end;gap:.35rem;flex-wrap:wrap}.todo-badge{display:inline-flex;margin-right:.3rem;padding:.08rem .28rem;border:1px solid #24304f;border-radius:4px}.todo-badge.overdue,.todo-badge.reminder-due{border-color:#633;color:#ffb0b0}.todo-badge.due-soon{border-color:#7a6633;color:#f2d17b}',
|
||||||
|
'.todo-modal-host[hidden]{display:none}.todo-modal-overlay{position:fixed;inset:0;z-index:10000;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(0,0,0,.58)}.todo-modal{width:540px;max-width:96vw;display:grid;gap:.75rem;padding:1rem;border:1px solid #2c456a;border-radius:8px;background:#15152c;box-shadow:0 18px 44px rgba(0,0,0,.38)}.todo-modal-title{font-size:.95rem;font-weight:600}.todo-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:.6rem}.todo-field{display:grid;gap:.3rem;font-size:.72rem;color:#8b8ba8}.todo-field.wide{grid-column:1/-1}.todo-modal-actions{display:flex;justify-content:flex-end;gap:.5rem}'
|
||||||
|
].join('');
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function el(tag, attrs, children) {
|
||||||
|
var node = document.createElement(tag);
|
||||||
|
attrs = attrs || {};
|
||||||
|
Object.keys(attrs).forEach(function (key) {
|
||||||
|
if (key === 'textContent') node.textContent = attrs[key];
|
||||||
|
else if (key === 'className') node.className = attrs[key];
|
||||||
|
else if (key === 'value') node.value = attrs[key];
|
||||||
|
else if (key === 'checked') node.checked = !!attrs[key];
|
||||||
|
else if (key === 'disabled') node.disabled = !!attrs[key];
|
||||||
|
else if (key.indexOf('data-') === 0) node.setAttribute(key, attrs[key]);
|
||||||
|
else if (key === 'onClick') node.addEventListener('click', attrs[key]);
|
||||||
|
else if (key === 'onChange') node.addEventListener('change', attrs[key]);
|
||||||
|
else if (key === 'onInput') node.addEventListener('input', attrs[key]);
|
||||||
|
else node[key] = attrs[key];
|
||||||
|
});
|
||||||
|
(children || []).forEach(function (child) {
|
||||||
|
if (child) node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(value) { return String(value == null ? '' : value); }
|
||||||
|
function rootFromProps(props) {
|
||||||
|
return text((props && (props.workspaceRootPath || props.workspaceName || props.workspaceNodeId)) || (props && props.workspaceNode && (props.workspaceNode.rootPath || props.workspaceNode.name || props.workspaceNode.id))).trim();
|
||||||
|
}
|
||||||
|
function rows(value) { return Array.isArray(value) ? value.filter(function (item) { return item && typeof item === 'object'; }) : []; }
|
||||||
|
function now() { return new Date().toISOString(); }
|
||||||
|
function dateMs(value) {
|
||||||
|
value = text(value).trim();
|
||||||
|
if (!value) return 0;
|
||||||
|
var normalized = /^\d{4}-\d{2}-\d{2}$/.test(value) ? value + 'T00:00:00' : value;
|
||||||
|
var date = new Date(normalized);
|
||||||
|
return isNaN(date.getTime()) ? 0 : date.getTime();
|
||||||
|
}
|
||||||
|
function todoId(root, title) { return 'todo:' + (root || 'global') + ':' + Date.now() + ':' + text(title).trim().replace(/\s+/g, '-'); }
|
||||||
|
function normalizeTodo(value) {
|
||||||
|
value = value || {};
|
||||||
|
var status = ['open', 'done', 'cancelled'].indexOf(text(value.status).toLowerCase()) === -1 ? 'open' : text(value.status).toLowerCase();
|
||||||
|
var createdAt = text(value.createdAt).trim() || now();
|
||||||
|
return {
|
||||||
|
id: text(value.id).trim() || todoId(text(value.workspaceRootPath).trim(), value.title),
|
||||||
|
title: text(value.title).trim(),
|
||||||
|
description: text(value.description || value.body),
|
||||||
|
workspaceRootPath: text(value.workspaceRootPath || value.workspaceName).trim(),
|
||||||
|
workspaceName: text(value.workspaceName || value.workspaceRootPath).trim(),
|
||||||
|
status: status,
|
||||||
|
priority: ['low', 'normal', 'high'].indexOf(text(value.priority).toLowerCase()) === -1 ? 'normal' : text(value.priority).toLowerCase(),
|
||||||
|
dueAt: /^\d{4}-\d{2}-\d{2}$/.test(text(value.dueAt)) ? text(value.dueAt) : '',
|
||||||
|
reminderAt: text(value.reminderAt),
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: text(value.updatedAt).trim() || createdAt,
|
||||||
|
completedAt: status === 'done' ? (text(value.completedAt).trim() || createdAt) : '',
|
||||||
|
sourceUrl: text(value.sourceUrl),
|
||||||
|
linkedJournalEntryId: text(value.linkedJournalEntryId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function sortTodos(list, sortMode) {
|
||||||
|
return list.slice().sort(function (a, b) {
|
||||||
|
if (sortMode === 'updated') return text(b.updatedAt).localeCompare(text(a.updatedAt));
|
||||||
|
var aValue = dateMs(sortMode === 'reminder' ? a.reminderAt : a.dueAt) || Number.MAX_SAFE_INTEGER;
|
||||||
|
var bValue = dateMs(sortMode === 'reminder' ? b.reminderAt : b.dueAt) || Number.MAX_SAFE_INTEGER;
|
||||||
|
return aValue - bValue || text(b.updatedAt).localeCompare(text(a.updatedAt));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function dueState(todo) {
|
||||||
|
var dueAt = dateMs(todo.dueAt);
|
||||||
|
if (!dueAt || todo.status !== 'open') return '';
|
||||||
|
var nowMs = Date.now();
|
||||||
|
if (dueAt < nowMs) return 'overdue';
|
||||||
|
if (dueAt <= nowMs + 3 * 24 * 60 * 60 * 1000) return 'due-soon';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function reminderIsDue(todo) { return todo.status === 'open' && dateMs(todo.reminderAt) > 0 && dateMs(todo.reminderAt) <= Date.now(); }
|
||||||
|
|
||||||
|
function TodoView() {}
|
||||||
|
|
||||||
|
TodoView.mount = function (containerEl, props, api) {
|
||||||
|
injectStyles();
|
||||||
|
var workspaceRoot = rootFromProps(props || {});
|
||||||
|
var isWorkspace = !!workspaceRoot;
|
||||||
|
var todos = [];
|
||||||
|
var statusFilter = 'all';
|
||||||
|
var workspaceFilter = '';
|
||||||
|
var sortMode = 'due';
|
||||||
|
var searchQuery = '';
|
||||||
|
var modalHost = el('div', { className: 'todo-modal-host', hidden: true });
|
||||||
|
var titleEl = el('span', { className: 'todo-title', textContent: isWorkspace ? 'Todos · ' + workspaceRoot : 'Todos' });
|
||||||
|
var countEl = el('span', { className: 'todo-count' });
|
||||||
|
var statusEl = el('span', { className: 'todo-status' });
|
||||||
|
var statusFilterEl = el('select', { className: 'todo-select', 'data-todo-filter': 'status', onChange: function (event) { statusFilter = event.target.value; render(); } }, [
|
||||||
|
el('option', { value: 'all', textContent: 'All statuses' }),
|
||||||
|
el('option', { value: 'open', textContent: 'Open' }),
|
||||||
|
el('option', { value: 'done', textContent: 'Done' }),
|
||||||
|
el('option', { value: 'cancelled', textContent: 'Cancelled' })
|
||||||
|
]);
|
||||||
|
var workspaceFilterEl = el('select', { className: 'todo-select', 'data-todo-filter': 'workspace', onChange: function (event) { workspaceFilter = event.target.value; render(); } });
|
||||||
|
var sortEl = el('select', { className: 'todo-select', 'data-todo-filter': 'sort', onChange: function (event) { sortMode = event.target.value; render(); } }, [
|
||||||
|
el('option', { value: 'due', textContent: 'Sort by due date' }),
|
||||||
|
el('option', { value: 'reminder', textContent: 'Sort by reminder' }),
|
||||||
|
el('option', { value: 'updated', textContent: 'Sort by updated' })
|
||||||
|
]);
|
||||||
|
var searchEl = el('input', { className: 'todo-input search', type: 'search', placeholder: 'Search todos', 'data-todo-filter': 'search', onInput: function (event) { searchQuery = text(event.target.value).trim().toLowerCase(); render(); } });
|
||||||
|
var addBtn = el('button', { className: 'todo-btn primary', 'data-todo-action': 'add', textContent: 'Add Todo', onClick: function () { showTodoModal(null); } });
|
||||||
|
var listEl = el('div', { className: 'todo-list' });
|
||||||
|
var root = el('div', { className: 'todo-root', 'data-plugin-id': PLUGIN_ID });
|
||||||
|
var toolbar = el('div', { className: 'todo-toolbar' });
|
||||||
|
var filters = el('div', { className: 'todo-filters' });
|
||||||
|
toolbar.appendChild(titleEl);
|
||||||
|
toolbar.appendChild(countEl);
|
||||||
|
filters.appendChild(statusFilterEl);
|
||||||
|
if (!isWorkspace) filters.appendChild(workspaceFilterEl);
|
||||||
|
filters.appendChild(sortEl);
|
||||||
|
filters.appendChild(searchEl);
|
||||||
|
toolbar.appendChild(filters);
|
||||||
|
toolbar.appendChild(el('span', { className: 'todo-spacer' }));
|
||||||
|
toolbar.appendChild(statusEl);
|
||||||
|
toolbar.appendChild(addBtn);
|
||||||
|
root.appendChild(toolbar);
|
||||||
|
root.appendChild(listEl);
|
||||||
|
root.appendChild(modalHost);
|
||||||
|
containerEl.innerHTML = '';
|
||||||
|
containerEl.appendChild(root);
|
||||||
|
|
||||||
|
function workspaceRoots() {
|
||||||
|
var found = {};
|
||||||
|
todos.forEach(function (todo) { if (todo.workspaceRootPath) found[todo.workspaceRootPath] = true; });
|
||||||
|
if (workspaceRoot) found[workspaceRoot] = true;
|
||||||
|
return Object.keys(found).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWorkspaceOptions() {
|
||||||
|
if (isWorkspace) return;
|
||||||
|
workspaceFilterEl.innerHTML = '';
|
||||||
|
workspaceFilterEl.appendChild(el('option', { value: '', textContent: 'All workspaces' }));
|
||||||
|
workspaceFilterEl.appendChild(el('option', { value: '__unassigned__', textContent: 'Unassigned' }));
|
||||||
|
workspaceRoots().forEach(function (rootName) {
|
||||||
|
workspaceFilterEl.appendChild(el('option', { value: rootName, textContent: rootName }));
|
||||||
|
});
|
||||||
|
workspaceFilterEl.value = workspaceFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleTodos() {
|
||||||
|
return sortTodos(todos.filter(function (todo) {
|
||||||
|
if (isWorkspace && todo.workspaceRootPath !== workspaceRoot) return false;
|
||||||
|
if (!isWorkspace && workspaceFilter === '__unassigned__' && todo.workspaceRootPath) return false;
|
||||||
|
if (!isWorkspace && workspaceFilter && workspaceFilter !== '__unassigned__' && todo.workspaceRootPath !== workspaceFilter) return false;
|
||||||
|
if (statusFilter !== 'all' && todo.status !== statusFilter) return false;
|
||||||
|
if (!searchQuery) return true;
|
||||||
|
return [todo.title, todo.description, todo.workspaceRootPath].join('\n').toLowerCase().indexOf(searchQuery) !== -1;
|
||||||
|
}), sortMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function persist() { return api.settings.write(GLOBAL_KEY, todos); }
|
||||||
|
function closeTodoModal() { modalHost.innerHTML = ''; modalHost.hidden = true; }
|
||||||
|
|
||||||
|
function showTodoModal(existingTodo) {
|
||||||
|
var editing = !!existingTodo;
|
||||||
|
var titleInput = el('input', { className: 'todo-input', type: 'text', value: editing ? existingTodo.title : '', 'data-todo-input': 'title' });
|
||||||
|
var descriptionInput = el('textarea', { className: 'todo-input textarea', value: editing ? existingTodo.description : '', 'data-todo-input': 'description' });
|
||||||
|
var priorityInput = el('select', { className: 'todo-select', 'data-todo-input': 'priority' }, [
|
||||||
|
el('option', { value: 'low', textContent: 'Low' }),
|
||||||
|
el('option', { value: 'normal', textContent: 'Normal' }),
|
||||||
|
el('option', { value: 'high', textContent: 'High' })
|
||||||
|
]);
|
||||||
|
priorityInput.value = editing ? existingTodo.priority : 'normal';
|
||||||
|
var dueInput = el('input', { className: 'todo-input', type: 'date', value: editing ? existingTodo.dueAt : '', 'data-todo-input': 'dueAt' });
|
||||||
|
var reminderInput = el('input', { className: 'todo-input', type: 'datetime-local', value: editing ? existingTodo.reminderAt : '', 'data-todo-input': 'reminderAt' });
|
||||||
|
var workspaceInput = null;
|
||||||
|
if (!isWorkspace) {
|
||||||
|
workspaceInput = el('select', { className: 'todo-select', 'data-todo-input': 'workspaceRootPath' });
|
||||||
|
workspaceInput.appendChild(el('option', { value: '', textContent: 'Unassigned' }));
|
||||||
|
workspaceRoots().forEach(function (rootName) { workspaceInput.appendChild(el('option', { value: rootName, textContent: rootName })); });
|
||||||
|
workspaceInput.value = editing ? existingTodo.workspaceRootPath : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTodo() {
|
||||||
|
var title = text(titleInput.value).trim();
|
||||||
|
if (!title) return;
|
||||||
|
var rootName = isWorkspace ? workspaceRoot : text(workspaceInput && workspaceInput.value).trim();
|
||||||
|
var timestamp = now();
|
||||||
|
var todo = normalizeTodo({
|
||||||
|
id: editing ? existingTodo.id : todoId(rootName, title),
|
||||||
|
title: title,
|
||||||
|
description: descriptionInput.value,
|
||||||
|
workspaceRootPath: rootName,
|
||||||
|
workspaceName: rootName,
|
||||||
|
status: editing ? existingTodo.status : 'open',
|
||||||
|
priority: priorityInput.value,
|
||||||
|
dueAt: dueInput.value,
|
||||||
|
reminderAt: reminderInput.value,
|
||||||
|
createdAt: editing ? existingTodo.createdAt : timestamp,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
completedAt: editing ? existingTodo.completedAt : '',
|
||||||
|
sourceUrl: editing ? existingTodo.sourceUrl : '',
|
||||||
|
linkedJournalEntryId: editing ? existingTodo.linkedJournalEntryId : ''
|
||||||
|
});
|
||||||
|
todos = editing ? todos.map(function (item) { return item.id === existingTodo.id ? todo : item; }) : [todo].concat(todos);
|
||||||
|
closeTodoModal();
|
||||||
|
statusEl.textContent = editing ? 'Todo updated' : 'Todo added';
|
||||||
|
persist().then(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fields = [
|
||||||
|
el('label', { className: 'todo-field wide' }, ['Title', titleInput]),
|
||||||
|
el('label', { className: 'todo-field wide' }, ['Description', descriptionInput]),
|
||||||
|
el('label', { className: 'todo-field' }, ['Priority', priorityInput]),
|
||||||
|
el('label', { className: 'todo-field' }, ['Due date', dueInput]),
|
||||||
|
el('label', { className: 'todo-field' }, ['Reminder', reminderInput])
|
||||||
|
];
|
||||||
|
if (workspaceInput) fields.push(el('label', { className: 'todo-field' }, ['Workspace', workspaceInput]));
|
||||||
|
else fields.push(el('div', { className: 'todo-field', textContent: 'Workspace: ' + workspaceRoot }));
|
||||||
|
modalHost.innerHTML = '';
|
||||||
|
modalHost.hidden = false;
|
||||||
|
modalHost.appendChild(el('div', { className: 'todo-modal-overlay' }, [
|
||||||
|
el('div', { className: 'todo-modal' }, [
|
||||||
|
el('div', { className: 'todo-modal-title', textContent: editing ? 'Edit Todo' : 'Add Todo' }),
|
||||||
|
el('div', { className: 'todo-form-grid' }, fields),
|
||||||
|
el('div', { className: 'todo-modal-actions' }, [
|
||||||
|
el('button', { className: 'todo-btn', textContent: 'Cancel', onClick: closeTodoModal }),
|
||||||
|
el('button', { className: 'todo-btn primary', 'data-todo-action': 'save', textContent: editing ? 'Save changes' : 'Add Todo', onClick: saveTodo })
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
titleInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTodoStatus(todo, status) {
|
||||||
|
var timestamp = now();
|
||||||
|
todos = todos.map(function (item) {
|
||||||
|
return item.id === todo.id ? Object.assign({}, item, { status: status, completedAt: status === 'done' ? timestamp : '', updatedAt: timestamp }) : item;
|
||||||
|
});
|
||||||
|
statusEl.textContent = status === 'done' ? 'Todo marked done' : (status === 'cancelled' ? 'Todo cancelled' : 'Todo reopened');
|
||||||
|
persist().then(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTodo(todo) {
|
||||||
|
todos = todos.filter(function (item) { return item.id !== todo.id; });
|
||||||
|
statusEl.textContent = 'Todo deleted';
|
||||||
|
persist().then(render);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWorkspace(todo) {
|
||||||
|
if (!todo.workspaceRootPath) return;
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:workspace-selected', { detail: { workspaceName: todo.workspaceRootPath } }));
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:workspace-open-tool', { detail: { kind: 'todo' } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createJournalEntry(todo) {
|
||||||
|
if (!isWorkspace || todo.status !== 'done') return;
|
||||||
|
window.dispatchEvent(new CustomEvent('verstak:workspace-open-tool', {
|
||||||
|
detail: {
|
||||||
|
kind: 'journal',
|
||||||
|
toolRequest: {
|
||||||
|
type: 'completed-todo',
|
||||||
|
todo: {
|
||||||
|
id: todo.id,
|
||||||
|
title: todo.title,
|
||||||
|
description: todo.description,
|
||||||
|
workspaceRootPath: workspaceRoot,
|
||||||
|
completedAt: todo.completedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function todoMeta(todo) {
|
||||||
|
var due = dueState(todo);
|
||||||
|
var reminderDue = reminderIsDue(todo);
|
||||||
|
var badges = [
|
||||||
|
el('span', { className: 'todo-badge', textContent: todo.priority + ' priority' }),
|
||||||
|
el('span', { className: 'todo-badge', textContent: todo.status })
|
||||||
|
];
|
||||||
|
if (!isWorkspace) badges.unshift(el('span', { className: 'todo-badge', textContent: todo.workspaceRootPath || 'Unassigned' }));
|
||||||
|
if (todo.dueAt) badges.push(el('span', { className: 'todo-badge ' + due, textContent: (due === 'overdue' ? 'Overdue · ' : (due === 'due-soon' ? 'Due soon · ' : '')) + 'Due ' + todo.dueAt }));
|
||||||
|
if (todo.reminderAt) badges.push(el('span', { className: 'todo-badge ' + (reminderDue ? 'reminder-due' : ''), textContent: (reminderDue ? 'Reminder due ' : 'Reminder ') + todo.reminderAt }));
|
||||||
|
return el('div', { className: 'todo-row-meta' }, badges);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderList() {
|
||||||
|
var visible = visibleTodos();
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
if (!visible.length) {
|
||||||
|
listEl.appendChild(el('div', { className: 'todo-empty', textContent: todos.length ? 'No todos match the current filters.' : 'No todos yet.' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visible.forEach(function (todo) {
|
||||||
|
var actions = [];
|
||||||
|
if (!isWorkspace && todo.workspaceRootPath) actions.push(el('button', { className: 'todo-btn', 'data-todo-action': 'open-workspace', textContent: 'Open workspace', onClick: function () { openWorkspace(todo); } }));
|
||||||
|
if (todo.status === 'open') {
|
||||||
|
actions.push(el('button', { className: 'todo-btn', 'data-todo-action': 'mark-done', textContent: 'Done', onClick: function () { setTodoStatus(todo, 'done'); } }));
|
||||||
|
actions.push(el('button', { className: 'todo-btn', 'data-todo-action': 'cancel', textContent: 'Cancel', onClick: function () { setTodoStatus(todo, 'cancelled'); } }));
|
||||||
|
} else {
|
||||||
|
actions.push(el('button', { className: 'todo-btn', 'data-todo-action': 'reopen', textContent: 'Reopen', onClick: function () { setTodoStatus(todo, 'open'); } }));
|
||||||
|
}
|
||||||
|
if (isWorkspace && todo.status === 'done') actions.push(el('button', { className: 'todo-btn', 'data-todo-action': 'create-journal-entry', textContent: 'Create Journal Entry', onClick: function () { createJournalEntry(todo); } }));
|
||||||
|
actions.push(el('button', { className: 'todo-btn', 'data-todo-action': 'edit', textContent: 'Edit', onClick: function () { showTodoModal(todo); } }));
|
||||||
|
actions.push(el('button', { className: 'todo-btn danger', 'data-todo-action': 'delete', textContent: 'Delete', onClick: function () { deleteTodo(todo); } }));
|
||||||
|
listEl.appendChild(el('div', { className: 'todo-row' + (todo.status === 'done' ? ' done' : ''), 'data-todo-id': todo.id }, [
|
||||||
|
el('div', {}, [
|
||||||
|
el('div', { className: 'todo-row-title', textContent: todo.title || 'Untitled todo' }),
|
||||||
|
todo.description ? el('div', { className: 'todo-row-description', textContent: todo.description }) : null,
|
||||||
|
todoMeta(todo)
|
||||||
|
]),
|
||||||
|
el('div', { className: 'todo-row-actions' }, actions)
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
var visible = visibleTodos();
|
||||||
|
countEl.textContent = visible.length === todos.length ? todos.length + ' todo' + (todos.length === 1 ? '' : 's') : visible.length + ' of ' + todos.length + ' todos';
|
||||||
|
statusFilterEl.value = statusFilter;
|
||||||
|
sortEl.value = sortMode;
|
||||||
|
searchEl.value = searchQuery;
|
||||||
|
renderWorkspaceOptions();
|
||||||
|
renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
api.settings.read().then(function (settings) {
|
||||||
|
todos = rows((settings || {})[GLOBAL_KEY]).map(normalizeTodo);
|
||||||
|
render();
|
||||||
|
}).catch(render);
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
TodoView.unmount = function (containerEl) { containerEl.innerHTML = ''; };
|
||||||
|
window.VerstakPluginRegister('verstak.todo', { components: { TodoView: TodoView } });
|
||||||
|
}.toString() + ')();';
|
||||||
|
}
|
||||||
|
|
||||||
function searchPluginBundle() {
|
function searchPluginBundle() {
|
||||||
return '(' + function () {
|
return '(' + function () {
|
||||||
function el(tag, attrs, children) {
|
function el(tag, attrs, children) {
|
||||||
|
|
@ -2873,6 +3278,9 @@
|
||||||
if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(browserInboxBundle());
|
return Promise.resolve(browserInboxBundle());
|
||||||
}
|
}
|
||||||
|
if (pluginId === 'verstak.todo' && assetPath === 'frontend/dist/index.js') {
|
||||||
|
return Promise.resolve(todoBundle());
|
||||||
|
}
|
||||||
if (pluginId === 'verstak.search' && assetPath === 'frontend/dist/index.js') {
|
if (pluginId === 'verstak.search' && assetPath === 'frontend/dist/index.js') {
|
||||||
return Promise.resolve(searchPluginBundle());
|
return Promise.resolve(searchPluginBundle());
|
||||||
}
|
}
|
||||||
|
|
@ -3482,6 +3890,7 @@
|
||||||
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
rootPath: '/tmp/verstak-test/plugins/browser-inbox',
|
||||||
error: ''
|
error: ''
|
||||||
},
|
},
|
||||||
|
'verstak.todo': makeTodoPluginState(),
|
||||||
'verstak.search': {
|
'verstak.search': {
|
||||||
status: 'loaded',
|
status: 'loaded',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -3512,6 +3921,8 @@
|
||||||
vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files', 'verstak.notes', 'verstak.sync', 'verstak.activity', 'verstak.journal', 'verstak.browser-inbox', 'verstak.search'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }, { id: 'verstak.notes', version: '0.1.0', source: 'official' }, { id: 'verstak.sync', version: '0.1.0', source: 'official' }, { id: 'verstak.activity', version: '0.1.0', source: 'official' }, { id: 'verstak.journal', version: '0.1.0', source: 'official' }, { id: 'verstak.browser-inbox', version: '0.1.0', source: 'official' }, { id: 'verstak.search', version: '0.1.0', source: 'official' }] };
|
||||||
vaultPluginState.enabledPlugins.push('verstak.trash');
|
vaultPluginState.enabledPlugins.push('verstak.trash');
|
||||||
vaultPluginState.desiredPlugins.push({ id: 'verstak.trash', version: '0.1.0', source: 'official' });
|
vaultPluginState.desiredPlugins.push({ id: 'verstak.trash', version: '0.1.0', source: 'official' });
|
||||||
|
vaultPluginState.enabledPlugins.push('verstak.todo');
|
||||||
|
vaultPluginState.desiredPlugins.push({ id: 'verstak.todo', version: '0.1.0', source: 'official' });
|
||||||
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
|
||||||
workbenchPreferences = {};
|
workbenchPreferences = {};
|
||||||
openedResources = [];
|
openedResources = [];
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
import Folder from 'lucide-svelte/icons/folder';
|
import Folder from 'lucide-svelte/icons/folder';
|
||||||
import Inbox from 'lucide-svelte/icons/inbox';
|
import Inbox from 'lucide-svelte/icons/inbox';
|
||||||
import LayoutGrid from 'lucide-svelte/icons/layout-grid';
|
import LayoutGrid from 'lucide-svelte/icons/layout-grid';
|
||||||
|
import ListTodo from 'lucide-svelte/icons/list-todo';
|
||||||
import PanelsTopLeft from 'lucide-svelte/icons/panels-top-left';
|
import PanelsTopLeft from 'lucide-svelte/icons/panels-top-left';
|
||||||
import Pencil from 'lucide-svelte/icons/pencil';
|
import Pencil from 'lucide-svelte/icons/pencil';
|
||||||
import Plug from 'lucide-svelte/icons/plug';
|
import Plug from 'lucide-svelte/icons/plug';
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
folder: Folder,
|
folder: Folder,
|
||||||
gear: Settings,
|
gear: Settings,
|
||||||
inbox: Inbox,
|
inbox: Inbox,
|
||||||
|
'list-todo': ListTodo,
|
||||||
logo: PanelsTopLeft,
|
logo: PanelsTopLeft,
|
||||||
plugin: Plug,
|
plugin: Plug,
|
||||||
puzzle: Puzzle,
|
puzzle: Puzzle,
|
||||||
|
|
@ -61,6 +63,7 @@
|
||||||
'🧪': 'flask',
|
'🧪': 'flask',
|
||||||
danger: 'warning',
|
danger: 'warning',
|
||||||
settings: 'gear',
|
settings: 'gear',
|
||||||
|
todo: 'list-todo',
|
||||||
};
|
};
|
||||||
|
|
||||||
$: iconKey = aliases[name] || name || 'dot';
|
$: iconKey = aliases[name] || name || 'dot';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue