@widgetic/canvas 0.5.4 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/canvas/Canvas.svelte +854 -168
- package/dist/canvas/Canvas.svelte.d.ts +54 -2
- package/dist/canvas/CanvasToolbar.svelte +18 -6
- package/dist/canvas/CanvasToolbar.svelte.d.ts +1 -0
- package/dist/canvas/PanZoomPanel.svelte +90 -3
- package/dist/canvas/PanZoomPanel.svelte.d.ts +1 -0
- package/dist/canvas/props-panel/PropsPanel.svelte +148 -4
- package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +6 -0
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ declare const __propDef: {
|
|
|
22
22
|
}) => void) | null;
|
|
23
23
|
panZoomPosition?: "TL" | "TC" | "TR" | "BL" | "BC" | "BR";
|
|
24
24
|
wireframeMode?: boolean;
|
|
25
|
+
advancedFeatures?: boolean;
|
|
25
26
|
onReady?: (() => void) | null;
|
|
26
27
|
isExternalDataLoading?: boolean;
|
|
27
28
|
onBeforeUnmount?: ((json: string | null) => void) | null;
|
|
@@ -36,17 +37,40 @@ declare const __propDef: {
|
|
|
36
37
|
onWidgetShapeDeleting?: ((widgetId: string) => Promise<boolean>) | null;
|
|
37
38
|
onWidgetRename?: ((widgetId: string, newName: string) => void) | null;
|
|
38
39
|
onRefreshScreenshot?: ((widgetId: string) => void) | null;
|
|
40
|
+
/** Host persists canvases.primary_widget_id after Canvas paints the red stroke. */ onSetPrimaryWidget?: ((widgetId: string) => void) | null;
|
|
39
41
|
getWidgetName?: ((widgetId: string) => string | null) | null;
|
|
40
42
|
/** Returns true when widgetId still exists in the host widget list. */ isWidgetKnown?: ((widgetId: string) => boolean) | null;
|
|
41
43
|
showLogs?: boolean;
|
|
42
44
|
shapeFillColor?: () => string;
|
|
43
45
|
deselectAll?: () => void;
|
|
46
|
+
duplicateSelectedObjects?: () => Promise<void>;
|
|
47
|
+
deleteSelectedObjects?: () => Promise<void>;
|
|
44
48
|
markSelectedAsDuplicating?: () => void;
|
|
45
49
|
linkObjectToWidget?: (fabricObject: any, widgetId: string, widgetName?: string | null) => void;
|
|
46
50
|
linkFrameToWidget?: (objectId: string, widgetId: string) => void;
|
|
47
51
|
replaceObjectWithWidgetImage?: (objectId: string, widgetId: string, screenshotDataUrl: string) => Promise<string | null>;
|
|
48
52
|
updateWidgetImage?: (widgetId: string, newScreenshotDataUrl: string) => void;
|
|
49
53
|
selectWidgetShape?: (widgetId: string) => boolean;
|
|
54
|
+
getWidgetShapeScreenRect?: (widgetId: string) => {
|
|
55
|
+
left: number;
|
|
56
|
+
top: number;
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
viewportLeft: number;
|
|
60
|
+
viewportTop: number;
|
|
61
|
+
viewportWidth: number;
|
|
62
|
+
viewportHeight: number;
|
|
63
|
+
} | null;
|
|
64
|
+
getObjectScreenRect?: (objectId: string) => {
|
|
65
|
+
left: number;
|
|
66
|
+
top: number;
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
viewportLeft: number;
|
|
70
|
+
viewportTop: number;
|
|
71
|
+
viewportWidth: number;
|
|
72
|
+
viewportHeight: number;
|
|
73
|
+
} | null;
|
|
50
74
|
getWidgetShapeDataUrl?: (widgetId: string) => string | null;
|
|
51
75
|
addWidgetShape?: (widgetId: string, widgetName?: string, gridIndex?: number, totalCount?: number, previewImageUrl?: string | null) => string | null;
|
|
52
76
|
getLinkedWidgetIds?: () => Set<string>;
|
|
@@ -64,6 +88,8 @@ declare const __propDef: {
|
|
|
64
88
|
onSelectArrowTool?: () => void;
|
|
65
89
|
onSelectShapeTool?: (shapeType: "rect" | "circle" | "circle-true" | "triangle" | "arrow") => void;
|
|
66
90
|
onSelectFrameTool?: () => void;
|
|
91
|
+
setPrimaryWidgetShape?: (widgetId: string) => boolean;
|
|
92
|
+
getPrimaryWidgetId?: () => string | null;
|
|
67
93
|
onSelectImageTool?: () => void;
|
|
68
94
|
onSelectPanTool?: () => void;
|
|
69
95
|
zoomIn?: () => void;
|
|
@@ -71,10 +97,11 @@ declare const __propDef: {
|
|
|
71
97
|
zoomTo?: (percent: number) => void;
|
|
72
98
|
zoomReset?: () => void;
|
|
73
99
|
zoomFit?: () => void;
|
|
74
|
-
onGroupSelection?: () =>
|
|
100
|
+
onGroupSelection?: () => string | null;
|
|
75
101
|
onUngroupSelection?: () => void;
|
|
76
102
|
onSelectAllObjects?: () => void;
|
|
77
103
|
onDeselectAllObjects?: () => void;
|
|
104
|
+
saveNow?: () => void;
|
|
78
105
|
undo?: () => Promise<void>;
|
|
79
106
|
redo?: () => Promise<void>;
|
|
80
107
|
saveCanvas?: () => string | null;
|
|
@@ -98,12 +125,34 @@ export type CanvasSlots = typeof __propDef.slots;
|
|
|
98
125
|
export default class Canvas extends SvelteComponentTyped<CanvasProps, CanvasEvents, CanvasSlots> {
|
|
99
126
|
get shapeFillColor(): () => string;
|
|
100
127
|
get deselectAll(): () => void;
|
|
128
|
+
get duplicateSelectedObjects(): () => Promise<void>;
|
|
129
|
+
get deleteSelectedObjects(): () => Promise<void>;
|
|
101
130
|
get markSelectedAsDuplicating(): () => void;
|
|
102
131
|
get linkObjectToWidget(): (fabricObject: any, widgetId: string, widgetName?: string | null) => void;
|
|
103
132
|
get linkFrameToWidget(): (objectId: string, widgetId: string) => void;
|
|
104
133
|
get replaceObjectWithWidgetImage(): (objectId: string, widgetId: string, screenshotDataUrl: string) => Promise<string | null>;
|
|
105
134
|
get updateWidgetImage(): (widgetId: string, newScreenshotDataUrl: string) => void;
|
|
106
135
|
get selectWidgetShape(): (widgetId: string) => boolean;
|
|
136
|
+
get getWidgetShapeScreenRect(): (widgetId: string) => {
|
|
137
|
+
left: number;
|
|
138
|
+
top: number;
|
|
139
|
+
width: number;
|
|
140
|
+
height: number;
|
|
141
|
+
viewportLeft: number;
|
|
142
|
+
viewportTop: number;
|
|
143
|
+
viewportWidth: number;
|
|
144
|
+
viewportHeight: number;
|
|
145
|
+
} | null;
|
|
146
|
+
get getObjectScreenRect(): (objectId: string) => {
|
|
147
|
+
left: number;
|
|
148
|
+
top: number;
|
|
149
|
+
width: number;
|
|
150
|
+
height: number;
|
|
151
|
+
viewportLeft: number;
|
|
152
|
+
viewportTop: number;
|
|
153
|
+
viewportWidth: number;
|
|
154
|
+
viewportHeight: number;
|
|
155
|
+
} | null;
|
|
107
156
|
get getWidgetShapeDataUrl(): (widgetId: string) => string | null;
|
|
108
157
|
get addWidgetShape(): (widgetId: string, widgetName?: string, gridIndex?: number, totalCount?: number, previewImageUrl?: string | null) => string | null;
|
|
109
158
|
get getLinkedWidgetIds(): () => Set<string>;
|
|
@@ -121,6 +170,8 @@ export default class Canvas extends SvelteComponentTyped<CanvasProps, CanvasEven
|
|
|
121
170
|
get onSelectArrowTool(): () => void;
|
|
122
171
|
get onSelectShapeTool(): (shapeType: "rect" | "circle" | "circle-true" | "triangle" | "arrow") => void;
|
|
123
172
|
get onSelectFrameTool(): () => void;
|
|
173
|
+
get setPrimaryWidgetShape(): (widgetId: string) => boolean;
|
|
174
|
+
get getPrimaryWidgetId(): () => string | null;
|
|
124
175
|
get onSelectImageTool(): () => void;
|
|
125
176
|
get onSelectPanTool(): () => void;
|
|
126
177
|
get zoomIn(): () => void;
|
|
@@ -128,10 +179,11 @@ export default class Canvas extends SvelteComponentTyped<CanvasProps, CanvasEven
|
|
|
128
179
|
get zoomTo(): (percent: number) => void;
|
|
129
180
|
get zoomReset(): () => void;
|
|
130
181
|
get zoomFit(): () => void;
|
|
131
|
-
get onGroupSelection(): () =>
|
|
182
|
+
get onGroupSelection(): () => string | null;
|
|
132
183
|
get onUngroupSelection(): () => void;
|
|
133
184
|
get onSelectAllObjects(): () => void;
|
|
134
185
|
get onDeselectAllObjects(): () => void;
|
|
186
|
+
get saveNow(): () => void;
|
|
135
187
|
get undo(): () => Promise<void>;
|
|
136
188
|
get redo(): () => Promise<void>;
|
|
137
189
|
get saveCanvas(): () => string | null;
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
/** Draw-to-place: activates library shape tool so user clicks-and-drags to size it. */
|
|
64
64
|
export let onSelectLibraryShapeTool: (shapeKey: string, pathData: string) => void = () => {};
|
|
65
65
|
|
|
66
|
+
/** Extra tools (undo/redo/lock, frame, image, shape library). Code stays; UI is gated. */
|
|
67
|
+
export let advancedFeatures: boolean = false;
|
|
68
|
+
|
|
66
69
|
// Pre-group shapes for the dropdown
|
|
67
70
|
const libraryShapesByCategory = getShapesByCategory();
|
|
68
71
|
|
|
@@ -385,12 +388,12 @@
|
|
|
385
388
|
case 'D': event.preventDefault(); onSelectDrawTool(); break;
|
|
386
389
|
case 'H': event.preventDefault(); onSelectPanTool(); break;
|
|
387
390
|
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;
|
|
391
|
+
case 'R': if (advancedFeatures) { event.preventDefault(); selectShape('rect'); } break;
|
|
392
|
+
case 'G': if (advancedFeatures) { event.preventDefault(); selectShape('triangle'); } break;
|
|
393
|
+
case 'O': if (advancedFeatures) { event.preventDefault(); selectShape('circle'); } break;
|
|
394
|
+
case 'F': if (advancedFeatures) { event.preventDefault(); onSelectFrameTool(); } break;
|
|
392
395
|
case 'T': event.preventDefault(); onSelectTextTool(); break;
|
|
393
|
-
case 'I': event.preventDefault(); onSelectImageTool(); break;
|
|
396
|
+
case 'I': if (advancedFeatures) { event.preventDefault(); onSelectImageTool(); } break;
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
399
|
|
|
@@ -464,6 +467,7 @@
|
|
|
464
467
|
<!-- Scrollable container for tools — overflow-x-auto clips absolute children,
|
|
465
468
|
so dropdowns and tooltips are rendered as siblings OUTSIDE this div -->
|
|
466
469
|
<div class="tools-ct flex flex-row">
|
|
470
|
+
{#if advancedFeatures}
|
|
467
471
|
<!-- ── Utility cluster: Undo, Redo, Hand, Lock | Divider ── -->
|
|
468
472
|
<Tooltip text="Undo (Ctrl+Z)">
|
|
469
473
|
<div class="disabled-tooltip-wrap">
|
|
@@ -528,6 +532,7 @@
|
|
|
528
532
|
</Tooltip>
|
|
529
533
|
|
|
530
534
|
<div class="toolbar-divider"></div>
|
|
535
|
+
{/if}
|
|
531
536
|
|
|
532
537
|
<!-- ══ Illustrated tools: Pencil & Eraser — "pulled from sheath" on select ══ -->
|
|
533
538
|
<!-- Each tool is wrapped in a mask container that clips its body at rest.
|
|
@@ -623,6 +628,7 @@
|
|
|
623
628
|
</button>
|
|
624
629
|
</Tooltip>
|
|
625
630
|
|
|
631
|
+
{#if advancedFeatures}
|
|
626
632
|
<div class="toolbar-divider"></div>
|
|
627
633
|
|
|
628
634
|
<Tooltip text="Frame (F)">
|
|
@@ -678,6 +684,7 @@
|
|
|
678
684
|
</button>
|
|
679
685
|
</Tooltip>
|
|
680
686
|
</div>
|
|
687
|
+
{/if}
|
|
681
688
|
</div>
|
|
682
689
|
</div>
|
|
683
690
|
|
|
@@ -708,7 +715,7 @@
|
|
|
708
715
|
dropdownOpenAbove=true: appears above the trigger (toolbar at bottom).
|
|
709
716
|
dropdownOpenAbove=false: appears below (toolbar at top).
|
|
710
717
|
═══════════════════════════════════════════════════════════════ -->
|
|
711
|
-
{#if shapesDropdownOpen}
|
|
718
|
+
{#if shapesDropdownOpen && advancedFeatures}
|
|
712
719
|
<!-- position:absolute within .canvas-container (position:relative) so the dropdown
|
|
713
720
|
is correctly positioned regardless of any CSS transform on ancestors. -->
|
|
714
721
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
@@ -974,6 +981,11 @@ button:disabled {
|
|
|
974
981
|
color: white !important;
|
|
975
982
|
box-shadow: 0 2px 8px rgba(26, 58, 42, 0.25);
|
|
976
983
|
}
|
|
984
|
+
.wdg-tool-active :global(svg),
|
|
985
|
+
.wdg-tool-active :global(svg *) {
|
|
986
|
+
color: white !important;
|
|
987
|
+
stroke: currentColor !important;
|
|
988
|
+
}
|
|
977
989
|
.wdg-tool-active:hover {
|
|
978
990
|
background: #244b36 !important;
|
|
979
991
|
color: white !important;
|
|
@@ -32,6 +32,7 @@ declare const __propDef: {
|
|
|
32
32
|
onToggleStickyTool?: () => void;
|
|
33
33
|
/** Called when user clicks a library shape. Canvas inserts it as a Path. */ onInsertLibraryShape?: (shapeKey: string, pathData: string) => void;
|
|
34
34
|
/** Draw-to-place: activates library shape tool so user clicks-and-drags to size it. */ onSelectLibraryShapeTool?: (shapeKey: string, pathData: string) => void;
|
|
35
|
+
/** Extra tools (undo/redo/lock, frame, image, shape library). Code stays; UI is gated. */ advancedFeatures?: boolean;
|
|
35
36
|
addText?: () => void;
|
|
36
37
|
addRect?: () => void;
|
|
37
38
|
addTriangle?: () => void;
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
// Reflects open state so the button can show an active style
|
|
28
28
|
export let shortcutsOpen: boolean = false;
|
|
29
29
|
|
|
30
|
+
/** Extra view controls (grid, pan, 1:1, fit, shortcuts). Zoom ± / % and save stay available. */
|
|
31
|
+
export let advancedFeatures: boolean = false;
|
|
32
|
+
|
|
30
33
|
// Save button: visible when showSaveButton is true
|
|
31
34
|
export let showSaveButton: boolean = false;
|
|
32
35
|
export let hasUnsavedChanges: boolean = false;
|
|
@@ -56,9 +59,29 @@
|
|
|
56
59
|
(e.target as HTMLInputElement).blur();
|
|
57
60
|
}
|
|
58
61
|
}
|
|
62
|
+
|
|
63
|
+
let isMinimized = true;
|
|
64
|
+
|
|
65
|
+
function toggleMinimized() {
|
|
66
|
+
isMinimized = !isMinimized;
|
|
67
|
+
console.log('[PanZoomPanel] minimized:', isMinimized);
|
|
68
|
+
}
|
|
59
69
|
</script>
|
|
60
70
|
|
|
61
|
-
<div class="{className ? className + ' ' : ''}pan-zoom-panel" style={style}>
|
|
71
|
+
<div class="pan-zoom-panel-ct {className ? className + ' ' : ''}pan-zoom-panel {isMinimized ? 'pan-zoom-panel-minimized' : ''}" style={style}>
|
|
72
|
+
{#if isMinimized}
|
|
73
|
+
<Tooltip text="Zoom {zoomLevel}% — click to expand" position="above">
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
class="panel-btn pan-zoom-expand-btn"
|
|
77
|
+
onclick={toggleMinimized}
|
|
78
|
+
aria-label="Expand zoom controls"
|
|
79
|
+
>
|
|
80
|
+
<span class="text-btn">{zoomLevel}%</span>
|
|
81
|
+
</button>
|
|
82
|
+
</Tooltip>
|
|
83
|
+
{:else}
|
|
84
|
+
{#if advancedFeatures}
|
|
62
85
|
<!-- Grid Toggle -->
|
|
63
86
|
<Tooltip text="Toggle Grid [G]" position="above">
|
|
64
87
|
<button
|
|
@@ -92,6 +115,7 @@
|
|
|
92
115
|
</Tooltip>
|
|
93
116
|
|
|
94
117
|
<div class="divider"></div>
|
|
118
|
+
{/if}
|
|
95
119
|
|
|
96
120
|
<!-- Zoom Out -->
|
|
97
121
|
<Tooltip text="Zoom Out [Ctrl/Cmd] + [−]" position="above">
|
|
@@ -138,6 +162,7 @@
|
|
|
138
162
|
</button>
|
|
139
163
|
</Tooltip>
|
|
140
164
|
|
|
165
|
+
{#if advancedFeatures}
|
|
141
166
|
<div class="divider"></div>
|
|
142
167
|
|
|
143
168
|
<!-- Reset Zoom (100%) -->
|
|
@@ -181,6 +206,7 @@
|
|
|
181
206
|
</svg>
|
|
182
207
|
</button>
|
|
183
208
|
</Tooltip>
|
|
209
|
+
{/if}
|
|
184
210
|
|
|
185
211
|
{#if showSaveButton}
|
|
186
212
|
<div class="divider"></div>
|
|
@@ -201,18 +227,66 @@
|
|
|
201
227
|
</button>
|
|
202
228
|
</Tooltip>
|
|
203
229
|
{/if}
|
|
230
|
+
|
|
231
|
+
<div class="pan-zoom-minimize-anchor">
|
|
232
|
+
<Tooltip text="Minimize zoom" position="above">
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
class="panel-btn pan-zoom-minimize-btn"
|
|
236
|
+
onclick={toggleMinimized}
|
|
237
|
+
aria-label="Minimize zoom controls"
|
|
238
|
+
>
|
|
239
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
|
|
240
|
+
<polyline points="4 14 10 14 10 20"/>
|
|
241
|
+
<polyline points="20 10 14 10 14 4"/>
|
|
242
|
+
<line x1="14" y1="10" x2="21" y2="3"/>
|
|
243
|
+
<line x1="3" y1="21" x2="10" y2="14"/>
|
|
244
|
+
</svg>
|
|
245
|
+
</button>
|
|
246
|
+
</Tooltip>
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
204
249
|
</div>
|
|
205
250
|
|
|
206
251
|
<style>
|
|
207
252
|
.pan-zoom-panel {
|
|
253
|
+
position: relative;
|
|
208
254
|
display: flex;
|
|
209
255
|
align-items: center;
|
|
210
256
|
gap: 4px;
|
|
257
|
+
width: max-content;
|
|
258
|
+
max-width: 100%;
|
|
211
259
|
padding: 6px 10px;
|
|
212
260
|
background: white;
|
|
213
261
|
border: 2px solid #e5e7eb;
|
|
214
262
|
border-radius: 8px;
|
|
215
263
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
264
|
+
overflow: visible;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.pan-zoom-minimize-anchor {
|
|
268
|
+
position: absolute;
|
|
269
|
+
top: -8px;
|
|
270
|
+
right: -8px;
|
|
271
|
+
z-index: 2;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.pan-zoom-minimize-btn {
|
|
275
|
+
width: 18px;
|
|
276
|
+
height: 18px;
|
|
277
|
+
background: white;
|
|
278
|
+
border: 1px solid #e5e7eb;
|
|
279
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.pan-zoom-panel-minimized {
|
|
283
|
+
padding: 4px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.pan-zoom-expand-btn {
|
|
287
|
+
min-width: 44px;
|
|
288
|
+
width: auto;
|
|
289
|
+
padding: 0 8px;
|
|
216
290
|
}
|
|
217
291
|
|
|
218
292
|
.panel-btn {
|
|
@@ -241,13 +315,26 @@
|
|
|
241
315
|
|
|
242
316
|
.panel-btn.active {
|
|
243
317
|
background: var(--main-color, #314F2F);
|
|
244
|
-
color: white;
|
|
318
|
+
color: white !important;
|
|
245
319
|
border-color: var(--main-color, #314F2F);
|
|
246
320
|
}
|
|
247
321
|
|
|
322
|
+
/* Host apps (widget-creator / site) may set a global svg color; force icons to follow the button. */
|
|
323
|
+
.panel-btn.active :global(svg),
|
|
324
|
+
.panel-btn.active :global(svg *) {
|
|
325
|
+
color: white !important;
|
|
326
|
+
stroke: currentColor !important;
|
|
327
|
+
}
|
|
328
|
+
|
|
248
329
|
.panel-btn.active:hover {
|
|
249
330
|
background: var(--main-color-hover, #263d24);
|
|
250
|
-
color: white;
|
|
331
|
+
color: white !important;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.panel-btn.active:hover :global(svg),
|
|
335
|
+
.panel-btn.active:hover :global(svg *) {
|
|
336
|
+
color: white !important;
|
|
337
|
+
stroke: currentColor !important;
|
|
251
338
|
}
|
|
252
339
|
|
|
253
340
|
.panel-btn:disabled {
|
|
@@ -15,6 +15,7 @@ declare const __propDef: {
|
|
|
15
15
|
onGridToggle?: () => void;
|
|
16
16
|
onShortcutsToggle?: () => void;
|
|
17
17
|
shortcutsOpen?: boolean;
|
|
18
|
+
/** Extra view controls (grid, pan, 1:1, fit, shortcuts). Zoom ± / % and save stay available. */ advancedFeatures?: boolean;
|
|
18
19
|
showSaveButton?: boolean;
|
|
19
20
|
hasUnsavedChanges?: boolean;
|
|
20
21
|
onSave?: (() => void) | null;
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
export let onStrokeTypeChange: (type: 'solid' | 'dashed' | 'dotted') => void = () => {};
|
|
28
28
|
export let onOpacityChange: (opacity: number) => void = () => {};
|
|
29
29
|
export let onFillOpacityChange: (opacity: number) => void = () => {};
|
|
30
|
+
export let onGroupFillChange: (color: string) => void = () => {};
|
|
31
|
+
export let onGroupFillOpacityChange: (opacity: number) => void = () => {};
|
|
32
|
+
export let onGroupStrokeChange: (color: string) => void = () => {};
|
|
33
|
+
export let onGroupStrokeWidthChange: (width: number) => void = () => {};
|
|
34
|
+
export let onGroupStrokeTypeChange: (type: 'solid' | 'dashed' | 'dotted') => void = () => {};
|
|
30
35
|
export let onBackgroundColorChange: (color: string) => void = () => {};
|
|
31
36
|
export let onBackgroundOpacityChange: (opacity: number) => void = () => {};
|
|
32
37
|
|
|
@@ -81,6 +86,7 @@
|
|
|
81
86
|
|
|
82
87
|
// Callback: fires when the user requests a screenshot refresh for the linked widget
|
|
83
88
|
export let onRefreshScreenshot: ((widgetId: string) => void) | null = null;
|
|
89
|
+
export let onSetAsPrimary: ((widgetId: string) => void) | null = null;
|
|
84
90
|
|
|
85
91
|
// Widget name resolver: given a widgetId, returns the widget name (or null)
|
|
86
92
|
export let getWidgetName: ((widgetId: string) => string | null) | null = null;
|
|
@@ -133,6 +139,7 @@
|
|
|
133
139
|
// FrameShape objects have type === 'FrameShape', not 'group', so isGroup is always false for them.
|
|
134
140
|
// isMultiSelect is only true for plain Groups and ActiveSelections (not Frames).
|
|
135
141
|
$: isMultiSelect = (isGroup && !isFrame) || isActiveSelection;
|
|
142
|
+
$: isPlainGroup = isGroup && !isFrame;
|
|
136
143
|
$: isLine = objectType === 'line';
|
|
137
144
|
$: isImage = objectType === 'image' || customType === 'image';
|
|
138
145
|
// Widget link: true when the selected object has already been converted to a widget
|
|
@@ -220,7 +227,6 @@
|
|
|
220
227
|
// For Frames (Group-based), read fill/stroke from custom properties (_frameFill, _frameStroke)
|
|
221
228
|
// because the actual fill/stroke is on the background Rect child, not the Group itself
|
|
222
229
|
$: currentFill = isFrame ? (selectedObject?._frameFill ?? '#FFFFFF') : (selectedObject?.fill ?? '#FFFFFF');
|
|
223
|
-
// Fill opacity is stored as custom property _fillOpacity (0-1), display as percentage
|
|
224
230
|
$: currentFillOpacity = Math.round((selectedObject?._fillOpacity ?? 1) * 100);
|
|
225
231
|
$: currentStroke = isFrame ? (selectedObject?._frameStroke ?? '#000000') : (selectedObject?.stroke ?? '#000000');
|
|
226
232
|
$: currentStrokeWidth = isFrame ? (selectedObject?._frameStrokeWidth ?? 1) : (selectedObject?.strokeWidth ?? 1);
|
|
@@ -238,6 +244,11 @@
|
|
|
238
244
|
? (selectedObject as any)?._frameStrokeDashArray
|
|
239
245
|
: selectedObject?.strokeDashArray
|
|
240
246
|
);
|
|
247
|
+
$: groupFill = selectedObject?._groupFill ?? '#FFFFFF';
|
|
248
|
+
$: groupFillOpacity = Math.round((selectedObject?._groupFillOpacity ?? 1) * 100);
|
|
249
|
+
$: groupStroke = selectedObject?._groupStroke ?? '#000000';
|
|
250
|
+
$: groupStrokeWidth = selectedObject?._groupStrokeWidth ?? 0;
|
|
251
|
+
$: groupStrokeType = getStrokeType((selectedObject as any)?._groupStrokeDashArray);
|
|
241
252
|
|
|
242
253
|
// Text properties - for groups, get from first textbox
|
|
243
254
|
$: firstTextboxInGroup = (selectedObject?.type === 'group' || selectedObject?.type === 'activeSelection' || selectedObject?.type === 'activeselection')
|
|
@@ -538,12 +549,26 @@
|
|
|
538
549
|
Refresh Widget Preview
|
|
539
550
|
</button>
|
|
540
551
|
{/if}
|
|
552
|
+
{#if onSetAsPrimary && widgetId}
|
|
553
|
+
{#if selectedObject?._isPrimaryWidget}
|
|
554
|
+
<span class="primary-widget-badge">Primary widget</span>
|
|
555
|
+
{:else}
|
|
556
|
+
<button
|
|
557
|
+
class="set-primary-btn"
|
|
558
|
+
type="button"
|
|
559
|
+
title="Use this widget as the canvas primary (red stroke)"
|
|
560
|
+
onclick={() => onSetAsPrimary && widgetId && onSetAsPrimary(widgetId)}
|
|
561
|
+
>
|
|
562
|
+
Set as primary
|
|
563
|
+
</button>
|
|
564
|
+
{/if}
|
|
565
|
+
{/if}
|
|
541
566
|
{/if}
|
|
542
567
|
{:else}
|
|
543
568
|
<span class="prop-label">Convert to Widget</span>
|
|
544
569
|
<div class="capture-section">
|
|
545
570
|
<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">
|
|
571
|
+
<svg class="convert-widget-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
547
572
|
<rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"/>
|
|
548
573
|
<line x1="7" y1="2" x2="7" y2="22"/>
|
|
549
574
|
<line x1="17" y1="2" x2="17" y2="22"/>
|
|
@@ -1115,6 +1140,81 @@
|
|
|
1115
1140
|
{/if}
|
|
1116
1141
|
{/if}
|
|
1117
1142
|
|
|
1143
|
+
<!-- ==================== GROUP BOX (chrome around children, like Frame) ==================== -->
|
|
1144
|
+
{#if isPlainGroup && !isWidgetImage}
|
|
1145
|
+
<div class="section-divider">
|
|
1146
|
+
<span class="section-title">Group Box</span>
|
|
1147
|
+
</div>
|
|
1148
|
+
{#if wireframeMode || !isMinimized}
|
|
1149
|
+
<div class="prop-section">
|
|
1150
|
+
<ColorIC
|
|
1151
|
+
label="Group Fill Color"
|
|
1152
|
+
value={groupFill || '#FFFFFF'}
|
|
1153
|
+
onChange={onGroupFillChange}
|
|
1154
|
+
showPicker={!wireframeMode}
|
|
1155
|
+
presets={wireframeMode ? wireframeColorValues : null}
|
|
1156
|
+
/>
|
|
1157
|
+
</div>
|
|
1158
|
+
{/if}
|
|
1159
|
+
<SliderUnitIC
|
|
1160
|
+
label="Group Fill Opacity"
|
|
1161
|
+
value={groupFillOpacity}
|
|
1162
|
+
min={0}
|
|
1163
|
+
max={100}
|
|
1164
|
+
step={1}
|
|
1165
|
+
unit="%"
|
|
1166
|
+
onChange={onGroupFillOpacityChange}
|
|
1167
|
+
/>
|
|
1168
|
+
{#if wireframeMode || !isMinimized}
|
|
1169
|
+
<div class="prop-section">
|
|
1170
|
+
<ColorIC
|
|
1171
|
+
label="Group Stroke Color"
|
|
1172
|
+
value={groupStroke || '#000000'}
|
|
1173
|
+
onChange={onGroupStrokeChange}
|
|
1174
|
+
showPicker={!wireframeMode}
|
|
1175
|
+
presets={wireframeMode ? wireframeColorValues : null}
|
|
1176
|
+
/>
|
|
1177
|
+
</div>
|
|
1178
|
+
{/if}
|
|
1179
|
+
<SliderUnitIC
|
|
1180
|
+
label="Group Stroke Width"
|
|
1181
|
+
value={groupStrokeWidth}
|
|
1182
|
+
min={0}
|
|
1183
|
+
max={20}
|
|
1184
|
+
step={1}
|
|
1185
|
+
unit="px"
|
|
1186
|
+
onChange={onGroupStrokeWidthChange}
|
|
1187
|
+
/>
|
|
1188
|
+
{#if !isMinimized}
|
|
1189
|
+
<div class="prop-section">
|
|
1190
|
+
<span class="prop-label">Group Stroke Type</span>
|
|
1191
|
+
<div class="stroke-type-buttons">
|
|
1192
|
+
<button
|
|
1193
|
+
class="stroke-type-btn {groupStrokeType === 'solid' ? 'active' : ''}"
|
|
1194
|
+
onclick={() => onGroupStrokeTypeChange('solid')}
|
|
1195
|
+
>
|
|
1196
|
+
<span class="stroke-preview solid"></span>
|
|
1197
|
+
Solid
|
|
1198
|
+
</button>
|
|
1199
|
+
<button
|
|
1200
|
+
class="stroke-type-btn {groupStrokeType === 'dashed' ? 'active' : ''}"
|
|
1201
|
+
onclick={() => onGroupStrokeTypeChange('dashed')}
|
|
1202
|
+
>
|
|
1203
|
+
<span class="stroke-preview dashed"></span>
|
|
1204
|
+
Dashed
|
|
1205
|
+
</button>
|
|
1206
|
+
<button
|
|
1207
|
+
class="stroke-type-btn {groupStrokeType === 'dotted' ? 'active' : ''}"
|
|
1208
|
+
onclick={() => onGroupStrokeTypeChange('dotted')}
|
|
1209
|
+
>
|
|
1210
|
+
<span class="stroke-preview dotted"></span>
|
|
1211
|
+
Dotted
|
|
1212
|
+
</button>
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
{/if}
|
|
1216
|
+
{/if}
|
|
1217
|
+
|
|
1118
1218
|
<!-- ==================== TEXT PROPERTIES (for selection/group containing textboxes or frames) ==================== -->
|
|
1119
1219
|
{#if !isMinimized && hasTextboxInSelection}
|
|
1120
1220
|
<div class="section-divider">
|
|
@@ -1450,9 +1550,14 @@
|
|
|
1450
1550
|
box-shadow: 0 2px 4px rgba(49, 79, 47, 0.3);
|
|
1451
1551
|
}
|
|
1452
1552
|
|
|
1453
|
-
|
|
1553
|
+
/* Host apps (site / DS) leak global svg fill/stroke; keep this icon white on the green button. */
|
|
1554
|
+
.convert-widget-btn :global(svg),
|
|
1555
|
+
.convert-widget-btn :global(svg *) {
|
|
1454
1556
|
width: 16px;
|
|
1455
1557
|
height: 16px;
|
|
1558
|
+
color: #ffffff !important;
|
|
1559
|
+
fill: none !important;
|
|
1560
|
+
stroke: currentColor !important;
|
|
1456
1561
|
}
|
|
1457
1562
|
|
|
1458
1563
|
.widget-linked-section {
|
|
@@ -1569,7 +1674,9 @@
|
|
|
1569
1674
|
border-radius: 4px;
|
|
1570
1675
|
cursor: pointer;
|
|
1571
1676
|
transition: all 0.15s;
|
|
1572
|
-
width:
|
|
1677
|
+
width: 100%;
|
|
1678
|
+
box-sizing: border-box;
|
|
1679
|
+
justify-content: center;
|
|
1573
1680
|
}
|
|
1574
1681
|
.refresh-screenshot-btn:hover {
|
|
1575
1682
|
color: #166534;
|
|
@@ -1583,6 +1690,43 @@
|
|
|
1583
1690
|
flex-shrink: 0;
|
|
1584
1691
|
}
|
|
1585
1692
|
|
|
1693
|
+
.set-primary-btn {
|
|
1694
|
+
display: flex;
|
|
1695
|
+
align-items: center;
|
|
1696
|
+
margin-top: 4px;
|
|
1697
|
+
padding: 3px 8px;
|
|
1698
|
+
font-size: 11px;
|
|
1699
|
+
font-weight: 500;
|
|
1700
|
+
color: #991b1b;
|
|
1701
|
+
background: #fef2f2;
|
|
1702
|
+
border: 1px solid #fecaca;
|
|
1703
|
+
border-radius: 4px;
|
|
1704
|
+
cursor: pointer;
|
|
1705
|
+
width: 100%;
|
|
1706
|
+
box-sizing: border-box;
|
|
1707
|
+
justify-content: center;
|
|
1708
|
+
}
|
|
1709
|
+
.set-primary-btn:hover {
|
|
1710
|
+
background: #fee2e2;
|
|
1711
|
+
border-color: #f87171;
|
|
1712
|
+
}
|
|
1713
|
+
.primary-widget-badge {
|
|
1714
|
+
display: flex;
|
|
1715
|
+
align-items: center;
|
|
1716
|
+
justify-content: center;
|
|
1717
|
+
margin-top: 4px;
|
|
1718
|
+
padding: 3px 8px;
|
|
1719
|
+
font-size: 11px;
|
|
1720
|
+
font-weight: 600;
|
|
1721
|
+
color: #991b1b;
|
|
1722
|
+
background: #fef2f2;
|
|
1723
|
+
border: 1px solid #fecaca;
|
|
1724
|
+
border-radius: 4px;
|
|
1725
|
+
width: 100%;
|
|
1726
|
+
box-sizing: border-box;
|
|
1727
|
+
text-align: center;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1586
1730
|
.widget-orphan-notice {
|
|
1587
1731
|
font-size: 11px;
|
|
1588
1732
|
line-height: 1.45;
|
|
@@ -12,6 +12,11 @@ declare const __propDef: {
|
|
|
12
12
|
onStrokeTypeChange?: (type: "solid" | "dashed" | "dotted") => void;
|
|
13
13
|
onOpacityChange?: (opacity: number) => void;
|
|
14
14
|
onFillOpacityChange?: (opacity: number) => void;
|
|
15
|
+
onGroupFillChange?: (color: string) => void;
|
|
16
|
+
onGroupFillOpacityChange?: (opacity: number) => void;
|
|
17
|
+
onGroupStrokeChange?: (color: string) => void;
|
|
18
|
+
onGroupStrokeWidthChange?: (width: number) => void;
|
|
19
|
+
onGroupStrokeTypeChange?: (type: "solid" | "dashed" | "dotted") => void;
|
|
15
20
|
onBackgroundColorChange?: (color: string) => void;
|
|
16
21
|
onBackgroundOpacityChange?: (opacity: number) => void;
|
|
17
22
|
onFontSizeChange?: (size: number) => void;
|
|
@@ -44,6 +49,7 @@ declare const __propDef: {
|
|
|
44
49
|
onWidgetClick?: ((widgetId: string) => void) | null;
|
|
45
50
|
onWidgetRename?: ((widgetId: string, newName: string) => void) | null;
|
|
46
51
|
onRefreshScreenshot?: ((widgetId: string) => void) | null;
|
|
52
|
+
onSetAsPrimary?: ((widgetId: string) => void) | null;
|
|
47
53
|
getWidgetName?: ((widgetId: string) => string | null) | null;
|
|
48
54
|
/** Returns false when the linked widget was deleted or moved off this canvas. */ isWidgetKnown?: ((widgetId: string) => boolean) | null;
|
|
49
55
|
/** Remove the selected orphaned widget shape from the canvas. */ onRemoveOrphanedShape?: (() => void) | null;
|