@ikenga/pkg-sales 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,182 @@
1
+ // MCP Apps SDK bridge — the canonical iframe⇄host protocol Ikenga uses.
2
+ //
3
+ // Pattern from @modelcontextprotocol/ext-apps Quickstart + the shell's
4
+ // pkg-iframe-host.tsx implementation:
5
+ // 1. new App(...) — register handlers before connect
6
+ // 2. await app.connect() — runs ui/initialize handshake automatically
7
+ // 3. app.getHostContext() — read theme / styles / supabase / royaltiAuth
8
+ // 4. app.callServerTool({ name: 'host.<x>', arguments }) — invoke host tools
9
+ // (shell intercepts `host.*` names in dispatchHostCall; everything else
10
+ // proxies to pkg MCP servers if any).
11
+ //
12
+ // The host re-emits hostContext on theme change via onhostcontextchanged.
13
+
14
+ // Use the bundled `app-with-deps` build — the default entry pulls
15
+ // `zod/v4` as a peer-via-esm.sh and dependency resolution sometimes
16
+ // produces a Zod build missing `.custom()`. The bundled variant
17
+ // inlines its deps so it works regardless of esm.sh's resolver state.
18
+ // NOTE: we deliberately do NOT import the SDK's applyDocumentTheme /
19
+ // applyHostStyleVariables / applyHostFonts helpers. Theme is owned by app.js's
20
+ // parent-<html> mirror (the artifact pattern). applyDocumentTheme in particular
21
+ // clobbers our workspace `data-theme` (A/B/C) with 'light'|'dark', breaking the
22
+ // bundled @ikenga/tokens palette — so the bridge stays out of theming entirely.
23
+ import { App } from 'https://esm.sh/@modelcontextprotocol/ext-apps@1.7.1/app-with-deps';
24
+
25
+ let app = null;
26
+
27
+ export async function connectBridge({ name, version, onContextChange }) {
28
+ app = new App({ name, version }, {
29
+ // Capabilities the pkg advertises to the host. Keep minimal — declare only
30
+ // what we actually use.
31
+ tools: { listChanged: false },
32
+ });
33
+
34
+ app.onerror = (err) => console.error('[sales] bridge error', err);
35
+ // Theme is NOT applied here — app.js mirrors it from the parent <html>.
36
+ // We still forward context so live activeFeature (side-menu) updates reach
37
+ // the app; data flows through host.dbQuery/dbExec, not the context payload.
38
+ app.onhostcontextchanged = (ctx) => {
39
+ onContextChange?.(ctx);
40
+ };
41
+ app.onteardown = async () => ({});
42
+
43
+ await app.connect();
44
+ return app.getHostContext();
45
+ }
46
+
47
+ /** Navigate the focused shell pane (cross-pkg or in-pkg sub-route). */
48
+ export async function hostNavigate(path) {
49
+ if (!app) throw new Error('bridge not connected');
50
+ return app.callServerTool({
51
+ name: 'host.navigate',
52
+ arguments: { path },
53
+ });
54
+ }
55
+
56
+ /** Open an external link via the host. */
57
+ export async function openLink(url) {
58
+ if (!app) throw new Error('bridge not connected');
59
+ return app.openLink({ url });
60
+ }
61
+
62
+ /** Publish the pkg's sidebar menu to the shell. PkgMode renders these items in
63
+ * the left side panel when this pkg's pane is focused (normal AppMode). Item
64
+ * clicks come back as hostContext changes via `royaltiSuite.activeFeature` —
65
+ * listen via onContextChange in connectBridge.
66
+ * items: [{ id, label, icon?, badge? }] */
67
+ export async function setMenu(items) {
68
+ if (!app) throw new Error('bridge not connected');
69
+ return app.callServerTool({
70
+ name: 'host.pkg.setMenu',
71
+ arguments: { items },
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Seed a user turn into the shell's active Claude session. This is how the
77
+ * Tasks pkg "creates" work: anon RLS only grants UPDATE of status/completed_at
78
+ * (never INSERT), so a new task can't be written client-side. Instead we
79
+ * dispatch a natural-language request to the agent, which creates the task via
80
+ * its privileged path. Verb confirmed in shell/src/components/pkg/
81
+ * pkg-iframe-host.tsx (`host.sendToActiveSession`).
82
+ *
83
+ * prompt: string — the instruction shown as the user turn
84
+ * source?: string — provenance tag (defaults to the pkg id)
85
+ */
86
+ export async function hostSendToActiveSession(prompt, source = 'com.ikenga.tasks') {
87
+ if (!app) throw new Error('bridge not connected');
88
+ return app.callServerTool({
89
+ name: 'host.sendToActiveSession',
90
+ arguments: { prompt, source },
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Dispatch a structured PA action through the host. Kept as a thin alias for
96
+ * forward-compat: if/when the shell exposes a dedicated `host.paActionsRun`
97
+ * verb, point this at it. Today the shell does NOT expose that verb (only
98
+ * host.navigate / host.sendToActiveSession / host.openSessionDialog /
99
+ * host.pkg.setMenu exist), so the create path uses hostSendToActiveSession.
100
+ */
101
+ export async function hostPaActionsRun(args) {
102
+ if (!app) throw new Error('bridge not connected');
103
+ return app.callServerTool({
104
+ name: 'host.paActionsRun',
105
+ arguments: args,
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Read the local `ikenga.db` via the host's `host.dbQuery` verb (WP-04 read-swap).
111
+ * SELECT/WITH only — the shell rejects writes and gates this on the pkg
112
+ * declaring `capabilities.sqlite`. Returns the row array
113
+ * (`structuredContent.rows`); throws on a closed/failed bridge so callers can
114
+ * surface the error in the query layer. Requires a connected bridge — there is
115
+ * no standalone fallback (reads no longer go through supabase-js).
116
+ *
117
+ * sql: string — a single SELECT/WITH statement with `?` params
118
+ * params: SqlValue[] — positional bind values
119
+ */
120
+ export async function hostDbQuery(sql, params = []) {
121
+ if (!app) throw new Error('[sales] bridge not connected — db_query unavailable');
122
+ const res = await app.callServerTool({
123
+ name: 'host.dbQuery',
124
+ arguments: { sql, params },
125
+ });
126
+ const sc = res?.structuredContent;
127
+ if (!sc || sc.ok !== true) {
128
+ throw new Error(sc?.error ?? res?.content?.[0]?.text ?? 'host.dbQuery failed');
129
+ }
130
+ return Array.isArray(sc.rows) ? sc.rows : [];
131
+ }
132
+
133
+ /**
134
+ * Write to the local `ikenga.db` via the host's `host.dbExec` verb (write-path WP).
135
+ * INSERT/UPDATE/DELETE only — the shell rejects reads/DDL, gates on the pkg
136
+ * declaring `capabilities.sqlite`, and scopes the target table to the pkg's
137
+ * declared `permissions['sqlite.tables']`. Resolves on success; throws on a
138
+ * closed/failed bridge so callers can surface the error in the mutation layer.
139
+ *
140
+ * sql: string — a single INSERT/UPDATE/DELETE statement with `?` params
141
+ * params: SqlValue[] — positional bind values
142
+ */
143
+ export async function hostDbExec(sql, params = []) {
144
+ if (!app) throw new Error('[sales] bridge not connected — db_exec unavailable');
145
+ const res = await app.callServerTool({
146
+ name: 'host.dbExec',
147
+ arguments: { sql, params },
148
+ });
149
+ const sc = res?.structuredContent;
150
+ if (!sc || sc.ok !== true) {
151
+ throw new Error(sc?.error ?? res?.content?.[0]?.text ?? 'host.dbExec failed');
152
+ }
153
+ }
154
+
155
+ /** Read the current hostContext snapshot. */
156
+ export function getContext() {
157
+ return app?.getHostContext() ?? null;
158
+ }
159
+
160
+ /** Detect standalone-dev (no parent shell). */
161
+ export function isStandalone() {
162
+ return typeof window !== 'undefined' && window.parent === window;
163
+ }
164
+
165
+ /**
166
+ * Publish a key/value into the shell's iyke iframe-state registry, so
167
+ * external agents can read "what's open in this pane" via
168
+ * `iyke iframe-state` / `iyke state` instead of guessing from the DB.
169
+ *
170
+ * This is NOT the AppBridge wire: the shell's window-level iyke listener
171
+ * (iframe-registry.ts) matches `{__iyke:true, kind:'state'}` postMessages by
172
+ * source window for any registered iframe — pkg iframes are registered by the
173
+ * host (pkg-iframe-host.tsx Step 1c). Fire-and-forget; no-ops standalone.
174
+ */
175
+ export function publishIykeState(key, value) {
176
+ if (isStandalone()) return;
177
+ try {
178
+ window.parent.postMessage({ __iyke: true, kind: 'state', payload: { key, value } }, '*');
179
+ } catch {
180
+ /* never let debug-surface publishing break the app */
181
+ }
182
+ }
@@ -0,0 +1,4 @@
1
+ // GENERATED by scripts/build.mjs from dist/sales.css — do not edit.
2
+ // CSS-as-string: WebKitGTK cannot load link/fetch subresources from the
3
+ // about:srcdoc the shell mounts, so app.js injects this as an inline <style>.
4
+ export default "/* sales.css — domain residue for com.ikenga.sales.\n *\n * Only sales-local classes live here:\n * - .sl-* Forecast view layout (.sl-forecast-kpis, .sl-forecast-kpi, etc.)\n * - .sl-won-* Won view layout (.sl-won-wrap, .sl-won-table, .sl-won-badge)\n * - Kanban + pipeline overrides specific to this domain (.kb-mini-avatar.is-agent,\n * .split-row-when.is-urgent already in kit — no re-derive needed)\n *\n * Kit primitives (.frame*, .ip-split*, .dense-row*, .btn*, .tag, .chip, .badge,\n * .atelier-state*, .nav-group*, .nav-item*, .kb-*, .seg*, .split-*, etc.) are\n * consumed from app-kit-css.js — never re-derived here.\n *\n * Inject order: tokens-css → app-kit-css → sales-css (this file).\n * data-workspace=\"sessions\" on <html> → warm amber-ochre tint for active nav + accents.\n */\n\n/* ── Reduced motion gate ── */\n@media (prefers-reduced-motion: reduce) {\n .split-row { transition: none; }\n .kb-card { transition: none; }\n .kb-card:hover { transform: none; }\n}\n\n/* ── Forecast view (.sl-*) ─────────────────────────────────────────────────── */\n\n.sl-forecast-wrap {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 16px;\n overflow-y: auto;\n height: 100%;\n}\n\n.sl-forecast-kpis {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 10px;\n}\n\n.sl-forecast-kpi {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.sl-kpi-k {\n font-family: var(--font-mono, monospace);\n font-size: 10px;\n font-weight: 500;\n color: var(--fg-faint, var(--fg-muted));\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n\n.sl-kpi-v {\n font-family: var(--font-display, Georgia, serif);\n font-size: 26px;\n font-weight: 500;\n font-style: normal;\n color: var(--fg);\n margin-top: 4px;\n}\n\n.sl-kpi-sub {\n font-size: 0.7rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n}\n\n.sl-forecast-card {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n overflow: hidden;\n}\n\n.sl-forecast-card-h {\n padding: 10px 14px;\n font-size: 0.8rem;\n font-weight: 600;\n color: var(--fg-muted);\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.06));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.sl-funnel-row {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 14px;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.04));\n}\n\n.sl-funnel-row:last-child {\n border-bottom: none;\n}\n\n.sl-funnel-name {\n width: 100px;\n font-size: 0.78rem;\n color: var(--fg);\n flex-shrink: 0;\n}\n\n.sl-funnel-bar {\n flex: 1;\n height: 22px;\n border-radius: var(--radius-xs, 3px);\n background: var(--bg-sunken, rgba(0,0,0,0.06));\n position: relative;\n overflow: hidden;\n}\n\n.sl-funnel-bar-fill {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--bg-raised, rgba(128,128,128,0.3));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: var(--radius-xs, 3px);\n}\n\n.sl-funnel-bar-wt {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--primary, hsl(215,70%,55%));\n border-radius: var(--radius-xs, 3px);\n opacity: 0.9;\n}\n\n.sl-funnel-val {\n width: 72px;\n font-size: 0.72rem;\n font-family: var(--font-mono, monospace);\n color: var(--fg-muted);\n text-align: right;\n flex-shrink: 0;\n}\n\n.sl-months {\n display: flex;\n align-items: flex-end;\n gap: 16px;\n padding: 14px;\n height: 170px;\n}\n\n.sl-month {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n flex: 1;\n}\n\n.sl-month-bar-wrap {\n flex: 1;\n display: flex;\n align-items: flex-end;\n width: 100%;\n}\n\n.sl-month-bar {\n width: 100%;\n background: var(--primary, hsl(215,70%,55%));\n border-radius: 3px 3px 0 0;\n opacity: 0.7;\n min-height: 4px;\n}\n\n.sl-month-val {\n font-size: 0.65rem;\n font-family: var(--font-mono, monospace);\n color: var(--fg-muted);\n}\n\n.sl-month-lab {\n font-size: 0.65rem;\n color: var(--fg-faint, var(--fg-muted));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n/* ── Won view (.sl-won-*) ───────────────────────────────────────────────────── */\n\n.sl-won-wrap {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 16px;\n overflow-y: auto;\n height: 100%;\n}\n\n.sl-won-kpis {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 10px;\n}\n\n.sl-won-table-wrap {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n overflow: hidden;\n}\n\n.sl-won-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.8rem;\n}\n\n.sl-won-table thead tr {\n background: var(--bg-inset, rgba(0,0,0,0.04));\n}\n\n.sl-won-table th {\n padding: 8px 12px;\n text-align: left;\n font-size: 0.7rem;\n font-weight: 600;\n color: var(--fg-muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n}\n\n.sl-won-table td {\n padding: 9px 12px;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.04));\n color: var(--fg);\n vertical-align: middle;\n}\n\n.sl-won-table tbody tr:last-child td {\n border-bottom: none;\n}\n\n.sl-won-table tbody tr:hover td {\n background: var(--bg-hover, rgba(0,0,0,0.025));\n}\n\n.sl-won-badge {\n display: inline-flex;\n align-items: center;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.65rem;\n font-weight: 500;\n background: var(--bg-inset, rgba(0,0,0,0.08));\n color: var(--fg-muted);\n text-transform: capitalize;\n letter-spacing: 0.02em;\n}\n\n.sl-won-amt {\n font-family: var(--font-mono, monospace);\n font-size: 0.8rem;\n font-weight: 600;\n color: var(--live, hsl(150,48%,52%));\n}\n\n/* ── Kanban domain overrides ────────────────────────────────────────────────── */\n\n/* Stage dot colours per the screen doc */\n.kb-col[data-stage=\"lead\"] .kb-col-dot { background: var(--fg-faint, rgba(128,128,128,0.4)); }\n.kb-col[data-stage=\"qualified\"] .kb-col-dot { background: var(--systemic, hsl(140,55%,42%)); }\n.kb-col[data-stage=\"proposal\"] .kb-col-dot { background: var(--primary, hsl(215,70%,55%)); }\n.kb-col[data-stage=\"negotiation\"] .kb-col-dot { background: var(--achievement, hsl(38,85%,52%)); }\n.kb-col[data-stage=\"closing\"] .kb-col-dot { background: var(--live, hsl(348,72%,52%)); }\n\n/* Deal row ux-mode dots */\n.ux-dot {\n display: inline-block;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-right: 4px;\n}\n.ux-dot.ux-confirm { background: var(--achievement, hsl(38,85%,52%)); }\n.ux-dot.ux-silent { background: var(--fg-faint, rgba(128,128,128,0.4)); }\n.ux-dot.ux-approve { background: var(--primary, hsl(215,70%,55%)); }\n\n/* Next-action chip in list rows */\n.next-chip {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 0.68rem;\n color: var(--fg-muted);\n background: var(--bg-inset, rgba(0,0,0,0.06));\n border-radius: 4px;\n padding: 2px 6px;\n max-width: 180px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Stage chip in detail pane */\n.stage-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.7rem;\n font-weight: 600;\n padding: 3px 8px;\n border-radius: 5px;\n background: var(--bg-inset, rgba(0,0,0,0.07));\n color: var(--fg-muted);\n text-transform: capitalize;\n letter-spacing: 0.03em;\n}\n\n/* Hot filter (Closing soon — achievement tint) */\n.nav-item.is-hot .nav-item-count {\n color: var(--achievement, hsl(38,85%,52%));\n font-weight: 700;\n}\n\n/* Deal amount in list rows */\n.split-row-amt {\n font-family: var(--font-mono, monospace);\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--fg);\n white-space: nowrap;\n}\n\n/* Age / urgency */\n.split-row-when {\n font-size: 0.68rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n white-space: nowrap;\n}\n\n.split-row-when.is-urgent {\n color: var(--danger, hsl(0,65%,52%));\n}\n\n/* F-03 · Selected-row accent rail → explicit warm-primary (design uses\n var(--primary), not the sessions amber-ochre --tint-fg-active). */\n.split-row.is-selected .split-row-accent,\n.split-row.is-on .split-row-accent {\n background: var(--primary, hsl(20,50%,34%));\n border-radius: 2px;\n}\n\n/* F-02 · Pipeline list-pane head — sticky Fraunces \"Sales\" title + mono\n count above the stage groups. Ported from the design's .pl-list-head /\n .pl-list-title / .pl-list-meta (not an app-kit primitive). */\n.sl-list-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--space-3, 12px) var(--space-4, 16px);\n border-bottom: 1px solid var(--border-soft);\n position: sticky;\n top: 0;\n background: var(--bg-surface);\n z-index: 6;\n}\n\n.sl-list-title {\n font-family: var(--font-display, Georgia, serif);\n font-weight: 500;\n font-size: var(--text-h3, 18px);\n color: var(--fg);\n}\n\n.sl-list-meta {\n font-family: var(--font-mono, monospace);\n font-size: 10.5px;\n color: var(--fg-faint, var(--fg-muted));\n letter-spacing: 0.04em;\n}\n\n/* F-05 · Detail-pane title → Fraunces 28px (design spec). The app-kit\n primitive sets Fraunces at --text-h2 (24px); sales bumps it to 28px to\n match the locked sales design's .pl-detail-title. */\n.split-detail-title {\n font-family: var(--font-display, Georgia, serif);\n font-size: 28px;\n font-weight: 500;\n line-height: 1.15;\n letter-spacing: -0.012em;\n color: var(--fg);\n margin: 0 0 var(--space-2, 8px);\n}\n\n/* F-06 / F-07 / F-08 · The facts grid (3-col card), next-action card\n (live-tinted border + bg), and next-action head (UPPERCASE mono 10px)\n are all consumed verbatim from the app-kit primitives (.split-facts,\n .split-next, .split-next-head) — no sales-local override. The previous\n 2-col plain-grid / un-tinted overrides were drift and have been removed\n so the kit's design-correct rules apply. */\n\n/* Activity timeline in detail pane */\n.split-timeline {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 12px;\n}\n\n.split-tl-row {\n display: flex;\n gap: 10px;\n align-items: flex-start;\n}\n\n.split-tl-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--fg-faint, rgba(128,128,128,0.4));\n flex-shrink: 0;\n margin-top: 5px;\n}\n\n.split-tl-body {\n flex: 1;\n}\n\n.split-tl-title {\n font-size: 0.78rem;\n color: var(--fg);\n}\n\n.split-tl-when {\n font-size: 0.65rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n}\n\n/* Seg toggle (list/kanban) injected into sidebar nav-group */\n.nav-view-seg {\n margin: 4px 8px 6px;\n}\n\n/* Kanban board horizontal scroll mask */\n.kb-board-wrap {\n overflow-x: auto;\n height: 100%;\n -webkit-mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);\n mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);\n}\n"
@@ -0,0 +1,3 @@
1
+ // GENERATED by scripts/build-css.mjs from tokens.css. Do not edit.
2
+ // No-build srcdoc pkgs import this and inject it as an inline <style>.
3
+ export default "/**\n * @ikenga/tokens — 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, …)\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 — those override\n * these defaults at runtime when the user flips theme/mode in the shell.\n *\n * Convention:\n * :root → mode-agnostic (typography, spacing, radii, shadows)\n * :root[data-mode=\"dark\"] → dark-mode color slots (DEFAULT)\n * :root[data-mode=\"light\"] → 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 — last-match wins.\n */\n\n/* ─── mode-agnostic ───────────────────────────────────────────────────────── */\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 --space-10: 40px;\n --space-12: 48px;\n --space-16: 64px;\n --space-20: 80px;\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. --font-sans kept for back-compat; --font-body/-display/-mono are\n the Atelier faces the shell already loads (Inter / Fraunces / JetBrains Mono).\n Type *sizes* stay on the shipped package scale — the shell renders on these;\n the app-kit + panes inherit them via var(). */\n --font-sans: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;\n --font-body: 'Inter', system-ui, -apple-system, sans-serif;\n --font-display: 'Fraunces', ui-serif, Georgia, serif;\n --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace;\n --text-micro: 11px;\n --text-caption: 11.5px;\n --text-body-sm: 13px;\n --text-body: 13px;\n --text-body-lg: 16px;\n --text-code: 13px;\n --text-h4: 14px;\n --text-h3: 16px;\n --text-h2: 20px;\n --text-h1: 28px;\n --text-display: 40px;\n --text-display-xl: 56px;\n /* line-heights (leading), paired with the sizes above */\n --lead-micro: 1.4;\n --lead-caption: 1.45;\n --lead-body-sm: 1.5;\n --lead-body: 1.55;\n --lead-body-lg: 1.6;\n --lead-code: 1.55;\n --lead-h3: 1.35;\n --lead-h2: 1.2;\n --lead-h1: 1.15;\n --lead-display: 1.08;\n --lead-display-xl: 1.02;\n\n /* Layout */\n --header-height: 44px;\n --sidebar-width: 240px;\n\n /* Motion — added for the app-kit (the tasks pkg shim previously hardcoded\n --motion-fast / --ease-calm). */\n --motion-fast: 120ms;\n --motion-base: 180ms;\n --motion-slow: 260ms;\n --motion-slower: 360ms;\n --ease-calm: cubic-bezier(0.2, 0.6, 0.2, 1);\n --ease-decisive: cubic-bezier(0.4, 0, 0.2, 1);\n --ease-soft-bounce: cubic-bezier(0.34, 1.32, 0.64, 1);\n\n /* Density (default: comfortable). Chrome heights derive from this; the\n [data-density] blocks below override for compact / spacious. */\n --row-h: 36px;\n --row-pad-y: 8px;\n --gap: var(--space-2);\n --body-size: var(--text-body);\n --body-lead: var(--lead-body);\n --tab-h: 38px;\n --btn-h: 32px;\n --btn-h-sm: 26px;\n --btn-h-lg: 40px;\n --input-h: 32px;\n\n /* Default mode is dark — 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/* ─── density variants (override the comfortable defaults in :root) ───────── */\n[data-density='compact'] {\n --row-h: 28px; --row-pad-y: 5px; --gap: var(--space-1);\n --body-size: var(--text-body-sm); --body-lead: 1.4;\n --tab-h: 32px; --btn-h: 28px; --btn-h-sm: 22px; --btn-h-lg: 34px; --input-h: 28px;\n}\n[data-density='spacious'] {\n --row-h: 44px; --row-pad-y: 11px; --gap: var(--space-3);\n --body-size: var(--text-body-lg); --body-lead: 1.6;\n --tab-h: 44px; --btn-h: 36px; --btn-h-sm: 30px; --btn-h-lg: 44px; --input-h: 36px;\n}\n\n/* ─── dark mode (DEFAULT) ─────────────────────────────────────────────────── */\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 --fg-faint: var(--fg-subtle);\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 --agent-soft: color-mix(in srgb, var(--agent) 14%, transparent);\n --live: var(--success);\n --live-soft: color-mix(in srgb, var(--success) 14%, transparent);\n --live-fg: hsl(150, 45%, 10%);\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 — 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 ▽▽▽ 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/* ─── light mode ──────────────────────────────────────────────────────────── */\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 --fg-faint: var(--fg-subtle);\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 --agent-soft: color-mix(in srgb, var(--agent) 14%, transparent);\n --live: var(--success);\n --live-soft: color-mix(in srgb, var(--success) 14%, transparent);\n --live-fg: hsl(150, 24%, 97%);\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 · 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 --agent: hsl(265,52%,64%); --agent-soft: hsl(265,32%,20%);\n --live: hsl(150,48%,52%); --live-soft: hsl(150,30%,18%); --live-fg: hsl(150,50%,9%);\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 --agent: hsl(265,55%,44%); --agent-soft: hsl(265,40%,92%);\n --live: hsl(150,55%,32%); --live-soft: hsl(150,40%,92%); --live-fg: hsl(0,0%,100%);\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 · 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 · 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 — 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,107 @@
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
+ // ViewTabs glyphs for Sweeper + Done. `broom` mirrors the design's SWEEP
61
+ // glyph (a broom strokes + handle); `check-check` is the lucide name for
62
+ // the "double check" used for completed items.
63
+ broom: 'M9.8 12.2 5 17l3 3 4.8-4.8M14 8l-3.5 3.5 4 4L18 12l-4-4z M14 8l5-5M19 3l2 2',
64
+ 'check-check': 'M18 6 7 17l-5-5M22 10l-7.5 7.5L13 16',
65
+ };
66
+
67
+ // Class-name joiner — replaces clsx + tailwind-merge. With Tailwind gone there
68
+ // are no utility conflicts to dedupe, so a truthy-join is sufficient.
69
+ export function cn(...inputs) {
70
+ /** @type {string[]} */ const out = [];
71
+ for (const i of inputs) {
72
+ if (!i) continue;
73
+ if (typeof i === 'string') out.push(i);
74
+ else if (Array.isArray(i)) out.push(cn(...i));
75
+ else if (typeof i === 'object') {
76
+ for (const k of Object.keys(i)) if (i[k]) out.push(k);
77
+ }
78
+ }
79
+ return out.join(' ');
80
+ }
81
+
82
+ // Button — ported from src/components/Button.tsx. Tailwind utility classes
83
+ // became .tk-btn / sz-* / v-* rules in tasks.css.
84
+ export function Button({ variant = 'default', size = 'md', class: cls = '', children, ...props }) {
85
+ const className = ['tk-btn', `sz-${size}`, `v-${variant}`, cls]
86
+ .filter(Boolean)
87
+ .join(' ');
88
+ return html`<button class=${className} ...${props}>${children}</button>`;
89
+ }
90
+
91
+ export function Icon({ name, size = 16, className, strokeWidth = 2 }) {
92
+ const path = ICONS[name];
93
+ if (!path) return null;
94
+ // Multi-subpath glyphs are encoded as a single `d` string with multiple M
95
+ // segments — render as one <path>; works for every glyph above.
96
+ return html`<svg
97
+ class=${className}
98
+ width=${size}
99
+ height=${size}
100
+ viewBox="0 0 24 24"
101
+ fill="none"
102
+ stroke="currentColor"
103
+ stroke-width=${strokeWidth}
104
+ stroke-linecap="round"
105
+ stroke-linejoin="round"
106
+ ><path d=${path} /></svg>`;
107
+ }