@1agh/maude 0.30.0 → 0.32.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.
- package/README.md +5 -5
- package/apps/studio/acp/bridge.ts +285 -0
- package/apps/studio/acp/env.ts +48 -0
- package/apps/studio/acp/index.ts +132 -0
- package/apps/studio/acp/probe.ts +112 -0
- package/apps/studio/acp/transcript.ts +149 -0
- package/apps/studio/annotations-layer.tsx +6 -1
- package/apps/studio/api.ts +225 -66
- package/apps/studio/bin/chat-open.sh +44 -0
- package/apps/studio/canvas-lib.tsx +112 -19
- package/apps/studio/canvas-list-watch.ts +177 -0
- package/apps/studio/canvas-shell.tsx +22 -2
- package/apps/studio/client/app.jsx +788 -26
- package/apps/studio/client/canvas-url.js +5 -0
- package/apps/studio/client/github.js +99 -0
- package/apps/studio/client/panels/ChatPanel.jsx +796 -0
- package/apps/studio/client/panels/CollabModelInfographic.jsx +71 -0
- package/apps/studio/client/panels/CreateProject.jsx +334 -0
- package/apps/studio/client/panels/DiffView.jsx +590 -0
- package/apps/studio/client/panels/GitPanel.jsx +767 -0
- package/apps/studio/client/panels/IdentityBar.jsx +294 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +598 -0
- package/apps/studio/client/panels/ReadinessList.jsx +189 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +349 -0
- package/apps/studio/client/panels/acp-runtime.js +286 -0
- package/apps/studio/client/panels/chat-markdown.jsx +138 -0
- package/apps/studio/client/panels/git-grouping.js +86 -0
- package/apps/studio/client/styles/0-reset.css +4 -0
- package/apps/studio/client/styles/3-shell-maude.css +625 -3
- package/apps/studio/client/styles/5-maude-overrides.css +25 -0
- package/apps/studio/client/styles/6-acp-chat.css +821 -0
- package/apps/studio/client/styles/_index.css +2 -0
- package/apps/studio/client/tour/collab-tour.js +61 -0
- package/apps/studio/client/tour/overlay.jsx +11 -1
- package/apps/studio/client/whats-new.jsx +25 -10
- package/apps/studio/collab/registry.ts +13 -0
- package/apps/studio/collab/room.ts +36 -0
- package/apps/studio/cursors-overlay.tsx +17 -1
- package/apps/studio/dist/client.bundle.js +29097 -1534
- package/apps/studio/dist/comment-mount.js +4 -2
- package/apps/studio/dist/styles.css +5816 -1614
- package/apps/studio/git/endpoints.ts +338 -0
- package/apps/studio/git/service.ts +1334 -0
- package/apps/studio/git/watch.ts +97 -0
- package/apps/studio/github/endpoints.ts +358 -0
- package/apps/studio/github/service.ts +231 -0
- package/apps/studio/github/token.ts +53 -0
- package/apps/studio/hmr-broadcast.ts +9 -2
- package/apps/studio/http.ts +399 -1
- package/apps/studio/participants-chrome.tsx +69 -9
- package/apps/studio/paths.ts +12 -0
- package/apps/studio/readiness.ts +220 -0
- package/apps/studio/scaffold-design.ts +57 -0
- package/apps/studio/server.ts +65 -2
- package/apps/studio/sync/agent.ts +81 -1
- package/apps/studio/sync/codec.ts +24 -0
- package/apps/studio/sync/cold-start.ts +40 -0
- package/apps/studio/sync/hub-link.ts +137 -0
- package/apps/studio/test/acp-bridge.test.ts +127 -0
- package/apps/studio/test/acp-env.test.ts +65 -0
- package/apps/studio/test/acp-origin-gate.test.ts +95 -0
- package/apps/studio/test/acp-transcript.test.ts +112 -0
- package/apps/studio/test/canvas-create-api.test.ts +72 -0
- package/apps/studio/test/canvas-list-watch.test.ts +322 -0
- package/apps/studio/test/canvas-meta-api.test.ts +161 -27
- package/apps/studio/test/canvas-origin-gate.test.ts +35 -0
- package/apps/studio/test/chat-markdown.test.tsx +58 -0
- package/apps/studio/test/collab-session-survival.test.tsx +176 -0
- package/apps/studio/test/csrf-write-guard.test.ts +26 -0
- package/apps/studio/test/editing-presence.test.ts +103 -0
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +45 -0
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +106 -0
- package/apps/studio/test/git-grouping.test.ts +106 -0
- package/apps/studio/test/git-watch.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +465 -0
- package/apps/studio/test/hub-link.test.ts +69 -0
- package/apps/studio/test/participants-chrome.test.ts +36 -1
- package/apps/studio/test/readiness.test.ts +127 -0
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +187 -0
- package/apps/studio/test/sync-cold-start.test.ts +61 -1
- package/apps/studio/test/tour-overlay.test.tsx +18 -0
- package/apps/studio/tool-palette.tsx +18 -9
- package/apps/studio/use-chrome-visibility.tsx +66 -0
- package/apps/studio/use-collab.tsx +414 -187
- package/apps/studio/whats-new.json +82 -0
- package/apps/studio/ws.ts +44 -1
- package/cli/commands/design.mjs +1 -0
- package/cli/lib/gitignore-block.mjs +16 -3
- package/cli/lib/gitignore-block.test.mjs +13 -1
- package/package.json +11 -9
- package/plugins/design/dependencies.json +17 -0
- package/plugins/design/templates/_shell.html +30 -8
|
@@ -102,6 +102,7 @@ import {
|
|
|
102
102
|
matchesArtboard,
|
|
103
103
|
useCanvasActivity,
|
|
104
104
|
} from './use-canvas-activity.tsx';
|
|
105
|
+
import { useChromeVisibility } from './use-chrome-visibility.tsx';
|
|
105
106
|
import { CollabProvider, canvasSlugFromPath } from './use-collab.tsx';
|
|
106
107
|
import { useSelectionSetOptional } from './use-selection-set.tsx';
|
|
107
108
|
import { MaybeToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
|
|
@@ -118,6 +119,17 @@ const WHEEL_ZOOM_K = 0.0015; // larger = more sensitive wheel
|
|
|
118
119
|
const SETTLE_MS = 500;
|
|
119
120
|
const PUBLISH_MS = 50;
|
|
120
121
|
|
|
122
|
+
// WebKit (Safari + the Tauri WKWebView native shell, DDR-106) vs Blink (Chrome).
|
|
123
|
+
// The two engines render the CSS `zoom` property incompatibly (see writeTransform),
|
|
124
|
+
// so the scale dimension takes a different path on each. Detected via Apple's
|
|
125
|
+
// `navigator.vendor` and the WebKit-only `GestureEvent` global — both absent in
|
|
126
|
+
// Chrome/Edge/Firefox, present in Safari and WKWebView. Evaluated once at module
|
|
127
|
+
// load; the canvas always runs in a browser/webview context.
|
|
128
|
+
const IS_WEBKIT =
|
|
129
|
+
typeof navigator !== 'undefined' &&
|
|
130
|
+
(/apple/i.test(navigator.vendor || '') ||
|
|
131
|
+
(typeof window !== 'undefined' && 'GestureEvent' in window));
|
|
132
|
+
|
|
121
133
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
134
|
// Engine CSS (Phase 4) — injected once per iframe inside DesignCanvas's mount.
|
|
123
135
|
// The visual chrome of `.dc-artboard` (borders, label strip, SKU type) still
|
|
@@ -130,6 +142,10 @@ const ENGINE_CSS = `
|
|
|
130
142
|
inset: 0;
|
|
131
143
|
overflow: hidden;
|
|
132
144
|
outline: none;
|
|
145
|
+
/* WebKit text inflation must not re-grow artboard text that the world scales
|
|
146
|
+
down on zoom-out. Inherited, so this covers every descendant. No-op on Blink. */
|
|
147
|
+
-webkit-text-size-adjust: 100%;
|
|
148
|
+
text-size-adjust: 100%;
|
|
133
149
|
/* DDR-046 — snap-layer magenta is distinct from --accent so the snap chrome
|
|
134
150
|
never visually melts into the selection halo during a drag-snap gesture.
|
|
135
151
|
OKLCH default approximates FigJam magenta in the project's color space. */
|
|
@@ -653,27 +669,47 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
653
669
|
jumpTargetsRef.current = jumpTargets;
|
|
654
670
|
|
|
655
671
|
// worldRef is stable across renders — read inside callbacks lazily, no dep.
|
|
656
|
-
// Use CSS `zoom` (not `transform: scale`) for the scale dimension. `zoom`
|
|
657
|
-
// re-flows layout at the new size so the browser re-rasterizes text at the
|
|
658
|
-
// target resolution — text stays crisp at any zoom level. `transform: scale`
|
|
659
|
-
// upscales a cached layer, which produces the pixelation users see at
|
|
660
|
-
// zoom > ~1.5. CSS `zoom` is supported in Chrome / Safari / Edge (always)
|
|
661
|
-
// and Firefox 126+; for a dev-server design tool that's full coverage.
|
|
662
672
|
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
673
|
+
// The scale dimension takes an engine-specific path because the CSS `zoom`
|
|
674
|
+
// property is NOT interoperable between Blink and WebKit:
|
|
675
|
+
//
|
|
676
|
+
// • Blink (Chrome): CSS `zoom`. `zoom` re-flows layout at the new size so the
|
|
677
|
+
// browser re-rasterizes text at the target resolution — text stays crisp at
|
|
678
|
+
// any zoom level (whereas `transform: scale` upscales a cached layer →
|
|
679
|
+
// pixelation at zoom > ~1.5). Blink composites `zoom` changes on a fast path,
|
|
680
|
+
// so pinch stays smooth. Unchanged from the original implementation.
|
|
681
|
+
// ! Subtle: under `zoom: N`, a co-located `transform: translate(Xpx, Ypx)`
|
|
682
|
+
// ! translates by N×X / N×Y screen px (translate is in pre-zoom coords, then
|
|
683
|
+
// ! the layer is zoomed). `vpRef` holds the translate in *screen* px, so we
|
|
684
|
+
// ! divide by zoom at write time. pan/zoom math stays screen-px throughout.
|
|
685
|
+
//
|
|
686
|
+
// • WebKit (Safari + the Tauri WKWebView shell): `transform: translate() scale()`.
|
|
687
|
+
// On WebKit, `zoom` co-located with a `transform` does NOT cascade scale into
|
|
688
|
+
// descendant text (fonts stay fixed size — the reported bug) and forces a
|
|
689
|
+
// main-thread relayout on every pinch tick (janky trackpad zoom). A composited
|
|
690
|
+
// `transform: scale` scales all descendants uniformly incl. text and never
|
|
691
|
+
// relayouts. transform-origin is pinned top-left so the affine matches the
|
|
692
|
+
// `screen = translate + scale·world` model the rest of the math assumes — so
|
|
693
|
+
// here the translate is plain screen px (no /z). Crispness at high zoom is
|
|
694
|
+
// recovered by releasing the compositor layer on settle (`crisp` → will-change
|
|
695
|
+
// auto), which prompts WebKit to re-rasterize text at the target scale.
|
|
670
696
|
// biome-ignore lint/correctness/useExhaustiveDependencies: refs only — stable identity by design.
|
|
671
|
-
const writeTransform = useCallback((v: ViewportState) => {
|
|
697
|
+
const writeTransform = useCallback((v: ViewportState, opts?: { crisp?: boolean }) => {
|
|
672
698
|
const el = worldRef.current;
|
|
673
699
|
if (!el) return;
|
|
674
700
|
const z = v.zoom || 1;
|
|
675
|
-
|
|
676
|
-
|
|
701
|
+
if (IS_WEBKIT) {
|
|
702
|
+
el.style.zoom = '';
|
|
703
|
+
el.style.transformOrigin = '0 0';
|
|
704
|
+
el.style.transform = `translate(${v.x}px, ${v.y}px) scale(${z})`;
|
|
705
|
+
// Mid-gesture keep the layer promoted (smooth); when settled release it so
|
|
706
|
+
// WebKit re-paints text crisp at the new scale instead of upscaling the
|
|
707
|
+
// cached bitmap.
|
|
708
|
+
el.style.willChange = opts?.crisp ? 'auto' : 'transform';
|
|
709
|
+
} else {
|
|
710
|
+
el.style.transform = `translate(${v.x / z}px, ${v.y / z}px)`;
|
|
711
|
+
el.style.zoom = String(z);
|
|
712
|
+
}
|
|
677
713
|
el.style.visibility = 'visible';
|
|
678
714
|
}, []);
|
|
679
715
|
|
|
@@ -710,8 +746,12 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
710
746
|
isInteractingStateRef.current = false;
|
|
711
747
|
setIsInteracting(false);
|
|
712
748
|
interactEndTimerRef.current = null;
|
|
749
|
+
// Motion has stopped — re-write the world transform in crisp mode so the
|
|
750
|
+
// WebKit path drops its compositor layer and re-rasterizes text sharply at
|
|
751
|
+
// the settled scale. No-op on the Blink path (CSS `zoom` is already crisp).
|
|
752
|
+
writeTransform(vpRef.current, { crisp: true });
|
|
713
753
|
}, 220);
|
|
714
|
-
}, []);
|
|
754
|
+
}, [writeTransform]);
|
|
715
755
|
|
|
716
756
|
const applyViewport = useCallback(
|
|
717
757
|
(next: ViewportState) => {
|
|
@@ -866,7 +906,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
866
906
|
const initial = getInitial();
|
|
867
907
|
if (initial) {
|
|
868
908
|
vpRef.current = { ...initial };
|
|
869
|
-
writeTransform(vpRef.current);
|
|
909
|
+
writeTransform(vpRef.current, { crisp: true });
|
|
870
910
|
setViewportPublished({ ...vpRef.current });
|
|
871
911
|
}
|
|
872
912
|
// If host has no size yet, refit when ResizeObserver delivers one.
|
|
@@ -879,7 +919,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
879
919
|
hadSize = true;
|
|
880
920
|
const refit = computeFitRef.current();
|
|
881
921
|
vpRef.current = { ...refit };
|
|
882
|
-
writeTransform(vpRef.current);
|
|
922
|
+
writeTransform(vpRef.current, { crisp: true });
|
|
883
923
|
setViewportPublished({ ...vpRef.current });
|
|
884
924
|
}
|
|
885
925
|
});
|
|
@@ -917,6 +957,11 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
917
957
|
lastY: number;
|
|
918
958
|
} = { active: false, pointerId: -1, lastX: 0, lastY: 0 };
|
|
919
959
|
|
|
960
|
+
// Safari/WKWebView trackpad pinch — see gesture* handlers below. While a
|
|
961
|
+
// native gesture is in flight, the ctrlKey-wheel branch defers to it so a
|
|
962
|
+
// pinch doesn't double-apply (WebKit may emit both event streams).
|
|
963
|
+
const gesture = { active: false, lastScale: 1 };
|
|
964
|
+
|
|
920
965
|
const onWheel = (e: WheelEvent) => {
|
|
921
966
|
e.preventDefault();
|
|
922
967
|
const rect = host.getBoundingClientRect();
|
|
@@ -926,6 +971,7 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
926
971
|
// without a physical Ctrl press — so the same branch covers both
|
|
927
972
|
// Ctrl+wheel (mouse) and pinch-zoom (trackpad).
|
|
928
973
|
if (e.ctrlKey || e.metaKey) {
|
|
974
|
+
if (gesture.active) return; // WebKit GestureEvent owns this pinch
|
|
929
975
|
// T32 — clamp deltaY into [-50, 50] before the exp() to bring
|
|
930
976
|
// trackpad-pinch (small per-frame delta) and mouse-wheel (one
|
|
931
977
|
// notch = ±100) onto the same perceived-speed curve. Mouse-wheel
|
|
@@ -951,6 +997,38 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
951
997
|
panBy(-e.deltaX, -e.deltaY);
|
|
952
998
|
};
|
|
953
999
|
|
|
1000
|
+
// Safari/WKWebView-only trackpad pinch. WebKit exposes the native pinch via
|
|
1001
|
+
// `gesture*` events (absent in Blink — these listeners simply never fire
|
|
1002
|
+
// there). Two reasons to handle them: (1) preventDefault suppresses Safari's
|
|
1003
|
+
// built-in page-zoom that would otherwise fight the canvas; (2) `e.scale`
|
|
1004
|
+
// (cumulative since gesturestart) gives a smoother, 1:1 pinch than reverse-
|
|
1005
|
+
// engineering it from synthesized ctrlKey-wheel deltas. The onWheel ctrlKey
|
|
1006
|
+
// branch defers while `gesture.active` so a pinch is never applied twice.
|
|
1007
|
+
type SafariGestureEvent = Event & { scale: number; clientX: number; clientY: number };
|
|
1008
|
+
const onGestureStart = (ev: Event) => {
|
|
1009
|
+
ev.preventDefault();
|
|
1010
|
+
const e = ev as SafariGestureEvent;
|
|
1011
|
+
gesture.active = true;
|
|
1012
|
+
gesture.lastScale = e.scale || 1;
|
|
1013
|
+
};
|
|
1014
|
+
const onGestureChange = (ev: Event) => {
|
|
1015
|
+
ev.preventDefault();
|
|
1016
|
+
if (!gesture.active) return;
|
|
1017
|
+
const e = ev as SafariGestureEvent;
|
|
1018
|
+
const rect = host.getBoundingClientRect();
|
|
1019
|
+
const cx = e.clientX - rect.left;
|
|
1020
|
+
const cy = e.clientY - rect.top;
|
|
1021
|
+
const s = e.scale || 1;
|
|
1022
|
+
const factor = gesture.lastScale > 0 ? s / gesture.lastScale : 1;
|
|
1023
|
+
gesture.lastScale = s;
|
|
1024
|
+
zoomAt(factor, cx, cy);
|
|
1025
|
+
};
|
|
1026
|
+
const onGestureEnd = (ev: Event) => {
|
|
1027
|
+
ev.preventDefault();
|
|
1028
|
+
gesture.active = false;
|
|
1029
|
+
gesture.lastScale = 1;
|
|
1030
|
+
};
|
|
1031
|
+
|
|
954
1032
|
const onPointerDown = (e: PointerEvent) => {
|
|
955
1033
|
const isMiddle = e.button === 1;
|
|
956
1034
|
const isLeftWithSpace = e.button === 0 && spaceHeld.current;
|
|
@@ -1072,6 +1150,11 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1072
1150
|
host.addEventListener('pointermove', onPointerMove);
|
|
1073
1151
|
host.addEventListener('pointerup', endPan);
|
|
1074
1152
|
host.addEventListener('pointercancel', endPan);
|
|
1153
|
+
// WebKit-only; no-ops elsewhere. passive:false so preventDefault can cancel
|
|
1154
|
+
// Safari's native page-zoom.
|
|
1155
|
+
host.addEventListener('gesturestart', onGestureStart, { passive: false });
|
|
1156
|
+
host.addEventListener('gesturechange', onGestureChange, { passive: false });
|
|
1157
|
+
host.addEventListener('gestureend', onGestureEnd, { passive: false });
|
|
1075
1158
|
|
|
1076
1159
|
return () => {
|
|
1077
1160
|
doc.removeEventListener('wheel', captureWheel, { capture: true } as EventListenerOptions);
|
|
@@ -1082,6 +1165,9 @@ export function useViewportController(opts: ViewportControllerOptions): Viewport
|
|
|
1082
1165
|
host.removeEventListener('pointermove', onPointerMove);
|
|
1083
1166
|
host.removeEventListener('pointerup', endPan);
|
|
1084
1167
|
host.removeEventListener('pointercancel', endPan);
|
|
1168
|
+
host.removeEventListener('gesturestart', onGestureStart);
|
|
1169
|
+
host.removeEventListener('gesturechange', onGestureChange);
|
|
1170
|
+
host.removeEventListener('gestureend', onGestureEnd);
|
|
1085
1171
|
};
|
|
1086
1172
|
}, [hostRef]);
|
|
1087
1173
|
|
|
@@ -2035,6 +2121,7 @@ export function DCMiniMap() {
|
|
|
2035
2121
|
ensureOverlayStyles();
|
|
2036
2122
|
const world = useWorldContext();
|
|
2037
2123
|
const controller = useViewportControllerContext();
|
|
2124
|
+
const chrome = useChromeVisibility();
|
|
2038
2125
|
const bodyRef = useRef<HTMLDivElement | null>(null);
|
|
2039
2126
|
// 132 - 22 (header) = 110 body height; width matches the chrome.
|
|
2040
2127
|
const MAP_W = 196;
|
|
@@ -2045,6 +2132,9 @@ export function DCMiniMap() {
|
|
|
2045
2132
|
});
|
|
2046
2133
|
|
|
2047
2134
|
if (!world || !controller) return null;
|
|
2135
|
+
// Menubar "View ▸ Minimap" toggle + Presentation Mode (which hides ALL
|
|
2136
|
+
// chrome). `chrome` is null in a bare DS specimen — then default-visible.
|
|
2137
|
+
if (chrome && (!chrome.minimap || chrome.present)) return null;
|
|
2048
2138
|
|
|
2049
2139
|
const geometry = computeMiniMapGeometry(world.artboards, MAP_W, MAP_BODY_H);
|
|
2050
2140
|
const host = world.hostRef.current;
|
|
@@ -2165,7 +2255,10 @@ DCMiniMap.displayName = 'DCMiniMap';
|
|
|
2165
2255
|
export function DCZoomToolbar() {
|
|
2166
2256
|
ensureOverlayStyles();
|
|
2167
2257
|
const controller = useViewportControllerContext();
|
|
2258
|
+
const chrome = useChromeVisibility();
|
|
2168
2259
|
if (!controller) return null;
|
|
2260
|
+
// Menubar "View ▸ Zoom controls" toggle + Presentation Mode.
|
|
2261
|
+
if (chrome && (!chrome.zoom || chrome.present)) return null;
|
|
2169
2262
|
const pct = Math.round(controller.viewport.zoom * 100);
|
|
2170
2263
|
return (
|
|
2171
2264
|
<div className="dc-zoom-tb" role="toolbar" aria-label="Zoom">
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// External-canvas list watcher.
|
|
2
|
+
//
|
|
3
|
+
// The browser file tree re-reads `/_index-data` only when it receives a
|
|
4
|
+
// `canvas-list-update` WS message. Until now that message was emitted ONLY by
|
|
5
|
+
// the dev-server's own create/delete API (`api.ts` createCanvas / deleteCanvas),
|
|
6
|
+
// so a canvas written straight to disk by something OUTSIDE the server — the ACP
|
|
7
|
+
// chat agent running `/design:new`, an agent `Write`, `git checkout`, a terminal
|
|
8
|
+
// `cp`/`rm` — never refreshed the tree. The file landed on disk, `fs:any` fired,
|
|
9
|
+
// the HMR/activity/git-status subscribers reacted, but the tree stayed stale
|
|
10
|
+
// until a full page reload. See `.ai/logs/rca/issue-acp-new-canvas-not-in-filetree.md`.
|
|
11
|
+
//
|
|
12
|
+
// This is the symmetric server-side counterpart: subscribe to `fs:any`, and when
|
|
13
|
+
// the set of openable canvas files under the configured canvas groups changes
|
|
14
|
+
// (add or remove), emit the same `canvas-list-update` the API does. The client's
|
|
15
|
+
// handler just calls `loadTree()` (a wholesale `/_index-data` re-read), so a
|
|
16
|
+
// nudge is all that's required — the diff makes it precise enough to skip every
|
|
17
|
+
// non-structural edit (a canvas body save, a `.meta.json` rewrite) and so avoid
|
|
18
|
+
// thrashing the tree on every `/design:edit`.
|
|
19
|
+
//
|
|
20
|
+
// Bun-native (DDR-009); the canvas listing reuses `findHtmlFiles` and the slug
|
|
21
|
+
// reuses `canvasSlugFromRel`, both from api.ts — the single source of truth that
|
|
22
|
+
// also backs `/_index-data` and the UI create/delete emits (no duplicated
|
|
23
|
+
// SKIP_DIRS / group rules to drift; DDR-115 runtime-state exclusions come along
|
|
24
|
+
// for free because `findHtmlFiles` already skips `_`-prefixed + SKIP_DIRS paths).
|
|
25
|
+
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
|
|
28
|
+
import { canvasSlugFromRel, findHtmlFiles, SKIP_DIRS } from './api.ts';
|
|
29
|
+
import type { Context } from './context.ts';
|
|
30
|
+
|
|
31
|
+
/** Burst-collapse window. A new canvas writes both `.tsx` and `.meta.json`; a
|
|
32
|
+
* git checkout touches many files at once — one snapshot per quiet window. */
|
|
33
|
+
export const CANVAS_LIST_DEBOUNCE_MS = 150;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cheap gate: is this `fs:any` path worth a (debounced) full recompute? True
|
|
37
|
+
* only for an openable canvas file (`.tsx` / `.html`) that lives under one of
|
|
38
|
+
* the configured canvas groups and carries no `_`-prefixed or SKIP_DIRS segment.
|
|
39
|
+
* A loose gate just means an occasional needless recompute — never a wrong emit,
|
|
40
|
+
* since `snapshot()` is authoritative. Input is design-root-relative (the shape
|
|
41
|
+
* fs-watch emits, e.g. `ui/project/Pricing.tsx`).
|
|
42
|
+
*
|
|
43
|
+
* Pure — no disk access. Accepts back-slash or forward-slash input.
|
|
44
|
+
*/
|
|
45
|
+
export function isCanvasCandidate(rel: string, groupPaths: string[]): boolean {
|
|
46
|
+
if (typeof rel !== 'string') return false;
|
|
47
|
+
const p = rel.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
48
|
+
if (!p) return false;
|
|
49
|
+
const low = p.toLowerCase();
|
|
50
|
+
if (!low.endsWith('.tsx') && !low.endsWith('.html')) return false;
|
|
51
|
+
const segs = p.split('/');
|
|
52
|
+
for (const s of segs) {
|
|
53
|
+
if (!s) return false;
|
|
54
|
+
if (s.startsWith('_')) return false;
|
|
55
|
+
if (SKIP_DIRS.has(s)) return false;
|
|
56
|
+
}
|
|
57
|
+
return groupPaths.some((gp) => {
|
|
58
|
+
const g = gp.replace(/^\/+|\/+$/g, '');
|
|
59
|
+
return !!g && (p === g || p.startsWith(`${g}/`));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CanvasListWatch {
|
|
64
|
+
/** Resolves once the initial canvas set has been seeded (no emit). */
|
|
65
|
+
readonly ready: Promise<void>;
|
|
66
|
+
/** Test seam — force a recompute + diff + emit. Serialized after the seed and
|
|
67
|
+
* any in-flight refresh, so a create observed between calls always diffs
|
|
68
|
+
* against the pre-create set. */
|
|
69
|
+
refresh(): Promise<void>;
|
|
70
|
+
/** Current known openable-canvas set (design-rel-prefixed). For assertions. */
|
|
71
|
+
readonly known: ReadonlySet<string>;
|
|
72
|
+
stop(): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CanvasListWatchOptions {
|
|
76
|
+
debounceMs?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createCanvasListWatch(
|
|
80
|
+
ctx: Context,
|
|
81
|
+
opts: CanvasListWatchOptions = {}
|
|
82
|
+
): CanvasListWatch {
|
|
83
|
+
const debounceMs = opts.debounceMs ?? CANVAS_LIST_DEBOUNCE_MS;
|
|
84
|
+
// Mirror /_index-data's group set exactly (cfg.canvasGroups) — NOT api.ts's
|
|
85
|
+
// create-allowlist, which additionally permits cfg.newCanvasDir. On the
|
|
86
|
+
// default config newCanvasDir IS a group; a config pointing it outside
|
|
87
|
+
// canvasGroups would have the canvas absent from /_index-data too, so the
|
|
88
|
+
// watcher stays consistent with what the tree can actually show.
|
|
89
|
+
const groupPaths = (ctx.cfg.canvasGroups ?? []).map((g) => g.path);
|
|
90
|
+
|
|
91
|
+
// null = not yet seeded; the first refresh captures the baseline and emits
|
|
92
|
+
// nothing (canvases already on disk at boot aren't "added").
|
|
93
|
+
let known: Set<string> | null = null;
|
|
94
|
+
let chain: Promise<void> = Promise.resolve();
|
|
95
|
+
let pending: ReturnType<typeof setTimeout> | null = null;
|
|
96
|
+
|
|
97
|
+
async function snapshot(): Promise<Set<string>> {
|
|
98
|
+
const out = new Set<string>();
|
|
99
|
+
for (const g of ctx.cfg.canvasGroups ?? []) {
|
|
100
|
+
const groupAbs = path.join(ctx.paths.designRoot, g.path);
|
|
101
|
+
const groupRel = path.posix.join(ctx.paths.designRel, g.path);
|
|
102
|
+
let files: string[];
|
|
103
|
+
try {
|
|
104
|
+
files = await findHtmlFiles(groupAbs, groupRel);
|
|
105
|
+
} catch {
|
|
106
|
+
continue; // group dir missing / unreadable — treat as empty
|
|
107
|
+
}
|
|
108
|
+
for (const f of files) out.add(f);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function emit(action: 'added' | 'removed', rel: string) {
|
|
114
|
+
// `rel` from findHtmlFiles is designRel-prefixed (`.design/ui/X.tsx`); strip
|
|
115
|
+
// the prefix so the wire shape matches api.ts's group-relative emit
|
|
116
|
+
// (`ui/X.tsx`). The client IGNORES the payload entirely — it just re-reads
|
|
117
|
+
// /_index-data — so rel/slug are advisory only. SECURITY TRIPWIRE: any FUTURE
|
|
118
|
+
// consumer of this event must treat rel/slug as ATTACKER-CONTROLLED (agent /
|
|
119
|
+
// `git checkout`-authored filenames) and must NOT feed them to a
|
|
120
|
+
// render/open/build sink without re-validating against /_index-data.
|
|
121
|
+
const prefix = `${ctx.paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
|
|
122
|
+
const relOut = rel.startsWith(prefix) ? rel.slice(prefix.length) : rel;
|
|
123
|
+
ctx.bus.emit('canvas-list-update', {
|
|
124
|
+
action,
|
|
125
|
+
rel: relOut,
|
|
126
|
+
slug: canvasSlugFromRel(rel, ctx.paths.designRel),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function doRefresh(): Promise<void> {
|
|
131
|
+
const next = await snapshot();
|
|
132
|
+
if (known === null) {
|
|
133
|
+
known = next; // seed — no emit
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const rel of next) if (!known.has(rel)) emit('added', rel);
|
|
137
|
+
for (const rel of known) if (!next.has(rel)) emit('removed', rel);
|
|
138
|
+
known = next;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Serialize every recompute through one chain so the boot-time seed always
|
|
142
|
+
// runs first and concurrent snapshots can't race a create into a missed diff.
|
|
143
|
+
function refresh(): Promise<void> {
|
|
144
|
+
chain = chain.then(doRefresh, doRefresh);
|
|
145
|
+
return chain;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ready = refresh(); // seed at construction
|
|
149
|
+
|
|
150
|
+
function schedule() {
|
|
151
|
+
if (pending) clearTimeout(pending);
|
|
152
|
+
pending = setTimeout(() => {
|
|
153
|
+
pending = null;
|
|
154
|
+
void refresh();
|
|
155
|
+
}, debounceMs);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const off = ctx.bus.on('fs:any', (rel: string) => {
|
|
159
|
+
if (!isCanvasCandidate(rel, groupPaths)) return;
|
|
160
|
+
schedule();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
function stop() {
|
|
164
|
+
off();
|
|
165
|
+
if (pending) clearTimeout(pending);
|
|
166
|
+
pending = null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
ready,
|
|
171
|
+
refresh,
|
|
172
|
+
get known() {
|
|
173
|
+
return known ?? new Set<string>();
|
|
174
|
+
},
|
|
175
|
+
stop,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
useAnnotationSelectionOptional,
|
|
83
83
|
} from './use-annotation-selection.tsx';
|
|
84
84
|
import { AnnotationsVisibilityProvider } from './use-annotations-visibility.tsx';
|
|
85
|
+
import { ChromeVisibilityProvider, useChromeVisibility } from './use-chrome-visibility.tsx';
|
|
85
86
|
import { useCollab } from './use-collab.tsx';
|
|
86
87
|
import { useCursorModifiers } from './use-cursor-modifiers.tsx';
|
|
87
88
|
import { useKeyboardDiscipline } from './use-keyboard-discipline.tsx';
|
|
@@ -481,7 +482,9 @@ export function CanvasShell({
|
|
|
481
482
|
<MaybeSelectionSetProvider>
|
|
482
483
|
<AnnotationSelectionProvider>
|
|
483
484
|
<AnnotationsVisibilityProvider>
|
|
484
|
-
<
|
|
485
|
+
<ChromeVisibilityProvider>
|
|
486
|
+
<CanvasCore hostRef={hostRef}>{children}</CanvasCore>
|
|
487
|
+
</ChromeVisibilityProvider>
|
|
485
488
|
</AnnotationsVisibilityProvider>
|
|
486
489
|
</AnnotationSelectionProvider>
|
|
487
490
|
</MaybeSelectionSetProvider>
|
|
@@ -1371,6 +1374,11 @@ function CanvasRouter({
|
|
|
1371
1374
|
const undoStack = useUndoStack();
|
|
1372
1375
|
// Shell View-menu zoom bridge (dgn:'zoom') — same controller the zoom pill uses.
|
|
1373
1376
|
const zoomController = useViewportControllerContext();
|
|
1377
|
+
// Shell View-menu chrome-visibility bridge (dgn:'view-chrome') — minimap /
|
|
1378
|
+
// zoom-controls toggles + Presentation Mode. Drives the shared chrome store
|
|
1379
|
+
// the floating chrome (DCMiniMap, DCZoomToolbar, ToolPalette, AnnotationsLayer)
|
|
1380
|
+
// consumes.
|
|
1381
|
+
const chromeCtx = useChromeVisibility();
|
|
1374
1382
|
|
|
1375
1383
|
// Hover state drives the floating .dc-cv-halo--hover overlay. The overlay
|
|
1376
1384
|
// itself reads getBoundingClientRect on every rAF tick to follow pan/zoom.
|
|
@@ -1539,10 +1547,22 @@ function CanvasRouter({
|
|
|
1539
1547
|
else if (op === 'actual') zoomController.reset();
|
|
1540
1548
|
return;
|
|
1541
1549
|
}
|
|
1550
|
+
// Shell View-menu chrome toggles + Presentation Mode. Only the fields the
|
|
1551
|
+
// shell sends are merged, so a `{ present: true }` enter keeps the user's
|
|
1552
|
+
// individual minimap/zoom toggles for the eventual exit.
|
|
1553
|
+
if (m.dgn === 'view-chrome' && chromeCtx) {
|
|
1554
|
+
const mm = m as { minimap?: boolean; zoom?: boolean; present?: boolean };
|
|
1555
|
+
const patch: { minimap?: boolean; zoom?: boolean; present?: boolean } = {};
|
|
1556
|
+
if (typeof mm.minimap === 'boolean') patch.minimap = mm.minimap;
|
|
1557
|
+
if (typeof mm.zoom === 'boolean') patch.zoom = mm.zoom;
|
|
1558
|
+
if (typeof mm.present === 'boolean') patch.present = mm.present;
|
|
1559
|
+
chromeCtx.setChrome(patch);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1542
1562
|
};
|
|
1543
1563
|
window.addEventListener('message', onMessage);
|
|
1544
1564
|
return () => window.removeEventListener('message', onMessage);
|
|
1545
|
-
}, [selSet, annotSel, setTool, undoStack, zoomController]);
|
|
1565
|
+
}, [selSet, annotSel, setTool, undoStack, zoomController, chromeCtx]);
|
|
1546
1566
|
|
|
1547
1567
|
// Phase 12 (DDR-103) — double-click a LEAF-TEXT element (children all text
|
|
1548
1568
|
// nodes) to edit its copy in place. Commit (blur / Enter) posts `dgn:edit-text`
|