@widgetic/canvas 0.5.4

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.
Files changed (51) hide show
  1. package/README.md +45 -0
  2. package/dist/canvas/Canvas.svelte +10678 -0
  3. package/dist/canvas/Canvas.svelte.d.ts +147 -0
  4. package/dist/canvas/CanvasToolbar.svelte +1422 -0
  5. package/dist/canvas/CanvasToolbar.svelte.d.ts +54 -0
  6. package/dist/canvas/ContextMenu.svelte +279 -0
  7. package/dist/canvas/ContextMenu.svelte.d.ts +36 -0
  8. package/dist/canvas/PanZoomPanel.svelte +315 -0
  9. package/dist/canvas/PanZoomPanel.svelte.d.ts +32 -0
  10. package/dist/canvas/canvasLogger.d.ts +5 -0
  11. package/dist/canvas/canvasLogger.js +19 -0
  12. package/dist/canvas/index.d.ts +13 -0
  13. package/dist/canvas/index.js +12 -0
  14. package/dist/canvas/props-panel/PropsPanel.svelte +1902 -0
  15. package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +73 -0
  16. package/dist/canvas/props-panel/TextPropsICSection.svelte +238 -0
  17. package/dist/canvas/props-panel/TextPropsICSection.svelte.d.ts +36 -0
  18. package/dist/canvas/shapes/FrameShape.d.ts +97 -0
  19. package/dist/canvas/shapes/FrameShape.js +951 -0
  20. package/dist/canvas/shapes/ImageShape.d.ts +38 -0
  21. package/dist/canvas/shapes/ImageShape.js +245 -0
  22. package/dist/canvas/shapes/ShapeLibrary.d.ts +64 -0
  23. package/dist/canvas/shapes/ShapeLibrary.js +526 -0
  24. package/dist/canvas/shapes/WidgetShape.d.ts +21 -0
  25. package/dist/canvas/shapes/WidgetShape.js +132 -0
  26. package/dist/canvas/types.d.ts +26 -0
  27. package/dist/canvas/types.js +5 -0
  28. package/dist/components/Tooltip.svelte +179 -0
  29. package/dist/components/Tooltip.svelte.d.ts +21 -0
  30. package/dist/components/index.d.ts +11 -0
  31. package/dist/components/index.js +13 -0
  32. package/dist/components/input-controls/ColorIC.svelte +388 -0
  33. package/dist/components/input-controls/ColorIC.svelte.d.ts +22 -0
  34. package/dist/components/input-controls/FontSelectorIC.svelte +69 -0
  35. package/dist/components/input-controls/FontSelectorIC.svelte.d.ts +17 -0
  36. package/dist/components/input-controls/SliderUnitIC.svelte +217 -0
  37. package/dist/components/input-controls/SliderUnitIC.svelte.d.ts +24 -0
  38. package/dist/components/input-controls/TextAlignIC.svelte +84 -0
  39. package/dist/components/input-controls/TextAlignIC.svelte.d.ts +17 -0
  40. package/dist/components/input-controls/TextStyleIC.svelte +85 -0
  41. package/dist/components/input-controls/TextStyleIC.svelte.d.ts +21 -0
  42. package/dist/icons/arrow.svg +3 -0
  43. package/dist/icons/checkmark.svg +3 -0
  44. package/dist/icons/draw.svg +22 -0
  45. package/dist/icons/eraser.svg +23 -0
  46. package/dist/icons/hand.svg +6 -0
  47. package/dist/icons/select.svg +3 -0
  48. package/dist/icons/text.svg +5 -0
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.js +12 -0
  51. package/package.json +101 -0
@@ -0,0 +1,54 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ class?: string;
5
+ style?: string;
6
+ activeTool?: "select" | "rect" | "erase" | "draw" | "arrow" | "text" | "circle" | "circle-true" | "triangle" | "frame" | "pan" | "image" | "library-shape";
7
+ activeClass?: string;
8
+ inactiveClass?: string;
9
+ buttonClass?: string;
10
+ onSelectSelectTool?: () => void;
11
+ onSelectEraseTool?: () => void;
12
+ onSelectDrawTool?: () => void;
13
+ onSelectShapeTool?: (shapeType: "rect" | "circle" | "circle-true" | "triangle") => void;
14
+ onSelectFrameTool?: () => void;
15
+ onSelectTextTool?: () => void;
16
+ onSelectPanTool?: () => void;
17
+ onSelectImageTool?: () => void;
18
+ onZoomIn?: () => void;
19
+ onZoomOut?: () => void;
20
+ onZoomReset?: () => void;
21
+ onZoomFit?: () => void;
22
+ onGroupSelection?: () => void;
23
+ onUngroupSelection?: () => void;
24
+ onDuplicateSelection?: () => void;
25
+ onSelectAllObjects?: () => void;
26
+ onClearAllObjects?: () => void;
27
+ onUndo?: () => void;
28
+ onRedo?: () => void;
29
+ /** Disables the Undo button when there is no prior state to go back to. */ canUndo?: boolean;
30
+ /** Disables the Redo button when there is no future state to redo to. */ canRedo?: boolean;
31
+ /** When true, the drawing tool stays active after placing a shape. */ stickyToolEnabled?: boolean;
32
+ onToggleStickyTool?: () => void;
33
+ /** Called when user clicks a library shape. Canvas inserts it as a Path. */ onInsertLibraryShape?: (shapeKey: string, pathData: string) => void;
34
+ /** Draw-to-place: activates library shape tool so user clicks-and-drags to size it. */ onSelectLibraryShapeTool?: (shapeKey: string, pathData: string) => void;
35
+ addText?: () => void;
36
+ addRect?: () => void;
37
+ addTriangle?: () => void;
38
+ addCircle?: () => void;
39
+ addFrame?: () => void;
40
+ addImage?: () => void;
41
+ shortcutsPanelOpen?: boolean;
42
+ onShortcutsToggle?: () => void;
43
+ };
44
+ events: {
45
+ [evt: string]: CustomEvent<any>;
46
+ };
47
+ slots: {};
48
+ };
49
+ export type CanvasToolbarProps = typeof __propDef.props;
50
+ export type CanvasToolbarEvents = typeof __propDef.events;
51
+ export type CanvasToolbarSlots = typeof __propDef.slots;
52
+ export default class CanvasToolbar extends SvelteComponentTyped<CanvasToolbarProps, CanvasToolbarEvents, CanvasToolbarSlots> {
53
+ }
54
+ export {};
@@ -0,0 +1,279 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ // ── Position & visibility ──────────────────────────────────────────────────
5
+ export let x: number = 0;
6
+ export let y: number = 0;
7
+ export let visible: boolean = false;
8
+
9
+ // ── Context: what was right-clicked ───────────────────────────────────────
10
+ export let hasSelection: boolean = false;
11
+ export let selectionCount: number = 0;
12
+ export let canGroup: boolean = false;
13
+ export let canUngroup: boolean = false;
14
+ export let canPaste: boolean = false;
15
+
16
+ // ── Action callbacks ──────────────────────────────────────────────────────
17
+ export let onCut: () => void = () => {};
18
+ export let onCopy: () => void = () => {};
19
+ export let onPaste: () => void = () => {};
20
+ export let onDuplicate: () => void = () => {};
21
+ export let onDelete: () => void = () => {};
22
+ export let onGroup: () => void = () => {};
23
+ export let onUngroup: () => void = () => {};
24
+ export let onBringToFront: () => void = () => {};
25
+ export let onSendToBack: () => void = () => {};
26
+ export let onBringForward: () => void = () => {};
27
+ export let onSendBackward: () => void = () => {};
28
+ export let onSelectAll: () => void = () => {};
29
+ export let onClose: () => void = () => {};
30
+
31
+ let menuEl: HTMLDivElement;
32
+
33
+ // Close on click outside or Escape
34
+ function handleKeydown(e: KeyboardEvent) {
35
+ if (e.key === 'Escape' && visible) {
36
+ onClose();
37
+ }
38
+ }
39
+
40
+ function handlePointerDown(e: PointerEvent) {
41
+ if (visible && menuEl && !menuEl.contains(e.target as Node)) {
42
+ onClose();
43
+ }
44
+ }
45
+
46
+ // Use onMount's return function for cleanup — both the setup and teardown are
47
+ // browser-only. onDestroy runs during SSR where window is not defined.
48
+ onMount(() => {
49
+ window.addEventListener('keydown', handleKeydown);
50
+ window.addEventListener('pointerdown', handlePointerDown, { capture: true });
51
+
52
+ return () => {
53
+ window.removeEventListener('keydown', handleKeydown);
54
+ window.removeEventListener('pointerdown', handlePointerDown, true);
55
+ };
56
+ });
57
+
58
+ // Keep menu inside viewport
59
+ $: safeX = x;
60
+ $: safeY = y;
61
+
62
+ function adjustPosition(el: HTMLDivElement) {
63
+ if (!el) return;
64
+ const rect = el.getBoundingClientRect();
65
+ const vw = window.innerWidth;
66
+ const vh = window.innerHeight;
67
+ if (rect.right > vw) safeX = Math.max(0, x - rect.width);
68
+ if (rect.bottom > vh) safeY = Math.max(0, y - rect.height);
69
+ }
70
+
71
+ $: if (visible && menuEl) {
72
+ // Run after DOM update so getBoundingClientRect is accurate
73
+ requestAnimationFrame(() => adjustPosition(menuEl));
74
+ }
75
+
76
+ function handleAction(fn: () => void) {
77
+ fn();
78
+ onClose();
79
+ }
80
+ </script>
81
+
82
+ {#if visible}
83
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
84
+ <div
85
+ bind:this={menuEl}
86
+ class="context-menu-el"
87
+ style="left: {safeX}px; top: {safeY}px;"
88
+ oncontextmenu={(e) => e.preventDefault()}
89
+ >
90
+ <!-- Selection actions -->
91
+ {#if hasSelection}
92
+ <button class="item" onclick={() => handleAction(onCut)}>
93
+ <span class="icon">✂️</span>
94
+ <span class="label">Cut</span>
95
+ <span class="shortcut">⌘X</span>
96
+ </button>
97
+ <button class="item" onclick={() => handleAction(onCopy)}>
98
+ <span class="icon">📋</span>
99
+ <span class="label">Copy</span>
100
+ <span class="shortcut">⌘C</span>
101
+ </button>
102
+ {/if}
103
+
104
+ <button class="item" class:disabled={!canPaste} onclick={() => canPaste && handleAction(onPaste)}>
105
+ <span class="icon">📌</span>
106
+ <span class="label">Paste</span>
107
+ <span class="shortcut">⌘V</span>
108
+ </button>
109
+
110
+ {#if hasSelection}
111
+ <button class="item" onclick={() => handleAction(onDuplicate)}>
112
+ <span class="icon">⧉</span>
113
+ <span class="label">Duplicate</span>
114
+ <span class="shortcut">⌘D</span>
115
+ </button>
116
+
117
+ <div class="divider"></div>
118
+
119
+ <!-- Grouping -->
120
+ {#if canGroup}
121
+ <button class="item" onclick={() => handleAction(onGroup)}>
122
+ <span class="icon">
123
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
124
+ <rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/>
125
+ <rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/>
126
+ </svg>
127
+ </span>
128
+ <span class="label">Group</span>
129
+ <span class="shortcut">⌘G</span>
130
+ </button>
131
+ {/if}
132
+ {#if canUngroup}
133
+ <button class="item" onclick={() => handleAction(onUngroup)}>
134
+ <span class="icon">
135
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
136
+ <rect x="2" y="2" width="8" height="8" rx="1" stroke-dasharray="3 2"/><rect x="14" y="2" width="8" height="8" rx="1" stroke-dasharray="3 2"/>
137
+ <rect x="2" y="14" width="8" height="8" rx="1" stroke-dasharray="3 2"/><rect x="14" y="14" width="8" height="8" rx="1" stroke-dasharray="3 2"/>
138
+ </svg>
139
+ </span>
140
+ <span class="label">Ungroup</span>
141
+ <span class="shortcut">⌘⇧G</span>
142
+ </button>
143
+ {/if}
144
+
145
+ <div class="divider"></div>
146
+
147
+ <!-- Layer order -->
148
+ <button class="item" onclick={() => handleAction(onBringToFront)}>
149
+ <span class="icon">
150
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
+ <rect x="3" y="3" width="13" height="13"/><rect x="8" y="8" width="13" height="13" fill="white"/>
152
+ <path d="M12 4v4M14 6l-2-2-2 2"/>
153
+ </svg>
154
+ </span>
155
+ <span class="label">Bring to Front</span>
156
+ <span class="shortcut">⌘]</span>
157
+ </button>
158
+ <button class="item" onclick={() => handleAction(onBringForward)}>
159
+ <span class="icon">
160
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
161
+ <rect x="3" y="8" width="13" height="13"/><rect x="8" y="3" width="13" height="13" fill="white" stroke="currentColor"/>
162
+ <path d="M12 5v3M14 6l-2-2-2 2"/>
163
+ </svg>
164
+ </span>
165
+ <span class="label">Bring Forward</span>
166
+ <span class="shortcut">]</span>
167
+ </button>
168
+ <button class="item" onclick={() => handleAction(onSendBackward)}>
169
+ <span class="icon">
170
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
171
+ <rect x="8" y="3" width="13" height="13"/><rect x="3" y="8" width="13" height="13" fill="white" stroke="currentColor"/>
172
+ <path d="M12 16v3M10 18l2 2 2-2"/>
173
+ </svg>
174
+ </span>
175
+ <span class="label">Send Backward</span>
176
+ <span class="shortcut">[</span>
177
+ </button>
178
+ <button class="item" onclick={() => handleAction(onSendToBack)}>
179
+ <span class="icon">
180
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
181
+ <rect x="8" y="8" width="13" height="13"/><rect x="3" y="3" width="13" height="13" fill="white"/>
182
+ <path d="M12 17v3M10 19l2 2 2-2"/>
183
+ </svg>
184
+ </span>
185
+ <span class="label">Send to Back</span>
186
+ <span class="shortcut">⌘[</span>
187
+ </button>
188
+
189
+ <div class="divider"></div>
190
+
191
+ <button class="item danger" onclick={() => handleAction(onDelete)}>
192
+ <span class="icon">🗑</span>
193
+ <span class="label">Delete</span>
194
+ <span class="shortcut">⌫</span>
195
+ </button>
196
+ {:else}
197
+ <!-- No selection: only canvas-level actions -->
198
+ <div class="divider"></div>
199
+ <button class="item" onclick={() => handleAction(onSelectAll)}>
200
+ <span class="icon">⊹</span>
201
+ <span class="label">Select All</span>
202
+ <span class="shortcut">⌘A</span>
203
+ </button>
204
+ {/if}
205
+ </div>
206
+ {/if}
207
+
208
+ <style>
209
+ .context-menu-el {
210
+ position: fixed;
211
+ z-index: 9999;
212
+ background: white;
213
+ border: 1px solid #e5e7eb;
214
+ border-radius: 8px;
215
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.08);
216
+ padding: 4px;
217
+ min-width: 180px;
218
+ font-size: 13px;
219
+ user-select: none;
220
+ }
221
+
222
+ .item {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 8px;
226
+ width: 100%;
227
+ padding: 6px 10px;
228
+ border: none;
229
+ background: transparent;
230
+ border-radius: 5px;
231
+ cursor: pointer;
232
+ color: #111827;
233
+ text-align: left;
234
+ transition: background 0.1s;
235
+ }
236
+
237
+ .item:hover:not(.disabled) {
238
+ background: #f3f4f6;
239
+ }
240
+
241
+ .item.disabled {
242
+ opacity: 0.4;
243
+ cursor: default;
244
+ }
245
+
246
+ .item.danger {
247
+ color: #dc2626;
248
+ }
249
+
250
+ .item.danger:hover {
251
+ background: #fef2f2;
252
+ }
253
+
254
+ .icon {
255
+ width: 16px;
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ flex-shrink: 0;
260
+ font-size: 12px;
261
+ }
262
+
263
+ .label {
264
+ flex: 1;
265
+ font-weight: 400;
266
+ }
267
+
268
+ .shortcut {
269
+ color: #9ca3af;
270
+ font-size: 11px;
271
+ margin-left: auto;
272
+ }
273
+
274
+ .divider {
275
+ height: 1px;
276
+ background: #e5e7eb;
277
+ margin: 3px 0;
278
+ }
279
+ </style>
@@ -0,0 +1,36 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: {
4
+ x?: number;
5
+ y?: number;
6
+ visible?: boolean;
7
+ hasSelection?: boolean;
8
+ selectionCount?: number;
9
+ canGroup?: boolean;
10
+ canUngroup?: boolean;
11
+ canPaste?: boolean;
12
+ onCut?: () => void;
13
+ onCopy?: () => void;
14
+ onPaste?: () => void;
15
+ onDuplicate?: () => void;
16
+ onDelete?: () => void;
17
+ onGroup?: () => void;
18
+ onUngroup?: () => void;
19
+ onBringToFront?: () => void;
20
+ onSendToBack?: () => void;
21
+ onBringForward?: () => void;
22
+ onSendBackward?: () => void;
23
+ onSelectAll?: () => void;
24
+ onClose?: () => void;
25
+ };
26
+ events: {
27
+ [evt: string]: CustomEvent<any>;
28
+ };
29
+ slots: {};
30
+ };
31
+ export type ContextMenuProps = typeof __propDef.props;
32
+ export type ContextMenuEvents = typeof __propDef.events;
33
+ export type ContextMenuSlots = typeof __propDef.slots;
34
+ export default class ContextMenu extends SvelteComponentTyped<ContextMenuProps, ContextMenuEvents, ContextMenuSlots> {
35
+ }
36
+ export {};
@@ -0,0 +1,315 @@
1
+ <script lang="ts">
2
+ // Pan Zoom Panel - Canvas navigation and view controls
3
+ import Tooltip from '../components/Tooltip.svelte';
4
+
5
+ let className = '';
6
+ export { className as class };
7
+ export let style = '';
8
+
9
+ // Current zoom level (percentage)
10
+ export let zoomLevel: number = 100;
11
+
12
+ // Is pan tool active
13
+ export let isPanActive: boolean = false;
14
+
15
+ // Is grid visible
16
+ export let showGrid: boolean = false;
17
+
18
+ // Callbacks
19
+ export let onPanToggle: () => void = () => {};
20
+ export let onZoomIn: () => void = () => {};
21
+ export let onZoomOut: () => void = () => {};
22
+ export let onZoomReset: () => void = () => {};
23
+ export let onZoomFit: () => void = () => {};
24
+ export let onZoomChange: (percent: number) => void = () => {};
25
+ export let onGridToggle: () => void = () => {};
26
+ export let onShortcutsToggle: () => void = () => {};
27
+ // Reflects open state so the button can show an active style
28
+ export let shortcutsOpen: boolean = false;
29
+
30
+ // Save button: visible when showSaveButton is true
31
+ export let showSaveButton: boolean = false;
32
+ export let hasUnsavedChanges: boolean = false;
33
+ export let onSave: (() => void) | null = null;
34
+
35
+ // Editable zoom input
36
+ let zoomInputValue = zoomLevel.toString();
37
+ $: zoomInputValue = zoomLevel.toString();
38
+
39
+ function handleZoomInputChange(e: Event) {
40
+ const input = e.target as HTMLInputElement;
41
+ zoomInputValue = input.value;
42
+ }
43
+
44
+ function handleZoomInputBlur() {
45
+ const value = parseInt(zoomInputValue);
46
+ if (!isNaN(value) && value >= 10 && value <= 500) {
47
+ onZoomChange(value);
48
+ } else {
49
+ zoomInputValue = zoomLevel.toString();
50
+ }
51
+ }
52
+
53
+ function handleZoomInputKeydown(e: KeyboardEvent) {
54
+ if (e.key === 'Enter') {
55
+ handleZoomInputBlur();
56
+ (e.target as HTMLInputElement).blur();
57
+ }
58
+ }
59
+ </script>
60
+
61
+ <div class="{className ? className + ' ' : ''}pan-zoom-panel" style={style}>
62
+ <!-- Grid Toggle -->
63
+ <Tooltip text="Toggle Grid [G]" position="above">
64
+ <button
65
+ class="panel-btn grid-btn {showGrid ? 'active' : ''}"
66
+ onclick={onGridToggle}
67
+ >
68
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
69
+ <rect x="3" y="3" width="7" height="7"/>
70
+ <rect x="14" y="3" width="7" height="7"/>
71
+ <rect x="14" y="14" width="7" height="7"/>
72
+ <rect x="3" y="14" width="7" height="7"/>
73
+ </svg>
74
+ </button>
75
+ </Tooltip>
76
+
77
+ <div class="divider"></div>
78
+
79
+ <!-- Pan Tool -->
80
+ <Tooltip text="Hand / Pan [H]" position="above">
81
+ <button
82
+ class="panel-btn pan-btn {isPanActive ? 'active' : ''}"
83
+ onclick={onPanToggle}
84
+ >
85
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
86
+ <path d="M18 11V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v0"/>
87
+ <path d="M14 10V4a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v2"/>
88
+ <path d="M10 10.5V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v8"/>
89
+ <path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/>
90
+ </svg>
91
+ </button>
92
+ </Tooltip>
93
+
94
+ <div class="divider"></div>
95
+
96
+ <!-- Zoom Out -->
97
+ <Tooltip text="Zoom Out [Ctrl/Cmd] + [−]" position="above">
98
+ <button
99
+ class="panel-btn"
100
+ onclick={onZoomOut}
101
+ disabled={zoomLevel <= 10}
102
+ >
103
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
104
+ <circle cx="11" cy="11" r="8"/>
105
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
106
+ <line x1="8" y1="11" x2="14" y2="11"/>
107
+ </svg>
108
+ </button>
109
+ </Tooltip>
110
+
111
+ <!-- Zoom Level Input -->
112
+ <div class="zoom-level">
113
+ <input
114
+ type="text"
115
+ class="zoom-input"
116
+ value={zoomInputValue}
117
+ oninput={handleZoomInputChange}
118
+ onblur={handleZoomInputBlur}
119
+ onkeydown={handleZoomInputKeydown}
120
+ maxlength="3"
121
+ />
122
+ <span class="zoom-unit">%</span>
123
+ </div>
124
+
125
+ <!-- Zoom In -->
126
+ <Tooltip text="Zoom In [Ctrl/Cmd] + [+]" position="above">
127
+ <button
128
+ class="panel-btn"
129
+ onclick={onZoomIn}
130
+ disabled={zoomLevel >= 500}
131
+ >
132
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
133
+ <circle cx="11" cy="11" r="8"/>
134
+ <line x1="21" y1="21" x2="16.65" y2="16.65"/>
135
+ <line x1="11" y1="8" x2="11" y2="14"/>
136
+ <line x1="8" y1="11" x2="14" y2="11"/>
137
+ </svg>
138
+ </button>
139
+ </Tooltip>
140
+
141
+ <div class="divider"></div>
142
+
143
+ <!-- Reset Zoom (100%) -->
144
+ <Tooltip text="Reset Zoom 100% [Ctrl/Cmd] + [0]" position="above">
145
+ <button
146
+ class="panel-btn"
147
+ onclick={onZoomReset}
148
+ >
149
+ <span class="text-btn">1:1</span>
150
+ </button>
151
+ </Tooltip>
152
+
153
+ <!-- Fit to View -->
154
+ <Tooltip text="Fit Content to View [Ctrl/Cmd] + [9]" position="above">
155
+ <button
156
+ class="panel-btn"
157
+ onclick={onZoomFit}
158
+ >
159
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
160
+ <path d="M8 3H5a2 2 0 0 0-2 2v3"/>
161
+ <path d="M21 8V5a2 2 0 0 0-2-2h-3"/>
162
+ <path d="M3 16v3a2 2 0 0 0 2 2h3"/>
163
+ <path d="M16 21h3a2 2 0 0 0 2-2v-3"/>
164
+ </svg>
165
+ </button>
166
+ </Tooltip>
167
+
168
+ <div class="divider"></div>
169
+
170
+ <!-- Keyboard Shortcuts — moved here from toolbar so it lives with view controls, not drawing tools -->
171
+ <Tooltip text="Keyboard Shortcuts [?]" position="above">
172
+ <button
173
+ class="panel-btn shortcuts-btn {shortcutsOpen ? 'active' : ''}"
174
+ onclick={onShortcutsToggle}
175
+ aria-label="Toggle keyboard shortcuts panel"
176
+ >
177
+ <!-- Keyboard icon — larger so it's clearly readable -->
178
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
179
+ <rect x="2" y="6" width="20" height="13" rx="2"/>
180
+ <path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M8 14h8M6 14h.01M18 14h.01"/>
181
+ </svg>
182
+ </button>
183
+ </Tooltip>
184
+
185
+ {#if showSaveButton}
186
+ <div class="divider"></div>
187
+ <Tooltip text={hasUnsavedChanges ? 'Unsaved changes — click to save [Cmd+S]' : 'All changes saved [Cmd+S]'} position="above">
188
+ <button
189
+ class="panel-btn save-btn {hasUnsavedChanges ? 'save-btn--dirty' : ''}"
190
+ onclick={() => onSave?.()}
191
+ >
192
+ {#if hasUnsavedChanges}
193
+ <span class="save-dirty-dot">●</span>
194
+ {:else}
195
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
196
+ <path d="M2 2h8l2 2v8a1 1 0 01-1 1H3a1 1 0 01-1-1V2z" stroke="currentColor" stroke-width="1.3"/>
197
+ <rect x="4.5" y="2" width="3" height="3.5" rx="0.5" stroke="currentColor" stroke-width="1.2"/>
198
+ <rect x="3" y="7.5" width="8" height="4" rx="0.5" stroke="currentColor" stroke-width="1.2"/>
199
+ </svg>
200
+ {/if}
201
+ </button>
202
+ </Tooltip>
203
+ {/if}
204
+ </div>
205
+
206
+ <style>
207
+ .pan-zoom-panel {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 4px;
211
+ padding: 6px 10px;
212
+ background: white;
213
+ border: 2px solid #e5e7eb;
214
+ border-radius: 8px;
215
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
216
+ }
217
+
218
+ .panel-btn {
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ width: 32px;
223
+ height: 32px;
224
+ padding: 0;
225
+ border: 1px solid transparent;
226
+ border-radius: 6px;
227
+ background: transparent;
228
+ color: #6b7280;
229
+ cursor: pointer;
230
+ transition: all 0.15s ease;
231
+ }
232
+
233
+ .panel-btn:hover:not(:disabled) {
234
+ background: #f3f4f6;
235
+ color: #374151;
236
+ }
237
+
238
+ .panel-btn:active:not(:disabled) {
239
+ background: #e5e7eb;
240
+ }
241
+
242
+ .panel-btn.active {
243
+ background: var(--main-color, #314F2F);
244
+ color: white;
245
+ border-color: var(--main-color, #314F2F);
246
+ }
247
+
248
+ .panel-btn.active:hover {
249
+ background: var(--main-color-hover, #263d24);
250
+ color: white;
251
+ }
252
+
253
+ .panel-btn:disabled {
254
+ opacity: 0.4;
255
+ cursor: not-allowed;
256
+ }
257
+
258
+ /* Keyboard shortcuts button — slightly bigger to ensure the icon is clearly readable */
259
+ .shortcuts-btn {
260
+ width: 36px;
261
+ height: 36px;
262
+ }
263
+
264
+ .text-btn {
265
+ font-size: 11px;
266
+ font-weight: 600;
267
+ letter-spacing: -0.5px;
268
+ }
269
+
270
+ .divider {
271
+ width: 1px;
272
+ height: 24px;
273
+ background: #e5e7eb;
274
+ margin: 0 4px;
275
+ }
276
+
277
+ .zoom-level {
278
+ display: flex;
279
+ align-items: center;
280
+ border: 1px solid #e5e7eb;
281
+ border-radius: 4px;
282
+ overflow: hidden;
283
+ background: #f9fafb;
284
+ }
285
+
286
+ .zoom-input {
287
+ width: 36px;
288
+ padding: 4px 6px;
289
+ border: none;
290
+ background: transparent;
291
+ font-size: 12px;
292
+ font-weight: 500;
293
+ text-align: right;
294
+ outline: none;
295
+ }
296
+
297
+ .zoom-input:focus {
298
+ background: white;
299
+ }
300
+
301
+ .zoom-unit {
302
+ padding: 4px 6px 4px 0;
303
+ font-size: 11px;
304
+ color: #6b7280;
305
+ }
306
+
307
+ .save-btn--dirty {
308
+ color: #ef4444 !important;
309
+ }
310
+
311
+ .save-dirty-dot {
312
+ font-size: 16px;
313
+ line-height: 1;
314
+ }
315
+ </style>