@widgetic/canvas 0.5.6 → 0.5.9

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.
@@ -22,6 +22,8 @@ declare const __propDef: {
22
22
  }) => void) | null;
23
23
  panZoomPosition?: "TL" | "TC" | "TR" | "BL" | "BC" | "BR";
24
24
  wireframeMode?: boolean;
25
+ advancedFeatures?: boolean;
26
+ /** When true and advanced is on, Frame/Image/Video stay on the toolbar. Default true. */ mediaBtsOnBar?: boolean;
25
27
  onReady?: (() => void) | null;
26
28
  isExternalDataLoading?: boolean;
27
29
  onBeforeUnmount?: ((json: string | null) => void) | null;
@@ -60,6 +62,16 @@ declare const __propDef: {
60
62
  viewportWidth: number;
61
63
  viewportHeight: number;
62
64
  } | null;
65
+ getObjectScreenRect?: (objectId: string) => {
66
+ left: number;
67
+ top: number;
68
+ width: number;
69
+ height: number;
70
+ viewportLeft: number;
71
+ viewportTop: number;
72
+ viewportWidth: number;
73
+ viewportHeight: number;
74
+ } | null;
63
75
  getWidgetShapeDataUrl?: (widgetId: string) => string | null;
64
76
  addWidgetShape?: (widgetId: string, widgetName?: string, gridIndex?: number, totalCount?: number, previewImageUrl?: string | null) => string | null;
65
77
  getLinkedWidgetIds?: () => Set<string>;
@@ -80,6 +92,7 @@ declare const __propDef: {
80
92
  setPrimaryWidgetShape?: (widgetId: string) => boolean;
81
93
  getPrimaryWidgetId?: () => string | null;
82
94
  onSelectImageTool?: () => void;
95
+ onSelectVideoTool?: () => void;
83
96
  onSelectPanTool?: () => void;
84
97
  zoomIn?: () => void;
85
98
  zoomOut?: () => void;
@@ -132,6 +145,16 @@ export default class Canvas extends SvelteComponentTyped<CanvasProps, CanvasEven
132
145
  viewportWidth: number;
133
146
  viewportHeight: number;
134
147
  } | null;
148
+ get getObjectScreenRect(): (objectId: string) => {
149
+ left: number;
150
+ top: number;
151
+ width: number;
152
+ height: number;
153
+ viewportLeft: number;
154
+ viewportTop: number;
155
+ viewportWidth: number;
156
+ viewportHeight: number;
157
+ } | null;
135
158
  get getWidgetShapeDataUrl(): (widgetId: string) => string | null;
136
159
  get addWidgetShape(): (widgetId: string, widgetName?: string, gridIndex?: number, totalCount?: number, previewImageUrl?: string | null) => string | null;
137
160
  get getLinkedWidgetIds(): () => Set<string>;
@@ -152,6 +175,7 @@ export default class Canvas extends SvelteComponentTyped<CanvasProps, CanvasEven
152
175
  get setPrimaryWidgetShape(): (widgetId: string) => boolean;
153
176
  get getPrimaryWidgetId(): () => string | null;
154
177
  get onSelectImageTool(): () => void;
178
+ get onSelectVideoTool(): () => void;
155
179
  get onSelectPanTool(): () => void;
156
180
  get zoomIn(): () => void;
157
181
  get zoomOut(): () => void;
@@ -7,7 +7,7 @@
7
7
  * COMPONENT PROPS
8
8
  */
9
9
  // Tool type definition
10
- type ToolName = 'select' | 'erase' | 'draw' | 'arrow' | 'text' | 'rect' | 'circle' | 'circle-true' | 'triangle' | 'frame' | 'pan' | 'image' | 'library-shape';
10
+ type ToolName = 'select' | 'erase' | 'draw' | 'arrow' | 'text' | 'rect' | 'circle' | 'circle-true' | 'triangle' | 'frame' | 'pan' | 'image' | 'video' | 'library-shape';
11
11
  // Shape tool sub-type for the shapes dropdown (Arrow is a dedicated button, not in dropdown)
12
12
  type ShapeToolName = 'rect' | 'circle' | 'circle-true' | 'triangle';
13
13
 
@@ -31,6 +31,7 @@
31
31
  export let onSelectTextTool: () => void = () => {};
32
32
  export let onSelectPanTool: () => void = () => {};
33
33
  export let onSelectImageTool: () => void = () => {};
34
+ export let onSelectVideoTool: () => void = () => {};
34
35
  // Zoom callbacks — optional, used for Ctrl+/Ctrl- shortcuts
35
36
  export let onZoomIn: () => void = () => {};
36
37
  export let onZoomOut: () => void = () => {};
@@ -63,6 +64,17 @@
63
64
  /** Draw-to-place: activates library shape tool so user clicks-and-drags to size it. */
64
65
  export let onSelectLibraryShapeTool: (shapeKey: string, pathData: string) => void = () => {};
65
66
 
67
+ /** Extra tools (undo/redo/lock). Code stays; UI is gated. */
68
+ export let advancedFeatures: boolean = false;
69
+ /**
70
+ * When true (default) and advancedFeatures is on, Frame / Image / Video sit on the toolbar.
71
+ * When false, they leave the bar and appear under More Shapes → Media Shapes.
72
+ */
73
+ export let mediaBtsOnBar: boolean = true;
74
+
75
+ $: showMediaOnBar = advancedFeatures && mediaBtsOnBar;
76
+ $: showMediaInDropdown = !showMediaOnBar;
77
+
66
78
  // Pre-group shapes for the dropdown
67
79
  const libraryShapesByCategory = getShapesByCategory();
68
80
 
@@ -76,7 +88,7 @@
76
88
 
77
89
  // ─── Shapes + Image dropdown state ───
78
90
  // The last-selected tool within the dropdown (shapes, image, OR library shape).
79
- type DropdownToolName = ShapeToolName | 'image' | 'library-shape';
91
+ type DropdownToolName = ShapeToolName | 'image' | 'video' | 'frame' | 'library-shape';
80
92
  let lastSelectedDropdownTool: DropdownToolName = 'rect';
81
93
  // When a library shape was last selected, store its info for the trigger button
82
94
  let lastSelectedLibraryShape: LibraryShape | null = null;
@@ -134,12 +146,29 @@
134
146
  };
135
147
 
136
148
  // The currently displayed tool in the dropdown button (active tool if it's a dropdown tool, else last selected)
137
- const dropdownTools: DropdownToolName[] = ['rect', 'circle', 'circle-true', 'triangle', 'image', 'library-shape'];
138
- const dropdownToolsWithoutImage = dropdownTools.filter(t => t !== 'image');
139
- $: activeDropdownTool = dropdownToolsWithoutImage.includes(activeTool as DropdownToolName)
149
+ const mediaToolInfo: Record<'frame' | 'image' | 'video', { iconSvg: string; label: string; key: string }> = {
150
+ frame: {
151
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 3v6"/></svg>`,
152
+ label: 'Frame',
153
+ key: 'F',
154
+ },
155
+ image: {
156
+ iconSvg: imageDropdownInfo.iconSvg,
157
+ label: 'Image',
158
+ key: 'I',
159
+ },
160
+ video: {
161
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>`,
162
+ label: 'Video',
163
+ key: '',
164
+ },
165
+ };
166
+
167
+ const dropdownTools: DropdownToolName[] = ['rect', 'circle', 'circle-true', 'triangle', 'image', 'video', 'frame', 'library-shape'];
168
+ $: activeDropdownTool = dropdownTools.includes(activeTool as DropdownToolName)
140
169
  ? (activeTool as DropdownToolName)
141
170
  : lastSelectedDropdownTool;
142
- $: activeDropdownIsActive = dropdownToolsWithoutImage.includes(activeTool as DropdownToolName);
171
+ $: activeDropdownIsActive = dropdownTools.includes(activeTool as DropdownToolName);
143
172
 
144
173
  // Backwards-compat aliases used in template
145
174
  $: activeShapeInDropdown = activeDropdownTool as ShapeToolName;
@@ -152,10 +181,23 @@
152
181
  }
153
182
 
154
183
  function selectImageFromDropdown() {
184
+ lastSelectedDropdownTool = 'image';
155
185
  if (!KEEP_SHAPES_DROPDOWN_OPEN) shapesDropdownOpen = false;
156
186
  onSelectImageTool();
157
187
  }
158
188
 
189
+ function selectFrameFromDropdown() {
190
+ lastSelectedDropdownTool = 'frame';
191
+ if (!KEEP_SHAPES_DROPDOWN_OPEN) shapesDropdownOpen = false;
192
+ onSelectFrameTool();
193
+ }
194
+
195
+ function selectVideoFromDropdown() {
196
+ lastSelectedDropdownTool = 'video';
197
+ if (!KEEP_SHAPES_DROPDOWN_OPEN) shapesDropdownOpen = false;
198
+ onSelectVideoTool();
199
+ }
200
+
159
201
  function selectLibraryShape(shape: LibraryShape) {
160
202
  lastSelectedDropdownTool = 'library-shape';
161
203
  lastSelectedLibraryShape = shape;
@@ -167,14 +209,26 @@
167
209
  function activateDropdownTool() {
168
210
  if (activeDropdownTool === 'library-shape' && lastSelectedLibraryShape) {
169
211
  selectLibraryShape(lastSelectedLibraryShape);
212
+ } else if (activeDropdownTool === 'image') {
213
+ selectImageFromDropdown();
214
+ } else if (activeDropdownTool === 'video') {
215
+ selectVideoFromDropdown();
216
+ } else if (activeDropdownTool === 'frame') {
217
+ selectFrameFromDropdown();
170
218
  } else {
171
219
  selectShape(activeDropdownTool as ShapeToolName);
172
220
  }
173
221
  }
174
222
 
223
+ $: mediaTrigger = (activeDropdownTool === 'frame' || activeDropdownTool === 'image' || activeDropdownTool === 'video')
224
+ ? mediaToolInfo[activeDropdownTool]
225
+ : null;
226
+
175
227
  // Tooltip text for the main dropdown button
176
228
  $: dropdownButtonTooltip = activeDropdownTool === 'library-shape' && lastSelectedLibraryShape
177
229
  ? lastSelectedLibraryShape.label
230
+ : mediaTrigger
231
+ ? (mediaTrigger.key ? `${mediaTrigger.label} (${mediaTrigger.key})` : mediaTrigger.label)
178
232
  : shapeInfo[activeDropdownTool as ShapeToolName]?.label
179
233
  ? (shapeInfo[activeDropdownTool as ShapeToolName].key
180
234
  ? `${shapeInfo[activeDropdownTool as ShapeToolName].label} (${shapeInfo[activeDropdownTool as ShapeToolName].key})`
@@ -464,6 +518,7 @@
464
518
  <!-- Scrollable container for tools — overflow-x-auto clips absolute children,
465
519
  so dropdowns and tooltips are rendered as siblings OUTSIDE this div -->
466
520
  <div class="tools-ct flex flex-row">
521
+ {#if advancedFeatures}
467
522
  <!-- ── Utility cluster: Undo, Redo, Hand, Lock | Divider ── -->
468
523
  <Tooltip text="Undo (Ctrl+Z)">
469
524
  <div class="disabled-tooltip-wrap">
@@ -528,6 +583,7 @@
528
583
  </Tooltip>
529
584
 
530
585
  <div class="toolbar-divider"></div>
586
+ {/if}
531
587
 
532
588
  <!-- ══ Illustrated tools: Pencil & Eraser — "pulled from sheath" on select ══ -->
533
589
  <!-- Each tool is wrapped in a mask container that clips its body at rest.
@@ -623,6 +679,7 @@
623
679
  </button>
624
680
  </Tooltip>
625
681
 
682
+ {#if showMediaOnBar}
626
683
  <div class="toolbar-divider"></div>
627
684
 
628
685
  <Tooltip text="Frame (F)">
@@ -649,7 +706,19 @@
649
706
  </button>
650
707
  </Tooltip>
651
708
 
652
- <!-- Shapes: tldraw-style split — left activates tool, right opens dropdown -->
709
+ <Tooltip text="Video">
710
+ <button class="video-bt {buttonClass} {activeTool === 'video' ? activeClass : inactiveClass}"
711
+ onclick={onSelectVideoTool}
712
+ aria-label="Video">
713
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
714
+ <rect x="2" y="6" width="14" height="12" rx="2"/>
715
+ <path d="M16 10l6-3v10l-6-3z"/>
716
+ </svg>
717
+ </button>
718
+ </Tooltip>
719
+ {/if}
720
+
721
+ <!-- Shapes: tldraw-style split — left activates tool, right opens dropdown. Always available. -->
653
722
  <div class="shapes-split-ct" bind:this={dropdownTriggerEl}>
654
723
  <Tooltip text={dropdownButtonTooltip}>
655
724
  <button
@@ -659,6 +728,8 @@
659
728
  >
660
729
  {#if activeDropdownTool === 'library-shape' && lastSelectedLibraryShape}
661
730
  {@html lastSelectedLibraryShape.iconSvg}
731
+ {:else if mediaTrigger}
732
+ {@html mediaTrigger.iconSvg}
662
733
  {:else if shapeInfo[activeDropdownTool as ShapeToolName]}
663
734
  {@html shapeInfo[activeDropdownTool as ShapeToolName].iconSvg}
664
735
  {:else}
@@ -722,6 +793,29 @@
722
793
  use:overlayScrollbar
723
794
  style={shapesDropdownNeedsScroll ? 'margin-right: -6px' : ''}
724
795
  >
796
+ <!-- Media Shapes — first so Frame / Image / Video are easy to reach when not on the bar -->
797
+ {#if showMediaInDropdown}
798
+ <div class="shapes-category-label">Media Shapes</div>
799
+ <div class="shapes-library-grid">
800
+ <Tooltip text="Frame (F)" position="above">
801
+ <button class="shape-library-bt {activeTool === 'frame' ? 'active' : ''}" onclick={selectFrameFromDropdown} aria-label="Frame">
802
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 3v6"/></svg>
803
+ </button>
804
+ </Tooltip>
805
+ <Tooltip text="Image (I)" position="above">
806
+ <button class="shape-library-bt {activeTool === 'image' ? 'active' : ''}" onclick={selectImageFromDropdown} aria-label="Image">
807
+ {@html imageDropdownInfo.iconSvg}
808
+ </button>
809
+ </Tooltip>
810
+ <Tooltip text="Video" position="above">
811
+ <button class="shape-library-bt {activeTool === 'video' ? 'active' : ''}" onclick={selectVideoFromDropdown} aria-label="Video">
812
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="14" height="12" rx="2"/><path d="M16 10l6-3v10l-6-3z"/></svg>
813
+ </button>
814
+ </Tooltip>
815
+ </div>
816
+ <div class="shapes-dropdown-divider"></div>
817
+ {/if}
818
+
725
819
  <!-- Geometric Shapes — 3×2 grid -->
726
820
  <div class="shapes-category-label">Geometric Shapes</div>
727
821
  <div class="shapes-library-grid">
@@ -761,21 +855,6 @@
761
855
  {/each}
762
856
  </div>
763
857
 
764
- <!-- Media (hidden — Image tool moved to main toolbar) -->
765
- {#if false}
766
- <div class="shapes-dropdown-divider"></div>
767
- <div class="shapes-category-label">Media</div>
768
- <Tooltip text="Image (I)" position="above">
769
- <button
770
- class="shape-option-bt {activeTool === 'image' ? 'active' : ''}"
771
- onclick={selectImageFromDropdown}
772
- >
773
- {@html imageDropdownInfo.iconSvg}
774
- {imageDropdownInfo.label}
775
- </button>
776
- </Tooltip>
777
- {/if}
778
-
779
858
  <!-- Library shape categories (skip Geometric, already rendered above) -->
780
859
  {#each [...libraryShapesByCategory.entries()].filter(([cat]) => cat !== 'Geometric Shapes') as [category, shapes]}
781
860
  <div class="shapes-dropdown-divider"></div>
@@ -894,6 +973,13 @@
894
973
  background: transparent !important;
895
974
  box-shadow: none !important;
896
975
  }
976
+ .canvas-toolbar-el :global(button:focus),
977
+ .canvas-toolbar-el :global(button:focus-visible),
978
+ .canvas-toolbar-el :global(a:focus),
979
+ .canvas-toolbar-el :global(a:focus-visible) {
980
+ outline: none !important;
981
+ box-shadow: none !important;
982
+ }
897
983
 
898
984
  /* ── "Sheath" — the white container holding all buttons ── */
899
985
  .tools-ct {
@@ -901,7 +987,7 @@
901
987
  border-radius: 1.25rem;
902
988
  box-shadow: 0 4px 24px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.04);
903
989
  height: 80px;
904
- padding: 0 12px;
990
+ padding: 0 24px;
905
991
  gap: 4px;
906
992
  align-items: center;
907
993
  overflow: visible;
@@ -919,9 +1005,10 @@ button {
919
1005
  background: transparent;
920
1006
  }
921
1007
 
922
- button:focus {
923
- outline: none;
924
- box-shadow: none;
1008
+ button:focus,
1009
+ button:focus-visible {
1010
+ outline: none !important;
1011
+ box-shadow: none !important;
925
1012
  }
926
1013
 
927
1014
  button:disabled {
@@ -3,7 +3,7 @@ declare const __propDef: {
3
3
  props: {
4
4
  class?: string;
5
5
  style?: string;
6
- activeTool?: "select" | "rect" | "erase" | "draw" | "arrow" | "text" | "circle" | "circle-true" | "triangle" | "frame" | "pan" | "image" | "library-shape";
6
+ activeTool?: "select" | "video" | "rect" | "erase" | "draw" | "arrow" | "text" | "circle" | "circle-true" | "triangle" | "frame" | "pan" | "image" | "library-shape";
7
7
  activeClass?: string;
8
8
  inactiveClass?: string;
9
9
  buttonClass?: string;
@@ -15,6 +15,7 @@ declare const __propDef: {
15
15
  onSelectTextTool?: () => void;
16
16
  onSelectPanTool?: () => void;
17
17
  onSelectImageTool?: () => void;
18
+ onSelectVideoTool?: () => void;
18
19
  onZoomIn?: () => void;
19
20
  onZoomOut?: () => void;
20
21
  onZoomReset?: () => void;
@@ -32,6 +33,11 @@ declare const __propDef: {
32
33
  onToggleStickyTool?: () => void;
33
34
  /** Called when user clicks a library shape. Canvas inserts it as a Path. */ onInsertLibraryShape?: (shapeKey: string, pathData: string) => void;
34
35
  /** Draw-to-place: activates library shape tool so user clicks-and-drags to size it. */ onSelectLibraryShapeTool?: (shapeKey: string, pathData: string) => void;
36
+ /** Extra tools (undo/redo/lock). Code stays; UI is gated. */ advancedFeatures?: boolean;
37
+ /**
38
+ * When true (default) and advancedFeatures is on, Frame / Image / Video sit on the toolbar.
39
+ * When false, they leave the bar and appear under More Shapes → Media Shapes.
40
+ */ mediaBtsOnBar?: boolean;
35
41
  addText?: () => void;
36
42
  addRect?: () => void;
37
43
  addTriangle?: () => void;
@@ -27,6 +27,9 @@
27
27
  // Reflects open state so the button can show an active style
28
28
  export let shortcutsOpen: boolean = false;
29
29
 
30
+ /** Extra view controls (grid, pan, 1:1, fit, shortcuts). Zoom ± / % and save stay available. */
31
+ export let advancedFeatures: boolean = false;
32
+
30
33
  // Save button: visible when showSaveButton is true
31
34
  export let showSaveButton: boolean = false;
32
35
  export let hasUnsavedChanges: boolean = false;
@@ -56,9 +59,29 @@
56
59
  (e.target as HTMLInputElement).blur();
57
60
  }
58
61
  }
62
+
63
+ let isMinimized = true;
64
+
65
+ function toggleMinimized() {
66
+ isMinimized = !isMinimized;
67
+ console.log('[PanZoomPanel] minimized:', isMinimized);
68
+ }
59
69
  </script>
60
70
 
61
- <div class="{className ? className + ' ' : ''}pan-zoom-panel" style={style}>
71
+ <div class="pan-zoom-panel-ct {className ? className + ' ' : ''}pan-zoom-panel {isMinimized ? 'pan-zoom-panel-minimized' : ''}" style={style}>
72
+ {#if isMinimized}
73
+ <Tooltip text="Zoom {zoomLevel}% — click to expand" position="above">
74
+ <button
75
+ type="button"
76
+ class="panel-btn pan-zoom-expand-btn"
77
+ onclick={toggleMinimized}
78
+ aria-label="Expand zoom controls"
79
+ >
80
+ <span class="text-btn">{zoomLevel}%</span>
81
+ </button>
82
+ </Tooltip>
83
+ {:else}
84
+ {#if advancedFeatures}
62
85
  <!-- Grid Toggle -->
63
86
  <Tooltip text="Toggle Grid [G]" position="above">
64
87
  <button
@@ -92,6 +115,7 @@
92
115
  </Tooltip>
93
116
 
94
117
  <div class="divider"></div>
118
+ {/if}
95
119
 
96
120
  <!-- Zoom Out -->
97
121
  <Tooltip text="Zoom Out [Ctrl/Cmd] + [−]" position="above">
@@ -110,6 +134,19 @@
110
134
 
111
135
  <!-- Zoom Level Input -->
112
136
  <div class="zoom-level">
137
+ <Tooltip text="Reset zoom to 100% [Ctrl/Cmd] + [0]" position="above">
138
+ <button
139
+ type="button"
140
+ class="zoom-reset-in-input"
141
+ onclick={onZoomReset}
142
+ aria-label="Reset zoom to 100%"
143
+ >
144
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
145
+ <polyline points="1 4 1 10 7 10"/>
146
+ <path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
147
+ </svg>
148
+ </button>
149
+ </Tooltip>
113
150
  <input
114
151
  type="text"
115
152
  class="zoom-input"
@@ -138,6 +175,7 @@
138
175
  </button>
139
176
  </Tooltip>
140
177
 
178
+ {#if advancedFeatures}
141
179
  <div class="divider"></div>
142
180
 
143
181
  <!-- Reset Zoom (100%) -->
@@ -181,8 +219,9 @@
181
219
  </svg>
182
220
  </button>
183
221
  </Tooltip>
222
+ {/if}
184
223
 
185
- {#if showSaveButton}
224
+ {#if showSaveButton && advancedFeatures}
186
225
  <div class="divider"></div>
187
226
  <Tooltip text={hasUnsavedChanges ? 'Unsaved changes — click to save [Cmd+S]' : 'All changes saved [Cmd+S]'} position="above">
188
227
  <button
@@ -201,18 +240,77 @@
201
240
  </button>
202
241
  </Tooltip>
203
242
  {/if}
243
+
244
+ <div class="pan-zoom-minimize-anchor">
245
+ <Tooltip text="Minimize Zoom Panel" position="above">
246
+ <button
247
+ type="button"
248
+ class="pan-zoom-minimize-btn"
249
+ onclick={toggleMinimized}
250
+ aria-label="Minimize Zoom Panel"
251
+ >
252
+ <svg width="8" height="8" viewBox="0 0 12 12" fill="none" aria-hidden="true">
253
+ <rect x="2" y="5.25" width="8" height="1.5" rx="0.25" fill="currentColor"/>
254
+ </svg>
255
+ </button>
256
+ </Tooltip>
257
+ </div>
258
+ {/if}
204
259
  </div>
205
260
 
206
261
  <style>
207
262
  .pan-zoom-panel {
263
+ position: relative;
208
264
  display: flex;
209
265
  align-items: center;
210
266
  gap: 4px;
267
+ width: max-content;
268
+ max-width: 100%;
211
269
  padding: 6px 10px;
212
270
  background: white;
213
271
  border: 2px solid #e5e7eb;
214
272
  border-radius: 8px;
215
273
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
274
+ overflow: visible;
275
+ }
276
+
277
+ .pan-zoom-minimize-anchor {
278
+ position: absolute;
279
+ top: -8px;
280
+ right: -8px;
281
+ z-index: 2;
282
+ }
283
+
284
+ .pan-zoom-minimize-btn {
285
+ display: flex;
286
+ align-items: center;
287
+ justify-content: center;
288
+ width: 14px;
289
+ height: 14px;
290
+ padding: 0;
291
+ border-radius: 9999px;
292
+ background: #ffffff;
293
+ border: 1px solid #9ca3af;
294
+ color: #9ca3af;
295
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
296
+ cursor: pointer;
297
+ transition: color 0.15s ease, border-color 0.15s ease;
298
+ }
299
+
300
+ .pan-zoom-minimize-btn:hover:not(:disabled) {
301
+ background: #ffffff;
302
+ border-color: #111827;
303
+ color: #111827;
304
+ }
305
+
306
+ .pan-zoom-panel-minimized {
307
+ padding: 4px;
308
+ }
309
+
310
+ .pan-zoom-expand-btn {
311
+ min-width: 44px;
312
+ width: auto;
313
+ padding: 0 8px;
216
314
  }
217
315
 
218
316
  .panel-btn {
@@ -268,6 +366,14 @@
268
366
  cursor: not-allowed;
269
367
  }
270
368
 
369
+ .pan-zoom-panel button:focus,
370
+ .pan-zoom-panel button:focus-visible,
371
+ .pan-zoom-panel input:focus,
372
+ .pan-zoom-panel input:focus-visible {
373
+ outline: none;
374
+ box-shadow: none;
375
+ }
376
+
271
377
  /* Keyboard shortcuts button — slightly bigger to ensure the icon is clearly readable */
272
378
  .shortcuts-btn {
273
379
  width: 36px;
@@ -296,14 +402,34 @@
296
402
  background: #f9fafb;
297
403
  }
298
404
 
405
+ .zoom-reset-in-input {
406
+ display: flex;
407
+ align-items: center;
408
+ justify-content: center;
409
+ width: 18px;
410
+ height: 24px;
411
+ padding: 0;
412
+ margin: 0 0 0 2px;
413
+ border: none;
414
+ background: transparent;
415
+ color: #6b7280;
416
+ cursor: pointer;
417
+ flex-shrink: 0;
418
+ }
419
+
420
+ .zoom-reset-in-input:hover {
421
+ color: #111827;
422
+ background: #f3f4f6;
423
+ }
424
+
299
425
  .zoom-input {
300
- width: 36px;
301
- padding: 4px 6px;
426
+ width: 28px;
427
+ padding: 4px 0;
302
428
  border: none;
303
429
  background: transparent;
304
430
  font-size: 12px;
305
431
  font-weight: 500;
306
- text-align: right;
432
+ text-align: center;
307
433
  outline: none;
308
434
  }
309
435
 
@@ -312,9 +438,13 @@
312
438
  }
313
439
 
314
440
  .zoom-unit {
315
- padding: 4px 6px 4px 0;
441
+ width: 18px;
442
+ padding: 4px 0;
443
+ margin: 0 2px 0 0;
316
444
  font-size: 11px;
317
445
  color: #6b7280;
446
+ text-align: center;
447
+ flex-shrink: 0;
318
448
  }
319
449
 
320
450
  .save-btn--dirty {
@@ -15,6 +15,7 @@ declare const __propDef: {
15
15
  onGridToggle?: () => void;
16
16
  onShortcutsToggle?: () => void;
17
17
  shortcutsOpen?: boolean;
18
+ /** Extra view controls (grid, pan, 1:1, fit, shortcuts). Zoom ± / % and save stay available. */ advancedFeatures?: boolean;
18
19
  showSaveButton?: boolean;
19
20
  hasUnsavedChanges?: boolean;
20
21
  onSave?: (() => void) | null;