@1agh/maude 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +8 -8
  2. package/plugins/design/dependencies.json +30 -2
  3. package/plugins/design/dev-server/annotations-context-toolbar.tsx +481 -78
  4. package/plugins/design/dev-server/annotations-layer.tsx +817 -170
  5. package/plugins/design/dev-server/api.ts +15 -1
  6. package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
  7. package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
  8. package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
  9. package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
  10. package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
  11. package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
  12. package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
  13. package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
  14. package/plugins/design/dev-server/bin/smoke.sh +114 -12
  15. package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
  16. package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
  17. package/plugins/design/dev-server/canvas-cursors.ts +130 -82
  18. package/plugins/design/dev-server/canvas-icons.tsx +169 -0
  19. package/plugins/design/dev-server/canvas-shell.tsx +113 -89
  20. package/plugins/design/dev-server/client/app.jsx +1084 -417
  21. package/plugins/design/dev-server/dist/client.bundle.js +242 -20
  22. package/plugins/design/dev-server/dist/comment-mount.js +40 -62
  23. package/plugins/design/dev-server/export-dialog.tsx +189 -1
  24. package/plugins/design/dev-server/exporters/html.ts +4 -1
  25. package/plugins/design/dev-server/exporters/index.ts +4 -1
  26. package/plugins/design/dev-server/exporters/pdf.ts +7 -1
  27. package/plugins/design/dev-server/exporters/png.ts +21 -2
  28. package/plugins/design/dev-server/exporters/pptx.ts +330 -201
  29. package/plugins/design/dev-server/exporters/scope.ts +107 -11
  30. package/plugins/design/dev-server/exporters/svg.ts +4 -1
  31. package/plugins/design/dev-server/http.ts +40 -9
  32. package/plugins/design/dev-server/input-router.tsx +9 -2
  33. package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
  34. package/plugins/design/dev-server/test/annotations-layer.test.ts +83 -3
  35. package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +243 -0
  36. package/plugins/design/dev-server/test/canvas-cursors.test.ts +95 -6
  37. package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
  38. package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
  39. package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
  40. package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
  41. package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
  42. package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
  43. package/plugins/design/dev-server/test/input-router.test.ts +11 -4
  44. package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +32 -0
  45. package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
  46. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +9 -11
  47. package/plugins/design/dev-server/tool-palette.tsx +140 -11
  48. package/plugins/design/dev-server/use-annotation-resize.tsx +208 -61
  49. package/plugins/design/dev-server/use-tool-mode.tsx +55 -9
  50. package/plugins/design/templates/_shell.html +36 -9
@@ -1,18 +1,30 @@
1
1
  /**
2
- * @file canvas-cursors.ts — Phase 21 custom tool cursors
2
+ * @file canvas-cursors.ts — Phase 24 custom tool cursors (Kenney CC0)
3
3
  * @scope plugins/design/dev-server/canvas-cursors.ts
4
- * @purpose FigJam/Figma-style custom cursors for the annotation + nav tools.
5
- * The native `crosshair` / `text` / `cell` cursors are tiny, thin,
6
- * and vanish on busy canvases (the "pen is almost invisible"
7
- * complaint). These are 32×32 SVG cursors with a WHITE outline halo
8
- * so the glyph reads on any background, light or dark, and each one
9
- * mirrors its tool-palette icon so the cursor and the tool button
10
- * share one visual identity.
4
+ * @purpose FigJam/Figma-style custom cursors for EVERY canvas tool. The
5
+ * native crosshair / text / arrow cursors are tiny + thin and
6
+ * vanish on busy canvases; these are 32×32 SVG cursors that read on
7
+ * any background.
8
+ *
9
+ * Phase 24 ALL tools draw from ONE library, the **Kenney Cursor
10
+ * Pack (1.1)** "Outline" set, which is **CC0 / public domain**
11
+ * (https://kenney.nl/assets/cursor-pack — no attribution required,
12
+ * redistributable inside this MIT package + marketplace clone). See
13
+ * DDR-067 for the licence gate (Bibata GPL-3.0 was rejected as
14
+ * copyleft). One library = one visual identity across move / hand /
15
+ * comment / pen / shape / sticky / text / eraser. The glyph paths
16
+ * are Kenney's; we RECOLOUR them (see `kenney()`) to a dark-glyph +
17
+ * white-halo treatment so they read crisply on light AND dark
18
+ * canvases. Per-tool glyph map:
19
+ * move→pointer_a · hand→hand_open · comment→message_dots_square ·
20
+ * pen→drawing_pencil · shape→line_cross · eraser→drawing_eraser.
21
+ * text (I-beam) + sticky (folded-corner note) have no clean Kenney
22
+ * equivalent, so they are AUTHORED in the identical dark-glyph +
23
+ * white-halo treatment to keep one visual identity.
11
24
  *
12
25
  * Delivered as CSS `cursor: url("data:image/svg+xml,…") hx hy, fb`
13
- * — no runtime dependency, no asset files (Figma ships cursors the
14
- * same way). 32×32 is the cross-browser-safe ceiling; the hotspot
15
- * (hx, hy) is the pixel the click actually registers at.
26
+ * — no runtime dependency, no asset files. 32×32 is the
27
+ * cross-browser-safe ceiling; (hx, hy) is the click hotspot.
16
28
  */
17
29
 
18
30
  import type { Tool } from './input-router.tsx';
@@ -23,103 +35,139 @@ function svgCursor(svg: string, hx: number, hy: number, fallback: string): strin
23
35
  return `url("data:image/svg+xml,${encodeURIComponent(svg.trim())}") ${hx} ${hy}, ${fallback}`;
24
36
  }
25
37
 
26
- const W = `width='32' height='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'`;
27
- const HALO = "stroke='#ffffff' stroke-linejoin='round' stroke-linecap='round'";
38
+ // Phase 24 24px rendered box (down from 32) so cursors aren't oversized; the
39
+ // glyph coords stay in the 32-unit viewBox and scale into 24px. Hotspots are
40
+ // scaled to match (see the generator).
41
+ const W = `width='24' height='24' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'`;
28
42
  const INK = '#1f1f1f';
43
+ const HALO = "stroke='#ffffff' stroke-linejoin='round' stroke-linecap='round'";
29
44
 
30
- // Precise crosshair with a centre gap rect / ellipse / arrow. Hotspot 16,16.
31
- const CROSSHAIR = svgCursor(
32
- `<svg ${W}>
33
- <path d='M16 4V13M16 19V28M4 16H13M19 16H28' fill='none' ${HALO} stroke-width='4'/>
34
- <path d='M16 4V13M16 19V28M4 16H13M19 16H28' fill='none' stroke='${INK}' stroke-width='1.75' stroke-linecap='round'/>
35
- <circle cx='16' cy='16' r='1.3' fill='${INK}'/>
36
- </svg>`,
37
- 16,
38
- 16,
39
- 'crosshair'
40
- );
45
+ // Kenney CC0 glyph, recoloured to a FigJam-style **dark glyph + white halo**:
46
+ // Kenney's outline band (`b`) is painted WHITE (the halo) and its inner fill
47
+ // (`w`) is painted dark INK. Reads crisply on a LIGHT canvas (solid dark glyph)
48
+ // AND a dark one (white halo edge).
49
+ function kenney(b: string, w: string): string {
50
+ return `<svg ${W}><path fill='#ffffff' d='${b}'/><path fill='${INK}' d='${w}'/></svg>`;
51
+ }
41
52
 
42
- // Marker/pen nib points at the draw point (bottom-left). Hotspot 6,26.
43
- const PEN = svgCursor(
44
- `<svg ${W}>
45
- <path d='M22 4l6 6-15 15-7 1 1-7z' fill='${INK}' ${HALO} stroke-width='2.25'/>
46
- <path d='M18.5 7.5l6 6' ${HALO} stroke-width='2'/>
47
- <path d='M7 25l-1.5 1.5' ${HALO} stroke-width='2.5'/>
48
- </svg>`,
53
+ const MOVE = svgCursor(
54
+ kenney(
55
+ 'M16.75 23.9 L20 22.55 20.5 22.15 20.35 21.55 18.4 17.75 21.25 17.25 21.75 17 22 16.45 Q22 16.1 21.6 15.9 L11.45 8.2 11.4 8.2 10.85 8 10.2 8.25 10 8.8 10 20.75 Q10 21.55 10.85 21.55 L11.4 21.35 13.6 19.8 15.6 23.5 16.05 23.95 16.75 23.9 M8 8.8 Q7.95 8.05 8.55 7.15 L8.95 6.75 Q9.8 6 10.85 6 11.55 6 12.2 6.35 L12.65 6.6 22.65 14.15 Q24.15 15 24 16.45 24.05 17.55 23.2 18.45 L23.05 18.55 Q22.1 19.3 21.45 19.3 L22.1 20.55 Q22.5 21.2 22.5 22.15 L22.05 23.4 20.9 24.35 20.8 24.4 17.55 25.75 15.25 25.8 Q14.15 25.4 13.75 24.25 L12.95 22.75 12.65 22.9 12.55 23 Q11.75 23.55 10.85 23.55 7.95 23.55 8 20.75 L8 8.8',
56
+ 'M16.75 23.9 L16.05 23.95 15.6 23.5 13.6 19.8 11.4 21.35 10.85 21.55 Q10 21.55 10 20.75 L10 8.8 10.2 8.25 10.85 8 11.4 8.2 11.45 8.2 21.6 15.9 Q22 16.1 22 16.45 L21.75 17 21.25 17.25 18.4 17.75 20.35 21.55 20.5 22.15 20 22.55 16.75 23.9'
57
+ ),
49
58
  6,
50
- 26,
51
- 'crosshair'
59
+ 5,
60
+ 'default'
52
61
  );
53
-
54
- // Bigger I-beam for text. Hotspot 16,16.
55
- const TEXT = svgCursor(
56
- `<svg ${W}>
57
- <path d='M16 5V27M11 5H21M11 27H21' fill='none' ${HALO} stroke-width='4.5'/>
58
- <path d='M16 5V27M11 5H21M11 27H21' fill='none' stroke='${INK}' stroke-width='2' stroke-linecap='round'/>
59
- </svg>`,
60
- 16,
61
- 16,
62
- 'text'
62
+ const HAND = svgCursor(
63
+ kenney(
64
+ 'M28.55 17.8 Q29.4 20.3 28.75 22.8 L28.7 23 28.15 24.55 28.05 24.75 Q26.4 28.2 22.75 29.4 L22.45 29.5 18 29.9 17.75 29.85 Q15.8 29.25 13.8 28.1 L13.45 27.95 Q12 27.05 10.35 26.8 L10.25 26.75 7.9 26.7 7.8 26.7 Q6.1 26.9 4.8 25.8 L4.75 25.8 4.65 25.7 4.6 25.65 Q3.25 24.5 3.05 22.85 L3 22.75 Q2.8 20.95 4 19.5 5.05 18.15 6.75 17.95 L8.4 17.9 7.5 15.55 7.3 15.2 7.2 15 5 10.95 Q3.9 9.15 4.3 7.75 L4.4 7.5 Q4.75 6.15 6.45 5.2 9 3.65 11 5 11.05 4.1 11.5 3.45 12.15 2.1 14.2 1.6 L14.35 1.6 Q18.05 0.6 19.6 4.4 L19.65 4.55 Q20.75 3.65 22.7 3.75 L22.75 3.75 Q26.75 4 27 8.15 L27 8.25 27 8.35 27 8.45 Q26.9 10.75 27.1 12.6 27.15 14.65 28.2 16.75 L28.55 17.8 M26.65 18.4 L26.1 16.8 26.1 16.85 Q24.8 13.7 25 8.35 L25 8.25 Q24.85 5.9 22.6 5.75 20.7 5.65 20.4 7.35 L20.1 7.95 19.5 8.2 18.85 8.05 18.45 7.5 17.75 5.15 Q16.85 2.95 14.7 3.55 L14.65 3.55 Q12.55 4.05 13.15 6.15 L13.85 8.75 13.75 9.45 13.25 9.95 12.55 9.95 12 9.55 10.9 7.7 Q9.5 5.7 7.45 6.9 6.5 7.45 6.25 8.15 6.1 8.9 6.7 9.85 9.2 13.9 10.8 18.65 L10.8 19.3 10.4 19.8 9.75 19.95 Q8.35 19.8 6.95 19.95 6.1 20.05 5.55 20.75 4.9 21.55 5 22.55 5.15 23.55 6 24.2 L6.05 24.25 Q6.75 24.85 7.65 24.7 L7.7 24.7 10.65 24.8 Q12.7 25.15 14.45 26.2 L14.5 26.2 Q16.4 27.35 18.3 27.9 L22.05 27.5 22.1 27.5 Q24.95 26.55 26.25 23.85 L26.8 22.3 Q27.3 20.35 26.65 18.4',
65
+ 'M26.65 18.4 Q27.3 20.35 26.8 22.3 L26.25 23.85 Q24.95 26.55 22.1 27.5 L22.05 27.5 18.3 27.9 Q16.4 27.35 14.5 26.2 L14.45 26.2 Q12.7 25.15 10.65 24.8 L7.7 24.7 7.65 24.7 Q6.75 24.85 6.05 24.25 L6 24.2 Q5.15 23.55 5 22.55 4.9 21.55 5.55 20.75 6.1 20.05 6.95 19.95 8.35 19.8 9.75 19.95 L10.4 19.8 10.8 19.3 10.8 18.65 Q9.2 13.9 6.7 9.85 6.1 8.9 6.25 8.15 6.5 7.45 7.45 6.9 9.5 5.7 10.9 7.7 L12 9.55 12.55 9.95 13.25 9.95 13.75 9.45 13.85 8.75 13.15 6.15 Q12.55 4.05 14.65 3.55 L14.7 3.55 Q16.85 2.95 17.75 5.15 L18.45 7.5 18.85 8.05 19.5 8.2 20.1 7.95 20.4 7.35 Q20.7 5.65 22.6 5.75 24.85 5.9 25 8.25 L25 8.35 Q24.8 13.7 26.1 16.85 L26.1 16.8 26.65 18.4'
66
+ ),
67
+ 12,
68
+ 12,
69
+ 'grab'
63
70
  );
64
-
65
- // Speech bubble — drops a comment at the tail tip (bottom-left). Hotspot 8,27.
66
71
  const COMMENT = svgCursor(
67
- `<svg ${W}>
68
- <path d='M8 5H26a4 4 0 0 1 4 4V18a4 4 0 0 1-4 4H14l-6 6v-6H8a4 4 0 0 1-4-4V9a4 4 0 0 1 4-4Z' fill='${INK}' ${HALO} stroke-width='2.25'/>
69
- <circle cx='12' cy='13.5' r='1.4' fill='#fff'/>
70
- <circle cx='17' cy='13.5' r='1.4' fill='#fff'/>
71
- <circle cx='22' cy='13.5' r='1.4' fill='#fff'/>
72
- </svg>`,
73
- 8,
74
- 27,
72
+ kenney(
73
+ 'M8 4 Q4 4 4 8 L4 20 Q4 24 8 24 L12 24 15.3 27.3 16 27.6 16.7 27.3 20 24 24 24 Q28 24 28 20 L28 8 Q28 4 24 4 L8 4 M24 2 Q30 2 30 8 L30 20 Q30 26 24 26 L20.85 26 18.15 28.75 Q17.2 29.6 16 29.6 14.8 29.6 13.9 28.75 L11.15 26 8 26 Q2 26 2 20 L2 8 Q2 2 8 2 L24 2 M24 14 Q24 14.85 23.4 15.4 22.85 16 22 16 21.15 16 20.55 15.4 20 14.85 20 14 20 13.15 20.55 12.55 21.15 12 22 12 22.85 12 23.4 12.55 24 13.15 24 14 M12 14 Q12 14.85 11.4 15.4 10.85 16 10 16 9.15 16 8.55 15.4 8 14.85 8 14 8 13.15 8.55 12.55 9.15 12 10 12 10.85 12 11.4 12.55 12 13.15 12 14 M18 14 Q18 14.85 17.4 15.4 16.85 16 16 16 15.15 16 14.55 15.4 14 14.85 14 14 14 13.15 14.55 12.55 15.15 12 16 12 16.85 12 17.4 12.55 18 13.15 18 14',
74
+ 'M18 14 Q18 13.15 17.4 12.55 16.85 12 16 12 15.15 12 14.55 12.55 14 13.15 14 14 14 14.85 14.55 15.4 15.15 16 16 16 16.85 16 17.4 15.4 18 14.85 18 14 M8 4 L24 4 Q28 4 28 8 L28 20 Q28 24 24 24 L20 24 16.7 27.3 16 27.6 15.3 27.3 12 24 8 24 Q4 24 4 20 L4 8 Q4 4 8 4 M12 14 Q12 13.15 11.4 12.55 10.85 12 10 12 9.15 12 8.55 12.55 8 13.15 8 14 8 14.85 8.55 15.4 9.15 16 10 16 10.85 16 11.4 15.4 12 14.85 12 14 M24 14 Q24 13.15 23.4 12.55 22.85 12 22 12 21.15 12 20.55 12.55 20 13.15 20 14 20 14.85 20.55 15.4 21.15 16 22 16 22.85 16 23.4 15.4 24 14.85 24 14'
75
+ ),
76
+ 12,
77
+ 22,
75
78
  'crosshair'
76
79
  );
77
-
78
- // Open hand for pan/grab. Hotspot 16,17.
79
- const HAND = svgCursor(
80
- `<svg ${W}>
81
- <path d='M10 19v-8a1.8 1.8 0 0 1 3.6 0v-1.2a1.8 1.8 0 0 1 3.6 0v.6a1.8 1.8 0 0 1 3.6 0V12a1.8 1.8 0 0 1 3.6 0v6.5a7.5 7.5 0 0 1-7.5 7.5h-1a7.5 7.5 0 0 1-6.4-3.6l-2.4-3.7a1.9 1.9 0 0 1 3-2.3z' fill='${INK}' ${HALO} stroke-width='2'/>
82
- </svg>`,
83
- 16,
84
- 17,
85
- 'grab'
80
+ const PEN = svgCursor(
81
+ kenney(
82
+ 'M5.15 10.85 Q4 9.65 4 8.1 L4 7.95 4 7.85 Q3.95 6.2 5.1 5.15 6.25 4 7.85 4 L7.95 4 8.1 4 Q9.65 4 10.85 5.15 L11 5.3 14.3 5.25 Q15.15 5.25 15.75 5.85 L29.9 20 Q31 21.15 31 22.75 L31 22.85 31 23 Q31.05 24.5 29.9 25.7 L29.55 25.95 25.65 29.9 25.65 29.95 Q24.45 31.1 22.75 31.1 21.2 31.15 20 29.95 L5.85 15.8 Q5.25 15.2 5.25 14.35 L5.25 10.95 5.15 10.85 M19.95 24.2 L18.55 22.8 22.8 18.55 24.2 19.95 19.95 24.2 M28.45 21.4 L14.3 7.25 10.15 7.3 9.4 6.55 Q8.8 5.95 7.95 6 7.1 5.95 6.5 6.55 5.95 7.1 6 7.95 5.95 8.8 6.55 9.4 L7.25 10.1 7.25 14.35 21.4 28.5 Q22 29.1 22.75 29.1 23.6 29.1 24.2 28.5 L28.45 24.25 Q29.05 23.65 29 22.85 29.05 22 28.45 21.4 M9.25 13.5 L9.25 9.3 13.5 9.25 14.3 10.05 10.05 14.3 9.25 13.5 M17.15 21.4 L11.5 15.75 15.75 11.5 21.4 17.15 17.15 21.4',
83
+ 'M17.15 21.4 L21.4 17.15 15.75 11.5 11.5 15.75 17.15 21.4 M28.45 21.4 Q29.05 22 29 22.85 29.05 23.65 28.45 24.25 L24.2 28.5 Q23.6 29.1 22.75 29.1 22 29.1 21.4 28.5 L7.25 14.35 7.25 10.1 6.55 9.4 Q5.95 8.8 6 7.95 5.95 7.1 6.5 6.55 7.1 5.95 7.95 6 8.8 5.95 9.4 6.55 L10.15 7.3 14.3 7.25 28.45 21.4 M19.95 24.2 L24.2 19.95 22.8 18.55 18.55 22.8 19.95 24.2 M9.25 13.5 L10.05 14.3 14.3 10.05 13.5 9.25 9.25 9.3 9.25 13.5'
84
+ ),
85
+ 20,
86
+ 20,
87
+ 'crosshair'
88
+ );
89
+ const CROSSHAIR = svgCursor(
90
+ kenney(
91
+ 'M17 2 Q18.2 2 19.15 2.9 20 3.8 20 5 L20 12 27 12 Q28.2 12 29.15 12.9 30 13.8 30 15 L30 17 Q30 18.2 29.15 19.15 28.2 20 27 20 L20 20 20 27 Q20 28.2 19.15 29.15 18.2 30 17 30 L15 30 Q13.8 30 12.9 29.15 12 28.2 12 27 L12 20 5 20 Q3.8 20 2.9 19.15 2 18.2 2 17 L2 15 Q2 13.8 2.9 12.9 3.8 12 5 12 L12 12 12 5 Q12 3.8 12.9 2.9 13.8 2 15 2 L17 2 M14 27 L14.3 27.7 Q14.6 28 15 28 L17 28 17.7 27.7 18 27 18 18.05 18.05 18 27 18 Q27.4 18 27.7 17.7 L28 17 28 15 27.7 14.3 Q27.4 14 27 14 L18 14 18 5 17.7 4.3 17 4 15 4 Q14.6 4 14.3 4.3 14 4.6 14 5 L14 14 5 14 Q4.6 14 4.3 14.3 4 14.6 4 15 L4 17 Q4 17.4 4.3 17.7 4.6 18 5 18 L14 18 14 27',
92
+ 'M14 27 L14 18 5 18 Q4.6 18 4.3 17.7 4 17.4 4 17 L4 15 Q4 14.6 4.3 14.3 4.6 14 5 14 L14 14 14 5 Q14 4.6 14.3 4.3 14.6 4 15 4 L17 4 17.7 4.3 18 5 18 14 27 14 Q27.4 14 27.7 14.3 L28 15 28 17 27.7 17.7 Q27.4 18 27 18 L18.05 18 18 18.05 18 27 17.7 27.7 17 28 15 28 Q14.6 28 14.3 27.7 L14 27'
93
+ ),
94
+ 12,
95
+ 12,
96
+ 'crosshair'
86
97
  );
87
-
88
- // Eraser — working edge at bottom-left. Hotspot 8,24.
89
98
  const ERASER = svgCursor(
90
- `<svg ${W}>
91
- <path d='M19 5l8 8-10 10H9l-4-4z' fill='${INK}' ${HALO} stroke-width='2.25'/>
92
- <path d='M13 11l8 8' ${HALO} stroke-width='2'/>
93
- </svg>`,
94
- 8,
95
- 24,
99
+ kenney(
100
+ 'M3.9 11.35 Q2.3 12.5 3.7 13.9 L16.4 26.65 Q17.85 28.05 19.45 26.9 L27.65 21.15 Q29.3 20 27.85 18.6 L15.15 5.85 Q13.75 4.45 12.1 5.6 L3.9 11.35 M1.05 12.4 Q1 10.95 2.75 9.75 L10.95 4 10.95 3.95 Q14 1.85 16.6 4.45 L29.3 17.2 Q30.8 18.65 30.55 20.1 30.55 21.55 28.8 22.8 L20.6 28.55 20.65 28.55 Q17.7 30.65 15 28.1 L15 28.05 2.3 15.3 2.3 15.35 Q0.8 13.85 1.05 12.4 M13.5 7 L19.85 13.35 11.65 19.15 5.3 12.75 13.5 7',
101
+ 'M13.5 7 L5.3 12.75 11.65 19.15 19.85 13.35 13.5 7 M3.9 11.35 L12.1 5.6 Q13.75 4.45 15.15 5.85 L27.85 18.6 Q29.3 20 27.65 21.15 L19.45 26.9 Q17.85 28.05 16.4 26.65 L3.7 13.9 Q2.3 12.5 3.9 11.35'
102
+ ),
103
+ 6,
104
+ 14,
96
105
  'cell'
97
106
  );
98
107
 
99
- // Sticky — a mini yellow note (matches the card). Drag starts at its top-left.
108
+ // Text — a classic I-beam (vertical stem + top/bottom serifs). Authored in the
109
+ // same dark-INK glyph + white-HALO treatment as the Kenney glyphs (Kenney's
110
+ // pointer_i reads as a small bracket, not an I-beam). Hotspot dead-centre.
111
+ const TEXT = svgCursor(
112
+ `<svg ${W}><path d='M16 4V28M11 4H21M11 28H21' fill='none' ${HALO} stroke-width='5'/><path d='M16 4V28M11 4H21M11 28H21' fill='none' stroke='${INK}' stroke-width='2.25' stroke-linecap='round'/></svg>`,
113
+ 12,
114
+ 12,
115
+ 'text'
116
+ );
117
+
118
+ // Sticky — a note with a peeled/folded top-right corner (the universal sticky
119
+ // glyph). Authored in the same treatment (Kenney has no sticky-note). Hotspot
120
+ // at the note's top-left, where the note is dropped.
100
121
  const STICKY = svgCursor(
101
- `<svg ${W}>
102
- <path d='M7 6h11l8 8v12H7z' fill='#ffe27a' stroke='${INK}' stroke-width='2' stroke-linejoin='round'/>
103
- <path d='M18 6v8h8' fill='none' stroke='${INK}' stroke-width='2' stroke-linejoin='round'/>
104
- </svg>`,
105
- 8,
106
- 7,
122
+ `<svg ${W}><path d='M6 5H21L26 10V27H6Z' fill='${INK}' ${HALO} stroke-width='2.5'/><path d='M21 5V11H26' fill='none' ${HALO} stroke-width='2.25'/></svg>`,
123
+ 5,
124
+ 5,
107
125
  'crosshair'
108
126
  );
109
127
 
110
128
  /**
111
- * Tool → CSS cursor value. `move` keeps the system arrow (the universal
112
- * select/move affordance); everything else gets a crafted cursor.
129
+ * Tool → CSS cursor value. Phase 24 every tool ships a Kenney glyph (no
130
+ * system fallback for `move` anymore; the brief is ONE unified library). The
131
+ * single Shape tool uses the crosshair; `rect`/`ellipse` stay keyed (still in
132
+ * the Tool union) but are no longer directly selectable.
113
133
  */
114
- export const TOOL_CURSORS: Record<Tool, string> = {
115
- move: 'default',
134
+ // Frozen so the docstring's "frozen map" guarantee is real: `resolveToolCursor`
135
+ // is a pure lookup into this object, and freezing forecloses any same-realm
136
+ // shell-side code ever mutating an entry out from under the lookup (the canvas
137
+ // runs cross-origin and can't reach it regardless — this is belt-and-braces).
138
+ export const TOOL_CURSORS: Record<Tool, string> = Object.freeze({
139
+ move: MOVE,
116
140
  hand: HAND,
117
141
  comment: COMMENT,
118
142
  pen: PEN,
143
+ shape: CROSSHAIR,
119
144
  rect: CROSSHAIR,
120
145
  ellipse: CROSSHAIR,
121
146
  sticky: STICKY,
122
147
  arrow: CROSSHAIR,
123
148
  text: TEXT,
124
149
  eraser: ERASER,
125
- };
150
+ });
151
+
152
+ /**
153
+ * Resolve a tool token — received over the **untrusted** canvas→shell
154
+ * `tool-cursor` postMessage bridge — to a TRUSTED cursor string from
155
+ * {@link TOOL_CURSORS}, or `null` when the token is not a known tool.
156
+ *
157
+ * The app shell applies the value this RETURNS, never the raw message. That is
158
+ * the security boundary: a malicious *synced* canvas (DDR-054 — canvas content
159
+ * is untrusted and may be hub-pushed) can call
160
+ * `window.parent.postMessage({ dgn: 'tool-cursor', tool: … }, '*')` directly,
161
+ * bypassing the honest `ToolProvider` sender. By echoing only a token and
162
+ * looking the cursor up here, the worst that canvas can do is pick *which*
163
+ * known, always-visible glyph the shell shows — it can no longer smuggle an
164
+ * invisible / displaced / zero-content SVG cursor that would act as a
165
+ * clickjacking aid over the un-CSP'd shell (phase-24 ethical-hacker Finding 2;
166
+ * see DDR-067). `hasOwnProperty` (not `in`) keeps `constructor`/`__proto__`
167
+ * off the allowlist, and the `[a-z-]` shape gate bars anything but a tool id.
168
+ */
169
+ export function resolveToolCursor(token: unknown): string | null {
170
+ if (typeof token !== 'string' || !/^[a-z-]+$/.test(token)) return null;
171
+ if (!Object.prototype.hasOwnProperty.call(TOOL_CURSORS, token)) return null;
172
+ return TOOL_CURSORS[token as Tool];
173
+ }
@@ -247,11 +247,170 @@ export function IconTrash(props: IconProps) {
247
247
  );
248
248
  }
249
249
 
250
+ // ── Phase 24 shape-tool icons ────────────────────────────────────────────────
251
+ // IconShape — the Shape tool button: a square + circle composite (FigJam-style).
252
+ export function IconShape(props: IconProps) {
253
+ return (
254
+ <Svg {...props}>
255
+ <rect x="4" y="4" width="11" height="11" rx="1.5" />
256
+ <circle cx="15" cy="15" r="5" />
257
+ </Svg>
258
+ );
259
+ }
260
+ export function IconSquare(props: IconProps) {
261
+ return (
262
+ <Svg {...props}>
263
+ <rect x="4" y="4" width="16" height="16" rx="0.5" />
264
+ </Svg>
265
+ );
266
+ }
267
+ export function IconRoundedSquare(props: IconProps) {
268
+ return (
269
+ <Svg {...props}>
270
+ <rect x="4" y="4" width="16" height="16" rx="4" />
271
+ </Svg>
272
+ );
273
+ }
274
+ export function IconCircle(props: IconProps) {
275
+ return (
276
+ <Svg {...props}>
277
+ <circle cx="12" cy="12" r="8" />
278
+ </Svg>
279
+ );
280
+ }
281
+ export function IconDiamond(props: IconProps) {
282
+ return (
283
+ <Svg {...props}>
284
+ <path d="M12 3l9 9-9 9-9-9z" />
285
+ </Svg>
286
+ );
287
+ }
288
+ export function IconTriangle(props: IconProps) {
289
+ return (
290
+ <Svg {...props}>
291
+ <path d="M12 4l8 16H4z" />
292
+ </Svg>
293
+ );
294
+ }
295
+ export function IconTriangleDown(props: IconProps) {
296
+ return (
297
+ <Svg {...props}>
298
+ <path d="M4 4h16l-8 16z" />
299
+ </Svg>
300
+ );
301
+ }
302
+
303
+ // ── Phase 24 arrowhead-picker icons ──────────────────────────────────────────
304
+ // Each shows a short shaft ending in the head style on the right.
305
+ export function IconArrowheadLine(props: IconProps) {
306
+ return (
307
+ <Svg {...props}>
308
+ <path d="M3 12h13" />
309
+ <path d="M12 8l5 4-5 4" />
310
+ </Svg>
311
+ );
312
+ }
313
+ export function IconArrowheadTriangle(props: IconProps) {
314
+ return (
315
+ <Svg {...props}>
316
+ <path d="M3 12h11" />
317
+ <path d="M13 8l6 4-6 4z" fill="currentColor" />
318
+ </Svg>
319
+ );
320
+ }
321
+ export function IconArrowheadTriangleOutline(props: IconProps) {
322
+ return (
323
+ <Svg {...props}>
324
+ <path d="M3 12h11" />
325
+ <path d="M13 8l6 4-6 4z" />
326
+ </Svg>
327
+ );
328
+ }
329
+ export function IconArrowheadCircle(props: IconProps) {
330
+ return (
331
+ <Svg {...props}>
332
+ <path d="M3 12h11" />
333
+ <circle cx="17" cy="12" r="3" fill="currentColor" />
334
+ </Svg>
335
+ );
336
+ }
337
+ export function IconArrowheadDiamond(props: IconProps) {
338
+ return (
339
+ <Svg {...props}>
340
+ <path d="M3 12h10" />
341
+ <path d="M17 8l4 4-4 4-4-4z" fill="currentColor" />
342
+ </Svg>
343
+ );
344
+ }
345
+
346
+ // ── Phase 24 line-type icons ─────────────────────────────────────────────────
347
+ export function IconLineStraight(props: IconProps) {
348
+ return (
349
+ <Svg {...props}>
350
+ <path d="M4 12h16" />
351
+ </Svg>
352
+ );
353
+ }
354
+ export function IconLineCurved(props: IconProps) {
355
+ return (
356
+ <Svg {...props}>
357
+ <path d="M4 18C4 9 20 15 20 6" />
358
+ </Svg>
359
+ );
360
+ }
361
+ export function IconLineElbow(props: IconProps) {
362
+ return (
363
+ <Svg {...props}>
364
+ <path d="M4 6v6a2 2 0 002 2h14" />
365
+ </Svg>
366
+ );
367
+ }
368
+
369
+ // ── Phase 24 text-style icons ────────────────────────────────────────────────
370
+ export function IconBold(props: IconProps) {
371
+ return (
372
+ <Svg {...props}>
373
+ <path d="M7 5h6a3.5 3.5 0 010 7H7zM7 12h7a3.5 3.5 0 010 7H7z" />
374
+ </Svg>
375
+ );
376
+ }
377
+ export function IconStrike(props: IconProps) {
378
+ return (
379
+ <Svg {...props}>
380
+ <path d="M5 12h14" />
381
+ <path d="M8 7a4 3 0 016-1M16 16a4 3 0 01-7 1" />
382
+ </Svg>
383
+ );
384
+ }
385
+ export function IconAlignLeft(props: IconProps) {
386
+ return (
387
+ <Svg {...props}>
388
+ <path d="M4 6h16M4 12h10M4 18h13" />
389
+ </Svg>
390
+ );
391
+ }
392
+ export function IconAlignCenter(props: IconProps) {
393
+ return (
394
+ <Svg {...props}>
395
+ <path d="M4 6h16M7 12h10M5 18h14" />
396
+ </Svg>
397
+ );
398
+ }
399
+ export function IconAlignRight(props: IconProps) {
400
+ return (
401
+ <Svg {...props}>
402
+ <path d="M4 6h16M10 12h10M7 18h13" />
403
+ </Svg>
404
+ );
405
+ }
406
+
250
407
  export const TOOL_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
251
408
  move: IconMove,
252
409
  hand: IconHand,
253
410
  comment: IconComment,
254
411
  pen: IconPen,
412
+ // Phase 24 — single Shape tool. rect/ellipse kept for any legacy lookups.
413
+ shape: IconShape,
255
414
  rect: IconRect,
256
415
  ellipse: IconEllipse,
257
416
  sticky: IconSticky,
@@ -259,3 +418,13 @@ export const TOOL_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
259
418
  text: IconText,
260
419
  eraser: IconEraser,
261
420
  };
421
+
422
+ // Phase 24 — shape-kind → icon, for the palette popover + context toolbar.
423
+ export const SHAPE_KIND_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
424
+ square: IconSquare,
425
+ rounded: IconRoundedSquare,
426
+ circle: IconCircle,
427
+ diamond: IconDiamond,
428
+ triangle: IconTriangle,
429
+ 'triangle-down': IconTriangleDown,
430
+ };