@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,112 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import "./legend.css";
6
+ import { cn } from "../lib/utils.js";
7
+
8
+ // =========================
9
+ // TYPES
10
+ // =========================
11
+ type LegendItem = {
12
+ label: string;
13
+ value: number;
14
+ /** Required when `showProgress` or `showPercentage` is set. */
15
+ maxValue?: number;
16
+ color: string;
17
+ };
18
+
19
+ interface Props {
20
+ items: LegendItem[];
21
+ title?: string;
22
+ /** Stacked rows vs inline wrap. */
23
+ layout?: "vertical" | "horizontal";
24
+ /** Render a progress bar under each row (needs maxValue). */
25
+ showProgress?: boolean;
26
+ /** Show the value beside each label. */
27
+ showValue?: boolean;
28
+ /** Append a percentage (needs maxValue). */
29
+ showPercentage?: boolean;
30
+ formatValue?: (v: number) => string;
31
+ formatPercentage?: (p: number) => string;
32
+ /** Hovered index, bindable for chart hover-sync. */
33
+ hoveredIndex?: number | null;
34
+ onHoverChange?: (index: number | null) => void;
35
+ class?: string;
36
+ };
37
+
38
+ // =========================
39
+ // PROPS
40
+ // =========================
41
+ let {
42
+ items,
43
+ title,
44
+ layout = "vertical",
45
+ showProgress = false,
46
+ showValue = true,
47
+ showPercentage = false,
48
+ formatValue = (v) => v.toLocaleString(),
49
+ formatPercentage = (p) => `${p.toFixed(0)}%`,
50
+ hoveredIndex = $bindable(null),
51
+ onHoverChange,
52
+ class: className,
53
+ }: Props = $props();
54
+
55
+ // =========================
56
+ // FUNCTIONS
57
+ // =========================
58
+ function pct(item: LegendItem): number {
59
+ if (!item.maxValue || item.maxValue <= 0) return 0;
60
+ return Math.max(0, Math.min(100, (item.value / item.maxValue) * 100));
61
+ }
62
+
63
+ function setHover(index: number | null) {
64
+ hoveredIndex = index;
65
+ onHoverChange?.(index);
66
+ }
67
+ </script>
68
+
69
+ <!-- =========================== -->
70
+ <!-- MARKUP -->
71
+ <!-- =========================== -->
72
+ <div class={cn("hr-legend", className)} class:horizontal={layout === "horizontal"}>
73
+ {#if title}
74
+ <p class="hr-legend-title">{title}</p>
75
+ {/if}
76
+ <ul class="hr-legend-items">
77
+ {#each items as item, i (item.label + i)}
78
+ {@const dimmed = hoveredIndex !== null && hoveredIndex !== i}
79
+ <li>
80
+ <button
81
+ type="button"
82
+ class="hr-legend-item"
83
+ class:dimmed
84
+ onmouseenter={() => setHover(i)}
85
+ onmouseleave={() => setHover(null)}
86
+ onfocus={() => setHover(i)}
87
+ onblur={() => setHover(null)}
88
+ >
89
+ <span class="hr-legend-marker" style:background={item.color}></span>
90
+ <span class="hr-legend-label">{item.label}</span>
91
+ {#if showValue}
92
+ <span class="hr-legend-value">
93
+ {formatValue(item.value)}{#if showPercentage && item.maxValue}<span
94
+ class="hr-legend-pct">{formatPercentage(pct(item))}</span
95
+ >{/if}
96
+ </span>
97
+ {/if}
98
+ {#if showProgress && item.maxValue}
99
+ <span class="hr-legend-track">
100
+ <span
101
+ class="hr-legend-bar"
102
+ style:--hr-bar-scale={pct(item) / 100}
103
+ style:background={item.color}
104
+ style:animation-delay="{160 + i * 120}ms"
105
+ ></span>
106
+ </span>
107
+ {/if}
108
+ </button>
109
+ </li>
110
+ {/each}
111
+ </ul>
112
+ </div>
@@ -0,0 +1,26 @@
1
+ /* Styles for LineChart. Plain stylesheet (see area-chart.css for why). Selectors
2
+ are namespaced under `.hr-line-chart`. */
3
+
4
+ .hr-line-chart {
5
+ width: 100%;
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: 0.5rem;
9
+ }
10
+ .hr-line-chart .hr-chart-body {
11
+ width: 100%;
12
+ }
13
+
14
+ .hr-line-chart .empty {
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ height: 100%;
19
+ border: 1px dashed var(--chart-grid, oklch(0.5 0.01 286 / 0.2));
20
+ border-radius: 0.5rem;
21
+ }
22
+ .hr-line-chart .empty p {
23
+ font-size: 0.8125rem;
24
+ color: var(--chart-label, oklch(0.5 0.01 286));
25
+ margin: 0;
26
+ }
@@ -0,0 +1,256 @@
1
+ <script lang="ts" generics="TData extends Record<string, unknown>">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import "./line-chart.css";
6
+ import {
7
+ Area,
8
+ Axis,
9
+ Chart,
10
+ Highlight,
11
+ LinearGradient,
12
+ Points,
13
+ Spline,
14
+ Svg,
15
+ Tooltip,
16
+ } from "layerchart";
17
+ import { curveMonotoneX } from "d3-shape";
18
+ import Crosshair from "./crosshair.svelte";
19
+ import EdgeFade from "./edge-fade.svelte";
20
+ import { resolveEdgeFade } from "./edge-fade.js";
21
+ import Figure from "./figure.svelte";
22
+ import HoverFocus from "./hover-focus.svelte";
23
+ import { DOMAIN_MOTION, TOOLTIP_GAP, TOOLTIP_TOP } from "./motion.js";
24
+ import Reveal from "./reveal.svelte";
25
+
26
+ // =========================
27
+ // TYPES
28
+ // =========================
29
+ type Series = {
30
+ key: keyof TData & string;
31
+ label?: string;
32
+ color?: string;
33
+ };
34
+
35
+ interface Props {
36
+ data: TData[];
37
+ xKey: keyof TData & string;
38
+ yKey?: keyof TData & string;
39
+ series?: Series[];
40
+ color?: string;
41
+ height?: number;
42
+ smooth?: boolean;
43
+ /**
44
+ * Softens the left and right ends of the gradient fill instead of cutting them off at the plot edge.
45
+ */
46
+
47
+ /*
48
+ * `true` fades 8% of the width per side, a number sets the fraction (0-0.45), `false` keeps the hard edges.
49
+ */
50
+ edgeFade?: boolean | number;
51
+ /** Draw a ring marker at every data point. */
52
+ showMarkers?: boolean;
53
+ /** Inclusive index where a dashed "in progress" tail begins (e.g. the latest, still-open period). */
54
+ dashFromIndex?: number;
55
+ /** Dash pattern for the tail segment. */
56
+ dashArray?: string;
57
+ formatValue?: (v: number) => string;
58
+ formatX?: (v: unknown) => string;
59
+ class?: string;
60
+ };
61
+
62
+ // =========================
63
+ // PROPS
64
+ // =========================
65
+ let {
66
+ data,
67
+ xKey,
68
+ yKey,
69
+ series: seriesInput,
70
+ color = "var(--chart-1)",
71
+ height = 240,
72
+ smooth = true,
73
+ edgeFade = true,
74
+ showMarkers = false,
75
+ dashFromIndex,
76
+ dashArray = "5 4",
77
+ formatValue = (v) => v.toLocaleString(),
78
+ formatX = (v) => String(v),
79
+ class: className,
80
+ }: Props = $props();
81
+
82
+ // =========================
83
+ // STATE
84
+ // =========================
85
+ let active = $state<TData | null>(null);
86
+
87
+ // ========================= FUNCTIONS ========================= WHY: called from the tooltip header's value expression so it re-runs on every active-point change; an unread {@const} side effect only evaluates once.
88
+
89
+ // The microtask defers the state write out of template evaluation.
90
+ function trackActive(d: TData): string {
91
+ queueMicrotask(() => {
92
+ active = d;
93
+ });
94
+ return formatX(d[xKey]);
95
+ }
96
+
97
+ // =========================
98
+ // DERIVED
99
+ // =========================
100
+ const uid = `hr-line-${Math.random().toString(36).slice(2, 8)}`;
101
+ const series = $derived(
102
+ seriesInput && seriesInput.length > 0
103
+ ? seriesInput.map((s, i) => ({
104
+ key: s.key,
105
+ label: s.label ?? s.key,
106
+ color: s.color ?? `var(--chart-${(i % 5) + 1})`,
107
+ }))
108
+ : yKey
109
+ ? [{ key: yKey, label: yKey, color }]
110
+ : [],
111
+ );
112
+ const yMax = $derived(
113
+ Math.max(0, ...data.flatMap((d) => series.map((s) => Number(d[s.key] ?? 0)))),
114
+ );
115
+ const curve = $derived(smooth ? curveMonotoneX : undefined);
116
+ const fade = $derived(resolveEdgeFade(edgeFade));
117
+ const fadeMask = $derived(fade > 0 ? `url(#${uid}-fade)` : undefined);
118
+ const fallback = $derived(data.length > 0 ? data[data.length - 1] : null);
119
+ const display = $derived(active ?? fallback);
120
+ // WHY: a dashed tail is two splines sharing the boundary point so the solid
121
+ // and dashed segments meet without a gap.
122
+ const hasTail = $derived(
123
+ dashFromIndex != null && dashFromIndex >= 0 && dashFromIndex < data.length - 1,
124
+ );
125
+ const solidData = $derived(hasTail ? data.slice(0, (dashFromIndex as number) + 1) : data);
126
+ const tailData = $derived(hasTail ? data.slice(dashFromIndex as number) : []);
127
+ const figureRows = $derived(
128
+ display
129
+ ? series.map((s) => ({ value: Number(display[s.key]), label: s.label, color: s.color }))
130
+ : null,
131
+ );
132
+ </script>
133
+
134
+ <!-- =========================== -->
135
+ <!-- LINE CHART -->
136
+ <!-- =========================== -->
137
+ <div
138
+ class="hr-line-chart {className ?? ''}"
139
+ role="img"
140
+ aria-label="Line chart"
141
+ onpointerleave={() => (active = null)}
142
+ >
143
+ {#if figureRows && display}
144
+ <Figure series={figureRows} eyebrow={formatX(display[xKey])} format={formatValue} size="md" />
145
+ {:else}
146
+ <Figure value="—" eyebrow="Latest" size="md" />
147
+ {/if}
148
+
149
+ <div class="hr-chart-body" style:height="{height}px">
150
+ {#snippet strokes(s: (typeof series)[number])}
151
+ {#if hasTail}
152
+ <Spline
153
+ data={solidData}
154
+ y={s.key}
155
+ {curve}
156
+ style="stroke: {s.color}; fill: none"
157
+ stroke-width="2"
158
+ stroke-linecap="round"
159
+ />
160
+ <Spline
161
+ data={tailData}
162
+ y={s.key}
163
+ {curve}
164
+ style="stroke: {s.color}; fill: none"
165
+ stroke-width="2"
166
+ stroke-dasharray={dashArray}
167
+ />
168
+ {:else}
169
+ <Spline
170
+ y={s.key}
171
+ {curve}
172
+ style="stroke: {s.color}; fill: none"
173
+ stroke-width="2"
174
+ stroke-linecap="round"
175
+ />
176
+ {/if}
177
+ {/snippet}
178
+ {#if data.length === 0}
179
+ <div class="empty"><p>No data</p></div>
180
+ {:else}
181
+ <Chart
182
+ {data}
183
+ x={xKey}
184
+ y={series[0]?.key}
185
+ yDomain={[0, yMax || 1]}
186
+ yNice
187
+ padding={{ left: 48, bottom: 28, top: 8, right: 12 }}
188
+ tooltipContext={{ mode: "bisect-x" }}
189
+ motion={DOMAIN_MOTION}
190
+ >
191
+ <Svg>
192
+ <Axis placement="left" format={(v) => formatValue(Number(v))} ticks={4} />
193
+ <Axis placement="bottom" format={(v) => formatX(v)} />
194
+ <g class="hr-line-grid">
195
+ <Axis placement="left" grid rule={false} ticks={4} format={() => ""} />
196
+ </g>
197
+ {#if fade > 0}
198
+ <EdgeFade id="{uid}-fade" amount={fade} />
199
+ {/if}
200
+ <Crosshair id="{uid}-cross" />
201
+ <HoverFocus>
202
+ <Reveal>
203
+ {#each series as s, i (s.key)}
204
+ <LinearGradient id="{uid}-{i}" vertical>
205
+ {#snippet stopsContent()}
206
+ <stop offset="0%" stop-color={s.color} stop-opacity="0.22" />
207
+ <stop offset="100%" stop-color={s.color} stop-opacity="0.01" />
208
+ {/snippet}
209
+ </LinearGradient>
210
+ <Area
211
+ y1={s.key}
212
+ {curve}
213
+ fill="url(#{uid}-{i})"
214
+ mask={fadeMask}
215
+ line={false}
216
+ class="hr-line-area"
217
+ />
218
+ {@render strokes(s)}
219
+ {#if showMarkers}
220
+ <Points
221
+ y={s.key}
222
+ r={3}
223
+ fill="var(--background, white)"
224
+ stroke={s.color}
225
+ stroke-width={2}
226
+ class="hr-line-marker"
227
+ />
228
+ {/if}
229
+ {/each}
230
+ </Reveal>
231
+ {#snippet segment()}
232
+ {#each series as s (s.key)}
233
+ {@render strokes(s)}
234
+ {/each}
235
+ {/snippet}
236
+ </HoverFocus>
237
+ <Highlight points lines={{ style: `stroke: url(#${uid}-cross)` }} />
238
+ </Svg>
239
+ <Tooltip.Root x="data" y={TOOLTIP_TOP} xOffset={TOOLTIP_GAP}>
240
+ {#snippet children({ data: d })}
241
+ <Tooltip.Header value={trackActive(d as TData)} />
242
+ <Tooltip.List>
243
+ {#each series as s (s.key)}
244
+ <Tooltip.Item
245
+ label={s.label}
246
+ value={formatValue(Number(d[s.key]))}
247
+ color={s.color}
248
+ />
249
+ {/each}
250
+ </Tooltip.List>
251
+ {/snippet}
252
+ </Tooltip.Root>
253
+ </Chart>
254
+ {/if}
255
+ </div>
256
+ </div>
@@ -0,0 +1,19 @@
1
+ import { cubicInOut } from "svelte/easing";
2
+
3
+ /** Mirrors `--chart-motion-reveal` in charts.css, for when no element is mounted to read it from. */
4
+ export const REVEAL_MS = 1100;
5
+
6
+ /** Rescales the axes when the data changes; the first render never animates. */
7
+ export const DOMAIN_MOTION = { type: "tween", duration: 500, easing: cubicInOut } as const;
8
+
9
+ /** The tooltip sits beside the hovered point at the top of the plot, so it never chases the cursor. */
10
+ export const TOOLTIP_TOP = 8;
11
+ export const TOOLTIP_GAP = 16;
12
+
13
+ /** A chart duration token in ms, read off a mounted element; both `ms` and `s` parse. */
14
+ export function tokenMs(el: Element, name: string, fallback: number): number {
15
+ const raw = getComputedStyle(el).getPropertyValue(name).trim();
16
+ const n = Number.parseFloat(raw);
17
+ if (Number.isNaN(n)) return fallback;
18
+ return raw.endsWith("ms") ? n : n * 1000;
19
+ }
@@ -0,0 +1,179 @@
1
+ /* Styles for NumberChart. Plain stylesheet (see area-chart.css for why). All
2
+ selectors are already namespaced under `.hr-number-chart`. */
3
+
4
+ .hr-number-chart {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 0.375rem;
8
+ min-width: 0;
9
+ padding: 1rem 1.25rem;
10
+ border: 1px solid var(--border, oklch(0.92 0.004 286));
11
+ border-radius: 0.75rem;
12
+ background: var(--card, white);
13
+ box-shadow: var(--chart-card-shadow);
14
+ transition: border-color var(--chart-motion, 250ms) var(--chart-ease-out, ease);
15
+ }
16
+ @media (hover: hover) and (pointer: fine) {
17
+ .hr-number-chart:hover {
18
+ border-color: color-mix(in srgb, var(--ring, var(--chart-1)) 28%, var(--border, transparent));
19
+ }
20
+ }
21
+ /* WHY a bare mode and not a second component: a band is one panel with
22
+ hairlines through it, so a figure inside it must bring no edge of its own.
23
+ Same object, one less border. */
24
+ .hr-number-chart[data-bare] {
25
+ padding: 1rem;
26
+ border: 0;
27
+ border-radius: 0;
28
+ box-shadow: none;
29
+ }
30
+ @media (hover: hover) and (pointer: fine) {
31
+ .hr-number-chart[data-bare]:hover {
32
+ border-color: transparent;
33
+ }
34
+ }
35
+
36
+ .hr-number-label {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 0.375rem;
40
+ margin: 0;
41
+ font-size: 0.75rem;
42
+ font-weight: 500;
43
+ line-height: 1.4;
44
+ color: var(--muted-foreground, oklch(0.5 0.01 286));
45
+ }
46
+ .hr-number-label-text {
47
+ min-width: 0;
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ }
52
+ .hr-number-icon {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ flex: none;
56
+ }
57
+ .hr-number-trailing {
58
+ margin-left: auto;
59
+ flex: none;
60
+ font-size: 0.625rem;
61
+ }
62
+
63
+ /* WHY the row is baseline-aligned and top-anchored: a band mixes figures that
64
+ carry a caption with figures that do not, and every value in the row has to
65
+ sit on one line. The slack falls to the bottom, where nothing reads it. */
66
+ .hr-number-row {
67
+ display: flex;
68
+ align-items: baseline;
69
+ justify-content: space-between;
70
+ gap: 0.5rem;
71
+ }
72
+
73
+ /* WHY the heading family and not the body one: a figure is structure, not
74
+ prose, and tabular digits are what makes a column of them scannable. */
75
+ .hr-number-value {
76
+ font-family: var(--font-heading, inherit);
77
+ font-weight: 600;
78
+ font-variant-numeric: tabular-nums;
79
+ font-feature-settings: "tnum";
80
+ line-height: 1.15;
81
+ letter-spacing: -0.015em;
82
+ color: var(--foreground, oklch(0.2 0.01 286));
83
+ min-width: 0;
84
+ }
85
+ .hr-number-chart[data-size="md"] .hr-number-value {
86
+ font-size: 1.25rem;
87
+ }
88
+ /* WHY the lead figure is size and not colour: the accent is spent on actions,
89
+ active states, marks and links. A number is none of those. */
90
+ .hr-number-chart[data-size="lg"] .hr-number-value {
91
+ font-size: 1.5rem;
92
+ }
93
+ /* A loss is the one case where the figure itself carries colour. */
94
+ .hr-number-chart[data-negative] .hr-number-value {
95
+ color: var(--destructive);
96
+ }
97
+
98
+ .hr-number-delta {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ gap: 0.125rem;
102
+ flex: none;
103
+ font-size: 0.75rem;
104
+ font-weight: 500;
105
+ line-height: 1.2;
106
+ color: var(--muted-foreground, oklch(0.5 0.01 286));
107
+ }
108
+ .hr-number-delta-text {
109
+ font-variant-numeric: tabular-nums;
110
+ }
111
+ /* The readable form of a trend colour: a hue picked to read as a mark measures
112
+ about 3.5:1 as 12px text, so it is mixed toward the page foreground. One rule
113
+ serves both themes and the package needs to know nothing about which is on. */
114
+ .hr-number-delta[data-tone="up"] {
115
+ color: color-mix(
116
+ in oklab,
117
+ var(--stat-up, var(--success, oklch(0.6 0.13 163))) 72%,
118
+ var(--foreground, oklch(0.2 0.01 286))
119
+ );
120
+ }
121
+ .hr-number-delta[data-tone="down"] {
122
+ color: color-mix(
123
+ in oklab,
124
+ var(--stat-down, var(--destructive, oklch(0.58 0.22 25))) 72%,
125
+ var(--foreground, oklch(0.2 0.01 286))
126
+ );
127
+ }
128
+ .hr-number-delta[data-tone="none"] {
129
+ color: color-mix(in srgb, var(--muted-foreground, oklch(0.5 0.01 286)) 40%, transparent);
130
+ }
131
+ .hr-number-chart .hr-number-arrow {
132
+ width: 0.875rem;
133
+ height: 0.875rem;
134
+ flex: none;
135
+ }
136
+
137
+ .hr-number-track {
138
+ height: 0.25rem;
139
+ border-radius: 999px;
140
+ overflow: hidden;
141
+ background: color-mix(in srgb, var(--border, oklch(0.92 0.004 286)) 80%, transparent);
142
+ }
143
+ .hr-number-fill {
144
+ display: block;
145
+ height: 100%;
146
+ border-radius: inherit;
147
+ /* The share is a scale, not a width, so a change never reflows the card. */
148
+ transform: scaleX(var(--hr-number-share, 0));
149
+ transform-origin: left center;
150
+ /* backwards, not both: once the fill-in ends, the transition owns transform. */
151
+ animation: hr-number-fill-in var(--chart-motion-slow, 800ms) var(--chart-ease-out, ease) backwards;
152
+ transition: transform var(--chart-motion, 250ms) var(--chart-ease-out, ease);
153
+ }
154
+ @keyframes hr-number-fill-in {
155
+ from {
156
+ transform: scaleX(0);
157
+ }
158
+ }
159
+
160
+ .hr-number-caption {
161
+ margin: 0;
162
+ font-size: 0.75rem;
163
+ line-height: 1.4;
164
+ color: var(--muted-foreground, oklch(0.5 0.01 286));
165
+ overflow: hidden;
166
+ text-overflow: ellipsis;
167
+ white-space: nowrap;
168
+ }
169
+ .hr-number-share {
170
+ font-variant-numeric: tabular-nums;
171
+ font-weight: 600;
172
+ color: var(--foreground, oklch(0.2 0.01 286));
173
+ }
174
+
175
+ @media (prefers-reduced-motion: reduce) {
176
+ .hr-number-fill {
177
+ animation: none;
178
+ }
179
+ }