@heyramzi/ui 1.54.2 → 2.0.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 (361) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +198 -6
  4. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  5. package/src/ambient.d.ts +8 -0
  6. package/src/brand/brand.test.ts +67 -0
  7. package/src/brand/index.ts +137 -0
  8. package/src/canvas/README.md +236 -0
  9. package/src/canvas/card/card-frame.svelte +61 -0
  10. package/src/canvas/card/card.css +39 -0
  11. package/src/canvas/card/resize-handles.svelte +119 -0
  12. package/src/canvas/card/resize.ts +33 -0
  13. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  14. package/src/canvas/chrome/chrome.css +231 -0
  15. package/src/canvas/chrome/lock-badge.svelte +18 -0
  16. package/src/canvas/chrome/side-handles.svelte +30 -0
  17. package/src/canvas/chrome/size-chip.svelte +45 -0
  18. package/src/canvas/chrome/snap-guides.svelte +69 -0
  19. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  20. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  21. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  22. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  23. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  24. package/src/canvas/edges/bend-handle.svelte +76 -0
  25. package/src/canvas/edges/edge-gradient.svelte +30 -0
  26. package/src/canvas/edges/edge-path.test.ts +58 -0
  27. package/src/canvas/edges/edge-path.ts +293 -0
  28. package/src/canvas/edges/gradient-edge.svelte +43 -0
  29. package/src/canvas/editor/context-menu.test.ts +65 -0
  30. package/src/canvas/editor/context-menu.ts +48 -0
  31. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  32. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  33. package/src/canvas/editor/editor-context.ts +62 -0
  34. package/src/canvas/editor/editor-mode.ts +67 -0
  35. package/src/canvas/editor/frame.ts +98 -0
  36. package/src/canvas/editor/hand-context.ts +15 -0
  37. package/src/canvas/editor/history-core.test.ts +239 -0
  38. package/src/canvas/editor/history-core.ts +247 -0
  39. package/src/canvas/editor/keymap.test.ts +161 -0
  40. package/src/canvas/editor/keymap.ts +151 -0
  41. package/src/canvas/editor/layout-actions.test.ts +156 -0
  42. package/src/canvas/editor/layout-actions.ts +219 -0
  43. package/src/canvas/editor/lock.test.ts +68 -0
  44. package/src/canvas/editor/lock.ts +87 -0
  45. package/src/canvas/editor/rebase.test.ts +208 -0
  46. package/src/canvas/editor/rebase.ts +272 -0
  47. package/src/canvas/editor/style.ts +184 -0
  48. package/src/canvas/editor/text-field-context.ts +41 -0
  49. package/src/canvas/editor/z-order.test.ts +75 -0
  50. package/src/canvas/editor/z-order.ts +87 -0
  51. package/src/canvas/headless/alignment.test.ts +59 -0
  52. package/src/canvas/headless/alignment.ts +23 -0
  53. package/src/canvas/headless/break-apart-find.ts +418 -0
  54. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  55. package/src/canvas/headless/break-apart-plan.ts +439 -0
  56. package/src/canvas/headless/break-apart-style.ts +106 -0
  57. package/src/canvas/headless/break-apart.ts +244 -0
  58. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  59. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  60. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  61. package/src/canvas/headless/edge-anchors.ts +143 -0
  62. package/src/canvas/headless/edge-memo.test.ts +58 -0
  63. package/src/canvas/headless/edge-memo.ts +32 -0
  64. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  65. package/src/canvas/headless/edge-serialization.ts +32 -0
  66. package/src/canvas/headless/figure-geometry.ts +293 -0
  67. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  68. package/src/canvas/headless/fit-viewport.ts +137 -0
  69. package/src/canvas/headless/grid.test.ts +34 -0
  70. package/src/canvas/headless/grid.ts +94 -0
  71. package/src/canvas/headless/icon-node.test.ts +23 -0
  72. package/src/canvas/headless/icon-node.ts +66 -0
  73. package/src/canvas/headless/index.ts +9 -0
  74. package/src/canvas/headless/linkify.test.ts +23 -0
  75. package/src/canvas/headless/linkify.ts +25 -0
  76. package/src/canvas/headless/pen-marks.ts +133 -0
  77. package/src/canvas/headless/sketch.test.ts +99 -0
  78. package/src/canvas/headless/sketch.ts +302 -0
  79. package/src/canvas/headless/snap-guides.test.ts +61 -0
  80. package/src/canvas/headless/snap-guides.ts +146 -0
  81. package/src/canvas/headless/svg-figure.test.ts +49 -0
  82. package/src/canvas/headless/svg-figure.ts +176 -0
  83. package/src/canvas/headless/table-layout.test.ts +158 -0
  84. package/src/canvas/headless/table-layout.ts +309 -0
  85. package/src/canvas/index.ts +35 -0
  86. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  87. package/src/canvas/lod/lod-switch.svelte +140 -0
  88. package/src/canvas/lod/render-all.svelte.ts +52 -0
  89. package/src/canvas/ops/arrange.ts +108 -0
  90. package/src/canvas/ops/ops.test.ts +98 -0
  91. package/src/canvas/ops/ops.ts +347 -0
  92. package/src/canvas/pen/PenField.svelte +128 -0
  93. package/src/canvas/pen/PenField.test.ts +87 -0
  94. package/src/canvas/pen/PenOverlay.svelte +172 -0
  95. package/src/canvas/pen/ShapeNode.svelte +64 -0
  96. package/src/canvas/pen/TextNode.svelte +97 -0
  97. package/src/canvas/pen/colors.ts +28 -0
  98. package/src/canvas/pen/eraser.ts +46 -0
  99. package/src/canvas/pen/geometry.ts +148 -0
  100. package/src/canvas/pen/host-field.test.svelte +13 -0
  101. package/src/canvas/pen/index.ts +40 -0
  102. package/src/canvas/pen/ops.ts +16 -0
  103. package/src/canvas/pen/pen-session.ts +177 -0
  104. package/src/canvas/pen/pen.css +149 -0
  105. package/src/canvas/pen/plugin.ts +139 -0
  106. package/src/canvas/pen/shapes.ts +104 -0
  107. package/src/canvas/pen/stroke-path.ts +54 -0
  108. package/src/canvas/pen/types.ts +96 -0
  109. package/src/canvas/pen/ui.ts +26 -0
  110. package/src/canvas/plugin.test.ts +45 -0
  111. package/src/canvas/plugin.ts +159 -0
  112. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  113. package/src/canvas/screen/PartView.svelte +176 -0
  114. package/src/canvas/screen/ScreenBody.svelte +142 -0
  115. package/src/canvas/screen/ScreenCard.svelte +32 -0
  116. package/src/canvas/screen/glyphs/button.svg +1 -0
  117. package/src/canvas/screen/glyphs/close.svg +1 -0
  118. package/src/canvas/screen/glyphs/down.svg +1 -0
  119. package/src/canvas/screen/glyphs/header.svg +1 -0
  120. package/src/canvas/screen/glyphs/hills.svg +1 -0
  121. package/src/canvas/screen/glyphs/image.svg +1 -0
  122. package/src/canvas/screen/glyphs/input.svg +1 -0
  123. package/src/canvas/screen/glyphs/list.svg +1 -0
  124. package/src/canvas/screen/glyphs/modal.svg +1 -0
  125. package/src/canvas/screen/glyphs/plus.svg +1 -0
  126. package/src/canvas/screen/glyphs/up.svg +1 -0
  127. package/src/canvas/screen/glyphs.ts +40 -0
  128. package/src/canvas/screen/labels.ts +41 -0
  129. package/src/canvas/screen/parts.ts +164 -0
  130. package/src/canvas/screen/plugin.ts +67 -0
  131. package/src/canvas/screen/screen.css +593 -0
  132. package/src/canvas/screen/types.ts +47 -0
  133. package/src/canvas/screen/ui.ts +13 -0
  134. package/src/canvas/test-utils.ts +18 -0
  135. package/src/canvas/types.ts +53 -0
  136. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  137. package/src/canvas/ui/FrameLayer.svelte +116 -0
  138. package/src/canvas/ui/PenLayer.svelte +39 -0
  139. package/src/canvas/ui/StylePanel.svelte +268 -0
  140. package/src/canvas/ui/canvas-grid.svelte +156 -0
  141. package/src/canvas/ui/canvas-ui.css +350 -0
  142. package/src/canvas/ui/forward-wheel.ts +29 -0
  143. package/src/canvas/ui/icons.ts +124 -0
  144. package/src/canvas/ui/place-panel.ts +80 -0
  145. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  146. package/src/canvas/ui/shortcut-tip.ts +17 -0
  147. package/src/canvas/ui/tools.ts +146 -0
  148. package/src/charts/README.md +104 -0
  149. package/src/charts/ambient.d.ts +4 -0
  150. package/src/charts/animated-value.css +7 -0
  151. package/src/charts/animated-value.svelte +110 -0
  152. package/src/charts/area-chart.css +29 -0
  153. package/src/charts/area-chart.svelte +204 -0
  154. package/src/charts/bar-chart.css +119 -0
  155. package/src/charts/bar-chart.svelte +275 -0
  156. package/src/charts/charts.css +201 -0
  157. package/src/charts/crosshair.svelte +35 -0
  158. package/src/charts/dashboard/chart.ts +5 -0
  159. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  160. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  161. package/src/charts/dashboard/data-panel.svelte +81 -0
  162. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  163. package/src/charts/dashboard/data-table.svelte +278 -0
  164. package/src/charts/dashboard/data-table.ts +26 -0
  165. package/src/charts/dashboard/index.ts +25 -0
  166. package/src/charts/dashboard/panel-grid.svelte +43 -0
  167. package/src/charts/dashboard/period-filter.svelte +53 -0
  168. package/src/charts/dashboard/rating-chip.svelte +35 -0
  169. package/src/charts/dashboard/stat-band.svelte +47 -0
  170. package/src/charts/dashboard/stat-tile.svelte +176 -0
  171. package/src/charts/dashboard/windows.ts +8 -0
  172. package/src/charts/donut-chart.css +56 -0
  173. package/src/charts/donut-chart.svelte +158 -0
  174. package/src/charts/edge-fade.svelte +43 -0
  175. package/src/charts/edge-fade.ts +16 -0
  176. package/src/charts/figure.css +100 -0
  177. package/src/charts/figure.svelte +122 -0
  178. package/src/charts/gauge-chart.css +94 -0
  179. package/src/charts/gauge-chart.svelte +302 -0
  180. package/src/charts/hover-band.test.ts +30 -0
  181. package/src/charts/hover-band.ts +21 -0
  182. package/src/charts/hover-focus.svelte +85 -0
  183. package/src/charts/index.ts +31 -0
  184. package/src/charts/legend.css +136 -0
  185. package/src/charts/legend.svelte +112 -0
  186. package/src/charts/line-chart.css +26 -0
  187. package/src/charts/line-chart.svelte +256 -0
  188. package/src/charts/motion.ts +19 -0
  189. package/src/charts/number-chart.css +179 -0
  190. package/src/charts/number-chart.svelte +187 -0
  191. package/src/charts/primitives/chart-container.svelte +57 -0
  192. package/src/charts/primitives/chart-style.svelte +36 -0
  193. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  194. package/src/charts/primitives/chart-utils.ts +54 -0
  195. package/src/charts/primitives/index.ts +11 -0
  196. package/src/charts/reference-line.css +18 -0
  197. package/src/charts/reference-line.svelte +31 -0
  198. package/src/charts/reveal.svelte +61 -0
  199. package/src/charts/ring-chart.css +104 -0
  200. package/src/charts/ring-chart.svelte +166 -0
  201. package/src/charts/sparkline.css +45 -0
  202. package/src/charts/sparkline.svelte +141 -0
  203. package/src/charts/stat-card.css +105 -0
  204. package/src/charts/stat-card.svelte +267 -0
  205. package/src/charts/time-series-chart.css +28 -0
  206. package/src/charts/time-series-chart.svelte +179 -0
  207. package/src/charts/trend-pill.css +38 -0
  208. package/src/charts/trend-pill.svelte +48 -0
  209. package/src/components/copy/copy-button.svelte +1 -1
  210. package/src/components/docs/doc-page.svelte +1 -1
  211. package/src/components/docs/on-this-page.svelte +2 -2
  212. package/src/components/list-page/empty-state.svelte +1 -1
  213. package/src/components/list-page/filter-popover.svelte +1 -1
  214. package/src/components/list-page/section-label.svelte +1 -1
  215. package/src/components/segmented/segment.svelte +4 -4
  216. package/src/components/theme/theme-provider.svelte +6 -4
  217. package/src/components/toolbar/index.ts +5 -0
  218. package/src/components/toolbar/toolbar.svelte +168 -0
  219. package/src/components/toolbar/toolbar.test.ts +66 -0
  220. package/src/editor/README.md +58 -0
  221. package/src/editor/ambient.d.ts +1 -0
  222. package/src/editor/block-id.ts +77 -0
  223. package/src/editor/block-ops.test.ts +237 -0
  224. package/src/editor/block-ops.ts +157 -0
  225. package/src/editor/content.css +418 -0
  226. package/src/editor/editor-toolbar.svelte +303 -0
  227. package/src/editor/editor.svelte +462 -0
  228. package/src/editor/extensions.ts +145 -0
  229. package/src/editor/image-toolbar.svelte +123 -0
  230. package/src/editor/index.ts +17 -0
  231. package/src/editor/inline-editor.svelte +261 -0
  232. package/src/editor/markdown-escaping.test.ts +158 -0
  233. package/src/editor/markdown-marks.test.ts +89 -0
  234. package/src/editor/markdown-test-helpers.ts +13 -0
  235. package/src/editor/markdown-youtube.test.ts +93 -0
  236. package/src/editor/markdown.test.ts +250 -0
  237. package/src/editor/markdown.ts +511 -0
  238. package/src/editor/renderer.svelte +92 -0
  239. package/src/editor/serializer.test.ts +133 -0
  240. package/src/editor/serializer.ts +370 -0
  241. package/src/editor/slash-items.ts +115 -0
  242. package/src/editor/slash-menu.svelte +159 -0
  243. package/src/editor/youtube-url.ts +35 -0
  244. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  245. package/src/lib/result.ts +19 -0
  246. package/src/onboarding/README.md +120 -0
  247. package/src/onboarding/define-tours.test.ts +50 -0
  248. package/src/onboarding/define-tours.ts +31 -0
  249. package/src/onboarding/demo.svelte +86 -0
  250. package/src/onboarding/index.ts +12 -0
  251. package/src/onboarding/onboarding-provider.svelte +120 -0
  252. package/src/onboarding/onboarding.svelte.ts +24 -0
  253. package/src/onboarding/pick.test.ts +79 -0
  254. package/src/onboarding/pick.ts +36 -0
  255. package/src/onboarding/stores.test.ts +103 -0
  256. package/src/onboarding/stores.ts +58 -0
  257. package/src/onboarding/tour-card.svelte +70 -0
  258. package/src/onboarding/tour-runner.svelte +197 -0
  259. package/src/onboarding/types.ts +34 -0
  260. package/src/onboarding/wait.ts +29 -0
  261. package/src/shadcn/command/command-input.svelte +1 -1
  262. package/src/slides/Controls.svelte +138 -0
  263. package/src/slides/Deck.svelte +271 -0
  264. package/src/slides/Fragment.svelte +71 -0
  265. package/src/slides/Overview.svelte +113 -0
  266. package/src/slides/Presenter.svelte +292 -0
  267. package/src/slides/README.md +89 -0
  268. package/src/slides/Slide.svelte +179 -0
  269. package/src/slides/SlideList.svelte +19 -0
  270. package/src/slides/ambient.d.ts +1 -0
  271. package/src/slides/bridges.ts +69 -0
  272. package/src/slides/deck-controller.svelte.ts +292 -0
  273. package/src/slides/demo-slides.svelte +358 -0
  274. package/src/slides/demo.svelte +176 -0
  275. package/src/slides/export/capture.ts +113 -0
  276. package/src/slides/export/ffmpeg.ts +120 -0
  277. package/src/slides/export/index.ts +9 -0
  278. package/src/slides/export/launch.ts +10 -0
  279. package/src/slides/export/pdf.ts +152 -0
  280. package/src/slides/export/types.ts +48 -0
  281. package/src/slides/index.ts +19 -0
  282. package/src/slides/labels.ts +48 -0
  283. package/src/slides/layout.ts +51 -0
  284. package/src/slides/model.ts +47 -0
  285. package/src/slides/navigation.ts +100 -0
  286. package/src/slides/theme.css +205 -0
  287. package/src/slides/types.ts +86 -0
  288. package/src/tasks/README.md +312 -0
  289. package/src/tasks/ambient.d.ts +3 -0
  290. package/src/tasks/board-order.ts +137 -0
  291. package/src/tasks/dates.ts +31 -0
  292. package/src/tasks/define-statuses.ts +32 -0
  293. package/src/tasks/demo.svelte +389 -0
  294. package/src/tasks/detail/detail-demo.svelte +186 -0
  295. package/src/tasks/detail/detail-types.ts +54 -0
  296. package/src/tasks/detail/task-chip.svelte +28 -0
  297. package/src/tasks/detail/task-detail.svelte +315 -0
  298. package/src/tasks/detail/task-property.svelte +17 -0
  299. package/src/tasks/detail/task-thread.svelte +258 -0
  300. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  301. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  302. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  303. package/src/tasks/gantt/gantt-header.svelte +71 -0
  304. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  305. package/src/tasks/gantt/gantt.css +22 -0
  306. package/src/tasks/gantt/gantt.test.ts +77 -0
  307. package/src/tasks/gantt/logic.test.ts +163 -0
  308. package/src/tasks/gantt/logic.ts +248 -0
  309. package/src/tasks/inbox/inbox-card.svelte +283 -0
  310. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  311. package/src/tasks/inbox/inbox-logic.ts +55 -0
  312. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  313. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  314. package/src/tasks/inbox/inbox-types.ts +108 -0
  315. package/src/tasks/inbox/inbox.svelte +203 -0
  316. package/src/tasks/inbox/index.ts +23 -0
  317. package/src/tasks/index.ts +85 -0
  318. package/src/tasks/kanban-board.svelte +561 -0
  319. package/src/tasks/kanban-column.svelte +117 -0
  320. package/src/tasks/kanban-drag.ts +38 -0
  321. package/src/tasks/kanban-types.ts +65 -0
  322. package/src/tasks/presets/sales.ts +16 -0
  323. package/src/tasks/record/demo-data.ts +286 -0
  324. package/src/tasks/record/field-section.svelte +53 -0
  325. package/src/tasks/record/fields.ts +142 -0
  326. package/src/tasks/record/format.ts +87 -0
  327. package/src/tasks/record/gutter.ts +2 -0
  328. package/src/tasks/record/index.ts +54 -0
  329. package/src/tasks/record/info-list.svelte +316 -0
  330. package/src/tasks/record/kinds.ts +42 -0
  331. package/src/tasks/record/logic.ts +324 -0
  332. package/src/tasks/record/next-up.svelte +70 -0
  333. package/src/tasks/record/project-list.svelte +133 -0
  334. package/src/tasks/record/record-demo.svelte +324 -0
  335. package/src/tasks/record/record-header.svelte +70 -0
  336. package/src/tasks/record/record-shell.svelte +138 -0
  337. package/src/tasks/record/record-stats.svelte +48 -0
  338. package/src/tasks/record/record-timeline.svelte +231 -0
  339. package/src/tasks/record/record.test.ts +350 -0
  340. package/src/tasks/record/steps-rail.svelte +474 -0
  341. package/src/tasks/record/types.ts +280 -0
  342. package/src/tasks/record/where-we-are.svelte +121 -0
  343. package/src/tasks/reorder.ts +19 -0
  344. package/src/tasks/side-panel-types.ts +16 -0
  345. package/src/tasks/side-panel.svelte +193 -0
  346. package/src/tasks/status-badge.svelte +63 -0
  347. package/src/tasks/status-dot.svelte +228 -0
  348. package/src/tasks/status-icon.svelte +30 -0
  349. package/src/tasks/status-types.ts +40 -0
  350. package/src/tasks/task-gantt.svelte +230 -0
  351. package/src/tasks/task-list.svelte +470 -0
  352. package/src/tasks/task-types.ts +33 -0
  353. package/src/tasks/time/duration.ts +69 -0
  354. package/src/tasks/time/time-chip.svelte +62 -0
  355. package/src/tasks/time/time-dot.svelte +58 -0
  356. package/src/tasks/time/time-ring.svelte +74 -0
  357. package/src/tasks/time/time-tracker.svelte +565 -0
  358. package/src/tasks/time/time-types.ts +74 -0
  359. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  360. package/src/tokens/base.css +5 -5
  361. package/src/tokens/palette.css +115 -120
package/COMPONENTS.md CHANGED
@@ -67,6 +67,7 @@ Generic pieces built on the primitives, imported from `@heyramzi/ui/components/<
67
67
  - `icon` — Icon, a Phosphor component at a size
68
68
  - `language-flag` — LanguageFlag
69
69
  - `list-page` — ListPageShell, MasterDetail, GroupedList, RailRow and the rest of the master-detail page kit, NoticeBar, EmptyState, FilterPopover, setSectionHost
70
+ - `toolbar` — Toolbar: one row at any width; search, tabs and the view switch fold into menus in a narrow pane
70
71
  - `locale-switch` — LocaleSwitch, a language switch on SegmentedFilter
71
72
  - `mark` — Mark, a logo or photo glyph with an initial fallback
72
73
  - `open-in` — OpenIn, a link out to a tool that takes the tool's name, label and mark as a prop
@@ -134,6 +135,6 @@ Generic pieces built on the primitives, imported from `@heyramzi/ui/components/<
134
135
 
135
136
  ---
136
137
 
137
- Skipped on purpose: brand assets (logos, marks, brand renderer), kanban, list, gantt and status (they live in `@heyramzi/tasks`), and charts
138
- (they live in `@heyramzi/charts`). The bot avatars
138
+ Skipped on purpose: brand assets (logos, marks, brand renderer), kanban, list, gantt and status (they live in `@heyramzi/ui/tasks`), and charts
139
+ (they live in `@heyramzi/ui/charts`). The bot avatars
139
140
  are in `src/agents`, imported from `@heyramzi/ui/agents`.
package/README.md CHANGED
@@ -33,21 +33,46 @@ Import the style layer once, after Tailwind:
33
33
  @import "@heyramzi/ui/animations";
34
34
  ```
35
35
 
36
- `tokens.css` carries the palette, the semantic tokens and the craft layer
36
+ `tokens.css` carries a neutral palette, the semantic tokens and the craft layer
37
37
  (radius, elevation, focus, motion). `animations` carries the keyframes the
38
38
  components reference. If you use the Attachment or Message component, also
39
39
  import `@heyramzi/ui/shadcn/utilities.css`.
40
40
 
41
- Retheme by overriding any variable on `:root`, for example
42
- `--color-brand-indigo` or `--radius-lg`. Dark mode keys off a `.dark`
43
- class on `html`.
41
+ ## A brand
44
42
 
45
- Rich text lives in [`@heyramzi/rich-editor`](https://github.com/heyramzi/ui/tree/main/packages/rich-editor),
46
- so the kit doesn't pull Tiptap into every app.
43
+ The default look is neutral, and its colours are named by role: `brand`, `ink`, `surface`,
44
+ `line`, `raised`, `paper`, `highlight` and `signal` (`bg-brand`, `text-ink`, `border-line`).
45
+ A client's look is one typed file with no build step:
47
46
 
48
- Charts live in their own package now,
49
- [`@heyramzi/charts`](https://github.com/heyramzi/ui/tree/main/packages/charts). It reads
50
- the same tokens, so import `tokens.css` once and both work.
47
+ ```ts
48
+ // brand.ts
49
+ import { defineBrand } from "@heyramzi/ui/brand";
50
+
51
+ export const brand = defineBrand({
52
+ colors: { brand: "oklch(0.5 0.2 272)", ink: "oklch(0.12 0.04 258)" },
53
+ fonts: { sans: '"Satoshi", system-ui, sans-serif' },
54
+ });
55
+ ```
56
+
57
+ Apply it with `brandStyle(brand)` on `<html style>` (right on first paint), `applyBrand(brand)`
58
+ in the browser, or `brandCss(brand)` in a stylesheet. A brand names only the roles it changes; light
59
+ and dark both follow from the tokens, and the tint scales (`brand-100` to `brand-800`) derive
60
+ from the three hues.
61
+
62
+ ## Subpaths
63
+
64
+ One package, six larger parts, each imported from its own subpath:
65
+
66
+ | Subpath | What it is |
67
+ | --- | --- |
68
+ | `@heyramzi/ui/tasks` | Board, list, gantt, status badges, task detail, record, time tracker, inbox |
69
+ | `@heyramzi/ui/charts` | LayerChart charts, sparklines, stat cards, and `/charts/dashboard` |
70
+ | `@heyramzi/ui/slides` | Decks on a fixed canvas, plus `/slides/model` and `/slides/export` |
71
+ | `@heyramzi/ui/editor` | The Tiptap editor, its renderer, `/editor/markdown`, `/editor/block-ops` |
72
+ | `@heyramzi/ui/onboarding` | Page tours |
73
+ | `@heyramzi/ui/canvas` | The xyflow canvas with a pen and headless ops. Add `@xyflow/svelte` yourself |
74
+
75
+ Each has a README next to its source under `src/<subpath>/`.
51
76
 
52
77
  React and other framework ports are out of scope. The source is Svelte 5
53
78
  runes only.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@heyramzi/ui",
3
- "version": "1.54.2",
4
- "description": "Svelte 5 UI primitives: effects, animations, and shared component CSS.",
3
+ "version": "2.0.0",
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": {
7
7
  "type": "git",
@@ -29,6 +29,11 @@
29
29
  "default": "./src/index.ts"
30
30
  },
31
31
  "./tokens.css": "./src/tokens/index.css",
32
+ "./brand": {
33
+ "svelte": "./src/brand/index.ts",
34
+ "types": "./src/brand/index.ts",
35
+ "default": "./src/brand/index.ts"
36
+ },
32
37
  "./lib/*.js": {
33
38
  "svelte": "./src/lib/*.ts",
34
39
  "types": "./src/lib/*.ts",
@@ -166,26 +171,208 @@
166
171
  "types": "./src/agents/index.ts",
167
172
  "default": "./src/agents/index.ts"
168
173
  },
169
- "./agents/demo.svelte": "./src/agents/demo.svelte"
174
+ "./agents/demo.svelte": "./src/agents/demo.svelte",
175
+ "./tasks": {
176
+ "svelte": "./src/tasks/index.ts",
177
+ "types": "./src/tasks/index.ts",
178
+ "default": "./src/tasks/index.ts"
179
+ },
180
+ "./tasks/kanban": {
181
+ "svelte": "./src/tasks/kanban-board.svelte",
182
+ "types": "./src/tasks/kanban-board.svelte",
183
+ "default": "./src/tasks/kanban-board.svelte"
184
+ },
185
+ "./tasks/list": {
186
+ "svelte": "./src/tasks/task-list.svelte",
187
+ "types": "./src/tasks/task-list.svelte",
188
+ "default": "./src/tasks/task-list.svelte"
189
+ },
190
+ "./tasks/gantt": {
191
+ "svelte": "./src/tasks/task-gantt.svelte",
192
+ "types": "./src/tasks/task-gantt.svelte",
193
+ "default": "./src/tasks/task-gantt.svelte"
194
+ },
195
+ "./tasks/status": {
196
+ "svelte": "./src/tasks/status-badge.svelte",
197
+ "types": "./src/tasks/status-badge.svelte",
198
+ "default": "./src/tasks/status-badge.svelte"
199
+ },
200
+ "./tasks/status-dot": {
201
+ "svelte": "./src/tasks/status-dot.svelte",
202
+ "types": "./src/tasks/status-dot.svelte",
203
+ "default": "./src/tasks/status-dot.svelte"
204
+ },
205
+ "./tasks/statuses": {
206
+ "svelte": "./src/tasks/define-statuses.ts",
207
+ "types": "./src/tasks/define-statuses.ts",
208
+ "default": "./src/tasks/define-statuses.ts"
209
+ },
210
+ "./tasks/time": {
211
+ "svelte": "./src/tasks/time/time-tracker.svelte",
212
+ "types": "./src/tasks/time/time-tracker.svelte",
213
+ "default": "./src/tasks/time/time-tracker.svelte"
214
+ },
215
+ "./tasks/inbox": {
216
+ "svelte": "./src/tasks/inbox/index.ts",
217
+ "types": "./src/tasks/inbox/index.ts",
218
+ "default": "./src/tasks/inbox/index.ts"
219
+ },
220
+ "./tasks/side-panel": {
221
+ "svelte": "./src/tasks/side-panel.svelte",
222
+ "types": "./src/tasks/side-panel.svelte",
223
+ "default": "./src/tasks/side-panel.svelte"
224
+ },
225
+ "./tasks/presets/sales": {
226
+ "svelte": "./src/tasks/presets/sales.ts",
227
+ "types": "./src/tasks/presets/sales.ts",
228
+ "default": "./src/tasks/presets/sales.ts"
229
+ },
230
+ "./tasks/detail": {
231
+ "svelte": "./src/tasks/detail/task-detail.svelte",
232
+ "types": "./src/tasks/detail/task-detail.svelte",
233
+ "default": "./src/tasks/detail/task-detail.svelte"
234
+ },
235
+ "./tasks/detail-demo.svelte": "./src/tasks/detail/detail-demo.svelte",
236
+ "./tasks/record": {
237
+ "svelte": "./src/tasks/record/index.ts",
238
+ "types": "./src/tasks/record/index.ts",
239
+ "default": "./src/tasks/record/index.ts"
240
+ },
241
+ "./tasks/record-demo.svelte": "./src/tasks/record/record-demo.svelte",
242
+ "./tasks/demo": {
243
+ "svelte": "./src/tasks/demo.svelte",
244
+ "default": "./src/tasks/demo.svelte"
245
+ },
246
+ "./tasks/demo.svelte": "./src/tasks/demo.svelte",
247
+ "./charts": {
248
+ "svelte": "./src/charts/index.ts",
249
+ "types": "./src/charts/index.ts",
250
+ "default": "./src/charts/index.ts"
251
+ },
252
+ "./charts/dashboard": {
253
+ "svelte": "./src/charts/dashboard/index.ts",
254
+ "types": "./src/charts/dashboard/index.ts",
255
+ "default": "./src/charts/dashboard/index.ts"
256
+ },
257
+ "./slides": {
258
+ "types": "./src/slides/index.ts",
259
+ "svelte": "./src/slides/index.ts",
260
+ "default": "./src/slides/index.ts"
261
+ },
262
+ "./slides/model": {
263
+ "types": "./src/slides/model.ts",
264
+ "default": "./src/slides/model.ts"
265
+ },
266
+ "./slides/export": {
267
+ "types": "./src/slides/export/index.ts",
268
+ "default": "./src/slides/export/index.ts"
269
+ },
270
+ "./slides/demo": {
271
+ "svelte": "./src/slides/demo.svelte",
272
+ "default": "./src/slides/demo.svelte"
273
+ },
274
+ "./slides/demo.svelte": "./src/slides/demo.svelte",
275
+ "./slides/demo-slides": {
276
+ "svelte": "./src/slides/demo-slides.svelte",
277
+ "default": "./src/slides/demo-slides.svelte"
278
+ },
279
+ "./slides/demo-slides.svelte": "./src/slides/demo-slides.svelte",
280
+ "./editor": {
281
+ "svelte": "./src/editor/index.ts",
282
+ "types": "./src/editor/index.ts",
283
+ "default": "./src/editor/index.ts"
284
+ },
285
+ "./editor/serializer": {
286
+ "types": "./src/editor/serializer.ts",
287
+ "default": "./src/editor/serializer.ts"
288
+ },
289
+ "./editor/markdown": {
290
+ "types": "./src/editor/markdown.ts",
291
+ "default": "./src/editor/markdown.ts"
292
+ },
293
+ "./editor/block-ops": {
294
+ "types": "./src/editor/block-ops.ts",
295
+ "default": "./src/editor/block-ops.ts"
296
+ },
297
+ "./editor/inline-editor": {
298
+ "svelte": "./src/editor/inline-editor.svelte",
299
+ "default": "./src/editor/inline-editor.svelte"
300
+ },
301
+ "./editor/content.css": {
302
+ "style": "./src/editor/content.css",
303
+ "default": "./src/editor/content.css"
304
+ },
305
+ "./onboarding": {
306
+ "svelte": "./src/onboarding/index.ts",
307
+ "types": "./src/onboarding/index.ts",
308
+ "default": "./src/onboarding/index.ts"
309
+ },
310
+ "./onboarding/stores": {
311
+ "svelte": "./src/onboarding/stores.ts",
312
+ "types": "./src/onboarding/stores.ts",
313
+ "default": "./src/onboarding/stores.ts"
314
+ },
315
+ "./onboarding/demo": {
316
+ "svelte": "./src/onboarding/demo.svelte",
317
+ "default": "./src/onboarding/demo.svelte"
318
+ },
319
+ "./onboarding/demo.svelte": "./src/onboarding/demo.svelte",
320
+ "./canvas": {
321
+ "types": "./src/canvas/index.ts",
322
+ "svelte": "./src/canvas/index.ts",
323
+ "default": "./src/canvas/index.ts"
324
+ },
325
+ "./canvas/*.svelte": {
326
+ "types": "./src/canvas/*.svelte",
327
+ "svelte": "./src/canvas/*.svelte",
328
+ "default": "./src/canvas/*.svelte"
329
+ },
330
+ "./canvas/*.css": "./src/canvas/*.css",
331
+ "./canvas/*.js": {
332
+ "types": "./src/canvas/*.ts",
333
+ "svelte": "./src/canvas/*.ts",
334
+ "default": "./src/canvas/*.ts"
335
+ }
170
336
  },
171
337
  "dependencies": {
338
+ "@dagrejs/dagre": "^3.1.1",
172
339
  "@shadcn-svelte/primitives": "^0.0.1",
340
+ "@tiptap/core": "^3.31.4",
341
+ "@tiptap/extension-drag-handle": "^3.31.4",
342
+ "@tiptap/extension-image": "^3.31.4",
343
+ "@tiptap/extension-placeholder": "^3.31.4",
344
+ "@tiptap/pm": "^3.31.4",
345
+ "@tiptap/starter-kit": "^3.31.4",
346
+ "@zag-js/svelte": "^1.46.0",
347
+ "@zag-js/tour": "^1.46.0",
173
348
  "clsx": "^2.1.1",
349
+ "d3-scale": "^4.0.2",
350
+ "d3-shape": "^3.2.0",
351
+ "layerchart": "^2.5.1",
352
+ "markdown-it": "^15.0.2",
174
353
  "marked": "^18.1.0",
354
+ "perfect-freehand": "^1.2.3",
175
355
  "phosphor-svelte": "^3.1.0",
356
+ "roughjs": "^4.6.6",
176
357
  "shiki": "^4.5.0",
177
358
  "tailwind-merge": "^3.7.0",
178
- "tailwind-variants": "^3.3.1"
359
+ "tailwind-variants": "^3.3.1",
360
+ "zod": "^4.6.5"
179
361
  },
180
362
  "devDependencies": {
181
363
  "@internationalized/date": "^3.12.4",
182
364
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
365
+ "@types/d3-scale": "^4.0.9",
366
+ "@types/d3-shape": "^3.2.0",
183
367
  "@types/node": "^26.6.4",
368
+ "@xyflow/svelte": "^1.7.0",
184
369
  "bits-ui": "^2.19.5",
185
370
  "jsdom": "^30.1.2",
186
371
  "mode-watcher": "^1.1.0",
187
372
  "svelte": "^5.57.2",
373
+ "svelte-check": "^4.7.6",
188
374
  "svelte-sonner": "^1.2.1",
375
+ "typescript": "~7.0.2",
189
376
  "vite-plus": "^1.0.0",
190
377
  "vitest": "^5.0.3"
191
378
  },
@@ -195,7 +382,8 @@
195
382
  "bits-ui": "^2.19.5",
196
383
  "mode-watcher": "^1.1.0",
197
384
  "svelte": "^5.57.2",
198
- "svelte-sonner": "^1.2.1"
385
+ "svelte-sonner": "^1.2.1",
386
+ "@xyflow/svelte": "^1.7.0"
199
387
  },
200
388
  "peerDependenciesMeta": {
201
389
  "mode-watcher": {
@@ -209,6 +397,9 @@
209
397
  },
210
398
  "@tabler/icons-svelte-runes": {
211
399
  "optional": true
400
+ },
401
+ "@xyflow/svelte": {
402
+ "optional": true
212
403
  }
213
404
  },
214
405
  "scripts": {
@@ -216,6 +407,7 @@
216
407
  "sync:transitions": "node scripts/sync-transitions.mjs",
217
408
  "sync:shadcn": "node scripts/sync-shadcn.mjs",
218
409
  "sync": "pnpm sync:transitions && pnpm sync:shadcn",
219
- "sync:check": "node scripts/sync-transitions.mjs --check && node scripts/sync-shadcn.mjs --check"
410
+ "sync:check": "node scripts/sync-transitions.mjs --check && node scripts/sync-shadcn.mjs --check",
411
+ "check": "svelte-check --tsconfig ./tsconfig.json"
220
412
  }
221
413
  }
@@ -111,10 +111,10 @@
111
111
  /* ── Surface ─────────────────────────────────────────── */
112
112
  .gen {
113
113
  --shimmer-dur: 2200ms;
114
- --shimmer-base: var(--color-brand-indigo);
114
+ --shimmer-base: var(--color-brand);
115
115
  /* The band is darker than the base on a pale tint and lighter on ink.
116
116
  Otherwise the sweep looks like the label dropping out. */
117
- --shimmer-highlight: color-mix(in oklab, black 45%, var(--color-brand-indigo));
117
+ --shimmer-highlight: color-mix(in oklab, black 45%, var(--color-brand));
118
118
  --shimmer-band: 400%;
119
119
  --shimmer-ease: linear;
120
120
  --digit-dur: var(--duration-very-slow);
@@ -126,11 +126,11 @@
126
126
 
127
127
  padding: 0.625rem 0.875rem;
128
128
  border-radius: var(--radius-lg);
129
- background: color-mix(in oklab, var(--color-brand-indigo) 10%, transparent);
129
+ background: color-mix(in oklab, var(--color-brand) 10%, transparent);
130
130
  }
131
131
 
132
132
  :global(.dark) .gen {
133
- --shimmer-highlight: color-mix(in oklab, white 70%, var(--color-brand-indigo));
133
+ --shimmer-highlight: color-mix(in oklab, white 70%, var(--color-brand));
134
134
  }
135
135
 
136
136
  .gen-failed {
@@ -150,7 +150,7 @@
150
150
  width: 0.375rem;
151
151
  height: 0.375rem;
152
152
  border-radius: 9999px;
153
- background: var(--color-brand-indigo);
153
+ background: var(--color-brand);
154
154
  animation: gen-pulse 1.8s var(--ease-in-out, ease-in-out) infinite;
155
155
  }
156
156
 
@@ -166,14 +166,14 @@
166
166
  line-height: 1.4;
167
167
  }
168
168
 
169
- /* terra-600, not --color-destructive: that token is tuned for fills, and at
169
+ /* signal-600, not --color-destructive: that token is tuned for fills, and at
170
170
  L=0.6 small text on the pale tint fails. Dark mode uses the lighter step. */
171
171
  .gen-failed .gen-label {
172
- color: var(--color-terra-600);
172
+ color: var(--color-signal-600);
173
173
  }
174
174
 
175
175
  :global(.dark) .gen-failed .gen-label {
176
- color: var(--color-terra-400);
176
+ color: var(--color-signal-400);
177
177
  }
178
178
 
179
179
  .gen-meta {
@@ -183,7 +183,7 @@
183
183
  font-size: 0.75rem;
184
184
  font-weight: 500;
185
185
  font-variant-numeric: tabular-nums;
186
- color: var(--color-brand-indigo);
186
+ color: var(--color-brand);
187
187
  }
188
188
 
189
189
  .gen-count {
@@ -203,7 +203,7 @@
203
203
  margin-top: 0.5rem;
204
204
  height: 0.25rem;
205
205
  border-radius: 9999px;
206
- background: color-mix(in oklab, var(--color-brand-indigo) 22%, transparent);
206
+ background: color-mix(in oklab, var(--color-brand) 22%, transparent);
207
207
  }
208
208
 
209
209
  .gen-fill {
@@ -211,7 +211,7 @@
211
211
  height: 100%;
212
212
  transform-origin: left;
213
213
  border-radius: 9999px;
214
- background: var(--color-brand-indigo);
214
+ background: var(--color-brand);
215
215
  transition: transform var(--duration-slow, 400ms) var(--ease-smooth-out, ease);
216
216
  }
217
217
 
@@ -232,7 +232,7 @@
232
232
  text-overflow: ellipsis;
233
233
  /* Mix toward the foreground, not transparent: a 70% indigo wash on its
234
234
  own track measured 2.9:1. This stays quieter than the label and AA. */
235
- color: color-mix(in oklab, var(--color-brand-indigo) 70%, var(--color-foreground));
235
+ color: color-mix(in oklab, var(--color-brand) 70%, var(--color-foreground));
236
236
  animation: gen-step-in var(--duration-quick, 150ms) var(--ease-in-out, ease-in-out) both;
237
237
  }
238
238
 
package/src/ambient.d.ts CHANGED
@@ -2,3 +2,11 @@
2
2
  * A stylesheet imported for its side effect, bundled by the consumer's Vite.
3
3
  */
4
4
  declare module "*.css";
5
+
6
+ /**
7
+ * An SVG imported as a URL, resolved by the consumer's Vite.
8
+ */
9
+ declare module "*.svg" {
10
+ const src: string;
11
+ export default src;
12
+ }
@@ -0,0 +1,67 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vite-plus/test";
4
+ import { BRAND_COLOR_VARS, applyBrand, brandCss, brandStyle, brandVars, defineBrand } from "./index.js";
5
+
6
+ const TOKENS = join(import.meta.dirname, "..", "tokens");
7
+ const palette = readFileSync(join(TOKENS, "palette.css"), "utf8");
8
+
9
+ describe("brandVars", () => {
10
+ it("sets only the roles a brand names", () => {
11
+ const brand = defineBrand({ colors: { brand: "oklch(0.5 0.2 272)", signal: "#f0503d" } });
12
+ expect(brandVars(brand)).toEqual({ "--brand": "oklch(0.5 0.2 272)", "--brand-signal": "#f0503d" });
13
+ });
14
+
15
+ it("maps fonts and extra properties", () => {
16
+ const brand = defineBrand({
17
+ colors: { brand: "red" },
18
+ fonts: { sans: '"Satoshi", system-ui', mono: "ui-monospace" },
19
+ vars: { "--logo-height": "24px" },
20
+ });
21
+ expect(brandVars(brand)).toEqual({
22
+ "--brand": "red",
23
+ "--font-sans": '"Satoshi", system-ui',
24
+ "--font-mono": "ui-monospace",
25
+ "--logo-height": "24px",
26
+ });
27
+ });
28
+
29
+ it("refuses a value that would break out of its declaration", () => {
30
+ expect(() => brandVars({ colors: { brand: "red; } body { display: none" } })).toThrow(/--brand/);
31
+ });
32
+ });
33
+
34
+ describe("the three ways to apply one", () => {
35
+ const brand = defineBrand({ colors: { brand: "blue", ink: "black" } });
36
+
37
+ it("writes a style attribute and a rule", () => {
38
+ expect(brandStyle(brand)).toBe("--brand:blue;--brand-ink:black");
39
+ expect(brandCss(brand, ".client")).toBe(".client {\n --brand: blue;\n --brand-ink: black;\n}\n");
40
+ });
41
+
42
+ it("applies to an element and puts back what it found", () => {
43
+ const root = document.createElement("div");
44
+ root.style.setProperty("--brand", "green");
45
+ const reset = applyBrand(brand, root);
46
+ expect(root.style.getPropertyValue("--brand")).toBe("blue");
47
+ expect(root.style.getPropertyValue("--brand-ink")).toBe("black");
48
+ reset();
49
+ expect(root.style.getPropertyValue("--brand")).toBe("green");
50
+ expect(root.style.getPropertyValue("--brand-ink")).toBe("");
51
+ });
52
+ });
53
+
54
+ describe("the tokens", () => {
55
+ it("define a neutral default for every role a brand can set", () => {
56
+ for (const name of Object.values(BRAND_COLOR_VARS)) {
57
+ expect(palette, name).toMatch(new RegExp(`^\\s+${name}: `, "m"));
58
+ }
59
+ });
60
+
61
+ it("carry no client palette", () => {
62
+ for (const file of readdirSync(TOKENS).filter((f) => f.endsWith(".css"))) {
63
+ const css = readFileSync(join(TOKENS, file), "utf8");
64
+ expect(css, file).not.toMatch(/brand-(indigo|sand|terra|night|veil|dusk|cream|linen|mist)|--(indigo|sand|terra)-\d/);
65
+ }
66
+ });
67
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * A client's brand is a typed object in its own `brand.ts`, with no build step:
3
+ *
4
+ * export const brand = defineBrand({ colors: { brand: "oklch(0.5 0.2 272)" } });
5
+ *
6
+ * `@heyramzi/ui/tokens.css` ships a neutral look. A brand replaces only the roles it names:
7
+ * - on the server, put `brandStyle(brand)` on the `<html style>` so the first paint is right;
8
+ * - or call `applyBrand(brand)` once in the browser;
9
+ * - or write `brandCss(brand)` into a stylesheet.
10
+ * Light and dark come from the tokens, which read these roles, so a brand never types both.
11
+ */
12
+
13
+ /** Any CSS colour: oklch(), hex, a named colour or a var(). */
14
+ export type CssColor = string;
15
+
16
+ export type BrandColors = {
17
+ /** The signature colour: focus ring, links, active states, the mark. */
18
+ brand: CssColor;
19
+ /** The same colour lifted for a dark page. Falls back to the neutral default. */
20
+ brandOnDark?: CssColor;
21
+ /** Warmth: badges, highlights, soft accents. */
22
+ highlight?: CssColor;
23
+ /** Attention: warnings, errors, energy. */
24
+ signal?: CssColor;
25
+ /** The dark page background. */
26
+ ink?: CssColor;
27
+ /** Cards and panels on a dark page. */
28
+ surface?: CssColor;
29
+ /** Borders and dividers on a dark page. */
30
+ line?: CssColor;
31
+ /** Hover surfaces and inputs on a dark page. */
32
+ raised?: CssColor;
33
+ /** Text on a dark page, and a light page's background tone. */
34
+ paper?: CssColor;
35
+ /** Cards on a light page. */
36
+ surfaceLight?: CssColor;
37
+ /** Borders on a light page. */
38
+ lineLight?: CssColor;
39
+ };
40
+
41
+ export type BrandFonts = {
42
+ /** The body family, as CSS: `"Satoshi", system-ui, sans-serif`. */
43
+ sans?: string;
44
+ /** Headings. Falls back to the body family. */
45
+ heading?: string;
46
+ /** Code and figures. */
47
+ mono?: string;
48
+ /** Quotes. Falls back to the mono family. */
49
+ quote?: string;
50
+ };
51
+
52
+ export type Brand = {
53
+ colors: BrandColors;
54
+ fonts?: BrandFonts;
55
+ /** Extra custom properties the brand owns, such as a logo size. Each key starts with `--`. */
56
+ vars?: Record<`--${string}`, string>;
57
+ };
58
+
59
+ const COLOR_VARS = {
60
+ brand: "--brand",
61
+ brandOnDark: "--brand-on-dark",
62
+ highlight: "--brand-highlight",
63
+ signal: "--brand-signal",
64
+ ink: "--brand-ink",
65
+ surface: "--brand-surface",
66
+ line: "--brand-line",
67
+ raised: "--brand-raised",
68
+ paper: "--brand-paper",
69
+ surfaceLight: "--brand-surface-light",
70
+ lineLight: "--brand-line-light",
71
+ } as const satisfies Record<keyof BrandColors, string>;
72
+
73
+ const FONT_VARS = {
74
+ sans: "--font-sans",
75
+ heading: "--font-heading",
76
+ mono: "--font-mono",
77
+ quote: "--font-quote",
78
+ } as const satisfies Record<keyof BrandFonts, string>;
79
+
80
+ /** The custom property each colour role sets. The tokens read these and nothing else. */
81
+ export const BRAND_COLOR_VARS: Readonly<Record<keyof BrandColors, string>> = COLOR_VARS;
82
+
83
+ /** Keeps a typed brand typed at the call site. It returns the object unchanged. */
84
+ export function defineBrand<const B extends Brand>(brand: B): B {
85
+ return brand;
86
+ }
87
+
88
+ function checked(name: string, value: string): string {
89
+ if (/[;{}<>]/.test(value)) throw new Error(`brand: ${name} holds a character CSS would read as more than a value`);
90
+ return value;
91
+ }
92
+
93
+ /** The custom properties a brand sets, by name. Roles the brand leaves out keep the neutral default. */
94
+ export function brandVars(brand: Brand): Record<string, string> {
95
+ const out: Record<string, string> = {};
96
+ for (const [key, name] of Object.entries(COLOR_VARS)) {
97
+ const value = brand.colors[key as keyof BrandColors];
98
+ if (value !== undefined) out[name] = checked(name, value);
99
+ }
100
+ for (const [key, name] of Object.entries(FONT_VARS)) {
101
+ const value = brand.fonts?.[key as keyof BrandFonts];
102
+ if (value !== undefined) out[name] = checked(name, value);
103
+ }
104
+ for (const [name, value] of Object.entries(brand.vars ?? {})) out[name] = checked(name, value);
105
+ return out;
106
+ }
107
+
108
+ /** The brand as a `style` attribute value: put it on `<html>` in `app.html` or a layout. */
109
+ export function brandStyle(brand: Brand): string {
110
+ return Object.entries(brandVars(brand))
111
+ .map(([name, value]) => `${name}:${value}`)
112
+ .join(";");
113
+ }
114
+
115
+ /** The brand as a rule, for a stylesheet or a `<style>` in `<svelte:head>`. */
116
+ export function brandCss(brand: Brand, selector = ":root"): string {
117
+ const body = Object.entries(brandVars(brand))
118
+ .map(([name, value]) => ` ${name}: ${value};`)
119
+ .join("\n");
120
+ return `${selector} {\n${body}\n}\n`;
121
+ }
122
+
123
+ /** Sets the brand on the page in the browser. Returns a function that puts the neutral look back. */
124
+ export function applyBrand(brand: Brand, root: HTMLElement = document.documentElement): () => void {
125
+ const vars = brandVars(brand);
126
+ const before = new Map<string, string>();
127
+ for (const [name, value] of Object.entries(vars)) {
128
+ before.set(name, root.style.getPropertyValue(name));
129
+ root.style.setProperty(name, value);
130
+ }
131
+ return () => {
132
+ for (const [name, value] of before) {
133
+ if (value) root.style.setProperty(name, value);
134
+ else root.style.removeProperty(name);
135
+ }
136
+ };
137
+ }