nudge-ui 0.2.1 → 0.2.3
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 +164 -46
- package/dist/client.mjs +252 -54
- package/dist/css/value-semantics/colorSemantics.d.ts +4 -0
- package/dist/css/value-semantics/colorSemantics.js +17 -0
- package/dist/hosts/astro/astroRuntimeConfig.js +1 -0
- package/dist/hosts/astro/clientTransport.js +41 -32
- package/dist/hosts/astro/integration.d.ts +2 -2
- package/dist/hosts/astro/integration.js +9 -4
- package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
- package/dist/hosts/astro/markdownIdentity.js +81 -0
- package/dist/hosts/environment.d.ts +15 -0
- package/dist/hosts/environment.js +16 -0
- package/dist/hosts/next/manifest.js +1 -1
- package/dist/hosts/next/sidecar.js +12 -2
- package/dist/hosts/next/wrapper.d.ts +2 -0
- package/dist/hosts/next/wrapper.js +2 -1
- package/dist/hosts/projectBridge.d.ts +1 -0
- package/dist/hosts/projectBridge.js +1 -0
- package/dist/hosts/static/manifest.js +3 -1
- package/dist/hosts/static/server.js +4 -0
- package/dist/hosts/vite/vite.d.ts +4 -0
- package/dist/hosts/vite/vite.js +28 -5
- package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
- package/dist/inspector/agent/McpConnectionDialog.js +23 -22
- package/dist/inspector/agent/activity.d.ts +18 -0
- package/dist/inspector/agent/activity.js +70 -0
- package/dist/inspector/agent/client.d.ts +8 -0
- package/dist/inspector/agent/client.js +65 -5
- package/dist/inspector/agent/connectionStatus.d.ts +3 -3
- package/dist/inspector/agent/connectionStatus.js +9 -2
- package/dist/inspector/agent/httpTransport.d.ts +6 -0
- package/dist/inspector/agent/httpTransport.js +50 -3
- package/dist/inspector/agent/protocol.d.ts +9 -3
- package/dist/inspector/agent/protocol.js +1 -1
- package/dist/inspector/agent/verification.d.ts +11 -1
- package/dist/inspector/agent/verification.js +53 -25
- package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
- package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
- package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
- package/dist/inspector/canvas/CanvasCard.js +133 -38
- package/dist/inspector/canvas/CanvasElementOverlay.js +200 -22
- package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
- package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
- package/dist/inspector/canvas/CanvasToolbar.js +8 -4
- package/dist/inspector/canvas/CanvasWorkspace.js +243 -56
- package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
- package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
- package/dist/inspector/canvas/agentPresentation.js +64 -52
- package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
- package/dist/inspector/canvas/canvasGestures.js +1 -1
- package/dist/inspector/canvas/canvasStore.d.ts +46 -82
- package/dist/inspector/canvas/canvasStore.js +251 -284
- package/dist/inspector/canvas/frameContent.d.ts +21 -0
- package/dist/inspector/canvas/frameContent.js +44 -0
- package/dist/inspector/canvas/frameProtocol.d.ts +34 -4
- package/dist/inspector/canvas/frameProtocol.js +9 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -1
- package/dist/inspector/canvas/keyboardShortcuts.js +5 -3
- package/dist/inspector/canvas/layout.d.ts +52 -0
- package/dist/inspector/canvas/layout.js +106 -0
- package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
- package/dist/inspector/canvas/linkEligibility.js +0 -29
- package/dist/inspector/canvas/projection.d.ts +15 -5
- package/dist/inspector/canvas/projection.js +69 -13
- package/dist/inspector/canvas/rendererBootstrap.js +45 -4
- package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
- package/dist/inspector/canvas/rendererElementSelector.js +102 -45
- package/dist/inspector/canvas/rendererStylesheet.js +2 -2
- package/dist/inspector/canvas/resizeHandleScale.js +1 -1
- package/dist/inspector/canvas/sessionStore.d.ts +11 -26
- package/dist/inspector/canvas/sessionStore.js +43 -183
- package/dist/inspector/canvas/staleChangeDetector.js +9 -3
- package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
- package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
- package/dist/inspector/canvas/viewStore.d.ts +38 -0
- package/dist/inspector/canvas/viewStore.js +104 -0
- package/dist/inspector/canvas/wheelPan.d.ts +8 -0
- package/dist/inspector/canvas/wheelPan.js +8 -0
- package/dist/inspector/changes/changesLog.d.ts +6 -5
- package/dist/inspector/changes/changesLog.js +40 -32
- package/dist/inspector/changes/draftChanges.d.ts +56 -0
- package/dist/inspector/changes/draftChanges.js +300 -0
- package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
- package/dist/inspector/changes/previewDiagnostics.js +11 -25
- package/dist/inspector/clientManifest.d.ts +3 -1
- package/dist/inspector/clientManifest.js +1 -0
- package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
- package/dist/inspector/comments/CommentsOverlay.js +250 -0
- package/dist/inspector/comments/element.d.ts +6 -0
- package/dist/inspector/comments/element.js +47 -0
- package/dist/inspector/comments/store.d.ts +29 -0
- package/dist/inspector/comments/store.js +121 -0
- package/dist/inspector/comments/verification.d.ts +5 -0
- package/dist/inspector/comments/verification.js +52 -0
- package/dist/inspector/drafts/model.d.ts +35 -0
- package/dist/inspector/drafts/model.js +45 -0
- package/dist/inspector/drafts/store.d.ts +28 -0
- package/dist/inspector/drafts/store.js +231 -0
- package/dist/inspector/index.js +26 -35
- package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
- package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
- package/dist/inspector/iterations/capture.d.ts +2 -0
- package/dist/inspector/iterations/capture.js +173 -0
- package/dist/inspector/iterations/client.d.ts +7 -0
- package/dist/inspector/iterations/client.js +52 -0
- package/dist/inspector/iterations/coordinator.d.ts +15 -0
- package/dist/inspector/iterations/coordinator.js +130 -0
- package/dist/inspector/iterations/prompt.d.ts +3 -0
- package/dist/inspector/iterations/prompt.js +26 -0
- package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
- package/dist/inspector/overlay/DomNavigator.js +46 -0
- package/dist/inspector/overlay/InspectorOverlay.js +6 -2
- package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
- package/dist/inspector/overlay/clickPolicy.js +34 -3
- package/dist/inspector/overlay/elementSelector.d.ts +1 -1
- package/dist/inspector/overlay/elementSelector.js +18 -7
- package/dist/inspector/overlay/interactionStyles.js +3 -1
- package/dist/inspector/overlay/spacingGestures.d.ts +15 -3
- package/dist/inspector/overlay/spacingGestures.js +88 -13
- package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
- package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
- package/dist/inspector/projection/renderedInstance.d.ts +2 -1
- package/dist/inspector/projection/renderedInstance.js +9 -5
- package/dist/inspector/projection/structuralProjection.d.ts +2 -1
- package/dist/inspector/projection/structuralProjection.js +19 -15
- package/dist/inspector/projection/textProjection.d.ts +2 -1
- package/dist/inspector/projection/textProjection.js +8 -4
- package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
- package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
- package/dist/inspector/prompt/clipboardHandoff.js +92 -143
- package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
- package/dist/inspector/prompt/generatePrompt.js +17 -16
- package/dist/inspector/runtime/runtimeConfig.d.ts +10 -1
- package/dist/inspector/runtime/runtimeConfig.js +24 -0
- package/dist/inspector/selection/hierarchy.d.ts +6 -0
- package/dist/inspector/selection/hierarchy.js +8 -0
- package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
- package/dist/inspector/settings/SettingsDialog.js +22 -9
- package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
- package/dist/inspector/shell/AgentStatusButton.js +36 -0
- package/dist/inspector/shell/AppShell.js +4 -2
- package/dist/inspector/shell/ChangesLog.js +47 -27
- package/dist/inspector/shell/CopyPromptButton.js +146 -90
- package/dist/inspector/shell/EmptyState.js +9 -8
- package/dist/inspector/shell/InspectorShell.js +14 -25
- package/dist/inspector/shell/PeekOriginalButton.js +24 -23
- package/dist/inspector/shell/originalPreview.js +5 -5
- package/dist/inspector/shell/shortcuts.d.ts +3 -0
- package/dist/inspector/shell/shortcuts.js +11 -0
- package/dist/inspector/sketch/SketchEditor.js +14 -14
- package/dist/inspector/sketch/SketchEntryButton.js +1 -1
- package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
- package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
- package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
- package/dist/inspector/sketch/SketchOverlay.js +7 -7
- package/dist/inspector/sketch/SketchWorkspace.js +18 -12
- package/dist/inspector/sketch/interaction.d.ts +1 -0
- package/dist/inspector/sketch/interaction.js +8 -4
- package/dist/inspector/sketch/model.d.ts +5 -0
- package/dist/inspector/sketch/model.js +6 -2
- package/dist/inspector/sketch/raster.d.ts +1 -2
- package/dist/inspector/sketch/raster.js +68 -4
- package/dist/inspector/sketch/store.js +48 -23
- package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
- package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
- package/dist/inspector/styleEditors/BorderEditor.js +49 -78
- package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
- package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
- package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
- package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
- package/dist/inspector/styleEditors/ColorPicker.js +10 -4
- package/dist/inspector/styleEditors/GapField.js +3 -5
- package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
- package/dist/inspector/styleEditors/GridChildSection.js +18 -9
- package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
- package/dist/inspector/styleEditors/GridPicker.js +8 -2
- package/dist/inspector/styleEditors/GridSection.js +1 -1
- package/dist/inspector/styleEditors/GridValueField.js +19 -14
- package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
- package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
- package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
- package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
- package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
- package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
- package/dist/inspector/styleEditors/LayoutSection.js +32 -11
- package/dist/inspector/styleEditors/SpacingBox.js +16 -9
- package/dist/inspector/styleEditors/Typography.js +11 -3
- package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
- package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
- package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
- package/dist/inspector/styleEditors/gradientValue.js +114 -0
- package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
- package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
- package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
- package/dist/inspector/styleEditors/layoutValue.js +0 -42
- package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
- package/dist/inspector/styleEditors/nudgeValue.js +41 -11
- package/dist/inspector/tokens/TokenField.d.ts +7 -0
- package/dist/inspector/tokens/TokenField.js +49 -22
- package/dist/inspector/tokens/TokenLabel.js +5 -14
- package/dist/inspector/tokens/TokensPanel.js +2 -1
- package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
- package/dist/inspector/tokens/dragPointerLock.js +70 -0
- package/dist/inspector/tokens/editActions.d.ts +1 -1
- package/dist/inspector/tokens/editActions.js +1 -1
- package/dist/inspector/ui/AtRuleContext.js +4 -6
- package/dist/inspector/ui/Button.d.ts +6 -2
- package/dist/inspector/ui/Button.js +4 -3
- package/dist/inspector/ui/ControlSurface.d.ts +2 -2
- package/dist/inspector/ui/ControlSurface.js +4 -3
- package/dist/inspector/ui/Disclosure.d.ts +22 -0
- package/dist/inspector/ui/Disclosure.js +13 -0
- package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
- package/dist/inspector/ui/InlineStyleWarning.js +13 -0
- package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
- package/dist/inspector/ui/InspectorPopover.js +4 -5
- package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
- package/dist/inspector/ui/PopoverListbox.js +8 -7
- package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
- package/dist/inspector/ui/SegmentedControl.js +2 -2
- package/dist/inspector/ui/Select.d.ts +6 -1
- package/dist/inspector/ui/Select.js +7 -11
- package/dist/inspector/ui/SideValuesField.js +1 -1
- package/dist/inspector/ui/Tooltip.d.ts +26 -0
- package/dist/inspector/ui/Tooltip.js +32 -0
- package/dist/inspector/ui/classNames.d.ts +2 -0
- package/dist/inspector/ui/classNames.js +4 -0
- package/dist/inspector/ui/customCursors.d.ts +23 -0
- package/dist/inspector/ui/customCursors.js +122 -0
- package/dist/inspector/ui/dragCursor.d.ts +6 -0
- package/dist/inspector/ui/dragCursor.js +13 -0
- package/dist/inspector/ui/portalContainer.d.ts +2 -0
- package/dist/inspector/ui/portalContainer.js +6 -0
- package/dist/inspector/ui/styles.js +34 -24
- package/dist/inspector/workspace/commands.d.ts +9 -0
- package/dist/inspector/workspace/commands.js +82 -0
- package/dist/inspector/workspace/controller.d.ts +4 -0
- package/dist/inspector/workspace/controller.js +66 -0
- package/dist/inspector/workspace/handoff.d.ts +42 -0
- package/dist/inspector/workspace/handoff.js +115 -0
- package/dist/inspector/workspace/timeline.d.ts +30 -0
- package/dist/inspector/workspace/timeline.js +108 -0
- package/dist/internal/agent-protocol.d.ts +19 -2
- package/dist/internal/agent-protocol.js +13 -0
- package/dist/nudge-ui.mjs +560 -299
- package/dist/project/artifacts.d.ts +13 -0
- package/dist/project/artifacts.js +253 -0
- package/dist/transport/artifacts.d.ts +8 -0
- package/dist/transport/artifacts.js +9 -0
- package/dist/transport/editor.d.ts +20 -1
- package/dist/transport/editor.js +44 -11
- package/dist/transport/routes.d.ts +6 -1
- package/dist/transport/routes.js +6 -1
- package/package.json +6 -2
- package/dist/inspector/changes/workspaceChanges.d.ts +0 -43
- package/dist/inspector/changes/workspaceChanges.js +0 -239
- package/dist/inspector/shell/DomNavigation.d.ts +0 -8
- package/dist/inspector/shell/DomNavigation.js +0 -30
|
@@ -1,29 +1,48 @@
|
|
|
1
|
+
import { iterationId, contentSourceUrl, contentEditTarget } from "./frameContent.js";
|
|
2
|
+
import { recordCanvasChange, discardCanvasHistory, isDraftLocked, forgetDraft } from "../changes/draftChanges.js";
|
|
3
|
+
import { targetKey } from "../drafts/model.js";
|
|
1
4
|
import { useSyncExternalStore } from "react";
|
|
5
|
+
import { DEFAULT_CAMERA, getBoardCamera, placeBoardCamera, restoreCanvasView, setCanvasMode, } from "./viewStore.js";
|
|
2
6
|
import { normalizeUrl, normalizedUrlKey } from "./normalizeUrl.js";
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
import { removeHtmlArtifact } from "../iterations/client.js";
|
|
8
|
+
import { applyLayoutShifts, boundsOf, centerCamera, fitCamera, layoutShifts, nextCardPosition, pushOverlappingCards, reinsertInRecordedOrder, iterationPosition, } from "./layout.js";
|
|
9
|
+
const frameGroups = new Map();
|
|
10
|
+
export function setFrameGroup(group) { frameGroups.set(group.id, group); }
|
|
11
|
+
export function getFrameGroup(id) { return frameGroups.get(id); }
|
|
12
|
+
export function removeFrameGroup(id) { frameGroups.delete(id); }
|
|
13
|
+
export function getFrameGroups() {
|
|
14
|
+
return [...frameGroups.values()].filter((group) => cards.some((card) => card.groupId === group.id));
|
|
15
|
+
}
|
|
16
|
+
export function isFrameGroup(value) {
|
|
17
|
+
if (!value || typeof value !== "object")
|
|
18
|
+
return false;
|
|
19
|
+
const group = value;
|
|
20
|
+
if (typeof group.id !== "string" || group.id.length > 256)
|
|
21
|
+
return false;
|
|
22
|
+
return group.kind === "agent" && typeof group.label === "string" && typeof group.agentId === "string"
|
|
23
|
+
&& Array.isArray(group.routes) && group.routes.every((route) => {
|
|
24
|
+
try {
|
|
25
|
+
return typeof route.url === "string" && new URL(route.url).origin === window.location.origin;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
5
32
|
export const CARD_GAP = 40;
|
|
6
33
|
export const FIT_ALL_PADDING = 80;
|
|
7
34
|
export const PRIMARY_CARD_INSET = 40;
|
|
8
|
-
const DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
|
|
9
35
|
function defaultViewportSize() {
|
|
10
36
|
return {
|
|
11
37
|
width: window.innerWidth || 1024,
|
|
12
38
|
height: window.innerHeight || 768,
|
|
13
39
|
};
|
|
14
40
|
}
|
|
15
|
-
let mode = "inspect";
|
|
16
|
-
let presentation = "focus";
|
|
17
|
-
let presentationTransitioning = false;
|
|
18
|
-
let presentationTransitionTimer = null;
|
|
19
41
|
let cards = [];
|
|
20
|
-
let comparisonGroups = [];
|
|
21
42
|
let focusedCardId = null;
|
|
22
43
|
let selectedCardId = null;
|
|
23
44
|
let cardIdCounter = 0;
|
|
24
|
-
let cachedBoardCamera = { ...DEFAULT_CAMERA };
|
|
25
45
|
let lastUsedCardSize = null;
|
|
26
|
-
let fitAllRan = false;
|
|
27
46
|
const listeners = new Set();
|
|
28
47
|
function subscribe(cb) {
|
|
29
48
|
listeners.add(cb);
|
|
@@ -31,102 +50,39 @@ function subscribe(cb) {
|
|
|
31
50
|
listeners.delete(cb);
|
|
32
51
|
};
|
|
33
52
|
}
|
|
34
|
-
function getMode() {
|
|
35
|
-
return mode;
|
|
36
|
-
}
|
|
37
|
-
function getPresentation() {
|
|
38
|
-
return presentation;
|
|
39
|
-
}
|
|
40
|
-
function getPresentationTransitioning() {
|
|
41
|
-
return presentationTransitioning;
|
|
42
|
-
}
|
|
43
53
|
function getCards() {
|
|
44
54
|
return cards;
|
|
45
55
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function computeNewCardPosition(existingCards, gap) {
|
|
53
|
-
if (existingCards.length === 0)
|
|
54
|
-
return { x: 0, y: 0 };
|
|
55
|
-
let rightmostEdge = -Infinity;
|
|
56
|
-
for (const c of existingCards) {
|
|
57
|
-
const edge = c.x + c.width + gap;
|
|
58
|
-
if (edge > rightmostEdge)
|
|
59
|
-
rightmostEdge = edge;
|
|
60
|
-
}
|
|
61
|
-
const rowY = existingCards[0].y;
|
|
62
|
-
return { x: rightmostEdge, y: rowY };
|
|
63
|
-
}
|
|
64
|
-
function boundsOf(cards) {
|
|
65
|
-
let minX = Infinity;
|
|
66
|
-
let minY = Infinity;
|
|
67
|
-
let maxX = -Infinity;
|
|
68
|
-
let maxY = -Infinity;
|
|
69
|
-
for (const card of cards) {
|
|
70
|
-
minX = Math.min(minX, card.x);
|
|
71
|
-
minY = Math.min(minY, card.y);
|
|
72
|
-
maxX = Math.max(maxX, card.x + card.width);
|
|
73
|
-
maxY = Math.max(maxY, card.y + card.height);
|
|
56
|
+
let notificationDepth = 0;
|
|
57
|
+
let notificationPending = false;
|
|
58
|
+
export function batchCanvasChanges(operation) {
|
|
59
|
+
notificationDepth += 1;
|
|
60
|
+
try {
|
|
61
|
+
return operation();
|
|
74
62
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return;
|
|
80
|
-
mode = newMode;
|
|
81
|
-
notify();
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Changes how the mounted iframe workspace is presented.
|
|
85
|
-
*
|
|
86
|
-
* Canvas starts from the active card centered in the supplied viewport. Card
|
|
87
|
-
* geometry remains durable, but the camera does not carry over from a prior
|
|
88
|
-
* Canvas session.
|
|
89
|
-
*/
|
|
90
|
-
export function setCanvasPresentation(next, viewport) {
|
|
91
|
-
if (presentation === next)
|
|
92
|
-
return;
|
|
93
|
-
if (next === "canvas")
|
|
94
|
-
resetCameraToActiveCard(viewport);
|
|
95
|
-
presentation = next;
|
|
96
|
-
presentationTransitioning = !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
97
|
-
if (presentationTransitionTimer !== null)
|
|
98
|
-
window.clearTimeout(presentationTransitionTimer);
|
|
99
|
-
presentationTransitionTimer = presentationTransitioning
|
|
100
|
-
? window.setTimeout(() => {
|
|
101
|
-
presentationTransitionTimer = null;
|
|
102
|
-
presentationTransitioning = false;
|
|
63
|
+
finally {
|
|
64
|
+
notificationDepth -= 1;
|
|
65
|
+
if (notificationDepth === 0 && notificationPending) {
|
|
66
|
+
notificationPending = false;
|
|
103
67
|
notify();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
notify();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
107
70
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!activeCard) {
|
|
112
|
-
cachedBoardCamera = { ...DEFAULT_CAMERA };
|
|
71
|
+
function notify() {
|
|
72
|
+
if (notificationDepth > 0) {
|
|
73
|
+
notificationPending = true;
|
|
113
74
|
return;
|
|
114
75
|
}
|
|
115
|
-
|
|
116
|
-
const view = viewport ?? defaultViewportSize();
|
|
117
|
-
cachedBoardCamera = {
|
|
118
|
-
x: view.width / 2 - (activeCard.x + activeCard.width / 2) * zoom,
|
|
119
|
-
y: view.height / 2 - (activeCard.y + activeCard.height / 2) * zoom,
|
|
120
|
-
zoom,
|
|
121
|
-
};
|
|
76
|
+
listeners.forEach((l) => l());
|
|
122
77
|
}
|
|
123
78
|
/** Adds one card while retaining the existing placement policy. */
|
|
124
79
|
export function addCanvasCard(url, title) {
|
|
80
|
+
const before = cards;
|
|
125
81
|
const size = lastUsedCardSize ?? defaultViewportSize();
|
|
126
|
-
const pos =
|
|
82
|
+
const pos = nextCardPosition(cards, CARD_GAP);
|
|
127
83
|
const card = {
|
|
128
84
|
id: `card-${++cardIdCounter}`,
|
|
129
|
-
url,
|
|
85
|
+
content: { kind: "route", url },
|
|
130
86
|
title: title ?? null,
|
|
131
87
|
x: pos.x,
|
|
132
88
|
y: pos.y,
|
|
@@ -135,65 +91,90 @@ export function addCanvasCard(url, title) {
|
|
|
135
91
|
};
|
|
136
92
|
cards = [...cards, card];
|
|
137
93
|
lastUsedCardSize = { width: size.width, height: size.height };
|
|
94
|
+
recordCreation(before, [card]);
|
|
138
95
|
notify();
|
|
139
96
|
return card;
|
|
140
97
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* Callers validate route URLs and labels at the command boundary; this store
|
|
144
|
-
* function only enforces the durable identity and placement invariants.
|
|
145
|
-
*/
|
|
146
|
-
export function appendCanvasComparisonGroup(input) {
|
|
147
|
-
if (!input.id || !input.label || !input.agentId || input.routes.length === 0)
|
|
148
|
-
return null;
|
|
149
|
-
if (comparisonGroups.some((group) => group.id === input.id))
|
|
98
|
+
export function appendAgentRouteCards(groupId, routes) {
|
|
99
|
+
if (!groupId || routes.length === 0)
|
|
150
100
|
return null;
|
|
101
|
+
const before = cards;
|
|
151
102
|
const nextCards = [];
|
|
152
103
|
let nextCardsSnapshot = cards;
|
|
153
|
-
for (const route of
|
|
104
|
+
for (const route of routes) {
|
|
154
105
|
const size = lastUsedCardSize ?? defaultViewportSize();
|
|
155
|
-
const pos =
|
|
106
|
+
const pos = nextCardPosition(nextCardsSnapshot, CARD_GAP);
|
|
156
107
|
const card = {
|
|
157
108
|
id: `card-${++cardIdCounter}`,
|
|
158
|
-
url: route.url,
|
|
109
|
+
content: { kind: "route", url: route.url },
|
|
159
110
|
title: route.title ?? null,
|
|
160
111
|
x: pos.x,
|
|
161
112
|
y: pos.y,
|
|
162
113
|
width: size.width,
|
|
163
114
|
height: size.height,
|
|
164
|
-
|
|
115
|
+
groupId: groupId,
|
|
165
116
|
};
|
|
166
117
|
nextCards.push(card);
|
|
167
118
|
nextCardsSnapshot = [...nextCardsSnapshot, card];
|
|
168
119
|
lastUsedCardSize = { width: size.width, height: size.height };
|
|
169
120
|
}
|
|
170
|
-
const group = {
|
|
171
|
-
id: input.id,
|
|
172
|
-
label: input.label,
|
|
173
|
-
owner: "agent",
|
|
174
|
-
agentId: input.agentId,
|
|
175
|
-
cardIds: nextCards.map((card) => card.id),
|
|
176
|
-
routes: input.routes.map((route) => ({ ...route })),
|
|
177
|
-
};
|
|
178
121
|
cards = [...cards, ...nextCards];
|
|
179
|
-
|
|
180
|
-
notify();
|
|
181
|
-
return { group, cards: nextCards };
|
|
182
|
-
}
|
|
183
|
-
export function getCanvasComparisonGroup(id) {
|
|
184
|
-
return comparisonGroups.find((group) => group.id === id);
|
|
185
|
-
}
|
|
186
|
-
/** Removes a group and exactly the cards owned by that group. */
|
|
187
|
-
export function removeCanvasComparisonGroup(id) {
|
|
188
|
-
const group = getCanvasComparisonGroup(id);
|
|
189
|
-
if (!group)
|
|
190
|
-
return null;
|
|
191
|
-
const groupCardIds = new Set(group.cardIds);
|
|
192
|
-
cards = cards.filter((card) => !groupCardIds.has(card.id));
|
|
193
|
-
comparisonGroups = comparisonGroups.filter((candidate) => candidate.id !== id);
|
|
194
|
-
selectFallbackAfterRemoval(groupCardIds);
|
|
122
|
+
recordCreation(before, nextCards);
|
|
195
123
|
notify();
|
|
196
|
-
return
|
|
124
|
+
return nextCards;
|
|
125
|
+
}
|
|
126
|
+
let activateHistoryCard = () => undefined;
|
|
127
|
+
/** Connects canvas replay to the draft controller without coupling the stores. */
|
|
128
|
+
export function setCanvasHistoryActivator(activate) {
|
|
129
|
+
activateHistoryCard = activate;
|
|
130
|
+
}
|
|
131
|
+
function recordCreation(before, created) {
|
|
132
|
+
const createdIds = new Set(created.map((card) => card.id));
|
|
133
|
+
const after = cards.map((card) => ({ ...card }));
|
|
134
|
+
const createdGroups = new Map(created.flatMap((card) => {
|
|
135
|
+
const group = card.groupId ? getFrameGroup(card.groupId) : undefined;
|
|
136
|
+
return group ? [[group.id, group]] : [];
|
|
137
|
+
}));
|
|
138
|
+
const oldFocus = selectedCardId ?? focusedCardId ?? before[0]?.id;
|
|
139
|
+
const shifts = layoutShifts(before, cards);
|
|
140
|
+
const restore = (redo) => {
|
|
141
|
+
if (!redo) {
|
|
142
|
+
// Direct creation drags finish placement after the creation was recorded.
|
|
143
|
+
// Redo must recover the dropped position, dimensions, and loaded title.
|
|
144
|
+
for (const [index, recorded] of after.entries()) {
|
|
145
|
+
const current = createdIds.has(recorded.id) && cards.find((card) => card.id === recorded.id);
|
|
146
|
+
if (current)
|
|
147
|
+
after[index] = { ...current };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
cards = applyLayoutShifts(cards.filter((card) => !createdIds.has(card.id)), shifts, redo);
|
|
151
|
+
if (redo)
|
|
152
|
+
cards = reinsertInRecordedOrder(cards, after, createdIds);
|
|
153
|
+
for (const [id, group] of createdGroups) {
|
|
154
|
+
if (!cards.some((card) => card.groupId === id))
|
|
155
|
+
removeFrameGroup(id);
|
|
156
|
+
else if (!getFrameGroup(id))
|
|
157
|
+
setFrameGroup(group);
|
|
158
|
+
}
|
|
159
|
+
const focus = redo ? created[0]?.id : oldFocus;
|
|
160
|
+
const target = cards.find((card) => card.id === focus) ?? cards[0];
|
|
161
|
+
focusedCardId = target?.id ?? null;
|
|
162
|
+
selectedCardId = focusedCardId;
|
|
163
|
+
if (target) {
|
|
164
|
+
activateHistoryCard(target.id);
|
|
165
|
+
focusCanvasCards([target.id]);
|
|
166
|
+
}
|
|
167
|
+
notify();
|
|
168
|
+
};
|
|
169
|
+
recordCanvasChange({
|
|
170
|
+
cardIds: [...createdIds],
|
|
171
|
+
undo: () => restore(false),
|
|
172
|
+
redo: () => restore(true),
|
|
173
|
+
dispose: () => {
|
|
174
|
+
for (const card of created)
|
|
175
|
+
releaseUnusedIteration(card);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
197
178
|
}
|
|
198
179
|
function selectFallbackAfterRemoval(removedIds) {
|
|
199
180
|
const fallbackId = cards[0]?.id ?? null;
|
|
@@ -202,65 +183,98 @@ function selectFallbackAfterRemoval(removedIds) {
|
|
|
202
183
|
if (removedIds.has(focusedCardId ?? ""))
|
|
203
184
|
focusedCardId = fallbackId;
|
|
204
185
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
186
|
+
function releaseUnusedIteration(card) {
|
|
187
|
+
const artifactId = iterationId(card.content);
|
|
188
|
+
if (!artifactId || cards.some((candidate) => iterationId(candidate.content) === artifactId))
|
|
208
189
|
return;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
190
|
+
forgetDraft(targetKey(contentEditTarget(card.content)));
|
|
191
|
+
void removeHtmlArtifact(artifactId).catch((error) => {
|
|
192
|
+
console.warn("Nudge UI could not remove an unused HTML iteration:", error);
|
|
193
|
+
});
|
|
212
194
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
195
|
+
function recordRemoval(before, removed) {
|
|
196
|
+
let snapshot = removed;
|
|
197
|
+
const group = removed.groupId ? getFrameGroup(removed.groupId) : undefined;
|
|
198
|
+
recordCanvasChange({
|
|
199
|
+
cardIds: [removed.id],
|
|
200
|
+
undo: () => {
|
|
201
|
+
cards = reinsertInRecordedOrder(cards, before.map((card) => card.id === removed.id ? snapshot : card), new Set([removed.id]));
|
|
202
|
+
if (group)
|
|
203
|
+
setFrameGroup(group);
|
|
204
|
+
selectCard(removed.id);
|
|
205
|
+
activateHistoryCard(removed.id);
|
|
206
|
+
focusCanvasCards([removed.id]);
|
|
207
|
+
},
|
|
208
|
+
redo: () => {
|
|
209
|
+
snapshot = cards.find((card) => card.id === removed.id) ?? snapshot;
|
|
210
|
+
cards = cards.filter((card) => card.id !== removed.id);
|
|
211
|
+
selectFallbackAfterRemoval(new Set([removed.id]));
|
|
212
|
+
notify();
|
|
213
|
+
},
|
|
214
|
+
dispose: () => releaseUnusedIteration(snapshot),
|
|
226
215
|
});
|
|
227
216
|
}
|
|
228
|
-
export function removeCanvasCard(id) {
|
|
217
|
+
export function removeCanvasCard(id, { undoable = false } = {}) {
|
|
218
|
+
if (isDraftLocked())
|
|
219
|
+
return;
|
|
229
220
|
const removed = cards.find((card) => card.id === id);
|
|
230
221
|
if (!removed)
|
|
231
222
|
return;
|
|
223
|
+
const before = cards;
|
|
224
|
+
if (!undoable)
|
|
225
|
+
discardCanvasHistory(id);
|
|
232
226
|
cards = cards.filter((c) => c.id !== id);
|
|
233
227
|
selectFallbackAfterRemoval(new Set([id]));
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
...group,
|
|
237
|
-
cardIds: group.cardIds.filter((_cardId, index) => index !== routeIndex),
|
|
238
|
-
routes: group.routes.filter((_route, index) => index !== routeIndex),
|
|
239
|
-
}));
|
|
240
|
-
comparisonGroups = comparisonGroups.filter((group) => group.cardIds.length > 0);
|
|
241
|
-
}
|
|
228
|
+
if (undoable)
|
|
229
|
+
recordRemoval(before, removed);
|
|
242
230
|
notify();
|
|
231
|
+
if (!undoable)
|
|
232
|
+
releaseUnusedIteration(removed);
|
|
243
233
|
}
|
|
244
234
|
/**
|
|
245
235
|
* Opens one editor target in the iframe workspace without replacing restored
|
|
246
236
|
* cards or their dimensions.
|
|
247
237
|
*/
|
|
248
|
-
export function activateIframeWorkspace(url, viewport) {
|
|
238
|
+
export function activateIframeWorkspace(url, viewport, options = {}) {
|
|
249
239
|
const normalized = normalizeUrl(url);
|
|
250
240
|
if (!normalized || normalized.origin !== window.location.origin)
|
|
251
241
|
return null;
|
|
252
242
|
const targetHref = new URL(url).href;
|
|
253
|
-
|
|
243
|
+
setCanvasMode("canvas");
|
|
244
|
+
const active = cards.find((card) => card.id === (selectedCardId ?? focusedCardId));
|
|
245
|
+
if (options.preserveIterationFocus && active?.content.kind === "iteration") {
|
|
246
|
+
const sourceRoute = normalizeUrl(active.content.sourceUrl);
|
|
247
|
+
if (sourceRoute && normalizedUrlKey(sourceRoute) === normalizedUrlKey(normalized)) {
|
|
248
|
+
focusedCardId = active.id;
|
|
249
|
+
selectedCardId = active.id;
|
|
250
|
+
notify();
|
|
251
|
+
return active;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (options.replaceActiveCard && active && !iterationId(active.content)) {
|
|
255
|
+
const oldRoute = normalizeUrl(contentSourceUrl(active.content));
|
|
256
|
+
const changedRoute = !oldRoute || normalizedUrlKey(oldRoute) !== normalizedUrlKey(normalized);
|
|
257
|
+
const next = {
|
|
258
|
+
...active,
|
|
259
|
+
content: { kind: "route", url: targetHref, ...(contentSourceUrl(active.content) !== targetHref ? { navigationUrl: targetHref } : {}) },
|
|
260
|
+
...(changedRoute ? { title: null, groupId: undefined, entrance: undefined } : {}),
|
|
261
|
+
};
|
|
262
|
+
cards = cards.map((card) => card.id === active.id ? next : card);
|
|
263
|
+
focusedCardId = active.id;
|
|
264
|
+
selectedCardId = active.id;
|
|
265
|
+
notify();
|
|
266
|
+
return next;
|
|
267
|
+
}
|
|
254
268
|
const focused = cards.find((card) => card.id === focusedCardId);
|
|
255
|
-
const focusedUrl = focused ? normalizeUrl(focused.url) : null;
|
|
269
|
+
const focusedUrl = focused?.content.kind === "route" ? normalizeUrl(focused.content.url) : null;
|
|
256
270
|
const existing = focused && focusedUrl
|
|
257
271
|
&& normalizedUrlKey(focusedUrl) === normalizedUrlKey(normalized)
|
|
258
272
|
? focused
|
|
259
|
-
: cards.find((card) => card.url === targetHref) ?? findCardByNormalizedUrl(normalized);
|
|
273
|
+
: cards.find((card) => card.content.kind === "route" && card.content.url === targetHref) ?? findCardByNormalizedUrl(normalized);
|
|
260
274
|
if (existing) {
|
|
261
275
|
let activated = existing;
|
|
262
|
-
if (existing.
|
|
263
|
-
activated = { ...existing, url: targetHref, navigationUrl: targetHref };
|
|
276
|
+
if (contentSourceUrl(existing.content) !== targetHref) {
|
|
277
|
+
activated = { ...existing, content: { kind: "route", url: targetHref, navigationUrl: targetHref } };
|
|
264
278
|
cards = cards.map((card) => card.id === existing.id ? activated : card);
|
|
265
279
|
}
|
|
266
280
|
focusedCardId = existing.id;
|
|
@@ -273,7 +287,7 @@ export function activateIframeWorkspace(url, viewport) {
|
|
|
273
287
|
const height = Math.max(150, viewport.height - PRIMARY_CARD_INSET * 2);
|
|
274
288
|
const card = {
|
|
275
289
|
id: `card-${++cardIdCounter}`,
|
|
276
|
-
url: targetHref,
|
|
290
|
+
content: { kind: "route", url: targetHref },
|
|
277
291
|
title: null,
|
|
278
292
|
x: 0,
|
|
279
293
|
y: 0,
|
|
@@ -284,8 +298,7 @@ export function activateIframeWorkspace(url, viewport) {
|
|
|
284
298
|
lastUsedCardSize = { width, height };
|
|
285
299
|
focusedCardId = card.id;
|
|
286
300
|
selectedCardId = card.id;
|
|
287
|
-
|
|
288
|
-
fitAllRan = true;
|
|
301
|
+
placeBoardCamera({ x: PRIMARY_CARD_INSET, y: PRIMARY_CARD_INSET, zoom: 1 });
|
|
289
302
|
notify();
|
|
290
303
|
return card;
|
|
291
304
|
}
|
|
@@ -296,46 +309,51 @@ export function activateIframeWorkspace(url, viewport) {
|
|
|
296
309
|
return card;
|
|
297
310
|
}
|
|
298
311
|
export function updateCardTitle(id, title) {
|
|
299
|
-
const card = cards.find((candidate) => candidate.id === id);
|
|
300
312
|
cards = cards.map((c) => (c.id === id ? { ...c, title } : c));
|
|
301
|
-
if (card?.comparisonGroupId) {
|
|
302
|
-
updateComparisonGroupRoutes(card.comparisonGroupId, id, (group, routeIndex) => ({
|
|
303
|
-
...group,
|
|
304
|
-
routes: group.routes.map((route, index) => index === routeIndex ? { ...route, title } : route),
|
|
305
|
-
}));
|
|
306
|
-
}
|
|
307
313
|
notify();
|
|
308
314
|
}
|
|
309
315
|
export function updateCardUrl(id, url) {
|
|
310
|
-
|
|
311
|
-
cards = cards.map((c) => (c.id === id ? { ...c, url } : c));
|
|
312
|
-
if (card?.comparisonGroupId) {
|
|
313
|
-
updateComparisonGroupRoutes(card.comparisonGroupId, id, (group, routeIndex) => ({
|
|
314
|
-
...group,
|
|
315
|
-
routes: group.routes.map((route, index) => index === routeIndex ? { ...route, url } : route),
|
|
316
|
-
}));
|
|
317
|
-
}
|
|
316
|
+
cards = cards.map((c) => (c.id === id && c.content.kind === "route" ? { ...c, content: { ...c.content, url } } : c));
|
|
318
317
|
notify();
|
|
319
318
|
}
|
|
320
319
|
export function duplicateCard(sourceId) {
|
|
320
|
+
const before = cards;
|
|
321
321
|
const source = cards.find((c) => c.id === sourceId);
|
|
322
|
-
if (!source)
|
|
322
|
+
if (!source || source.content.kind !== "route")
|
|
323
323
|
return null;
|
|
324
324
|
const size = lastUsedCardSize ?? { width: source.width, height: source.height };
|
|
325
|
-
const pos = computeNewCardPosition(cards, CARD_GAP);
|
|
326
325
|
const card = {
|
|
327
326
|
id: `card-${++cardIdCounter}`,
|
|
328
|
-
url: source.url,
|
|
327
|
+
content: { kind: "route", url: source.content.url },
|
|
328
|
+
entrance: "linked",
|
|
329
329
|
title: source.title,
|
|
330
|
-
x:
|
|
331
|
-
y:
|
|
330
|
+
x: source.x + source.width + CARD_GAP,
|
|
331
|
+
y: source.y,
|
|
332
332
|
width: size.width,
|
|
333
333
|
height: size.height,
|
|
334
334
|
};
|
|
335
|
-
|
|
335
|
+
const index = cards.indexOf(source);
|
|
336
|
+
cards = [...cards.slice(0, index + 1), card, ...cards.slice(index + 1)];
|
|
337
|
+
cards = pushOverlappingCards(cards, new Set([source.id, card.id]), CARD_GAP);
|
|
336
338
|
lastUsedCardSize = { width: size.width, height: size.height };
|
|
339
|
+
recordCreation(before, [card]);
|
|
337
340
|
notify();
|
|
338
|
-
return card;
|
|
341
|
+
return cards.find((c) => c.id === card.id);
|
|
342
|
+
}
|
|
343
|
+
export function addCanvasIteration(sourceId, artifactId, position, animateEntrance = true) {
|
|
344
|
+
const before = cards;
|
|
345
|
+
const source = cards.find((card) => card.id === sourceId);
|
|
346
|
+
if (!source)
|
|
347
|
+
return null;
|
|
348
|
+
const { x, y } = position ?? iterationPosition(cards, source, CARD_GAP);
|
|
349
|
+
const iteration = {
|
|
350
|
+
id: `card-${++cardIdCounter}`, content: { kind: "iteration", sourceUrl: contentSourceUrl(source.content), artifactId }, title: source.title,
|
|
351
|
+
width: source.width, height: source.height, x, y, entrance: "iteration", animateEntrance,
|
|
352
|
+
};
|
|
353
|
+
cards = [...cards, iteration];
|
|
354
|
+
recordCreation(before, [iteration]);
|
|
355
|
+
notify();
|
|
356
|
+
return iteration;
|
|
339
357
|
}
|
|
340
358
|
export function resizeCard(id, width, height, position) {
|
|
341
359
|
cards = cards.map((c) => (c.id === id
|
|
@@ -344,88 +362,33 @@ export function resizeCard(id, width, height, position) {
|
|
|
344
362
|
lastUsedCardSize = { width, height };
|
|
345
363
|
notify();
|
|
346
364
|
}
|
|
347
|
-
export function getBoardCamera() {
|
|
348
|
-
return cachedBoardCamera;
|
|
349
|
-
}
|
|
350
|
-
export function setBoardCamera(camera) {
|
|
351
|
-
cachedBoardCamera = {
|
|
352
|
-
x: camera.x,
|
|
353
|
-
y: camera.y,
|
|
354
|
-
zoom: Math.max(MIN_CAMERA_ZOOM, Math.min(MAX_CAMERA_ZOOM, camera.zoom)),
|
|
355
|
-
};
|
|
356
|
-
notify();
|
|
357
|
-
}
|
|
358
|
-
export function updateBoardCamera(partial) {
|
|
359
|
-
const next = { ...cachedBoardCamera, ...partial };
|
|
360
|
-
if (partial.zoom !== undefined) {
|
|
361
|
-
next.zoom = Math.max(MIN_CAMERA_ZOOM, Math.min(MAX_CAMERA_ZOOM, partial.zoom));
|
|
362
|
-
}
|
|
363
|
-
cachedBoardCamera = next;
|
|
364
|
-
notify();
|
|
365
|
-
}
|
|
366
365
|
export function fitAllCards(viewport) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const selectedIds = new Set(cardIds);
|
|
372
|
-
const selectedCards = cards.filter((card) => selectedIds.has(card.id));
|
|
373
|
-
if (selectedCards.length === 0)
|
|
374
|
-
return false;
|
|
375
|
-
const { minX, minY, maxX, maxY } = boundsOf(selectedCards);
|
|
376
|
-
const contentW = maxX - minX;
|
|
377
|
-
const contentH = maxY - minY;
|
|
378
|
-
if (contentW <= 0 || contentH <= 0) {
|
|
379
|
-
cachedBoardCamera = { ...DEFAULT_CAMERA };
|
|
380
|
-
fitAllRan = true;
|
|
381
|
-
notify();
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
const viewW = Math.max(1, (viewport?.width ?? window.innerWidth) - FIT_ALL_PADDING * 2);
|
|
385
|
-
const viewH = Math.max(1, (viewport?.height ?? window.innerHeight) - FIT_ALL_PADDING * 2);
|
|
386
|
-
const zoomX = viewW / contentW;
|
|
387
|
-
const zoomY = viewH / contentH;
|
|
388
|
-
const zoom = Math.max(MIN_CAMERA_ZOOM, Math.min(zoomX, zoomY, MAX_CAMERA_ZOOM));
|
|
389
|
-
const contentCenterX = minX + contentW / 2;
|
|
390
|
-
const contentCenterY = minY + contentH / 2;
|
|
391
|
-
cachedBoardCamera = {
|
|
392
|
-
x: -(contentCenterX * zoom) + (viewport?.width ?? window.innerWidth) / 2,
|
|
393
|
-
y: -(contentCenterY * zoom) + (viewport?.height ?? window.innerHeight) / 2,
|
|
394
|
-
zoom,
|
|
395
|
-
};
|
|
396
|
-
fitAllRan = true;
|
|
397
|
-
notify();
|
|
398
|
-
return true;
|
|
366
|
+
const bounds = selectedBounds(cards.map((card) => card.id));
|
|
367
|
+
if (!bounds)
|
|
368
|
+
return;
|
|
369
|
+
placeBoardCamera(fitCamera(bounds, viewport ?? windowViewport(), FIT_ALL_PADDING) ?? DEFAULT_CAMERA);
|
|
399
370
|
}
|
|
400
371
|
/** Centers a selected set of cards without changing the current zoom. */
|
|
401
372
|
export function focusCanvasCards(cardIds, viewport) {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
if (selectedCards.length === 0)
|
|
373
|
+
const bounds = selectedBounds(cardIds);
|
|
374
|
+
if (!bounds)
|
|
405
375
|
return false;
|
|
406
|
-
|
|
407
|
-
const viewW = viewport?.width ?? window.innerWidth;
|
|
408
|
-
const viewH = viewport?.height ?? window.innerHeight;
|
|
409
|
-
const contentCenterX = minX + (maxX - minX) / 2;
|
|
410
|
-
const contentCenterY = minY + (maxY - minY) / 2;
|
|
411
|
-
cachedBoardCamera = {
|
|
412
|
-
x: -(contentCenterX * cachedBoardCamera.zoom) + viewW / 2,
|
|
413
|
-
y: -(contentCenterY * cachedBoardCamera.zoom) + viewH / 2,
|
|
414
|
-
zoom: cachedBoardCamera.zoom,
|
|
415
|
-
};
|
|
416
|
-
fitAllRan = true;
|
|
417
|
-
notify();
|
|
376
|
+
placeBoardCamera(centerCamera(bounds, getBoardCamera().zoom, viewport ?? windowViewport()));
|
|
418
377
|
return true;
|
|
419
378
|
}
|
|
420
|
-
|
|
421
|
-
|
|
379
|
+
function selectedBounds(cardIds) {
|
|
380
|
+
const selectedIds = new Set(cardIds);
|
|
381
|
+
const selected = cards.filter((card) => selectedIds.has(card.id));
|
|
382
|
+
return selected.length ? boundsOf(selected) : null;
|
|
422
383
|
}
|
|
423
|
-
|
|
424
|
-
|
|
384
|
+
function windowViewport() {
|
|
385
|
+
return { width: window.innerWidth, height: window.innerHeight };
|
|
425
386
|
}
|
|
426
387
|
export function findCardByNormalizedUrl(normalized) {
|
|
427
388
|
return cards.find((c) => {
|
|
428
|
-
|
|
389
|
+
if (c.content.kind !== "route")
|
|
390
|
+
return false;
|
|
391
|
+
const n = normalizeUrl(c.content.url);
|
|
429
392
|
if (!n)
|
|
430
393
|
return false;
|
|
431
394
|
return normalizedUrlKey(n) === normalizedUrlKey(normalized);
|
|
@@ -461,20 +424,18 @@ export function setCardPosition(id, x, y) {
|
|
|
461
424
|
cards = cards.map((c) => (c.id === id ? { ...c, x, y } : c));
|
|
462
425
|
notify();
|
|
463
426
|
}
|
|
464
|
-
export function hydrateCanvasStore(newMode, newCards, newCamera,
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
routes: group.routes.map((route) => ({ ...route })),
|
|
427
|
+
export function hydrateCanvasStore(newMode, newCards, newCamera, restoredFocusedCardId = null, restoredPresentation = "focus", restoredGroups = []) {
|
|
428
|
+
frameGroups.clear();
|
|
429
|
+
for (const group of restoredGroups)
|
|
430
|
+
frameGroups.set(group.id, group);
|
|
431
|
+
cards = newCards.map((card) => ({
|
|
432
|
+
...card,
|
|
433
|
+
groupId: card.content.kind === "route" && card.groupId && frameGroups.has(card.groupId) ? card.groupId : undefined,
|
|
472
434
|
}));
|
|
473
435
|
focusedCardId = newCards.some((card) => card.id === restoredFocusedCardId)
|
|
474
436
|
? restoredFocusedCardId
|
|
475
437
|
: null;
|
|
476
438
|
selectedCardId = null;
|
|
477
|
-
cachedBoardCamera = { ...newCamera };
|
|
478
439
|
const lastCard = newCards.at(-1);
|
|
479
440
|
lastUsedCardSize = lastCard
|
|
480
441
|
? { width: lastCard.width, height: lastCard.height }
|
|
@@ -491,25 +452,31 @@ export function hydrateCanvasStore(newMode, newCards, newCamera, newComparisonGr
|
|
|
491
452
|
}
|
|
492
453
|
cardIdCounter = Math.max(cardIdCounter, maxNum);
|
|
493
454
|
}
|
|
494
|
-
|
|
455
|
+
restoreCanvasView({ mode: newMode, presentation: restoredPresentation, camera: newCamera });
|
|
495
456
|
notify();
|
|
496
457
|
}
|
|
497
|
-
export { subscribe,
|
|
498
|
-
export function useCanvasMode() {
|
|
499
|
-
return useSyncExternalStore(subscribe, getMode, getMode);
|
|
500
|
-
}
|
|
501
|
-
export function useCanvasPresentation() {
|
|
502
|
-
return useSyncExternalStore(subscribe, getPresentation, getPresentation);
|
|
503
|
-
}
|
|
504
|
-
export function useCanvasPresentationTransitioning() {
|
|
505
|
-
return useSyncExternalStore(subscribe, getPresentationTransitioning, getPresentationTransitioning);
|
|
506
|
-
}
|
|
458
|
+
export { subscribe, getCards, getCards as getCanvasCards, };
|
|
507
459
|
export function useCanvasCards() {
|
|
508
460
|
return useSyncExternalStore(subscribe, getCards, getCards);
|
|
509
461
|
}
|
|
510
|
-
|
|
511
|
-
|
|
462
|
+
function frameRoute(card) {
|
|
463
|
+
const sourceUrl = contentSourceUrl(card.content);
|
|
464
|
+
const normalized = normalizeUrl(sourceUrl);
|
|
465
|
+
if (!normalized)
|
|
466
|
+
return { key: sourceUrl, label: sourceUrl };
|
|
467
|
+
return {
|
|
468
|
+
key: `${normalized.origin}${normalized.pathname}`,
|
|
469
|
+
label: normalized.pathname,
|
|
470
|
+
};
|
|
512
471
|
}
|
|
513
|
-
|
|
514
|
-
|
|
472
|
+
/** Returns a route-based label that distinguishes copies and HTML iterations. */
|
|
473
|
+
export function getCanvasCardLabel(card, frameCards = cards) {
|
|
474
|
+
const route = frameRoute(card);
|
|
475
|
+
const candidates = frameCards.some((candidate) => candidate.id === card.id) ? frameCards : [...frameCards, card];
|
|
476
|
+
const matchingFrames = candidates.filter((candidate) => frameRoute(candidate).key === route.key);
|
|
477
|
+
const matchingKind = matchingFrames.filter((candidate) => candidate.content.kind === card.content.kind);
|
|
478
|
+
const number = matchingKind.findIndex((candidate) => candidate.id === card.id) + 1;
|
|
479
|
+
if (card.content.kind === "iteration")
|
|
480
|
+
return `${route.label} · Iteration ${number}`;
|
|
481
|
+
return number === 1 ? route.label : `${route.label} · Copy ${number}`;
|
|
515
482
|
}
|