@heyramzi/ui 2.1.0 → 2.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.
Files changed (312) hide show
  1. package/package.json +7 -8
  2. package/scripts/sync-shadcn.mjs +3 -4
  3. package/src/agents/agent-avatar.svelte +604 -575
  4. package/src/agents/agent-cast.svelte +179 -183
  5. package/src/agents/demo.svelte +125 -113
  6. package/src/agents/helpers.ts +23 -17
  7. package/src/agents/index.ts +4 -4
  8. package/src/agents/page-state.ts +6 -6
  9. package/src/agents/types.ts +6 -6
  10. package/src/ai/ai-button/AiButton.svelte +4 -12
  11. package/src/ai/generation-progress/GenerationProgress.svelte +10 -3
  12. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -2
  13. package/src/ai/response/Response.css +2 -3
  14. package/src/animations/avatar-group-hover.css +2 -16
  15. package/src/animations/card-tilt.css +3 -6
  16. package/src/animations/digit-pop-in.css +2 -15
  17. package/src/animations/dropdown.css +4 -17
  18. package/src/animations/input-clear-dissolve.css +5 -18
  19. package/src/animations/input-shake.css +9 -41
  20. package/src/animations/learn-more-hover.css +17 -9
  21. package/src/animations/like-button.css +2 -4
  22. package/src/animations/page-side-by-side.css +2 -10
  23. package/src/animations/plus-menu-morph.css +2 -5
  24. package/src/animations/shimmer-text.css +5 -18
  25. package/src/animations/spinning-counter.css +2 -6
  26. package/src/animations/success-check.css +2 -15
  27. package/src/animations/thinking-states.css +2 -7
  28. package/src/brand/brand.test.ts +21 -5
  29. package/src/brand/index.ts +4 -10
  30. package/src/canvas/card/card-frame.svelte +43 -39
  31. package/src/canvas/card/resize-handles.svelte +89 -80
  32. package/src/canvas/chrome/chrome-zoom.svelte +101 -100
  33. package/src/canvas/chrome/lock-badge.svelte +8 -8
  34. package/src/canvas/chrome/side-handles.svelte +24 -19
  35. package/src/canvas/chrome/size-chip.svelte +30 -30
  36. package/src/canvas/chrome/snap-guides.svelte +20 -20
  37. package/src/canvas/components/icons/ToolIcon.svelte +8 -8
  38. package/src/canvas/components/shared/svg/PreviewSvg.svelte +19 -19
  39. package/src/canvas/components/shared/svg/ShapeOutline.svelte +71 -48
  40. package/src/canvas/components/shared/svg/StrokeNode.svelte +21 -27
  41. package/src/canvas/components/shared/svg/TextRule.svelte +25 -17
  42. package/src/canvas/edges/bend-handle.svelte +46 -46
  43. package/src/canvas/edges/edge-gradient.svelte +10 -10
  44. package/src/canvas/edges/gradient-edge.svelte +45 -29
  45. package/src/canvas/editor/lock.test.ts +6 -1
  46. package/src/canvas/editor/style.ts +7 -1
  47. package/src/canvas/headless/svg-figure.test.ts +5 -1
  48. package/src/canvas/lod/lod-switch.svelte +111 -106
  49. package/src/canvas/pen/PenField.svelte +87 -81
  50. package/src/canvas/pen/PenOverlay.svelte +127 -127
  51. package/src/canvas/pen/ShapeNode.svelte +31 -32
  52. package/src/canvas/pen/TextNode.svelte +54 -53
  53. package/src/canvas/pen/host-field.test.svelte +7 -7
  54. package/src/canvas/pen/shapes.ts +7 -1
  55. package/src/canvas/screen/GlyphIcon.svelte +22 -17
  56. package/src/canvas/screen/PartView.svelte +138 -72
  57. package/src/canvas/screen/ScreenBody.svelte +81 -60
  58. package/src/canvas/screen/ScreenCard.svelte +20 -20
  59. package/src/canvas/ui/CanvasToolbar.svelte +173 -85
  60. package/src/canvas/ui/FrameLayer.svelte +59 -59
  61. package/src/canvas/ui/PenLayer.svelte +39 -30
  62. package/src/canvas/ui/StylePanel.svelte +119 -102
  63. package/src/canvas/ui/canvas-grid.svelte +137 -129
  64. package/src/charts/animated-value.svelte +1 -1
  65. package/src/charts/area-chart.css +2 -5
  66. package/src/charts/area-chart.svelte +1 -1
  67. package/src/charts/bar-chart.css +2 -3
  68. package/src/charts/bar-chart.svelte +1 -1
  69. package/src/charts/charts.css +5 -17
  70. package/src/charts/crosshair.svelte +1 -1
  71. package/src/charts/dashboard/dashboard-empty.svelte +25 -25
  72. package/src/charts/dashboard/dashboard-section.svelte +24 -24
  73. package/src/charts/dashboard/data-panel.svelte +50 -50
  74. package/src/charts/dashboard/data-sort-head.svelte +56 -56
  75. package/src/charts/dashboard/data-table.svelte +138 -138
  76. package/src/charts/dashboard/panel-grid.svelte +30 -30
  77. package/src/charts/dashboard/period-filter.svelte +25 -25
  78. package/src/charts/dashboard/rating-chip.svelte +19 -19
  79. package/src/charts/dashboard/stat-band.svelte +32 -32
  80. package/src/charts/dashboard/stat-tile.svelte +106 -99
  81. package/src/charts/donut-chart.svelte +1 -1
  82. package/src/charts/edge-fade.svelte +1 -1
  83. package/src/charts/figure.svelte +1 -1
  84. package/src/charts/gauge-chart.css +4 -7
  85. package/src/charts/gauge-chart.svelte +1 -1
  86. package/src/charts/hover-focus.svelte +1 -1
  87. package/src/charts/legend.css +1 -3
  88. package/src/charts/legend.svelte +1 -1
  89. package/src/charts/line-chart.svelte +1 -1
  90. package/src/charts/number-chart.css +6 -9
  91. package/src/charts/number-chart.svelte +1 -1
  92. package/src/charts/reference-line.svelte +19 -19
  93. package/src/charts/reveal.svelte +1 -1
  94. package/src/charts/ring-chart.css +2 -4
  95. package/src/charts/ring-chart.svelte +1 -1
  96. package/src/charts/sparkline.svelte +5 -2
  97. package/src/charts/stat-card.svelte +1 -1
  98. package/src/charts/time-series-chart.css +2 -5
  99. package/src/charts/trend-pill.css +2 -4
  100. package/src/charts/trend-pill.svelte +1 -1
  101. package/src/components/copy/copy-button.svelte +93 -98
  102. package/src/components/copy/copy-button.test.ts +10 -2
  103. package/src/components/data-table/infinite-scroll.svelte +42 -42
  104. package/src/components/data-table/pagination.svelte +34 -22
  105. package/src/components/data-table/row-arrow.svelte +19 -16
  106. package/src/components/data-table/row-context-menu.svelte +52 -52
  107. package/src/components/data-table/sortable-head.svelte +25 -25
  108. package/src/components/data-table/table-skeleton.svelte +41 -36
  109. package/src/components/docs/doc-page.svelte +18 -9
  110. package/src/components/docs/on-this-page.svelte +16 -3
  111. package/src/components/docs/scroll-spy.svelte.ts +2 -3
  112. package/src/components/field-row/field-row.svelte +10 -8
  113. package/src/components/hold-to-confirm/hold-to-confirm.svelte +5 -1
  114. package/src/components/icon/icon.svelte +14 -14
  115. package/src/components/language-flag/language-flag.svelte +3 -1
  116. package/src/components/list-page/detail-empty.svelte +34 -34
  117. package/src/components/list-page/detail-header.svelte +55 -47
  118. package/src/components/list-page/detail-panel.svelte +51 -59
  119. package/src/components/list-page/detail-row.svelte +19 -19
  120. package/src/components/list-page/detail-stack.svelte +21 -21
  121. package/src/components/list-page/empty-state.svelte +30 -30
  122. package/src/components/list-page/filter-item.svelte +20 -20
  123. package/src/components/list-page/filter-popover.svelte +60 -55
  124. package/src/components/list-page/grouped-list.svelte +4 -2
  125. package/src/components/list-page/index.ts +5 -1
  126. package/src/components/list-page/list-page-shell.svelte +2 -5
  127. package/src/components/list-page/master-detail.svelte +87 -88
  128. package/src/components/list-page/notice-bar.svelte +38 -38
  129. package/src/components/list-page/prev-next.svelte +63 -63
  130. package/src/components/list-page/rail-body.svelte +18 -18
  131. package/src/components/list-page/rail-empty.svelte +23 -30
  132. package/src/components/list-page/rail-head.svelte +24 -24
  133. package/src/components/list-page/rail-row.svelte +77 -73
  134. package/src/components/list-page/section-host.ts +2 -3
  135. package/src/components/locale-switch/locale-switch.svelte +41 -41
  136. package/src/components/mark/mark.svelte +34 -38
  137. package/src/components/open-in/index.ts +1 -5
  138. package/src/components/open-in/open-in.svelte +40 -40
  139. package/src/components/picker/mark-picker.svelte +49 -49
  140. package/src/components/picker/picker.svelte +5 -3
  141. package/src/components/section-header/section-header.test.ts +7 -1
  142. package/src/components/section-header/section-label.svelte +20 -20
  143. package/src/components/segmented/animated-tabs.svelte +31 -2
  144. package/src/components/segmented/collapse.test.ts +2 -1
  145. package/src/components/segmented/index.ts +2 -0
  146. package/src/components/segmented/segment.svelte +52 -49
  147. package/src/components/segmented/segmented-filter.svelte +79 -80
  148. package/src/components/segmented/segmented-nav.svelte +34 -40
  149. package/src/components/segmented/segmented-overflow.svelte +53 -50
  150. package/src/components/segmented/segmented-track.svelte +62 -46
  151. package/src/components/segmented/segmented.ts +2 -3
  152. package/src/components/star-rating-input/star-rating-input.svelte +103 -103
  153. package/src/components/svg/svg.svelte +4 -4
  154. package/src/components/table-view/table-view.svelte +17 -2
  155. package/src/components/table-view/table-view.test.ts +7 -1
  156. package/src/components/table-view/types.ts +11 -11
  157. package/src/components/tags-input/index.ts +13 -0
  158. package/src/components/tags-input/tags-input.svelte +305 -0
  159. package/src/components/tags-input/tags.test.ts +64 -0
  160. package/src/components/tags-input/tags.ts +142 -0
  161. package/src/components/theme/index.ts +2 -4
  162. package/src/components/theme/theme-provider.svelte +1 -3
  163. package/src/components/toolbar/toolbar-search.svelte +25 -25
  164. package/src/components/toolbar/toolbar.svelte +11 -7
  165. package/src/components/toolbar/toolbar.test.ts +10 -1
  166. package/src/design/index.css +21 -78
  167. package/src/design/polish.css +26 -35
  168. package/src/editor/editor-toolbar.svelte +104 -99
  169. package/src/editor/editor.svelte +296 -266
  170. package/src/editor/image-toolbar.svelte +20 -22
  171. package/src/editor/index.ts +1 -4
  172. package/src/editor/inline-editor.svelte +280 -238
  173. package/src/editor/markdown.ts +15 -15
  174. package/src/editor/renderer.svelte +71 -71
  175. package/src/editor/slash-menu.svelte +24 -27
  176. package/src/effects/animated-beam/diagrams/BeamSvg.svelte +1 -1
  177. package/src/effects/image-comparison/ImageComparison.svelte +11 -1
  178. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +59 -46
  179. package/src/effects/magic-card/MagicCard.css +14 -7
  180. package/src/effects/marquee/Marquee.svelte +3 -5
  181. package/src/effects/reveal/observe.ts +0 -3
  182. package/src/effects/spotlight/Spotlight.svelte +1 -1
  183. package/src/effects/theme-toggle/ThemeToggle.css +25 -22
  184. package/src/effects/theme-toggle/ThemeToggle.svelte +6 -7
  185. package/src/gallery.test.ts +14 -4
  186. package/src/inbox/README.md +1 -3
  187. package/src/inbox/inbox.test.ts +181 -138
  188. package/src/inbox/index.ts +20 -20
  189. package/src/inbox/logic.ts +114 -111
  190. package/src/inbox/thread-inbox.svelte +227 -151
  191. package/src/inbox/types.ts +130 -108
  192. package/src/lib/theme-transition.ts +2 -4
  193. package/src/lib/tone.ts +2 -4
  194. package/src/lib/utils.ts +2 -2
  195. package/src/modules.test.ts +79 -69
  196. package/src/onboarding/define-tours.test.ts +55 -40
  197. package/src/onboarding/define-tours.ts +23 -23
  198. package/src/onboarding/demo.svelte +24 -5
  199. package/src/onboarding/index.ts +1 -8
  200. package/src/onboarding/onboarding-provider.svelte +19 -7
  201. package/src/onboarding/onboarding.svelte.ts +16 -16
  202. package/src/onboarding/pick.test.ts +61 -52
  203. package/src/onboarding/pick.ts +16 -13
  204. package/src/onboarding/stores.test.ts +80 -80
  205. package/src/onboarding/stores.ts +41 -39
  206. package/src/onboarding/tour-card.svelte +30 -5
  207. package/src/onboarding/tour-runner.svelte +12 -3
  208. package/src/onboarding/types.ts +20 -12
  209. package/src/onboarding/wait.ts +22 -22
  210. package/src/panel/side-panel.svelte +94 -93
  211. package/src/record/demo-data.ts +291 -225
  212. package/src/record/field-section.svelte +38 -34
  213. package/src/record/fields.ts +148 -96
  214. package/src/record/format.ts +63 -56
  215. package/src/record/index.ts +49 -35
  216. package/src/record/info-list.svelte +145 -114
  217. package/src/record/kinds.ts +31 -31
  218. package/src/record/logic.ts +216 -214
  219. package/src/record/next-up.svelte +45 -34
  220. package/src/record/project-list.svelte +56 -50
  221. package/src/record/record-demo.svelte +130 -110
  222. package/src/record/record-header.svelte +37 -28
  223. package/src/record/record-shell.svelte +54 -54
  224. package/src/record/record-stats.svelte +25 -25
  225. package/src/record/record-timeline.svelte +134 -114
  226. package/src/record/record.test.ts +430 -309
  227. package/src/record/steps-rail.svelte +225 -216
  228. package/src/record/types.ts +234 -215
  229. package/src/record/where-we-are.svelte +62 -58
  230. package/src/shadcn/button/button.svelte +1 -1
  231. package/src/shadcn/card/card-title.svelte +1 -5
  232. package/src/shadcn/hooks/use-highlighted-code.svelte.ts +2 -13
  233. package/src/shadcn/sheet/sheet-content.svelte +2 -4
  234. package/src/shadcn/table/table-cell.svelte +2 -3
  235. package/src/shadcn/utilities.css +3 -8
  236. package/src/slides/Controls.svelte +150 -124
  237. package/src/slides/Deck.svelte +243 -238
  238. package/src/slides/Fragment.svelte +64 -57
  239. package/src/slides/Overview.svelte +106 -101
  240. package/src/slides/Presenter.svelte +300 -266
  241. package/src/slides/Slide.svelte +185 -170
  242. package/src/slides/SlideList.svelte +16 -12
  243. package/src/slides/ambient.d.ts +1 -1
  244. package/src/slides/bridges.ts +42 -23
  245. package/src/slides/deck-controller.svelte.ts +47 -35
  246. package/src/slides/demo-slides.svelte +330 -306
  247. package/src/slides/demo.svelte +164 -160
  248. package/src/slides/export/capture.ts +20 -16
  249. package/src/slides/export/ffmpeg.ts +43 -38
  250. package/src/slides/export/index.ts +5 -5
  251. package/src/slides/export/launch.ts +3 -1
  252. package/src/slides/export/pdf.ts +30 -27
  253. package/src/slides/export/types.ts +10 -8
  254. package/src/slides/index.ts +11 -11
  255. package/src/slides/labels.ts +22 -19
  256. package/src/slides/layout.ts +9 -2
  257. package/src/slides/model.ts +4 -4
  258. package/src/slides/navigation.ts +43 -31
  259. package/src/slides/theme.css +5 -7
  260. package/src/slides/types.ts +9 -10
  261. package/src/status/define-statuses.ts +17 -24
  262. package/src/status/index.ts +5 -5
  263. package/src/status/status-badge.svelte +3 -1
  264. package/src/status/status-dot.svelte +1 -3
  265. package/src/status/status-icon.svelte +1 -1
  266. package/src/status/status-types.ts +21 -21
  267. package/src/tasks/board-order.ts +2 -4
  268. package/src/tasks/demo.svelte +189 -31
  269. package/src/tasks/detail/detail-demo.svelte +73 -13
  270. package/src/tasks/detail/detail-types.ts +45 -45
  271. package/src/tasks/detail/task-detail.svelte +40 -10
  272. package/src/tasks/detail/task-property.svelte +8 -2
  273. package/src/tasks/detail/task-thread.svelte +53 -12
  274. package/src/tasks/gantt/gantt-bar.svelte +17 -6
  275. package/src/tasks/gantt/gantt-grid.svelte +40 -9
  276. package/src/tasks/gantt/gantt-group-row.svelte +8 -2
  277. package/src/tasks/gantt/gantt-header.svelte +9 -3
  278. package/src/tasks/gantt/gantt-task-row.svelte +26 -6
  279. package/src/tasks/gantt/gantt.css +2 -1
  280. package/src/tasks/gantt/gantt.test.ts +3 -1
  281. package/src/tasks/gantt/logic.test.ts +110 -20
  282. package/src/tasks/gantt/logic.ts +42 -6
  283. package/src/tasks/inbox/inbox-card.svelte +13 -5
  284. package/src/tasks/inbox/inbox-draft.svelte +1 -1
  285. package/src/tasks/inbox/inbox-logic.ts +6 -40
  286. package/src/tasks/inbox/inbox-statuses.ts +23 -23
  287. package/src/tasks/inbox/inbox-trace.svelte +9 -3
  288. package/src/tasks/inbox/inbox-types.ts +54 -58
  289. package/src/tasks/inbox/index.ts +22 -21
  290. package/src/tasks/index.ts +49 -52
  291. package/src/tasks/kanban-board.svelte +16 -12
  292. package/src/tasks/kanban-types.ts +2 -3
  293. package/src/tasks/presets/sales.ts +6 -6
  294. package/src/tasks/task-gantt.svelte +28 -8
  295. package/src/tasks/task-list.svelte +41 -17
  296. package/src/tasks/time/duration.ts +40 -40
  297. package/src/tasks/time/time-chip.svelte +9 -2
  298. package/src/tasks/time/time-dot.svelte +1 -1
  299. package/src/tasks/time/time-ring.svelte +11 -3
  300. package/src/tasks/time/time-tracker.svelte +57 -15
  301. package/src/tasks/time/time-types.ts +57 -57
  302. package/src/tasks/use-kanban-dnd.svelte.ts +2 -3
  303. package/src/thread/index.ts +7 -7
  304. package/src/thread/logic.ts +36 -33
  305. package/src/thread/thread.svelte +82 -16
  306. package/src/thread/thread.test.ts +58 -47
  307. package/src/thread/types.ts +68 -68
  308. package/src/tokens/craft.css +17 -53
  309. package/src/tokens/index.css +3 -13
  310. package/src/tokens/palette.css +13 -11
  311. package/src/tokens/primitives.css +10 -23
  312. package/src/tasks/inbox/inbox.svelte +0 -203
@@ -6,57 +6,68 @@ const ada = { kind: "user", name: "Ada" } as const;
6
6
  const bot = { kind: "agent", name: "Scribe" } as const;
7
7
 
8
8
  function comment(id: string, at: string, extra: Partial<ThreadComment> = {}): ThreadComment {
9
- return { id, author: ada, body: id, createdAt: at, ...extra };
9
+ return { id, author: ada, body: id, createdAt: at, ...extra };
10
10
  }
11
11
 
12
12
  describe("buildThread", () => {
13
- it("puts replies under their root in time order", () => {
14
- const nodes = buildThread([
15
- comment("r2", "2026-10-22T10:05:00Z", { parentId: "a" }),
16
- comment("a", "2026-10-22T10:00:00Z"),
17
- comment("b", "2026-10-22T10:02:00Z", { author: bot }),
18
- comment("r1", "2026-10-22T10:03:00Z", { parentId: "a" }),
19
- ]);
20
- expect(nodes.map((n) => n.comment.id)).toEqual(["a", "b"]);
21
- expect(nodes[0].replies.map((r) => r.id)).toEqual(["r1", "r2"]);
22
- });
23
-
24
- it("keeps a reply to a reply under the root", () => {
25
- const nodes = buildThread([
26
- comment("a", "2026-10-22T10:00:00Z"),
27
- comment("r1", "2026-10-22T10:01:00Z", { parentId: "a" }),
28
- comment("r2", "2026-10-22T10:02:00Z", { parentId: "r1" }),
29
- ]);
30
- expect(nodes).toHaveLength(1);
31
- expect(nodes[0].replies.map((r) => r.id)).toEqual(["r1", "r2"]);
32
- });
33
-
34
- it("turns an orphan reply into a root and survives a parent loop", () => {
35
- const nodes = buildThread([
36
- comment("x", "2026-10-22T10:00:00Z", { parentId: "gone" }),
37
- comment("p", "2026-10-22T10:01:00Z", { parentId: "q" }),
38
- comment("q", "2026-10-22T10:02:00Z", { parentId: "p" }),
39
- ]);
40
- expect(nodes.map((n) => n.comment.id)).toContain("x");
41
- expect(nodes.flatMap((n) => [n.comment.id, ...n.replies.map((r) => r.id)]).sort()).toEqual(["p", "q", "x"]);
42
- });
43
-
44
- it("sorts a malformed timestamp last", () => {
45
- const nodes = buildThread([comment("bad", "soon"), comment("ok", "2026-10-22T10:00:00Z")]);
46
- expect(nodes.map((n) => n.comment.id)).toEqual(["ok", "bad"]);
47
- });
48
-
49
- it("counts resolved roots", () => {
50
- const nodes = buildThread([comment("a", "2026-10-22T10:00:00Z", { resolved: true }), comment("b", "2026-10-22T10:01:00Z")]);
51
- expect(resolvedCount(nodes)).toBe(1);
52
- });
13
+ it("puts replies under their root in time order", () => {
14
+ const nodes = buildThread([
15
+ comment("r2", "2026-10-22T10:05:00Z", { parentId: "a" }),
16
+ comment("a", "2026-10-22T10:00:00Z"),
17
+ comment("b", "2026-10-22T10:02:00Z", { author: bot }),
18
+ comment("r1", "2026-10-22T10:03:00Z", { parentId: "a" }),
19
+ ]);
20
+ expect(nodes.map((n) => n.comment.id)).toEqual(["a", "b"]);
21
+ expect(nodes[0].replies.map((r) => r.id)).toEqual(["r1", "r2"]);
22
+ });
23
+
24
+ it("keeps a reply to a reply under the root", () => {
25
+ const nodes = buildThread([
26
+ comment("a", "2026-10-22T10:00:00Z"),
27
+ comment("r1", "2026-10-22T10:01:00Z", { parentId: "a" }),
28
+ comment("r2", "2026-10-22T10:02:00Z", { parentId: "r1" }),
29
+ ]);
30
+ expect(nodes).toHaveLength(1);
31
+ expect(nodes[0].replies.map((r) => r.id)).toEqual(["r1", "r2"]);
32
+ });
33
+
34
+ it("turns an orphan reply into a root and survives a parent loop", () => {
35
+ const nodes = buildThread([
36
+ comment("x", "2026-10-22T10:00:00Z", { parentId: "gone" }),
37
+ comment("p", "2026-10-22T10:01:00Z", { parentId: "q" }),
38
+ comment("q", "2026-10-22T10:02:00Z", { parentId: "p" }),
39
+ ]);
40
+ expect(nodes.map((n) => n.comment.id)).toContain("x");
41
+ expect(nodes.flatMap((n) => [n.comment.id, ...n.replies.map((r) => r.id)]).sort()).toEqual([
42
+ "p",
43
+ "q",
44
+ "x",
45
+ ]);
46
+ });
47
+
48
+ it("sorts a malformed timestamp last", () => {
49
+ const nodes = buildThread([comment("bad", "soon"), comment("ok", "2026-10-22T10:00:00Z")]);
50
+ expect(nodes.map((n) => n.comment.id)).toEqual(["ok", "bad"]);
51
+ });
52
+
53
+ it("counts resolved roots", () => {
54
+ const nodes = buildThread([
55
+ comment("a", "2026-10-22T10:00:00Z", { resolved: true }),
56
+ comment("b", "2026-10-22T10:01:00Z"),
57
+ ]);
58
+ expect(resolvedCount(nodes)).toBe(1);
59
+ });
53
60
  });
54
61
 
55
62
  describe("labels", () => {
56
- it("lets a host write French over the English defaults", () => {
57
- const fr = resolveThreadLabels({ title: "Activité", send: "Commenter", showResolved: "Afficher {count} résolus" });
58
- expect(fr.title).toBe("Activité");
59
- expect(fr.cancel).toBe(DEFAULT_THREAD_LABELS.cancel);
60
- expect(fillCount(fr.showResolved, 3)).toBe("Afficher 3 résolus");
61
- });
63
+ it("lets a host write French over the English defaults", () => {
64
+ const fr = resolveThreadLabels({
65
+ title: "Activité",
66
+ send: "Commenter",
67
+ showResolved: "Afficher {count} résolus",
68
+ });
69
+ expect(fr.title).toBe("Activité");
70
+ expect(fr.cancel).toBe(DEFAULT_THREAD_LABELS.cancel);
71
+ expect(fillCount(fr.showResolved, 3)).toBe("Afficher 3 résolus");
72
+ });
62
73
  });
@@ -1,93 +1,93 @@
1
1
  export type ThreadAuthorKind = "user" | "agent";
2
2
 
3
3
  export type ThreadAuthor = {
4
- kind: ThreadAuthorKind;
5
- name: string;
6
- /** Image URL for a person. The host's own portrait goes through the `avatar` snippet. */
7
- avatar?: string;
4
+ kind: ThreadAuthorKind;
5
+ name: string;
6
+ /** Image URL for a person. The host's own portrait goes through the `avatar` snippet. */
7
+ avatar?: string;
8
8
  };
9
9
 
10
10
  /** Where on a canvas a comment is pinned: a node and an offset from it, or a point. */
11
11
  export type ThreadAnchor = {
12
- nodeId?: string;
13
- dx?: number;
14
- dy?: number;
15
- x?: number;
16
- y?: number;
12
+ nodeId?: string;
13
+ dx?: number;
14
+ dy?: number;
15
+ x?: number;
16
+ y?: number;
17
17
  };
18
18
 
19
19
  /** One comment on any subject. The host resolves the author's name. */
20
20
  export type ThreadComment = {
21
- id: string;
22
- author: ThreadAuthor;
23
- /** Plain text, shown as written. Also the fallback when `content` is missing. */
24
- body: string;
25
- /** Tiptap JSON. When set, the thread draws it through the editor's renderer instead of `body`. */
26
- content?: Record<string, unknown>;
27
- /** ISO timestamp. */
28
- createdAt: string;
29
- /** Set when the body changed after posting. */
30
- edited?: boolean;
31
- /** The comment this one answers. A reply to a reply sits under the same root. */
32
- parentId?: string | null;
33
- /** Only a root carries it: the whole conversation is resolved. */
34
- resolved?: boolean;
35
- anchor?: ThreadAnchor | null;
21
+ id: string;
22
+ author: ThreadAuthor;
23
+ /** Plain text, shown as written. Also the fallback when `content` is missing. */
24
+ body: string;
25
+ /** Tiptap JSON. When set, the thread draws it through the editor's renderer instead of `body`. */
26
+ content?: Record<string, unknown>;
27
+ /** ISO timestamp. */
28
+ createdAt: string;
29
+ /** Set when the body changed after posting. */
30
+ edited?: boolean;
31
+ /** The comment this one answers. A reply to a reply sits under the same root. */
32
+ parentId?: string | null;
33
+ /** Only a root carries it: the whole conversation is resolved. */
34
+ resolved?: boolean;
35
+ anchor?: ThreadAnchor | null;
36
36
  };
37
37
 
38
38
  export type ThreadPost = {
39
- body: string;
40
- parentId: string | null;
41
- anchor?: ThreadAnchor | null;
39
+ body: string;
40
+ parentId: string | null;
41
+ anchor?: ThreadAnchor | null;
42
42
  };
43
43
 
44
44
  /** A root comment with its replies in time order. */
45
45
  export type ThreadNode = {
46
- comment: ThreadComment;
47
- replies: ThreadComment[];
46
+ comment: ThreadComment;
47
+ replies: ThreadComment[];
48
48
  };
49
49
 
50
50
  export type ThreadLabels = {
51
- title: string;
52
- empty: string;
53
- placeholder: string;
54
- replyPlaceholder: string;
55
- send: string;
56
- sending: string;
57
- reply: string;
58
- edit: string;
59
- delete: string;
60
- edited: string;
61
- save: string;
62
- cancel: string;
63
- resolve: string;
64
- unresolve: string;
65
- resolved: string;
66
- /** Shows `{count}` resolved conversations. */
67
- showResolved: string;
68
- hideResolved: string;
69
- agent: string;
70
- pinned: string;
51
+ title: string;
52
+ empty: string;
53
+ placeholder: string;
54
+ replyPlaceholder: string;
55
+ send: string;
56
+ sending: string;
57
+ reply: string;
58
+ edit: string;
59
+ delete: string;
60
+ edited: string;
61
+ save: string;
62
+ cancel: string;
63
+ resolve: string;
64
+ unresolve: string;
65
+ resolved: string;
66
+ /** Shows `{count}` resolved conversations. */
67
+ showResolved: string;
68
+ hideResolved: string;
69
+ agent: string;
70
+ pinned: string;
71
71
  };
72
72
 
73
73
  export const DEFAULT_THREAD_LABELS: ThreadLabels = {
74
- title: "Activity",
75
- empty: "No comments yet",
76
- placeholder: "Leave a comment",
77
- replyPlaceholder: "Reply",
78
- send: "Comment",
79
- sending: "Sending",
80
- reply: "Reply",
81
- edit: "Edit comment",
82
- delete: "Delete comment",
83
- edited: "edited",
84
- save: "Save",
85
- cancel: "Cancel",
86
- resolve: "Resolve",
87
- unresolve: "Reopen",
88
- resolved: "Resolved",
89
- showResolved: "Show {count} resolved",
90
- hideResolved: "Hide resolved",
91
- agent: "AI",
92
- pinned: "Pinned",
74
+ title: "Activity",
75
+ empty: "No comments yet",
76
+ placeholder: "Leave a comment",
77
+ replyPlaceholder: "Reply",
78
+ send: "Comment",
79
+ sending: "Sending",
80
+ reply: "Reply",
81
+ edit: "Edit comment",
82
+ delete: "Delete comment",
83
+ edited: "edited",
84
+ save: "Save",
85
+ cancel: "Cancel",
86
+ resolve: "Resolve",
87
+ unresolve: "Reopen",
88
+ resolved: "Resolved",
89
+ showResolved: "Show {count} resolved",
90
+ hideResolved: "Hide resolved",
91
+ agent: "AI",
92
+ pinned: "Pinned",
93
93
  };
@@ -1,8 +1,6 @@
1
1
  /* ── Radius, by role not by size ─────────────────────────────
2
- A chip inside a control inside a card has to read as nested, which needs
3
- three radii that step, not one radius reused. The Tailwind names are the
4
- home so `rounded-lg` on a button already resolves to the control radius and
5
- no component has to learn a new class. */
2
+ These override Tailwind's own names, so `rounded-lg` on a button already
3
+ resolves to the control radius. */
6
4
  @theme {
7
5
  --radius-xs: 4px; /* tag, dot, inline code */
8
6
  --radius-sm: 6px; /* chip, badge, small pill */
@@ -14,30 +12,9 @@
14
12
  }
15
13
 
16
14
  /* ── Elevation ───────────────────────────────────────────────
17
- Four steps of lift, plus the two edges that are not lift.
18
-
19
- WHY the steps are shadow-only and the hairline is its own token: most boxes
20
- in a kit already draw a border, and an elevation that bakes a `0 0 0 1px`
21
- ring into every step gives those boxes two edges a pixel apart. Compose them
22
- where a component drops its border:
23
-
24
- box-shadow: var(--elev-hairline), var(--elev-card);
25
-
26
- WHY each step is stacked rather than one blurred drop: a single shadow reads
27
- as a sticker. Depth is several offsets falling off together, which is what
28
- the eye reads as distance. They cost nothing extra because every stop is the
29
- same colour at a different alpha.
30
-
31
- WHY the blur radii are large and the alphas small: a 4px blur at 20% draws a
32
- visible grey edge, which the eye reads as a second border. A 28px blur at 7%
33
- is barely a colour anywhere and reads as height. Reach for more blur before
34
- more alpha, every time.
35
-
36
- `--elev-tint` is the one knob an app turns. Pure black greys a warm palette
37
- out; the hue of the app's own darkest surface keeps the shadow in the
38
- family. */
15
+ The steps carry no ring, since a bordered box would get two edges a pixel
16
+ apart; a borderless one adds `var(--elev-hairline)` in front. */
39
17
  :root {
40
-
41
18
  /* Resting on the page. A button, a chip, a row. */
42
19
  --elev-btn:
43
20
  0 1px 1px -0.5px color-mix(in oklab, var(--elev-tint) 7%, transparent),
@@ -61,9 +38,8 @@
61
38
  0 12px 26px -8px color-mix(in oklab, var(--elev-tint) 12%, transparent),
62
39
  0 40px 72px -20px color-mix(in oklab, var(--elev-tint) 20%, transparent);
63
40
 
64
- /* The two edges. A hairline separates a surface from what is behind it; a rim
65
- is the light catching its top edge, which is what makes a filled control
66
- read as a physical key rather than a coloured rectangle. */
41
+ /* A hairline separates a surface from what's behind it; a rim is light on the
42
+ top edge, which makes a filled control read as a key. */
67
43
  --elev-hairline: 0 0 0 1px var(--color-border, var(--border));
68
44
  --elev-rim: inset 0 1px 0 color-mix(in oklab, white 16%, transparent);
69
45
 
@@ -71,10 +47,8 @@
71
47
  --elev-field: inset 0 1px 2px color-mix(in oklab, var(--elev-tint) 7%, transparent);
72
48
  }
73
49
 
74
- /* A dark surface cannot cast a darker shadow than the page it sits on, so the
75
- hairline does the separating and the shadow only sinks what is behind it. On
76
- dark the hairline is white at a low alpha, which is what reads as a lit edge
77
- rather than as a drawn border. */
50
+ /* A shadow can't get darker than a dark page, so on dark the low-alpha white
51
+ hairline does the separating. */
78
52
  .dark {
79
53
  --elev-btn: 0 1px 2px color-mix(in oklab, black 30%, transparent);
80
54
  --elev-card:
@@ -93,25 +67,19 @@
93
67
  }
94
68
 
95
69
  /* ── Focus ───────────────────────────────────────────────────
96
- One ring, two parts: a solid edge that redraws the control's own outline in
97
- the accent, and a soft halo that carries it off the surface. Written as a
98
- variable so a component spends one line on it and every control focuses the
99
- same way. */
70
+ One variable, so every control focuses the same way: a solid accent edge,
71
+ then a soft halo. */
100
72
  :root {
101
73
  --ring-color: var(--color-ring, var(--ring));
102
74
  --ring-halo: color-mix(in oklab, var(--ring-color) 16%, transparent);
103
- /* A solid pixel redrawing the control's own edge, then a wide soft band. At a
104
- higher alpha the band reads as a second border rather than as a glow, which
105
- is why it is 16% over 4px and not 30% over 2px. */
75
+ /* The halo is 16% over 4px, not 30% over 2px, because a stronger band reads
76
+ as a second border instead of a glow. */
106
77
  --ring-focus: 0 0 0 1px var(--ring-color), 0 0 0 5px var(--ring-halo);
107
78
  }
108
79
 
109
80
  /* ── Motion ──────────────────────────────────────────────────
110
- The transitions-dev scale, verbatim, so a snippet pasted from that skill and
111
- a component written here read from the same names.
112
-
113
- The doctrine in one line: an open is an invitation and a close should get out
114
- of the way, so every close is shorter than its open and no close bounces. */
81
+ The transitions-dev scale, verbatim, so a pasted snippet reads the same
82
+ names. Every close is shorter than its open, and no close bounces. */
115
83
  :root {
116
84
  --duration-stagger: 40ms;
117
85
  --duration-micro: 80ms;
@@ -163,13 +131,9 @@
163
131
 
164
132
  --ease-smooth-out: var(--ease-smooth-out);
165
133
 
166
- /* WHY the durations are re-declared under a second name: Tailwind's
167
- `duration-*` utility reads `--transition-duration-*`, not `--duration-*`, so
168
- the six names above generated no class at all. `duration-fast` therefore
169
- compiled to nothing and the element ended up at `transition-duration: 0s`:
170
- no error, no warning, and a component that simply does not animate. The
171
- comment at the top of this block had claimed `duration-fast` was a class
172
- since the block was written. Found 31 Aug 2026 by probing a live page. */
134
+ /* Tailwind's `duration-*` reads `--transition-duration-*`, so without these
135
+ `duration-fast` silently compiles to nothing and the element doesn't animate
136
+ (31 Aug 2026). */
173
137
  --transition-duration-micro: var(--duration-micro);
174
138
  --transition-duration-quick: var(--duration-quick);
175
139
  --transition-duration-fast: var(--duration-fast);
@@ -1,16 +1,6 @@
1
- /* tokens.css
2
- *
3
- * The shared style layer for @heyramzi/ui, imported once after Tailwind:
4
- *
5
- * @import "tailwindcss";
6
- * @import "@heyramzi/ui/tokens.css";
7
- * @import "@heyramzi/ui/animations";
8
- * @import "@heyramzi/ui/shadcn/utilities.css"; // only if you use Attachment or Message
9
- *
10
- * It carries three things: the color palette and semantic tokens, the craft
11
- * layer (radius, elevation, focus and motion), and the small set of motion
12
- * primitives the components share. Override any variable on :root to retheme.
13
- * The parts load in this order because later blocks read earlier variables.
1
+ /* tokens.css: import once, right after `@import "tailwindcss"`. Override any
2
+ * variable on :root to retheme; the parts load in this order because later ones
3
+ * read earlier variables.
14
4
  */
15
5
 
16
6
  @import "./palette.css";
@@ -22,9 +22,8 @@
22
22
  --gray-900: oklch(0.15 0 0); /* Near-black — headings, primary text */
23
23
 
24
24
  /* === Brand roles ===
25
- The look of a product, named by what each colour does. These defaults are neutral, and a
26
- brand replaces them through `@heyramzi/ui/brand` (typed colours, no build step). Every
27
- other token below reads from these, so nothing else in the package names a hue. */
25
+ Neutral defaults a brand replaces through `@heyramzi/ui/brand`; every token below reads
26
+ from these, so nothing else in the package names a hue. */
28
27
  --brand: oklch(0.28 0 0); /* The signature colour: focus ring, links, active states, the mark */
29
28
  --brand-on-dark: oklch(0.92 0 0); /* The same colour lifted for a dark page */
30
29
  --brand-highlight: oklch(0.78 0 0); /* Warmth: badges, highlights, soft accents */
@@ -39,9 +38,9 @@
39
38
  --brand-surface-light: oklch(0.99 0 0); /* Cards on a light page */
40
39
  --brand-line-light: oklch(0.91 0 0); /* Borders on a light page */
41
40
 
42
- /* === Tint scales, derived from the three hues so a brand types one colour each ===
43
- WHY mixes and not typed steps: a brand that sets `--brand` gets a matching 100 to 800
44
- with nothing else to maintain. 400 is the colour lifted for dark, 600 the colour itself. */
41
+ /* === Tint scales ===
42
+ Mixed from the three hues so `--brand` alone yields 100 to 800; 400 is the colour lifted
43
+ for dark, 600 the colour itself. */
45
44
  --brand-100: color-mix(in oklab, var(--brand) 12%, white);
46
45
  --brand-200: color-mix(in oklab, var(--brand) 28%, white);
47
46
  --brand-400: var(--brand-on-dark);
@@ -117,8 +116,7 @@
117
116
  --sidebar-ring: var(--brand);
118
117
 
119
118
  /* === Shadow tint ===
120
- The one knob the design layer exposes. Pure black greys out warm surfaces,
121
- so the shadow takes the darkest surface's hue. Radius, focus and motion live there. */
119
+ Pure black greys out warm surfaces, so the shadow takes the darkest surface's hue. */
122
120
  --elev-tint: var(--brand-ink);
123
121
  }
124
122
 
@@ -198,14 +196,18 @@
198
196
  --color-cat-indigo: var(--cat-indigo);
199
197
  --color-cat-purple: var(--cat-purple);
200
198
 
201
- /* A hue as text or icon on its own tint: 40% toward the foreground clears AA in both themes.
202
- A tone's 10% tint under plain destructive text is 3.6:1; the ink mix is not. */
199
+ /* A hue as text on its own tint mixes 40% toward the foreground to clear AA in both themes,
200
+ where plain destructive text on that tint is 3.6:1. */
203
201
  --color-cat-cyan-ink: color-mix(in oklab, var(--color-cat-cyan) 60%, var(--color-foreground));
204
202
  --color-cat-teal-ink: color-mix(in oklab, var(--color-cat-teal) 60%, var(--color-foreground));
205
203
  --color-cat-purple-ink: color-mix(in oklab, var(--color-cat-purple) 60%, var(--color-foreground));
206
204
  --color-cat-amber-ink: color-mix(in oklab, var(--color-cat-amber) 60%, var(--color-foreground));
207
205
  --color-success-ink: color-mix(in oklab, var(--color-success) 60%, var(--color-foreground));
208
- --color-destructive-ink: color-mix(in oklab, var(--color-destructive) 60%, var(--color-foreground));
206
+ --color-destructive-ink: color-mix(
207
+ in oklab,
208
+ var(--color-destructive) 60%,
209
+ var(--color-foreground)
210
+ );
209
211
 
210
212
  /* Light theme color system: the light surfaces and lines of the brand roles */
211
213
  --color-background: var(--gray-50);
@@ -1,11 +1,5 @@
1
- /* The bits-ui state variants every primitive in `./shadcn` is written against.
2
- *
3
- * bits-ui v2 writes `data-checked` / `data-open` and v1 wrote
4
- * `data-state="checked"`, so each variant matches both and a component does not
5
- * have to know which it is talking to. They live here rather than in a
6
- * consumer's stylesheet because the classes that need them ship in this
7
- * package: an app that defines only the palette would render a switch that
8
- * never moves. */
1
+ /* Each variant matches both bits-ui v2's `data-checked` and v1's `data-state="checked"`, and
2
+ * ships here because an app that defines only the palette gets a switch that never moves. */
9
3
  @custom-variant data-open {
10
4
  &:where([data-state="open"]),
11
5
  &:where([data-open]:not([data-open="false"])) {
@@ -80,9 +74,8 @@
80
74
  background: color-mix(in oklab, var(--ring-color) 24%, transparent);
81
75
  }
82
76
 
83
- /* A default macOS overlay scrollbar paints a black bar on a dark panel. Track
84
- transparent, thumb on the line token, so it belongs to whatever surface it
85
- is over. The border is what insets the thumb without a second element. */
77
+ /* A default macOS overlay scrollbar paints a black bar on a dark panel, so the track stays
78
+ transparent and the thumb takes the line token. */
86
79
  :where(html) {
87
80
  scrollbar-color: var(--color-border, var(--border)) transparent;
88
81
  }
@@ -111,16 +104,11 @@
111
104
  }
112
105
 
113
106
  @layer components {
114
- /* Three moves the kit repeats. They are classes rather than tokens because a
115
- component wants one name, not four declarations, and because the reduced
116
- motion guard then lives in one place instead of in every component.
107
+ /* Classes, not tokens, so a component takes one name and the reduced-motion guard lives in
108
+ one place; the `t-` prefix matches the transitions-dev snippets. */
117
109
 
118
- Namespaced `t-` to match the transitions-dev snippets, which these sit
119
- beside rather than duplicate. */
120
-
121
- /* A control that reads as a key: it travels down a pixel under the finger.
122
- The travel is what sells it, because a colour change alone reads as a hover
123
- state, not as a press. */
110
+ /* A key-like control travels down a pixel under the finger, because a colour change alone
111
+ reads as a hover, not a press. */
124
112
  .t-press {
125
113
  transition:
126
114
  background-color var(--duration-quick) var(--ease-smooth-out),
@@ -134,9 +122,8 @@
134
122
  transform: translateY(1px);
135
123
  }
136
124
 
137
- /* A card that answers the pointer. It rises by a pixel and gains the next
138
- step of elevation, so the hover reads as the card coming forward rather
139
- than as its border changing colour. */
125
+ /* A hover card rises a pixel and gains the next elevation step, so it reads as coming
126
+ forward, not as its border changing colour. */
140
127
  .t-lift {
141
128
  transition:
142
129
  box-shadow var(--duration-fast) var(--ease-smooth-out),