@heyramzi/ui 1.54.1 → 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 (664) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +200 -7
  4. package/scripts/sync-shadcn.mjs +2 -2
  5. package/src/ai/ai-button/AiButton.svelte +1 -1
  6. package/src/ai/confirmation/Confirmation.svelte +1 -1
  7. package/src/ai/confirmation/ConfirmationAction.svelte +1 -1
  8. package/src/ai/confirmation/ConfirmationActions.svelte +1 -1
  9. package/src/ai/confirmation/ConfirmationTitle.svelte +1 -1
  10. package/src/ai/generation-progress/GenerationProgress.svelte +13 -13
  11. package/src/ai/loading-state/PixelLoader.svelte +1 -1
  12. package/src/ai/prompt-input/PromptInput.svelte +1 -1
  13. package/src/ai/prompt-input/PromptInputBody.svelte +1 -1
  14. package/src/ai/prompt-input/PromptInputSubmit.svelte +1 -1
  15. package/src/ai/prompt-input/PromptInputTextarea.svelte +1 -1
  16. package/src/ai/prompt-input/PromptInputToolbar.svelte +1 -1
  17. package/src/ai/reasoning/Reasoning.svelte +1 -1
  18. package/src/ai/reasoning/ReasoningContent.svelte +1 -1
  19. package/src/ai/reasoning/ReasoningTrigger.svelte +1 -1
  20. package/src/ai/response/Response.svelte +1 -1
  21. package/src/ai/suggestion/Suggestion.svelte +1 -1
  22. package/src/ai/suggestion/Suggestions.svelte +1 -1
  23. package/src/ai/thinking-dots/ThinkingDots.svelte +1 -1
  24. package/src/ai/typing-dots/TypingDots.svelte +1 -1
  25. package/src/ai/voice-waveform/VoiceWaveform.svelte +1 -1
  26. package/src/ambient.d.ts +8 -0
  27. package/src/brand/brand.test.ts +67 -0
  28. package/src/brand/index.ts +137 -0
  29. package/src/canvas/README.md +236 -0
  30. package/src/canvas/card/card-frame.svelte +61 -0
  31. package/src/canvas/card/card.css +39 -0
  32. package/src/canvas/card/resize-handles.svelte +119 -0
  33. package/src/canvas/card/resize.ts +33 -0
  34. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  35. package/src/canvas/chrome/chrome.css +231 -0
  36. package/src/canvas/chrome/lock-badge.svelte +18 -0
  37. package/src/canvas/chrome/side-handles.svelte +30 -0
  38. package/src/canvas/chrome/size-chip.svelte +45 -0
  39. package/src/canvas/chrome/snap-guides.svelte +69 -0
  40. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  41. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  42. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  43. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  44. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  45. package/src/canvas/edges/bend-handle.svelte +76 -0
  46. package/src/canvas/edges/edge-gradient.svelte +30 -0
  47. package/src/canvas/edges/edge-path.test.ts +58 -0
  48. package/src/canvas/edges/edge-path.ts +293 -0
  49. package/src/canvas/edges/gradient-edge.svelte +43 -0
  50. package/src/canvas/editor/context-menu.test.ts +65 -0
  51. package/src/canvas/editor/context-menu.ts +48 -0
  52. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  53. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  54. package/src/canvas/editor/editor-context.ts +62 -0
  55. package/src/canvas/editor/editor-mode.ts +67 -0
  56. package/src/canvas/editor/frame.ts +98 -0
  57. package/src/canvas/editor/hand-context.ts +15 -0
  58. package/src/canvas/editor/history-core.test.ts +239 -0
  59. package/src/canvas/editor/history-core.ts +247 -0
  60. package/src/canvas/editor/keymap.test.ts +161 -0
  61. package/src/canvas/editor/keymap.ts +151 -0
  62. package/src/canvas/editor/layout-actions.test.ts +156 -0
  63. package/src/canvas/editor/layout-actions.ts +219 -0
  64. package/src/canvas/editor/lock.test.ts +68 -0
  65. package/src/canvas/editor/lock.ts +87 -0
  66. package/src/canvas/editor/rebase.test.ts +208 -0
  67. package/src/canvas/editor/rebase.ts +272 -0
  68. package/src/canvas/editor/style.ts +184 -0
  69. package/src/canvas/editor/text-field-context.ts +41 -0
  70. package/src/canvas/editor/z-order.test.ts +75 -0
  71. package/src/canvas/editor/z-order.ts +87 -0
  72. package/src/canvas/headless/alignment.test.ts +59 -0
  73. package/src/canvas/headless/alignment.ts +23 -0
  74. package/src/canvas/headless/break-apart-find.ts +418 -0
  75. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  76. package/src/canvas/headless/break-apart-plan.ts +439 -0
  77. package/src/canvas/headless/break-apart-style.ts +106 -0
  78. package/src/canvas/headless/break-apart.ts +244 -0
  79. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  80. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  81. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  82. package/src/canvas/headless/edge-anchors.ts +143 -0
  83. package/src/canvas/headless/edge-memo.test.ts +58 -0
  84. package/src/canvas/headless/edge-memo.ts +32 -0
  85. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  86. package/src/canvas/headless/edge-serialization.ts +32 -0
  87. package/src/canvas/headless/figure-geometry.ts +293 -0
  88. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  89. package/src/canvas/headless/fit-viewport.ts +137 -0
  90. package/src/canvas/headless/grid.test.ts +34 -0
  91. package/src/canvas/headless/grid.ts +94 -0
  92. package/src/canvas/headless/icon-node.test.ts +23 -0
  93. package/src/canvas/headless/icon-node.ts +66 -0
  94. package/src/canvas/headless/index.ts +9 -0
  95. package/src/canvas/headless/linkify.test.ts +23 -0
  96. package/src/canvas/headless/linkify.ts +25 -0
  97. package/src/canvas/headless/pen-marks.ts +133 -0
  98. package/src/canvas/headless/sketch.test.ts +99 -0
  99. package/src/canvas/headless/sketch.ts +302 -0
  100. package/src/canvas/headless/snap-guides.test.ts +61 -0
  101. package/src/canvas/headless/snap-guides.ts +146 -0
  102. package/src/canvas/headless/svg-figure.test.ts +49 -0
  103. package/src/canvas/headless/svg-figure.ts +176 -0
  104. package/src/canvas/headless/table-layout.test.ts +158 -0
  105. package/src/canvas/headless/table-layout.ts +309 -0
  106. package/src/canvas/index.ts +35 -0
  107. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  108. package/src/canvas/lod/lod-switch.svelte +140 -0
  109. package/src/canvas/lod/render-all.svelte.ts +52 -0
  110. package/src/canvas/ops/arrange.ts +108 -0
  111. package/src/canvas/ops/ops.test.ts +98 -0
  112. package/src/canvas/ops/ops.ts +347 -0
  113. package/src/canvas/pen/PenField.svelte +128 -0
  114. package/src/canvas/pen/PenField.test.ts +87 -0
  115. package/src/canvas/pen/PenOverlay.svelte +172 -0
  116. package/src/canvas/pen/ShapeNode.svelte +64 -0
  117. package/src/canvas/pen/TextNode.svelte +97 -0
  118. package/src/canvas/pen/colors.ts +28 -0
  119. package/src/canvas/pen/eraser.ts +46 -0
  120. package/src/canvas/pen/geometry.ts +148 -0
  121. package/src/canvas/pen/host-field.test.svelte +13 -0
  122. package/src/canvas/pen/index.ts +40 -0
  123. package/src/canvas/pen/ops.ts +16 -0
  124. package/src/canvas/pen/pen-session.ts +177 -0
  125. package/src/canvas/pen/pen.css +149 -0
  126. package/src/canvas/pen/plugin.ts +139 -0
  127. package/src/canvas/pen/shapes.ts +104 -0
  128. package/src/canvas/pen/stroke-path.ts +54 -0
  129. package/src/canvas/pen/types.ts +96 -0
  130. package/src/canvas/pen/ui.ts +26 -0
  131. package/src/canvas/plugin.test.ts +45 -0
  132. package/src/canvas/plugin.ts +159 -0
  133. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  134. package/src/canvas/screen/PartView.svelte +176 -0
  135. package/src/canvas/screen/ScreenBody.svelte +142 -0
  136. package/src/canvas/screen/ScreenCard.svelte +32 -0
  137. package/src/canvas/screen/glyphs/button.svg +1 -0
  138. package/src/canvas/screen/glyphs/close.svg +1 -0
  139. package/src/canvas/screen/glyphs/down.svg +1 -0
  140. package/src/canvas/screen/glyphs/header.svg +1 -0
  141. package/src/canvas/screen/glyphs/hills.svg +1 -0
  142. package/src/canvas/screen/glyphs/image.svg +1 -0
  143. package/src/canvas/screen/glyphs/input.svg +1 -0
  144. package/src/canvas/screen/glyphs/list.svg +1 -0
  145. package/src/canvas/screen/glyphs/modal.svg +1 -0
  146. package/src/canvas/screen/glyphs/plus.svg +1 -0
  147. package/src/canvas/screen/glyphs/up.svg +1 -0
  148. package/src/canvas/screen/glyphs.ts +40 -0
  149. package/src/canvas/screen/labels.ts +41 -0
  150. package/src/canvas/screen/parts.ts +164 -0
  151. package/src/canvas/screen/plugin.ts +67 -0
  152. package/src/canvas/screen/screen.css +593 -0
  153. package/src/canvas/screen/types.ts +47 -0
  154. package/src/canvas/screen/ui.ts +13 -0
  155. package/src/canvas/test-utils.ts +18 -0
  156. package/src/canvas/types.ts +53 -0
  157. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  158. package/src/canvas/ui/FrameLayer.svelte +116 -0
  159. package/src/canvas/ui/PenLayer.svelte +39 -0
  160. package/src/canvas/ui/StylePanel.svelte +268 -0
  161. package/src/canvas/ui/canvas-grid.svelte +156 -0
  162. package/src/canvas/ui/canvas-ui.css +350 -0
  163. package/src/canvas/ui/forward-wheel.ts +29 -0
  164. package/src/canvas/ui/icons.ts +124 -0
  165. package/src/canvas/ui/place-panel.ts +80 -0
  166. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  167. package/src/canvas/ui/shortcut-tip.ts +17 -0
  168. package/src/canvas/ui/tools.ts +146 -0
  169. package/src/charts/README.md +104 -0
  170. package/src/charts/ambient.d.ts +4 -0
  171. package/src/charts/animated-value.css +7 -0
  172. package/src/charts/animated-value.svelte +110 -0
  173. package/src/charts/area-chart.css +29 -0
  174. package/src/charts/area-chart.svelte +204 -0
  175. package/src/charts/bar-chart.css +119 -0
  176. package/src/charts/bar-chart.svelte +275 -0
  177. package/src/charts/charts.css +201 -0
  178. package/src/charts/crosshair.svelte +35 -0
  179. package/src/charts/dashboard/chart.ts +5 -0
  180. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  181. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  182. package/src/charts/dashboard/data-panel.svelte +81 -0
  183. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  184. package/src/charts/dashboard/data-table.svelte +278 -0
  185. package/src/charts/dashboard/data-table.ts +26 -0
  186. package/src/charts/dashboard/index.ts +25 -0
  187. package/src/charts/dashboard/panel-grid.svelte +43 -0
  188. package/src/charts/dashboard/period-filter.svelte +53 -0
  189. package/src/charts/dashboard/rating-chip.svelte +35 -0
  190. package/src/charts/dashboard/stat-band.svelte +47 -0
  191. package/src/charts/dashboard/stat-tile.svelte +176 -0
  192. package/src/charts/dashboard/windows.ts +8 -0
  193. package/src/charts/donut-chart.css +56 -0
  194. package/src/charts/donut-chart.svelte +158 -0
  195. package/src/charts/edge-fade.svelte +43 -0
  196. package/src/charts/edge-fade.ts +16 -0
  197. package/src/charts/figure.css +100 -0
  198. package/src/charts/figure.svelte +122 -0
  199. package/src/charts/gauge-chart.css +94 -0
  200. package/src/charts/gauge-chart.svelte +302 -0
  201. package/src/charts/hover-band.test.ts +30 -0
  202. package/src/charts/hover-band.ts +21 -0
  203. package/src/charts/hover-focus.svelte +85 -0
  204. package/src/charts/index.ts +31 -0
  205. package/src/charts/legend.css +136 -0
  206. package/src/charts/legend.svelte +112 -0
  207. package/src/charts/line-chart.css +26 -0
  208. package/src/charts/line-chart.svelte +256 -0
  209. package/src/charts/motion.ts +19 -0
  210. package/src/charts/number-chart.css +179 -0
  211. package/src/charts/number-chart.svelte +187 -0
  212. package/src/charts/primitives/chart-container.svelte +57 -0
  213. package/src/charts/primitives/chart-style.svelte +36 -0
  214. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  215. package/src/charts/primitives/chart-utils.ts +54 -0
  216. package/src/charts/primitives/index.ts +11 -0
  217. package/src/charts/reference-line.css +18 -0
  218. package/src/charts/reference-line.svelte +31 -0
  219. package/src/charts/reveal.svelte +61 -0
  220. package/src/charts/ring-chart.css +104 -0
  221. package/src/charts/ring-chart.svelte +166 -0
  222. package/src/charts/sparkline.css +45 -0
  223. package/src/charts/sparkline.svelte +141 -0
  224. package/src/charts/stat-card.css +105 -0
  225. package/src/charts/stat-card.svelte +267 -0
  226. package/src/charts/time-series-chart.css +28 -0
  227. package/src/charts/time-series-chart.svelte +179 -0
  228. package/src/charts/trend-pill.css +38 -0
  229. package/src/charts/trend-pill.svelte +48 -0
  230. package/src/components/copy/clipboard.ts +13 -9
  231. package/src/components/copy/copy-button.svelte +2 -2
  232. package/src/components/data-table/row-arrow.svelte +1 -1
  233. package/src/components/data-table/sortable-head.svelte +1 -1
  234. package/src/components/docs/doc-page.svelte +1 -1
  235. package/src/components/docs/on-this-page.svelte +2 -2
  236. package/src/components/field-row/field-row.svelte +1 -1
  237. package/src/components/hold-to-confirm/hold-to-confirm.svelte +1 -1
  238. package/src/components/language-flag/language-flag.svelte +1 -1
  239. package/src/components/list-page/detail-empty.svelte +1 -1
  240. package/src/components/list-page/detail-header.svelte +1 -1
  241. package/src/components/list-page/detail-panel.svelte +1 -1
  242. package/src/components/list-page/detail-row.svelte +1 -1
  243. package/src/components/list-page/detail-stack.svelte +1 -1
  244. package/src/components/list-page/empty-state.svelte +1 -1
  245. package/src/components/list-page/filter-item.svelte +1 -1
  246. package/src/components/list-page/filter-popover.svelte +2 -2
  247. package/src/components/list-page/filter-section.ts +1 -1
  248. package/src/components/list-page/grouped-list.svelte +1 -1
  249. package/src/components/list-page/list-page-shell.svelte +1 -1
  250. package/src/components/list-page/list-table.ts +1 -1
  251. package/src/components/list-page/master-detail.svelte +1 -1
  252. package/src/components/list-page/notice-bar.svelte +1 -1
  253. package/src/components/list-page/prev-next.svelte +1 -1
  254. package/src/components/list-page/rail-body.svelte +1 -1
  255. package/src/components/list-page/rail-empty.svelte +1 -1
  256. package/src/components/list-page/rail-head.svelte +1 -1
  257. package/src/components/list-page/rail-row.svelte +1 -1
  258. package/src/components/list-page/section-label.svelte +2 -2
  259. package/src/components/list-page/toolbar-search.svelte +1 -1
  260. package/src/components/list-page/toolbar-trigger.ts +1 -1
  261. package/src/components/mark/mark.svelte +1 -1
  262. package/src/components/open-in/open-in.svelte +1 -1
  263. package/src/components/picker/mark-picker.svelte +1 -1
  264. package/src/components/picker/picker.svelte +1 -1
  265. package/src/components/section-card/section-card.svelte +1 -1
  266. package/src/components/segmented/animated-tabs.svelte +1 -1
  267. package/src/components/segmented/section-runs.svelte +1 -1
  268. package/src/components/segmented/segment.svelte +5 -5
  269. package/src/components/segmented/segmented-filter.svelte +1 -1
  270. package/src/components/segmented/segmented-nav.svelte +1 -1
  271. package/src/components/segmented/segmented-overflow.svelte +1 -1
  272. package/src/components/segmented/segmented-track.svelte +1 -1
  273. package/src/components/star-rating-input/star-rating-input.svelte +1 -1
  274. package/src/components/table-view/table-view.svelte +1 -1
  275. package/src/components/theme/theme-provider.svelte +6 -4
  276. package/src/components/toolbar/index.ts +5 -0
  277. package/src/components/toolbar/toolbar.svelte +168 -0
  278. package/src/components/toolbar/toolbar.test.ts +66 -0
  279. package/src/editor/README.md +58 -0
  280. package/src/editor/ambient.d.ts +1 -0
  281. package/src/editor/block-id.ts +77 -0
  282. package/src/editor/block-ops.test.ts +237 -0
  283. package/src/editor/block-ops.ts +157 -0
  284. package/src/editor/content.css +418 -0
  285. package/src/editor/editor-toolbar.svelte +303 -0
  286. package/src/editor/editor.svelte +462 -0
  287. package/src/editor/extensions.ts +145 -0
  288. package/src/editor/image-toolbar.svelte +123 -0
  289. package/src/editor/index.ts +17 -0
  290. package/src/editor/inline-editor.svelte +261 -0
  291. package/src/editor/markdown-escaping.test.ts +158 -0
  292. package/src/editor/markdown-marks.test.ts +89 -0
  293. package/src/editor/markdown-test-helpers.ts +13 -0
  294. package/src/editor/markdown-youtube.test.ts +93 -0
  295. package/src/editor/markdown.test.ts +250 -0
  296. package/src/editor/markdown.ts +511 -0
  297. package/src/editor/renderer.svelte +92 -0
  298. package/src/editor/serializer.test.ts +133 -0
  299. package/src/editor/serializer.ts +370 -0
  300. package/src/editor/slash-items.ts +115 -0
  301. package/src/editor/slash-menu.svelte +159 -0
  302. package/src/editor/youtube-url.ts +35 -0
  303. package/src/effects/animated-beam/AnimatedBeam.svelte +1 -1
  304. package/src/effects/border-beam/register-properties.ts +4 -2
  305. package/src/effects/image-comparison/ImageComparison.svelte +1 -1
  306. package/src/effects/image-reveal/ImageReveal.svelte +1 -1
  307. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  308. package/src/effects/magic-card/MagicCard.svelte +1 -1
  309. package/src/effects/marquee/Marquee.svelte +1 -1
  310. package/src/effects/progressive-blur/ProgressiveBlur.svelte +1 -1
  311. package/src/effects/reveal/AnimatedGroup.svelte +1 -1
  312. package/src/effects/reveal/InView.svelte +1 -1
  313. package/src/effects/reveal/TextEffect.svelte +1 -1
  314. package/src/effects/shader-gradient/ShaderGradient.svelte +1 -1
  315. package/src/effects/spotlight/Spotlight.svelte +1 -1
  316. package/src/effects/theme-toggle/ThemeToggle.svelte +4 -4
  317. package/src/lib/result.ts +19 -0
  318. package/src/lib/utils.ts +18 -0
  319. package/src/onboarding/README.md +120 -0
  320. package/src/onboarding/define-tours.test.ts +50 -0
  321. package/src/onboarding/define-tours.ts +31 -0
  322. package/src/onboarding/demo.svelte +86 -0
  323. package/src/onboarding/index.ts +12 -0
  324. package/src/onboarding/onboarding-provider.svelte +120 -0
  325. package/src/onboarding/onboarding.svelte.ts +24 -0
  326. package/src/onboarding/pick.test.ts +79 -0
  327. package/src/onboarding/pick.ts +36 -0
  328. package/src/onboarding/stores.test.ts +103 -0
  329. package/src/onboarding/stores.ts +58 -0
  330. package/src/onboarding/tour-card.svelte +70 -0
  331. package/src/onboarding/tour-runner.svelte +197 -0
  332. package/src/onboarding/types.ts +34 -0
  333. package/src/onboarding/wait.ts +29 -0
  334. package/src/shadcn/accordion/accordion-content.svelte +1 -1
  335. package/src/shadcn/accordion/accordion-item.svelte +1 -1
  336. package/src/shadcn/accordion/accordion-trigger.svelte +1 -1
  337. package/src/shadcn/accordion/accordion.svelte +1 -1
  338. package/src/shadcn/alert/alert-description.svelte +1 -1
  339. package/src/shadcn/alert/alert-title.svelte +1 -1
  340. package/src/shadcn/alert/alert.svelte +1 -1
  341. package/src/shadcn/alert-dialog/alert-dialog-action.svelte +1 -1
  342. package/src/shadcn/alert-dialog/alert-dialog-cancel.svelte +1 -1
  343. package/src/shadcn/alert-dialog/alert-dialog-content.svelte +1 -1
  344. package/src/shadcn/alert-dialog/alert-dialog-description.svelte +1 -1
  345. package/src/shadcn/alert-dialog/alert-dialog-footer.svelte +1 -1
  346. package/src/shadcn/alert-dialog/alert-dialog-header.svelte +1 -1
  347. package/src/shadcn/alert-dialog/alert-dialog-overlay.svelte +1 -1
  348. package/src/shadcn/alert-dialog/alert-dialog-title.svelte +1 -1
  349. package/src/shadcn/attachment/attachment-action.svelte +1 -1
  350. package/src/shadcn/attachment/attachment-actions.svelte +1 -1
  351. package/src/shadcn/attachment/attachment-content.svelte +1 -1
  352. package/src/shadcn/attachment/attachment-description.svelte +1 -1
  353. package/src/shadcn/attachment/attachment-group.svelte +1 -1
  354. package/src/shadcn/attachment/attachment-media.svelte +1 -1
  355. package/src/shadcn/attachment/attachment-title.svelte +1 -1
  356. package/src/shadcn/attachment/attachment-trigger.svelte +1 -1
  357. package/src/shadcn/attachment/attachment.svelte +1 -1
  358. package/src/shadcn/avatar/avatar-badge.svelte +1 -1
  359. package/src/shadcn/avatar/avatar-fallback.svelte +1 -1
  360. package/src/shadcn/avatar/avatar-group-count.svelte +1 -1
  361. package/src/shadcn/avatar/avatar-group.svelte +1 -1
  362. package/src/shadcn/avatar/avatar-image.svelte +1 -1
  363. package/src/shadcn/avatar/avatar.svelte +1 -1
  364. package/src/shadcn/badge/badge.svelte +1 -1
  365. package/src/shadcn/breadcrumb/breadcrumb-ellipsis.svelte +1 -1
  366. package/src/shadcn/breadcrumb/breadcrumb-item.svelte +1 -1
  367. package/src/shadcn/breadcrumb/breadcrumb-link.svelte +1 -1
  368. package/src/shadcn/breadcrumb/breadcrumb-list.svelte +1 -1
  369. package/src/shadcn/breadcrumb/breadcrumb-page.svelte +1 -1
  370. package/src/shadcn/breadcrumb/breadcrumb-separator.svelte +1 -1
  371. package/src/shadcn/breadcrumb/breadcrumb.svelte +1 -1
  372. package/src/shadcn/bubble/bubble-content.svelte +1 -1
  373. package/src/shadcn/bubble/bubble-group.svelte +1 -1
  374. package/src/shadcn/bubble/bubble-reactions.svelte +1 -1
  375. package/src/shadcn/bubble/bubble.svelte +1 -1
  376. package/src/shadcn/button/button.svelte +1 -1
  377. package/src/shadcn/calendar/calendar-cell.svelte +1 -1
  378. package/src/shadcn/calendar/calendar-day.svelte +1 -1
  379. package/src/shadcn/calendar/calendar-grid-body.svelte +1 -1
  380. package/src/shadcn/calendar/calendar-grid-head.svelte +1 -1
  381. package/src/shadcn/calendar/calendar-grid-row.svelte +1 -1
  382. package/src/shadcn/calendar/calendar-grid.svelte +1 -1
  383. package/src/shadcn/calendar/calendar-head-cell.svelte +1 -1
  384. package/src/shadcn/calendar/calendar-header.svelte +1 -1
  385. package/src/shadcn/calendar/calendar-heading.svelte +1 -1
  386. package/src/shadcn/calendar/calendar-month-select.svelte +1 -1
  387. package/src/shadcn/calendar/calendar-month.svelte +1 -1
  388. package/src/shadcn/calendar/calendar-months.svelte +1 -1
  389. package/src/shadcn/calendar/calendar-nav.svelte +1 -1
  390. package/src/shadcn/calendar/calendar-next-button.svelte +1 -1
  391. package/src/shadcn/calendar/calendar-prev-button.svelte +1 -1
  392. package/src/shadcn/calendar/calendar-year-select.svelte +1 -1
  393. package/src/shadcn/calendar/calendar.svelte +1 -1
  394. package/src/shadcn/card/card-action.svelte +1 -1
  395. package/src/shadcn/card/card-content.svelte +1 -1
  396. package/src/shadcn/card/card-description.svelte +1 -1
  397. package/src/shadcn/card/card-footer.svelte +1 -1
  398. package/src/shadcn/card/card-header.svelte +1 -1
  399. package/src/shadcn/card/card-title.svelte +1 -1
  400. package/src/shadcn/card/card.svelte +1 -1
  401. package/src/shadcn/checkbox/checkbox.svelte +1 -1
  402. package/src/shadcn/code-block/code-block-actions.svelte +1 -1
  403. package/src/shadcn/code-block/code-block-content.svelte +1 -1
  404. package/src/shadcn/code-block/code-block-footer.svelte +1 -1
  405. package/src/shadcn/code-block/code-block-header.svelte +1 -1
  406. package/src/shadcn/code-block/code-block-line.svelte +1 -1
  407. package/src/shadcn/code-block/code-block-title.svelte +1 -1
  408. package/src/shadcn/code-block/code-block.svelte +1 -1
  409. package/src/shadcn/command/command-dialog.svelte +1 -1
  410. package/src/shadcn/command/command-empty.svelte +1 -1
  411. package/src/shadcn/command/command-group.svelte +1 -1
  412. package/src/shadcn/command/command-input.svelte +2 -2
  413. package/src/shadcn/command/command-item.svelte +1 -1
  414. package/src/shadcn/command/command-link-item.svelte +1 -1
  415. package/src/shadcn/command/command-list.svelte +1 -1
  416. package/src/shadcn/command/command-separator.svelte +1 -1
  417. package/src/shadcn/command/command-shortcut.svelte +1 -1
  418. package/src/shadcn/command/command.svelte +1 -1
  419. package/src/shadcn/context-menu/context-menu-checkbox-item.svelte +1 -1
  420. package/src/shadcn/context-menu/context-menu-content.svelte +2 -2
  421. package/src/shadcn/context-menu/context-menu-group-heading.svelte +1 -1
  422. package/src/shadcn/context-menu/context-menu-item.svelte +1 -1
  423. package/src/shadcn/context-menu/context-menu-label.svelte +1 -1
  424. package/src/shadcn/context-menu/context-menu-radio-item.svelte +1 -1
  425. package/src/shadcn/context-menu/context-menu-separator.svelte +1 -1
  426. package/src/shadcn/context-menu/context-menu-shortcut.svelte +1 -1
  427. package/src/shadcn/context-menu/context-menu-sub-content.svelte +1 -1
  428. package/src/shadcn/context-menu/context-menu-sub-trigger.svelte +1 -1
  429. package/src/shadcn/context-menu/context-menu-trigger.svelte +1 -1
  430. package/src/shadcn/dialog/dialog-content.svelte +1 -1
  431. package/src/shadcn/dialog/dialog-description.svelte +1 -1
  432. package/src/shadcn/dialog/dialog-footer.svelte +1 -1
  433. package/src/shadcn/dialog/dialog-header.svelte +1 -1
  434. package/src/shadcn/dialog/dialog-overlay.svelte +1 -1
  435. package/src/shadcn/dialog/dialog-title.svelte +1 -1
  436. package/src/shadcn/dropdown-menu/dropdown-menu-checkbox-item.svelte +1 -1
  437. package/src/shadcn/dropdown-menu/dropdown-menu-content.svelte +1 -1
  438. package/src/shadcn/dropdown-menu/dropdown-menu-group-heading.svelte +1 -1
  439. package/src/shadcn/dropdown-menu/dropdown-menu-item.svelte +1 -1
  440. package/src/shadcn/dropdown-menu/dropdown-menu-label.svelte +1 -1
  441. package/src/shadcn/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  442. package/src/shadcn/dropdown-menu/dropdown-menu-separator.svelte +1 -1
  443. package/src/shadcn/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  444. package/src/shadcn/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  445. package/src/shadcn/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  446. package/src/shadcn/empty/empty-content.svelte +1 -1
  447. package/src/shadcn/empty/empty-description.svelte +1 -1
  448. package/src/shadcn/empty/empty-header.svelte +1 -1
  449. package/src/shadcn/empty/empty-media.svelte +1 -1
  450. package/src/shadcn/empty/empty-title.svelte +1 -1
  451. package/src/shadcn/empty/empty.svelte +1 -1
  452. package/src/shadcn/field/field-content.svelte +1 -1
  453. package/src/shadcn/field/field-description.svelte +1 -1
  454. package/src/shadcn/field/field-error.svelte +1 -1
  455. package/src/shadcn/field/field-group.svelte +1 -1
  456. package/src/shadcn/field/field-label.svelte +1 -1
  457. package/src/shadcn/field/field-legend.svelte +1 -1
  458. package/src/shadcn/field/field-separator.svelte +1 -1
  459. package/src/shadcn/field/field-set.svelte +1 -1
  460. package/src/shadcn/field/field-title.svelte +1 -1
  461. package/src/shadcn/field/field.svelte +1 -1
  462. package/src/shadcn/field/simple-field.svelte +1 -1
  463. package/src/shadcn/hover-card/hover-card-content.svelte +1 -1
  464. package/src/shadcn/input/input.svelte +1 -1
  465. package/src/shadcn/input/secret-input.svelte +1 -1
  466. package/src/shadcn/input-otp/input-otp-group.svelte +1 -1
  467. package/src/shadcn/input-otp/input-otp-separator.svelte +1 -1
  468. package/src/shadcn/input-otp/input-otp-slot.svelte +1 -1
  469. package/src/shadcn/input-otp/input-otp.svelte +1 -1
  470. package/src/shadcn/kbd/kbd-group.svelte +1 -1
  471. package/src/shadcn/kbd/kbd.svelte +1 -1
  472. package/src/shadcn/label/label.svelte +1 -1
  473. package/src/shadcn/marker/marker-content.svelte +1 -1
  474. package/src/shadcn/marker/marker-icon.svelte +1 -1
  475. package/src/shadcn/marker/marker.svelte +1 -1
  476. package/src/shadcn/message/message-avatar.svelte +1 -1
  477. package/src/shadcn/message/message-content.svelte +1 -1
  478. package/src/shadcn/message/message-footer.svelte +1 -1
  479. package/src/shadcn/message/message-group.svelte +1 -1
  480. package/src/shadcn/message/message-header.svelte +1 -1
  481. package/src/shadcn/message/message.svelte +1 -1
  482. package/src/shadcn/message-scroller/message-scroller-button.svelte +1 -1
  483. package/src/shadcn/message-scroller/message-scroller-content.svelte +1 -1
  484. package/src/shadcn/message-scroller/message-scroller-item.svelte +1 -1
  485. package/src/shadcn/message-scroller/message-scroller-viewport.svelte +1 -1
  486. package/src/shadcn/message-scroller/message-scroller.svelte +1 -1
  487. package/src/shadcn/popover/popover-content.svelte +1 -1
  488. package/src/shadcn/popover/popover-description.svelte +1 -1
  489. package/src/shadcn/popover/popover-header.svelte +1 -1
  490. package/src/shadcn/popover/popover-title.svelte +1 -1
  491. package/src/shadcn/popover/popover-trigger.svelte +1 -1
  492. package/src/shadcn/progress/progress.svelte +1 -1
  493. package/src/shadcn/radio-group/radio-group-item.svelte +1 -1
  494. package/src/shadcn/radio-group/radio-group.svelte +1 -1
  495. package/src/shadcn/scroll-area/scroll-area-scrollbar.svelte +1 -1
  496. package/src/shadcn/scroll-area/scroll-area.svelte +1 -1
  497. package/src/shadcn/select/select-content.svelte +2 -2
  498. package/src/shadcn/select/select-group-heading.svelte +1 -1
  499. package/src/shadcn/select/select-group.svelte +1 -1
  500. package/src/shadcn/select/select-item.svelte +1 -1
  501. package/src/shadcn/select/select-label.svelte +1 -1
  502. package/src/shadcn/select/select-scroll-down-button.svelte +1 -1
  503. package/src/shadcn/select/select-scroll-up-button.svelte +1 -1
  504. package/src/shadcn/select/select-separator.svelte +1 -1
  505. package/src/shadcn/select/select-trigger.svelte +1 -1
  506. package/src/shadcn/separator/separator.svelte +1 -1
  507. package/src/shadcn/sheet/sheet-content.svelte +1 -1
  508. package/src/shadcn/sheet/sheet-description.svelte +1 -1
  509. package/src/shadcn/sheet/sheet-footer.svelte +1 -1
  510. package/src/shadcn/sheet/sheet-header.svelte +1 -1
  511. package/src/shadcn/sheet/sheet-overlay.svelte +1 -1
  512. package/src/shadcn/sheet/sheet-title.svelte +1 -1
  513. package/src/shadcn/sidebar/sidebar-content.svelte +1 -1
  514. package/src/shadcn/sidebar/sidebar-footer.svelte +1 -1
  515. package/src/shadcn/sidebar/sidebar-group-action.svelte +1 -1
  516. package/src/shadcn/sidebar/sidebar-group-content.svelte +1 -1
  517. package/src/shadcn/sidebar/sidebar-group-label.svelte +1 -1
  518. package/src/shadcn/sidebar/sidebar-group.svelte +1 -1
  519. package/src/shadcn/sidebar/sidebar-header.svelte +1 -1
  520. package/src/shadcn/sidebar/sidebar-input.svelte +1 -1
  521. package/src/shadcn/sidebar/sidebar-inset.svelte +1 -1
  522. package/src/shadcn/sidebar/sidebar-menu-action.svelte +1 -1
  523. package/src/shadcn/sidebar/sidebar-menu-badge.svelte +1 -1
  524. package/src/shadcn/sidebar/sidebar-menu-button.svelte +1 -1
  525. package/src/shadcn/sidebar/sidebar-menu-item.svelte +1 -1
  526. package/src/shadcn/sidebar/sidebar-menu-skeleton.svelte +1 -1
  527. package/src/shadcn/sidebar/sidebar-menu-sub-button.svelte +1 -1
  528. package/src/shadcn/sidebar/sidebar-menu-sub-item.svelte +1 -1
  529. package/src/shadcn/sidebar/sidebar-menu-sub.svelte +1 -1
  530. package/src/shadcn/sidebar/sidebar-menu.svelte +1 -1
  531. package/src/shadcn/sidebar/sidebar-provider.svelte +1 -1
  532. package/src/shadcn/sidebar/sidebar-rail.svelte +1 -1
  533. package/src/shadcn/sidebar/sidebar-separator.svelte +1 -1
  534. package/src/shadcn/sidebar/sidebar-trigger.svelte +1 -1
  535. package/src/shadcn/sidebar/sidebar.svelte +1 -1
  536. package/src/shadcn/skeleton/skeleton.svelte +1 -1
  537. package/src/shadcn/slider/slider.svelte +1 -1
  538. package/src/shadcn/spinner/spinner.svelte +1 -1
  539. package/src/shadcn/switch/switch.svelte +1 -1
  540. package/src/shadcn/table/table-body.svelte +1 -1
  541. package/src/shadcn/table/table-caption.svelte +1 -1
  542. package/src/shadcn/table/table-cell.svelte +1 -1
  543. package/src/shadcn/table/table-footer.svelte +1 -1
  544. package/src/shadcn/table/table-head.svelte +1 -1
  545. package/src/shadcn/table/table-header.svelte +1 -1
  546. package/src/shadcn/table/table-row.svelte +1 -1
  547. package/src/shadcn/table/table.svelte +1 -1
  548. package/src/shadcn/tabs/tabs-content.svelte +1 -1
  549. package/src/shadcn/tabs/tabs-list.svelte +1 -1
  550. package/src/shadcn/tabs/tabs-trigger.svelte +1 -1
  551. package/src/shadcn/tabs/tabs.svelte +1 -1
  552. package/src/shadcn/textarea/textarea.svelte +1 -1
  553. package/src/shadcn/toggle/toggle.svelte +1 -1
  554. package/src/shadcn/toggle-group/toggle-group-item.svelte +1 -1
  555. package/src/shadcn/toggle-group/toggle-group.svelte +1 -1
  556. package/src/shadcn/tooltip/tooltip-content.svelte +2 -2
  557. package/src/shadcn/trace/trace-collapsible-content.svelte +1 -1
  558. package/src/shadcn/trace/trace-collapsible-trigger.svelte +1 -1
  559. package/src/shadcn/trace/trace-collapsible.svelte +1 -1
  560. package/src/shadcn/trace/trace-description.svelte +1 -1
  561. package/src/shadcn/trace/trace-icon.svelte +1 -1
  562. package/src/shadcn/trace/trace-line.svelte +1 -1
  563. package/src/shadcn/trace/trace-title.svelte +1 -1
  564. package/src/shadcn/trace/trace.svelte +1 -1
  565. package/src/slides/Controls.svelte +138 -0
  566. package/src/slides/Deck.svelte +271 -0
  567. package/src/slides/Fragment.svelte +71 -0
  568. package/src/slides/Overview.svelte +113 -0
  569. package/src/slides/Presenter.svelte +292 -0
  570. package/src/slides/README.md +89 -0
  571. package/src/slides/Slide.svelte +179 -0
  572. package/src/slides/SlideList.svelte +19 -0
  573. package/src/slides/ambient.d.ts +1 -0
  574. package/src/slides/bridges.ts +69 -0
  575. package/src/slides/deck-controller.svelte.ts +292 -0
  576. package/src/slides/demo-slides.svelte +358 -0
  577. package/src/slides/demo.svelte +176 -0
  578. package/src/slides/export/capture.ts +113 -0
  579. package/src/slides/export/ffmpeg.ts +120 -0
  580. package/src/slides/export/index.ts +9 -0
  581. package/src/slides/export/launch.ts +10 -0
  582. package/src/slides/export/pdf.ts +152 -0
  583. package/src/slides/export/types.ts +48 -0
  584. package/src/slides/index.ts +19 -0
  585. package/src/slides/labels.ts +48 -0
  586. package/src/slides/layout.ts +51 -0
  587. package/src/slides/model.ts +47 -0
  588. package/src/slides/navigation.ts +100 -0
  589. package/src/slides/theme.css +205 -0
  590. package/src/slides/types.ts +86 -0
  591. package/src/tasks/README.md +312 -0
  592. package/src/tasks/ambient.d.ts +3 -0
  593. package/src/tasks/board-order.ts +137 -0
  594. package/src/tasks/dates.ts +31 -0
  595. package/src/tasks/define-statuses.ts +32 -0
  596. package/src/tasks/demo.svelte +389 -0
  597. package/src/tasks/detail/detail-demo.svelte +186 -0
  598. package/src/tasks/detail/detail-types.ts +54 -0
  599. package/src/tasks/detail/task-chip.svelte +28 -0
  600. package/src/tasks/detail/task-detail.svelte +315 -0
  601. package/src/tasks/detail/task-property.svelte +17 -0
  602. package/src/tasks/detail/task-thread.svelte +258 -0
  603. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  604. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  605. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  606. package/src/tasks/gantt/gantt-header.svelte +71 -0
  607. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  608. package/src/tasks/gantt/gantt.css +22 -0
  609. package/src/tasks/gantt/gantt.test.ts +77 -0
  610. package/src/tasks/gantt/logic.test.ts +163 -0
  611. package/src/tasks/gantt/logic.ts +248 -0
  612. package/src/tasks/inbox/inbox-card.svelte +283 -0
  613. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  614. package/src/tasks/inbox/inbox-logic.ts +55 -0
  615. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  616. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  617. package/src/tasks/inbox/inbox-types.ts +108 -0
  618. package/src/tasks/inbox/inbox.svelte +203 -0
  619. package/src/tasks/inbox/index.ts +23 -0
  620. package/src/tasks/index.ts +85 -0
  621. package/src/tasks/kanban-board.svelte +561 -0
  622. package/src/tasks/kanban-column.svelte +117 -0
  623. package/src/tasks/kanban-drag.ts +38 -0
  624. package/src/tasks/kanban-types.ts +65 -0
  625. package/src/tasks/presets/sales.ts +16 -0
  626. package/src/tasks/record/demo-data.ts +286 -0
  627. package/src/tasks/record/field-section.svelte +53 -0
  628. package/src/tasks/record/fields.ts +142 -0
  629. package/src/tasks/record/format.ts +87 -0
  630. package/src/tasks/record/gutter.ts +2 -0
  631. package/src/tasks/record/index.ts +54 -0
  632. package/src/tasks/record/info-list.svelte +316 -0
  633. package/src/tasks/record/kinds.ts +42 -0
  634. package/src/tasks/record/logic.ts +324 -0
  635. package/src/tasks/record/next-up.svelte +70 -0
  636. package/src/tasks/record/project-list.svelte +133 -0
  637. package/src/tasks/record/record-demo.svelte +324 -0
  638. package/src/tasks/record/record-header.svelte +70 -0
  639. package/src/tasks/record/record-shell.svelte +138 -0
  640. package/src/tasks/record/record-stats.svelte +48 -0
  641. package/src/tasks/record/record-timeline.svelte +231 -0
  642. package/src/tasks/record/record.test.ts +350 -0
  643. package/src/tasks/record/steps-rail.svelte +474 -0
  644. package/src/tasks/record/types.ts +280 -0
  645. package/src/tasks/record/where-we-are.svelte +121 -0
  646. package/src/tasks/reorder.ts +19 -0
  647. package/src/tasks/side-panel-types.ts +16 -0
  648. package/src/tasks/side-panel.svelte +193 -0
  649. package/src/tasks/status-badge.svelte +63 -0
  650. package/src/tasks/status-dot.svelte +228 -0
  651. package/src/tasks/status-icon.svelte +30 -0
  652. package/src/tasks/status-types.ts +40 -0
  653. package/src/tasks/task-gantt.svelte +230 -0
  654. package/src/tasks/task-list.svelte +470 -0
  655. package/src/tasks/task-types.ts +33 -0
  656. package/src/tasks/time/duration.ts +69 -0
  657. package/src/tasks/time/time-chip.svelte +62 -0
  658. package/src/tasks/time/time-dot.svelte +58 -0
  659. package/src/tasks/time/time-ring.svelte +74 -0
  660. package/src/tasks/time/time-tracker.svelte +565 -0
  661. package/src/tasks/time/time-types.ts +74 -0
  662. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  663. package/src/tokens/base.css +5 -5
  664. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,38 @@
1
+ const FRAME_PAD = 16;
2
+
3
+ /**
4
+ * Native drag shows a flat, faded snapshot. This hands the browser a tilted
5
+ * copy with a lifted shadow instead. Call it from `ondragstart`.
6
+ */
7
+ export function liftDragImage(e: DragEvent, tilt = -3): void {
8
+ const source = e.currentTarget as HTMLElement | null;
9
+ if (!source || !e.dataTransfer) return;
10
+ const rect = source.getBoundingClientRect();
11
+
12
+ // The padded frame leaves room for the tilt and shadow, or the snapshot crops them.
13
+ const frame = document.createElement("div");
14
+ Object.assign(frame.style, {
15
+ position: "fixed",
16
+ top: "-10000px",
17
+ left: "-10000px",
18
+ padding: `${FRAME_PAD}px`,
19
+ pointerEvents: "none",
20
+ });
21
+
22
+ const ghost = source.cloneNode(true) as HTMLElement;
23
+ Object.assign(ghost.style, {
24
+ width: `${rect.width}px`,
25
+ opacity: "1",
26
+ rotate: `${tilt}deg`,
27
+ filter:
28
+ "drop-shadow(0 2px 4px rgb(35 35 35 / 0.08)) drop-shadow(0 10px 18px rgb(35 35 35 / 0.14))",
29
+ });
30
+ frame.appendChild(ghost);
31
+ document.body.appendChild(frame);
32
+ e.dataTransfer.setDragImage(
33
+ frame,
34
+ e.clientX - rect.left + FRAME_PAD,
35
+ e.clientY - rect.top + FRAME_PAD,
36
+ );
37
+ setTimeout(() => frame.remove());
38
+ }
@@ -0,0 +1,65 @@
1
+ import type { Snippet } from "svelte";
2
+
3
+ /** A column the board renders. `id` is the value `getColumn` returns for a card. */
4
+ export type KanbanColumn = {
5
+ id: string;
6
+ label: string;
7
+ /** Any CSS color. Shown as a tint on the column while a card is held over it. */
8
+ hue?: string;
9
+ /** Screen-reader label for the column region. Defaults to `label`. */
10
+ ariaLabel?: string;
11
+ /**
12
+ * Set false to make the column a display-only lane: cards there cannot be
13
+ * dragged over or dropped onto (e.g. an Overdue lane). Defaults to true.
14
+ */
15
+ droppable?: boolean;
16
+ };
17
+
18
+ /** What the board asks the caller to persist after a drop. */
19
+ export type KanbanMove = {
20
+ columnId: string;
21
+ /**
22
+ * Only set when the active sort is manual: a fractional key that places the
23
+ * card in its new slot. Ignore it when you persist order some other way.
24
+ */
25
+ boardOrder?: string;
26
+ /**
27
+ * The cards that sit above and below the drop slot once the dragged card is
28
+ * set aside. Set on manual drops only, null at an end. For a backend that
29
+ * orders by neighbour instead of by key.
30
+ */
31
+ beforeId?: string | null;
32
+ afterId?: string | null;
33
+ };
34
+
35
+ /** A named sort the board can apply instead of manual (`boardOrder`) ordering. */
36
+ export type KanbanSortKey<T> = {
37
+ id: string;
38
+ label: string;
39
+ compare: (a: T, b: T) => number;
40
+ };
41
+
42
+ /** Second argument handed to the card snippet. */
43
+ export type KanbanCardContext = {
44
+ columnId: string;
45
+ index: number;
46
+ };
47
+
48
+ /** Second argument handed to the column header and footer snippets. */
49
+ export type KanbanColumnContext = {
50
+ /** Every card in the column, including the ones a `limit` keeps out of view. */
51
+ count: number;
52
+ /** A card is held over the column. */
53
+ active: boolean;
54
+ /** How many cards render. Fewer than `count` while the column is capped. */
55
+ shown: number;
56
+ /** The column's `limit` is hiding its tail. */
57
+ capped: boolean;
58
+ /** Fold the column into the hidden rail. Needs `hidden` or `onHiddenChange` on the board. */
59
+ hide: () => void;
60
+ /** Lift the `limit` so every card shows. */
61
+ expand: () => void;
62
+ };
63
+
64
+ /** The default header renderer when no `columnHeader` snippet is given. */
65
+ export type KanbanCardSnippet<T> = Snippet<[T, KanbanCardContext]>;
@@ -0,0 +1,16 @@
1
+ import { defineStatuses } from "../define-statuses.js";
2
+
3
+ /**
4
+ * An example status map for a sales pipeline. Names are neutral on purpose:
5
+ * swap the keys and labels for your own domain.
6
+ */
7
+ export const salesStatuses = defineStatuses({
8
+ new: { label: "New", tone: "accent", variant: "open" },
9
+ contacted: { label: "Contacted", tone: "info", variant: "open" },
10
+ proposal: { label: "Proposal", tone: "accent", variant: "progress", progress: 0.25 },
11
+ negotiation: { label: "Negotiation", tone: "warning", variant: "progress", progress: 0.75 },
12
+ won: { label: "Won", tone: "success", variant: "completed" },
13
+ lost: { label: "Lost", tone: "destructive", variant: "lost" },
14
+ });
15
+
16
+ export type SalesStatus = (typeof salesStatuses.ids)[number];
@@ -0,0 +1,286 @@
1
+ // Fake data for the demo and the screenshots. Every name here is invented.
2
+ import type {
3
+ FieldDef,
4
+ FieldSectionDef,
5
+ FieldValue,
6
+ RecordEvent,
7
+ RecordSummary,
8
+ StageDef,
9
+ StepDef,
10
+ StepGroupDef,
11
+ } from "./types.js";
12
+
13
+ /** The demo's clock, so a screenshot reads the same on any day. */
14
+ export const DEMO_NOW = new Date("2026-10-21T12:00:00Z");
15
+ export const DEMO_TODAY = "2026-10-21";
16
+
17
+ export const stages: StageDef[] = [
18
+ { key: "lead", label: "Lead", tone: "neutral", category: "open" },
19
+ { key: "sent", label: "Proposal sent", tone: "info", category: "proposal" },
20
+ { key: "build", label: "In build", tone: "accent", category: "ongoing" },
21
+ { key: "paid", label: "Paid", tone: "success", category: "completed" },
22
+ { key: "lost", label: "Lost", tone: "destructive", category: "lost" },
23
+ ];
24
+
25
+ export const stepGroups: StepGroupDef[] = [
26
+ { key: "sales", label: "Sales" },
27
+ { key: "delivery", label: "Delivery" },
28
+ ];
29
+
30
+ export const websiteSteps: StepDef[] = [
31
+ { id: "s1", label: "Discovery call", done: true, group: "sales", start: "2026-10-03" },
32
+ { id: "s2", label: "Proposal", done: false, group: "sales", start: "2026-10-21" },
33
+ { id: "s3", label: "Kickoff", done: false, group: "delivery", start: "2026-10-26" },
34
+ { id: "s4", label: "Design review", done: false, group: "delivery", start: "2026-11-10" },
35
+ { id: "s5", label: "Build", done: false, group: "delivery", start: "2026-11-12", end: "2026-12-04" },
36
+ { id: "s6", label: "Cutover", done: false, group: "delivery", start: "2026-12-12", end: "2026-12-13" },
37
+ { id: "s7", label: "Training", done: false, group: "delivery", hidden: true },
38
+ ];
39
+
40
+ export const projectSections: FieldSectionDef[] = [
41
+ { key: "scope", label: "Scope" },
42
+ { key: "loss", label: "Loss" },
43
+ ];
44
+
45
+ export const projectFields: FieldDef[] = [
46
+ {
47
+ key: "type",
48
+ label: "Type",
49
+ type: "choice",
50
+ section: "scope",
51
+ options: [
52
+ { key: "strategy", label: "Strategy" },
53
+ { key: "implementation", label: "Implementation" },
54
+ ],
55
+ },
56
+ { key: "budget", label: "Budget", type: "money", section: "scope" },
57
+ {
58
+ key: "urgency",
59
+ label: "Urgency",
60
+ type: "choice",
61
+ section: "scope",
62
+ options: [
63
+ { key: "low", label: "Low", tone: "neutral" },
64
+ { key: "high", label: "High", tone: "warning" },
65
+ ],
66
+ },
67
+ { key: "goLive", label: "Go live", type: "date", section: "scope" },
68
+ { key: "context", label: "Context", type: "longtext", section: "scope" },
69
+ {
70
+ key: "reason",
71
+ label: "Reason",
72
+ type: "choice",
73
+ section: "loss",
74
+ showWhen: { key: "lossReasonOn", equals: "yes" },
75
+ options: [{ key: "price", label: "Price" }],
76
+ },
77
+ ];
78
+
79
+ export const clientSections: FieldSectionDef[] = [{ key: "about", label: "About" }];
80
+
81
+ export const clientFields: FieldDef[] = [
82
+ { key: "business", label: "Business", type: "longtext", section: "about" },
83
+ {
84
+ key: "size",
85
+ label: "Size",
86
+ type: "choice",
87
+ section: "about",
88
+ options: [
89
+ { key: "1-10", label: "1 to 10" },
90
+ { key: "11-50", label: "11 to 50" },
91
+ { key: "51+", label: "51 or more" },
92
+ ],
93
+ },
94
+ {
95
+ key: "sector",
96
+ label: "Sector",
97
+ type: "choice",
98
+ section: "about",
99
+ options: ["Dental", "Legal", "Retail", "Logistics", "Education", "Finance", "Health", "Hospitality", "Media"].map(
100
+ (label) => ({ key: label.toLowerCase(), label }),
101
+ ),
102
+ },
103
+ {
104
+ key: "needs",
105
+ label: "Needs",
106
+ type: "multi",
107
+ section: "about",
108
+ options: [
109
+ { key: "site", label: "Website" },
110
+ { key: "booking", label: "Booking" },
111
+ { key: "crm", label: "CRM" },
112
+ { key: "old", label: "Print", archived: true },
113
+ ],
114
+ },
115
+ { key: "site", label: "Website", type: "url", section: "about" },
116
+ { key: "email", label: "Email", type: "email", section: "about" },
117
+ { key: "tz", label: "Time zone", type: "timezone", section: "about" },
118
+ ];
119
+
120
+ export const websiteValues: Record<string, FieldValue> = {
121
+ type: "implementation",
122
+ budget: 12000,
123
+ urgency: "high",
124
+ goLive: "2026-12-14",
125
+ context: "Replace the brochure site. Booking stays on the old tool until spring.",
126
+ };
127
+
128
+ export const clientValues: Record<string, FieldValue> = {
129
+ business: "A dental group in Lyon with 3 clinics and a shared front desk.",
130
+ size: "11-50",
131
+ sector: "dental",
132
+ needs: ["site", "booking", "old"],
133
+ site: "https://marlowfinch.example",
134
+ email: "jane@marlowfinch.example",
135
+ tz: "Europe/Paris",
136
+ };
137
+
138
+ const note = (id: string, at: string, title: string, body?: string): RecordEvent => ({
139
+ id,
140
+ kind: "note",
141
+ occurredAt: at,
142
+ title,
143
+ body,
144
+ actor: "Ramzi",
145
+ });
146
+
147
+ export const websiteEvents: RecordEvent[] = [
148
+ {
149
+ id: "w1",
150
+ kind: "status",
151
+ occurredAt: "2026-10-18T09:30:00Z",
152
+ title: "Sent SOW v2, legal is reviewing",
153
+ body: "The payment terms have 2 redlines. Sofia answers Monday and asks for the signature by the 26th, so the kickoff holds.",
154
+ actor: "Sofia",
155
+ },
156
+ {
157
+ id: "w2",
158
+ kind: "call",
159
+ occurredAt: "2026-10-21T08:15:00Z",
160
+ title: "Scoping review",
161
+ body: "45 minutes with Jane and the clinic manager. Booking stays out of scope. They want the new site to carry the clinic pages and nothing else.",
162
+ details: [
163
+ { label: "Length", value: "45 min" },
164
+ { label: "With", value: "Jane Roe, Tom Bell" },
165
+ ],
166
+ href: "#call-scoping",
167
+ },
168
+ {
169
+ id: "w3",
170
+ kind: "stage",
171
+ occurredAt: "2026-10-12T14:00:00Z",
172
+ title: "Stage set to Proposal sent",
173
+ actor: "Sofia",
174
+ },
175
+ note("w4", "2026-10-09T16:40:00Z", "Their fear is another agency that vanishes", "Said it twice. Lead the proposal with who answers the phone."),
176
+ {
177
+ id: "w5",
178
+ kind: "invoice.sent",
179
+ occurredAt: "2026-10-06T10:00:00Z",
180
+ title: "Deposit invoice",
181
+ amount: 3600,
182
+ currency: "EUR",
183
+ },
184
+ {
185
+ id: "w6",
186
+ kind: "call",
187
+ occurredAt: "2026-10-03T13:00:00Z",
188
+ title: "Discovery call",
189
+ body: "Jane runs 3 clinics and the site is 4 years old.",
190
+ },
191
+ { id: "w7", kind: "milestone.done", occurredAt: "2026-10-03T15:00:00Z", title: "Discovery call done" },
192
+ ...Array.from({ length: 34 }, (_, i): RecordEvent => ({
193
+ id: `old${i}`,
194
+ kind: i % 3 === 0 ? "email" : "note",
195
+ occurredAt: new Date(Date.UTC(2026, 8, 28 - i, 9, 0)).toISOString(),
196
+ title: i % 3 === 0 ? "Re: clinic pages" : "Warm intro follow-up",
197
+ actor: "Ramzi",
198
+ })),
199
+ ];
200
+
201
+ export const website: RecordSummary = {
202
+ id: "p-web",
203
+ name: "Website rebuild",
204
+ href: "#project-web",
205
+ stageKey: "sent",
206
+ value: 12000,
207
+ currency: "EUR",
208
+ startDate: "2026-11-01",
209
+ endDate: "2026-12-15",
210
+ whereWeAre: {
211
+ text: "Sent SOW v2, legal is reviewing. The payment terms have 2 redlines, Sofia answers Monday and asks for the signature by the 26th, so the kickoff holds.",
212
+ at: "2026-10-18T09:30:00Z",
213
+ actor: "Sofia",
214
+ health: { label: "Waiting", tone: "warning" },
215
+ },
216
+ next: { label: "Kickoff call", date: "2026-10-26", owner: "Ramzi" },
217
+ lastCallAt: "2026-10-21T08:15:00Z",
218
+ lastTouchAt: "2026-10-21T08:15:00Z",
219
+ progress: { done: 1, total: 6 },
220
+ };
221
+
222
+ export const audit: RecordSummary = {
223
+ id: "p-audit",
224
+ name: "Brand audit",
225
+ href: "#project-audit",
226
+ stageKey: "lost",
227
+ stageDetail: "Price",
228
+ value: 4500,
229
+ currency: "EUR",
230
+ whereWeAre: { text: "They went with a freelancer at half the fee.", at: "2026-06-11T10:00:00Z", actor: "Sofia" },
231
+ lastCallAt: "2026-06-04T10:00:00Z",
232
+ lastTouchAt: "2026-06-11T10:00:00Z",
233
+ progress: { done: 1, total: 3 },
234
+ };
235
+
236
+ export const portal: RecordSummary = {
237
+ id: "p-portal",
238
+ name: "Booking portal",
239
+ href: "#project-portal",
240
+ stageKey: "paid",
241
+ value: 18000,
242
+ paid: 18000,
243
+ currency: "EUR",
244
+ whereWeAre: { text: "Live since March. Final invoice paid.", at: "2026-04-02T10:00:00Z", actor: "Ramzi" },
245
+ lastCallAt: "2026-03-20T10:00:00Z",
246
+ lastTouchAt: "2026-04-02T10:00:00Z",
247
+ progress: { done: 5, total: 5 },
248
+ };
249
+
250
+ export const auditEvents: RecordEvent[] = [
251
+ {
252
+ id: "a1",
253
+ kind: "stage",
254
+ occurredAt: "2026-06-11T10:00:00Z",
255
+ title: "Stage set to Lost",
256
+ actor: "Sofia",
257
+ details: [
258
+ { label: "Reason", value: "Price" },
259
+ { label: "In their words", value: "A freelancer quoted half." },
260
+ ],
261
+ },
262
+ { id: "a2", kind: "call", occurredAt: "2026-06-04T10:00:00Z", title: "Proposal walk-through", body: "Jane liked the audit and balked at the fee." },
263
+ ];
264
+
265
+ export const portalEvents: RecordEvent[] = [
266
+ { id: "b1", kind: "payment", occurredAt: "2026-04-02T10:00:00Z", title: "Final invoice paid", amount: 9000, currency: "EUR" },
267
+ { id: "b2", kind: "payment", occurredAt: "2026-02-10T10:00:00Z", title: "First instalment paid", amount: 9000, currency: "EUR" },
268
+ { id: "b3", kind: "call", occurredAt: "2026-03-20T10:00:00Z", title: "Launch check", body: "Live. A few copy fixes after." },
269
+ ];
270
+
271
+ /** Something that happened before any project existed: it belongs to the client. */
272
+ export const clientOnlyEvents: RecordEvent[] = [
273
+ {
274
+ id: "c1",
275
+ kind: "form",
276
+ occurredAt: "2026-02-01T09:00:00Z",
277
+ title: "Contact form: asked about online booking",
278
+ body: "Calls pile up at lunch and the front desk can't keep up.",
279
+ },
280
+ ];
281
+
282
+ export const clientProjects = [
283
+ { summary: website, events: websiteEvents },
284
+ { summary: audit, events: auditEvents },
285
+ { summary: portal, events: portalEvents },
286
+ ];
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { cn } from "../../lib/utils.js";
6
+ import { fieldRow, groupFields, type FieldValues } from "./fields.js";
7
+ import InfoList from "./info-list.svelte";
8
+ import type {
9
+ FieldDef,
10
+ FieldSectionDef,
11
+ FieldValue,
12
+ RecordLabels,
13
+ } from "./types.js";
14
+
15
+ // =========================
16
+ // PROPS
17
+ // =========================
18
+ interface Props {
19
+ /** What a module declares for this record. */
20
+ fields: FieldDef[];
21
+ /** Section headings in the order to draw them. A section with no field isn't drawn. */
22
+ sections?: FieldSectionDef[];
23
+ values: FieldValues;
24
+ /** Without it every field is read-only and an empty one is left out. */
25
+ onSave?: (key: string, value: FieldValue) => void | Promise<void>;
26
+ /** For a money field that names none. */
27
+ currency?: string;
28
+ labels?: Partial<RecordLabels>;
29
+ locale?: string;
30
+ class?: string;
31
+ }
32
+ let { fields, sections = [], values, onSave, currency, labels, locale, class: className }: Props = $props();
33
+
34
+ // =========================
35
+ // DERIVED
36
+ // =========================
37
+ let groups = $derived(groupFields(fields, sections, values));
38
+ </script>
39
+
40
+ <!-- =========================== -->
41
+ <!-- MARKUP -->
42
+ <!-- =========================== -->
43
+ {#if groups.length > 0}
44
+ <div class={cn("flex flex-col gap-5", className)}>
45
+ {#each groups as group (group.key)}
46
+ <InfoList
47
+ title={group.label}
48
+ {labels}
49
+ rows={group.fields.map((f) => fieldRow(f, values[f.key], { locale, currency, onSave }))}
50
+ />
51
+ {/each}
52
+ </div>
53
+ {/if}
@@ -0,0 +1,142 @@
1
+ // Turns a module's field declarations and a record's values into rows to draw.
2
+ import { formatDay, formatMoney } from "./format.js";
3
+ import type {
4
+ FieldDef,
5
+ FieldOption,
6
+ FieldSectionDef,
7
+ FieldValue,
8
+ InfoRow,
9
+ } from "./types.js";
10
+
11
+ export type FieldValues = Record<string, FieldValue | undefined>;
12
+
13
+ function isEmpty(value: FieldValue | undefined): boolean {
14
+ return value == null || value === "" || (Array.isArray(value) && value.length === 0);
15
+ }
16
+
17
+ /** A field shows unless its `showWhen` names a field that doesn't hold the wanted value. */
18
+ export function isFieldVisible(field: FieldDef, values: FieldValues): boolean {
19
+ const rule = field.showWhen;
20
+ if (!rule) return true;
21
+ const current = values[rule.key];
22
+ if (rule.equals === undefined) return !isEmpty(current);
23
+ const wanted = Array.isArray(rule.equals) ? rule.equals : [rule.equals];
24
+ const held = Array.isArray(current) ? current : current == null ? [] : [String(current)];
25
+ return held.some((v) => wanted.includes(v));
26
+ }
27
+
28
+ /**
29
+ * The options a picker offers: an archived one is left out unless it is what the
30
+ * record already holds, so an old value still reads and is never chosen again.
31
+ */
32
+ export function pickableOptions(field: FieldDef, value: FieldValue | undefined): FieldOption[] {
33
+ const held = Array.isArray(value) ? value : value == null ? [] : [String(value)];
34
+ return (field.options ?? []).filter((o) => !o.archived || held.includes(o.key));
35
+ }
36
+
37
+ export type FieldGroup = { key: string; label: string | null; fields: FieldDef[] };
38
+
39
+ /**
40
+ * Visible fields bucketed by section in the declared order. A section with no
41
+ * field is not drawn. Fields with no or an unknown section go last under no heading.
42
+ */
43
+ export function groupFields(
44
+ fields: readonly FieldDef[],
45
+ sections: readonly FieldSectionDef[],
46
+ values: FieldValues,
47
+ ): FieldGroup[] {
48
+ const visible = fields.filter((f) => isFieldVisible(f, values));
49
+ const known = new Set(sections.map((s) => s.key));
50
+ const groups: FieldGroup[] = sections
51
+ .map((s) => ({ key: s.key, label: s.label as string | null, fields: visible.filter((f) => f.section === s.key) }))
52
+ .filter((g) => g.fields.length > 0);
53
+ const loose = visible.filter((f) => !f.section || !known.has(f.section));
54
+ if (loose.length > 0) groups.push({ key: "", label: null, fields: loose });
55
+ return groups;
56
+ }
57
+
58
+ /** What the editor hands back, as the type the field declares. Empty text is null. */
59
+ export function parseSaved(field: FieldDef, raw: string | string[]): FieldValue {
60
+ if (Array.isArray(raw)) return raw;
61
+ const text = raw.trim();
62
+ if (text === "") return null;
63
+ if (field.type === "money") {
64
+ const n = Number(text.replace(",", "."));
65
+ return Number.isFinite(n) ? n : null;
66
+ }
67
+ return text;
68
+ }
69
+
70
+ export type RowContext = {
71
+ locale?: string;
72
+ /** The record's currency, for a money field with none of its own. */
73
+ currency?: string;
74
+ onSave?: (key: string, value: FieldValue) => void | Promise<void>;
75
+ };
76
+
77
+ const INPUT: Record<FieldDef["type"], NonNullable<InfoRow["input"]>> = {
78
+ text: "text",
79
+ longtext: "longtext",
80
+ choice: "select",
81
+ multi: "multi",
82
+ money: "number",
83
+ date: "date",
84
+ url: "url",
85
+ email: "email",
86
+ timezone: "text",
87
+ };
88
+
89
+ /** One field and its value as an `InfoRow`. */
90
+ export function fieldRow(field: FieldDef, value: FieldValue | undefined, ctx: RowContext = {}): InfoRow {
91
+ const row: InfoRow = {
92
+ id: field.key,
93
+ label: field.label,
94
+ input: INPUT[field.type],
95
+ placeholder: field.placeholder,
96
+ required: field.required,
97
+ };
98
+ const optionOf = (key: string) => field.options?.find((o) => o.key === key);
99
+
100
+ if (!isEmpty(value)) {
101
+ switch (field.type) {
102
+ case "choice": {
103
+ const option = optionOf(String(value));
104
+ row.value = option?.label ?? String(value);
105
+ row.tone = option?.tone;
106
+ break;
107
+ }
108
+ case "multi": {
109
+ const keys = Array.isArray(value) ? value : [String(value)];
110
+ row.chips = keys.map((k) => ({ label: optionOf(k)?.label ?? k, tone: optionOf(k)?.tone }));
111
+ break;
112
+ }
113
+ case "money":
114
+ row.value = formatMoney(Number(value), field.currency ?? ctx.currency, ctx.locale);
115
+ break;
116
+ case "date":
117
+ row.value = formatDay(String(value), ctx.locale);
118
+ break;
119
+ case "url":
120
+ row.value = String(value).replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
121
+ row.href = String(value);
122
+ break;
123
+ case "email":
124
+ row.value = String(value);
125
+ row.href = `mailto:${value}`;
126
+ break;
127
+ default:
128
+ row.value = String(value);
129
+ }
130
+ }
131
+
132
+ if (field.type === "choice" || field.type === "multi") {
133
+ row.options = pickableOptions(field, value).map((o) => ({ value: o.key, label: o.label, tone: o.tone }));
134
+ row.editValue = Array.isArray(value) ? value : value == null ? (field.type === "multi" ? [] : "") : String(value);
135
+ } else if (!isEmpty(value)) {
136
+ row.editValue = String(value);
137
+ }
138
+
139
+ const onSave = ctx.onSave;
140
+ if (onSave) row.onSave = (raw) => onSave(field.key, parseSaved(field, raw));
141
+ return row;
142
+ }
@@ -0,0 +1,87 @@
1
+ // Display formatting through Intl, so a locale string is all a host passes.
2
+ // A date-only value ("2026-10-26") is a calendar day, read in UTC so no zone shifts it.
3
+
4
+ const DAY_ONLY = /^\d{4}-\d{2}-\d{2}$/;
5
+
6
+ function parse(iso: string): { date: Date; timeZone?: string } {
7
+ return DAY_ONLY.test(iso)
8
+ ? { date: new Date(`${iso}T00:00:00Z`), timeZone: "UTC" }
9
+ : { date: new Date(iso) };
10
+ }
11
+
12
+ /** "Mon 26 Oct". */
13
+ export function formatDay(iso: string, locale = "en-GB"): string {
14
+ const { date, timeZone } = parse(iso);
15
+ return new Intl.DateTimeFormat(locale, {
16
+ weekday: "short",
17
+ day: "numeric",
18
+ month: "short",
19
+ timeZone,
20
+ }).format(date);
21
+ }
22
+
23
+ /** "Sat 24 to Sun 25 Oct" in one month, two full days across months, one day when the ends match. */
24
+ export function formatRange(start: string, end: string | null | undefined, locale = "en-GB"): string {
25
+ if (!end || end.slice(0, 10) === start.slice(0, 10)) return formatDay(start, locale);
26
+ const a = parse(start);
27
+ const b = parse(end);
28
+ const sameMonth =
29
+ a.date.getUTCFullYear() === b.date.getUTCFullYear() && a.date.getUTCMonth() === b.date.getUTCMonth();
30
+ if (!sameMonth) return `${formatDay(start, locale)} to ${formatDay(end, locale)}`;
31
+ const part = (d: Date) =>
32
+ new Intl.DateTimeFormat(locale, { weekday: "short", day: "numeric", timeZone: "UTC" }).format(d);
33
+ const month = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" }).format(b.date);
34
+ return `${part(a.date)} to ${part(b.date)} ${month}`;
35
+ }
36
+
37
+ /** "Oct 21" without a weekday, for a timeline day header. */
38
+ export function formatDayHeader(day: string, locale = "en-GB"): string {
39
+ return new Intl.DateTimeFormat(locale, {
40
+ weekday: "short",
41
+ day: "numeric",
42
+ month: "short",
43
+ year: "numeric",
44
+ timeZone: "UTC",
45
+ }).format(new Date(`${day}T00:00:00Z`));
46
+ }
47
+
48
+ /** "14:30" in the viewer's zone, or the given one. */
49
+ export function formatTime(iso: string, locale = "en-GB", timeZone?: string): string {
50
+ return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", timeZone }).format(
51
+ new Date(iso),
52
+ );
53
+ }
54
+
55
+ /** "3 days ago", "in 2 weeks", "now". */
56
+ export function formatAge(iso: string, now: Date | number = Date.now(), locale = "en-GB"): string {
57
+ const seconds = Math.round((new Date(iso).getTime() - new Date(now).getTime()) / 1000);
58
+ const abs = Math.abs(seconds);
59
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "long" });
60
+ const units: [number, Intl.RelativeTimeFormatUnit][] = [
61
+ [31_536_000, "year"],
62
+ [2_592_000, "month"],
63
+ [604_800, "week"],
64
+ [86_400, "day"],
65
+ [3_600, "hour"],
66
+ [60, "minute"],
67
+ ];
68
+ for (const [size, unit] of units) {
69
+ if (abs >= size) return rtf.format(Math.round(seconds / size), unit);
70
+ }
71
+ return rtf.format(0, "second");
72
+ }
73
+
74
+ /** "€12,000". `compact` gives "€12K". */
75
+ export function formatMoney(
76
+ amount: number,
77
+ currency = "EUR",
78
+ locale = "en-GB",
79
+ compact = false,
80
+ ): string {
81
+ return new Intl.NumberFormat(locale, {
82
+ style: "currency",
83
+ currency,
84
+ maximumFractionDigits: compact || Number.isInteger(amount) ? 0 : 2,
85
+ notation: compact ? "compact" : "standard",
86
+ }).format(amount);
87
+ }
@@ -0,0 +1,2 @@
1
+ /** The record's horizontal padding, shared by the header, banner, tabs and body so they line up. */
2
+ export const gutterClass = "px-4 @3xl:px-6";