@1agh/maude 0.29.0 → 0.30.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 (99) hide show
  1. package/README.md +1 -1
  2. package/apps/studio/ai-banner.tsx +0 -1
  3. package/apps/studio/annotations-align.ts +149 -0
  4. package/apps/studio/annotations-bindings.ts +197 -0
  5. package/apps/studio/annotations-context-toolbar.tsx +436 -186
  6. package/apps/studio/annotations-groups.ts +270 -0
  7. package/apps/studio/annotations-layer.tsx +1708 -1660
  8. package/apps/studio/annotations-model.ts +2077 -0
  9. package/apps/studio/annotations-snap.ts +125 -0
  10. package/apps/studio/api.ts +208 -116
  11. package/apps/studio/bin/_png-playwright.mjs +1 -1
  12. package/apps/studio/bin/annotate.mjs +732 -0
  13. package/apps/studio/bin/annotate.sh +17 -0
  14. package/apps/studio/bin/read-annotations.mjs +152 -17
  15. package/apps/studio/build.ts +1 -1
  16. package/apps/studio/canvas-arrowheads.ts +78 -9
  17. package/apps/studio/canvas-cursors.ts +2 -0
  18. package/apps/studio/canvas-edit.ts +257 -7
  19. package/apps/studio/canvas-icons.tsx +105 -0
  20. package/apps/studio/canvas-shell.tsx +305 -8
  21. package/apps/studio/client/app.jsx +2812 -238
  22. package/apps/studio/client/index.html +1 -1
  23. package/apps/studio/client/styles/3-shell-maude.css +713 -11
  24. package/apps/studio/client/styles/4-components.css +39 -0
  25. package/apps/studio/client/styles/5-maude-overrides.css +16 -2
  26. package/apps/studio/client/tour/overlay.jsx +105 -17
  27. package/apps/studio/client/tour/usage-tour.js +23 -10
  28. package/apps/studio/commands/annotation-strokes-command.ts +1 -1
  29. package/apps/studio/commands/move-artboards-command.ts +1 -1
  30. package/apps/studio/comments-overlay.tsx +7 -5
  31. package/apps/studio/context-menu.tsx +4 -3
  32. package/apps/studio/contextual-toolbar.tsx +14 -0
  33. package/apps/studio/cursors-overlay.tsx +20 -20
  34. package/apps/studio/dist/client.bundle.js +3322 -602
  35. package/apps/studio/dist/comment-mount.js +95 -9
  36. package/apps/studio/dist/styles.css +2490 -167
  37. package/apps/studio/dom-selection.ts +115 -0
  38. package/apps/studio/export-dialog.tsx +3 -3
  39. package/apps/studio/history.ts +20 -3
  40. package/apps/studio/http.ts +124 -5
  41. package/apps/studio/input-router.tsx +7 -0
  42. package/apps/studio/inspect.ts +37 -8
  43. package/apps/studio/participants-chrome.tsx +1 -7
  44. package/apps/studio/server.ts +5 -2
  45. package/apps/studio/sync/agent.ts +231 -57
  46. package/apps/studio/sync/codec.ts +45 -0
  47. package/apps/studio/sync/cold-start.ts +158 -0
  48. package/apps/studio/sync/connection-state.ts +58 -2
  49. package/apps/studio/sync/index.ts +563 -221
  50. package/apps/studio/sync/journal.ts +190 -0
  51. package/apps/studio/sync/migrate-seed.ts +202 -44
  52. package/apps/studio/sync/projection.ts +22 -3
  53. package/apps/studio/sync/status.ts +15 -4
  54. package/apps/studio/test/activity.test.ts +1 -7
  55. package/apps/studio/test/annotate-write.test.ts +184 -0
  56. package/apps/studio/test/annotations-align.test.ts +88 -0
  57. package/apps/studio/test/annotations-bindings.test.ts +124 -0
  58. package/apps/studio/test/annotations-groups.test.ts +231 -0
  59. package/apps/studio/test/annotations-snap.test.ts +79 -0
  60. package/apps/studio/test/canvas-edit.test.ts +181 -1
  61. package/apps/studio/test/canvas-origin-gate.test.ts +8 -0
  62. package/apps/studio/test/collab-bridge.test.ts +0 -2
  63. package/apps/studio/test/collab-room.test.ts +2 -7
  64. package/apps/studio/test/csp-canvas-shell.test.ts +5 -0
  65. package/apps/studio/test/csrf-write-guard.test.ts +52 -0
  66. package/apps/studio/test/figjam-v3-model.test.ts +342 -0
  67. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
  68. package/apps/studio/test/shared-doc-migrate.test.ts +224 -14
  69. package/apps/studio/test/sync-agent.test.ts +272 -2
  70. package/apps/studio/test/sync-codec.test.ts +65 -0
  71. package/apps/studio/test/sync-cold-start.test.ts +244 -0
  72. package/apps/studio/test/sync-connection-state.test.ts +68 -0
  73. package/apps/studio/test/sync-hardening.test.ts +0 -8
  74. package/apps/studio/test/sync-hubs-config.test.ts +0 -1
  75. package/apps/studio/test/sync-incident-replay.test.ts +211 -0
  76. package/apps/studio/test/sync-journal.test.ts +176 -0
  77. package/apps/studio/test/sync-runtime.test.ts +360 -5
  78. package/apps/studio/test/sync-status.test.ts +66 -0
  79. package/apps/studio/test/use-annotation-resize.test.ts +159 -0
  80. package/apps/studio/test/use-artboard-drag.test.ts +0 -1
  81. package/apps/studio/test/use-collab.test.ts +0 -0
  82. package/apps/studio/test/use-tool-mode.test.tsx +5 -2
  83. package/apps/studio/tool-palette.tsx +12 -2
  84. package/apps/studio/undo-hud.tsx +0 -1
  85. package/apps/studio/use-agent-presence.tsx +4 -2
  86. package/apps/studio/use-annotation-resize.tsx +484 -54
  87. package/apps/studio/use-annotation-selection.tsx +16 -4
  88. package/apps/studio/use-canvas-activity.tsx +2 -2
  89. package/apps/studio/use-collab.tsx +20 -15
  90. package/apps/studio/use-selection-set.tsx +9 -0
  91. package/apps/studio/use-tool-mode.tsx +5 -1
  92. package/apps/studio/whats-new.json +135 -10
  93. package/apps/studio/whats-new.schema.json +18 -2
  94. package/cli/commands/design-link.test.mjs +84 -0
  95. package/cli/commands/design.mjs +2 -1
  96. package/cli/commands/design.test.mjs +5 -1
  97. package/cli/lib/design-link.mjs +51 -1
  98. package/package.json +8 -8
  99. package/plugins/design/templates/_shell.html +28 -4
@@ -68,6 +68,8 @@ function computeId(componentName: string, idx: number): string {
68
68
 
69
69
  interface OpeningHit {
70
70
  opening: AnyNode;
71
+ /** The full JSXElement node — `editText` needs `.children` (JSXText), not just the opening tag. */
72
+ element: AnyNode;
71
73
  }
72
74
 
73
75
  /**
@@ -108,7 +110,7 @@ function findOpening(program: AnyNode, targetId: string): OpeningHit | null {
108
110
  frame.jsxIndex += 1;
109
111
  const id = computeId(frame.componentName, idx);
110
112
  if (id === targetId) {
111
- hit = { opening: node.openingElement };
113
+ hit = { opening: node.openingElement, element: node };
112
114
  }
113
115
  if (!hit) {
114
116
  if (node.openingElement) visit(node.openingElement.attributes);
@@ -200,6 +202,111 @@ export async function editAttribute(
200
202
  });
201
203
  }
202
204
 
205
+ /**
206
+ * Remove an attribute (or one inline-style property) from the element with the
207
+ * given `data-cd-id` — the "reset to original" path (Phase 12.3). `attr` follows
208
+ * the same shape as `editAttribute`: `style.<camelOrKebab>` removes one inline
209
+ * style key (dropping the whole `style={{}}` when it was the last key); any other
210
+ * name removes that plain JSX attribute. A missing key/attribute is a no-op
211
+ * (delta 0), never an error. Same atomic write + per-file lock as `editAttribute`.
212
+ */
213
+ export async function removeAttribute(
214
+ canvasAbsPath: string,
215
+ id: string,
216
+ attr: string
217
+ ): Promise<EditResult> {
218
+ return withLock(canvasAbsPath, async () => {
219
+ const file = Bun.file(canvasAbsPath);
220
+ if (!(await file.exists())) {
221
+ throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
222
+ canvas: canvasAbsPath,
223
+ id,
224
+ });
225
+ }
226
+ const source = await file.text();
227
+ const next = applyRemove(canvasAbsPath, source, id, attr);
228
+ if (next.source === source) return { source, delta: 0 };
229
+ const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
230
+ await Bun.write(tmp, next.source);
231
+ const { rename } = await import('node:fs/promises');
232
+ await rename(tmp, canvasAbsPath);
233
+ return next;
234
+ });
235
+ }
236
+
237
+ /** Pure variant of `removeAttribute` — exposed for tests. */
238
+ export function applyRemove(
239
+ canvasAbsPath: string,
240
+ source: string,
241
+ id: string,
242
+ attr: string
243
+ ): EditResult {
244
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
245
+ if (parsed.errors && parsed.errors.length > 0) {
246
+ const first = parsed.errors[0];
247
+ throw new CanvasEditError(
248
+ `oxc-parser failed on ${canvasAbsPath}: ${first?.message ?? 'unknown'}`,
249
+ {
250
+ canvas: canvasAbsPath,
251
+ id,
252
+ }
253
+ );
254
+ }
255
+ const hit = findOpening(parsed.program, id);
256
+ if (!hit) {
257
+ throw new CanvasEditError(`data-cd-id "${id}" not found in ${canvasAbsPath}`, {
258
+ canvas: canvasAbsPath,
259
+ id,
260
+ });
261
+ }
262
+ const s = new MagicString(source);
263
+ if (attr.startsWith('style.')) {
264
+ removeStyleProp(s, hit.opening, attr.slice('style.'.length), source);
265
+ } else if (attr === 'data-cd-id') {
266
+ throw new CanvasEditError('data-cd-id is owned by the pipeline; cannot be removed', {
267
+ canvas: canvasAbsPath,
268
+ id,
269
+ });
270
+ } else {
271
+ removeStringAttr(s, hit.opening, attr, source);
272
+ }
273
+ const out = s.toString();
274
+ return { source: out, delta: out.length - source.length };
275
+ }
276
+
277
+ /**
278
+ * Apply an inline TEXT-content edit to the JSX element with the given
279
+ * `data-cd-id`. Leaf-text only: the element's children must be exactly one
280
+ * `JSXText` node (whitespace-only siblings are ignored). Mixed/expression
281
+ * children (`<b>x</b>`, `{count}`) throw `CanvasEditError` — the caller should
282
+ * surface a "use /design:edit" refusal rather than guess. The text is
283
+ * JSX-escaped before it touches source. Same atomic write + per-file lock as
284
+ * `editAttribute`. See DDR-103.
285
+ */
286
+ export async function editText(
287
+ canvasAbsPath: string,
288
+ id: string,
289
+ text: string
290
+ ): Promise<EditResult> {
291
+ return withLock(canvasAbsPath, async () => {
292
+ const file = Bun.file(canvasAbsPath);
293
+ if (!(await file.exists())) {
294
+ throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
295
+ canvas: canvasAbsPath,
296
+ id,
297
+ });
298
+ }
299
+ const source = await file.text();
300
+ const next = applyTextEdit(canvasAbsPath, source, id, text);
301
+ if (next.source === source) return { source, delta: 0 };
302
+ const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
303
+ await Bun.write(tmp, next.source);
304
+ const { rename } = await import('node:fs/promises');
305
+ await rename(tmp, canvasAbsPath);
306
+ return next;
307
+ });
308
+ }
309
+
203
310
  /**
204
311
  * Pure variant — exposed for tests + in-memory pipelines. Caller owns
205
312
  * persistence. Throws CanvasEditError if the ID isn't found or the edit shape
@@ -245,6 +352,68 @@ export function applyEdit(
245
352
  return { source: out, delta: out.length - source.length };
246
353
  }
247
354
 
355
+ /**
356
+ * Pure variant of `editText` — parse, locate the JSXText child, overwrite its
357
+ * source span (preserving the original leading/trailing whitespace so JSX
358
+ * indentation survives), escaping the new text. Throws `CanvasEditError` for a
359
+ * missing id, no text content, or mixed/expression children.
360
+ */
361
+ export function applyTextEdit(
362
+ canvasAbsPath: string,
363
+ source: string,
364
+ id: string,
365
+ text: string
366
+ ): EditResult {
367
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
368
+ if (parsed.errors && parsed.errors.length > 0) {
369
+ const first = parsed.errors[0];
370
+ throw new CanvasEditError(
371
+ `oxc-parser failed on ${canvasAbsPath}: ${first?.message ?? 'unknown'}`,
372
+ { canvas: canvasAbsPath, id }
373
+ );
374
+ }
375
+
376
+ const hit = findOpening(parsed.program, id);
377
+ if (!hit) {
378
+ throw new CanvasEditError(`data-cd-id "${id}" not found in ${canvasAbsPath}`, {
379
+ canvas: canvasAbsPath,
380
+ id,
381
+ });
382
+ }
383
+
384
+ const children: AnyNode[] = Array.isArray(hit.element?.children) ? hit.element.children : [];
385
+ // Ignore whitespace-only JSXText siblings (`<button>\n Save\n</button>` parses
386
+ // as one JSXText; `<a>\n <b/>\n</a>` parses as ws + element + ws — the ws is
387
+ // noise). What's left is the "real" content.
388
+ const meaningful = children.filter(
389
+ (c) => !(c?.type === 'JSXText' && typeof c.value === 'string' && c.value.trim() === '')
390
+ );
391
+ if (meaningful.length === 0) {
392
+ throw new CanvasEditError(`element "${id}" has no editable text content`, {
393
+ canvas: canvasAbsPath,
394
+ id,
395
+ });
396
+ }
397
+ const only = meaningful[0];
398
+ if (meaningful.length > 1 || only?.type !== 'JSXText') {
399
+ throw new CanvasEditError(
400
+ `element "${id}" has mixed or expression content — edit it via /design:edit`,
401
+ { canvas: canvasAbsPath, id }
402
+ );
403
+ }
404
+
405
+ const start = only.start as number;
406
+ const end = only.end as number;
407
+ const raw = source.slice(start, end);
408
+ // Preserve the original indentation/newline framing; swap only the visible text.
409
+ const lead = /^\s*/.exec(raw)?.[0] ?? '';
410
+ const trail = /\s*$/.exec(raw)?.[0] ?? '';
411
+ const s = new MagicString(source);
412
+ s.overwrite(start, end, `${lead}${escapeJsxText(text)}${trail}`);
413
+ const out = s.toString();
414
+ return { source: out, delta: out.length - source.length };
415
+ }
416
+
248
417
  // ---------------------------------------------------------------------------
249
418
  // Edit shapes.
250
419
 
@@ -263,14 +432,20 @@ function editStringAttr(s: MagicString, opening: AnyNode, name: string, value: s
263
432
  return;
264
433
  }
265
434
  if (v.type === 'Literal' || v.type === 'StringLiteral') {
266
- // Replace just the value text, keeping surrounding quotes.
267
- s.overwrite(v.start, v.end, JSON.stringify(value));
435
+ // Replace the whole `"value"` (quotes included) so we control the escaping.
436
+ // The value lands in a JSX *attribute*, where `"` must become `&quot;` and
437
+ // `<`/`>` their entities — NOT JS backslash escaping. `JSON.stringify` would
438
+ // emit `\"`, which is invalid in a JSX attribute and corrupts the source on
439
+ // any value containing a double quote. Use the same `escapeAttr` as the two
440
+ // insert branches so all four paths agree. See DDR-103 / DDR-105.
441
+ s.overwrite(v.start, v.end, `"${escapeAttr(value)}"`);
268
442
  return;
269
443
  }
270
444
  if (v.type === 'JSXExpressionContainer') {
271
445
  // Replace the whole `{...}` with a plain quoted literal — keeps the
272
- // resulting JSX readable, no escaping gymnastics.
273
- s.overwrite(v.start, v.end, JSON.stringify(value));
446
+ // resulting JSX readable. Same JSX-attribute escaping as above (NOT
447
+ // `JSON.stringify`, which would JS-escape a `"` and corrupt the source).
448
+ s.overwrite(v.start, v.end, `"${escapeAttr(value)}"`);
274
449
  return;
275
450
  }
276
451
  // Unknown shape — refuse rather than corrupt.
@@ -289,6 +464,24 @@ function escapeAttr(value: string): string {
289
464
  return value.replace(/"/g, '&quot;').replace(/[<>]/g, (c) => (c === '<' ? '&lt;' : '&gt;'));
290
465
  }
291
466
 
467
+ /**
468
+ * Escape JSX-significant characters so user-typed text lands in source `.tsx`
469
+ * as inert TEXT, never as markup or an expression. The text is written between
470
+ * an element's tags (`<button>HERE</button>`), where `<`/`>` would start a tag
471
+ * and `{`/`}` would open a JSX expression — so all four (plus a bare `&`, which
472
+ * begins an entity) are encoded. This is the load-bearing guard that keeps the
473
+ * inline text editor (Phase 12) from being a source-injection vector. See
474
+ * DDR-103.
475
+ */
476
+ function escapeJsxText(value: string): string {
477
+ return value
478
+ .replace(/&/g, '&amp;')
479
+ .replace(/</g, '&lt;')
480
+ .replace(/>/g, '&gt;')
481
+ .replace(/\{/g, '&#123;')
482
+ .replace(/\}/g, '&#125;');
483
+ }
484
+
292
485
  function editStyleProp(
293
486
  s: MagicString,
294
487
  opening: AnyNode,
@@ -308,14 +501,14 @@ function editStyleProp(
308
501
  return;
309
502
  }
310
503
  const v = attr.value;
311
- if (!v || v.type !== 'JSXExpressionContainer') {
504
+ if (v?.type !== 'JSXExpressionContainer') {
312
505
  throw new CanvasEditError(
313
506
  `style attribute on ${id} is not a {{...}} expression — refusing to edit`,
314
507
  { canvas: canvasAbsPath, id }
315
508
  );
316
509
  }
317
510
  const obj = v.expression;
318
- if (!obj || obj.type !== 'ObjectExpression') {
511
+ if (obj?.type !== 'ObjectExpression') {
319
512
  throw new CanvasEditError(
320
513
  `style={...} on ${id} is not an inline ObjectExpression — refusing to edit`,
321
514
  { canvas: canvasAbsPath, id }
@@ -349,6 +542,63 @@ function editStyleProp(
349
542
  void tail;
350
543
  }
351
544
 
545
+ /**
546
+ * Remove a single inline-style property (the "reset to original" path — DDR-104
547
+ * Phase 12.3). No-op when the style attribute or the key is absent. When the key
548
+ * was the object's ONLY property, the whole `style={{…}}` attribute is removed so
549
+ * we don't leave an empty `style={{}}` behind.
550
+ */
551
+ function removeStyleProp(s: MagicString, opening: AnyNode, prop: string, source: string): boolean {
552
+ const attr = findAttribute(opening, 'style');
553
+ if (!attr) return false;
554
+ const v = attr.value;
555
+ if (v?.type !== 'JSXExpressionContainer') return false;
556
+ const obj = v.expression;
557
+ if (obj?.type !== 'ObjectExpression') return false;
558
+ const props = (obj.properties as AnyNode[]).filter(
559
+ (p) => p?.type === 'Property' || p?.type === 'ObjectProperty'
560
+ );
561
+ const propCamel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
562
+ const idx = props.findIndex((p) => {
563
+ const k = p.key;
564
+ const kname =
565
+ k?.type === 'Identifier' ? k.name : k?.type === 'Literal' ? String(k.value) : null;
566
+ return kname === prop || kname === propCamel;
567
+ });
568
+ if (idx === -1) return false;
569
+
570
+ // Only property → drop the whole `style={{…}}` attribute (plus the leading space).
571
+ if (props.length === 1) {
572
+ const start = source[attr.start - 1] === ' ' ? attr.start - 1 : attr.start;
573
+ s.remove(start, attr.end);
574
+ return true;
575
+ }
576
+ // Otherwise remove the one property + one adjacent comma/whitespace run so the
577
+ // remaining object stays well-formed: consume forward to the next prop's start
578
+ // (eats the trailing `, `), or — for the last prop — backward from the previous
579
+ // prop's end (eats the leading `, `).
580
+ const target = props[idx];
581
+ if (idx < props.length - 1) {
582
+ s.remove(target.start as number, props[idx + 1].start as number);
583
+ } else {
584
+ s.remove(props[idx - 1].end as number, target.end as number);
585
+ }
586
+ return true;
587
+ }
588
+
589
+ /**
590
+ * Remove a plain JSX attribute (the custom-attribute "reset" path). No-op when
591
+ * absent. Refuses `data-cd-id` / `style` (pipeline-owned / wrong endpoint).
592
+ */
593
+ function removeStringAttr(s: MagicString, opening: AnyNode, name: string, source: string): boolean {
594
+ if (name === 'data-cd-id' || name === 'style') return false;
595
+ const attr = findAttribute(opening, name);
596
+ if (!attr) return false;
597
+ const start = source[attr.start - 1] === ' ' ? attr.start - 1 : attr.start;
598
+ s.remove(start, attr.end);
599
+ return true;
600
+ }
601
+
352
602
  /**
353
603
  * Render a JS object key — bare identifier when the prop is camelCase + valid
354
604
  * JS id, quoted otherwise. Mirrors how authors write JSX styles.
@@ -473,6 +473,8 @@ export const TOOL_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
473
473
  rect: IconRect,
474
474
  ellipse: IconEllipse,
475
475
  sticky: IconSticky,
476
+ // FigJam v3 — labelled organizing container.
477
+ section: IconSection,
476
478
  arrow: IconArrow,
477
479
  text: IconText,
478
480
  eraser: IconEraser,
@@ -487,3 +489,106 @@ export const SHAPE_KIND_ICONS: Record<string, (p: IconProps) => JSX.Element> = {
487
489
  triangle: IconTriangle,
488
490
  'triangle-down': IconTriangleDown,
489
491
  };
492
+
493
+ // ── FigJam v3 — multi-select manipulation icons ──────────────────────────────
494
+ // Object align (an edge line + two boxes), distribute (rails + a box), and
495
+ // group/ungroup. Same 24×24 stroke language as the rest of the set.
496
+
497
+ export function IconObjAlignLeft(props: IconProps) {
498
+ return (
499
+ <Svg {...props}>
500
+ <path d="M4 3v18" />
501
+ <rect x="7" y="6" width="11" height="4" rx="1" />
502
+ <rect x="7" y="14" width="6" height="4" rx="1" />
503
+ </Svg>
504
+ );
505
+ }
506
+ export function IconObjAlignHCenter(props: IconProps) {
507
+ return (
508
+ <Svg {...props}>
509
+ <path d="M12 3v2M12 19v2" />
510
+ <rect x="6" y="6" width="12" height="4" rx="1" />
511
+ <rect x="8" y="14" width="8" height="4" rx="1" />
512
+ </Svg>
513
+ );
514
+ }
515
+ export function IconObjAlignRight(props: IconProps) {
516
+ return (
517
+ <Svg {...props}>
518
+ <path d="M20 3v18" />
519
+ <rect x="6" y="6" width="11" height="4" rx="1" />
520
+ <rect x="11" y="14" width="6" height="4" rx="1" />
521
+ </Svg>
522
+ );
523
+ }
524
+ export function IconObjAlignTop(props: IconProps) {
525
+ return (
526
+ <Svg {...props}>
527
+ <path d="M3 4h18" />
528
+ <rect x="6" y="7" width="4" height="11" rx="1" />
529
+ <rect x="14" y="7" width="4" height="6" rx="1" />
530
+ </Svg>
531
+ );
532
+ }
533
+ export function IconObjAlignVCenter(props: IconProps) {
534
+ return (
535
+ <Svg {...props}>
536
+ <path d="M3 12h2M19 12h2" />
537
+ <rect x="6" y="6" width="4" height="12" rx="1" />
538
+ <rect x="14" y="8" width="4" height="8" rx="1" />
539
+ </Svg>
540
+ );
541
+ }
542
+ export function IconObjAlignBottom(props: IconProps) {
543
+ return (
544
+ <Svg {...props}>
545
+ <path d="M3 20h18" />
546
+ <rect x="6" y="6" width="4" height="11" rx="1" />
547
+ <rect x="14" y="11" width="4" height="6" rx="1" />
548
+ </Svg>
549
+ );
550
+ }
551
+ export function IconDistributeH(props: IconProps) {
552
+ return (
553
+ <Svg {...props}>
554
+ <path d="M4 3v18M20 3v18" />
555
+ <rect x="9" y="8" width="6" height="8" rx="1" />
556
+ </Svg>
557
+ );
558
+ }
559
+ export function IconDistributeV(props: IconProps) {
560
+ return (
561
+ <Svg {...props}>
562
+ <path d="M3 4h18M3 20h18" />
563
+ <rect x="8" y="9" width="8" height="6" rx="1" />
564
+ </Svg>
565
+ );
566
+ }
567
+ export function IconGroup(props: IconProps) {
568
+ return (
569
+ <Svg {...props}>
570
+ <path d="M3 7V4a1 1 0 011-1h3M17 3h3a1 1 0 011 1v3M21 17v3a1 1 0 01-1 1h-3M7 21H4a1 1 0 01-1-1v-3" />
571
+ <rect x="7" y="7" width="6" height="6" rx="1" />
572
+ <rect x="11" y="11" width="6" height="6" rx="1" />
573
+ </Svg>
574
+ );
575
+ }
576
+ export function IconUngroup(props: IconProps) {
577
+ return (
578
+ <Svg {...props}>
579
+ <rect x="4" y="4" width="7" height="7" rx="1" />
580
+ <rect x="13" y="13" width="7" height="7" rx="1" />
581
+ <path d="M15 6l3 3M6 15l3 3" />
582
+ </Svg>
583
+ );
584
+ }
585
+
586
+ /** FigJam v3 — section tool: a region with its name chip docked top-left. */
587
+ export function IconSection(props: IconProps) {
588
+ return (
589
+ <Svg {...props}>
590
+ <rect x="4" y="8" width="16" height="12" rx="2" />
591
+ <path d="M4 4h7" />
592
+ </Svg>
593
+ );
594
+ }