@ikenga/pkg-tasks 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,315 @@
1
+ // Pure transforms + label helpers — ported from src/routes/tasks/-_shared.ts.
2
+ // Types carried via JSDoc; imports the Task typedef from queries.js.
3
+
4
+ /** @typedef {import('./queries.js').Task} Task */
5
+ /** @typedef {import('./queries.js').TaskPriority} TaskPriority */
6
+ /** @typedef {import('./queries.js').TaskStatus} TaskStatus */
7
+
8
+ /** @typedef {'full'|'compact'|'side'} Density */
9
+ /** @typedef {'overdue'|'today'|'week'|'later'|'autoclosed'} GroupKey */
10
+ /** @typedef {{ key: GroupKey, label: string, tasks: Task[] }} TaskGroup */
11
+
12
+ const ONE_DAY = 24 * 60 * 60 * 1000;
13
+
14
+ /** @param {Date} d */
15
+ function startOfDay(d) {
16
+ const x = new Date(d);
17
+ x.setHours(0, 0, 0, 0);
18
+ return x;
19
+ }
20
+
21
+ /**
22
+ * @param {Task[]} tasks
23
+ * @param {boolean} showAutoclosed
24
+ * @returns {TaskGroup[]}
25
+ */
26
+ export function groupTasks(tasks, showAutoclosed) {
27
+ const now = new Date();
28
+ const today = startOfDay(now);
29
+ const tomorrow = new Date(today.getTime() + ONE_DAY);
30
+ const weekEnd = new Date(today.getTime() + 7 * ONE_DAY);
31
+
32
+ /** @type {Task[]} */ const overdue = [];
33
+ /** @type {Task[]} */ const todayG = [];
34
+ /** @type {Task[]} */ const week = [];
35
+ /** @type {Task[]} */ const later = [];
36
+ /** @type {Task[]} */ const autoclosed = [];
37
+
38
+ for (const t of tasks) {
39
+ const isAutoClosedT =
40
+ t.status === 'completed' &&
41
+ !!t.outcome_notes &&
42
+ t.outcome_notes.startsWith('Auto-closed by task-health');
43
+ if (isAutoClosedT) {
44
+ if (showAutoclosed) autoclosed.push(t);
45
+ continue;
46
+ }
47
+ if (t.status === 'completed' || t.status === 'cancelled') continue;
48
+ const due = t.due_date ? new Date(t.due_date) : null;
49
+ if (!due) {
50
+ later.push(t);
51
+ continue;
52
+ }
53
+ if (due < today) overdue.push(t);
54
+ else if (due < tomorrow) todayG.push(t);
55
+ else if (due < weekEnd) week.push(t);
56
+ else later.push(t);
57
+ }
58
+
59
+ /** @type {TaskGroup[]} */ const out = [];
60
+ if (overdue.length) out.push({ key: 'overdue', label: 'Overdue', tasks: overdue });
61
+ if (todayG.length) out.push({ key: 'today', label: 'Today', tasks: todayG });
62
+ if (week.length) out.push({ key: 'week', label: 'This week', tasks: week });
63
+ if (later.length) out.push({ key: 'later', label: 'Later', tasks: later });
64
+ if (autoclosed.length)
65
+ out.push({ key: 'autoclosed', label: 'Auto-closed', tasks: autoclosed });
66
+ return out;
67
+ }
68
+
69
+ // ─── In-body view switcher (Round 16 · D-2 / D-3) ───────────────────────────
70
+ /** @typedef {'tasks'|'agenda'|'triage'} TaskView */
71
+
72
+ // ─── Agenda / Today (D-2) ────────────────────────────────────────────────────
73
+ /** @typedef {'me'|'agent'|'silent'|'deadline'} AgendaLane */
74
+ /** @typedef {{ task: Task, lane: AgendaLane, done: boolean }} AgendaBlock */
75
+ /** @typedef {{ key: string, time: string, hour: number, blocks: AgendaBlock[] }} AgendaSlot */
76
+
77
+ /**
78
+ * @param {Task} t
79
+ * @param {boolean} overdue
80
+ * @returns {AgendaLane}
81
+ */
82
+ function agendaLane(t, overdue) {
83
+ if (overdue) return 'deadline';
84
+ if (t.execution_mode === 'report') return 'silent';
85
+ if (assigneeIsAgent(t)) return 'agent';
86
+ return 'me';
87
+ }
88
+
89
+ /**
90
+ * Project the already-fetched task list onto a time rail for *today* — overdue
91
+ * items pulled to a leading bucket, the rest grouped by due-hour.
92
+ * @param {Task[]} tasks
93
+ * @param {Date} [now]
94
+ * @returns {AgendaSlot[]}
95
+ */
96
+ export function buildAgenda(tasks, now = new Date()) {
97
+ const today = startOfDay(now);
98
+ const tomorrow = new Date(today.getTime() + ONE_DAY);
99
+
100
+ /** @type {AgendaBlock[]} */ const overdueBlocks = [];
101
+ /** @type {Map<number, AgendaBlock[]>} */ const byHour = new Map();
102
+
103
+ for (const t of tasks) {
104
+ if (t.status === 'cancelled') continue;
105
+ const done = isAutoClosed(t) || t.status === 'completed';
106
+ if (!t.due_date) continue; // no due → not on today's rail
107
+ const due = new Date(t.due_date);
108
+ if (due >= tomorrow) continue; // future days not shown on the Today rail
109
+ if (due < today) {
110
+ if (!done) overdueBlocks.push({ task: t, lane: agendaLane(t, true), done });
111
+ continue;
112
+ }
113
+ const hour = due.getHours();
114
+ /** @type {AgendaBlock} */ const block = { task: t, lane: agendaLane(t, false), done };
115
+ const bucket = byHour.get(hour);
116
+ if (bucket) bucket.push(block);
117
+ else byHour.set(hour, [block]);
118
+ }
119
+
120
+ /** @type {AgendaSlot[]} */ const slots = [];
121
+ if (overdueBlocks.length) {
122
+ slots.push({ key: 'overdue', time: 'overdue', hour: -1, blocks: overdueBlocks });
123
+ }
124
+ for (const hour of [...byHour.keys()].sort((a, b) => a - b)) {
125
+ slots.push({
126
+ key: `${hour}:00`,
127
+ time: `${String(hour).padStart(2, '0')}:00`,
128
+ hour,
129
+ blocks: /** @type {AgendaBlock[]} */ (byHour.get(hour)),
130
+ });
131
+ }
132
+ return slots;
133
+ }
134
+
135
+ // ─── Triage / Health (D-3) ───────────────────────────────────────────────────
136
+ /** @typedef {'overdue'|'stale'|'unassigned'|'blocked'} TriageBucketKey */
137
+ /** @typedef {{ key: TriageBucketKey, label: string, sample: Task[] }} TriageBucket */
138
+
139
+ const STALE_DAYS = 7;
140
+
141
+ /**
142
+ * @param {Task} t
143
+ * @returns {boolean}
144
+ */
145
+ function isActive(t) {
146
+ return t.status === 'pending' || t.status === 'in_progress' || t.status === 'blocked';
147
+ }
148
+
149
+ /**
150
+ * Client-side sample rows for each health bucket, from the loaded list.
151
+ * @param {Task[]} tasks
152
+ * @param {Date} [now]
153
+ * @returns {TriageBucket[]}
154
+ */
155
+ export function buildTriage(tasks, now = new Date()) {
156
+ const today = startOfDay(now);
157
+ const staleBefore = now.getTime() - STALE_DAYS * ONE_DAY;
158
+
159
+ /** @type {Task[]} */ const overdue = [];
160
+ /** @type {Task[]} */ const stale = [];
161
+ /** @type {Task[]} */ const unassigned = [];
162
+ /** @type {Task[]} */ const blocked = [];
163
+
164
+ for (const t of tasks) {
165
+ if (!isActive(t)) continue;
166
+ if (t.due_date && new Date(t.due_date) < today) overdue.push(t);
167
+ if (t.updated_at && new Date(t.updated_at).getTime() < staleBefore) stale.push(t);
168
+ if (!t.assigned_to) unassigned.push(t);
169
+ if (t.status === 'blocked') blocked.push(t);
170
+ }
171
+
172
+ /** @param {Task[]} xs */
173
+ const cap = (xs) => xs.slice(0, 3);
174
+ return [
175
+ { key: 'overdue', label: 'Overdue', sample: cap(overdue) },
176
+ { key: 'stale', label: `Stale · no activity > ${STALE_DAYS}d`, sample: cap(stale) },
177
+ { key: 'unassigned', label: 'Unassigned', sample: cap(unassigned) },
178
+ { key: 'blocked', label: 'Blocked', sample: cap(blocked) },
179
+ ];
180
+ }
181
+
182
+ /**
183
+ * Human label for an execution mode.
184
+ * @param {Task['execution_mode']} m
185
+ * @returns {string}
186
+ */
187
+ export function execModeLabel(m) {
188
+ if (!m) return '';
189
+ if (m === 'approval_required') return 'approval req';
190
+ if (m === 'report') return 'silent';
191
+ return m;
192
+ }
193
+
194
+ /**
195
+ * @param {TaskPriority|null|undefined} p
196
+ * @returns {string}
197
+ */
198
+ export function priorityClass(p) {
199
+ if (!p) return 'is-low';
200
+ return `is-${p}`;
201
+ }
202
+
203
+ /**
204
+ * @param {TaskStatus} s
205
+ * @returns {string}
206
+ */
207
+ export function statusClass(s) {
208
+ return `is-${s}`;
209
+ }
210
+
211
+ /**
212
+ * @param {string|null} d
213
+ * @returns {{ label: string, cls: string }}
214
+ */
215
+ export function dueLabel(d) {
216
+ if (!d) return { label: '—', cls: '' };
217
+ const due = new Date(d);
218
+ const now = new Date();
219
+ const today = startOfDay(now);
220
+ const dueDay = startOfDay(due);
221
+ const dayDiff = Math.round((dueDay.getTime() - today.getTime()) / ONE_DAY);
222
+
223
+ if (dueDay.getTime() < today.getTime()) {
224
+ const overdueDays = Math.abs(dayDiff);
225
+ return {
226
+ label: overdueDays === 0 ? 'overdue' : `${overdueDays}d overdue`,
227
+ cls: 'is-overdue',
228
+ };
229
+ }
230
+ if (dayDiff === 0) {
231
+ const hh = String(due.getHours()).padStart(2, '0');
232
+ const mm = String(due.getMinutes()).padStart(2, '0');
233
+ return { label: `today · ${hh}:${mm}`, cls: 'is-today' };
234
+ }
235
+ if (dayDiff < 7) {
236
+ return {
237
+ label: due.toLocaleDateString(undefined, { weekday: 'short' }),
238
+ cls: '',
239
+ };
240
+ }
241
+ return {
242
+ label: due.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
243
+ cls: '',
244
+ };
245
+ }
246
+
247
+ /**
248
+ * @param {string|null} iso
249
+ * @returns {string}
250
+ */
251
+ export function relativeAgo(iso) {
252
+ if (!iso) return '';
253
+ const t = new Date(iso).getTime();
254
+ const diff = Date.now() - t;
255
+ if (diff < 60_000) return 'just now';
256
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
257
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
258
+ return `${Math.floor(diff / 86_400_000)}d ago`;
259
+ }
260
+
261
+ /**
262
+ * @param {Pick<Task, 'status'|'outcome_notes'>} t
263
+ * @returns {boolean}
264
+ */
265
+ export function isAutoClosed(t) {
266
+ return (
267
+ t.status === 'completed' &&
268
+ !!t.outcome_notes &&
269
+ t.outcome_notes.startsWith('Auto-closed by task-health')
270
+ );
271
+ }
272
+
273
+ /**
274
+ * @param {string|null} notes
275
+ * @returns {string|null}
276
+ */
277
+ export function autoCloseSignal(notes) {
278
+ if (!notes) return null;
279
+ if (!notes.startsWith('Auto-closed by task-health')) return null;
280
+ // "Auto-closed by task-health: email_draft 4f12 sent ..."
281
+ const lower = notes.toLowerCase();
282
+ if (lower.includes('email_draft')) return 'email-sent';
283
+ if (lower.includes('social_queue')) return 'social-posted';
284
+ if (lower.includes('blog')) return 'blog-published';
285
+ if (lower.includes('commit')) return 'git-commit';
286
+ if (lower.includes('deal')) return 'deal-closed';
287
+ return 'auto-closed';
288
+ }
289
+
290
+ /**
291
+ * @param {string} id
292
+ * @returns {string}
293
+ */
294
+ export function shortId(id) {
295
+ return id.slice(0, 8);
296
+ }
297
+
298
+ /**
299
+ * @param {Task} t
300
+ * @returns {boolean}
301
+ */
302
+ export function assigneeIsAgent(t) {
303
+ if (t.assignee_type === 'agent') return true;
304
+ if (t.assignee_type === 'human') return false;
305
+ return !!(t.assigned_to && t.assigned_to.endsWith('-agent'));
306
+ }
307
+
308
+ /**
309
+ * @param {string|null} name
310
+ * @returns {string}
311
+ */
312
+ export function avatarInitial(name) {
313
+ if (!name) return '?';
314
+ return name.charAt(0).toUpperCase();
315
+ }
@@ -0,0 +1,35 @@
1
+ // Lazy Supabase client — keys arrive async via hostContext, so createClient()
2
+ // cannot run at module-eval time. Call setSupabaseConfig() once after the
3
+ // bridge resolves, then getSupabase() everywhere else.
4
+ //
5
+ // Single-user / RLS-only — no JWT handshake. The shell resolves the Supabase
6
+ // url + anon key from its Stronghold vault and threads them through the
7
+ // AppBridge `hostContext.supabase` block (capabilities.supabase in
8
+ // manifest.json). In standalone dev, app.js reads them from the query string.
9
+
10
+ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
11
+
12
+ let client = null;
13
+
14
+ export function setSupabaseConfig(config) {
15
+ if (!config?.url || !config?.anonKey) {
16
+ client = null;
17
+ return;
18
+ }
19
+ client = createClient(config.url, config.anonKey, {
20
+ auth: {
21
+ persistSession: false,
22
+ autoRefreshToken: false,
23
+ detectSessionInUrl: false,
24
+ },
25
+ });
26
+ }
27
+
28
+ export function getSupabase() {
29
+ if (!client) throw new Error('[tasks] supabase client not configured yet');
30
+ return client;
31
+ }
32
+
33
+ export function hasSupabase() {
34
+ return client !== null;
35
+ }
@@ -0,0 +1,5 @@
1
+ // Auto-derived from tasks.css — CSS-as-string so it loads via the script path
2
+ // (WebKitGTK can't load <link>/fetch subresources from the about:srcdoc the
3
+ // shell mounts — only inlined scripts work; see index.html). app.js injects
4
+ // this as an inline <style> (allowed by style-src 'unsafe-inline').
5
+ export default "/* Tasks screen — Ikenga Rung 3 / Batch 3 (08-tasks.html)\n * Reuses tokens from src/lib/ikenga/tokens.css. No new tokens.\n */\n\n/* === Frame ================================================== */\n.tk-frame {\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n background: var(--bg-surface);\n overflow: hidden;\n box-shadow: var(--shadow-2);\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.tk-frame-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--space-4);\n padding: var(--space-4) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: linear-gradient(180deg, var(--tint-bg-active, var(--bg-surface)) 0%, var(--bg-surface) 100%);\n}\n.tk-frame-title-wrap { display: flex; align-items: center; gap: var(--space-2); }\n.tk-frame-title-mark { width: 18px; height: 18px; color: var(--tint-fg-active, var(--primary)); }\n.tk-frame-title {\n font-family: var(--font-display);\n font-weight: 500;\n font-size: var(--text-h3);\n margin: 0;\n color: var(--fg);\n}\n.tk-frame-sub {\n margin-top: 2px;\n font-size: var(--text-caption);\n color: var(--fg-muted);\n}\n.tk-frame-count {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--fg-muted);\n font-weight: 400;\n margin-left: 6px;\n}\n\n/* === Filter bar ============================================= */\n.tk-filterbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n padding: var(--space-3) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n}\n.tk-filterbar .input-search-wrap { position: relative; display: inline-flex; }\n.tk-filterbar .input-search-wrap svg {\n position: absolute;\n left: 8px;\n top: 50%;\n transform: translateY(-50%);\n width: 13px;\n height: 13px;\n color: var(--fg-faint);\n}\n.tk-filterbar input[type='text'] {\n width: 280px;\n height: 28px;\n padding: 0 8px 0 28px;\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg);\n font-family: inherit;\n font-size: 11.5px;\n}\n.tk-filterbar select {\n height: 28px;\n font-size: 11.5px;\n padding: 0 var(--space-2);\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg);\n font-family: inherit;\n}\n.tk-filterbar .label {\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.tk-filterbar .spacer { flex: 1; }\n.tk-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 28px;\n padding: 0 10px;\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n color: var(--fg-muted);\n font-family: inherit;\n font-size: 11px;\n cursor: pointer;\n}\n.tk-toggle:hover { color: var(--fg); border-color: var(--fg-faint); }\n.tk-toggle.is-on {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border));\n}\n.tk-toggle .checkbox {\n width: 11px;\n height: 11px;\n border: 1px solid currentColor;\n border-radius: 2px;\n display: inline-grid;\n place-items: center;\n flex-shrink: 0;\n}\n.tk-toggle.is-on .checkbox::after {\n content: '✓';\n font-size: 9px;\n line-height: 1;\n color: currentColor;\n}\n\n/* === Master/detail split ==================================== */\n.tk-split {\n --list-w: 360px;\n display: grid;\n grid-template-columns: minmax(280px, var(--list-w)) 4px minmax(420px, 1fr);\n /* Bind the single row to the split's own height. Without this the implicit\n * row is `auto` (content-sized), so a tall list/detail grows the row past the\n * split and gets clipped by overflow:hidden — neither pane scrolls. */\n grid-template-rows: minmax(0, 1fr);\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n.tk-list {\n overflow-y: auto;\n /* Grid items default to min-height:auto (won't shrink below content), which\n * defeats overflow:auto. Allow shrink so the list scrolls within its track. */\n min-height: 0;\n background: var(--bg-surface);\n}\n.tk-divider {\n background: var(--border-soft);\n cursor: col-resize;\n}\n.tk-divider:hover { background: var(--tint-fg-active, var(--primary)); }\n.tk-detail {\n background: var(--bg-base);\n overflow-y: auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* === Group dividers ========================================= */\n.tk-group-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px var(--space-4) 6px var(--space-3);\n background: var(--bg-sunken);\n border-bottom: 1px solid var(--border-soft);\n border-top: 1px solid var(--border-soft);\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-faint);\n letter-spacing: 0.1em;\n text-transform: uppercase;\n position: sticky;\n top: 0;\n z-index: 4;\n cursor: pointer;\n user-select: none;\n}\n.tk-group-head:first-child { border-top: 0; }\n.tk-group-head .ct {\n color: var(--fg-muted);\n font-variant-numeric: tabular-nums;\n}\n.tk-group-head.is-overdue { color: var(--danger); }\n.tk-group-head.is-overdue .ct { color: var(--danger); }\n.tk-group-head.is-autoclosed { color: var(--live); }\n.tk-group-head.is-autoclosed .ct { color: var(--live); }\n.tk-group-head.is-collapsed { background: var(--bg-base); }\n.tk-group-head .chev {\n width: 10px;\n height: 10px;\n color: var(--fg-faint);\n transition: transform var(--motion-fast) var(--ease-calm);\n}\n.tk-group-head.is-collapsed .chev { transform: rotate(-90deg); }\n.tk-group-label {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n/* === Task row =============================================== */\n.tk-row {\n display: grid;\n grid-template-columns: 14px 1fr auto;\n gap: var(--space-3);\n padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);\n border-bottom: 1px solid var(--border-soft);\n cursor: pointer;\n position: relative;\n background: transparent;\n text-align: left;\n width: 100%;\n border-left: 0;\n border-right: 0;\n border-top: 0;\n font: inherit;\n color: inherit;\n transition: background var(--motion-fast) var(--ease-calm);\n}\n.tk-row:hover { background: var(--bg-raised); }\n.tk-row.is-on { background: var(--bg-raised); }\n.tk-row.is-on::before {\n content: '';\n position: absolute;\n left: 0;\n top: 8px;\n bottom: 8px;\n width: 2px;\n border-radius: 2px;\n background: var(--tint-fg-active, var(--primary));\n}\n.tk-row .pri-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n margin-top: 5px;\n background: var(--fg-faint);\n flex-shrink: 0;\n}\n.tk-row .pri-dot.is-critical {\n background: var(--danger);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent);\n}\n.tk-row .pri-dot.is-high { background: var(--achievement); }\n.tk-row .pri-dot.is-medium { background: var(--systemic); }\n.tk-row .pri-dot.is-low { background: var(--fg-faint); }\n.tk-row .body {\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.tk-row .title {\n font-size: var(--text-body-sm);\n color: var(--fg);\n font-weight: 500;\n line-height: 1.35;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.tk-row.is-completed .title {\n color: var(--fg-muted);\n text-decoration: line-through;\n text-decoration-thickness: 1px;\n}\n.tk-row .meta {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-row .meta .cat {\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n color: var(--fg-muted);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n text-transform: lowercase;\n}\n.tk-row .right {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n flex-shrink: 0;\n}\n.tk-row .due {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-muted);\n letter-spacing: 0.04em;\n white-space: nowrap;\n}\n.tk-row .due.is-overdue { color: var(--danger); font-weight: 500; }\n.tk-row .due.is-today { color: var(--achievement); }\n\n/* === Status badge =========================================== */\n.tk-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n color: var(--fg-muted);\n background: var(--bg-base);\n white-space: nowrap;\n}\n.tk-badge.is-pending { color: var(--fg-muted); }\n.tk-badge.is-in_progress {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border-soft));\n}\n.tk-badge.is-blocked {\n color: var(--danger);\n background: color-mix(in srgb, var(--danger) 12%, transparent);\n border-color: color-mix(in srgb, var(--danger) 30%, var(--border-soft));\n}\n.tk-badge.is-completed {\n color: var(--live);\n background: var(--live-soft);\n border-color: color-mix(in srgb, var(--live) 30%, var(--border-soft));\n}\n.tk-badge.is-cancelled { color: var(--fg-faint); }\n.tk-badge .dot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: currentColor;\n}\n\n/* === Auto-close badge ======================================= */\n.tk-autoclose {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n color: var(--live);\n background: var(--live-soft);\n border: 1px solid color-mix(in srgb, var(--live) 25%, var(--border-soft));\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n white-space: nowrap;\n}\n.tk-autoclose svg { width: 9px; height: 9px; }\n\n/* === Assignee chips ========================================= */\n.tk-assignee {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.04em;\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n background: var(--bg-base);\n color: var(--fg-muted);\n white-space: nowrap;\n}\n.tk-assignee.is-agent {\n color: var(--agent);\n background: var(--agent-soft);\n border-color: color-mix(in srgb, var(--agent) 30%, var(--border-soft));\n}\n.tk-assignee .avatar {\n width: 11px;\n height: 11px;\n border-radius: 50%;\n background: var(--bg-raised);\n color: var(--fg);\n display: inline-grid;\n place-items: center;\n font-size: 7.5px;\n font-weight: 600;\n text-transform: uppercase;\n}\n.tk-assignee.is-agent .avatar {\n background: color-mix(in srgb, var(--agent) 24%, transparent);\n color: var(--agent);\n}\n.tk-assignee .dot {\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--agent);\n}\n\n/* === Exec mode pill ========================================= */\n.tk-execmode {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--fg-faint);\n border: 1px dashed var(--border-soft);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: transparent;\n}\n.tk-execmode.is-autonomous {\n color: var(--agent);\n border-color: color-mix(in srgb, var(--agent) 30%, var(--border-soft));\n}\n.tk-execmode.is-approval_required {\n color: var(--achievement);\n border-color: color-mix(in srgb, var(--achievement) 35%, var(--border-soft));\n}\n.tk-execmode.is-report {\n color: var(--systemic);\n border-color: color-mix(in srgb, var(--systemic) 30%, var(--border-soft));\n}\n\n/* === Detail panel =========================================== */\n.tk-det-head {\n padding: var(--space-4) var(--space-5);\n border-bottom: 1px solid var(--border-soft);\n background: linear-gradient(180deg, var(--tint-bg-active, var(--bg-surface)) 0%, var(--bg-base) 100%);\n}\n.tk-det-topline {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-3);\n margin-bottom: 8px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-det-topline .id {\n background: var(--bg-base);\n border: 1px solid var(--border-soft);\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n color: var(--fg);\n}\n.tk-det-actions { display: flex; gap: 4px; align-items: center; }\n.tk-det-title {\n font-family: var(--font-display);\n font-weight: 500;\n font-size: var(--text-h3);\n margin: 0;\n color: var(--fg);\n line-height: 1.25;\n}\n.tk-det-meta-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n margin-top: var(--space-3);\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n}\n.tk-det-meta-row .sep { color: var(--fg-faint); }\n.tk-det-meta-row .pri-label { color: var(--achievement); display: inline-flex; align-items: center; gap: 4px; }\n.tk-det-meta-row .pri-label.is-critical { color: var(--danger); }\n.tk-det-meta-row .pri-label.is-high { color: var(--achievement); }\n.tk-det-meta-row .pri-label.is-medium { color: var(--systemic); }\n.tk-det-meta-row .pri-label.is-low { color: var(--fg-muted); }\n.tk-det-meta-row .pri-label .dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: currentColor;\n}\n.tk-det-meta-row .due-text { color: var(--fg-muted); }\n.tk-det-meta-row .due-text.is-overdue { color: var(--danger); }\n.tk-det-body {\n padding: var(--space-5);\n display: flex;\n flex-direction: column;\n gap: var(--space-5);\n flex: 1;\n}\n.tk-det-grid {\n display: grid;\n grid-template-columns: 110px 1fr;\n gap: 10px var(--space-4);\n font-size: var(--text-body-sm);\n}\n.tk-det-grid dt {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n letter-spacing: 0.1em;\n text-transform: uppercase;\n align-self: center;\n}\n.tk-det-grid dd {\n margin: 0;\n color: var(--fg);\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-2);\n font-size: var(--text-body-sm);\n}\n.tk-det-grid dd code {\n font-family: var(--font-mono);\n font-size: 11.5px;\n background: var(--bg-sunken);\n border: 1px solid var(--border-soft);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n color: var(--fg);\n}\n.tk-section-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--fg-faint);\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n margin-bottom: 6px;\n}\n.tk-section-label .ct {\n font-family: var(--font-mono);\n font-size: 9.5px;\n color: var(--fg-muted);\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.04em;\n text-transform: none;\n}\n.tk-deferred-pill {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--fg-faint);\n border: 1px dashed var(--border);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: transparent;\n}\n\n.tk-desc {\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n line-height: 1.6;\n background: var(--bg-surface);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-md);\n padding: var(--space-3) var(--space-4);\n white-space: pre-wrap;\n}\n\n/* === Progress bar =========================================== */\n.tk-progress {\n flex: 1;\n height: 6px;\n background: var(--bg-sunken);\n border-radius: 3px;\n overflow: hidden;\n border: 1px solid var(--border-soft);\n min-width: 120px;\n}\n.tk-progress > span {\n display: block;\n height: 100%;\n background: var(--live);\n border-radius: 3px;\n transition: width var(--motion-fast) var(--ease-calm);\n}\n\n/* === Evidence card ========================================== */\n.tk-evidence {\n border: 1px solid color-mix(in srgb, var(--live) 30%, var(--border-soft));\n background: var(--live-soft);\n border-radius: var(--radius-md);\n padding: var(--space-3) var(--space-4);\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.tk-evidence-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-3);\n}\n.tk-evidence .rule-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n letter-spacing: 0.04em;\n color: var(--live);\n background: var(--bg-surface);\n border: 1px solid color-mix(in srgb, var(--live) 30%, var(--border-soft));\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n}\n.tk-evidence .rule-chip svg { width: 10px; height: 10px; }\n.tk-evidence .rule-chip.is-flag {\n color: var(--achievement);\n border-color: color-mix(in srgb, var(--achievement) 30%, var(--border-soft));\n background: var(--bg-surface);\n}\n.tk-evidence .timestamp {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-muted);\n letter-spacing: 0.04em;\n}\n.tk-evidence .body {\n font-size: var(--text-body-sm);\n color: var(--fg);\n line-height: 1.55;\n}\n\n/* === Source-ref chips ======================================= */\n.tk-source-row {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n.tk-src {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n font-family: var(--font-mono);\n font-size: 10.5px;\n color: var(--fg-muted);\n background: var(--bg-sunken);\n border: 1px solid var(--border-soft);\n padding: 3px 7px;\n border-radius: var(--radius-sm);\n text-decoration: none;\n letter-spacing: 0.02em;\n cursor: pointer;\n}\n.tk-src:hover { color: var(--fg); border-color: var(--fg-faint); }\n.tk-src svg { width: 11px; height: 11px; flex-shrink: 0; color: var(--fg-faint); }\n.tk-src.is-email svg { color: var(--mail-fg, var(--achievement)); }\n.tk-src.is-session svg { color: var(--agent); }\n.tk-src.is-git svg { color: var(--systemic); }\n\n/* === Subtasks =============================================== */\n.tk-subtasks { display: flex; flex-direction: column; gap: 4px; }\n.tk-sub-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: var(--space-2);\n padding: 6px var(--space-3);\n background: var(--bg-surface);\n border: 1px solid var(--border-soft);\n border-radius: var(--radius-sm);\n font-size: var(--text-body-sm);\n cursor: pointer;\n text-align: left;\n width: 100%;\n font: inherit;\n color: inherit;\n transition: background var(--motion-fast) var(--ease-calm);\n}\n.tk-sub-row:hover { background: var(--bg-raised); }\n.tk-sub-row .name {\n color: var(--fg);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.tk-sub-row.is-completed .name {\n color: var(--fg-muted);\n text-decoration: line-through;\n}\n.tk-sub-row .due {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n}\n\n/* === Activity timeline ====================================== */\n.tk-timeline {\n display: flex;\n flex-direction: column;\n gap: 0;\n border-left: 1px dashed var(--border);\n margin-left: 6px;\n padding-left: var(--space-4);\n position: relative;\n}\n.tk-tl-item {\n position: relative;\n padding: 6px 0;\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n.tk-tl-item::before {\n content: '';\n position: absolute;\n left: calc(-1 * var(--space-4) - 4px);\n top: 12px;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--bg-sunken);\n border: 1px solid var(--border);\n}\n.tk-tl-item.is-mark::before {\n background: var(--tint-fg-active, var(--primary));\n border-color: var(--tint-fg-active, var(--primary));\n}\n.tk-tl-item.is-ok::before {\n background: var(--live);\n border-color: var(--live);\n}\n.tk-tl-item .when {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg-faint);\n letter-spacing: 0.04em;\n margin-right: 6px;\n}\n.tk-tl-item .actor {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--fg);\n background: var(--bg-sunken);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n border: 1px solid var(--border-soft);\n margin-right: 6px;\n}\n.tk-tl-item .actor.is-agent { color: var(--agent); }\n\n/* === Action footer ========================================== */\n.tk-action-bar {\n border-top: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n padding: var(--space-3) var(--space-5);\n display: flex;\n align-items: center;\n gap: var(--space-2);\n margin-top: auto;\n}\n.tk-action-bar .spacer { flex: 1; }\n\n/* === Empty state in detail =================================== */\n.tk-empty {\n flex: 1;\n display: grid;\n place-items: center;\n color: var(--fg-faint);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n/* === Density variants for $taskId standalone (Section B) ===== */\n.tk-detail-pane.is-compact .tk-det-head { padding: var(--space-3) var(--space-4); }\n.tk-detail-pane.is-compact .tk-det-title { font-size: var(--text-h4); }\n.tk-detail-pane.is-compact .tk-det-body { padding: var(--space-4); gap: var(--space-4); }\n.tk-detail-pane.is-compact .tk-det-grid { grid-template-columns: 90px 1fr; }\n.tk-detail-pane.is-side .tk-det-head { padding: var(--space-3); }\n.tk-detail-pane.is-side .tk-det-title { font-size: 13.5px; line-height: 1.35; }\n.tk-detail-pane.is-side .tk-det-body { padding: var(--space-3); gap: var(--space-3); }\n.tk-detail-pane.is-side .tk-det-grid { display: none; }\n.tk-detail-pane.is-side .tk-section-label { margin-bottom: 4px; }\n\n/* === In-body view switcher (D-2 / D-3) ====================== */\n.ip-tabs {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 var(--space-3);\n height: var(--tab-h, 38px);\n border-bottom: 1px solid var(--border-soft);\n background: var(--bg-sunken);\n}\n.ip-tab {\n height: var(--tab-h, 38px);\n padding: 0 var(--space-3);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-family: var(--font-body);\n font-size: var(--text-body-sm);\n color: var(--fg-faint);\n border: 0;\n background: transparent;\n cursor: pointer;\n border-bottom: 2px solid transparent;\n margin-bottom: -1px;\n}\n.ip-tab svg { width: 13px; height: 13px; opacity: 0.8; }\n.ip-tab:hover { color: var(--fg-muted); }\n.ip-tab.is-on { color: var(--fg); border-bottom-color: var(--tint-fg-active, var(--primary)); }\n.ip-tab.is-on svg { opacity: 1; color: var(--tint-fg-active, var(--primary)); }\n.ip-tab:focus-visible,\n.tk-toggle:focus-visible,\n.tk-group-head:focus-visible,\n.tk-row:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }\n.ip-tab-badge {\n font-family: var(--font-mono);\n font-size: 9.5px;\n line-height: 1;\n color: var(--achievement);\n background: var(--achievement-soft);\n border: 1px solid color-mix(in srgb, var(--achievement) 30%, var(--border-soft));\n border-radius: var(--radius-pill, 999px);\n padding: 2px 5px;\n}\n\n/* === Agenda / Today (D-2) =================================== */\n/* AgendaView root — rendered directly inside .tk-frame (which is overflow:hidden),\n * so it must be the scroll region itself: grow to fill the leftover height, allow\n * shrink (min-height:0), scroll its own overflow. Mirrors the Browse page's\n * `1fr` + overflow-y:auto leaf (shell .ccfg-list/.ccfg-detail). Without this the\n * agenda is clipped by the frame and can't scroll. */\n.ag-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-5); max-width: 920px; }\n.ag-head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-3);\n margin-bottom: var(--space-5);\n}\n.ag-date { font-family: var(--font-display); font-weight: 500; font-size: var(--text-h3); color: var(--fg); }\n.ag-summary { font-family: var(--font-mono); font-size: 10.5px; color: var(--fg-faint); letter-spacing: 0.04em; }\n.ag-summary b { color: var(--fg-muted); font-weight: 500; }\n.ag-filter-note { color: var(--achievement); font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.04em; }\n.ag-grid { display: grid; grid-template-columns: 54px 1fr; column-gap: var(--space-3); }\n.ag-time { font-family: var(--font-mono); font-size: 10px; color: var(--fg-faint); text-align: right; padding-top: 8px; letter-spacing: 0.04em; }\n.ag-lane { min-width: 0; padding-bottom: var(--space-3); }\n.ag-block {\n padding: 8px var(--space-3);\n border-radius: var(--radius-sm);\n border: 1px solid var(--border-soft);\n background: var(--bg-surface);\n border-left: 3px solid var(--fg-faint);\n margin-bottom: 6px;\n}\n.ag-block:last-child { margin-bottom: 0; }\n.ag-block.is-me { border-left-color: var(--primary); }\n.ag-block.is-agent { border-left-color: var(--agent); background: var(--agent-soft); }\n.ag-block.is-silent { border-left-color: var(--systemic); border-style: dashed; background: transparent; }\n.ag-block.is-deadline { border-left-color: var(--danger); background: color-mix(in srgb, var(--danger) 8%, transparent); }\n.ag-block.is-done { opacity: 0.6; }\n.ag-block .t { font-size: var(--text-body-sm); color: var(--fg); font-weight: 500; line-height: 1.4; }\n.ag-block.is-done .t { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--fg-muted); }\n.ag-block .m { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 5px; }\n.ag-now { grid-column: 1 / -1; position: relative; height: 0; border-top: 2px solid var(--live); margin: 4px 0 10px; }\n.ag-now::after {\n content: 'now';\n position: absolute;\n right: 0;\n top: -8px;\n font-family: var(--font-mono);\n font-size: 8.5px;\n letter-spacing: 0.06em;\n color: var(--live);\n background: var(--bg-base);\n padding: 0 5px;\n}\n.ag-now::before { content: ''; position: absolute; left: -5px; top: -3px; width: 6px; height: 6px; border-radius: 50%; background: var(--live); }\n\n/* === Triage / Health (D-3) ================================== */\n/* TriageView root — same as .ag-wrap: own scroll region inside the overflow-hidden\n * frame (grow to fill, min-height:0, scroll). */\n.tr-wrap { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-5); max-width: 980px; }\n.tr-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3); margin-bottom: var(--space-5); }\n.tr-stat { padding: var(--space-3) var(--space-4); border: 1px solid var(--border-soft); border-radius: var(--radius-md); background: var(--bg-surface); }\n/* Numbers stay --fg (12:1+ in every theme/mode) — the colored border carries\n the semantic. --achievement big-number measured 2.57:1 on a light surface. */\n.tr-stat .n { font-family: var(--font-display); font-weight: 500; font-size: 28px; line-height: 1; color: var(--fg); font-variant-numeric: tabular-nums; }\n.tr-stat .k { font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-faint); margin-top: 8px; }\n.tr-stat .sub { font-family: var(--font-mono); font-size: 9px; color: var(--fg-faint); letter-spacing: 0.02em; margin-top: 3px; }\n.tr-stat.is-danger { border-color: color-mix(in srgb, var(--danger) 35%, var(--border-soft)); }\n.tr-stat.is-warn { border-color: color-mix(in srgb, var(--achievement) 35%, var(--border-soft)); }\n.tr-stat.is-sys { border-color: color-mix(in srgb, var(--systemic) 35%, var(--border-soft)); }\n.tr-bucket { margin-bottom: var(--space-5); }\n.tr-bucket-head {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n color: var(--fg-faint);\n margin-bottom: var(--space-2);\n}\n.tr-bucket-head .ct { color: var(--fg-muted); font-variant-numeric: tabular-nums; }\n.tr-bucket-head::after { content: ''; flex: 1; height: 1px; background: var(--border-soft); }\n.tr-mini {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: var(--space-3);\n padding: 8px var(--space-3);\n border-bottom: 1px solid var(--border-soft);\n}\n.tr-mini:last-child { border-bottom: 0; }\n.tr-mini .title { font-size: var(--text-body-sm); color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }\n.tr-mini .age { font-family: var(--font-mono); font-size: 10px; color: var(--fg-muted); letter-spacing: 0.04em; white-space: nowrap; }\n.tr-mini .age.is-bad { color: var(--danger); }\n.tr-more { font-family: var(--font-mono); font-size: 10px; color: var(--fg-faint); letter-spacing: 0.04em; padding: 6px var(--space-3); }\n.tr-health {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n padding: var(--space-3) var(--space-4);\n border: 1px dashed var(--border);\n border-radius: var(--radius-md);\n background: var(--bg-sunken);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n line-height: 1.55;\n}\n.tr-health .score { font-family: var(--font-display); font-weight: 500; font-size: var(--text-h2); color: var(--achievement); line-height: 1; flex-shrink: 0; }\n\n/* === De-Tailwinded utilities ================================== */\n/* The source app leaned on a handful of Tailwind utility classes for layout\n * chrome around the locked .tk-* / .ag-* / .tr-* visuals. With no Tailwind in\n * the no-build pkg, those are reproduced here as a tiny, scoped utility set\n * (named after their Tailwind origin so the htm transcription reads 1:1).\n * Everything load-bearing is still a .tk-*/.ag-*/.tr-* rule above. */\n\n/* Root layout shell (was: `flex h-full flex-col p-5`) */\n.tk-screen {\n display: flex;\n height: 100%;\n flex-direction: column;\n padding: var(--space-5);\n}\n\n/* Loading / error / empty inline states (was Tailwind utility soup) */\n.tk-loading {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n padding: var(--space-4);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n.tk-error {\n margin: var(--space-4);\n display: flex;\n align-items: flex-start;\n gap: var(--space-2);\n border-radius: var(--radius-md);\n border: 1px solid color-mix(in srgb, var(--danger) 50%, transparent);\n background: color-mix(in srgb, var(--danger) 10%, transparent);\n padding: var(--space-3);\n font-size: var(--text-body-sm);\n color: var(--danger);\n}\n.tk-error svg { flex-shrink: 0; margin-top: 2px; }\n.tk-error .t { font-weight: 500; }\n.tk-error .d { font-size: var(--text-caption); opacity: 0.8; margin-top: 2px; }\n.tk-empty-box {\n margin: var(--space-4);\n display: flex;\n height: 128px;\n align-items: center;\n justify-content: center;\n border-radius: var(--radius-md);\n border: 1px dashed var(--border);\n font-size: var(--text-body-sm);\n color: var(--fg-muted);\n}\n\n/* Spinner (was: `animate-spin`) */\n.tk-spin { animation: tk-spin 1s linear infinite; }\n@keyframes tk-spin { to { transform: rotate(360deg); } }\n\n/* Detail-pane status select inline-styled in source; kept inline in the htm. */\n\n/* === Button (was: src/components/Button.tsx, Tailwind utilities) ============ */\n.tk-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n font-family: inherit;\n font-weight: 500;\n white-space: nowrap;\n cursor: pointer;\n transition: background var(--motion-fast, 120ms) var(--ease-calm, ease),\n color var(--motion-fast, 120ms) var(--ease-calm, ease),\n opacity var(--motion-fast, 120ms) var(--ease-calm, ease);\n}\n.tk-btn svg { flex-shrink: 0; }\n.tk-btn:disabled { opacity: 0.5; pointer-events: none; }\n/* sizes */\n.tk-btn.sz-md { height: 32px; padding: 0 12px; font-size: var(--text-body-sm); border-radius: var(--radius-md); }\n.tk-btn.sz-sm { height: 28px; padding: 0 10px; font-size: var(--text-caption); border-radius: var(--radius-md); gap: 4px; }\n/* variants */\n.tk-btn.v-default {\n background: var(--primary);\n color: var(--primary-fg);\n border: 1px solid transparent;\n}\n.tk-btn.v-default:hover:not(:disabled) { opacity: 0.9; }\n.tk-btn.v-outline {\n background: transparent;\n color: var(--fg);\n border: 1px solid var(--border);\n}\n.tk-btn.v-outline:hover:not(:disabled) { background: var(--bg-raised); }\n.tk-btn.v-ghost {\n background: transparent;\n color: var(--fg);\n border: 1px solid transparent;\n}\n.tk-btn.v-ghost:hover:not(:disabled) { background: var(--bg-raised); }\n.tk-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }\n\n/* Mutation error line in the detail pane footer. */\n.tk-mut-error {\n padding: 0 var(--space-5) var(--space-3);\n font-size: 11px;\n color: var(--danger);\n}\n";
@@ -0,0 +1,5 @@
1
+ // @ikenga/tokens — tokens.css, shipped as a JS string (no-build: subresource
2
+ // <link>/fetch fail in the shell's about:srcdoc iframe; CSS rides the script
3
+ // path and app.js injects it BEFORE tasks.css. Tokens resolve under
4
+ // :root[data-mode] / [data-theme] — app.js sets those attrs on <html>.
5
+ export default "/**\n * @ikenga/tokens \u2014 canonical design tokens for the Ikenga design system.\n *\n * Single source of truth for CSS custom properties used across:\n * - shell (Tauri desktop app)\n * - every UI pkg (Tasks, Studio, Outbound, \u2026)\n *\n * Consumers `@import '@ikenga/tokens/tokens.css'` and inherit the full token\n * surface. Pkgs running inside the shell iframe additionally receive a\n * curated subset via the AppBridge `host-context` handshake \u2014 those override\n * these defaults at runtime when the user flips theme/mode in the shell.\n *\n * Convention:\n * :root \u2192 mode-agnostic (typography, spacing, radii, shadows)\n * :root[data-mode=\"dark\"] \u2192 dark-mode color slots (DEFAULT)\n * :root[data-mode=\"light\"] \u2192 light-mode color slots (override)\n *\n * `data-theme` is reserved for theme variants beyond the default Ikenga palette\n * (e.g. seasonal, label-branded). Add `:root[data-theme=\"X\"][data-mode=\"dark\"]`\n * blocks below the defaults \u2014 last-match wins.\n */\n\n/* \u2500\u2500\u2500 mode-agnostic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:root {\n /* Spacing scale (4px base) */\n --space-1: 4px;\n --space-2: 8px;\n --space-3: 12px;\n --space-4: 16px;\n --space-5: 20px;\n --space-6: 24px;\n --space-8: 32px;\n\n /* Radii */\n --radius-xs: 3px;\n --radius-sm: 4px;\n --radius-md: 6px;\n --radius-lg: 10px;\n --radius-xl: 14px;\n --radius-pill: 999px;\n\n /* Typography */\n --font-sans: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;\n --font-display: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;\n --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;\n --text-caption: 11.5px;\n --text-body: 13px;\n --text-h3: 16px;\n --text-h2: 20px;\n --text-h1: 28px;\n\n /* Layout */\n --header-height: 44px;\n --sidebar-width: 240px;\n\n /* Default mode is dark \u2014 ensures correct rendering even without\n [data-mode] attribute set on <html>. */\n color-scheme: dark;\n\n /* Ikenga brand accents (mode-agnostic). Used by ADR-011 chat redesign:\n ember = active streaming / hot states; kola-amber = ritual pills,\n tool calls, artifact pills, cost amounts; oxblood = errors;\n verdigris = info / passive systemic. */\n --ember: hsl(14, 78%, 52%);\n --ember-soft: hsl(14, 70%, 60%);\n --kola-amber: hsl(42, 78%, 50%);\n --kola-amber-soft: hsl(42, 60%, 64%);\n --oxblood: hsl(8, 70%, 42%);\n --verdigris: hsl(170, 30%, 38%);\n}\n\n/* \u2500\u2500\u2500 dark mode (DEFAULT) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:root,\n:root[data-mode='dark'] {\n /* Backgrounds */\n --bg-base: hsl(220, 14%, 8%);\n --bg-surface: hsl(220, 13%, 11%);\n --bg-raised: hsl(220, 12%, 14%);\n --bg-sunken: hsl(220, 14%, 6%);\n\n /* Foregrounds */\n --fg: hsl(0, 0%, 96%);\n --fg-muted: hsl(220, 8%, 58%);\n --fg-subtle: hsl(220, 8%, 42%);\n\n /* Borders */\n --border: hsl(220, 13%, 20%);\n --border-soft: hsl(220, 13%, 16%);\n --border-strong: hsl(220, 13%, 28%);\n\n /* Brand + semantic */\n --primary: hsl(220, 90%, 60%);\n --primary-fg: hsl(0, 0%, 100%);\n --secondary: hsl(220, 12%, 14%);\n --accent: hsl(280, 70%, 65%);\n --info: hsl(200, 90%, 60%);\n --success: hsl(142, 70%, 50%);\n --warning: hsl(38, 92%, 60%);\n --danger: hsl(0, 72%, 60%);\n --agent: hsl(280, 70%, 65%);\n\n /* Tints (subtle backgrounds for active/hovered states) */\n --tint-bg-active: hsl(220, 90%, 60%, 0.08);\n --tint-fg-active: hsl(220, 90%, 70%);\n\n /* Shadows */\n --shadow-1: 0 1px 1px hsl(0 0% 0% / 0.3);\n --shadow-2: 0 1px 2px hsl(0 0% 0% / 0.4);\n --shadow-3: 0 4px 12px hsl(0 0% 0% / 0.5);\n\n /* MCP UI Apps schema bridge \u2014 these are the names the host pushes via\n AppBridge `styles.variables`. Map them to Ikenga semantic slots so a pkg\n that consumes the host context overlay (or one that doesn't) renders the\n same way. */\n --color-background-primary: var(--bg-base);\n --color-background-secondary: var(--bg-surface);\n --color-background-tertiary: var(--bg-raised);\n --color-background-inverse: hsl(0, 0%, 96%);\n --color-background-ghost: transparent;\n --color-background-info: var(--info);\n --color-background-danger: var(--danger);\n --color-background-success: var(--success);\n --color-background-warning: var(--warning);\n --color-background-disabled: hsl(220, 13%, 20%);\n\n --color-text-primary: var(--fg);\n --color-text-secondary: var(--fg-muted);\n --color-text-tertiary: var(--fg-subtle);\n --color-text-inverse: hsl(220, 14%, 8%);\n --color-text-ghost: var(--fg-muted);\n --color-text-info: var(--info);\n --color-text-danger: var(--danger);\n --color-text-success: var(--success);\n --color-text-warning: var(--warning);\n --color-text-disabled: var(--fg-subtle);\n\n --color-border-primary: var(--border);\n --color-border-secondary: var(--border-soft);\n --color-border-tertiary: var(--border-strong);\n --color-border-inverse: hsl(0, 0%, 96%);\n --color-border-ghost: transparent;\n --color-border-info: var(--info);\n --color-border-danger: var(--danger);\n --color-border-success: var(--success);\n --color-border-warning: var(--warning);\n --color-border-disabled: var(--border-soft);\n\n --color-ring-primary: var(--primary);\n --color-ring-secondary: var(--accent);\n --color-ring-inverse: hsl(0, 0%, 96%);\n --color-ring-info: var(--info);\n --color-ring-danger: var(--danger);\n --color-ring-success: var(--success);\n --color-ring-warning: var(--warning);\n\n /* Ikenga hairline + chip-carve (mode-specific). `--rule` is the canonical\n 1px divider between turns in the chat; `--rule-soft` is for grouping\n within a turn; `--chip-carve` is the dim ink used for the \u25bd\u25bd\u25bd motif\n and uppercase mono labels. */\n --rule: hsl(28, 14%, 18%);\n --rule-soft: hsl(28, 12%, 14%);\n --chip-carve: hsl(28, 14%, 32%);\n\n /* Code editor syntax palette (dark). Consumed by @ikenga/ui-lib's\n CodeEditor via CodeMirror 6 highlight tags. Tuned for HTML/TSX/CSS\n legibility on top of `--bg-surface`. */\n --syntax-keyword: hsl(280, 70%, 72%);\n --syntax-string: hsl(160, 55%, 60%);\n --syntax-comment: hsl(220, 10%, 50%);\n --syntax-number: hsl(28, 78%, 64%);\n --syntax-atom: hsl(28, 78%, 64%);\n --syntax-regexp: hsl(330, 60%, 64%);\n --syntax-operator: hsl(0, 0%, 86%);\n --syntax-punctuation: hsl(220, 8%, 62%);\n --syntax-variable: hsl(0, 0%, 92%);\n --syntax-function: hsl(200, 80%, 68%);\n --syntax-type: hsl(180, 60%, 64%);\n --syntax-tag: hsl(0, 70%, 66%);\n --syntax-attribute: hsl(42, 80%, 64%);\n --syntax-heading: hsl(0, 0%, 96%);\n --syntax-link: hsl(200, 90%, 62%);\n}\n\n/* \u2500\u2500\u2500 light mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:root[data-mode='light'] {\n color-scheme: light;\n\n --bg-base: hsl(0, 0%, 98%);\n --bg-surface: hsl(0, 0%, 100%);\n --bg-raised: hsl(220, 14%, 96%);\n --bg-sunken: hsl(220, 14%, 94%);\n\n --fg: hsl(220, 14%, 12%);\n --fg-muted: hsl(220, 10%, 45%);\n --fg-subtle: hsl(220, 8%, 60%);\n\n --border: hsl(220, 13%, 88%);\n --border-soft: hsl(220, 13%, 93%);\n --border-strong: hsl(220, 13%, 78%);\n\n --primary: hsl(220, 90%, 52%);\n --primary-fg: hsl(0, 0%, 100%);\n --secondary: hsl(220, 14%, 96%);\n --accent: hsl(280, 65%, 50%);\n --info: hsl(200, 90%, 45%);\n --success: hsl(142, 65%, 38%);\n --warning: hsl(38, 88%, 48%);\n --danger: hsl(0, 65%, 48%);\n --agent: hsl(280, 65%, 50%);\n\n --tint-bg-active: hsl(220, 90%, 52%, 0.10);\n --tint-fg-active: hsl(220, 90%, 42%);\n\n --shadow-1: 0 1px 1px hsl(220 14% 50% / 0.06);\n --shadow-2: 0 1px 2px hsl(220 14% 50% / 0.08);\n --shadow-3: 0 4px 12px hsl(220 14% 50% / 0.10);\n\n --color-background-primary: var(--bg-base);\n --color-background-secondary: var(--bg-surface);\n --color-background-tertiary: var(--bg-raised);\n --color-background-inverse: hsl(220, 14%, 12%);\n --color-text-primary: var(--fg);\n --color-text-secondary: var(--fg-muted);\n --color-text-tertiary: var(--fg-subtle);\n --color-text-inverse: hsl(0, 0%, 98%);\n --color-border-primary: var(--border);\n --color-border-secondary: var(--border-soft);\n --color-border-tertiary: var(--border-strong);\n\n /* Light-mode hairline + chip-carve. See dark-mode block for semantics. */\n --rule: hsl(36, 14%, 84%);\n --rule-soft: hsl(36, 14%, 90%);\n --chip-carve: hsl(28, 14%, 62%);\n\n /* Code editor syntax palette (light). See dark block for semantics. */\n --syntax-keyword: hsl(280, 70%, 38%);\n --syntax-string: hsl(160, 55%, 30%);\n --syntax-comment: hsl(220, 10%, 48%);\n --syntax-number: hsl(28, 78%, 38%);\n --syntax-atom: hsl(28, 78%, 38%);\n --syntax-regexp: hsl(330, 60%, 40%);\n --syntax-operator: hsl(220, 14%, 18%);\n --syntax-punctuation: hsl(220, 10%, 42%);\n --syntax-variable: hsl(220, 14%, 14%);\n --syntax-function: hsl(220, 90%, 40%);\n --syntax-type: hsl(180, 60%, 30%);\n --syntax-tag: hsl(0, 70%, 42%);\n --syntax-attribute: hsl(42, 90%, 36%);\n --syntax-heading: hsl(220, 14%, 12%);\n --syntax-link: hsl(200, 90%, 40%);\n}\n/* === Theme A \u00b7 Dusk Wood (canonical default) ==================== */\n[data-theme=\"A\"][data-mode=\"dark\"] {\n --bg-base: hsl(28,18%,4%); --bg-surface: hsl(28,14%,7%);\n --bg-raised: hsl(28,11%,11%); --bg-sunken: hsl(28,22%,2.5%);\n --fg: hsl(36,28%,90%); --fg-muted: hsl(32,11%,56%); --fg-faint: hsl(28,9%,36%);\n --border: hsl(28,14%,15%); --border-soft: hsl(28,14%,11%);\n --primary: hsl(20,50%,34%); --primary-fg: hsl(36,30%,92%); --primary-soft: hsl(20,40%,14%);\n --achievement: hsl(42,78%,54%); --achievement-soft: hsl(42,48%,18%);\n --danger: hsl(8,68%,46%); --danger-fg: hsl(0,0%,98%); --danger-soft: hsl(8,50%,16%);\n --systemic: hsl(170,28%,34%); --systemic-soft: hsl(170,22%,16%);\n --shadow-1: 0 1px 2px rgba(0,0,0,.55), 0 0 0 1px rgba(0,0,0,.25);\n --shadow-2: 0 4px 12px -2px rgba(0,0,0,.55);\n --shadow-3: 0 12px 28px -8px rgba(0,0,0,.6);\n --shadow-4: 0 24px 48px -16px rgba(0,0,0,.65);\n}\n[data-theme=\"A\"][data-mode=\"light\"] {\n --bg-base: hsl(36,22%,96%); --bg-surface: hsl(36,18%,93%);\n --bg-raised: hsl(36,14%,89%); --bg-sunken: hsl(36,26%,98%);\n --fg: hsl(28,30%,14%); --fg-muted: hsl(28,14%,38%); --fg-faint: hsl(28,12%,56%);\n --border: hsl(32,16%,78%); --border-soft: hsl(32,16%,84%);\n --primary: hsl(20,50%,34%); --primary-fg: hsl(36,26%,98%); --primary-soft: hsl(20,50%,90%);\n --achievement: hsl(42,78%,42%); --achievement-soft: hsl(42,60%,90%);\n --danger: hsl(8,68%,42%); --danger-fg: hsl(0,0%,98%); --danger-soft: hsl(8,60%,92%);\n --systemic: hsl(170,36%,32%); --systemic-soft: hsl(170,30%,90%);\n --shadow-1: 0 1px 2px rgba(28,18,8,.08);\n --shadow-2: 0 4px 12px -2px rgba(28,18,8,.10);\n --shadow-3: 0 12px 28px -8px rgba(28,18,8,.14);\n --shadow-4: 0 24px 48px -16px rgba(28,18,8,.18);\n}\n\n/* === Theme B \u00b7 Kola Daylight ==================================== */\n[data-theme=\"B\"][data-mode=\"light\"] {\n --bg-base: hsl(36,28%,96%); --bg-surface: hsl(36,22%,94%);\n --bg-raised: hsl(36,18%,90%); --bg-sunken: hsl(36,32%,98%);\n --fg: hsl(28,30%,12%); --fg-muted: hsl(28,14%,38%); --fg-faint: hsl(28,12%,56%);\n --border: hsl(32,18%,80%); --border-soft: hsl(32,18%,86%);\n --primary: hsl(42,82%,46%); --primary-fg: hsl(28,30%,8%); --primary-soft: hsl(42,60%,88%);\n --achievement: hsl(14,72%,46%); --achievement-soft: hsl(14,60%,90%);\n --danger: hsl(8,68%,42%); --danger-fg: hsl(0,0%,98%); --danger-soft: hsl(8,60%,92%);\n --systemic: hsl(170,36%,32%); --systemic-soft: hsl(170,30%,90%);\n --shadow-1: 0 1px 2px rgba(28,18,8,.08);\n --shadow-2: 0 4px 12px -2px rgba(28,18,8,.10);\n --shadow-3: 0 12px 28px -8px rgba(28,18,8,.14);\n --shadow-4: 0 24px 48px -16px rgba(28,18,8,.18);\n}\n[data-theme=\"B\"][data-mode=\"dark\"] {\n --bg-base: hsl(36,12%,8%); --bg-surface: hsl(36,10%,12%);\n --bg-raised: hsl(36,8%,16%); --bg-sunken: hsl(36,16%,6%);\n --fg: hsl(40,28%,92%); --fg-muted: hsl(36,10%,62%); --fg-faint: hsl(36,8%,42%);\n --border: hsl(36,12%,22%); --border-soft: hsl(36,12%,18%);\n --primary: hsl(42,84%,60%); --primary-fg: hsl(36,30%,8%); --primary-soft: hsl(42,60%,18%);\n --achievement: hsl(14,76%,56%); --achievement-soft: hsl(14,50%,18%);\n --danger: hsl(8,70%,52%); --danger-fg: hsl(0,0%,98%); --danger-soft: hsl(8,50%,18%);\n --systemic: hsl(170,32%,46%); --systemic-soft: hsl(170,25%,18%);\n --shadow-1: 0 1px 2px rgba(0,0,0,.55);\n --shadow-2: 0 4px 12px -2px rgba(0,0,0,.55);\n --shadow-3: 0 12px 28px -8px rgba(0,0,0,.6);\n --shadow-4: 0 24px 48px -16px rgba(0,0,0,.65);\n}\n\n/* === Theme C \u00b7 Bronze Shrine ==================================== */\n[data-theme=\"C\"][data-mode=\"dark\"] {\n --bg-base: hsl(180,14%,7%); --bg-surface: hsl(180,12%,11%);\n --bg-raised: hsl(180,10%,15%);--bg-sunken: hsl(180,18%,5%);\n --fg: hsl(40,18%,90%); --fg-muted: hsl(180,8%,60%); --fg-faint: hsl(180,8%,42%);\n --border: hsl(180,12%,22%); --border-soft: hsl(180,12%,18%);\n --primary: hsl(170,35%,50%); --primary-fg: hsl(180,18%,6%); --primary-soft: hsl(170,30%,18%);\n --achievement: hsl(40,58%,64%); --achievement-soft: hsl(40,30%,18%);\n --danger: hsl(8,70%,50%); --danger-fg: hsl(0,0%,98%); --danger-soft: hsl(8,50%,18%);\n --systemic: hsl(220,22%,56%); --systemic-soft: hsl(220,14%,18%);\n --shadow-1: 0 1px 2px rgba(0,0,0,.6);\n --shadow-2: 0 4px 12px -2px rgba(0,0,0,.6);\n --shadow-3: 0 12px 28px -8px rgba(0,0,0,.65);\n --shadow-4: 0 24px 48px -16px rgba(0,0,0,.7);\n}\n[data-theme=\"C\"][data-mode=\"light\"] {\n --bg-base: hsl(180,10%,95%); --bg-surface: hsl(180,8%,92%);\n --bg-raised: hsl(180,6%,88%); --bg-sunken: hsl(180,12%,97%);\n --fg: hsl(200,22%,14%); --fg-muted: hsl(200,10%,38%); --fg-faint: hsl(200,8%,58%);\n --border: hsl(180,12%,80%); --border-soft: hsl(180,12%,86%);\n --primary: hsl(170,42%,34%); --primary-fg: hsl(180,12%,97%); --primary-soft: hsl(170,30%,88%);\n --achievement: hsl(40,64%,38%); --achievement-soft: hsl(40,50%,90%);\n --danger: hsl(8,70%,42%); --danger-fg: hsl(0,0%,98%); --danger-soft: hsl(8,60%,92%);\n --systemic: hsl(220,24%,40%); --systemic-soft: hsl(220,22%,90%);\n --shadow-1: 0 1px 2px rgba(20,28,32,.08);\n --shadow-2: 0 4px 12px -2px rgba(20,28,32,.10);\n --shadow-3: 0 12px 28px -8px rgba(20,28,32,.14);\n --shadow-4: 0 24px 48px -16px rgba(20,28,32,.18);\n}\n\n/* === Workspace tints \u2014 constant hues, mode-adaptive ============= */\n[data-mode=\"dark\"] {\n --tint-app-bg: hsl(36,14%,11%); --tint-app-fg: hsl(36,28%,78%);\n --tint-mail-bg: hsl(42,30%,11%); --tint-mail-fg: hsl(42,70%,62%);\n --tint-outbox-bg: hsl(14,38%,11%); --tint-outbox-fg: hsl(14,72%,60%);\n --tint-studio-bg: hsl(8,40%,10%); --tint-studio-fg: hsl(8,72%,60%);\n --tint-agents-bg: hsl(170,26%,10%); --tint-agents-fg: hsl(170,40%,58%);\n --tint-files-bg: hsl(28,14%,11%); --tint-files-fg: hsl(28,30%,60%);\n --tint-sessions-bg: hsl(28,28%,11%); --tint-sessions-fg: hsl(28,60%,62%);\n --tint-settings-bg: hsl(220,14%,11%); --tint-settings-fg: hsl(220,26%,66%);\n}\n[data-mode=\"light\"] {\n --tint-app-bg: hsl(36,22%,92%); --tint-app-fg: hsl(28,30%,22%);\n --tint-mail-bg: hsl(42,60%,92%); --tint-mail-fg: hsl(42,80%,28%);\n --tint-outbox-bg: hsl(14,60%,93%); --tint-outbox-fg: hsl(14,70%,32%);\n --tint-studio-bg: hsl(8,60%,93%); --tint-studio-fg: hsl(8,70%,32%);\n --tint-agents-bg: hsl(170,36%,92%); --tint-agents-fg: hsl(170,50%,24%);\n --tint-files-bg: hsl(36,26%,92%); --tint-files-fg: hsl(28,30%,22%);\n --tint-sessions-bg: hsl(28,50%,92%); --tint-sessions-fg: hsl(14,60%,30%);\n --tint-settings-bg: hsl(220,22%,93%); --tint-settings-fg: hsl(220,30%,24%);\n}\n\n/* Active tint vars (resolved per workspace) */\n[data-workspace=\"app\"] { --tint-bg-active:var(--tint-app-bg); --tint-fg-active:var(--tint-app-fg); }\n[data-workspace=\"mail\"] { --tint-bg-active:var(--tint-mail-bg); --tint-fg-active:var(--tint-mail-fg); }\n[data-workspace=\"outbox\"] { --tint-bg-active:var(--tint-outbox-bg); --tint-fg-active:var(--tint-outbox-fg); }\n[data-workspace=\"studio\"] { --tint-bg-active:var(--tint-studio-bg); --tint-fg-active:var(--tint-studio-fg); }\n[data-workspace=\"agents\"] { --tint-bg-active:var(--tint-agents-bg); --tint-fg-active:var(--tint-agents-fg); }\n[data-workspace=\"files\"] { --tint-bg-active:var(--tint-files-bg); --tint-fg-active:var(--tint-files-fg); }\n[data-workspace=\"sessions\"] { --tint-bg-active:var(--tint-sessions-bg); --tint-fg-active:var(--tint-sessions-fg); }\n[data-workspace=\"settings\"] { --tint-bg-active:var(--tint-settings-bg); --tint-fg-active:var(--tint-settings-fg); }\n";
package/dist/lib/ui.js ADDED
@@ -0,0 +1,102 @@
1
+ // React + htm via esm.sh — no JSX transpile needed. Forkers edit JS, reload.
2
+ //
3
+ // htm uses tagged template literals to render React elements. Same mental model
4
+ // as JSX (component tags, expressions in ${}), just without a build step.
5
+
6
+ import * as React from 'https://esm.sh/react@19.0.0';
7
+ import * as ReactDOMClient from 'https://esm.sh/react-dom@19.0.0/client';
8
+ import htm from 'https://esm.sh/htm@3.1.1';
9
+ import {
10
+ QueryClient,
11
+ QueryClientProvider,
12
+ useQuery,
13
+ useMutation,
14
+ useQueryClient,
15
+ } from 'https://esm.sh/@tanstack/react-query@5?deps=react@19.0.0';
16
+
17
+ export const {
18
+ useState,
19
+ useEffect,
20
+ useMemo,
21
+ useCallback,
22
+ useRef,
23
+ useReducer,
24
+ Fragment,
25
+ } = React;
26
+ export const createRoot = ReactDOMClient.createRoot;
27
+ export const html = htm.bind(React.createElement);
28
+
29
+ // TanStack Query — caching layer carried over from the source app (triage
30
+ // counts query + list/detail caches). Pinned to the source's installed major
31
+ // (@tanstack/react-query ^5.100.6 → 5). `?deps=react@19` keeps a single React.
32
+ export {
33
+ QueryClient,
34
+ QueryClientProvider,
35
+ useQuery,
36
+ useMutation,
37
+ useQueryClient,
38
+ };
39
+
40
+ // Tiny icon helper — lucide-static SVG paths inlined as needed to avoid an
41
+ // extra CDN hop (the source app used lucide-react; we mirror suite's inline
42
+ // pattern instead). Add more glyphs as features need them.
43
+ const ICONS = {
44
+ // Source app glyphs (lucide path data):
45
+ 'check-square': 'M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11',
46
+ 'calendar-days':
47
+ 'M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01',
48
+ stethoscope:
49
+ 'M11 2v2M5 2v2M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1M8 15a6 6 0 0 0 12 0v-3M20 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4z',
50
+ search: 'M11 17.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13zM21 21l-4.35-4.35',
51
+ plus: 'M5 12h14M12 5v14',
52
+ 'chevron-down': 'M6 9l6 6 6-6',
53
+ 'alert-circle': 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 8v4M12 16h.01',
54
+ loader: 'M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83',
55
+ check: 'M20 6L9 17l-5-5',
56
+ 'check-circle': 'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4L12 14.01l-3-3',
57
+ mail: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6l-10 7L2 6',
58
+ terminal: 'M4 17l6-6-6-6M12 19h8',
59
+ 'git-branch': 'M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9',
60
+ };
61
+
62
+ // Class-name joiner — replaces clsx + tailwind-merge. With Tailwind gone there
63
+ // are no utility conflicts to dedupe, so a truthy-join is sufficient.
64
+ export function cn(...inputs) {
65
+ /** @type {string[]} */ const out = [];
66
+ for (const i of inputs) {
67
+ if (!i) continue;
68
+ if (typeof i === 'string') out.push(i);
69
+ else if (Array.isArray(i)) out.push(cn(...i));
70
+ else if (typeof i === 'object') {
71
+ for (const k of Object.keys(i)) if (i[k]) out.push(k);
72
+ }
73
+ }
74
+ return out.join(' ');
75
+ }
76
+
77
+ // Button — ported from src/components/Button.tsx. Tailwind utility classes
78
+ // became .tk-btn / sz-* / v-* rules in tasks.css.
79
+ export function Button({ variant = 'default', size = 'md', class: cls = '', children, ...props }) {
80
+ const className = ['tk-btn', `sz-${size}`, `v-${variant}`, cls]
81
+ .filter(Boolean)
82
+ .join(' ');
83
+ return html`<button class=${className} ...${props}>${children}</button>`;
84
+ }
85
+
86
+ export function Icon({ name, size = 16, className, strokeWidth = 2 }) {
87
+ const path = ICONS[name];
88
+ if (!path) return null;
89
+ // Multi-subpath glyphs are encoded as a single `d` string with multiple M
90
+ // segments — render as one <path>; works for every glyph above.
91
+ return html`<svg
92
+ class=${className}
93
+ width=${size}
94
+ height=${size}
95
+ viewBox="0 0 24 24"
96
+ fill="none"
97
+ stroke="currentColor"
98
+ stroke-width=${strokeWidth}
99
+ stroke-linecap="round"
100
+ stroke-linejoin="round"
101
+ ><path d=${path} /></svg>`;
102
+ }