@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,80 @@
1
+ // Where a contextual panel sits so it never covers the selection's grips: beside the selection, on the side with room.
2
+
3
+ export interface Box {
4
+ left: number;
5
+ top: number;
6
+ right: number;
7
+ bottom: number;
8
+ }
9
+
10
+ export interface Size {
11
+ width: number;
12
+ height: number;
13
+ }
14
+
15
+ export type Side = "right" | "left" | "below" | "above";
16
+
17
+ /** The panel's gap from the selection: past the grips, which stick out half their size and have a hit area around them. */
18
+ export const CLEARANCE = 20;
19
+ /** The size chip hangs under a selected card, so a panel below leaves room for it. */
20
+ const CHIP_ROOM = 40;
21
+ /** The canvas's own docks: the rail on the left, the pill along the bottom, a small margin elsewhere. */
22
+ const INSET = { left: 72, top: 12, right: 12, bottom: 76 };
23
+ /** The minimap fills the bottom-right corner, so a panel that reaches it stops above it. */
24
+ const MINIMAP = { width: 232, height: 172 };
25
+
26
+ /**
27
+ * The first side with room for the panel (right, left, below, above), centred on the
28
+ * selection. Null when none fits, and the caller docks it at the edge.
29
+ */
30
+ export function placeBeside(
31
+ selection: Box,
32
+ panel: Size,
33
+ canvas: Size,
34
+ ): { x: number; y: number; side: Side } | null {
35
+ const free = {
36
+ left: INSET.left,
37
+ top: INSET.top,
38
+ right: canvas.width - INSET.right,
39
+ bottom: canvas.height - INSET.bottom,
40
+ };
41
+ const centreX = (selection.left + selection.right) / 2 - panel.width / 2;
42
+ const centreY = (selection.top + selection.bottom) / 2 - panel.height / 2;
43
+ // The lowest a panel may reach where it stands: above the minimap when it reaches that corner.
44
+ const floor = (x: number, width: number) =>
45
+ x + width > canvas.width - MINIMAP.width ? canvas.height - MINIMAP.height : free.bottom;
46
+ const clampX = (x: number) => Math.min(Math.max(x, free.left), free.right - panel.width);
47
+ const clampY = (y: number, x: number) =>
48
+ Math.min(Math.max(y, free.top), floor(x, panel.width) - panel.height);
49
+
50
+ const tries: Array<{ side: Side; x: number; y: number; fits: boolean }> = [
51
+ {
52
+ side: "right",
53
+ x: selection.right + CLEARANCE,
54
+ y: clampY(centreY, selection.right + CLEARANCE),
55
+ fits: selection.right + CLEARANCE + panel.width <= free.right,
56
+ },
57
+ {
58
+ side: "left",
59
+ x: selection.left - CLEARANCE - panel.width,
60
+ y: clampY(centreY, selection.left - CLEARANCE - panel.width),
61
+ fits: selection.left - CLEARANCE - panel.width >= free.left,
62
+ },
63
+ {
64
+ side: "below",
65
+ x: clampX(centreX),
66
+ y: selection.bottom + CLEARANCE + CHIP_ROOM,
67
+ fits:
68
+ selection.bottom + CLEARANCE + CHIP_ROOM + panel.height <=
69
+ floor(clampX(centreX), panel.width),
70
+ },
71
+ {
72
+ side: "above",
73
+ x: clampX(centreX),
74
+ y: selection.top - CLEARANCE - panel.height,
75
+ fits: selection.top - CLEARANCE - panel.height >= free.top,
76
+ },
77
+ ];
78
+ const found = tries.find((attempt) => attempt.fits);
79
+ return found ? { x: found.x, y: found.y, side: found.side } : null;
80
+ }
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { shortcutTip } from "./shortcut-tip.js";
3
+
4
+ describe("shortcutTip", () => {
5
+ it("joins Mac glyphs with no separator", () => {
6
+ expect(shortcutTip("Redo", ["mod", "shift", "Z"], true)).toBe("Redo ⌘⇧Z");
7
+ });
8
+
9
+ it("spells the modifiers out elsewhere", () => {
10
+ expect(shortcutTip("Undo", ["mod", "Z"], false)).toBe("Undo Ctrl+Z");
11
+ });
12
+
13
+ it("returns the bare label with no key", () => {
14
+ expect(shortcutTip("Fit", [])).toBe("Fit");
15
+ });
16
+ });
@@ -0,0 +1,17 @@
1
+ // The text a tool bar tooltip shows beside its label, so every button names its key the same way.
2
+
3
+ const GLYPHS: Record<string, { mac: string; other: string }> = {
4
+ mod: { mac: "⌘", other: "Ctrl" },
5
+ shift: { mac: "⇧", other: "Shift" },
6
+ };
7
+
8
+ export function isMacPlatform(): boolean {
9
+ return typeof navigator !== "undefined" && navigator.platform.includes("Mac");
10
+ }
11
+
12
+ /** "Undo ⌘Z" on a Mac, "Undo Ctrl+Z" elsewhere. `keys` are the same words the shortcut list uses. */
13
+ export function shortcutTip(label: string, keys: readonly string[], mac = isMacPlatform()): string {
14
+ if (keys.length === 0) return label;
15
+ const parts = keys.map((key) => GLYPHS[key]?.[mac ? "mac" : "other"] ?? key);
16
+ return `${label} ${parts.join(mac ? "" : "+")}`;
17
+ }
@@ -0,0 +1,146 @@
1
+ // The tool bar's tools, their keys and the shortcut list the help panel renders.
2
+ // Pure data and two lookups, so the keyboard, the bar and the panel can't disagree.
3
+
4
+ import { PEN_DEFAULTS, type PenTool, type ShapeKind } from "../pen/types.js";
5
+
6
+ export type CanvasTool = "select" | "hand" | "connect" | "frame" | PenTool;
7
+
8
+ export interface ToolDef {
9
+ id: CanvasTool;
10
+ key: string;
11
+ }
12
+
13
+ /** Every tool with its single-letter key. Shapes share one bar button but keep a key each. */
14
+ export const TOOLS: readonly ToolDef[] = [
15
+ { id: "select", key: "V" },
16
+ { id: "hand", key: "H" },
17
+ { id: "connect", key: "L" },
18
+ { id: "pen", key: "P" },
19
+ { id: "highlighter", key: "M" },
20
+ { id: "eraser", key: "E" },
21
+ { id: "text", key: "T" },
22
+ { id: "rectangle", key: "R" },
23
+ { id: "ellipse", key: "O" },
24
+ { id: "diamond", key: "D" },
25
+ { id: "frame", key: "F" },
26
+ ];
27
+
28
+ const TOOL_BY_KEY = new Map(TOOLS.map((tool) => [tool.key.toLowerCase(), tool.id]));
29
+
30
+ /** The tool a bare letter selects. Callers rule out modifiers and typing targets first. */
31
+ export function toolForKey(key: string): CanvasTool | null {
32
+ return TOOL_BY_KEY.get(key.toLowerCase()) ?? null;
33
+ }
34
+
35
+ export function keyForTool(tool: CanvasTool): string {
36
+ return TOOLS.find((entry) => entry.id === tool)?.key ?? "";
37
+ }
38
+
39
+ /** True for every tool the pen overlay handles. Select, hand and connect leave the pointer to xyflow, and frame has its own layer. */
40
+ export function isDrawingTool(tool: CanvasTool): tool is PenTool {
41
+ return tool !== "select" && tool !== "hand" && tool !== "connect" && tool !== "frame";
42
+ }
43
+
44
+ /** The frame tool drags out a box rather than a mark, so it has its own layer. */
45
+ export function isFrameTool(tool: CanvasTool): tool is "frame" {
46
+ return tool === "frame";
47
+ }
48
+
49
+ export function isShapeTool(tool: CanvasTool): tool is ShapeKind {
50
+ return tool === "rectangle" || tool === "ellipse" || tool === "diamond";
51
+ }
52
+
53
+ // =========================
54
+ // BRUSH SIZES
55
+ // =========================
56
+
57
+ export type BrushLevel = 0 | 1 | 2;
58
+
59
+ const BRUSH_SIZES = {
60
+ pen: [2, PEN_DEFAULTS.pen.size, 8],
61
+ highlighter: [12, PEN_DEFAULTS.highlighter.size, 32],
62
+ } as const;
63
+
64
+ export const DEFAULT_BRUSH_LEVEL: BrushLevel = 1;
65
+
66
+ /** Flow-unit width for a tool at a level. Tools with no brush return undefined and take their default. */
67
+ export function brushSize(tool: CanvasTool, level: BrushLevel): number | undefined {
68
+ if (tool === "pen" || tool === "highlighter") return BRUSH_SIZES[tool][level];
69
+ return undefined;
70
+ }
71
+
72
+ export function hasBrush(tool: CanvasTool): boolean {
73
+ return tool === "pen" || tool === "highlighter";
74
+ }
75
+
76
+ /** The three tools that share one rail button, the last one used. */
77
+ export type BrushTool = "pen" | "highlighter" | "eraser";
78
+
79
+ export function isBrushTool(tool: CanvasTool): tool is BrushTool {
80
+ return tool === "pen" || tool === "highlighter" || tool === "eraser";
81
+ }
82
+
83
+ /** Colour applies to every drawing tool except the eraser. */
84
+ export function hasColor(tool: CanvasTool): boolean {
85
+ return isDrawingTool(tool) && tool !== "eraser";
86
+ }
87
+
88
+ // =========================
89
+ // SHORTCUT LIST
90
+ // =========================
91
+
92
+ export type ShortcutGroup = "tools" | "edit" | "view";
93
+
94
+ export interface Shortcut {
95
+ /** A stable id the app maps to a translated label. */
96
+ id: string;
97
+ group: ShortcutGroup;
98
+ /** "mod" renders as Cmd or Ctrl, "shift" as the shift glyph. Anything else is shown as typed. */
99
+ keys: readonly string[];
100
+ }
101
+
102
+ /** One row per key the canvas owns. The panel is built from this list and nothing else. */
103
+ export const SHORTCUTS: readonly Shortcut[] = [
104
+ { id: "select", group: "tools", keys: ["V"] },
105
+ { id: "hand", group: "tools", keys: ["H"] },
106
+ { id: "connect", group: "tools", keys: ["L"] },
107
+ { id: "pen", group: "tools", keys: ["P"] },
108
+ { id: "highlighter", group: "tools", keys: ["M"] },
109
+ { id: "eraser", group: "tools", keys: ["E"] },
110
+ { id: "text", group: "tools", keys: ["T"] },
111
+ { id: "rectangle", group: "tools", keys: ["R"] },
112
+ { id: "ellipse", group: "tools", keys: ["O"] },
113
+ { id: "diamond", group: "tools", keys: ["D"] },
114
+ { id: "frame", group: "tools", keys: ["F"] },
115
+ { id: "note", group: "tools", keys: ["N"] },
116
+ { id: "cards", group: "tools", keys: ["C"] },
117
+ { id: "comment", group: "tools", keys: ["B"] },
118
+ { id: "panTemporarily", group: "tools", keys: ["Space"] },
119
+ { id: "undo", group: "edit", keys: ["mod", "Z"] },
120
+ { id: "redo", group: "edit", keys: ["mod", "shift", "Z"] },
121
+ { id: "copy", group: "edit", keys: ["mod", "C"] },
122
+ { id: "paste", group: "edit", keys: ["mod", "V"] },
123
+ { id: "selectAll", group: "edit", keys: ["mod", "A"] },
124
+ { id: "multiSelect", group: "edit", keys: ["shift", "Click"] },
125
+ { id: "duplicate", group: "edit", keys: ["mod", "D"] },
126
+ { id: "group", group: "edit", keys: ["mod", "G"] },
127
+ { id: "ungroup", group: "edit", keys: ["mod", "shift", "G"] },
128
+ { id: "bringForward", group: "edit", keys: ["]"] },
129
+ { id: "sendBackward", group: "edit", keys: ["["] },
130
+ { id: "bringToFront", group: "edit", keys: ["mod", "]"] },
131
+ { id: "sendToBack", group: "edit", keys: ["mod", "["] },
132
+ { id: "lock", group: "edit", keys: ["mod", "shift", "L"] },
133
+ { id: "copyAsPng", group: "edit", keys: ["mod", "shift", "C"] },
134
+ { id: "nudge", group: "edit", keys: ["Arrows"] },
135
+ { id: "nudgeFar", group: "edit", keys: ["shift", "Arrows"] },
136
+ { id: "delete", group: "edit", keys: ["Del"] },
137
+ { id: "save", group: "edit", keys: ["mod", "S"] },
138
+ { id: "zoomIn", group: "view", keys: ["+"] },
139
+ { id: "zoomOut", group: "view", keys: ["-"] },
140
+ { id: "zoom100", group: "view", keys: ["shift", "0"] },
141
+ { id: "zoomFit", group: "view", keys: ["shift", "1"] },
142
+ { id: "zoomSelection", group: "view", keys: ["shift", "2"] },
143
+ { id: "present", group: "view", keys: ["mod", "Enter"] },
144
+ { id: "shortcuts", group: "view", keys: ["?"] },
145
+ { id: "escape", group: "view", keys: ["Esc"] },
146
+ ];
@@ -0,0 +1,104 @@
1
+ # @heyramzi/ui/charts
2
+
3
+ Svelte 5 charts on [LayerChart](https://layerchart.com): area, line, bar, donut, ring, gauge, sparkline, time series and stat cards. One set of tokens, a crosshair tooltip, a screen reader data table on the bar chart, and an entrance sweep that respects `prefers-reduced-motion`.
4
+
5
+ Live demos: [ui.heyramzi.com/charts](https://ui.heyramzi.com/charts).
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pnpm add @heyramzi/ui
11
+ ```
12
+
13
+ `svelte` (5.57 or newer) is a peer. LayerChart, d3-scale and d3-shape come with the package.
14
+
15
+ ## Setup
16
+
17
+ The charts read the shadcn token set (`--foreground`, `--border`, `--popover`, `--chart-1` to `--chart-5`) and a few motion tokens. The quickest way to get all of them is the token sheet from `@heyramzi/ui`:
18
+
19
+ ```css
20
+ @import "tailwindcss";
21
+ @import "@heyramzi/ui/tokens.css";
22
+ @import "@heyramzi/ui/animations";
23
+
24
+ @source "../node_modules/@heyramzi/ui";
25
+ ```
26
+
27
+ The `@source` line matters: the chart markup carries Tailwind classes, and Tailwind only scans your own app unless you point it at the package. If you define the tokens yourself, import only the charts subpath. Dark mode keys off a `.dark` class on `html`.
28
+
29
+ ## Usage
30
+
31
+ ```svelte
32
+ <script lang="ts">
33
+ import { AreaChart } from "@heyramzi/ui/charts";
34
+
35
+ const data = [
36
+ { month: "Jan", revenue: 3200, cost: 1800 },
37
+ { month: "Feb", revenue: 4100, cost: 2100 },
38
+ { month: "Mar", revenue: 3800, cost: 2400 },
39
+ ];
40
+ </script>
41
+
42
+ <AreaChart
43
+ {data}
44
+ xKey="month"
45
+ series={[
46
+ { key: "revenue", label: "Revenue", color: "var(--chart-1)" },
47
+ { key: "cost", label: "Cost", color: "var(--chart-3)" },
48
+ ]}
49
+ formatValue={(v) => `$${v.toLocaleString()}`}
50
+ height={260}
51
+ />
52
+ ```
53
+
54
+ ## What is inside
55
+
56
+ | Export | What it is |
57
+ | --- | --- |
58
+ | `AreaChart`, `LineChart`, `BarChart` | Cartesian charts with a crosshair, soft edges and an entrance sweep |
59
+ | `TimeSeriesChart` | Dates on the x axis, a comparison line and a tooltip |
60
+ | `DonutChart`, `RingChart`, `GaugeChart` | Round charts, with a hover readout in the middle |
61
+ | `Sparkline` | A tiny trend line for tables and cards |
62
+ | `StatCard`, `NumberChart`, `TrendPill`, `AnimatedValue` | A headline number that counts up, with a delta that knows whether up is good news |
63
+ | `Legend`, `Figure`, `EdgeFade` | Legend rows, a captioned figure frame and an edge mask |
64
+ | `ChartContainer`, `ChartTooltip`, `ChartStyle`, `useChart` | The shadcn-style wrapper, for a chart you build yourself on LayerChart |
65
+
66
+ ## Dashboard
67
+
68
+ `@heyramzi/ui/charts/dashboard` is the page kit around the charts: the parts a reporting page repeats. It sits on the segmented control, the data table pager and the panel label from the rest of `@heyramzi/ui`.
69
+
70
+ | Export | What it is |
71
+ | --- | --- |
72
+ | `DashboardSection`, `PanelGrid`, `StatBand` | A named area, a row of panels side by side, a row of number cards |
73
+ | `StatTile` | A number card on `NumberChart`: a delta, a share track, a trend shape behind the figure, an optional link |
74
+ | `DataPanel`, `DashboardEmpty` | A titled panel for a table or chart, and the dashed empty state |
75
+ | `DataTable`, `DataSortHead`, `RatingChip` | The one data table: sortable heads, a row that opens, columns that hide on a phone, share bars, loading and empty states. An A to E grade chip |
76
+ | `PeriodFilter`, `DAY_WINDOWS` | The 7, 30 and 90 day window, as a filter or as links |
77
+ | `CHART_HEIGHT`, `dataTableClass` and friends | The one chart height and the table skin |
78
+
79
+ ```svelte
80
+ <script lang="ts">
81
+ import { DashboardSection, StatBand, StatTile } from "@heyramzi/ui/charts/dashboard";
82
+ </script>
83
+
84
+ <DashboardSection title="Revenue" note="Last 30 days">
85
+ <StatBand>
86
+ <StatTile label="Sales" value="$12,480" deltaPct={8.2} direction="up-good" lead />
87
+ </StatBand>
88
+ </DashboardSection>
89
+ ```
90
+
91
+ ## Moving from `@heyramzi/ui/charts`
92
+
93
+ The charts left `@heyramzi/ui` in its next major. Swap the import and install the new package:
94
+
95
+ ```diff
96
+ -import { AreaChart } from "@heyramzi/ui/charts";
97
+ +import { AreaChart } from "@heyramzi/ui/charts";
98
+ ```
99
+
100
+ Nothing else changed: the same exports, the same props, the same CSS.
101
+
102
+ ## License
103
+
104
+ MIT
@@ -0,0 +1,4 @@
1
+ /**
2
+ * A stylesheet imported for its side effect, bundled by the consumer's Vite.
3
+ */
4
+ declare module "*.css";
@@ -0,0 +1,7 @@
1
+ /* Styles for AnimatedValue. Plain stylesheet (see area-chart.css for why).
2
+ `.hr-animated-value` is the component's own namespaced fallback for tabular
3
+ figures (it also carries the `tabular-nums` utility class). */
4
+
5
+ .hr-animated-value {
6
+ font-variant-numeric: tabular-nums;
7
+ }
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import "./animated-value.css";
6
+ import { untrack } from "svelte";
7
+ import { cn } from "../lib/utils.js";
8
+
9
+ // =========================
10
+ // TYPES
11
+ // =========================
12
+ interface Props {
13
+ /** Target number. The display counts up to it on mount and tweens on change. */
14
+ value: number;
15
+ /** Fixed decimal places when no `formatOptions` is given. */
16
+ decimals?: number;
17
+ /** Count-up duration in ms. */
18
+ duration?: number;
19
+ /** Custom formatter for the live number. Wins over `formatOptions`/`decimals`. */
20
+ format?: (value: number) => string;
21
+ /** Intl number formatting (currency, compact, percent, ...). Wins over `decimals`. */
22
+ formatOptions?: Intl.NumberFormatOptions;
23
+ /** BCP-47 locale for `formatOptions`. */
24
+ locale?: string;
25
+ /** Rendered before the number. */
26
+ prefix?: string;
27
+ /** Rendered after the number. */
28
+ suffix?: string;
29
+ class?: string;
30
+ style?: string;
31
+ };
32
+
33
+ // =========================
34
+ // PROPS
35
+ // =========================
36
+ let {
37
+ value,
38
+ decimals = 0,
39
+ duration = 800,
40
+ format,
41
+ formatOptions,
42
+ locale,
43
+ prefix,
44
+ suffix,
45
+ class: className,
46
+ style,
47
+ }: Props = $props();
48
+
49
+ // =========================
50
+ // STATE
51
+ // =========================
52
+ let displayed = $state(0);
53
+ let started = $state(false);
54
+
55
+ // =========================
56
+ // DERIVED
57
+ // =========================
58
+ const formatter = $derived(formatOptions ? new Intl.NumberFormat(locale, formatOptions) : null);
59
+ const currentValue = $derived(started ? displayed : value);
60
+ const text = $derived(
61
+ format
62
+ ? format(currentValue)
63
+ : formatter
64
+ ? formatter.format(currentValue)
65
+ : currentValue.toFixed(decimals),
66
+ );
67
+
68
+ // ========================= EFFECTS ========================= WHY: $effect only runs in the browser, so SSR renders the final value (no flash).
69
+
70
+ // `value` is the sole tracked dependency; `displayed` is read and written inside untrack() so the per-frame writes don't retrigger the effect (that self-dependency loops and Svelte bails, freezing the number).
71
+
72
+ // The first run sweeps from 0; later value changes tween from the current display.
73
+ $effect(() => {
74
+ const target = value;
75
+ let raf = 0;
76
+ untrack(() => {
77
+ const reduced =
78
+ typeof window !== "undefined" &&
79
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
80
+
81
+ const from = started ? displayed : 0;
82
+ started = true;
83
+
84
+ // WHY: snap (no tween) when motion is off, the delta is tiny, or the tab
85
+
86
+ // is hidden. rAF is paused in background tabs, so animating there would leave the number stale until the tab is shown; snapping keeps it correct.
87
+ if (reduced || document.hidden || Math.abs(target - from) < 0.005) {
88
+ displayed = target;
89
+ return;
90
+ }
91
+
92
+ const start = performance.now();
93
+ const tick = (now: number) => {
94
+ const t = Math.min((now - start) / duration, 1);
95
+ const eased = 1 - (1 - t) ** 3;
96
+ displayed = from + (target - from) * eased;
97
+ if (t < 1) raf = requestAnimationFrame(tick);
98
+ };
99
+ raf = requestAnimationFrame(tick);
100
+ });
101
+ return () => cancelAnimationFrame(raf);
102
+ });
103
+ </script>
104
+
105
+ <!-- =========================== -->
106
+ <!-- MARKUP -->
107
+ <!-- =========================== -->
108
+ <span class={cn("hr-animated-value tabular-nums", className)} {style}
109
+ >{prefix ?? ""}{text}{suffix ?? ""}</span
110
+ >
@@ -0,0 +1,29 @@
1
+ /* Styles for AreaChart. Shipped as a plain stylesheet (imported by the
2
+ component) instead of a Svelte `<style>` block: a `<style>` block becomes a
3
+ `?type=style&lang.css` virtual module that Tailwind v4's Vite plugin mis-parses
4
+ when this package is consumed as source from node_modules (tailwindlabs/
5
+ tailwindcss#20000). Selectors are namespaced under `.hr-area-chart`. */
6
+
7
+ .hr-area-chart {
8
+ width: 100%;
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 0.5rem;
12
+ }
13
+ .hr-area-chart .hr-chart-body {
14
+ width: 100%;
15
+ }
16
+
17
+ .hr-area-chart .empty {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ height: 100%;
22
+ border: 1px dashed var(--chart-grid, oklch(0.5 0.01 286 / 0.2));
23
+ border-radius: 0.5rem;
24
+ }
25
+ .hr-area-chart .empty p {
26
+ font-size: 0.8125rem;
27
+ color: var(--chart-label, oklch(0.5 0.01 286));
28
+ margin: 0;
29
+ }