@heyramzi/ui 2.11.1 → 2.11.2

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 (266) hide show
  1. package/dist/agents/helpers.js +38 -38
  2. package/dist/agents/page-state.js +49 -50
  3. package/dist/agents/vendor/color.js +146 -127
  4. package/dist/agents/vendor/draw.js +617 -508
  5. package/dist/agents/vendor/engine.js +698 -599
  6. package/dist/agents/vendor/mesh.js +653 -683
  7. package/dist/agents/vendor/plastic.js +3913 -3112
  8. package/dist/agents/vendor/presets.js +163 -164
  9. package/dist/agents/vendor/shapes.js +21 -20
  10. package/dist/agents/vendor/ticker.js +41 -42
  11. package/dist/agents/vendor/types.js +1 -0
  12. package/dist/agents/vendor/wear.js +1421 -1448
  13. package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
  14. package/dist/ai/confirmation/index.js +3 -1
  15. package/dist/ai/index.js +33 -0
  16. package/dist/ai/prompt-input/index.js +3 -1
  17. package/dist/ai/reasoning/index.js +3 -1
  18. package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
  19. package/dist/ai/suggestion/index.js +3 -1
  20. package/dist/ai/typing-dots/index.js +3 -1
  21. package/dist/animations/text-swap.js +24 -24
  22. package/dist/brand/index.js +54 -48
  23. package/dist/canvas/card/resize.js +5 -3
  24. package/dist/canvas/edges/edge-path.js +255 -242
  25. package/dist/canvas/editor/context-menu.js +39 -36
  26. package/dist/canvas/editor/dispatch.svelte.js +154 -151
  27. package/dist/canvas/editor/editor-context.js +7 -2
  28. package/dist/canvas/editor/editor-mode.js +1 -0
  29. package/dist/canvas/editor/frame.js +73 -76
  30. package/dist/canvas/editor/hand-context.js +5 -2
  31. package/dist/canvas/editor/history-core.js +163 -191
  32. package/dist/canvas/editor/keymap.js +95 -169
  33. package/dist/canvas/editor/layout-actions.js +143 -144
  34. package/dist/canvas/editor/lock.js +52 -51
  35. package/dist/canvas/editor/rebase.js +217 -240
  36. package/dist/canvas/editor/style.js +132 -137
  37. package/dist/canvas/editor/text-field-context.js +7 -2
  38. package/dist/canvas/editor/z-order.js +65 -70
  39. package/dist/canvas/headless/alignment.js +22 -16
  40. package/dist/canvas/headless/break-apart-find.js +286 -279
  41. package/dist/canvas/headless/break-apart-pieces.js +62 -58
  42. package/dist/canvas/headless/break-apart-plan.js +299 -325
  43. package/dist/canvas/headless/break-apart-style.js +75 -70
  44. package/dist/canvas/headless/break-apart.js +188 -181
  45. package/dist/canvas/headless/canvas-cleanup.js +78 -58
  46. package/dist/canvas/headless/edge-anchors.js +115 -116
  47. package/dist/canvas/headless/edge-memo.js +24 -22
  48. package/dist/canvas/headless/edge-serialization.js +17 -27
  49. package/dist/canvas/headless/figure-geometry.js +232 -264
  50. package/dist/canvas/headless/fit-viewport.js +84 -67
  51. package/dist/canvas/headless/grid.js +88 -86
  52. package/dist/canvas/headless/icon-node.js +44 -36
  53. package/dist/canvas/headless/index.js +1 -0
  54. package/dist/canvas/headless/linkify.js +17 -19
  55. package/dist/canvas/headless/pen-marks.js +101 -95
  56. package/dist/canvas/headless/sketch.js +231 -209
  57. package/dist/canvas/headless/snap-guides.js +99 -83
  58. package/dist/canvas/headless/svg-figure.js +133 -117
  59. package/dist/canvas/headless/table-layout.js +146 -107
  60. package/dist/canvas/index.js +2 -0
  61. package/dist/canvas/lod/lod-mode.svelte.js +20 -18
  62. package/dist/canvas/lod/render-all.svelte.js +36 -39
  63. package/dist/canvas/ops/arrange.js +78 -71
  64. package/dist/canvas/ops/ops.js +260 -255
  65. package/dist/canvas/pen/colors.js +37 -35
  66. package/dist/canvas/pen/eraser.js +28 -27
  67. package/dist/canvas/pen/geometry.js +120 -130
  68. package/dist/canvas/pen/index.js +1 -0
  69. package/dist/canvas/pen/ops.js +11 -11
  70. package/dist/canvas/pen/pen-session.js +150 -159
  71. package/dist/canvas/pen/plugin.js +107 -104
  72. package/dist/canvas/pen/shapes.js +73 -68
  73. package/dist/canvas/pen/stroke-path.js +32 -30
  74. package/dist/canvas/pen/types.js +21 -21
  75. package/dist/canvas/pen/ui.js +10 -8
  76. package/dist/canvas/plugin.js +46 -46
  77. package/dist/canvas/screen/glyphs.js +12 -11
  78. package/dist/canvas/screen/labels.js +22 -22
  79. package/dist/canvas/screen/parts.js +98 -111
  80. package/dist/canvas/screen/plugin.js +47 -56
  81. package/dist/canvas/screen/types.js +30 -35
  82. package/dist/canvas/screen/ui.js +7 -7
  83. package/dist/canvas/test-utils.js +14 -14
  84. package/dist/canvas/types.js +1 -0
  85. package/dist/canvas/ui/forward-wheel.js +24 -25
  86. package/dist/canvas/ui/icons.js +64 -131
  87. package/dist/canvas/ui/place-panel.js +58 -49
  88. package/dist/canvas/ui/shortcut-tip.js +14 -12
  89. package/dist/canvas/ui/tools.js +300 -385
  90. package/dist/charts/dashboard/chart.js +4 -0
  91. package/dist/charts/dashboard/data-table.js +11 -0
  92. package/dist/charts/dashboard/windows.js +8 -3
  93. package/dist/charts/edge-fade.js +10 -5
  94. package/dist/charts/hover-band.js +16 -11
  95. package/dist/charts/index.js +0 -1
  96. package/dist/charts/motion.js +11 -7
  97. package/dist/charts/primitives/chart-utils.js +12 -12
  98. package/dist/components/copy/clipboard.js +32 -32
  99. package/dist/components/copy/copied.svelte.js +31 -23
  100. package/dist/components/data-table/data-table.js +39 -39
  101. package/dist/components/docs/index.js +4 -0
  102. package/dist/components/docs/scroll-spy.svelte.js +73 -68
  103. package/dist/components/list-page/filter-section.js +4 -1
  104. package/dist/components/list-page/index.js +2 -0
  105. package/dist/components/list-page/list-table.js +7 -2
  106. package/dist/components/list-page/queue-rail.js +14 -0
  107. package/dist/components/list-page/section-host.js +4 -2
  108. package/dist/components/list-page/step.js +4 -3
  109. package/dist/components/list-page/types.js +1 -0
  110. package/dist/components/picker/trigger.js +48 -30
  111. package/dist/components/picker/types.js +1 -0
  112. package/dist/components/segmented/collapse.js +38 -27
  113. package/dist/components/segmented/index.js +8 -0
  114. package/dist/components/segmented/segmented.js +67 -42
  115. package/dist/components/star-rating-input/index.js +3 -1
  116. package/dist/components/table-view/types.js +1 -0
  117. package/dist/components/tags-input/tags.js +81 -69
  118. package/dist/components/theme/index.js +4 -0
  119. package/dist/components/toolbar/index.js +4 -0
  120. package/dist/components/toolbar/toolbar-trigger.js +14 -1
  121. package/dist/editor/anchors.js +17 -13
  122. package/dist/editor/block-id.js +52 -51
  123. package/dist/editor/block-ops.js +108 -80
  124. package/dist/editor/doc-blocks.js +155 -166
  125. package/dist/editor/extensions.js +139 -180
  126. package/dist/editor/index.js +4 -0
  127. package/dist/editor/markdown-test-helpers.js +4 -2
  128. package/dist/editor/markdown.js +420 -408
  129. package/dist/editor/serializer.js +204 -200
  130. package/dist/editor/slash-items.js +89 -101
  131. package/dist/editor/youtube-url.js +25 -20
  132. package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
  133. package/dist/effects/animated-beam/types.js +1 -0
  134. package/dist/effects/border-beam/css.js +92 -78
  135. package/dist/effects/border-beam/presets.js +1705 -1720
  136. package/dist/effects/border-beam/register-properties.js +43 -33
  137. package/dist/effects/border-beam/types.js +1 -0
  138. package/dist/effects/image-comparison/compare.js +3 -2
  139. package/dist/effects/image-reveal/types.js +1 -0
  140. package/dist/effects/magnetic/magnetic.js +58 -52
  141. package/dist/effects/reveal/observe.js +50 -40
  142. package/dist/effects/reveal/types.js +1 -0
  143. package/dist/effects/shader-gradient/color.js +36 -29
  144. package/dist/effects/shader-gradient/renderer.js +384 -295
  145. package/dist/effects/shader-gradient/shaders.js +31 -0
  146. package/dist/effects/shader-gradient/types.js +1 -0
  147. package/dist/icons/apple-symbol-exports.js +99 -124
  148. package/dist/icons/sf-symbols.js +357 -461
  149. package/dist/icons/trades.js +43 -28
  150. package/dist/inbox/index.js +1 -0
  151. package/dist/inbox/logic.js +126 -122
  152. package/dist/inbox/types.js +43 -43
  153. package/dist/lib/arrive.js +19 -12
  154. package/dist/lib/icon-types.js +1 -0
  155. package/dist/lib/initials.js +16 -10
  156. package/dist/lib/layout.js +5 -0
  157. package/dist/lib/panel.js +13 -0
  158. package/dist/lib/result.js +22 -22
  159. package/dist/lib/sliding-pill.js +59 -44
  160. package/dist/lib/theme-transition.js +43 -43
  161. package/dist/lib/tone.js +31 -24
  162. package/dist/lib/utils.js +3 -1
  163. package/dist/onboarding/define-tours.js +24 -23
  164. package/dist/onboarding/onboarding.svelte.js +16 -12
  165. package/dist/onboarding/pick.js +10 -6
  166. package/dist/onboarding/stores.js +40 -41
  167. package/dist/onboarding/wait.js +24 -23
  168. package/dist/panel/side-panel-types.js +1 -0
  169. package/dist/record/demo-data.js +459 -486
  170. package/dist/record/fields.js +129 -131
  171. package/dist/record/format.js +70 -82
  172. package/dist/record/gutter.js +1 -0
  173. package/dist/record/kinds.js +95 -105
  174. package/dist/record/logic.js +238 -219
  175. package/dist/record/types.js +49 -48
  176. package/dist/shadcn/accordion/index.js +3 -1
  177. package/dist/shadcn/alert/index.js +3 -1
  178. package/dist/shadcn/alert-dialog/index.js +3 -1
  179. package/dist/shadcn/attachment/index.js +3 -1
  180. package/dist/shadcn/avatar/index.js +3 -1
  181. package/dist/shadcn/breadcrumb/index.js +3 -1
  182. package/dist/shadcn/bubble/index.js +3 -1
  183. package/dist/shadcn/button/index.js +3 -1
  184. package/dist/shadcn/calendar/index.js +3 -1
  185. package/dist/shadcn/card/index.js +3 -1
  186. package/dist/shadcn/checkbox/index.js +3 -1
  187. package/dist/shadcn/code-block/index.js +3 -1
  188. package/dist/shadcn/collapsible/index.js +3 -1
  189. package/dist/shadcn/command/index.js +4 -1
  190. package/dist/shadcn/context-menu/index.js +3 -1
  191. package/dist/shadcn/dialog/index.js +3 -1
  192. package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
  193. package/dist/shadcn/empty/index.js +3 -1
  194. package/dist/shadcn/field/index.js +3 -1
  195. package/dist/shadcn/hooks/highlighter.js +77 -75
  196. package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
  197. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
  198. package/dist/shadcn/input/index.js +3 -1
  199. package/dist/shadcn/kbd/index.js +3 -1
  200. package/dist/shadcn/label/index.js +3 -1
  201. package/dist/shadcn/marker/index.js +3 -1
  202. package/dist/shadcn/message/index.js +3 -1
  203. package/dist/shadcn/message-scroller/index.js +3 -1
  204. package/dist/shadcn/popover/index.js +3 -1
  205. package/dist/shadcn/progress/index.js +3 -1
  206. package/dist/shadcn/progress/tone.js +8 -7
  207. package/dist/shadcn/radio-group/index.js +3 -1
  208. package/dist/shadcn/scroll-area/index.js +3 -1
  209. package/dist/shadcn/select/index.js +3 -1
  210. package/dist/shadcn/separator/index.js +3 -1
  211. package/dist/shadcn/sheet/index.js +3 -1
  212. package/dist/shadcn/sidebar/context.svelte.js +42 -28
  213. package/dist/shadcn/sidebar/index.js +3 -1
  214. package/dist/shadcn/skeleton/index.js +3 -1
  215. package/dist/shadcn/slider/index.js +3 -1
  216. package/dist/shadcn/switch/index.js +3 -1
  217. package/dist/shadcn/table/index.js +3 -1
  218. package/dist/shadcn/tabs/index.js +3 -1
  219. package/dist/shadcn/textarea/index.js +3 -1
  220. package/dist/shadcn/toggle/index.js +3 -1
  221. package/dist/shadcn/toggle-group/index.js +3 -1
  222. package/dist/shadcn/tooltip/context.js +7 -2
  223. package/dist/shadcn/tooltip/index.js +3 -1
  224. package/dist/shadcn/trace/index.js +3 -1
  225. package/dist/slides/bridges.js +65 -59
  226. package/dist/slides/deck-controller.svelte.js +210 -207
  227. package/dist/slides/export/capture.js +66 -71
  228. package/dist/slides/export/ffmpeg.js +76 -72
  229. package/dist/slides/export/index.js +3 -0
  230. package/dist/slides/export/launch.js +9 -6
  231. package/dist/slides/export/pdf.js +60 -54
  232. package/dist/slides/export/types.js +1 -0
  233. package/dist/slides/labels.js +24 -24
  234. package/dist/slides/layout.js +39 -41
  235. package/dist/slides/model.js +9 -7
  236. package/dist/slides/navigation.js +78 -73
  237. package/dist/slides/types.js +2 -2
  238. package/dist/status/define-statuses.js +18 -14
  239. package/dist/status/status-types.js +1 -0
  240. package/dist/test-setup.js +24 -20
  241. package/dist/thread/logic.js +73 -77
  242. package/dist/thread/types.js +21 -21
  243. package/dist/work/board-order.js +94 -90
  244. package/dist/work/dates.js +16 -9
  245. package/dist/work/detail/detail-types.js +18 -18
  246. package/dist/work/docs/edited.js +32 -29
  247. package/dist/work/docs/index.js +4 -0
  248. package/dist/work/docs/labels.js +34 -34
  249. package/dist/work/docs/menu.js +113 -106
  250. package/dist/work/docs/outline.js +39 -29
  251. package/dist/work/gantt/logic.js +211 -235
  252. package/dist/work/inbox/inbox-logic.js +71 -75
  253. package/dist/work/inbox/inbox-statuses.js +75 -72
  254. package/dist/work/inbox/inbox-types.js +1 -0
  255. package/dist/work/kanban-drag.js +27 -22
  256. package/dist/work/kanban-types.js +1 -0
  257. package/dist/work/presets/sales.js +36 -32
  258. package/dist/work/reorder.js +9 -8
  259. package/dist/work/task-types.js +1 -0
  260. package/dist/work/time/duration.js +51 -46
  261. package/dist/work/time/time-types.js +23 -23
  262. package/dist/work/use-kanban-dnd.svelte.js +126 -125
  263. package/package.json +3 -1
  264. package/src/components/list-page/master-detail.svelte +3 -1
  265. package/src/components/list-page/rail-row.svelte +4 -4
  266. package/src/editor/markdown.ts +30 -35
@@ -1,100 +1,116 @@
1
+ /** Default pull, in flow units. Callers divide by zoom to hold it on screen. */
1
2
  export const SNAP_THRESHOLD = 6;
2
3
  const SIZE_KEY = {
3
- x: "width",
4
- y: "height"
4
+ x: "width",
5
+ y: "height"
5
6
  };
6
7
  const CROSS = {
7
- x: "y",
8
- y: "x"
8
+ x: "y",
9
+ y: "x"
9
10
  };
11
+ /** The three lines a rect offers on one axis: its two edges and its centre. */
10
12
  function linesOf(rect, axis) {
11
- const start = rect[axis];
12
- const size = rect[SIZE_KEY[axis]];
13
- return [
14
- start,
15
- start + size / 2,
16
- start + size
17
- ];
13
+ const start = rect[axis];
14
+ const size = rect[SIZE_KEY[axis]];
15
+ return [
16
+ start,
17
+ start + size / 2,
18
+ start + size
19
+ ];
18
20
  }
19
21
  function snapAxis(dragged, others, axis, threshold) {
20
- const mine = linesOf(dragged, axis);
21
- const pairings = others.flatMap((other)=>linesOf(other, axis).flatMap((theirs)=>mine.map((own)=>({
22
- delta: theirs - own,
23
- position: theirs
24
- }))));
25
- const inRange = pairings.filter((pair)=>Math.abs(pair.delta) <= threshold);
26
- if (inRange.length === 0) return {
27
- delta: 0,
28
- position: null
29
- };
30
- return inRange.reduce((a, b)=>Math.abs(b.delta) < Math.abs(a.delta) ? b : a);
22
+ const mine = linesOf(dragged, axis);
23
+ // Flat rather than nested: every pairing of one of our lines with one of
24
+ // theirs, then the nearest pairing inside the threshold.
25
+ const pairings = others.flatMap((other) => linesOf(other, axis).flatMap((theirs) => mine.map((own) => ({
26
+ delta: theirs - own,
27
+ position: theirs
28
+ }))));
29
+ const inRange = pairings.filter((pair) => Math.abs(pair.delta) <= threshold);
30
+ if (inRange.length === 0) return {
31
+ delta: 0,
32
+ position: null
33
+ };
34
+ return inRange.reduce((a, b) => Math.abs(b.delta) < Math.abs(a.delta) ? b : a);
31
35
  }
36
+ /** Every other rect that sits on `position`, so the guide spans all of them. */
32
37
  function guideFor(snapped, others, axis, position) {
33
- const cross = CROSS[axis];
34
- const size = SIZE_KEY[cross];
35
- const onLine = others.filter((other)=>linesOf(other, axis).some((line)=>Math.abs(line - position) < 0.5));
36
- return {
37
- axis,
38
- position,
39
- start: Math.min(snapped[cross], ...onLine.map((o)=>o[cross])),
40
- end: Math.max(snapped[cross] + snapped[size], ...onLine.map((o)=>o[cross] + o[size]))
41
- };
38
+ const cross = CROSS[axis];
39
+ const size = SIZE_KEY[cross];
40
+ const onLine = others.filter((other) => linesOf(other, axis).some((line) => Math.abs(line - position) < .5));
41
+ return {
42
+ axis,
43
+ position,
44
+ start: Math.min(snapped[cross], ...onLine.map((o) => o[cross])),
45
+ end: Math.max(snapped[cross] + snapped[size], ...onLine.map((o) => o[cross] + o[size]))
46
+ };
42
47
  }
48
+ /** Pull `dragged` onto the nearest line of `others`, one axis at a time. */
43
49
  export function computeSnap(dragged, others, threshold = SNAP_THRESHOLD) {
44
- if (others.length === 0) return {
45
- x: dragged.x,
46
- y: dragged.y,
47
- guides: []
48
- };
49
- const horizontal = snapAxis(dragged, others, "x", threshold);
50
- const vertical = snapAxis(dragged, others, "y", threshold);
51
- const snapped = {
52
- ...dragged,
53
- x: dragged.x + horizontal.delta,
54
- y: dragged.y + vertical.delta
55
- };
56
- const guides = [];
57
- if (horizontal.position !== null) {
58
- guides.push(guideFor(snapped, others, "x", horizontal.position));
59
- }
60
- if (vertical.position !== null) {
61
- guides.push(guideFor(snapped, others, "y", vertical.position));
62
- }
63
- return {
64
- x: snapped.x,
65
- y: snapped.y,
66
- guides
67
- };
50
+ if (others.length === 0) return {
51
+ x: dragged.x,
52
+ y: dragged.y,
53
+ guides: []
54
+ };
55
+ const horizontal = snapAxis(dragged, others, "x", threshold);
56
+ const vertical = snapAxis(dragged, others, "y", threshold);
57
+ const snapped = {
58
+ ...dragged,
59
+ x: dragged.x + horizontal.delta,
60
+ y: dragged.y + vertical.delta
61
+ };
62
+ const guides = [];
63
+ if (horizontal.position !== null) {
64
+ guides.push(guideFor(snapped, others, "x", horizontal.position));
65
+ }
66
+ if (vertical.position !== null) {
67
+ guides.push(guideFor(snapped, others, "y", vertical.position));
68
+ }
69
+ return {
70
+ x: snapped.x,
71
+ y: snapped.y,
72
+ guides
73
+ };
68
74
  }
75
+ /**
76
+ * The editor's seam. Pull a held free node onto the others and report the
77
+ * lines. A node in a container is not a candidate.
78
+ */
79
+ // The correction goes back into the returned nodes, which is what makes it
80
+ // magnetism and not a hint. xyflow recomputes from the drag origin, so it
81
+ // never stacks.
69
82
  export function snapDraggedNode(nodes, held, zoom) {
70
- const rect = freeNodeRect(held);
71
- if (!held || !rect) return {
72
- nodes,
73
- guides: []
74
- };
75
- const others = nodes.filter((n)=>n.id !== held.id).map(freeNodeRect).filter((r)=>r !== null);
76
- const snap = computeSnap(rect, others, SNAP_THRESHOLD / (zoom || 1));
77
- const moved = snap.x !== held.position.x || snap.y !== held.position.y;
78
- const next = moved ? nodes.map((n)=>n.id === held.id ? {
79
- ...n,
80
- position: {
81
- x: snap.x,
82
- y: snap.y
83
- }
84
- } : n) : nodes;
85
- return {
86
- nodes: next,
87
- guides: snap.guides
88
- };
83
+ const rect = freeNodeRect(held);
84
+ if (!held || !rect) return {
85
+ nodes,
86
+ guides: []
87
+ };
88
+ const others = nodes.filter((n) => n.id !== held.id).map(freeNodeRect).filter((r) => r !== null);
89
+ // The threshold is in flow units, so the pull is the same few pixels on
90
+ // screen whether the board is zoomed to a wall or to one card.
91
+ const snap = computeSnap(rect, others, SNAP_THRESHOLD / (zoom || 1));
92
+ const moved = snap.x !== held.position.x || snap.y !== held.position.y;
93
+ const next = moved ? nodes.map((n) => n.id === held.id ? {
94
+ ...n,
95
+ position: {
96
+ x: snap.x,
97
+ y: snap.y
98
+ }
99
+ } : n) : nodes;
100
+ return {
101
+ nodes: next,
102
+ guides: snap.guides
103
+ };
89
104
  }
105
+ /** A candidate rect, or null for a node a container positions or hides. */
90
106
  function freeNodeRect(node) {
91
- if (!node || node.parentId || node.hidden) return null;
92
- const { width, height } = node.measured ?? {};
93
- if (!width || !height) return null;
94
- return {
95
- x: node.position.x,
96
- y: node.position.y,
97
- width,
98
- height
99
- };
107
+ if (!node || node.parentId || node.hidden) return null;
108
+ const { width, height } = node.measured ?? {};
109
+ if (!width || !height) return null;
110
+ return {
111
+ x: node.position.x,
112
+ y: node.position.y,
113
+ width,
114
+ height
115
+ };
100
116
  }
@@ -1,137 +1,153 @@
1
+ // A small drawing on a board. Markup arrives over the API, so it's rebuilt tag by
2
+ // tag from an allowlist. Authoring: see the package README.
3
+ /** Shapes, text and the local defs they reference. No `a`, `use` or `image`:
4
+ * each one can point somewhere off the page. */
1
5
  const ALLOWED_TAGS = new Set([
2
- "svg",
3
- "g",
4
- "defs",
5
- "title",
6
- "desc",
7
- "linearGradient",
8
- "radialGradient",
9
- "stop",
10
- "marker",
11
- "clipPath",
12
- "path",
13
- "rect",
14
- "circle",
15
- "ellipse",
16
- "line",
17
- "polyline",
18
- "polygon",
19
- "text",
20
- "tspan"
6
+ "svg",
7
+ "g",
8
+ "defs",
9
+ "title",
10
+ "desc",
11
+ "linearGradient",
12
+ "radialGradient",
13
+ "stop",
14
+ "marker",
15
+ "clipPath",
16
+ "path",
17
+ "rect",
18
+ "circle",
19
+ "ellipse",
20
+ "line",
21
+ "polyline",
22
+ "polygon",
23
+ "text",
24
+ "tspan"
21
25
  ]);
26
+ /** Geometry and paint only. An allowlist and not a denylist, so `onload` and
27
+ * `href` are out by being absent rather than by being remembered. */
22
28
  const ALLOWED_ATTRS = new Set([
23
- "viewBox",
24
- "width",
25
- "height",
26
- "x",
27
- "y",
28
- "x1",
29
- "x2",
30
- "y1",
31
- "y2",
32
- "cx",
33
- "cy",
34
- "r",
35
- "rx",
36
- "ry",
37
- "d",
38
- "points",
39
- "dx",
40
- "dy",
41
- "transform",
42
- "fill",
43
- "fill-opacity",
44
- "fill-rule",
45
- "stroke",
46
- "stroke-width",
47
- "stroke-dasharray",
48
- "stroke-dashoffset",
49
- "stroke-linecap",
50
- "stroke-linejoin",
51
- "stroke-opacity",
52
- "opacity",
53
- "font-size",
54
- "font-weight",
55
- "font-style",
56
- "letter-spacing",
57
- "text-anchor",
58
- "dominant-baseline",
59
- "offset",
60
- "stop-color",
61
- "stop-opacity",
62
- "gradientUnits",
63
- "gradientTransform",
64
- "clip-path",
65
- "clipPathUnits",
66
- "marker-end",
67
- "marker-start",
68
- "markerWidth",
69
- "markerHeight",
70
- "refX",
71
- "refY",
72
- "orient",
73
- "id",
74
- "class",
75
- "preserveAspectRatio"
29
+ "viewBox",
30
+ "width",
31
+ "height",
32
+ "x",
33
+ "y",
34
+ "x1",
35
+ "x2",
36
+ "y1",
37
+ "y2",
38
+ "cx",
39
+ "cy",
40
+ "r",
41
+ "rx",
42
+ "ry",
43
+ "d",
44
+ "points",
45
+ "dx",
46
+ "dy",
47
+ "transform",
48
+ "fill",
49
+ "fill-opacity",
50
+ "fill-rule",
51
+ "stroke",
52
+ "stroke-width",
53
+ "stroke-dasharray",
54
+ "stroke-dashoffset",
55
+ "stroke-linecap",
56
+ "stroke-linejoin",
57
+ "stroke-opacity",
58
+ "opacity",
59
+ "font-size",
60
+ "font-weight",
61
+ "font-style",
62
+ "letter-spacing",
63
+ "text-anchor",
64
+ "dominant-baseline",
65
+ "offset",
66
+ "stop-color",
67
+ "stop-opacity",
68
+ "gradientUnits",
69
+ "gradientTransform",
70
+ "clip-path",
71
+ "clipPathUnits",
72
+ "marker-end",
73
+ "marker-start",
74
+ "markerWidth",
75
+ "markerHeight",
76
+ "refX",
77
+ "refY",
78
+ "orient",
79
+ "id",
80
+ "class",
81
+ "preserveAspectRatio"
76
82
  ]);
77
- export const MAX_FIGURE_CHARS = 16000;
83
+ /** A figure is a diagram, not a pasted illustration: past this it is an asset
84
+ * with a URL, and the board holds an image node instead. */
85
+ export const MAX_FIGURE_CHARS = 16e3;
78
86
  const TAG = /<\/?([a-zA-Z][\w:-]*)((?:"[^"]*"|'[^']*'|[^>])*?)\/?>/g;
79
87
  const ATTR = /([a-zA-Z][\w:-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
88
+ /** A reference out of the document. `url(#local)` stays; everything else goes. */
80
89
  const FOREIGN_URL = /url\(\s*['"]?(?!#)/i;
81
90
  function cleanAttrs(raw) {
82
- const kept = [];
83
- for (const match of raw.matchAll(ATTR)){
84
- const name = match[1];
85
- const value = match[3] ?? match[4] ?? "";
86
- if (!ALLOWED_ATTRS.has(name)) continue;
87
- if (FOREIGN_URL.test(value)) continue;
88
- kept.push(`${name}="${value.replace(/"/g, "&quot;")}"`);
89
- }
90
- return kept.length > 0 ? ` ${kept.join(" ")}` : "";
91
+ const kept = [];
92
+ for (const match of raw.matchAll(ATTR)) {
93
+ const name = match[1];
94
+ const value = match[3] ?? match[4] ?? "";
95
+ if (!ALLOWED_ATTRS.has(name)) continue;
96
+ if (FOREIGN_URL.test(value)) continue;
97
+ kept.push(`${name}="${value.replace(/"/g, "&quot;")}"`);
98
+ }
99
+ return kept.length > 0 ? ` ${kept.join(" ")}` : "";
91
100
  }
101
+ /** The figure as it's safe to draw, or null without an `<svg>`. Script, style and foreignObject go with contents; unknown tags drop, keeping text. */
92
102
  export function sanitizeFigureSvg(raw) {
93
- if (typeof raw !== "string" || raw.length > MAX_FIGURE_CHARS) return null;
94
- const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<\?[\s\S]*?\?>/g, "").replace(/<!DOCTYPE[^>]*>/gi, "").replace(/<(script|style|foreignObject|iframe)\b[\s\S]*?<\/\1\s*>/gi, "");
95
- if (!/<svg\b/i.test(stripped)) return null;
96
- let out = "";
97
- let cursor = 0;
98
- for (const match of stripped.matchAll(TAG)){
99
- const [tag, name, attrs] = match;
100
- const start = match.index ?? 0;
101
- out += stripped.slice(cursor, start);
102
- cursor = start + tag.length;
103
- if (!ALLOWED_TAGS.has(name)) continue;
104
- if (tag.startsWith("</")) {
105
- out += `</${name}>`;
106
- continue;
107
- }
108
- out += `<${name}${cleanAttrs(attrs)}${tag.endsWith("/>") ? " />" : ">"}`;
109
- }
110
- out += stripped.slice(cursor);
111
- return out.trim();
103
+ if (typeof raw !== "string" || raw.length > MAX_FIGURE_CHARS) return null;
104
+ const stripped = raw.replace(/<!--[\s\S]*?-->/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<\?[\s\S]*?\?>/g, "").replace(/<!DOCTYPE[^>]*>/gi, "").replace(/<(script|style|foreignObject|iframe)\b[\s\S]*?<\/\1\s*>/gi, "");
105
+ if (!/<svg\b/i.test(stripped)) return null;
106
+ let out = "";
107
+ let cursor = 0;
108
+ for (const match of stripped.matchAll(TAG)) {
109
+ const [tag, name, attrs] = match;
110
+ const start = match.index ?? 0;
111
+ out += stripped.slice(cursor, start);
112
+ cursor = start + tag.length;
113
+ if (!ALLOWED_TAGS.has(name)) continue;
114
+ if (tag.startsWith("</")) {
115
+ out += `</${name}>`;
116
+ continue;
117
+ }
118
+ out += `<${name}${cleanAttrs(attrs)}${tag.endsWith("/>") ? " />" : ">"}`;
119
+ }
120
+ out += stripped.slice(cursor);
121
+ return out.trim();
112
122
  }
113
123
  function rootAttr(svg, name) {
114
- const root = svg.match(/<svg\b([^>]*)>/i);
115
- if (!root) return undefined;
116
- const found = root[1].match(new RegExp(`${name}\\s*=\\s*"([^"]*)"`, "i"));
117
- return found?.[1];
124
+ const root = svg.match(/<svg\b([^>]*)>/i);
125
+ if (!root) return undefined;
126
+ const found = root[1].match(new RegExp(`${name}\\s*=\\s*"([^"]*)"`, "i"));
127
+ return found?.[1];
118
128
  }
129
+ /** Height over width, from the viewBox the author drew in. */
119
130
  export function figureRatio(svg) {
120
- const box = rootAttr(svg, "viewBox");
121
- if (box) {
122
- const [, , w, h] = box.trim().split(/[\s,]+/).map(Number);
123
- if (w > 0 && h > 0) return h / w;
124
- }
125
- const w = Number.parseFloat(rootAttr(svg, "width") ?? "");
126
- const h = Number.parseFloat(rootAttr(svg, "height") ?? "");
127
- return w > 0 && h > 0 ? h / w : null;
131
+ const box = rootAttr(svg, "viewBox");
132
+ if (box) {
133
+ const [, , w, h] = box.trim().split(/[\s,]+/).map(Number);
134
+ if (w > 0 && h > 0) return h / w;
135
+ }
136
+ const w = Number.parseFloat(rootAttr(svg, "width") ?? "");
137
+ const h = Number.parseFloat(rootAttr(svg, "height") ?? "");
138
+ return w > 0 && h > 0 ? h / w : null;
128
139
  }
129
- export function figureBoxHeight(svg, width, fallback = 0.4) {
130
- return Math.round(width * (figureRatio(svg) ?? fallback));
140
+ /** The box the drawing needs. The fallback is a wide banner, the shape a
141
+ * board figure takes when the author gave no viewBox. */
142
+ export function figureBoxHeight(svg, width, fallback = .4) {
143
+ return Math.round(width * (figureRatio(svg) ?? fallback));
131
144
  }
145
+ /** The heading over a figure and the line under it, with their margins. These
146
+ * mirror sketch-figure.svelte and move when its type sizes do. */
132
147
  const LABEL_H = 27;
133
148
  const CAPTION_H = 24;
149
+ /** The whole node: the drawing, plus whatever text sits around it. */
134
150
  export function figureNodeHeight(figure, width) {
135
- const art = figure.height ?? figureBoxHeight(figure.svg ?? "", width);
136
- return art + (figure.label ? LABEL_H : 0) + (figure.caption ? CAPTION_H : 0);
151
+ const art = figure.height ?? figureBoxHeight(figure.svg ?? "", width);
152
+ return art + (figure.label ? LABEL_H : 0) + (figure.caption ? CAPTION_H : 0);
137
153
  }