@dryui/feedback 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/components/annotation-marker.svelte +163 -0
  2. package/dist/components/annotation-marker.svelte.d.ts +11 -0
  3. package/dist/components/annotation-popup.svelte +669 -0
  4. package/dist/components/annotation-popup.svelte.d.ts +42 -0
  5. package/dist/components/highlight-overlay.svelte +48 -0
  6. package/dist/components/highlight-overlay.svelte.d.ts +8 -0
  7. package/dist/components/settings-panel.svelte +446 -0
  8. package/dist/components/settings-panel.svelte.d.ts +24 -0
  9. package/dist/components/toolbar.svelte +1111 -0
  10. package/dist/components/toolbar.svelte.d.ts +46 -0
  11. package/dist/constants.d.ts +9 -0
  12. package/dist/constants.js +37 -0
  13. package/dist/feedback.svelte +2879 -0
  14. package/dist/feedback.svelte.d.ts +4 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.js +7 -0
  17. package/dist/layout-mode/catalog.d.ts +16 -0
  18. package/dist/layout-mode/catalog.js +81 -0
  19. package/dist/layout-mode/component-actions.svelte +84 -0
  20. package/dist/layout-mode/component-actions.svelte.d.ts +18 -0
  21. package/dist/layout-mode/component-picker.svelte +73 -0
  22. package/dist/layout-mode/component-picker.svelte.d.ts +10 -0
  23. package/dist/layout-mode/design-mode.svelte +1115 -0
  24. package/dist/layout-mode/design-mode.svelte.d.ts +24 -0
  25. package/dist/layout-mode/design-palette.svelte +396 -0
  26. package/dist/layout-mode/design-palette.svelte.d.ts +20 -0
  27. package/dist/layout-mode/element-heuristics.d.ts +5 -0
  28. package/dist/layout-mode/element-heuristics.js +51 -0
  29. package/dist/layout-mode/freeze.d.ts +6 -0
  30. package/dist/layout-mode/freeze.js +163 -0
  31. package/dist/layout-mode/generated-library.d.ts +940 -0
  32. package/dist/layout-mode/generated-library.js +1445 -0
  33. package/dist/layout-mode/geometry.d.ts +38 -0
  34. package/dist/layout-mode/geometry.js +133 -0
  35. package/dist/layout-mode/history.d.ts +10 -0
  36. package/dist/layout-mode/history.js +45 -0
  37. package/dist/layout-mode/index.d.ts +23 -0
  38. package/dist/layout-mode/index.js +18 -0
  39. package/dist/layout-mode/live-mount.d.ts +20 -0
  40. package/dist/layout-mode/live-mount.js +70 -0
  41. package/dist/layout-mode/output.d.ts +26 -0
  42. package/dist/layout-mode/output.js +550 -0
  43. package/dist/layout-mode/placement-skeleton.d.ts +9 -0
  44. package/dist/layout-mode/placement-skeleton.js +535 -0
  45. package/dist/layout-mode/rearrange-overlay.svelte +1293 -0
  46. package/dist/layout-mode/rearrange-overlay.svelte.d.ts +18 -0
  47. package/dist/layout-mode/responsive-bar.svelte +39 -0
  48. package/dist/layout-mode/responsive-bar.svelte.d.ts +8 -0
  49. package/dist/layout-mode/route-creator.svelte +70 -0
  50. package/dist/layout-mode/route-creator.svelte.d.ts +8 -0
  51. package/dist/layout-mode/section-detection.d.ts +6 -0
  52. package/dist/layout-mode/section-detection.js +214 -0
  53. package/dist/layout-mode/spatial.d.ts +42 -0
  54. package/dist/layout-mode/spatial.js +156 -0
  55. package/dist/layout-mode/types.d.ts +144 -0
  56. package/dist/layout-mode/types.js +84 -0
  57. package/dist/types.d.ts +157 -0
  58. package/dist/types.js +1 -0
  59. package/dist/utils/dryui-detection.d.ts +1 -0
  60. package/dist/utils/dryui-detection.js +219 -0
  61. package/dist/utils/element-id.d.ts +12 -0
  62. package/dist/utils/element-id.js +333 -0
  63. package/dist/utils/freeze.d.ts +7 -0
  64. package/dist/utils/freeze.js +168 -0
  65. package/dist/utils/output.d.ts +15 -0
  66. package/dist/utils/output.js +245 -0
  67. package/dist/utils/selection.d.ts +22 -0
  68. package/dist/utils/selection.js +58 -0
  69. package/dist/utils/shadow-dom.d.ts +4 -0
  70. package/dist/utils/shadow-dom.js +39 -0
  71. package/dist/utils/storage.d.ts +30 -0
  72. package/dist/utils/storage.js +206 -0
  73. package/dist/utils/svelte-detection.d.ts +8 -0
  74. package/dist/utils/svelte-detection.js +86 -0
  75. package/dist/utils/svelte-meta.d.ts +6 -0
  76. package/dist/utils/svelte-meta.js +69 -0
  77. package/dist/utils/sync.d.ts +18 -0
  78. package/dist/utils/sync.js +62 -0
  79. package/package.json +65 -0
@@ -0,0 +1,1115 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import { Badge, Stack, Text } from '@dryui/ui';
4
+ import AnnotationPopup from '../components/annotation-popup.svelte';
5
+ import { computeSnap, createRectFromPoint, isMeaningfulDrag, MIN_SIZE, type Guide } from './geometry.js';
6
+ import { createMountManager } from './live-mount.js';
7
+ import ResponsiveBar from './responsive-bar.svelte';
8
+ import { COMPONENT_REGISTRY, DEFAULT_SIZES, type CanvasWidth, type DesignPlacement, type LayoutModeComponentType, type Rect } from './types.js';
9
+
10
+ interface Props {
11
+ placements: DesignPlacement[];
12
+ activeComponent: LayoutModeComponentType | null;
13
+ wireframe?: boolean;
14
+ passthrough?: boolean;
15
+ extraSnapRects?: Rect[];
16
+ deselectSignal?: number;
17
+ clearSignal?: number;
18
+ exiting?: boolean;
19
+ canvasWidth?: CanvasWidth;
20
+ class?: string;
21
+ onChange?: (placements: DesignPlacement[]) => void;
22
+ onActiveComponentChange?: (type: LayoutModeComponentType | null) => void;
23
+ onInteractionChange?: (active: boolean) => void;
24
+ onSelectionChange?: (selectedIds: Set<string>, isShift: boolean) => void;
25
+ onDragMove?: (dx: number, dy: number) => void;
26
+ onDragEnd?: (dx: number, dy: number, committed: boolean) => void;
27
+ onCanvasWidthChange?: (width: CanvasWidth) => void;
28
+ onHistoryPush?: () => void;
29
+ }
30
+
31
+ type Point = { x: number; y: number };
32
+ type HandleDir = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w';
33
+ type Interaction = 'place' | 'move' | 'resize';
34
+ const CORNER_HANDLES: HandleDir[] = ['nw', 'ne', 'se', 'sw'];
35
+ const EDGE_HANDLES: HandleDir[] = ['n', 'e', 's', 'w'];
36
+ const HANDLE_OFFSETS: HandleDir[] = [...CORNER_HANDLES, ...EDGE_HANDLES];
37
+ const TEXT_PLACEHOLDERS: Partial<Record<LayoutModeComponentType, string>> = {
38
+ alert: 'Alert message',
39
+ banner: 'Banner text',
40
+ badge: 'Badge label',
41
+ breadcrumb: 'Breadcrumb labels',
42
+ button: 'Button label',
43
+ card: 'Card title',
44
+ cta: 'Call to action text',
45
+ hero: 'Headline text',
46
+ input: 'Placeholder text',
47
+ modal: 'Dialog title',
48
+ navigation: 'Brand or nav items',
49
+ notification: 'Notification message',
50
+ pricing: 'Plan name or price',
51
+ search: 'Search placeholder',
52
+ stat: 'Metric value',
53
+ tabs: 'Tab labels',
54
+ tag: 'Tag label',
55
+ testimonial: 'Quote text',
56
+ text: 'Label or content text',
57
+ toast: 'Notification message',
58
+ };
59
+ const COMPONENT_LABELS = new Map(
60
+ COMPONENT_REGISTRY.flatMap((section) => section.items.map((item) => [item.type, item.label] as const)),
61
+ );
62
+
63
+ let {
64
+ placements,
65
+ activeComponent = $bindable(null),
66
+ wireframe = false,
67
+ passthrough = false,
68
+ extraSnapRects,
69
+ deselectSignal,
70
+ clearSignal,
71
+ exiting = false,
72
+ canvasWidth = 1280 as CanvasWidth,
73
+ class: className,
74
+ onChange,
75
+ onActiveComponentChange,
76
+ onInteractionChange,
77
+ onSelectionChange,
78
+ onDragMove,
79
+ onDragEnd,
80
+ onCanvasWidthChange,
81
+ onHistoryPush,
82
+ }: Props = $props();
83
+
84
+ const mountManager = createMountManager();
85
+
86
+ let selectedIds = $state<string[]>([]);
87
+ let guides = $state<Guide[]>([]);
88
+ let drawBox = $state<Rect | null>(null);
89
+ let selectBox = $state<Rect | null>(null);
90
+ let sizeIndicator = $state<{ x: number; y: number; text: string } | null>(null);
91
+ let pointerStart = $state<Point | null>(null);
92
+ let interaction = $state<Interaction | null>(null);
93
+ let editingPlacementId = $state<string | null>(null);
94
+ let interactingId = $state<string | null>(null);
95
+ let documentHeight = $state(0);
96
+ let canvasOverlay = $state<HTMLDivElement | null>(null);
97
+ let lastDeselectSignal: number | undefined = undefined;
98
+ let lastClearSignal: number | undefined = undefined;
99
+
100
+ function generateId(): string {
101
+ return `dp-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
102
+ }
103
+
104
+ function isPrimaryButton(event: PointerEvent | MouseEvent): boolean {
105
+ return event.button === 0;
106
+ }
107
+
108
+ function isSelected(id: string): boolean {
109
+ return selectedIds.includes(id);
110
+ }
111
+
112
+ function setSelection(ids: string[], shiftKey: boolean = false) {
113
+ selectedIds = ids;
114
+ onSelectionChange?.(new Set(ids), shiftKey);
115
+ }
116
+
117
+ function updatePlacements(nextPlacements: DesignPlacement[], commit = true) {
118
+ onChange?.(nextPlacements);
119
+ if (commit) onHistoryPush?.();
120
+ }
121
+
122
+ function placementAt(id: string): DesignPlacement | undefined {
123
+ return placements.find((placement) => placement.id === id);
124
+ }
125
+
126
+ function canvasScrollTop(): number {
127
+ return canvasOverlay?.scrollTop ?? 0;
128
+ }
129
+
130
+ function placementViewportRect(placement: DesignPlacement): Rect {
131
+ return {
132
+ x: placement.x,
133
+ y: placement.y - canvasScrollTop(),
134
+ width: placement.width,
135
+ height: placement.height,
136
+ };
137
+ }
138
+
139
+ function notePopupPosition(placement: DesignPlacement): { x: number; y: number } {
140
+ const rect = placementViewportRect(placement);
141
+ const popupWidth = 360;
142
+ const popupHeight = 240;
143
+ const x = Math.max(12, Math.min(window.innerWidth - popupWidth - 12, rect.x + rect.width / 2 - popupWidth / 2));
144
+ const fitsAbove = rect.y > popupHeight + 24;
145
+ const fitsBelow = rect.y + rect.height + popupHeight + 24 < window.innerHeight;
146
+
147
+ if (fitsAbove) {
148
+ return { x, y: Math.max(16, rect.y - popupHeight - 12) };
149
+ }
150
+
151
+ if (fitsBelow) {
152
+ return { x, y: rect.y + rect.height + 12 };
153
+ }
154
+
155
+ return {
156
+ x,
157
+ y: Math.max(16, Math.min(window.innerHeight - popupHeight - 16, window.innerHeight / 2 - popupHeight / 2)),
158
+ };
159
+ }
160
+
161
+ function captureDefaultPlacement(point: Point): Rect {
162
+ const size = activeComponent ? DEFAULT_SIZES[activeComponent] : { width: 120, height: 72 };
163
+ return {
164
+ x: point.x - size.width / 2,
165
+ y: point.y - size.height / 2,
166
+ width: size.width,
167
+ height: size.height,
168
+ };
169
+ }
170
+
171
+ function applySelection(id: string, shiftKey: boolean): string[] {
172
+ if (!shiftKey) {
173
+ return [id];
174
+ }
175
+
176
+ return isSelected(id) ? selectedIds.filter((selected) => selected !== id) : [...selectedIds, id];
177
+ }
178
+
179
+ function placementLabel(type: LayoutModeComponentType): string {
180
+ return COMPONENT_LABELS.get(type) ?? type;
181
+ }
182
+
183
+ function layoutAccent(): string {
184
+ return wireframe ? '#f97316' : 'var(--dry-color-fill-brand, #7c3aed)';
185
+ }
186
+
187
+ function handleCursor(handle: HandleDir): string {
188
+ return handle === 'n' || handle === 's'
189
+ ? 'ns-resize'
190
+ : handle === 'e' || handle === 'w'
191
+ ? 'ew-resize'
192
+ : handle === 'ne' || handle === 'sw'
193
+ ? 'nesw-resize'
194
+ : 'nwse-resize';
195
+ }
196
+
197
+ function cornerHandleOffset(handle: HandleDir): string {
198
+ return `${handle.includes('n') ? 'top: -4px;' : 'bottom: -4px;'} ${handle.includes('w') ? 'left: -4px;' : 'right: -4px;'}`;
199
+ }
200
+
201
+ function edgeHandleInset(handle: HandleDir): string {
202
+ switch (handle) {
203
+ case 'n':
204
+ return 'left: 12px; right: 12px; top: -6px; height: 12px;';
205
+ case 's':
206
+ return 'left: 12px; right: 12px; bottom: -6px; height: 12px;';
207
+ case 'e':
208
+ return 'top: 12px; bottom: 12px; right: -6px; width: 12px;';
209
+ case 'w':
210
+ return 'top: 12px; bottom: 12px; left: -6px; width: 12px;';
211
+ default:
212
+ return '';
213
+ }
214
+ }
215
+
216
+ function resetTransientState() {
217
+ drawBox = null;
218
+ selectBox = null;
219
+ sizeIndicator = null;
220
+ guides = [];
221
+ pointerStart = null;
222
+ interaction = null;
223
+ onInteractionChange?.(false);
224
+ }
225
+
226
+ function startInteraction(event: PointerEvent, placementId?: string, movingIds: string[] = placementId ? [placementId] : []) {
227
+ if (!isPrimaryButton(event) || passthrough) return;
228
+ if (!placementId && !activeComponent) return;
229
+
230
+ pointerStart = { x: event.clientX, y: event.clientY };
231
+ interaction = placementId ? 'move' : 'place';
232
+ onInteractionChange?.(true);
233
+
234
+ const selectedPlacement = placementId ? placementAt(placementId) : undefined;
235
+ const initialRect = selectedPlacement
236
+ ? { x: selectedPlacement.x, y: selectedPlacement.y, width: selectedPlacement.width, height: selectedPlacement.height }
237
+ : captureDefaultPlacement(pointerStart);
238
+ const baseRects = new Map(
239
+ placements
240
+ .filter((placement) => movingIds.includes(placement.id))
241
+ .map((placement) => [
242
+ placement.id,
243
+ { x: placement.x, y: placement.y, width: placement.width, height: placement.height },
244
+ ] as const),
245
+ );
246
+
247
+ if (!placementId && activeComponent) {
248
+ const defaultSize = DEFAULT_SIZES[activeComponent];
249
+ drawBox = {
250
+ x: initialRect.x,
251
+ y: initialRect.y,
252
+ width: defaultSize.width,
253
+ height: defaultSize.height,
254
+ };
255
+ } else if (!placementId) {
256
+ drawBox = {
257
+ x: initialRect.x,
258
+ y: initialRect.y,
259
+ width: initialRect.width,
260
+ height: initialRect.height,
261
+ };
262
+ }
263
+
264
+ const excluded = new Set<string>(placementId ? movingIds : selectedIds);
265
+ let moved = false;
266
+ let lastDx = 0;
267
+ let lastDy = 0;
268
+ let duplicated = false;
269
+ let basePlacements = placements;
270
+
271
+ const handleMove = (moveEvent: PointerEvent) => {
272
+ if (!pointerStart) return;
273
+ if (placementId) {
274
+ const deltaX = moveEvent.clientX - pointerStart.x;
275
+ const deltaY = moveEvent.clientY - pointerStart.y;
276
+ if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
277
+ moved = true;
278
+ }
279
+ if (moveEvent.altKey && !duplicated) {
280
+ duplicated = true;
281
+ basePlacements = [
282
+ ...placements,
283
+ ...placements
284
+ .filter((placement) => movingIds.includes(placement.id))
285
+ .map((placement) => ({
286
+ ...placement,
287
+ id: generateId(),
288
+ timestamp: Date.now(),
289
+ })),
290
+ ];
291
+ }
292
+
293
+ const movingPlacements = basePlacements.filter((placement) => movingIds.includes(placement.id));
294
+ if (movingPlacements.length === 0) return;
295
+
296
+ const bounds = movingPlacements.reduce(
297
+ (acc, placement) => {
298
+ const start = baseRects.get(placement.id);
299
+ if (!start) return acc;
300
+ const x = start.x + deltaX;
301
+ const y = start.y + deltaY;
302
+ return {
303
+ x: Math.min(acc.x, x),
304
+ y: Math.min(acc.y, y),
305
+ right: Math.max(acc.right, x + start.width),
306
+ bottom: Math.max(acc.bottom, y + start.height),
307
+ };
308
+ },
309
+ { x: Infinity, y: Infinity, right: -Infinity, bottom: -Infinity },
310
+ );
311
+
312
+ const nextRect = {
313
+ x: bounds.x,
314
+ y: bounds.y,
315
+ width: bounds.right - bounds.x,
316
+ height: bounds.bottom - bounds.y,
317
+ };
318
+ const snapped = computeSnap(nextRect, basePlacements, excluded, undefined, extraSnapRects);
319
+ const appliedDx = deltaX + snapped.dx;
320
+ const appliedDy = deltaY + snapped.dy;
321
+ lastDx = appliedDx;
322
+ lastDy = appliedDy;
323
+ guides = snapped.guides;
324
+
325
+ updatePlacements(
326
+ basePlacements.map((placement) => {
327
+ const start = baseRects.get(placement.id);
328
+ if (!start) return placement;
329
+ return {
330
+ ...placement,
331
+ x: Math.max(0, start.x + appliedDx),
332
+ y: Math.max(0, start.y + appliedDy),
333
+ };
334
+ }),
335
+ false,
336
+ );
337
+ onDragMove?.(appliedDx, appliedDy);
338
+ return;
339
+ }
340
+
341
+ const nextRect = createRectFromPoint(pointerStart, { x: moveEvent.clientX, y: moveEvent.clientY });
342
+ const snapped = computeSnap(nextRect, placements, excluded, undefined, extraSnapRects);
343
+ const rect = {
344
+ x: nextRect.x + snapped.dx,
345
+ y: nextRect.y + snapped.dy,
346
+ width: nextRect.width,
347
+ height: nextRect.height,
348
+ };
349
+
350
+ guides = snapped.guides;
351
+ if (!isMeaningfulDrag(rect, MIN_SIZE)) return;
352
+
353
+ drawBox = rect;
354
+ sizeIndicator = {
355
+ x: moveEvent.clientX + 12,
356
+ y: moveEvent.clientY + 12,
357
+ text: `${Math.round(rect.width)} x ${Math.round(rect.height)}`,
358
+ };
359
+ };
360
+
361
+ const handleUp = () => {
362
+ window.removeEventListener('pointermove', handleMove);
363
+ window.removeEventListener('pointerup', handleUp);
364
+
365
+ const committed = Boolean(drawBox && isMeaningfulDrag(drawBox, MIN_SIZE));
366
+ if (placementId) {
367
+ onDragEnd?.(lastDx, lastDy, moved);
368
+ } else if (committed && drawBox) {
369
+ const scrollY = canvasScrollTop();
370
+ if (!placementId && activeComponent) {
371
+ const nextPlacement: DesignPlacement = {
372
+ id: generateId(),
373
+ type: activeComponent,
374
+ x: Math.max(0, drawBox.x),
375
+ y: Math.max(0, drawBox.y) + scrollY,
376
+ width: Math.max(MIN_SIZE, drawBox.width),
377
+ height: Math.max(MIN_SIZE, drawBox.height),
378
+ scrollY,
379
+ timestamp: Date.now(),
380
+ };
381
+ updatePlacements([...placements, nextPlacement]);
382
+ setSelection([nextPlacement.id], false);
383
+ } else {
384
+ const nextPlacement: DesignPlacement = {
385
+ id: generateId(),
386
+ type: activeComponent ?? 'card',
387
+ x: Math.max(0, drawBox.x),
388
+ y: Math.max(0, drawBox.y) + scrollY,
389
+ width: Math.max(MIN_SIZE, drawBox.width),
390
+ height: Math.max(MIN_SIZE, drawBox.height),
391
+ scrollY,
392
+ timestamp: Date.now(),
393
+ };
394
+ updatePlacements([...placements, nextPlacement]);
395
+ setSelection([nextPlacement.id], false);
396
+ }
397
+ } else if (!placementId && activeComponent && pointerStart) {
398
+ const scrollY = canvasScrollTop();
399
+ const size = DEFAULT_SIZES[activeComponent];
400
+ const nextPlacement: DesignPlacement = {
401
+ id: generateId(),
402
+ type: activeComponent,
403
+ x: Math.max(0, pointerStart.x - size.width / 2),
404
+ y: Math.max(0, pointerStart.y - size.height / 2) + scrollY,
405
+ width: size.width,
406
+ height: size.height,
407
+ scrollY,
408
+ timestamp: Date.now(),
409
+ };
410
+ updatePlacements([...placements, nextPlacement]);
411
+ setSelection([nextPlacement.id], false);
412
+ }
413
+
414
+ if (!placementId && activeComponent) {
415
+ activeComponent = null;
416
+ onActiveComponentChange?.(null);
417
+ }
418
+
419
+ resetTransientState();
420
+ };
421
+
422
+ window.addEventListener('pointermove', handleMove);
423
+ window.addEventListener('pointerup', handleUp);
424
+ }
425
+
426
+ function handleOverlayPointerDown(event: PointerEvent) {
427
+ if (!isPrimaryButton(event) || passthrough) return;
428
+ if (event.target instanceof HTMLElement && event.target.closest('[data-placement-id]')) return;
429
+
430
+ const startScrollY = canvasScrollTop();
431
+ const startPoint = { x: event.clientX, y: event.clientY };
432
+
433
+ if (activeComponent) {
434
+ onActiveComponentChange?.(activeComponent);
435
+ startInteraction(event);
436
+ return;
437
+ }
438
+
439
+ if (!(event.shiftKey || event.metaKey || event.ctrlKey)) {
440
+ setSelection([], false);
441
+ }
442
+
443
+ interaction = 'place';
444
+ onInteractionChange?.(true);
445
+ let dragged = false;
446
+
447
+ const handleMove = (moveEvent: PointerEvent) => {
448
+ const box = createRectFromPoint(startPoint, { x: moveEvent.clientX, y: moveEvent.clientY });
449
+ if (box.width > 4 || box.height > 4) {
450
+ dragged = true;
451
+ }
452
+ if (!dragged) return;
453
+ selectBox = box;
454
+ };
455
+
456
+ const handleUp = (moveEvent: PointerEvent) => {
457
+ window.removeEventListener('pointermove', handleMove);
458
+ window.removeEventListener('pointerup', handleUp);
459
+
460
+ if (dragged) {
461
+ const box = createRectFromPoint(startPoint, { x: moveEvent.clientX, y: moveEvent.clientY });
462
+ const boxRect = {
463
+ x: box.x,
464
+ y: box.y + startScrollY,
465
+ width: box.width,
466
+ height: box.height,
467
+ };
468
+ const selected = new Set(
469
+ placements
470
+ .filter((placement) => {
471
+ const placementRect = {
472
+ x: placement.x,
473
+ y: placement.y,
474
+ width: placement.width,
475
+ height: placement.height,
476
+ };
477
+ return (
478
+ placementRect.x + placementRect.width > boxRect.x &&
479
+ placementRect.x < boxRect.x + boxRect.width &&
480
+ placementRect.y + placementRect.height > boxRect.y &&
481
+ placementRect.y < boxRect.y + boxRect.height
482
+ );
483
+ })
484
+ .map((placement) => placement.id),
485
+ );
486
+
487
+ const shiftKey = event.shiftKey || event.metaKey || event.ctrlKey;
488
+ if (shiftKey) {
489
+ setSelection([...new Set([...selectedIds, ...selected])], true);
490
+ } else {
491
+ setSelection(Array.from(selected), false);
492
+ }
493
+ }
494
+
495
+ resetTransientState();
496
+ };
497
+
498
+ window.addEventListener('pointermove', handleMove);
499
+ window.addEventListener('pointerup', handleUp);
500
+ }
501
+
502
+ function handlePlacementPointerDown(event: PointerEvent, id: string) {
503
+ event.stopPropagation();
504
+ const shiftKey = event.shiftKey || event.metaKey || event.ctrlKey;
505
+ const nextSelection = applySelection(id, shiftKey);
506
+ const movingIds = isSelected(id) && !shiftKey ? [...selectedIds] : nextSelection;
507
+ setSelection(nextSelection, shiftKey);
508
+ startInteraction(event, id, movingIds);
509
+ }
510
+
511
+ function openPlacementEditor(id: string) {
512
+ setSelection([id], false);
513
+ editingPlacementId = id;
514
+ }
515
+
516
+ function closePlacementEditor() {
517
+ editingPlacementId = null;
518
+ }
519
+
520
+ function savePlacementText(text: string) {
521
+ if (!editingPlacementId) return;
522
+
523
+ updatePlacements(
524
+ placements.map((placement) =>
525
+ placement.id === editingPlacementId ? { ...placement, text: text.trim() || undefined } : placement,
526
+ ),
527
+ );
528
+ editingPlacementId = null;
529
+ }
530
+
531
+ function clearPlacementText() {
532
+ if (!editingPlacementId) return;
533
+
534
+ updatePlacements(
535
+ placements.map((placement) =>
536
+ placement.id === editingPlacementId ? { ...placement, text: undefined } : placement,
537
+ ),
538
+ );
539
+ editingPlacementId = null;
540
+ }
541
+
542
+ function resizeRect(baseRect: Rect, dir: HandleDir, dx: number, dy: number, keepAspectRatio: boolean = false): Rect {
543
+ let next = { ...baseRect };
544
+ const aspectRatio = baseRect.height > 0 ? baseRect.width / baseRect.height : 1;
545
+
546
+ if (dir.includes('e')) {
547
+ next.width = Math.max(MIN_SIZE, baseRect.width + dx);
548
+ }
549
+ if (dir.includes('s')) {
550
+ next.height = Math.max(MIN_SIZE, baseRect.height + dy);
551
+ }
552
+ if (dir.includes('w')) {
553
+ const width = Math.max(MIN_SIZE, baseRect.width - dx);
554
+ next.x = baseRect.x + (baseRect.width - width);
555
+ next.width = width;
556
+ }
557
+ if (dir.includes('n')) {
558
+ const height = Math.max(MIN_SIZE, baseRect.height - dy);
559
+ next.y = baseRect.y + (baseRect.height - height);
560
+ next.height = height;
561
+ }
562
+
563
+ if (keepAspectRatio) {
564
+ const isCorner = dir.length === 2;
565
+ if (isCorner) {
566
+ const widthDelta = Math.abs(next.width - baseRect.width);
567
+ const heightDelta = Math.abs(next.height - baseRect.height);
568
+ if (widthDelta > heightDelta) {
569
+ next.height = Math.max(MIN_SIZE, next.width / aspectRatio);
570
+ } else {
571
+ next.width = Math.max(MIN_SIZE, next.height * aspectRatio);
572
+ }
573
+ } else if (dir === 'e' || dir === 'w') {
574
+ next.height = Math.max(MIN_SIZE, next.width / aspectRatio);
575
+ } else {
576
+ next.width = Math.max(MIN_SIZE, next.height * aspectRatio);
577
+ }
578
+
579
+ if (dir.includes('w')) {
580
+ next.x = baseRect.x + (baseRect.width - next.width);
581
+ }
582
+ if (dir.includes('n')) {
583
+ next.y = baseRect.y + (baseRect.height - next.height);
584
+ }
585
+ }
586
+
587
+ return next;
588
+ }
589
+
590
+ function startResize(event: MouseEvent, placementId: string, dir: HandleDir) {
591
+ if (event.button !== 0) return;
592
+ event.preventDefault();
593
+ event.stopPropagation();
594
+
595
+ const placement = placementAt(placementId);
596
+ if (!placement) return;
597
+
598
+ setSelection([placementId], false);
599
+ onInteractionChange?.(true);
600
+ interaction = 'resize';
601
+
602
+ const baseRect = {
603
+ x: placement.x,
604
+ y: placement.y,
605
+ width: placement.width,
606
+ height: placement.height,
607
+ };
608
+ const activeEdges = {
609
+ left: dir.includes('w'),
610
+ right: dir.includes('e'),
611
+ top: dir.includes('n'),
612
+ bottom: dir.includes('s'),
613
+ };
614
+
615
+ const handleMove = (moveEvent: MouseEvent) => {
616
+ const nextRect = resizeRect(
617
+ baseRect,
618
+ dir,
619
+ moveEvent.clientX - event.clientX,
620
+ moveEvent.clientY - event.clientY,
621
+ moveEvent.shiftKey,
622
+ );
623
+ const snapped = computeSnap(nextRect, placements, new Set([placementId]), activeEdges, extraSnapRects);
624
+ const resized = { ...nextRect };
625
+
626
+ if (snapped.dx !== 0) {
627
+ if (activeEdges.right) {
628
+ resized.width = Math.max(MIN_SIZE, resized.width + snapped.dx);
629
+ } else if (activeEdges.left) {
630
+ resized.x += snapped.dx;
631
+ resized.width = Math.max(MIN_SIZE, resized.width - snapped.dx);
632
+ }
633
+ }
634
+
635
+ if (snapped.dy !== 0) {
636
+ if (activeEdges.bottom) {
637
+ resized.height = Math.max(MIN_SIZE, resized.height + snapped.dy);
638
+ } else if (activeEdges.top) {
639
+ resized.y += snapped.dy;
640
+ resized.height = Math.max(MIN_SIZE, resized.height - snapped.dy);
641
+ }
642
+ }
643
+
644
+ guides = snapped.guides;
645
+ sizeIndicator = {
646
+ x: moveEvent.clientX + 12,
647
+ y: moveEvent.clientY + 12,
648
+ text: `${Math.round(resized.width)} x ${Math.round(resized.height)}`,
649
+ };
650
+
651
+ updatePlacements(
652
+ placements.map((candidate) => (candidate.id === placementId ? { ...candidate, ...resized } : candidate)),
653
+ false,
654
+ );
655
+ };
656
+
657
+ const handleUp = () => {
658
+ window.removeEventListener('mousemove', handleMove);
659
+ window.removeEventListener('mouseup', handleUp);
660
+ sizeIndicator = null;
661
+ guides = [];
662
+ interaction = null;
663
+ onInteractionChange?.(false);
664
+ };
665
+
666
+ window.addEventListener('mousemove', handleMove);
667
+ window.addEventListener('mouseup', handleUp);
668
+ }
669
+
670
+ function deletePlacement(id: string) {
671
+ mountManager.unmountComponent(id);
672
+ if (interactingId === id) interactingId = null;
673
+ updatePlacements(placements.filter((placement) => placement.id !== id));
674
+ setSelection(selectedIds.filter((selectedId) => selectedId !== id), false);
675
+ if (editingPlacementId === id) {
676
+ editingPlacementId = null;
677
+ }
678
+ }
679
+
680
+ function handleKeyDown(event: KeyboardEvent) {
681
+ if (event.key === 'Escape' && interactingId) {
682
+ interactingId = null;
683
+ return;
684
+ }
685
+
686
+ if (event.key === 'Escape' && editingPlacementId) {
687
+ closePlacementEditor();
688
+ return;
689
+ }
690
+
691
+ if (event.key === 'Escape') {
692
+ setSelection([], false);
693
+ activeComponent = null;
694
+ onActiveComponentChange?.(null);
695
+ return;
696
+ }
697
+
698
+ if (event.key.toLowerCase() === 'i' && !event.metaKey && !event.ctrlKey && selectedIds.length === 1) {
699
+ event.preventDefault();
700
+ const targetId = selectedIds[0]!;
701
+ interactingId = interactingId === targetId ? null : targetId;
702
+ return;
703
+ }
704
+
705
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a') {
706
+ event.preventDefault();
707
+ setSelection(
708
+ placements.map((placement) => placement.id),
709
+ false,
710
+ );
711
+ return;
712
+ }
713
+
714
+ if (selectedIds.length === 0) return;
715
+
716
+ if (!event.metaKey && !event.ctrlKey && event.key.toLowerCase() === 'd') {
717
+ event.preventDefault();
718
+ const duplicated = placements
719
+ .filter((placement) => selectedIds.includes(placement.id))
720
+ .map((placement) => ({
721
+ ...placement,
722
+ id: generateId(),
723
+ x: Math.max(0, placement.x + 20),
724
+ y: Math.max(0, placement.y + 20),
725
+ timestamp: Date.now(),
726
+ }));
727
+
728
+ if (duplicated.length === 0) return;
729
+
730
+ updatePlacements([...placements, ...duplicated]);
731
+ setSelection(
732
+ duplicated.map((placement) => placement.id),
733
+ false,
734
+ );
735
+ return;
736
+ }
737
+
738
+ if (event.key === 'Delete' || event.key === 'Backspace') {
739
+ event.preventDefault();
740
+ updatePlacements(placements.filter((placement) => !selectedIds.includes(placement.id)));
741
+ setSelection([], false);
742
+ return;
743
+ }
744
+
745
+ if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) return;
746
+
747
+ event.preventDefault();
748
+ const step = event.shiftKey ? 20 : 1;
749
+ const dx = event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0;
750
+ const dy = event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0;
751
+
752
+ updatePlacements(
753
+ placements.map((placement) =>
754
+ selectedIds.includes(placement.id)
755
+ ? { ...placement, x: Math.max(0, placement.x + dx), y: Math.max(0, placement.y + dy) }
756
+ : placement,
757
+ ),
758
+ );
759
+ }
760
+
761
+ $effect(() => {
762
+ if (deselectSignal === lastDeselectSignal) return;
763
+ lastDeselectSignal = deselectSignal;
764
+ queueMicrotask(() => {
765
+ selectedIds = [];
766
+ onSelectionChange?.(new Set(), false);
767
+ });
768
+ });
769
+
770
+ $effect(() => {
771
+ if (clearSignal === lastClearSignal) return;
772
+ lastClearSignal = clearSignal;
773
+ queueMicrotask(() => {
774
+ drawBox = null;
775
+ selectBox = null;
776
+ sizeIndicator = null;
777
+ guides = [];
778
+ pointerStart = null;
779
+ interaction = null;
780
+ onInteractionChange?.(false);
781
+ selectedIds = [];
782
+ onSelectionChange?.(new Set(), false);
783
+ onChange?.([]);
784
+ });
785
+ });
786
+
787
+ // Measure document height for inner scroll container
788
+ $effect(() => {
789
+ function measure() {
790
+ documentHeight = Math.max(document.documentElement.scrollHeight, window.innerHeight);
791
+ }
792
+ measure();
793
+ window.addEventListener('resize', measure);
794
+ return () => window.removeEventListener('resize', measure);
795
+ });
796
+
797
+ // Exit interact mode when clicking outside the interacting placement
798
+ $effect(() => {
799
+ if (!interactingId) return;
800
+ function handleClickOutside(event: MouseEvent) {
801
+ if (!(event.target instanceof HTMLElement)) return;
802
+ const placementEl = event.target.closest(`[data-placement-id="${interactingId}"]`);
803
+ if (!placementEl) {
804
+ interactingId = null;
805
+ }
806
+ }
807
+ window.addEventListener('pointerdown', handleClickOutside, true);
808
+ return () => window.removeEventListener('pointerdown', handleClickOutside, true);
809
+ });
810
+
811
+ onDestroy(() => mountManager.unmountAll());
812
+
813
+ // Svelte action: mount live thumbnail into placement content wrapper
814
+ function mountLiveThumbnail(node: HTMLElement, params: { id: string; type: LayoutModeComponentType }) {
815
+ mountManager.mountComponent(params.id, params.type, node);
816
+ return {
817
+ destroy() {
818
+ mountManager.unmountComponent(params.id);
819
+ },
820
+ };
821
+ }
822
+ </script>
823
+
824
+ <svelte:window onkeydown={handleKeyDown} />
825
+
826
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
827
+ <div
828
+ bind:this={canvasOverlay}
829
+ class={className}
830
+ data-layout-mode
831
+ data-exiting={exiting ? 'true' : 'false'}
832
+ role="application"
833
+ aria-label="Layout mode canvas"
834
+ style="position: fixed; inset: 0; z-index: 1000; pointer-events: auto; overflow: auto; --layout-accent: {layoutAccent()}; --dry-color-fill-brand: var(--layout-accent);"
835
+ onpointerdown={handleOverlayPointerDown}
836
+ >
837
+ <ResponsiveBar value={canvasWidth} onchange={(w) => onCanvasWidthChange?.(w)} />
838
+
839
+ <div
840
+ data-canvas-inner
841
+ style="position: relative; min-height: {documentHeight}px; max-width: {canvasWidth}px; margin: 0 auto;"
842
+ >
843
+ {#if wireframe}
844
+ <div style="position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.02), transparent); pointer-events: none;"></div>
845
+ {/if}
846
+
847
+ {#if placements.length === 0}
848
+ <div style="position: fixed; left: 24px; bottom: 24px; pointer-events: none;">
849
+ <Stack gap="sm">
850
+ <Text as="div" size="md">Layout mode</Text>
851
+ <Text as="div" size="sm" color="secondary">
852
+ Click or drag to place the selected component.
853
+ </Text>
854
+ </Stack>
855
+ </div>
856
+ {/if}
857
+
858
+ {#each placements as placement (placement.id)}
859
+ {@const selected = isSelected(placement.id)}
860
+ {@const interacting = interactingId === placement.id}
861
+ {@const label = placementLabel(placement.type)}
862
+ <div
863
+ data-placement-id={placement.id}
864
+ data-selected={selected ? 'true' : 'false'}
865
+ data-interacting={interacting ? 'true' : 'false'}
866
+ role="button"
867
+ tabindex="0"
868
+ aria-label={label}
869
+ style="
870
+ position: absolute;
871
+ left: {placement.x}px;
872
+ top: {placement.y}px;
873
+ width: {placement.width}px;
874
+ height: {placement.height}px;
875
+ border: {interacting ? '2px solid var(--dry-color-fill-brand, #7c3aed)' : `1px solid color-mix(in srgb, var(--layout-accent) ${selected ? '52%' : '34%'}, transparent)`};
876
+ background: color-mix(in srgb, var(--layout-accent) {selected ? '9%' : '4%'}, transparent);
877
+ box-shadow: 0 10px 28px color-mix(in srgb, var(--layout-accent) {selected ? '18%' : '8%'}, transparent);
878
+ border-radius: 12px;
879
+ overflow: visible;
880
+ cursor: {interacting ? 'default' : 'move'};
881
+ "
882
+ onpointerdown={(event) => {
883
+ if (!interacting) handlePlacementPointerDown(event, placement.id);
884
+ }}
885
+ ondblclick={() => openPlacementEditor(placement.id)}
886
+ >
887
+ <span
888
+ data-design-placement-label
889
+ style="
890
+ position: absolute;
891
+ left: 0;
892
+ bottom: calc(100% + 6px);
893
+ font-size: 11px;
894
+ line-height: 1;
895
+ font-weight: 600;
896
+ color: color-mix(in srgb, var(--dry-color-text-strong, #111827) 72%, transparent);
897
+ pointer-events: none;
898
+ white-space: nowrap;
899
+ "
900
+ >
901
+ {label}{#if interacting} (interact){/if}
902
+ </span>
903
+ {#if placement.text}
904
+ <span
905
+ data-design-placement-note
906
+ style="
907
+ position: absolute;
908
+ left: 0;
909
+ top: calc(100% + 6px);
910
+ max-width: min(22rem, 100vw - 48px);
911
+ font-size: 11px;
912
+ line-height: 1.35;
913
+ color: color-mix(in srgb, var(--dry-color-text-strong, #111827) 56%, transparent);
914
+ pointer-events: none;
915
+ "
916
+ >
917
+ {placement.text}
918
+ </span>
919
+ {/if}
920
+ <div
921
+ data-design-placement-content
922
+ style="
923
+ inline-size: 100%;
924
+ block-size: 100%;
925
+ overflow: hidden;
926
+ border-radius: inherit;
927
+ pointer-events: {interacting ? 'auto' : 'none'};
928
+ background: color-mix(in srgb, var(--dry-color-bg-overlay, #fff) 92%, transparent);
929
+ backdrop-filter: blur(8px);
930
+ "
931
+ use:mountLiveThumbnail={{ id: placement.id, type: placement.type }}
932
+ ></div>
933
+ {#if selected && !interacting}
934
+ <button
935
+ type="button"
936
+ aria-label={`Delete ${label}`}
937
+ data-testid="design-delete-btn"
938
+ style="
939
+ position: absolute;
940
+ top: -16px;
941
+ right: -16px;
942
+ inline-size: 22px;
943
+ block-size: 22px;
944
+ border-radius: 999px;
945
+ border: 1px solid color-mix(in srgb, var(--layout-accent) 28%, transparent);
946
+ background: var(--dry-color-bg-overlay, #fff);
947
+ box-shadow: 0 6px 14px rgba(15, 23, 42, 0.16);
948
+ color: color-mix(in srgb, var(--dry-color-text-strong, #111827) 72%, transparent);
949
+ cursor: pointer;
950
+ z-index: 30;
951
+ "
952
+ onpointerdown={(event) => event.stopPropagation()}
953
+ onclick={(event) => {
954
+ event.stopPropagation();
955
+ deletePlacement(placement.id);
956
+ }}
957
+ >
958
+ x
959
+ </button>
960
+ {/if}
961
+ {#if selected && selectedIds.length === 1 && !interacting}
962
+ {#each CORNER_HANDLES as handle (handle)}
963
+ <button
964
+ type="button"
965
+ aria-label={`Resize ${handle}`}
966
+ style={`
967
+ position: absolute;
968
+ width: 8px;
969
+ height: 8px;
970
+ border-radius: 3px;
971
+ border: 1px solid var(--dry-color-bg-overlay, #fff);
972
+ background: var(--dry-color-bg-overlay, #fff);
973
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--layout-accent) 36%, transparent);
974
+ pointer-events: auto;
975
+ z-index: 20;
976
+ cursor: ${handleCursor(handle)};
977
+ ${cornerHandleOffset(handle)}
978
+ `}
979
+ onmousedown={(event) => startResize(event, placement.id, handle)}
980
+ ></button>
981
+ {/each}
982
+ {#each EDGE_HANDLES as handle (handle)}
983
+ <button
984
+ type="button"
985
+ aria-label={`Resize ${handle}`}
986
+ style={`
987
+ position: absolute;
988
+ display: flex;
989
+ align-items: center;
990
+ justify-content: center;
991
+ padding: 0;
992
+ border: none;
993
+ background: transparent;
994
+ pointer-events: auto;
995
+ z-index: 18;
996
+ cursor: ${handleCursor(handle)};
997
+ ${edgeHandleInset(handle)}
998
+ `}
999
+ onmousedown={(event) => startResize(event, placement.id, handle)}
1000
+ >
1001
+ <span
1002
+ aria-hidden="true"
1003
+ style={`
1004
+ position: absolute;
1005
+ border-radius: 999px;
1006
+ background: color-mix(in srgb, var(--layout-accent) 82%, white);
1007
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--layout-accent) 20%, transparent);
1008
+ ${handle === 'n' || handle === 's' ? 'width: 24px; height: 4px;' : 'width: 4px; height: 24px;'}
1009
+ `}
1010
+ ></span>
1011
+ <svg
1012
+ aria-hidden="true"
1013
+ viewBox="0 0 12 12"
1014
+ style="position: relative; z-index: 1; width: 12px; height: 12px; opacity: 0.72; color: var(--dry-color-bg-overlay, #fff); filter: drop-shadow(0 0 2px color-mix(in srgb, var(--dry-color-text-strong, #111827) 18%, transparent));"
1015
+ >
1016
+ {#if handle === 'n' || handle === 's'}
1017
+ <path d="M6 2 L3.5 4.5 H8.5 Z" fill="currentColor"></path>
1018
+ <path d="M6 10 L3.5 7.5 H8.5 Z" fill="currentColor"></path>
1019
+ {:else}
1020
+ <path d="M2 6 L4.5 3.5 V8.5 Z" fill="currentColor"></path>
1021
+ <path d="M10 6 L7.5 3.5 V8.5 Z" fill="currentColor"></path>
1022
+ {/if}
1023
+ </svg>
1024
+ </button>
1025
+ {/each}
1026
+ {/if}
1027
+ </div>
1028
+ {/each}
1029
+
1030
+ {#if editingPlacementId}
1031
+ {@const editingPlacement = placementAt(editingPlacementId)}
1032
+ {#if editingPlacement}
1033
+ {#key editingPlacement.id}
1034
+ <AnnotationPopup
1035
+ element={editingPlacement.type}
1036
+ fieldLabel="Text"
1037
+ helperText="Use this to describe the component label or content."
1038
+ initialValue={editingPlacement.text ?? ''}
1039
+ placeholder={TEXT_PLACEHOLDERS[editingPlacement.type] ?? 'Label or content text'}
1040
+ position={notePopupPosition(editingPlacement)}
1041
+ showDelete={Boolean(editingPlacement.text)}
1042
+ submitLabel={editingPlacement.text ? 'Save' : 'Set'}
1043
+ onsubmit={savePlacementText}
1044
+ oncancel={closePlacementEditor}
1045
+ ondelete={clearPlacementText}
1046
+ />
1047
+ {/key}
1048
+ {/if}
1049
+ {/if}
1050
+
1051
+ {#if drawBox}
1052
+ <div
1053
+ style="
1054
+ position: absolute;
1055
+ left: {drawBox.x}px;
1056
+ top: {drawBox.y}px;
1057
+ width: {drawBox.width}px;
1058
+ height: {drawBox.height}px;
1059
+ border: 1px dashed var(--layout-accent);
1060
+ background: color-mix(in srgb, var(--layout-accent) 8%, transparent);
1061
+ pointer-events: none;
1062
+ "
1063
+ ></div>
1064
+ {/if}
1065
+
1066
+ {#if selectBox}
1067
+ <div
1068
+ data-design-select-box
1069
+ style="
1070
+ position: absolute;
1071
+ left: {selectBox.x}px;
1072
+ top: {selectBox.y}px;
1073
+ width: {selectBox.width}px;
1074
+ height: {selectBox.height}px;
1075
+ border: 1px dashed var(--layout-accent);
1076
+ background: color-mix(in srgb, var(--layout-accent) 10%, transparent);
1077
+ pointer-events: none;
1078
+ "
1079
+ ></div>
1080
+ {/if}
1081
+
1082
+ {#each guides as guide, index (index)}
1083
+ <div
1084
+ style={`position: absolute; pointer-events: none; background: var(--layout-accent); opacity: 0.4; ${guide.axis === 'x' ? `left: ${guide.pos}px; top: 0; width: 1px; height: 100%;` : `top: ${guide.pos}px; left: 0; height: 1px; width: 100%;`}`}
1085
+ ></div>
1086
+ {/each}
1087
+ </div>
1088
+
1089
+ {#if sizeIndicator}
1090
+ <div
1091
+ style="
1092
+ position: fixed;
1093
+ left: {sizeIndicator.x}px;
1094
+ top: {sizeIndicator.y}px;
1095
+ z-index: 1002;
1096
+ padding: 6px 10px;
1097
+ border-radius: 999px;
1098
+ background: var(--dry-color-bg-overlay, #fff);
1099
+ border: 1px solid var(--dry-color-stroke-weak, #ddd);
1100
+ pointer-events: none;
1101
+ "
1102
+ >
1103
+ <Text as="div" size="sm">{sizeIndicator.text}</Text>
1104
+ </div>
1105
+ {/if}
1106
+
1107
+ {#if activeComponent}
1108
+ <div style="position: fixed; right: 16px; bottom: 16px; pointer-events: none;">
1109
+ <Stack gap="sm" align="end">
1110
+ <Badge variant="solid">{activeComponent}</Badge>
1111
+ <Text as="div" size="sm" color="secondary">Drag on the page to size the placement.</Text>
1112
+ </Stack>
1113
+ </div>
1114
+ {/if}
1115
+ </div>