@1agh/maude 0.41.0 → 0.43.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 (260) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/annotations-layer.tsx +1055 -278
  4. package/apps/studio/annotations-model.ts +58 -0
  5. package/apps/studio/api.ts +173 -63
  6. package/apps/studio/bin/_html-playwright.mjs +26 -4
  7. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  8. package/apps/studio/bin/_pdf-playwright.mjs +13 -2
  9. package/apps/studio/bin/_png-playwright.mjs +15 -2
  10. package/apps/studio/bin/_pptx-playwright.mjs +17 -4
  11. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  12. package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
  13. package/apps/studio/bin/_svg-playwright.mjs +26 -4
  14. package/apps/studio/bin/_video-playwright.mjs +141 -71
  15. package/apps/studio/bin/ingest-footage.sh +27 -0
  16. package/apps/studio/bin/photo-adjust.sh +163 -0
  17. package/apps/studio/bin/photo-bg-remove.sh +160 -0
  18. package/apps/studio/bin/probe-footage.sh +28 -0
  19. package/apps/studio/bin/read-annotations.mjs +125 -2
  20. package/apps/studio/bin/screenshot.sh +53 -4
  21. package/apps/studio/canvas-edit.ts +459 -10
  22. package/apps/studio/canvas-lib.tsx +523 -5
  23. package/apps/studio/canvas-pipeline.ts +65 -7
  24. package/apps/studio/canvas-shell.tsx +156 -16
  25. package/apps/studio/client/app.jsx +476 -88
  26. package/apps/studio/client/export-center.jsx +460 -0
  27. package/apps/studio/client/github.js +4 -0
  28. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  29. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  30. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  31. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  32. package/apps/studio/client/photo-knobs.jsx +437 -0
  33. package/apps/studio/client/styles/3-shell-maude.css +59 -0
  34. package/apps/studio/client/styles/4-components.css +150 -0
  35. package/apps/studio/commands/edit-source-command.ts +31 -4
  36. package/apps/studio/config.schema.json +2 -2
  37. package/apps/studio/context-menu.tsx +31 -6
  38. package/apps/studio/dist/client.bundle.js +5561 -19
  39. package/apps/studio/dist/comment-mount.js +2 -2
  40. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  41. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  42. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  43. package/apps/studio/dist/styles.css +1 -1
  44. package/apps/studio/dom-selection.ts +25 -0
  45. package/apps/studio/export-dialog.tsx +18 -25
  46. package/apps/studio/exporters/_runtime.ts +104 -0
  47. package/apps/studio/exporters/html.ts +12 -20
  48. package/apps/studio/exporters/index.ts +14 -2
  49. package/apps/studio/exporters/jobs.ts +376 -0
  50. package/apps/studio/exporters/pdf.ts +16 -20
  51. package/apps/studio/exporters/png.ts +12 -20
  52. package/apps/studio/exporters/pptx.ts +22 -23
  53. package/apps/studio/exporters/scope.ts +1 -0
  54. package/apps/studio/exporters/svg.ts +14 -22
  55. package/apps/studio/exporters/video.ts +37 -19
  56. package/apps/studio/footage/schema.test.ts +195 -0
  57. package/apps/studio/footage/schema.ts +484 -0
  58. package/apps/studio/footage-store.ts +235 -0
  59. package/apps/studio/fs-watch.ts +10 -0
  60. package/apps/studio/git/endpoints.ts +68 -7
  61. package/apps/studio/git/service.ts +198 -20
  62. package/apps/studio/github/service.ts +67 -0
  63. package/apps/studio/handoff.ts +3 -2
  64. package/apps/studio/http.ts +318 -64
  65. package/apps/studio/input-router.tsx +10 -0
  66. package/apps/studio/inspect.ts +53 -1
  67. package/apps/studio/paths.ts +10 -0
  68. package/apps/studio/photo/filters.ts +285 -0
  69. package/apps/studio/photo/pipeline.ts +607 -0
  70. package/apps/studio/photo/schema.ts +367 -0
  71. package/apps/studio/photo-store.ts +154 -0
  72. package/apps/studio/runtime-bundle.ts +29 -6
  73. package/apps/studio/server.ts +8 -2
  74. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  75. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  76. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  77. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  78. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  79. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  80. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  81. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  82. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  83. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  84. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  85. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  86. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  87. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  88. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  89. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  90. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  91. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  92. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  93. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  94. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  95. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  96. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  97. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  98. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  99. package/apps/studio/stickers/life-style/best.png +0 -0
  100. package/apps/studio/stickers/life-style/come-on.png +0 -0
  101. package/apps/studio/stickers/life-style/cut.png +0 -0
  102. package/apps/studio/stickers/life-style/detox.png +0 -0
  103. package/apps/studio/stickers/life-style/just.png +0 -0
  104. package/apps/studio/stickers/life-style/manifest.json +68 -0
  105. package/apps/studio/stickers/life-style/mirror.png +0 -0
  106. package/apps/studio/stickers/life-style/nice.png +0 -0
  107. package/apps/studio/stickers/life-style/objects.png +0 -0
  108. package/apps/studio/stickers/life-style/ok.png +0 -0
  109. package/apps/studio/stickers/life-style/queen.png +0 -0
  110. package/apps/studio/stickers/life-style/star.png +0 -0
  111. package/apps/studio/stickers/life-style/sun.png +0 -0
  112. package/apps/studio/stickers/life-style/water.png +0 -0
  113. package/apps/studio/stickers/life-style/yeah.png +0 -0
  114. package/apps/studio/stickers/life-style/you-can.png +0 -0
  115. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  116. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  117. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  118. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  119. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  120. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  121. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  122. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  123. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  124. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  125. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  126. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  127. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  128. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  129. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  130. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  131. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  132. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  133. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  134. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  135. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  136. package/apps/studio/stickers/project-status/group-100.png +0 -0
  137. package/apps/studio/stickers/project-status/group-101.png +0 -0
  138. package/apps/studio/stickers/project-status/group-102.png +0 -0
  139. package/apps/studio/stickers/project-status/group-103.png +0 -0
  140. package/apps/studio/stickers/project-status/group-104.png +0 -0
  141. package/apps/studio/stickers/project-status/group-105.png +0 -0
  142. package/apps/studio/stickers/project-status/group-106.png +0 -0
  143. package/apps/studio/stickers/project-status/group-107.png +0 -0
  144. package/apps/studio/stickers/project-status/group-108.png +0 -0
  145. package/apps/studio/stickers/project-status/group-109.png +0 -0
  146. package/apps/studio/stickers/project-status/group-110.png +0 -0
  147. package/apps/studio/stickers/project-status/group-111.png +0 -0
  148. package/apps/studio/stickers/project-status/group-112.png +0 -0
  149. package/apps/studio/stickers/project-status/group-113.png +0 -0
  150. package/apps/studio/stickers/project-status/group-114.png +0 -0
  151. package/apps/studio/stickers/project-status/group-115.png +0 -0
  152. package/apps/studio/stickers/project-status/group-117.png +0 -0
  153. package/apps/studio/stickers/project-status/group-118.png +0 -0
  154. package/apps/studio/stickers/project-status/group-119.png +0 -0
  155. package/apps/studio/stickers/project-status/group-120.png +0 -0
  156. package/apps/studio/stickers/project-status/group-121.png +0 -0
  157. package/apps/studio/stickers/project-status/group-122.png +0 -0
  158. package/apps/studio/stickers/project-status/group-41.png +0 -0
  159. package/apps/studio/stickers/project-status/group-42.png +0 -0
  160. package/apps/studio/stickers/project-status/group-43.png +0 -0
  161. package/apps/studio/stickers/project-status/group-44.png +0 -0
  162. package/apps/studio/stickers/project-status/group-45.png +0 -0
  163. package/apps/studio/stickers/project-status/group-46.png +0 -0
  164. package/apps/studio/stickers/project-status/group-47.png +0 -0
  165. package/apps/studio/stickers/project-status/group-48.png +0 -0
  166. package/apps/studio/stickers/project-status/group-49.png +0 -0
  167. package/apps/studio/stickers/project-status/group-50.png +0 -0
  168. package/apps/studio/stickers/project-status/group-51.png +0 -0
  169. package/apps/studio/stickers/project-status/group-52.png +0 -0
  170. package/apps/studio/stickers/project-status/group-53.png +0 -0
  171. package/apps/studio/stickers/project-status/group-54.png +0 -0
  172. package/apps/studio/stickers/project-status/group-55.png +0 -0
  173. package/apps/studio/stickers/project-status/group-56.png +0 -0
  174. package/apps/studio/stickers/project-status/group-57.png +0 -0
  175. package/apps/studio/stickers/project-status/group-58.png +0 -0
  176. package/apps/studio/stickers/project-status/group-59.png +0 -0
  177. package/apps/studio/stickers/project-status/group-60.png +0 -0
  178. package/apps/studio/stickers/project-status/group-61.png +0 -0
  179. package/apps/studio/stickers/project-status/group-62.png +0 -0
  180. package/apps/studio/stickers/project-status/group-63.png +0 -0
  181. package/apps/studio/stickers/project-status/group-64.png +0 -0
  182. package/apps/studio/stickers/project-status/group-65.png +0 -0
  183. package/apps/studio/stickers/project-status/group-66.png +0 -0
  184. package/apps/studio/stickers/project-status/group-67.png +0 -0
  185. package/apps/studio/stickers/project-status/group-68.png +0 -0
  186. package/apps/studio/stickers/project-status/group-69.png +0 -0
  187. package/apps/studio/stickers/project-status/group-70.png +0 -0
  188. package/apps/studio/stickers/project-status/group-71.png +0 -0
  189. package/apps/studio/stickers/project-status/group-72.png +0 -0
  190. package/apps/studio/stickers/project-status/group-73.png +0 -0
  191. package/apps/studio/stickers/project-status/group-74.png +0 -0
  192. package/apps/studio/stickers/project-status/group-75.png +0 -0
  193. package/apps/studio/stickers/project-status/group-76.png +0 -0
  194. package/apps/studio/stickers/project-status/group-77.png +0 -0
  195. package/apps/studio/stickers/project-status/group-78.png +0 -0
  196. package/apps/studio/stickers/project-status/group-79.png +0 -0
  197. package/apps/studio/stickers/project-status/group-80.png +0 -0
  198. package/apps/studio/stickers/project-status/group-81.png +0 -0
  199. package/apps/studio/stickers/project-status/group-82.png +0 -0
  200. package/apps/studio/stickers/project-status/group-83.png +0 -0
  201. package/apps/studio/stickers/project-status/group-84.png +0 -0
  202. package/apps/studio/stickers/project-status/group-85.png +0 -0
  203. package/apps/studio/stickers/project-status/group-86.png +0 -0
  204. package/apps/studio/stickers/project-status/group-87.png +0 -0
  205. package/apps/studio/stickers/project-status/group-88.png +0 -0
  206. package/apps/studio/stickers/project-status/group-89.png +0 -0
  207. package/apps/studio/stickers/project-status/group-90.png +0 -0
  208. package/apps/studio/stickers/project-status/group-91.png +0 -0
  209. package/apps/studio/stickers/project-status/group-92.png +0 -0
  210. package/apps/studio/stickers/project-status/group-93.png +0 -0
  211. package/apps/studio/stickers/project-status/group-94.png +0 -0
  212. package/apps/studio/stickers/project-status/group-96.png +0 -0
  213. package/apps/studio/stickers/project-status/group-97.png +0 -0
  214. package/apps/studio/stickers/project-status/group-98.png +0 -0
  215. package/apps/studio/stickers/project-status/group-99.png +0 -0
  216. package/apps/studio/stickers/project-status/manifest.json +328 -0
  217. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  218. package/apps/studio/test/acp-commands.test.ts +2 -1
  219. package/apps/studio/test/asset-api.test.ts +3 -3
  220. package/apps/studio/test/canvas-edit.test.ts +17 -6
  221. package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
  222. package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
  223. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  224. package/apps/studio/test/edit-source-command.test.ts +23 -0
  225. package/apps/studio/test/element-structural-edit.test.ts +46 -0
  226. package/apps/studio/test/export-center.test.tsx +287 -0
  227. package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
  228. package/apps/studio/test/exporters/history.test.ts +32 -3
  229. package/apps/studio/test/exporters/jobs.test.ts +263 -0
  230. package/apps/studio/test/footage-store.test.ts +100 -0
  231. package/apps/studio/test/git-branches.test.ts +97 -0
  232. package/apps/studio/test/github-api.test.ts +56 -1
  233. package/apps/studio/test/input-router.test.ts +33 -0
  234. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  235. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  236. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  237. package/apps/studio/test/photo-filters.test.ts +149 -0
  238. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  239. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  240. package/apps/studio/test/read-annotations.test.ts +164 -0
  241. package/apps/studio/test/shell-importmap.test.ts +49 -0
  242. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  243. package/apps/studio/test/timeline-parse.test.ts +57 -0
  244. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  245. package/apps/studio/test/undo-stack.test.ts +33 -0
  246. package/apps/studio/test/video-asset.test.ts +5 -5
  247. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  248. package/apps/studio/text-caret.ts +239 -0
  249. package/apps/studio/tool-palette.tsx +43 -20
  250. package/apps/studio/undo-stack.ts +94 -2
  251. package/apps/studio/use-annotation-resize.tsx +4 -4
  252. package/apps/studio/use-canvas-media-drop.tsx +38 -1
  253. package/apps/studio/use-selection-set.tsx +21 -0
  254. package/apps/studio/video-comp.tsx +48 -7
  255. package/apps/studio/whats-new.json +70 -0
  256. package/apps/studio/ws.ts +6 -0
  257. package/cli/commands/design.mjs +34 -3
  258. package/cli/lib/gitignore-block.mjs +2 -0
  259. package/package.json +10 -10
  260. package/plugins/design/templates/_shell.html +1 -0
@@ -41,6 +41,7 @@ import { AnnotationsLayer } from './annotations-layer.tsx';
41
41
  import { ArtboardMarqueeOverlay } from './artboard-marquee.tsx';
42
42
  import {
43
43
  type ArtboardRect,
44
+ PhotoPreviewBridge,
44
45
  SnapGuideOverlay,
45
46
  useArtboardsContext,
46
47
  useDragStateContext,
@@ -88,6 +89,7 @@ import {
88
89
  import { ElementMarqueeOverlay } from './marquee-overlay.tsx';
89
90
  import { MeasureOverlay } from './measure-overlay.tsx';
90
91
  import { ParticipantsChrome } from './participants-chrome.tsx';
92
+ import { mountCaret, placeCaretAt } from './text-caret.ts';
91
93
  import { ToolPalette } from './tool-palette.tsx';
92
94
  import { UndoHud } from './undo-hud.tsx';
93
95
  import {
@@ -327,6 +329,22 @@ const HALO_CSS = `
327
329
  outline-offset: 1px;
328
330
  border-radius: 1px;
329
331
  cursor: text;
332
+ /* Unify the caret with the annotation editors (CARET_FIX_STYLE in
333
+ annotations-layer.tsx) — same accent, visible against any background.
334
+ No compositing trigger here (no translateZ/will-change) so WebKit keeps
335
+ blinking the native caret. */
336
+ caret-color: var(--maude-hud-accent, #4a63e7);
337
+ }
338
+ /* Dogfood fix — hover affordance for the same leaf-text elements the dblclick
339
+ handler above enters edit mode on. The :not(:empty):not(:has(> *)) selector
340
+ proxies "all children are text nodes" (the exact isLeafText check happens
341
+ in JS on dblclick — this only needs to be a good-enough visual hint). Loses
342
+ to an active draw/annotation tool's !important global cursor override
343
+ (use-tool-mode.tsx), so it only shows in Move mode, same as the edit itself.
344
+ NOTE: keep this comment backtick-free — it lives inside a JS template
345
+ literal and a stray backtick closes it early (bun parse fail). */
346
+ [data-cd-id]:not(:empty):not(:has(> *)):hover {
347
+ cursor: text;
330
348
  }
331
349
  /* Phase 12.1 (DDR-138) — in-canvas drag-to-reorder (Figma-style). Dragging a
332
350
  SELECTED element floats it with the cursor (via transform, applied inline —
@@ -549,6 +567,18 @@ function getLastLayersArtboardId(): string | null {
549
567
  }
550
568
  }
551
569
 
570
+ // DDR-148 — an artboard hosting a <VideoComp> stamps a `[data-comp-id]`
571
+ // element inside it (see video-comp.tsx). Gates the artboard-chrome context
572
+ // menu's "Open Timeline" entry to artboards that actually have a sequence.
573
+ function artboardHasVideo(artboardId: string | null | undefined): boolean {
574
+ if (typeof document === 'undefined' || !artboardId) return false;
575
+ try {
576
+ return !!document.querySelector(`[data-dc-screen="${CSS.escape(artboardId)}"] [data-comp-id]`);
577
+ } catch {
578
+ return false;
579
+ }
580
+ }
581
+
552
582
  function postLayersTree(artboardId: string | null | undefined): void {
553
583
  if (typeof document === 'undefined' || !artboardId) return;
554
584
  const root = document.querySelector(`[data-dc-screen="${CSS.escape(artboardId)}"]`);
@@ -1440,6 +1470,34 @@ function buildRegistry(deps: {
1440
1470
  }
1441
1471
  },
1442
1472
  },
1473
+ {
1474
+ // feature-photo-editor (Task 16) — only on a content-addressed `<img>`.
1475
+ // Selects it (so the Inspector derives the Photo target from the
1476
+ // selection's `photoAsset`) + opens the Inspector on the Photo tab.
1477
+ id: 'edit-photo',
1478
+ label: 'Edit Photo…',
1479
+ hidden: (target) => {
1480
+ const el = target.el as HTMLImageElement | null;
1481
+ if (el?.tagName?.toLowerCase() !== 'img') return true;
1482
+ return !/assets\/[0-9a-f]{8}\.[a-z0-9]+/i.test(el.getAttribute?.('src') || '');
1483
+ },
1484
+ onSelect: (target) => {
1485
+ if (target.el) {
1486
+ selSet.replace(
1487
+ hoverTargetToSelection({
1488
+ el: target.el,
1489
+ cdId: target.cdId ?? null,
1490
+ artboardId: target.artboardId ?? null,
1491
+ } as HoverTarget)
1492
+ );
1493
+ }
1494
+ try {
1495
+ window.parent.postMessage({ dgn: 'open-inspector', tab: 'photo' }, '*');
1496
+ } catch {
1497
+ /* detached / cross-origin */
1498
+ }
1499
+ },
1500
+ },
1443
1501
  {
1444
1502
  id: 'select-layer',
1445
1503
  label: 'Select layer',
@@ -1631,6 +1689,29 @@ function buildRegistry(deps: {
1631
1689
  fitItem,
1632
1690
  resetItem,
1633
1691
  ],
1692
+ [
1693
+ // DDR-148 — only meaningful (and only enabled) on a video-comp
1694
+ // artboard; scopes + opens the shell's Timeline dock.
1695
+ {
1696
+ id: 'open-timeline',
1697
+ label: 'Open Timeline',
1698
+ shortcut: '⌘⇧T',
1699
+ disabled: (target) => !artboardHasVideo(target.artboardId),
1700
+ disabledHint: 'This artboard has no video sequence',
1701
+ onSelect: (target) => {
1702
+ if (!target.artboardId) return;
1703
+ focusArtboard(target.artboardId);
1704
+ try {
1705
+ window.parent.postMessage(
1706
+ { dgn: 'open-timeline-request', artboardId: target.artboardId },
1707
+ '*'
1708
+ );
1709
+ } catch {
1710
+ /* detached / cross-origin */
1711
+ }
1712
+ },
1713
+ },
1714
+ ],
1634
1715
  [
1635
1716
  // G7 — align commands. Six modes; gated on ≥ 2 selected artboards
1636
1717
  // (alignment is well-defined with 2). Primary surface is the
@@ -2134,6 +2215,9 @@ function CanvasRouter({
2134
2215
  if (!host) return;
2135
2216
  let editing: HTMLElement | null = null;
2136
2217
  let original = '';
2218
+ // Unified custom blinking caret (text-caret.ts) — mounted for the whole
2219
+ // edit session, disposed in teardown. Hides the native caret while up.
2220
+ let caretDispose: (() => void) | null = null;
2137
2221
  // DDR-150 P1 — the committed-but-unconfirmed edit, so a shell rejection
2138
2222
  // (dynamic/mixed content the engine refuses) can revert the optimistic
2139
2223
  // contenteditable text instead of letting it silently vanish on the next
@@ -2150,6 +2234,8 @@ function CanvasRouter({
2150
2234
  }
2151
2235
  };
2152
2236
  function teardown(elx: HTMLElement): void {
2237
+ caretDispose?.();
2238
+ caretDispose = null;
2153
2239
  elx.removeAttribute('contenteditable');
2154
2240
  elx.classList.remove('dc-text-editing');
2155
2241
  elx.removeEventListener('blur', onBlur, true);
@@ -2169,8 +2255,22 @@ function CanvasRouter({
2169
2255
  const cdId = elx.getAttribute('data-cd-id');
2170
2256
  if (!cdId) return;
2171
2257
  const file = deriveFile();
2258
+ // For `{variable}` text the engine needs to know WHICH rendered instance
2259
+ // this is (a `.map()` renders one source element N×) — its index among
2260
+ // same-cd-id nodes — plus the pre-edit text to target the right array
2261
+ // item. Harmless for literal text (the engine ignores both there).
2262
+ let occurrence = 0;
2172
2263
  try {
2173
- window.parent.postMessage({ dgn: 'edit-text', id: cdId, file, text }, '*');
2264
+ const sameId = Array.from(document.querySelectorAll(`[data-cd-id="${CSS.escape(cdId)}"]`));
2265
+ occurrence = Math.max(0, sameId.indexOf(elx));
2266
+ } catch {
2267
+ /* querySelectorAll/CSS.escape unavailable — occurrence stays 0 */
2268
+ }
2269
+ try {
2270
+ window.parent.postMessage(
2271
+ { dgn: 'edit-text', id: cdId, file, text, occurrence, before: original.trim() },
2272
+ '*'
2273
+ );
2174
2274
  } catch {
2175
2275
  /* detached / cross-origin */
2176
2276
  }
@@ -2187,9 +2287,36 @@ function CanvasRouter({
2187
2287
  key: '',
2188
2288
  before: original.trim(),
2189
2289
  after: text,
2290
+ // Carry the occurrence so undo/redo can re-target a `{variable}` edit
2291
+ // (the .map()/component-prop resolution needs it — canvas-edit.ts).
2292
+ occurrence,
2190
2293
  })
2191
2294
  );
2192
2295
  }
2296
+ // Enter contenteditable edit mode on a leaf-text `stamped` element, caret
2297
+ // placed at (clientX, clientY) rather than a full select-all (that used to
2298
+ // force a delete-and-retype for any edit that isn't a full replace).
2299
+ // `caretRangeFromPoint` (WebKit/Chromium) and its standardized successor
2300
+ // `caretPositionFromPoint` (Firefox + newer engines) both resolve the
2301
+ // exact character under the pointer; select-all-contents is the fallback
2302
+ // only when neither exists. Extracted so both the native dblclick path
2303
+ // AND the annotation Text-tool's click-through (maude:enter-text-edit,
2304
+ // dispatched from annotations-layer.tsx when its own click lands on an
2305
+ // existing editable element) share one entry point.
2306
+ function enterEditModeAt(stamped: HTMLElement, clientX: number, clientY: number): void {
2307
+ if (editing) return;
2308
+ editing = stamped;
2309
+ original = stamped.textContent ?? '';
2310
+ stamped.setAttribute('contenteditable', 'plaintext-only');
2311
+ stamped.classList.add('dc-text-editing');
2312
+ stamped.addEventListener('blur', onBlur, true);
2313
+ stamped.addEventListener('keydown', onKey, true);
2314
+ stamped.focus();
2315
+ // Unified placement chain (text-caret.ts) — the annotation editors use
2316
+ // the exact same helper, so caret-at-click can never diverge per surface.
2317
+ placeCaretAt(stamped, window, { x: clientX, y: clientY });
2318
+ caretDispose = mountCaret(stamped, window);
2319
+ }
2193
2320
  const onDbl = (e: MouseEvent): void => {
2194
2321
  if (editing) return;
2195
2322
  const t = e.target as HTMLElement | null;
@@ -2229,22 +2356,32 @@ function CanvasRouter({
2229
2356
  }
2230
2357
  e.preventDefault();
2231
2358
  e.stopPropagation();
2232
- editing = stamped;
2233
- original = stamped.textContent ?? '';
2234
- stamped.setAttribute('contenteditable', 'plaintext-only');
2235
- stamped.classList.add('dc-text-editing');
2236
- stamped.addEventListener('blur', onBlur, true);
2237
- stamped.addEventListener('keydown', onKey, true);
2238
- stamped.focus();
2239
- try {
2240
- const range = document.createRange();
2241
- range.selectNodeContents(stamped);
2242
- const sel = window.getSelection();
2243
- sel?.removeAllRanges();
2244
- sel?.addRange(range);
2245
- } catch {
2246
- /* selection API unavailable */
2359
+ // Phase 6 (unified-text-editing) only offer inline edit where the
2360
+ // engine will actually save: the build-time data-cd-editable marker
2361
+ // (canvas-pipeline.ts) mirrors applyTextEdit's acceptance. The DOM-only
2362
+ // isLeafText heuristic above can't tell `<p>Total: {1 + 1} items</p>`
2363
+ // (mixed source, engine refuses, DDR-150 dead end) from honest static
2364
+ // text — the AST-stamped marker can.
2365
+ if (!stamped.hasAttribute('data-cd-editable')) {
2366
+ showCanvasToast(
2367
+ 'This text is filled in from code (a variable) — edit it via chat or /design:edit.'
2368
+ );
2369
+ return;
2247
2370
  }
2371
+ enterEditModeAt(stamped, e.clientX, e.clientY);
2372
+ };
2373
+ // Dogfood fix — the annotation Text tool's click-through: clicking an
2374
+ // existing editable element with the Text (T) tool active should edit
2375
+ // THAT text in place instead of dropping a new standalone annotation on
2376
+ // top of it. annotations-layer.tsx hit-tests past its own input-capture
2377
+ // overlay and dispatches this when the click resolves to a leaf-text
2378
+ // `[data-cd-id]` element; canvas-shell owns the DOM/contentEditable side
2379
+ // of editing, so it's the one that must act on it.
2380
+ const onEnterTextEdit = (e: Event): void => {
2381
+ const detail = (e as CustomEvent<{ el?: HTMLElement; clientX?: number; clientY?: number }>)
2382
+ .detail;
2383
+ if (!detail?.el || !host.contains(detail.el)) return;
2384
+ enterEditModeAt(detail.el, detail.clientX ?? 0, detail.clientY ?? 0);
2248
2385
  };
2249
2386
  // The shell rejected the last inline text edit — revert the optimistic
2250
2387
  // contenteditable text so the canvas reflects the true (unsaved) source
@@ -2266,9 +2403,11 @@ function CanvasRouter({
2266
2403
  // Capture phase so we beat the fit-to-view dblclick handler.
2267
2404
  host.addEventListener('dblclick', onDbl, true);
2268
2405
  window.addEventListener('message', onEditReverted);
2406
+ document.addEventListener('maude:enter-text-edit', onEnterTextEdit);
2269
2407
  return () => {
2270
2408
  host.removeEventListener('dblclick', onDbl, true);
2271
2409
  window.removeEventListener('message', onEditReverted);
2410
+ document.removeEventListener('maude:enter-text-edit', onEnterTextEdit);
2272
2411
  if (editing) teardown(editing);
2273
2412
  };
2274
2413
  }, [hostRef, selSet]);
@@ -2378,6 +2517,7 @@ function CanvasRouter({
2378
2517
  alignArtboards={alignArtboards}
2379
2518
  />
2380
2519
  <SnapGuideOverlay />
2520
+ <PhotoPreviewBridge />
2381
2521
  <UndoHud />
2382
2522
  <CursorsOverlay />
2383
2523
  <AiBanner />