@1agh/maude 1.4.5 → 1.5.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 (106) hide show
  1. package/apps/studio/annotations/ai-read.ts +288 -0
  2. package/apps/studio/annotations/ai-write.ts +533 -0
  3. package/apps/studio/annotations/board-io.ts +94 -0
  4. package/apps/studio/annotations/board-text.ts +45 -0
  5. package/apps/studio/annotations/constants.ts +58 -0
  6. package/apps/studio/annotations/elements/_shared.ts +125 -0
  7. package/apps/studio/annotations/elements/arrow.model.ts +196 -0
  8. package/apps/studio/annotations/elements/media.model.ts +79 -0
  9. package/apps/studio/annotations/elements/pen.model.ts +82 -0
  10. package/apps/studio/annotations/elements/section.model.ts +70 -0
  11. package/apps/studio/annotations/elements/shape.model.ts +141 -0
  12. package/apps/studio/annotations/elements/sticky.model.ts +48 -0
  13. package/apps/studio/annotations/elements/text.model.ts +52 -0
  14. package/apps/studio/annotations/fields.ts +347 -0
  15. package/apps/studio/annotations/fractional-index.ts +234 -0
  16. package/apps/studio/annotations/legacy/mini-dom.ts +207 -0
  17. package/apps/studio/annotations/migrate-boot.ts +172 -0
  18. package/apps/studio/annotations/migrate-cli.ts +37 -0
  19. package/apps/studio/annotations/migrate-v1.ts +383 -0
  20. package/apps/studio/annotations/ops.ts +474 -0
  21. package/apps/studio/annotations/registry.ts +159 -0
  22. package/apps/studio/annotations/replica.ts +264 -0
  23. package/apps/studio/annotations/scene.ts +235 -0
  24. package/apps/studio/annotations/schema.ts +188 -0
  25. package/apps/studio/annotations/types.ts +77 -0
  26. package/apps/studio/annotations/ui/board.ts +126 -0
  27. package/apps/studio/annotations/ui/containment.ts +118 -0
  28. package/apps/studio/annotations/ui/edit-actions.ts +551 -0
  29. package/apps/studio/annotations/ui/editor-channel.ts +62 -0
  30. package/apps/studio/annotations/ui/element-node.tsx +993 -0
  31. package/apps/studio/annotations/ui/pipeline-context.ts +17 -0
  32. package/apps/studio/annotations/ui/pointer-pipeline.ts +150 -0
  33. package/apps/studio/annotations/ui/render-model.ts +264 -0
  34. package/apps/studio/annotations/ui/scene.tsx +63 -0
  35. package/apps/studio/annotations/ui/text-editor.tsx +420 -0
  36. package/apps/studio/annotations/ui/text-session.ts +140 -0
  37. package/apps/studio/annotations/ui/text-style.ts +111 -0
  38. package/apps/studio/annotations/ui/world.ts +47 -0
  39. package/apps/studio/annotations/v1-adapter.ts +428 -0
  40. package/apps/studio/annotations-align.ts +21 -6
  41. package/apps/studio/annotations-bindings.ts +2 -0
  42. package/apps/studio/annotations-context-toolbar.tsx +9 -13
  43. package/apps/studio/annotations-groups.ts +3 -0
  44. package/apps/studio/annotations-layer.tsx +923 -1925
  45. package/apps/studio/annotations-model.ts +97 -4
  46. package/apps/studio/annotations-sync.ts +4 -47
  47. package/apps/studio/api.ts +262 -68
  48. package/apps/studio/bin/_import-figma.mjs +22 -12
  49. package/apps/studio/bin/annotate.mjs +331 -838
  50. package/apps/studio/bin/annotate.sh +4 -4
  51. package/apps/studio/bin/perf.sh +21 -7
  52. package/apps/studio/bin/read-annotations.mjs +184 -666
  53. package/apps/studio/bin/read-annotations.sh +9 -5
  54. package/apps/studio/canvas-artifacts.ts +9 -0
  55. package/apps/studio/canvas-comment-mount.tsx +35 -2
  56. package/apps/studio/canvas-lib.tsx +18 -1
  57. package/apps/studio/canvas-shell.tsx +54 -1
  58. package/apps/studio/client/app.jsx +101 -35
  59. package/apps/studio/client/comments-overlay.css +10 -0
  60. package/apps/studio/client/hmr.mjs +1 -1
  61. package/apps/studio/client/panels/git-grouping.js +2 -2
  62. package/apps/studio/client/tree-expansion.js +217 -0
  63. package/apps/studio/collab/index.ts +58 -5
  64. package/apps/studio/collab/persistence.ts +80 -11
  65. package/apps/studio/collab/registry.ts +63 -26
  66. package/apps/studio/commands/annotation-ops-command.ts +72 -0
  67. package/apps/studio/comment-anchor.ts +116 -0
  68. package/apps/studio/comments-overlay.tsx +78 -35
  69. package/apps/studio/context.ts +1 -0
  70. package/apps/studio/cursors-overlay.tsx +337 -109
  71. package/apps/studio/dist/client.bundle.js +850 -850
  72. package/apps/studio/dist/comment-mount.js +2 -2
  73. package/apps/studio/figma/to-strokes.ts +31 -10
  74. package/apps/studio/git/endpoints.ts +1 -1
  75. package/apps/studio/git/service.ts +1 -1
  76. package/apps/studio/git/watch.ts +1 -1
  77. package/apps/studio/http.ts +90 -15
  78. package/apps/studio/server.ts +16 -0
  79. package/apps/studio/sync/accepted-cold-start.ts +72 -8
  80. package/apps/studio/sync/agent.ts +36 -6
  81. package/apps/studio/sync/codec.ts +95 -40
  82. package/apps/studio/sync/comment-ledger.ts +229 -0
  83. package/apps/studio/sync/file-membership.ts +12 -2
  84. package/apps/studio/sync/file-plane.ts +78 -2
  85. package/apps/studio/sync/index.ts +75 -16
  86. package/apps/studio/sync/journal-client.ts +5 -0
  87. package/apps/studio/sync/limits.ts +7 -2
  88. package/apps/studio/sync/migrate-seed.ts +17 -7
  89. package/apps/studio/sync/projection.ts +7 -2
  90. package/apps/studio/sync/remote-docs.ts +34 -0
  91. package/apps/studio/sync/writer-registry.ts +10 -0
  92. package/apps/studio/text-caret.ts +11 -2
  93. package/apps/studio/tree-state.ts +45 -0
  94. package/apps/studio/undo-stack.ts +2 -2
  95. package/apps/studio/use-annotation-resize.tsx +48 -23
  96. package/apps/studio/use-annotation-selection.tsx +9 -2
  97. package/apps/studio/use-collab.tsx +104 -1
  98. package/apps/studio/use-selection-set.tsx +4 -0
  99. package/apps/studio/whats-new.json +63 -0
  100. package/cli/lib/design-link.mjs +5 -1
  101. package/cli/lib/gitignore-block.mjs +1 -1
  102. package/cli/lib/gitignore-drift.mjs +2 -1
  103. package/package.json +9 -8
  104. package/plugins/design/templates/brief-board.tsx.template +1 -1
  105. package/apps/studio/annotation-edit-base.ts +0 -36
  106. package/apps/studio/commands/annotation-strokes-command.ts +0 -137
@@ -25,6 +25,8 @@
25
25
  * - sticky / polygon / image / link — see the per-tool serializers below.
26
26
  */
27
27
 
28
+ import { defOf } from './annotations/registry.ts';
29
+ import type { AnnotationElement, GeomCtx } from './annotations/types.ts';
28
30
  import {
29
31
  ARROW_HEADS,
30
32
  type ArrowHead,
@@ -318,7 +320,21 @@ export interface SectionStroke extends StrokeBase {
318
320
  color: string;
319
321
  }
320
322
 
323
+ /**
324
+ * DDR-242 (Task 25) — an element of a registered type with no stroke form of
325
+ * its own. Its world-space record rides along and every geometry question is
326
+ * answered by its registry definition, so a new element type is one model file:
327
+ * the whiteboard draws, selects, moves, syncs and hands it to the AI verbs
328
+ * without another `tool ===` branch.
329
+ */
330
+ export interface ElementStroke extends StrokeBase {
331
+ tool: 'element';
332
+ /** The element in WORLD coordinates, without `parent`. */
333
+ el: AnnotationElement;
334
+ }
335
+
321
336
  export type Stroke =
337
+ | ElementStroke
322
338
  | PenStroke
323
339
  | RectStroke
324
340
  | EllipseStroke
@@ -654,6 +670,37 @@ export function rid(): string {
654
670
  return `s_${Math.random().toString(36).slice(2, 10)}`;
655
671
  }
656
672
 
673
+ /**
674
+ * The id of an annotation element that was written WITHOUT a `data-id` (a
675
+ * hand-edited or externally generated SVG). It used to be `rid()` — a new
676
+ * random id on every parse — so the same element had a different id in every
677
+ * tab, on every peer and after every reload, and nothing could point at it: a
678
+ * comment anchored to it (#134/#136), a peer's selection halo, an agent's
679
+ * `annotate update`. Now it is derived from the element's own markup, plus its
680
+ * occurrence among identical elements, so every parse of the same SVG gives
681
+ * every element the same id everywhere. The first edit writes it back as a real
682
+ * `data-id` (strokesToSvg always emits one), after which it never changes.
683
+ *
684
+ * Contract for the annotations-v2 element model
685
+ * (.ai/plans/feature-annotations-v2-element-model.md): an annotation's id is
686
+ * stable for the life of the element — across edits, moves, undo/redo, sync
687
+ * and reload — and is what external references (comments' `annotationId`)
688
+ * hold. A migration must carry ids over unchanged.
689
+ */
690
+ export function stableAnnotationId(el: SvgElLike, seen: Map<string, number>): string {
691
+ const content = el.outerHTML;
692
+ const n = seen.get(content) ?? 0;
693
+ seen.set(content, n + 1);
694
+ // FNV-1a, 32-bit — deterministic, dependency-free, plenty for a per-board id.
695
+ let h = 0x811c9dc5;
696
+ const key = `${content}#${n}`;
697
+ for (let i = 0; i < key.length; i++) {
698
+ h ^= key.charCodeAt(i);
699
+ h = Math.imul(h, 0x01000193) >>> 0;
700
+ }
701
+ return `s_h${h.toString(36)}`;
702
+ }
703
+
657
704
  /** FigJam v3 — group ids mirror the stroke id scheme (`g_` prefix). */
658
705
  export function gid(): string {
659
706
  return `g_${Math.random().toString(36).slice(2, 10)}`;
@@ -1016,6 +1063,8 @@ export function strokeToSvgEl(s: Stroke): string {
1016
1063
  }
1017
1064
 
1018
1065
  function strokeToSvgElBase(s: Stroke): string {
1066
+ // No legacy SVG form: such an element only ever lives in a v2 board.
1067
+ if (s.tool === 'element') return '';
1019
1068
  if (s.tool === 'text') {
1020
1069
  // Phase 21 — anchored text keeps the byte-identical Phase 5.1 form;
1021
1070
  // standalone text (no anchorId) writes its own world x/y and omits
@@ -1317,7 +1366,7 @@ function parseFill(raw: string | null): string | null {
1317
1366
  * between them, so first-pair = start, last-pair = end recovers the ends
1318
1367
  * exactly → idempotent re-serialize).
1319
1368
  */
1320
- function arrowEndpoints(el: Element): { x1: number; y1: number; x2: number; y2: number } | null {
1369
+ function arrowEndpoints(el: SvgElLike): { x1: number; y1: number; x2: number; y2: number } | null {
1321
1370
  const line = el.querySelector('line');
1322
1371
  if (line) {
1323
1372
  return {
@@ -1390,7 +1439,7 @@ function sanitizeAuthorName(raw: string): string {
1390
1439
  }
1391
1440
 
1392
1441
  /** FigJam v3 — read the cross-tool root attrs back onto a parsed stroke. */
1393
- function readSharedAttrs(el: Element, s: Stroke): void {
1442
+ function readSharedAttrs(el: SvgElLike, s: Stroke): void {
1394
1443
  const g = el.getAttribute('data-group-ids');
1395
1444
  if (g) {
1396
1445
  const ids = g.split(/\s+/).filter(Boolean);
@@ -1416,17 +1465,45 @@ function readSharedAttrs(el: Element, s: Stroke): void {
1416
1465
  }
1417
1466
  }
1418
1467
 
1468
+ /**
1469
+ * The slice of the DOM `Element` API the parser reads. A browser `Element`
1470
+ * satisfies it; so does the DOM-free tree in `annotations/legacy/mini-dom.ts`,
1471
+ * which lets the hub (Node, no DOMParser) upconvert legacy SVG history for the
1472
+ * annotations-v2 migration (DDR-242 §6).
1473
+ */
1474
+ export interface SvgElLike {
1475
+ getAttribute(name: string): string | null;
1476
+ querySelector(selector: string): SvgElLike | null;
1477
+ querySelectorAll(selector: string): ArrayLike<SvgElLike>;
1478
+ readonly textContent: string | null;
1479
+ readonly outerHTML: string;
1480
+ }
1481
+
1482
+ export interface SvgDocLike {
1483
+ querySelector(selector: string): SvgElLike | null;
1484
+ querySelectorAll(selector: string): ArrayLike<SvgElLike>;
1485
+ }
1486
+
1419
1487
  export function svgToStrokes(svgText: string): Stroke[] {
1420
1488
  const text = (svgText ?? '').trim();
1421
1489
  if (!text) return [];
1422
1490
  if (typeof DOMParser === 'undefined') return [];
1423
1491
  try {
1424
- const doc = new DOMParser().parseFromString(text, 'image/svg+xml');
1492
+ return strokesFromDocument(new DOMParser().parseFromString(text, 'image/svg+xml'));
1493
+ } catch {
1494
+ return [];
1495
+ }
1496
+ }
1497
+
1498
+ /** The parser proper, over any document exposing `SvgDocLike`. Never throws. */
1499
+ export function strokesFromDocument(doc: SvgDocLike): Stroke[] {
1500
+ try {
1425
1501
  if (doc.querySelector('parsererror')) return [];
1426
1502
  const out: Stroke[] = [];
1503
+ const seenContent = new Map<string, number>();
1427
1504
  for (const el of Array.from(doc.querySelectorAll('[data-tool]'))) {
1428
1505
  const tool = el.getAttribute('data-tool');
1429
- const id = el.getAttribute('data-id') || rid();
1506
+ const id = el.getAttribute('data-id') || stableAnnotationId(el, seenContent);
1430
1507
  const color = el.getAttribute('stroke') || el.getAttribute('fill') || DEFAULT_COLOR;
1431
1508
  const width = Number.parseFloat(el.getAttribute('stroke-width') || '2') || 2;
1432
1509
  // FigJam v3 — every branch funnels through push() so the shared attrs
@@ -1741,6 +1818,7 @@ function pointSegmentDist(
1741
1818
 
1742
1819
  /** FigJam v3 — strokes whose `rotation` is honoured (see StrokeBase doc). */
1743
1820
  export function canRotate(s: Stroke): boolean {
1821
+ if (s.tool === 'element') return false;
1744
1822
  if (s.tool === 'pen' || s.tool === 'arrow' || s.tool === 'section') return false;
1745
1823
  if (s.tool === 'text') return s.anchorId == null || s.anchorId === '';
1746
1824
  return true;
@@ -1808,6 +1886,9 @@ export function strokeHitTest(s: Stroke, wx: number, wy: number, tol: number): b
1808
1886
  wx >= bb.x - tol && wx <= bb.x + bb.w + tol && wy >= bb.y - tol && wy <= bb.y + bb.h + tol
1809
1887
  );
1810
1888
  }
1889
+ if (s.tool === 'element') {
1890
+ return defOf(s.el.type)?.hitTest(s.el, wx, wy, tol, WORLD_CTX) ?? false;
1891
+ }
1811
1892
  if (s.tool === 'section') {
1812
1893
  // FigJam — a section is grabbed by its BORDER or its label chip; the
1813
1894
  // interior stays click-through so content on the section selects normally.
@@ -2013,6 +2094,7 @@ export function normalizeSticky(s: StickyStroke): StickyStroke {
2013
2094
  }
2014
2095
 
2015
2096
  export function isStrokeMeaningful(s: Stroke): boolean {
2097
+ if (s.tool === 'element') return defOf(s.el.type)?.meaningful(s.el) ?? false;
2016
2098
  if (s.tool === 'pen') return s.points.length >= 2;
2017
2099
  if (s.tool === 'rect') return Math.abs(s.w) >= 4 && Math.abs(s.h) >= 4;
2018
2100
  if (s.tool === 'polygon') return Math.abs(s.w) >= 4 && Math.abs(s.h) >= 4;
@@ -2031,10 +2113,17 @@ export function isStrokeMeaningful(s: Stroke): boolean {
2031
2113
  return Math.hypot(s.x2 - s.x1, s.y2 - s.y1) >= 4;
2032
2114
  }
2033
2115
 
2116
+ const WORLD_CTX: GeomCtx = { origin: { x: 0, y: 0 }, resolve: () => null };
2117
+
2118
+ function elementBox(s: ElementStroke): { x: number; y: number; w: number; h: number } | null {
2119
+ return defOf(s.el.type)?.bounds(s.el, WORLD_CTX) ?? null;
2120
+ }
2121
+
2034
2122
  export function strokeBBox(
2035
2123
  s: Stroke,
2036
2124
  anchors?: Map<string, AnchorHost>
2037
2125
  ): { x: number; y: number; w: number; h: number } | null {
2126
+ if (s.tool === 'element') return elementBox(s);
2038
2127
  if (s.tool === 'pen') {
2039
2128
  if (!s.points.length) return null;
2040
2129
  let xMin = Number.POSITIVE_INFINITY;
@@ -2109,6 +2198,10 @@ export function strokeBBox(
2109
2198
  }
2110
2199
 
2111
2200
  export function translateOne(s: Stroke, dx: number, dy: number): Stroke {
2201
+ if (s.tool === 'element') {
2202
+ const def = defOf(s.el.type);
2203
+ return def ? { ...s, el: { ...s.el, ...def.translate(s.el, dx, dy) } } : s;
2204
+ }
2112
2205
  if (s.tool === 'pen') {
2113
2206
  return { ...s, points: s.points.map(([x, y]) => [x + dx, y + dy] as WorldPoint) };
2114
2207
  }
@@ -1,50 +1,7 @@
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
-
1
+ /**
2
+ * Write/action ids travel with annotation writes as echo metadata (DDR-242:
3
+ * the replica's `~action`), never as authorization or a durable ACK.
4
+ */
6
5
  export function validAnnotationWriteId(value: unknown): value is string {
7
6
  return typeof value === 'string' && /^[a-zA-Z0-9_-]{1,96}$/.test(value);
8
7
  }
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
- }