@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
@@ -0,0 +1,56 @@
1
+ /* Styles for DonutChart. Plain stylesheet (see area-chart.css for why).
2
+ Selectors are namespaced under `.hr-donut-chart`. */
3
+
4
+ .hr-donut-chart {
5
+ width: 100%;
6
+ display: flex;
7
+ justify-content: center;
8
+ }
9
+
10
+ .hr-donut-chart .empty {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ width: 100%;
15
+ border: 1px dashed var(--chart-grid, oklch(0.5 0.01 286 / 0.2));
16
+ border-radius: 0.5rem;
17
+ }
18
+ .hr-donut-chart .empty p {
19
+ font-size: 0.8125rem;
20
+ color: var(--chart-label, oklch(0.5 0.01 286));
21
+ margin: 0;
22
+ }
23
+
24
+ /* The slice slides out a little slower than it slides back: a close is always the quicker. */
25
+ .hr-donut-chart .hr-donut-arc {
26
+ cursor: pointer;
27
+ transition:
28
+ opacity var(--chart-motion-fast, 150ms) ease,
29
+ translate var(--duration-quick, 150ms) var(--ease-smooth-out, ease-out);
30
+ }
31
+ .hr-donut-chart .hr-donut-arc[data-hovered] {
32
+ transition:
33
+ opacity var(--chart-motion-fast, 150ms) ease,
34
+ translate var(--duration-fast, 250ms) var(--ease-smooth-out);
35
+ }
36
+
37
+ .hr-donut-chart .hr-donut-center-value {
38
+ fill: var(--foreground, oklch(0.18 0.02 240));
39
+ font-size: 16px;
40
+ font-weight: 600;
41
+ font-feature-settings: "tnum";
42
+ transition: opacity 180ms ease;
43
+ }
44
+
45
+ .hr-donut-chart .hr-donut-center-label {
46
+ fill: var(--chart-label, oklch(0.5 0.01 286));
47
+ font-size: 11px;
48
+ transition: opacity 180ms ease;
49
+ }
50
+
51
+ @media (prefers-reduced-motion: reduce) {
52
+ .hr-donut-chart .hr-donut-arc,
53
+ .hr-donut-chart .hr-donut-arc[data-hovered] {
54
+ transition: opacity 120ms ease;
55
+ }
56
+ }
@@ -0,0 +1,158 @@
1
+ <script lang="ts" generics="TData extends Record<string, unknown>">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import "./donut-chart.css";
6
+ import { Arc, Chart, Group, Pie, Svg, Text } from "layerchart";
7
+ import { scaleOrdinal } from "d3-scale";
8
+ import { expoInOut } from "svelte/easing";
9
+ import { prefersReducedMotion, Tween } from "svelte/motion";
10
+ import { REVEAL_MS } from "./motion.js";
11
+
12
+ // =========================
13
+ // TYPES
14
+ // =========================
15
+ interface Props {
16
+ data: TData[];
17
+ valueKey: keyof TData & string;
18
+ labelKey: keyof TData & string;
19
+ colorKey?: keyof TData & string;
20
+ height?: number;
21
+ innerRadius?: number;
22
+ outerRadius?: number;
23
+ /** Center label shown when nothing is hovered */
24
+ centerLabel?: string;
25
+ /** Format the center value */
26
+ formatValue?: (v: number) => string;
27
+ class?: string;
28
+ };
29
+
30
+ // =========================
31
+ // PROPS
32
+ // =========================
33
+ let {
34
+ data,
35
+ valueKey,
36
+ labelKey,
37
+ colorKey,
38
+ height = 240,
39
+ innerRadius = 60,
40
+ outerRadius = 100,
41
+ centerLabel,
42
+ formatValue = (v) => v.toLocaleString(),
43
+ class: className,
44
+ }: Props = $props();
45
+
46
+ // =========================
47
+ // STATE
48
+ // =========================
49
+ let hovered = $state<string | null>(null);
50
+ // Milliseconds into the entrance; each slice sweeps on its own offset.
51
+ const clock = new Tween(0);
52
+ let swept = $state(false);
53
+ let started = false;
54
+
55
+ // =========================
56
+ // FUNCTIONS
57
+ // =========================
58
+ const SLICE_DELAY = 100;
59
+ const SLICE_STEP = 80;
60
+ const HOVER_OFFSET = 6;
61
+
62
+ function sweep(i: number): number {
63
+ if (swept) return 1;
64
+ const t = (clock.current - SLICE_DELAY - i * SLICE_STEP) / REVEAL_MS;
65
+ return expoInOut(Math.min(1, Math.max(0, t)));
66
+ }
67
+
68
+ // =========================
69
+ // EFFECTS
70
+ // =========================
71
+ $effect(() => {
72
+ if (started || filtered.length === 0) return;
73
+ started = true;
74
+ const end = SLICE_DELAY + (filtered.length - 1) * SLICE_STEP + REVEAL_MS;
75
+ const duration = prefersReducedMotion.current ? 0 : end;
76
+ clock.set(end, { duration }).then(() => (swept = true));
77
+ });
78
+
79
+ // =========================
80
+ // DERIVED
81
+ // =========================
82
+ const filtered = $derived(data.filter((d) => Number(d[valueKey] ?? 0) > 0));
83
+ const total = $derived(filtered.reduce((sum, d) => sum + Number(d[valueKey] ?? 0), 0));
84
+ const idAccessor = $derived((d: TData) => String(d[labelKey] ?? ""));
85
+ const colorRange = $derived(colorKey ? filtered.map((d) => String(d[colorKey] ?? "")) : []);
86
+ const colorDomain = $derived(filtered.map(idAccessor));
87
+ const hoveredItem = $derived(filtered.find((d) => idAccessor(d) === hovered) ?? null);
88
+ </script>
89
+
90
+ <!-- =========================== -->
91
+ <!-- DONUT CHART -->
92
+ <!-- =========================== -->
93
+ <div
94
+ class="hr-donut-chart {className ?? ''}"
95
+ style:height="{height}px"
96
+ role="img"
97
+ aria-label="{centerLabel ?? 'Donut chart'}: {formatValue(total)} total"
98
+ >
99
+ {#if filtered.length === 0}
100
+ <div class="empty">
101
+ <p>No data</p>
102
+ </div>
103
+ {:else}
104
+ <Chart
105
+ data={filtered}
106
+ x={valueKey}
107
+ c={colorKey ? idAccessor : undefined}
108
+ cScale={scaleOrdinal()}
109
+ cDomain={colorDomain}
110
+ cRange={colorRange}
111
+ >
112
+ <Svg>
113
+ <Group center>
114
+ <Pie {innerRadius} {outerRadius} cornerRadius={3} padAngle={0.02} sort={null}>
115
+ {#snippet children({ arcs })}
116
+ {#each arcs as arc, i (i)}
117
+ {@const id = idAccessor(arc.data as TData)}
118
+ {@const isHovered = hovered === id}
119
+ {@const dimmed = hovered !== null && !isHovered}
120
+ {@const mid = (arc.startAngle + arc.endAngle) / 2}
121
+ {@const dx = isHovered ? Math.sin(mid) * HOVER_OFFSET : 0}
122
+ {@const dy = isHovered ? -Math.cos(mid) * HOVER_OFFSET : 0}
123
+ <Arc
124
+ startAngle={arc.startAngle}
125
+ endAngle={arc.startAngle + (arc.endAngle - arc.startAngle) * sweep(i)}
126
+ padAngle={arc.padAngle}
127
+ {innerRadius}
128
+ {outerRadius}
129
+ cornerRadius={3}
130
+ fill={colorKey
131
+ ? String((arc.data as TData)[colorKey])
132
+ : `var(--chart-${(i % 5) + 1})`}
133
+ class="hr-donut-arc"
134
+ data-hovered={isHovered ? "" : undefined}
135
+ style="opacity: {dimmed ? 0.4 : 1}; translate: {dx}px {dy}px"
136
+ onmouseenter={() => (hovered = id)}
137
+ onmouseleave={() => (hovered = null)}
138
+ />
139
+ {/each}
140
+ {/snippet}
141
+ </Pie>
142
+ <Text
143
+ value={hoveredItem ? formatValue(Number(hoveredItem[valueKey])) : formatValue(total)}
144
+ textAnchor="middle"
145
+ dy={-4}
146
+ class="hr-donut-center-value"
147
+ />
148
+ <Text
149
+ value={hoveredItem ? String(hoveredItem[labelKey]) : (centerLabel ?? "Total")}
150
+ textAnchor="middle"
151
+ dy={14}
152
+ class="hr-donut-center-label"
153
+ />
154
+ </Group>
155
+ </Svg>
156
+ </Chart>
157
+ {/if}
158
+ </div>
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { EDGE_FADE_DEFAULT } from "./edge-fade.js";
6
+
7
+ // =========================
8
+ // TYPES
9
+ // =========================
10
+ interface Props {
11
+ /** Mask id. Apply it to the shape with `mask="url(#id)"`. */
12
+ id: string;
13
+ /** Fraction of the shape's width faded at each side, 0-0.45. */
14
+ amount?: number;
15
+ };
16
+
17
+ // =========================
18
+ // PROPS
19
+ // =========================
20
+ let { id, amount = EDGE_FADE_DEFAULT }: Props = $props();
21
+
22
+ // =========================
23
+ // DERIVED
24
+ // =========================
25
+ const stop = $derived(Math.min(0.45, Math.max(0, amount)));
26
+ </script>
27
+
28
+ <!-- =========================== -->
29
+ <!-- EDGE FADE -->
30
+ <!-- =========================== -->
31
+ <!-- WHY: the fill gradient of an area is vertical, so the horizontal fade cannot ride on it. A luminance mask in objectBoundingBox units needs no pixel -->
32
+ <!-- dimensions: the ramp is expressed as a fraction of the masked path's own box, so it follows the plot width through every resize. -->
33
+ <defs>
34
+ <linearGradient id="{id}-ramp" x1="0" y1="0" x2="1" y2="0">
35
+ <stop offset="0" stop-color="#fff" stop-opacity="0" />
36
+ <stop offset={stop} stop-color="#fff" stop-opacity="1" />
37
+ <stop offset={1 - stop} stop-color="#fff" stop-opacity="1" />
38
+ <stop offset="1" stop-color="#fff" stop-opacity="0" />
39
+ </linearGradient>
40
+ <mask {id} maskContentUnits="objectBoundingBox">
41
+ <rect x="0" y="0" width="1" height="1" fill="url(#{id}-ramp)" />
42
+ </mask>
43
+ </defs>
@@ -0,0 +1,16 @@
1
+ /** `true` fades this much of the width at each side. */
2
+ export const EDGE_FADE_DEFAULT = 0.08;
3
+
4
+ /**
5
+ * Turns an `edgeFade` prop into the fraction of the width to fade at each side.
6
+ * `false` (or 0) means no mask is rendered at all.
7
+ */
8
+ export function resolveEdgeFade(
9
+ value: boolean | number | undefined,
10
+ fallback = EDGE_FADE_DEFAULT,
11
+ ): number {
12
+ if (value === false) return 0;
13
+ if (value === true || value == null) return fallback;
14
+ if (!Number.isFinite(value)) return 0;
15
+ return Math.min(0.45, Math.max(0, value));
16
+ }
@@ -0,0 +1,100 @@
1
+ /* Styles for Figure. Plain stylesheet (see area-chart.css for why). All
2
+ selectors are already namespaced under `.hr-figure`. */
3
+
4
+ .hr-figure {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 0.1875rem;
8
+ min-width: 0;
9
+ }
10
+ .hr-figure[data-align="center"] {
11
+ align-items: center;
12
+ text-align: center;
13
+ }
14
+
15
+ /* WHY the title truncates: title and pill share one narrow row (the gauge card
16
+ leaves it ~160px), and a shrunken title box let the word paint over the pill. */
17
+ .hr-figure-head {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ gap: 0.75rem;
22
+ }
23
+ .hr-figure-title {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: 0.4375rem;
27
+ min-width: 0;
28
+ font-size: 0.9375rem;
29
+ font-weight: 600;
30
+ color: var(--foreground, oklch(0.2 0.01 286));
31
+ letter-spacing: -0.005em;
32
+ }
33
+ .hr-figure-title-text {
34
+ min-width: 0;
35
+ overflow: hidden;
36
+ white-space: nowrap;
37
+ text-overflow: ellipsis;
38
+ }
39
+ .hr-figure-icon {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ }
44
+
45
+ .hr-figure-values {
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ gap: 0.75rem;
49
+ align-items: baseline;
50
+ }
51
+ .hr-figure-value-row {
52
+ display: inline-flex;
53
+ align-items: baseline;
54
+ gap: 0.375rem;
55
+ }
56
+
57
+ .hr-figure-value {
58
+ font-feature-settings: "tnum";
59
+ line-height: 1.1;
60
+ transition: color 180ms ease;
61
+ }
62
+ /* md: chart header (monospace, compact). */
63
+ .hr-figure[data-size="md"] .hr-figure-value {
64
+ font-size: 1.25rem;
65
+ font-weight: 600;
66
+ font-family: ui-monospace, SFMono-Regular, monospace;
67
+ }
68
+ /* lg: KPI card headline. */
69
+ .hr-figure[data-size="lg"] .hr-figure-value {
70
+ font-size: 1.875rem;
71
+ font-weight: 700;
72
+ line-height: 1;
73
+ letter-spacing: -0.025em;
74
+ color: var(--foreground, oklch(0.2 0.01 286));
75
+ }
76
+
77
+ .hr-figure-series-label {
78
+ font-size: 0.6875rem;
79
+ }
80
+
81
+ /* The readable form of a series colour: see the note in figure.svelte. The wash
82
+ and the line keep the raw hue; only text is pulled toward the foreground. */
83
+ .hr-figure-value.is-series,
84
+ .hr-figure-series-label.is-series {
85
+ color: color-mix(
86
+ in oklab,
87
+ var(--hr-series, currentColor) 70%,
88
+ var(--foreground, oklch(0.2 0.01 286))
89
+ );
90
+ }
91
+ .hr-figure-caption {
92
+ font-size: 0.75rem;
93
+ color: var(--muted-foreground, oklch(0.5 0.01 286));
94
+ }
95
+ .hr-figure-eyebrow {
96
+ font-size: 0.6875rem;
97
+ color: var(--chart-label, var(--muted-foreground, oklch(0.5 0.01 286)));
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.04em;
100
+ }
@@ -0,0 +1,122 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import "./figure.css";
6
+ import { cn } from "../lib/utils.js";
7
+ import type { Snippet } from "svelte";
8
+ import AnimatedValue from "./animated-value.svelte";
9
+ import TrendPill from "./trend-pill.svelte";
10
+
11
+ // =========================
12
+ // TYPES
13
+ // =========================
14
+ type Trend = "up" | "down" | "neutral";
15
+ type SeriesValue = { value: number; label?: string; color?: string };
16
+
17
+ interface Props {
18
+ /** Single metric value. Numbers count up via AnimatedValue. Ignored when `series` is set. */
19
+ value?: number | string;
20
+ /** Multiple value rows (chart headers with more than one series). */
21
+ series?: SeriesValue[];
22
+ /** Single-value color. */
23
+ color?: string;
24
+ /** Title above the value (KPI card). Pairs with `delta` for a trend pill. */
25
+ title?: string;
26
+ /** Percentage change; renders a TrendPill beside the title. */
27
+ delta?: number;
28
+ trend?: Trend;
29
+ /** Sub-caption under the value. */
30
+ caption?: string;
31
+ /** Uppercase context label under the value (chart headers, e.g. "JUN"). */
32
+ eyebrow?: string;
33
+ /** Left header (md, monospace) vs centered overlay reuse later. */
34
+ align?: "start" | "center";
35
+ /** Value scale: md = chart header, lg = KPI card. */
36
+ size?: "md" | "lg";
37
+ format?: (v: number) => string;
38
+ formatOptions?: Intl.NumberFormatOptions;
39
+ locale?: string;
40
+ animate?: boolean;
41
+ /** Leading icon before the title. */
42
+ icon?: Snippet;
43
+ class?: string;
44
+ };
45
+
46
+ // =========================
47
+ // PROPS
48
+ // =========================
49
+ let {
50
+ value,
51
+ series,
52
+ color,
53
+ title,
54
+ delta,
55
+ trend,
56
+ caption,
57
+ eyebrow,
58
+ align = "start",
59
+ size = "md",
60
+ format,
61
+ formatOptions,
62
+ locale,
63
+ animate = true,
64
+ icon,
65
+ class: className,
66
+ }: Props = $props();
67
+
68
+ // =========================
69
+ // DERIVED
70
+ // =========================
71
+ const rows = $derived(series && series.length > 0 ? series : null);
72
+ </script>
73
+
74
+ <!-- =========================== -->
75
+ <!-- FIGURE -->
76
+ <!-- =========================== -->
77
+ <div class={cn("hr-figure", className)} data-align={align} data-size={size}>
78
+ {#if title}
79
+ <div class="hr-figure-head">
80
+ <span class="hr-figure-title">
81
+ {#if icon}<span class="hr-figure-icon" style:color>{@render icon()}</span>{/if}
82
+ <span class="hr-figure-title-text">{title}</span>
83
+ </span>
84
+ {#if delta != null}
85
+ <TrendPill {delta} {trend} />
86
+ {/if}
87
+ </div>
88
+ {/if}
89
+
90
+ {#if rows}
91
+ <div class="hr-figure-values">
92
+ {#each rows as row, i (i)}
93
+ <!-- WHY the series colour goes in as a custom property rather than as `color`: a palette entry is picked to read as a line on a chart, which -->
94
+ <!-- is a 3:1 job, and the same value as 11px text measured 2.7:1. The stylesheet mixes it toward the page foreground, so the number still -->
95
+ <!-- names its series and the text still clears AA in either theme. -->
96
+ <span class="hr-figure-value-row" style:--hr-series={row.color}>
97
+ <span class="hr-figure-value is-series">
98
+ <AnimatedValue value={row.value} {format} {formatOptions} {locale} duration={450} />
99
+ </span>
100
+ {#if rows.length > 1 && row.label}
101
+ <span class="hr-figure-series-label is-series">{row.label}</span>
102
+ {/if}
103
+ </span>
104
+ {/each}
105
+ </div>
106
+ {:else if value != null}
107
+ <span class="hr-figure-value" style:color>
108
+ {#if typeof value === "number" && animate}
109
+ <AnimatedValue {value} {format} {formatOptions} {locale} duration={600} />
110
+ {:else}
111
+ {value}
112
+ {/if}
113
+ </span>
114
+ {/if}
115
+
116
+ {#if caption}
117
+ <span class="hr-figure-caption">{caption}</span>
118
+ {/if}
119
+ {#if eyebrow}
120
+ <span class="hr-figure-eyebrow">{eyebrow}</span>
121
+ {/if}
122
+ </div>
@@ -0,0 +1,94 @@
1
+ /* Styles for GaugeChart. Plain stylesheet (see area-chart.css for why).
2
+ Selectors are namespaced under `.hr-gauge` (the generic ones below would
3
+ otherwise collide with RingChart's identical `.value`/`.label`/`.center`/
4
+ `.fill`/`svg` names once they are no longer Svelte-scoped). */
5
+
6
+ .hr-gauge {
7
+ position: relative;
8
+ display: inline-grid;
9
+ place-items: center;
10
+ aspect-ratio: 1;
11
+ container-type: size;
12
+ }
13
+
14
+ .hr-gauge svg {
15
+ grid-area: 1 / 1;
16
+ width: 100%;
17
+ height: 100%;
18
+ overflow: visible;
19
+ }
20
+
21
+ .hr-gauge .center {
22
+ grid-area: 1 / 1;
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ justify-content: center;
27
+ gap: 0.15cqmin;
28
+ /* WHY: keep the readout inside the inner ring so long values never collide
29
+ with the notches. */
30
+ max-width: 62cqmin;
31
+ pointer-events: none;
32
+ }
33
+
34
+ .hr-gauge .value {
35
+ font-size: max(0.78rem, 11cqmin);
36
+ font-weight: 650;
37
+ line-height: 1;
38
+ letter-spacing: -0.02em;
39
+ font-variant-numeric: tabular-nums;
40
+ color: var(--foreground);
41
+ }
42
+
43
+ .hr-gauge .label {
44
+ margin-top: 1.5cqmin;
45
+ font-size: max(0.5rem, 5cqmin);
46
+ font-weight: 500;
47
+ white-space: nowrap;
48
+ color: var(--muted-foreground);
49
+ }
50
+
51
+ /* WHY: active paint comes from the fill attribute (flat or gradient url), so
52
+ no CSS fill here, which would override it. */
53
+ .hr-gauge .fill {
54
+ /* WHY: each active notch fades up in arc order; the per-notch stagger
55
+ sweeps the colour around the arc so the gauge reads as filling, instead
56
+ of every notch popping in from the centre at once. */
57
+ animation: hr-gauge-notch-in 0.3s var(--chart-ease-out, cubic-bezier(0.22, 1, 0.36, 1)) both;
58
+ }
59
+
60
+ @keyframes hr-gauge-notch-in {
61
+ from {
62
+ opacity: 0;
63
+ }
64
+ }
65
+
66
+ .hr-gauge .ring-track,
67
+ .hr-gauge .ring-fill {
68
+ fill: none;
69
+ stroke-linecap: round;
70
+ }
71
+
72
+ .hr-gauge .ring-fill {
73
+ stroke-dasharray: 100;
74
+ transition: stroke-dashoffset var(--chart-motion-slow, 1.1s)
75
+ var(--chart-ease-out, cubic-bezier(0.22, 1, 0.36, 1));
76
+ animation: hr-gauge-ring-sweep var(--chart-motion-reveal, 1100ms)
77
+ var(--chart-ease-reveal, cubic-bezier(0.85, 0, 0.15, 1)) both;
78
+ }
79
+
80
+ @keyframes hr-gauge-ring-sweep {
81
+ from {
82
+ stroke-dashoffset: 100;
83
+ }
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .hr-gauge .fill {
88
+ animation: none;
89
+ }
90
+ .hr-gauge .ring-fill {
91
+ transition: none;
92
+ animation: none;
93
+ }
94
+ }