@heyramzi/ui 1.54.2 → 2.0.1

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 (363) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +205 -13
  4. package/scripts/sync-shadcn.mjs +2 -0
  5. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  6. package/src/ambient.d.ts +8 -0
  7. package/src/brand/brand.test.ts +67 -0
  8. package/src/brand/index.ts +137 -0
  9. package/src/canvas/README.md +236 -0
  10. package/src/canvas/card/card-frame.svelte +61 -0
  11. package/src/canvas/card/card.css +39 -0
  12. package/src/canvas/card/resize-handles.svelte +119 -0
  13. package/src/canvas/card/resize.ts +33 -0
  14. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  15. package/src/canvas/chrome/chrome.css +231 -0
  16. package/src/canvas/chrome/lock-badge.svelte +18 -0
  17. package/src/canvas/chrome/side-handles.svelte +30 -0
  18. package/src/canvas/chrome/size-chip.svelte +45 -0
  19. package/src/canvas/chrome/snap-guides.svelte +69 -0
  20. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  21. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  22. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  23. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  24. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  25. package/src/canvas/edges/bend-handle.svelte +76 -0
  26. package/src/canvas/edges/edge-gradient.svelte +30 -0
  27. package/src/canvas/edges/edge-path.test.ts +58 -0
  28. package/src/canvas/edges/edge-path.ts +293 -0
  29. package/src/canvas/edges/gradient-edge.svelte +43 -0
  30. package/src/canvas/editor/context-menu.test.ts +65 -0
  31. package/src/canvas/editor/context-menu.ts +48 -0
  32. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  33. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  34. package/src/canvas/editor/editor-context.ts +62 -0
  35. package/src/canvas/editor/editor-mode.ts +67 -0
  36. package/src/canvas/editor/frame.ts +98 -0
  37. package/src/canvas/editor/hand-context.ts +15 -0
  38. package/src/canvas/editor/history-core.test.ts +239 -0
  39. package/src/canvas/editor/history-core.ts +247 -0
  40. package/src/canvas/editor/keymap.test.ts +161 -0
  41. package/src/canvas/editor/keymap.ts +151 -0
  42. package/src/canvas/editor/layout-actions.test.ts +156 -0
  43. package/src/canvas/editor/layout-actions.ts +219 -0
  44. package/src/canvas/editor/lock.test.ts +68 -0
  45. package/src/canvas/editor/lock.ts +87 -0
  46. package/src/canvas/editor/rebase.test.ts +208 -0
  47. package/src/canvas/editor/rebase.ts +272 -0
  48. package/src/canvas/editor/style.ts +184 -0
  49. package/src/canvas/editor/text-field-context.ts +41 -0
  50. package/src/canvas/editor/z-order.test.ts +75 -0
  51. package/src/canvas/editor/z-order.ts +87 -0
  52. package/src/canvas/headless/alignment.test.ts +59 -0
  53. package/src/canvas/headless/alignment.ts +23 -0
  54. package/src/canvas/headless/break-apart-find.ts +418 -0
  55. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  56. package/src/canvas/headless/break-apart-plan.ts +439 -0
  57. package/src/canvas/headless/break-apart-style.ts +106 -0
  58. package/src/canvas/headless/break-apart.ts +244 -0
  59. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  60. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  61. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  62. package/src/canvas/headless/edge-anchors.ts +143 -0
  63. package/src/canvas/headless/edge-memo.test.ts +58 -0
  64. package/src/canvas/headless/edge-memo.ts +32 -0
  65. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  66. package/src/canvas/headless/edge-serialization.ts +32 -0
  67. package/src/canvas/headless/figure-geometry.ts +293 -0
  68. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  69. package/src/canvas/headless/fit-viewport.ts +137 -0
  70. package/src/canvas/headless/grid.test.ts +34 -0
  71. package/src/canvas/headless/grid.ts +94 -0
  72. package/src/canvas/headless/icon-node.test.ts +23 -0
  73. package/src/canvas/headless/icon-node.ts +66 -0
  74. package/src/canvas/headless/index.ts +9 -0
  75. package/src/canvas/headless/linkify.test.ts +23 -0
  76. package/src/canvas/headless/linkify.ts +25 -0
  77. package/src/canvas/headless/pen-marks.ts +133 -0
  78. package/src/canvas/headless/sketch.test.ts +99 -0
  79. package/src/canvas/headless/sketch.ts +302 -0
  80. package/src/canvas/headless/snap-guides.test.ts +61 -0
  81. package/src/canvas/headless/snap-guides.ts +146 -0
  82. package/src/canvas/headless/svg-figure.test.ts +49 -0
  83. package/src/canvas/headless/svg-figure.ts +176 -0
  84. package/src/canvas/headless/table-layout.test.ts +158 -0
  85. package/src/canvas/headless/table-layout.ts +309 -0
  86. package/src/canvas/index.ts +35 -0
  87. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  88. package/src/canvas/lod/lod-switch.svelte +140 -0
  89. package/src/canvas/lod/render-all.svelte.ts +52 -0
  90. package/src/canvas/ops/arrange.ts +108 -0
  91. package/src/canvas/ops/ops.test.ts +98 -0
  92. package/src/canvas/ops/ops.ts +347 -0
  93. package/src/canvas/pen/PenField.svelte +128 -0
  94. package/src/canvas/pen/PenField.test.ts +87 -0
  95. package/src/canvas/pen/PenOverlay.svelte +172 -0
  96. package/src/canvas/pen/ShapeNode.svelte +64 -0
  97. package/src/canvas/pen/TextNode.svelte +97 -0
  98. package/src/canvas/pen/colors.ts +28 -0
  99. package/src/canvas/pen/eraser.ts +46 -0
  100. package/src/canvas/pen/geometry.ts +148 -0
  101. package/src/canvas/pen/host-field.test.svelte +13 -0
  102. package/src/canvas/pen/index.ts +40 -0
  103. package/src/canvas/pen/ops.ts +16 -0
  104. package/src/canvas/pen/pen-session.ts +177 -0
  105. package/src/canvas/pen/pen.css +149 -0
  106. package/src/canvas/pen/plugin.ts +139 -0
  107. package/src/canvas/pen/shapes.ts +104 -0
  108. package/src/canvas/pen/stroke-path.ts +54 -0
  109. package/src/canvas/pen/types.ts +96 -0
  110. package/src/canvas/pen/ui.ts +26 -0
  111. package/src/canvas/plugin.test.ts +45 -0
  112. package/src/canvas/plugin.ts +159 -0
  113. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  114. package/src/canvas/screen/PartView.svelte +176 -0
  115. package/src/canvas/screen/ScreenBody.svelte +142 -0
  116. package/src/canvas/screen/ScreenCard.svelte +32 -0
  117. package/src/canvas/screen/glyphs/button.svg +1 -0
  118. package/src/canvas/screen/glyphs/close.svg +1 -0
  119. package/src/canvas/screen/glyphs/down.svg +1 -0
  120. package/src/canvas/screen/glyphs/header.svg +1 -0
  121. package/src/canvas/screen/glyphs/hills.svg +1 -0
  122. package/src/canvas/screen/glyphs/image.svg +1 -0
  123. package/src/canvas/screen/glyphs/input.svg +1 -0
  124. package/src/canvas/screen/glyphs/list.svg +1 -0
  125. package/src/canvas/screen/glyphs/modal.svg +1 -0
  126. package/src/canvas/screen/glyphs/plus.svg +1 -0
  127. package/src/canvas/screen/glyphs/up.svg +1 -0
  128. package/src/canvas/screen/glyphs.ts +40 -0
  129. package/src/canvas/screen/labels.ts +41 -0
  130. package/src/canvas/screen/parts.ts +164 -0
  131. package/src/canvas/screen/plugin.ts +67 -0
  132. package/src/canvas/screen/screen.css +593 -0
  133. package/src/canvas/screen/types.ts +47 -0
  134. package/src/canvas/screen/ui.ts +13 -0
  135. package/src/canvas/test-utils.ts +18 -0
  136. package/src/canvas/types.ts +53 -0
  137. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  138. package/src/canvas/ui/FrameLayer.svelte +116 -0
  139. package/src/canvas/ui/PenLayer.svelte +39 -0
  140. package/src/canvas/ui/StylePanel.svelte +268 -0
  141. package/src/canvas/ui/canvas-grid.svelte +156 -0
  142. package/src/canvas/ui/canvas-ui.css +350 -0
  143. package/src/canvas/ui/forward-wheel.ts +29 -0
  144. package/src/canvas/ui/icons.ts +124 -0
  145. package/src/canvas/ui/place-panel.ts +80 -0
  146. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  147. package/src/canvas/ui/shortcut-tip.ts +17 -0
  148. package/src/canvas/ui/tools.ts +146 -0
  149. package/src/charts/README.md +104 -0
  150. package/src/charts/ambient.d.ts +4 -0
  151. package/src/charts/animated-value.css +7 -0
  152. package/src/charts/animated-value.svelte +110 -0
  153. package/src/charts/area-chart.css +29 -0
  154. package/src/charts/area-chart.svelte +204 -0
  155. package/src/charts/bar-chart.css +119 -0
  156. package/src/charts/bar-chart.svelte +275 -0
  157. package/src/charts/charts.css +201 -0
  158. package/src/charts/crosshair.svelte +35 -0
  159. package/src/charts/dashboard/chart.ts +5 -0
  160. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  161. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  162. package/src/charts/dashboard/data-panel.svelte +81 -0
  163. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  164. package/src/charts/dashboard/data-table.svelte +278 -0
  165. package/src/charts/dashboard/data-table.ts +26 -0
  166. package/src/charts/dashboard/index.ts +25 -0
  167. package/src/charts/dashboard/panel-grid.svelte +43 -0
  168. package/src/charts/dashboard/period-filter.svelte +53 -0
  169. package/src/charts/dashboard/rating-chip.svelte +35 -0
  170. package/src/charts/dashboard/stat-band.svelte +47 -0
  171. package/src/charts/dashboard/stat-tile.svelte +176 -0
  172. package/src/charts/dashboard/windows.ts +8 -0
  173. package/src/charts/donut-chart.css +56 -0
  174. package/src/charts/donut-chart.svelte +158 -0
  175. package/src/charts/edge-fade.svelte +43 -0
  176. package/src/charts/edge-fade.ts +16 -0
  177. package/src/charts/figure.css +100 -0
  178. package/src/charts/figure.svelte +122 -0
  179. package/src/charts/gauge-chart.css +94 -0
  180. package/src/charts/gauge-chart.svelte +302 -0
  181. package/src/charts/hover-band.test.ts +30 -0
  182. package/src/charts/hover-band.ts +21 -0
  183. package/src/charts/hover-focus.svelte +85 -0
  184. package/src/charts/index.ts +31 -0
  185. package/src/charts/legend.css +136 -0
  186. package/src/charts/legend.svelte +112 -0
  187. package/src/charts/line-chart.css +26 -0
  188. package/src/charts/line-chart.svelte +256 -0
  189. package/src/charts/motion.ts +19 -0
  190. package/src/charts/number-chart.css +179 -0
  191. package/src/charts/number-chart.svelte +187 -0
  192. package/src/charts/primitives/chart-container.svelte +57 -0
  193. package/src/charts/primitives/chart-style.svelte +36 -0
  194. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  195. package/src/charts/primitives/chart-utils.ts +54 -0
  196. package/src/charts/primitives/index.ts +11 -0
  197. package/src/charts/reference-line.css +18 -0
  198. package/src/charts/reference-line.svelte +31 -0
  199. package/src/charts/reveal.svelte +61 -0
  200. package/src/charts/ring-chart.css +104 -0
  201. package/src/charts/ring-chart.svelte +166 -0
  202. package/src/charts/sparkline.css +45 -0
  203. package/src/charts/sparkline.svelte +141 -0
  204. package/src/charts/stat-card.css +105 -0
  205. package/src/charts/stat-card.svelte +267 -0
  206. package/src/charts/time-series-chart.css +28 -0
  207. package/src/charts/time-series-chart.svelte +179 -0
  208. package/src/charts/trend-pill.css +38 -0
  209. package/src/charts/trend-pill.svelte +48 -0
  210. package/src/components/copy/copy-button.svelte +1 -1
  211. package/src/components/docs/doc-page.svelte +1 -1
  212. package/src/components/docs/on-this-page.svelte +2 -2
  213. package/src/components/list-page/empty-state.svelte +1 -1
  214. package/src/components/list-page/filter-popover.svelte +1 -1
  215. package/src/components/list-page/section-label.svelte +1 -1
  216. package/src/components/segmented/segment.svelte +4 -4
  217. package/src/components/theme/theme-provider.svelte +6 -4
  218. package/src/components/toolbar/index.ts +5 -0
  219. package/src/components/toolbar/toolbar.svelte +168 -0
  220. package/src/components/toolbar/toolbar.test.ts +66 -0
  221. package/src/editor/README.md +58 -0
  222. package/src/editor/ambient.d.ts +1 -0
  223. package/src/editor/block-id.ts +77 -0
  224. package/src/editor/block-ops.test.ts +237 -0
  225. package/src/editor/block-ops.ts +157 -0
  226. package/src/editor/content.css +418 -0
  227. package/src/editor/editor-toolbar.svelte +303 -0
  228. package/src/editor/editor.svelte +469 -0
  229. package/src/editor/extensions.ts +145 -0
  230. package/src/editor/image-toolbar.svelte +123 -0
  231. package/src/editor/index.ts +17 -0
  232. package/src/editor/inline-editor.svelte +261 -0
  233. package/src/editor/markdown-escaping.test.ts +158 -0
  234. package/src/editor/markdown-marks.test.ts +89 -0
  235. package/src/editor/markdown-test-helpers.ts +13 -0
  236. package/src/editor/markdown-youtube.test.ts +93 -0
  237. package/src/editor/markdown.test.ts +250 -0
  238. package/src/editor/markdown.ts +511 -0
  239. package/src/editor/renderer.svelte +92 -0
  240. package/src/editor/serializer.test.ts +133 -0
  241. package/src/editor/serializer.ts +370 -0
  242. package/src/editor/slash-items.ts +115 -0
  243. package/src/editor/slash-menu.svelte +159 -0
  244. package/src/editor/youtube-url.ts +35 -0
  245. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  246. package/src/gallery.test.ts +20 -1
  247. package/src/lib/result.ts +19 -0
  248. package/src/onboarding/README.md +120 -0
  249. package/src/onboarding/define-tours.test.ts +50 -0
  250. package/src/onboarding/define-tours.ts +31 -0
  251. package/src/onboarding/demo.svelte +86 -0
  252. package/src/onboarding/index.ts +12 -0
  253. package/src/onboarding/onboarding-provider.svelte +120 -0
  254. package/src/onboarding/onboarding.svelte.ts +24 -0
  255. package/src/onboarding/pick.test.ts +79 -0
  256. package/src/onboarding/pick.ts +36 -0
  257. package/src/onboarding/stores.test.ts +103 -0
  258. package/src/onboarding/stores.ts +58 -0
  259. package/src/onboarding/tour-card.svelte +70 -0
  260. package/src/onboarding/tour-runner.svelte +197 -0
  261. package/src/onboarding/types.ts +34 -0
  262. package/src/onboarding/wait.ts +29 -0
  263. package/src/shadcn/command/command-input.svelte +1 -1
  264. package/src/slides/Controls.svelte +138 -0
  265. package/src/slides/Deck.svelte +271 -0
  266. package/src/slides/Fragment.svelte +71 -0
  267. package/src/slides/Overview.svelte +113 -0
  268. package/src/slides/Presenter.svelte +292 -0
  269. package/src/slides/README.md +89 -0
  270. package/src/slides/Slide.svelte +179 -0
  271. package/src/slides/SlideList.svelte +19 -0
  272. package/src/slides/ambient.d.ts +1 -0
  273. package/src/slides/bridges.ts +69 -0
  274. package/src/slides/deck-controller.svelte.ts +292 -0
  275. package/src/slides/demo-slides.svelte +358 -0
  276. package/src/slides/demo.svelte +176 -0
  277. package/src/slides/export/capture.ts +113 -0
  278. package/src/slides/export/ffmpeg.ts +120 -0
  279. package/src/slides/export/index.ts +9 -0
  280. package/src/slides/export/launch.ts +10 -0
  281. package/src/slides/export/pdf.ts +152 -0
  282. package/src/slides/export/types.ts +48 -0
  283. package/src/slides/index.ts +19 -0
  284. package/src/slides/labels.ts +48 -0
  285. package/src/slides/layout.ts +51 -0
  286. package/src/slides/model.ts +47 -0
  287. package/src/slides/navigation.ts +100 -0
  288. package/src/slides/theme.css +205 -0
  289. package/src/slides/types.ts +86 -0
  290. package/src/tasks/README.md +312 -0
  291. package/src/tasks/ambient.d.ts +3 -0
  292. package/src/tasks/board-order.ts +137 -0
  293. package/src/tasks/dates.ts +31 -0
  294. package/src/tasks/define-statuses.ts +32 -0
  295. package/src/tasks/demo.svelte +389 -0
  296. package/src/tasks/detail/detail-demo.svelte +186 -0
  297. package/src/tasks/detail/detail-types.ts +54 -0
  298. package/src/tasks/detail/task-chip.svelte +28 -0
  299. package/src/tasks/detail/task-detail.svelte +315 -0
  300. package/src/tasks/detail/task-property.svelte +17 -0
  301. package/src/tasks/detail/task-thread.svelte +258 -0
  302. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  303. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  304. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  305. package/src/tasks/gantt/gantt-header.svelte +71 -0
  306. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  307. package/src/tasks/gantt/gantt.css +22 -0
  308. package/src/tasks/gantt/gantt.test.ts +77 -0
  309. package/src/tasks/gantt/logic.test.ts +163 -0
  310. package/src/tasks/gantt/logic.ts +248 -0
  311. package/src/tasks/inbox/inbox-card.svelte +283 -0
  312. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  313. package/src/tasks/inbox/inbox-logic.ts +55 -0
  314. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  315. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  316. package/src/tasks/inbox/inbox-types.ts +108 -0
  317. package/src/tasks/inbox/inbox.svelte +203 -0
  318. package/src/tasks/inbox/index.ts +23 -0
  319. package/src/tasks/index.ts +85 -0
  320. package/src/tasks/kanban-board.svelte +561 -0
  321. package/src/tasks/kanban-column.svelte +117 -0
  322. package/src/tasks/kanban-drag.ts +38 -0
  323. package/src/tasks/kanban-types.ts +65 -0
  324. package/src/tasks/presets/sales.ts +16 -0
  325. package/src/tasks/record/demo-data.ts +286 -0
  326. package/src/tasks/record/field-section.svelte +53 -0
  327. package/src/tasks/record/fields.ts +142 -0
  328. package/src/tasks/record/format.ts +87 -0
  329. package/src/tasks/record/gutter.ts +2 -0
  330. package/src/tasks/record/index.ts +54 -0
  331. package/src/tasks/record/info-list.svelte +316 -0
  332. package/src/tasks/record/kinds.ts +42 -0
  333. package/src/tasks/record/logic.ts +324 -0
  334. package/src/tasks/record/next-up.svelte +70 -0
  335. package/src/tasks/record/project-list.svelte +133 -0
  336. package/src/tasks/record/record-demo.svelte +324 -0
  337. package/src/tasks/record/record-header.svelte +70 -0
  338. package/src/tasks/record/record-shell.svelte +138 -0
  339. package/src/tasks/record/record-stats.svelte +48 -0
  340. package/src/tasks/record/record-timeline.svelte +231 -0
  341. package/src/tasks/record/record.test.ts +350 -0
  342. package/src/tasks/record/steps-rail.svelte +474 -0
  343. package/src/tasks/record/types.ts +280 -0
  344. package/src/tasks/record/where-we-are.svelte +121 -0
  345. package/src/tasks/reorder.ts +19 -0
  346. package/src/tasks/side-panel-types.ts +16 -0
  347. package/src/tasks/side-panel.svelte +193 -0
  348. package/src/tasks/status-badge.svelte +63 -0
  349. package/src/tasks/status-dot.svelte +228 -0
  350. package/src/tasks/status-icon.svelte +30 -0
  351. package/src/tasks/status-types.ts +40 -0
  352. package/src/tasks/task-gantt.svelte +230 -0
  353. package/src/tasks/task-list.svelte +470 -0
  354. package/src/tasks/task-types.ts +33 -0
  355. package/src/tasks/time/duration.ts +69 -0
  356. package/src/tasks/time/time-chip.svelte +62 -0
  357. package/src/tasks/time/time-dot.svelte +58 -0
  358. package/src/tasks/time/time-ring.svelte +74 -0
  359. package/src/tasks/time/time-tracker.svelte +565 -0
  360. package/src/tasks/time/time-types.ts +74 -0
  361. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  362. package/src/tokens/base.css +5 -5
  363. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,474 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import {
6
+ ArrowCounterClockwise,
7
+ ArrowDown,
8
+ ArrowUp,
9
+ Check,
10
+ DotsThreeVertical,
11
+ Eye,
12
+ EyeSlash,
13
+ PencilSimple,
14
+ } from "phosphor-svelte";
15
+ import { cn } from "../../lib/utils.js";
16
+ import { Button } from "../../shadcn/button/index.js";
17
+ import { Input } from "../../shadcn/input/index.js";
18
+ import { Skeleton } from "../../shadcn/skeleton/index.js";
19
+ import * as Popover from "../../shadcn/popover/index.js";
20
+ import * as ContextMenu from "../../shadcn/context-menu/index.js";
21
+ import { todayIso } from "../dates.js";
22
+ import { formatRange } from "./format.js";
23
+ import {
24
+ groupSteps,
25
+ nudgeStep,
26
+ reorderIds,
27
+ stepMarkers,
28
+ stepProgress,
29
+ visibleSteps,
30
+ } from "./logic.js";
31
+ import {
32
+ resolveLabels,
33
+ type RecordLabels,
34
+ type StepDef,
35
+ type StepGroupDef,
36
+ type StepMarker,
37
+ } from "./types.js";
38
+
39
+ // =========================
40
+ // PROPS
41
+ // =========================
42
+ interface Props {
43
+ steps: StepDef[];
44
+ /** Group headers in the order to draw them, such as Sales then Delivery. */
45
+ groups?: StepGroupDef[];
46
+ activeId?: string | null;
47
+ onSelect?: (id: string) => void;
48
+ /** Show or hide a step. Without it the visibility menu isn't drawn. */
49
+ onToggle?: (id: string, visible: boolean) => void;
50
+ /** The whole new id order, after a drag or a keyboard move. */
51
+ onReorder?: (ids: string[]) => void;
52
+ /** An empty label resets the name. */
53
+ onRename?: (id: string, label: string) => void;
54
+ loading?: boolean;
55
+ labels?: Partial<RecordLabels>;
56
+ locale?: string;
57
+ /** ISO date. Decides "late". Defaults to the viewer's today. */
58
+ today?: string;
59
+ class?: string;
60
+ }
61
+ let {
62
+ steps,
63
+ groups = [],
64
+ activeId = null,
65
+ onSelect,
66
+ onToggle,
67
+ onReorder,
68
+ onRename,
69
+ loading = false,
70
+ labels,
71
+ locale,
72
+ today = todayIso(),
73
+ class: className,
74
+ }: Props = $props();
75
+
76
+ // =========================
77
+ // STATE
78
+ // =========================
79
+ let menuOpen = $state(false);
80
+ // The order shown while the host round-trips. Cleared when the steps catch up,
81
+ // or after five seconds so a failed save can't pin a wrong order.
82
+ let pendingOrder = $state<string[] | null>(null);
83
+ let pendingTimer: ReturnType<typeof setTimeout> | null = null;
84
+ let draggedId = $state<string | null>(null);
85
+ let draggedGroup = $state<string | undefined>(undefined);
86
+ let overId = $state<string | null>(null);
87
+ let insertBefore = $state(true);
88
+ let renamingId = $state<string | null>(null);
89
+ let renameValue = $state("");
90
+ let renameInput = $state<HTMLInputElement | null>(null);
91
+ // WHY: the context menu closing hands focus back and blurs a fresh input, which
92
+ // would commit a rename nobody typed. Blur counts only once the input has had a frame.
93
+ let renameArmed = $state(false);
94
+
95
+ // =========================
96
+ // DERIVED
97
+ // =========================
98
+ let t = $derived(resolveLabels(labels));
99
+ let ordered = $derived.by(() => {
100
+ if (!pendingOrder) return steps;
101
+ const byId = new Map(steps.map((s) => [s.id, s]));
102
+ return pendingOrder.flatMap((id) => byId.get(id) ?? []);
103
+ });
104
+ let visible = $derived(visibleSteps(ordered));
105
+ let markers = $derived(stepMarkers(ordered, today));
106
+ let buckets = $derived(groupSteps(visible, groups));
107
+ let menuBuckets = $derived(groupSteps(ordered, groups));
108
+ let progress = $derived(stepProgress(ordered));
109
+ let hiddenCount = $derived(ordered.length - visible.length);
110
+ let canReorder = $derived(Boolean(onReorder));
111
+
112
+ // =========================
113
+ // EFFECTS
114
+ // =========================
115
+ $effect(() => {
116
+ if (!pendingOrder) return;
117
+ if (steps.map((s) => s.id).join(",") === pendingOrder.join(",")) clearPending();
118
+ });
119
+
120
+ $effect(() => {
121
+ if (renamingId && renameInput) {
122
+ renameInput.focus();
123
+ renameInput.select();
124
+ const frame = requestAnimationFrame(() => (renameArmed = true));
125
+ return () => {
126
+ cancelAnimationFrame(frame);
127
+ renameArmed = false;
128
+ };
129
+ }
130
+ renameArmed = false;
131
+ });
132
+
133
+ // =========================
134
+ // FUNCTIONS
135
+ // =========================
136
+ function clearPending() {
137
+ pendingOrder = null;
138
+ if (pendingTimer) clearTimeout(pendingTimer);
139
+ pendingTimer = null;
140
+ }
141
+
142
+ function applyOrder(ids: string[]) {
143
+ if (ids.join(",") === ordered.map((s) => s.id).join(",")) return;
144
+ pendingOrder = ids;
145
+ if (pendingTimer) clearTimeout(pendingTimer);
146
+ pendingTimer = setTimeout(clearPending, 5000);
147
+ onReorder?.(ids);
148
+ }
149
+
150
+ function dateLabel(step: StepDef): string | null {
151
+ const from = step.start ?? step.end;
152
+ return from ? formatRange(from, step.start ? step.end : null, locale) : null;
153
+ }
154
+
155
+ function resetDrag() {
156
+ draggedId = null;
157
+ draggedGroup = undefined;
158
+ overId = null;
159
+ }
160
+
161
+ function dragStart(event: DragEvent, step: StepDef) {
162
+ draggedId = step.id;
163
+ draggedGroup = step.group;
164
+ if (event.dataTransfer) {
165
+ event.dataTransfer.effectAllowed = "move";
166
+ event.dataTransfer.setData("text/plain", step.id);
167
+ }
168
+ }
169
+
170
+ // A step moves inside its own group only: a group is a phase, and dragging
171
+ // between two would silently change what a step means.
172
+ function dragOver(event: DragEvent, target: StepDef) {
173
+ if (!draggedId || draggedId === target.id || target.group !== draggedGroup) return;
174
+ event.preventDefault();
175
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
176
+ const box = (event.currentTarget as HTMLElement).getBoundingClientRect();
177
+ const before = event.clientY < box.top + box.height / 2;
178
+ if (overId !== target.id || insertBefore !== before) {
179
+ overId = target.id;
180
+ insertBefore = before;
181
+ }
182
+ }
183
+
184
+ function drop(event: DragEvent, target: StepDef) {
185
+ const moved = draggedId;
186
+ const before = insertBefore;
187
+ const sameGroup = target.group === draggedGroup;
188
+ resetDrag();
189
+ if (!moved || moved === target.id || !sameGroup) return;
190
+ event.preventDefault();
191
+ applyOrder(
192
+ reorderIds(
193
+ ordered.map((s) => s.id),
194
+ moved,
195
+ target.id,
196
+ before,
197
+ ),
198
+ );
199
+ }
200
+
201
+ // Alt plus an arrow, not a bare arrow: the rail is walked with arrow keys and
202
+ // taking them would lock keyboard users out.
203
+ function keydown(event: KeyboardEvent, step: StepDef) {
204
+ if (!canReorder || !event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
205
+ event.preventDefault();
206
+ applyOrder(nudgeStep(ordered, step.id, event.key === "ArrowUp" ? -1 : 1));
207
+ }
208
+
209
+ function startRename(step: StepDef) {
210
+ renamingId = step.id;
211
+ renameValue = step.label;
212
+ }
213
+
214
+ function commitRename() {
215
+ if (!renamingId) return;
216
+ const step = ordered.find((s) => s.id === renamingId);
217
+ const next = renameValue.trim();
218
+ renamingId = null;
219
+ // WHY: an unchanged name sends nothing, so a stray blur can't mark a step renamed.
220
+ if (!step || next === step.label) return;
221
+ onRename?.(step.id, next);
222
+ }
223
+
224
+ function renameKey(event: KeyboardEvent) {
225
+ if (event.key === "Enter") {
226
+ event.preventDefault();
227
+ commitRename();
228
+ } else if (event.key === "Escape") {
229
+ event.preventDefault();
230
+ renamingId = null;
231
+ }
232
+ }
233
+ </script>
234
+
235
+ <!-- =========================== -->
236
+ <!-- MARKUP -->
237
+ <!-- =========================== -->
238
+
239
+ <!-- One marker per state, one colour each. The accent marks only the step the
240
+ record is on, and the warning ring only a step it has moved past. -->
241
+ {#snippet marker(kind: StepMarker)}
242
+ {#if kind === "complete"}
243
+ <span class="flex size-4 items-center justify-center rounded-full bg-success text-white">
244
+ <Check class="size-2.5" weight="bold" />
245
+ </span>
246
+ {:else if kind === "current"}
247
+ <span class="rounded-full bg-background p-0.75">
248
+ <span
249
+ class="block size-2 rounded-full bg-[color:var(--tone-accent,var(--color-primary))] ring-3 ring-[color-mix(in_oklab,var(--tone-accent,var(--color-primary))_20%,transparent)]"
250
+ ></span>
251
+ </span>
252
+ {:else if kind === "late"}
253
+ <span class="size-2 rounded-full border-2 border-warning bg-background"></span>
254
+ {:else}
255
+ <span class="size-2 rounded-full border border-muted-foreground/40 bg-background"></span>
256
+ {/if}
257
+ {/snippet}
258
+
259
+ <div class={cn("flex flex-col", className)}>
260
+ <div class="mb-2 flex min-h-7 items-center gap-2 px-2">
261
+ <h2 class="text-xs font-medium tracking-wide text-muted-foreground uppercase">{t.steps}</h2>
262
+ {#if !loading}
263
+ <span class="text-xs tabular-nums text-muted-foreground">
264
+ {t.stepsCount(progress.done, progress.total)}
265
+ </span>
266
+ {/if}
267
+ {#if onToggle && !loading && ordered.length > 0}
268
+ <Popover.Root bind:open={menuOpen}>
269
+ <Popover.Trigger>
270
+ {#snippet child({ props })}
271
+ <Button
272
+ {...props}
273
+ variant="ghost"
274
+ size="sm"
275
+ icon
276
+ class="relative ml-auto text-muted-foreground"
277
+ aria-label={t.editSteps}
278
+ >
279
+ <DotsThreeVertical class="size-4" weight="bold" />
280
+ {#if hiddenCount > 0}
281
+ <span
282
+ class="absolute -top-0.5 -right-0.5 flex size-4 items-center justify-center rounded-full bg-muted text-xs font-medium tabular-nums"
283
+ >{hiddenCount}</span
284
+ >
285
+ {/if}
286
+ </Button>
287
+ {/snippet}
288
+ </Popover.Trigger>
289
+ <Popover.Content align="end" class="w-64 p-1">
290
+ {#each menuBuckets as bucket (bucket.key)}
291
+ {#if bucket.label}
292
+ <p class="px-2 pt-2 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase">
293
+ {bucket.label}
294
+ </p>
295
+ {/if}
296
+ {#each bucket.steps as step (step.id)}
297
+ <Button
298
+ variant="ghost"
299
+ class="w-full justify-start gap-2.5 px-2 text-left"
300
+ aria-pressed={!step.hidden}
301
+ onclick={() => onToggle(step.id, Boolean(step.hidden))}
302
+ >
303
+ <span class="flex size-4 shrink-0 items-center justify-center">
304
+ {#if step.hidden}
305
+ <EyeSlash class="size-3.5 text-muted-foreground" />
306
+ {:else}
307
+ <Eye class="size-3.5" />
308
+ {/if}
309
+ </span>
310
+ <span
311
+ class={cn(
312
+ "min-w-0 flex-1 truncate",
313
+ step.hidden && "text-muted-foreground line-through",
314
+ )}>{step.label}</span
315
+ >
316
+ </Button>
317
+ {/each}
318
+ {/each}
319
+ <p class="mt-1 border-t border-border px-2 pt-2 pb-1 text-xs tabular-nums text-muted-foreground">
320
+ {t.shownCount(visible.length, ordered.length)}
321
+ </p>
322
+ </Popover.Content>
323
+ </Popover.Root>
324
+ {/if}
325
+ </div>
326
+
327
+ {#if loading}
328
+ <div class="flex flex-col gap-2 px-2 py-1">
329
+ {#each [0, 1, 2, 3] as i (i)}
330
+ <Skeleton class="h-9" />
331
+ {/each}
332
+ </div>
333
+ {:else if visible.length === 0}
334
+ <p class="px-2 py-1 text-sm text-muted-foreground">{t.noSteps}</p>
335
+ {:else}
336
+ <div class="flex flex-col gap-4">
337
+ {#each buckets as bucket (bucket.key)}
338
+ <div class="flex flex-col">
339
+ {#if bucket.label}
340
+ <h3 class="mb-1 px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase select-none">
341
+ {bucket.label}
342
+ </h3>
343
+ {/if}
344
+ {#each bucket.steps as step, index (step.id)}
345
+ {@const kind = markers.get(step.id) ?? "pending"}
346
+ {@const active = step.id === activeId}
347
+ {@const when = dateLabel(step)}
348
+ {@const renaming = renamingId === step.id}
349
+ {@const live = draggedId !== null && draggedId !== step.id && overId === step.id}
350
+ <ContextMenu.Root>
351
+ <ContextMenu.Trigger>
352
+ <div
353
+ class={cn("relative", draggedId === step.id && "opacity-40")}
354
+ role="presentation"
355
+ ondragover={(e) => dragOver(e, step)}
356
+ ondragleave={() => overId === step.id && (overId = null)}
357
+ ondrop={(e) => drop(e, step)}
358
+ >
359
+ {#if live && insertBefore}
360
+ <span
361
+ class="pointer-events-none absolute -top-px right-2 left-2 z-10 h-0.5 rounded-full bg-[color:var(--tone-accent,var(--color-primary))]"
362
+ ></span>
363
+ {/if}
364
+ <!-- Raw button: the row is a drag source with a rename field and a spine
365
+ inside it, and Button's centred, nowrap layout can't hold that. -->
366
+ <button
367
+ type="button"
368
+ draggable={canReorder && !renaming}
369
+ class={cn(
370
+ "relative flex min-h-9 w-full items-start gap-2.5 rounded-lg px-2 py-2 text-left transition-colors any-pointer-coarse:min-h-11 any-pointer-coarse:py-3",
371
+ "outline-none focus-visible:ring-3 focus-visible:ring-ring/50",
372
+ canReorder && "active:cursor-grabbing",
373
+ active ? "bg-muted" : "hover:bg-muted/60",
374
+ )}
375
+ aria-current={active ? "step" : undefined}
376
+ onclick={() => onSelect?.(step.id)}
377
+ ondragstart={(e) => dragStart(e, step)}
378
+ ondragend={resetDrag}
379
+ onkeydown={(e) => keydown(e, step)}
380
+ >
381
+ {#if index < bucket.steps.length - 1}
382
+ <!-- Marker centre to the next marker's centre: py-2 plus mt-0.5 plus
383
+ half the size-4 marker, which paints over both ends. -->
384
+ <span
385
+ class={cn(
386
+ "pointer-events-none absolute top-4.5 -bottom-4.5 left-4 z-5 w-px -translate-x-1/2 any-pointer-coarse:top-5.5 any-pointer-coarse:-bottom-5.5",
387
+ kind === "complete" ? "bg-success/40" : "bg-border",
388
+ )}
389
+ ></span>
390
+ {/if}
391
+ <span class="relative z-10 mt-0.5 flex size-4 shrink-0 items-center justify-center">
392
+ {@render marker(kind)}
393
+ </span>
394
+ <span class="min-w-0 flex-1">
395
+ {#if renaming}
396
+ <Input
397
+ type="text"
398
+ class="h-6 px-1.5 py-0 text-sm font-medium"
399
+ aria-label={t.rename}
400
+ bind:value={renameValue}
401
+ bind:ref={renameInput}
402
+ onkeydown={renameKey}
403
+ onblur={() => renameArmed && commitRename()}
404
+ onclick={(e: MouseEvent) => e.stopPropagation()}
405
+ />
406
+ {:else}
407
+ <span
408
+ class={cn(
409
+ "block truncate text-sm",
410
+ kind === "complete" || kind === "pending"
411
+ ? "text-muted-foreground"
412
+ : "text-foreground",
413
+ (kind === "current" || active) && "font-medium text-foreground",
414
+ )}>{step.label}</span
415
+ >
416
+ {#if when}
417
+ <span
418
+ class={cn(
419
+ "mt-0.5 block text-xs tabular-nums",
420
+ kind === "late"
421
+ ? "text-warning-800 dark:text-warning-400"
422
+ : "text-muted-foreground",
423
+ )}>{when}</span
424
+ >
425
+ {/if}
426
+ {/if}
427
+ </span>
428
+ </button>
429
+ {#if live && !insertBefore}
430
+ <span
431
+ class="pointer-events-none absolute right-2 -bottom-px left-2 z-10 h-0.5 rounded-full bg-[color:var(--tone-accent,var(--color-primary))]"
432
+ ></span>
433
+ {/if}
434
+ </div>
435
+ </ContextMenu.Trigger>
436
+ {#if onRename || onToggle || onReorder}
437
+ <ContextMenu.Content class="w-48">
438
+ {#if onRename}
439
+ <ContextMenu.Item onclick={() => startRename(step)}>
440
+ <PencilSimple class="mr-2 size-3.5" />
441
+ {t.rename}
442
+ </ContextMenu.Item>
443
+ {#if step.renamed}
444
+ <ContextMenu.Item onclick={() => onRename(step.id, "")}>
445
+ <ArrowCounterClockwise class="mr-2 size-3.5" />
446
+ {t.resetName}
447
+ </ContextMenu.Item>
448
+ {/if}
449
+ {/if}
450
+ {#if onReorder}
451
+ <ContextMenu.Item onclick={() => applyOrder(nudgeStep(ordered, step.id, -1))}>
452
+ <ArrowUp class="mr-2 size-3.5" />
453
+ {t.moveUp}
454
+ </ContextMenu.Item>
455
+ <ContextMenu.Item onclick={() => applyOrder(nudgeStep(ordered, step.id, 1))}>
456
+ <ArrowDown class="mr-2 size-3.5" />
457
+ {t.moveDown}
458
+ </ContextMenu.Item>
459
+ {/if}
460
+ {#if onToggle}
461
+ <ContextMenu.Item onclick={() => onToggle(step.id, false)}>
462
+ <EyeSlash class="mr-2 size-3.5" />
463
+ {t.hide}
464
+ </ContextMenu.Item>
465
+ {/if}
466
+ </ContextMenu.Content>
467
+ {/if}
468
+ </ContextMenu.Root>
469
+ {/each}
470
+ </div>
471
+ {/each}
472
+ </div>
473
+ {/if}
474
+ </div>