@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.
- package/README.md +45 -0
- package/dist/canvas/Canvas.svelte +10678 -0
- package/dist/canvas/Canvas.svelte.d.ts +147 -0
- package/dist/canvas/CanvasToolbar.svelte +1422 -0
- package/dist/canvas/CanvasToolbar.svelte.d.ts +54 -0
- package/dist/canvas/ContextMenu.svelte +279 -0
- package/dist/canvas/ContextMenu.svelte.d.ts +36 -0
- package/dist/canvas/PanZoomPanel.svelte +315 -0
- package/dist/canvas/PanZoomPanel.svelte.d.ts +32 -0
- package/dist/canvas/canvasLogger.d.ts +5 -0
- package/dist/canvas/canvasLogger.js +19 -0
- package/dist/canvas/index.d.ts +13 -0
- package/dist/canvas/index.js +12 -0
- package/dist/canvas/props-panel/PropsPanel.svelte +1902 -0
- package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +73 -0
- package/dist/canvas/props-panel/TextPropsICSection.svelte +238 -0
- package/dist/canvas/props-panel/TextPropsICSection.svelte.d.ts +36 -0
- package/dist/canvas/shapes/FrameShape.d.ts +97 -0
- package/dist/canvas/shapes/FrameShape.js +951 -0
- package/dist/canvas/shapes/ImageShape.d.ts +38 -0
- package/dist/canvas/shapes/ImageShape.js +245 -0
- package/dist/canvas/shapes/ShapeLibrary.d.ts +64 -0
- package/dist/canvas/shapes/ShapeLibrary.js +526 -0
- package/dist/canvas/shapes/WidgetShape.d.ts +21 -0
- package/dist/canvas/shapes/WidgetShape.js +132 -0
- package/dist/canvas/types.d.ts +26 -0
- package/dist/canvas/types.js +5 -0
- package/dist/components/Tooltip.svelte +179 -0
- package/dist/components/Tooltip.svelte.d.ts +21 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.js +13 -0
- package/dist/components/input-controls/ColorIC.svelte +388 -0
- package/dist/components/input-controls/ColorIC.svelte.d.ts +22 -0
- package/dist/components/input-controls/FontSelectorIC.svelte +69 -0
- package/dist/components/input-controls/FontSelectorIC.svelte.d.ts +17 -0
- package/dist/components/input-controls/SliderUnitIC.svelte +217 -0
- package/dist/components/input-controls/SliderUnitIC.svelte.d.ts +24 -0
- package/dist/components/input-controls/TextAlignIC.svelte +84 -0
- package/dist/components/input-controls/TextAlignIC.svelte.d.ts +17 -0
- package/dist/components/input-controls/TextStyleIC.svelte +85 -0
- package/dist/components/input-controls/TextStyleIC.svelte.d.ts +21 -0
- package/dist/icons/arrow.svg +3 -0
- package/dist/icons/checkmark.svg +3 -0
- package/dist/icons/draw.svg +22 -0
- package/dist/icons/eraser.svg +23 -0
- package/dist/icons/hand.svg +6 -0
- package/dist/icons/select.svg +3 -0
- package/dist/icons/text.svg +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/package.json +101 -0
|
@@ -0,0 +1,1422 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import Tooltip from '../components/Tooltip.svelte';
|
|
4
|
+
import { SHAPE_LIBRARY, getShapesByCategory, type LibraryShape } from './shapes/ShapeLibrary';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* COMPONENT PROPS
|
|
8
|
+
*/
|
|
9
|
+
// Tool type definition
|
|
10
|
+
type ToolName = 'select' | 'erase' | 'draw' | 'arrow' | 'text' | 'rect' | 'circle' | 'circle-true' | 'triangle' | 'frame' | 'pan' | 'image' | 'library-shape';
|
|
11
|
+
// Shape tool sub-type for the shapes dropdown (Arrow is a dedicated button, not in dropdown)
|
|
12
|
+
type ShapeToolName = 'rect' | 'circle' | 'circle-true' | 'triangle';
|
|
13
|
+
|
|
14
|
+
// Class
|
|
15
|
+
let className = '';
|
|
16
|
+
export { className as class };
|
|
17
|
+
// Style
|
|
18
|
+
export let style = '';
|
|
19
|
+
// component state props
|
|
20
|
+
export let activeTool: ToolName = 'select';
|
|
21
|
+
export let activeClass: string = 'wdg-tool-active';
|
|
22
|
+
export let inactiveClass: string = 'wdg-tool-inactive';
|
|
23
|
+
export let buttonClass: string = 'wdg-tool-bt';
|
|
24
|
+
|
|
25
|
+
// tools handler functions
|
|
26
|
+
export let onSelectSelectTool: () => void = () => {};
|
|
27
|
+
export let onSelectEraseTool: () => void = () => {};
|
|
28
|
+
export let onSelectDrawTool: () => void = () => {};
|
|
29
|
+
export let onSelectShapeTool: (shapeType: ShapeToolName) => void = () => {};
|
|
30
|
+
export let onSelectFrameTool: () => void = () => {};
|
|
31
|
+
export let onSelectTextTool: () => void = () => {};
|
|
32
|
+
export let onSelectPanTool: () => void = () => {};
|
|
33
|
+
export let onSelectImageTool: () => void = () => {};
|
|
34
|
+
// Zoom callbacks — optional, used for Ctrl+/Ctrl- shortcuts
|
|
35
|
+
export let onZoomIn: () => void = () => {};
|
|
36
|
+
export let onZoomOut: () => void = () => {};
|
|
37
|
+
export let onZoomReset: () => void = () => {};
|
|
38
|
+
export let onZoomFit: () => void = () => {};
|
|
39
|
+
|
|
40
|
+
// actions handler functions
|
|
41
|
+
export let onGroupSelection: () => void = () => {};
|
|
42
|
+
export let onUngroupSelection: () => void = () => {};
|
|
43
|
+
export let onDuplicateSelection: () => void = () => {};
|
|
44
|
+
export let onSelectAllObjects: () => void = () => {};
|
|
45
|
+
export let onClearAllObjects: () => void = () => {};
|
|
46
|
+
|
|
47
|
+
// ── Undo / Redo ────────────────────────────────────────────────────────────
|
|
48
|
+
export let onUndo: () => void = () => {};
|
|
49
|
+
export let onRedo: () => void = () => {};
|
|
50
|
+
/** Disables the Undo button when there is no prior state to go back to. */
|
|
51
|
+
export let canUndo: boolean = false;
|
|
52
|
+
/** Disables the Redo button when there is no future state to redo to. */
|
|
53
|
+
export let canRedo: boolean = false;
|
|
54
|
+
|
|
55
|
+
// ── Sticky tool mode ────────────────────────────────────────────────────────
|
|
56
|
+
/** When true, the drawing tool stays active after placing a shape. */
|
|
57
|
+
export let stickyToolEnabled: boolean = false;
|
|
58
|
+
export let onToggleStickyTool: () => void = () => {};
|
|
59
|
+
|
|
60
|
+
// ── Shape library (special shapes: media, arrows, UI) ────────────────────
|
|
61
|
+
/** Called when user clicks a library shape. Canvas inserts it as a Path. */
|
|
62
|
+
export let onInsertLibraryShape: (shapeKey: string, pathData: string) => void = () => {};
|
|
63
|
+
/** Draw-to-place: activates library shape tool so user clicks-and-drags to size it. */
|
|
64
|
+
export let onSelectLibraryShapeTool: (shapeKey: string, pathData: string) => void = () => {};
|
|
65
|
+
|
|
66
|
+
// Pre-group shapes for the dropdown
|
|
67
|
+
const libraryShapesByCategory = getShapesByCategory();
|
|
68
|
+
|
|
69
|
+
// add shapes handler functions
|
|
70
|
+
export let addText: () => void = () => {};
|
|
71
|
+
export let addRect: () => void = () => {};
|
|
72
|
+
export let addTriangle: () => void = () => {};
|
|
73
|
+
export let addCircle: () => void = () => {};
|
|
74
|
+
export let addFrame: () => void = () => {};
|
|
75
|
+
export let addImage: () => void = () => {};
|
|
76
|
+
|
|
77
|
+
// ─── Shapes + Image dropdown state ───
|
|
78
|
+
// The last-selected tool within the dropdown (shapes, image, OR library shape).
|
|
79
|
+
type DropdownToolName = ShapeToolName | 'image' | 'library-shape';
|
|
80
|
+
let lastSelectedDropdownTool: DropdownToolName = 'rect';
|
|
81
|
+
// When a library shape was last selected, store its info for the trigger button
|
|
82
|
+
let lastSelectedLibraryShape: LibraryShape | null = null;
|
|
83
|
+
let shapesDropdownOpen = false;
|
|
84
|
+
let shapesDropdownNeedsScroll = false;
|
|
85
|
+
// DEV FLAG: when true the dropdown stays open after selecting a shape so
|
|
86
|
+
// multiple shapes can be placed in a row. Set to false for production.
|
|
87
|
+
const KEEP_SHAPES_DROPDOWN_OPEN = true;
|
|
88
|
+
// Position for the dropdown (absolute within canvas-container)
|
|
89
|
+
let dropdownLeft = 0;
|
|
90
|
+
// When opening ABOVE: distance from the container's bottom edge (CSS 'bottom' property)
|
|
91
|
+
let dropdownBottomOffset = 0;
|
|
92
|
+
// When opening BELOW: distance from the container's top edge (CSS 'top' property)
|
|
93
|
+
let dropdownTopOffset = 0;
|
|
94
|
+
// Minimum width of the dropdown — at least as wide as the trigger button
|
|
95
|
+
let dropdownMinWidth = 0;
|
|
96
|
+
let dropdownMaxHeight = 400;
|
|
97
|
+
// True when the dropdown should open ABOVE the button (toolbar is at bottom of canvas)
|
|
98
|
+
let dropdownOpenAbove = false;
|
|
99
|
+
let dropdownTriggerEl: HTMLElement | null = null;
|
|
100
|
+
|
|
101
|
+
// Map shape type to display info (Arrow is a dedicated button, not in this map).
|
|
102
|
+
const shapeInfo: Record<ShapeToolName, { icon: string; iconSvg: string; label: string; key: string }> = {
|
|
103
|
+
rect: {
|
|
104
|
+
icon: '⬜',
|
|
105
|
+
iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="16" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>`,
|
|
106
|
+
label: 'Rectangle',
|
|
107
|
+
key: 'R',
|
|
108
|
+
},
|
|
109
|
+
triangle: {
|
|
110
|
+
icon: '△',
|
|
111
|
+
iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><polygon points="12,3 22,21 2,21" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
|
|
112
|
+
label: 'Triangle',
|
|
113
|
+
key: 'G',
|
|
114
|
+
},
|
|
115
|
+
circle: {
|
|
116
|
+
icon: '⚪',
|
|
117
|
+
iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><ellipse cx="12" cy="12" rx="10" ry="7" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>`,
|
|
118
|
+
label: 'Ellipse',
|
|
119
|
+
key: 'O',
|
|
120
|
+
},
|
|
121
|
+
'circle-true': {
|
|
122
|
+
icon: '⚪',
|
|
123
|
+
iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>`,
|
|
124
|
+
label: 'Circle',
|
|
125
|
+
key: '',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Image dropdown entry — placed at the bottom of the shapes dropdown with a divider
|
|
130
|
+
const imageDropdownInfo = {
|
|
131
|
+
iconSvg: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect x="1" y="2" width="14" height="12" rx="1.5" fill="white" stroke="#9ca3af" stroke-width="1.5"/><polyline points="1.5,10.5 5,6.5 8,9 11,6.5 14.5,10.5" fill="none" stroke="#9ca3af" stroke-width="1.2"/><circle cx="11.5" cy="5" r="1.2" fill="#9ca3af"/></svg>`,
|
|
132
|
+
label: 'Image',
|
|
133
|
+
key: 'I',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 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)
|
|
140
|
+
? (activeTool as DropdownToolName)
|
|
141
|
+
: lastSelectedDropdownTool;
|
|
142
|
+
$: activeDropdownIsActive = dropdownToolsWithoutImage.includes(activeTool as DropdownToolName);
|
|
143
|
+
|
|
144
|
+
// Backwards-compat aliases used in template
|
|
145
|
+
$: activeShapeInDropdown = activeDropdownTool as ShapeToolName;
|
|
146
|
+
$: activeShapeIsActive = activeDropdownIsActive;
|
|
147
|
+
|
|
148
|
+
function selectShape(shape: ShapeToolName) {
|
|
149
|
+
lastSelectedDropdownTool = shape;
|
|
150
|
+
if (!KEEP_SHAPES_DROPDOWN_OPEN) shapesDropdownOpen = false;
|
|
151
|
+
onSelectShapeTool(shape);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function selectImageFromDropdown() {
|
|
155
|
+
if (!KEEP_SHAPES_DROPDOWN_OPEN) shapesDropdownOpen = false;
|
|
156
|
+
onSelectImageTool();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function selectLibraryShape(shape: LibraryShape) {
|
|
160
|
+
lastSelectedDropdownTool = 'library-shape';
|
|
161
|
+
lastSelectedLibraryShape = shape;
|
|
162
|
+
if (!KEEP_SHAPES_DROPDOWN_OPEN) shapesDropdownOpen = false;
|
|
163
|
+
onSelectLibraryShapeTool(shape.key, shape.pathData);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Click action for the main dropdown button (left part)
|
|
167
|
+
function activateDropdownTool() {
|
|
168
|
+
if (activeDropdownTool === 'library-shape' && lastSelectedLibraryShape) {
|
|
169
|
+
selectLibraryShape(lastSelectedLibraryShape);
|
|
170
|
+
} else {
|
|
171
|
+
selectShape(activeDropdownTool as ShapeToolName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Tooltip text for the main dropdown button
|
|
176
|
+
$: dropdownButtonTooltip = activeDropdownTool === 'library-shape' && lastSelectedLibraryShape
|
|
177
|
+
? lastSelectedLibraryShape.label
|
|
178
|
+
: shapeInfo[activeDropdownTool as ShapeToolName]?.label
|
|
179
|
+
? (shapeInfo[activeDropdownTool as ShapeToolName].key
|
|
180
|
+
? `${shapeInfo[activeDropdownTool as ShapeToolName].label} (${shapeInfo[activeDropdownTool as ShapeToolName].key})`
|
|
181
|
+
: shapeInfo[activeDropdownTool as ShapeToolName].label)
|
|
182
|
+
: 'Shapes';
|
|
183
|
+
|
|
184
|
+
// Dropdown width — sized to fit 3 × 40px icon grid with minimal side padding
|
|
185
|
+
const DROPDOWN_WIDTH = 152;
|
|
186
|
+
|
|
187
|
+
function computeDropdownPosition() {
|
|
188
|
+
if (!dropdownTriggerEl) return;
|
|
189
|
+
const rect = dropdownTriggerEl.getBoundingClientRect();
|
|
190
|
+
const containerEl = dropdownTriggerEl.closest('.canvas-container') as HTMLElement | null;
|
|
191
|
+
const containerRect = containerEl
|
|
192
|
+
? containerEl.getBoundingClientRect()
|
|
193
|
+
: { left: 0, top: 0, bottom: window.innerHeight, height: window.innerHeight };
|
|
194
|
+
|
|
195
|
+
const estimatedDropdownHeight = 150;
|
|
196
|
+
dropdownOpenAbove = rect.top > estimatedDropdownHeight;
|
|
197
|
+
|
|
198
|
+
// Center the dropdown horizontally relative to the trigger button
|
|
199
|
+
const buttonCenterX = rect.left + rect.width / 2 - containerRect.left;
|
|
200
|
+
dropdownLeft = Math.max(4, buttonCenterX - DROPDOWN_WIDTH / 2);
|
|
201
|
+
|
|
202
|
+
if (dropdownOpenAbove) {
|
|
203
|
+
dropdownBottomOffset = containerRect.height - (rect.top - containerRect.top) + 8;
|
|
204
|
+
dropdownMaxHeight = rect.top - containerRect.top - 16;
|
|
205
|
+
} else {
|
|
206
|
+
dropdownTopOffset = rect.bottom - containerRect.top + 8;
|
|
207
|
+
dropdownMaxHeight = containerRect.bottom - rect.bottom - 16;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
dropdownMinWidth = DROPDOWN_WIDTH;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function toggleShapesDropdown(e?: MouseEvent) {
|
|
214
|
+
e?.stopPropagation();
|
|
215
|
+
e?.preventDefault();
|
|
216
|
+
if (!shapesDropdownOpen) {
|
|
217
|
+
computeDropdownPosition();
|
|
218
|
+
}
|
|
219
|
+
shapesDropdownOpen = !shapesDropdownOpen;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Close dropdown when clicking outside
|
|
223
|
+
function handleOutsideClick(e: MouseEvent) {
|
|
224
|
+
const target = e.target as HTMLElement;
|
|
225
|
+
if (!target.closest('.shapes-split-ct') && !target.closest('.shapes-dropdown-menu-fixed')) {
|
|
226
|
+
shapesDropdownOpen = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Reposition dropdown on window resize so it stays aligned with the trigger
|
|
231
|
+
function handleWindowResize() {
|
|
232
|
+
if (shapesDropdownOpen) computeDropdownPosition();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Keyboard shortcuts panel state ───
|
|
236
|
+
// Exported so Canvas.svelte can lift state and share it with PanZoomPanel's toggle button
|
|
237
|
+
export let shortcutsPanelOpen = false;
|
|
238
|
+
export let onShortcutsToggle: () => void = () => { shortcutsPanelOpen = !shortcutsPanelOpen; };
|
|
239
|
+
|
|
240
|
+
// Shortcut key data structure:
|
|
241
|
+
// string value → rendered as a key badge <kbd>
|
|
242
|
+
// null → rendered as a '+' separator (plain text between badges)
|
|
243
|
+
// Using null instead of '+' avoids ambiguity when '+' itself is a shortcut key.
|
|
244
|
+
const shortcutGroups: { title: string; shortcuts: { keys: (string | null)[]; label: string }[] }[] = [
|
|
245
|
+
{
|
|
246
|
+
title: 'Tools',
|
|
247
|
+
shortcuts: [
|
|
248
|
+
{ keys: ['V'], label: 'Select' },
|
|
249
|
+
{ keys: ['H'], label: 'Hand / Pan' },
|
|
250
|
+
{ keys: ['E'], label: 'Erase' },
|
|
251
|
+
{ keys: ['D'], label: 'Draw' },
|
|
252
|
+
{ keys: ['A'], label: 'Arrow' },
|
|
253
|
+
{ keys: ['R'], label: 'Rectangle' },
|
|
254
|
+
{ keys: ['G'], label: 'Triangle' },
|
|
255
|
+
{ keys: ['O'], label: 'Ellipse' },
|
|
256
|
+
{ keys: ['F'], label: 'Frame' },
|
|
257
|
+
{ keys: ['T'], label: 'Text' },
|
|
258
|
+
{ keys: ['I'], label: 'Image' },
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
title: 'Selection',
|
|
263
|
+
shortcuts: [
|
|
264
|
+
{ keys: ['Ctrl/Cmd', null, 'A'], label: 'Select All' },
|
|
265
|
+
{ keys: ['Alt', null, 'Click'], label: 'Drill-through Select' },
|
|
266
|
+
{ keys: ['Ctrl/Cmd', null, 'D'], label: 'Duplicate' },
|
|
267
|
+
{ keys: ['Ctrl/Cmd', null, 'C'], label: 'Copy' },
|
|
268
|
+
{ keys: ['Ctrl/Cmd', null, 'V'], label: 'Paste' },
|
|
269
|
+
{ keys: ['Del'], label: 'Delete' },
|
|
270
|
+
{ keys: ['Ctrl/Cmd', null, 'G'], label: 'Group' },
|
|
271
|
+
{ keys: ['Ctrl/Cmd', null, 'Shift', null, 'G'], label: 'Ungroup' },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
title: 'History',
|
|
276
|
+
shortcuts: [
|
|
277
|
+
{ keys: ['Ctrl/Cmd', null, 'Z'], label: 'Undo' },
|
|
278
|
+
{ keys: ['Ctrl/Cmd', null, 'Shift', null, 'Z'], label: 'Redo' },
|
|
279
|
+
{ keys: ['Ctrl/Cmd', null, 'Y'], label: 'Redo (alt)' },
|
|
280
|
+
{ keys: ['Ctrl/Cmd', null, 'S'], label: 'Save' },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
title: 'Zoom & Canvas',
|
|
285
|
+
shortcuts: [
|
|
286
|
+
// null = '+' separator; the final '+' here is the actual key badge
|
|
287
|
+
{ keys: ['Ctrl/Cmd', null, '+'], label: 'Zoom In' },
|
|
288
|
+
{ keys: ['Ctrl/Cmd', null, '−'], label: 'Zoom Out' },
|
|
289
|
+
{ keys: ['Ctrl/Cmd', null, '0'], label: 'Reset Zoom (100%)' },
|
|
290
|
+
{ keys: ['Ctrl/Cmd', null, '9'], label: 'Fit to screen' },
|
|
291
|
+
{ keys: ['Space', null, 'Drag'], label: 'Pan canvas' },
|
|
292
|
+
// Four arrow keys shown as individual square badges (no separator between them)
|
|
293
|
+
{ keys: ['↑', '↓', '←', '→'], label: 'Move selected (1px)' },
|
|
294
|
+
{ keys: ['Shift', null, '↑', '↓', '←', '→'], label: 'Move selected (10px)' },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
title: 'Layer Order',
|
|
299
|
+
shortcuts: [
|
|
300
|
+
{ keys: [']'], label: 'Bring Forward' },
|
|
301
|
+
{ keys: ['['], label: 'Send Backward' },
|
|
302
|
+
{ keys: ['Ctrl/Cmd', null, ']'], label: 'Bring to Front' },
|
|
303
|
+
{ keys: ['Ctrl/Cmd', null, '['], label: 'Send to Back' },
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
title: 'Frame',
|
|
308
|
+
shortcuts: [
|
|
309
|
+
{ keys: ['Alt', null, 'Drag corner'], label: 'Resize, keep children size' },
|
|
310
|
+
{ keys: ['Double-click'], label: 'Enter / exit frame edit' },
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle keyboard shortcuts
|
|
317
|
+
*/
|
|
318
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
319
|
+
// Toggle shortcuts panel with '?'
|
|
320
|
+
if (event.key === '?' && !event.ctrlKey && !event.metaKey) {
|
|
321
|
+
const activeElement = document.activeElement as HTMLElement | null;
|
|
322
|
+
if (activeElement) {
|
|
323
|
+
const tagName = activeElement.tagName;
|
|
324
|
+
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || activeElement.isContentEditable) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
onShortcutsToggle();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Close panel / dropdown with Escape
|
|
334
|
+
if (event.key === 'Escape') {
|
|
335
|
+
shortcutsPanelOpen = false;
|
|
336
|
+
shapesDropdownOpen = false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Ctrl / Cmd + zoom shortcuts
|
|
340
|
+
if (event.ctrlKey || event.metaKey) {
|
|
341
|
+
const key = event.key;
|
|
342
|
+
if (key === '+' || key === '=' || key === 'Add') {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
onZoomIn();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (key === '-' || key === 'Subtract') {
|
|
348
|
+
event.preventDefault();
|
|
349
|
+
onZoomOut();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (key === '0') {
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
onZoomReset();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (key === '9') {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
onZoomFit();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Other Ctrl/Cmd shortcuts are handled in Canvas.svelte — skip here
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Skip if focus is on text input
|
|
367
|
+
const activeElement = document.activeElement as HTMLElement | null;
|
|
368
|
+
if (activeElement) {
|
|
369
|
+
const tagName = activeElement.tagName;
|
|
370
|
+
const isTextInput = tagName === 'INPUT' || tagName === 'TEXTAREA';
|
|
371
|
+
if (isTextInput || activeElement.isContentEditable) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Skip other alt combinations (handled in Canvas.svelte)
|
|
377
|
+
if (event.altKey) return;
|
|
378
|
+
|
|
379
|
+
const key = event.key.toUpperCase();
|
|
380
|
+
|
|
381
|
+
// Map keys to handlers — using current prop references
|
|
382
|
+
switch (key) {
|
|
383
|
+
case 'V': event.preventDefault(); onSelectSelectTool(); break;
|
|
384
|
+
case 'E': event.preventDefault(); onSelectEraseTool(); break;
|
|
385
|
+
case 'D': event.preventDefault(); onSelectDrawTool(); break;
|
|
386
|
+
case 'H': event.preventDefault(); onSelectPanTool(); break;
|
|
387
|
+
case 'A': event.preventDefault(); selectShape('arrow'); break;
|
|
388
|
+
case 'R': event.preventDefault(); selectShape('rect'); break;
|
|
389
|
+
case 'G': event.preventDefault(); selectShape('triangle'); break;
|
|
390
|
+
case 'O': event.preventDefault(); selectShape('circle'); break;
|
|
391
|
+
case 'F': event.preventDefault(); onSelectFrameTool(); break;
|
|
392
|
+
case 'T': event.preventDefault(); onSelectTextTool(); break;
|
|
393
|
+
case 'I': event.preventDefault(); onSelectImageTool(); break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Capture-phase handler to prevent browser zoom (Cmd/Ctrl + = / - / 0 / 9).
|
|
398
|
+
// Must use capture:true to intercept before the browser handles it natively.
|
|
399
|
+
// We check both event.key (character-based) AND event.code (physical key-based)
|
|
400
|
+
// so this works across keyboard layouts and Numpad keys.
|
|
401
|
+
function preventBrowserZoom(event: KeyboardEvent) {
|
|
402
|
+
if (event.ctrlKey || event.metaKey) {
|
|
403
|
+
const key = event.key;
|
|
404
|
+
const code = event.code;
|
|
405
|
+
const isZoomKey =
|
|
406
|
+
// Character-based (varies by layout)
|
|
407
|
+
key === '=' || key === '+' || key === '-' ||
|
|
408
|
+
key === '0' || key === '9' ||
|
|
409
|
+
key === 'Add' || key === 'Subtract' ||
|
|
410
|
+
// Physical key codes (layout-independent) — covers Numpad keys too
|
|
411
|
+
code === 'Equal' || code === 'Minus' ||
|
|
412
|
+
code === 'Digit0' || code === 'Digit9' ||
|
|
413
|
+
code === 'NumpadAdd' || code === 'NumpadSubtract' || code === 'NumpadEqual';
|
|
414
|
+
if (isZoomKey) {
|
|
415
|
+
event.preventDefault();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Prevent browser pinch-to-zoom and Ctrl+wheel zoom (trackpad gesture).
|
|
421
|
+
// passive:false is required so we can call preventDefault().
|
|
422
|
+
function preventWheelZoom(event: WheelEvent) {
|
|
423
|
+
if (event.ctrlKey || event.metaKey) {
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Use onMount's return function for cleanup — this only runs in browser
|
|
429
|
+
onMount(() => {
|
|
430
|
+
window.addEventListener('keydown', handleKeydown);
|
|
431
|
+
window.addEventListener('click', handleOutsideClick);
|
|
432
|
+
window.addEventListener('resize', handleWindowResize);
|
|
433
|
+
// Capture phase: intercept browser zoom keys before the browser acts on them
|
|
434
|
+
document.addEventListener('keydown', preventBrowserZoom, { capture: true });
|
|
435
|
+
// Non-passive wheel listener to block Ctrl/Cmd + scroll (trackpad pinch zoom)
|
|
436
|
+
document.addEventListener('wheel', preventWheelZoom as EventListener, { passive: false });
|
|
437
|
+
|
|
438
|
+
return () => {
|
|
439
|
+
window.removeEventListener('keydown', handleKeydown);
|
|
440
|
+
window.removeEventListener('click', handleOutsideClick);
|
|
441
|
+
window.removeEventListener('resize', handleWindowResize);
|
|
442
|
+
document.removeEventListener('keydown', preventBrowserZoom, { capture: true } as EventListenerOptions);
|
|
443
|
+
document.removeEventListener('wheel', preventWheelZoom as EventListener);
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
/** Svelte action: applies negative right margin only when scrollable,
|
|
448
|
+
* so the scrollbar overlays the parent padding zone. */
|
|
449
|
+
function overlayScrollbar(node: HTMLElement) {
|
|
450
|
+
function check() {
|
|
451
|
+
shapesDropdownNeedsScroll = node.scrollHeight > node.clientHeight;
|
|
452
|
+
}
|
|
453
|
+
const ro = new ResizeObserver(check);
|
|
454
|
+
ro.observe(node);
|
|
455
|
+
check();
|
|
456
|
+
return { destroy() { ro.disconnect(); } };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
</script>
|
|
460
|
+
|
|
461
|
+
<div class="{className ? className + ' ' : ''}canvas-toolbar-el flex flex-col justify-between gap-2" style={style}>
|
|
462
|
+
<!-- Tools Bar -->
|
|
463
|
+
<div class="tools-bar-el flex flex-row gap-2 w-full">
|
|
464
|
+
<!-- Scrollable container for tools — overflow-x-auto clips absolute children,
|
|
465
|
+
so dropdowns and tooltips are rendered as siblings OUTSIDE this div -->
|
|
466
|
+
<div class="tools-ct flex flex-row">
|
|
467
|
+
<!-- ── Utility cluster: Undo, Redo, Hand, Lock | Divider ── -->
|
|
468
|
+
<Tooltip text="Undo (Ctrl+Z)">
|
|
469
|
+
<div class="disabled-tooltip-wrap">
|
|
470
|
+
<button
|
|
471
|
+
class="undo-bt {buttonClass} {inactiveClass}"
|
|
472
|
+
onclick={onUndo}
|
|
473
|
+
disabled={!canUndo}
|
|
474
|
+
aria-label="Undo"
|
|
475
|
+
>
|
|
476
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
477
|
+
<path d="M3 7v6h6"/>
|
|
478
|
+
<path d="M3 13C3 13 5.5 7 12 7c4.42 0 8 3.13 8 7s-3.58 7-8 7a8.23 8.23 0 0 1-5.66-2.34"/>
|
|
479
|
+
</svg>
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
</Tooltip>
|
|
483
|
+
<Tooltip text="Redo (Ctrl+Shift+Z)">
|
|
484
|
+
<div class="disabled-tooltip-wrap">
|
|
485
|
+
<button
|
|
486
|
+
class="redo-bt {buttonClass} {inactiveClass}"
|
|
487
|
+
onclick={onRedo}
|
|
488
|
+
disabled={!canRedo}
|
|
489
|
+
aria-label="Redo"
|
|
490
|
+
>
|
|
491
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
492
|
+
<path d="M21 7v6h-6"/>
|
|
493
|
+
<path d="M21 13C21 13 18.5 7 12 7c-4.42 0-8 3.13-8 7s3.58 7 8 7a8.23 8.23 0 0 0 5.66-2.34"/>
|
|
494
|
+
</svg>
|
|
495
|
+
</button>
|
|
496
|
+
</div>
|
|
497
|
+
</Tooltip>
|
|
498
|
+
<!-- Hand / Pan — hidden (available in zoom panel) -->
|
|
499
|
+
<!-- <Tooltip text="Hand / Pan (H)">
|
|
500
|
+
<button class="hand-bt {buttonClass} {activeTool === 'pan' ? activeClass : inactiveClass}"
|
|
501
|
+
onclick={onSelectPanTool}
|
|
502
|
+
aria-label="Hand / Pan">
|
|
503
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
504
|
+
<path d="M18 11V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v0"/>
|
|
505
|
+
<path d="M14 10V4a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v2"/>
|
|
506
|
+
<path d="M10 10.5V6a2 2 0 0 0-2-2v0a2 2 0 0 0-2 2v8"/>
|
|
507
|
+
<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"/>
|
|
508
|
+
</svg>
|
|
509
|
+
</button>
|
|
510
|
+
</Tooltip> -->
|
|
511
|
+
<Tooltip text={stickyToolEnabled ? 'Tool Lock: ON — drawing tool stays active' : 'Tool Lock: OFF — reverts to Select after placing'}>
|
|
512
|
+
<button class="sticky-tool-bt {buttonClass} {stickyToolEnabled ? activeClass : inactiveClass}"
|
|
513
|
+
onclick={onToggleStickyTool}
|
|
514
|
+
aria-label="Toggle sticky tool mode"
|
|
515
|
+
aria-pressed={stickyToolEnabled}>
|
|
516
|
+
{#if stickyToolEnabled}
|
|
517
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
518
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
519
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
520
|
+
</svg>
|
|
521
|
+
{:else}
|
|
522
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
523
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
|
524
|
+
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
|
|
525
|
+
</svg>
|
|
526
|
+
{/if}
|
|
527
|
+
</button>
|
|
528
|
+
</Tooltip>
|
|
529
|
+
|
|
530
|
+
<div class="toolbar-divider"></div>
|
|
531
|
+
|
|
532
|
+
<!-- ══ Illustrated tools: Pencil & Eraser — "pulled from sheath" on select ══ -->
|
|
533
|
+
<!-- Each tool is wrapped in a mask container that clips its body at rest.
|
|
534
|
+
When active, overflow becomes visible so the tool "pops out" above the toolbar. -->
|
|
535
|
+
<div class="wdg-illustrated-mask {activeTool === 'draw' ? 'wdg-illustrated-mask-active' : ''}">
|
|
536
|
+
<Tooltip text="Draw (D)" offsetY={8}>
|
|
537
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
538
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
539
|
+
<div class="wdg-illustrated-bt {activeTool === 'draw' ? 'wdg-illustrated-active' : ''}"
|
|
540
|
+
onclick={onSelectDrawTool}
|
|
541
|
+
role="button"
|
|
542
|
+
tabindex="-1"
|
|
543
|
+
aria-label="Draw">
|
|
544
|
+
<svg class="wdg-pencil-svg" viewBox="0 0 32 159" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
545
|
+
<path d="M20.8823 15.927L18.439 3.29447C17.3288 0.137968 14.6702 0.137967 13.5604 3.29374L11.114 15.927H20.8823Z" fill="#4A4A4C"/>
|
|
546
|
+
<path d="M32 88.8211L32 68.6342C32 66.2211 31.7486 63.9551 31.3079 61.9972L20.8785 15.927H11.1176L0.841019 61.3789L0.692138 61.9972C0.251452 63.9551 6.10352e-05 66.2211 6.10352e-05 68.6342L0.000481631 88.8221L32 88.8211Z" fill="url(#pencilBody)"/>
|
|
547
|
+
<path d="M8.00006 158.93L7.99997 69.1565C7.99997 66.1271 6.20959 61.6724 4.00002 61.6724C2.58481 61.6724 1.34125 62.6801 0.630773 64.2001L0.519114 64.4521C0.188602 65.2499 6.10352e-05 68.1732 6.10352e-05 69.1565L0.000150606 158.93H8.00006Z" fill="#A4A3A8"/>
|
|
548
|
+
<path d="M15.4888 61.6724C11.347 61.6724 7.98895 66.1279 7.98895 69.1565V158.93H22.9888L22.9888 69.1565C22.9888 66.1279 19.6306 61.6724 15.4888 61.6724Z" fill="#707477"/>
|
|
549
|
+
<path d="M32 158.93L32 68.1565C32 67.1732 31.7879 64.2499 31.4161 63.4521L31.2908 63.2006C30.4915 61.6806 29.0921 61.6724 27.5 61.6724C25.0142 61.6724 23 66.1271 23 69.1565L23 158.93H32Z" fill="#5E6064"/>
|
|
550
|
+
<defs>
|
|
551
|
+
<linearGradient id="pencilBody" x1="2.45" y1="53.89" x2="38.29" y2="57.66" gradientUnits="userSpaceOnUse">
|
|
552
|
+
<stop offset="0.145" stop-color="#EFDBCC"/><stop offset="0.185" stop-color="#FBDDCB"/>
|
|
553
|
+
<stop offset="0.25" stop-color="#E2B8A4"/><stop offset="0.44" stop-color="#C49F8F"/>
|
|
554
|
+
<stop offset="0.77" stop-color="#AF9280"/>
|
|
555
|
+
</linearGradient>
|
|
556
|
+
</defs>
|
|
557
|
+
</svg>
|
|
558
|
+
</div>
|
|
559
|
+
</Tooltip>
|
|
560
|
+
</div>
|
|
561
|
+
<div class="wdg-illustrated-mask {activeTool === 'erase' ? 'wdg-illustrated-mask-active' : ''}">
|
|
562
|
+
<Tooltip text="Erase (E)" offsetY={8}>
|
|
563
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
564
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
565
|
+
<div class="wdg-illustrated-bt {activeTool === 'erase' ? 'wdg-illustrated-active' : ''}"
|
|
566
|
+
onclick={onSelectEraseTool}
|
|
567
|
+
role="button"
|
|
568
|
+
tabindex="-1"
|
|
569
|
+
aria-label="Erase">
|
|
570
|
+
<svg class="wdg-eraser-svg" viewBox="0 0 32 127" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
571
|
+
<path d="M31.9959 29.9282L1.3e-06 29.9282L0 36.9282L31.9959 36.9282L31.9959 29.9282Z" fill="#ABB1B4"/>
|
|
572
|
+
<path d="M4.4e-05 36.9241L0 36.5283H10.6654V36.9241L4.4e-05 36.9241Z" fill="white"/>
|
|
573
|
+
<path d="M10.6655 36.9241V36.5283L21.3307 36.5283V36.9241L10.6655 36.9241Z" fill="white"/>
|
|
574
|
+
<path d="M21.3305 36.9242L21.3305 36.5283L31.9958 36.5284L31.9958 36.9242H21.3305Z" fill="white"/>
|
|
575
|
+
<path d="M1.58716 36.5284L1.58716 29.9282H10.1257L10.1257 36.5284H1.58716Z" fill="#858E92"/>
|
|
576
|
+
<path d="M0.003 36.93L8.003 36.93V126.93H0.003V36.93Z" fill="#A4A3A8"/>
|
|
577
|
+
<path d="M8.003 36.93L23.003 36.93V126.93H8.003V36.93Z" fill="#707477"/>
|
|
578
|
+
<path d="M23.003 36.93L32 36.93V126.93H23.003V36.93Z" fill="#5E6064"/>
|
|
579
|
+
<path d="M23.362 36.5284L23.362 29.9282L30.6204 29.9283L30.6204 36.5284H23.362Z" fill="#858E92"/>
|
|
580
|
+
<path d="M20.0186 29.9282H14.8553L14.8553 36.5284L20.0186 36.5284L20.0186 29.9282Z" fill="#E4EBED"/>
|
|
581
|
+
<path d="M-2.1e-05 29.9282L31.9959 29.9283L31.9959 10.7812C31.9959 5.34073 27.7514 0.930376 22.5156 0.93042H9.48026C4.24447 0.93042 1.9e-05 5.34073 1.9e-05 10.7811L-2.1e-05 29.9282Z" fill="url(#eraserHead)"/>
|
|
582
|
+
<path opacity="0.4" d="M31.9959 29.9278L31.9959 10.7812C31.9959 10.7649 31.9959 10.7485 31.9958 10.7322C31.9948 10.5134 31.9869 10.2957 31.9723 10.0804L31.9701 10.0488L31.969 10.0331C31.601 4.94215 27.5092 0.93042 22.5156 0.93042L14.6346 0.93042C19.0593 0.978238 22.6318 4.71971 22.6318 9.32859L22.6318 29.9278L31.9959 29.9278Z" fill="#A04B5C"/>
|
|
583
|
+
<path d="M31.9959 30.324L4.3e-05 30.324L0 29.9283L31.9959 29.9282V30.324Z" fill="white"/>
|
|
584
|
+
<defs>
|
|
585
|
+
<linearGradient id="eraserHead" x1="-2.1e-05" y1="29.9283" x2="31.9959" y2="29.9283" gradientUnits="userSpaceOnUse">
|
|
586
|
+
<stop offset="0.215" stop-color="#ED95A1"/><stop offset="0.33" stop-color="#E77E8D"/>
|
|
587
|
+
<stop offset="0.667" stop-color="#BA5C6E"/><stop offset="1" stop-color="#863F4E"/>
|
|
588
|
+
</linearGradient>
|
|
589
|
+
</defs>
|
|
590
|
+
</svg>
|
|
591
|
+
</div>
|
|
592
|
+
</Tooltip>
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
<!-- ══ Standard icon-only tool buttons ══ -->
|
|
596
|
+
<Tooltip text="Select (V)">
|
|
597
|
+
<button class="select-bt {buttonClass} {activeTool === 'select' ? activeClass : inactiveClass}"
|
|
598
|
+
onclick={onSelectSelectTool}
|
|
599
|
+
aria-label="Select">
|
|
600
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
601
|
+
<path d="M5 3L5 18.5L9.5 14L14.5 21L17 19.5L12 12.5L18 12.5L5 3Z" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linejoin="round"/>
|
|
602
|
+
</svg>
|
|
603
|
+
</button>
|
|
604
|
+
</Tooltip>
|
|
605
|
+
<Tooltip text="Arrow (A)">
|
|
606
|
+
<button class="arrow-bt {buttonClass} {activeTool === 'arrow' ? activeClass : inactiveClass}"
|
|
607
|
+
onclick={() => onSelectShapeTool('arrow')}
|
|
608
|
+
aria-label="Arrow">
|
|
609
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
610
|
+
<path d="M6 18C6 18 6.06617 12.5162 8.00001 10.5521C10.862 7.64522 19 9 19 9M19 9L15 5M19 9L15 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
611
|
+
</svg>
|
|
612
|
+
</button>
|
|
613
|
+
</Tooltip>
|
|
614
|
+
<Tooltip text="Text (T)">
|
|
615
|
+
<button class="text-bt {buttonClass} {activeTool === 'text' ? activeClass : inactiveClass}"
|
|
616
|
+
onclick={onSelectTextTool}
|
|
617
|
+
aria-label="Text">
|
|
618
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
619
|
+
<path d="M6 7V5H18V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
620
|
+
<path d="M12 5V19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
621
|
+
<path d="M14 19H10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
622
|
+
</svg>
|
|
623
|
+
</button>
|
|
624
|
+
</Tooltip>
|
|
625
|
+
|
|
626
|
+
<div class="toolbar-divider"></div>
|
|
627
|
+
|
|
628
|
+
<Tooltip text="Frame (F)">
|
|
629
|
+
<button class="frame-bt {buttonClass} {activeTool === 'frame' ? activeClass : inactiveClass}"
|
|
630
|
+
onclick={onSelectFrameTool}
|
|
631
|
+
aria-label="Frame">
|
|
632
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
633
|
+
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
634
|
+
<path d="M3 9h18"/>
|
|
635
|
+
<path d="M9 3v6"/>
|
|
636
|
+
</svg>
|
|
637
|
+
</button>
|
|
638
|
+
</Tooltip>
|
|
639
|
+
|
|
640
|
+
<Tooltip text="Image (I)">
|
|
641
|
+
<button class="image-bt {buttonClass} {activeTool === 'image' ? activeClass : inactiveClass}"
|
|
642
|
+
onclick={() => { onSelectImageTool(); }}
|
|
643
|
+
aria-label="Image">
|
|
644
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
645
|
+
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
646
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
647
|
+
<path d="M21 15l-5-5L5 21"/>
|
|
648
|
+
</svg>
|
|
649
|
+
</button>
|
|
650
|
+
</Tooltip>
|
|
651
|
+
|
|
652
|
+
<!-- Shapes: tldraw-style split — left activates tool, right opens dropdown -->
|
|
653
|
+
<div class="shapes-split-ct" bind:this={dropdownTriggerEl}>
|
|
654
|
+
<Tooltip text={dropdownButtonTooltip}>
|
|
655
|
+
<button
|
|
656
|
+
class="shapes-main-bt {buttonClass} {activeDropdownIsActive ? activeClass : inactiveClass}"
|
|
657
|
+
onclick={activateDropdownTool}
|
|
658
|
+
aria-label={activeDropdownTool === 'library-shape' && lastSelectedLibraryShape ? lastSelectedLibraryShape.label : shapeInfo[activeDropdownTool as ShapeToolName]?.label ?? 'Shapes'}
|
|
659
|
+
>
|
|
660
|
+
{#if activeDropdownTool === 'library-shape' && lastSelectedLibraryShape}
|
|
661
|
+
{@html lastSelectedLibraryShape.iconSvg}
|
|
662
|
+
{:else if shapeInfo[activeDropdownTool as ShapeToolName]}
|
|
663
|
+
{@html shapeInfo[activeDropdownTool as ShapeToolName].iconSvg}
|
|
664
|
+
{:else}
|
|
665
|
+
{@html shapeInfo['rect'].iconSvg}
|
|
666
|
+
{/if}
|
|
667
|
+
</button>
|
|
668
|
+
</Tooltip>
|
|
669
|
+
<Tooltip text="More Shapes" disabled={shapesDropdownOpen}>
|
|
670
|
+
<button
|
|
671
|
+
class="shapes-caret-bt {buttonClass} {activeDropdownIsActive ? activeClass : inactiveClass}"
|
|
672
|
+
onclick={(e) => toggleShapesDropdown(e)}
|
|
673
|
+
aria-label="More shapes"
|
|
674
|
+
>
|
|
675
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
|
676
|
+
<path d="M2 4L5 7L8 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
677
|
+
</svg>
|
|
678
|
+
</button>
|
|
679
|
+
</Tooltip>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<!-- Actions Bar (hidden by default, kept for future use) -->
|
|
685
|
+
<div class="actions-bar-el relative hidden flex-row gap-2 w-full">
|
|
686
|
+
<div class="actions-ct flex flex-row gap-2 overflow-x-auto rounded-xl p-2 bg-white/80 shadow-md border border-gray-100">
|
|
687
|
+
<!-- Grouping Actions -->
|
|
688
|
+
<button class="add-group-selection-bt {buttonClass}" onclick={onGroupSelection}><span class="icon">📦</span> Group</button>
|
|
689
|
+
<button class="add-ungroup-selection-bt {buttonClass}" onclick={onUngroupSelection}><span class="icon">📤</span> Ungroup</button>
|
|
690
|
+
<button class="add-duplicate-selection-bt {buttonClass}" onclick={onDuplicateSelection}><span class="icon">📋</span> Duplicate</button>
|
|
691
|
+
<button class="add-select-all-bt {buttonClass}" onclick={onSelectAllObjects}><span class="icon">🎯</span> Select All</button>
|
|
692
|
+
<button class="add-clear-all-bt {buttonClass}" onclick={onClearAllObjects}><span class="icon">🗑️</span> Clear All</button>
|
|
693
|
+
|
|
694
|
+
<!-- Add Objects Actions -->
|
|
695
|
+
<button class="add-text-bt {buttonClass}" onclick={addText}><span class="icon">📝</span> Text</button>
|
|
696
|
+
<button class="add-rext-bt {buttonClass}" onclick={addRect}><span class="icon">⬜</span> Rectangle</button>
|
|
697
|
+
<button class="add-triangle-bt {buttonClass}" onclick={addTriangle}><span class="icon">🔺</span> Triangle</button>
|
|
698
|
+
<button class="add-circle-bt {buttonClass}" onclick={addCircle}><span class="icon">🔴</span> Circle</button>
|
|
699
|
+
<button class="add-frame-bt {buttonClass}" onclick={addFrame}><span class="icon frame-icon">▢</span> Frame</button>
|
|
700
|
+
<button class="add-image-bt {buttonClass}" onclick={addImage}><span class="icon">🖼️</span> Image</button>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
|
|
705
|
+
<!-- ═══════════════════════════════════════════════════════════════
|
|
706
|
+
Shapes Dropdown Menu — rendered as sibling (outside overflow),
|
|
707
|
+
uses position:fixed with coordinates from getBoundingClientRect.
|
|
708
|
+
dropdownOpenAbove=true: appears above the trigger (toolbar at bottom).
|
|
709
|
+
dropdownOpenAbove=false: appears below (toolbar at top).
|
|
710
|
+
═══════════════════════════════════════════════════════════════ -->
|
|
711
|
+
{#if shapesDropdownOpen}
|
|
712
|
+
<!-- position:absolute within .canvas-container (position:relative) so the dropdown
|
|
713
|
+
is correctly positioned regardless of any CSS transform on ancestors. -->
|
|
714
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
715
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
716
|
+
<div
|
|
717
|
+
class="shapes-dropdown-menu-fixed"
|
|
718
|
+
style="position:absolute; left:{dropdownLeft}px; {dropdownOpenAbove ? `bottom:${dropdownBottomOffset}px` : `top:${dropdownTopOffset}px`}; width:{dropdownMinWidth}px; max-height:{dropdownMaxHeight}px; z-index:9999;"
|
|
719
|
+
>
|
|
720
|
+
<div
|
|
721
|
+
class="shapes-dropdown-scroll"
|
|
722
|
+
use:overlayScrollbar
|
|
723
|
+
style={shapesDropdownNeedsScroll ? 'margin-right: -6px' : ''}
|
|
724
|
+
>
|
|
725
|
+
<!-- Geometric Shapes — 3×2 grid -->
|
|
726
|
+
<div class="shapes-category-label">Geometric Shapes</div>
|
|
727
|
+
<div class="shapes-library-grid">
|
|
728
|
+
<Tooltip text="Triangle (G)" position="above">
|
|
729
|
+
<button class="shape-library-bt" onclick={() => selectShape('triangle')} aria-label="Triangle">
|
|
730
|
+
{@html shapeInfo.triangle.iconSvg}
|
|
731
|
+
</button>
|
|
732
|
+
</Tooltip>
|
|
733
|
+
<Tooltip text="Rectangle (R)" position="above">
|
|
734
|
+
<button class="shape-library-bt" onclick={() => selectShape('rect')} aria-label="Rectangle">
|
|
735
|
+
{@html shapeInfo.rect.iconSvg}
|
|
736
|
+
</button>
|
|
737
|
+
</Tooltip>
|
|
738
|
+
{#each (libraryShapesByCategory.get('Geometric Shapes') ?? []).filter(s => s.key === 'hexagon') as shape}
|
|
739
|
+
<Tooltip text={shape.label} position="above">
|
|
740
|
+
<button class="shape-library-bt" onclick={() => selectLibraryShape(shape)} aria-label={shape.label}>
|
|
741
|
+
{@html shape.iconSvg}
|
|
742
|
+
</button>
|
|
743
|
+
</Tooltip>
|
|
744
|
+
{/each}
|
|
745
|
+
<Tooltip text="Circle" position="above">
|
|
746
|
+
<button class="shape-library-bt" onclick={() => selectShape('circle-true')} aria-label="Circle">
|
|
747
|
+
{@html shapeInfo['circle-true'].iconSvg}
|
|
748
|
+
</button>
|
|
749
|
+
</Tooltip>
|
|
750
|
+
<Tooltip text="Ellipse (O)" position="above">
|
|
751
|
+
<button class="shape-library-bt" onclick={() => selectShape('circle')} aria-label="Ellipse">
|
|
752
|
+
{@html shapeInfo.circle.iconSvg}
|
|
753
|
+
</button>
|
|
754
|
+
</Tooltip>
|
|
755
|
+
{#each (libraryShapesByCategory.get('Geometric Shapes') ?? []).filter(s => s.key === 'oval') as shape}
|
|
756
|
+
<Tooltip text={shape.label} position="above">
|
|
757
|
+
<button class="shape-library-bt" onclick={() => selectLibraryShape(shape)} aria-label={shape.label}>
|
|
758
|
+
{@html shape.iconSvg}
|
|
759
|
+
</button>
|
|
760
|
+
</Tooltip>
|
|
761
|
+
{/each}
|
|
762
|
+
</div>
|
|
763
|
+
|
|
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
|
+
<!-- Library shape categories (skip Geometric, already rendered above) -->
|
|
780
|
+
{#each [...libraryShapesByCategory.entries()].filter(([cat]) => cat !== 'Geometric Shapes') as [category, shapes]}
|
|
781
|
+
<div class="shapes-dropdown-divider"></div>
|
|
782
|
+
<div class="shapes-category-label">{category}</div>
|
|
783
|
+
<div class="shapes-library-grid">
|
|
784
|
+
{#each shapes as shape}
|
|
785
|
+
<Tooltip text={shape.label} position="above">
|
|
786
|
+
<button
|
|
787
|
+
class="shape-library-bt"
|
|
788
|
+
onclick={() => selectLibraryShape(shape)}
|
|
789
|
+
aria-label={shape.label}
|
|
790
|
+
>
|
|
791
|
+
{@html shape.iconSvg}
|
|
792
|
+
</button>
|
|
793
|
+
</Tooltip>
|
|
794
|
+
{/each}
|
|
795
|
+
</div>
|
|
796
|
+
{/each}
|
|
797
|
+
</div><!-- end shapes-dropdown-scroll -->
|
|
798
|
+
</div><!-- end shapes-dropdown-menu-fixed -->
|
|
799
|
+
{/if}
|
|
800
|
+
|
|
801
|
+
<!-- ═══════════════════════════════════════════════════════════════
|
|
802
|
+
Keyboard Shortcuts Panel — rendered as sibling in canvas-container,
|
|
803
|
+
uses position:absolute (relative to canvas-container) so it is
|
|
804
|
+
always centered within the canvas area, not the viewport.
|
|
805
|
+
═══════════════════════════════════════════════════════════════ -->
|
|
806
|
+
{#if shortcutsPanelOpen}
|
|
807
|
+
<!-- Backdrop — covers the canvas area -->
|
|
808
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
809
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
810
|
+
<div class="shortcuts-backdrop" onclick={() => shortcutsPanelOpen = false}></div>
|
|
811
|
+
|
|
812
|
+
<!-- Shortcuts Panel Overlay -->
|
|
813
|
+
<div class="shortcuts-panel">
|
|
814
|
+
<div class="shortcuts-panel-header">
|
|
815
|
+
<div class="shortcuts-panel-title-ct">
|
|
816
|
+
<span class="shortcuts-panel-icon">⌨</span>
|
|
817
|
+
<h2 class="shortcuts-panel-title">Keyboard Shortcuts</h2>
|
|
818
|
+
</div>
|
|
819
|
+
<button
|
|
820
|
+
class="close-shortcuts-bt"
|
|
821
|
+
onclick={() => shortcutsPanelOpen = false}
|
|
822
|
+
>✕</button>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<div class="shortcuts-panel-body">
|
|
826
|
+
<div class="shortcuts-column">
|
|
827
|
+
{#each shortcutGroups.filter((_, i) => i % 2 === 0) as group}
|
|
828
|
+
<div class="shortcut-group">
|
|
829
|
+
<h3 class="shortcut-group-title">{group.title}</h3>
|
|
830
|
+
<ul class="shortcut-list">
|
|
831
|
+
{#each group.shortcuts as shortcut}
|
|
832
|
+
<li class="shortcut-row">
|
|
833
|
+
<span class="shortcut-label">{shortcut.label}</span>
|
|
834
|
+
<div class="shortcut-keys">
|
|
835
|
+
{#each shortcut.keys as k}
|
|
836
|
+
{#if k === null}
|
|
837
|
+
<span class="shortcut-sep">+</span>
|
|
838
|
+
{:else}
|
|
839
|
+
<kbd class="shortcut-key">{k}</kbd>
|
|
840
|
+
{/if}
|
|
841
|
+
{/each}
|
|
842
|
+
</div>
|
|
843
|
+
</li>
|
|
844
|
+
{/each}
|
|
845
|
+
</ul>
|
|
846
|
+
</div>
|
|
847
|
+
{/each}
|
|
848
|
+
</div>
|
|
849
|
+
<div class="shortcuts-column">
|
|
850
|
+
{#each shortcutGroups.filter((_, i) => i % 2 === 1) as group}
|
|
851
|
+
<div class="shortcut-group">
|
|
852
|
+
<h3 class="shortcut-group-title">{group.title}</h3>
|
|
853
|
+
<ul class="shortcut-list">
|
|
854
|
+
{#each group.shortcuts as shortcut}
|
|
855
|
+
<li class="shortcut-row">
|
|
856
|
+
<span class="shortcut-label">{shortcut.label}</span>
|
|
857
|
+
<div class="shortcut-keys">
|
|
858
|
+
{#each shortcut.keys as k}
|
|
859
|
+
{#if k === null}
|
|
860
|
+
<span class="shortcut-sep">+</span>
|
|
861
|
+
{:else}
|
|
862
|
+
<kbd class="shortcut-key">{k}</kbd>
|
|
863
|
+
{/if}
|
|
864
|
+
{/each}
|
|
865
|
+
</div>
|
|
866
|
+
</li>
|
|
867
|
+
{/each}
|
|
868
|
+
</ul>
|
|
869
|
+
</div>
|
|
870
|
+
{/each}
|
|
871
|
+
</div>
|
|
872
|
+
</div>
|
|
873
|
+
|
|
874
|
+
<div class="shortcuts-panel-footer flex items-center justify-center gap-2">
|
|
875
|
+
Press <kbd class="shortcut-key font-medium">?</kbd> <span class="shortcuts-footer-hint">(Shift + /)</span> or click <kbd class="shortcut-key" style="font-size: 31px;">⌨</kbd> in the zoom panel to toggle
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
{/if}
|
|
879
|
+
|
|
880
|
+
<style>
|
|
881
|
+
/* ══════════════════════════════════════════════════════════════
|
|
882
|
+
Widgetic Canvas Toolbar — custom design
|
|
883
|
+
• "Sheath" container: 80px height, white bg + shadow
|
|
884
|
+
• All buttons 48×48 px, icons fill proportionally
|
|
885
|
+
• Inactive: gray bg. Active: dark green. Hover: pale green.
|
|
886
|
+
• Pencil/Eraser: illustrated, extend above, pull-up on select
|
|
887
|
+
• Shapes: split button (shape + caret), dropdown centered
|
|
888
|
+
══════════════════════════════════════════════════════════════ */
|
|
889
|
+
|
|
890
|
+
/* ── Toolbar outer wrapper — transparent, no own shadow ── */
|
|
891
|
+
.canvas-toolbar-el {
|
|
892
|
+
border-radius: 1.25rem;
|
|
893
|
+
overflow: visible;
|
|
894
|
+
background: transparent !important;
|
|
895
|
+
box-shadow: none !important;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/* ── "Sheath" — the white container holding all buttons ── */
|
|
899
|
+
.tools-ct {
|
|
900
|
+
background: white;
|
|
901
|
+
border-radius: 1.25rem;
|
|
902
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.04);
|
|
903
|
+
height: 80px;
|
|
904
|
+
padding: 0 12px;
|
|
905
|
+
gap: 4px;
|
|
906
|
+
align-items: center;
|
|
907
|
+
overflow: visible;
|
|
908
|
+
position: relative;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/* ── Base button reset ── */
|
|
912
|
+
button {
|
|
913
|
+
cursor: pointer;
|
|
914
|
+
display: inline-flex;
|
|
915
|
+
align-items: center;
|
|
916
|
+
justify-content: center;
|
|
917
|
+
gap: 0;
|
|
918
|
+
border: none;
|
|
919
|
+
background: transparent;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
button:focus {
|
|
923
|
+
outline: none;
|
|
924
|
+
box-shadow: none;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
button:disabled {
|
|
928
|
+
opacity: 0.35;
|
|
929
|
+
cursor: not-allowed;
|
|
930
|
+
pointer-events: none;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* ── Uniform tool button — ALL buttons 48×48, no border ── */
|
|
934
|
+
.wdg-tool-bt {
|
|
935
|
+
width: 48px;
|
|
936
|
+
height: 48px;
|
|
937
|
+
padding: 0;
|
|
938
|
+
border-radius: 12px;
|
|
939
|
+
border: none;
|
|
940
|
+
background: #f0f0f0;
|
|
941
|
+
color: #374151;
|
|
942
|
+
transition: all 0.15s ease;
|
|
943
|
+
flex-shrink: 0;
|
|
944
|
+
box-shadow: none;
|
|
945
|
+
}
|
|
946
|
+
/* Scale icon SVGs inside tool buttons to fill them nicely */
|
|
947
|
+
.wdg-tool-bt :global(svg) {
|
|
948
|
+
width: 24px !important;
|
|
949
|
+
height: 24px !important;
|
|
950
|
+
}
|
|
951
|
+
/* Utility buttons (undo, redo, lock) — slightly smaller icons */
|
|
952
|
+
.undo-bt :global(svg),
|
|
953
|
+
.redo-bt :global(svg),
|
|
954
|
+
.sticky-tool-bt :global(svg) {
|
|
955
|
+
width: 20px !important;
|
|
956
|
+
height: 20px !important;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/* Inactive state — light gray bg */
|
|
960
|
+
.wdg-tool-inactive {
|
|
961
|
+
background: #f0f0f0;
|
|
962
|
+
color: #374151;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/* Hover — pale green */
|
|
966
|
+
.wdg-tool-bt:hover {
|
|
967
|
+
background: #e6f0eb !important;
|
|
968
|
+
color: #1a3a2a !important;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/* Active (selected) — dark green bg, white icon */
|
|
972
|
+
.wdg-tool-active {
|
|
973
|
+
background: #1a3a2a !important;
|
|
974
|
+
color: white !important;
|
|
975
|
+
box-shadow: 0 2px 8px rgba(26, 58, 42, 0.25);
|
|
976
|
+
}
|
|
977
|
+
.wdg-tool-active:hover {
|
|
978
|
+
background: #244b36 !important;
|
|
979
|
+
color: white !important;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/* Wrapper to intercept hover events on disabled buttons for tooltip.
|
|
983
|
+
display:inline-flex ensures the div is a visible box that receives mouseenter. */
|
|
984
|
+
.disabled-tooltip-wrap {
|
|
985
|
+
display: inline-flex;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/* ── Divider ── */
|
|
989
|
+
.toolbar-divider {
|
|
990
|
+
width: 1px;
|
|
991
|
+
height: 32px;
|
|
992
|
+
align-self: center;
|
|
993
|
+
background-color: #e5e7eb;
|
|
994
|
+
margin: 0 2px;
|
|
995
|
+
flex-shrink: 0;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/* ══ Illustrated tool mask container ══
|
|
999
|
+
Clips the pencil/eraser body so only the tip peeks out at rest.
|
|
1000
|
+
When active, overflow becomes visible so the full tool is revealed above the sheath. */
|
|
1001
|
+
.wdg-illustrated-mask {
|
|
1002
|
+
width: 36px;
|
|
1003
|
+
position: relative;
|
|
1004
|
+
align-self: stretch;
|
|
1005
|
+
flex-shrink: 0;
|
|
1006
|
+
z-index: 0;
|
|
1007
|
+
/* Allow tip to extend above, clip at sides and bottom */
|
|
1008
|
+
clip-path: inset(-100px 0 0 0);
|
|
1009
|
+
}
|
|
1010
|
+
.wdg-illustrated-mask-active {
|
|
1011
|
+
/* Active: allow tall extension above (−200px top), but still clip at bottom (0) */
|
|
1012
|
+
clip-path: inset(-200px 0 0 0);
|
|
1013
|
+
z-index: 3;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/* ══ Illustrated tool buttons (Pencil & Eraser) ══
|
|
1017
|
+
SVGs are taller than the 80px mask. At rest they are pushed down so only
|
|
1018
|
+
the tip peeks ~15px above the toolbar top edge. On active, translateY(0)
|
|
1019
|
+
reveals the full extent above the mask. */
|
|
1020
|
+
.wdg-illustrated-bt {
|
|
1021
|
+
display: flex;
|
|
1022
|
+
align-items: flex-end;
|
|
1023
|
+
justify-content: center;
|
|
1024
|
+
width: 36px;
|
|
1025
|
+
cursor: pointer;
|
|
1026
|
+
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1),
|
|
1027
|
+
filter 0.2s ease;
|
|
1028
|
+
flex-shrink: 0;
|
|
1029
|
+
position: absolute;
|
|
1030
|
+
bottom: 0;
|
|
1031
|
+
left: 0;
|
|
1032
|
+
filter: none;
|
|
1033
|
+
}
|
|
1034
|
+
/* Pencil: SVG ~149px tall, mask 48px.
|
|
1035
|
+
Rest: translateY(54px) → tip peeks ~15px above toolbar.
|
|
1036
|
+
Active: translateY(18px) → shows most of pencil without going too high.
|
|
1037
|
+
Active:hover: translateY(16px) → subtle 2px lift. */
|
|
1038
|
+
.wdg-illustrated-bt:has(.wdg-pencil-svg) {
|
|
1039
|
+
transform: translateY(54px);
|
|
1040
|
+
}
|
|
1041
|
+
.wdg-illustrated-bt:has(.wdg-pencil-svg):hover {
|
|
1042
|
+
transform: translateY(46px);
|
|
1043
|
+
}
|
|
1044
|
+
.wdg-illustrated-bt:has(.wdg-pencil-svg).wdg-illustrated-active {
|
|
1045
|
+
transform: translateY(18px);
|
|
1046
|
+
filter: drop-shadow(0 6px 16px rgba(0,0,0,0.22));
|
|
1047
|
+
}
|
|
1048
|
+
.wdg-illustrated-bt:has(.wdg-pencil-svg).wdg-illustrated-active:hover {
|
|
1049
|
+
transform: translateY(16px);
|
|
1050
|
+
}
|
|
1051
|
+
/* Eraser: SVG ~127px tall, mask 48px.
|
|
1052
|
+
Rest: translateY(32px) → tip peeks ~15px above toolbar.
|
|
1053
|
+
Active: translateY(10px) → shows most of eraser without going too high.
|
|
1054
|
+
Active:hover: translateY(8px) → subtle 2px lift. */
|
|
1055
|
+
.wdg-illustrated-bt:has(.wdg-eraser-svg) {
|
|
1056
|
+
transform: translateY(32px);
|
|
1057
|
+
}
|
|
1058
|
+
.wdg-illustrated-bt:has(.wdg-eraser-svg):hover {
|
|
1059
|
+
transform: translateY(24px);
|
|
1060
|
+
}
|
|
1061
|
+
.wdg-illustrated-bt:has(.wdg-eraser-svg).wdg-illustrated-active {
|
|
1062
|
+
transform: translateY(10px);
|
|
1063
|
+
filter: drop-shadow(0 6px 16px rgba(0,0,0,0.22));
|
|
1064
|
+
}
|
|
1065
|
+
.wdg-illustrated-bt:has(.wdg-eraser-svg).wdg-illustrated-active:hover {
|
|
1066
|
+
transform: translateY(8px);
|
|
1067
|
+
}
|
|
1068
|
+
/* Click feedback (both tools) */
|
|
1069
|
+
.wdg-illustrated-bt:active {
|
|
1070
|
+
transition-duration: 0.08s;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.wdg-pencil-svg {
|
|
1074
|
+
width: 30px;
|
|
1075
|
+
height: auto;
|
|
1076
|
+
}
|
|
1077
|
+
.wdg-eraser-svg {
|
|
1078
|
+
width: 32px;
|
|
1079
|
+
height: auto;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/* ── Shapes split button container (tldraw-style) ── */
|
|
1083
|
+
.shapes-split-ct {
|
|
1084
|
+
display: flex;
|
|
1085
|
+
flex-direction: row;
|
|
1086
|
+
align-items: center;
|
|
1087
|
+
gap: 2px;
|
|
1088
|
+
flex-shrink: 0;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/* Left: shape icon button — same size as other tool buttons */
|
|
1092
|
+
.shapes-main-bt {
|
|
1093
|
+
/* Inherits .wdg-tool-bt 48×48 */
|
|
1094
|
+
}
|
|
1095
|
+
/* When active, give all shape SVGs a semi-transparent fill for visual weight */
|
|
1096
|
+
.shapes-main-bt.wdg-tool-active :global(svg rect),
|
|
1097
|
+
.shapes-main-bt.wdg-tool-active :global(svg circle),
|
|
1098
|
+
.shapes-main-bt.wdg-tool-active :global(svg ellipse),
|
|
1099
|
+
.shapes-main-bt.wdg-tool-active :global(svg polygon),
|
|
1100
|
+
.shapes-main-bt.wdg-tool-active :global(svg path) {
|
|
1101
|
+
fill: rgba(255, 255, 255, 0.2);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/* Right: small caret button to open dropdown */
|
|
1105
|
+
.shapes-caret-bt {
|
|
1106
|
+
width: 24px !important;
|
|
1107
|
+
height: 48px !important;
|
|
1108
|
+
border-radius: 8px !important;
|
|
1109
|
+
padding: 0 !important;
|
|
1110
|
+
}
|
|
1111
|
+
.shapes-caret-bt :global(svg) {
|
|
1112
|
+
width: 10px !important;
|
|
1113
|
+
height: 10px !important;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/* ── Shortcuts footer hint text ── */
|
|
1117
|
+
.shortcuts-footer-hint {
|
|
1118
|
+
font-size: 0.75rem;
|
|
1119
|
+
opacity: 0.6;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/* ── Shapes dropdown menu — centered above trigger (tldraw-style) ── */
|
|
1123
|
+
.shapes-dropdown-menu-fixed {
|
|
1124
|
+
background: white;
|
|
1125
|
+
border: 1px solid #e5e7eb;
|
|
1126
|
+
border-radius: 12px;
|
|
1127
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06);
|
|
1128
|
+
padding: 6px;
|
|
1129
|
+
overflow: hidden;
|
|
1130
|
+
display: flex;
|
|
1131
|
+
flex-direction: column;
|
|
1132
|
+
}
|
|
1133
|
+
/* Inner scrollable wrapper — margin-right set dynamically when overflowing */
|
|
1134
|
+
.shapes-dropdown-scroll {
|
|
1135
|
+
overflow-y: auto;
|
|
1136
|
+
overflow-x: hidden;
|
|
1137
|
+
scrollbar-width: thin;
|
|
1138
|
+
scrollbar-color: rgba(0,0,0,0.15) transparent;
|
|
1139
|
+
}
|
|
1140
|
+
.shapes-dropdown-scroll::-webkit-scrollbar {
|
|
1141
|
+
width: 4px;
|
|
1142
|
+
}
|
|
1143
|
+
.shapes-dropdown-scroll::-webkit-scrollbar-track {
|
|
1144
|
+
background: transparent;
|
|
1145
|
+
}
|
|
1146
|
+
.shapes-dropdown-scroll::-webkit-scrollbar-thumb {
|
|
1147
|
+
background: rgba(0,0,0,0.15);
|
|
1148
|
+
border-radius: 4px;
|
|
1149
|
+
}
|
|
1150
|
+
.shapes-dropdown-scroll::-webkit-scrollbar-thumb:hover {
|
|
1151
|
+
background: rgba(0,0,0,0.25);
|
|
1152
|
+
}
|
|
1153
|
+
.shapes-dropdown-menu-fixed .shape-option-bt {
|
|
1154
|
+
width: 100%;
|
|
1155
|
+
justify-content: flex-start;
|
|
1156
|
+
padding: 8px 10px;
|
|
1157
|
+
background: transparent;
|
|
1158
|
+
border: none;
|
|
1159
|
+
border-radius: 8px;
|
|
1160
|
+
font-size: 0.875rem;
|
|
1161
|
+
color: #374151;
|
|
1162
|
+
box-shadow: none;
|
|
1163
|
+
gap: 8px;
|
|
1164
|
+
white-space: nowrap;
|
|
1165
|
+
}
|
|
1166
|
+
.shapes-dropdown-menu-fixed .shape-option-bt:hover {
|
|
1167
|
+
background: #f3f4f6;
|
|
1168
|
+
color: #111827;
|
|
1169
|
+
}
|
|
1170
|
+
.shapes-dropdown-menu-fixed .shape-option-bt.active {
|
|
1171
|
+
background: #eff6ff;
|
|
1172
|
+
color: #2563eb;
|
|
1173
|
+
font-weight: 500;
|
|
1174
|
+
}
|
|
1175
|
+
.shapes-dropdown-menu-fixed .shape-option-bt.active:hover {
|
|
1176
|
+
background: #dbeafe;
|
|
1177
|
+
color: #1d4ed8;
|
|
1178
|
+
}
|
|
1179
|
+
/* Divider between shapes and image in the dropdown */
|
|
1180
|
+
.shapes-dropdown-divider {
|
|
1181
|
+
height: 1px;
|
|
1182
|
+
background: #e5e7eb;
|
|
1183
|
+
margin: 4px 0;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/* Category label for library shape sections */
|
|
1187
|
+
.shapes-category-label {
|
|
1188
|
+
padding: 4px 8px 2px;
|
|
1189
|
+
font-size: 9px;
|
|
1190
|
+
font-weight: 600;
|
|
1191
|
+
text-transform: uppercase;
|
|
1192
|
+
white-space: nowrap;
|
|
1193
|
+
letter-spacing: 0.05em;
|
|
1194
|
+
color: #9ca3af;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/* Grid layout for library shapes (icon-only buttons, 3 columns) */
|
|
1198
|
+
.shapes-library-grid {
|
|
1199
|
+
display: grid;
|
|
1200
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1201
|
+
gap: 2px;
|
|
1202
|
+
padding: 4px 2px;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.shape-library-bt {
|
|
1206
|
+
display: flex;
|
|
1207
|
+
align-items: center;
|
|
1208
|
+
justify-content: center;
|
|
1209
|
+
width: 40px;
|
|
1210
|
+
height: 40px;
|
|
1211
|
+
border: none;
|
|
1212
|
+
border-radius: 8px;
|
|
1213
|
+
background: transparent;
|
|
1214
|
+
color: #374151;
|
|
1215
|
+
cursor: pointer;
|
|
1216
|
+
transition: background 0.1s;
|
|
1217
|
+
}
|
|
1218
|
+
/* Scale SVG icons inside library shape buttons */
|
|
1219
|
+
.shape-library-bt :global(svg) {
|
|
1220
|
+
width: 20px;
|
|
1221
|
+
height: 20px;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.shape-library-bt:hover {
|
|
1225
|
+
background: #f3f4f6;
|
|
1226
|
+
color: #111827;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/* ── Keyboard Shortcuts Panel ── */
|
|
1230
|
+
/* position:absolute so it centers within the canvas-container (position:relative),
|
|
1231
|
+
not the viewport — avoids issues with transformed ancestors. */
|
|
1232
|
+
.shortcuts-backdrop {
|
|
1233
|
+
position: absolute;
|
|
1234
|
+
inset: 0;
|
|
1235
|
+
background: rgba(0,0,0,0.3);
|
|
1236
|
+
z-index: 40;
|
|
1237
|
+
animation: backdrop-fade 0.1s ease-out forwards;
|
|
1238
|
+
}
|
|
1239
|
+
@keyframes backdrop-fade {
|
|
1240
|
+
from { opacity: 0; }
|
|
1241
|
+
to { opacity: 1; }
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.shortcuts-panel {
|
|
1245
|
+
position: absolute;
|
|
1246
|
+
top: 50%;
|
|
1247
|
+
left: 50%;
|
|
1248
|
+
transform: translate(-50%, -50%);
|
|
1249
|
+
background: white;
|
|
1250
|
+
border-radius: 0.75rem;
|
|
1251
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.1);
|
|
1252
|
+
z-index: 50;
|
|
1253
|
+
width: min(580px, 90%);
|
|
1254
|
+
/* Panel has no own scroll — only the body section scrolls */
|
|
1255
|
+
overflow: hidden;
|
|
1256
|
+
display: flex;
|
|
1257
|
+
flex-direction: column;
|
|
1258
|
+
max-height: 85%;
|
|
1259
|
+
/* Fade and slide in — starts invisible to prevent flash */
|
|
1260
|
+
opacity: 0;
|
|
1261
|
+
animation: panel-appear 0.15s ease-out 0.02s forwards;
|
|
1262
|
+
}
|
|
1263
|
+
@keyframes panel-appear {
|
|
1264
|
+
from { opacity: 0; transform: translate(-50%, -48%); }
|
|
1265
|
+
to { opacity: 1; transform: translate(-50%, -50%); }
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.shortcuts-panel-header {
|
|
1269
|
+
display: flex;
|
|
1270
|
+
align-items: center;
|
|
1271
|
+
justify-content: space-between;
|
|
1272
|
+
padding: 14px 16px;
|
|
1273
|
+
border-bottom: 1px solid #f3f4f6;
|
|
1274
|
+
flex-shrink: 0;
|
|
1275
|
+
}
|
|
1276
|
+
.shortcuts-panel-title-ct {
|
|
1277
|
+
display: flex;
|
|
1278
|
+
align-items: center;
|
|
1279
|
+
gap: 8px;
|
|
1280
|
+
}
|
|
1281
|
+
.shortcuts-panel-title {
|
|
1282
|
+
font-size: 0.9375rem;
|
|
1283
|
+
font-weight: 600;
|
|
1284
|
+
color: #111827;
|
|
1285
|
+
margin: 0;
|
|
1286
|
+
}
|
|
1287
|
+
.close-shortcuts-bt {
|
|
1288
|
+
color: #9ca3af;
|
|
1289
|
+
width: 28px;
|
|
1290
|
+
height: 28px;
|
|
1291
|
+
border: none;
|
|
1292
|
+
border-radius: 6px;
|
|
1293
|
+
padding: 0;
|
|
1294
|
+
background: transparent;
|
|
1295
|
+
box-shadow: none;
|
|
1296
|
+
font-size: 0.875rem;
|
|
1297
|
+
justify-content: center;
|
|
1298
|
+
flex-shrink: 0;
|
|
1299
|
+
}
|
|
1300
|
+
.close-shortcuts-bt:hover {
|
|
1301
|
+
background: #f3f4f6;
|
|
1302
|
+
color: #374151;
|
|
1303
|
+
}
|
|
1304
|
+
/* Scrollable section — only this part scrolls */
|
|
1305
|
+
.shortcuts-panel-body {
|
|
1306
|
+
display: flex;
|
|
1307
|
+
gap: 20px;
|
|
1308
|
+
padding: 8px 16px;
|
|
1309
|
+
overflow-y: auto;
|
|
1310
|
+
flex: 1 1 auto;
|
|
1311
|
+
}
|
|
1312
|
+
.shortcuts-column {
|
|
1313
|
+
flex: 1;
|
|
1314
|
+
display: flex;
|
|
1315
|
+
flex-direction: column;
|
|
1316
|
+
gap: 2px;
|
|
1317
|
+
}
|
|
1318
|
+
/* Custom scrollbar inside the panel body */
|
|
1319
|
+
.shortcuts-panel-body::-webkit-scrollbar {
|
|
1320
|
+
width: 4px;
|
|
1321
|
+
}
|
|
1322
|
+
.shortcuts-panel-body::-webkit-scrollbar-track {
|
|
1323
|
+
background: transparent;
|
|
1324
|
+
}
|
|
1325
|
+
.shortcuts-panel-body::-webkit-scrollbar-thumb {
|
|
1326
|
+
background: rgba(0,0,0,0.15);
|
|
1327
|
+
border-radius: 4px;
|
|
1328
|
+
}
|
|
1329
|
+
.shortcut-group {
|
|
1330
|
+
margin-bottom: 2px;
|
|
1331
|
+
}
|
|
1332
|
+
/* Remove extra bottom margin on the last group so panel body padding handles spacing */
|
|
1333
|
+
.shortcut-group:last-child {
|
|
1334
|
+
margin-bottom: 0;
|
|
1335
|
+
}
|
|
1336
|
+
.shortcut-group-title {
|
|
1337
|
+
font-size: 0.6875rem;
|
|
1338
|
+
font-weight: 700;
|
|
1339
|
+
color: #1f2937;
|
|
1340
|
+
text-transform: uppercase;
|
|
1341
|
+
letter-spacing: 0.06em;
|
|
1342
|
+
margin-bottom: 4px;
|
|
1343
|
+
}
|
|
1344
|
+
.shortcut-list {
|
|
1345
|
+
list-style: none;
|
|
1346
|
+
padding: 0;
|
|
1347
|
+
margin: 0;
|
|
1348
|
+
}
|
|
1349
|
+
.shortcut-row {
|
|
1350
|
+
display: flex;
|
|
1351
|
+
align-items: center;
|
|
1352
|
+
justify-content: space-between;
|
|
1353
|
+
padding: 3px 0;
|
|
1354
|
+
}
|
|
1355
|
+
.shortcut-label {
|
|
1356
|
+
font-size: 0.8125rem;
|
|
1357
|
+
color: #4b5563;
|
|
1358
|
+
}
|
|
1359
|
+
.shortcut-keys {
|
|
1360
|
+
display: flex;
|
|
1361
|
+
align-items: center;
|
|
1362
|
+
gap: 3px;
|
|
1363
|
+
flex-shrink: 0;
|
|
1364
|
+
margin-left: 8px;
|
|
1365
|
+
}
|
|
1366
|
+
.shortcut-sep {
|
|
1367
|
+
font-size: 0.6875rem;
|
|
1368
|
+
color: #9ca3af;
|
|
1369
|
+
font-weight: 500;
|
|
1370
|
+
padding: 0 1px;
|
|
1371
|
+
}
|
|
1372
|
+
/* Key badge — single-char keys get equal width for a square look */
|
|
1373
|
+
.shortcut-key {
|
|
1374
|
+
background: #f3f4f6;
|
|
1375
|
+
color: #374151;
|
|
1376
|
+
font-size: 0.6875rem;
|
|
1377
|
+
font-family: ui-monospace, 'SFMono-Regular', monospace;
|
|
1378
|
+
padding: 3px 6px;
|
|
1379
|
+
border-radius: 4px;
|
|
1380
|
+
border: 1px solid #d1d5db;
|
|
1381
|
+
box-shadow: 0 1px 1px rgba(0,0,0,0.08);
|
|
1382
|
+
white-space: nowrap;
|
|
1383
|
+
min-width: 22px;
|
|
1384
|
+
height: 22px;
|
|
1385
|
+
display: inline-flex;
|
|
1386
|
+
align-items: center;
|
|
1387
|
+
justify-content: center;
|
|
1388
|
+
}
|
|
1389
|
+
.shortcuts-panel-footer {
|
|
1390
|
+
padding: 10px 16px 14px;
|
|
1391
|
+
text-align: center;
|
|
1392
|
+
font-size: 0.75rem;
|
|
1393
|
+
color: #9ca3af;
|
|
1394
|
+
border-top: 1px solid #f3f4f6;
|
|
1395
|
+
flex-shrink: 0;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/* Custom scrollbar for tools and actions containers */
|
|
1399
|
+
.tools-ct::-webkit-scrollbar,
|
|
1400
|
+
.actions-ct::-webkit-scrollbar {
|
|
1401
|
+
height: 4px;
|
|
1402
|
+
}
|
|
1403
|
+
.tools-ct::-webkit-scrollbar-track,
|
|
1404
|
+
.actions-ct::-webkit-scrollbar-track {
|
|
1405
|
+
background: transparent;
|
|
1406
|
+
}
|
|
1407
|
+
.tools-ct::-webkit-scrollbar-thumb,
|
|
1408
|
+
.actions-ct::-webkit-scrollbar-thumb {
|
|
1409
|
+
background: rgba(0, 0, 0, 0.15);
|
|
1410
|
+
border-radius: 4px;
|
|
1411
|
+
}
|
|
1412
|
+
.tools-ct::-webkit-scrollbar-thumb:hover,
|
|
1413
|
+
.actions-ct::-webkit-scrollbar-thumb:hover {
|
|
1414
|
+
background: rgba(0, 0, 0, 0.25);
|
|
1415
|
+
}
|
|
1416
|
+
/* Firefox scrollbar */
|
|
1417
|
+
.tools-ct,
|
|
1418
|
+
.actions-ct {
|
|
1419
|
+
scrollbar-width: thin;
|
|
1420
|
+
scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
|
|
1421
|
+
}
|
|
1422
|
+
</style>
|