@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.
- package/README.md +3 -1
- package/apps/studio/acp/index.ts +43 -7
- package/apps/studio/annotations-layer.tsx +191 -116
- package/apps/studio/annotations-model.ts +39 -0
- package/apps/studio/annotations-sync.ts +50 -0
- package/apps/studio/api.ts +732 -38
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-build.ts +49 -8
- package/apps/studio/canvas-edit.ts +468 -7
- package/apps/studio/canvas-lib.tsx +82 -18
- package/apps/studio/canvas-list-watch.ts +33 -2
- package/apps/studio/canvas-notice-message.ts +16 -0
- package/apps/studio/canvas-notifications.tsx +19 -0
- package/apps/studio/canvas-shell.tsx +57 -6
- package/apps/studio/client/app.jsx +783 -245
- package/apps/studio/client/apply-edit-request.ts +55 -0
- package/apps/studio/client/canvas-url.js +39 -1
- package/apps/studio/client/export-center.jsx +111 -61
- package/apps/studio/client/github.js +38 -1
- package/apps/studio/client/index-loader.ts +76 -0
- package/apps/studio/client/panels/CloudBar.jsx +52 -6
- package/apps/studio/client/panels/GitPanel.jsx +103 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
- package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
- package/apps/studio/client/panels/SyncPanel.jsx +113 -0
- package/apps/studio/client/panels/TeamProjects.jsx +376 -0
- package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
- package/apps/studio/client/photo-knobs.jsx +3 -0
- package/apps/studio/client/share-dialog.jsx +91 -0
- package/apps/studio/client/share-link.js +94 -0
- package/apps/studio/client/styles/3-shell-maude.css +36 -18
- package/apps/studio/client/styles/4-components.css +18 -1
- package/apps/studio/client/tree-row-menu.jsx +48 -3
- package/apps/studio/client/whats-new.jsx +26 -32
- package/apps/studio/cloud/endpoints.ts +132 -0
- package/apps/studio/collab/awareness-bridge.ts +65 -14
- package/apps/studio/collab/index.ts +4 -0
- package/apps/studio/collab/persistence.ts +5 -1
- package/apps/studio/collab/registry.ts +9 -3
- package/apps/studio/collab/room.ts +13 -2
- package/apps/studio/context.ts +59 -0
- package/apps/studio/dist/client.bundle.js +1594 -1562
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/sonner.js +1 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/git/accepted-guard.ts +36 -0
- package/apps/studio/hmr-broadcast.ts +34 -1
- package/apps/studio/http.ts +300 -18
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +172 -0
- package/apps/studio/notifications.tsx +292 -0
- package/apps/studio/runtime-bundle.ts +2 -0
- package/apps/studio/server.ts +59 -3
- package/apps/studio/sync/accepted-cold-start.ts +225 -0
- package/apps/studio/sync/accepted-link.ts +320 -0
- package/apps/studio/sync/action-stage.ts +343 -0
- package/apps/studio/sync/agent.ts +12 -60
- package/apps/studio/sync/cell-file-events.ts +3 -0
- package/apps/studio/sync/codec.ts +73 -6
- package/apps/studio/sync/ctl-provider.ts +15 -2
- package/apps/studio/sync/document-discovery.ts +81 -0
- package/apps/studio/sync/file-membership.ts +10 -2
- package/apps/studio/sync/file-plane.ts +309 -26
- package/apps/studio/sync/index.ts +1174 -21
- package/apps/studio/sync/migrate-seed.ts +110 -19
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +315 -2
- package/apps/studio/sync/projection.ts +820 -23
- package/apps/studio/sync/repeated-module.ts +73 -0
- package/apps/studio/sync/revision-barrier.ts +129 -0
- package/apps/studio/sync/seed-repair.ts +46 -0
- package/apps/studio/sync/source-merge.ts +100 -0
- package/apps/studio/sync/source-ops.ts +289 -0
- package/apps/studio/sync/source-recovery.ts +70 -0
- package/apps/studio/sync/source-validation.ts +56 -0
- package/apps/studio/sync/status.ts +83 -1
- package/apps/studio/sync/transaction-client.ts +486 -0
- package/apps/studio/sync/writer-registry.ts +236 -0
- package/apps/studio/text-caret.ts +35 -0
- package/apps/studio/undo-hud.tsx +9 -87
- package/apps/studio/use-canvas-media-drop.tsx +4 -39
- package/apps/studio/use-tool-mode.tsx +44 -0
- package/apps/studio/whats-new.json +137 -0
- package/cli/lib/harness/codex-runtime.mjs +4 -1
- package/package.json +9 -8
- package/plugins/design/dependencies.json +3 -3
- package/plugins/design/templates/_shell.html +95 -7
- 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
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
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 {
|
|
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')
|
|
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
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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 () =>
|
|
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:
|