@1agh/maude 0.60.7 → 1.0.2
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/apps/studio/acp/index.ts +1 -0
- package/apps/studio/ai-banner.tsx +1 -0
- package/apps/studio/annotations-context-toolbar.tsx +3 -1
- package/apps/studio/annotations-layer.tsx +33 -16
- package/apps/studio/api.ts +108 -18
- package/apps/studio/artboard-guides-overlay.tsx +5 -1
- package/apps/studio/assets-s3.ts +6 -1
- package/apps/studio/bin/_import-figma.mjs +8 -3
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-artifacts.ts +21 -0
- package/apps/studio/canvas-build.ts +14 -9
- package/apps/studio/canvas-comment-mount.tsx +27 -30
- package/apps/studio/canvas-icons.tsx +1 -1
- package/apps/studio/canvas-lib.tsx +94 -5
- package/apps/studio/canvas-list-watch.ts +14 -1
- package/apps/studio/canvas-shell.tsx +8 -0
- package/apps/studio/client/app.jsx +224 -45
- package/apps/studio/client/panels/GitPanel.jsx +121 -13
- package/apps/studio/client/panels/SettingsPanel.jsx +1 -1
- package/apps/studio/client/panels/SyncConsentDialog.jsx +182 -0
- package/apps/studio/client/panels/SyncPanel.jsx +595 -1
- package/apps/studio/client/styles/3-shell-maude.css +38 -0
- package/apps/studio/clip-ops.ts +8 -1
- package/apps/studio/cloud/endpoints.ts +265 -3
- package/apps/studio/collab/origins.ts +3 -1
- package/apps/studio/comments-overlay.tsx +5 -0
- package/apps/studio/config.schema.json +24 -0
- package/apps/studio/context-menu.tsx +40 -27
- package/apps/studio/context.ts +57 -8
- package/apps/studio/cursors-overlay.tsx +25 -13
- package/apps/studio/dist/client.bundle.js +686 -686
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/jobs.ts +77 -15
- package/apps/studio/exporters/remote.ts +190 -0
- package/apps/studio/exporters/video-encode-lib.ts +10 -4
- package/apps/studio/figma/to-strokes.ts +11 -9
- package/apps/studio/gifenc.d.ts +51 -0
- package/apps/studio/git/log-format.ts +88 -0
- package/apps/studio/git/safe-rel.ts +96 -0
- package/apps/studio/git/service.ts +46 -27
- package/apps/studio/hmr-broadcast.ts +10 -0
- package/apps/studio/http.ts +384 -6
- package/apps/studio/participants-chrome.tsx +1 -0
- package/apps/studio/photo-store.ts +7 -0
- package/apps/studio/react-augment.d.ts +17 -0
- package/apps/studio/runtime-bundle.ts +6 -1
- package/apps/studio/server.ts +43 -8
- package/apps/studio/sync/agent.ts +70 -82
- package/apps/studio/sync/asset-push.ts +28 -71
- package/apps/studio/sync/autocommit.ts +106 -5
- package/apps/studio/sync/cell-file-events.ts +117 -0
- package/apps/studio/sync/cell-pairing.ts +20 -5
- package/apps/studio/sync/cell-write-nudge.ts +244 -0
- package/apps/studio/sync/codec.ts +155 -3
- package/apps/studio/sync/cold-start-apply.ts +211 -0
- package/apps/studio/sync/ctl-heal.ts +253 -0
- package/apps/studio/sync/ctl-provider.ts +217 -0
- package/apps/studio/sync/decide-file.ts +335 -0
- package/apps/studio/sync/file-ledger.ts +581 -0
- package/apps/studio/sync/file-membership.ts +32 -0
- package/apps/studio/sync/file-plane.ts +1400 -0
- package/apps/studio/sync/file-pull.ts +41 -4
- package/apps/studio/sync/hub-link.ts +16 -1
- package/apps/studio/sync/hub-listing.ts +46 -0
- package/apps/studio/sync/hubs-config.ts +16 -0
- package/apps/studio/sync/index.ts +915 -308
- package/apps/studio/sync/journal-client.ts +200 -0
- package/apps/studio/sync/migrate-seed.ts +99 -67
- package/apps/studio/sync/poke.ts +50 -0
- package/apps/studio/sync/projection.ts +13 -0
- package/apps/studio/sync/pull-budget.ts +86 -0
- package/apps/studio/sync/settings.ts +110 -0
- package/apps/studio/sync/status.ts +68 -0
- package/apps/studio/sync/trash.ts +243 -0
- package/apps/studio/sync/untrusted.ts +30 -10
- package/apps/studio/test/_helpers.ts +8 -0
- package/apps/studio/test/canvas-build.test.ts +63 -0
- package/apps/studio/test/canvas-list-watch.test.ts +17 -0
- package/apps/studio/test/canvas-move-api.test.ts +31 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +12 -0
- package/apps/studio/test/canvas-shell-build-error.test.ts +49 -0
- package/apps/studio/test/cloud-history-hardening.test.ts +165 -0
- package/apps/studio/test/cloud-history-posture.test.ts +230 -0
- package/apps/studio/test/cloud-session-role.test.ts +30 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +39 -0
- package/apps/studio/test/cold-start-apply.test.ts +303 -0
- package/apps/studio/test/collab-stress.test.ts +9 -1
- package/apps/studio/test/export-lane.test.ts +245 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +1 -1
- package/apps/studio/test/git-log-format.test.ts +95 -0
- package/apps/studio/test/git-safe-rel.test.ts +132 -0
- package/apps/studio/test/hmr-broadcast.test.ts +26 -0
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +131 -0
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +5 -2
- package/apps/studio/test/sync-agent.test.ts +78 -0
- package/apps/studio/test/sync-asset-push.test.ts +71 -108
- package/apps/studio/test/sync-autocommit.test.ts +80 -0
- package/apps/studio/test/sync-cell-write-nudge.test.ts +346 -0
- package/apps/studio/test/sync-ctl-channel.test.ts +508 -0
- package/apps/studio/test/sync-decide-file.test.ts +420 -0
- package/apps/studio/test/sync-file-ledger.test.ts +334 -0
- package/apps/studio/test/sync-file-membership.test.ts +17 -1
- package/apps/studio/test/sync-file-plane.test.ts +976 -0
- package/apps/studio/test/sync-hub-listing.test.ts +46 -0
- package/apps/studio/test/sync-meta-codec.test.ts +76 -0
- package/apps/studio/test/sync-move-retirement.test.ts +231 -0
- package/apps/studio/test/sync-panel-surface.test.ts +20 -0
- package/apps/studio/test/sync-path-pull.test.ts +67 -1
- package/apps/studio/test/sync-pull-budget.test.ts +169 -0
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +83 -0
- package/apps/studio/test/sync-settings-routes.test.ts +195 -0
- package/apps/studio/test/sync-settings.test.ts +151 -0
- package/apps/studio/test/sync-status.test.ts +69 -0
- package/apps/studio/test/sync-trash.test.ts +132 -0
- package/apps/studio/test/workspace-containment.test.ts +45 -9
- package/apps/studio/tsconfig.json +9 -10
- package/apps/studio/use-annotation-resize.tsx +14 -3
- package/apps/studio/use-collab.tsx +3 -1
- package/apps/studio/whats-new.json +90 -0
- package/apps/studio/workspace-mode.ts +110 -62
- package/apps/studio/ws.ts +22 -1
- package/cli/bin/claude-design-server.mjs +19 -0
- package/cli/commands/design.mjs +25 -6
- package/cli/commands/hub-workspace.mjs +243 -22
- package/cli/commands/hub-workspace.test.mjs +171 -0
- package/cli/commands/hub.mjs +71 -1
- package/cli/lib/design-link.mjs +186 -1
- package/cli/lib/design-ownership.mjs +330 -0
- package/cli/lib/design-ownership.test.mjs +329 -0
- package/cli/lib/hubs-config.mjs +21 -0
- package/cli/lib/hubs-config.test.mjs +47 -1
- package/cli/lib/workspace-plan.mjs +298 -5
- package/cli/lib/workspace-plan.test.mjs +215 -1
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +43 -2
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/apps/studio/server.mjs +0 -1312
- package/apps/studio/sync/asset-pull.ts +0 -210
- package/apps/studio/sync/asset-push-worker.ts +0 -84
- package/apps/studio/sync/asset-sweep.ts +0 -262
- package/apps/studio/test/sync-asset-pull.test.ts +0 -161
- package/apps/studio/test/sync-asset-push-worker.test.ts +0 -183
- package/apps/studio/test/sync-asset-sweep.test.ts +0 -243
package/apps/studio/acp/index.ts
CHANGED
|
@@ -713,6 +713,7 @@ export function createAcp(ctx: Context, aiActivity?: AiActivity): Acp {
|
|
|
713
713
|
decision?: unknown;
|
|
714
714
|
action?: unknown;
|
|
715
715
|
content?: unknown;
|
|
716
|
+
seq?: unknown;
|
|
716
717
|
};
|
|
717
718
|
|
|
718
719
|
const chatId = typeof frame.chat === 'string' && frame.chat ? frame.chat : 'default';
|
|
@@ -1008,7 +1008,9 @@ export function AnnotationContextToolbar({
|
|
|
1008
1008
|
const canAlign = selectedStrokes.length >= 2;
|
|
1009
1009
|
|
|
1010
1010
|
// Active values for swatch highlighting — when uniform across selection.
|
|
1011
|
-
|
|
1011
|
+
// Image / link / mediaref strokes carry no `color` — `uniformValue` already
|
|
1012
|
+
// filters undefined out, the `in` check just says so to the checker.
|
|
1013
|
+
const uniqColor = uniformValue(selectedStrokes.map((s) => ('color' in s ? s.color : undefined)));
|
|
1012
1014
|
const uniqFill = caps.fill
|
|
1013
1015
|
? uniformValue(
|
|
1014
1016
|
selectedStrokes.map((s) =>
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* on commit, debounced 200 ms.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import type { JSX } from 'react';
|
|
24
25
|
import {
|
|
25
26
|
type CSSProperties,
|
|
26
27
|
createContext,
|
|
@@ -72,6 +73,7 @@ import {
|
|
|
72
73
|
type DrawMods,
|
|
73
74
|
defaultFillFor,
|
|
74
75
|
type EditorFmt,
|
|
76
|
+
type EllipseStroke,
|
|
75
77
|
FILL_PALETTE,
|
|
76
78
|
fmtEqual,
|
|
77
79
|
HALO_PAD_PX,
|
|
@@ -90,6 +92,7 @@ import {
|
|
|
90
92
|
LINK_GLYPH_D2,
|
|
91
93
|
LINK_GLYPH_STROKE,
|
|
92
94
|
LINK_TITLE_FILL,
|
|
95
|
+
type LinkStroke,
|
|
93
96
|
type ListType,
|
|
94
97
|
linkCardLayout,
|
|
95
98
|
listPrefixedBody,
|
|
@@ -104,6 +107,7 @@ import {
|
|
|
104
107
|
normalizeRect,
|
|
105
108
|
normalizeSticky,
|
|
106
109
|
normFmt,
|
|
110
|
+
type PenStroke,
|
|
107
111
|
penPathD,
|
|
108
112
|
polygonPoints,
|
|
109
113
|
resolveDefaultInk,
|
|
@@ -726,9 +730,8 @@ const ANNOT_CSS = `
|
|
|
726
730
|
* positioning. An SVG inside with width:100%/height:100% resolves to 0 px
|
|
727
731
|
* and Chrome clips children even under overflow:visible. We hardcode a
|
|
728
732
|
* very large width/height instead so the SVG viewport easily covers any
|
|
729
|
-
* world-coord stroke
|
|
730
|
-
*
|
|
731
|
-
* rare edge case of a stroke straying outside this 200k box.
|
|
733
|
+
* world-coord stroke; overflow:visible covers the rare edge case of a
|
|
734
|
+
* stroke straying outside this 200k box.
|
|
732
735
|
*/
|
|
733
736
|
width: 200000px;
|
|
734
737
|
height: 200000px;
|
|
@@ -2220,15 +2223,17 @@ export function AnnotationsLayer() {
|
|
|
2220
2223
|
// — byte-identical to the old hardcode — browse in preview, so drawing an
|
|
2221
2224
|
// annotation never silently exits the alive posture). Sticky lets the user
|
|
2222
2225
|
// draw many shapes in a row (canonical pattern: tldraw double-click to
|
|
2223
|
-
// lock).
|
|
2224
|
-
//
|
|
2225
|
-
//
|
|
2226
|
-
//
|
|
2226
|
+
// lock). Map a highlighter pen (a 'pen' stroke with the flag) back to the
|
|
2227
|
+
// 'highlighter' tool id so its sticky-lock check matches the active tool.
|
|
2228
|
+
//
|
|
2229
|
+
// No eraser case here: an eraser commits no stroke, so `cur.tool` (a stroke
|
|
2230
|
+
// KIND, off `drawingRef`) can never be 'eraser' — erase leaves through the
|
|
2231
|
+
// `isErase` branch above. It stays armed by being unreachable from here,
|
|
2232
|
+
// not by a guard. The typecheck surface completion (A2) proved the old
|
|
2233
|
+
// `!== 'eraser'` test was dead.
|
|
2227
2234
|
const toolJustUsed = cur.tool === 'pen' && cur.highlighter ? 'highlighter' : cur.tool;
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
if (!stickyOnThis) resetTool();
|
|
2231
|
-
}
|
|
2235
|
+
const stickyOnThis = sticky.locked && sticky.tool === toolJustUsed;
|
|
2236
|
+
if (!stickyOnThis) resetTool();
|
|
2232
2237
|
drawAnchorRef.current = null;
|
|
2233
2238
|
lastDrawPointRef.current = null;
|
|
2234
2239
|
setDrawing(null);
|
|
@@ -3840,8 +3845,10 @@ function AnnotationsInput({
|
|
|
3840
3845
|
|
|
3841
3846
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
3842
3847
|
// SVG — portaled INTO `.dc-world` so the world's CSS zoom + translate apply
|
|
3843
|
-
// natively
|
|
3844
|
-
//
|
|
3848
|
+
// natively, which is what makes drawn ink scale with the zoom. Selection chrome
|
|
3849
|
+
// and card hairlines opt into `vector-effect="non-scaling-stroke"` per element
|
|
3850
|
+
// to stay px-thick; drawn ink deliberately does not (see `common` in
|
|
3851
|
+
// `renderStroke`). `pointer-events: none` on the container — strokes are decorative
|
|
3845
3852
|
// for now (Phase 5.1 Task 6 will reintroduce hit-test via the selection store).
|
|
3846
3853
|
|
|
3847
3854
|
function AnnotationsSvg({
|
|
@@ -4192,11 +4199,15 @@ function MediaRefPlayers({
|
|
|
4192
4199
|
strokes,
|
|
4193
4200
|
visible,
|
|
4194
4201
|
}: {
|
|
4195
|
-
|
|
4202
|
+
// Same shape the context actually hands out (nullable OUTSIDE a world
|
|
4203
|
+
// provider, exactly like the sibling overlay at MediaRefChrome) — the old
|
|
4204
|
+
// `RefObject<HTMLElement | null>` both mis-typed the caller and let
|
|
4205
|
+
// `worldRef.current` be dereferenced on a null context.
|
|
4206
|
+
worldRef: ReturnType<typeof useWorldRefContext>;
|
|
4196
4207
|
strokes: readonly Stroke[];
|
|
4197
4208
|
visible: boolean;
|
|
4198
4209
|
}) {
|
|
4199
|
-
const target = worldRef
|
|
4210
|
+
const target = worldRef?.current ?? null;
|
|
4200
4211
|
if (!target || !visible) return null;
|
|
4201
4212
|
const HEADER = 26;
|
|
4202
4213
|
const refs = strokes.filter(
|
|
@@ -5768,6 +5779,13 @@ function StrokeNodeBase({
|
|
|
5768
5779
|
</g>
|
|
5769
5780
|
);
|
|
5770
5781
|
}
|
|
5782
|
+
// Deliberately NO `vector-effect="non-scaling-stroke"` here: drawn ink is
|
|
5783
|
+
// world-space content and must thicken/thin with the zoom, the way it does in
|
|
5784
|
+
// Figma/FigJam. The attribute also rendered differently per engine — Blink
|
|
5785
|
+
// honours it under the world's CSS `zoom` (ink stayed a fixed screen width at
|
|
5786
|
+
// every zoom level), WebKit ignores it under `transform: scale`, so the same
|
|
5787
|
+
// board drew differently in Chrome than in the desktop shell. Card hairlines
|
|
5788
|
+
// and selection chrome below still opt in — those ARE fixed-px by intent.
|
|
5771
5789
|
const common = {
|
|
5772
5790
|
'data-id': stroke.id,
|
|
5773
5791
|
'data-tool': stroke.tool,
|
|
@@ -5775,7 +5793,6 @@ function StrokeNodeBase({
|
|
|
5775
5793
|
strokeWidth: stroke.width,
|
|
5776
5794
|
strokeLinecap: 'round' as const,
|
|
5777
5795
|
strokeLinejoin: 'round' as const,
|
|
5778
|
-
vectorEffect: 'non-scaling-stroke' as const,
|
|
5779
5796
|
};
|
|
5780
5797
|
if (stroke.tool === 'pen') {
|
|
5781
5798
|
// Highlighter (item 8) — overlaps darken via multiply; the translucent hue
|
package/apps/studio/api.ts
CHANGED
|
@@ -99,6 +99,7 @@ import { clearLocatorSlug, readLocator, writeLocator } from './locator.ts';
|
|
|
99
99
|
import { STICKERS_DIR } from './paths.ts';
|
|
100
100
|
import { getPaperPreset, MAX_PRINT_MM } from './print/units.ts';
|
|
101
101
|
import { sessionDir } from './session-scope.ts';
|
|
102
|
+
import { isWorkspaceMode } from './workspace-mode.ts';
|
|
102
103
|
|
|
103
104
|
// Directories that never hold user-facing canvases. Exported so the
|
|
104
105
|
// external-canvas watcher (`canvas-list-watch.ts`) shares one source instead of
|
|
@@ -728,6 +729,16 @@ export interface ApiHooks {
|
|
|
728
729
|
* move rather than rename a file out from under a live hub session.
|
|
729
730
|
*/
|
|
730
731
|
isRoomPinned?: (slug: string) => boolean;
|
|
732
|
+
/**
|
|
733
|
+
* The MOVE protocol (codec `stampMovedTo`): stamp the slug's shared document
|
|
734
|
+
* retired-to-`toRel`, push the stamp, and detach the sync provider — so the
|
|
735
|
+
* move can proceed instead of being refused. On a cell EVERY canvas is
|
|
736
|
+
* pinned (the studio child's own runtime holds the doc), which made the
|
|
737
|
+
* pinned refusal a universal "cannot move anything in the cloud".
|
|
738
|
+
* Returns false when no runtime carries the slug — the caller keeps the
|
|
739
|
+
* refusal for that case (an unretired pinned room is still unsafe to move).
|
|
740
|
+
*/
|
|
741
|
+
retireCanvasForMove?: (fromSlug: string, toRel: string) => Promise<boolean>;
|
|
731
742
|
/** Flush + force-tear-down a canvas's collab room ahead of a move (best
|
|
732
743
|
* effort — a room may not be live for the slug at all). */
|
|
733
744
|
flushAndDropRoom?: (slug: string) => Promise<void>;
|
|
@@ -1877,6 +1888,17 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1877
1888
|
// Trailing newline — consistent with canvas-create.ts + sync/codec.ts
|
|
1878
1889
|
// (mergeSharedMetaIntoLocal), so a layout edit doesn't churn the newline.
|
|
1879
1890
|
await Bun.write(metaAbs, `${JSON.stringify(next, null, 2)}\n`);
|
|
1891
|
+
// Same reason as the annotations sidecar below, and the same bug: this
|
|
1892
|
+
// lane writes the FILE and nothing else, so in a cell — where there is no
|
|
1893
|
+
// `fs.watch` — the layout never entered the doc and never reached a peer.
|
|
1894
|
+
// Not "late", NEVER: a canvas `.meta.json` is `canvas-owned`, so it is
|
|
1895
|
+
// excluded from the journal by design and the walk-import belt behind
|
|
1896
|
+
// every other class does not cover it either. Dragging an artboard in the
|
|
1897
|
+
// cloud left every other machine showing the old position indefinitely,
|
|
1898
|
+
// while the same drag on a laptop crossed in seconds (a laptop watcher
|
|
1899
|
+
// fires) — which reads as "sync is broken one way" rather than as a
|
|
1900
|
+
// missing announce.
|
|
1901
|
+
announceWritten(path.relative(paths.designRoot, metaAbs));
|
|
1880
1902
|
}
|
|
1881
1903
|
|
|
1882
1904
|
// Return the merged view (shared meta + camera) — identical to GET, so the
|
|
@@ -1925,6 +1947,13 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1925
1947
|
const clean = sanitizeAnnotationSvg(svg);
|
|
1926
1948
|
await Bun.write(annotationsPath(file), clean);
|
|
1927
1949
|
onAnnotationsChanged?.(file, clean);
|
|
1950
|
+
// Annotations reach OTHER VIEWERS over the collab room, which is why this
|
|
1951
|
+
// never needed an `fs:any`. But the file is also a versioned, file-plane
|
|
1952
|
+
// sidecar (DDR-115), and the file plane learns about a cell's own writes
|
|
1953
|
+
// through exactly this event — so without it, a sticky note drawn in the
|
|
1954
|
+
// cloud crossed to open browsers instantly and to a peer's DISK a quarter
|
|
1955
|
+
// of an hour later.
|
|
1956
|
+
announceWritten(`${fileSlug(file)}.annotations.svg`);
|
|
1928
1957
|
return true;
|
|
1929
1958
|
}
|
|
1930
1959
|
|
|
@@ -1961,6 +1990,30 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
1961
1990
|
* ever parsed — only magic bytes are read. `saveAsset(bytes)` wraps this so
|
|
1962
1991
|
* both the route and any programmatic caller share ONE tested path.
|
|
1963
1992
|
*/
|
|
1993
|
+
/**
|
|
1994
|
+
* Say that a file this process wrote has landed.
|
|
1995
|
+
*
|
|
1996
|
+
* Most write paths get this for free: they arm `activity:suppress` before
|
|
1997
|
+
* writing, and `createContainerWriteBridge` turns that into the `fs:any` a
|
|
1998
|
+
* cell's `fs.watch` never fires. The asset writer does not — it streams to a
|
|
1999
|
+
* temp file and content-addresses the name, so there is no `rel` to suppress
|
|
2000
|
+
* until after the rename, by which point suppression means nothing.
|
|
2001
|
+
*
|
|
2002
|
+
* That gap was invisible while `fs:any` only drove hot-reload (the uploader
|
|
2003
|
+
* already knew, and other viewers reloaded eventually). It stopped being
|
|
2004
|
+
* invisible when the same event became how a cell tells its hub to journal a
|
|
2005
|
+
* write: an image dropped in the cloud got no row until the 15-minute
|
|
2006
|
+
* walk-import belt found it, so it reached a peer's laptop up to fifteen
|
|
2007
|
+
* minutes later while every other kind of edit crossed in seconds.
|
|
2008
|
+
*
|
|
2009
|
+
* WORKSPACE-MODE ONLY, on the same reasoning as the bridge: locally
|
|
2010
|
+
* `fs.watch` fires for this rename and a second source would double-load.
|
|
2011
|
+
*/
|
|
2012
|
+
function announceWritten(rel: string): void {
|
|
2013
|
+
if (!isWorkspaceMode()) return;
|
|
2014
|
+
ctx.bus.emit('fs:any', rel);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1964
2017
|
async function saveAssetFromStream(stream: ReadableStream<Uint8Array>): Promise<SaveAssetResult> {
|
|
1965
2018
|
const assetsDir = path.join(paths.designRoot, 'assets');
|
|
1966
2019
|
const tmpName = `.tmp-${crypto.randomBytes(8).toString('hex')}`;
|
|
@@ -2081,6 +2134,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2081
2134
|
await rename(tmpAbs, fileAbs);
|
|
2082
2135
|
assetBytesWritten += total;
|
|
2083
2136
|
const rel = `assets/${name}`;
|
|
2137
|
+
announceWritten(rel);
|
|
2084
2138
|
// S3/R2 lane (Cloud Phase 3) — mirror the bytes so a second machine can
|
|
2085
2139
|
// resolve them without the file riding git. Deliberately awaited but
|
|
2086
2140
|
// never able to fail the save: the asset is already on disk, the mirror
|
|
@@ -2105,7 +2159,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
2105
2159
|
|
|
2106
2160
|
async function saveAsset(bytes: Uint8Array): Promise<SaveAssetResult> {
|
|
2107
2161
|
if (!bytes || bytes.length === 0) return { ok: false, status: 400, error: 'empty body' };
|
|
2108
|
-
|
|
2162
|
+
// TS 5.7 widens a plain Uint8Array to Uint8Array<ArrayBufferLike>, which
|
|
2163
|
+
// BodyInit rejects (it excludes SharedArrayBuffer-backed views). These
|
|
2164
|
+
// bytes never are one.
|
|
2165
|
+
return saveAssetFromStream(
|
|
2166
|
+
new Response(bytes as Uint8Array<ArrayBuffer>).body as ReadableStream<Uint8Array>
|
|
2167
|
+
);
|
|
2109
2168
|
}
|
|
2110
2169
|
|
|
2111
2170
|
// Stage F1 (feature-element-editing-robustness) — list the versioned content-
|
|
@@ -3030,15 +3089,29 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3030
3089
|
}
|
|
3031
3090
|
}
|
|
3032
3091
|
|
|
3033
|
-
// Collab guard —
|
|
3034
|
-
//
|
|
3035
|
-
//
|
|
3092
|
+
// Collab guard — a pinned room (shared-doc hub provider attached, DDR-064)
|
|
3093
|
+
// must not have its file renamed out from under it. The old behaviour was
|
|
3094
|
+
// to REFUSE, which on a cell meant moving was impossible outright: cell
|
|
3095
|
+
// pairing pins every canvas, so a guard written for a rare local state
|
|
3096
|
+
// fired on 100% of cloud moves. Now the move COORDINATES: the sync runtime
|
|
3097
|
+
// stamps the document retired-to-the-new-path (the move protocol, codec
|
|
3098
|
+
// stampMovedTo) and detaches — after which the rename is exactly as safe
|
|
3099
|
+
// as on an unpinned desktop. The refusal remains only for the case where
|
|
3100
|
+
// no runtime can do that, because then the pin really is unownable here.
|
|
3036
3101
|
if (hooks.isRoomPinned?.(fromSlug)) {
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3102
|
+
const retired = (await hooks.retireCanvasForMove?.(fromSlug, toRel)) ?? false;
|
|
3103
|
+
if (!retired) {
|
|
3104
|
+
return {
|
|
3105
|
+
ok: false,
|
|
3106
|
+
status: 409,
|
|
3107
|
+
error: 'canvas has a live shared session — cannot move while pinned to the hub',
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
} else {
|
|
3111
|
+
// An unpinned canvas may still have a live synced document (the desktop
|
|
3112
|
+
// agent lane pins nothing) — retire it too, or the old document lives on
|
|
3113
|
+
// and re-materialises the canvas at its old path on every machine.
|
|
3114
|
+
await hooks.retireCanvasForMove?.(fromSlug, toRel);
|
|
3042
3115
|
}
|
|
3043
3116
|
await hooks.flushAndDropRoom?.(fromSlug);
|
|
3044
3117
|
|
|
@@ -3054,6 +3127,16 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3054
3127
|
const moved: string[] = [path.relative(paths.repoRoot, toAbs)];
|
|
3055
3128
|
for (const artifact of canvasArtifacts({ rel, paths })) {
|
|
3056
3129
|
if (artifact.kind === 'primary') continue; // already moved above
|
|
3130
|
+
if (artifact.carryOnMove === false) {
|
|
3131
|
+
// The old slug's CRDT cache — carrying it would hand the new document
|
|
3132
|
+
// the retirement stamp we just wrote. Drop it; the canvas is on disk.
|
|
3133
|
+
// A FILE, not a tree: `recursive` would turn a future slug regression
|
|
3134
|
+
// into a directory delete, and this path is only ever one cache file.
|
|
3135
|
+
await rm(artifact.abs, { force: true }).catch((err) => {
|
|
3136
|
+
console.warn(`[move] could not drop the stale doc cache: ${(err as Error).message}`);
|
|
3137
|
+
});
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3057
3140
|
const dest = relocatedName(artifact, rel, toRel, paths);
|
|
3058
3141
|
if (path.resolve(dest) === path.resolve(artifact.abs)) continue;
|
|
3059
3142
|
try {
|
|
@@ -3255,11 +3338,15 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3255
3338
|
}
|
|
3256
3339
|
}
|
|
3257
3340
|
|
|
3258
|
-
// Collab guard for every canvas found, BEFORE any disk mutation
|
|
3259
|
-
//
|
|
3260
|
-
//
|
|
3341
|
+
// Collab guard for every canvas found, BEFORE any disk mutation. Same
|
|
3342
|
+
// coordinated retire as moveCanvas — refuse only when a pinned room could
|
|
3343
|
+
// not be retired, and retire even unpinned synced documents so none of
|
|
3344
|
+
// them lives on to resurrect its canvas at the old path.
|
|
3261
3345
|
for (const r of canvasRels) {
|
|
3262
|
-
|
|
3346
|
+
const slug = fileSlug(r);
|
|
3347
|
+
const toRel = toRelDir + r.slice(relDir.length);
|
|
3348
|
+
const retired = (await hooks.retireCanvasForMove?.(slug, toRel)) ?? false;
|
|
3349
|
+
if (!retired && hooks.isRoomPinned?.(slug)) {
|
|
3263
3350
|
return {
|
|
3264
3351
|
ok: false,
|
|
3265
3352
|
status: 409,
|
|
@@ -3962,7 +4049,11 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
3962
4049
|
const hash = typeof input.contentHash === 'string' ? input.contentHash : undefined;
|
|
3963
4050
|
const verb = typeof input.verb === 'string' ? input.verb : '';
|
|
3964
4051
|
const abs = r.abs;
|
|
3965
|
-
|
|
4052
|
+
// Just `{ source: string }`: the `& Record<string, unknown>` was there for
|
|
4053
|
+
// the rest-spread below, but an INTERFACE return (SpeedResult and friends)
|
|
4054
|
+
// has no implicit index signature, so no verb's function was actually
|
|
4055
|
+
// assignable — every branch failed the check the same way.
|
|
4056
|
+
let run: ((source: string) => { source: string }) | null = null;
|
|
3966
4057
|
switch (verb) {
|
|
3967
4058
|
case 'speed': {
|
|
3968
4059
|
const rate = Number(input.rate);
|
|
@@ -4059,6 +4150,8 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
4059
4150
|
ctx.bus.emit('activity:suppress', rel);
|
|
4060
4151
|
try {
|
|
4061
4152
|
const before = await Bun.file(abs).text();
|
|
4153
|
+
// The switch above either assigned `run` or returned; say so to the checker.
|
|
4154
|
+
if (!run) return { ok: false, status: 400, error: `unknown clip verb "${verb}"` };
|
|
4062
4155
|
const result = await applyOnDisk(abs, run);
|
|
4063
4156
|
const after = await Bun.file(abs).text();
|
|
4064
4157
|
let seq: number | undefined;
|
|
@@ -5634,10 +5727,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
5634
5727
|
for (const m of matches) {
|
|
5635
5728
|
const files = await findFiles(m.abs, m.rel, ['.tsx', '.html']);
|
|
5636
5729
|
for (const f of files) {
|
|
5637
|
-
const fname = f
|
|
5638
|
-
.split('/')
|
|
5639
|
-
.pop()
|
|
5640
|
-
?.replace(/\.(tsx|html)$/i, '');
|
|
5730
|
+
const fname = (f.split('/').pop() ?? '').replace(/\.(tsx|html)$/i, '');
|
|
5641
5731
|
const group = f.split('/').slice(-2, -1)[0] || folderName;
|
|
5642
5732
|
const label = fname.toLowerCase() === 'index' ? group : fname;
|
|
5643
5733
|
items.push({ label, path: f, group });
|
|
@@ -75,7 +75,11 @@ export interface KindOverlayProps {
|
|
|
75
75
|
visibility: OverlayVisibility;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
export type KindOverlayRenderFn = (props: KindOverlayProps) => ReactNode
|
|
78
|
+
export type KindOverlayRenderFn = ((props: KindOverlayProps) => ReactNode) & {
|
|
79
|
+
/** React DevTools label. Every registered renderer sets it; the registry
|
|
80
|
+
* holds plain functions, so it is not inherited from a component type. */
|
|
81
|
+
displayName?: string;
|
|
82
|
+
};
|
|
79
83
|
|
|
80
84
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
85
|
// Per-kind content registry. Foundation registers nothing here — the generic
|
package/apps/studio/assets-s3.ts
CHANGED
|
@@ -230,7 +230,12 @@ export function createAssetMirror(
|
|
|
230
230
|
|
|
231
231
|
const send = (method: string, key: string, body: Uint8Array | null = null) => {
|
|
232
232
|
const { url, headers } = signRequest(cfg, { method, key, body });
|
|
233
|
-
|
|
233
|
+
// Uint8Array<ArrayBufferLike> vs BodyInit — see saveAsset in api.ts.
|
|
234
|
+
return fetch(url, {
|
|
235
|
+
method,
|
|
236
|
+
headers,
|
|
237
|
+
...(body === null ? {} : { body: body as Uint8Array<ArrayBuffer> }),
|
|
238
|
+
});
|
|
234
239
|
};
|
|
235
240
|
|
|
236
241
|
return {
|
|
@@ -372,7 +372,10 @@ export async function importBoard({
|
|
|
372
372
|
url,
|
|
373
373
|
root,
|
|
374
374
|
designRootRel = '.design',
|
|
375
|
-
|
|
375
|
+
// `= undefined` is the default it already had implicitly — spelled out so the
|
|
376
|
+
// parameter reads as OPTIONAL (the studio's panel route passes no slug and
|
|
377
|
+
// relies on the `slug ?? figjam-<key>` fallback below).
|
|
378
|
+
slug = undefined,
|
|
376
379
|
dryRun = false,
|
|
377
380
|
confirmLarge = false,
|
|
378
381
|
local = null,
|
|
@@ -1145,7 +1148,7 @@ export async function importFrames({
|
|
|
1145
1148
|
url,
|
|
1146
1149
|
root,
|
|
1147
1150
|
designRootRel = '.design',
|
|
1148
|
-
slug,
|
|
1151
|
+
slug = undefined,
|
|
1149
1152
|
dryRun = false,
|
|
1150
1153
|
kind = 'digital',
|
|
1151
1154
|
local = null,
|
|
@@ -1322,7 +1325,9 @@ export async function explodeArtboard({
|
|
|
1322
1325
|
artboardId,
|
|
1323
1326
|
confirmDocument = false,
|
|
1324
1327
|
dryRun = false,
|
|
1325
|
-
|
|
1328
|
+
// Optional: falls back to a fresh CodegenSession below. The studio route
|
|
1329
|
+
// never supplies one.
|
|
1330
|
+
session = undefined,
|
|
1326
1331
|
// Injected for the same reason `assets.ts` injects `ResolveDeps`: so this can
|
|
1327
1332
|
// be exercised without the network. A test that used the real one would spend
|
|
1328
1333
|
// the developer's actual PAT against a fixture file key.
|
package/apps/studio/build.ts
CHANGED
|
@@ -354,7 +354,7 @@ export function writeCompileEntry(target: PlatformTarget): string {
|
|
|
354
354
|
import bindingPath from ${JSON.stringify(bindingSpec)} with { type: 'file' };
|
|
355
355
|
process.env.NAPI_RS_NATIVE_LIBRARY_PATH = bindingPath;
|
|
356
356
|
// DDR-177 — a compiled binary must be self-contained: every runtime-spawned
|
|
357
|
-
// child (canvas-build worker
|
|
357
|
+
// child (the canvas-build worker) resolves its JS runtime via
|
|
358
358
|
// resolveBunPath() = MAUDE_BUN_PATH || 'bun'. The desktop app launches this
|
|
359
359
|
// binary with a GUI PATH that has no 'bun', and NOTHING set MAUDE_BUN_PATH —
|
|
360
360
|
// so the asset sweep died with "Executable not found in $PATH" on machines
|
|
@@ -30,6 +30,14 @@ export interface CanvasArtifact {
|
|
|
30
30
|
* see DDR-115). Informational only; callers don't need to branch on it
|
|
31
31
|
* today, but it keeps the taxonomy legible at the one place it's listed. */
|
|
32
32
|
versioned: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* May this artifact follow the canvas to a new path? Absent means yes.
|
|
35
|
+
*
|
|
36
|
+
* Only the `.ydoc.bin` cache says no, and for a specific reason — see the
|
|
37
|
+
* comment at its entry. A `false` here means the mover DELETES it rather than
|
|
38
|
+
* renaming it: it belongs to the slug that is going away.
|
|
39
|
+
*/
|
|
40
|
+
carryOnMove?: boolean;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/**
|
|
@@ -85,10 +93,23 @@ export function canvasArtifacts(input: { rel: string; paths: Paths }): CanvasArt
|
|
|
85
93
|
});
|
|
86
94
|
out.push({
|
|
87
95
|
// collab/persistence.ts ydocBinPath — the `.ydoc.bin` cache under `_state/`.
|
|
96
|
+
//
|
|
97
|
+
// DROPPED ON A MOVE, NEVER CARRIED (`carryOnMove: false`). This file is the
|
|
98
|
+
// OLD document's CRDT state, and a move's last act before the rename is to
|
|
99
|
+
// stamp that document retired (`retireForMove` → `movedTo`) and flush it —
|
|
100
|
+
// so carrying it to the new slug hands the NEW document a cache whose first
|
|
101
|
+
// word is "I have moved away". Every peer that opened it released the canvas
|
|
102
|
+
// as retired, the destination path therefore never appeared anywhere else,
|
|
103
|
+
// and the move looked like "folders don't sync": each machine showed its own
|
|
104
|
+
// move and the other's canvas still sitting at the root.
|
|
105
|
+
//
|
|
106
|
+
// Nothing is lost by dropping it. `flushAndDropRoom` has already written the
|
|
107
|
+
// canvas to disk, and the new document is seeded from those bytes.
|
|
88
108
|
abs: path.join(paths.designRoot, '_state', `${slug}.ydoc.bin`),
|
|
89
109
|
kind: 'slug-keyed',
|
|
90
110
|
rekey: true,
|
|
91
111
|
versioned: false,
|
|
112
|
+
carryOnMove: false,
|
|
92
113
|
});
|
|
93
114
|
out.push({
|
|
94
115
|
// Versioned — a real git rename on move, which is correct and expected.
|
|
@@ -255,15 +255,10 @@ function buildCssInjector(slug: string, css: string): string {
|
|
|
255
255
|
function importAllowlist(
|
|
256
256
|
designRoot: string,
|
|
257
257
|
denials: Array<{ specifier: string; reason: string }>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
opts: { filter: RegExp },
|
|
263
|
-
cb: (args: { path: string; importer: string }) => unknown
|
|
264
|
-
) => void;
|
|
265
|
-
}) => void;
|
|
266
|
-
} {
|
|
258
|
+
// Bun's own plugin type, not a hand-rolled structural twin: the local shape
|
|
259
|
+
// described a narrower `setup` than Bun.build's `plugins` accepts, so the
|
|
260
|
+
// value was not assignable at the one place it is used.
|
|
261
|
+
): import('bun').BunPlugin {
|
|
267
262
|
const root = path.resolve(designRoot);
|
|
268
263
|
const inRoot = (p: string) => p === root || p.startsWith(root + path.sep);
|
|
269
264
|
const deny = (specifier: string, reason: string): never => {
|
|
@@ -274,6 +269,16 @@ function importAllowlist(
|
|
|
274
269
|
name: 'maude-import-allowlist',
|
|
275
270
|
setup(builder) {
|
|
276
271
|
builder.onResolve({ filter: /.*/ }, (args: { path: string; importer: string }) => {
|
|
272
|
+
// `data:` / `blob:` are SELF-CONTAINED — no request leaves the page, so
|
|
273
|
+
// the sandbox has nothing to say about them. They reach this hook
|
|
274
|
+
// because bundled CSS runs every `url()` through onResolve, and
|
|
275
|
+
// `url("data:image/svg+xml,…")` — the standard idiom for grain,
|
|
276
|
+
// textures and tiny inline icons — is neither relative nor absolute,
|
|
277
|
+
// so the bare-specifier branch below used to reject it with the
|
|
278
|
+
// npm-packages message (spike finding M9: a design system's always-on
|
|
279
|
+
// film-grain took the whole canvas build down). The denial is for
|
|
280
|
+
// schemes that go to the NETWORK; these never do.
|
|
281
|
+
if (/^(data|blob):/i.test(args.path)) return { path: args.path, external: true };
|
|
277
282
|
const importer = args.importer ? path.resolve(args.importer) : '';
|
|
278
283
|
// Our own modules (canvas-lib and its graph) resolve normally.
|
|
279
284
|
if (importer && !inRoot(importer)) return null;
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
Component,
|
|
31
31
|
type ComponentType,
|
|
32
32
|
createElement,
|
|
33
|
-
Fragment,
|
|
34
33
|
type ReactNode,
|
|
35
34
|
useCallback,
|
|
36
35
|
useEffect,
|
|
@@ -494,20 +493,20 @@ export interface MountCanvasOptions {
|
|
|
494
493
|
// from the one DCArtboard (canvas-lib) consumes. Same reasoning the real
|
|
495
494
|
// ToolProvider lives in DesignCanvas, not in this layer's MaybeToolProvider.
|
|
496
495
|
function buildCanvasTree(Canvas: ComponentType, file: string | undefined): ReactNode {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
496
|
+
// DDR-223 — this layer's provider instance keeps the DDR-187 posture:
|
|
497
|
+
// bare DS specimens stay ALIVE with native cursors (there is no palette,
|
|
498
|
+
// no editing, nothing to boot into edit for). On a UI canvas the inner
|
|
499
|
+
// canvas-lib ToolProvider (a separate bundle, separate context) owns the
|
|
500
|
+
// real boot posture (edit/`move`); this ancestor instance only carries the
|
|
501
|
+
// comment tool + claim layer and converges on the first tool keydown.
|
|
502
|
+
return (
|
|
503
|
+
<MaybeToolProvider initial="browse">
|
|
504
|
+
<MaybeSelectionSetProvider>
|
|
505
|
+
<CommentHost file={file}>
|
|
506
|
+
<Canvas />
|
|
507
|
+
</CommentHost>
|
|
508
|
+
</MaybeSelectionSetProvider>
|
|
509
|
+
</MaybeToolProvider>
|
|
511
510
|
);
|
|
512
511
|
}
|
|
513
512
|
|
|
@@ -576,20 +575,20 @@ export class CanvasErrorBoundary extends Component<
|
|
|
576
575
|
},
|
|
577
576
|
{ hasError: boolean }
|
|
578
577
|
> {
|
|
579
|
-
state = { hasError: false };
|
|
578
|
+
override state = { hasError: false };
|
|
580
579
|
static getDerivedStateFromError(): { hasError: boolean } {
|
|
581
580
|
return { hasError: true };
|
|
582
581
|
}
|
|
583
|
-
componentDidCatch(): void {
|
|
582
|
+
override componentDidCatch(): void {
|
|
584
583
|
this.props.onError();
|
|
585
584
|
}
|
|
586
|
-
componentDidUpdate(prev: { attempt: number }): void {
|
|
585
|
+
override componentDidUpdate(prev: { attempt: number }): void {
|
|
587
586
|
// A new canvas (new attempt) arrived → clear the error and try rendering it.
|
|
588
587
|
if (prev.attempt !== this.props.attempt && this.state.hasError) {
|
|
589
588
|
this.setState({ hasError: false });
|
|
590
589
|
}
|
|
591
590
|
}
|
|
592
|
-
render(): ReactNode {
|
|
591
|
+
override render(): ReactNode {
|
|
593
592
|
if (this.state.hasError) return this.props.fallback();
|
|
594
593
|
return this.props.children;
|
|
595
594
|
}
|
|
@@ -648,16 +647,14 @@ function CanvasHmrRuntime({
|
|
|
648
647
|
return LG ? buildCanvasTree(LG, file) : null;
|
|
649
648
|
}, [file]);
|
|
650
649
|
|
|
651
|
-
return
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
),
|
|
661
|
-
holding.on ? createElement(HmrHoldingToast, { message: holding.message }) : null
|
|
650
|
+
return (
|
|
651
|
+
<>
|
|
652
|
+
<CanvasErrorBoundary attempt={attempt} onError={handleError} fallback={fallback}>
|
|
653
|
+
{/* key=attempt → each soft-reload remounts a fresh subtree (and resets the
|
|
654
|
+
boundary's child), matching the clean-slate semantics of a full reload. */}
|
|
655
|
+
<OkSignal key={attempt} Canvas={canvas} file={file} onOk={handleOk} />
|
|
656
|
+
</CanvasErrorBoundary>
|
|
657
|
+
{holding.on ? <HmrHoldingToast message={holding.message} /> : null}
|
|
658
|
+
</>
|
|
662
659
|
);
|
|
663
660
|
}
|