@1agh/maude 1.2.0 → 1.3.1

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 (91) hide show
  1. package/README.md +3 -1
  2. package/apps/studio/acp/index.ts +43 -7
  3. package/apps/studio/annotations-layer.tsx +191 -116
  4. package/apps/studio/annotations-model.ts +39 -0
  5. package/apps/studio/annotations-sync.ts +50 -0
  6. package/apps/studio/api.ts +732 -38
  7. package/apps/studio/bin/annotate.mjs +3 -1
  8. package/apps/studio/bin/server-up.sh +17 -2
  9. package/apps/studio/canvas-build.ts +49 -8
  10. package/apps/studio/canvas-edit.ts +468 -7
  11. package/apps/studio/canvas-lib.tsx +82 -18
  12. package/apps/studio/canvas-list-watch.ts +33 -2
  13. package/apps/studio/canvas-notice-message.ts +16 -0
  14. package/apps/studio/canvas-notifications.tsx +19 -0
  15. package/apps/studio/canvas-shell.tsx +57 -6
  16. package/apps/studio/client/app.jsx +783 -245
  17. package/apps/studio/client/apply-edit-request.ts +55 -0
  18. package/apps/studio/client/canvas-url.js +39 -1
  19. package/apps/studio/client/export-center.jsx +111 -61
  20. package/apps/studio/client/github.js +38 -1
  21. package/apps/studio/client/index-loader.ts +76 -0
  22. package/apps/studio/client/panels/CloudBar.jsx +52 -6
  23. package/apps/studio/client/panels/GitPanel.jsx +103 -0
  24. package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
  25. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
  26. package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
  27. package/apps/studio/client/panels/SyncPanel.jsx +113 -0
  28. package/apps/studio/client/panels/TeamProjects.jsx +376 -0
  29. package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
  30. package/apps/studio/client/photo-knobs.jsx +3 -0
  31. package/apps/studio/client/share-dialog.jsx +91 -0
  32. package/apps/studio/client/share-link.js +94 -0
  33. package/apps/studio/client/styles/3-shell-maude.css +36 -18
  34. package/apps/studio/client/styles/4-components.css +18 -1
  35. package/apps/studio/client/tree-row-menu.jsx +48 -3
  36. package/apps/studio/client/whats-new.jsx +26 -32
  37. package/apps/studio/cloud/endpoints.ts +132 -0
  38. package/apps/studio/collab/awareness-bridge.ts +65 -14
  39. package/apps/studio/collab/index.ts +4 -0
  40. package/apps/studio/collab/persistence.ts +5 -1
  41. package/apps/studio/collab/registry.ts +9 -3
  42. package/apps/studio/collab/room.ts +13 -2
  43. package/apps/studio/context.ts +59 -0
  44. package/apps/studio/dist/client.bundle.js +1594 -1562
  45. package/apps/studio/dist/comment-mount.js +2 -2
  46. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  47. package/apps/studio/dist/runtime/sonner.js +1 -0
  48. package/apps/studio/dist/styles.css +1 -1
  49. package/apps/studio/git/accepted-guard.ts +36 -0
  50. package/apps/studio/hmr-broadcast.ts +34 -1
  51. package/apps/studio/http.ts +300 -18
  52. package/apps/studio/inspect.ts +19 -0
  53. package/apps/studio/managed-projects.ts +172 -0
  54. package/apps/studio/notifications.tsx +292 -0
  55. package/apps/studio/runtime-bundle.ts +2 -0
  56. package/apps/studio/server.ts +59 -3
  57. package/apps/studio/sync/accepted-cold-start.ts +225 -0
  58. package/apps/studio/sync/accepted-link.ts +320 -0
  59. package/apps/studio/sync/action-stage.ts +343 -0
  60. package/apps/studio/sync/agent.ts +12 -60
  61. package/apps/studio/sync/cell-file-events.ts +3 -0
  62. package/apps/studio/sync/codec.ts +73 -6
  63. package/apps/studio/sync/ctl-provider.ts +15 -2
  64. package/apps/studio/sync/document-discovery.ts +81 -0
  65. package/apps/studio/sync/file-membership.ts +10 -2
  66. package/apps/studio/sync/file-plane.ts +309 -26
  67. package/apps/studio/sync/index.ts +1174 -21
  68. package/apps/studio/sync/migrate-seed.ts +110 -19
  69. package/apps/studio/sync/poke.ts +4 -2
  70. package/apps/studio/sync/presentation.ts +315 -2
  71. package/apps/studio/sync/projection.ts +820 -23
  72. package/apps/studio/sync/repeated-module.ts +73 -0
  73. package/apps/studio/sync/revision-barrier.ts +129 -0
  74. package/apps/studio/sync/seed-repair.ts +46 -0
  75. package/apps/studio/sync/source-merge.ts +100 -0
  76. package/apps/studio/sync/source-ops.ts +289 -0
  77. package/apps/studio/sync/source-recovery.ts +70 -0
  78. package/apps/studio/sync/source-validation.ts +56 -0
  79. package/apps/studio/sync/status.ts +83 -1
  80. package/apps/studio/sync/transaction-client.ts +486 -0
  81. package/apps/studio/sync/writer-registry.ts +236 -0
  82. package/apps/studio/text-caret.ts +35 -0
  83. package/apps/studio/undo-hud.tsx +9 -87
  84. package/apps/studio/use-canvas-media-drop.tsx +4 -39
  85. package/apps/studio/use-tool-mode.tsx +44 -0
  86. package/apps/studio/whats-new.json +137 -0
  87. package/cli/lib/harness/codex-runtime.mjs +4 -1
  88. package/package.json +9 -8
  89. package/plugins/design/dependencies.json +3 -3
  90. package/plugins/design/templates/_shell.html +95 -7
  91. package/plugins/flow/dependencies.json +3 -3
@@ -475,6 +475,17 @@ button.dc-artboard-label {
475
475
  width: 100%;
476
476
  }
477
477
  button.dc-artboard-label:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
478
+ input.dc-artboard-rename {
479
+ appearance: none;
480
+ border-width: 0 0 1px 0;
481
+ font: inherit;
482
+ display: block;
483
+ width: 100%;
484
+ box-sizing: border-box;
485
+ text-transform: none;
486
+ outline: 2px solid var(--maude-hud-accent, #d63b1f);
487
+ outline-offset: -2px;
488
+ }
478
489
  /* DDR-148 — video-artboard badge, overlaid top-right of the header. Opens
479
490
  the timeline panel (same postMessage the context-menu entry sends). */
480
491
  .dc-canvas .dc-artboard-video-badge {
@@ -2447,6 +2458,25 @@ export function DCArtboard({
2447
2458
  // edited), so it never ratchets up from a previously-larger measured value
2448
2459
  // the way feeding `rect.h` back as the floor would.
2449
2460
  const heightFloor = typeof height === 'number' ? height : VP_GRID.h;
2461
+ // Plan T25/L08 — rename in place: double-click the name, Enter commits (the
2462
+ // shell writes the `label` prop), Escape or an unchanged name cancels.
2463
+ const [renaming, setRenaming] = useState(false);
2464
+ const renameDoneRef = useRef(false);
2465
+ const commitRename = (value: string) => {
2466
+ if (renameDoneRef.current) return;
2467
+ renameDoneRef.current = true;
2468
+ setRenaming(false);
2469
+ const next = value.trim();
2470
+ if (!next || next === label) return;
2471
+ try {
2472
+ window.parent.postMessage(
2473
+ { dgn: 'rename-artboard-request', artboardId: id, label: next },
2474
+ '*'
2475
+ );
2476
+ } catch {
2477
+ /* detached / cross-origin */
2478
+ }
2479
+ };
2450
2480
  const articleRef = useRef<HTMLElement | null>(null);
2451
2481
  const measured = useArtboardBounds(articleRef as RefObject<HTMLElement | null>);
2452
2482
  useEffect(() => {
@@ -2590,24 +2620,58 @@ export function DCArtboard({
2590
2620
  }}
2591
2621
  {...handleProps}
2592
2622
  >
2593
- <button
2594
- type="button"
2595
- className="dc-artboard-label sku"
2596
- // a11y-auditor (T3 review) — the kind chip is aria-hidden (decorative,
2597
- // redundant with the Inspector's Kind picker), so a non-digital kind
2598
- // must still reach the artboard's own accessible name or it's
2599
- // invisible to AT users entirely.
2600
- aria-label={
2601
- resolvedKind !== 'digital' ? `Artboard ${label}, ${resolvedKind}` : `Artboard ${label}`
2602
- }
2603
- >
2604
- {resolvedKind !== 'digital' ? (
2605
- <span className="dc-artboard-kind-chip" aria-hidden="true">
2606
- <ArtboardKindIcon kind={resolvedKind} />
2607
- </span>
2608
- ) : null}
2609
- {label}
2610
- </button>
2623
+ {renaming ? (
2624
+ <input
2625
+ className="dc-artboard-label sku dc-artboard-rename"
2626
+ data-testid={`artboard-rename-${id}`}
2627
+ aria-label="Artboard name"
2628
+ defaultValue={label}
2629
+ maxLength={80}
2630
+ // biome-ignore lint/a11y/noAutofocus: the rename field opens on the designer's own double-click
2631
+ autoFocus
2632
+ onFocus={(e) => e.currentTarget.select()}
2633
+ // The label is the drag handle; typing and clicking in the field
2634
+ // must not start a move or reach the canvas shortcuts.
2635
+ onPointerDown={(e) => e.stopPropagation()}
2636
+ onMouseDown={(e) => e.stopPropagation()}
2637
+ onKeyDown={(e) => {
2638
+ e.stopPropagation();
2639
+ if (e.key === 'Enter') commitRename(e.currentTarget.value);
2640
+ else if (e.key === 'Escape') {
2641
+ renameDoneRef.current = true;
2642
+ setRenaming(false);
2643
+ }
2644
+ }}
2645
+ onBlur={(e) => commitRename(e.currentTarget.value)}
2646
+ />
2647
+ ) : (
2648
+ <button
2649
+ type="button"
2650
+ className="dc-artboard-label sku"
2651
+ onDoubleClick={(e) => {
2652
+ if (isReadOnlyCanvas()) return;
2653
+ e.stopPropagation();
2654
+ renameDoneRef.current = false;
2655
+ setRenaming(true);
2656
+ }}
2657
+ // a11y-auditor (T3 review) — the kind chip is aria-hidden (decorative,
2658
+ // redundant with the Inspector's Kind picker), so a non-digital kind
2659
+ // must still reach the artboard's own accessible name or it's
2660
+ // invisible to AT users entirely.
2661
+ aria-label={
2662
+ resolvedKind !== 'digital'
2663
+ ? `Artboard ${label}, ${resolvedKind}`
2664
+ : `Artboard ${label}`
2665
+ }
2666
+ >
2667
+ {resolvedKind !== 'digital' ? (
2668
+ <span className="dc-artboard-kind-chip" aria-hidden="true">
2669
+ <ArtboardKindIcon kind={resolvedKind} />
2670
+ </span>
2671
+ ) : null}
2672
+ {label}
2673
+ </button>
2674
+ )}
2611
2675
  {hasVideo ? (
2612
2676
  <button
2613
2677
  type="button"
@@ -25,9 +25,28 @@
25
25
 
26
26
  import path from 'node:path';
27
27
 
28
- import { canvasSlugFromRel, findHtmlFiles, SKIP_DIRS } from './api.ts';
28
+ import {
29
+ canvasSlugFromRel,
30
+ findFiles,
31
+ findHtmlFiles,
32
+ PREVIEW_ASSET_EXTS,
33
+ SKIP_DIRS,
34
+ } from './api.ts';
29
35
  import type { Context } from './context.ts';
30
36
 
37
+ /**
38
+ * Supporting files the tree lists beside the canvases (images, fonts, media —
39
+ * the same `PREVIEW_ASSET_EXTS` `/_index-data` walks). A new one written from
40
+ * outside the app — an agent's export, a teammate's image arriving through
41
+ * sync — is a tree change too; without it the row stayed missing until
42
+ * something else reloaded the tree (plan T17/L03). Sidecars and stylesheets are
43
+ * deliberately NOT here: they are rewritten on every edit, and the tree nests
44
+ * them under their canvas anyway.
45
+ */
46
+ function isListedFile(low: string): boolean {
47
+ return PREVIEW_ASSET_EXTS.some((x) => low.endsWith(x));
48
+ }
49
+
31
50
  /** Burst-collapse window. A new canvas writes both `.tsx` and `.meta.json`; a
32
51
  * git checkout touches many files at once — one snapshot per quiet window. */
33
52
  export const CANVAS_LIST_DEBOUNCE_MS = 150;
@@ -59,7 +78,8 @@ export function isCanvasCandidate(rel: string, groupPaths: string[]): boolean {
59
78
  // canvases costs one recompute and emits nothing.
60
79
  const last = p.slice(p.lastIndexOf('/') + 1);
61
80
  const looksLikeDir = !last.includes('.');
62
- if (!looksLikeDir && !low.endsWith('.tsx') && !low.endsWith('.html')) return false;
81
+ if (!looksLikeDir && !low.endsWith('.tsx') && !low.endsWith('.html') && !isListedFile(low))
82
+ return false;
63
83
  const segs = p.split('/');
64
84
  for (const s of segs) {
65
85
  if (!s) return false;
@@ -123,6 +143,11 @@ export function createCanvasListWatch(
123
143
  continue; // group dir missing / unreadable — treat as empty
124
144
  }
125
145
  for (const f of files) out.add(f);
146
+ try {
147
+ for (const f of await findFiles(groupAbs, groupRel, PREVIEW_ASSET_EXTS)) out.add(f);
148
+ } catch {
149
+ /* unreadable — the canvases above still diff */
150
+ }
126
151
  }
127
152
  return out;
128
153
  }
@@ -137,6 +162,12 @@ export function createCanvasListWatch(
137
162
  // render/open/build sink without re-validating against /_index-data.
138
163
  const prefix = `${ctx.paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
139
164
  const relOut = rel.startsWith(prefix) ? rel.slice(prefix.length) : rel;
165
+ if (isListedFile(rel.toLowerCase())) {
166
+ // Not a canvas: its own action and no slug, so no consumer mistakes an
167
+ // image for a canvas to close, retarget or adopt.
168
+ ctx.bus.emit('canvas-list-update', { action: `file-${action}`, rel: relOut });
169
+ return;
170
+ }
140
171
  ctx.bus.emit('canvas-list-update', {
141
172
  action,
142
173
  rel: relOut,
@@ -0,0 +1,16 @@
1
+ import type { Notice } from './notifications.tsx';
2
+
3
+ /** Canvas content can send text, never shell actions or executable markup. */
4
+ export function acceptCanvasNotice(
5
+ event: Pick<MessageEvent, 'origin' | 'source' | 'data'>,
6
+ expectedOrigin: string,
7
+ activeWindow: Window | null | undefined
8
+ ): Notice | null {
9
+ if (!activeWindow || event.origin !== expectedOrigin || event.source !== activeWindow)
10
+ return null;
11
+ const data = event.data;
12
+ if (data?.dgn !== 'canvas-notice' || typeof data.message !== 'string' || !data.message.trim())
13
+ return null;
14
+ if (!['info', 'success', 'error', 'warning', 'undo'].includes(data.kind)) return null;
15
+ return { title: data.message.slice(0, 4000), kind: data.kind, group: 'canvas' };
16
+ }
@@ -0,0 +1,19 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import { type NoticeKind, NotificationHost, notifyCanvasText } from './notifications.tsx';
3
+
4
+ /** Embedded canvases use the shell's stack. Standalone canvases own one host. */
5
+ export function showCanvasToast(message: string, kind: NoticeKind = 'info'): void {
6
+ if (typeof document === 'undefined') return;
7
+ if (window.parent !== window) {
8
+ window.parent.postMessage({ dgn: 'canvas-notice', message, kind }, '*');
9
+ return;
10
+ }
11
+ let host = document.getElementById('maude-canvas-notifications');
12
+ if (!host) {
13
+ host = document.createElement('div');
14
+ host.id = 'maude-canvas-notifications';
15
+ document.body.appendChild(host);
16
+ createRoot(host).render(<NotificationHost />);
17
+ }
18
+ notifyCanvasText(message, kind);
19
+ }
@@ -2448,16 +2448,67 @@ function CanvasRouter({
2448
2448
  // sink so an `edit-source` command's do()/undo() reaches the main-origin-only
2449
2449
  // `/_api/edit-*` write — which the untrusted canvas iframe can't call (DDR-054).
2450
2450
  // It posts `dgn:'apply-edit'` to the parent shell, which performs the write.
2451
+ //
2452
+ // The shell answers with `dgn:'apply-edit-result'`. do()/undo() await it, so
2453
+ // a refusal (a teammate changed the value since — audit 2026-09-13 P1 #5 —
2454
+ // or any failed write) rejects, the stack keeps the entry instead of moving
2455
+ // its cursor, and the user is told why.
2451
2456
  useEffect(() => {
2452
- const applyFn: EditSourceApplyFn = (apply) => {
2453
- try {
2454
- window.parent.postMessage({ dgn: 'apply-edit', ...apply }, '*');
2455
- } catch {
2456
- /* detached / cross-origin teardown — nothing to apply */
2457
+ const pending = new Map<
2458
+ string,
2459
+ { resolve: () => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }
2460
+ >();
2461
+ const onResult = (event: MessageEvent) => {
2462
+ if (event.source !== window.parent) return;
2463
+ const data = event.data as
2464
+ | { dgn?: unknown; requestId?: unknown; ok?: unknown; error?: unknown; conflict?: unknown }
2465
+ | undefined;
2466
+ if (data?.dgn !== 'apply-edit-result' || typeof data.requestId !== 'string') return;
2467
+ const entry = pending.get(data.requestId);
2468
+ if (!entry) return;
2469
+ pending.delete(data.requestId);
2470
+ clearTimeout(entry.timer);
2471
+ if (data.ok === true) {
2472
+ entry.resolve();
2473
+ return;
2457
2474
  }
2475
+ const reason = typeof data.error === 'string' && data.error ? data.error : 'the edit failed';
2476
+ // A conflict's reason already reads as a sentence ("color was changed by
2477
+ // someone else — kept their value"); it applies to undo and redo alike.
2478
+ showCanvasToast(data.conflict === true ? reason : `Couldn't apply — ${reason}`, 'warning');
2479
+ entry.reject(new Error(reason));
2458
2480
  };
2481
+ window.addEventListener('message', onResult);
2482
+ let sequence = 0;
2483
+ const applyFn: EditSourceApplyFn = (apply) =>
2484
+ new Promise<void>((resolve, reject) => {
2485
+ sequence += 1;
2486
+ const requestId = `${Date.now().toString(36)}-${sequence}-${Math.random().toString(36).slice(2, 8)}`;
2487
+ // Generous: the shell serializes these writes behind any in-flight one.
2488
+ const timer = setTimeout(() => {
2489
+ pending.delete(requestId);
2490
+ reject(new Error('the edit was not confirmed'));
2491
+ }, 30_000);
2492
+ pending.set(requestId, { resolve, reject, timer });
2493
+ try {
2494
+ window.parent.postMessage({ dgn: 'apply-edit', requestId, ...apply }, '*');
2495
+ } catch {
2496
+ /* detached / cross-origin teardown — nothing to apply */
2497
+ pending.delete(requestId);
2498
+ clearTimeout(timer);
2499
+ reject(new Error('the canvas is detached'));
2500
+ }
2501
+ });
2459
2502
  undoSinks.setSink('editSourceApplyFn', applyFn);
2460
- return () => undoSinks.setSink('editSourceApplyFn', undefined);
2503
+ return () => {
2504
+ window.removeEventListener('message', onResult);
2505
+ for (const entry of pending.values()) {
2506
+ clearTimeout(entry.timer);
2507
+ entry.reject(new Error('the canvas closed'));
2508
+ }
2509
+ pending.clear();
2510
+ undoSinks.setSink('editSourceApplyFn', undefined);
2511
+ };
2461
2512
  }, [undoSinks]);
2462
2513
 
2463
2514
  // Phase 12.1 — reorder undo. Same origin-split shape as editSourceApplyFn: