@hilum/designer-canvas 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1190 @@
1
+ import { createContext, useContext, useMemo, useReducer, useRef, useEffect, useState, useCallback } from 'react';
2
+ import { ShellProvider, useShellContext, useHistory, DesignerToolbarButton, DesignerToolbarGroup, DesignerToolbarSeparator } from '@hilum/designer';
3
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
+ import { cn } from '@hilum/ui';
5
+ import { Plus, AlignLeft, AlignCenter, AlignRight, AlignStartHorizontal, AlignVerticalJustifyCenter, AlignEndHorizontal, AlignHorizontalSpaceAround, AlignVerticalSpaceAround, ChevronsUp, ArrowUp, ArrowDown, ChevronsDown, Trash2, Copy, Group, Ungroup, Unlock, Lock, EyeOff, Eye, MousePointer2, Hand, FlipHorizontal, FlipVertical, Undo2, Redo2, Minus, Maximize2 } from 'lucide-react';
6
+
7
+ // src/context/state.ts
8
+ function createInitialState(partial = {}) {
9
+ return {
10
+ layers: [],
11
+ zoom: 1,
12
+ pan: { x: 0, y: 0 },
13
+ frameSize: { width: 800, height: 600 },
14
+ artboardName: "Artboard",
15
+ artboardColor: "#ffffff",
16
+ artboardOpacity: 1,
17
+ artboardClipContent: true,
18
+ gridContainer: void 0,
19
+ copiedLayers: null,
20
+ uiColor: "light",
21
+ canvasColor: "light",
22
+ accentColor: "var(--brand-primary)",
23
+ layerTypes: [],
24
+ readOnly: false,
25
+ ...partial
26
+ };
27
+ }
28
+
29
+ // src/context/reducer.ts
30
+ function canvasReducer(state, action) {
31
+ if (state.readOnly && action.type !== "SET_READ_ONLY" && !isReadActionAllowed(action.type)) {
32
+ return state;
33
+ }
34
+ switch (action.type) {
35
+ /* ---------------- Layer CRUD ---------------- */
36
+ case "SET_LAYERS":
37
+ return { ...state, layers: action.payload };
38
+ case "ADD_LAYER":
39
+ return { ...state, layers: [...state.layers, action.payload] };
40
+ case "UPDATE_LAYER": {
41
+ const { id, updates } = action.payload;
42
+ return {
43
+ ...state,
44
+ layers: state.layers.map((l) => l.id === id ? { ...l, ...updates } : l)
45
+ };
46
+ }
47
+ case "UPDATE_LAYERS": {
48
+ const map = new Map(action.payload.map((u) => [u.id, u.updates]));
49
+ return {
50
+ ...state,
51
+ layers: state.layers.map((l) => map.has(l.id) ? { ...l, ...map.get(l.id) } : l)
52
+ };
53
+ }
54
+ case "DELETE_LAYER":
55
+ return { ...state, layers: state.layers.filter((l) => l.id !== action.payload) };
56
+ case "DELETE_LAYERS": {
57
+ const ids = new Set(action.payload);
58
+ return { ...state, layers: state.layers.filter((l) => !ids.has(l.id)) };
59
+ }
60
+ case "REORDER_LAYERS": {
61
+ const { fromIndex, toIndex } = action.payload;
62
+ const next = [...state.layers];
63
+ const [moved] = next.splice(fromIndex, 1);
64
+ if (moved) next.splice(toIndex, 0, moved);
65
+ return { ...state, layers: next };
66
+ }
67
+ /* ---------------- Selection-driven mutations ---------------- */
68
+ case "NUDGE_LAYERS": {
69
+ const { targetLayerIds, dx, dy } = action.payload;
70
+ const ids = new Set(targetLayerIds);
71
+ return {
72
+ ...state,
73
+ layers: state.layers.map(
74
+ (l) => ids.has(l.id) && !l.isLocked ? { ...l, x: l.x + dx, y: l.y + dy } : l
75
+ )
76
+ };
77
+ }
78
+ case "ALIGN_LAYERS":
79
+ return {
80
+ ...state,
81
+ layers: alignLayers(state.layers, action.payload.targetLayerIds, action.payload.align)
82
+ };
83
+ case "DISTRIBUTE_LAYERS":
84
+ return {
85
+ ...state,
86
+ layers: distributeLayers(state.layers, action.payload.targetLayerIds, action.payload.axis)
87
+ };
88
+ case "GROUP_LAYERS": {
89
+ const { targetLayerIds, groupId } = action.payload;
90
+ const ids = new Set(targetLayerIds);
91
+ return {
92
+ ...state,
93
+ layers: state.layers.map((l) => ids.has(l.id) ? { ...l, groupId } : l)
94
+ };
95
+ }
96
+ case "UNGROUP_LAYERS": {
97
+ const ids = new Set(action.payload.targetLayerIds);
98
+ return {
99
+ ...state,
100
+ layers: state.layers.map((l) => {
101
+ if (!ids.has(l.id)) return l;
102
+ const { groupId: _gid, ...rest } = l;
103
+ return rest;
104
+ })
105
+ };
106
+ }
107
+ case "ARRANGE_LAYERS":
108
+ return {
109
+ ...state,
110
+ layers: arrangeLayers(state.layers, action.payload.targetLayerIds, action.payload.mode)
111
+ };
112
+ case "TRANSFORM_LAYERS": {
113
+ const { targetLayerIds, mode } = action.payload;
114
+ const ids = new Set(targetLayerIds);
115
+ return {
116
+ ...state,
117
+ layers: state.layers.map((l) => {
118
+ if (!ids.has(l.id) || l.isLocked) return l;
119
+ const data = l.data;
120
+ const flipKey = mode === "flip-h" ? "_flipX" : "_flipY";
121
+ const current = data[flipKey] ?? 1;
122
+ return { ...l, data: { ...data, [flipKey]: -current } };
123
+ })
124
+ };
125
+ }
126
+ case "SET_LAYER_PROPERTY": {
127
+ const { targetLayerIds, key, value } = action.payload;
128
+ const ids = new Set(targetLayerIds);
129
+ return {
130
+ ...state,
131
+ layers: state.layers.map(
132
+ (l) => ids.has(l.id) ? { ...l, [key]: value } : l
133
+ )
134
+ };
135
+ }
136
+ /* ---------------- Viewport ---------------- */
137
+ case "SET_ZOOM":
138
+ return { ...state, zoom: clamp(action.payload, 0.05, 32) };
139
+ case "SET_PAN":
140
+ return { ...state, pan: action.payload };
141
+ case "SET_FRAME_SIZE":
142
+ return { ...state, frameSize: action.payload };
143
+ /* ---------------- Artboard ---------------- */
144
+ case "SET_ARTBOARD_NAME":
145
+ return { ...state, artboardName: action.payload };
146
+ case "SET_ARTBOARD_COLOR":
147
+ return { ...state, artboardColor: action.payload };
148
+ case "SET_ARTBOARD_OPACITY":
149
+ return { ...state, artboardOpacity: clamp(action.payload, 0, 1) };
150
+ case "SET_ARTBOARD_CLIP_CONTENT":
151
+ return { ...state, artboardClipContent: action.payload };
152
+ /* ---------------- Grid ---------------- */
153
+ case "SET_GRID_CONTAINER":
154
+ return { ...state, gridContainer: action.payload };
155
+ case "CLEAR_GRID_CONTAINER":
156
+ return { ...state, gridContainer: void 0 };
157
+ case "UPDATE_GRID_CONTAINER":
158
+ return state.gridContainer ? { ...state, gridContainer: { ...state.gridContainer, ...action.payload } } : state;
159
+ /* ---------------- Theme ---------------- */
160
+ case "SET_UI_COLOR":
161
+ return { ...state, uiColor: action.payload };
162
+ case "SET_CANVAS_COLOR":
163
+ return { ...state, canvasColor: action.payload };
164
+ case "SET_ACCENT_COLOR":
165
+ return { ...state, accentColor: action.payload };
166
+ /* ---------------- Clipboard ---------------- */
167
+ case "COPY_LAYERS": {
168
+ const ids = new Set(action.payload.targetLayerIds);
169
+ const copied = state.layers.filter((l) => ids.has(l.id)).map(deepClone);
170
+ return { ...state, copiedLayers: copied };
171
+ }
172
+ case "PASTE_LAYERS": {
173
+ if (!state.copiedLayers || state.copiedLayers.length === 0) return state;
174
+ const offset = action.payload?.offset ?? { dx: 16, dy: 16 };
175
+ const fresh = state.copiedLayers.map((l) => ({
176
+ ...deepClone(l),
177
+ id: generateId(),
178
+ x: l.x + offset.dx,
179
+ y: l.y + offset.dy
180
+ }));
181
+ return { ...state, layers: [...state.layers, ...fresh] };
182
+ }
183
+ case "CLEAR_CLIPBOARD":
184
+ return { ...state, copiedLayers: null };
185
+ /* ---------------- Read-only ---------------- */
186
+ case "SET_READ_ONLY":
187
+ return { ...state, readOnly: action.payload };
188
+ default:
189
+ return state;
190
+ }
191
+ }
192
+ function isReadActionAllowed(type) {
193
+ return type === "SET_ZOOM" || type === "SET_PAN" || type === "SET_UI_COLOR" || type === "SET_CANVAS_COLOR";
194
+ }
195
+ function clamp(n, min, max) {
196
+ return Math.min(max, Math.max(min, n));
197
+ }
198
+ function deepClone(v) {
199
+ if (typeof structuredClone === "function") return structuredClone(v);
200
+ return JSON.parse(JSON.stringify(v));
201
+ }
202
+ function generateId() {
203
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto)
204
+ return crypto.randomUUID();
205
+ return `layer-${Math.random().toString(36).slice(2, 11)}`;
206
+ }
207
+ function alignLayers(layers, ids, align) {
208
+ const idSet = new Set(ids);
209
+ const targets = layers.filter((l) => idSet.has(l.id));
210
+ if (targets.length < 2) return layers;
211
+ let value = 0;
212
+ switch (align) {
213
+ case "left":
214
+ value = Math.min(...targets.map((l) => l.x));
215
+ break;
216
+ case "right": {
217
+ const maxRight = Math.max(...targets.map((l) => l.x + l.width));
218
+ return layers.map((l) => idSet.has(l.id) ? { ...l, x: maxRight - l.width } : l);
219
+ }
220
+ case "center": {
221
+ const minX = Math.min(...targets.map((l) => l.x));
222
+ const maxX = Math.max(...targets.map((l) => l.x + l.width));
223
+ const center = (minX + maxX) / 2;
224
+ return layers.map((l) => idSet.has(l.id) ? { ...l, x: center - l.width / 2 } : l);
225
+ }
226
+ case "top":
227
+ value = Math.min(...targets.map((l) => l.y));
228
+ return layers.map((l) => idSet.has(l.id) ? { ...l, y: value } : l);
229
+ case "bottom": {
230
+ const maxBottom = Math.max(...targets.map((l) => l.y + l.height));
231
+ return layers.map((l) => idSet.has(l.id) ? { ...l, y: maxBottom - l.height } : l);
232
+ }
233
+ case "middle": {
234
+ const minY = Math.min(...targets.map((l) => l.y));
235
+ const maxY = Math.max(...targets.map((l) => l.y + l.height));
236
+ const middle = (minY + maxY) / 2;
237
+ return layers.map((l) => idSet.has(l.id) ? { ...l, y: middle - l.height / 2 } : l);
238
+ }
239
+ }
240
+ return layers.map((l) => idSet.has(l.id) ? { ...l, x: value } : l);
241
+ }
242
+ function distributeLayers(layers, ids, axis) {
243
+ const idSet = new Set(ids);
244
+ const targets = layers.filter((l) => idSet.has(l.id));
245
+ if (targets.length < 3) return layers;
246
+ if (axis === "horizontal") {
247
+ const sorted = [...targets].sort((a, b) => a.x - b.x);
248
+ const first = sorted[0];
249
+ const last = sorted[sorted.length - 1];
250
+ const totalSpan = last.x + last.width - first.x;
251
+ const totalWidth = sorted.reduce((s, l) => s + l.width, 0);
252
+ const gap = (totalSpan - totalWidth) / (sorted.length - 1);
253
+ let cursor = first.x;
254
+ const updates = /* @__PURE__ */ new Map();
255
+ for (const l of sorted) {
256
+ updates.set(l.id, cursor);
257
+ cursor += l.width + gap;
258
+ }
259
+ return layers.map((l) => updates.has(l.id) ? { ...l, x: updates.get(l.id) } : l);
260
+ } else {
261
+ const sorted = [...targets].sort((a, b) => a.y - b.y);
262
+ const first = sorted[0];
263
+ const last = sorted[sorted.length - 1];
264
+ const totalSpan = last.y + last.height - first.y;
265
+ const totalHeight = sorted.reduce((s, l) => s + l.height, 0);
266
+ const gap = (totalSpan - totalHeight) / (sorted.length - 1);
267
+ let cursor = first.y;
268
+ const updates = /* @__PURE__ */ new Map();
269
+ for (const l of sorted) {
270
+ updates.set(l.id, cursor);
271
+ cursor += l.height + gap;
272
+ }
273
+ return layers.map((l) => updates.has(l.id) ? { ...l, y: updates.get(l.id) } : l);
274
+ }
275
+ }
276
+ function arrangeLayers(layers, ids, mode) {
277
+ const idSet = new Set(ids);
278
+ const indices = layers.map((l, i) => idSet.has(l.id) ? i : -1).filter((i) => i >= 0);
279
+ if (indices.length === 0) return layers;
280
+ switch (mode) {
281
+ case "front": {
282
+ const targets = indices.map((i) => layers[i]);
283
+ const others = layers.filter((l) => !idSet.has(l.id));
284
+ return [...others, ...targets];
285
+ }
286
+ case "back": {
287
+ const targets = indices.map((i) => layers[i]);
288
+ const others = layers.filter((l) => !idSet.has(l.id));
289
+ return [...targets, ...others];
290
+ }
291
+ case "forward": {
292
+ const next = [...layers];
293
+ for (let i = indices.length - 1; i >= 0; i--) {
294
+ const idx = indices[i];
295
+ if (idx < next.length - 1) {
296
+ [next[idx], next[idx + 1]] = [next[idx + 1], next[idx]];
297
+ }
298
+ }
299
+ return next;
300
+ }
301
+ case "backward": {
302
+ const next = [...layers];
303
+ for (let i = 0; i < indices.length; i++) {
304
+ const idx = indices[i];
305
+ if (idx > 0) {
306
+ [next[idx], next[idx - 1]] = [next[idx - 1], next[idx]];
307
+ }
308
+ }
309
+ return next;
310
+ }
311
+ }
312
+ }
313
+ var __test = { alignLayers, distributeLayers, arrangeLayers };
314
+ var CanvasContext = createContext(null);
315
+ function useCanvasContext() {
316
+ const ctx = useContext(
317
+ CanvasContext
318
+ );
319
+ if (!ctx) {
320
+ throw new Error(
321
+ "@hilum/designer-canvas: useCanvasContext must be used inside <Designer> / <CanvasProvider>."
322
+ );
323
+ }
324
+ return ctx;
325
+ }
326
+ var CanvasContextProvider = CanvasContext.Provider;
327
+ function CanvasProvider({
328
+ initial,
329
+ services = {},
330
+ readOnly = false,
331
+ onChange,
332
+ children
333
+ }) {
334
+ const initialState = useMemo(
335
+ () => createInitialState({ ...initial, readOnly }),
336
+ []
337
+ // initial state captured once at mount
338
+ );
339
+ const reducer = canvasReducer;
340
+ const [state, dispatch] = useReducer(reducer, initialState);
341
+ const revisionRef = useRef(0);
342
+ if (state !== initialState) revisionRef.current += 0;
343
+ const resolveKind = useMemo(() => {
344
+ const map = /* @__PURE__ */ new Map();
345
+ for (const l of state.layers) map.set(l.id, l.type);
346
+ return (id) => map.get(id);
347
+ }, [state.layers]);
348
+ if (onChange) {
349
+ onChange(state);
350
+ }
351
+ const value = useMemo(
352
+ () => ({ state, dispatch, services, revision: revisionRef.current }),
353
+ [state, dispatch, services]
354
+ );
355
+ return /* @__PURE__ */ jsx(ShellProvider, { readOnly, resolveKind, children: /* @__PURE__ */ jsx(CanvasContextProvider, { value, children }) });
356
+ }
357
+ var RendererContext = createContext({});
358
+ function RendererProvider({ renderers, children }) {
359
+ const value = useMemo(() => renderers, [renderers]);
360
+ return /* @__PURE__ */ jsx(RendererContext.Provider, { value, children });
361
+ }
362
+ function useLayerRenderer(type) {
363
+ return useContext(RendererContext)[type];
364
+ }
365
+ function useAllRenderers() {
366
+ return useContext(RendererContext);
367
+ }
368
+ function Designer({
369
+ initial,
370
+ renderers = {},
371
+ services = {},
372
+ readOnly,
373
+ onChange,
374
+ children
375
+ }) {
376
+ return /* @__PURE__ */ jsx(
377
+ CanvasProvider,
378
+ {
379
+ initial,
380
+ services,
381
+ readOnly,
382
+ onChange,
383
+ children: /* @__PURE__ */ jsx(RendererProvider, { renderers, children })
384
+ }
385
+ );
386
+ }
387
+ function DesignerCanvas({ className, children }) {
388
+ const { state, dispatch } = useCanvasContext();
389
+ const ref = useRef(null);
390
+ useEffect(() => {
391
+ const node = ref.current;
392
+ if (!node) return;
393
+ const onWheel = (e) => {
394
+ if (e.ctrlKey || e.metaKey) {
395
+ e.preventDefault();
396
+ const factor = Math.exp(-e.deltaY * 0.01);
397
+ const next = Math.min(32, Math.max(0.05, state.zoom * factor));
398
+ dispatch({ type: "SET_ZOOM", payload: next });
399
+ } else {
400
+ e.preventDefault();
401
+ dispatch({
402
+ type: "SET_PAN",
403
+ payload: { x: state.pan.x - e.deltaX, y: state.pan.y - e.deltaY }
404
+ });
405
+ }
406
+ };
407
+ node.addEventListener("wheel", onWheel, { passive: false });
408
+ return () => node.removeEventListener("wheel", onWheel);
409
+ }, [state.zoom, state.pan, dispatch]);
410
+ return /* @__PURE__ */ jsx(
411
+ "div",
412
+ {
413
+ ref,
414
+ className: cn("relative flex-1 overflow-hidden bg-taupe-100 select-none", className),
415
+ "data-canvas-root": true,
416
+ children: /* @__PURE__ */ jsx(
417
+ "div",
418
+ {
419
+ className: "absolute inset-0 origin-center",
420
+ style: {
421
+ transform: `translate(${state.pan.x}px, ${state.pan.y}px) scale(${state.zoom})`,
422
+ transformOrigin: "50% 50%"
423
+ },
424
+ children
425
+ }
426
+ )
427
+ }
428
+ );
429
+ }
430
+ function useDragInteraction({ layerId, scale = 1 }) {
431
+ const { state, dispatch } = useCanvasContext();
432
+ const { selectedIds } = useShellContext();
433
+ const [dragging, setDragging] = useState(false);
434
+ const startRef = useRef(null);
435
+ const onPointerDown = useCallback(
436
+ (e) => {
437
+ if (state.readOnly) return;
438
+ const layer = state.layers.find((l) => l.id === layerId);
439
+ if (!layer || layer.isLocked) return;
440
+ const ids = selectedIds.includes(layerId) ? selectedIds : [layerId];
441
+ const initial = /* @__PURE__ */ new Map();
442
+ for (const id of ids) {
443
+ const l = state.layers.find((x) => x.id === id);
444
+ if (l) initial.set(id, { x: l.x, y: l.y });
445
+ }
446
+ startRef.current = { pointerX: e.clientX, pointerY: e.clientY, initial };
447
+ setDragging(true);
448
+ e.target.setPointerCapture(e.pointerId);
449
+ },
450
+ [state, selectedIds, layerId]
451
+ );
452
+ useEffect(() => {
453
+ if (!dragging) return;
454
+ const onMove = (e) => {
455
+ const start = startRef.current;
456
+ if (!start) return;
457
+ let dx = (e.clientX - start.pointerX) * scale;
458
+ let dy = (e.clientY - start.pointerY) * scale;
459
+ if (e.shiftKey) {
460
+ if (Math.abs(dx) > Math.abs(dy)) dy = 0;
461
+ else dx = 0;
462
+ }
463
+ const updates = [];
464
+ start.initial.forEach((origin, id) => {
465
+ updates.push({ id, updates: { x: origin.x + dx, y: origin.y + dy } });
466
+ });
467
+ dispatch({ type: "UPDATE_LAYERS", payload: updates });
468
+ };
469
+ const onUp = () => {
470
+ startRef.current = null;
471
+ setDragging(false);
472
+ };
473
+ window.addEventListener("pointermove", onMove);
474
+ window.addEventListener("pointerup", onUp);
475
+ window.addEventListener("pointercancel", onUp);
476
+ return () => {
477
+ window.removeEventListener("pointermove", onMove);
478
+ window.removeEventListener("pointerup", onUp);
479
+ window.removeEventListener("pointercancel", onUp);
480
+ };
481
+ }, [dragging, dispatch, scale]);
482
+ return { dragging, onPointerDown };
483
+ }
484
+ function LayerView({ layer, staticMode = false }) {
485
+ const { state } = useCanvasContext();
486
+ const { selectedIds, setSelectedIds, readOnly } = useShellContext();
487
+ const Renderer = useLayerRenderer(layer.type);
488
+ const { onPointerDown } = useDragInteraction({
489
+ layerId: layer.id,
490
+ scale: 1 / state.zoom
491
+ });
492
+ const selected = selectedIds.includes(layer.id);
493
+ const isVisible = layer.isVisible !== false;
494
+ if (!isVisible) return null;
495
+ const transform = `rotate(${layer.rotation ?? 0}deg)`;
496
+ const cursor = layer.isLocked ? "default" : "move";
497
+ return /* @__PURE__ */ jsx(
498
+ "div",
499
+ {
500
+ "data-layer-id": layer.id,
501
+ onPointerDown: staticMode ? void 0 : (e) => {
502
+ if (readOnly) return;
503
+ if (e.button !== 0) return;
504
+ if (!selected) {
505
+ setSelectedIds(e.shiftKey ? [...selectedIds, layer.id] : [layer.id]);
506
+ }
507
+ onPointerDown(e);
508
+ },
509
+ className: cn("absolute", !staticMode && "cursor-move", layer.isLocked && "cursor-default"),
510
+ style: {
511
+ left: layer.x,
512
+ top: layer.y,
513
+ width: layer.width,
514
+ height: layer.height,
515
+ opacity: layer.opacity ?? 1,
516
+ transform,
517
+ cursor: staticMode ? "default" : cursor
518
+ },
519
+ children: Renderer ? /* @__PURE__ */ jsx(
520
+ Renderer,
521
+ {
522
+ layer,
523
+ ctx: { selected, zoom: state.zoom, readOnly: readOnly || staticMode }
524
+ }
525
+ ) : /* @__PURE__ */ jsx(FallbackRenderer, { layer })
526
+ }
527
+ );
528
+ }
529
+ function FallbackRenderer({ layer }) {
530
+ return /* @__PURE__ */ jsx(
531
+ "div",
532
+ {
533
+ className: "size-full flex items-center justify-center bg-white border border-dashed border-taupe-300 caption text-taupe-400",
534
+ title: `No renderer registered for type "${layer.type}"`,
535
+ children: layer.type
536
+ }
537
+ );
538
+ }
539
+ function LayerSelectionOverlay() {
540
+ const { state } = useCanvasContext();
541
+ const { selectedIds } = useShellContext();
542
+ if (selectedIds.length === 0) return null;
543
+ const ids = new Set(selectedIds);
544
+ const selected = state.layers.filter((l) => ids.has(l.id));
545
+ return /* @__PURE__ */ jsx(Fragment, { children: selected.map((l) => /* @__PURE__ */ jsx(
546
+ "div",
547
+ {
548
+ className: "absolute pointer-events-none",
549
+ style: {
550
+ left: l.x,
551
+ top: l.y,
552
+ width: l.width,
553
+ height: l.height,
554
+ transform: `rotate(${l.rotation ?? 0}deg)`,
555
+ outline: `1.5px solid ${state.accentColor}`,
556
+ outlineOffset: -1
557
+ },
558
+ "aria-hidden": true
559
+ },
560
+ l.id
561
+ )) });
562
+ }
563
+ function MarqueeOverlay({ containerRef }) {
564
+ const { state } = useCanvasContext();
565
+ const { setSelectedIds, readOnly } = useShellContext();
566
+ const [box, setBox] = useState(null);
567
+ useEffect(() => {
568
+ const node = containerRef.current;
569
+ if (!node || readOnly) return;
570
+ let start = null;
571
+ const onDown = (e) => {
572
+ if (e.target !== node) return;
573
+ if (e.button !== 0) return;
574
+ const rect = node.getBoundingClientRect();
575
+ start = {
576
+ x: (e.clientX - rect.left) / rect.width * state.frameSize.width,
577
+ y: (e.clientY - rect.top) / rect.height * state.frameSize.height
578
+ };
579
+ setBox({ x: start.x, y: start.y, w: 0, h: 0 });
580
+ };
581
+ const onMove = (e) => {
582
+ if (!start) return;
583
+ const rect = node.getBoundingClientRect();
584
+ const cx = (e.clientX - rect.left) / rect.width * state.frameSize.width;
585
+ const cy = (e.clientY - rect.top) / rect.height * state.frameSize.height;
586
+ setBox({
587
+ x: Math.min(start.x, cx),
588
+ y: Math.min(start.y, cy),
589
+ w: Math.abs(cx - start.x),
590
+ h: Math.abs(cy - start.y)
591
+ });
592
+ };
593
+ const onUp = (e) => {
594
+ if (!start || !box) {
595
+ start = null;
596
+ setBox(null);
597
+ return;
598
+ }
599
+ const x0 = box.x;
600
+ const y0 = box.y;
601
+ const x1 = box.x + box.w;
602
+ const y1 = box.y + box.h;
603
+ const intersects = state.layers.filter((l) => {
604
+ const lx0 = l.x;
605
+ const ly0 = l.y;
606
+ const lx1 = l.x + l.width;
607
+ const ly1 = l.y + l.height;
608
+ return lx0 < x1 && lx1 > x0 && ly0 < y1 && ly1 > y0;
609
+ });
610
+ const ids = intersects.map((l) => l.id);
611
+ if (e.shiftKey) {
612
+ setSelectedIds((prev) => Array.from(/* @__PURE__ */ new Set([...prev, ...ids])));
613
+ } else {
614
+ setSelectedIds(ids);
615
+ }
616
+ start = null;
617
+ setBox(null);
618
+ };
619
+ node.addEventListener("pointerdown", onDown);
620
+ window.addEventListener("pointermove", onMove);
621
+ window.addEventListener("pointerup", onUp);
622
+ return () => {
623
+ node.removeEventListener("pointerdown", onDown);
624
+ window.removeEventListener("pointermove", onMove);
625
+ window.removeEventListener("pointerup", onUp);
626
+ };
627
+ }, [containerRef, readOnly, state.frameSize, state.layers, setSelectedIds, box]);
628
+ if (!box || box.w < 2 && box.h < 2) return null;
629
+ return /* @__PURE__ */ jsx(
630
+ "div",
631
+ {
632
+ className: "absolute pointer-events-none",
633
+ style: {
634
+ left: box.x,
635
+ top: box.y,
636
+ width: box.w,
637
+ height: box.h,
638
+ background: "color-mix(in srgb, var(--brand-primary) 10%, transparent)",
639
+ outline: "1px solid var(--brand-primary)"
640
+ },
641
+ "aria-hidden": true
642
+ }
643
+ );
644
+ }
645
+ function GridOverlay() {
646
+ const { state } = useCanvasContext();
647
+ const { frameSize, gridContainer } = state;
648
+ if (!gridContainer) return null;
649
+ const { cols, rows, gap, padding } = gridContainer;
650
+ const cellW = (frameSize.width - padding * 2 - gap * (cols - 1)) / cols;
651
+ const cellH = (frameSize.height - padding * 2 - gap * (rows - 1)) / rows;
652
+ const lines = [];
653
+ for (let r = 0; r < rows; r++) {
654
+ for (let c = 0; c < cols; c++) {
655
+ lines.push({
656
+ x: padding + c * (cellW + gap),
657
+ y: padding + r * (cellH + gap),
658
+ w: cellW,
659
+ h: cellH
660
+ });
661
+ }
662
+ }
663
+ return /* @__PURE__ */ jsx(
664
+ "svg",
665
+ {
666
+ className: "absolute inset-0 pointer-events-none",
667
+ width: frameSize.width,
668
+ height: frameSize.height,
669
+ "aria-hidden": true,
670
+ children: lines.map((cell, i) => /* @__PURE__ */ jsx(
671
+ "rect",
672
+ {
673
+ x: cell.x,
674
+ y: cell.y,
675
+ width: cell.w,
676
+ height: cell.h,
677
+ fill: "none",
678
+ stroke: "var(--brand-primary)",
679
+ strokeOpacity: 0.18,
680
+ strokeDasharray: "3 3",
681
+ strokeWidth: 1
682
+ },
683
+ i
684
+ ))
685
+ }
686
+ );
687
+ }
688
+ function DesignerFrame({
689
+ className,
690
+ hideSelectionOverlay = false,
691
+ hideMarquee = false,
692
+ showGrid = false,
693
+ children
694
+ }) {
695
+ const { state } = useCanvasContext();
696
+ const { setSelectedIds } = useShellContext();
697
+ const ref = useRef(null);
698
+ return /* @__PURE__ */ jsxs(
699
+ "div",
700
+ {
701
+ ref,
702
+ onPointerDown: (e) => {
703
+ if (e.target === ref.current) setSelectedIds([]);
704
+ },
705
+ className: cn("relative shadow-elevated", className),
706
+ style: {
707
+ width: state.frameSize.width,
708
+ height: state.frameSize.height,
709
+ background: state.artboardColor,
710
+ opacity: state.artboardOpacity,
711
+ overflow: state.artboardClipContent ? "hidden" : "visible"
712
+ },
713
+ "data-frame": true,
714
+ children: [
715
+ showGrid && /* @__PURE__ */ jsx(GridOverlay, {}),
716
+ state.layers.map((l) => /* @__PURE__ */ jsx(LayerView, { layer: l }, l.id)),
717
+ !hideSelectionOverlay && /* @__PURE__ */ jsx(LayerSelectionOverlay, {}),
718
+ !hideMarquee && /* @__PURE__ */ jsx(MarqueeOverlay, { containerRef: ref }),
719
+ children
720
+ ]
721
+ }
722
+ );
723
+ }
724
+ function DesignerStaticFrame({ className, width, height }) {
725
+ const { state } = useCanvasContext();
726
+ const w = width ?? state.frameSize.width;
727
+ const h = height ?? state.frameSize.height;
728
+ return /* @__PURE__ */ jsx(
729
+ "div",
730
+ {
731
+ className: cn("relative", className),
732
+ style: {
733
+ width: w,
734
+ height: h,
735
+ background: state.artboardColor,
736
+ opacity: state.artboardOpacity,
737
+ overflow: state.artboardClipContent ? "hidden" : "visible"
738
+ },
739
+ "data-static-frame": true,
740
+ children: state.layers.map((l) => /* @__PURE__ */ jsx(LayerView, { layer: l, staticMode: true }, l.id))
741
+ }
742
+ );
743
+ }
744
+ function useLayers() {
745
+ return useCanvasContext().state.layers;
746
+ }
747
+ function useLayer(id) {
748
+ const layers = useLayers();
749
+ return useMemo(() => layers.find((l) => l.id === id), [layers, id]);
750
+ }
751
+ function useSelectedLayerIds() {
752
+ return useShellContext().selectedIds;
753
+ }
754
+ function useSelectedLayers() {
755
+ const ids = useSelectedLayerIds();
756
+ const layers = useLayers();
757
+ return useMemo(() => layers.filter((l) => ids.includes(l.id)), [layers, ids]);
758
+ }
759
+ function useSelectedLayer() {
760
+ const selected = useSelectedLayers();
761
+ return selected[0];
762
+ }
763
+ function useIsLayerSelected(id) {
764
+ return useSelectedLayerIds().includes(id);
765
+ }
766
+ var ZOOM_STEPS = [0.05, 0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24, 32];
767
+ function nextStep(zoom, dir) {
768
+ if (dir === "in") {
769
+ return ZOOM_STEPS.find((s) => s > zoom) ?? ZOOM_STEPS[ZOOM_STEPS.length - 1];
770
+ }
771
+ let prev = ZOOM_STEPS[0];
772
+ for (const s of ZOOM_STEPS) {
773
+ if (s >= zoom) break;
774
+ prev = s;
775
+ }
776
+ return prev;
777
+ }
778
+ function useZoom() {
779
+ const { state, dispatch } = useCanvasContext();
780
+ const setZoom = useCallback(
781
+ (next) => dispatch({ type: "SET_ZOOM", payload: next }),
782
+ [dispatch]
783
+ );
784
+ const zoomIn = useCallback(
785
+ () => dispatch({ type: "SET_ZOOM", payload: nextStep(state.zoom, "in") }),
786
+ [dispatch, state.zoom]
787
+ );
788
+ const zoomOut = useCallback(
789
+ () => dispatch({ type: "SET_ZOOM", payload: nextStep(state.zoom, "out") }),
790
+ [dispatch, state.zoom]
791
+ );
792
+ const resetZoom = useCallback(() => dispatch({ type: "SET_ZOOM", payload: 1 }), [dispatch]);
793
+ const fitZoom = useCallback(
794
+ ({ width, height }, padding = 64) => {
795
+ const ratio = Math.min(
796
+ (width - padding) / state.frameSize.width,
797
+ (height - padding) / state.frameSize.height
798
+ );
799
+ dispatch({ type: "SET_ZOOM", payload: Math.max(0.05, Math.min(32, ratio)) });
800
+ dispatch({ type: "SET_PAN", payload: { x: 0, y: 0 } });
801
+ },
802
+ [dispatch, state.frameSize]
803
+ );
804
+ return { zoom: state.zoom, setZoom, zoomIn, zoomOut, resetZoom, fitZoom };
805
+ }
806
+ function useHistoryActions() {
807
+ const { state, dispatch } = useCanvasContext();
808
+ const initialRef = useRef(state.layers);
809
+ const hist = useHistory(initialRef.current);
810
+ const lastSeenRef = useRef(state.layers);
811
+ useEffect(() => {
812
+ if (state.layers === lastSeenRef.current) return;
813
+ if (state.layers === hist.state) {
814
+ lastSeenRef.current = state.layers;
815
+ return;
816
+ }
817
+ hist.setState(state.layers);
818
+ lastSeenRef.current = state.layers;
819
+ }, [state.layers, hist]);
820
+ const undo = () => {
821
+ hist.undo();
822
+ queueMicrotask(() => dispatch({ type: "SET_LAYERS", payload: hist.state }));
823
+ };
824
+ const redo = () => {
825
+ hist.redo();
826
+ queueMicrotask(() => dispatch({ type: "SET_LAYERS", payload: hist.state }));
827
+ };
828
+ return { undo, redo, canUndo: hist.canUndo, canRedo: hist.canRedo };
829
+ }
830
+ function generateId2() {
831
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
832
+ return `layer-${Math.random().toString(36).slice(2, 11)}`;
833
+ }
834
+ function ActionAddLayer() {
835
+ const { state, dispatch } = useCanvasContext();
836
+ const { setSelectedIds } = useShellContext();
837
+ if (state.layerTypes.length === 0) {
838
+ return /* @__PURE__ */ jsx(DesignerToolbarButton, { label: "Add layer", icon: Plus, disabled: true });
839
+ }
840
+ return /* @__PURE__ */ jsx(DesignerToolbarGroup, { children: state.layerTypes.map((descriptor) => {
841
+ const Icon = descriptor.icon ?? Plus;
842
+ return /* @__PURE__ */ jsx(
843
+ DesignerToolbarButton,
844
+ {
845
+ label: `Add ${descriptor.label}`,
846
+ icon: Icon,
847
+ onClick: () => {
848
+ const id = generateId2();
849
+ const size = descriptor.defaultSize ?? { width: 200, height: 100 };
850
+ const layer = {
851
+ id,
852
+ type: descriptor.type,
853
+ name: descriptor.label,
854
+ x: state.frameSize.width / 2 - size.width / 2,
855
+ y: state.frameSize.height / 2 - size.height / 2,
856
+ width: size.width,
857
+ height: size.height,
858
+ rotation: 0,
859
+ opacity: 1,
860
+ isLocked: false,
861
+ isVisible: true,
862
+ data: descriptor.defaultData
863
+ };
864
+ dispatch({ type: "ADD_LAYER", payload: layer });
865
+ setSelectedIds([id]);
866
+ }
867
+ },
868
+ descriptor.type
869
+ );
870
+ }) });
871
+ }
872
+ function ActionAlign() {
873
+ const { dispatch } = useCanvasContext();
874
+ const { selectedIds } = useShellContext();
875
+ const disabled = selectedIds.length < 2;
876
+ const align = (a) => () => dispatch({ type: "ALIGN_LAYERS", payload: { targetLayerIds: selectedIds, align: a } });
877
+ const distribute = (axis) => () => dispatch({ type: "DISTRIBUTE_LAYERS", payload: { targetLayerIds: selectedIds, axis } });
878
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
879
+ /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
880
+ /* @__PURE__ */ jsx(
881
+ DesignerToolbarButton,
882
+ {
883
+ label: "Align left",
884
+ icon: AlignLeft,
885
+ onClick: align("left"),
886
+ disabled
887
+ }
888
+ ),
889
+ /* @__PURE__ */ jsx(
890
+ DesignerToolbarButton,
891
+ {
892
+ label: "Align center",
893
+ icon: AlignCenter,
894
+ onClick: align("center"),
895
+ disabled
896
+ }
897
+ ),
898
+ /* @__PURE__ */ jsx(
899
+ DesignerToolbarButton,
900
+ {
901
+ label: "Align right",
902
+ icon: AlignRight,
903
+ onClick: align("right"),
904
+ disabled
905
+ }
906
+ )
907
+ ] }),
908
+ /* @__PURE__ */ jsx(DesignerToolbarSeparator, {}),
909
+ /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
910
+ /* @__PURE__ */ jsx(
911
+ DesignerToolbarButton,
912
+ {
913
+ label: "Align top",
914
+ icon: AlignStartHorizontal,
915
+ onClick: align("top"),
916
+ disabled
917
+ }
918
+ ),
919
+ /* @__PURE__ */ jsx(
920
+ DesignerToolbarButton,
921
+ {
922
+ label: "Align middle",
923
+ icon: AlignVerticalJustifyCenter,
924
+ onClick: align("middle"),
925
+ disabled
926
+ }
927
+ ),
928
+ /* @__PURE__ */ jsx(
929
+ DesignerToolbarButton,
930
+ {
931
+ label: "Align bottom",
932
+ icon: AlignEndHorizontal,
933
+ onClick: align("bottom"),
934
+ disabled
935
+ }
936
+ )
937
+ ] }),
938
+ /* @__PURE__ */ jsx(DesignerToolbarSeparator, {}),
939
+ /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
940
+ /* @__PURE__ */ jsx(
941
+ DesignerToolbarButton,
942
+ {
943
+ label: "Distribute horizontally",
944
+ icon: AlignHorizontalSpaceAround,
945
+ onClick: distribute("horizontal"),
946
+ disabled: selectedIds.length < 3
947
+ }
948
+ ),
949
+ /* @__PURE__ */ jsx(
950
+ DesignerToolbarButton,
951
+ {
952
+ label: "Distribute vertically",
953
+ icon: AlignVerticalSpaceAround,
954
+ onClick: distribute("vertical"),
955
+ disabled: selectedIds.length < 3
956
+ }
957
+ )
958
+ ] })
959
+ ] });
960
+ }
961
+ function ActionArrange() {
962
+ const { dispatch } = useCanvasContext();
963
+ const { selectedIds } = useShellContext();
964
+ const disabled = selectedIds.length === 0;
965
+ const arrange = (mode) => () => dispatch({ type: "ARRANGE_LAYERS", payload: { targetLayerIds: selectedIds, mode } });
966
+ return /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
967
+ /* @__PURE__ */ jsx(
968
+ DesignerToolbarButton,
969
+ {
970
+ label: "Bring to front",
971
+ icon: ChevronsUp,
972
+ onClick: arrange("front"),
973
+ disabled
974
+ }
975
+ ),
976
+ /* @__PURE__ */ jsx(
977
+ DesignerToolbarButton,
978
+ {
979
+ label: "Bring forward",
980
+ icon: ArrowUp,
981
+ onClick: arrange("forward"),
982
+ disabled
983
+ }
984
+ ),
985
+ /* @__PURE__ */ jsx(
986
+ DesignerToolbarButton,
987
+ {
988
+ label: "Send backward",
989
+ icon: ArrowDown,
990
+ onClick: arrange("backward"),
991
+ disabled
992
+ }
993
+ ),
994
+ /* @__PURE__ */ jsx(
995
+ DesignerToolbarButton,
996
+ {
997
+ label: "Send to back",
998
+ icon: ChevronsDown,
999
+ onClick: arrange("back"),
1000
+ disabled
1001
+ }
1002
+ )
1003
+ ] });
1004
+ }
1005
+ function ActionDelete() {
1006
+ const { dispatch } = useCanvasContext();
1007
+ const { selectedIds, setSelectedIds } = useShellContext();
1008
+ return /* @__PURE__ */ jsx(
1009
+ DesignerToolbarButton,
1010
+ {
1011
+ label: "Delete",
1012
+ icon: Trash2,
1013
+ shortcut: "Del",
1014
+ disabled: selectedIds.length === 0,
1015
+ onClick: () => {
1016
+ dispatch({ type: "DELETE_LAYERS", payload: selectedIds });
1017
+ setSelectedIds([]);
1018
+ }
1019
+ }
1020
+ );
1021
+ }
1022
+ function ActionDuplicate() {
1023
+ const { dispatch } = useCanvasContext();
1024
+ const { selectedIds } = useShellContext();
1025
+ return /* @__PURE__ */ jsx(
1026
+ DesignerToolbarButton,
1027
+ {
1028
+ label: "Duplicate",
1029
+ icon: Copy,
1030
+ shortcut: "\u2318D",
1031
+ disabled: selectedIds.length === 0,
1032
+ onClick: () => {
1033
+ dispatch({ type: "COPY_LAYERS", payload: { targetLayerIds: selectedIds } });
1034
+ dispatch({ type: "PASTE_LAYERS" });
1035
+ }
1036
+ }
1037
+ );
1038
+ }
1039
+ function ActionGroup() {
1040
+ const { dispatch } = useCanvasContext();
1041
+ const { selectedIds } = useShellContext();
1042
+ return /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
1043
+ /* @__PURE__ */ jsx(
1044
+ DesignerToolbarButton,
1045
+ {
1046
+ label: "Group",
1047
+ icon: Group,
1048
+ shortcut: "\u2318G",
1049
+ disabled: selectedIds.length < 2,
1050
+ onClick: () => {
1051
+ const groupId = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `group-${Date.now()}`;
1052
+ dispatch({ type: "GROUP_LAYERS", payload: { targetLayerIds: selectedIds, groupId } });
1053
+ }
1054
+ }
1055
+ ),
1056
+ /* @__PURE__ */ jsx(
1057
+ DesignerToolbarButton,
1058
+ {
1059
+ label: "Ungroup",
1060
+ icon: Ungroup,
1061
+ shortcut: "\u21E7\u2318G",
1062
+ disabled: selectedIds.length === 0,
1063
+ onClick: () => dispatch({ type: "UNGROUP_LAYERS", payload: { targetLayerIds: selectedIds } })
1064
+ }
1065
+ )
1066
+ ] });
1067
+ }
1068
+ function ActionLock() {
1069
+ const { dispatch } = useCanvasContext();
1070
+ const { selectedIds } = useShellContext();
1071
+ const selected = useSelectedLayers();
1072
+ const disabled = selectedIds.length === 0;
1073
+ const allLocked = selected.length > 0 && selected.every((l) => l.isLocked);
1074
+ const allHidden = selected.length > 0 && selected.every((l) => l.isVisible === false);
1075
+ return /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
1076
+ /* @__PURE__ */ jsx(
1077
+ DesignerToolbarButton,
1078
+ {
1079
+ label: allLocked ? "Unlock" : "Lock",
1080
+ icon: allLocked ? Unlock : Lock,
1081
+ disabled,
1082
+ onClick: () => dispatch({
1083
+ type: "SET_LAYER_PROPERTY",
1084
+ payload: { targetLayerIds: selectedIds, key: "isLocked", value: !allLocked }
1085
+ })
1086
+ }
1087
+ ),
1088
+ /* @__PURE__ */ jsx(
1089
+ DesignerToolbarButton,
1090
+ {
1091
+ label: allHidden ? "Show" : "Hide",
1092
+ icon: allHidden ? EyeOff : Eye,
1093
+ disabled,
1094
+ onClick: () => dispatch({
1095
+ type: "SET_LAYER_PROPERTY",
1096
+ payload: { targetLayerIds: selectedIds, key: "isVisible", value: allHidden }
1097
+ })
1098
+ }
1099
+ )
1100
+ ] });
1101
+ }
1102
+ var DEFAULT_TOOLS = [
1103
+ { id: "select", label: "Select", icon: MousePointer2, shortcut: "V" },
1104
+ { id: "hand", label: "Pan", icon: Hand, shortcut: "H" }
1105
+ ];
1106
+ function ActionTool({ tools = DEFAULT_TOOLS }) {
1107
+ const { activeTool, setActiveTool } = useShellContext();
1108
+ return /* @__PURE__ */ jsx(DesignerToolbarGroup, { children: tools.map((tool) => /* @__PURE__ */ jsx(
1109
+ DesignerToolbarButton,
1110
+ {
1111
+ label: tool.label,
1112
+ icon: tool.icon,
1113
+ shortcut: tool.shortcut,
1114
+ active: activeTool === tool.id,
1115
+ onClick: () => setActiveTool(tool.id)
1116
+ },
1117
+ tool.id
1118
+ )) });
1119
+ }
1120
+ function ActionTransform() {
1121
+ const { dispatch } = useCanvasContext();
1122
+ const { selectedIds } = useShellContext();
1123
+ const disabled = selectedIds.length === 0;
1124
+ return /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
1125
+ /* @__PURE__ */ jsx(
1126
+ DesignerToolbarButton,
1127
+ {
1128
+ label: "Flip horizontal",
1129
+ icon: FlipHorizontal,
1130
+ disabled,
1131
+ onClick: () => dispatch({
1132
+ type: "TRANSFORM_LAYERS",
1133
+ payload: { targetLayerIds: selectedIds, mode: "flip-h" }
1134
+ })
1135
+ }
1136
+ ),
1137
+ /* @__PURE__ */ jsx(
1138
+ DesignerToolbarButton,
1139
+ {
1140
+ label: "Flip vertical",
1141
+ icon: FlipVertical,
1142
+ disabled,
1143
+ onClick: () => dispatch({
1144
+ type: "TRANSFORM_LAYERS",
1145
+ payload: { targetLayerIds: selectedIds, mode: "flip-v" }
1146
+ })
1147
+ }
1148
+ )
1149
+ ] });
1150
+ }
1151
+ function ActionUndoRedo() {
1152
+ const { undo, redo, canUndo, canRedo } = useHistoryActions();
1153
+ return /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
1154
+ /* @__PURE__ */ jsx(
1155
+ DesignerToolbarButton,
1156
+ {
1157
+ label: "Undo",
1158
+ icon: Undo2,
1159
+ shortcut: "\u2318Z",
1160
+ onClick: undo,
1161
+ disabled: !canUndo
1162
+ }
1163
+ ),
1164
+ /* @__PURE__ */ jsx(
1165
+ DesignerToolbarButton,
1166
+ {
1167
+ label: "Redo",
1168
+ icon: Redo2,
1169
+ shortcut: "\u21E7\u2318Z",
1170
+ onClick: redo,
1171
+ disabled: !canRedo
1172
+ }
1173
+ )
1174
+ ] });
1175
+ }
1176
+ function ActionZoom() {
1177
+ const { zoom, zoomIn, zoomOut, resetZoom } = useZoom();
1178
+ return /* @__PURE__ */ jsxs(DesignerToolbarGroup, { children: [
1179
+ /* @__PURE__ */ jsx(DesignerToolbarButton, { label: "Zoom out", icon: Minus, shortcut: "\u2318-", onClick: zoomOut }),
1180
+ /* @__PURE__ */ jsx(DesignerToolbarButton, { label: "Reset zoom", icon: Maximize2, shortcut: "\u23180", onClick: resetZoom, children: /* @__PURE__ */ jsxs("span", { className: "caption-xs ml-1 tabular-nums", children: [
1181
+ Math.round(zoom * 100),
1182
+ "%"
1183
+ ] }) }),
1184
+ /* @__PURE__ */ jsx(DesignerToolbarButton, { label: "Zoom in", icon: Plus, shortcut: "\u2318=", onClick: zoomIn })
1185
+ ] });
1186
+ }
1187
+
1188
+ export { ActionAddLayer, ActionAlign, ActionArrange, ActionDelete, ActionDuplicate, ActionGroup, ActionLock, ActionTool, ActionTransform, ActionUndoRedo, ActionZoom, CanvasContextProvider, CanvasProvider, Designer, DesignerCanvas, DesignerFrame, DesignerStaticFrame, GridOverlay, LayerSelectionOverlay, MarqueeOverlay, RendererProvider, __test, canvasReducer, createInitialState, useAllRenderers, useCanvasContext, useDragInteraction, useHistoryActions, useIsLayerSelected, useLayer, useLayerRenderer, useLayers, useSelectedLayer, useSelectedLayerIds, useSelectedLayers, useZoom };
1189
+ //# sourceMappingURL=index.js.map
1190
+ //# sourceMappingURL=index.js.map