@widgetic/editor 4.0.10 → 4.1.0

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 (84) hide show
  1. package/README.md +242 -0
  2. package/dist/components/WidgetPreview.svelte +123 -86
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/color-select/ColorSelect.svelte +4 -4
  5. package/dist/components/editors/CompositionEditorApp.svelte +562 -0
  6. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
  7. package/dist/components/editors/EmbedEditor.svelte +33 -22
  8. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  9. package/dist/components/editors/PropsEditor.svelte +267 -850
  10. package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
  11. package/dist/components/ic/AngleInputController.svelte +1 -1
  12. package/dist/components/ic/BrowserInputController.svelte +58 -31
  13. package/dist/components/ic/ColorPickerInputController.svelte +55 -21
  14. package/dist/components/ic/DateFieldInputController.svelte +34 -14
  15. package/dist/components/ic/DatePickerInputController.svelte +37 -21
  16. package/dist/components/ic/DropdownInputController.svelte +21 -9
  17. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  18. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  19. package/dist/components/ic/FontInputController.svelte +100 -76
  20. package/dist/components/ic/InputControllerHeader.svelte +16 -9
  21. package/dist/components/ic/NumberControls.svelte +4 -4
  22. package/dist/components/ic/NumberInputController.svelte +6 -4
  23. package/dist/components/ic/PositionInputController.svelte +23 -15
  24. package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
  25. package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
  26. package/dist/components/ic/SliderInputController.svelte +7 -2
  27. package/dist/components/ic/SwitchInputController.svelte +12 -3
  28. package/dist/components/ic/TagsInputController.svelte +19 -4
  29. package/dist/components/ic/TextAreaInputController.svelte +79 -51
  30. package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
  31. package/dist/components/ic/TextInputController.svelte +17 -1
  32. package/dist/components/ic/TimeInputController.svelte +11 -4
  33. package/dist/components/ic/VectorInputController.svelte +101 -70
  34. package/dist/components/ic/shared/ItemGrid.svelte +12 -4
  35. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  36. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  37. package/dist/composition/compositionApi.d.ts +95 -0
  38. package/dist/composition/compositionApi.js +289 -0
  39. package/dist/composition/compositionEditorController.d.ts +263 -0
  40. package/dist/composition/compositionEditorController.js +746 -0
  41. package/dist/composition/compositionHelpers.d.ts +74 -0
  42. package/dist/composition/compositionHelpers.js +405 -0
  43. package/dist/i18n/index.d.ts +4 -0
  44. package/dist/i18n/index.js +4 -0
  45. package/dist/i18n/labels.d.ts +15 -0
  46. package/dist/i18n/labels.js +57 -0
  47. package/dist/i18n/language.d.ts +228 -0
  48. package/dist/i18n/language.js +34 -0
  49. package/dist/i18n/languageKit.d.ts +54 -0
  50. package/dist/i18n/languageKit.js +102 -0
  51. package/dist/i18n/messages.d.ts +241 -0
  52. package/dist/i18n/messages.js +253 -0
  53. package/dist/i18n/translate.d.ts +6 -0
  54. package/dist/i18n/translate.js +8 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +8 -0
  57. package/dist/mediaLibraryContext.d.ts +10 -0
  58. package/dist/mediaLibraryContext.js +30 -0
  59. package/dist/utils/FontManager.d.ts +21 -5
  60. package/dist/utils/FontManager.js +62 -67
  61. package/dist/utils/VectorManager.d.ts +0 -1
  62. package/dist/utils/VectorManager.js +10 -12
  63. package/dist/utils/dockPlacement.d.ts +12 -0
  64. package/dist/utils/dockPlacement.js +13 -0
  65. package/dist/utils/editorStorage.d.ts +23 -0
  66. package/dist/utils/editorStorage.js +95 -0
  67. package/dist/utils/fieldDirection.d.ts +13 -0
  68. package/dist/utils/fieldDirection.js +25 -0
  69. package/dist/utils/fontUtils.js +5 -3
  70. package/dist/utils/htmlSafety.d.ts +21 -0
  71. package/dist/utils/htmlSafety.js +38 -0
  72. package/dist/utils/itemReorder.d.ts +90 -0
  73. package/dist/utils/itemReorder.js +354 -0
  74. package/dist/utils/recentColors.js +4 -2
  75. package/dist/utils/richTextPreview.d.ts +38 -0
  76. package/dist/utils/richTextPreview.js +207 -0
  77. package/dist/utils/scrollActivity.d.ts +14 -0
  78. package/dist/utils/scrollActivity.js +63 -0
  79. package/dist/utils/shadowDom.d.ts +67 -0
  80. package/dist/utils/shadowDom.js +211 -0
  81. package/dist/utils/vectorUtils.js +5 -3
  82. package/dist/utils/visibilityCondition.d.ts +40 -0
  83. package/dist/utils/visibilityCondition.js +535 -0
  84. package/package.json +7 -6
@@ -78,7 +78,20 @@
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 { useEditorLanguage } from '../../i18n/language';
94
+ import type { MediaLibraryProvider } from '@widgetic/file-browser';
82
95
 
83
96
  /**
84
97
  * STYLES from Widgetic Design System
@@ -123,9 +136,9 @@
123
136
  export let designSchema: DesignSchemaView | null = null;
124
137
  export let contentSchema: ContentSchemaView | undefined = undefined;
125
138
  export let itemTemplate: any = null; // Store template for new items
126
- // Tab titles
127
- export let designTabTitle: string = 'Design';
128
- export let contentTabTitle: string = 'Content';
139
+ // Tab titles. Without one, the editor's own word, in the language of the page.
140
+ export let designTabTitle: string | undefined = undefined;
141
+ export let contentTabTitle: string | undefined = undefined;
129
142
  // Default tab selection
130
143
  export let defaultTab: 'design' | 'content' = 'content';
131
144
 
@@ -151,7 +164,7 @@
151
164
  export let headerContent = undefined;
152
165
  export let headerClass = '';
153
166
  export let dragHandleClass = 'text-grey2';
154
- export let title = 'Edit Properties';
167
+ export let title: string | undefined = undefined;
155
168
  /** Hide the default missing-schema message for header-only editors. */
156
169
  export let showSchemaError = true;
157
170
  /** Optional layout wrapper moved by this editor's built-in drag and position API. */
@@ -161,6 +174,21 @@
161
174
  export let initialDesignValues: Record<string, any> = {};
162
175
  export let initialContentValues: Record<string, any> = {};
163
176
 
177
+ /**
178
+ * Where the file controls (images, video, audio, documents) get their files from. Without it they show the
179
+ * Widgetic account's uploads, as they always have (configured with `setFileBrowserUploadConfig`). With a
180
+ * `MediaLibraryProvider` they list, upload, rename and delete through its functions instead, so a partner's own
181
+ * storage (a CMS, an S3 bucket, a DAM) can be used with no Widgetic storage at all.
182
+ */
183
+ export let mediaLibrary: MediaLibraryProvider | undefined = undefined;
184
+ // The file controls are created from the schema several components down: give them the library through context.
185
+ provideEditorMediaLibrary(() => mediaLibrary);
186
+
187
+ // The words of the editor in the language of the page: the editor above this one chose it (see `provideEditorLanguage`).
188
+ const { t } = useEditorLanguage();
189
+ // The title of the bar on top: the host's own, or the editor's word; an empty title hides the bar's title.
190
+ $: titleLabel = title ?? $t('editor.props.title');
191
+
164
192
  /**
165
193
  * CONSTANTS
166
194
  */
@@ -254,18 +282,9 @@
254
282
 
255
283
  // Clear font weight keys immediately when script loads
256
284
  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
- }
285
+ // Only the editor's own keys are looked at: this used to walk the whole localStorage and remove every key with
286
+ // this text in it, and inside a customer's page that reaches keys that are the customer's.
287
+ removeEditorStorageValuesStartingWith('font-weight-state-');
269
288
  }
270
289
 
271
290
  /**
@@ -356,521 +375,71 @@
356
375
  }
357
376
  };
358
377
 
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
- }
378
+ /**
379
+ * REORDERING CONTENT ITEMS
380
+ * Dragging an item's handle moves the item in the list. The dragging itself is in utils/itemReorder.ts, which is
381
+ * not a library because the editor can be inside a shadow root on a customer's page (see the file for why no
382
+ * library works there). While an item is dragged the stylesheet folds every item to one row so the whole list is
383
+ * within reach (an item that is open keeps its content, it is only hidden), so nothing has to be closed and
384
+ * opened again around a drag.
385
+ */
386
+ let isReordering = false;
369
387
 
370
- isInitializing = true;
371
- // console.log('🔄 [PropsEditor] Starting draggable initialization');
388
+ function handleReorderStart() {
389
+ isReordering = true;
390
+ }
372
391
 
392
+ /** An item was dropped at another place in the list: `oldIndex` and `newIndex` are its places before and after. */
393
+ function handleItemsReordered({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
373
394
  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 })));
395
+ if (!Array.isArray(contentSchema?.contentItems)) {
396
+ console.warn('[Reorder] contentItems is not an array, skipping reorder');
397
+ return;
398
+ }
751
399
 
752
- const items = [...contentSchema.contentItems];
753
- const [movedItem] = items.splice(oldIndex, 1);
754
- items.splice(newIndex, 0, movedItem);
400
+ // Robust checks for valid indices
401
+ const itemCount = contentSchema.contentItems.length;
402
+ if (
403
+ !Number.isInteger(oldIndex) || !Number.isInteger(newIndex) ||
404
+ oldIndex < 0 || newIndex < 0 || oldIndex >= itemCount || newIndex >= itemCount
405
+ ) {
406
+ console.error('[Reorder] Invalid indices for the dropped item. Aborting reorder.', { oldIndex, newIndex, itemCount });
407
+ return;
408
+ }
755
409
 
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
- });
410
+ const items = moveListItem<ContentItem>(contentSchema.contentItems, oldIndex, newIndex);
781
411
 
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
- });
412
+ // Safety checks before updating state
413
+ if (hasDuplicateIds(items)) {
414
+ console.error('[Reorder] Duplicate IDs detected after reordering with indices. Aborting update.', items.map(i => i.id));
415
+ return;
416
+ }
417
+ if (items.length !== itemCount) {
418
+ console.error('[Reorder] Array length mismatch after reordering with indices. Aborting update.');
419
+ return;
420
+ }
853
421
 
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
- });
422
+ const renumbered = renumberAutoItemTitles(items);
423
+ contentSchema = { ...contentSchema, contentItems: renumbered };
424
+ dispatch('updated', { contentSchema });
425
+ dispatchContentItemsChanged(contentSchema.contentItems);
426
+ } catch (err) {
427
+ console.error('[Reorder] Error updating the order after a drop:', err);
428
+ }
429
+ }
861
430
 
862
- logOrder('after init');
431
+ /** The drag is over (dropped or cancelled): the list is drawn as usual again. */
432
+ function handleReorderEnd() {
433
+ isReordering = false;
434
+ }
863
435
 
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
- }
436
+ /** What the list of content items needs to be reorderable; used with `use:reorderableList` on the list. */
437
+ const contentItemsReorderOptions: ItemReorderOptions = {
438
+ itemSelector: '.content-item',
439
+ handleSelector: '.drag-handle',
440
+ onDragStart: handleReorderStart,
441
+ onReorder: handleItemsReordered,
442
+ onDragEnd: handleReorderEnd
874
443
  };
875
444
 
876
445
  // Add this near other reactive declarations at the top level
@@ -910,6 +479,14 @@
910
479
  }
911
480
  }
912
481
 
482
+ // The tree the editor is drawn in (the shadow root inside a module, the document on a normal page), kept from the
483
+ // moment it is mounted: when the editor is torn down it is no longer in that tree to ask.
484
+ let editorTree: SearchableTree | null = null;
485
+
486
+ onMount(() => {
487
+ editorTree = treeOf(rootElement);
488
+ });
489
+
913
490
  onMount(() => {
914
491
  // console.log('PropsEditor Mounted!');
915
492
 
@@ -956,7 +533,8 @@
956
533
  if (browser) {
957
534
  document.removeEventListener('mousemove', handleDragMove);
958
535
  document.removeEventListener('mouseup', handleDragEnd);
959
- if (dragState.active) document.body.style.cursor = '';
536
+ releasePointerStyle?.();
537
+ releasePointerStyle = null;
960
538
  }
961
539
 
962
540
  // Clean up scroll listeners
@@ -967,9 +545,9 @@
967
545
  contentScrollViewport.removeEventListener('scroll', (event) => handleScroll(event, 'content'));
968
546
  }
969
547
 
970
- // Clean up any remaining tooltips
971
- if (browser) {
972
- const tooltips = document.querySelectorAll('[role="tooltip"]');
548
+ // Clean up any remaining tooltips, the editor's own: the page it is on has tooltips of its own that must stay
549
+ if (browser && editorTree) {
550
+ const tooltips = Array.from(editorTree.querySelectorAll('[role="tooltip"]'));
973
551
  tooltips.forEach(tooltip => {
974
552
  if (tooltip.parentNode) {
975
553
  tooltip.parentNode.removeChild(tooltip);
@@ -1024,12 +602,11 @@
1024
602
  // Replace ALL hyphens with underscores in the condition string
1025
603
  const safeCondition = condition.replace(/-/g, '_');
1026
604
 
1027
- const evalFunction = new Function(
1028
- ...Object.keys(context),
1029
- `return ${safeCondition}`
1030
- );
1031
-
1032
- return evalFunction(...Object.values(context));
605
+ // The condition is text from the widget's schema and this editor can run inside a customer's page, so it
606
+ // is read by a small parser and never run as code (it used to go through `new Function`, which ran the
607
+ // widget author's JavaScript with the page's rights and needed 'unsafe-eval'). A condition the parser
608
+ // cannot use keeps the control visible, as a failing one always did, and is reported once in the console.
609
+ return conditionHolds(safeCondition, context);
1033
610
  } catch (error) {
1034
611
  // console.error('Error evaluating visibility condition:', error);
1035
612
  // console.log('Condition:', condition);
@@ -1200,10 +777,12 @@
1200
777
  // });
1201
778
  }
1202
779
 
1203
- // First find and remove all modal-related elements, but preserve draggable
780
+ // First find and remove all modal-related elements, but preserve the item being dragged
1204
781
  const cleanup = () => {
1205
- // Get all possible modal elements with more specific selectors
1206
- const modalElements = document.querySelectorAll([
782
+ // Get all possible modal elements with more specific selectors. Only the ones in the editor's own tree (the
783
+ // shadow root inside a module): the customer's page has dialogs and popovers of its own that are none of the
784
+ // editor's business.
785
+ const modalElements = queryAllInTreeOf(rootElement, [
1207
786
  '[data-radix-popper-content-wrapper]',
1208
787
  '[role="dialog"]',
1209
788
  '[cmdk-root]',
@@ -1228,9 +807,9 @@
1228
807
  // }))
1229
808
  // });
1230
809
 
1231
- // Remove each modal immediately, but protect scroll-related and draggable elements
810
+ // Remove each modal immediately, but protect scroll-related and dragged elements
1232
811
  modalElements.forEach(element => {
1233
- // Don't remove if it's part of the core UI structure or draggable
812
+ // Don't remove if it's part of the core UI structure or being dragged
1234
813
  if (
1235
814
  element.closest('[role="tabpanel"]') ||
1236
815
  element.closest('[data-accordion-content]') ||
@@ -1242,8 +821,8 @@
1242
821
  element.classList.contains('scrollbar') ||
1243
822
  element.classList.contains('scroll-area') ||
1244
823
  element.hasAttribute('data-scrollable') ||
1245
- element.classList.contains('draggable-mirror') ||
1246
- element.classList.contains('draggable-source--is-dragging')
824
+ element.classList.contains('reorder-mirror') ||
825
+ element.classList.contains('is-reorder-source')
1247
826
  ) {
1248
827
  return;
1249
828
  }
@@ -1270,28 +849,6 @@
1270
849
  // Reset active controller
1271
850
  activeControllerId = null;
1272
851
 
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
852
  // Restore open items state for the selected tab
1296
853
  openItems = currentValue === 'design' ? designOpenItems : contentOpenItems;
1297
854
  const accordionToOpen = currentValue === 'design' ? designOpenAccordion : contentOpenAccordion;
@@ -1308,7 +865,7 @@
1308
865
  setTimeout(() => {
1309
866
  const itemsToOpen = Array.isArray(accordionToOpen) ? accordionToOpen : (accordionToOpen ? [accordionToOpen] : []);
1310
867
  for (const name of itemsToOpen) {
1311
- const accordion = document.querySelector(`[data-accordion-id="${name}"]`);
868
+ const accordion = queryInTreeOf(rootElement, `[data-accordion-id="${name}"]`);
1312
869
  if (accordion) {
1313
870
  accordion.setAttribute('data-state', 'open');
1314
871
  }
@@ -1330,7 +887,8 @@
1330
887
  focusController(controllerToFocus);
1331
888
  } else {
1332
889
  // Focus first controller
1333
- const firstController = document.querySelector(
890
+ const firstController = queryInTreeOf(
891
+ rootElement,
1334
892
  `[data-tab="${currentValue}"] .input-controller:first-child`
1335
893
  );
1336
894
  if (firstController) {
@@ -1774,7 +1332,9 @@
1774
1332
  function focusController(controllerId: string) {
1775
1333
  // console.log('Attempting to focus controller:', controllerId);
1776
1334
  activeControllerId = controllerId;
1777
- const element = document.getElementById(controllerId);
1335
+ // Looked up in the editor's own tree: in a module that is the shadow root, where `document.getElementById`
1336
+ // finds nothing (and on the customer's page it could find an element of theirs with the same id).
1337
+ const element = treeOf(rootElement)?.getElementById(controllerId);
1778
1338
  if (element) {
1779
1339
  const componentInstance = element.querySelector(':scope > *');
1780
1340
  if (componentInstance && 'focus' in componentInstance) {
@@ -1840,6 +1400,9 @@
1840
1400
  // Keydown handler: SHIFT + '+' expands all, SHIFT + '-' collapses all
1841
1401
  function handleKeyDown(event: KeyboardEvent) {
1842
1402
  // console.log("handleKeyDown triggered: key=", event.key, "shiftKey=", event.shiftKey);
1403
+ // The listener is on the window, so it hears every key typed on the page. Only the ones typed in the editor
1404
+ // count: on a customer's page Shift+Arrow in their own text boxes must not open and close our items.
1405
+ if (rootElement && !event.composedPath().includes(rootElement)) return;
1843
1406
  if (event.shiftKey && event.key === 'ArrowDown') {
1844
1407
  // console.log("Expanding all content items");
1845
1408
  expandAllContentItems();
@@ -2063,33 +1626,11 @@
2063
1626
  // console.log('[Add] After (should be at end):', contentSchema.contentItems.map((i: ContentItem) => ({ id: i.id, title: i.title })));
2064
1627
  // console.log('[Add] New item added:', newItem);
2065
1628
 
2066
- // Ensure DOM order matches array order after adding new item
1629
+ // 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
1630
+ // its content. (The DOM order no longer needs to be put right by hand here: nothing moves the items in the
1631
+ // DOM behind the list's back since the drag and drop is done by utils/itemReorder.ts.)
2067
1632
  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
1633
+ if (Array.isArray(contentSchema?.contentItems)) {
2093
1634
  openItems = new Set([...openItems, uniqueId]);
2094
1635
  }
2095
1636
  }, 0);
@@ -2235,10 +1776,6 @@
2235
1776
  const targetIndex = Math.max(0, deletedIndex - 1);
2236
1777
  dispatch('goToItem', { index: targetIndex });
2237
1778
  }
2238
- setTimeout(() => {
2239
- // console.log('🔄 Reinitializing draggable after deletion');
2240
- initializeDraggable();
2241
- }, 0);
2242
1779
  }
2243
1780
  } catch (error) {
2244
1781
  console.error('❌ Error deleting content item:', error);
@@ -2264,8 +1801,9 @@
2264
1801
  }
2265
1802
  );
2266
1803
 
2267
- // Observe content items
2268
- document.querySelectorAll('.content-item').forEach(item => {
1804
+ // Observe content items: the ones in the editor's own list. `.content-item` is a name any page may use, and
1805
+ // an element of the page's that is observed here would be hidden whenever it scrolls out of view.
1806
+ contentItemsContainer?.querySelectorAll('.content-item').forEach(item => {
2269
1807
  observer.observe(item);
2270
1808
  });
2271
1809
 
@@ -2274,13 +1812,14 @@
2274
1812
 
2275
1813
  // Keep CSS --index in sync with DOM position **after** Svelte has rendered updates
2276
1814
  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) {
1815
+ if (isDragging || isReordering) return;
1816
+ // The items of the editor's own list, in the order they are drawn (a lookup by data-id on the whole document
1817
+ // would find nothing inside a shadow root, and could find an element of the customer's page).
1818
+ const itemElements = Array.from(contentItemsContainer?.children ?? []);
1819
+ if (itemElements.length === 0) return;
1820
+
1821
+ itemElements.forEach((el: Element, index: number) => {
1822
+ if (el instanceof HTMLElement && el.classList.contains('content-item')) {
2284
1823
  el.style.setProperty('--index', index.toString());
2285
1824
  }
2286
1825
  });
@@ -2305,11 +1844,8 @@
2305
1844
  currentTab = 'content';
2306
1845
 
2307
1846
  // Wait for DOM to be ready
2308
- setTimeout(async () => {
1847
+ setTimeout(() => {
2309
1848
  try {
2310
- // console.log('🔄 Initializing draggable for content tab');
2311
- await initializeDraggable();
2312
-
2313
1849
  // Set initial states
2314
1850
  isInitialLoad = false;
2315
1851
  isFirstLoad = false;
@@ -2392,6 +1928,8 @@
2392
1928
  // ─── BUILT-IN DRAG ───────────────────────────────────────────
2393
1929
  let rootElement: HTMLElement;
2394
1930
  let dragState = { active: false, startX: 0, startY: 0, initialLeft: 0, initialTop: 0 };
1931
+ // Puts the pointer back as it was once the panel is no longer dragged (see lockPointerStyle).
1932
+ let releasePointerStyle: (() => void) | null = null;
2395
1933
 
2396
1934
  function handleDragStart(e: MouseEvent) {
2397
1935
  const target = positionTarget || rootElement;
@@ -2423,7 +1961,10 @@
2423
1961
  target.style.zIndex = '50';
2424
1962
  document.addEventListener('mousemove', handleDragMove);
2425
1963
  document.addEventListener('mouseup', handleDragEnd);
2426
- document.body.style.cursor = 'move';
1964
+ // 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):
1965
+ // the <body> of a customer's page is not the editor's to change.
1966
+ releasePointerStyle?.();
1967
+ releasePointerStyle = lockPointerStyle(rootElement, 'move', { blockTextSelection: false });
2427
1968
  }
2428
1969
 
2429
1970
  function handleDragMove(e: MouseEvent) {
@@ -2439,7 +1980,8 @@
2439
1980
  dragState.active = false;
2440
1981
  document.removeEventListener('mousemove', handleDragMove);
2441
1982
  document.removeEventListener('mouseup', handleDragEnd);
2442
- document.body.style.cursor = '';
1983
+ releasePointerStyle?.();
1984
+ releasePointerStyle = null;
2443
1985
  dispatch('moveEnd', getPosition());
2444
1986
  }
2445
1987
 
@@ -2452,6 +1994,7 @@
2452
1994
  export function setPosition(left: number, top: number) {
2453
1995
  const target = positionTarget || rootElement;
2454
1996
  if (!target) return;
1997
+ // rtl-ok: the panel is placed in pixels from the left edge of the editor; the dock mirrors the pair on a right to left screen
2455
1998
  target.style.left = `${left}px`;
2456
1999
  target.style.top = `${top}px`;
2457
2000
  }
@@ -2481,17 +2024,6 @@
2481
2024
  return ids.length !== new Set(ids).size;
2482
2025
  }
2483
2026
 
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
2027
  </script>
2496
2028
 
2497
2029
  <div
@@ -2512,7 +2044,7 @@
2512
2044
  <!-- A single toolbar owns drag and Save for all editor variants. -->
2513
2045
  {#if showDragHandle && (designSchema || contentSchema || headerContent)}
2514
2046
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2515
- <div class="props-editor-drag-bar top-0 left-0 w-full flex h-7 shrink-0 items-center justify-center z-999 px-3 {headerContent ? 'relative ' + headerClass : 'absolute'}">
2047
+ <div class="props-editor-drag-bar top-0 start-0 w-full flex h-7 shrink-0 items-center justify-center z-999 px-3 {headerContent ? 'relative ' + headerClass : 'absolute'}">
2516
2048
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2517
2049
  <div
2518
2050
  class="flex items-center justify-center h-7 cursor-move transition-all duration-200 widgetic-colors opacity-0 hover:opacity-100 drag-handle-container"
@@ -2521,10 +2053,10 @@
2521
2053
  <DragHandle class="w-16.5 h-6 {dragHandleClass} hover:text-grey3 transition-colors" />
2522
2054
  </div>
2523
2055
  {#if showSaveButton}
2524
- {#if title}<span class="props-editor-title-label absolute left-6 top-1.75 pointer-events-none select-none text-[13px] font-bold leading-none text-[hsl(var(--grey5))]">{title}</span>{/if}
2056
+ {#if titleLabel}<span class="props-editor-title-label absolute start-6 top-1.75 pointer-events-none select-none text-[13px] font-bold leading-none text-[hsl(var(--grey5))]">{titleLabel}</span>{/if}
2525
2057
  <button
2526
2058
  type="button"
2527
- class="props-editor-save-btn absolute right-6 top-1 inline-flex h-5 w-19 items-center justify-center rounded-md text-[10px] font-semibold leading-none transition-colors focus:outline-none focus-visible:outline-none
2059
+ class="props-editor-save-btn absolute end-6 top-1 inline-flex h-5 w-19 items-center justify-center rounded-md text-[10px] font-semibold leading-none transition-colors focus:outline-none focus-visible:outline-none
2528
2060
  {saveSaving
2529
2061
  ? 'text-[hsl(var(--deep-forest-green))] bg-[hsl(var(--light-green))]'
2530
2062
  : saveDirty
@@ -2537,12 +2069,12 @@
2537
2069
  }}
2538
2070
  onmousedown={(e) => e.stopPropagation()}
2539
2071
  >
2540
- <svg class="props-editor-save-icon absolute left-2 h-3 w-3 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2072
+ <svg class="props-editor-save-icon absolute start-2 h-3 w-3 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2541
2073
  <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
2542
2074
  <polyline points="17 21 17 13 7 13 7 21" />
2543
2075
  <polyline points="7 3 7 8 15 8" />
2544
2076
  </svg>
2545
- <span class="props-editor-save-label">{saveSaving ? 'Saving' : 'Save'}</span>
2077
+ <span class="props-editor-save-label">{saveSaving ? $t('editor.props.saving') : $t('editor.props.save')}</span>
2546
2078
  </button>
2547
2079
  {/if}
2548
2080
  </div>
@@ -2555,8 +2087,8 @@
2555
2087
  {#if !designSchema && !contentSchema}
2556
2088
  {#if showSchemaError}
2557
2089
  <div class="props-editor-schema-error flex flex-col items-center justify-center h-full">
2558
- <div class="props-editor-error-title text-error text-lg font-medium">No schema configuration available</div>
2559
- <div class="props-editor-error-code text-grey5 subtext mt-1">Error code: CONFIG_NOT_FOUND</div>
2090
+ <div class="props-editor-error-title text-error text-lg font-medium">{$t('editor.props.noConfiguration')}</div>
2091
+ <div class="props-editor-error-code text-grey5 subtext mt-1">{$t('editor.failure.errorCode', { code: 'CONFIG_NOT_FOUND' })}</div>
2560
2092
  </div>
2561
2093
  {/if}
2562
2094
  {:else}
@@ -2572,17 +2104,14 @@
2572
2104
  value="design"
2573
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"
2574
2106
  >
2575
- <span class="body">{designTabTitle}</span>
2107
+ <span class="body">{designTabTitle ?? $t('editor.props.designTab')}</span>
2576
2108
  </Tabs.Trigger>
2577
2109
  {#if contentSchema}
2578
2110
  <Tabs.Trigger
2579
- onclick={() => {
2580
- initializeDraggable();
2581
- }}
2582
2111
  value="content"
2583
2112
  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
2113
  >
2585
- <span class="body">{contentTabTitle}</span>
2114
+ <span class="body">{contentTabTitle ?? $t('editor.props.contentTab')}</span>
2586
2115
  </Tabs.Trigger>
2587
2116
  {/if}
2588
2117
  </Tabs.List>
@@ -2668,7 +2197,7 @@
2668
2197
  </div>
2669
2198
  {/if}
2670
2199
 
2671
- <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0 ml-auto'}" style={isFontIc ? '' : 'width: 185px;'}>
2200
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0 ms-auto'}" style={isFontIc ? '' : 'width: 185px;'}>
2672
2201
  <InputControllerRenderer
2673
2202
  controller={describeInputController(InputComponent, {
2674
2203
  ...props,
@@ -2688,13 +2217,13 @@
2688
2217
  {:else}
2689
2218
  <div class="p-4 text-gray-500">
2690
2219
  {#if !designSchema}
2691
- No design schema available
2220
+ {$t('editor.props.noDesignSchema')}
2692
2221
  {:else if !designSchema[0]}
2693
- No schema data available
2222
+ {$t('editor.props.noSchemaData')}
2694
2223
  {:else if !designSchema[0].components}
2695
- No components defined in schema
2224
+ {$t('editor.props.noComponents')}
2696
2225
  {:else}
2697
- No properties available
2226
+ {$t('editor.props.noProperties')}
2698
2227
  {/if}
2699
2228
  </div>
2700
2229
  {/if}
@@ -2734,8 +2263,8 @@
2734
2263
  onOpenChange={() => isGenericContentOpen = !isGenericContentOpen}
2735
2264
  >
2736
2265
  <Collapsible.Trigger class="generic-content-trigger w-full">
2737
- <div class="flex items-center h-13" style="padding-left: {CONTENT_INNER_PADDING}px; padding-right: {CONTENT_INNER_PADDING}px;">
2738
- <h3 class="body-medium text-grey7 flex-1 content-title">{contentSchema?.genericContent?.title || 'Content'}</h3>
2266
+ <div class="flex items-center h-13" style="padding-inline-start: {CONTENT_INNER_PADDING}px; padding-inline-end: {CONTENT_INNER_PADDING}px;">
2267
+ <h3 class="body-medium text-grey7 flex-1 content-title">{contentSchema?.genericContent?.title || $t('editor.props.contentCard')}</h3>
2739
2268
  <ChevronDown class="h-6 w-6 text-grey5 transition-transform chevron" />
2740
2269
  </div>
2741
2270
  </Collapsible.Trigger>
@@ -2772,7 +2301,7 @@
2772
2301
  helpText={header.helpText}
2773
2302
  />
2774
2303
  {:else if !isFontIc}
2775
- <div class="flex-1 min-w-0 pr-4">
2304
+ <div class="flex-1 min-w-0 pe-4">
2776
2305
  <InputControllerHeader
2777
2306
  label={header.label}
2778
2307
  helpText={header.helpText}
@@ -2800,7 +2329,7 @@
2800
2329
  <!-- Content Items Container -->
2801
2330
  <div class="content-items-wrapper mt-0" style="position: relative; z-index: 1;">
2802
2331
  <!-- Sortable Container -->
2803
- <div class="content-items flex flex-col gap-4" bind:this={contentItemsContainer} style="position: relative; z-index: 1;">
2332
+ <div class="content-items flex flex-col gap-4" bind:this={contentItemsContainer} use:reorderableList={contentItemsReorderOptions} style="position: relative; z-index: 1;">
2804
2333
  {@html (() => {
2805
2334
  const items = contentSchema?.contentItems;
2806
2335
  if (!Array.isArray(items)) return '';
@@ -2831,7 +2360,7 @@
2831
2360
  </h4>
2832
2361
 
2833
2362
  <!-- Arrow Icon -->
2834
- <div class="pr-4">
2363
+ <div class="pe-4">
2835
2364
  <ChevronDown class="h-6 w-6 text-grey7 transition-transform chevron" />
2836
2365
  </div>
2837
2366
  </div>
@@ -2870,7 +2399,7 @@
2870
2399
  helpText={header.helpText}
2871
2400
  />
2872
2401
  {:else if !isFontIc}
2873
- <div class="flex-1 min-w-0 pr-4">
2402
+ <div class="flex-1 min-w-0 pe-4">
2874
2403
  <InputControllerHeader
2875
2404
  label={header.label}
2876
2405
  helpText={header.helpText}
@@ -2913,7 +2442,7 @@
2913
2442
  }}
2914
2443
  >
2915
2444
  <span class="subtext text-white transition-colors duration-200">
2916
- Delete item
2445
+ {$t('editor.props.deleteItem')}
2917
2446
  </span>
2918
2447
  </button>
2919
2448
  </div>
@@ -2937,7 +2466,7 @@
2937
2466
  >
2938
2467
  <div class="flex items-center justify-center gap-2">
2939
2468
  <AddNew class="w-5 h-5 text-grey5" />
2940
- <span class="body-medium text-grey7 group-hover:underline">Add new</span>
2469
+ <span class="body-medium text-grey7 group-hover:underline">{$t('editor.props.addItem')}</span>
2941
2470
  </div>
2942
2471
  </Button>
2943
2472
  </div>
@@ -3018,7 +2547,7 @@
3018
2547
  /* Icon stays put next to the text; label is centered in the remaining space
3019
2548
  with a small left bias so "Save" and "Saving" never shift the icon. */
3020
2549
  .props-editor-save-label {
3021
- padding-left: 14px;
2550
+ padding-inline-start: 14px;
3022
2551
  line-height: 1;
3023
2552
  }
3024
2553
  .props-editor-save-btn .props-editor-save-icon {
@@ -3101,20 +2630,20 @@
3101
2630
 
3102
2631
  /* First item - rounded at top only */
3103
2632
  :global(.accordion-item[data-position="first"]) {
3104
- border-top-left-radius: var(--radius-small);
3105
- border-top-right-radius: var(--radius-small);
3106
- border-bottom-left-radius: 0;
3107
- border-bottom-right-radius: 0;
2633
+ border-start-start-radius: var(--radius-small);
2634
+ border-start-end-radius: var(--radius-small);
2635
+ border-end-start-radius: 0;
2636
+ border-end-end-radius: 0;
3108
2637
  margin-top: 0; /* Explicitly remove top margin */
3109
2638
  }
3110
2639
 
3111
2640
  /* Last item - rounded at bottom only */
3112
2641
  :global(.accordion-item[data-position="last"]) {
3113
2642
  margin-bottom: 0;
3114
- border-top-left-radius: 0;
3115
- border-top-right-radius: 0;
3116
- border-bottom-left-radius: var(--radius-small);
3117
- border-bottom-right-radius: var(--radius-small);
2643
+ border-start-start-radius: 0;
2644
+ border-start-end-radius: 0;
2645
+ border-end-start-radius: var(--radius-small);
2646
+ border-end-end-radius: var(--radius-small);
3118
2647
  }
3119
2648
 
3120
2649
  /* Middle items - no rounded corners */
@@ -3134,24 +2663,6 @@
3134
2663
  justify-content: center !important;
3135
2664
  }
3136
2665
 
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
2666
  :global([data-scrollable="false"] .scrollbar) {
3156
2667
  display: none;
3157
2668
  }
@@ -3234,13 +2745,13 @@
3234
2745
  /* Update scrollbar styling to match */
3235
2746
  :global([data-scrollable="true"] .scrollbar) {
3236
2747
  width: 8px !important; /* Thinner scrollbar */
3237
- margin-right: 2px !important; /* Add padding from the right */
2748
+ margin-inline-end: 2px !important; /* Add padding from the right */
3238
2749
  }
3239
2750
 
3240
2751
  :global([data-scrollable="true"] .thumb) {
3241
2752
  background-color: hsl(var(--grey1)) !important;
3242
2753
  border-radius: 3px !important;
3243
- margin-right: 2px !important;
2754
+ margin-inline-end: 2px !important;
3244
2755
  }
3245
2756
 
3246
2757
  :global([data-scrollable="true"] .thumb:hover) {
@@ -3249,11 +2760,11 @@
3249
2760
 
3250
2761
  :global([data-scrollable="true"] .track) {
3251
2762
  background-color: transparent !important;
3252
- margin-right: 2px !important;
2763
+ margin-inline-end: 2px !important;
3253
2764
  }
3254
2765
 
3255
2766
  :global([data-scrollable="true"]) {
3256
- padding-right: 2px !important; /* Add padding for scrollbar */
2767
+ padding-inline-end: 2px !important; /* Add padding for scrollbar */
3257
2768
  }
3258
2769
 
3259
2770
  /* Add these styles to ensure proper z-index layering */
@@ -3266,11 +2777,6 @@
3266
2777
  z-index: 99999 !important; /* Force highest z-index */
3267
2778
  }
3268
2779
 
3269
- /* Update existing styles to ensure proper stacking */
3270
- :global(.draggable-mirror) {
3271
- z-index: 100;
3272
- }
3273
-
3274
2780
  :global(.popover-content) {
3275
2781
  z-index: 50;
3276
2782
  }
@@ -3382,7 +2888,7 @@
3382
2888
  /* Add hover effect for title text */
3383
2889
  :global(.content-item-card [data-radix-collapsible-trigger] h4),
3384
2890
  :global(.generic-content-card [data-radix-collapsible-trigger] h3) {
3385
- text-align: left !important;
2891
+ text-align: start !important;
3386
2892
  }
3387
2893
 
3388
2894
  /* Add hover overlay for the entire trigger area */
@@ -3454,7 +2960,7 @@
3454
2960
  /* Keep text left-aligned */
3455
2961
  :global(.content-item-card h4),
3456
2962
  :global(.generic-content-card h3) {
3457
- text-align: left !important;
2963
+ text-align: start !important;
3458
2964
  }
3459
2965
 
3460
2966
  /* Add this new rule to remove focus borders */
@@ -3524,7 +3030,7 @@
3524
3030
  :global(.input-controller > div:first-child) {
3525
3031
  flex: 1 !important;
3526
3032
  min-width: 0 !important;
3527
- margin-right: 16px !important;
3033
+ margin-inline-end: 16px !important;
3528
3034
  }
3529
3035
 
3530
3036
  :global(.input-controller > div:last-child) {
@@ -3546,7 +3052,7 @@
3546
3052
  width: 100% !important;
3547
3053
  max-width: 100% !important;
3548
3054
  flex: none !important;
3549
- margin-right: 0 !important;
3055
+ margin-inline-end: 0 !important;
3550
3056
  }
3551
3057
 
3552
3058
  /* New animation system */
@@ -3726,17 +3232,17 @@
3726
3232
 
3727
3233
  /* Modify existing position rules */
3728
3234
  :global(.accordion-item[data-position="first"]) {
3729
- border-top-left-radius: var(--radius-small);
3730
- border-top-right-radius: var(--radius-small);
3731
- border-bottom-left-radius: 0;
3732
- border-bottom-right-radius: 0;
3235
+ border-start-start-radius: var(--radius-small);
3236
+ border-start-end-radius: var(--radius-small);
3237
+ border-end-start-radius: 0;
3238
+ border-end-end-radius: 0;
3733
3239
  }
3734
3240
 
3735
3241
  :global(.accordion-item[data-position="last"]) {
3736
- border-top-left-radius: 0;
3737
- border-top-right-radius: 0;
3738
- border-bottom-left-radius: var(--radius-small);
3739
- border-bottom-right-radius: var(--radius-small);
3242
+ border-start-start-radius: 0;
3243
+ border-start-end-radius: 0;
3244
+ border-end-start-radius: var(--radius-small);
3245
+ border-end-end-radius: var(--radius-small);
3740
3246
  }
3741
3247
 
3742
3248
  :global(.accordion-item[data-position="middle"]) {
@@ -3771,26 +3277,26 @@
3771
3277
 
3772
3278
  /* Ensure scroll area maintains border radius */
3773
3279
  :global([data-radix-scroll-area-viewport]) {
3774
- border-bottom-right-radius: var(--radius-large) !important;
3280
+ border-end-end-radius: var(--radius-large) !important;
3775
3281
  }
3776
3282
 
3777
3283
  :global(.scroll-area) {
3778
- border-bottom-right-radius: var(--radius-large) !important;
3284
+ border-end-end-radius: var(--radius-large) !important;
3779
3285
  }
3780
3286
 
3781
3287
  :global(.scroll-viewport) {
3782
- border-bottom-right-radius: var(--radius-large) !important;
3288
+ border-end-end-radius: var(--radius-large) !important;
3783
3289
  }
3784
3290
 
3785
3291
  /* Ensure scrollbar doesn't affect border radius */
3786
3292
  :global([data-radix-scroll-area-scrollbar]) {
3787
- border-bottom-right-radius: var(--radius-large) !important;
3788
- padding-right: 2px !important;
3293
+ border-end-end-radius: var(--radius-large) !important;
3294
+ padding-inline-end: 2px !important;
3789
3295
  }
3790
3296
 
3791
3297
  :global([data-radix-scroll-area-thumb]) {
3792
3298
  border-radius: var(--radius-small) !important;
3793
- margin-right: 2px !important;
3299
+ margin-inline-end: 2px !important;
3794
3300
  }
3795
3301
 
3796
3302
  /* Add z-index management for tooltips */
@@ -3830,7 +3336,7 @@
3830
3336
  /* Content Item Hover Styles */
3831
3337
  :global(.content-item-card [data-radix-collapsible-trigger] h4),
3832
3338
  :global(.generic-content-card [data-radix-collapsible-trigger] h3) {
3833
- text-align: left !important;
3339
+ text-align: start !important;
3834
3340
  }
3835
3341
 
3836
3342
  /* Add new specific hover styles for content titles */
@@ -3863,7 +3369,6 @@
3863
3369
  text-decoration: underline;
3864
3370
  }
3865
3371
 
3866
- /* Add Draggable Sort Animation styles */
3867
3372
  :global(.content-item) {
3868
3373
  position: relative;
3869
3374
  z-index: 1;
@@ -3871,95 +3376,77 @@
3871
3376
  transform: translate3d(0, 0, 0);
3872
3377
  }
3873
3378
 
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;
3379
+ /*
3380
+ * Reordering the items by dragging a handle (utils/itemReorder.ts). While an item is dragged, every item is
3381
+ * folded to one row at once (the items that were open keep their content, it is only hidden), so the whole
3382
+ * list is within reach and the places to drop do not move under the pointer.
3383
+ */
3384
+ :global(.content-items.is-reordering) {
3385
+ user-select: none;
3386
+ -webkit-user-select: none;
3884
3387
  }
3885
3388
 
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;
3894
- }
3895
-
3896
- :global(.content-item.draggable-mirror) {
3897
- transform: scale(1.05);
3389
+ /* The card is what is folded, not the item: the line that shows the drop place sits outside the item's box */
3390
+ :global(.content-items.is-reordering .content-item-card) {
3391
+ height: 56px !important;
3392
+ min-height: 56px !important;
3393
+ max-height: 56px !important;
3394
+ overflow: hidden !important;
3898
3395
  }
3899
3396
 
3900
- :global(.content-item.draggable--original) {
3901
- opacity: 0;
3397
+ :global(.content-items.is-reordering [data-collapsible-content]) {
3398
+ display: none !important;
3902
3399
  }
3903
3400
 
3904
- :global(.content-item.draggable-source--is-dragging) {
3401
+ /* The item being dragged stays in place, faded; the floating copy is what follows the pointer */
3402
+ :global(.content-item.is-reorder-source) {
3905
3403
  opacity: 0.3;
3906
3404
  }
3907
3405
 
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;
3406
+ /* A line in the gap where the dragged item would be dropped: in front of an item, or behind the last one */
3407
+ :global(.content-item.is-reorder-drop-before)::before,
3408
+ :global(.content-item.is-reorder-drop-after)::after {
3409
+ content: '';
3410
+ position: absolute;
3411
+ left: 0;
3412
+ right: 0;
3413
+ height: 3px;
3414
+ border-radius: 2px;
3415
+ background-color: hsl(var(--sage-green));
3416
+ pointer-events: none;
3922
3417
  }
3923
3418
 
3924
- :global(.content-items--is-dragging) {
3925
- cursor: grabbing;
3419
+ :global(.content-item.is-reorder-drop-before)::before {
3420
+ top: -9px;
3926
3421
  }
3927
3422
 
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;
3423
+ :global(.content-item.is-reorder-drop-after)::after {
3424
+ bottom: -9px;
3931
3425
  }
3932
3426
 
3933
- :global(.content-items--is-dragging .content-item:not(.draggable-mirror) .content-title:hover) {
3934
- text-decoration: none !important;
3427
+ /* The floating copy of the dragged item: one row, with the editor's green outline */
3428
+ :global(.reorder-mirror) {
3429
+ box-sizing: border-box;
3430
+ border: 2px solid hsl(var(--sage-green));
3431
+ border-radius: var(--radius-small);
3432
+ background-color: white;
3433
+ box-shadow: none;
3434
+ will-change: transform;
3935
3435
  }
3936
3436
 
3937
- :global(.content-items--is-dragging .content-item-trigger:hover .content-title) {
3437
+ :global(.reorder-mirror .content-title) {
3438
+ color: hsl(var(--sage-green));
3938
3439
  text-decoration: none !important;
3939
3440
  }
3940
3441
 
3941
- :global(.content-items--is-dragging .content-item.draggable--over) {
3942
- transform: scale(1.015) !important;
3442
+ :global(.reorder-mirror .chevron),
3443
+ :global(.reorder-mirror .drag-handle svg) {
3444
+ color: hsl(var(--sage-green));
3943
3445
  }
3944
3446
 
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;
3447
+ /* The card inside the copy has its own border: the outline above is the only one */
3448
+ :global(.reorder-mirror .content-item-card) {
3449
+ border: none;
3963
3450
  }
3964
3451
 
3965
3452
  /* Update drag handle hover area and content title behavior */
@@ -3970,6 +3457,8 @@
3970
3457
  justify-content: center !important;
3971
3458
  height: 100% !important;
3972
3459
  cursor: move !important;
3460
+ /* Touching the handle drags the item; it must not scroll the page */
3461
+ touch-action: none;
3973
3462
  }
3974
3463
 
3975
3464
  /* Keep the icon size consistent */
@@ -3995,40 +3484,15 @@
3995
3484
 
3996
3485
  /* Remove any previous left padding from the flex container */
3997
3486
  :global(.content-item-trigger .flex) {
3998
- padding-left: 0 !important;
3999
- }
4000
-
4001
- :global(.draggable-mirror .drag-handle svg) {
4002
- color: hsl(var(--grey3)) !important;
4003
- }
4004
-
4005
- :global(.draggable--original) {
4006
- display: none;
3487
+ padding-inline-start: 0 !important;
4007
3488
  }
4008
3489
 
4009
- :global(.content-items--is-dragging) {
4010
- cursor: grabbing;
4011
- }
4012
-
4013
-
4014
3490
  :global(.content-item) {
4015
3491
  contain: layout style;
4016
3492
  will-change: transform;
4017
3493
  transform: translate3d(0, 0, 0);
4018
3494
  }
4019
3495
 
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
3496
  /* Animation classes */
4033
3497
  :global(.design-tab-content[data-state="active"]) {
4034
3498
  animation: none;
@@ -4050,6 +3514,15 @@
4050
3514
  transform: none;
4051
3515
  }
4052
3516
 
3517
+ /* The two tabs trade places on a right to left screen, so each panel slides in from the side that its tab is on. */
3518
+ :global(.design-tab-content.animate-slide-left[data-state="active"]:dir(rtl)) {
3519
+ animation-name: slideFromRight;
3520
+ }
3521
+
3522
+ :global(.content-tab-content.animate-slide-right[data-state="active"]:dir(rtl)) {
3523
+ animation-name: slideFromLeft;
3524
+ }
3525
+
4053
3526
  @keyframes slideFromLeft {
4054
3527
  from {
4055
3528
  opacity: 0;
@@ -4094,8 +3567,6 @@
4094
3567
  }
4095
3568
  }
4096
3569
 
4097
-
4098
-
4099
3570
  @keyframes slideUp {
4100
3571
  from {
4101
3572
  opacity: 0;
@@ -4147,46 +3618,6 @@
4147
3618
  }
4148
3619
  }
4149
3620
 
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
3621
  :global(body.dragging-preserve-borders .form-input),
4191
3622
  :global(body.dragging-preserve-borders [id^="number-input"]),
4192
3623
  :global(body.dragging-preserve-borders .number-input-button),
@@ -4197,20 +3628,6 @@
4197
3628
  border: 1px solid hsl(var(--grey2)) !important;
4198
3629
  }
4199
3630
 
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
3631
  /* Add explicit transition for content tab chevrons */
4215
3632
  :global(.content-item-card .chevron),
4216
3633
  :global(.generic-content-card .chevron) {