@1agh/maude 1.0.11 → 1.3.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 (567) hide show
  1. package/README.md +3 -1
  2. package/apps/studio/acp/bridge.ts +18 -13
  3. package/apps/studio/acp/index.ts +43 -7
  4. package/apps/studio/acp/transcript-io.ts +394 -0
  5. package/apps/studio/acp/transcript.ts +103 -38
  6. package/apps/studio/annotations-layer.tsx +191 -116
  7. package/apps/studio/annotations-model.ts +39 -0
  8. package/apps/studio/annotations-sync.ts +50 -0
  9. package/apps/studio/api.ts +821 -55
  10. package/apps/studio/bin/_import-figma.mjs +57 -11
  11. package/apps/studio/bin/_seed-fixture.mjs +163 -0
  12. package/apps/studio/bin/annotate.mjs +3 -1
  13. package/apps/studio/bin/server-up.sh +17 -2
  14. package/apps/studio/canvas-edit.ts +391 -7
  15. package/apps/studio/canvas-imports.ts +89 -0
  16. package/apps/studio/canvas-lib.tsx +82 -18
  17. package/apps/studio/canvas-notice-message.ts +16 -0
  18. package/apps/studio/canvas-notifications.tsx +19 -0
  19. package/apps/studio/canvas-shell.tsx +57 -6
  20. package/apps/studio/client/app.jsx +943 -248
  21. package/apps/studio/client/apply-edit-request.ts +55 -0
  22. package/apps/studio/client/export-center.jsx +125 -67
  23. package/apps/studio/client/github.js +38 -1
  24. package/apps/studio/client/index-loader.ts +76 -0
  25. package/apps/studio/client/panels/CloudBar.jsx +52 -6
  26. package/apps/studio/client/panels/GitPanel.jsx +103 -0
  27. package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
  28. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
  29. package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
  30. package/apps/studio/client/panels/SyncPanel.jsx +238 -4
  31. package/apps/studio/client/panels/TeamProjects.jsx +376 -0
  32. package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
  33. package/apps/studio/client/photo-knobs.jsx +3 -0
  34. package/apps/studio/client/report-bug.jsx +19 -1
  35. package/apps/studio/client/share-dialog.jsx +91 -0
  36. package/apps/studio/client/share-link.js +94 -0
  37. package/apps/studio/client/styles/3-shell-maude.css +69 -18
  38. package/apps/studio/client/styles/4-components.css +44 -1
  39. package/apps/studio/client/tree-row-menu.jsx +48 -3
  40. package/apps/studio/client/whats-new.jsx +26 -32
  41. package/apps/studio/cloud/endpoints.ts +132 -0
  42. package/apps/studio/collab/index.ts +4 -0
  43. package/apps/studio/collab/persistence.ts +137 -17
  44. package/apps/studio/collab/registry.ts +19 -21
  45. package/apps/studio/collab/room.ts +13 -2
  46. package/apps/studio/context.ts +59 -0
  47. package/apps/studio/debug-bundle.ts +13 -0
  48. package/apps/studio/dist/client.bundle.js +1911 -1879
  49. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  50. package/apps/studio/dist/runtime/sonner.js +1 -0
  51. package/apps/studio/dist/styles.css +1 -1
  52. package/apps/studio/export-dialog.tsx +54 -0
  53. package/apps/studio/exporters/degraded.ts +19 -3
  54. package/apps/studio/exporters/ghostscript.ts +194 -0
  55. package/apps/studio/exporters/pdf-fonts.ts +261 -0
  56. package/apps/studio/exporters/pdf.ts +152 -3
  57. package/apps/studio/exporters/remote.ts +36 -1
  58. package/apps/studio/fs-watch.ts +45 -1
  59. package/apps/studio/http.ts +296 -19
  60. package/apps/studio/inspect.ts +19 -0
  61. package/apps/studio/managed-projects.ts +150 -0
  62. package/apps/studio/mem.ts +45 -9
  63. package/apps/studio/notifications.tsx +292 -0
  64. package/apps/studio/runtime-bundle.ts +2 -0
  65. package/apps/studio/server.ts +69 -6
  66. package/apps/studio/sync/accepted-cold-start.ts +225 -0
  67. package/apps/studio/sync/accepted-link.ts +320 -0
  68. package/apps/studio/sync/action-stage.ts +343 -0
  69. package/apps/studio/sync/agent.ts +104 -45
  70. package/apps/studio/sync/asset-push.ts +1 -47
  71. package/apps/studio/sync/cell-file-events.ts +3 -0
  72. package/apps/studio/sync/codec.ts +192 -34
  73. package/apps/studio/sync/cold-start-apply.ts +7 -0
  74. package/apps/studio/sync/cold-start.ts +118 -22
  75. package/apps/studio/sync/comment-identity.ts +68 -0
  76. package/apps/studio/sync/connection-state.ts +15 -1
  77. package/apps/studio/sync/ctl-provider.ts +15 -2
  78. package/apps/studio/sync/document-discovery.ts +81 -0
  79. package/apps/studio/sync/file-ledger.ts +117 -0
  80. package/apps/studio/sync/file-membership.ts +10 -2
  81. package/apps/studio/sync/file-plane.ts +926 -47
  82. package/apps/studio/sync/index.ts +1786 -36
  83. package/apps/studio/sync/journal-client.ts +13 -1
  84. package/apps/studio/sync/migrate-seed.ts +150 -22
  85. package/apps/studio/sync/poke.ts +4 -2
  86. package/apps/studio/sync/presentation.ts +311 -5
  87. package/apps/studio/sync/projection.ts +820 -23
  88. package/apps/studio/sync/repeated-module.ts +73 -0
  89. package/apps/studio/sync/retry-after.ts +169 -0
  90. package/apps/studio/sync/revision-barrier.ts +129 -0
  91. package/apps/studio/sync/seed-progress.ts +218 -0
  92. package/apps/studio/sync/seed-repair.ts +46 -0
  93. package/apps/studio/sync/source-merge.ts +100 -0
  94. package/apps/studio/sync/source-ops.ts +289 -0
  95. package/apps/studio/sync/source-recovery.ts +70 -0
  96. package/apps/studio/sync/source-validation.ts +56 -0
  97. package/apps/studio/sync/status.ts +201 -6
  98. package/apps/studio/sync/transaction-client.ts +460 -0
  99. package/apps/studio/sync/transport-error.ts +111 -0
  100. package/apps/studio/sync/writer-registry.ts +236 -0
  101. package/apps/studio/text-caret.ts +35 -0
  102. package/apps/studio/undo-hud.tsx +9 -87
  103. package/apps/studio/use-canvas-media-drop.tsx +4 -39
  104. package/apps/studio/whats-new.json +173 -0
  105. package/cli/bin/maude.mjs +2 -0
  106. package/cli/commands/codex.mjs +191 -0
  107. package/cli/commands/harness.mjs +1051 -0
  108. package/cli/commands/help.mjs +13 -0
  109. package/cli/lib/design-link.mjs +190 -2
  110. package/cli/lib/harness/capabilities.mjs +302 -0
  111. package/cli/lib/harness/codex-runtime.mjs +892 -0
  112. package/cli/lib/harness/compatibility.mjs +27 -0
  113. package/cli/lib/harness/discover-claude.mjs +1393 -0
  114. package/cli/lib/harness/managed-state.mjs +1296 -0
  115. package/cli/lib/harness/model.mjs +86 -0
  116. package/cli/lib/harness/secrets.mjs +390 -0
  117. package/cli/lib/harness/targets/codex.mjs +1141 -0
  118. package/cli/lib/harness/targets/opencode.mjs +807 -0
  119. package/cli/lib/harness/transaction.mjs +1099 -0
  120. package/cli/lib/hubs-config.mjs +35 -8
  121. package/cli/templates/harness/opencode/maude-projector.ts +58 -0
  122. package/docs/harness-capability-matrix.md +125 -0
  123. package/docs/harness-environment-projection.md +95 -0
  124. package/package.json +33 -9
  125. package/plugins/design/dependencies.json +3 -3
  126. package/plugins/design/templates/_shell.html +47 -0
  127. package/plugins/flow/.claude-plugin/config.schema.json +8 -0
  128. package/plugins/flow/dependencies.json +3 -3
  129. package/apps/studio/.ai/cache/_stats.json +0 -7
  130. package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
  131. package/apps/studio/bin/_curl-local.test.mjs +0 -280
  132. package/apps/studio/bin/_pw-launch.test.mjs +0 -90
  133. package/apps/studio/bin/_smart-frames.test.mjs +0 -195
  134. package/apps/studio/bin/_transcribe.test.mjs +0 -80
  135. package/apps/studio/bun.lock +0 -928
  136. package/apps/studio/draw/test/animate.test.ts +0 -172
  137. package/apps/studio/draw/test/brush.test.ts +0 -213
  138. package/apps/studio/draw/test/composition.test.ts +0 -141
  139. package/apps/studio/draw/test/geometry.test.ts +0 -199
  140. package/apps/studio/draw/test/gradient.test.ts +0 -167
  141. package/apps/studio/draw/test/layout.test.ts +0 -120
  142. package/apps/studio/draw/test/morph.test.ts +0 -107
  143. package/apps/studio/draw/test/optimize.test.ts +0 -40
  144. package/apps/studio/draw/test/palette.test.ts +0 -123
  145. package/apps/studio/draw/test/primitives.test.ts +0 -129
  146. package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
  147. package/apps/studio/draw/test/serialize.test.ts +0 -105
  148. package/apps/studio/figma/assets.test.ts +0 -464
  149. package/apps/studio/figma/client.test.ts +0 -395
  150. package/apps/studio/figma/codegen-client.test.ts +0 -276
  151. package/apps/studio/figma/codegen-fonts.test.ts +0 -103
  152. package/apps/studio/figma/codegen-values.test.ts +0 -179
  153. package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
  154. package/apps/studio/figma/fig-decode.test.ts +0 -788
  155. package/apps/studio/figma/fig-differential.test.ts +0 -182
  156. package/apps/studio/figma/fig-translator.test.ts +0 -192
  157. package/apps/studio/figma/fig-vector.test.ts +0 -113
  158. package/apps/studio/figma/from-codegen.test.ts +0 -408
  159. package/apps/studio/figma/sanitize.test.ts +0 -325
  160. package/apps/studio/figma/tailwind-map.test.ts +0 -142
  161. package/apps/studio/figma/to-artboard.test.ts +0 -808
  162. package/apps/studio/figma/to-render.test.ts +0 -180
  163. package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
  164. package/apps/studio/figma/to-strokes.test.ts +0 -705
  165. package/apps/studio/figma/to-tokens.test.ts +0 -321
  166. package/apps/studio/figma/url.test.ts +0 -167
  167. package/apps/studio/footage/schema.test.ts +0 -287
  168. package/apps/studio/generation/adapters/groq.test.ts +0 -80
  169. package/apps/studio/generation/audio-library.test.ts +0 -95
  170. package/apps/studio/generation/captions.test.ts +0 -78
  171. package/apps/studio/generation/elevenlabs.test.ts +0 -223
  172. package/apps/studio/generation/gemini.test.ts +0 -350
  173. package/apps/studio/generation/jobs.test.ts +0 -128
  174. package/apps/studio/generation/keys.test.ts +0 -75
  175. package/apps/studio/generation/prefs.test.ts +0 -95
  176. package/apps/studio/generation/types.test.ts +0 -123
  177. package/apps/studio/generation/whisper-models.test.ts +0 -179
  178. package/apps/studio/test/_helpers.ts +0 -108
  179. package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
  180. package/apps/studio/test/acp-activity.test.ts +0 -154
  181. package/apps/studio/test/acp-ai-activity.test.ts +0 -182
  182. package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
  183. package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
  184. package/apps/studio/test/acp-branch-guard.test.ts +0 -123
  185. package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
  186. package/apps/studio/test/acp-bridge.test.ts +0 -545
  187. package/apps/studio/test/acp-capabilities.test.ts +0 -123
  188. package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
  189. package/apps/studio/test/acp-commands.test.ts +0 -119
  190. package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
  191. package/apps/studio/test/acp-elicitation.test.ts +0 -251
  192. package/apps/studio/test/acp-env.test.ts +0 -95
  193. package/apps/studio/test/acp-mode-banner.test.ts +0 -45
  194. package/apps/studio/test/acp-origin-gate.test.ts +0 -160
  195. package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
  196. package/apps/studio/test/acp-permission.test.ts +0 -278
  197. package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
  198. package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
  199. package/apps/studio/test/acp-session-plugins.test.ts +0 -209
  200. package/apps/studio/test/acp-toolgroup.test.ts +0 -76
  201. package/apps/studio/test/acp-transcript-view.test.ts +0 -71
  202. package/apps/studio/test/acp-transcript.test.ts +0 -238
  203. package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
  204. package/apps/studio/test/acp-usage.test.ts +0 -143
  205. package/apps/studio/test/acp-write-gate.test.ts +0 -323
  206. package/apps/studio/test/acp-write-scope.test.ts +0 -533
  207. package/apps/studio/test/active-state.test.ts +0 -186
  208. package/apps/studio/test/activity.test.ts +0 -189
  209. package/apps/studio/test/ai-activity.test.ts +0 -113
  210. package/apps/studio/test/annotate-write.test.ts +0 -1074
  211. package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
  212. package/apps/studio/test/annotations-align.test.ts +0 -88
  213. package/apps/studio/test/annotations-api.test.ts +0 -145
  214. package/apps/studio/test/annotations-bindings.test.ts +0 -262
  215. package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
  216. package/apps/studio/test/annotations-groups.test.ts +0 -231
  217. package/apps/studio/test/annotations-layer.test.ts +0 -1011
  218. package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
  219. package/apps/studio/test/annotations-snap.test.ts +0 -135
  220. package/apps/studio/test/annotations-text-format.test.ts +0 -396
  221. package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
  222. package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
  223. package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
  224. package/apps/studio/test/artboard-kinds.test.tsx +0 -83
  225. package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
  226. package/apps/studio/test/asset-api.test.ts +0 -149
  227. package/apps/studio/test/assets-s3.test.ts +0 -249
  228. package/apps/studio/test/binary-smoke.test.ts +0 -47
  229. package/apps/studio/test/boot-self-heal.test.ts +0 -87
  230. package/apps/studio/test/browse-posture.test.tsx +0 -107
  231. package/apps/studio/test/bundle-smoke.test.ts +0 -46
  232. package/apps/studio/test/camera-reveal.test.tsx +0 -173
  233. package/apps/studio/test/canvas-artifacts.test.ts +0 -125
  234. package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
  235. package/apps/studio/test/canvas-build.test.ts +0 -141
  236. package/apps/studio/test/canvas-create-api.test.ts +0 -676
  237. package/apps/studio/test/canvas-cursors.test.ts +0 -164
  238. package/apps/studio/test/canvas-edit.test.ts +0 -470
  239. package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
  240. package/apps/studio/test/canvas-header.test.ts +0 -127
  241. package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
  242. package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
  243. package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
  244. package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
  245. package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
  246. package/apps/studio/test/canvas-list-watch.test.ts +0 -388
  247. package/apps/studio/test/canvas-media-drop.test.ts +0 -187
  248. package/apps/studio/test/canvas-meta-api.test.ts +0 -707
  249. package/apps/studio/test/canvas-move-api.test.ts +0 -587
  250. package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
  251. package/apps/studio/test/canvas-pipeline.test.ts +0 -180
  252. package/apps/studio/test/canvas-rects.test.ts +0 -198
  253. package/apps/studio/test/canvas-reorder.test.ts +0 -211
  254. package/apps/studio/test/canvas-route.test.ts +0 -179
  255. package/apps/studio/test/canvas-shell-base.test.ts +0 -107
  256. package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
  257. package/apps/studio/test/canvas-url.test.ts +0 -138
  258. package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
  259. package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
  260. package/apps/studio/test/capture-core.test.ts +0 -32
  261. package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
  262. package/apps/studio/test/chat-attachments.test.ts +0 -117
  263. package/apps/studio/test/chat-context.test.ts +0 -129
  264. package/apps/studio/test/chat-markdown.test.tsx +0 -58
  265. package/apps/studio/test/clip-addressing.test.ts +0 -738
  266. package/apps/studio/test/clip-ops.test.ts +0 -922
  267. package/apps/studio/test/cloud-build.test.ts +0 -95
  268. package/apps/studio/test/cloud-connect-note.test.ts +0 -135
  269. package/apps/studio/test/cloud-endpoints.test.ts +0 -590
  270. package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
  271. package/apps/studio/test/cloud-history-posture.test.ts +0 -230
  272. package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
  273. package/apps/studio/test/cloud-renew.test.ts +0 -205
  274. package/apps/studio/test/cloud-session-role.test.ts +0 -161
  275. package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
  276. package/apps/studio/test/cold-start-apply.test.ts +0 -303
  277. package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
  278. package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
  279. package/apps/studio/test/collab-bridge.test.ts +0 -79
  280. package/apps/studio/test/collab-loopback.test.ts +0 -63
  281. package/apps/studio/test/collab-origin-gate.test.ts +0 -323
  282. package/apps/studio/test/collab-protocol.test.ts +0 -146
  283. package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
  284. package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
  285. package/apps/studio/test/collab-room.test.ts +0 -188
  286. package/apps/studio/test/collab-session-survival.test.tsx +0 -176
  287. package/apps/studio/test/collab-stress.test.ts +0 -129
  288. package/apps/studio/test/comment-mount.test.ts +0 -97
  289. package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
  290. package/apps/studio/test/comments-api.test.ts +0 -277
  291. package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
  292. package/apps/studio/test/comments-overlay.test.ts +0 -117
  293. package/apps/studio/test/compile-entry.test.ts +0 -133
  294. package/apps/studio/test/component-map.test.ts +0 -48
  295. package/apps/studio/test/config-projection.test.ts +0 -104
  296. package/apps/studio/test/config-reload.test.ts +0 -230
  297. package/apps/studio/test/config-version.test.ts +0 -88
  298. package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
  299. package/apps/studio/test/convert-to-absolute.test.ts +0 -333
  300. package/apps/studio/test/credential-grammar.test.ts +0 -107
  301. package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
  302. package/apps/studio/test/csrf-write-guard.test.ts +0 -141
  303. package/apps/studio/test/debug-bundle.test.ts +0 -105
  304. package/apps/studio/test/design-setup-readiness.test.ts +0 -155
  305. package/apps/studio/test/detach-component.test.ts +0 -94
  306. package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
  307. package/apps/studio/test/dom-selection.test.ts +0 -198
  308. package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
  309. package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
  310. package/apps/studio/test/edit-persistence.test.ts +0 -91
  311. package/apps/studio/test/edit-scope-api.test.ts +0 -119
  312. package/apps/studio/test/edit-source-command.test.ts +0 -142
  313. package/apps/studio/test/edit-suppress.test.ts +0 -170
  314. package/apps/studio/test/editing-presence.test.ts +0 -103
  315. package/apps/studio/test/element-resize.test.ts +0 -136
  316. package/apps/studio/test/element-structural-api.test.ts +0 -434
  317. package/apps/studio/test/element-structural-edit.test.ts +0 -750
  318. package/apps/studio/test/ensure-browser.test.ts +0 -92
  319. package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
  320. package/apps/studio/test/export-assemble.test.ts +0 -53
  321. package/apps/studio/test/export-browser-lane.test.ts +0 -126
  322. package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
  323. package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
  324. package/apps/studio/test/export-center.test.tsx +0 -287
  325. package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
  326. package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
  327. package/apps/studio/test/export-lane.test.ts +0 -245
  328. package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
  329. package/apps/studio/test/exporters/canva.test.ts +0 -63
  330. package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
  331. package/apps/studio/test/exporters/endpoint.test.ts +0 -121
  332. package/apps/studio/test/exporters/history.test.ts +0 -107
  333. package/apps/studio/test/exporters/html.test.ts +0 -26
  334. package/apps/studio/test/exporters/jobs.test.ts +0 -342
  335. package/apps/studio/test/exporters/pdf.test.ts +0 -85
  336. package/apps/studio/test/exporters/png.test.ts +0 -103
  337. package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
  338. package/apps/studio/test/exporters/pptx.test.ts +0 -31
  339. package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
  340. package/apps/studio/test/exporters/runtime.test.ts +0 -59
  341. package/apps/studio/test/exporters/scope.test.ts +0 -292
  342. package/apps/studio/test/exporters/svg.test.ts +0 -29
  343. package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
  344. package/apps/studio/test/exporters/zip.test.ts +0 -104
  345. package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
  346. package/apps/studio/test/figjam-v3-model.test.ts +0 -342
  347. package/apps/studio/test/figma-explode.test.ts +0 -438
  348. package/apps/studio/test/figma-provenance.test.ts +0 -108
  349. package/apps/studio/test/figma-routes.test.ts +0 -294
  350. package/apps/studio/test/file-lock.test.ts +0 -84
  351. package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
  352. package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
  353. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
  354. package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
  355. package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
  356. package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
  357. package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
  358. package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
  359. package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
  360. package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
  361. package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
  362. package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
  363. package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
  364. package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
  365. package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
  366. package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
  367. package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
  368. package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
  369. package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
  370. package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
  371. package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
  372. package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
  373. package/apps/studio/test/footage-store.test.ts +0 -100
  374. package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
  375. package/apps/studio/test/fs-watch.test.ts +0 -41
  376. package/apps/studio/test/gemma-models.test.ts +0 -245
  377. package/apps/studio/test/generate-route.test.ts +0 -106
  378. package/apps/studio/test/git-api.test.ts +0 -0
  379. package/apps/studio/test/git-branches.test.ts +0 -341
  380. package/apps/studio/test/git-cloud-posture.test.ts +0 -59
  381. package/apps/studio/test/git-grouping.test.ts +0 -106
  382. package/apps/studio/test/git-lifecycle.test.ts +0 -100
  383. package/apps/studio/test/git-log-format.test.ts +0 -95
  384. package/apps/studio/test/git-safe-rel.test.ts +0 -132
  385. package/apps/studio/test/git-system-engine.test.ts +0 -135
  386. package/apps/studio/test/git-watch.test.ts +0 -97
  387. package/apps/studio/test/github-api.test.ts +0 -577
  388. package/apps/studio/test/grid-track-handles.test.ts +0 -160
  389. package/apps/studio/test/handoff-static-frames.test.ts +0 -215
  390. package/apps/studio/test/handoff.test.ts +0 -329
  391. package/apps/studio/test/history-rollback.test.ts +0 -88
  392. package/apps/studio/test/hmr-broadcast.test.ts +0 -236
  393. package/apps/studio/test/hmr-classify.test.ts +0 -87
  394. package/apps/studio/test/hub-link.test.ts +0 -80
  395. package/apps/studio/test/identity-cache.test.ts +0 -83
  396. package/apps/studio/test/import-asset-browser.test.ts +0 -64
  397. package/apps/studio/test/import-asset-route.test.ts +0 -71
  398. package/apps/studio/test/import-asset.test.ts +0 -427
  399. package/apps/studio/test/import-brand-browser.test.ts +0 -88
  400. package/apps/studio/test/import-brand-route.test.ts +0 -120
  401. package/apps/studio/test/import-brand.test.ts +0 -206
  402. package/apps/studio/test/import-figma.test.ts +0 -667
  403. package/apps/studio/test/import-tokens.test.ts +0 -722
  404. package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
  405. package/apps/studio/test/input-router.test.ts +0 -757
  406. package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
  407. package/apps/studio/test/inspect-selections.test.ts +0 -219
  408. package/apps/studio/test/knob-props-authored.test.ts +0 -87
  409. package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
  410. package/apps/studio/test/locator.test.ts +0 -214
  411. package/apps/studio/test/marquee-overlay.test.ts +0 -94
  412. package/apps/studio/test/media-commit-chain.test.ts +0 -210
  413. package/apps/studio/test/mode-toggle.test.tsx +0 -226
  414. package/apps/studio/test/motion-sample.test.ts +0 -35
  415. package/apps/studio/test/move-artboards-command.test.ts +0 -108
  416. package/apps/studio/test/participants-chrome.test.ts +0 -65
  417. package/apps/studio/test/paths.test.ts +0 -88
  418. package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
  419. package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
  420. package/apps/studio/test/perf-harness.ts +0 -193
  421. package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
  422. package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
  423. package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
  424. package/apps/studio/test/photo-edit-api.test.ts +0 -201
  425. package/apps/studio/test/photo-filters.test.ts +0 -149
  426. package/apps/studio/test/photo-pipeline.test.ts +0 -106
  427. package/apps/studio/test/photo-taxonomy.test.ts +0 -96
  428. package/apps/studio/test/print-marks.test.ts +0 -113
  429. package/apps/studio/test/print-units.test.ts +0 -173
  430. package/apps/studio/test/read-annotations.test.ts +0 -727
  431. package/apps/studio/test/read-only-gate.test.ts +0 -188
  432. package/apps/studio/test/read-only-role.test.ts +0 -95
  433. package/apps/studio/test/readiness.test.ts +0 -337
  434. package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
  435. package/apps/studio/test/reorder-api.test.ts +0 -210
  436. package/apps/studio/test/repo-concurrency.test.ts +0 -146
  437. package/apps/studio/test/repo-lock.test.ts +0 -350
  438. package/apps/studio/test/report-proxy.test.ts +0 -125
  439. package/apps/studio/test/ripple.test.ts +0 -197
  440. package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
  441. package/apps/studio/test/runtime-bundle.test.ts +0 -69
  442. package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
  443. package/apps/studio/test/scaffold-design.test.ts +0 -122
  444. package/apps/studio/test/server-lifecycle.test.ts +0 -38
  445. package/apps/studio/test/session-runtime-state.test.ts +0 -226
  446. package/apps/studio/test/session-scope.test.ts +0 -100
  447. package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
  448. package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
  449. package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
  450. package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
  451. package/apps/studio/test/shared-doc-projection.test.ts +0 -238
  452. package/apps/studio/test/shell-importmap.test.ts +0 -56
  453. package/apps/studio/test/sizing-mode.test.ts +0 -102
  454. package/apps/studio/test/slash-commands.test.ts +0 -117
  455. package/apps/studio/test/snap-distance-pill.test.ts +0 -68
  456. package/apps/studio/test/spacing-handles.test.ts +0 -138
  457. package/apps/studio/test/specimen-select.test.ts +0 -88
  458. package/apps/studio/test/sync-agent.test.ts +0 -660
  459. package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
  460. package/apps/studio/test/sync-asset-push.test.ts +0 -931
  461. package/apps/studio/test/sync-atomic-write.test.ts +0 -62
  462. package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
  463. package/apps/studio/test/sync-autocommit.test.ts +0 -537
  464. package/apps/studio/test/sync-canvas-path.test.ts +0 -200
  465. package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
  466. package/apps/studio/test/sync-codec.test.ts +0 -230
  467. package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
  468. package/apps/studio/test/sync-cold-start.test.ts +0 -304
  469. package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
  470. package/apps/studio/test/sync-connection-state.test.ts +0 -337
  471. package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
  472. package/apps/studio/test/sync-decide-file.test.ts +0 -420
  473. package/apps/studio/test/sync-doc-name.test.ts +0 -281
  474. package/apps/studio/test/sync-echo-guard.test.ts +0 -96
  475. package/apps/studio/test/sync-file-ledger.test.ts +0 -334
  476. package/apps/studio/test/sync-file-membership.test.ts +0 -347
  477. package/apps/studio/test/sync-file-plane.test.ts +0 -976
  478. package/apps/studio/test/sync-file-pull.test.ts +0 -333
  479. package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
  480. package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
  481. package/apps/studio/test/sync-hardening.test.ts +0 -586
  482. package/apps/studio/test/sync-hub-listing.test.ts +0 -46
  483. package/apps/studio/test/sync-hubs-config.test.ts +0 -133
  484. package/apps/studio/test/sync-incident-replay.test.ts +0 -211
  485. package/apps/studio/test/sync-journal.test.ts +0 -176
  486. package/apps/studio/test/sync-meta-codec.test.ts +0 -199
  487. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
  488. package/apps/studio/test/sync-move-retirement.test.ts +0 -231
  489. package/apps/studio/test/sync-panel-surface.test.ts +0 -153
  490. package/apps/studio/test/sync-path-pull.test.ts +0 -531
  491. package/apps/studio/test/sync-presentation.test.ts +0 -262
  492. package/apps/studio/test/sync-pull-budget.test.ts +0 -169
  493. package/apps/studio/test/sync-remote-docs.test.ts +0 -333
  494. package/apps/studio/test/sync-resync-routes.test.ts +0 -168
  495. package/apps/studio/test/sync-runtime.test.ts +0 -1675
  496. package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
  497. package/apps/studio/test/sync-settings-routes.test.ts +0 -195
  498. package/apps/studio/test/sync-settings.test.ts +0 -151
  499. package/apps/studio/test/sync-status.test.ts +0 -261
  500. package/apps/studio/test/sync-supervisor.test.ts +0 -262
  501. package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
  502. package/apps/studio/test/sync-trash.test.ts +0 -132
  503. package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
  504. package/apps/studio/test/sync-untrusted.test.ts +0 -103
  505. package/apps/studio/test/system-endpoint.test.ts +0 -144
  506. package/apps/studio/test/text-editability-stamp.test.ts +0 -131
  507. package/apps/studio/test/timeline-comp-target.test.ts +0 -139
  508. package/apps/studio/test/timeline-media-cache.test.ts +0 -41
  509. package/apps/studio/test/timeline-parse.test.ts +0 -184
  510. package/apps/studio/test/timeline-scale.test.ts +0 -65
  511. package/apps/studio/test/timeline-snap.test.ts +0 -85
  512. package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
  513. package/apps/studio/test/tool-palette.test.tsx +0 -55
  514. package/apps/studio/test/tour-overlay.test.tsx +0 -90
  515. package/apps/studio/test/trusted-request-host.test.ts +0 -66
  516. package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
  517. package/apps/studio/test/undo-stack.test.ts +0 -244
  518. package/apps/studio/test/use-agent-presence.test.tsx +0 -117
  519. package/apps/studio/test/use-annotation-resize.test.ts +0 -402
  520. package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
  521. package/apps/studio/test/use-artboard-drag.test.ts +0 -324
  522. package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
  523. package/apps/studio/test/use-collab.test.ts +0 -0
  524. package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
  525. package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
  526. package/apps/studio/test/use-selection-set.test.tsx +0 -166
  527. package/apps/studio/test/use-snap-guides.test.ts +0 -271
  528. package/apps/studio/test/use-tool-mode.test.tsx +0 -186
  529. package/apps/studio/test/use-undo-stack.test.tsx +0 -223
  530. package/apps/studio/test/video-asset.test.ts +0 -163
  531. package/apps/studio/test/video-comp-fixture.test.ts +0 -50
  532. package/apps/studio/test/video-comp.test.ts +0 -270
  533. package/apps/studio/test/video-encode-lib.test.ts +0 -63
  534. package/apps/studio/test/video-render-bridge.test.ts +0 -171
  535. package/apps/studio/test/whats-new.test.ts +0 -113
  536. package/apps/studio/test/whisper-setup.test.ts +0 -97
  537. package/apps/studio/test/workspace-containment.test.ts +0 -382
  538. package/apps/studio/test/workspace-signin.test.ts +0 -292
  539. package/apps/studio/test/ws-handshake.test.ts +0 -33
  540. package/cli/commands/cache.test.mjs +0 -166
  541. package/cli/commands/design-link.test.mjs +0 -419
  542. package/cli/commands/design.test.mjs +0 -240
  543. package/cli/commands/doctor.test.mjs +0 -237
  544. package/cli/commands/hub-workspace.test.mjs +0 -275
  545. package/cli/commands/hub.test.mjs +0 -225
  546. package/cli/commands/kg.test.mjs +0 -142
  547. package/cli/commands/preflight.test.mjs +0 -46
  548. package/cli/lib/argv.test.mjs +0 -46
  549. package/cli/lib/cache.test.mjs +0 -303
  550. package/cli/lib/cell-plan.test.mjs +0 -225
  551. package/cli/lib/config-lint.test.mjs +0 -117
  552. package/cli/lib/ddr-to-kgai.test.mjs +0 -118
  553. package/cli/lib/design-ownership.test.mjs +0 -329
  554. package/cli/lib/fetch-asset.test.mjs +0 -213
  555. package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
  556. package/cli/lib/figma-import-controls.test.mjs +0 -70
  557. package/cli/lib/flow-design-integration.test.mjs +0 -165
  558. package/cli/lib/gitignore-block.test.mjs +0 -136
  559. package/cli/lib/gitignore-drift.test.mjs +0 -156
  560. package/cli/lib/hubs-config.test.mjs +0 -165
  561. package/cli/lib/pkg-root.test.mjs +0 -111
  562. package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
  563. package/cli/lib/plugin-name-namespace.test.mjs +0 -71
  564. package/cli/lib/reconstruct-toolset.test.mjs +0 -194
  565. package/cli/lib/stack-detect.test.mjs +0 -121
  566. package/cli/lib/update-check.test.mjs +0 -32
  567. package/cli/lib/workspace-plan.test.mjs +0 -699
@@ -1,3 +1,4 @@
1
+ import { createAnnotationEchoGuard, observeAnnotationSnapshots } from './annotations-sync.ts';
1
2
  /**
2
3
  * @file annotations-layer.tsx — FigJam-style annotation overlay
3
4
  * @scope apps/studio/annotations-layer.tsx
@@ -76,6 +77,7 @@ import {
76
77
  type EllipseStroke,
77
78
  FILL_PALETTE,
78
79
  fmtEqual,
80
+ grownStickyBox,
79
81
  HALO_PAD_PX,
80
82
  HIGHLIGHTER_PALETTE,
81
83
  HIGHLIGHTER_WIDTHS,
@@ -169,7 +171,7 @@ import { buildAnnotationStrokesRecord } from './commands/annotation-strokes-comm
169
171
  import { ensureMenuStyles as ensureCtxMenuStyles } from './context-menu.tsx';
170
172
  import { crossedDragThreshold, type Tool } from './input-router.tsx';
171
173
  import { createMediaCommitChain, type MediaCommitResult } from './media-commit-chain.ts';
172
- import { mountCaret, placeCaretAt } from './text-caret.ts';
174
+ import { collapseEntrySelectAll, mountCaret, placeCaretAt } from './text-caret.ts';
173
175
  import {
174
176
  AnnotationResizeOverlay,
175
177
  bboxResize,
@@ -414,7 +416,7 @@ function resolveAssetHref(href: string): string {
414
416
  * recognize — keeping the `<image>` element, dropping only the attribute —
415
417
  * so the server's STORED + broadcast SVG silently diverges from whatever the
416
418
  * client just sent. That divergence defeats the collab-echo self-suppression
417
- * guard (the `recentSelfSvgsRef` history, near `putStrokes` below): the
419
+ * guard (the `annotationEchoRef` operation history, near `putStrokes` below): the
418
420
  * echo's content no longer matches anything we recorded as "already
419
421
  * applied", so a real `setStrokesState` fires from the (href-stripped)
420
422
  * server copy — which can wipe out a SIBLING stroke's still-in-flight
@@ -467,6 +469,17 @@ export function reconcileCommit(
467
469
  * ones that are still purely local, not-yet-synced optimistic previews (an
468
470
  * ephemeral blob:/data: href image mid-upload) that a foreign broadcast could
469
471
  * never have known about in the first place.
472
+ *
473
+ * issue-106 deliberately does NOT carve out the stroke a local editor has open.
474
+ * That was tried: hold back its `text` so a peer's commit could not overwrite
475
+ * the editor. But this function's output IS the persisted store — it is
476
+ * serialized to `*.annotations.svg` and re-broadcast on the next commit of ANY
477
+ * stroke — so holding a field back doesn't shield a view, it silently reverts
478
+ * the peer's write and republishes the stale text under the local user's name,
479
+ * for as long as the editor stays open. The editor never needed it: the clobber
480
+ * was React rewriting an UNCONTROLLED contentEditable, which `useSessionValue`
481
+ * fixes at the DOM, and the commit reads `innerText` off the node rather than
482
+ * off the stroke. Last-write-wins stays the model here.
470
483
  */
471
484
  export function reconcileForeignEcho(
472
485
  prev: readonly Stroke[],
@@ -1101,7 +1114,7 @@ export function AnnotationsLayer() {
1101
1114
  // the resurrection to every collab peer). Tracked explicitly here instead
1102
1115
  // of inferred: `deleteStrokes` records every id it removes; the swap
1103
1116
  // checks (and consumes) this before assuming absence means lag. Bounded
1104
- // like `recentSelfSvgsRef` below — only ever holds ids a delete has
1117
+ // like the annotation echo history below — only ever holds ids a delete has
1105
1118
  // touched, which is small in practice, but capped for safety.
1106
1119
  const DELETED_STROKE_IDS_CAP = 128;
1107
1120
  const deletedStrokeIdsRef = useRef<Set<string>>(new Set());
@@ -1140,29 +1153,10 @@ export function AnnotationsLayer() {
1140
1153
  if (!ghostCapable || !visible) setGhost(null);
1141
1154
  }, [ghostCapable, visible]);
1142
1155
 
1143
- // Load existing annotations on mount.
1144
- // Self-echo suppression (Phase 8 Task 5, hardened — feature-bulk-media-
1145
- // insert follow-up). A single "last applied" string only catches the MOST
1146
- // RECENT self-write: when the chain fires several rapid commits (a batch
1147
- // drop), the server's broadcast of an EARLIER commit can arrive after a
1148
- // LATER local commit has already moved "last applied" on — so the earlier
1149
- // echo no longer matches, gets misread as a foreign change, and rolls
1150
- // local state BACK to that stale snapshot. This is the confirmed cause of
1151
- // images silently vanishing after a multi-file drop (live-tested: 12
1152
- // mismatched/misapplied echoes correlated exactly with lost strokes across
1153
- // a 30-batch stress run). Track a bounded HISTORY of our own recent writes
1154
- // instead of just the latest one, so an out-of-order echo of any recent
1155
- // self-write is still recognized and suppressed.
1156
- const RECENT_SELF_SVG_CAP = 64;
1157
- const recentSelfSvgsRef = useRef<Set<string>>(new Set());
1158
- const rememberSelfSvg = useCallback((svg: string) => {
1159
- const set = recentSelfSvgsRef.current;
1160
- set.add(svg);
1161
- if (set.size > RECENT_SELF_SVG_CAP) {
1162
- const oldest = set.values().next().value;
1163
- if (oldest !== undefined) set.delete(oldest);
1164
- }
1165
- }, []);
1156
+ // Match specific authored operations, including delayed earlier PUTs. A
1157
+ // peer undo/delete can legitimately return to any previously rendered SVG.
1158
+ const annotationEchoRef = useRef(createAnnotationEchoGuard());
1159
+ const annotationsChangedRef = useRef(false);
1166
1160
  useEffect(() => {
1167
1161
  const file = deriveFile();
1168
1162
  fileRef.current = file;
@@ -1173,11 +1167,10 @@ export function AnnotationsLayer() {
1173
1167
  })
1174
1168
  .then((r) => (r.ok ? r.text() : ''))
1175
1169
  .then((text) => {
1176
- if (cancelled) return;
1170
+ if (cancelled || annotationsChangedRef.current) return;
1177
1171
  const loaded = svgToStrokes(text);
1178
1172
  if (loaded.length) {
1179
1173
  setStrokesState(loaded);
1180
- rememberSelfSvg(text);
1181
1174
  }
1182
1175
  })
1183
1176
  .catch(() => {
@@ -1186,37 +1179,18 @@ export function AnnotationsLayer() {
1186
1179
  return () => {
1187
1180
  cancelled = true;
1188
1181
  };
1189
- }, [rememberSelfSvg]);
1190
-
1191
- // Phase 8 Task 5 — observe the Y.Map.annotations for live updates from
1192
- // other tabs. Bail when the incoming SVG STRING matches any RECENT
1193
- // self-write (covers the local echo round-trip, including an
1194
- // out-of-order one, without missing real foreign changes). The prior
1195
- // length+first/last-id check was wrong: a resize / move keeps the same id
1196
- // list, so all three predicates matched even though geometry changed —
1197
- // foreign edits silently disappeared.
1182
+ }, []);
1183
+
1198
1184
  const collab = useCollab();
1199
1185
  useEffect(() => {
1200
1186
  if (!collab) return;
1201
- const map = collab.doc.getMap<string>('annotations');
1202
- const apply = () => {
1203
- const svg = map.get('svg');
1204
- if (typeof svg !== 'string' || !svg) return;
1205
- if (recentSelfSvgsRef.current.has(svg)) return;
1206
- rememberSelfSvg(svg);
1187
+ return observeAnnotationSnapshots(collab.doc, (svg, writeId) => {
1188
+ annotationsChangedRef.current = true;
1189
+ if (annotationEchoRef.current.isOwn(svg, writeId)) return;
1207
1190
  const incoming = svgToStrokes(svg);
1208
1191
  setStrokesState((prev) => reconcileForeignEcho(prev, incoming));
1209
- };
1210
- apply();
1211
- map.observe(apply);
1212
- return () => {
1213
- try {
1214
- map.unobserve(apply);
1215
- } catch {
1216
- /* doc destroyed before unmount */
1217
- }
1218
- };
1219
- }, [collab, rememberSelfSvg]);
1192
+ });
1193
+ }, [collab]);
1220
1194
 
1221
1195
  const undoStack = useUndoStackOptional();
1222
1196
  const undoSinks = useUndoSinks();
@@ -1252,50 +1226,55 @@ export function AnnotationsLayer() {
1252
1226
  * we push a command, so the server only sees one PUT per edit instead
1253
1227
  * of two-step racing.
1254
1228
  */
1255
- const putStrokes = useCallback(
1256
- (next: readonly Stroke[], before: readonly Stroke[]) => {
1257
- // See reconcileCommit — a direct setStrokesState(next) here can
1258
- // clobber a sibling file's concurrent optimistic insert; folding
1259
- // blindly against `prev` (no baseline) can just as easily revert this
1260
- // very mutation's own delete. `before` (this command's own baseline)
1261
- // disambiguates the two.
1262
- setStrokesState((prev) => reconcileCommit(prev, before, next));
1263
- const file = fileRef.current;
1264
- if (!file) return Promise.resolve();
1265
- const persistable = next.some(isEphemeralHref)
1266
- ? next.filter((s) => !isEphemeralHref(s))
1267
- : next;
1268
- const svg = strokesToSvg(persistable);
1269
- // Phase 8 Task 5 — record the SVG we just authored locally so the
1270
- // server-broadcast echo (PUT → onAnnotationsChanged → syncRoom* →
1271
- // Y.Map.observe) doesn't trigger a redundant setStrokesState.
1272
- rememberSelfSvg(svg);
1273
- const dispatch = () =>
1274
- fetch('/_api/annotations', {
1275
- method: 'PUT',
1276
- headers: { 'Content-Type': 'application/json' },
1277
- body: JSON.stringify({ file, svg }),
1229
+ const putStrokes = useCallback((next: readonly Stroke[], before: readonly Stroke[]) => {
1230
+ // See reconcileCommit — a direct setStrokesState(next) here can
1231
+ // clobber a sibling file's concurrent optimistic insert; folding
1232
+ // blindly against `prev` (no baseline) can just as easily revert this
1233
+ // very mutation's own delete. `before` (this command's own baseline)
1234
+ // disambiguates the two.
1235
+ annotationsChangedRef.current = true;
1236
+ setStrokesState((prev) => reconcileCommit(prev, before, next));
1237
+ const file = fileRef.current;
1238
+ if (!file) return Promise.resolve();
1239
+ const persistable = next.some(isEphemeralHref) ? next.filter((s) => !isEphemeralHref(s)) : next;
1240
+ const svg = strokesToSvg(persistable);
1241
+ // What this edit was derived from — the project merges a peer's
1242
+ // concurrent strokes from it instead of replacing them (DDR-241).
1243
+ const persistableBefore = before.some(isEphemeralHref)
1244
+ ? before.filter((s) => !isEphemeralHref(s))
1245
+ : before;
1246
+ const base = strokesToSvg(persistableBefore);
1247
+ // Phase 8 Task 5 — record the SVG we just authored locally so the
1248
+ // server-broadcast echo (PUT → onAnnotationsChanged → syncRoom* →
1249
+ // Y.Map.observe) doesn't trigger a redundant setStrokesState.
1250
+ const writeId = crypto.randomUUID();
1251
+ annotationEchoRef.current.remember(writeId, svg);
1252
+ const dispatch = () =>
1253
+ fetch('/_api/annotations', {
1254
+ method: 'PUT',
1255
+ headers: { 'Content-Type': 'application/json' },
1256
+ body: JSON.stringify({ file, svg, writeId, base }),
1257
+ })
1258
+ .then((r) => {
1259
+ // A refused save (403 read-only, 405 at a proxy door) previously
1260
+ // dissolved here without a trace — the user kept drawing on state
1261
+ // that never reached disk, a peer, or a reload (the cloud
1262
+ // canvas-writes RCA). Optimistic local state is still the right
1263
+ // UX; a persistence failure being INVISIBLE is not.
1264
+ if (!r.ok) {
1265
+ annotationEchoRef.current.forget(writeId);
1266
+ console.warn(`[annotations] save refused (${r.status}) — strokes are local-only`);
1267
+ }
1268
+ return undefined;
1278
1269
  })
1279
- .then((r) => {
1280
- // A refused save (403 read-only, 405 at a proxy door) previously
1281
- // dissolved here without a trace — the user kept drawing on state
1282
- // that never reached disk, a peer, or a reload (the cloud
1283
- // canvas-writes RCA). Optimistic local state is still the right
1284
- // UX; a persistence failure being INVISIBLE is not.
1285
- if (!r.ok) {
1286
- console.warn(`[annotations] save refused (${r.status}) — strokes are local-only`);
1287
- }
1288
- return undefined;
1289
- })
1290
- .catch(() => {
1291
- /* swallow — user sees uncommitted state until the next stroke */
1292
- });
1293
- const chained = putChainRef.current.then(dispatch, dispatch);
1294
- putChainRef.current = chained;
1295
- return chained;
1296
- },
1297
- [rememberSelfSvg]
1298
- );
1270
+ .catch(() => {
1271
+ annotationEchoRef.current.forget(writeId);
1272
+ /* Pending persistence UX is handled by the project outbox work. */
1273
+ });
1274
+ const chained = putChainRef.current.then(dispatch, dispatch);
1275
+ putChainRef.current = chained;
1276
+ return chained;
1277
+ }, []);
1299
1278
 
1300
1279
  // Register the strokes put sink with the undo provider so the rebuilt
1301
1280
  // AnnotationStrokesCommand (after a canvas switch + return) routes through
@@ -1745,14 +1724,14 @@ export function AnnotationsLayer() {
1745
1724
  // chain-driven setStrokesState queued in the same batch.
1746
1725
  setStrokesState((prev) => prev.filter((s) => s.id !== id));
1747
1726
  deletedStrokeIdsRef.current.delete(id); // never reached the chain — nothing to consume there
1748
- showCanvasToast(`Image upload failed — ${res.error}`);
1727
+ showCanvasToast(`Image upload failed — ${res.error}`, 'error');
1749
1728
  }
1750
1729
  URL.revokeObjectURL(blobUrl);
1751
1730
  });
1752
1731
  };
1753
1732
  probe.onerror = () => {
1754
1733
  URL.revokeObjectURL(blobUrl);
1755
- showCanvasToast('Could not read that image file');
1734
+ showCanvasToast('Could not read that image file', 'error');
1756
1735
  };
1757
1736
  probe.src = blobUrl;
1758
1737
  },
@@ -1797,7 +1776,7 @@ export function AnnotationsLayer() {
1797
1776
  const h = mediaKind === 'video' ? MEDIAREF_VIDEO_H : MEDIAREF_DEFAULT_H;
1798
1777
  void uploadAsset(file).then((res) => {
1799
1778
  if (!('path' in res)) {
1800
- showCanvasToast(`Couldn't add ${mediaKind}: ${res.error}`);
1779
+ showCanvasToast(`Couldn't add ${mediaKind}: ${res.error}`, 'error');
1801
1780
  return;
1802
1781
  }
1803
1782
  const id = rid();
@@ -2884,13 +2863,24 @@ export function AnnotationsLayer() {
2884
2863
  // Phase 21 — sticky body edit. Sticky text is freeform (newlines preserved,
2885
2864
  // no trim) and the card persists even when blank, so this only updates text.
2886
2865
  const commitStickyText = useCallback(
2887
- (id: string, text: string, fmt?: EditorFmt) => {
2866
+ (id: string, text: string, fmt?: EditorFmt, measuredH?: number) => {
2888
2867
  const prev = strokesRef.current;
2889
2868
  const existing = prev.find((s) => s.id === id && s.tool === 'sticky') as
2890
2869
  | StickyStroke
2891
2870
  | undefined;
2892
- if (!existing || (existing.text === text && fmtEqual(existing, fmt))) return;
2893
- const next = prev.map((s) => (s.id === id ? { ...existing, text, ...normFmt(fmt) } : s));
2871
+ if (!existing) return;
2872
+ // issue-106 — grow the card to whatever the editor actually rendered, so
2873
+ // the lines the user just typed survive the return to the clipped read
2874
+ // view. Null when the text already fits (the common case).
2875
+ // The growth rides along with a real edit — it is NOT itself a reason to
2876
+ // write. Otherwise opening a peer's already-overflowing note and clicking
2877
+ // away, having typed nothing, would produce an undo entry, a
2878
+ // full-document PUT and a sync broadcast.
2879
+ if (existing.text === text && fmtEqual(existing, fmt)) return;
2880
+ const grown = measuredH == null ? null : grownStickyBox(existing, measuredH);
2881
+ const next = prev.map((s) =>
2882
+ s.id === id ? { ...existing, text, ...normFmt(fmt), ...(grown ?? {}) } : s
2883
+ );
2894
2884
  commitStrokes(prev, next, 'edit sticky');
2895
2885
  },
2896
2886
  [commitStrokes]
@@ -2970,14 +2960,14 @@ export function AnnotationsLayer() {
2970
2960
  editingTargetRef.current = editingTarget;
2971
2961
 
2972
2962
  const commitEditing = useCallback(
2973
- (text: string, fmt?: EditorFmt) => {
2963
+ (text: string, fmt?: EditorFmt, measuredH?: number) => {
2974
2964
  const target = editingTargetRef.current;
2975
2965
  setEditingId(null);
2976
2966
  setPendingText(null);
2977
2967
  setEditCaretPoint(null);
2978
2968
  if (!target) return;
2979
2969
  if (target.kind === 'anchored') commitText(target.anchorId, text, fmt);
2980
- else if (target.kind === 'sticky') commitStickyText(target.sticky.id, text, fmt);
2970
+ else if (target.kind === 'sticky') commitStickyText(target.sticky.id, text, fmt, measuredH);
2981
2971
  else if (target.kind === 'standalone') commitStandaloneText(target.text.id, text, fmt);
2982
2972
  else if (target.kind === 'section') {
2983
2973
  const label = text.trim().replace(/\s*\n+\s*/g, ' ') || 'Section';
@@ -3902,7 +3892,7 @@ function AnnotationsSvg({
3902
3892
  editCaretPoint: { x: number; y: number } | null;
3903
3893
  /** Live default ink (theme-aware) for a not-yet-born pending text caret. */
3904
3894
  inkColor: string;
3905
- onCommitEdit: (text: string, fmt?: EditorFmt) => void;
3895
+ onCommitEdit: (text: string, fmt?: EditorFmt, measuredH?: number) => void;
3906
3896
  onCancelEdit: () => void;
3907
3897
  }) {
3908
3898
  const [, force] = useState({});
@@ -4118,7 +4108,7 @@ function AnnotEditors({
4118
4108
  anchoredExisting: TextStroke | undefined;
4119
4109
  caretPoint: { x: number; y: number } | null;
4120
4110
  inkColor: string;
4121
- onCommitEdit: (text: string, fmt?: EditorFmt) => void;
4111
+ onCommitEdit: (text: string, fmt?: EditorFmt, measuredH?: number) => void;
4122
4112
  onCancelEdit: () => void;
4123
4113
  }) {
4124
4114
  const target = worldRef?.current ?? null;
@@ -4346,6 +4336,33 @@ function useEditorCaret(
4346
4336
  return { onPointerDown, onPointerUp };
4347
4337
  }
4348
4338
 
4339
+ /**
4340
+ * issue-106 — the body an annotation editor hands React as its contentEditable
4341
+ * children, snapshotted for the life of ONE edit session.
4342
+ *
4343
+ * These editors are uncontrolled: the user types straight into the DOM. If the
4344
+ * rendered children value changes while the session is open, React writes the
4345
+ * new string into the live node and everything typed since — line breaks
4346
+ * included — is gone. That is exactly what a peer's commit arriving over
4347
+ * shared-doc sync (DDR-064) used to do, and what the hazard note above
4348
+ * `useEditorFormat` warns about for local mutations. Snapshotting per target id
4349
+ * makes the children a constant for the session, so no store update can reach
4350
+ * the DOM; the commit path reads `innerText` from the node itself, which is the
4351
+ * user's real content either way. Re-keys when the editor is reused for a
4352
+ * different stroke.
4353
+ *
4354
+ * `listType` is snapshotted through the same hook: the body is prefixed with
4355
+ * list markers at OPEN time and stripped again at commit, so if a peer flipped
4356
+ * the stroke's list style mid-session the strip would run a different rule than
4357
+ * the prefix did — turning a bullet into a literal "• " in the stored text, or
4358
+ * eating a leading "1. " the user actually typed.
4359
+ */
4360
+ function useSessionValue<T>(id: string, compute: () => T): T {
4361
+ const ref = useRef<{ id: string; value: T } | null>(null);
4362
+ if (ref.current === null || ref.current.id !== id) ref.current = { id, value: compute() };
4363
+ return ref.current.value;
4364
+ }
4365
+
4349
4366
  function TextEditor({
4350
4367
  anchorId,
4351
4368
  host,
@@ -4364,9 +4381,10 @@ function TextEditor({
4364
4381
  const ref = useRef<HTMLDivElement | null>(null);
4365
4382
  // Show list markers WHILE editing so the read↔edit swap doesn't flicker
4366
4383
  // (item 4c) — stripped back to raw text on commit.
4367
- const initial = listPrefixedBody(existing?.text ?? '', existing?.listType);
4368
- const initialRef = useRef(initial);
4369
- initialRef.current = initial;
4384
+ const sessionListType = useSessionValue(anchorId, () => existing?.listType);
4385
+ const initial = useSessionValue(anchorId, () =>
4386
+ listPrefixedBody(existing?.text ?? '', sessionListType)
4387
+ );
4370
4388
  // Cmd/Ctrl+B/I/U formatting while editing (item 4d).
4371
4389
  const {
4372
4390
  fmtRef,
@@ -4385,8 +4403,8 @@ function TextEditor({
4385
4403
  // effect below can depend on it directly instead of its own copy of
4386
4404
  // existing?.listType (lint/correctness/useExhaustiveDependencies).
4387
4405
  const toCommittedText = useCallback(
4388
- (raw: string) => stripEditorMarkers(stripJumpSentinel(raw), existing?.listType),
4389
- [existing?.listType]
4406
+ (raw: string) => stripEditorMarkers(stripJumpSentinel(raw), sessionListType),
4407
+ [sessionListType]
4390
4408
  );
4391
4409
 
4392
4410
  // Caret-at-click on entry + custom blinking caret + click re-placement
@@ -4471,6 +4489,14 @@ function TextEditor({
4471
4489
  onCancel();
4472
4490
  return;
4473
4491
  }
4492
+ // issue-106 — Shift+Enter ADDS a line. On keyboard entry the whole
4493
+ // body is still select-all'd (the retype convention), so letting the
4494
+ // break replace the selection wiped the text. Collapse that entry
4495
+ // selection to its end first; a user's own partial selection is left
4496
+ // alone. See collapseEntrySelectAll.
4497
+ if (e.key === 'Enter' && e.shiftKey && ref.current) {
4498
+ collapseEntrySelectAll(ref.current, window);
4499
+ }
4474
4500
  // Unified across every text surface: plain Enter commits,
4475
4501
  // Shift+Enter inserts a newline (falls through untouched). ⌘/Ctrl
4476
4502
  // +Enter also commits AND chains a connected sibling (quick-create).
@@ -4504,7 +4530,7 @@ function StickyEditor({
4504
4530
  }: {
4505
4531
  sticky: StickyStroke;
4506
4532
  caretPoint?: { x: number; y: number } | null;
4507
- onCommit: (text: string, fmt?: EditorFmt) => void;
4533
+ onCommit: (text: string, fmt?: EditorFmt, measuredH?: number) => void;
4508
4534
  onCancel: () => void;
4509
4535
  }) {
4510
4536
  // A flex-centered contentEditable (NOT a textarea) so the edit view matches
@@ -4513,6 +4539,8 @@ function StickyEditor({
4513
4539
  // Esc cancels; blur commits; Cmd/Ctrl+B/I/U format (unified with the others).
4514
4540
  const ref = useRef<HTMLDivElement | null>(null);
4515
4541
  const doneRef = useRef(false);
4542
+ const sessionListType = useSessionValue(sticky.id, () => sticky.listType);
4543
+ const sessionBody = useSessionValue(sticky.id, () => stickyBodyText(sticky));
4516
4544
  const {
4517
4545
  fmtRef,
4518
4546
  style: fmtStyle,
@@ -4528,7 +4556,16 @@ function StickyEditor({
4528
4556
  const commit = () => {
4529
4557
  if (doneRef.current) return;
4530
4558
  doneRef.current = true;
4531
- onCommit(stripEditorMarkers(ref.current?.innerText ?? '', sticky.listType), fmtRef.current);
4559
+ const el = ref.current;
4560
+ // issue-106 — the editor IS the measurement: same class, same width, same
4561
+ // font as the committed body, and laid out in world units (the pan/zoom
4562
+ // transform on `.dc-world` doesn't change layout), so its scrollHeight is
4563
+ // the height the card needs. No offscreen measuring rig required.
4564
+ onCommit(
4565
+ stripEditorMarkers(el?.innerText ?? '', sessionListType),
4566
+ fmtRef.current,
4567
+ el?.scrollHeight
4568
+ );
4532
4569
  };
4533
4570
  // FigJam v3 — a toolbar click steals focus for a tick; don't treat it as
4534
4571
  // "done editing" (the button's onMouseDown preventDefault usually stops the
@@ -4544,10 +4581,33 @@ function StickyEditor({
4544
4581
  const y = Math.min(sticky.y, sticky.y + sticky.h);
4545
4582
  const w = Math.abs(sticky.w);
4546
4583
  const h = Math.abs(sticky.h);
4584
+ const r = sticky.cornerRadius ?? STICKY_CORNER_RADIUS;
4547
4585
  return (
4548
4586
  <div
4549
4587
  data-annot-editor="1"
4550
- style={{ position: 'absolute', left: x, top: y, width: w, height: h, zIndex: 5 }}
4588
+ style={{
4589
+ position: 'absolute',
4590
+ left: x,
4591
+ top: y,
4592
+ width: w,
4593
+ // issue-106 — NOT a fixed `height: h`. The card clips at `overflow:
4594
+ // hidden`, so on a sticky that is already full the line Shift+Enter
4595
+ // inserts landed outside the box: the keystroke worked, nothing moved
4596
+ // on screen, and it read as "shift+enter does nothing". While the
4597
+ // editor is open it grows downward instead, painting its own paper so
4598
+ // the overflow still reads as the note (the SVG card behind is still
4599
+ // the old size until the commit below persists the grown height).
4600
+ minHeight: h,
4601
+ zIndex: 5,
4602
+ // `backgroundColor`, never the `background` shorthand: `color` comes
4603
+ // verbatim from a synced stroke's `fill` (peer-controlled, and the
4604
+ // sanitizer does not touch `fill`), and the shorthand would accept a
4605
+ // `url(...)` — an outbound request from the studio origin the moment a
4606
+ // note is opened. `background-color` cannot take one.
4607
+ backgroundColor: sticky.color,
4608
+ // Mirrors stickyCornerPath: TL/TR/BL rounded, BR sharp.
4609
+ borderRadius: `${r}px ${r}px 0 ${r}px`,
4610
+ }}
4551
4611
  >
4552
4612
  <div
4553
4613
  ref={ref}
@@ -4561,6 +4621,11 @@ function StickyEditor({
4561
4621
  outline: 'none',
4562
4622
  cursor: 'text',
4563
4623
  ...CARET_FIX_STYLE,
4624
+ // Beats `.dc-sticky-body { height: 100%; overflow: hidden }` for the
4625
+ // duration of the edit — see the wrapper note above.
4626
+ height: 'auto',
4627
+ minHeight: h,
4628
+ overflow: 'visible',
4564
4629
  }}
4565
4630
  onBlur={onBlur}
4566
4631
  onPointerDown={caretHandlers.onPointerDown}
@@ -4573,6 +4638,11 @@ function StickyEditor({
4573
4638
  onCancel();
4574
4639
  return;
4575
4640
  }
4641
+ // issue-106 — see the same guard in TextEditor: Shift+Enter must add
4642
+ // a line, never consume the entry select-all and delete the body.
4643
+ if (e.key === 'Enter' && e.shiftKey && ref.current) {
4644
+ collapseEntrySelectAll(ref.current, window);
4645
+ }
4576
4646
  // Unified: plain Enter commits, Shift+Enter inserts a newline.
4577
4647
  // ⌘/Ctrl+Enter also commits AND chains the next sticky beside it.
4578
4648
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -4588,7 +4658,7 @@ function StickyEditor({
4588
4658
  >
4589
4659
  {/* Show the list markers while editing (item 4c) so the read↔edit swap
4590
4660
  doesn't flicker; stripped back to raw text on commit. */}
4591
- {stickyBodyText(sticky)}
4661
+ {sessionBody}
4592
4662
  </div>
4593
4663
  </div>
4594
4664
  );
@@ -4736,6 +4806,11 @@ function StandaloneTextEditor({
4736
4806
  onCancel();
4737
4807
  return;
4738
4808
  }
4809
+ // issue-106 — same guard as the other two editors, for the multi-line
4810
+ // case only (a singleLine field commits on Shift+Enter anyway).
4811
+ if (e.key === 'Enter' && e.shiftKey && !singleLine && ref.current) {
4812
+ collapseEntrySelectAll(ref.current, window);
4813
+ }
4739
4814
  // Unified: plain Enter commits, Shift+Enter inserts a newline. A
4740
4815
  // singleLine field (section rename) is a title — Shift+Enter commits
4741
4816
  // too rather than adding a newline the one-line chip can't show.
@@ -542,6 +542,45 @@ export const DEFAULT_HIGHLIGHTER_WIDTH = HIGHLIGHTER_WIDTHS[1];
542
542
  export const STICKY_DEFAULT_W = 200;
543
543
  export const STICKY_DEFAULT_H = 200;
544
544
  export const STICKY_MIN_SIZE = 40;
545
+ /**
546
+ * Ceiling for the grow-to-fit in `grownStickyBox` — ~370 lines at the default
547
+ * type. `fontSize` comes from a synced stroke's `data-fs` and is only checked
548
+ * for finiteness, so without this a peer could set `data-fs="200000"`, wait for
549
+ * the local user to edit that note, and have THEIR client persist a card
550
+ * millions of units tall — grow-only, undoable by hand alone.
551
+ */
552
+ export const STICKY_MAX_GROWN_H = 8000;
553
+
554
+ /**
555
+ * issue-106 — the box a sticky needs so its committed text is not clipped, or
556
+ * null when the text already fits.
557
+ *
558
+ * `.dc-sticky-body` is `overflow: hidden` on a card whose size was fixed at
559
+ * creation, and nothing ever grew it: past a card's capacity every further line
560
+ * was simply invisible, which is what made Shift+Enter look inert on a full
561
+ * note. `measuredH` is the editor's own `scrollHeight` — same class, width and
562
+ * font as the committed body, in world units — so the growth is exact rather
563
+ * than estimated.
564
+ *
565
+ * GROWS ONLY, and never past `STICKY_MAX_GROWN_H`: a sticky the user
566
+ * deliberately made roomy never shrinks back on a text edit. The box is normalized on the way out (a sticky dragged bottom-up
567
+ * carries a negative w/h), which is idempotent — every renderer already reads
568
+ * it through the same min/abs pair.
569
+ */
570
+ export function grownStickyBox(
571
+ s: Pick<StickyStroke, 'x' | 'y' | 'w' | 'h'>,
572
+ measuredH: number
573
+ ): { x: number; y: number; w: number; h: number } | null {
574
+ if (!Number.isFinite(measuredH) || measuredH <= 0) return null;
575
+ const next = Math.min(Math.ceil(measuredH), STICKY_MAX_GROWN_H);
576
+ if (next <= Math.abs(s.h)) return null;
577
+ return {
578
+ x: Math.min(s.x, s.x + s.w),
579
+ y: Math.min(s.y, s.y + s.h),
580
+ w: Math.abs(s.w),
581
+ h: next,
582
+ };
583
+ }
545
584
  // Phase 24 — a bare tap with the Shape tool drops a default-sized shape at the
546
585
  // tap point (FigJam parity: click commits, drag sizes). Square aspect.
547
586
  export const SHAPE_DEFAULT_SIZE = 120;
@@ -0,0 +1,50 @@
1
+ import type * as Y from 'yjs';
2
+
3
+ /** Echo identity is transport metadata, never authorization or a durable ACK. */
4
+ export const ANNOTATION_WRITE_ID = 'writeId';
5
+
6
+ export function validAnnotationWriteId(value: unknown): value is string {
7
+ return typeof value === 'string' && /^[a-zA-Z0-9_-]{1,96}$/.test(value);
8
+ }
9
+
10
+ /** Remember authored operations, not previously rendered content (A → B → A). */
11
+ export function createAnnotationEchoGuard() {
12
+ const authored = new Map<string, string>();
13
+ return {
14
+ remember(id: string, svg: string) {
15
+ authored.set(id, svg);
16
+ if (authored.size > 64) {
17
+ const oldest = authored.keys().next().value;
18
+ if (oldest !== undefined) authored.delete(oldest);
19
+ }
20
+ },
21
+ forget(id: string) {
22
+ authored.delete(id);
23
+ },
24
+ isOwn(svg: string, id: unknown) {
25
+ return validAnnotationWriteId(id) && authored.get(id) === svg;
26
+ },
27
+ };
28
+ }
29
+
30
+ /** Old clients/importers may change svg without changing its old writeId. */
31
+ export function observeAnnotationSnapshots(
32
+ doc: Y.Doc,
33
+ receive: (svg: string, writeId: unknown) => void
34
+ ): () => void {
35
+ const map = doc.getMap<unknown>('annotations');
36
+ const apply = (event?: Y.YMapEvent<unknown>) => {
37
+ if (event && !event.keysChanged.has('svg') && !event.keysChanged.has(ANNOTATION_WRITE_ID))
38
+ return;
39
+ const svg = map.get('svg');
40
+ if (typeof svg !== 'string' && !event?.keysChanged.has('svg')) return;
41
+ const id =
42
+ !event || event.keysChanged.has(ANNOTATION_WRITE_ID)
43
+ ? map.get(ANNOTATION_WRITE_ID)
44
+ : undefined;
45
+ receive(typeof svg === 'string' ? svg : '', id);
46
+ };
47
+ map.observe(apply);
48
+ apply();
49
+ return () => map.unobserve(apply);
50
+ }