@heyramzi/ui 2.0.0 → 2.1.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.
- package/package.json +59 -34
- package/scripts/sync-shadcn.mjs +2 -0
- package/src/canvas/README.md +9 -12
- package/src/charts/dashboard/data-panel.svelte +1 -1
- package/src/components/list-page/detail-panel.svelte +1 -1
- package/src/components/list-page/filter-popover.svelte +1 -1
- package/src/components/list-page/index.ts +3 -3
- package/src/components/list-page/rail-head.svelte +1 -1
- package/src/components/section-header/index.ts +1 -0
- package/src/components/section-header/section-header.svelte +1 -1
- package/src/components/toolbar/index.ts +2 -0
- package/src/components/toolbar/toolbar.svelte +2 -2
- package/src/editor/editor.svelte +8 -1
- package/src/gallery.test.ts +20 -1
- package/src/inbox/README.md +29 -0
- package/src/inbox/inbox.test.ts +171 -0
- package/src/inbox/index.ts +31 -0
- package/src/inbox/logic.ts +166 -0
- package/src/inbox/thread-inbox.svelte +363 -0
- package/src/inbox/types.ts +139 -0
- package/src/modules.test.ts +95 -0
- package/src/panel/README.md +5 -0
- package/src/panel/index.ts +2 -0
- package/src/{tasks → panel}/side-panel.svelte +1 -1
- package/src/record/README.md +3 -0
- package/src/{tasks/record → record}/field-section.svelte +1 -1
- package/src/{tasks/record → record}/info-list.svelte +9 -9
- package/src/{tasks/record → record}/next-up.svelte +4 -4
- package/src/{tasks/record → record}/project-list.svelte +5 -5
- package/src/{tasks/record → record}/record-demo.svelte +4 -4
- package/src/{tasks/record → record}/record-header.svelte +3 -3
- package/src/{tasks/record → record}/record-shell.svelte +4 -4
- package/src/{tasks/record → record}/record-stats.svelte +2 -2
- package/src/{tasks/record → record}/record-timeline.svelte +6 -6
- package/src/{tasks/record → record}/steps-rail.svelte +7 -7
- package/src/{tasks/record → record}/types.ts +2 -2
- package/src/{tasks/record → record}/where-we-are.svelte +4 -4
- package/src/status/README.md +18 -0
- package/src/status/index.ts +11 -0
- package/src/tasks/README.md +1 -1
- package/src/tasks/demo.svelte +3 -3
- package/src/tasks/detail/detail-demo.svelte +2 -2
- package/src/tasks/gantt/gantt.test.ts +1 -1
- package/src/tasks/inbox/inbox-card.svelte +2 -2
- package/src/tasks/inbox/inbox-statuses.ts +1 -1
- package/src/tasks/inbox/inbox.svelte +1 -1
- package/src/tasks/index.ts +9 -7
- package/src/tasks/presets/sales.ts +1 -1
- package/src/thread/README.md +31 -0
- package/src/thread/index.ts +12 -0
- package/src/thread/logic.ts +58 -0
- package/src/thread/thread.svelte +366 -0
- package/src/thread/thread.test.ts +62 -0
- package/src/thread/types.ts +93 -0
- /package/src/components/{list-page → section-header}/section-label.svelte +0 -0
- /package/src/components/{list-page → toolbar}/toolbar-search.svelte +0 -0
- /package/src/components/{list-page → toolbar}/toolbar-trigger.ts +0 -0
- /package/src/{tasks → panel}/side-panel-types.ts +0 -0
- /package/src/{tasks/record → record}/demo-data.ts +0 -0
- /package/src/{tasks/record → record}/fields.ts +0 -0
- /package/src/{tasks/record → record}/format.ts +0 -0
- /package/src/{tasks/record → record}/gutter.ts +0 -0
- /package/src/{tasks/record → record}/index.ts +0 -0
- /package/src/{tasks/record → record}/kinds.ts +0 -0
- /package/src/{tasks/record → record}/logic.ts +0 -0
- /package/src/{tasks/record → record}/record.test.ts +0 -0
- /package/src/{tasks → status}/define-statuses.ts +0 -0
- /package/src/{tasks → status}/status-badge.svelte +0 -0
- /package/src/{tasks → status}/status-dot.svelte +0 -0
- /package/src/{tasks → status}/status-icon.svelte +0 -0
- /package/src/{tasks → status}/status-types.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyramzi/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Svelte 5 UI for apps: primitives, effects and animations, plus tasks, charts, slides, an editor, onboarding and a canvas as subpaths.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -172,6 +172,31 @@
|
|
|
172
172
|
"default": "./src/agents/index.ts"
|
|
173
173
|
},
|
|
174
174
|
"./agents/demo.svelte": "./src/agents/demo.svelte",
|
|
175
|
+
"./status": {
|
|
176
|
+
"svelte": "./src/status/index.ts",
|
|
177
|
+
"types": "./src/status/index.ts",
|
|
178
|
+
"default": "./src/status/index.ts"
|
|
179
|
+
},
|
|
180
|
+
"./panel": {
|
|
181
|
+
"svelte": "./src/panel/index.ts",
|
|
182
|
+
"types": "./src/panel/index.ts",
|
|
183
|
+
"default": "./src/panel/index.ts"
|
|
184
|
+
},
|
|
185
|
+
"./record": {
|
|
186
|
+
"svelte": "./src/record/index.ts",
|
|
187
|
+
"types": "./src/record/index.ts",
|
|
188
|
+
"default": "./src/record/index.ts"
|
|
189
|
+
},
|
|
190
|
+
"./thread": {
|
|
191
|
+
"svelte": "./src/thread/index.ts",
|
|
192
|
+
"types": "./src/thread/index.ts",
|
|
193
|
+
"default": "./src/thread/index.ts"
|
|
194
|
+
},
|
|
195
|
+
"./inbox": {
|
|
196
|
+
"svelte": "./src/inbox/index.ts",
|
|
197
|
+
"types": "./src/inbox/index.ts",
|
|
198
|
+
"default": "./src/inbox/index.ts"
|
|
199
|
+
},
|
|
175
200
|
"./tasks": {
|
|
176
201
|
"svelte": "./src/tasks/index.ts",
|
|
177
202
|
"types": "./src/tasks/index.ts",
|
|
@@ -193,19 +218,19 @@
|
|
|
193
218
|
"default": "./src/tasks/task-gantt.svelte"
|
|
194
219
|
},
|
|
195
220
|
"./tasks/status": {
|
|
196
|
-
"svelte": "./src/
|
|
197
|
-
"types": "./src/
|
|
198
|
-
"default": "./src/
|
|
221
|
+
"svelte": "./src/status/status-badge.svelte",
|
|
222
|
+
"types": "./src/status/status-badge.svelte",
|
|
223
|
+
"default": "./src/status/status-badge.svelte"
|
|
199
224
|
},
|
|
200
225
|
"./tasks/status-dot": {
|
|
201
|
-
"svelte": "./src/
|
|
202
|
-
"types": "./src/
|
|
203
|
-
"default": "./src/
|
|
226
|
+
"svelte": "./src/status/status-dot.svelte",
|
|
227
|
+
"types": "./src/status/status-dot.svelte",
|
|
228
|
+
"default": "./src/status/status-dot.svelte"
|
|
204
229
|
},
|
|
205
230
|
"./tasks/statuses": {
|
|
206
|
-
"svelte": "./src/
|
|
207
|
-
"types": "./src/
|
|
208
|
-
"default": "./src/
|
|
231
|
+
"svelte": "./src/status/define-statuses.ts",
|
|
232
|
+
"types": "./src/status/define-statuses.ts",
|
|
233
|
+
"default": "./src/status/define-statuses.ts"
|
|
209
234
|
},
|
|
210
235
|
"./tasks/time": {
|
|
211
236
|
"svelte": "./src/tasks/time/time-tracker.svelte",
|
|
@@ -218,9 +243,9 @@
|
|
|
218
243
|
"default": "./src/tasks/inbox/index.ts"
|
|
219
244
|
},
|
|
220
245
|
"./tasks/side-panel": {
|
|
221
|
-
"svelte": "./src/
|
|
222
|
-
"types": "./src/
|
|
223
|
-
"default": "./src/
|
|
246
|
+
"svelte": "./src/panel/side-panel.svelte",
|
|
247
|
+
"types": "./src/panel/side-panel.svelte",
|
|
248
|
+
"default": "./src/panel/side-panel.svelte"
|
|
224
249
|
},
|
|
225
250
|
"./tasks/presets/sales": {
|
|
226
251
|
"svelte": "./src/tasks/presets/sales.ts",
|
|
@@ -234,11 +259,11 @@
|
|
|
234
259
|
},
|
|
235
260
|
"./tasks/detail-demo.svelte": "./src/tasks/detail/detail-demo.svelte",
|
|
236
261
|
"./tasks/record": {
|
|
237
|
-
"svelte": "./src/
|
|
238
|
-
"types": "./src/
|
|
239
|
-
"default": "./src/
|
|
262
|
+
"svelte": "./src/record/index.ts",
|
|
263
|
+
"types": "./src/record/index.ts",
|
|
264
|
+
"default": "./src/record/index.ts"
|
|
240
265
|
},
|
|
241
|
-
"./tasks/record-demo.svelte": "./src/
|
|
266
|
+
"./tasks/record-demo.svelte": "./src/record/record-demo.svelte",
|
|
242
267
|
"./tasks/demo": {
|
|
243
268
|
"svelte": "./src/tasks/demo.svelte",
|
|
244
269
|
"default": "./src/tasks/demo.svelte"
|
|
@@ -337,44 +362,44 @@
|
|
|
337
362
|
"dependencies": {
|
|
338
363
|
"@dagrejs/dagre": "^3.1.1",
|
|
339
364
|
"@shadcn-svelte/primitives": "^0.0.1",
|
|
340
|
-
"@tiptap/core": "^3.
|
|
341
|
-
"@tiptap/extension-drag-handle": "^3.
|
|
342
|
-
"@tiptap/extension-image": "^3.
|
|
343
|
-
"@tiptap/extension-placeholder": "^3.
|
|
344
|
-
"@tiptap/pm": "^3.
|
|
345
|
-
"@tiptap/starter-kit": "^3.
|
|
365
|
+
"@tiptap/core": "^3.32.0",
|
|
366
|
+
"@tiptap/extension-drag-handle": "^3.32.0",
|
|
367
|
+
"@tiptap/extension-image": "^3.32.0",
|
|
368
|
+
"@tiptap/extension-placeholder": "^3.32.0",
|
|
369
|
+
"@tiptap/pm": "^3.32.0",
|
|
370
|
+
"@tiptap/starter-kit": "^3.32.0",
|
|
346
371
|
"@zag-js/svelte": "^1.46.0",
|
|
347
372
|
"@zag-js/tour": "^1.46.0",
|
|
348
373
|
"clsx": "^2.1.1",
|
|
349
374
|
"d3-scale": "^4.0.2",
|
|
350
375
|
"d3-shape": "^3.2.0",
|
|
351
|
-
"layerchart": "^2.
|
|
352
|
-
"markdown-it": "^15.0.
|
|
353
|
-
"marked": "^18.1.
|
|
376
|
+
"layerchart": "^2.8.1",
|
|
377
|
+
"markdown-it": "^15.0.3",
|
|
378
|
+
"marked": "^18.1.1",
|
|
354
379
|
"perfect-freehand": "^1.2.3",
|
|
355
|
-
"phosphor-svelte": "^3.1.
|
|
380
|
+
"phosphor-svelte": "^3.1.1",
|
|
356
381
|
"roughjs": "^4.6.6",
|
|
357
382
|
"shiki": "^4.5.0",
|
|
358
383
|
"tailwind-merge": "^3.7.0",
|
|
359
384
|
"tailwind-variants": "^3.3.1",
|
|
360
|
-
"zod": "^4.6.
|
|
385
|
+
"zod": "^4.6.7"
|
|
361
386
|
},
|
|
362
387
|
"devDependencies": {
|
|
363
388
|
"@internationalized/date": "^3.12.4",
|
|
364
389
|
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
365
390
|
"@types/d3-scale": "^4.0.9",
|
|
366
391
|
"@types/d3-shape": "^3.2.0",
|
|
367
|
-
"@types/node": "^26.6.
|
|
392
|
+
"@types/node": "^26.6.5",
|
|
368
393
|
"@xyflow/svelte": "^1.7.0",
|
|
369
|
-
"bits-ui": "^2.
|
|
370
|
-
"jsdom": "^30.1.
|
|
394
|
+
"bits-ui": "^2.20.0",
|
|
395
|
+
"jsdom": "^30.1.3",
|
|
371
396
|
"mode-watcher": "^1.1.0",
|
|
372
397
|
"svelte": "^5.57.2",
|
|
373
|
-
"svelte-check": "^4.7.
|
|
398
|
+
"svelte-check": "^4.7.7",
|
|
374
399
|
"svelte-sonner": "^1.2.1",
|
|
375
400
|
"typescript": "~7.0.2",
|
|
376
|
-
"vite-plus": "^1.
|
|
377
|
-
"vitest": "^5.0.
|
|
401
|
+
"vite-plus": "^1.3.0",
|
|
402
|
+
"vitest": "^5.0.5"
|
|
378
403
|
},
|
|
379
404
|
"peerDependencies": {
|
|
380
405
|
"@internationalized/date": "^3.12.4",
|
package/scripts/sync-shadcn.mjs
CHANGED
|
@@ -69,6 +69,8 @@ const DEMO_ALIASES = [
|
|
|
69
69
|
["$lib/utils.js", "@heyramzi/utils"],
|
|
70
70
|
// The peer here is the runes build, published under its own name.
|
|
71
71
|
["@tabler/icons-svelte/", "@tabler/icons-svelte-runes/"],
|
|
72
|
+
// Upstream's docs live under /docs/components; ours are at /components, and the prerender follows the link.
|
|
73
|
+
['href="/docs/components"', 'href="/components"'],
|
|
72
74
|
];
|
|
73
75
|
|
|
74
76
|
/* Registry primitives this package does not ship: no product imports them. Skipped, so a sync does not bring them back. */
|
package/src/canvas/README.md
CHANGED
|
@@ -9,7 +9,7 @@ MIT. It runs on any domain and costs nothing per install.
|
|
|
9
9
|
## What's in it
|
|
10
10
|
|
|
11
11
|
- **Pen layer.** Freehand strokes (perfect-freehand), a highlighter, an eraser, plain text, and rectangle, ellipse and diamond shapes. Colours are token names, so a board drawn in light mode reads fine in dark.
|
|
12
|
-
- **Tool bar.** A floating bar with the tools,
|
|
12
|
+
- **Tool bar.** A floating bar with the tools, a colour and a brush size. Pick from 8 colours and 3 brush sizes. Keys: V, H, L, P, M, E, T, R, O, D, F. Undo and redo are yours to wire to `createCanvasDispatcher`.
|
|
13
13
|
- **Plugin API.** Register your own card types, ops and layout passes. One registry builds xyflow's `nodeTypes` for you.
|
|
14
14
|
- **Ops.** 8 core ops (`addNode`, `removeNode`, `patchNode`, `moveNode`, `setParent`, `addEdge`, `removeEdge`, `patchEdge`) as plain functions over nodes and edges. Zod checks every one.
|
|
15
15
|
- **Undo history.** `createCanvasDispatcher` wraps each edit in a transaction. A whole pen stroke is one undo step.
|
|
@@ -68,14 +68,15 @@ Drop this in `src/routes/+page.svelte`.
|
|
|
68
68
|
toolbar: "Canvas tools",
|
|
69
69
|
style: "Style",
|
|
70
70
|
tools: {
|
|
71
|
-
select: "Select", hand: "Hand",
|
|
72
|
-
eraser: "Eraser", text: "Text", rectangle: "Rectangle",
|
|
73
|
-
diamond: "Diamond", shapes: "Shapes",
|
|
71
|
+
select: "Select", hand: "Hand", connect: "Connect", frame: "Frame", pen: "Pen",
|
|
72
|
+
highlighter: "Highlighter", eraser: "Eraser", text: "Text", rectangle: "Rectangle",
|
|
73
|
+
ellipse: "Ellipse", diamond: "Diamond", shapes: "Shapes", note: "Note",
|
|
74
|
+
},
|
|
75
|
+
colors: {
|
|
76
|
+
ink: "Ink", red: "Red", blue: "Blue", green: "Green", amber: "Amber",
|
|
77
|
+
violet: "Violet", indigo: "Indigo", terra: "Terracotta",
|
|
74
78
|
},
|
|
75
|
-
colors: { ink: "Ink", red: "Red", blue: "Blue", green: "Green", amber: "Amber", violet: "Violet" },
|
|
76
79
|
sizes: ["Small", "Medium", "Large"],
|
|
77
|
-
undo: "Undo",
|
|
78
|
-
redo: "Redo",
|
|
79
80
|
} as const;
|
|
80
81
|
</script>
|
|
81
82
|
|
|
@@ -100,10 +101,6 @@ Drop this in `src/routes/+page.svelte`.
|
|
|
100
101
|
bind:color
|
|
101
102
|
bind:level
|
|
102
103
|
{labels}
|
|
103
|
-
canUndo={history.canUndo}
|
|
104
|
-
canRedo={history.canRedo}
|
|
105
|
-
onUndo={history.undo}
|
|
106
|
-
onRedo={history.redo}
|
|
107
104
|
/>
|
|
108
105
|
</SvelteFlow>
|
|
109
106
|
</div>
|
|
@@ -192,7 +189,7 @@ The pen is `PenOverlay` (or `PenLayer` inside xyflow), and it needs 2 callbacks
|
|
|
192
189
|
|
|
193
190
|
The eraser removes whole strokes, never part of one. A swipe across a big shape doesn't wipe it.
|
|
194
191
|
|
|
195
|
-
The pen has
|
|
192
|
+
The pen has 8 colours: `ink`, `red`, `blue`, `green`, `amber`, `violet`, `indigo` and `terra`. `pen.css` maps them to CSS variables with a light and a dark value. Dark mode applies under a `.dark` class on an ancestor or on `.svelte-flow`.
|
|
196
193
|
|
|
197
194
|
## Headless ops
|
|
198
195
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
7
|
import { panelHeaderClass, panelShellClass } from "../../lib/panel.js";
|
|
8
|
-
import { SectionLabel } from "../../components/
|
|
8
|
+
import { SectionLabel } from "../../components/section-header/index.js";
|
|
9
9
|
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
10
10
|
|
|
11
11
|
// =========================
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
|
-
import SectionLabel from "
|
|
6
|
+
import SectionLabel from "../section-header/section-label.svelte";
|
|
7
7
|
import { cn } from "../../lib/utils.js";
|
|
8
8
|
import { panelHeaderClass, panelShellClass } from "../../lib/panel.js";
|
|
9
9
|
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
@@ -7,7 +7,7 @@ import * as Popover from "../../shadcn/popover/index.js";
|
|
|
7
7
|
import { Funnel as FunnelIcon } from "phosphor-svelte";
|
|
8
8
|
import { cn } from "../../lib/utils.js";
|
|
9
9
|
import { Button } from "../../shadcn/button/index.js";
|
|
10
|
-
import { toolbarTriggerClass } from "
|
|
10
|
+
import { toolbarTriggerClass } from "../toolbar/toolbar-trigger.js";
|
|
11
11
|
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
12
12
|
|
|
13
13
|
// phosphor-svelte v2 (resolved here) and v3 (what consumers run) differ only in
|
|
@@ -12,7 +12,7 @@ export { default as RailBody } from "./rail-body.svelte";
|
|
|
12
12
|
export { default as RailEmpty } from "./rail-empty.svelte";
|
|
13
13
|
export { default as RailHead } from "./rail-head.svelte";
|
|
14
14
|
export { default as RailRow } from "./rail-row.svelte";
|
|
15
|
-
export { default as SectionLabel } from "
|
|
15
|
+
export { default as SectionLabel } from "../section-header/section-label.svelte";
|
|
16
16
|
export { queueRailClass, queueRailWidth } from "./queue-rail.js";
|
|
17
17
|
export { default as FilterPopover } from "./filter-popover.svelte";
|
|
18
18
|
export { filterSectionClass } from "./filter-section.js";
|
|
@@ -21,8 +21,8 @@ export { default as ListPageShell } from "./list-page-shell.svelte";
|
|
|
21
21
|
export { default as MasterDetail } from "./master-detail.svelte";
|
|
22
22
|
// The segmented control isn't re-exported here. Two import paths drifted, so
|
|
23
23
|
// its one home is `@heyramzi/ui/components/segmented`.
|
|
24
|
-
export { default as ToolbarSearch } from "
|
|
25
|
-
export { toolbarTriggerClass } from "
|
|
24
|
+
export { default as ToolbarSearch } from "../toolbar/toolbar-search.svelte";
|
|
25
|
+
export { toolbarTriggerClass } from "../toolbar/toolbar-trigger.js";
|
|
26
26
|
export { listTable } from "./list-table.js";
|
|
27
27
|
export type { ViewMode } from "./types.js";
|
|
28
28
|
export { getSectionHost, setSectionHost, type SectionHost } from "./section-host.js";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
|
-
import SectionLabel from "
|
|
6
|
+
import SectionLabel from "../section-header/section-label.svelte";
|
|
7
7
|
import { cn } from "../../lib/utils.js";
|
|
8
8
|
|
|
9
9
|
// =========================
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { CaretDown, MagnifyingGlass } from "phosphor-svelte";
|
|
7
7
|
import { SegmentedFilter } from "../segmented/index.js";
|
|
8
8
|
import type { TabDef } from "../segmented/animated-tabs.svelte";
|
|
9
|
-
import { toolbarTriggerClass } from "
|
|
10
|
-
import ToolbarSearch from "
|
|
9
|
+
import { toolbarTriggerClass } from "./toolbar-trigger.js";
|
|
10
|
+
import ToolbarSearch from "./toolbar-search.svelte";
|
|
11
11
|
import { Input } from "../../shadcn/input/index.js";
|
|
12
12
|
import * as DropdownMenu from "../../shadcn/dropdown-menu/index.js";
|
|
13
13
|
import * as Popover from "../../shadcn/popover/index.js";
|
package/src/editor/editor.svelte
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
5
|
import { Editor } from "@tiptap/core";
|
|
6
|
+
import { untrack } from "svelte";
|
|
6
7
|
import "./content.css";
|
|
7
8
|
import { buildExtensions } from "./extensions.js";
|
|
8
9
|
import { buildSlashItems, type SlashItem } from "./slash-items.js";
|
|
@@ -176,7 +177,7 @@ function deleteActiveImage() {
|
|
|
176
177
|
activeImageElement = null;
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
function mount() {
|
|
180
181
|
if (!editorElement || editorInstance) return;
|
|
181
182
|
|
|
182
183
|
const extensions = buildExtensions({ preset, placeholder, blockIds });
|
|
@@ -270,6 +271,12 @@ $effect(() => {
|
|
|
270
271
|
editorInstance?.destroy();
|
|
271
272
|
editorInstance = null;
|
|
272
273
|
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Only the element mounts the editor. Read tracked, `content` (fed back by onchange) and the
|
|
277
|
+
// `tick++` each transaction makes would destroy and rebuild it until Svelte stops the loop.
|
|
278
|
+
$effect(() => {
|
|
279
|
+
if (editorElement) return untrack(mount);
|
|
273
280
|
});
|
|
274
281
|
</script>
|
|
275
282
|
|
package/src/gallery.test.ts
CHANGED
|
@@ -28,7 +28,12 @@ const SITE = join(PKG, "..", "..", "site", "src", "lib");
|
|
|
28
28
|
|
|
29
29
|
describe("site gallery coverage", () => {
|
|
30
30
|
it("gives every animation a stage", () => {
|
|
31
|
-
|
|
31
|
+
// Each animation's stage is its own file under demos/motion/.
|
|
32
|
+
const stages = join(SITE, "demos", "motion");
|
|
33
|
+
const section = readdirSync(stages)
|
|
34
|
+
.filter((file) => file.endsWith(".svelte"))
|
|
35
|
+
.map((file) => readFileSync(join(stages, file), "utf8"))
|
|
36
|
+
.join("\n");
|
|
32
37
|
const missing = readdirSync(join(PKG, "src", "animations"))
|
|
33
38
|
.filter((file) => file.endsWith(".css") && file !== "index.css")
|
|
34
39
|
.filter((file) => !section.includes(`animations/${file.replace(".css", "")}`));
|
|
@@ -43,6 +48,20 @@ describe("site gallery coverage", () => {
|
|
|
43
48
|
expect(vendored.filter((name) => !manifest.includes(`name: "${name}"`))).toEqual([]);
|
|
44
49
|
});
|
|
45
50
|
|
|
51
|
+
it("gives every component, AI part and effect a place on the site", () => {
|
|
52
|
+
const walk = (dir: string): string[] =>
|
|
53
|
+
readdirSync(dir, { withFileTypes: true }).flatMap((d) =>
|
|
54
|
+
d.isDirectory() ? walk(join(dir, d.name)) : /\.(svelte|ts)$/.test(d.name) ? [join(dir, d.name)] : [],
|
|
55
|
+
);
|
|
56
|
+
const site = walk(join(SITE, "..")).map((file) => readFileSync(file, "utf8")).join("\n");
|
|
57
|
+
const parts = ["components", "ai", "effects"].flatMap((top) =>
|
|
58
|
+
readdirSync(join(PKG, "src", top), { withFileTypes: true })
|
|
59
|
+
.filter((d) => d.isDirectory())
|
|
60
|
+
.map((d) => `${top}/${d.name}`),
|
|
61
|
+
);
|
|
62
|
+
expect(parts.filter((part) => !site.includes(`${part}/`) && !site.includes(`${part}"`))).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
46
65
|
it("keeps a demo file behind every listed primitive", () => {
|
|
47
66
|
const manifest = readFileSync(join(SITE, "shadcn-demos", "index.ts"), "utf8");
|
|
48
67
|
const listed = [...manifest.matchAll(/name: "([a-z-]+)"/g)].map((m) => m[1]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @heyramzi/ui/inbox
|
|
2
|
+
|
|
3
|
+
One threaded inbox. Entries from any source (a comment, a status change, an assignment, a mention, an agent reply, an agent's ask) group into one thread per subject, and the screen shows the thread list on the left and the selected thread on the right. The inbox never fetches. Everything leaves through callbacks.
|
|
4
|
+
|
|
5
|
+
```svelte
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { ThreadInbox, type InboxEntry } from "@heyramzi/ui/inbox";
|
|
8
|
+
|
|
9
|
+
let entries = $state<InboxEntry[]>([]);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<ThreadInbox
|
|
13
|
+
{entries}
|
|
14
|
+
unreadFirst
|
|
15
|
+
onRead={(thread, read) => mark(thread.key, read)}
|
|
16
|
+
onSnooze={({ thread, until }) => snooze(thread.key, until)}
|
|
17
|
+
onDecide={(verdict, thread) => decide(verdict)}
|
|
18
|
+
/>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **Grouping.** `threadsOf(entries, { unreadFirst })` keys on `subject.type` plus `subject.id`, sorts threads by their newest entry and entries oldest first. An entry with no subject id is dropped. A malformed timestamp sorts last. Each thread carries `unread`, `latestAt` and `pending`.
|
|
22
|
+
- **Needs you.** A `proposal` entry whose `proposal` (an `InboxItem`) is still open pins to the top of its thread, drawn with `InboxCard`. The "Needs you" filter keeps threads that hold one. Verdicts come out of `onDecide`.
|
|
23
|
+
- **Filters.** By subject type, by actor name and by Needs you. `filterThreads` runs them without the component.
|
|
24
|
+
- **Keys.** `j` and `k` move, `u` marks the thread read (or unread when nothing is unread), `h` opens the snooze row (`1`, `2`, `3` pick a time), and `a`, `d`, `s`, `e` act on the pinned proposal. A key does nothing while focus is in an input, a textarea, a select or an editable element, or while a modifier is held. `inboxKeyOf(event)` is the same check.
|
|
25
|
+
- **Snooze.** `onSnooze({ thread, until })` gets a `Date`: 3 hours on, tomorrow 9:00 or next Monday 9:00 (`snoozeChoices(now)`). The host decides what snoozed means and stops passing the entries.
|
|
26
|
+
- **Read.** `onRead(thread, read)` is true when the thread had unread entries, so the host marks them all read.
|
|
27
|
+
- **Words.** `labels` is a partial of `InboxLabels`. Status and assignment lines have `{from}`, `{to}` and `{assignee}` slots. `typeLabels` names the subject types. The `avatar` and `status` snippets let the app draw its own portrait and status badge.
|
|
28
|
+
|
|
29
|
+
`@heyramzi/ui/tasks/inbox` still exports the approvals-only `Inbox`.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
2
|
+
import {
|
|
3
|
+
NO_FILTER,
|
|
4
|
+
entryLine,
|
|
5
|
+
filterThreads,
|
|
6
|
+
inboxKeyOf,
|
|
7
|
+
resolveInboxLabels,
|
|
8
|
+
snoozeChoices,
|
|
9
|
+
threadsOf,
|
|
10
|
+
} from "./logic.js";
|
|
11
|
+
import type { InboxEntry, InboxSubject } from "./types.js";
|
|
12
|
+
import type { InboxItem } from "../tasks/inbox/inbox-types.js";
|
|
13
|
+
|
|
14
|
+
const tsk7: InboxSubject = { type: "task", id: "7", ref: "TSK-7", title: "Write the pricing page" };
|
|
15
|
+
const tsk9: InboxSubject = { type: "task", id: "9", ref: "TSK-9", title: "Fix the footer" };
|
|
16
|
+
const doc1: InboxSubject = { type: "doc", id: "1", title: "Brief" };
|
|
17
|
+
const ada = { kind: "user", name: "Ada" } as const;
|
|
18
|
+
const bot = { kind: "agent", name: "Scribe" } as const;
|
|
19
|
+
|
|
20
|
+
function entry(id: string, at: string, extra: Partial<InboxEntry> = {}): InboxEntry {
|
|
21
|
+
return { id, kind: "comment", subject: tsk7, actor: ada, at, ...extra };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const proposal = (state: InboxItem["state"] = "proposed", tier: InboxItem["tier"] = "ask"): InboxItem => ({
|
|
25
|
+
id: "p",
|
|
26
|
+
kind: "decision",
|
|
27
|
+
tier,
|
|
28
|
+
state,
|
|
29
|
+
title: "Send the quote",
|
|
30
|
+
project: { id: "x", name: "X" },
|
|
31
|
+
proposal: { summary: "Send it" },
|
|
32
|
+
confidence: { score: 0.9 },
|
|
33
|
+
trace: { source: { type: "email", label: "Mail", at: "Mon" }, evidence: [], reasoning: "" },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("threadsOf", () => {
|
|
37
|
+
it("makes one thread of an agent comment and a status change on TSK-7", () => {
|
|
38
|
+
const threads = threadsOf([
|
|
39
|
+
entry("c1", "2026-10-22T09:00:00Z", { kind: "agent_response", actor: bot, body: "Drafted" }),
|
|
40
|
+
entry("s1", "2026-10-22T09:05:00Z", { kind: "status", status: { from: "todo", to: "review" } }),
|
|
41
|
+
]);
|
|
42
|
+
expect(threads).toHaveLength(1);
|
|
43
|
+
expect(threads[0].entries).toHaveLength(2);
|
|
44
|
+
expect(threads[0].unread).toBe(2);
|
|
45
|
+
expect(threads[0].subject.ref).toBe("TSK-7");
|
|
46
|
+
expect(threads[0].latestAt).toBe("2026-10-22T09:05:00Z");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("counts only unread entries", () => {
|
|
50
|
+
const [thread] = threadsOf([entry("a", "2026-10-22T09:00:00Z", { read: true }), entry("b", "2026-10-22T09:01:00Z")]);
|
|
51
|
+
expect(thread.unread).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("sorts threads by their latest entry, newest first", () => {
|
|
55
|
+
const threads = threadsOf([
|
|
56
|
+
entry("a", "2026-10-22T09:00:00Z"),
|
|
57
|
+
entry("b", "2026-10-22T11:00:00Z", { subject: tsk9 }),
|
|
58
|
+
entry("c", "2026-10-22T10:00:00Z", { subject: doc1 }),
|
|
59
|
+
]);
|
|
60
|
+
expect(threads.map((t) => t.subject.id)).toEqual(["9", "1", "7"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("puts threads with unread entries first on request", () => {
|
|
64
|
+
const entries = [
|
|
65
|
+
entry("a", "2026-10-22T09:00:00Z"),
|
|
66
|
+
entry("b", "2026-10-22T11:00:00Z", { subject: tsk9, read: true }),
|
|
67
|
+
];
|
|
68
|
+
expect(threadsOf(entries).map((t) => t.subject.id)).toEqual(["9", "7"]);
|
|
69
|
+
expect(threadsOf(entries, { unreadFirst: true }).map((t) => t.subject.id)).toEqual(["7", "9"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("drops an entry with no subject id", () => {
|
|
73
|
+
const threads = threadsOf([
|
|
74
|
+
entry("a", "2026-10-22T09:00:00Z"),
|
|
75
|
+
entry("b", "2026-10-22T09:01:00Z", { subject: undefined }),
|
|
76
|
+
entry("c", "2026-10-22T09:02:00Z", { subject: { ...tsk9, id: "" } }),
|
|
77
|
+
]);
|
|
78
|
+
expect(threads).toHaveLength(1);
|
|
79
|
+
expect(threads[0].entries.map((e) => e.id)).toEqual(["a"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("sorts a malformed timestamp last, among threads and inside one", () => {
|
|
83
|
+
const threads = threadsOf([
|
|
84
|
+
entry("bad", "yesterday", { subject: tsk9 }),
|
|
85
|
+
entry("ok", "2026-10-22T09:00:00Z"),
|
|
86
|
+
entry("late", "2026-10-22T09:30:00Z"),
|
|
87
|
+
entry("junk", "??"),
|
|
88
|
+
]);
|
|
89
|
+
expect(threads.map((t) => t.subject.id)).toEqual(["7", "9"]);
|
|
90
|
+
expect(threads[0].entries.map((e) => e.id)).toEqual(["ok", "late", "junk"]);
|
|
91
|
+
expect(threads[0].latestAt).toBe("2026-10-22T09:30:00Z");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("keeps a pending proposal and ignores a decided one", () => {
|
|
95
|
+
const [open] = threadsOf([entry("p", "2026-10-22T09:00:00Z", { kind: "proposal", proposal: proposal() })]);
|
|
96
|
+
expect(open.pending).toHaveLength(1);
|
|
97
|
+
const [done] = threadsOf([entry("p", "2026-10-22T09:00:00Z", { kind: "proposal", proposal: proposal("approved") })]);
|
|
98
|
+
expect(done.pending).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("filterThreads", () => {
|
|
103
|
+
const threads = threadsOf([
|
|
104
|
+
entry("a", "2026-10-22T09:00:00Z", { actor: bot }),
|
|
105
|
+
entry("b", "2026-10-22T10:00:00Z", { subject: doc1 }),
|
|
106
|
+
entry("p", "2026-10-22T11:00:00Z", { subject: tsk9, kind: "proposal", proposal: proposal() }),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
it("filters by type, by who and by needs you", () => {
|
|
110
|
+
expect(filterThreads(threads, { ...NO_FILTER, type: "doc" }).map((t) => t.subject.id)).toEqual(["1"]);
|
|
111
|
+
expect(filterThreads(threads, { ...NO_FILTER, who: "Scribe" }).map((t) => t.subject.id)).toEqual(["7"]);
|
|
112
|
+
expect(filterThreads(threads, { ...NO_FILTER, needsYou: true }).map((t) => t.subject.id)).toEqual(["9"]);
|
|
113
|
+
expect(filterThreads(threads, NO_FILTER)).toHaveLength(3);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const press = (key: string, target: EventTarget | null = document.body) =>
|
|
118
|
+
inboxKeyOf({ key, metaKey: false, ctrlKey: false, altKey: false, defaultPrevented: false, target });
|
|
119
|
+
|
|
120
|
+
describe("keys", () => {
|
|
121
|
+
it("maps the nine keys", () => {
|
|
122
|
+
expect(["j", "k", "u", "h", "a", "d", "s", "e"].map((k) => press(k))).toEqual([
|
|
123
|
+
"next",
|
|
124
|
+
"previous",
|
|
125
|
+
"toggleRead",
|
|
126
|
+
"snooze",
|
|
127
|
+
"approve",
|
|
128
|
+
"decline",
|
|
129
|
+
"steer",
|
|
130
|
+
"edit",
|
|
131
|
+
]);
|
|
132
|
+
expect(press("x")).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("does nothing for u while typing", () => {
|
|
136
|
+
for (const tag of ["input", "textarea", "select"]) {
|
|
137
|
+
expect(press("u", document.createElement(tag))).toBeNull();
|
|
138
|
+
}
|
|
139
|
+
const editable = document.createElement("div");
|
|
140
|
+
Object.defineProperty(editable, "isContentEditable", { value: true });
|
|
141
|
+
expect(press("u", editable)).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("ignores a held modifier", () => {
|
|
145
|
+
expect(inboxKeyOf({ key: "u", metaKey: true, ctrlKey: false, altKey: false, defaultPrevented: false, target: document.body })).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("snoozeChoices", () => {
|
|
150
|
+
it("offers 3 hours on, tomorrow at 9 and the next Monday at 9", () => {
|
|
151
|
+
const wed = new Date(2026, 9, 21, 14, 30);
|
|
152
|
+
const [later, tomorrow, nextWeek] = snoozeChoices(wed);
|
|
153
|
+
expect(later.until.getTime() - wed.getTime()).toBe(3 * 60 * 60 * 1000);
|
|
154
|
+
expect([tomorrow.until.getDate(), tomorrow.until.getHours()]).toEqual([22, 9]);
|
|
155
|
+
expect([nextWeek.until.getDay(), nextWeek.until.getDate(), nextWeek.until.getHours()]).toEqual([1, 26, 9]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("moves a Monday to the following Monday", () => {
|
|
159
|
+
const mon = new Date(2026, 9, 26, 10, 0);
|
|
160
|
+
expect(snoozeChoices(mon)[2].until.getDate()).toBe(2);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("entryLine", () => {
|
|
165
|
+
it("fills the host's own words, French included", () => {
|
|
166
|
+
const fr = resolveInboxLabels({ statusMoved: "a déplacé ceci de {from} à {to}" });
|
|
167
|
+
expect(entryLine(entry("s", "2026-10-22T09:00:00Z", { kind: "status", status: { from: "A faire", to: "Revue" } }), fr)).toBe(
|
|
168
|
+
"a déplacé ceci de A faire à Revue",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { default as ThreadInbox } from "./thread-inbox.svelte";
|
|
2
|
+
export {
|
|
3
|
+
NO_FILTER,
|
|
4
|
+
actorNames,
|
|
5
|
+
entryLine,
|
|
6
|
+
filterThreads,
|
|
7
|
+
inboxKeyOf,
|
|
8
|
+
isPending,
|
|
9
|
+
resolveInboxLabels,
|
|
10
|
+
snoozeChoices,
|
|
11
|
+
subjectTypes,
|
|
12
|
+
threadsOf,
|
|
13
|
+
} from "./logic.js";
|
|
14
|
+
export type { SnoozeChoice, ThreadsOptions } from "./logic.js";
|
|
15
|
+
export { ALL, DEFAULT_INBOX_LABELS } from "./types.js";
|
|
16
|
+
export type {
|
|
17
|
+
InboxActor,
|
|
18
|
+
InboxActorKind,
|
|
19
|
+
InboxEntry,
|
|
20
|
+
InboxEntryKind,
|
|
21
|
+
InboxFilter,
|
|
22
|
+
InboxKey,
|
|
23
|
+
InboxLabels,
|
|
24
|
+
InboxSnooze,
|
|
25
|
+
InboxSubject,
|
|
26
|
+
InboxThread,
|
|
27
|
+
} from "./types.js";
|
|
28
|
+
|
|
29
|
+
// The approval card a pinned proposal is drawn with, and the types its `proposal` field carries.
|
|
30
|
+
export { default as InboxCard } from "../tasks/inbox/inbox-card.svelte";
|
|
31
|
+
export type { InboxItem, InboxVerdict } from "../tasks/inbox/inbox-types.js";
|