@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,418 @@
1
+ // Break apart, the reading half: which cards, shapes, frames and step numbers a figure holds.
2
+
3
+ import {
4
+ areaOf,
5
+ boxOfEl,
6
+ centerOf,
7
+ contains,
8
+ fontSize,
9
+ hasText,
10
+ isBold,
11
+ isEl,
12
+ num,
13
+ plus,
14
+ solid,
15
+ strokeEnds,
16
+ textLines,
17
+ translateOf,
18
+ visible,
19
+ within,
20
+ type Box,
21
+ type FigureChild,
22
+ type FigureEl,
23
+ type Pt,
24
+ } from "./figure-geometry.js";
25
+
26
+ /** A group with more text than this is a panel, not a card. */
27
+ const MAX_CARD_TEXTS = 8;
28
+ /** Smaller than this share of the drawing, or it is a panel. */
29
+ const MAX_CARD_AREA = 0.4;
30
+ /** How far an arrow end may sit from a card edge and still land on it, in drawing units. */
31
+ export const LAND_TOLERANCE = 8;
32
+ export const ARROWHEAD_MAX = 20;
33
+ /** A dot's number is at most this long. */
34
+ const BADGE_MAX_CHARS = 2;
35
+ const BADGE_MAX_R = 30;
36
+ /** Nothing under this on a side is a card or a shape: a rule or a speck. */
37
+ const MIN_SIDE = 4;
38
+
39
+ const ORIGIN: Pt = { x: 0, y: 0 };
40
+
41
+ export type Found = {
42
+ group: FigureEl;
43
+ offset: Pt;
44
+ rect: FigureEl;
45
+ box: Box;
46
+ claimed: Set<FigureChild>;
47
+ title: string;
48
+ kicker?: string;
49
+ body: string[];
50
+ /** The font size the title was drawn at, in drawing units. */
51
+ titleSize: number;
52
+ };
53
+
54
+ /** One drawn element with its box in drawing units and the siblings it sits among. */
55
+ type Atom = {
56
+ el: FigureEl;
57
+ box: Box | null;
58
+ offset: Pt;
59
+ kids: FigureEl[];
60
+ container: FigureEl;
61
+ pins: boolean;
62
+ };
63
+
64
+ type Boxed = Atom & { box: Box };
65
+ type Shape = { el: FigureEl; box: Box };
66
+ type Frame = { el: FigureEl; box: Box };
67
+ /** A number in a dot: a step number. */
68
+ export type Badge = { circle: FigureEl; text: FigureEl; center: Pt; r: number; label: string };
69
+
70
+ export type Loose = {
71
+ cards: Found[];
72
+ shapes: Shape[];
73
+ frames: Frame[];
74
+ badges: Badge[];
75
+ /** Panels that keep their chrome in the figure. */
76
+ panels: Box[];
77
+ };
78
+
79
+ /** What `cardFromRect` needs: the rect, the box it draws, and the siblings it may claim. */
80
+ type CardRequest = {
81
+ group: FigureEl;
82
+ kids: FigureEl[];
83
+ rect: FigureEl;
84
+ box: Box;
85
+ offset: Pt;
86
+ taken: Set<FigureChild>;
87
+ };
88
+
89
+ // =========================
90
+ // CARDS
91
+ // =========================
92
+
93
+ const anchorOf = (el: FigureEl, offset: Pt): Pt => ({
94
+ x: num(el.attrs.x) + offset.x,
95
+ y: num(el.attrs.y) + offset.y,
96
+ });
97
+
98
+ /** A solid rect that has text standing inside it. */
99
+ function holdsText(rect: FigureEl, texts: FigureEl[], offset: Pt): Box | null {
100
+ if (rect.tag !== "rect" || !visible(rect) || rect.attrs.fill === "none") return null;
101
+ const box = boxOfEl(rect, offset);
102
+ if (!box) return null;
103
+ return texts.some((t) => contains(box, anchorOf(t, offset), 3)) ? box : null;
104
+ }
105
+
106
+ /** A number in a dot is a pin marker, not copy: the ring that stays in the figure carries it. */
107
+ function splitBadge(inside: FigureEl[], kids: FigureEl[]) {
108
+ let badge: string | undefined;
109
+ const words: FigureEl[] = [];
110
+ const badgeParts: FigureEl[] = [];
111
+ for (const t of inside) {
112
+ const line = textLines(t).join(" ");
113
+ const dot = kids.find(
114
+ (k) =>
115
+ k.tag === "circle" &&
116
+ Math.hypot(num(k.attrs.cx) - num(t.attrs.x), num(k.attrs.cy) - num(t.attrs.y)) <=
117
+ num(k.attrs.r) + 2,
118
+ );
119
+ if (dot && line.length <= 2) {
120
+ badge = line;
121
+ badgeParts.push(dot, t);
122
+ } else words.push(t);
123
+ }
124
+ return { badge, words, badgeParts };
125
+ }
126
+
127
+ /** The title is the top bold line, not an end-pinned tag or a big count further down. */
128
+ function headlineOf(ordered: FigureEl[]) {
129
+ const titleEl =
130
+ ordered.find((t) => isBold(t) && t.attrs["text-anchor"] !== "end") ??
131
+ [...ordered].sort(
132
+ (a, b) => Number(isBold(b)) - Number(isBold(a)) || fontSize(b) - fontSize(a),
133
+ )[0];
134
+ const tag = ordered.find(
135
+ (t) =>
136
+ t !== titleEl &&
137
+ t.attrs["text-anchor"] === "end" &&
138
+ Math.abs(num(t.attrs.y) - num(titleEl.attrs.y)) < 8,
139
+ );
140
+ // A title set over two lines (CLAUDE, then CODE) is one title: the same face, directly after.
141
+ const titleEls = [titleEl];
142
+ const sameFace = (t: FigureEl) =>
143
+ fontSize(t) === fontSize(titleEl) &&
144
+ isBold(t) === isBold(titleEl) &&
145
+ t.attrs["text-anchor"] === titleEl.attrs["text-anchor"];
146
+ for (const t of ordered.slice(ordered.indexOf(titleEl) + 1)) {
147
+ if (t === tag || !sameFace(t)) break;
148
+ titleEls.push(t);
149
+ }
150
+ const bodyEls = ordered.filter((t) => !titleEls.includes(t) && t !== tag);
151
+ return { titleEls, tag, bodyEls };
152
+ }
153
+
154
+ /** Same-line fragments read as one line, left to right. */
155
+ function bodyLines(bodyEls: FigureEl[]): string[] {
156
+ const body: string[] = [];
157
+ let lastY = Number.NaN;
158
+ for (const t of bodyEls) {
159
+ const text = textLines(t).join(" ");
160
+ if (Math.abs(num(t.attrs.y) - lastY) < 3 && body.length > 0)
161
+ body[body.length - 1] += ` ${text}`;
162
+ else body.push(text);
163
+ lastY = num(t.attrs.y);
164
+ }
165
+ return body;
166
+ }
167
+
168
+ /** Anything else drawn inside the card's own rect is its chrome and goes with it. */
169
+ function chromeOf(req: CardRequest, claimed: Set<FigureChild>): FigureEl[] {
170
+ const { kids, box, offset, taken } = req;
171
+ return kids.filter((k) => {
172
+ if (claimed.has(k) || taken.has(k) || k.attrs.fill === "none") return false;
173
+ const kb = boxOfEl(k, offset);
174
+ return kb !== null && within(kb, box);
175
+ });
176
+ }
177
+
178
+ const readingOrder = (a: FigureEl, b: FigureEl) =>
179
+ num(a.attrs.y) - num(b.attrs.y) || num(a.attrs.x) - num(b.attrs.x);
180
+
181
+ /** One card: `rect` and whatever of its group's other children stands inside it. */
182
+ function cardFromRect(req: CardRequest): Found | null {
183
+ const { group, kids, rect, box, offset, taken } = req;
184
+ const inside = kids.filter(
185
+ (k) => hasText(k) && !taken.has(k) && contains(box, anchorOf(k, offset), 3),
186
+ );
187
+ if (inside.length === 0 || inside.length > MAX_CARD_TEXTS) return null;
188
+
189
+ const { badge, words, badgeParts } = splitBadge(inside, kids);
190
+ if (words.length === 0) return null;
191
+
192
+ const { titleEls, tag, bodyEls } = headlineOf([...words].sort(readingOrder));
193
+ const claimed = new Set<FigureChild>([
194
+ rect,
195
+ ...badgeParts,
196
+ ...titleEls,
197
+ ...(tag ? [tag] : []),
198
+ ...bodyEls,
199
+ ]);
200
+ for (const k of chromeOf(req, claimed)) claimed.add(k);
201
+
202
+ return {
203
+ group,
204
+ offset,
205
+ rect,
206
+ box,
207
+ claimed,
208
+ title: titleEls.map((t) => textLines(t).join(" ")).join(" "),
209
+ titleSize: fontSize(titleEls[0]),
210
+ kicker:
211
+ [badge, tag ? textLines(tag).join(" ") : undefined].filter(Boolean).join(" · ") || undefined,
212
+ body: bodyLines(bodyEls),
213
+ };
214
+ }
215
+
216
+ /** One group's children, split into the texts that might be copy. */
217
+ type GroupRead = { group: FigureEl; kids: FigureEl[]; texts: FigureEl[]; offset: Pt };
218
+
219
+ /** A `data-pins` group holds a pin per card rect. */
220
+ function readPinCards({ group, kids, texts, offset }: GroupRead): Found[] {
221
+ const taken = new Set<FigureChild>();
222
+ const out: Found[] = [];
223
+ for (const rect of kids) {
224
+ const box = holdsText(rect, texts, offset);
225
+ const found = box && cardFromRect({ group, kids, rect, box, offset, taken });
226
+ if (!found) continue;
227
+ for (const part of found.claimed) taken.add(part);
228
+ out.push(found);
229
+ }
230
+ return out;
231
+ }
232
+
233
+ /** Any other group is one card, unless it is a panel: too much text, a subgroup or too much of the drawing. */
234
+ function readOneCard({ group, kids, texts, offset }: GroupRead, drawing: Box): Found[] {
235
+ if (texts.length > MAX_CARD_TEXTS || kids.some((k) => k.tag === "g")) return [];
236
+ for (const rect of kids) {
237
+ const box = holdsText(rect, texts, offset);
238
+ if (!box) continue;
239
+ if (areaOf(box) > areaOf(drawing) * MAX_CARD_AREA) return [];
240
+ const found = cardFromRect({ group, kids, rect, box, offset, taken: new Set() });
241
+ return found ? [found] : [];
242
+ }
243
+ return [];
244
+ }
245
+
246
+ /** Cards in one group. */
247
+ function readCards(group: FigureEl, offset: Pt, drawing: Box): Found[] {
248
+ const kids = group.children.filter(isEl);
249
+ const texts = kids.filter(hasText);
250
+ if (texts.length === 0) return [];
251
+ const read = { group, kids, texts, offset };
252
+ return "data-pins" in group.attrs ? readPinCards(read) : readOneCard(read, drawing);
253
+ }
254
+
255
+ export function findCards(el: FigureEl, offset: Pt, drawing: Box, out: Found[]) {
256
+ for (const child of el.children) {
257
+ if (!isEl(child) || child.tag !== "g") continue;
258
+ const shift = translateOf(child);
259
+ if (!shift) continue;
260
+ const at = plus(offset, shift);
261
+ const found = readCards(child, at, drawing);
262
+ if (found.length > 0) out.push(...found);
263
+ else findCards(child, at, drawing, out);
264
+ }
265
+ }
266
+
267
+ // =========================
268
+ // LOOSE SHAPES
269
+ // =========================
270
+
271
+ function flatten(container: FigureEl, offset: Pt, pins: boolean, out: Atom[]) {
272
+ const kids = container.children.filter(isEl);
273
+ for (const el of kids) {
274
+ if (el.tag === "g") {
275
+ const shift = translateOf(el);
276
+ if (shift) flatten(el, plus(offset, shift), pins || "data-pins" in el.attrs, out);
277
+ continue;
278
+ }
279
+ if (el.tag === "defs" || el.tag === "title" || el.tag === "desc") continue;
280
+ out.push({ el, box: boxOfEl(el, offset), offset, kids, container, pins });
281
+ }
282
+ }
283
+
284
+ /** Whether a text sits on a dot and is short enough to be its number. */
285
+ function numbersDot(a: Atom, center: Pt, r: number): boolean {
286
+ const at = anchorOf(a.el, a.offset);
287
+ return (
288
+ textLines(a.el).join(" ").length <= BADGE_MAX_CHARS &&
289
+ Math.hypot(at.x - center.x, at.y - center.y) <= r + 2
290
+ );
291
+ }
292
+
293
+ /** A number in a dot is a step number. */
294
+ function findBadges(free: Atom[], texts: Atom[]) {
295
+ const parts = new Set<FigureChild>();
296
+ const badges: Badge[] = [];
297
+ for (const c of free.filter((a) => a.el.tag === "circle")) {
298
+ const r = num(c.el.attrs.r);
299
+ if (!c.box || r <= 0 || r > BADGE_MAX_R) continue;
300
+ const center = centerOf(c.box);
301
+ const t = texts.find((a) => !parts.has(a.el) && numbersDot(a, center, r));
302
+ if (!t) continue;
303
+ parts.add(c.el);
304
+ parts.add(t.el);
305
+ badges.push({ circle: c.el, text: t.el, center, r, label: textLines(t.el).join(" ") });
306
+ }
307
+ return { badges, parts };
308
+ }
309
+
310
+ const isTip = (box: Box | null) =>
311
+ box !== null && box.x2 - box.x1 <= ARROWHEAD_MAX && box.y2 - box.y1 <= ARROWHEAD_MAX;
312
+
313
+ /** A line that ends on two items joins them, and a small filled tip beside it is its arrowhead. */
314
+ function isConnector(a: Atom, itemBoxes: Box[]): boolean {
315
+ if (!["line", "polyline", "path"].includes(a.el.tag)) return false;
316
+ if (solid(a.el.attrs.fill)) return isTip(a.box);
317
+ const ends = strokeEnds(a.el, a.offset);
318
+ return ends !== null && ends.every((p) => itemBoxes.some((b) => contains(b, p, LAND_TOLERANCE)));
319
+ }
320
+
321
+ /** What `sweepRects` weighs each rect against. */
322
+ type Sweep = {
323
+ rects: Boxed[];
324
+ words: Atom[];
325
+ strays: Atom[];
326
+ groupCards: Found[];
327
+ drawing: Box;
328
+ taken: Set<FigureChild>;
329
+ };
330
+
331
+ /** A rect with no rect inside it: a bare shape, or a card when it holds its own text. */
332
+ function leafRect(sweep: Sweep, x: Boxed, hasWords: boolean, out: Loose) {
333
+ if (!hasWords) {
334
+ out.shapes.push({ el: x.el, box: x.box });
335
+ return;
336
+ }
337
+ if (areaOf(x.box) > areaOf(sweep.drawing) * MAX_CARD_AREA) return;
338
+ const found = cardFromRect({
339
+ group: x.container,
340
+ kids: x.kids,
341
+ rect: x.el,
342
+ box: x.box,
343
+ offset: x.offset,
344
+ taken: sweep.taken,
345
+ });
346
+ if (!found) return;
347
+ for (const part of found.claimed) sweep.taken.add(part);
348
+ out.cards.push(found);
349
+ }
350
+
351
+ /** A rect round other items is a panel, a frame or a shape, by what it holds. */
352
+ function classifyRect(sweep: Sweep, x: Boxed, dropped: Set<Atom>, out: Loose) {
353
+ const { box } = x;
354
+ const inner = sweep.rects.filter(
355
+ (o) => o !== x && !dropped.has(o) && within(o.box, box, 1) && areaOf(o.box) < areaOf(box),
356
+ );
357
+ const innerBoxes = [
358
+ ...inner.map((o) => o.box),
359
+ ...sweep.groupCards.filter((c) => within(c.box, box, 1)).map((c) => c.box),
360
+ ];
361
+ const held = (point: Pt) =>
362
+ innerBoxes.some((b) => contains(b, point, 3)) ? false : contains(box, point, 3);
363
+ const own = sweep.words.filter((w) => held(anchorOf(w.el, w.offset)));
364
+
365
+ if (innerBoxes.length === 0) {
366
+ leafRect(sweep, x, own.length > 0, out);
367
+ return;
368
+ }
369
+ const stray = sweep.strays.some(
370
+ ({ box: sb }) => sb !== null && within(sb, box, 1) && !innerBoxes.some((b) => within(sb, b, 1)),
371
+ );
372
+ if (own.length > 0 || stray) {
373
+ out.panels.push(box);
374
+ for (const o of inner) dropped.add(o);
375
+ return;
376
+ }
377
+ if (innerBoxes.length >= 2) out.frames.push({ el: x.el, box });
378
+ else out.shapes.push({ el: x.el, box });
379
+ }
380
+
381
+ function sweepRects(sweep: Sweep, out: Loose) {
382
+ const dropped = new Set<Atom>();
383
+ for (const x of sweep.rects) {
384
+ if (!dropped.has(x)) classifyRect(sweep, x, dropped, out);
385
+ }
386
+ }
387
+
388
+ /** Loose cards, bare rectangles, frames and step numbers. A rectangle with its own text and shapes inside is a panel, so its chrome stays. */
389
+ export function findLoose(
390
+ tree: FigureEl,
391
+ drawing: Box,
392
+ taken: Set<FigureChild>,
393
+ groupCards: Found[],
394
+ ): Loose {
395
+ const atoms: Atom[] = [];
396
+ flatten(tree, ORIGIN, false, atoms);
397
+ const free = atoms.filter((a) => !a.pins && a.box && !taken.has(a.el));
398
+ const texts = free.filter((a) => hasText(a.el));
399
+ const { badges, parts } = findBadges(free, texts);
400
+
401
+ const rects = free
402
+ .filter((a): a is Boxed => a.el.tag === "rect" && visible(a.el) && a.box !== null)
403
+ .filter((a) => a.box.x2 - a.box.x1 >= MIN_SIDE && a.box.y2 - a.box.y1 >= MIN_SIDE)
404
+ .sort((a, b) => areaOf(b.box) - areaOf(a.box));
405
+ const itemBoxes = [...rects.map((a) => a.box), ...groupCards.map((c) => c.box)];
406
+ const strays = free.filter(
407
+ (a) => !["rect", "text"].includes(a.el.tag) && !parts.has(a.el) && !isConnector(a, itemBoxes),
408
+ );
409
+
410
+ const out: Loose = { cards: [], shapes: [], frames: [], badges, panels: [] };
411
+ const words = texts.filter((a) => !parts.has(a.el));
412
+ sweepRects({ rects, words, strays, groupCards, drawing, taken }, out);
413
+
414
+ // A number a card already took is not a mark of its own.
415
+ out.badges = out.badges.filter((b) => !taken.has(b.circle) && !taken.has(b.text));
416
+ out.badges = out.badges.filter((b) => !out.panels.some((p) => contains(p, b.center, 0)));
417
+ return out;
418
+ }
@@ -0,0 +1,104 @@
1
+ // Break apart, the reading half: which cards, shapes, frames, step numbers and arrows a figure holds.
2
+
3
+ import {
4
+ boxOfEl,
5
+ centerOf,
6
+ isEl,
7
+ plus,
8
+ solid,
9
+ strokeEnds,
10
+ translateOf,
11
+ type Box,
12
+ type FigureChild,
13
+ type FigureEl,
14
+ type Pt,
15
+ } from "./figure-geometry.js";
16
+ import {
17
+ ARROWHEAD_MAX,
18
+ findCards,
19
+ findLoose,
20
+ type Found,
21
+ type Loose,
22
+ } from "./break-apart-find.js";
23
+
24
+ const ORIGIN: Pt = { x: 0, y: 0 };
25
+
26
+ export type Arrow = {
27
+ el: FigureEl;
28
+ head?: FigureEl;
29
+ from: Pt;
30
+ to: Pt;
31
+ startArrow: boolean;
32
+ endArrow: boolean;
33
+ };
34
+
35
+ /** Everything a figure breaks into, with the arrows that may link the cards. */
36
+ export type Pieces = { cards: Found[]; loose: Loose; arrows: Arrow[] };
37
+
38
+ /** A closed path with a fill: the shape of a drawn arrowhead. */
39
+ function isTipPath(el: FigureEl): boolean {
40
+ return el.tag === "path" && /z\s*$/i.test(el.attrs.d ?? "") && solid(el.attrs.fill);
41
+ }
42
+
43
+ /** A small filled closed path beside a line's end: the way the pin figures draw an arrowhead. */
44
+ function headNear(
45
+ siblings: FigureChild[],
46
+ from: number,
47
+ ends: [Pt, Pt],
48
+ offset: Pt,
49
+ ): { el: FigureEl; end: 0 | 1 } | null {
50
+ // Whitespace between tags is a string child, so the next element is not always the next child.
51
+ const next = siblings.slice(from + 1).find(isEl);
52
+ if (!next || !isTipPath(next)) return null;
53
+ const box = boxOfEl(next, offset);
54
+ if (!box || box.x2 - box.x1 > ARROWHEAD_MAX || box.y2 - box.y1 > ARROWHEAD_MAX) return null;
55
+ const middle = centerOf(box);
56
+ const d0 = Math.hypot(middle.x - ends[0].x, middle.y - ends[0].y);
57
+ const d1 = Math.hypot(middle.x - ends[1].x, middle.y - ends[1].y);
58
+ if (Math.min(d0, d1) > ARROWHEAD_MAX) return null;
59
+ return { el: next, end: d1 <= d0 ? 1 : 0 };
60
+ }
61
+
62
+ /** A stroked line, polyline or path with no fill: a candidate link. */
63
+ function isStrokeOnly(el: FigureEl): boolean {
64
+ if (el.tag !== "path" && el.tag !== "line" && el.tag !== "polyline") return false;
65
+ return solid(el.attrs.stroke) && !solid(el.attrs.fill);
66
+ }
67
+
68
+ function arrowOf(parent: FigureEl, child: FigureEl, index: number, offset: Pt): Arrow | null {
69
+ if (!isStrokeOnly(child)) return null;
70
+ const ends = strokeEnds(child, offset);
71
+ if (!ends) return null;
72
+ const head = headNear(parent.children, index, ends, offset);
73
+ const startArrow = Boolean(child.attrs["marker-start"]) || head?.end === 0;
74
+ const endArrow = Boolean(child.attrs["marker-end"]) || head?.end === 1;
75
+ // With no head at all the line still links two cards; it just draws without a tip.
76
+ return { el: child, head: head?.el, from: ends[0], to: ends[1], startArrow, endArrow };
77
+ }
78
+
79
+ function collectArrows(el: FigureEl, offset: Pt, out: Arrow[]) {
80
+ el.children.forEach((child, index) => {
81
+ if (!isEl(child)) return;
82
+ if (child.tag !== "g") {
83
+ const arrow = arrowOf(el, child, index, offset);
84
+ if (arrow) out.push(arrow);
85
+ return;
86
+ }
87
+ const shift = translateOf(child);
88
+ if (shift) collectArrows(child, plus(offset, shift), out);
89
+ });
90
+ }
91
+
92
+ /** Cards, loose pieces and arrows in a figure, or null when nothing in it can break apart. */
93
+ export function findPieces(tree: FigureEl, drawing: Box): Pieces | null {
94
+ const groupCards: Found[] = [];
95
+ findCards(tree, ORIGIN, drawing, groupCards);
96
+ const claimed = new Set<FigureChild>();
97
+ for (const card of groupCards) for (const part of card.claimed) claimed.add(part);
98
+ const loose = findLoose(tree, drawing, claimed, groupCards);
99
+ const cards = [...groupCards, ...loose.cards];
100
+ if (cards.length + loose.shapes.length + loose.frames.length === 0) return null;
101
+ const arrows: Arrow[] = [];
102
+ collectArrows(tree, ORIGIN, arrows);
103
+ return { cards, loose, arrows };
104
+ }