@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,30 @@
1
+ import { untrack } from "svelte";
2
+
3
+ // One flag per canvas: a card reading it re-renders when it flips, not on every zoom frame.
4
+
5
+ /** Below this zoom a card is a box. */
6
+ export const LOW_BELOW = 0.4;
7
+ /** Back to the full card above this. The gap stops a zoom held at the line from swapping every card each frame. */
8
+ export const FULL_ABOVE = 0.46;
9
+
10
+ type ZoomSource = { viewport: { zoom: number } };
11
+
12
+ const modes = new WeakMap<object, { low: boolean }>();
13
+
14
+ /** The shared flag for one canvas's store. The first card to ask starts the one effect that keeps it. */
15
+ export function lodMode(store: ZoomSource): { readonly low: boolean } {
16
+ const known = modes.get(store);
17
+ if (known) return known;
18
+ const mode = $state({ low: store.viewport.zoom < LOW_BELOW });
19
+ modes.set(store, mode);
20
+ $effect.root(() => {
21
+ $effect(() => {
22
+ const zoom = store.viewport.zoom;
23
+ untrack(() => {
24
+ if (mode.low && zoom > FULL_ABOVE) mode.low = false;
25
+ else if (!mode.low && zoom < LOW_BELOW) mode.low = true;
26
+ });
27
+ });
28
+ });
29
+ return mode;
30
+ }
@@ -0,0 +1,140 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import type { Snippet } from "svelte";
6
+ import { useStore } from "@xyflow/svelte";
7
+ import SideHandles from "../chrome/side-handles.svelte";
8
+ import { lodMode } from "./lod-mode.svelte.js";
9
+ import { renderAll } from "./render-all.svelte.js";
10
+
11
+ // Zoomed out, a card is a box at its measured size, so no arrow moves.
12
+ // Never measured, selected, dragged or exported: full.
13
+
14
+ // =========================
15
+ // TYPES
16
+ // =========================
17
+ interface Props {
18
+ id: string;
19
+ selected?: boolean;
20
+ dragging?: boolean;
21
+ /** A CSS colour: the card's own, for the box's border and icon well. */
22
+ tone: string;
23
+ title: string;
24
+ /** The full card. */
25
+ children: Snippet;
26
+ }
27
+
28
+ // =========================
29
+ // PROPS
30
+ // =========================
31
+ let { id, selected = false, dragging = false, tone, title, children }: Props = $props();
32
+
33
+ // =========================
34
+ // STATE
35
+ // =========================
36
+ const store = useStore();
37
+ const mode = lodMode(store);
38
+ let probe = $state<HTMLElement>();
39
+ // xyflow keeps the size of a card that scrolled away, so it comes back as a box.
40
+ let size = $state<{ width: number; height: number } | null>(measured());
41
+ // Plain on purpose: only the observer below reads it.
42
+ let boxed = false;
43
+
44
+ // =========================
45
+ // DERIVED
46
+ // =========================
47
+ let simple = $derived(mode.low && size !== null && !selected && !dragging && !renderAll.on);
48
+
49
+ // =========================
50
+ // EFFECTS
51
+ // =========================
52
+ // Sizes the simple box from the full card. The observer's first call carries it, so a mount forces no layout.
53
+ $effect(() => {
54
+ const node = probe?.closest<HTMLElement>(".svelte-flow__node");
55
+ if (!node) return;
56
+ const observer = new ResizeObserver(([entry]) => {
57
+ if (boxed) return;
58
+ const box = entry.borderBoxSize[0];
59
+ const width = Math.round(box.inlineSize);
60
+ const height = Math.round(box.blockSize);
61
+ if (width && height && (width !== size?.width || height !== size?.height)) size = { width, height };
62
+ });
63
+ observer.observe(node);
64
+ return () => observer.disconnect();
65
+ });
66
+
67
+ // Set after the render that swaps the box in, so the read above never sees the box's own size.
68
+ $effect(() => {
69
+ boxed = simple;
70
+ });
71
+
72
+ // =========================
73
+ // FUNCTIONS
74
+ // =========================
75
+ function measured(): { width: number; height: number } | null {
76
+ const known = store.nodeLookup.get(id)?.measured;
77
+ return known?.width && known.height ? { width: known.width, height: known.height } : null;
78
+ }
79
+ </script>
80
+
81
+ <!-- =========================== -->
82
+ <!-- MARKUP -->
83
+ <!-- =========================== -->
84
+ <span class="cv-lod-probe" bind:this={probe} hidden></span>
85
+ {#if simple && size}
86
+ <div class="cv-lod-box" style="width: {size.width}px; height: {size.height}px; --cv-lod-tone: {tone}" data-lod="simple">
87
+ <span class="cv-lod-well"></span>
88
+ <span class="cv-lod-title">{title}</span>
89
+ </div>
90
+ <SideHandles class="cv-lod-handle" />
91
+ {:else}
92
+ {@render children()}
93
+ {/if}
94
+
95
+ <!-- =========================== -->
96
+ <!-- STYLES -->
97
+ <!-- =========================== -->
98
+ <style>
99
+ :global([data-cv-every-card] .svelte-flow__node) {
100
+ animation: none;
101
+ }
102
+
103
+ /* A dot on a zoomed-out box is too small to link from; unpainted, it keeps its place for the links. */
104
+ :global(.cv-lod-handle) {
105
+ visibility: hidden;
106
+ }
107
+
108
+ /* The architecture card's own frame and layout (architecture-node.svelte), so selecting a card doesn't change how it looks. */
109
+ .cv-lod-box {
110
+ position: relative;
111
+ box-sizing: border-box;
112
+ display: flex;
113
+ align-items: flex-start;
114
+ gap: 10px;
115
+ overflow: hidden;
116
+ padding: var(--card-pad, 12px);
117
+ border: 1px solid color-mix(in oklab, var(--cv-lod-tone) 20%, transparent);
118
+ border-radius: var(--card-radius, 12px);
119
+ background: var(--card, var(--cv-chrome-surface, #fff));
120
+ color: var(--cv-ink, var(--foreground, #1a1a1f));
121
+ contain: strict;
122
+ }
123
+
124
+ /* The icon well, without the icon. */
125
+ .cv-lod-well {
126
+ flex: none;
127
+ width: 32px;
128
+ height: 32px;
129
+ border-radius: 9999px;
130
+ background: color-mix(in oklab, var(--cv-lod-tone) 10%, transparent);
131
+ }
132
+
133
+ .cv-lod-title {
134
+ overflow: hidden;
135
+ font-size: 14px;
136
+ font-weight: 500;
137
+ line-height: 1.25;
138
+ overflow-wrap: anywhere;
139
+ }
140
+ </style>
@@ -0,0 +1,52 @@
1
+ import { tick } from "svelte";
2
+
3
+ // An export reads the page's markup, so it holds this open: every card is drawn in full until it lets go.
4
+
5
+ const hold = $state({ count: 0 });
6
+
7
+ /** True while something needs every card drawn in full. */
8
+ export const renderAll = {
9
+ get on(): boolean {
10
+ return hold.count > 0;
11
+ },
12
+ };
13
+
14
+ const frame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
15
+ // Arrows draw a beat after their cards, once the cards are measured, so they are counted too.
16
+ const DRAWN = ".svelte-flow__node, .svelte-flow__edge";
17
+ const SETTLE_FRAMES = 8;
18
+ const GIVE_UP_MS = 4000;
19
+ /** The page's card fade-in is switched off under this attribute, so a card that remounts is captured solid. */
20
+ export const EVERY_CARD_ATTR = "data-cv-every-card";
21
+
22
+ /** Runs `use` with every card drawn in full. Waits until the number of cards and arrows on the page holds still. */
23
+ export async function withEveryCard<T>(root: ParentNode, use: () => Promise<T>): Promise<T> {
24
+ hold.count += 1;
25
+ const marked = root instanceof Element ? root : null;
26
+ marked?.setAttribute(EVERY_CARD_ATTR, "");
27
+ try {
28
+ await tick();
29
+ const start = performance.now();
30
+ let last = -1;
31
+ let still = 0;
32
+ while (still < SETTLE_FRAMES && performance.now() - start < GIVE_UP_MS) {
33
+ await frame();
34
+ const now = root.querySelectorAll(DRAWN).length;
35
+ still = now === last ? still + 1 : 0;
36
+ last = now;
37
+ }
38
+ // A fade that began before the switch-off still has to land.
39
+ if ("getAnimations" in root) {
40
+ const running = (root as Element).getAnimations({ subtree: true });
41
+ await Promise.race([
42
+ Promise.allSettled(running.map((animation) => animation.finished)),
43
+ new Promise((resolve) => setTimeout(resolve, 1000)),
44
+ ]);
45
+ }
46
+ await frame();
47
+ return await use();
48
+ } finally {
49
+ hold.count -= 1;
50
+ if (hold.count === 0) marked?.removeAttribute(EVERY_CARD_ATTR);
51
+ }
52
+ }
@@ -0,0 +1,108 @@
1
+ // Align, distribute and stack as pure maths, so the editor and the CLI place
2
+ // nodes the same way.
3
+
4
+ export interface ArrangeItem {
5
+ id: string;
6
+ position: { x: number; y: number };
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ export type Axis = "horizontal" | "vertical";
12
+
13
+ export type Positions = Map<string, { x: number; y: number }>;
14
+
15
+ export const ALIGNMENTS = [
16
+ "left",
17
+ "right",
18
+ "top",
19
+ "bottom",
20
+ "center-horizontal",
21
+ "center-vertical",
22
+ ] as const;
23
+
24
+ export type Alignment = (typeof ALIGNMENTS)[number];
25
+
26
+ // =========================
27
+ // ALIGN
28
+ // =========================
29
+
30
+ /** "center-horizontal" puts the centres on one vertical axis, as tldraw named it. */
31
+ export function alignPositions(items: readonly ArrangeItem[], alignment: Alignment): Positions {
32
+ const moved: Positions = new Map();
33
+ if (items.length < 2) return moved;
34
+ const left = Math.min(...items.map((item) => item.position.x));
35
+ const top = Math.min(...items.map((item) => item.position.y));
36
+ const right = Math.max(...items.map((item) => item.position.x + item.width));
37
+ const bottom = Math.max(...items.map((item) => item.position.y + item.height));
38
+
39
+ for (const item of items) {
40
+ const { x, y } = item.position;
41
+ const next = { x, y };
42
+ if (alignment === "left") next.x = left;
43
+ else if (alignment === "right") next.x = right - item.width;
44
+ else if (alignment === "top") next.y = top;
45
+ else if (alignment === "bottom") next.y = bottom - item.height;
46
+ else if (alignment === "center-horizontal") next.x = (left + right) / 2 - item.width / 2;
47
+ else next.y = (top + bottom) / 2 - item.height / 2;
48
+ if (next.x !== x || next.y !== y) moved.set(item.id, next);
49
+ }
50
+ return moved;
51
+ }
52
+
53
+ // =========================
54
+ // DISTRIBUTE AND STACK
55
+ // =========================
56
+
57
+ function along(axis: Axis) {
58
+ const horizontal = axis === "horizontal";
59
+ return {
60
+ start: (item: ArrangeItem) => (horizontal ? item.position.x : item.position.y),
61
+ size: (item: ArrangeItem) => (horizontal ? item.width : item.height),
62
+ place: (item: ArrangeItem, at: number) =>
63
+ horizontal ? { x: at, y: item.position.y } : { x: item.position.x, y: at },
64
+ };
65
+ }
66
+
67
+ function inOrder(items: readonly ArrangeItem[], axis: Axis): ArrangeItem[] {
68
+ const { start } = along(axis);
69
+ return [...items].sort((a, b) => start(a) - start(b));
70
+ }
71
+
72
+ /** Space the boxes so every gap matches. The first and last stay where they are. */
73
+ export function distributePositions(items: readonly ArrangeItem[], axis: Axis): Positions {
74
+ const moved: Positions = new Map();
75
+ if (items.length < 3) return moved;
76
+ const { start, size, place } = along(axis);
77
+ const sorted = inOrder(items, axis);
78
+ const first = sorted[0];
79
+ const last = sorted[sorted.length - 1];
80
+ if (!first || !last) return moved;
81
+ const span = start(last) + size(last) - start(first);
82
+ const used = sorted.reduce((sum, item) => sum + size(item), 0);
83
+ const gap = (span - used) / (sorted.length - 1);
84
+
85
+ let cursor = start(first);
86
+ for (const item of sorted) {
87
+ if (cursor !== start(item)) moved.set(item.id, place(item, cursor));
88
+ cursor += size(item) + gap;
89
+ }
90
+ return moved;
91
+ }
92
+
93
+ /** Put the boxes in a row or a column, `gap` apart, in the order they sit now. The first stays put. */
94
+ export function stackPositions(items: readonly ArrangeItem[], axis: Axis, gap: number): Positions {
95
+ const moved: Positions = new Map();
96
+ if (items.length < 2) return moved;
97
+ const { start, size, place } = along(axis);
98
+ const sorted = inOrder(items, axis);
99
+ const first = sorted[0];
100
+ if (!first) return moved;
101
+
102
+ let cursor = start(first);
103
+ for (const item of sorted) {
104
+ if (cursor !== start(item)) moved.set(item.id, place(item, cursor));
105
+ cursor += size(item) + gap;
106
+ }
107
+ return moved;
108
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from "vite-plus/test";
2
+ import { z } from "zod";
3
+ import type { OpDefinition } from "../plugin.js";
4
+ import { CanvasOpError, applyOps, canvasOpSchema } from "./ops.js";
5
+
6
+ const board = () => ({
7
+ nodes: [
8
+ { id: "a", type: "box", position: { x: 0, y: 0 }, data: { label: "A" } },
9
+ { id: "b", type: "box", position: { x: 10, y: 10 }, data: { label: "B" } },
10
+ ],
11
+ edges: [],
12
+ });
13
+
14
+ describe("applyOps", () => {
15
+ it("adds a parented node with its extent and rejects a duplicate id", () => {
16
+ const next = applyOps(board(), [
17
+ { kind: "addNode", node: { id: "c", type: "box", data: {}, parentId: "a" } },
18
+ ]);
19
+ expect(next.nodes.at(-1)).toMatchObject({ id: "c", parentId: "a", extent: "parent" });
20
+ expect(() =>
21
+ applyOps(board(), [{ kind: "addNode", node: { id: "a", type: "box", data: {} } }]),
22
+ ).toThrow(CanvasOpError);
23
+ });
24
+
25
+ it("merges a data patch and keeps the other fields", () => {
26
+ const next = applyOps(board(), [{ kind: "patchNode", nodeId: "a", data: { note: "x" } }]);
27
+ expect(next.nodes[0].data).toEqual({ label: "A", note: "x" });
28
+ });
29
+
30
+ it("removes a node with its edges", () => {
31
+ const withEdge = {
32
+ ...board(),
33
+ edges: [{ id: "e", source: "a", target: "b" }],
34
+ };
35
+ const next = applyOps(withEdge, [{ kind: "removeNode", nodeId: "a" }]);
36
+ expect(next.nodes.map((n) => n.id)).toEqual(["b"]);
37
+ expect(next.edges).toEqual([]);
38
+ });
39
+
40
+ it("re-points an edge with patchEdge and rejects a missing endpoint", () => {
41
+ const withEdge = { ...board(), edges: [{ id: "e", source: "a", target: "b" }] };
42
+ const next = applyOps(withEdge, [{ kind: "patchEdge", edgeId: "e", patch: { target: "a" } }]);
43
+ expect(next.edges[0]).toMatchObject({ id: "e", source: "a", target: "a" });
44
+ expect(() =>
45
+ applyOps(withEdge, [{ kind: "patchEdge", edgeId: "e", patch: { target: "gone" } }]),
46
+ ).toThrow(CanvasOpError);
47
+ expect(() =>
48
+ applyOps(withEdge, [{ kind: "patchEdge", edgeId: "e", patch: { source: "gone" } }]),
49
+ ).toThrow(CanvasOpError);
50
+ });
51
+
52
+ it("detaches a node with setParent null", () => {
53
+ const next = applyOps(board(), [
54
+ { kind: "setParent", nodeId: "b", parentId: "a" },
55
+ { kind: "setParent", nodeId: "b", parentId: null },
56
+ ]);
57
+ expect(next.nodes[1]).not.toHaveProperty("parentId");
58
+ });
59
+
60
+ it("names the op index on an error", () => {
61
+ try {
62
+ applyOps(board(), [
63
+ { kind: "moveNode", nodeId: "a", position: { x: 1, y: 1 } },
64
+ { kind: "removeNode", nodeId: "zzz" },
65
+ ]);
66
+ expect.unreachable();
67
+ } catch (e) {
68
+ expect((e as CanvasOpError).opIndex).toBe(1);
69
+ }
70
+ });
71
+
72
+ it("throws on a kind nobody registered", () => {
73
+ expect(() => applyOps(board(), [{ kind: "nope" }])).toThrow(/Unknown op kind/);
74
+ });
75
+
76
+ it("runs a plugin op beside the core ones", () => {
77
+ const rename = z.object({ kind: z.literal("rename"), nodeId: z.string(), label: z.string() });
78
+ const def: OpDefinition<z.infer<typeof rename>> = {
79
+ kind: "rename",
80
+ schema: rename,
81
+ apply: (state, op) => {
82
+ state.nodes = state.nodes.map((n) =>
83
+ n.id === op.nodeId ? { ...n, data: { ...(n.data as object), label: op.label } } : n,
84
+ );
85
+ },
86
+ };
87
+ const next = applyOps(board(), [{ kind: "rename", nodeId: "b", label: "Beta" }], [def]);
88
+ expect(next.nodes[1].data).toEqual({ label: "Beta" });
89
+ });
90
+ });
91
+
92
+ describe("canvasOpSchema", () => {
93
+ it("rejects an empty edge patch", () => {
94
+ expect(canvasOpSchema.safeParse({ kind: "patchEdge", edgeId: "e", patch: {} }).success).toBe(
95
+ false,
96
+ );
97
+ });
98
+ });