@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,1902 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ColorIC from '../../components/input-controls/ColorIC.svelte';
|
|
3
|
+
import SliderUnitIC from '../../components/input-controls/SliderUnitIC.svelte';
|
|
4
|
+
import TextPropsICSection from './TextPropsICSection.svelte';
|
|
5
|
+
import TextAlignIC from '../../components/input-controls/TextAlignIC.svelte';
|
|
6
|
+
import FontSelectorIC from '../../components/input-controls/FontSelectorIC.svelte';
|
|
7
|
+
import TextStyleIC from '../../components/input-controls/TextStyleIC.svelte';
|
|
8
|
+
import Tooltip from '../../components/Tooltip.svelte';
|
|
9
|
+
|
|
10
|
+
let className = '';
|
|
11
|
+
export { className as class };
|
|
12
|
+
export let style = '';
|
|
13
|
+
|
|
14
|
+
// Selected object from Canvas
|
|
15
|
+
export let selectedObject: any = null;
|
|
16
|
+
|
|
17
|
+
// Wireframe mode: when true, hides color pickers so the panel focuses on
|
|
18
|
+
// structural properties (size, position, font, stroke width, etc.).
|
|
19
|
+
// Opacity is always shown — useful for hiding objects without deleting them.
|
|
20
|
+
export let wireframeMode: boolean = false;
|
|
21
|
+
|
|
22
|
+
// Callback functions for property changes - General
|
|
23
|
+
export let onFillChange: (color: string) => void = () => {};
|
|
24
|
+
export let onStrokeChange: (color: string) => void = () => {};
|
|
25
|
+
export let onStrokeWidthChange: (width: number) => void = () => {};
|
|
26
|
+
export let onRoundnessChange: (roundness: number) => void = () => {};
|
|
27
|
+
export let onStrokeTypeChange: (type: 'solid' | 'dashed' | 'dotted') => void = () => {};
|
|
28
|
+
export let onOpacityChange: (opacity: number) => void = () => {};
|
|
29
|
+
export let onFillOpacityChange: (opacity: number) => void = () => {};
|
|
30
|
+
export let onBackgroundColorChange: (color: string) => void = () => {};
|
|
31
|
+
export let onBackgroundOpacityChange: (opacity: number) => void = () => {};
|
|
32
|
+
|
|
33
|
+
// Callback functions for text properties
|
|
34
|
+
export let onFontSizeChange: (size: number) => void = () => {};
|
|
35
|
+
export let onFontFamilyChange: (family: string) => void = () => {};
|
|
36
|
+
export let onFontWeightChange: (weight: 'normal' | 'bold') => void = () => {};
|
|
37
|
+
export let onFontStyleChange: (style: 'normal' | 'italic') => void = () => {};
|
|
38
|
+
export let onUnderlineChange: (underline: boolean) => void = () => {};
|
|
39
|
+
export let onTextAlignChange: (align: 'left' | 'center' | 'right') => void = () => {};
|
|
40
|
+
export let onTextPaddingChange: (padding: number) => void = () => {};
|
|
41
|
+
export let onTextColorChange: (color: string) => void = () => {};
|
|
42
|
+
|
|
43
|
+
// Callback functions for frame properties
|
|
44
|
+
export let onFrameLabelChange: (label: string) => void = () => {};
|
|
45
|
+
|
|
46
|
+
// Callback functions for frame label text properties
|
|
47
|
+
export let onFrameLabelFontSizeChange: (size: number) => void = () => {};
|
|
48
|
+
export let onFrameLabelFontFamilyChange: (family: string) => void = () => {};
|
|
49
|
+
export let onFrameLabelTextAlignChange: (align: 'left' | 'center' | 'right') => void = () => {};
|
|
50
|
+
export let onFrameLabelTextColorChange: (color: string) => void = () => {};
|
|
51
|
+
export let onFrameLabelFontWeightChange: (weight: 'normal' | 'bold') => void = () => {};
|
|
52
|
+
export let onFrameLabelFontStyleChange: (style: 'normal' | 'italic') => void = () => {};
|
|
53
|
+
export let onFrameLabelUnderlineChange: (underline: boolean) => void = () => {};
|
|
54
|
+
export let onFrameLabelBackgroundColorChange: (color: string) => void = () => {};
|
|
55
|
+
|
|
56
|
+
// Callback functions for textbox box properties
|
|
57
|
+
export let onTextboxBoxFillChange: (color: string) => void = () => {};
|
|
58
|
+
export let onTextboxBoxStrokeChange: (color: string) => void = () => {};
|
|
59
|
+
export let onTextboxBoxStrokeWidthChange: (width: number) => void = () => {};
|
|
60
|
+
export let onTextboxBoxStrokeTypeChange: (type: 'solid' | 'dashed' | 'dotted') => void = () => {};
|
|
61
|
+
|
|
62
|
+
// Callback functions for text stroke properties (stroke on the text itself)
|
|
63
|
+
export let onTextStrokeColorChange: (color: string) => void = () => {};
|
|
64
|
+
export let onTextStrokeWidthChange: (width: number) => void = () => {};
|
|
65
|
+
|
|
66
|
+
// Callback functions for image properties
|
|
67
|
+
export let onImageUrlLoad: (url: string) => void = () => {};
|
|
68
|
+
export let onImageSourceChange: () => void = () => {};
|
|
69
|
+
|
|
70
|
+
// Callback for capture action (captures screenshot and fires onObjectClick)
|
|
71
|
+
export let onCapture: (() => void) | null = null;
|
|
72
|
+
|
|
73
|
+
// Callback for "Convert to Widget" action (serializes frame + children, emits to host)
|
|
74
|
+
export let onConvertToWidget: (() => void) | null = null;
|
|
75
|
+
|
|
76
|
+
// Callback: fires when the user clicks the linked widget badge to open widget details
|
|
77
|
+
export let onWidgetClick: ((widgetId: string) => void) | null = null;
|
|
78
|
+
|
|
79
|
+
// Callback: fires when the user renames a widget from the Props Panel
|
|
80
|
+
export let onWidgetRename: ((widgetId: string, newName: string) => void) | null = null;
|
|
81
|
+
|
|
82
|
+
// Callback: fires when the user requests a screenshot refresh for the linked widget
|
|
83
|
+
export let onRefreshScreenshot: ((widgetId: string) => void) | null = null;
|
|
84
|
+
|
|
85
|
+
// Widget name resolver: given a widgetId, returns the widget name (or null)
|
|
86
|
+
export let getWidgetName: ((widgetId: string) => string | null) | null = null;
|
|
87
|
+
/** Returns false when the linked widget was deleted or moved off this canvas. */
|
|
88
|
+
export let isWidgetKnown: ((widgetId: string) => boolean) | null = null;
|
|
89
|
+
/** Remove the selected orphaned widget shape from the canvas. */
|
|
90
|
+
export let onRemoveOrphanedShape: (() => void) | null = null;
|
|
91
|
+
|
|
92
|
+
// Callback functions for layer ordering
|
|
93
|
+
export let onBringToFront: () => void = () => {};
|
|
94
|
+
export let onBringForward: () => void = () => {};
|
|
95
|
+
export let onSendBackward: () => void = () => {};
|
|
96
|
+
export let onSendToBack: () => void = () => {};
|
|
97
|
+
|
|
98
|
+
// Callback functions for alignment
|
|
99
|
+
// Single object → aligns to canvas edges; multiple objects → aligns to each other
|
|
100
|
+
export let onAlignLeft: () => void = () => {};
|
|
101
|
+
export let onAlignCenterH: () => void = () => {};
|
|
102
|
+
export let onAlignRight: () => void = () => {};
|
|
103
|
+
export let onAlignTop: () => void = () => {};
|
|
104
|
+
export let onAlignMiddleV: () => void = () => {};
|
|
105
|
+
export let onAlignBottom: () => void = () => {};
|
|
106
|
+
|
|
107
|
+
// Callback functions for dimensions
|
|
108
|
+
export let onWidthChange: (width: number) => void = () => {};
|
|
109
|
+
export let onHeightChange: (height: number) => void = () => {};
|
|
110
|
+
|
|
111
|
+
const wireframeColorValues = ['#000000', '#555555', '#999999', '#CCCCCC', '#FFFFFF'];
|
|
112
|
+
|
|
113
|
+
// Derived properties from selected object
|
|
114
|
+
$: objectType = selectedObject?.type ?? 'None';
|
|
115
|
+
$: customType = selectedObject?._customType ?? null;
|
|
116
|
+
$: isFrame = customType === 'frame' || objectType === 'frame';
|
|
117
|
+
// WidgetShape is a Rect subclass with _customType === 'image'
|
|
118
|
+
// isImagePlaceholder: when no image loaded yet (_hasImage is false)
|
|
119
|
+
$: isImagePlaceholder = customType === 'image' && !selectedObject?._hasImage;
|
|
120
|
+
// isRect excludes WidgetShape (which has _customType === 'image')
|
|
121
|
+
$: isRect = objectType === 'rect' && !isFrame && customType !== 'image';
|
|
122
|
+
$: isTextbox = objectType === 'textbox';
|
|
123
|
+
$: isPath = objectType === 'path' && !customType;
|
|
124
|
+
$: isArrow = objectType === 'path' && customType === 'arrow';
|
|
125
|
+
$: isTriangle = objectType === 'path' && customType === 'triangle';
|
|
126
|
+
// Library shapes whose cornerRadiusMode is 'none' (circle, heart) should NOT show the slider
|
|
127
|
+
$: libraryCornerRadiusMode = (selectedObject as any)?._cornerRadiusMode ?? null;
|
|
128
|
+
$: isLibraryShape = objectType === 'path' && customType === 'libraryShape' && libraryCornerRadiusMode !== 'none';
|
|
129
|
+
$: isCircle = objectType === 'circle';
|
|
130
|
+
$: isEllipse = objectType === 'ellipse';
|
|
131
|
+
$: isGroup = objectType === 'group';
|
|
132
|
+
$: isActiveSelection = objectType === 'activeSelection' || objectType === 'activeselection';
|
|
133
|
+
// FrameShape objects have type === 'FrameShape', not 'group', so isGroup is always false for them.
|
|
134
|
+
// isMultiSelect is only true for plain Groups and ActiveSelections (not Frames).
|
|
135
|
+
$: isMultiSelect = (isGroup && !isFrame) || isActiveSelection;
|
|
136
|
+
$: isLine = objectType === 'line';
|
|
137
|
+
$: isImage = objectType === 'image' || customType === 'image';
|
|
138
|
+
// Widget link: true when the selected object has already been converted to a widget
|
|
139
|
+
$: hasWidgetId = !!selectedObject?._widgetId;
|
|
140
|
+
$: widgetId = selectedObject?._widgetId ?? null;
|
|
141
|
+
$: isWidgetImage = !!selectedObject?._isWidgetImage;
|
|
142
|
+
$: isPendingWidgetLink = !!selectedObject?._pendingWidgetLink;
|
|
143
|
+
$: isDuplicating = !!selectedObject?._isDuplicating;
|
|
144
|
+
$: isConverting = !!selectedObject?._isConverting;
|
|
145
|
+
$: showWidgetLoader = isPendingWidgetLink || isDuplicating || isConverting;
|
|
146
|
+
$: resolvedWidgetName = (widgetId && getWidgetName) ? getWidgetName(widgetId) : null;
|
|
147
|
+
$: isOrphanedWidgetShape = !!(
|
|
148
|
+
widgetId
|
|
149
|
+
&& hasWidgetId
|
|
150
|
+
&& isWidgetKnown
|
|
151
|
+
&& !isWidgetKnown(widgetId)
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
let widgetRenameValue = '';
|
|
155
|
+
|
|
156
|
+
// Children count for containers (frames exclude the background rect, groups/selections count all)
|
|
157
|
+
$: childrenCount = getChildrenCount(selectedObject);
|
|
158
|
+
function getChildrenCount(obj: any): number | null {
|
|
159
|
+
if (!obj) return null;
|
|
160
|
+
if (obj._customType === 'frame' && obj.getObjects) {
|
|
161
|
+
return obj.getObjects().filter((child: any) => !child._isFrameBackground).length;
|
|
162
|
+
}
|
|
163
|
+
if ((obj.type === 'group' || obj.type === 'activeSelection' || obj.type === 'activeselection') && obj.getObjects) {
|
|
164
|
+
return obj.getObjects().length;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check if selection/group contains textboxes or frames (for showing Text Properties section).
|
|
170
|
+
// Explicitly exclude Frames: Frame children should not drive the Text Properties section.
|
|
171
|
+
$: hasTextboxInSelection = isMultiSelect && !isFrame && selectedObject?.getObjects?.()?.some((obj: any) =>
|
|
172
|
+
obj.type === 'textbox' || obj._customType === 'frame'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Has certain properties
|
|
176
|
+
// For Frames: hasFill/hasStroke is always true (background rect has fill/stroke)
|
|
177
|
+
$: hasStroke = selectedObject && ('stroke' in selectedObject || isFrame);
|
|
178
|
+
// Plain Drawing paths (isPath) are now included so users can set fill color on freehand shapes.
|
|
179
|
+
// Arrows and lines remain excluded (they are stroke-only by nature).
|
|
180
|
+
$: hasFill = selectedObject && ('fill' in selectedObject || isFrame) && !isLine && !isArrow;
|
|
181
|
+
|
|
182
|
+
// Get display name for object type
|
|
183
|
+
$: displayType = getDisplayType(selectedObject);
|
|
184
|
+
|
|
185
|
+
function getDisplayType(obj: any): string {
|
|
186
|
+
if (!obj) return 'None';
|
|
187
|
+
if (obj._isWidgetImage) return 'Widget';
|
|
188
|
+
if (obj._customType === 'frame') return 'Frame';
|
|
189
|
+
if (obj._customType === 'arrow') return 'Arrow';
|
|
190
|
+
if (obj._customType === 'triangle') return 'Triangle';
|
|
191
|
+
if (obj._customType === 'image' || obj.type === 'image') return 'Image';
|
|
192
|
+
// Library shapes: derive a readable name from _shapeKey (e.g. 'arrow-right' → 'Arrow Right')
|
|
193
|
+
// Fall back to 'Custom Shape (Drawing)' if _shapeKey is missing (old saves).
|
|
194
|
+
if (obj._customType === 'libraryShape') {
|
|
195
|
+
if (obj._shapeKey) {
|
|
196
|
+
const name = (obj._shapeKey as string)
|
|
197
|
+
.split('-')
|
|
198
|
+
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
199
|
+
.join(' ');
|
|
200
|
+
return `${name} (Drawing)`;
|
|
201
|
+
}
|
|
202
|
+
return 'Custom Shape (Drawing)';
|
|
203
|
+
}
|
|
204
|
+
const typeMap: Record<string, string> = {
|
|
205
|
+
'rect': 'Rectangle',
|
|
206
|
+
'circle': 'Circle',
|
|
207
|
+
'ellipse': 'Ellipse',
|
|
208
|
+
'triangle': 'Triangle',
|
|
209
|
+
'textbox': 'Text',
|
|
210
|
+
'path': 'Drawing',
|
|
211
|
+
'line': 'Line',
|
|
212
|
+
'group': 'Group',
|
|
213
|
+
'activeSelection': 'Selection',
|
|
214
|
+
'activeselection': 'Selection'
|
|
215
|
+
};
|
|
216
|
+
return typeMap[obj.type] || obj.type;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Get current values from selected object
|
|
220
|
+
// For Frames (Group-based), read fill/stroke from custom properties (_frameFill, _frameStroke)
|
|
221
|
+
// because the actual fill/stroke is on the background Rect child, not the Group itself
|
|
222
|
+
$: currentFill = isFrame ? (selectedObject?._frameFill ?? '#FFFFFF') : (selectedObject?.fill ?? '#FFFFFF');
|
|
223
|
+
// Fill opacity is stored as custom property _fillOpacity (0-1), display as percentage
|
|
224
|
+
$: currentFillOpacity = Math.round((selectedObject?._fillOpacity ?? 1) * 100);
|
|
225
|
+
$: currentStroke = isFrame ? (selectedObject?._frameStroke ?? '#000000') : (selectedObject?.stroke ?? '#000000');
|
|
226
|
+
$: currentStrokeWidth = isFrame ? (selectedObject?._frameStrokeWidth ?? 1) : (selectedObject?.strokeWidth ?? 1);
|
|
227
|
+
// Corner roundness is stored as percentage (0-100%) in _cornerRoundness for all objects
|
|
228
|
+
// 100% = perfect circle (for square shapes)
|
|
229
|
+
$: currentRoundness = selectedObject?._cornerRoundness ?? 0;
|
|
230
|
+
$: currentTriangleRoundness = selectedObject?._cornerRoundness ?? 0;
|
|
231
|
+
$: currentOpacity = Math.round((selectedObject?.opacity ?? 1) * 100);
|
|
232
|
+
$: currentBackgroundColor = selectedObject?.backgroundColor || 'transparent';
|
|
233
|
+
$: currentBackgroundOpacity = Math.round((selectedObject?._backgroundOpacity ?? 1) * 100);
|
|
234
|
+
// For Frames, stroke dash is stored on _frameStrokeDashArray (the background Rect's
|
|
235
|
+
// strokeDashArray is on a child, not the Group itself, so reading it directly is unreliable).
|
|
236
|
+
$: currentStrokeType = getStrokeType(
|
|
237
|
+
isFrame
|
|
238
|
+
? (selectedObject as any)?._frameStrokeDashArray
|
|
239
|
+
: selectedObject?.strokeDashArray
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Text properties - for groups, get from first textbox
|
|
243
|
+
$: firstTextboxInGroup = (selectedObject?.type === 'group' || selectedObject?.type === 'activeSelection' || selectedObject?.type === 'activeselection')
|
|
244
|
+
? selectedObject?.getObjects?.()?.find((obj: any) => obj.type === 'textbox')
|
|
245
|
+
: null;
|
|
246
|
+
$: currentFontSize = firstTextboxInGroup?.fontSize ?? selectedObject?.fontSize ?? 28;
|
|
247
|
+
$: currentFontFamily = firstTextboxInGroup?.fontFamily ?? selectedObject?.fontFamily ?? 'Arial';
|
|
248
|
+
$: currentFontWeight = firstTextboxInGroup?.fontWeight ?? selectedObject?.fontWeight ?? 'normal';
|
|
249
|
+
$: currentFontStyle = firstTextboxInGroup?.fontStyle ?? selectedObject?.fontStyle ?? 'normal';
|
|
250
|
+
$: currentUnderline = firstTextboxInGroup?.underline ?? selectedObject?.underline ?? false;
|
|
251
|
+
$: currentTextAlign = firstTextboxInGroup?.textAlign ?? selectedObject?.textAlign ?? 'left';
|
|
252
|
+
$: currentTextPadding = firstTextboxInGroup?._textPadding ?? selectedObject?._textPadding ?? 0;
|
|
253
|
+
|
|
254
|
+
// Frame label properties (using new custom properties)
|
|
255
|
+
$: currentFrameLabel = selectedObject?._labelText ?? 'Frame';
|
|
256
|
+
$: frameLabelFontSize = selectedObject?._labelFontSize ?? 16;
|
|
257
|
+
$: frameLabelFontFamily = selectedObject?._labelFontFamily ?? 'Arial';
|
|
258
|
+
$: frameLabelTextAlign = selectedObject?._labelTextAlign ?? 'left';
|
|
259
|
+
$: frameLabelTextColor = selectedObject?._labelColor ?? '#000000';
|
|
260
|
+
$: frameLabelFontWeight = selectedObject?._labelFontWeight ?? 'normal';
|
|
261
|
+
$: frameLabelFontStyle = selectedObject?._labelFontStyle ?? 'normal';
|
|
262
|
+
$: frameLabelUnderline = selectedObject?._labelUnderline ?? false;
|
|
263
|
+
$: frameLabelBackgroundColor = selectedObject?._labelBackgroundColor ?? '#ffffff';
|
|
264
|
+
|
|
265
|
+
// Text stroke properties (stroke on the text itself - Fabric's native stroke)
|
|
266
|
+
$: textStrokeColor = selectedObject?.stroke ?? '#000000';
|
|
267
|
+
// Show 0px when stroke is null/undefined (no visible stroke)
|
|
268
|
+
$: textStrokeWidth = selectedObject?.stroke ? (selectedObject?.strokeWidth ?? 1) : 0;
|
|
269
|
+
|
|
270
|
+
// Textbox box properties
|
|
271
|
+
$: textboxBoxFill = selectedObject?.backgroundColor ?? 'transparent';
|
|
272
|
+
// Box fill opacity: show 0% when backgroundColor is effectively invisible
|
|
273
|
+
// (transparent, unset, or rgba with alpha=0 like 'rgba(255,255,255,0)')
|
|
274
|
+
$: textboxBoxFillIsInvisible = !textboxBoxFill
|
|
275
|
+
|| textboxBoxFill === 'transparent'
|
|
276
|
+
|| /rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0\s*\)/.test(textboxBoxFill);
|
|
277
|
+
$: textboxBoxFillOpacity = textboxBoxFillIsInvisible ? 0 : currentFillOpacity;
|
|
278
|
+
// Box stroke uses custom properties (_boxStroke, _boxStrokeWidth, _boxStrokeDashArray)
|
|
279
|
+
$: textboxBoxStroke = selectedObject?._boxStroke ?? '#000000';
|
|
280
|
+
$: textboxBoxStrokeWidth = selectedObject?._boxStrokeWidth ?? 0;
|
|
281
|
+
$: textboxBoxStrokeType = getStrokeType(selectedObject?._boxStrokeDashArray);
|
|
282
|
+
|
|
283
|
+
// Image properties (WidgetShape is a Rect subclass)
|
|
284
|
+
$: imageFilename = selectedObject?._originalFilename ?? '';
|
|
285
|
+
$: imageUrl = selectedObject?._imageUrl ?? '';
|
|
286
|
+
// hasLoadedImage: when _hasImage is true (image data loaded into WidgetShape)
|
|
287
|
+
$: hasLoadedImage = isImage && selectedObject?._hasImage;
|
|
288
|
+
|
|
289
|
+
// URL input state
|
|
290
|
+
let imageUrlInput = '';
|
|
291
|
+
|
|
292
|
+
function getStrokeType(dashArray: number[] | null | undefined): 'solid' | 'dashed' | 'dotted' {
|
|
293
|
+
if (!dashArray || dashArray.length === 0) return 'solid';
|
|
294
|
+
if (dashArray[0] >= 8) return 'dashed';
|
|
295
|
+
return 'dotted';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function handleFrameLabelInput(e: Event) {
|
|
299
|
+
const input = e.target as HTMLInputElement;
|
|
300
|
+
onFrameLabelChange(input.value);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Dimensions (real size at 100% zoom) ──
|
|
304
|
+
// Fabric stores width/height as the "natural" dimensions; scaleX/scaleY multiply them.
|
|
305
|
+
// For Ellipse: Fabric uses rx/ry (radii), so displayed diameter = 2 * rx * scaleX.
|
|
306
|
+
$: objScaleX = selectedObject?.scaleX ?? 1;
|
|
307
|
+
$: objScaleY = selectedObject?.scaleY ?? 1;
|
|
308
|
+
$: rawWidth = selectedObject?.type === 'ellipse'
|
|
309
|
+
? (selectedObject?.rx ?? 0) * 2
|
|
310
|
+
: (selectedObject?.width ?? 0);
|
|
311
|
+
$: rawHeight = selectedObject?.type === 'ellipse'
|
|
312
|
+
? (selectedObject?.ry ?? 0) * 2
|
|
313
|
+
: (selectedObject?.height ?? 0);
|
|
314
|
+
$: displayWidth = Math.round(rawWidth * objScaleX);
|
|
315
|
+
$: displayHeight = Math.round(rawHeight * objScaleY);
|
|
316
|
+
// Forced lock: shapes created with _lockAspectRatio (hexagon, circle, etc.) cannot be unlocked
|
|
317
|
+
$: forcedLock = !!(selectedObject as any)?._forceLockAspectRatio;
|
|
318
|
+
// Current lock state from the object (may have been toggled by user)
|
|
319
|
+
$: dimensionLocked = !!(selectedObject as any)?._lockAspectRatio;
|
|
320
|
+
|
|
321
|
+
// User toggle state (reset when selected object changes)
|
|
322
|
+
let userDimensionLock = false;
|
|
323
|
+
let prevSelectedId: any = null;
|
|
324
|
+
$: {
|
|
325
|
+
const curId = selectedObject;
|
|
326
|
+
if (curId !== prevSelectedId) {
|
|
327
|
+
prevSelectedId = curId;
|
|
328
|
+
userDimensionLock = dimensionLocked;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
$: effectiveDimensionLock = forcedLock || userDimensionLock;
|
|
332
|
+
|
|
333
|
+
function handleWidthInput(e: Event) {
|
|
334
|
+
const input = e.target as HTMLInputElement;
|
|
335
|
+
const newWidth = Math.max(1, parseInt(input.value, 10) || 1);
|
|
336
|
+
if (effectiveDimensionLock && displayWidth > 0) {
|
|
337
|
+
const ratio = newWidth / displayWidth;
|
|
338
|
+
const newHeight = Math.round(displayHeight * ratio);
|
|
339
|
+
onWidthChange(newWidth);
|
|
340
|
+
onHeightChange(newHeight);
|
|
341
|
+
} else {
|
|
342
|
+
onWidthChange(newWidth);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function handleHeightInput(e: Event) {
|
|
347
|
+
const input = e.target as HTMLInputElement;
|
|
348
|
+
const newHeight = Math.max(1, parseInt(input.value, 10) || 1);
|
|
349
|
+
if (effectiveDimensionLock && displayHeight > 0) {
|
|
350
|
+
const ratio = newHeight / displayHeight;
|
|
351
|
+
const newWidth = Math.round(displayWidth * ratio);
|
|
352
|
+
onHeightChange(newHeight);
|
|
353
|
+
onWidthChange(newWidth);
|
|
354
|
+
} else {
|
|
355
|
+
onHeightChange(newHeight);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function toggleDimensionLock() {
|
|
360
|
+
if (forcedLock) return;
|
|
361
|
+
userDimensionLock = !userDimensionLock;
|
|
362
|
+
if (selectedObject) {
|
|
363
|
+
(selectedObject as any)._lockAspectRatio = userDimensionLock;
|
|
364
|
+
if (userDimensionLock) {
|
|
365
|
+
selectedObject.setControlsVisibility?.({ ml: false, mr: false, mt: false, mb: false });
|
|
366
|
+
} else {
|
|
367
|
+
selectedObject.setControlsVisibility?.({ ml: true, mr: true, mt: true, mb: true });
|
|
368
|
+
}
|
|
369
|
+
selectedObject.canvas?.requestRenderAll?.();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Panel minimize/maximize state: minimized shows only essential props
|
|
374
|
+
let isMinimized = true;
|
|
375
|
+
|
|
376
|
+
// Overlay-scrollbar: negative right margin = parent's horizontal padding so the
|
|
377
|
+
// scrollbar sits on top of content rather than pushing it inward.
|
|
378
|
+
// Only applied when the container actually overflows (needs scrolling).
|
|
379
|
+
let needsScroll = false;
|
|
380
|
+
let scrollableEl: HTMLElement | undefined;
|
|
381
|
+
const panelPaddingX = 12; // must match px-3 (12px) on .props-panel-el
|
|
382
|
+
|
|
383
|
+
function checkOverflow() {
|
|
384
|
+
if (!scrollableEl) return;
|
|
385
|
+
needsScroll = scrollableEl.scrollHeight > scrollableEl.clientHeight;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Svelte action: watches the scrollable container for overflow changes. */
|
|
389
|
+
function overlayScrollbar(node: HTMLElement) {
|
|
390
|
+
scrollableEl = node;
|
|
391
|
+
const ro = new ResizeObserver(checkOverflow);
|
|
392
|
+
ro.observe(node);
|
|
393
|
+
checkOverflow();
|
|
394
|
+
return { destroy() { ro.disconnect(); } };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Re-check when selected object or minimized state changes (DOM may reflow)
|
|
398
|
+
$: selectedObject, isMinimized, setTimeout(checkOverflow, 50);
|
|
399
|
+
</script>
|
|
400
|
+
|
|
401
|
+
<div class="{className ? className + ' ' : ''}props-panel-el px-3 py-3" style={style}>
|
|
402
|
+
|
|
403
|
+
<!-- Sticky Header: Selected Object Type + Children count + minimize toggle -->
|
|
404
|
+
<div class="props-panel-sticky-header">
|
|
405
|
+
<div class="header">
|
|
406
|
+
<span class="header-label">SHAPE:</span>
|
|
407
|
+
<span class="header-type" title={displayType}>{displayType}</span>
|
|
408
|
+
<Tooltip text={isMinimized ? 'Show all properties' : 'Show fewer properties'}>
|
|
409
|
+
<button
|
|
410
|
+
class="minimize-toggle-bt"
|
|
411
|
+
onclick={() => isMinimized = !isMinimized}
|
|
412
|
+
>
|
|
413
|
+
{#if isMinimized}
|
|
414
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
415
|
+
<polyline points="6 9 12 15 18 9"/>
|
|
416
|
+
</svg>
|
|
417
|
+
{:else}
|
|
418
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
419
|
+
<polyline points="18 15 12 9 6 15"/>
|
|
420
|
+
</svg>
|
|
421
|
+
{/if}
|
|
422
|
+
</button>
|
|
423
|
+
</Tooltip>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<!-- Children count: shown for frames, groups, and multi-selections -->
|
|
427
|
+
{#if childrenCount !== null}
|
|
428
|
+
<div class="children-count-row">
|
|
429
|
+
<svg class="children-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
430
|
+
<rect x="2" y="7" width="8" height="8" rx="1"/>
|
|
431
|
+
<rect x="14" y="3" width="8" height="8" rx="1"/>
|
|
432
|
+
<rect x="14" y="13" width="8" height="8" rx="1"/>
|
|
433
|
+
<line x1="10" y1="11" x2="14" y2="7"/>
|
|
434
|
+
<line x1="10" y1="11" x2="14" y2="17"/>
|
|
435
|
+
</svg>
|
|
436
|
+
<span class="prop-label">Children: {childrenCount}</span>
|
|
437
|
+
</div>
|
|
438
|
+
{/if}
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
<!-- Scrollable content: all property sections -->
|
|
442
|
+
<div class="props-panel-scrollable" use:overlayScrollbar style={needsScroll ? `margin-right: -${panelPaddingX}px` : ''}>
|
|
443
|
+
<div class="props-panel-scrollable-content flex flex-col gap-4">
|
|
444
|
+
<!-- Actions section: Capture, Convert to Widget, Layer Order (first section, no top border) -->
|
|
445
|
+
{#if onCapture || onConvertToWidget || selectedObject}
|
|
446
|
+
<div class="section-divider no-top-border">
|
|
447
|
+
<span class="section-title">Actions</span>
|
|
448
|
+
</div>
|
|
449
|
+
{#if onCapture && !(onConvertToWidget && selectedObject)}
|
|
450
|
+
<div class="prop-section">
|
|
451
|
+
<span class="prop-label">Create Preview</span>
|
|
452
|
+
<div class="capture-section">
|
|
453
|
+
<button class="capture-btn" onclick={onCapture}>
|
|
454
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
455
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
456
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
457
|
+
<polyline points="21 15 16 10 5 21"/>
|
|
458
|
+
</svg>
|
|
459
|
+
Capture
|
|
460
|
+
</button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
{/if}
|
|
464
|
+
{#if onConvertToWidget && selectedObject}
|
|
465
|
+
<div class="prop-section">
|
|
466
|
+
{#if showWidgetLoader}
|
|
467
|
+
<!-- Loading state: duplicate/convert in progress -->
|
|
468
|
+
<span class="prop-label">Widget Linked</span>
|
|
469
|
+
<div class="widget-linking-loader">
|
|
470
|
+
<div class="widget-linking-spinner"></div>
|
|
471
|
+
<span class="widget-linking-text">{isConverting ? 'Converting...' : isDuplicating ? 'Duplicating...' : 'Linking widget...'}</span>
|
|
472
|
+
</div>
|
|
473
|
+
{:else if hasWidgetId}
|
|
474
|
+
{#if isOrphanedWidgetShape}
|
|
475
|
+
<span class="prop-label">Unlinked Widget Shape</span>
|
|
476
|
+
<p class="widget-orphan-notice">
|
|
477
|
+
This shape points to a widget that was deleted or moved to another canvas.
|
|
478
|
+
Remove it from the canvas to avoid confusion.
|
|
479
|
+
</p>
|
|
480
|
+
<span class="widget-id-label" title={widgetId}>{widgetId}</span>
|
|
481
|
+
{#if onRemoveOrphanedShape}
|
|
482
|
+
<button
|
|
483
|
+
class="remove-orphan-btn"
|
|
484
|
+
type="button"
|
|
485
|
+
onclick={() => onRemoveOrphanedShape?.()}
|
|
486
|
+
>
|
|
487
|
+
Remove from canvas
|
|
488
|
+
</button>
|
|
489
|
+
{/if}
|
|
490
|
+
{:else}
|
|
491
|
+
<span class="prop-label">Widget Linked</span>
|
|
492
|
+
<div class="widget-linked-section">
|
|
493
|
+
<input
|
|
494
|
+
class="widget-rename-input"
|
|
495
|
+
type="text"
|
|
496
|
+
value={resolvedWidgetName || ''}
|
|
497
|
+
placeholder="Widget name"
|
|
498
|
+
onfocus={(e) => { widgetRenameValue = e.currentTarget.value; }}
|
|
499
|
+
onblur={(e) => {
|
|
500
|
+
const val = e.currentTarget.value.trim();
|
|
501
|
+
if (val && val !== resolvedWidgetName && onWidgetRename && widgetId) {
|
|
502
|
+
onWidgetRename(widgetId, val);
|
|
503
|
+
}
|
|
504
|
+
}}
|
|
505
|
+
onkeydown={(e) => {
|
|
506
|
+
if (e.key === 'Enter') { e.currentTarget.blur(); }
|
|
507
|
+
if (e.key === 'Escape') { e.currentTarget.value = resolvedWidgetName || ''; e.currentTarget.blur(); }
|
|
508
|
+
}}
|
|
509
|
+
/>
|
|
510
|
+
</div>
|
|
511
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
512
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
513
|
+
<span
|
|
514
|
+
class="widget-id-label"
|
|
515
|
+
class:widget-id-clickable={!!onWidgetClick}
|
|
516
|
+
onclick={() => onWidgetClick && widgetId && onWidgetClick(widgetId)}
|
|
517
|
+
title={onWidgetClick ? 'Click to open Widget Details' : widgetId}
|
|
518
|
+
>
|
|
519
|
+
{#if onWidgetClick}
|
|
520
|
+
<svg class="widget-id-link-icon" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
521
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
522
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
523
|
+
</svg>
|
|
524
|
+
{/if}
|
|
525
|
+
{widgetId}
|
|
526
|
+
</span>
|
|
527
|
+
{#if onRefreshScreenshot && widgetId}
|
|
528
|
+
<button
|
|
529
|
+
class="refresh-screenshot-btn"
|
|
530
|
+
title="Refresh widget preview"
|
|
531
|
+
onclick={() => onRefreshScreenshot && widgetId && onRefreshScreenshot(widgetId)}
|
|
532
|
+
>
|
|
533
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
534
|
+
<polyline points="23 4 23 10 17 10"/>
|
|
535
|
+
<polyline points="1 20 1 14 7 14"/>
|
|
536
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
537
|
+
</svg>
|
|
538
|
+
Refresh Widget Preview
|
|
539
|
+
</button>
|
|
540
|
+
{/if}
|
|
541
|
+
{/if}
|
|
542
|
+
{:else}
|
|
543
|
+
<span class="prop-label">Convert to Widget</span>
|
|
544
|
+
<div class="capture-section">
|
|
545
|
+
<button class="convert-widget-btn" onclick={onConvertToWidget}>
|
|
546
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
547
|
+
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/>
|
|
548
|
+
<line x1="7" y1="2" x2="7" y2="22"/>
|
|
549
|
+
<line x1="17" y1="2" x2="17" y2="22"/>
|
|
550
|
+
<line x1="2" y1="12" x2="22" y2="12"/>
|
|
551
|
+
</svg>
|
|
552
|
+
CONVERT TO WIDGET
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
{/if}
|
|
556
|
+
</div>
|
|
557
|
+
{/if}
|
|
558
|
+
{#if selectedObject && !isMinimized}
|
|
559
|
+
<div class="prop-section layer-order-section">
|
|
560
|
+
<span class="prop-label">Layer Order</span>
|
|
561
|
+
<div class="layer-order-buttons">
|
|
562
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
563
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
564
|
+
<button class="layer-btn" onclick={onBringToFront} title="Bring to Front">
|
|
565
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
566
|
+
<polyline points="17 11 12 6 7 11"/>
|
|
567
|
+
<polyline points="17 18 12 13 7 18"/>
|
|
568
|
+
</svg>
|
|
569
|
+
<span class="layer-btn-label">Front</span>
|
|
570
|
+
</button>
|
|
571
|
+
<button class="layer-btn" onclick={onBringForward} title="Bring Forward">
|
|
572
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
573
|
+
<polyline points="18 15 12 9 6 15"/>
|
|
574
|
+
</svg>
|
|
575
|
+
<span class="layer-btn-label">Up</span>
|
|
576
|
+
</button>
|
|
577
|
+
<button class="layer-btn" onclick={onSendBackward} title="Send Backward">
|
|
578
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
579
|
+
<polyline points="6 9 12 15 18 9"/>
|
|
580
|
+
</svg>
|
|
581
|
+
<span class="layer-btn-label">Down</span>
|
|
582
|
+
</button>
|
|
583
|
+
<button class="layer-btn" onclick={onSendToBack} title="Send to Back">
|
|
584
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
585
|
+
<polyline points="7 13 12 18 17 13"/>
|
|
586
|
+
<polyline points="7 6 12 11 17 6"/>
|
|
587
|
+
</svg>
|
|
588
|
+
<span class="layer-btn-label">Back</span>
|
|
589
|
+
</button>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
{/if}
|
|
593
|
+
|
|
594
|
+
<!-- Alignment: visible in both minimized and expanded modes -->
|
|
595
|
+
{#if selectedObject}
|
|
596
|
+
<div class="prop-section align-section">
|
|
597
|
+
<span class="prop-label">Align</span>
|
|
598
|
+
<div class="align-buttons">
|
|
599
|
+
<button class="align-btn" onclick={onAlignLeft} title="Align Left">
|
|
600
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
601
|
+
<rect x="3" y="4" width="2" height="16" rx="1"/>
|
|
602
|
+
<rect x="7" y="7" width="10" height="4" rx="1"/>
|
|
603
|
+
<rect x="7" y="13" width="14" height="4" rx="1"/>
|
|
604
|
+
</svg>
|
|
605
|
+
</button>
|
|
606
|
+
<button class="align-btn" onclick={onAlignCenterH} title="Align Center (horizontal)">
|
|
607
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
608
|
+
<rect x="11" y="3" width="2" height="18" rx="1"/>
|
|
609
|
+
<rect x="5" y="7" width="14" height="4" rx="1"/>
|
|
610
|
+
<rect x="8" y="13" width="8" height="4" rx="1"/>
|
|
611
|
+
</svg>
|
|
612
|
+
</button>
|
|
613
|
+
<button class="align-btn" onclick={onAlignRight} title="Align Right">
|
|
614
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
615
|
+
<rect x="19" y="4" width="2" height="16" rx="1"/>
|
|
616
|
+
<rect x="7" y="7" width="10" height="4" rx="1"/>
|
|
617
|
+
<rect x="3" y="13" width="14" height="4" rx="1"/>
|
|
618
|
+
</svg>
|
|
619
|
+
</button>
|
|
620
|
+
<button class="align-btn" onclick={onAlignTop} title="Align Top">
|
|
621
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
622
|
+
<rect x="4" y="3" width="16" height="2" rx="1"/>
|
|
623
|
+
<rect x="7" y="7" width="4" height="10" rx="1"/>
|
|
624
|
+
<rect x="13" y="7" width="4" height="14" rx="1"/>
|
|
625
|
+
</svg>
|
|
626
|
+
</button>
|
|
627
|
+
<button class="align-btn" onclick={onAlignMiddleV} title="Align Middle (vertical)">
|
|
628
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
629
|
+
<rect x="3" y="11" width="18" height="2" rx="1"/>
|
|
630
|
+
<rect x="7" y="5" width="4" height="14" rx="1"/>
|
|
631
|
+
<rect x="13" y="8" width="4" height="8" rx="1"/>
|
|
632
|
+
</svg>
|
|
633
|
+
</button>
|
|
634
|
+
<button class="align-btn" onclick={onAlignBottom} title="Align Bottom">
|
|
635
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
636
|
+
<rect x="4" y="19" width="16" height="2" rx="1"/>
|
|
637
|
+
<rect x="7" y="7" width="4" height="10" rx="1"/>
|
|
638
|
+
<rect x="13" y="3" width="4" height="14" rx="1"/>
|
|
639
|
+
</svg>
|
|
640
|
+
</button>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
{/if}
|
|
644
|
+
{/if}
|
|
645
|
+
|
|
646
|
+
<!-- ==================== IMAGE SOURCE (always visible for image shapes, even in reduced mode) ==================== -->
|
|
647
|
+
{#if !isWidgetImage && (isImagePlaceholder || hasLoadedImage)}
|
|
648
|
+
<div class="prop-section image-source-section">
|
|
649
|
+
<span class="section-title-inline">IMAGE SOURCE</span>
|
|
650
|
+
<!-- File source option -->
|
|
651
|
+
<div class="source-row">
|
|
652
|
+
<span class="source-label">File:</span>
|
|
653
|
+
{#if hasLoadedImage && imageFilename && !imageUrl}
|
|
654
|
+
<span class="source-value" title={imageFilename}>{imageFilename}</span>
|
|
655
|
+
{:else if isImagePlaceholder}
|
|
656
|
+
<span class="source-placeholder">No file selected</span>
|
|
657
|
+
{:else}
|
|
658
|
+
<span class="source-placeholder">-</span>
|
|
659
|
+
{/if}
|
|
660
|
+
</div>
|
|
661
|
+
<button class="source-btn" onclick={onImageSourceChange}>
|
|
662
|
+
{hasLoadedImage ? 'Change File' : 'Choose File'}
|
|
663
|
+
</button>
|
|
664
|
+
|
|
665
|
+
<!-- URL source option -->
|
|
666
|
+
<div class="source-row url-row">
|
|
667
|
+
<span class="source-label">URL:</span>
|
|
668
|
+
{#if hasLoadedImage && imageUrl}
|
|
669
|
+
<span class="source-value url" title={imageUrl}>{imageUrl}</span>
|
|
670
|
+
{:else}
|
|
671
|
+
<div class="url-input-row">
|
|
672
|
+
<input
|
|
673
|
+
type="text"
|
|
674
|
+
class="url-input"
|
|
675
|
+
placeholder="Paste URL..."
|
|
676
|
+
bind:value={imageUrlInput}
|
|
677
|
+
onkeydown={(e) => {
|
|
678
|
+
if (e.key === 'Enter' && imageUrlInput.trim()) {
|
|
679
|
+
onImageUrlLoad(imageUrlInput.trim());
|
|
680
|
+
imageUrlInput = '';
|
|
681
|
+
}
|
|
682
|
+
}}
|
|
683
|
+
/>
|
|
684
|
+
<button
|
|
685
|
+
class="url-load-btn"
|
|
686
|
+
onclick={() => {
|
|
687
|
+
if (imageUrlInput.trim()) {
|
|
688
|
+
onImageUrlLoad(imageUrlInput.trim());
|
|
689
|
+
imageUrlInput = '';
|
|
690
|
+
}
|
|
691
|
+
}}
|
|
692
|
+
>
|
|
693
|
+
Load
|
|
694
|
+
</button>
|
|
695
|
+
</div>
|
|
696
|
+
{/if}
|
|
697
|
+
</div>
|
|
698
|
+
{#if hasLoadedImage && imageUrl}
|
|
699
|
+
<div class="url-input-row">
|
|
700
|
+
<input
|
|
701
|
+
type="text"
|
|
702
|
+
class="url-input"
|
|
703
|
+
placeholder="New URL..."
|
|
704
|
+
bind:value={imageUrlInput}
|
|
705
|
+
onkeydown={(e) => {
|
|
706
|
+
if (e.key === 'Enter' && imageUrlInput.trim()) {
|
|
707
|
+
onImageUrlLoad(imageUrlInput.trim());
|
|
708
|
+
imageUrlInput = '';
|
|
709
|
+
}
|
|
710
|
+
}}
|
|
711
|
+
/>
|
|
712
|
+
<button
|
|
713
|
+
class="url-load-btn"
|
|
714
|
+
onclick={() => {
|
|
715
|
+
if (imageUrlInput.trim()) {
|
|
716
|
+
onImageUrlLoad(imageUrlInput.trim());
|
|
717
|
+
imageUrlInput = '';
|
|
718
|
+
}
|
|
719
|
+
}}
|
|
720
|
+
>
|
|
721
|
+
Load
|
|
722
|
+
</button>
|
|
723
|
+
</div>
|
|
724
|
+
{/if}
|
|
725
|
+
</div>
|
|
726
|
+
{/if}
|
|
727
|
+
|
|
728
|
+
<!-- ==================== GENERAL PROPERTIES (all objects) ==================== -->
|
|
729
|
+
<div class="section-divider">
|
|
730
|
+
<span class="section-title">General Properties</span>
|
|
731
|
+
</div>
|
|
732
|
+
|
|
733
|
+
<!-- Dimensions: width × height in real canvas pixels -->
|
|
734
|
+
<div class="prop-section dimensions-section">
|
|
735
|
+
<span class="prop-label dimensions-label">Dimensions</span>
|
|
736
|
+
<div class="dimensions-row">
|
|
737
|
+
<input
|
|
738
|
+
class="dimension-input"
|
|
739
|
+
type="number"
|
|
740
|
+
min="1"
|
|
741
|
+
value={displayWidth}
|
|
742
|
+
oninput={handleWidthInput}
|
|
743
|
+
aria-label="Width"
|
|
744
|
+
/>
|
|
745
|
+
<!-- Lock / unlock button between inputs -->
|
|
746
|
+
<Tooltip text={forcedLock ? 'Proportions locked (shape constraint)' : effectiveDimensionLock ? 'Unlock proportions' : 'Lock proportions'}>
|
|
747
|
+
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
748
|
+
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
749
|
+
<span
|
|
750
|
+
class="dimension-lock-bt {effectiveDimensionLock ? 'locked' : 'unlocked'} {forcedLock ? 'force-locked' : ''}"
|
|
751
|
+
onclick={toggleDimensionLock}
|
|
752
|
+
>
|
|
753
|
+
{#if effectiveDimensionLock}
|
|
754
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
755
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
|
756
|
+
</svg>
|
|
757
|
+
{:else}
|
|
758
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
759
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/>
|
|
760
|
+
</svg>
|
|
761
|
+
{/if}
|
|
762
|
+
</span>
|
|
763
|
+
</Tooltip>
|
|
764
|
+
<input
|
|
765
|
+
class="dimension-input"
|
|
766
|
+
type="number"
|
|
767
|
+
min="1"
|
|
768
|
+
value={displayHeight}
|
|
769
|
+
oninput={handleHeightInput}
|
|
770
|
+
aria-label="Height"
|
|
771
|
+
/>
|
|
772
|
+
<span class="dimension-unit">px</span>
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
|
|
776
|
+
<!-- Opacity: always visible, including wireframe mode.
|
|
777
|
+
Useful for hiding/showing objects without deleting them. -->
|
|
778
|
+
<div class="prop-section">
|
|
779
|
+
<SliderUnitIC
|
|
780
|
+
label="Opacity"
|
|
781
|
+
value={currentOpacity}
|
|
782
|
+
min={0}
|
|
783
|
+
max={100}
|
|
784
|
+
step={1}
|
|
785
|
+
unit="%"
|
|
786
|
+
onChange={onOpacityChange}
|
|
787
|
+
/>
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
<!-- Corner Roundness: in General Properties, visible even when minimized -->
|
|
791
|
+
{#if !isWidgetImage && (isRect || isTriangle || isPath || isImagePlaceholder || hasLoadedImage || isTextbox || isFrame || isMultiSelect || isLibraryShape)}
|
|
792
|
+
<SliderUnitIC
|
|
793
|
+
label="Corner Roundness"
|
|
794
|
+
value={isTriangle ? currentTriangleRoundness : currentRoundness}
|
|
795
|
+
min={0}
|
|
796
|
+
max={100}
|
|
797
|
+
step={1}
|
|
798
|
+
unit="%"
|
|
799
|
+
onChange={onRoundnessChange}
|
|
800
|
+
/>
|
|
801
|
+
{/if}
|
|
802
|
+
|
|
803
|
+
<!-- ==================== WIDGET BACKGROUND COLOR (visible behind transparent widget areas) ==================== -->
|
|
804
|
+
{#if isWidgetImage}
|
|
805
|
+
{#if wireframeMode || !isMinimized}
|
|
806
|
+
<div class="prop-section widget-bg-section">
|
|
807
|
+
<ColorIC
|
|
808
|
+
label="Background Color"
|
|
809
|
+
value={currentBackgroundColor === 'transparent' ? '#FFFFFF' : currentBackgroundColor}
|
|
810
|
+
onChange={onBackgroundColorChange}
|
|
811
|
+
showPicker={!wireframeMode}
|
|
812
|
+
presets={wireframeMode ? wireframeColorValues : null}
|
|
813
|
+
/>
|
|
814
|
+
</div>
|
|
815
|
+
{/if}
|
|
816
|
+
<SliderUnitIC
|
|
817
|
+
label="Background Opacity"
|
|
818
|
+
value={currentBackgroundOpacity}
|
|
819
|
+
min={0}
|
|
820
|
+
max={100}
|
|
821
|
+
step={1}
|
|
822
|
+
unit="%"
|
|
823
|
+
onChange={onBackgroundOpacityChange}
|
|
824
|
+
/>
|
|
825
|
+
{/if}
|
|
826
|
+
|
|
827
|
+
<!-- ==================== TEXTBOX BOX PROPERTIES (in General Properties) ==================== -->
|
|
828
|
+
{#if isTextbox}
|
|
829
|
+
{#if !wireframeMode}
|
|
830
|
+
<ColorIC
|
|
831
|
+
label="Fill Color"
|
|
832
|
+
value={textboxBoxFill === 'transparent' ? '#FFFFFF' : textboxBoxFill}
|
|
833
|
+
onChange={onTextboxBoxFillChange}
|
|
834
|
+
/>
|
|
835
|
+
{/if}
|
|
836
|
+
|
|
837
|
+
<SliderUnitIC
|
|
838
|
+
label="Fill Opacity"
|
|
839
|
+
value={textboxBoxFillOpacity}
|
|
840
|
+
min={0}
|
|
841
|
+
max={100}
|
|
842
|
+
step={1}
|
|
843
|
+
unit="%"
|
|
844
|
+
onChange={onFillOpacityChange}
|
|
845
|
+
/>
|
|
846
|
+
|
|
847
|
+
{#if !wireframeMode}
|
|
848
|
+
<ColorIC
|
|
849
|
+
label="Stroke Color"
|
|
850
|
+
value={textboxBoxStroke || '#000000'}
|
|
851
|
+
onChange={onTextboxBoxStrokeChange}
|
|
852
|
+
/>
|
|
853
|
+
{/if}
|
|
854
|
+
|
|
855
|
+
<SliderUnitIC
|
|
856
|
+
label="Stroke Width"
|
|
857
|
+
value={textboxBoxStrokeWidth}
|
|
858
|
+
min={0}
|
|
859
|
+
max={5}
|
|
860
|
+
step={0.5}
|
|
861
|
+
unit="px"
|
|
862
|
+
onChange={onTextboxBoxStrokeWidthChange}
|
|
863
|
+
/>
|
|
864
|
+
|
|
865
|
+
{#if !isMinimized}
|
|
866
|
+
<div class="prop-section">
|
|
867
|
+
<span class="prop-label">Stroke Type</span>
|
|
868
|
+
<div class="stroke-type-buttons">
|
|
869
|
+
<button
|
|
870
|
+
class="stroke-type-btn {textboxBoxStrokeType === 'solid' ? 'active' : ''}"
|
|
871
|
+
onclick={() => onTextboxBoxStrokeTypeChange('solid')}
|
|
872
|
+
>
|
|
873
|
+
<span class="stroke-preview solid"></span>
|
|
874
|
+
Solid
|
|
875
|
+
</button>
|
|
876
|
+
<button
|
|
877
|
+
class="stroke-type-btn {textboxBoxStrokeType === 'dashed' ? 'active' : ''}"
|
|
878
|
+
onclick={() => onTextboxBoxStrokeTypeChange('dashed')}
|
|
879
|
+
>
|
|
880
|
+
<span class="stroke-preview dashed"></span>
|
|
881
|
+
Dashed
|
|
882
|
+
</button>
|
|
883
|
+
<button
|
|
884
|
+
class="stroke-type-btn {textboxBoxStrokeType === 'dotted' ? 'active' : ''}"
|
|
885
|
+
onclick={() => onTextboxBoxStrokeTypeChange('dotted')}
|
|
886
|
+
>
|
|
887
|
+
<span class="stroke-preview dotted"></span>
|
|
888
|
+
Dotted
|
|
889
|
+
</button>
|
|
890
|
+
</div>
|
|
891
|
+
</div>
|
|
892
|
+
{/if}
|
|
893
|
+
{/if}
|
|
894
|
+
|
|
895
|
+
<!-- ==================== TEXT PROPERTIES ==================== -->
|
|
896
|
+
{#if isTextbox}
|
|
897
|
+
<div class="section-divider">
|
|
898
|
+
<span class="section-title">Text Properties</span>
|
|
899
|
+
</div>
|
|
900
|
+
|
|
901
|
+
<SliderUnitIC
|
|
902
|
+
label="Font Size"
|
|
903
|
+
value={currentFontSize}
|
|
904
|
+
min={8}
|
|
905
|
+
max={200}
|
|
906
|
+
step={1}
|
|
907
|
+
unit="px"
|
|
908
|
+
onChange={onFontSizeChange}
|
|
909
|
+
/>
|
|
910
|
+
|
|
911
|
+
<SliderUnitIC
|
|
912
|
+
label="Text Padding"
|
|
913
|
+
value={currentTextPadding}
|
|
914
|
+
min={0}
|
|
915
|
+
max={50}
|
|
916
|
+
step={1}
|
|
917
|
+
unit="px"
|
|
918
|
+
onChange={onTextPaddingChange}
|
|
919
|
+
/>
|
|
920
|
+
|
|
921
|
+
{#if !wireframeMode}
|
|
922
|
+
<ColorIC
|
|
923
|
+
label="Text Color"
|
|
924
|
+
value={currentFill || '#000000'}
|
|
925
|
+
onChange={onTextColorChange}
|
|
926
|
+
/>
|
|
927
|
+
|
|
928
|
+
<!-- Text Stroke Properties -->
|
|
929
|
+
<ColorIC
|
|
930
|
+
label="Text Stroke Color"
|
|
931
|
+
value={textStrokeColor || '#000000'}
|
|
932
|
+
onChange={onTextStrokeColorChange}
|
|
933
|
+
/>
|
|
934
|
+
{/if}
|
|
935
|
+
|
|
936
|
+
<SliderUnitIC
|
|
937
|
+
label="Text Stroke Width"
|
|
938
|
+
value={textStrokeWidth}
|
|
939
|
+
min={0}
|
|
940
|
+
max={5}
|
|
941
|
+
step={0.5}
|
|
942
|
+
unit="px"
|
|
943
|
+
onChange={onTextStrokeWidthChange}
|
|
944
|
+
/>
|
|
945
|
+
|
|
946
|
+
{#if !isMinimized}
|
|
947
|
+
<FontSelectorIC
|
|
948
|
+
value={currentFontFamily}
|
|
949
|
+
onChange={onFontFamilyChange}
|
|
950
|
+
/>
|
|
951
|
+
|
|
952
|
+
<TextStyleIC
|
|
953
|
+
fontWeight={currentFontWeight}
|
|
954
|
+
fontStyle={currentFontStyle}
|
|
955
|
+
underline={currentUnderline}
|
|
956
|
+
onFontWeightChange={onFontWeightChange}
|
|
957
|
+
onFontStyleChange={onFontStyleChange}
|
|
958
|
+
onUnderlineChange={onUnderlineChange}
|
|
959
|
+
/>
|
|
960
|
+
|
|
961
|
+
<TextAlignIC
|
|
962
|
+
value={currentTextAlign}
|
|
963
|
+
onChange={onTextAlignChange}
|
|
964
|
+
/>
|
|
965
|
+
{/if}
|
|
966
|
+
{/if}
|
|
967
|
+
|
|
968
|
+
<!-- ==================== FRAME: Text / Description (always visible for frames) ==================== -->
|
|
969
|
+
{#if isFrame}
|
|
970
|
+
<div class="section-divider">
|
|
971
|
+
<span class="section-title">Label Properties</span>
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
<div class="prop-section">
|
|
975
|
+
<span class="prop-label">Text/Description</span>
|
|
976
|
+
<input
|
|
977
|
+
type="text"
|
|
978
|
+
value={currentFrameLabel}
|
|
979
|
+
oninput={handleFrameLabelInput}
|
|
980
|
+
class="text-input"
|
|
981
|
+
placeholder="Describe this frame (used as widget description & prompt context)"
|
|
982
|
+
/>
|
|
983
|
+
</div>
|
|
984
|
+
{/if}
|
|
985
|
+
|
|
986
|
+
<!-- ==================== FRAME: Label styling (expanded only) ==================== -->
|
|
987
|
+
{#if !isMinimized && isFrame}
|
|
988
|
+
<SliderUnitIC
|
|
989
|
+
label="Font Size"
|
|
990
|
+
value={frameLabelFontSize}
|
|
991
|
+
min={8}
|
|
992
|
+
max={72}
|
|
993
|
+
step={1}
|
|
994
|
+
unit="px"
|
|
995
|
+
onChange={onFrameLabelFontSizeChange}
|
|
996
|
+
/>
|
|
997
|
+
|
|
998
|
+
<FontSelectorIC
|
|
999
|
+
value={frameLabelFontFamily}
|
|
1000
|
+
onChange={onFrameLabelFontFamilyChange}
|
|
1001
|
+
/>
|
|
1002
|
+
|
|
1003
|
+
<TextStyleIC
|
|
1004
|
+
fontWeight={frameLabelFontWeight}
|
|
1005
|
+
fontStyle={frameLabelFontStyle}
|
|
1006
|
+
underline={frameLabelUnderline}
|
|
1007
|
+
onFontWeightChange={onFrameLabelFontWeightChange}
|
|
1008
|
+
onFontStyleChange={onFrameLabelFontStyleChange}
|
|
1009
|
+
onUnderlineChange={onFrameLabelUnderlineChange}
|
|
1010
|
+
/>
|
|
1011
|
+
|
|
1012
|
+
<TextAlignIC
|
|
1013
|
+
value={frameLabelTextAlign}
|
|
1014
|
+
onChange={onFrameLabelTextAlignChange}
|
|
1015
|
+
/>
|
|
1016
|
+
|
|
1017
|
+
{#if !wireframeMode}
|
|
1018
|
+
<ColorIC
|
|
1019
|
+
label="Text Color"
|
|
1020
|
+
value={frameLabelTextColor}
|
|
1021
|
+
onChange={onFrameLabelTextColorChange}
|
|
1022
|
+
/>
|
|
1023
|
+
|
|
1024
|
+
<ColorIC
|
|
1025
|
+
label="Text Background"
|
|
1026
|
+
value={frameLabelBackgroundColor}
|
|
1027
|
+
onChange={onFrameLabelBackgroundColorChange}
|
|
1028
|
+
/>
|
|
1029
|
+
{/if}
|
|
1030
|
+
|
|
1031
|
+
<div class="section-divider">
|
|
1032
|
+
<span class="section-title">Box Properties</span>
|
|
1033
|
+
</div>
|
|
1034
|
+
|
|
1035
|
+
{/if}
|
|
1036
|
+
|
|
1037
|
+
<!-- ==================== FILL COLOR (shapes only, not path/text/arrow/image, or multi-select) ==================== -->
|
|
1038
|
+
{#if !isWidgetImage && ((hasFill && !isTextbox && !isImage) || isMultiSelect)}
|
|
1039
|
+
{#if wireframeMode || !isMinimized}
|
|
1040
|
+
<div class="prop-section">
|
|
1041
|
+
<ColorIC
|
|
1042
|
+
label="Fill Color"
|
|
1043
|
+
value={currentFill || '#FFFFFF'}
|
|
1044
|
+
onChange={onFillChange}
|
|
1045
|
+
showPicker={!wireframeMode}
|
|
1046
|
+
presets={wireframeMode ? wireframeColorValues : null}
|
|
1047
|
+
/>
|
|
1048
|
+
</div>
|
|
1049
|
+
{/if}
|
|
1050
|
+
|
|
1051
|
+
<!-- Fill Opacity stays visible even when minimized -->
|
|
1052
|
+
<SliderUnitIC
|
|
1053
|
+
label="Fill Opacity"
|
|
1054
|
+
value={currentFillOpacity}
|
|
1055
|
+
min={0}
|
|
1056
|
+
max={100}
|
|
1057
|
+
step={1}
|
|
1058
|
+
unit="%"
|
|
1059
|
+
onChange={onFillOpacityChange}
|
|
1060
|
+
/>
|
|
1061
|
+
{/if}
|
|
1062
|
+
|
|
1063
|
+
<!-- ==================== STROKE PROPERTIES ==================== -->
|
|
1064
|
+
{#if !isWidgetImage && ((hasStroke && !isTextbox) || isMultiSelect)}
|
|
1065
|
+
{#if wireframeMode || !isMinimized}
|
|
1066
|
+
<div class="prop-section">
|
|
1067
|
+
<ColorIC
|
|
1068
|
+
label="Stroke Color"
|
|
1069
|
+
value={currentStroke || '#000000'}
|
|
1070
|
+
onChange={onStrokeChange}
|
|
1071
|
+
showPicker={!wireframeMode}
|
|
1072
|
+
presets={wireframeMode ? wireframeColorValues : null}
|
|
1073
|
+
/>
|
|
1074
|
+
</div>
|
|
1075
|
+
{/if}
|
|
1076
|
+
|
|
1077
|
+
<!-- Stroke Width stays visible even when minimized -->
|
|
1078
|
+
<SliderUnitIC
|
|
1079
|
+
label="Stroke Width"
|
|
1080
|
+
value={currentStrokeWidth}
|
|
1081
|
+
min={0}
|
|
1082
|
+
max={20}
|
|
1083
|
+
step={1}
|
|
1084
|
+
unit="px"
|
|
1085
|
+
onChange={onStrokeWidthChange}
|
|
1086
|
+
/>
|
|
1087
|
+
|
|
1088
|
+
{#if !isMinimized}
|
|
1089
|
+
<div class="prop-section">
|
|
1090
|
+
<span class="prop-label">Stroke Type</span>
|
|
1091
|
+
<div class="stroke-type-buttons">
|
|
1092
|
+
<button
|
|
1093
|
+
class="stroke-type-btn {currentStrokeType === 'solid' ? 'active' : ''}"
|
|
1094
|
+
onclick={() => onStrokeTypeChange('solid')}
|
|
1095
|
+
>
|
|
1096
|
+
<span class="stroke-preview solid"></span>
|
|
1097
|
+
Solid
|
|
1098
|
+
</button>
|
|
1099
|
+
<button
|
|
1100
|
+
class="stroke-type-btn {currentStrokeType === 'dashed' ? 'active' : ''}"
|
|
1101
|
+
onclick={() => onStrokeTypeChange('dashed')}
|
|
1102
|
+
>
|
|
1103
|
+
<span class="stroke-preview dashed"></span>
|
|
1104
|
+
Dashed
|
|
1105
|
+
</button>
|
|
1106
|
+
<button
|
|
1107
|
+
class="stroke-type-btn {currentStrokeType === 'dotted' ? 'active' : ''}"
|
|
1108
|
+
onclick={() => onStrokeTypeChange('dotted')}
|
|
1109
|
+
>
|
|
1110
|
+
<span class="stroke-preview dotted"></span>
|
|
1111
|
+
Dotted
|
|
1112
|
+
</button>
|
|
1113
|
+
</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
{/if}
|
|
1116
|
+
{/if}
|
|
1117
|
+
|
|
1118
|
+
<!-- ==================== TEXT PROPERTIES (for selection/group containing textboxes or frames) ==================== -->
|
|
1119
|
+
{#if !isMinimized && hasTextboxInSelection}
|
|
1120
|
+
<div class="section-divider">
|
|
1121
|
+
<span class="section-title">Text Properties</span>
|
|
1122
|
+
</div>
|
|
1123
|
+
|
|
1124
|
+
<SliderUnitIC
|
|
1125
|
+
label="Font Size"
|
|
1126
|
+
value={currentFontSize}
|
|
1127
|
+
min={8}
|
|
1128
|
+
max={200}
|
|
1129
|
+
step={1}
|
|
1130
|
+
unit="px"
|
|
1131
|
+
onChange={onFontSizeChange}
|
|
1132
|
+
/>
|
|
1133
|
+
|
|
1134
|
+
<FontSelectorIC
|
|
1135
|
+
value={currentFontFamily}
|
|
1136
|
+
onChange={onFontFamilyChange}
|
|
1137
|
+
/>
|
|
1138
|
+
|
|
1139
|
+
<TextStyleIC
|
|
1140
|
+
fontWeight={currentFontWeight}
|
|
1141
|
+
fontStyle={currentFontStyle}
|
|
1142
|
+
underline={currentUnderline}
|
|
1143
|
+
onFontWeightChange={onFontWeightChange}
|
|
1144
|
+
onFontStyleChange={onFontStyleChange}
|
|
1145
|
+
onUnderlineChange={onUnderlineChange}
|
|
1146
|
+
/>
|
|
1147
|
+
|
|
1148
|
+
<TextAlignIC
|
|
1149
|
+
value={currentTextAlign}
|
|
1150
|
+
onChange={onTextAlignChange}
|
|
1151
|
+
/>
|
|
1152
|
+
|
|
1153
|
+
<SliderUnitIC
|
|
1154
|
+
label="Text Padding"
|
|
1155
|
+
value={currentTextPadding}
|
|
1156
|
+
min={0}
|
|
1157
|
+
max={50}
|
|
1158
|
+
step={1}
|
|
1159
|
+
unit="px"
|
|
1160
|
+
onChange={onTextPaddingChange}
|
|
1161
|
+
/>
|
|
1162
|
+
|
|
1163
|
+
<ColorIC
|
|
1164
|
+
label="Text Color"
|
|
1165
|
+
value={currentFill || '#000000'}
|
|
1166
|
+
onChange={onTextColorChange}
|
|
1167
|
+
/>
|
|
1168
|
+
|
|
1169
|
+
<!-- Text Stroke Properties for Selection/Group -->
|
|
1170
|
+
<ColorIC
|
|
1171
|
+
label="Text Stroke Color"
|
|
1172
|
+
value={textStrokeColor || '#000000'}
|
|
1173
|
+
onChange={onTextStrokeColorChange}
|
|
1174
|
+
/>
|
|
1175
|
+
|
|
1176
|
+
<SliderUnitIC
|
|
1177
|
+
label="Text Stroke Width"
|
|
1178
|
+
value={textStrokeWidth}
|
|
1179
|
+
min={0}
|
|
1180
|
+
max={5}
|
|
1181
|
+
step={0.5}
|
|
1182
|
+
unit="px"
|
|
1183
|
+
onChange={onTextStrokeWidthChange}
|
|
1184
|
+
/>
|
|
1185
|
+
{/if}
|
|
1186
|
+
</div><!-- end props-panel-scrollable-content -->
|
|
1187
|
+
</div><!-- end props-panel-scrollable -->
|
|
1188
|
+
</div>
|
|
1189
|
+
|
|
1190
|
+
<style>
|
|
1191
|
+
.props-panel-el {
|
|
1192
|
+
--accent: #314F2F;
|
|
1193
|
+
--accent-hover: #1f3520;
|
|
1194
|
+
--accent-light: rgba(49, 79, 47, 0.08);
|
|
1195
|
+
width: 248px;
|
|
1196
|
+
max-height: calc(100% - 80px);
|
|
1197
|
+
overflow: hidden;
|
|
1198
|
+
display: flex;
|
|
1199
|
+
flex-direction: column;
|
|
1200
|
+
border: 2px solid #e5e7eb;
|
|
1201
|
+
position: absolute;
|
|
1202
|
+
top: 8px;
|
|
1203
|
+
right: 8px;
|
|
1204
|
+
background: white;
|
|
1205
|
+
border-radius: 8px;
|
|
1206
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/* Sticky header: stays at top, never scrolls */
|
|
1210
|
+
.props-panel-sticky-header {
|
|
1211
|
+
flex-shrink: 0;
|
|
1212
|
+
display: flex;
|
|
1213
|
+
flex-direction: column;
|
|
1214
|
+
gap: 0;
|
|
1215
|
+
padding-bottom: 0;
|
|
1216
|
+
border-bottom: 2px solid #e5e7eb;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/* Scrollable content area for all property sections.
|
|
1220
|
+
margin-right is set dynamically via style= when content overflows,
|
|
1221
|
+
pulling the scrollbar over the parent's right padding zone. */
|
|
1222
|
+
.props-panel-scrollable {
|
|
1223
|
+
flex: 1;
|
|
1224
|
+
overflow-y: auto;
|
|
1225
|
+
overflow-x: hidden;
|
|
1226
|
+
display: flex;
|
|
1227
|
+
flex-direction: column;
|
|
1228
|
+
gap: 12px;
|
|
1229
|
+
padding: 0;
|
|
1230
|
+
scrollbar-width: thin;
|
|
1231
|
+
scrollbar-color: rgba(0,0,0,0.15) transparent;
|
|
1232
|
+
}
|
|
1233
|
+
.props-panel-scrollable::-webkit-scrollbar {
|
|
1234
|
+
width: 4px;
|
|
1235
|
+
}
|
|
1236
|
+
.props-panel-scrollable::-webkit-scrollbar-track {
|
|
1237
|
+
background: transparent;
|
|
1238
|
+
}
|
|
1239
|
+
.props-panel-scrollable::-webkit-scrollbar-thumb {
|
|
1240
|
+
background: rgba(0,0,0,0.15);
|
|
1241
|
+
border-radius: 4px;
|
|
1242
|
+
}
|
|
1243
|
+
.props-panel-scrollable::-webkit-scrollbar-thumb:hover {
|
|
1244
|
+
background: rgba(0,0,0,0.25);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
.header {
|
|
1248
|
+
display: flex;
|
|
1249
|
+
gap: 4px;
|
|
1250
|
+
padding-bottom: 8px;
|
|
1251
|
+
align-items: baseline;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.minimize-toggle-bt {
|
|
1255
|
+
margin-left: auto;
|
|
1256
|
+
display: flex;
|
|
1257
|
+
align-items: center;
|
|
1258
|
+
justify-content: center;
|
|
1259
|
+
width: 22px;
|
|
1260
|
+
height: 22px;
|
|
1261
|
+
background: transparent;
|
|
1262
|
+
border: 1px solid transparent;
|
|
1263
|
+
border-radius: 4px;
|
|
1264
|
+
cursor: pointer;
|
|
1265
|
+
color: #6b7280;
|
|
1266
|
+
padding: 0;
|
|
1267
|
+
flex-shrink: 0;
|
|
1268
|
+
transition: all 0.15s ease;
|
|
1269
|
+
align-self: center;
|
|
1270
|
+
}
|
|
1271
|
+
.minimize-toggle-bt:hover {
|
|
1272
|
+
background: #f3f4f6;
|
|
1273
|
+
border-color: #d1d5db;
|
|
1274
|
+
color: #374151;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.header-label {
|
|
1278
|
+
font-size: 12px;
|
|
1279
|
+
font-weight: 700;
|
|
1280
|
+
color: #6b7280;
|
|
1281
|
+
letter-spacing: 0.5px;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.header-type {
|
|
1285
|
+
font-size: 13px;
|
|
1286
|
+
font-weight: 600;
|
|
1287
|
+
color: #111827;
|
|
1288
|
+
white-space: nowrap;
|
|
1289
|
+
overflow: hidden;
|
|
1290
|
+
text-overflow: ellipsis;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.children-count-row {
|
|
1294
|
+
display: flex;
|
|
1295
|
+
align-items: center;
|
|
1296
|
+
gap: 6px;
|
|
1297
|
+
padding: 6px 0 8px 0;
|
|
1298
|
+
border-top: 1px solid #e5e7eb;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.children-icon {
|
|
1302
|
+
color: #6b7280;
|
|
1303
|
+
flex-shrink: 0;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
.capture-section {
|
|
1307
|
+
display: flex;
|
|
1308
|
+
justify-content: center;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.layer-order-section {
|
|
1312
|
+
padding: 4px 8px;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
.layer-order-buttons {
|
|
1316
|
+
display: flex;
|
|
1317
|
+
gap: 4px;
|
|
1318
|
+
justify-content: center;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
.layer-btn {
|
|
1322
|
+
display: flex;
|
|
1323
|
+
flex-direction: column;
|
|
1324
|
+
align-items: center;
|
|
1325
|
+
gap: 2px;
|
|
1326
|
+
padding: 6px 10px;
|
|
1327
|
+
background: #f3f4f6;
|
|
1328
|
+
border: 1px solid #d1d5db;
|
|
1329
|
+
border-radius: 6px;
|
|
1330
|
+
cursor: pointer;
|
|
1331
|
+
color: #374151;
|
|
1332
|
+
font-size: 10px;
|
|
1333
|
+
line-height: 1;
|
|
1334
|
+
transition: all 0.15s ease;
|
|
1335
|
+
flex: 1;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.layer-btn:hover {
|
|
1339
|
+
background: #e5e7eb;
|
|
1340
|
+
border-color: #9ca3af;
|
|
1341
|
+
color: #111827;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.layer-btn:active {
|
|
1345
|
+
background: #d1d5db;
|
|
1346
|
+
transform: scale(0.95);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.layer-btn-label {
|
|
1350
|
+
font-size: 9px;
|
|
1351
|
+
font-weight: 500;
|
|
1352
|
+
text-transform: uppercase;
|
|
1353
|
+
letter-spacing: 0.3px;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/* Alignment buttons */
|
|
1357
|
+
.align-section {
|
|
1358
|
+
padding: 4px 8px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.align-buttons {
|
|
1362
|
+
display: flex;
|
|
1363
|
+
gap: 3px;
|
|
1364
|
+
justify-content: center;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.align-btn {
|
|
1368
|
+
display: flex;
|
|
1369
|
+
align-items: center;
|
|
1370
|
+
justify-content: center;
|
|
1371
|
+
width: 30px;
|
|
1372
|
+
height: 30px;
|
|
1373
|
+
background: #f3f4f6;
|
|
1374
|
+
border: 1px solid #d1d5db;
|
|
1375
|
+
border-radius: 6px;
|
|
1376
|
+
cursor: pointer;
|
|
1377
|
+
color: #374151;
|
|
1378
|
+
transition: all 0.15s ease;
|
|
1379
|
+
flex: 1;
|
|
1380
|
+
padding: 0;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.align-btn:hover {
|
|
1384
|
+
background: #e5e7eb;
|
|
1385
|
+
border-color: #9ca3af;
|
|
1386
|
+
color: #111827;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
.align-btn:active {
|
|
1390
|
+
background: #d1d5db;
|
|
1391
|
+
transform: scale(0.95);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.capture-btn {
|
|
1395
|
+
display: flex;
|
|
1396
|
+
align-items: center;
|
|
1397
|
+
gap: 6px;
|
|
1398
|
+
padding: 8px 16px;
|
|
1399
|
+
background: var(--accent);
|
|
1400
|
+
color: white;
|
|
1401
|
+
border: none;
|
|
1402
|
+
border-radius: 6px;
|
|
1403
|
+
font-size: 13px;
|
|
1404
|
+
font-weight: 600;
|
|
1405
|
+
cursor: pointer;
|
|
1406
|
+
transition: all 0.2s ease;
|
|
1407
|
+
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.capture-btn:hover {
|
|
1411
|
+
background: var(--accent-hover);
|
|
1412
|
+
box-shadow: 0 4px 8px rgba(49, 79, 47, 0.4);
|
|
1413
|
+
transform: translateY(-1px);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.capture-btn:active {
|
|
1417
|
+
transform: translateY(0);
|
|
1418
|
+
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.capture-btn svg {
|
|
1422
|
+
width: 16px;
|
|
1423
|
+
height: 16px;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.convert-widget-btn {
|
|
1427
|
+
display: flex;
|
|
1428
|
+
align-items: center;
|
|
1429
|
+
gap: 6px;
|
|
1430
|
+
padding: 8px 16px;
|
|
1431
|
+
background: var(--accent);
|
|
1432
|
+
color: white;
|
|
1433
|
+
border: none;
|
|
1434
|
+
border-radius: 6px;
|
|
1435
|
+
font-size: 13px;
|
|
1436
|
+
font-weight: 600;
|
|
1437
|
+
cursor: pointer;
|
|
1438
|
+
transition: all 0.2s ease;
|
|
1439
|
+
box-shadow: 0 2px 4px rgba(49, 79, 47, 0.3);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
.convert-widget-btn:hover {
|
|
1443
|
+
background: var(--accent-hover);
|
|
1444
|
+
box-shadow: 0 4px 8px rgba(49, 79, 47, 0.4);
|
|
1445
|
+
transform: translateY(-1px);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.convert-widget-btn:active {
|
|
1449
|
+
transform: translateY(0);
|
|
1450
|
+
box-shadow: 0 2px 4px rgba(49, 79, 47, 0.3);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.convert-widget-btn svg {
|
|
1454
|
+
width: 16px;
|
|
1455
|
+
height: 16px;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
.widget-linked-section {
|
|
1459
|
+
display: flex;
|
|
1460
|
+
align-items: center;
|
|
1461
|
+
gap: 4px;
|
|
1462
|
+
}
|
|
1463
|
+
.widget-linking-loader {
|
|
1464
|
+
display: flex;
|
|
1465
|
+
align-items: center;
|
|
1466
|
+
gap: 8px;
|
|
1467
|
+
padding: 6px 8px;
|
|
1468
|
+
border-radius: 6px;
|
|
1469
|
+
background: #f0fdf4;
|
|
1470
|
+
border: 1px solid #bbf7d0;
|
|
1471
|
+
}
|
|
1472
|
+
.widget-linking-spinner {
|
|
1473
|
+
width: 14px;
|
|
1474
|
+
height: 14px;
|
|
1475
|
+
border: 2px solid #bbf7d0;
|
|
1476
|
+
border-top-color: #166534;
|
|
1477
|
+
border-radius: 50%;
|
|
1478
|
+
animation: widget-spin 0.6s linear infinite;
|
|
1479
|
+
flex-shrink: 0;
|
|
1480
|
+
}
|
|
1481
|
+
@keyframes widget-spin {
|
|
1482
|
+
to { transform: rotate(360deg); }
|
|
1483
|
+
}
|
|
1484
|
+
.widget-linking-text {
|
|
1485
|
+
font-size: 11px;
|
|
1486
|
+
color: #166534;
|
|
1487
|
+
font-weight: 500;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
.widget-linked-badge {
|
|
1491
|
+
display: flex;
|
|
1492
|
+
align-items: center;
|
|
1493
|
+
gap: 6px;
|
|
1494
|
+
padding: 6px 12px;
|
|
1495
|
+
background: #f0fdf4;
|
|
1496
|
+
border: 1px solid #86efac;
|
|
1497
|
+
border-radius: 6px;
|
|
1498
|
+
font-size: 12px;
|
|
1499
|
+
font-weight: 500;
|
|
1500
|
+
color: #166534;
|
|
1501
|
+
overflow: hidden;
|
|
1502
|
+
text-overflow: ellipsis;
|
|
1503
|
+
white-space: nowrap;
|
|
1504
|
+
max-width: 100%;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.widget-linked-badge svg {
|
|
1508
|
+
flex-shrink: 0;
|
|
1509
|
+
width: 14px;
|
|
1510
|
+
height: 14px;
|
|
1511
|
+
stroke: #166534;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.widget-rename-input {
|
|
1515
|
+
width: 100%;
|
|
1516
|
+
padding: 5px 8px;
|
|
1517
|
+
border: 1px solid #d1d5db;
|
|
1518
|
+
border-radius: 6px;
|
|
1519
|
+
font-size: 12px;
|
|
1520
|
+
font-weight: 500;
|
|
1521
|
+
color: #166534;
|
|
1522
|
+
background: #f9fafb;
|
|
1523
|
+
outline: none;
|
|
1524
|
+
min-width: 0;
|
|
1525
|
+
}
|
|
1526
|
+
.widget-rename-input:focus {
|
|
1527
|
+
border-color: #86efac;
|
|
1528
|
+
background: #f0fdf4;
|
|
1529
|
+
box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.2);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.widget-id-label {
|
|
1533
|
+
display: flex;
|
|
1534
|
+
align-items: center;
|
|
1535
|
+
gap: 4px;
|
|
1536
|
+
font-size: 11px;
|
|
1537
|
+
color: #9ca3af;
|
|
1538
|
+
overflow: hidden;
|
|
1539
|
+
text-overflow: ellipsis;
|
|
1540
|
+
white-space: nowrap;
|
|
1541
|
+
margin-top: 2px;
|
|
1542
|
+
padding: 1px 2px;
|
|
1543
|
+
border-radius: 3px;
|
|
1544
|
+
transition: background-color 0.15s, color 0.15s;
|
|
1545
|
+
}
|
|
1546
|
+
.widget-id-clickable {
|
|
1547
|
+
cursor: pointer;
|
|
1548
|
+
color: #166534;
|
|
1549
|
+
}
|
|
1550
|
+
.widget-id-clickable:hover {
|
|
1551
|
+
color: #15803d;
|
|
1552
|
+
background-color: #dcfce7;
|
|
1553
|
+
}
|
|
1554
|
+
:global(.widget-id-link-icon) {
|
|
1555
|
+
flex-shrink: 0;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.refresh-screenshot-btn {
|
|
1559
|
+
display: flex;
|
|
1560
|
+
align-items: center;
|
|
1561
|
+
gap: 5px;
|
|
1562
|
+
margin-top: 4px;
|
|
1563
|
+
padding: 3px 8px;
|
|
1564
|
+
font-size: 11px;
|
|
1565
|
+
font-weight: 500;
|
|
1566
|
+
color: #6b7280;
|
|
1567
|
+
background: #f3f4f6;
|
|
1568
|
+
border: 1px solid #e5e7eb;
|
|
1569
|
+
border-radius: 4px;
|
|
1570
|
+
cursor: pointer;
|
|
1571
|
+
transition: all 0.15s;
|
|
1572
|
+
width: fit-content;
|
|
1573
|
+
}
|
|
1574
|
+
.refresh-screenshot-btn:hover {
|
|
1575
|
+
color: #166534;
|
|
1576
|
+
background: #dcfce7;
|
|
1577
|
+
border-color: #86efac;
|
|
1578
|
+
}
|
|
1579
|
+
.refresh-screenshot-btn:active {
|
|
1580
|
+
transform: scale(0.97);
|
|
1581
|
+
}
|
|
1582
|
+
.refresh-screenshot-btn svg {
|
|
1583
|
+
flex-shrink: 0;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
.widget-orphan-notice {
|
|
1587
|
+
font-size: 11px;
|
|
1588
|
+
line-height: 1.45;
|
|
1589
|
+
color: #b45309;
|
|
1590
|
+
background: #fffbeb;
|
|
1591
|
+
border: 1px solid #fcd34d;
|
|
1592
|
+
border-radius: 6px;
|
|
1593
|
+
padding: 8px;
|
|
1594
|
+
margin: 4px 0 8px;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
.remove-orphan-btn {
|
|
1598
|
+
display: flex;
|
|
1599
|
+
align-items: center;
|
|
1600
|
+
justify-content: center;
|
|
1601
|
+
width: 100%;
|
|
1602
|
+
margin-top: 8px;
|
|
1603
|
+
padding: 8px 10px;
|
|
1604
|
+
font-size: 11px;
|
|
1605
|
+
font-weight: 600;
|
|
1606
|
+
color: #991b1b;
|
|
1607
|
+
background: #fef2f2;
|
|
1608
|
+
border: 1px solid #fecaca;
|
|
1609
|
+
border-radius: 6px;
|
|
1610
|
+
cursor: pointer;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
.remove-orphan-btn:hover {
|
|
1614
|
+
background: #fee2e2;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
.section-divider {
|
|
1618
|
+
padding-top: 8px;
|
|
1619
|
+
border-top: 2px solid #d1d5db;
|
|
1620
|
+
margin-top: 4px;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
/* First section in scrollable area: no top border/padding */
|
|
1624
|
+
.section-divider.no-top-border {
|
|
1625
|
+
border-top: none;
|
|
1626
|
+
margin-top: 0;
|
|
1627
|
+
padding-top: 0;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
.section-title {
|
|
1631
|
+
font-size: 11px;
|
|
1632
|
+
font-weight: 700;
|
|
1633
|
+
color: #374151;
|
|
1634
|
+
text-transform: uppercase;
|
|
1635
|
+
letter-spacing: 0.5px;
|
|
1636
|
+
text-decoration: underline;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
.section-title-inline {
|
|
1640
|
+
font-size: 11px;
|
|
1641
|
+
font-weight: 700;
|
|
1642
|
+
color: #6b7280;
|
|
1643
|
+
text-transform: uppercase;
|
|
1644
|
+
letter-spacing: 0.5px;
|
|
1645
|
+
margin-bottom: 4px;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.prop-section {
|
|
1649
|
+
display: flex;
|
|
1650
|
+
flex-direction: column;
|
|
1651
|
+
gap: 6px;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
/* ── Dimensions row (width × lock × height px) ── */
|
|
1655
|
+
.dimensions-section {
|
|
1656
|
+
gap: 4px;
|
|
1657
|
+
}
|
|
1658
|
+
.dimensions-label {
|
|
1659
|
+
margin-bottom: 0;
|
|
1660
|
+
}
|
|
1661
|
+
.dimensions-row {
|
|
1662
|
+
display: flex;
|
|
1663
|
+
align-items: center;
|
|
1664
|
+
gap: 4px;
|
|
1665
|
+
}
|
|
1666
|
+
.dimension-input {
|
|
1667
|
+
width: 60px;
|
|
1668
|
+
padding: 4px 6px;
|
|
1669
|
+
border: 1px solid #d1d5db;
|
|
1670
|
+
border-radius: 6px;
|
|
1671
|
+
font-size: 12px;
|
|
1672
|
+
text-align: center;
|
|
1673
|
+
color: #374151;
|
|
1674
|
+
background: white;
|
|
1675
|
+
outline: none;
|
|
1676
|
+
-moz-appearance: textfield;
|
|
1677
|
+
}
|
|
1678
|
+
.dimension-input::-webkit-outer-spin-button,
|
|
1679
|
+
.dimension-input::-webkit-inner-spin-button {
|
|
1680
|
+
-webkit-appearance: none;
|
|
1681
|
+
margin: 0;
|
|
1682
|
+
}
|
|
1683
|
+
.dimension-input:focus {
|
|
1684
|
+
border-color: var(--accent);
|
|
1685
|
+
}
|
|
1686
|
+
.dimension-lock-bt {
|
|
1687
|
+
display: flex;
|
|
1688
|
+
align-items: center;
|
|
1689
|
+
justify-content: center;
|
|
1690
|
+
width: 24px;
|
|
1691
|
+
height: 24px;
|
|
1692
|
+
border-radius: 4px;
|
|
1693
|
+
cursor: pointer;
|
|
1694
|
+
color: #9ca3af;
|
|
1695
|
+
flex-shrink: 0;
|
|
1696
|
+
transition: color 0.15s, background 0.15s;
|
|
1697
|
+
}
|
|
1698
|
+
.dimension-lock-bt:hover {
|
|
1699
|
+
background: #f3f4f6;
|
|
1700
|
+
color: #374151;
|
|
1701
|
+
}
|
|
1702
|
+
.dimension-lock-bt.locked {
|
|
1703
|
+
color: var(--accent);
|
|
1704
|
+
}
|
|
1705
|
+
.dimension-lock-bt.locked:hover {
|
|
1706
|
+
color: var(--accent-hover);
|
|
1707
|
+
background: var(--accent-light);
|
|
1708
|
+
}
|
|
1709
|
+
.dimension-lock-bt.force-locked {
|
|
1710
|
+
opacity: 0.6;
|
|
1711
|
+
cursor: default;
|
|
1712
|
+
}
|
|
1713
|
+
.dimension-lock-bt.force-locked:hover {
|
|
1714
|
+
background: transparent;
|
|
1715
|
+
}
|
|
1716
|
+
.dimension-unit {
|
|
1717
|
+
font-size: 11px;
|
|
1718
|
+
color: #9ca3af;
|
|
1719
|
+
flex-shrink: 0;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
.prop-label {
|
|
1723
|
+
font-size: 11px;
|
|
1724
|
+
font-weight: 600;
|
|
1725
|
+
color: #6b7280;
|
|
1726
|
+
text-transform: uppercase;
|
|
1727
|
+
letter-spacing: 0.5px;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
.stroke-type-buttons {
|
|
1731
|
+
display: flex;
|
|
1732
|
+
flex-direction: column;
|
|
1733
|
+
gap: 4px;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
.stroke-type-btn {
|
|
1737
|
+
display: flex;
|
|
1738
|
+
align-items: center;
|
|
1739
|
+
gap: 8px;
|
|
1740
|
+
padding: 6px 8px;
|
|
1741
|
+
border: 1px solid #d1d5db;
|
|
1742
|
+
border-radius: 4px;
|
|
1743
|
+
background: white;
|
|
1744
|
+
font-size: 11px;
|
|
1745
|
+
cursor: pointer;
|
|
1746
|
+
transition: all 0.15s ease;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
.stroke-type-btn:hover {
|
|
1750
|
+
background: #f3f4f6;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
.stroke-type-btn.active {
|
|
1754
|
+
background: var(--accent, #314F2F);
|
|
1755
|
+
color: white;
|
|
1756
|
+
border-color: var(--accent, #314F2F);
|
|
1757
|
+
box-shadow: none;
|
|
1758
|
+
outline: none;
|
|
1759
|
+
}
|
|
1760
|
+
.stroke-type-btn:focus-visible {
|
|
1761
|
+
outline: none;
|
|
1762
|
+
box-shadow: 0 0 0 2px var(--accent, #314F2F);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
.stroke-preview {
|
|
1766
|
+
width: 24px;
|
|
1767
|
+
height: 2px;
|
|
1768
|
+
background: currentColor;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
.stroke-preview.dashed {
|
|
1772
|
+
background: repeating-linear-gradient(
|
|
1773
|
+
90deg,
|
|
1774
|
+
currentColor 0px,
|
|
1775
|
+
currentColor 6px,
|
|
1776
|
+
transparent 6px,
|
|
1777
|
+
transparent 10px
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
.stroke-preview.dotted {
|
|
1782
|
+
background: repeating-linear-gradient(
|
|
1783
|
+
90deg,
|
|
1784
|
+
currentColor 0px,
|
|
1785
|
+
currentColor 2px,
|
|
1786
|
+
transparent 2px,
|
|
1787
|
+
transparent 5px
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/* Text input */
|
|
1792
|
+
.text-input {
|
|
1793
|
+
width: 100%;
|
|
1794
|
+
padding: 6px 8px;
|
|
1795
|
+
border: 1px solid #d1d5db;
|
|
1796
|
+
border-radius: 4px;
|
|
1797
|
+
font-size: 12px;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
.text-input:focus {
|
|
1801
|
+
outline: none;
|
|
1802
|
+
border-color: #3b82f6;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/* Image Source Section */
|
|
1806
|
+
.image-source-section {
|
|
1807
|
+
gap: 8px;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
.source-row {
|
|
1811
|
+
display: flex;
|
|
1812
|
+
gap: 6px;
|
|
1813
|
+
align-items: center;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
.source-row.url-row {
|
|
1817
|
+
flex-wrap: wrap;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
.source-label {
|
|
1821
|
+
font-size: 11px;
|
|
1822
|
+
font-weight: 600;
|
|
1823
|
+
color: #6b7280;
|
|
1824
|
+
white-space: nowrap;
|
|
1825
|
+
min-width: 32px;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.source-value {
|
|
1829
|
+
font-size: 11px;
|
|
1830
|
+
color: #111827;
|
|
1831
|
+
overflow: hidden;
|
|
1832
|
+
text-overflow: ellipsis;
|
|
1833
|
+
white-space: nowrap;
|
|
1834
|
+
max-width: 130px;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
.source-value.url {
|
|
1838
|
+
color: #3b82f6;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
.source-placeholder {
|
|
1842
|
+
font-size: 11px;
|
|
1843
|
+
color: #9ca3af;
|
|
1844
|
+
font-style: italic;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.source-btn {
|
|
1848
|
+
width: 100%;
|
|
1849
|
+
padding: 6px 12px;
|
|
1850
|
+
background: #f3f4f6;
|
|
1851
|
+
color: #374151;
|
|
1852
|
+
border: 1px solid #d1d5db;
|
|
1853
|
+
border-radius: 4px;
|
|
1854
|
+
font-size: 11px;
|
|
1855
|
+
font-weight: 500;
|
|
1856
|
+
cursor: pointer;
|
|
1857
|
+
transition: all 0.15s ease;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.source-btn:hover {
|
|
1861
|
+
background: #e5e7eb;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/* URL input */
|
|
1865
|
+
.url-input-row {
|
|
1866
|
+
display: flex;
|
|
1867
|
+
gap: 6px;
|
|
1868
|
+
width: 100%;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
.url-input {
|
|
1872
|
+
flex: 1;
|
|
1873
|
+
padding: 6px 8px;
|
|
1874
|
+
border: 1px solid #d1d5db;
|
|
1875
|
+
border-radius: 4px;
|
|
1876
|
+
font-size: 11px;
|
|
1877
|
+
min-width: 0;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
.url-input:focus {
|
|
1881
|
+
outline: none;
|
|
1882
|
+
border-color: var(--accent);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
.url-load-btn {
|
|
1886
|
+
padding: 6px 12px;
|
|
1887
|
+
background: var(--accent);
|
|
1888
|
+
color: white;
|
|
1889
|
+
border: none;
|
|
1890
|
+
border-radius: 4px;
|
|
1891
|
+
font-size: 11px;
|
|
1892
|
+
font-weight: 500;
|
|
1893
|
+
cursor: pointer;
|
|
1894
|
+
transition: background 0.15s ease;
|
|
1895
|
+
white-space: nowrap;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
.url-load-btn:hover {
|
|
1899
|
+
background: var(--accent-hover);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
</style>
|