@calca/canvas-flow 0.0.0-stage → 0.7.5
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 +38 -2
- package/dist/index.d.mts +243 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +876 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -4
package/README.md
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @calca/canvas-flow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React Flow adapter for the Calca canvas — the open canvas layer that renders a versioned document model as interactive, agent-editable frames. Turns a [`@calca/canvas-base`](https://www.npmjs.com/package/@calca/canvas-base) `CanvasStore` into a live [React Flow](https://reactflow.dev) projection, and maps user edits back into canvas commands with undo history.
|
|
4
|
+
|
|
5
|
+
Use it to build an infinite-canvas UI — design canvas, prototype board, AI generation surface, marketing layout tool — without owning the document model, ordering, undo, or change-projection logic yourself.
|
|
6
|
+
|
|
7
|
+
## What it gives you
|
|
8
|
+
|
|
9
|
+
- **`<CanvasProvider>` + `<CanvasArea>`** — a ready canvas surface: pan/zoom viewport, selection, drag, frame rendering.
|
|
10
|
+
- **`useCanvas()` / `useCanvasViewport()`** — imperative `CanvasHandle` (`fitToView`, viewport reads) inside the provider.
|
|
11
|
+
- **Store → React Flow projection** — `CanvasProjection`, `ShapeView`, `BindingView` adapt records and bindings to nodes/edges; register renderers per shape type via `ShapeView`.
|
|
12
|
+
- **Commands in, events out** — user edits become `canvas-base` commands (with origin + history), and store `change` events re-project the view.
|
|
13
|
+
- **Jotai atoms** — `canvasOffsetAtom`, `canvasScaleAtom`, `isPanningAtom`, `groupsAtom` for composing your own HUD/toolbars.
|
|
14
|
+
- **`@xyflow/*` confined here** — your app and the `@calca/*` base packages never depend on React Flow; swap renderers without touching the document layer.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { createCanvasStore } from "@calca/canvas-base";
|
|
18
|
+
import { CanvasProvider, CanvasArea } from "@calca/canvas-flow";
|
|
19
|
+
|
|
20
|
+
const store = createCanvasStore();
|
|
21
|
+
|
|
22
|
+
export function Board() {
|
|
23
|
+
return (
|
|
24
|
+
<CanvasProvider store={store}>
|
|
25
|
+
<CanvasArea />
|
|
26
|
+
</CanvasProvider>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Stack
|
|
32
|
+
|
|
33
|
+
TypeScript · React 19 · React Flow 12 (`@xyflow/react`) · Jotai. Pairs with `@calca/canvas-base` (document model), `@calca/canvas-ui` (headless chrome), `@calca/mcp` (drive the same canvas from an AI agent over MCP).
|
|
34
|
+
|
|
35
|
+
Part of [Calca](https://calca.illo.fyi) — open source, BYOK AI canvas for the web and desktop.
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
Apache-2.0
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Edge, Node, NodeChange, ReactFlowProps } from "@xyflow/react";
|
|
2
|
+
import "@xyflow/react/dist/style.css";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import { BindingRecord, CanvasStore, Command, ShapeRecord } from "@calca/canvas-base";
|
|
5
|
+
import { CanvasImage, Comment, DesignIteration, GenerationGroup, PipelineStatus, Point, ToolMode } from "@calca/shared";
|
|
6
|
+
//#region src/hooks/use-canvas.d.ts
|
|
7
|
+
interface Point$1 {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
}
|
|
11
|
+
type CanvasHandle = {
|
|
12
|
+
offset: Point$1;
|
|
13
|
+
scale: number;
|
|
14
|
+
resetView: () => void;
|
|
15
|
+
zoomIn: () => void;
|
|
16
|
+
zoomOut: () => void;
|
|
17
|
+
fitToView: () => void;
|
|
18
|
+
zoomToFit: (bounds: {
|
|
19
|
+
minX: number;
|
|
20
|
+
minY: number;
|
|
21
|
+
maxX: number;
|
|
22
|
+
maxY: number;
|
|
23
|
+
}) => void;
|
|
24
|
+
screenToCanvas: (screenX: number, screenY: number, rect: DOMRect) => Point$1;
|
|
25
|
+
};
|
|
26
|
+
export declare function useCanvasViewport(): CanvasHandle;
|
|
27
|
+
/** Backward-compatible alias for code that still imports `useCanvas`. */
|
|
28
|
+
export declare const useCanvas: typeof useCanvasViewport;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/components/CanvasArea.d.ts
|
|
31
|
+
type RubberBand = {
|
|
32
|
+
startX: number;
|
|
33
|
+
startY: number;
|
|
34
|
+
currentX: number;
|
|
35
|
+
currentY: number;
|
|
36
|
+
} | null;
|
|
37
|
+
type Updater<T> = T | ((prev: T) => T);
|
|
38
|
+
interface CanvasAreaProps {
|
|
39
|
+
/** @deprecated CanvasArea now uses React Flow's native viewport; this prop is kept for API compatibility. */
|
|
40
|
+
canvas: CanvasHandle;
|
|
41
|
+
groups: GenerationGroup[];
|
|
42
|
+
onGroupsChange: (update: Updater<GenerationGroup[]>) => void;
|
|
43
|
+
canvasImages: CanvasImage[];
|
|
44
|
+
onCanvasImagesChange: (update: Updater<CanvasImage[]>) => void;
|
|
45
|
+
selectedIds: Set<string>;
|
|
46
|
+
onSelectedIdsChange: (update: Updater<Set<string>>) => void;
|
|
47
|
+
toolMode: ToolMode;
|
|
48
|
+
spaceHeld: boolean;
|
|
49
|
+
/** @deprecated Rubber-band selection is handled by React Flow's built-in selection mode. */
|
|
50
|
+
rubberBand?: RubberBand;
|
|
51
|
+
/** @deprecated Rubber-band selection is handled by React Flow's built-in selection mode. */
|
|
52
|
+
setRubberBand?: (update: Updater<RubberBand>) => void;
|
|
53
|
+
draggingId: string | null;
|
|
54
|
+
setDraggingId: (id: string | null) => void;
|
|
55
|
+
draggingImageId: string | null;
|
|
56
|
+
setDraggingImageId: (id: string | null) => void;
|
|
57
|
+
pipelineStages?: Record<string, PipelineStatus>;
|
|
58
|
+
onAddComment: (draft: {
|
|
59
|
+
iterationId: string;
|
|
60
|
+
position: Point;
|
|
61
|
+
screenX: number;
|
|
62
|
+
screenY: number;
|
|
63
|
+
}) => void;
|
|
64
|
+
onClickComment: (comment: Comment, iterationId: string) => void;
|
|
65
|
+
onImageDrop?: (files: File[], dropX?: number, dropY?: number) => void;
|
|
66
|
+
onContextMenu?: (e: React.MouseEvent) => void;
|
|
67
|
+
emptyTitle?: string;
|
|
68
|
+
emptyDescription?: string;
|
|
69
|
+
toolbar?: React.ReactNode;
|
|
70
|
+
}
|
|
71
|
+
export declare const CanvasArea: ({ groups, onGroupsChange, canvasImages, onCanvasImagesChange, selectedIds, onSelectedIdsChange, toolMode, spaceHeld, draggingId: _draggingId, setDraggingId, draggingImageId: _draggingImageId, setDraggingImageId, pipelineStages, onAddComment, onClickComment, onImageDrop, onContextMenu, emptyTitle, emptyDescription, toolbar }: CanvasAreaProps) => import("react/jsx-runtime").JSX.Element;
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/components/CanvasProvider.d.ts
|
|
74
|
+
interface CanvasProviderProps {
|
|
75
|
+
children: ReactNode;
|
|
76
|
+
}
|
|
77
|
+
export declare function CanvasProvider({ children }: CanvasProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/lib/frame-clipboard.d.ts
|
|
80
|
+
/** Frames data stored in the internal Jotai clipboard atom */
|
|
81
|
+
interface ClipboardFramesData {
|
|
82
|
+
type: "frames";
|
|
83
|
+
/** Groups (each group becomes a new group on paste) */
|
|
84
|
+
groups: GenerationGroup[];
|
|
85
|
+
/** Standalone canvas images copied with the frames */
|
|
86
|
+
images: CanvasImage[];
|
|
87
|
+
/** Relative positions (iteration id → {dx, dy} from bounding-box origin) */
|
|
88
|
+
relativePositions: Record<string, Point>;
|
|
89
|
+
/** Bounding-box origin of the original selection */
|
|
90
|
+
origin: Point;
|
|
91
|
+
createdAt: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Serializes selected iterations + their images into the internal clipboard format.
|
|
95
|
+
* Returns null if no iterations were selected.
|
|
96
|
+
*/
|
|
97
|
+
export declare function copyFrames(selectedIds: Set<string>, groups: GenerationGroup[], images: CanvasImage[]): ClipboardFramesData | null;
|
|
98
|
+
/**
|
|
99
|
+
* Deserializes clipboard data and returns new groups + images positioned at viewportCenter.
|
|
100
|
+
* Assigns fresh unique IDs to all created entities.
|
|
101
|
+
*/
|
|
102
|
+
export declare function pasteFrames(clipboardData: ClipboardFramesData, viewportCenter: Point): {
|
|
103
|
+
groups: GenerationGroup[];
|
|
104
|
+
images: CanvasImage[];
|
|
105
|
+
};
|
|
106
|
+
export declare function cutFrames(selectedIds: Set<string>, groups: GenerationGroup[], images: CanvasImage[]): {
|
|
107
|
+
clipboardData: ClipboardFramesData | null;
|
|
108
|
+
groups: GenerationGroup[];
|
|
109
|
+
images: CanvasImage[];
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/state/canvas-atoms.d.ts
|
|
113
|
+
export declare const canvasOffsetAtom: import("jotai").PrimitiveAtom<Point> & {
|
|
114
|
+
init: Point;
|
|
115
|
+
};
|
|
116
|
+
export declare const canvasScaleAtom: import("jotai").PrimitiveAtom<number> & {
|
|
117
|
+
init: number;
|
|
118
|
+
};
|
|
119
|
+
export declare const isPanningAtom: import("jotai").PrimitiveAtom<boolean> & {
|
|
120
|
+
init: boolean;
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/state/groups-atom.d.ts
|
|
124
|
+
export declare const groupsAtom: import("jotai").WritableAtom<GenerationGroup[], [update: GenerationGroup[] | ((prev: GenerationGroup[]) => GenerationGroup[])], void>;
|
|
125
|
+
export declare const resetSessionAtom: import("jotai").WritableAtom<null, [], void> & {
|
|
126
|
+
init: null;
|
|
127
|
+
};
|
|
128
|
+
export declare function hydrateGroups(setGroups: (g: GenerationGroup[] | ((p: GenerationGroup[]) => GenerationGroup[])) => void): Promise<void>;
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/adapter/types.d.ts
|
|
131
|
+
/**
|
|
132
|
+
* Maps one ShapeRecord `type` onto a React Flow node. `ctx` is whatever
|
|
133
|
+
* render context the host passes to `<FlowCanvas data={ctx}>` — selection
|
|
134
|
+
* sets, mode flags, callbacks — and is handed to `data` for the node
|
|
135
|
+
* component.
|
|
136
|
+
*/
|
|
137
|
+
interface ShapeView<C> {
|
|
138
|
+
/** React Flow `nodeTypes` key this record type renders as. */
|
|
139
|
+
nodeType: string;
|
|
140
|
+
position: (record: ShapeRecord) => {
|
|
141
|
+
x: number;
|
|
142
|
+
y: number;
|
|
143
|
+
};
|
|
144
|
+
size?: (record: ShapeRecord) => {
|
|
145
|
+
width?: number;
|
|
146
|
+
height?: number;
|
|
147
|
+
};
|
|
148
|
+
data: (record: ShapeRecord, ctx: C) => Record<string, unknown>;
|
|
149
|
+
selected?: (record: ShapeRecord, ctx: C) => boolean;
|
|
150
|
+
draggable?: (record: ShapeRecord, ctx: C) => boolean;
|
|
151
|
+
selectable?: (record: ShapeRecord, ctx: C) => boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Props patch written back when the user drags this node. Defaults to
|
|
154
|
+
* `{ position }`.
|
|
155
|
+
*/
|
|
156
|
+
writePosition?: (record: ShapeRecord, position: {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
}) => Record<string, unknown>;
|
|
160
|
+
}
|
|
161
|
+
/** Maps one BindingRecord `type` onto a React Flow edge. */
|
|
162
|
+
interface BindingView<C> {
|
|
163
|
+
edge: (record: BindingRecord, ctx: C) => Partial<Edge>;
|
|
164
|
+
}
|
|
165
|
+
interface CanvasViews<C> {
|
|
166
|
+
shapes?: Record<string, ShapeView<C>>;
|
|
167
|
+
bindings?: Record<string, BindingView<C>>;
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/adapter/FlowCanvas.d.ts
|
|
171
|
+
interface FlowCanvasProps<C> extends Omit<ReactFlowProps, "nodes" | "edges"> {
|
|
172
|
+
/** Record-type → React Flow node/edge views. */
|
|
173
|
+
views: CanvasViews<C>;
|
|
174
|
+
/** Opaque context handed to every view's data/selectors. */
|
|
175
|
+
data: C;
|
|
176
|
+
/** React Flow `nodeTypes` map (components live outside the adapter). */
|
|
177
|
+
nodeTypes: ReactFlowProps["nodeTypes"];
|
|
178
|
+
/**
|
|
179
|
+
* Called after RF changes have been translated into commands and applied
|
|
180
|
+
* to the store (origin "user"). Use it to keep host state (e.g. jotai
|
|
181
|
+
* persistence) in sync with position/removal changes.
|
|
182
|
+
*/
|
|
183
|
+
onCommandsApplied?: (commands: Command[], changes: NodeChange[]) => void;
|
|
184
|
+
/** Selection is UI state, not document state — surfaced via callback. */
|
|
185
|
+
onSelectionChange?: ReactFlowProps["onSelectionChange"];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Generic store → React Flow adapter. The document is the source of truth;
|
|
189
|
+
* RF transient state (in-progress drags, selection) stays local.
|
|
190
|
+
*/
|
|
191
|
+
export declare const FlowCanvas: <C>({ views, data, nodeTypes, onCommandsApplied, onSelectionChange, children, ...rfProps }: FlowCanvasProps<C>) => import("react/jsx-runtime").JSX.Element;
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/adapter/projection.d.ts
|
|
194
|
+
interface CanvasProjection {
|
|
195
|
+
nodes: Node[];
|
|
196
|
+
edges: Edge[];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Projects the document into React Flow nodes/edges. Shapes render in
|
|
200
|
+
* fractional-index order (document order = paint order = z-order).
|
|
201
|
+
*/
|
|
202
|
+
export declare const projectStore: <C>(store: CanvasStore, views: CanvasViews<C>, ctx: C) => CanvasProjection;
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/adapter/store-context.d.ts
|
|
205
|
+
interface CanvasStoreProviderProps {
|
|
206
|
+
store?: CanvasStore;
|
|
207
|
+
children: ReactNode;
|
|
208
|
+
}
|
|
209
|
+
export declare const CanvasStoreProvider: ({ store, children }: CanvasStoreProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
210
|
+
/** The store scoped to the nearest provider, or null outside one. */
|
|
211
|
+
export declare const useCanvasStore: () => CanvasStore | null;
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/adapter/use-canvas-nodes.d.ts
|
|
214
|
+
/**
|
|
215
|
+
* Live projection of the document into React Flow nodes/edges. Recomputes
|
|
216
|
+
* when the store emits `change` (or when `views`/`ctx` identity changes).
|
|
217
|
+
* Requires a store: pass one directly or render inside CanvasStoreProvider.
|
|
218
|
+
*/
|
|
219
|
+
export declare const useCanvasNodes: <C>(views: CanvasViews<C>, ctx: C, storeOverride?: CanvasStore) => CanvasProjection;
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/views.d.ts
|
|
222
|
+
export declare const DESIGN_FRAME_TYPE = "design-frame";
|
|
223
|
+
export declare const CANVAS_IMAGE_TYPE = "canvas-image";
|
|
224
|
+
/** Render context the app hands to `<FlowCanvas data={ctx}>`. */
|
|
225
|
+
interface CalcaCanvasContext {
|
|
226
|
+
isSelectMode: boolean;
|
|
227
|
+
isCommentMode: boolean;
|
|
228
|
+
selectedIds: Set<string>;
|
|
229
|
+
pipelineStages?: Record<string, PipelineStatus>;
|
|
230
|
+
onAddComment: (draft: {
|
|
231
|
+
iterationId: string;
|
|
232
|
+
position: Point;
|
|
233
|
+
screenX: number;
|
|
234
|
+
screenY: number;
|
|
235
|
+
}) => void;
|
|
236
|
+
onClickComment: (comment: Comment, iterationId: string) => void;
|
|
237
|
+
}
|
|
238
|
+
export declare const iterationToRecord: (iteration: DesignIteration, groupId: string, index: string) => ShapeRecord;
|
|
239
|
+
export declare const imageToRecord: (image: CanvasImage, index: string) => ShapeRecord;
|
|
240
|
+
export declare const calcaViews: CanvasViews<CalcaCanvasContext>;
|
|
241
|
+
//#endregion
|
|
242
|
+
export type { BindingView, CalcaCanvasContext, CanvasAreaProps, CanvasHandle, CanvasProjection, CanvasViews, ClipboardFramesData, ShapeView };
|
|
243
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":["Point"],"sources":["../src/hooks/use-canvas.ts","../src/components/CanvasArea.tsx","../src/components/CanvasProvider.tsx","../src/lib/frame-clipboard.ts","../src/state/canvas-atoms.ts","../src/state/groups-atom.ts","../src/adapter/types.ts","../src/adapter/FlowCanvas.tsx","../src/adapter/projection.ts","../src/adapter/store-context.tsx","../src/adapter/use-canvas-nodes.ts","../src/views.ts"],"mappings":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
import { Background, Panel, ReactFlow, ReactFlowProvider, SelectionMode, applyNodeChanges, useReactFlow, useViewport } from "@xyflow/react";
|
|
2
|
+
import "@xyflow/react/dist/style.css";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { createCanvasStore, generateKeyBetween } from "@calca/canvas-base";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { DEFAULT_FRAME_WIDTH, DesignFrame, PipelineStatusOverlay } from "@calca/canvas-ui";
|
|
7
|
+
import { atom, useSetAtom } from "jotai";
|
|
8
|
+
|
|
9
|
+
//#region src/adapter/store-context.tsx
|
|
10
|
+
const CanvasStoreContext = createContext(null);
|
|
11
|
+
const CanvasStoreProvider = ({ store, children }) => {
|
|
12
|
+
const [owned] = useState(() => store ?? createCanvasStore());
|
|
13
|
+
return /* @__PURE__ */ jsx(CanvasStoreContext.Provider, {
|
|
14
|
+
value: owned,
|
|
15
|
+
children
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
/** The store scoped to the nearest provider, or null outside one. */
|
|
19
|
+
const useCanvasStore = () => useContext(CanvasStoreContext);
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/adapter/projection.ts
|
|
23
|
+
/**
|
|
24
|
+
* Projects the document into React Flow nodes/edges. Shapes render in
|
|
25
|
+
* fractional-index order (document order = paint order = z-order).
|
|
26
|
+
*/
|
|
27
|
+
const projectStore = (store, views, ctx) => {
|
|
28
|
+
const nodes = [];
|
|
29
|
+
const shapes = store.shapes().toSorted((a, b) => a.index < b.index ? -1 : 1);
|
|
30
|
+
for (const record of shapes) {
|
|
31
|
+
const view = views.shapes?.[record.type];
|
|
32
|
+
if (!view) continue;
|
|
33
|
+
nodes.push({
|
|
34
|
+
id: record.id,
|
|
35
|
+
type: view.nodeType,
|
|
36
|
+
position: view.position(record),
|
|
37
|
+
...view.size?.(record),
|
|
38
|
+
data: view.data(record, ctx),
|
|
39
|
+
selected: view.selected?.(record, ctx) ?? false,
|
|
40
|
+
draggable: view.draggable?.(record, ctx) ?? true,
|
|
41
|
+
selectable: view.selectable?.(record, ctx) ?? true
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
nodes,
|
|
46
|
+
edges: store.bindings().toSorted((a, b) => a.index < b.index ? -1 : 1).map((record) => Object.assign({
|
|
47
|
+
id: record.id,
|
|
48
|
+
source: record.fromId,
|
|
49
|
+
target: record.toId
|
|
50
|
+
}, views.bindings?.[record.type]?.edge(record, ctx)))
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/adapter/use-canvas-nodes.ts
|
|
56
|
+
/**
|
|
57
|
+
* Live projection of the document into React Flow nodes/edges. Recomputes
|
|
58
|
+
* when the store emits `change` (or when `views`/`ctx` identity changes).
|
|
59
|
+
* Requires a store: pass one directly or render inside CanvasStoreProvider.
|
|
60
|
+
*/
|
|
61
|
+
const useCanvasNodes = (views, ctx, storeOverride) => {
|
|
62
|
+
const contextStore = useCanvasStore();
|
|
63
|
+
const store = storeOverride ?? contextStore;
|
|
64
|
+
if (!store) throw new Error("useCanvasNodes requires a CanvasStore (prop or CanvasStoreProvider)");
|
|
65
|
+
const version = useRef(0);
|
|
66
|
+
const subscribe = useCallback((notify) => {
|
|
67
|
+
const listener = () => {
|
|
68
|
+
version.current += 1;
|
|
69
|
+
notify();
|
|
70
|
+
};
|
|
71
|
+
store.addEventListener("change", listener);
|
|
72
|
+
return () => store.removeEventListener("change", listener);
|
|
73
|
+
}, [store]);
|
|
74
|
+
const v = useSyncExternalStore(subscribe, () => version.current);
|
|
75
|
+
return useMemo(() => projectStore(store, views, ctx), [
|
|
76
|
+
store,
|
|
77
|
+
views,
|
|
78
|
+
ctx,
|
|
79
|
+
v
|
|
80
|
+
]);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/adapter/FlowCanvas.tsx
|
|
85
|
+
/**
|
|
86
|
+
* Generic store → React Flow adapter. The document is the source of truth;
|
|
87
|
+
* RF transient state (in-progress drags, selection) stays local.
|
|
88
|
+
*/
|
|
89
|
+
const FlowCanvas = ({ views, data, nodeTypes, onCommandsApplied, onSelectionChange, children, ...rfProps }) => {
|
|
90
|
+
const store = useCanvasStore();
|
|
91
|
+
if (!store) throw new Error("FlowCanvas must render inside a CanvasStoreProvider");
|
|
92
|
+
const projected = useCanvasNodes(views, data, store);
|
|
93
|
+
const [nodes, setNodes] = useState(projected.nodes);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
setNodes((prev) => {
|
|
96
|
+
const byId = new Map(prev.map((n) => [n.id, n]));
|
|
97
|
+
return projected.nodes.map((n) => {
|
|
98
|
+
const existing = byId.get(n.id);
|
|
99
|
+
if (!existing) return n;
|
|
100
|
+
return {
|
|
101
|
+
...n,
|
|
102
|
+
position: existing.position,
|
|
103
|
+
selected: existing.selected,
|
|
104
|
+
data: {
|
|
105
|
+
...n.data,
|
|
106
|
+
isDragging: existing.data?.isDragging ?? false
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}, [projected.nodes]);
|
|
112
|
+
const shapeById = useMemo(() => {
|
|
113
|
+
const map = /* @__PURE__ */ new Map();
|
|
114
|
+
for (const s of store.shapes()) map.set(s.id, s);
|
|
115
|
+
return map;
|
|
116
|
+
}, [store, projected]);
|
|
117
|
+
const onNodesChange = useCallback((changes) => {
|
|
118
|
+
setNodes((prev) => applyNodeChanges(changes, prev));
|
|
119
|
+
const applied = [];
|
|
120
|
+
for (const change of changes) {
|
|
121
|
+
if (change.type !== "position" && change.type !== "remove") continue;
|
|
122
|
+
const record = shapeById.get(change.id);
|
|
123
|
+
if (!record) continue;
|
|
124
|
+
const view = views.shapes?.[record.type];
|
|
125
|
+
if (change.type === "position" && change.position) {
|
|
126
|
+
const props = view?.writePosition?.(record, change.position) ?? { position: change.position };
|
|
127
|
+
const command = {
|
|
128
|
+
op: "update",
|
|
129
|
+
id: change.id,
|
|
130
|
+
props
|
|
131
|
+
};
|
|
132
|
+
applied.push(command);
|
|
133
|
+
store.apply([command], {
|
|
134
|
+
origin: "user",
|
|
135
|
+
history: change.dragging === false
|
|
136
|
+
});
|
|
137
|
+
} else if (change.type === "remove") {
|
|
138
|
+
const command = {
|
|
139
|
+
op: "delete",
|
|
140
|
+
id: change.id
|
|
141
|
+
};
|
|
142
|
+
applied.push(command);
|
|
143
|
+
store.apply([command], { origin: "user" });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (applied.length > 0) onCommandsApplied?.(applied, changes);
|
|
147
|
+
}, [
|
|
148
|
+
shapeById,
|
|
149
|
+
store,
|
|
150
|
+
views,
|
|
151
|
+
onCommandsApplied
|
|
152
|
+
]);
|
|
153
|
+
const onConnect = useCallback((connection) => {
|
|
154
|
+
if (!connection.source || !connection.target) return;
|
|
155
|
+
const record = {
|
|
156
|
+
id: `binding-${crypto.randomUUID()}`,
|
|
157
|
+
type: "edge",
|
|
158
|
+
fromId: connection.source,
|
|
159
|
+
toId: connection.target,
|
|
160
|
+
index: generateKeyBetween(null, null),
|
|
161
|
+
props: {}
|
|
162
|
+
};
|
|
163
|
+
store.apply([{
|
|
164
|
+
op: "create-binding",
|
|
165
|
+
record
|
|
166
|
+
}], { origin: "user" });
|
|
167
|
+
}, [store]);
|
|
168
|
+
return /* @__PURE__ */ jsx(ReactFlow, {
|
|
169
|
+
nodes,
|
|
170
|
+
edges: projected.edges,
|
|
171
|
+
nodeTypes,
|
|
172
|
+
onNodesChange,
|
|
173
|
+
onConnect,
|
|
174
|
+
onSelectionChange,
|
|
175
|
+
...rfProps,
|
|
176
|
+
children
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/views.ts
|
|
182
|
+
const DESIGN_FRAME_TYPE = "design-frame";
|
|
183
|
+
const CANVAS_IMAGE_TYPE = "canvas-image";
|
|
184
|
+
const iterationToRecord = (iteration, groupId, index) => ({
|
|
185
|
+
id: iteration.id,
|
|
186
|
+
type: DESIGN_FRAME_TYPE,
|
|
187
|
+
parentId: null,
|
|
188
|
+
index,
|
|
189
|
+
props: {
|
|
190
|
+
position: iteration.position,
|
|
191
|
+
width: iteration.width,
|
|
192
|
+
height: iteration.height,
|
|
193
|
+
isLoading: iteration.isLoading,
|
|
194
|
+
groupId,
|
|
195
|
+
iteration
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
const imageToRecord = (image, index) => ({
|
|
199
|
+
id: image.id,
|
|
200
|
+
type: CANVAS_IMAGE_TYPE,
|
|
201
|
+
parentId: null,
|
|
202
|
+
index,
|
|
203
|
+
props: {
|
|
204
|
+
position: image.position,
|
|
205
|
+
width: image.width,
|
|
206
|
+
height: image.height,
|
|
207
|
+
image
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const propsPoint = (record) => record.props.position;
|
|
211
|
+
const propsSize = (record) => {
|
|
212
|
+
const { width, height } = record.props;
|
|
213
|
+
return {
|
|
214
|
+
width,
|
|
215
|
+
height
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
const calcaViews = { shapes: {
|
|
219
|
+
[DESIGN_FRAME_TYPE]: {
|
|
220
|
+
nodeType: "designFrame",
|
|
221
|
+
position: propsPoint,
|
|
222
|
+
size: (record) => {
|
|
223
|
+
const { width, height, isLoading } = record.props;
|
|
224
|
+
return {
|
|
225
|
+
width: width ?? 480,
|
|
226
|
+
height: isLoading ? 320 : height ?? 320
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
data: (record, ctx) => {
|
|
230
|
+
const { groupId, iteration } = record.props;
|
|
231
|
+
return {
|
|
232
|
+
groupId,
|
|
233
|
+
isCommentMode: ctx.isCommentMode,
|
|
234
|
+
isDragging: false,
|
|
235
|
+
isSelectMode: ctx.isSelectMode,
|
|
236
|
+
iteration,
|
|
237
|
+
onAddComment: ctx.onAddComment,
|
|
238
|
+
onClickComment: ctx.onClickComment,
|
|
239
|
+
pipelineStatus: ctx.pipelineStages?.[record.id]
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
selected: (record, ctx) => ctx.selectedIds.has(record.id),
|
|
243
|
+
draggable: (_record, ctx) => ctx.isSelectMode,
|
|
244
|
+
selectable: (_record, ctx) => ctx.isSelectMode
|
|
245
|
+
},
|
|
246
|
+
[CANVAS_IMAGE_TYPE]: {
|
|
247
|
+
nodeType: "canvasImage",
|
|
248
|
+
position: propsPoint,
|
|
249
|
+
size: propsSize,
|
|
250
|
+
data: (record, ctx) => ({
|
|
251
|
+
image: record.props.image,
|
|
252
|
+
isDragging: false,
|
|
253
|
+
isSelectMode: ctx.isSelectMode,
|
|
254
|
+
isSelected: ctx.selectedIds.has(record.id)
|
|
255
|
+
}),
|
|
256
|
+
selected: (record, ctx) => ctx.selectedIds.has(record.id),
|
|
257
|
+
draggable: (_record, ctx) => ctx.isSelectMode,
|
|
258
|
+
selectable: (_record, ctx) => ctx.isSelectMode
|
|
259
|
+
}
|
|
260
|
+
} };
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/components/nodes/CanvasImageNode.tsx
|
|
264
|
+
function CanvasImageNode({ data }) {
|
|
265
|
+
const { image, isSelectMode, isDragging, isSelected } = data;
|
|
266
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
267
|
+
className: `group w-full h-full rounded-lg overflow-hidden shadow-md transition-shadow ${isSelected ? "ring-2 ring-blue-500 border-blue-400/50 shadow-lg" : "border border-white/40 hover:shadow-lg"} ${isSelectMode ? isDragging ? "cursor-grabbing shadow-xl ring-2 ring-blue-400/30" : "cursor-grab" : ""}`,
|
|
268
|
+
children: [/* @__PURE__ */ jsx("img", {
|
|
269
|
+
src: image.dataUrl,
|
|
270
|
+
alt: image.name,
|
|
271
|
+
className: "w-full h-full object-cover",
|
|
272
|
+
draggable: false
|
|
273
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
274
|
+
className: "absolute bottom-1 left-1 right-1 text-[9px] text-white bg-black/50 rounded px-1 py-0.5 truncate opacity-0 group-hover:opacity-100 transition-opacity",
|
|
275
|
+
children: image.name
|
|
276
|
+
})]
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/components/nodes/DesignFrameNode.tsx
|
|
282
|
+
function DesignFrameNode({ data, selected }) {
|
|
283
|
+
const { iteration, isCommentMode, isSelectMode, pipelineStatus, onAddComment, onClickComment } = data;
|
|
284
|
+
const { zoom } = useViewport();
|
|
285
|
+
const handleAddComment = (iterationId, position, screenX, screenY) => {
|
|
286
|
+
onAddComment({
|
|
287
|
+
iterationId,
|
|
288
|
+
position,
|
|
289
|
+
screenX,
|
|
290
|
+
screenY
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
const frameWidth = iteration.width || DEFAULT_FRAME_WIDTH;
|
|
294
|
+
const frameHeight = iteration.isLoading ? 320 : iteration.height || 320;
|
|
295
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
296
|
+
style: { width: frameWidth },
|
|
297
|
+
children: [
|
|
298
|
+
/* @__PURE__ */ jsx("div", {
|
|
299
|
+
className: "mb-2 flex items-center gap-2 group/label pointer-events-none",
|
|
300
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
301
|
+
className: "text-xs font-medium text-gray-500/80 bg-white/60 backdrop-blur-sm px-2.5 py-0.5 rounded-lg border border-white/40",
|
|
302
|
+
children: iteration.label
|
|
303
|
+
})
|
|
304
|
+
}),
|
|
305
|
+
/* @__PURE__ */ jsx(DesignFrame, {
|
|
306
|
+
iteration,
|
|
307
|
+
width: frameWidth,
|
|
308
|
+
isCommentMode,
|
|
309
|
+
isSelectMode,
|
|
310
|
+
isDragging: data.isDragging,
|
|
311
|
+
isSelected: selected,
|
|
312
|
+
scale: zoom,
|
|
313
|
+
onAddComment: handleAddComment,
|
|
314
|
+
onClickComment
|
|
315
|
+
}),
|
|
316
|
+
pipelineStatus && pipelineStatus.stage !== "done" && /* @__PURE__ */ jsx(PipelineStatusOverlay, {
|
|
317
|
+
status: pipelineStatus,
|
|
318
|
+
x: 0,
|
|
319
|
+
y: 0,
|
|
320
|
+
width: frameWidth,
|
|
321
|
+
frameHeight
|
|
322
|
+
})
|
|
323
|
+
]
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/components/CanvasArea.tsx
|
|
329
|
+
const nodeTypes = {
|
|
330
|
+
canvasImage: CanvasImageNode,
|
|
331
|
+
designFrame: DesignFrameNode
|
|
332
|
+
};
|
|
333
|
+
const propsEqual = (a, b) => {
|
|
334
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
335
|
+
for (const key of keys) if (!Object.is(a[key], b[key])) return false;
|
|
336
|
+
return true;
|
|
337
|
+
};
|
|
338
|
+
const CanvasArea = ({ groups, onGroupsChange, canvasImages, onCanvasImagesChange, selectedIds, onSelectedIdsChange, toolMode, spaceHeld, draggingId: _draggingId, setDraggingId, draggingImageId: _draggingImageId, setDraggingImageId, pipelineStages, onAddComment, onClickComment, onImageDrop, onContextMenu, emptyTitle, emptyDescription, toolbar }) => {
|
|
339
|
+
const store = useCanvasStore();
|
|
340
|
+
if (!store) throw new Error("CanvasArea must render inside a CanvasStoreProvider");
|
|
341
|
+
const isSelectMode = toolMode === "select" && !spaceHeld;
|
|
342
|
+
const isCommentMode = toolMode === "comment" && !spaceHeld;
|
|
343
|
+
const prevRecords = useRef(/* @__PURE__ */ new Map());
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
const next = /* @__PURE__ */ new Map();
|
|
346
|
+
const prev = prevRecords.current;
|
|
347
|
+
const indexOf = (id) => prev.get(id)?.index ?? store.nextIndex(null);
|
|
348
|
+
for (const group of groups) for (const iteration of group.iterations) next.set(iteration.id, iterationToRecord(iteration, group.id, indexOf(iteration.id)));
|
|
349
|
+
for (const image of canvasImages) next.set(image.id, imageToRecord(image, indexOf(image.id)));
|
|
350
|
+
const commands = [];
|
|
351
|
+
for (const [id, record] of next) {
|
|
352
|
+
const existing = prev.get(id);
|
|
353
|
+
if (!existing) commands.push({
|
|
354
|
+
op: "create-shape",
|
|
355
|
+
record
|
|
356
|
+
});
|
|
357
|
+
else if (!propsEqual(existing.props, record.props)) commands.push({
|
|
358
|
+
op: "update",
|
|
359
|
+
id,
|
|
360
|
+
props: record.props
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
for (const id of prev.keys()) if (!next.has(id)) commands.push({
|
|
364
|
+
op: "delete",
|
|
365
|
+
id
|
|
366
|
+
});
|
|
367
|
+
prevRecords.current = next;
|
|
368
|
+
if (commands.length > 0) store.apply(commands, {
|
|
369
|
+
origin: "remote",
|
|
370
|
+
history: false
|
|
371
|
+
});
|
|
372
|
+
}, [
|
|
373
|
+
groups,
|
|
374
|
+
canvasImages,
|
|
375
|
+
store
|
|
376
|
+
]);
|
|
377
|
+
const onCommandsApplied = useCallback((commands, _changes) => {
|
|
378
|
+
const positionUpdates = /* @__PURE__ */ new Map();
|
|
379
|
+
const removedIds = /* @__PURE__ */ new Set();
|
|
380
|
+
for (const command of commands) if (command.op === "update" && typeof command.props.position === "object") positionUpdates.set(command.id, command.props.position);
|
|
381
|
+
else if (command.op === "delete") removedIds.add(command.id);
|
|
382
|
+
if (positionUpdates.size > 0) {
|
|
383
|
+
onGroupsChange((prevGroups) => prevGroups.map((group) => ({
|
|
384
|
+
...group,
|
|
385
|
+
iterations: group.iterations.map((iter) => {
|
|
386
|
+
const update = positionUpdates.get(iter.id);
|
|
387
|
+
return update ? {
|
|
388
|
+
...iter,
|
|
389
|
+
position: update
|
|
390
|
+
} : iter;
|
|
391
|
+
})
|
|
392
|
+
})));
|
|
393
|
+
onCanvasImagesChange((prevImages) => prevImages.map((img) => {
|
|
394
|
+
const update = positionUpdates.get(img.id);
|
|
395
|
+
return update ? {
|
|
396
|
+
...img,
|
|
397
|
+
position: update
|
|
398
|
+
} : img;
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
if (removedIds.size > 0) {
|
|
402
|
+
onGroupsChange((prevGroups) => prevGroups.map((group) => ({
|
|
403
|
+
...group,
|
|
404
|
+
iterations: group.iterations.filter((iter) => !removedIds.has(iter.id))
|
|
405
|
+
})).filter((group) => group.iterations.length > 0));
|
|
406
|
+
onCanvasImagesChange((prevImages) => prevImages.filter((img) => !removedIds.has(img.id)));
|
|
407
|
+
}
|
|
408
|
+
}, [onGroupsChange, onCanvasImagesChange]);
|
|
409
|
+
const onNodeDragStart = useCallback((_event, node) => {
|
|
410
|
+
if (canvasImages.some((img) => img.id === node.id)) setDraggingImageId(node.id);
|
|
411
|
+
else setDraggingId(node.id);
|
|
412
|
+
}, [
|
|
413
|
+
canvasImages,
|
|
414
|
+
setDraggingId,
|
|
415
|
+
setDraggingImageId
|
|
416
|
+
]);
|
|
417
|
+
const onNodeDragStop = useCallback(() => {
|
|
418
|
+
setDraggingId(null);
|
|
419
|
+
setDraggingImageId(null);
|
|
420
|
+
}, [setDraggingId, setDraggingImageId]);
|
|
421
|
+
const onSelectionChange = useCallback(({ nodes: selectedNodes }) => {
|
|
422
|
+
onSelectedIdsChange(new Set(selectedNodes.map((n) => n.id)));
|
|
423
|
+
}, [onSelectedIdsChange]);
|
|
424
|
+
const onPaneClick = useCallback(() => {
|
|
425
|
+
onSelectedIdsChange(/* @__PURE__ */ new Set());
|
|
426
|
+
}, [onSelectedIdsChange]);
|
|
427
|
+
const handleDragOver = useCallback((e) => {
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
e.dataTransfer.dropEffect = "copy";
|
|
430
|
+
}, []);
|
|
431
|
+
const handleDrop = useCallback((e) => {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
const files = [...e.dataTransfer.files].filter((f) => f.type.startsWith("image/"));
|
|
434
|
+
if (files.length > 0 && onImageDrop) onImageDrop(files, e.clientX, e.clientY);
|
|
435
|
+
}, [onImageDrop]);
|
|
436
|
+
const handleContextMenu = useCallback((e) => {
|
|
437
|
+
if (onContextMenu) {
|
|
438
|
+
e.preventDefault();
|
|
439
|
+
onContextMenu(e);
|
|
440
|
+
}
|
|
441
|
+
}, [onContextMenu]);
|
|
442
|
+
const ctx = useMemo(() => ({
|
|
443
|
+
isSelectMode,
|
|
444
|
+
isCommentMode,
|
|
445
|
+
selectedIds,
|
|
446
|
+
pipelineStages,
|
|
447
|
+
onAddComment,
|
|
448
|
+
onClickComment
|
|
449
|
+
}), [
|
|
450
|
+
isSelectMode,
|
|
451
|
+
isCommentMode,
|
|
452
|
+
selectedIds,
|
|
453
|
+
pipelineStages,
|
|
454
|
+
onAddComment,
|
|
455
|
+
onClickComment
|
|
456
|
+
]);
|
|
457
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
458
|
+
className: "absolute inset-0 canvas-dots",
|
|
459
|
+
onDragOver: handleDragOver,
|
|
460
|
+
onDrop: handleDrop,
|
|
461
|
+
onContextMenu: handleContextMenu,
|
|
462
|
+
children: [
|
|
463
|
+
/* @__PURE__ */ jsxs(FlowCanvas, {
|
|
464
|
+
views: calcaViews,
|
|
465
|
+
data: ctx,
|
|
466
|
+
nodeTypes,
|
|
467
|
+
onCommandsApplied,
|
|
468
|
+
onSelectionChange,
|
|
469
|
+
onPaneClick,
|
|
470
|
+
onNodeDragStart,
|
|
471
|
+
onNodeDragStop,
|
|
472
|
+
fitView: true,
|
|
473
|
+
fitViewOptions: { padding: .2 },
|
|
474
|
+
minZoom: .1,
|
|
475
|
+
maxZoom: 5,
|
|
476
|
+
panOnScroll: true,
|
|
477
|
+
selectionOnDrag: isSelectMode,
|
|
478
|
+
selectionMode: SelectionMode.Partial,
|
|
479
|
+
panOnDrag: spaceHeld ? [
|
|
480
|
+
0,
|
|
481
|
+
1,
|
|
482
|
+
2
|
|
483
|
+
] : false,
|
|
484
|
+
nodesDraggable: isSelectMode,
|
|
485
|
+
elementsSelectable: isSelectMode,
|
|
486
|
+
selectNodesOnDrag: isSelectMode,
|
|
487
|
+
multiSelectionKeyCode: "Shift",
|
|
488
|
+
deleteKeyCode: null,
|
|
489
|
+
proOptions: { hideAttribution: true },
|
|
490
|
+
className: isCommentMode ? "cursor-crosshair" : "cursor-default",
|
|
491
|
+
children: [/* @__PURE__ */ jsx(Background, {
|
|
492
|
+
gap: 20,
|
|
493
|
+
size: 1,
|
|
494
|
+
color: "#e5e7eb"
|
|
495
|
+
}), toolbar && /* @__PURE__ */ jsx(Panel, {
|
|
496
|
+
position: "top-center",
|
|
497
|
+
style: { zIndex: 1001 },
|
|
498
|
+
children: toolbar
|
|
499
|
+
})]
|
|
500
|
+
}),
|
|
501
|
+
groups.length === 0 && canvasImages.length === 0 && emptyTitle && /* @__PURE__ */ jsx("div", {
|
|
502
|
+
className: "absolute inset-0 flex flex-col items-center justify-center pointer-events-none z-10",
|
|
503
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
504
|
+
className: "text-center",
|
|
505
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
506
|
+
className: "text-2xl font-semibold text-gray-300 mb-2",
|
|
507
|
+
children: emptyTitle
|
|
508
|
+
}), emptyDescription && /* @__PURE__ */ jsx("p", {
|
|
509
|
+
className: "text-gray-400/70 text-sm",
|
|
510
|
+
children: emptyDescription
|
|
511
|
+
})]
|
|
512
|
+
})
|
|
513
|
+
}),
|
|
514
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
515
|
+
.react-flow__node-designFrame,
|
|
516
|
+
.react-flow__node-canvasImage {
|
|
517
|
+
border: none !important;
|
|
518
|
+
background: transparent !important;
|
|
519
|
+
padding: 0 !important;
|
|
520
|
+
overflow: visible !important;
|
|
521
|
+
}
|
|
522
|
+
.react-flow__node-designFrame.selected,
|
|
523
|
+
.react-flow__node-canvasImage.selected {
|
|
524
|
+
box-shadow: none !important;
|
|
525
|
+
}
|
|
526
|
+
.react-flow__node-designFrame .react-flow__handle {
|
|
527
|
+
opacity: 0;
|
|
528
|
+
}
|
|
529
|
+
` })
|
|
530
|
+
]
|
|
531
|
+
});
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/components/CanvasProvider.tsx
|
|
536
|
+
function CanvasProvider({ children }) {
|
|
537
|
+
return /* @__PURE__ */ jsx(CanvasStoreProvider, { children: /* @__PURE__ */ jsx(ReactFlowProvider, { children }) });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/state/canvas-atoms.ts
|
|
542
|
+
const canvasOffsetAtom = atom({
|
|
543
|
+
x: 0,
|
|
544
|
+
y: 0
|
|
545
|
+
});
|
|
546
|
+
const canvasScaleAtom = atom(1);
|
|
547
|
+
const isPanningAtom = atom(false);
|
|
548
|
+
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region src/hooks/use-canvas.ts
|
|
551
|
+
function useCanvasViewport() {
|
|
552
|
+
const { x, y, zoom } = useViewport();
|
|
553
|
+
const { setViewport, fitView } = useReactFlow();
|
|
554
|
+
const setCanvasOffset = useSetAtom(canvasOffsetAtom);
|
|
555
|
+
const setCanvasScale = useSetAtom(canvasScaleAtom);
|
|
556
|
+
useEffect(() => {
|
|
557
|
+
setCanvasOffset({
|
|
558
|
+
x,
|
|
559
|
+
y
|
|
560
|
+
});
|
|
561
|
+
setCanvasScale(zoom);
|
|
562
|
+
}, [
|
|
563
|
+
x,
|
|
564
|
+
y,
|
|
565
|
+
zoom,
|
|
566
|
+
setCanvasOffset,
|
|
567
|
+
setCanvasScale
|
|
568
|
+
]);
|
|
569
|
+
const resetView = useCallback(() => {
|
|
570
|
+
setViewport({
|
|
571
|
+
x: 0,
|
|
572
|
+
y: 0,
|
|
573
|
+
zoom: 1
|
|
574
|
+
});
|
|
575
|
+
}, [setViewport]);
|
|
576
|
+
const zoomIn = useCallback(() => {
|
|
577
|
+
setViewport({
|
|
578
|
+
x,
|
|
579
|
+
y,
|
|
580
|
+
zoom: Math.min(zoom * 1.2, 5)
|
|
581
|
+
});
|
|
582
|
+
}, [
|
|
583
|
+
setViewport,
|
|
584
|
+
x,
|
|
585
|
+
y,
|
|
586
|
+
zoom
|
|
587
|
+
]);
|
|
588
|
+
const zoomOut = useCallback(() => {
|
|
589
|
+
setViewport({
|
|
590
|
+
x,
|
|
591
|
+
y,
|
|
592
|
+
zoom: Math.max(zoom * .8, .1)
|
|
593
|
+
});
|
|
594
|
+
}, [
|
|
595
|
+
setViewport,
|
|
596
|
+
x,
|
|
597
|
+
y,
|
|
598
|
+
zoom
|
|
599
|
+
]);
|
|
600
|
+
const fitToView = useCallback(() => {
|
|
601
|
+
fitView({
|
|
602
|
+
padding: .2,
|
|
603
|
+
duration: 300
|
|
604
|
+
});
|
|
605
|
+
}, [fitView]);
|
|
606
|
+
const zoomToFit = useCallback((bounds) => {
|
|
607
|
+
const contentW = bounds.maxX - bounds.minX;
|
|
608
|
+
const contentH = bounds.maxY - bounds.minY;
|
|
609
|
+
if (contentW <= 0 || contentH <= 0) return;
|
|
610
|
+
const vw = window.innerWidth;
|
|
611
|
+
const vh = window.innerHeight;
|
|
612
|
+
const scaleX = (vw - 160) / contentW;
|
|
613
|
+
const scaleY = (vh - 160) / contentH;
|
|
614
|
+
const nextZoom = Math.min(scaleX, scaleY, 1);
|
|
615
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
616
|
+
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
617
|
+
setViewport({
|
|
618
|
+
x: vw / 2 - centerX * nextZoom,
|
|
619
|
+
y: vh / 2 - centerY * nextZoom,
|
|
620
|
+
zoom: nextZoom
|
|
621
|
+
});
|
|
622
|
+
}, [setViewport]);
|
|
623
|
+
const screenToCanvas = useCallback((screenX, screenY, rect) => ({
|
|
624
|
+
x: (screenX - rect.left - x) / zoom,
|
|
625
|
+
y: (screenY - rect.top - y) / zoom
|
|
626
|
+
}), [
|
|
627
|
+
x,
|
|
628
|
+
y,
|
|
629
|
+
zoom
|
|
630
|
+
]);
|
|
631
|
+
return {
|
|
632
|
+
offset: {
|
|
633
|
+
x,
|
|
634
|
+
y
|
|
635
|
+
},
|
|
636
|
+
scale: zoom,
|
|
637
|
+
resetView,
|
|
638
|
+
zoomIn,
|
|
639
|
+
zoomOut,
|
|
640
|
+
fitToView,
|
|
641
|
+
zoomToFit,
|
|
642
|
+
screenToCanvas
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/** Backward-compatible alias for code that still imports `useCanvas`. */
|
|
646
|
+
const useCanvas = useCanvasViewport;
|
|
647
|
+
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/lib/frame-clipboard.ts
|
|
650
|
+
/**
|
|
651
|
+
* Serializes selected iterations + their images into the internal clipboard format.
|
|
652
|
+
* Returns null if no iterations were selected.
|
|
653
|
+
*/
|
|
654
|
+
function copyFrames(selectedIds, groups, images) {
|
|
655
|
+
if (selectedIds.size === 0) return null;
|
|
656
|
+
const selectedIterationsByGroup = /* @__PURE__ */ new Map();
|
|
657
|
+
for (const group of groups) {
|
|
658
|
+
const selectedIters = group.iterations.filter((iter) => selectedIds.has(iter.id));
|
|
659
|
+
if (selectedIters.length > 0) selectedIterationsByGroup.set(group.id, {
|
|
660
|
+
...group,
|
|
661
|
+
iterations: selectedIters
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (selectedIterationsByGroup.size === 0) return null;
|
|
665
|
+
const selectedImages = images.filter((img) => selectedIds.has(img.id));
|
|
666
|
+
let minX = Infinity;
|
|
667
|
+
let minY = Infinity;
|
|
668
|
+
for (const [, group] of selectedIterationsByGroup) for (const iter of group.iterations) {
|
|
669
|
+
if (iter.position.x < minX) minX = iter.position.x;
|
|
670
|
+
if (iter.position.y < minY) minY = iter.position.y;
|
|
671
|
+
}
|
|
672
|
+
const origin = {
|
|
673
|
+
x: minX,
|
|
674
|
+
y: minY
|
|
675
|
+
};
|
|
676
|
+
const relativePositions = {};
|
|
677
|
+
for (const [, group] of selectedIterationsByGroup) for (const iter of group.iterations) relativePositions[iter.id] = {
|
|
678
|
+
x: iter.position.x - origin.x,
|
|
679
|
+
y: iter.position.y - origin.y
|
|
680
|
+
};
|
|
681
|
+
return {
|
|
682
|
+
type: "frames",
|
|
683
|
+
groups: Array.from(selectedIterationsByGroup.values()),
|
|
684
|
+
images: selectedImages,
|
|
685
|
+
relativePositions,
|
|
686
|
+
origin,
|
|
687
|
+
createdAt: Date.now()
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Deserializes clipboard data and returns new groups + images positioned at viewportCenter.
|
|
692
|
+
* Assigns fresh unique IDs to all created entities.
|
|
693
|
+
*/
|
|
694
|
+
function pasteFrames(clipboardData, viewportCenter) {
|
|
695
|
+
const now = Date.now();
|
|
696
|
+
const newGroups = [];
|
|
697
|
+
const newImages = [];
|
|
698
|
+
for (const group of clipboardData.groups) {
|
|
699
|
+
const newGroupId = `${group.id}-paste-${now}`;
|
|
700
|
+
const newIterations = group.iterations.map((iter) => {
|
|
701
|
+
const rel = clipboardData.relativePositions[iter.id] ?? {
|
|
702
|
+
x: 0,
|
|
703
|
+
y: 0
|
|
704
|
+
};
|
|
705
|
+
return {
|
|
706
|
+
...iter,
|
|
707
|
+
id: `${iter.id}-paste-${now}`,
|
|
708
|
+
position: {
|
|
709
|
+
x: viewportCenter.x + rel.x,
|
|
710
|
+
y: viewportCenter.y + rel.y
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
});
|
|
714
|
+
newGroups.push({
|
|
715
|
+
...group,
|
|
716
|
+
id: newGroupId,
|
|
717
|
+
iterations: newIterations,
|
|
718
|
+
createdAt: now
|
|
719
|
+
});
|
|
720
|
+
for (const img of clipboardData.images) {
|
|
721
|
+
const rel = clipboardData.relativePositions[img.id] ?? {
|
|
722
|
+
x: 0,
|
|
723
|
+
y: 0
|
|
724
|
+
};
|
|
725
|
+
newImages.push({
|
|
726
|
+
...img,
|
|
727
|
+
id: `${img.id}-paste-${now}`,
|
|
728
|
+
position: {
|
|
729
|
+
x: viewportCenter.x + rel.x,
|
|
730
|
+
y: viewportCenter.y + rel.y
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
groups: newGroups,
|
|
737
|
+
images: newImages
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
function cutFrames(selectedIds, groups, images) {
|
|
741
|
+
return {
|
|
742
|
+
clipboardData: copyFrames(selectedIds, groups, images),
|
|
743
|
+
groups: groups.map((g) => ({
|
|
744
|
+
...g,
|
|
745
|
+
iterations: g.iterations.filter((iter) => !selectedIds.has(iter.id))
|
|
746
|
+
})).filter((g) => g.iterations.length > 0),
|
|
747
|
+
images: images.filter((img) => !selectedIds.has(img.id))
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region src/state/groups-atom.ts
|
|
753
|
+
const STORAGE_KEY = "calca-canvas-session";
|
|
754
|
+
const IMG_DB_NAME = "calca-canvas-images";
|
|
755
|
+
const IMG_STORE = "images";
|
|
756
|
+
function openImgDB() {
|
|
757
|
+
return new Promise((resolve, reject) => {
|
|
758
|
+
const req = indexedDB.open(IMG_DB_NAME, 1);
|
|
759
|
+
req.onupgradeneeded = () => req.result.createObjectStore(IMG_STORE);
|
|
760
|
+
req.onsuccess = () => resolve(req.result);
|
|
761
|
+
req.onerror = () => reject(req.error);
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
async function saveImagesToIDB(images) {
|
|
765
|
+
const db = await openImgDB();
|
|
766
|
+
const tx = db.transaction(IMG_STORE, "readwrite");
|
|
767
|
+
const store = tx.objectStore(IMG_STORE);
|
|
768
|
+
for (const [key, val] of Object.entries(images)) store.put(val, key);
|
|
769
|
+
return new Promise((resolve, reject) => {
|
|
770
|
+
tx.oncomplete = () => {
|
|
771
|
+
db.close();
|
|
772
|
+
resolve();
|
|
773
|
+
};
|
|
774
|
+
tx.onerror = () => {
|
|
775
|
+
db.close();
|
|
776
|
+
reject(tx.error);
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
async function loadImagesFromIDB() {
|
|
781
|
+
const db = await openImgDB();
|
|
782
|
+
const store = db.transaction(IMG_STORE, "readonly").objectStore(IMG_STORE);
|
|
783
|
+
const result = {};
|
|
784
|
+
return new Promise((resolve, reject) => {
|
|
785
|
+
const cursor = store.openCursor();
|
|
786
|
+
cursor.onsuccess = () => {
|
|
787
|
+
const c = cursor.result;
|
|
788
|
+
if (c) {
|
|
789
|
+
result[c.key] = c.value;
|
|
790
|
+
c.continue();
|
|
791
|
+
} else {
|
|
792
|
+
db.close();
|
|
793
|
+
resolve(result);
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
cursor.onerror = () => {
|
|
797
|
+
db.close();
|
|
798
|
+
reject(cursor.error);
|
|
799
|
+
};
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function extractBase64(groups) {
|
|
803
|
+
const images = {};
|
|
804
|
+
let counter = 0;
|
|
805
|
+
return {
|
|
806
|
+
images,
|
|
807
|
+
stripped: groups.map((g) => ({
|
|
808
|
+
...g,
|
|
809
|
+
iterations: g.iterations.map((it) => ({
|
|
810
|
+
...it,
|
|
811
|
+
html: it.html ? it.html.replace(/src="(data:image\/[^"]+)"/g, (_m, uri) => {
|
|
812
|
+
const key = `img_${g.id}_${it.id}_${counter++}`;
|
|
813
|
+
images[key] = uri;
|
|
814
|
+
return `src="[idb:${key}]"`;
|
|
815
|
+
}) : it.html
|
|
816
|
+
}))
|
|
817
|
+
}))
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function restoreBase64(groups, images) {
|
|
821
|
+
return groups.map((g) => ({
|
|
822
|
+
...g,
|
|
823
|
+
iterations: g.iterations.map((it) => ({
|
|
824
|
+
...it,
|
|
825
|
+
html: it.html ? it.html.replace(/src="\[idb:([^\]]+)\]"/g, (_m, key) => images[key] ? `src="${images[key]}"` : "src=\"[img-missing]\"") : it.html
|
|
826
|
+
}))
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
let saveTimer = null;
|
|
830
|
+
function debouncedPersist(groups) {
|
|
831
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
832
|
+
saveTimer = setTimeout(() => {
|
|
833
|
+
try {
|
|
834
|
+
const { stripped, images } = extractBase64(groups.filter((g) => g.iterations.some((it) => it.html && !it.isLoading)));
|
|
835
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(stripped));
|
|
836
|
+
if (Object.keys(images).length > 0) saveImagesToIDB(images).catch((error) => console.debug("Failed to save images to IndexedDB", { error: error instanceof Error ? error.message : String(error) }));
|
|
837
|
+
} catch (error) {
|
|
838
|
+
console.debug("Failed to save canvas session", { error: error instanceof Error ? error.message : String(error) });
|
|
839
|
+
}
|
|
840
|
+
}, 500);
|
|
841
|
+
}
|
|
842
|
+
const _groupsBase = atom([]);
|
|
843
|
+
const groupsAtom = atom((get) => get(_groupsBase), (_get, set, update) => {
|
|
844
|
+
set(_groupsBase, (prev) => {
|
|
845
|
+
const next = typeof update === "function" ? update(prev) : update;
|
|
846
|
+
debouncedPersist(next);
|
|
847
|
+
return next;
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
const resetSessionAtom = atom(null, (_get, set) => {
|
|
851
|
+
set(groupsAtom, []);
|
|
852
|
+
try {
|
|
853
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
854
|
+
} catch {}
|
|
855
|
+
});
|
|
856
|
+
async function hydrateGroups(setGroups) {
|
|
857
|
+
try {
|
|
858
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
859
|
+
if (raw) {
|
|
860
|
+
const valid = JSON.parse(raw).filter((g) => g.iterations.some((it) => it.html && !it.isLoading));
|
|
861
|
+
if (valid.length > 0) {
|
|
862
|
+
try {
|
|
863
|
+
const restored = restoreBase64(valid, await loadImagesFromIDB());
|
|
864
|
+
setGroups((prev) => prev.length === 0 ? restored : prev);
|
|
865
|
+
} catch {
|
|
866
|
+
setGroups((prev) => prev.length === 0 ? valid : prev);
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
} catch {}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
//#endregion
|
|
875
|
+
export { CANVAS_IMAGE_TYPE, CanvasArea, CanvasProvider, CanvasStoreProvider, DESIGN_FRAME_TYPE, FlowCanvas, calcaViews, canvasOffsetAtom, canvasScaleAtom, copyFrames, cutFrames, groupsAtom, hydrateGroups, imageToRecord, isPanningAtom, iterationToRecord, pasteFrames, projectStore, resetSessionAtom, useCanvas, useCanvasNodes, useCanvasStore, useCanvasViewport };
|
|
876
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter/store-context.tsx","../src/adapter/projection.ts","../src/adapter/use-canvas-nodes.ts","../src/adapter/FlowCanvas.tsx","../src/views.ts","../src/components/nodes/CanvasImageNode.tsx","../src/components/nodes/DesignFrameNode.tsx","../src/components/CanvasArea.tsx","../src/components/CanvasProvider.tsx","../src/state/canvas-atoms.ts","../src/hooks/use-canvas.ts","../src/lib/frame-clipboard.ts","../src/state/groups-atom.ts"],"sourcesContent":["import { createCanvasStore, type CanvasStore } from \"@calca/canvas-base\";\nimport { createContext, useContext, useState, type ReactNode } from \"react\";\n\nconst CanvasStoreContext = createContext<CanvasStore | null>(null);\n\ninterface CanvasStoreProviderProps {\n store?: CanvasStore;\n children: ReactNode;\n}\n\nexport const CanvasStoreProvider = ({ store, children }: CanvasStoreProviderProps) => {\n const [owned] = useState(() => store ?? createCanvasStore());\n return <CanvasStoreContext.Provider value={owned}>{children}</CanvasStoreContext.Provider>;\n};\n\n/** The store scoped to the nearest provider, or null outside one. */\nexport const useCanvasStore = () => useContext(CanvasStoreContext);\n","import type { CanvasStore } from \"@calca/canvas-base\";\nimport type { Edge, Node } from \"@xyflow/react\";\n\nimport type { CanvasViews } from \"./types\";\n\nexport interface CanvasProjection {\n nodes: Node[];\n edges: Edge[];\n}\n\n/**\n * Projects the document into React Flow nodes/edges. Shapes render in\n * fractional-index order (document order = paint order = z-order).\n */\nexport const projectStore = <C>(\n store: CanvasStore,\n views: CanvasViews<C>,\n ctx: C,\n): CanvasProjection => {\n const nodes: Node[] = [];\n const shapes = store.shapes().toSorted((a, b) => (a.index < b.index ? -1 : 1));\n for (const record of shapes) {\n const view = views.shapes?.[record.type];\n if (!view) continue;\n nodes.push({\n id: record.id,\n type: view.nodeType,\n position: view.position(record),\n ...view.size?.(record),\n data: view.data(record, ctx),\n selected: view.selected?.(record, ctx) ?? false,\n draggable: view.draggable?.(record, ctx) ?? true,\n selectable: view.selectable?.(record, ctx) ?? true,\n });\n }\n\n const edges: Edge[] = store\n .bindings()\n .toSorted((a, b) => (a.index < b.index ? -1 : 1))\n .map((record) =>\n Object.assign(\n {\n id: record.id,\n source: record.fromId,\n target: record.toId,\n },\n views.bindings?.[record.type]?.edge(record, ctx),\n ),\n );\n\n return { nodes, edges };\n};\n","import type { CanvasStore } from \"@calca/canvas-base\";\nimport { useCallback, useMemo, useRef, useSyncExternalStore } from \"react\";\n\nimport { projectStore, type CanvasProjection } from \"./projection\";\nimport { useCanvasStore } from \"./store-context\";\nimport type { CanvasViews } from \"./types\";\n\n/**\n * Live projection of the document into React Flow nodes/edges. Recomputes\n * when the store emits `change` (or when `views`/`ctx` identity changes).\n * Requires a store: pass one directly or render inside CanvasStoreProvider.\n */\nexport const useCanvasNodes = <C>(\n views: CanvasViews<C>,\n ctx: C,\n storeOverride?: CanvasStore,\n): CanvasProjection => {\n const contextStore = useCanvasStore();\n const store = storeOverride ?? contextStore;\n if (!store) {\n throw new Error(\"useCanvasNodes requires a CanvasStore (prop or CanvasStoreProvider)\");\n }\n\n const version = useRef(0);\n const subscribe = useCallback(\n (notify: () => void) => {\n const listener = () => {\n version.current += 1;\n notify();\n };\n store.addEventListener(\"change\", listener);\n return () => store.removeEventListener(\"change\", listener);\n },\n [store],\n );\n const v = useSyncExternalStore(subscribe, () => version.current);\n\n return useMemo(\n () => projectStore(store, views, ctx),\n // v is the projection trigger; store/views/ctx cover the inputs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [store, views, ctx, v],\n );\n};\n","import type { BindingRecord, Command, ShapeRecord } from \"@calca/canvas-base\";\nimport { generateKeyBetween } from \"@calca/canvas-base\";\nimport {\n applyNodeChanges,\n ReactFlow,\n type Connection,\n type Node,\n type NodeChange,\n type ReactFlowProps,\n} from \"@xyflow/react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\n\nimport { useCanvasStore } from \"./store-context\";\nimport type { CanvasViews } from \"./types\";\nimport { useCanvasNodes } from \"./use-canvas-nodes\";\n\ninterface FlowCanvasProps<C> extends Omit<ReactFlowProps, \"nodes\" | \"edges\"> {\n /** Record-type → React Flow node/edge views. */\n views: CanvasViews<C>;\n /** Opaque context handed to every view's data/selectors. */\n data: C;\n /** React Flow `nodeTypes` map (components live outside the adapter). */\n nodeTypes: ReactFlowProps[\"nodeTypes\"];\n /**\n * Called after RF changes have been translated into commands and applied\n * to the store (origin \"user\"). Use it to keep host state (e.g. jotai\n * persistence) in sync with position/removal changes.\n */\n onCommandsApplied?: (commands: Command[], changes: NodeChange[]) => void;\n /** Selection is UI state, not document state — surfaced via callback. */\n onSelectionChange?: ReactFlowProps[\"onSelectionChange\"];\n}\n\n/**\n * Generic store → React Flow adapter. The document is the source of truth;\n * RF transient state (in-progress drags, selection) stays local.\n */\nexport const FlowCanvas = <C,>({\n views,\n data,\n nodeTypes,\n onCommandsApplied,\n onSelectionChange,\n children,\n ...rfProps\n}: FlowCanvasProps<C>) => {\n const store = useCanvasStore();\n if (!store) {\n throw new Error(\"FlowCanvas must render inside a CanvasStoreProvider\");\n }\n\n const projected = useCanvasNodes(views, data, store);\n const [nodes, setNodes] = useState<Node[]>(projected.nodes);\n\n // Re-sync from the document while preserving RF transient drag/selection.\n useEffect(() => {\n setNodes((prev) => {\n const byId = new Map(prev.map((n) => [n.id, n]));\n return projected.nodes.map((n) => {\n const existing = byId.get(n.id);\n if (!existing) return n;\n return {\n ...n,\n position: existing.position,\n selected: existing.selected,\n data: { ...n.data, isDragging: existing.data?.isDragging ?? false },\n };\n });\n });\n }, [projected.nodes]);\n\n const shapeById = useMemo(() => {\n const map = new Map<string, ShapeRecord>();\n for (const s of store.shapes()) map.set(s.id, s);\n return map;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [store, projected]);\n\n const onNodesChange = useCallback(\n (changes: NodeChange[]) => {\n setNodes((prev) => applyNodeChanges(changes, prev));\n\n const applied: Command[] = [];\n for (const change of changes) {\n if (change.type !== \"position\" && change.type !== \"remove\") continue;\n const record = shapeById.get(change.id);\n if (!record) continue;\n const view = views.shapes?.[record.type];\n if (change.type === \"position\" && change.position) {\n const props = view?.writePosition?.(record, change.position) ?? {\n position: change.position,\n };\n const command: Command = { op: \"update\", id: change.id, props };\n applied.push(command);\n // Intermediate drag positions stay out of undo history.\n store.apply([command], { origin: \"user\", history: change.dragging === false });\n } else if (change.type === \"remove\") {\n const command: Command = { op: \"delete\", id: change.id };\n applied.push(command);\n store.apply([command], { origin: \"user\" });\n }\n }\n\n if (applied.length > 0) onCommandsApplied?.(applied, changes);\n },\n [shapeById, store, views, onCommandsApplied],\n );\n\n const onConnect = useCallback(\n (connection: Connection) => {\n if (!connection.source || !connection.target) return;\n const record: BindingRecord = {\n id: `binding-${crypto.randomUUID()}`,\n type: \"edge\",\n fromId: connection.source,\n toId: connection.target,\n index: generateKeyBetween(null, null),\n props: {},\n };\n store.apply([{ op: \"create-binding\", record }], { origin: \"user\" });\n },\n [store],\n );\n\n return (\n <ReactFlow\n nodes={nodes}\n edges={projected.edges}\n nodeTypes={nodeTypes}\n onNodesChange={onNodesChange}\n onConnect={onConnect}\n onSelectionChange={onSelectionChange}\n {...rfProps}\n >\n {children}\n </ReactFlow>\n );\n};\n","import type { ShapeRecord } from \"@calca/canvas-base\";\nimport type { CanvasImage, Comment, DesignIteration, PipelineStatus, Point } from \"@calca/shared\";\n\nimport type { CanvasViews } from \"./adapter\";\n\nexport const DESIGN_FRAME_TYPE = \"design-frame\";\nexport const CANVAS_IMAGE_TYPE = \"canvas-image\";\n\n/** Render context the app hands to `<FlowCanvas data={ctx}>`. */\nexport interface CalcaCanvasContext {\n isSelectMode: boolean;\n isCommentMode: boolean;\n selectedIds: Set<string>;\n pipelineStages?: Record<string, PipelineStatus>;\n onAddComment: (draft: {\n iterationId: string;\n position: Point;\n screenX: number;\n screenY: number;\n }) => void;\n onClickComment: (comment: Comment, iterationId: string) => void;\n}\n\nexport const iterationToRecord = (\n iteration: DesignIteration,\n groupId: string,\n index: string,\n): ShapeRecord => ({\n id: iteration.id,\n type: DESIGN_FRAME_TYPE,\n parentId: null,\n index,\n props: {\n position: iteration.position,\n width: iteration.width,\n height: iteration.height,\n isLoading: iteration.isLoading,\n groupId,\n iteration,\n },\n});\n\nexport const imageToRecord = (image: CanvasImage, index: string): ShapeRecord => ({\n id: image.id,\n type: CANVAS_IMAGE_TYPE,\n parentId: null,\n index,\n props: {\n position: image.position,\n width: image.width,\n height: image.height,\n image,\n },\n});\n\nconst propsPoint = (record: ShapeRecord) => (record.props as { position: Point }).position;\n\nconst propsSize = (record: ShapeRecord) => {\n const { width, height } = record.props as { width?: number; height?: number };\n return { width, height };\n};\n\nexport const calcaViews: CanvasViews<CalcaCanvasContext> = {\n shapes: {\n [DESIGN_FRAME_TYPE]: {\n nodeType: \"designFrame\",\n position: propsPoint,\n size: (record) => {\n const { width, height, isLoading } = record.props as {\n width?: number;\n height?: number;\n isLoading?: boolean;\n };\n return {\n width: width ?? 480,\n height: isLoading ? 320 : (height ?? 320),\n };\n },\n data: (record, ctx) => {\n const { groupId, iteration } = record.props as {\n groupId: string;\n iteration: DesignIteration;\n };\n return {\n groupId,\n isCommentMode: ctx.isCommentMode,\n isDragging: false,\n isSelectMode: ctx.isSelectMode,\n iteration,\n onAddComment: ctx.onAddComment,\n onClickComment: ctx.onClickComment,\n pipelineStatus: ctx.pipelineStages?.[record.id],\n };\n },\n selected: (record, ctx) => ctx.selectedIds.has(record.id),\n draggable: (_record, ctx) => ctx.isSelectMode,\n selectable: (_record, ctx) => ctx.isSelectMode,\n },\n [CANVAS_IMAGE_TYPE]: {\n nodeType: \"canvasImage\",\n position: propsPoint,\n size: propsSize,\n data: (record, ctx) => ({\n image: (record.props as { image: CanvasImage }).image,\n isDragging: false,\n isSelectMode: ctx.isSelectMode,\n isSelected: ctx.selectedIds.has(record.id),\n }),\n selected: (record, ctx) => ctx.selectedIds.has(record.id),\n draggable: (_record, ctx) => ctx.isSelectMode,\n selectable: (_record, ctx) => ctx.isSelectMode,\n },\n },\n};\n","import type { CanvasImage } from \"@calca/shared\";\nimport type { Node, NodeProps } from \"@xyflow/react\";\n\nexport type CanvasImageNodeData = {\n image: CanvasImage;\n isSelectMode: boolean;\n isDragging: boolean;\n isSelected: boolean;\n};\n\nexport type CanvasImageNodeType = Node<CanvasImageNodeData, \"canvasImage\">;\n\nexport function CanvasImageNode({ data }: NodeProps<CanvasImageNodeType>) {\n const { image, isSelectMode, isDragging, isSelected } = data;\n\n return (\n <div\n className={`group w-full h-full rounded-lg overflow-hidden shadow-md transition-shadow ${\n isSelected\n ? \"ring-2 ring-blue-500 border-blue-400/50 shadow-lg\"\n : \"border border-white/40 hover:shadow-lg\"\n } ${\n isSelectMode\n ? isDragging\n ? \"cursor-grabbing shadow-xl ring-2 ring-blue-400/30\"\n : \"cursor-grab\"\n : \"\"\n }`}\n >\n <img\n src={image.dataUrl}\n alt={image.name}\n className=\"w-full h-full object-cover\"\n draggable={false}\n />\n <span className=\"absolute bottom-1 left-1 right-1 text-[9px] text-white bg-black/50 rounded px-1 py-0.5 truncate opacity-0 group-hover:opacity-100 transition-opacity\">\n {image.name}\n </span>\n </div>\n );\n}\n","import { DesignFrame, DEFAULT_FRAME_WIDTH, PipelineStatusOverlay } from \"@calca/canvas-ui\";\nimport type { Comment as CommentType, DesignIteration, PipelineStatus } from \"@calca/shared\";\nimport { useViewport, type Node, type NodeProps } from \"@xyflow/react\";\n\nexport type DesignFrameNodeData = {\n iteration: DesignIteration;\n groupId: string;\n isCommentMode: boolean;\n isSelectMode: boolean;\n isDragging: boolean;\n pipelineStatus?: PipelineStatus;\n onAddComment: (draft: {\n iterationId: string;\n position: { x: number; y: number };\n screenX: number;\n screenY: number;\n }) => void;\n onClickComment: (comment: CommentType, iterationId: string) => void;\n};\n\nexport type DesignFrameNodeType = Node<DesignFrameNodeData, \"designFrame\">;\n\nexport function DesignFrameNode({ data, selected }: NodeProps<DesignFrameNodeType>) {\n const { iteration, isCommentMode, isSelectMode, pipelineStatus, onAddComment, onClickComment } =\n data;\n const { zoom } = useViewport();\n\n const handleAddComment = (\n iterationId: string,\n position: { x: number; y: number },\n screenX: number,\n screenY: number,\n ) => {\n onAddComment({ iterationId, position, screenX, screenY });\n };\n\n const frameWidth = iteration.width || DEFAULT_FRAME_WIDTH;\n const frameHeight = iteration.isLoading ? 320 : iteration.height || 320;\n\n return (\n <div style={{ width: frameWidth }}>\n <div className=\"mb-2 flex items-center gap-2 group/label pointer-events-none\">\n <span className=\"text-xs font-medium text-gray-500/80 bg-white/60 backdrop-blur-sm px-2.5 py-0.5 rounded-lg border border-white/40\">\n {iteration.label}\n </span>\n </div>\n\n <DesignFrame\n iteration={iteration}\n width={frameWidth}\n isCommentMode={isCommentMode}\n isSelectMode={isSelectMode}\n isDragging={data.isDragging}\n isSelected={selected}\n scale={zoom}\n onAddComment={handleAddComment}\n onClickComment={onClickComment}\n />\n\n {pipelineStatus && pipelineStatus.stage !== \"done\" && (\n <PipelineStatusOverlay\n status={pipelineStatus}\n x={0}\n y={0}\n width={frameWidth}\n frameHeight={frameHeight}\n />\n )}\n </div>\n );\n}\n","import type { Command, ShapeRecord } from \"@calca/canvas-base\";\nimport type {\n CanvasImage,\n Comment,\n GenerationGroup,\n PipelineStatus,\n Point,\n ToolMode,\n} from \"@calca/shared\";\nimport { Background, Panel, SelectionMode, type Node, type NodeChange } from \"@xyflow/react\";\n\nimport \"@xyflow/react/dist/style.css\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\n\nimport { FlowCanvas, useCanvasStore } from \"../adapter\";\nimport type { CanvasHandle } from \"../hooks/use-canvas\";\nimport { calcaViews, imageToRecord, iterationToRecord } from \"../views\";\nimport { CanvasImageNode } from \"./nodes/CanvasImageNode\";\nimport { DesignFrameNode } from \"./nodes/DesignFrameNode\";\n\ntype RubberBand = {\n startX: number;\n startY: number;\n currentX: number;\n currentY: number;\n} | null;\n\ntype Updater<T> = T | ((prev: T) => T);\n\ninterface CanvasAreaProps {\n /** @deprecated CanvasArea now uses React Flow's native viewport; this prop is kept for API compatibility. */\n canvas: CanvasHandle;\n groups: GenerationGroup[];\n onGroupsChange: (update: Updater<GenerationGroup[]>) => void;\n canvasImages: CanvasImage[];\n onCanvasImagesChange: (update: Updater<CanvasImage[]>) => void;\n selectedIds: Set<string>;\n onSelectedIdsChange: (update: Updater<Set<string>>) => void;\n toolMode: ToolMode;\n spaceHeld: boolean;\n /** @deprecated Rubber-band selection is handled by React Flow's built-in selection mode. */\n rubberBand?: RubberBand;\n /** @deprecated Rubber-band selection is handled by React Flow's built-in selection mode. */\n setRubberBand?: (update: Updater<RubberBand>) => void;\n draggingId: string | null;\n setDraggingId: (id: string | null) => void;\n draggingImageId: string | null;\n setDraggingImageId: (id: string | null) => void;\n pipelineStages?: Record<string, PipelineStatus>;\n onAddComment: (draft: {\n iterationId: string;\n position: Point;\n screenX: number;\n screenY: number;\n }) => void;\n onClickComment: (comment: Comment, iterationId: string) => void;\n onImageDrop?: (files: File[], dropX?: number, dropY?: number) => void;\n onContextMenu?: (e: React.MouseEvent) => void;\n emptyTitle?: string;\n emptyDescription?: string;\n toolbar?: React.ReactNode;\n}\n\nconst nodeTypes = {\n canvasImage: CanvasImageNode,\n designFrame: DesignFrameNode,\n};\n\nconst propsEqual = (a: Record<string, unknown>, b: Record<string, unknown>) => {\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n for (const key of keys) {\n if (!Object.is(a[key], b[key])) return false;\n }\n return true;\n};\n\nexport const CanvasArea = ({\n groups,\n onGroupsChange,\n canvasImages,\n onCanvasImagesChange,\n selectedIds,\n onSelectedIdsChange,\n toolMode,\n spaceHeld,\n draggingId: _draggingId,\n setDraggingId,\n draggingImageId: _draggingImageId,\n setDraggingImageId,\n pipelineStages,\n onAddComment,\n onClickComment,\n onImageDrop,\n onContextMenu,\n emptyTitle,\n emptyDescription,\n toolbar,\n}: CanvasAreaProps) => {\n const store = useCanvasStore();\n if (!store) {\n throw new Error(\"CanvasArea must render inside a CanvasStoreProvider\");\n }\n\n const isSelectMode = toolMode === \"select\" && !spaceHeld;\n const isCommentMode = toolMode === \"comment\" && !spaceHeld;\n\n // Bridge the app's controlled props into the document: diff by record id\n // and apply create/update/delete commands with history off (the app owns\n // persistence; the store is the render source of truth).\n const prevRecords = useRef(new Map<string, ShapeRecord>());\n useEffect(() => {\n const next = new Map<string, ShapeRecord>();\n const prev = prevRecords.current;\n const indexOf = (id: string) => prev.get(id)?.index ?? store.nextIndex(null);\n\n for (const group of groups) {\n for (const iteration of group.iterations) {\n next.set(iteration.id, iterationToRecord(iteration, group.id, indexOf(iteration.id)));\n }\n }\n for (const image of canvasImages) {\n next.set(image.id, imageToRecord(image, indexOf(image.id)));\n }\n\n const commands: Command[] = [];\n for (const [id, record] of next) {\n const existing = prev.get(id);\n if (!existing) {\n commands.push({ op: \"create-shape\", record });\n } else if (!propsEqual(existing.props, record.props)) {\n commands.push({ op: \"update\", id, props: record.props });\n }\n }\n for (const id of prev.keys()) {\n if (!next.has(id)) commands.push({ op: \"delete\", id });\n }\n\n prevRecords.current = next;\n if (commands.length > 0) {\n store.apply(commands, { origin: \"remote\", history: false });\n }\n }, [groups, canvasImages, store]);\n\n // Write document changes back into the app's jotai-backed props so\n // persistence and other consumers keep working unchanged.\n const onCommandsApplied = useCallback(\n (commands: Command[], _changes: NodeChange[]) => {\n const positionUpdates = new Map<string, Point>();\n const removedIds = new Set<string>();\n for (const command of commands) {\n if (command.op === \"update\" && typeof command.props.position === \"object\") {\n positionUpdates.set(command.id, command.props.position as Point);\n } else if (command.op === \"delete\") {\n removedIds.add(command.id);\n }\n }\n\n if (positionUpdates.size > 0) {\n onGroupsChange((prevGroups) =>\n prevGroups.map((group) => ({\n ...group,\n iterations: group.iterations.map((iter) => {\n const update = positionUpdates.get(iter.id);\n return update ? { ...iter, position: update } : iter;\n }),\n })),\n );\n onCanvasImagesChange((prevImages) =>\n prevImages.map((img) => {\n const update = positionUpdates.get(img.id);\n return update ? { ...img, position: update } : img;\n }),\n );\n }\n\n if (removedIds.size > 0) {\n onGroupsChange((prevGroups) =>\n prevGroups\n .map((group) => ({\n ...group,\n iterations: group.iterations.filter((iter) => !removedIds.has(iter.id)),\n }))\n .filter((group) => group.iterations.length > 0),\n );\n onCanvasImagesChange((prevImages) => prevImages.filter((img) => !removedIds.has(img.id)));\n }\n },\n [onGroupsChange, onCanvasImagesChange],\n );\n\n const onNodeDragStart = useCallback(\n (_event: MouseEvent | TouchEvent, node: Node) => {\n const isImage = canvasImages.some((img) => img.id === node.id);\n if (isImage) {\n setDraggingImageId(node.id);\n } else {\n setDraggingId(node.id);\n }\n },\n [canvasImages, setDraggingId, setDraggingImageId],\n );\n\n const onNodeDragStop = useCallback(() => {\n setDraggingId(null);\n setDraggingImageId(null);\n }, [setDraggingId, setDraggingImageId]);\n\n const onSelectionChange = useCallback(\n ({ nodes: selectedNodes }: { nodes: Node[] }) => {\n onSelectedIdsChange(new Set(selectedNodes.map((n) => n.id)));\n },\n [onSelectedIdsChange],\n );\n\n const onPaneClick = useCallback(() => {\n onSelectedIdsChange(new Set());\n }, [onSelectedIdsChange]);\n\n const handleDragOver = useCallback((e: React.DragEvent) => {\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n }, []);\n\n const handleDrop = useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n const files = [...e.dataTransfer.files].filter((f) => f.type.startsWith(\"image/\"));\n if (files.length > 0 && onImageDrop) {\n onImageDrop(files, e.clientX, e.clientY);\n }\n },\n [onImageDrop],\n );\n\n const handleContextMenu = useCallback(\n (e: React.MouseEvent) => {\n if (onContextMenu) {\n e.preventDefault();\n onContextMenu(e);\n }\n },\n [onContextMenu],\n );\n\n const ctx = useMemo(\n () => ({\n isSelectMode,\n isCommentMode,\n selectedIds,\n pipelineStages,\n onAddComment,\n onClickComment,\n }),\n [isSelectMode, isCommentMode, selectedIds, pipelineStages, onAddComment, onClickComment],\n );\n\n return (\n <div\n className=\"absolute inset-0 canvas-dots\"\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n onContextMenu={handleContextMenu}\n >\n <FlowCanvas\n views={calcaViews}\n data={ctx}\n nodeTypes={nodeTypes}\n onCommandsApplied={onCommandsApplied}\n onSelectionChange={onSelectionChange}\n onPaneClick={onPaneClick}\n onNodeDragStart={onNodeDragStart}\n onNodeDragStop={onNodeDragStop}\n fitView\n fitViewOptions={{ padding: 0.2 }}\n minZoom={0.1}\n maxZoom={5}\n panOnScroll\n selectionOnDrag={isSelectMode}\n selectionMode={SelectionMode.Partial}\n panOnDrag={spaceHeld ? [0, 1, 2] : false}\n nodesDraggable={isSelectMode}\n elementsSelectable={isSelectMode}\n selectNodesOnDrag={isSelectMode}\n multiSelectionKeyCode=\"Shift\"\n deleteKeyCode={null}\n proOptions={{ hideAttribution: true }}\n className={isCommentMode ? \"cursor-crosshair\" : \"cursor-default\"}\n >\n <Background gap={20} size={1} color=\"#e5e7eb\" />\n\n {/* 1001: selected nodes elevate to z-index 1000 and must not cover the toolbar */}\n {toolbar && (\n <Panel position=\"top-center\" style={{ zIndex: 1001 }}>\n {toolbar}\n </Panel>\n )}\n </FlowCanvas>\n\n {groups.length === 0 && canvasImages.length === 0 && emptyTitle && (\n <div className=\"absolute inset-0 flex flex-col items-center justify-center pointer-events-none z-10\">\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-semibold text-gray-300 mb-2\">{emptyTitle}</h1>\n {emptyDescription && <p className=\"text-gray-400/70 text-sm\">{emptyDescription}</p>}\n </div>\n </div>\n )}\n\n <style>{`\n .react-flow__node-designFrame,\n .react-flow__node-canvasImage {\n border: none !important;\n background: transparent !important;\n padding: 0 !important;\n overflow: visible !important;\n }\n .react-flow__node-designFrame.selected,\n .react-flow__node-canvasImage.selected {\n box-shadow: none !important;\n }\n .react-flow__node-designFrame .react-flow__handle {\n opacity: 0;\n }\n `}</style>\n </div>\n );\n};\n\nexport type { CanvasAreaProps };\n","import { ReactFlowProvider } from \"@xyflow/react\";\nimport type { ReactNode } from \"react\";\n\nimport { CanvasStoreProvider } from \"../adapter\";\n\ninterface CanvasProviderProps {\n children: ReactNode;\n}\n\nexport function CanvasProvider({ children }: CanvasProviderProps) {\n return (\n <CanvasStoreProvider>\n <ReactFlowProvider>{children}</ReactFlowProvider>\n </CanvasStoreProvider>\n );\n}\n","import type { Point } from \"@calca/shared\";\nimport { atom } from \"jotai\";\n\nexport const canvasOffsetAtom = atom<Point>({ x: 0, y: 0 });\nexport const canvasScaleAtom = atom<number>(1);\nexport const isPanningAtom = atom<boolean>(false);\n","import { useReactFlow, useViewport } from \"@xyflow/react\";\nimport { useSetAtom } from \"jotai\";\nimport { useCallback, useEffect } from \"react\";\n\nimport { canvasOffsetAtom, canvasScaleAtom } from \"../state/canvas-atoms\";\n\ninterface Point {\n x: number;\n y: number;\n}\n\nexport type CanvasHandle = {\n offset: Point;\n scale: number;\n resetView: () => void;\n zoomIn: () => void;\n zoomOut: () => void;\n fitToView: () => void;\n zoomToFit: (bounds: { minX: number; minY: number; maxX: number; maxY: number }) => void;\n screenToCanvas: (screenX: number, screenY: number, rect: DOMRect) => Point;\n};\n\nexport function useCanvasViewport(): CanvasHandle {\n const { x, y, zoom } = useViewport();\n const { setViewport, fitView } = useReactFlow();\n const setCanvasOffset = useSetAtom(canvasOffsetAtom);\n const setCanvasScale = useSetAtom(canvasScaleAtom);\n\n useEffect(() => {\n setCanvasOffset({ x, y });\n setCanvasScale(zoom);\n }, [x, y, zoom, setCanvasOffset, setCanvasScale]);\n\n const resetView = useCallback(() => {\n setViewport({ x: 0, y: 0, zoom: 1 });\n }, [setViewport]);\n\n const zoomIn = useCallback(() => {\n setViewport({ x, y, zoom: Math.min(zoom * 1.2, 5) });\n }, [setViewport, x, y, zoom]);\n\n const zoomOut = useCallback(() => {\n setViewport({ x, y, zoom: Math.max(zoom * 0.8, 0.1) });\n }, [setViewport, x, y, zoom]);\n\n const fitToView = useCallback(() => {\n void fitView({ padding: 0.2, duration: 300 });\n }, [fitView]);\n\n const zoomToFit = useCallback(\n (bounds: { minX: number; minY: number; maxX: number; maxY: number }) => {\n const padding = 80;\n const contentW = bounds.maxX - bounds.minX;\n const contentH = bounds.maxY - bounds.minY;\n if (contentW <= 0 || contentH <= 0) {\n return;\n }\n\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const scaleX = (vw - padding * 2) / contentW;\n const scaleY = (vh - padding * 2) / contentH;\n const nextZoom = Math.min(scaleX, scaleY, 1);\n\n const centerX = (bounds.minX + bounds.maxX) / 2;\n const centerY = (bounds.minY + bounds.maxY) / 2;\n\n setViewport({\n x: vw / 2 - centerX * nextZoom,\n y: vh / 2 - centerY * nextZoom,\n zoom: nextZoom,\n });\n },\n [setViewport],\n );\n\n const screenToCanvas = useCallback(\n (screenX: number, screenY: number, rect: DOMRect): Point => ({\n x: (screenX - rect.left - x) / zoom,\n y: (screenY - rect.top - y) / zoom,\n }),\n [x, y, zoom],\n );\n\n return {\n offset: { x, y },\n scale: zoom,\n resetView,\n zoomIn,\n zoomOut,\n fitToView,\n zoomToFit,\n screenToCanvas,\n };\n}\n\n/** Backward-compatible alias for code that still imports `useCanvas`. */\nexport const useCanvas = useCanvasViewport;\n","import type { CanvasImage } from \"@calca/shared\";\nimport type { GenerationGroup } from \"@calca/shared\";\nimport type { Point } from \"@calca/shared\";\n\n/** Frames data stored in the internal Jotai clipboard atom */\nexport interface ClipboardFramesData {\n type: \"frames\";\n /** Groups (each group becomes a new group on paste) */\n groups: GenerationGroup[];\n /** Standalone canvas images copied with the frames */\n images: CanvasImage[];\n /** Relative positions (iteration id → {dx, dy} from bounding-box origin) */\n relativePositions: Record<string, Point>;\n /** Bounding-box origin of the original selection */\n origin: Point;\n createdAt: number;\n}\n\n/**\n * Serializes selected iterations + their images into the internal clipboard format.\n * Returns null if no iterations were selected.\n */\nexport function copyFrames(\n selectedIds: Set<string>,\n groups: GenerationGroup[],\n images: CanvasImage[],\n): ClipboardFramesData | null {\n if (selectedIds.size === 0) return null;\n\n const selectedIterationsByGroup = new Map<string, GenerationGroup>();\n\n for (const group of groups) {\n const selectedIters = group.iterations.filter((iter) => selectedIds.has(iter.id));\n if (selectedIters.length > 0) {\n selectedIterationsByGroup.set(group.id, { ...group, iterations: selectedIters });\n }\n }\n\n if (selectedIterationsByGroup.size === 0) return null;\n\n const selectedImages = images.filter((img) => selectedIds.has(img.id));\n\n let minX = Infinity;\n let minY = Infinity;\n for (const [, group] of selectedIterationsByGroup) {\n for (const iter of group.iterations) {\n if (iter.position.x < minX) minX = iter.position.x;\n if (iter.position.y < minY) minY = iter.position.y;\n }\n }\n const origin: Point = { x: minX, y: minY };\n\n const relativePositions: Record<string, Point> = {};\n for (const [, group] of selectedIterationsByGroup) {\n for (const iter of group.iterations) {\n relativePositions[iter.id] = {\n x: iter.position.x - origin.x,\n y: iter.position.y - origin.y,\n };\n }\n }\n\n return {\n type: \"frames\",\n groups: Array.from(selectedIterationsByGroup.values()),\n images: selectedImages,\n relativePositions,\n origin,\n createdAt: Date.now(),\n };\n}\n\n/**\n * Deserializes clipboard data and returns new groups + images positioned at viewportCenter.\n * Assigns fresh unique IDs to all created entities.\n */\nexport function pasteFrames(\n clipboardData: ClipboardFramesData,\n viewportCenter: Point,\n): { groups: GenerationGroup[]; images: CanvasImage[] } {\n const now = Date.now();\n const newGroups: GenerationGroup[] = [];\n const newImages: CanvasImage[] = [];\n\n for (const group of clipboardData.groups) {\n const newGroupId = `${group.id}-paste-${now}`;\n const newIterations = group.iterations.map((iter) => {\n const rel = clipboardData.relativePositions[iter.id] ?? { x: 0, y: 0 };\n return {\n ...iter,\n id: `${iter.id}-paste-${now}`,\n position: {\n x: viewportCenter.x + rel.x,\n y: viewportCenter.y + rel.y,\n },\n };\n });\n\n newGroups.push({\n ...group,\n id: newGroupId,\n iterations: newIterations,\n createdAt: now,\n });\n\n for (const img of clipboardData.images) {\n const rel = clipboardData.relativePositions[img.id] ?? { x: 0, y: 0 };\n newImages.push({\n ...img,\n id: `${img.id}-paste-${now}`,\n position: {\n x: viewportCenter.x + rel.x,\n y: viewportCenter.y + rel.y,\n },\n });\n }\n }\n\n return { groups: newGroups, images: newImages };\n}\n\nexport function cutFrames(\n selectedIds: Set<string>,\n groups: GenerationGroup[],\n images: CanvasImage[],\n): {\n clipboardData: ClipboardFramesData | null;\n groups: GenerationGroup[];\n images: CanvasImage[];\n} {\n const clipboardData = copyFrames(selectedIds, groups, images);\n\n const updatedGroups = groups\n .map((g) => ({\n ...g,\n iterations: g.iterations.filter((iter) => !selectedIds.has(iter.id)),\n }))\n .filter((g) => g.iterations.length > 0);\n\n const updatedImages = images.filter((img) => !selectedIds.has(img.id));\n\n return {\n clipboardData,\n groups: updatedGroups,\n images: updatedImages,\n };\n}\n","import type { GenerationGroup } from \"@calca/shared\";\nimport { atom } from \"jotai\";\n\nconst STORAGE_KEY = \"calca-canvas-session\";\nconst IMG_DB_NAME = \"calca-canvas-images\";\nconst IMG_STORE = \"images\";\n\nfunction openImgDB(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(IMG_DB_NAME, 1);\n req.onupgradeneeded = () => req.result.createObjectStore(IMG_STORE);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nasync function saveImagesToIDB(images: Record<string, string>): Promise<void> {\n const db = await openImgDB();\n const tx = db.transaction(IMG_STORE, \"readwrite\");\n const store = tx.objectStore(IMG_STORE);\n for (const [key, val] of Object.entries(images)) {\n store.put(val, key);\n }\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => {\n db.close();\n resolve();\n };\n tx.onerror = () => {\n db.close();\n reject(tx.error);\n };\n });\n}\n\nasync function loadImagesFromIDB(): Promise<Record<string, string>> {\n const db = await openImgDB();\n const tx = db.transaction(IMG_STORE, \"readonly\");\n const store = tx.objectStore(IMG_STORE);\n const result: Record<string, string> = {};\n return new Promise((resolve, reject) => {\n const cursor = store.openCursor();\n cursor.onsuccess = () => {\n const c = cursor.result;\n if (c) {\n result[c.key as string] = c.value;\n c.continue();\n } else {\n db.close();\n resolve(result);\n }\n };\n cursor.onerror = () => {\n db.close();\n reject(cursor.error);\n };\n });\n}\n\n// Regex `src=\"(data:image/...)\"` → `[idb:key]` placeholder for IndexedDB offloading\nfunction extractBase64(groups: GenerationGroup[]): {\n stripped: GenerationGroup[];\n images: Record<string, string>;\n} {\n const images: Record<string, string> = {};\n let counter = 0;\n const stripped = groups.map((g) => ({\n ...g,\n iterations: g.iterations.map((it) => ({\n ...it,\n html: it.html\n ? it.html.replace(/src=\"(data:image\\/[^\"]+)\"/g, (_m, uri) => {\n const key = `img_${g.id}_${it.id}_${counter++}`;\n images[key] = uri;\n return `src=\"[idb:${key}]\"`;\n })\n : it.html,\n })),\n }));\n return { images, stripped };\n}\n\nfunction restoreBase64(\n groups: GenerationGroup[],\n images: Record<string, string>,\n): GenerationGroup[] {\n return groups.map((g) => ({\n ...g,\n iterations: g.iterations.map((it) => ({\n ...it,\n html: it.html\n ? it.html.replace(/src=\"\\[idb:([^\\]]+)\\]\"/g, (_m, key) =>\n images[key] ? `src=\"${images[key]}\"` : 'src=\"[img-missing]\"',\n )\n : it.html,\n })),\n }));\n}\n\nlet saveTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction debouncedPersist(groups: GenerationGroup[]): void {\n if (saveTimer) {\n clearTimeout(saveTimer);\n }\n saveTimer = setTimeout(() => {\n try {\n const toSave = groups.filter((g) => g.iterations.some((it) => it.html && !it.isLoading));\n const { stripped, images } = extractBase64(toSave);\n localStorage.setItem(STORAGE_KEY, JSON.stringify(stripped));\n if (Object.keys(images).length > 0) {\n saveImagesToIDB(images).catch((error) =>\n console.debug(\"Failed to save images to IndexedDB\", {\n error: error instanceof Error ? error.message : String(error),\n }),\n );\n }\n } catch (error) {\n console.debug(\"Failed to save canvas session\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }, 500);\n}\n\nconst _groupsBase = atom<GenerationGroup[]>([]);\n\nexport const groupsAtom = atom(\n (get: (a: typeof _groupsBase) => GenerationGroup[]) => get(_groupsBase),\n (\n _get: unknown,\n set: (\n a: typeof _groupsBase,\n v: GenerationGroup[] | ((p: GenerationGroup[]) => GenerationGroup[]),\n ) => void,\n update: GenerationGroup[] | ((prev: GenerationGroup[]) => GenerationGroup[]),\n ) => {\n set(_groupsBase, (prev) => {\n const next = typeof update === \"function\" ? update(prev) : update;\n debouncedPersist(next);\n return next;\n });\n },\n);\n\nexport const resetSessionAtom = atom(\n null,\n (_get: unknown, set: (a: typeof groupsAtom, v: GenerationGroup[]) => void) => {\n set(groupsAtom, []);\n try {\n localStorage.removeItem(STORAGE_KEY);\n } catch {}\n },\n);\n\nexport async function hydrateGroups(\n setGroups: (g: GenerationGroup[] | ((p: GenerationGroup[]) => GenerationGroup[])) => void,\n): Promise<void> {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as GenerationGroup[];\n const valid = parsed.filter((g) => g.iterations.some((it) => it.html && !it.isLoading));\n if (valid.length > 0) {\n // Apply only if nothing was written while the async reads were in flight.\n try {\n const images = await loadImagesFromIDB();\n const restored = restoreBase64(valid, images);\n setGroups((prev) => (prev.length === 0 ? restored : prev));\n } catch {\n setGroups((prev) => (prev.length === 0 ? valid : prev));\n }\n return;\n }\n }\n } catch {}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@calca/canvas-flow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.7.5",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"import": "./dist/index.mjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsdown",
|
|
22
|
+
"typecheck": "bunx tsc --noEmit",
|
|
23
|
+
"lint": "oxlint --fix .",
|
|
24
|
+
"format": "oxfmt --write .",
|
|
25
|
+
"test": "vitest run --passWithNoTests",
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"validate": "bun ../../scripts/validate.ts --cwd $INIT_CWD",
|
|
28
|
+
"check:pack": "publint && attw --pack . --profile esm-only"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@calca/canvas-base": "0.7.5",
|
|
32
|
+
"@calca/canvas-ui": "0.7.5",
|
|
33
|
+
"@calca/shared": "0.7.5",
|
|
34
|
+
"@xyflow/react": "^12.11.1",
|
|
35
|
+
"jotai": "^2.19.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
39
|
+
"@types/react": "^19.2.14",
|
|
40
|
+
"@types/react-dom": "^19.2.3",
|
|
41
|
+
"publint": "^0.3.25",
|
|
42
|
+
"react": "^19.2.4",
|
|
43
|
+
"react-dom": "^19.2.4",
|
|
44
|
+
"tsdown": "^0.23.0",
|
|
45
|
+
"typescript": "^6.0.3",
|
|
46
|
+
"vitest": "^4.1.5"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"react": "^19.2.4",
|
|
50
|
+
"react-dom": "^19.2.4"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/espetro/calca",
|
|
55
|
+
"directory": "packages/canvas-flow"
|
|
56
|
+
}
|
|
57
|
+
}
|