@widgetic/editor 4.0.9 → 4.0.11

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 (55) hide show
  1. package/README.md +118 -0
  2. package/dist/components/WidgetPreview.svelte +65 -27
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/editors/CompositionEditorApp.svelte +508 -0
  5. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +75 -0
  6. package/dist/components/editors/PropsEditor.svelte +193 -792
  7. package/dist/components/editors/PropsEditor.svelte.d.ts +7 -0
  8. package/dist/components/ic/BrowserInputController.svelte +21 -2
  9. package/dist/components/ic/ColorPickerInputController.svelte +24 -6
  10. package/dist/components/ic/DateFieldInputController.svelte +7 -3
  11. package/dist/components/ic/DatePickerInputController.svelte +10 -4
  12. package/dist/components/ic/DropdownInputController.svelte +11 -4
  13. package/dist/components/ic/FontInputController.svelte +68 -44
  14. package/dist/components/ic/InputControllerHeader.svelte +11 -8
  15. package/dist/components/ic/NumberInputController.svelte +4 -2
  16. package/dist/components/ic/PositionInputController.svelte +14 -11
  17. package/dist/components/ic/RangeCalendarInputController.svelte +10 -4
  18. package/dist/components/ic/RoundedCornersInputController.svelte +17 -9
  19. package/dist/components/ic/TextAreaInputController.svelte +20 -7
  20. package/dist/components/ic/TextAreaPreviewInputController.svelte +19 -69
  21. package/dist/components/ic/TimeInputController.svelte +9 -3
  22. package/dist/components/ic/VectorInputController.svelte +80 -54
  23. package/dist/components/ic/shared/ItemGrid.svelte +5 -1
  24. package/dist/composition/compositionApi.d.ts +95 -0
  25. package/dist/composition/compositionApi.js +289 -0
  26. package/dist/composition/compositionEditorController.d.ts +254 -0
  27. package/dist/composition/compositionEditorController.js +734 -0
  28. package/dist/composition/compositionHelpers.d.ts +72 -0
  29. package/dist/composition/compositionHelpers.js +403 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +4 -0
  32. package/dist/mediaLibraryContext.d.ts +10 -0
  33. package/dist/mediaLibraryContext.js +30 -0
  34. package/dist/utils/FontManager.d.ts +21 -5
  35. package/dist/utils/FontManager.js +62 -67
  36. package/dist/utils/VectorManager.d.ts +0 -1
  37. package/dist/utils/VectorManager.js +10 -12
  38. package/dist/utils/editorStorage.d.ts +23 -0
  39. package/dist/utils/editorStorage.js +95 -0
  40. package/dist/utils/fontUtils.js +5 -3
  41. package/dist/utils/htmlSafety.d.ts +21 -0
  42. package/dist/utils/htmlSafety.js +38 -0
  43. package/dist/utils/itemReorder.d.ts +90 -0
  44. package/dist/utils/itemReorder.js +354 -0
  45. package/dist/utils/recentColors.js +4 -2
  46. package/dist/utils/richTextPreview.d.ts +38 -0
  47. package/dist/utils/richTextPreview.js +207 -0
  48. package/dist/utils/scrollActivity.d.ts +14 -0
  49. package/dist/utils/scrollActivity.js +63 -0
  50. package/dist/utils/shadowDom.d.ts +67 -0
  51. package/dist/utils/shadowDom.js +211 -0
  52. package/dist/utils/vectorUtils.js +5 -3
  53. package/dist/utils/visibilityCondition.d.ts +40 -0
  54. package/dist/utils/visibilityCondition.js +535 -0
  55. package/package.json +6 -5
@@ -78,7 +78,19 @@
78
78
  import InputControllerRenderer, { type ControllerDescriptor } from '../ic/InputControllerRenderer.svelte';
79
79
  import MediaManager from '../../utils/MediaManager';
80
80
  import FontManager from '../../utils/FontManager';
81
+ import { conditionHolds } from '../../utils/visibilityCondition';
82
+ import { removeEditorStorageValuesStartingWith } from '../../utils/editorStorage';
83
+ import { reorderableList, moveListItem, type ItemReorderOptions } from '../../utils/itemReorder';
84
+ import {
85
+ lockPointerStyle,
86
+ queryAllInTreeOf,
87
+ queryInTreeOf,
88
+ treeOf,
89
+ type SearchableTree
90
+ } from '../../utils/shadowDom';
81
91
  import { config as appConfig } from '../../config';
92
+ import { provideEditorMediaLibrary } from '../../mediaLibraryContext';
93
+ import type { MediaLibraryProvider } from '@widgetic/file-browser';
82
94
 
83
95
  /**
84
96
  * STYLES from Widgetic Design System
@@ -161,6 +173,16 @@
161
173
  export let initialDesignValues: Record<string, any> = {};
162
174
  export let initialContentValues: Record<string, any> = {};
163
175
 
176
+ /**
177
+ * Where the file controls (images, video, audio, documents) get their files from. Without it they show the
178
+ * Widgetic account's uploads, as they always have (configured with `setFileBrowserUploadConfig`). With a
179
+ * `MediaLibraryProvider` they list, upload, rename and delete through its functions instead, so a partner's own
180
+ * storage (a CMS, an S3 bucket, a DAM) can be used with no Widgetic storage at all.
181
+ */
182
+ export let mediaLibrary: MediaLibraryProvider | undefined = undefined;
183
+ // The file controls are created from the schema several components down: give them the library through context.
184
+ provideEditorMediaLibrary(() => mediaLibrary);
185
+
164
186
  /**
165
187
  * CONSTANTS
166
188
  */
@@ -254,18 +276,9 @@
254
276
 
255
277
  // Clear font weight keys immediately when script loads
256
278
  if (browser) {
257
- const keysToRemove: string[] = [];
258
- for (let i = 0; i < localStorage.length; i++) {
259
- const key = localStorage.key(i);
260
- if (key && key.includes('font-weight-state-')) {
261
- keysToRemove.push(key);
262
- }
263
- }
264
-
265
- if (keysToRemove.length > 0) {
266
- // console.log('🧹 [PropsEditor] Clearing font weight states on init:', keysToRemove);
267
- keysToRemove.forEach(key => localStorage.removeItem(key));
268
- }
279
+ // Only the editor's own keys are looked at: this used to walk the whole localStorage and remove every key with
280
+ // this text in it, and inside a customer's page that reaches keys that are the customer's.
281
+ removeEditorStorageValuesStartingWith('font-weight-state-');
269
282
  }
270
283
 
271
284
  /**
@@ -356,521 +369,71 @@
356
369
  }
357
370
  };
358
371
 
359
- let isDraggableInitialized = false;
360
- let currentDraggable: any = null;
361
- let isInitializing = false;
362
-
363
- const initializeDraggable = async () => {
364
- // Prevent multiple simultaneous initializations
365
- if (isInitializing) {
366
- // console.log('🔄 Already initializing draggable, skipping...');
367
- return;
368
- }
372
+ /**
373
+ * REORDERING CONTENT ITEMS
374
+ * Dragging an item's handle moves the item in the list. The dragging itself is in utils/itemReorder.ts, which is
375
+ * not a library because the editor can be inside a shadow root on a customer's page (see the file for why no
376
+ * library works there). While an item is dragged the stylesheet folds every item to one row so the whole list is
377
+ * within reach (an item that is open keeps its content, it is only hidden), so nothing has to be closed and
378
+ * opened again around a drag.
379
+ */
380
+ let isReordering = false;
369
381
 
370
- isInitializing = true;
371
- // console.log('🔄 [PropsEditor] Starting draggable initialization');
382
+ function handleReorderStart() {
383
+ isReordering = true;
384
+ }
372
385
 
386
+ /** An item was dropped at another place in the list: `oldIndex` and `newIndex` are its places before and after. */
387
+ function handleItemsReordered({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
373
388
  try {
374
- if (contentSchema && typeof window !== 'undefined') {
375
- await new Promise(resolve => setTimeout(resolve, 0));
376
-
377
- // Target only the content-items container
378
- const containerElement = document.querySelector('.content-items');
379
- // console.log('🔄 [PropsEditor] Found content container:', containerElement);
380
-
381
- if (containerElement instanceof HTMLElement) {
382
-
383
-
384
- if (currentDraggable) {
385
- // console.log('🧹 Cleaning up existing draggable instance');
386
- currentDraggable.destroy();
387
- currentDraggable = null;
388
- }
389
-
390
- const { Sortable, Plugins } = await import('@shopify/draggable');
391
- const { SortAnimation } = Plugins;
392
-
393
- let dragStartTime = 0;
394
- let isDragStarted = false;
395
- let dragThreshold = 0; // pixels to move before considering it a drag
396
- let initialMousePosition = { x: 0, y: 0 };
397
- let grabOffsetY = 0;
398
- let grabOffsetX = 0;
399
-
400
- // Add forceCleanupState function
401
- function forceCleanupState() {
402
- // console.log('🧹 Performing emergency state cleanup');
403
-
404
- // Clean up any existing mirror elements first
405
- const mirrors = document.querySelectorAll('.draggable-mirror');
406
- mirrors.forEach(mirror => {
407
- if (mirror.parentNode) {
408
- mirror.parentNode.removeChild(mirror);
409
- }
410
- });
411
-
412
- // Reset all global state
413
- isDragging = false;
414
- isDragStarted = false;
415
- document.documentElement.classList.remove('draggable--is-dragging');
416
-
417
- // Remove dragging classes from body and document
418
- document.body.classList.remove('dragging-active');
419
- document.documentElement.classList.remove('dragging-active');
420
-
421
- // Force cleanup all items
422
- document.querySelectorAll('.content-item').forEach((item: Element) => {
423
- if (item instanceof HTMLElement) {
424
- // Remove all possible dragging classes
425
- item.classList.remove('draggable-source--is-dragging');
426
- item.classList.remove('draggable--over');
427
- item.classList.remove('draggable--original');
428
- item.classList.remove('draggable-source--placed');
429
- item.classList.remove('draggable-mirror');
430
-
431
- // Reset all possible styles
432
- item.style.transform = '';
433
- item.style.transition = 'none';
434
- // Ensure inline order values are cleared so visual order matches DOM order
435
- item.style.order = '';
436
- void item.offsetHeight; // Force reflow
437
- item.style.transition = '';
438
- item.style.position = 'relative';
439
- item.style.zIndex = '1';
440
- item.style.opacity = '1';
441
- item.style.visibility = 'visible';
442
- item.style.pointerEvents = '';
443
- item.style.cursor = 'pointer';
444
-
445
- // Update collapsible state based on our openItems
446
- const itemId = item.getAttribute('data-id');
447
- const isOpen = itemId && openItems.has(itemId);
448
-
449
- // Find the collapsible elements
450
- const collapsibleRoot = item.querySelector('[data-radix-collapsible-root]');
451
- const trigger = item.querySelector('[data-radix-collapsible-trigger]');
452
- const content = item.querySelector('[data-radix-collapsible-content]');
453
- const chevron = item.querySelector('.chevron');
454
-
455
- // Update states
456
- if (collapsibleRoot instanceof HTMLElement) {
457
- collapsibleRoot.setAttribute('data-state', isOpen ? 'open' : 'closed');
458
- }
459
-
460
- if (trigger instanceof HTMLElement) {
461
- trigger.setAttribute('data-state', isOpen ? 'open' : 'closed');
462
- trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
463
- }
464
-
465
- if (content instanceof HTMLElement) {
466
- if (!isOpen) {
467
- content.style.height = '0';
468
- content.style.overflow = 'hidden';
469
- } else {
470
- content.style.height = 'auto';
471
- content.style.overflow = 'visible';
472
- }
473
- content.setAttribute('data-state', isOpen ? 'open' : 'closed');
474
- content.style.display = ''; // Always reset display property
475
- }
476
-
477
- if (chevron instanceof HTMLElement) {
478
- chevron.style.transform = isOpen ? 'rotate(180deg)' : 'rotate(0deg)';
479
- }
480
- }
481
- });
482
-
483
- // Clean container
484
- const container = document.querySelector('.content-items');
485
- if (container instanceof HTMLElement) {
486
- container.classList.remove('content-items--is-dragging');
487
- container.classList.remove('dragging-active');
488
- container.classList.remove('draggable-container--over');
489
- container.style.pointerEvents = '';
490
- container.style.cursor = '';
491
-
492
- // Force reflow
493
- void container.offsetHeight;
494
- }
495
- }
496
-
497
- currentDraggable = new Sortable(containerElement, {
498
- draggable: '.content-item',
499
- handle: '.drag-handle',
500
- delay: 0,
501
- mirror: {
502
- constrainDimensions: true,
503
- appendTo: 'body',
504
- xAxis: true,
505
- yAxis: true
506
- },
507
- plugins: [SortAnimation],
508
- sortAnimation: {
509
- duration: 200,
510
- easingFunction: 'ease-in-out'
511
- },
512
- classes: {
513
- 'body:dragging': 'draggable--is-dragging',
514
- 'container:dragging': 'dragging-active',
515
- 'source:dragging': 'draggable-source--is-dragging',
516
- 'source:placed': 'draggable-source--placed',
517
- 'container:placed': 'draggable-container--placed',
518
- 'draggable:over': 'draggable--over',
519
- 'container:over': 'draggable-container--over',
520
- 'source:original': 'draggable--original',
521
- 'mirror': 'draggable-mirror'
522
- }
523
- });
524
-
525
-
526
-
527
- // Update event handlers
528
- currentDraggable.on('drag:start', (event: any) => {
529
- // console.log('[Draggable] drag:start event');
530
- isDragging = true; // Set flag immediately to prevent afterUpdate interference
531
-
532
- const dragHandle = event.originalSource?.querySelector('.drag-handle');
533
- const target = event.originalEvent?.target;
534
-
535
- if (!dragHandle?.contains(target)) {
536
- event.cancel();
537
- return;
538
- }
539
-
540
- // Store initial mouse position for threshold calculation
541
- if (event.originalEvent instanceof MouseEvent) {
542
- initialMousePosition = {
543
- x: event.originalEvent.clientX,
544
- y: event.originalEvent.clientY
545
- };
546
- }
547
-
548
- // Store previously open items
549
- previouslyOpen = new Set(openItems);
550
-
551
- // Reset open items
552
- openItems = new Set();
553
-
554
- // Add a class to body to trigger our CSS rules
555
- document.body.classList.add('dragging-active');
556
-
557
- // Force reflow to apply height changes immediately
558
- document.body.offsetHeight;
559
-
560
- // Add design system classes to all content items for consistent styling
561
- document.querySelectorAll('.content-item').forEach(item => {
562
- item.classList.add('widgetic-colors', 'widgetic-radius');
563
- });
564
-
565
- // Force visual update of all dropdown chevrons
566
- document.querySelectorAll('.content-item .chevron').forEach(chevron => {
567
- if (chevron instanceof HTMLElement) {
568
- chevron.style.transform = 'rotate(0deg)';
569
- }
570
- });
571
- });
572
-
573
- // Add drag:move handler - update mirror position to follow mouse
574
- currentDraggable.on('drag:move', (event: any) => {
575
- if (isDragStarted) {
576
- // Update mirror position to follow mouse cursor, accounting for scroll
577
- if (event.originalEvent instanceof MouseEvent) {
578
- requestAnimationFrame(() => {
579
- const mirror = document.querySelector('.draggable-mirror');
580
- if (mirror instanceof HTMLElement) {
581
- // Position mirror accounting for grab offset
582
- mirror.style.top = `${event.originalEvent.clientY - grabOffsetY}px`;
583
- mirror.style.left = `${event.originalEvent.clientX - grabOffsetX}px`;
584
- mirror.style.transform = 'translate3d(0,0,0)';
585
- }
586
- });
587
- }
588
- return;
589
- }
590
-
591
- if (event.originalEvent instanceof MouseEvent) {
592
- const deltaX = Math.abs(event.originalEvent.clientX - initialMousePosition.x);
593
- const deltaY = Math.abs(event.originalEvent.clientY - initialMousePosition.y);
594
-
595
- // Only start drag if moved more than threshold
596
- if (deltaX > dragThreshold || deltaY > dragThreshold) {
597
- isDragStarted = true;
598
- // isDragging = true; - This is now set in drag:start
599
- // console.log('[Draggable] isDragStarted set to true');
600
-
601
- requestAnimationFrame(() => {
602
- const mirror = document.querySelector('.draggable-mirror');
603
- if (mirror instanceof HTMLElement) {
604
- // First, snap to the correct position to prevent flickering
605
- mirror.style.top = `${event.originalEvent.clientY - grabOffsetY}px`;
606
- mirror.style.left = `${event.originalEvent.clientX - grabOffsetX}px`;
607
- mirror.style.transform = 'translate3d(0,0,0)';
608
-
609
- // Then apply styling
610
- mirror.style.height = '56px';
611
- mirror.style.overflow = 'hidden';
612
- mirror.style.border = '2px solid hsl(var(--grey2))';
613
- mirror.style.borderRadius = 'var(--radius-small)';
614
- mirror.style.backgroundColor = 'white';
615
- mirror.style.boxShadow = 'none';
616
-
617
- // Add our styling classes without changing any positioning
618
- mirror.classList.add('widgetic-colors', 'widgetic-radius');
619
-
620
- // Remove borders from all children
621
- Array.from(mirror.querySelectorAll('*')).forEach(el => {
622
- if (el instanceof HTMLElement) {
623
- el.style.border = 'none';
624
- el.style.borderTop = 'none';
625
- el.style.borderBottom = 'none';
626
- el.style.borderLeft = 'none';
627
- el.style.borderRight = 'none';
628
- }
629
- });
630
-
631
- // Ensure the mirror content is in collapsed state
632
- const collapsibleContent = mirror.querySelector('[data-radix-collapsible-content]');
633
- if (collapsibleContent instanceof HTMLElement) {
634
- collapsibleContent.style.height = '0';
635
- collapsibleContent.style.display = 'none';
636
- }
637
- }
638
- });
639
- } else {
640
- // Cancel the drag if we haven't moved enough
641
- event.cancel();
642
- }
643
- }
644
- });
645
-
646
- currentDraggable.on('drag:stop', (event: any) => {
647
- // Clean up any mirror elements immediately
648
- requestAnimationFrame(() => {
649
- const mirrors = document.querySelectorAll('.draggable-mirror');
650
- mirrors.forEach(mirror => {
651
- if (mirror.parentNode) {
652
- mirror.parentNode.removeChild(mirror);
653
- }
654
- });
655
- });
656
-
657
- // If we never started dragging, this was just a click
658
- if (!isDragStarted) {
659
- event.cancel();
660
- forceCleanupState();
661
- return;
662
- }
663
-
664
- // Reset drag state
665
- isDragStarted = false;
666
- isDragging = false;
667
- dragStartTime = 0;
668
- initialMousePosition = { x: 0, y: 0 };
669
- grabOffsetY = 0;
670
- grabOffsetX = 0;
671
-
672
- // Remove the class that forces fixed heights
673
- document.body.classList.remove('dragging-active');
674
-
675
- // Reset fixed heights from all items
676
- document.querySelectorAll('.content-item[data-forced-collapsed="true"]').forEach(item => {
677
- if (item instanceof HTMLElement) {
678
- item.style.height = '';
679
- item.style.minHeight = '';
680
- item.style.maxHeight = '';
681
- item.style.overflow = '';
682
- item.removeAttribute('data-forced-collapsed');
683
- }
684
- });
685
-
686
- // Reset all collapsible content elements
687
- document.querySelectorAll('[data-radix-collapsible-content]').forEach(content => {
688
- if (content instanceof HTMLElement) {
689
- content.style.display = '';
690
- }
691
- });
692
-
693
- // Restore previously open items
694
- setTimeout(() => {
695
- openItems = new Set(previouslyOpen);
696
-
697
- // Force update collapsible elements to match our state
698
- document.querySelectorAll('[data-radix-collapsible-root]').forEach(root => {
699
- if (root instanceof HTMLElement) {
700
- const itemId = root.closest('.content-item')?.getAttribute('data-id');
701
- if (itemId && openItems.has(itemId)) {
702
- root.setAttribute('data-state', 'open');
703
- const trigger = root.querySelector('[data-radix-collapsible-trigger]');
704
- if (trigger instanceof HTMLElement) {
705
- trigger.setAttribute('data-state', 'open');
706
- trigger.setAttribute('aria-expanded', 'true');
707
- }
708
- const chevron = root.querySelector('.chevron');
709
- if (chevron instanceof HTMLElement) {
710
- chevron.style.transform = 'rotate(180deg)';
711
- }
712
- const content = root.querySelector('[data-radix-collapsible-content]');
713
- if (content instanceof HTMLElement) {
714
- content.style.height = 'auto';
715
- content.style.overflow = 'visible';
716
- }
717
- }
718
- }
719
- });
720
- }, 50);
721
-
722
- // Perform normal cleanup
723
- forceCleanupState();
724
- });
725
-
726
- currentDraggable.on('sortable:sorted', (event: { newIndex: number; oldIndex: number }) => {
727
- // console.log('[Draggable] sortable:sorted event fired', { oldIndex: event.oldIndex, newIndex: event.newIndex });
728
- try {
729
- const { oldIndex, newIndex } = event;
730
-
731
- // Add robust checks for valid indices
732
- if (typeof oldIndex !== 'number' || typeof newIndex !== 'number' || oldIndex < 0 || newIndex < 0) {
733
- console.error('[Drag] Invalid or missing indices from event. Aborting reorder.');
734
- isDragStarted = false;
735
- return;
736
- }
737
-
738
- // No need to do anything if the item was dropped in the same place
739
- if (oldIndex === newIndex) {
740
- // console.log('[Drag] Item dropped in the same position. No change needed.');
741
- isDragStarted = false;
742
- return;
743
- }
744
-
745
- if (!Array.isArray(contentSchema?.contentItems)) {
746
- console.warn('[Drag] contentItems is not an array, skipping reorder');
747
- isDragStarted = false;
748
- return;
749
- }
750
- // console.log('[Drag] Before:', contentSchema.contentItems.map((i: ContentItem) => ({ id: i.id, title: i.title })));
389
+ if (!Array.isArray(contentSchema?.contentItems)) {
390
+ console.warn('[Reorder] contentItems is not an array, skipping reorder');
391
+ return;
392
+ }
751
393
 
752
- const items = [...contentSchema.contentItems];
753
- const [movedItem] = items.splice(oldIndex, 1);
754
- items.splice(newIndex, 0, movedItem);
394
+ // Robust checks for valid indices
395
+ const itemCount = contentSchema.contentItems.length;
396
+ if (
397
+ !Number.isInteger(oldIndex) || !Number.isInteger(newIndex) ||
398
+ oldIndex < 0 || newIndex < 0 || oldIndex >= itemCount || newIndex >= itemCount
399
+ ) {
400
+ console.error('[Reorder] Invalid indices for the dropped item. Aborting reorder.', { oldIndex, newIndex, itemCount });
401
+ return;
402
+ }
755
403
 
756
- // Safety checks before updating state
757
- if (hasDuplicateIds(items)) {
758
- console.error('[Drag] Duplicate IDs detected after reordering with indices. Aborting update.', items.map(i => i.id));
759
- isDragStarted = false;
760
- return;
761
- }
762
-
763
- if (items.length !== contentSchema.contentItems.length) {
764
- console.error('[Drag] Array length mismatch after reordering with indices. Aborting update.');
765
- isDragStarted = false;
766
- return;
767
- }
768
-
769
- const renumbered = renumberAutoItemTitles(items);
770
- contentSchema = { ...contentSchema, contentItems: renumbered };
771
- // console.log('[Drag] After (reordered with indices):', contentSchema.contentItems.map((i: ContentItem) => ({ id: i.id, title: i.title })));
772
- dispatch('updated', { contentSchema });
773
- dispatchContentItemsChanged(contentSchema.contentItems);
774
-
775
- } catch (err) {
776
- console.error('[Drag] Error updating order in sortable:sorted:', err);
777
- }
778
- isDragStarted = false;
779
- isDragging = false; // Allow afterUpdate to run again
780
- });
404
+ const items = moveListItem<ContentItem>(contentSchema.contentItems, oldIndex, newIndex);
781
405
 
782
- // Update mirror handling - position mirror to follow mouse cursor directly
783
- currentDraggable.on('mirror:created', (event: any) => {
784
- const mirror = event.mirror;
785
- if (mirror instanceof HTMLElement) {
786
- // Add widgetic-colors class to access CSS variables
787
- mirror.classList.add('widgetic-colors', 'widgetic-radius');
788
-
789
- // Set border color to sage-green using CSS variable
790
- mirror.style.border = '2px solid hsl(var(--sage-green))';
791
- mirror.style.borderRadius = 'var(--radius-small)';
792
- mirror.style.boxShadow = 'none';
793
-
794
- // Style the mirror element, but do NOT set top, left, or transform.
795
- mirror.style.height = '56px';
796
- mirror.style.overflow = 'hidden';
797
- mirror.style.position = 'fixed';
798
- mirror.style.width = `${event.originalSource.offsetWidth}px`;
799
- mirror.style.backgroundColor = 'white';
800
- mirror.style.pointerEvents = 'none'; // Ensure it doesn't interfere with mouse events
801
- mirror.style.zIndex = '1000'; // Ensure it's above other elements
802
-
803
- // Position mirror at exact mouse cursor location
804
- const mouseEvent = event.originalEvent;
805
- if (mouseEvent instanceof MouseEvent) {
806
- // Position mirror centered on mouse cursor
807
- const mirrorHeight = 56; // Fixed height we set
808
- const mirrorWidth = event.originalSource.offsetWidth;
809
-
810
- // Center the mirror on the mouse cursor
811
- grabOffsetY = mirrorHeight / 2;
812
- grabOffsetX = mirrorWidth / 2 + DRAG_SHIFT_X;
813
-
814
- // Position mirror so mouse is at center
815
- mirror.style.top = `${mouseEvent.clientY - grabOffsetY}px`;
816
- mirror.style.left = `${mouseEvent.clientX - grabOffsetX}px`;
817
- mirror.style.transform = 'translate3d(0,0,0)';
818
- }
819
-
820
- // Style text with sage-green CSS variable
821
- const textElements = mirror.querySelectorAll('h4, .content-title, span');
822
- textElements.forEach(el => {
823
- if (el instanceof HTMLElement) {
824
- el.style.color = 'hsl(var(--sage-green))';
825
- }
826
- });
827
-
828
- // Style only chevrons with sage-green CSS variable
829
- const chevrons = mirror.querySelectorAll('.chevron, svg');
830
- chevrons.forEach(el => {
831
- if (el instanceof SVGElement) {
832
- el.style.stroke = 'hsl(var(--sage-green))';
833
- el.style.fill = 'transparent';
834
-
835
- // Also handle any paths inside SVGs
836
- const paths = el.querySelectorAll('path');
837
- paths.forEach(path => {
838
- path.setAttribute('stroke', 'hsl(var(--sage-green))');
839
- path.setAttribute('fill', 'transparent');
840
- });
841
-
842
- // Check if this item was previously open and rotate the chevron accordingly
843
- const sourceId = event.originalSource.getAttribute('data-id');
844
- if (sourceId && previouslyOpen && previouslyOpen.has(sourceId) && el.classList.contains('chevron')) {
845
- // Apply the same transition effect as the normal open/close operation
846
- el.style.transition = 'transform 200ms ease-in-out';
847
- el.style.transform = 'rotate(360deg)';
848
- }
849
- }
850
- });
851
- }
852
- });
406
+ // Safety checks before updating state
407
+ if (hasDuplicateIds(items)) {
408
+ console.error('[Reorder] Duplicate IDs detected after reordering with indices. Aborting update.', items.map(i => i.id));
409
+ return;
410
+ }
411
+ if (items.length !== itemCount) {
412
+ console.error('[Reorder] Array length mismatch after reordering with indices. Aborting update.');
413
+ return;
414
+ }
853
415
 
854
- // After initialisation, force-clear any inline transforms left by previous Sortable instances or the plugin
855
- Array.from(containerElement.children).forEach(child => {
856
- const el = child as HTMLElement;
857
- el.style.transform = '';
858
- el.style.transition = '';
859
- el.style.order = '';
860
- });
416
+ const renumbered = renumberAutoItemTitles(items);
417
+ contentSchema = { ...contentSchema, contentItems: renumbered };
418
+ dispatch('updated', { contentSchema });
419
+ dispatchContentItemsChanged(contentSchema.contentItems);
420
+ } catch (err) {
421
+ console.error('[Reorder] Error updating the order after a drop:', err);
422
+ }
423
+ }
861
424
 
862
- logOrder('after init');
425
+ /** The drag is over (dropped or cancelled): the list is drawn as usual again. */
426
+ function handleReorderEnd() {
427
+ isReordering = false;
428
+ }
863
429
 
864
- isDraggableInitialized = true;
865
- }
866
- }
867
- } catch (error) {
868
- console.error('❌ Error initializing draggable:', error);
869
- isDraggableInitialized = false;
870
- currentDraggable = null;
871
- } finally {
872
- isInitializing = false;
873
- }
430
+ /** What the list of content items needs to be reorderable; used with `use:reorderableList` on the list. */
431
+ const contentItemsReorderOptions: ItemReorderOptions = {
432
+ itemSelector: '.content-item',
433
+ handleSelector: '.drag-handle',
434
+ onDragStart: handleReorderStart,
435
+ onReorder: handleItemsReordered,
436
+ onDragEnd: handleReorderEnd
874
437
  };
875
438
 
876
439
  // Add this near other reactive declarations at the top level
@@ -910,6 +473,14 @@
910
473
  }
911
474
  }
912
475
 
476
+ // The tree the editor is drawn in (the shadow root inside a module, the document on a normal page), kept from the
477
+ // moment it is mounted: when the editor is torn down it is no longer in that tree to ask.
478
+ let editorTree: SearchableTree | null = null;
479
+
480
+ onMount(() => {
481
+ editorTree = treeOf(rootElement);
482
+ });
483
+
913
484
  onMount(() => {
914
485
  // console.log('PropsEditor Mounted!');
915
486
 
@@ -956,7 +527,8 @@
956
527
  if (browser) {
957
528
  document.removeEventListener('mousemove', handleDragMove);
958
529
  document.removeEventListener('mouseup', handleDragEnd);
959
- if (dragState.active) document.body.style.cursor = '';
530
+ releasePointerStyle?.();
531
+ releasePointerStyle = null;
960
532
  }
961
533
 
962
534
  // Clean up scroll listeners
@@ -967,9 +539,9 @@
967
539
  contentScrollViewport.removeEventListener('scroll', (event) => handleScroll(event, 'content'));
968
540
  }
969
541
 
970
- // Clean up any remaining tooltips
971
- if (browser) {
972
- const tooltips = document.querySelectorAll('[role="tooltip"]');
542
+ // Clean up any remaining tooltips, the editor's own: the page it is on has tooltips of its own that must stay
543
+ if (browser && editorTree) {
544
+ const tooltips = Array.from(editorTree.querySelectorAll('[role="tooltip"]'));
973
545
  tooltips.forEach(tooltip => {
974
546
  if (tooltip.parentNode) {
975
547
  tooltip.parentNode.removeChild(tooltip);
@@ -1024,12 +596,11 @@
1024
596
  // Replace ALL hyphens with underscores in the condition string
1025
597
  const safeCondition = condition.replace(/-/g, '_');
1026
598
 
1027
- const evalFunction = new Function(
1028
- ...Object.keys(context),
1029
- `return ${safeCondition}`
1030
- );
1031
-
1032
- return evalFunction(...Object.values(context));
599
+ // The condition is text from the widget's schema and this editor can run inside a customer's page, so it
600
+ // is read by a small parser and never run as code (it used to go through `new Function`, which ran the
601
+ // widget author's JavaScript with the page's rights and needed 'unsafe-eval'). A condition the parser
602
+ // cannot use keeps the control visible, as a failing one always did, and is reported once in the console.
603
+ return conditionHolds(safeCondition, context);
1033
604
  } catch (error) {
1034
605
  // console.error('Error evaluating visibility condition:', error);
1035
606
  // console.log('Condition:', condition);
@@ -1200,10 +771,12 @@
1200
771
  // });
1201
772
  }
1202
773
 
1203
- // First find and remove all modal-related elements, but preserve draggable
774
+ // First find and remove all modal-related elements, but preserve the item being dragged
1204
775
  const cleanup = () => {
1205
- // Get all possible modal elements with more specific selectors
1206
- const modalElements = document.querySelectorAll([
776
+ // Get all possible modal elements with more specific selectors. Only the ones in the editor's own tree (the
777
+ // shadow root inside a module): the customer's page has dialogs and popovers of its own that are none of the
778
+ // editor's business.
779
+ const modalElements = queryAllInTreeOf(rootElement, [
1207
780
  '[data-radix-popper-content-wrapper]',
1208
781
  '[role="dialog"]',
1209
782
  '[cmdk-root]',
@@ -1228,9 +801,9 @@
1228
801
  // }))
1229
802
  // });
1230
803
 
1231
- // Remove each modal immediately, but protect scroll-related and draggable elements
804
+ // Remove each modal immediately, but protect scroll-related and dragged elements
1232
805
  modalElements.forEach(element => {
1233
- // Don't remove if it's part of the core UI structure or draggable
806
+ // Don't remove if it's part of the core UI structure or being dragged
1234
807
  if (
1235
808
  element.closest('[role="tabpanel"]') ||
1236
809
  element.closest('[data-accordion-content]') ||
@@ -1242,8 +815,8 @@
1242
815
  element.classList.contains('scrollbar') ||
1243
816
  element.classList.contains('scroll-area') ||
1244
817
  element.hasAttribute('data-scrollable') ||
1245
- element.classList.contains('draggable-mirror') ||
1246
- element.classList.contains('draggable-source--is-dragging')
818
+ element.classList.contains('reorder-mirror') ||
819
+ element.classList.contains('is-reorder-source')
1247
820
  ) {
1248
821
  return;
1249
822
  }
@@ -1270,28 +843,6 @@
1270
843
  // Reset active controller
1271
844
  activeControllerId = null;
1272
845
 
1273
- // If switching to content tab and content exists, ensure draggable is initialized
1274
- if (currentValue === 'content' && contentSchema) {
1275
- // Wait for tab content to be rendered
1276
- setTimeout(async () => {
1277
- // console.log('🎯 Checking draggable state in content tab');
1278
-
1279
- // Check if draggable needs initialization
1280
- const containers = document.querySelectorAll('.content-items');
1281
- const needsInit = !isDraggableInitialized ||
1282
- !currentDraggable ||
1283
- containers.length === 0 ||
1284
- !document.querySelector('.draggable-container');
1285
-
1286
- if (needsInit) {
1287
- // console.log('🔄 Reinitializing draggable');
1288
- await initializeDraggable();
1289
- } else {
1290
- // console.log('✅ Draggable already initialized and working');
1291
- }
1292
- }, 50);
1293
- }
1294
-
1295
846
  // Restore open items state for the selected tab
1296
847
  openItems = currentValue === 'design' ? designOpenItems : contentOpenItems;
1297
848
  const accordionToOpen = currentValue === 'design' ? designOpenAccordion : contentOpenAccordion;
@@ -1308,7 +859,7 @@
1308
859
  setTimeout(() => {
1309
860
  const itemsToOpen = Array.isArray(accordionToOpen) ? accordionToOpen : (accordionToOpen ? [accordionToOpen] : []);
1310
861
  for (const name of itemsToOpen) {
1311
- const accordion = document.querySelector(`[data-accordion-id="${name}"]`);
862
+ const accordion = queryInTreeOf(rootElement, `[data-accordion-id="${name}"]`);
1312
863
  if (accordion) {
1313
864
  accordion.setAttribute('data-state', 'open');
1314
865
  }
@@ -1330,7 +881,8 @@
1330
881
  focusController(controllerToFocus);
1331
882
  } else {
1332
883
  // Focus first controller
1333
- const firstController = document.querySelector(
884
+ const firstController = queryInTreeOf(
885
+ rootElement,
1334
886
  `[data-tab="${currentValue}"] .input-controller:first-child`
1335
887
  );
1336
888
  if (firstController) {
@@ -1774,7 +1326,9 @@
1774
1326
  function focusController(controllerId: string) {
1775
1327
  // console.log('Attempting to focus controller:', controllerId);
1776
1328
  activeControllerId = controllerId;
1777
- const element = document.getElementById(controllerId);
1329
+ // Looked up in the editor's own tree: in a module that is the shadow root, where `document.getElementById`
1330
+ // finds nothing (and on the customer's page it could find an element of theirs with the same id).
1331
+ const element = treeOf(rootElement)?.getElementById(controllerId);
1778
1332
  if (element) {
1779
1333
  const componentInstance = element.querySelector(':scope > *');
1780
1334
  if (componentInstance && 'focus' in componentInstance) {
@@ -1840,6 +1394,9 @@
1840
1394
  // Keydown handler: SHIFT + '+' expands all, SHIFT + '-' collapses all
1841
1395
  function handleKeyDown(event: KeyboardEvent) {
1842
1396
  // console.log("handleKeyDown triggered: key=", event.key, "shiftKey=", event.shiftKey);
1397
+ // The listener is on the window, so it hears every key typed on the page. Only the ones typed in the editor
1398
+ // count: on a customer's page Shift+Arrow in their own text boxes must not open and close our items.
1399
+ if (rootElement && !event.composedPath().includes(rootElement)) return;
1843
1400
  if (event.shiftKey && event.key === 'ArrowDown') {
1844
1401
  // console.log("Expanding all content items");
1845
1402
  expandAllContentItems();
@@ -2063,33 +1620,11 @@
2063
1620
  // console.log('[Add] After (should be at end):', contentSchema.contentItems.map((i: ContentItem) => ({ id: i.id, title: i.title })));
2064
1621
  // console.log('[Add] New item added:', newItem);
2065
1622
 
2066
- // Ensure DOM order matches array order after adding new item
1623
+ // The list is drawn from the array, so the new item is at the end of it. Once it is drawn, open it to reveal
1624
+ // its content. (The DOM order no longer needs to be put right by hand here: nothing moves the items in the
1625
+ // DOM behind the list's back since the drag and drop is done by utils/itemReorder.ts.)
2067
1626
  setTimeout(() => {
2068
- const container = document.querySelector('.content-items');
2069
- if (container && Array.isArray(contentSchema?.contentItems)) {
2070
- const elementMap = new Map<string, HTMLElement>();
2071
- Array.from(container.querySelectorAll('.content-item')).forEach((el: Element) => {
2072
- const id = (el as HTMLElement).dataset.id;
2073
- if (id) elementMap.set(id, el as HTMLElement);
2074
- });
2075
-
2076
- const desiredOrderIds = contentSchema.contentItems.map((item: ContentItem) => item.id);
2077
-
2078
- // Surgical re-ordering to minimize DOM moves and flicker
2079
- for (let i = 0; i < desiredOrderIds.length; i++) {
2080
- const desiredId = desiredOrderIds[i];
2081
- const currentDomNode = container.children[i] as HTMLElement | undefined;
2082
-
2083
- // Only move the element if it's not already in the correct position
2084
- if (currentDomNode?.dataset.id !== desiredId) {
2085
- const elementToMove = elementMap.get(desiredId);
2086
- if (elementToMove) {
2087
- container.insertBefore(elementToMove, currentDomNode || null);
2088
- }
2089
- }
2090
- }
2091
-
2092
- // Now that the item is positioned, open it to reveal content
1627
+ if (Array.isArray(contentSchema?.contentItems)) {
2093
1628
  openItems = new Set([...openItems, uniqueId]);
2094
1629
  }
2095
1630
  }, 0);
@@ -2235,10 +1770,6 @@
2235
1770
  const targetIndex = Math.max(0, deletedIndex - 1);
2236
1771
  dispatch('goToItem', { index: targetIndex });
2237
1772
  }
2238
- setTimeout(() => {
2239
- // console.log('🔄 Reinitializing draggable after deletion');
2240
- initializeDraggable();
2241
- }, 0);
2242
1773
  }
2243
1774
  } catch (error) {
2244
1775
  console.error('❌ Error deleting content item:', error);
@@ -2264,8 +1795,9 @@
2264
1795
  }
2265
1796
  );
2266
1797
 
2267
- // Observe content items
2268
- document.querySelectorAll('.content-item').forEach(item => {
1798
+ // Observe content items: the ones in the editor's own list. `.content-item` is a name any page may use, and
1799
+ // an element of the page's that is observed here would be hidden whenever it scrolls out of view.
1800
+ contentItemsContainer?.querySelectorAll('.content-item').forEach(item => {
2269
1801
  observer.observe(item);
2270
1802
  });
2271
1803
 
@@ -2274,13 +1806,14 @@
2274
1806
 
2275
1807
  // Keep CSS --index in sync with DOM position **after** Svelte has rendered updates
2276
1808
  afterUpdate(() => {
2277
- if (isDragging) return;
2278
- const items = getContentItems();
2279
- if (items.length === 0) return;
2280
-
2281
- items.forEach((item: ContentItem, index: number) => {
2282
- const el = document.querySelector(`[data-id="${item.id}"]`);
2283
- if (el instanceof HTMLElement) {
1809
+ if (isDragging || isReordering) return;
1810
+ // The items of the editor's own list, in the order they are drawn (a lookup by data-id on the whole document
1811
+ // would find nothing inside a shadow root, and could find an element of the customer's page).
1812
+ const itemElements = Array.from(contentItemsContainer?.children ?? []);
1813
+ if (itemElements.length === 0) return;
1814
+
1815
+ itemElements.forEach((el: Element, index: number) => {
1816
+ if (el instanceof HTMLElement && el.classList.contains('content-item')) {
2284
1817
  el.style.setProperty('--index', index.toString());
2285
1818
  }
2286
1819
  });
@@ -2305,11 +1838,8 @@
2305
1838
  currentTab = 'content';
2306
1839
 
2307
1840
  // Wait for DOM to be ready
2308
- setTimeout(async () => {
1841
+ setTimeout(() => {
2309
1842
  try {
2310
- // console.log('🔄 Initializing draggable for content tab');
2311
- await initializeDraggable();
2312
-
2313
1843
  // Set initial states
2314
1844
  isInitialLoad = false;
2315
1845
  isFirstLoad = false;
@@ -2392,6 +1922,8 @@
2392
1922
  // ─── BUILT-IN DRAG ───────────────────────────────────────────
2393
1923
  let rootElement: HTMLElement;
2394
1924
  let dragState = { active: false, startX: 0, startY: 0, initialLeft: 0, initialTop: 0 };
1925
+ // Puts the pointer back as it was once the panel is no longer dragged (see lockPointerStyle).
1926
+ let releasePointerStyle: (() => void) | null = null;
2395
1927
 
2396
1928
  function handleDragStart(e: MouseEvent) {
2397
1929
  const target = positionTarget || rootElement;
@@ -2423,7 +1955,10 @@
2423
1955
  target.style.zIndex = '50';
2424
1956
  document.addEventListener('mousemove', handleDragMove);
2425
1957
  document.addEventListener('mouseup', handleDragEnd);
2426
- document.body.style.cursor = 'move';
1958
+ // The cursor is shown on the host of the shadow root the editor is in (on <body> only when it is on a normal page):
1959
+ // the <body> of a customer's page is not the editor's to change.
1960
+ releasePointerStyle?.();
1961
+ releasePointerStyle = lockPointerStyle(rootElement, 'move', { blockTextSelection: false });
2427
1962
  }
2428
1963
 
2429
1964
  function handleDragMove(e: MouseEvent) {
@@ -2439,7 +1974,8 @@
2439
1974
  dragState.active = false;
2440
1975
  document.removeEventListener('mousemove', handleDragMove);
2441
1976
  document.removeEventListener('mouseup', handleDragEnd);
2442
- document.body.style.cursor = '';
1977
+ releasePointerStyle?.();
1978
+ releasePointerStyle = null;
2443
1979
  dispatch('moveEnd', getPosition());
2444
1980
  }
2445
1981
 
@@ -2481,17 +2017,6 @@
2481
2017
  return ids.length !== new Set(ids).size;
2482
2018
  }
2483
2019
 
2484
- // Helper: log DOM visual order for debugging
2485
- function logOrder(label: string) {
2486
- const titles = Array.from(document.querySelectorAll('.content-item .content-title')).map(el => el.textContent?.trim());
2487
- // console.log(`[Order-check] ${label}:`, titles);
2488
- const transforms = Array.from(document.querySelectorAll('.content-item')).map(el => ({ id: (el as HTMLElement).dataset.id, transform: (el as HTMLElement).style.transform }));
2489
- console.table(transforms);
2490
- }
2491
-
2492
-
2493
-
2494
-
2495
2020
  </script>
2496
2021
 
2497
2022
  <div
@@ -2576,9 +2101,6 @@
2576
2101
  </Tabs.Trigger>
2577
2102
  {#if contentSchema}
2578
2103
  <Tabs.Trigger
2579
- onclick={() => {
2580
- initializeDraggable();
2581
- }}
2582
2104
  value="content"
2583
2105
  class="flex-1 h-full rounded-[11px] transition-colors duration-250 text-grey5 hover:text-grey7 hover:underline data-[state=active]:bg-white data-[state=active]:text-grey7 data-[state=active]:hover:text-grey7"
2584
2106
  >
@@ -2800,7 +2322,7 @@
2800
2322
  <!-- Content Items Container -->
2801
2323
  <div class="content-items-wrapper mt-0" style="position: relative; z-index: 1;">
2802
2324
  <!-- Sortable Container -->
2803
- <div class="content-items flex flex-col gap-4" bind:this={contentItemsContainer} style="position: relative; z-index: 1;">
2325
+ <div class="content-items flex flex-col gap-4" bind:this={contentItemsContainer} use:reorderableList={contentItemsReorderOptions} style="position: relative; z-index: 1;">
2804
2326
  {@html (() => {
2805
2327
  const items = contentSchema?.contentItems;
2806
2328
  if (!Array.isArray(items)) return '';
@@ -3134,24 +2656,6 @@
3134
2656
  justify-content: center !important;
3135
2657
  }
3136
2658
 
3137
- :global(.draggable-mirror) {
3138
- opacity: 1 !important;
3139
- transform-origin: 50% 50%;
3140
- z-index: 100;
3141
- pointer-events: none;
3142
- background-color: white !important;
3143
- border: 2px solid hsl(var(--grey2)) !important;
3144
- border-radius: var(--radius-small) !important;
3145
- }
3146
-
3147
- :global(.draggable-source--is-dragging) {
3148
- opacity: 0.3 !important;
3149
- }
3150
-
3151
- :global(html.draggable--is-dragging) {
3152
- cursor: move !important;
3153
- }
3154
-
3155
2659
  :global([data-scrollable="false"] .scrollbar) {
3156
2660
  display: none;
3157
2661
  }
@@ -3266,11 +2770,6 @@
3266
2770
  z-index: 99999 !important; /* Force highest z-index */
3267
2771
  }
3268
2772
 
3269
- /* Update existing styles to ensure proper stacking */
3270
- :global(.draggable-mirror) {
3271
- z-index: 100;
3272
- }
3273
-
3274
2773
  :global(.popover-content) {
3275
2774
  z-index: 50;
3276
2775
  }
@@ -3863,7 +3362,6 @@
3863
3362
  text-decoration: underline;
3864
3363
  }
3865
3364
 
3866
- /* Add Draggable Sort Animation styles */
3867
3365
  :global(.content-item) {
3868
3366
  position: relative;
3869
3367
  z-index: 1;
@@ -3871,95 +3369,77 @@
3871
3369
  transform: translate3d(0, 0, 0);
3872
3370
  }
3873
3371
 
3874
- :global(.draggable-container--is-dragging) {
3875
- cursor: grabbing;
3876
- }
3877
-
3878
- :global(.draggable-source--is-dragging) {
3879
- opacity: 0.3;
3880
- }
3881
-
3882
- :global(.draggable-source--is-dragging .content-title) {
3883
- text-decoration: none !important;
3884
- }
3885
-
3886
- :global(.draggable-source--placed) {
3887
- visibility: visible !important;
3888
- transition: none !important;
3889
- opacity: 1 !important;
3890
- }
3891
-
3892
- :global(.content-items--is-dragging .content-item:not(.draggable-source--is-dragging)) {
3893
- transition: transform 200ms ease-in-out;
3372
+ /*
3373
+ * Reordering the items by dragging a handle (utils/itemReorder.ts). While an item is dragged, every item is
3374
+ * folded to one row at once (the items that were open keep their content, it is only hidden), so the whole
3375
+ * list is within reach and the places to drop do not move under the pointer.
3376
+ */
3377
+ :global(.content-items.is-reordering) {
3378
+ user-select: none;
3379
+ -webkit-user-select: none;
3894
3380
  }
3895
3381
 
3896
- :global(.content-item.draggable-mirror) {
3897
- transform: scale(1.05);
3382
+ /* The card is what is folded, not the item: the line that shows the drop place sits outside the item's box */
3383
+ :global(.content-items.is-reordering .content-item-card) {
3384
+ height: 56px !important;
3385
+ min-height: 56px !important;
3386
+ max-height: 56px !important;
3387
+ overflow: hidden !important;
3898
3388
  }
3899
3389
 
3900
- :global(.content-item.draggable--original) {
3901
- opacity: 0;
3390
+ :global(.content-items.is-reordering [data-collapsible-content]) {
3391
+ display: none !important;
3902
3392
  }
3903
3393
 
3904
- :global(.content-item.draggable-source--is-dragging) {
3394
+ /* The item being dragged stays in place, faded; the floating copy is what follows the pointer */
3395
+ :global(.content-item.is-reorder-source) {
3905
3396
  opacity: 0.3;
3906
3397
  }
3907
3398
 
3908
- :global(.content-item.draggable-source--is-dragging .content-title) {
3909
- text-decoration: none !important;
3910
- }
3911
-
3912
- :global(.content-item.draggable-mirror .content-title) {
3913
- text-decoration: none !important;
3914
- }
3915
-
3916
- :global(.draggable-mirror .drag-handle svg) {
3917
- color: hsl(var(--grey3)) !important;
3918
- }
3919
-
3920
- :global(.draggable--original) {
3921
- display: none;
3399
+ /* A line in the gap where the dragged item would be dropped: in front of an item, or behind the last one */
3400
+ :global(.content-item.is-reorder-drop-before)::before,
3401
+ :global(.content-item.is-reorder-drop-after)::after {
3402
+ content: '';
3403
+ position: absolute;
3404
+ left: 0;
3405
+ right: 0;
3406
+ height: 3px;
3407
+ border-radius: 2px;
3408
+ background-color: hsl(var(--sage-green));
3409
+ pointer-events: none;
3922
3410
  }
3923
3411
 
3924
- :global(.content-items--is-dragging) {
3925
- cursor: grabbing;
3412
+ :global(.content-item.is-reorder-drop-before)::before {
3413
+ top: -9px;
3926
3414
  }
3927
3415
 
3928
- /* Prevent hover effects during dragging */
3929
- :global(.content-items--is-dragging .content-item-card [data-radix-collapsible-trigger]:hover)::before {
3930
- background-color: transparent;
3416
+ :global(.content-item.is-reorder-drop-after)::after {
3417
+ bottom: -9px;
3931
3418
  }
3932
3419
 
3933
- :global(.content-items--is-dragging .content-item:not(.draggable-mirror) .content-title:hover) {
3934
- text-decoration: none !important;
3420
+ /* The floating copy of the dragged item: one row, with the editor's green outline */
3421
+ :global(.reorder-mirror) {
3422
+ box-sizing: border-box;
3423
+ border: 2px solid hsl(var(--sage-green));
3424
+ border-radius: var(--radius-small);
3425
+ background-color: white;
3426
+ box-shadow: none;
3427
+ will-change: transform;
3935
3428
  }
3936
3429
 
3937
- :global(.content-items--is-dragging .content-item-trigger:hover .content-title) {
3430
+ :global(.reorder-mirror .content-title) {
3431
+ color: hsl(var(--sage-green));
3938
3432
  text-decoration: none !important;
3939
3433
  }
3940
3434
 
3941
- :global(.content-items--is-dragging .content-item.draggable--over) {
3942
- transform: scale(1.015) !important;
3435
+ :global(.reorder-mirror .chevron),
3436
+ :global(.reorder-mirror .drag-handle svg) {
3437
+ color: hsl(var(--sage-green));
3943
3438
  }
3944
3439
 
3945
- :global(.content-item.draggable-mirror) {
3946
- transform: scale(1.05);
3947
- }
3948
-
3949
- :global(.content-item.draggable--original) {
3950
- opacity: 0;
3951
- }
3952
-
3953
- :global(.content-item.draggable-source--is-dragging) {
3954
- opacity: 0.3;
3955
- }
3956
-
3957
- :global(.content-item.draggable-source--is-dragging .content-title) {
3958
- text-decoration: none !important;
3959
- }
3960
-
3961
- :global(.content-item.draggable-mirror .content-title) {
3962
- text-decoration: none !important;
3440
+ /* The card inside the copy has its own border: the outline above is the only one */
3441
+ :global(.reorder-mirror .content-item-card) {
3442
+ border: none;
3963
3443
  }
3964
3444
 
3965
3445
  /* Update drag handle hover area and content title behavior */
@@ -3970,6 +3450,8 @@
3970
3450
  justify-content: center !important;
3971
3451
  height: 100% !important;
3972
3452
  cursor: move !important;
3453
+ /* Touching the handle drags the item; it must not scroll the page */
3454
+ touch-action: none;
3973
3455
  }
3974
3456
 
3975
3457
  /* Keep the icon size consistent */
@@ -3998,37 +3480,12 @@
3998
3480
  padding-left: 0 !important;
3999
3481
  }
4000
3482
 
4001
- :global(.draggable-mirror .drag-handle svg) {
4002
- color: hsl(var(--grey3)) !important;
4003
- }
4004
-
4005
- :global(.draggable--original) {
4006
- display: none;
4007
- }
4008
-
4009
- :global(.content-items--is-dragging) {
4010
- cursor: grabbing;
4011
- }
4012
-
4013
-
4014
3483
  :global(.content-item) {
4015
3484
  contain: layout style;
4016
3485
  will-change: transform;
4017
3486
  transform: translate3d(0, 0, 0);
4018
3487
  }
4019
3488
 
4020
- :global(.draggable-mirror) {
4021
- contain: layout style;
4022
- will-change: transform;
4023
- transform: translate3d(0, 0, 0);
4024
- }
4025
-
4026
-
4027
- :global(.content-items--is-dragging .content-item:not(.draggable-source--is-dragging)) {
4028
- transition: transform 200ms cubic-bezier(0.2, 0, 0.2, 1);
4029
- will-change: transform;
4030
- }
4031
-
4032
3489
  /* Animation classes */
4033
3490
  :global(.design-tab-content[data-state="active"]) {
4034
3491
  animation: none;
@@ -4094,8 +3551,6 @@
4094
3551
  }
4095
3552
  }
4096
3553
 
4097
-
4098
-
4099
3554
  @keyframes slideUp {
4100
3555
  from {
4101
3556
  opacity: 0;
@@ -4147,46 +3602,6 @@
4147
3602
  }
4148
3603
  }
4149
3604
 
4150
- /* Draggable mirror styles - styling only, no positioning */
4151
- :global(.draggable-mirror) {
4152
- opacity: 1 !important;
4153
- background-color: white !important;
4154
- border: 2px solid hsl(var(--grey2)) !important;
4155
- border-radius: var(--radius-small) !important;
4156
- box-shadow: none !important;
4157
- }
4158
-
4159
- /* Style for item being hovered over (potential drop target) */
4160
- :global(.content-items--is-dragging .content-item.draggable--over) {
4161
- transform: scale(1.015) !important;
4162
- border: 2px solid hsl(var(--grey2)) !important;
4163
- border-radius: var(--radius-small) !important;
4164
- background-color: hsl(var(--grey1)) !important;
4165
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05) !important;
4166
- z-index: 10 !important;
4167
- }
4168
-
4169
- /* Remove all borders and shadows from draggable mirror */
4170
- :global(.draggable-mirror) {
4171
- border: 2px solid hsl(var(--grey2)) !important;
4172
- border-radius: var(--radius-small) !important;
4173
- box-shadow: none !important;
4174
- }
4175
-
4176
- /* Remove borders from all children inside the mirror */
4177
- :global(.draggable-mirror *) {
4178
- border: none !important;
4179
- border-top: none !important;
4180
- border-bottom: none !important;
4181
- border-left: none !important;
4182
- border-right: none !important;
4183
- }
4184
-
4185
- /* Make the original item completely transparent during dragging */
4186
- :global(.draggable-source--is-dragging) {
4187
- opacity: 0 !important;
4188
- }
4189
-
4190
3605
  :global(body.dragging-preserve-borders .form-input),
4191
3606
  :global(body.dragging-preserve-borders [id^="number-input"]),
4192
3607
  :global(body.dragging-preserve-borders .number-input-button),
@@ -4197,20 +3612,6 @@
4197
3612
  border: 1px solid hsl(var(--grey2)) !important;
4198
3613
  }
4199
3614
 
4200
- /* Add a new rule to force fixed heights during dragging */
4201
- :global(.dragging-active .content-item) {
4202
- height: 56px !important;
4203
- min-height: 56px !important;
4204
- max-height: 56px !important;
4205
- overflow: hidden !important;
4206
- }
4207
-
4208
- :global(.dragging-active [data-radix-collapsible-content]) {
4209
- height: 0 !important;
4210
- display: none !important;
4211
- overflow: hidden !important;
4212
- }
4213
-
4214
3615
  /* Add explicit transition for content tab chevrons */
4215
3616
  :global(.content-item-card .chevron),
4216
3617
  :global(.generic-content-card .chevron) {