@widgetic/editor 4.0.6 → 4.0.8

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 (64) hide show
  1. package/README.md +12 -1
  2. package/dist/components/WidgetPreview.svelte +42 -1
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/editors/EmbedEditor.svelte +101 -530
  5. package/dist/components/editors/EmbedEditor.svelte.d.ts +7 -17
  6. package/dist/components/editors/PropsEditor.svelte +328 -142
  7. package/dist/components/editors/PropsEditor.svelte.d.ts +13 -2
  8. package/dist/components/ic/AngleInputController.svelte +21 -7
  9. package/dist/components/ic/AngleInputController.svelte.d.ts +16 -6
  10. package/dist/components/ic/BrowserInputController.svelte +28 -15
  11. package/dist/components/ic/BrowserInputController.svelte.d.ts +25 -19
  12. package/dist/components/ic/CheckboxInputController.svelte +21 -7
  13. package/dist/components/ic/CheckboxInputController.svelte.d.ts +25 -15
  14. package/dist/components/ic/ColorPickerInputController.svelte +47 -21
  15. package/dist/components/ic/ColorPickerInputController.svelte.d.ts +42 -19
  16. package/dist/components/ic/DateFieldInputController.svelte +21 -7
  17. package/dist/components/ic/DateFieldInputController.svelte.d.ts +24 -14
  18. package/dist/components/ic/DatePickerInputController.svelte +25 -8
  19. package/dist/components/ic/DatePickerInputController.svelte.d.ts +21 -8
  20. package/dist/components/ic/DropdownInputController.svelte +69 -19
  21. package/dist/components/ic/DropdownInputController.svelte.d.ts +33 -17
  22. package/dist/components/ic/FixedPositionInputController.svelte +24 -10
  23. package/dist/components/ic/FixedPositionInputController.svelte.d.ts +16 -4
  24. package/dist/components/ic/FontInputController.svelte +53 -18
  25. package/dist/components/ic/FontInputController.svelte.d.ts +33 -18
  26. package/dist/components/ic/InputControllerRenderer.svelte +98 -0
  27. package/dist/components/ic/InputControllerRenderer.svelte.d.ts +40 -0
  28. package/dist/components/ic/NumberInputController.svelte +23 -9
  29. package/dist/components/ic/NumberInputController.svelte.d.ts +18 -7
  30. package/dist/components/ic/PositionInputController.svelte +17 -5
  31. package/dist/components/ic/PositionInputController.svelte.d.ts +20 -11
  32. package/dist/components/ic/RadioGroupInputController.svelte +21 -7
  33. package/dist/components/ic/RadioGroupInputController.svelte.d.ts +22 -12
  34. package/dist/components/ic/RangeCalendarInputController.svelte +34 -16
  35. package/dist/components/ic/RangeCalendarInputController.svelte.d.ts +25 -11
  36. package/dist/components/ic/RoundedCornersInputController.svelte +26 -22
  37. package/dist/components/ic/RoundedCornersInputController.svelte.d.ts +26 -18
  38. package/dist/components/ic/SliderInputController.svelte +41 -18
  39. package/dist/components/ic/SliderInputController.svelte.d.ts +35 -19
  40. package/dist/components/ic/SliderRangeInputController.svelte +27 -11
  41. package/dist/components/ic/SliderRangeInputController.svelte.d.ts +22 -9
  42. package/dist/components/ic/SwitchInputController.svelte +17 -5
  43. package/dist/components/ic/SwitchInputController.svelte.d.ts +14 -5
  44. package/dist/components/ic/TagsInputController.svelte +21 -7
  45. package/dist/components/ic/TagsInputController.svelte.d.ts +16 -6
  46. package/dist/components/ic/TextAreaInputController.svelte +44 -25
  47. package/dist/components/ic/TextAreaInputController.svelte.d.ts +6 -4
  48. package/dist/components/ic/TextAreaPreviewInputController.svelte +21 -7
  49. package/dist/components/ic/TextAreaPreviewInputController.svelte.d.ts +16 -6
  50. package/dist/components/ic/TextInputController.svelte +23 -8
  51. package/dist/components/ic/TextInputController.svelte.d.ts +20 -8
  52. package/dist/components/ic/TimeInputController.svelte +17 -5
  53. package/dist/components/ic/TimeInputController.svelte.d.ts +14 -5
  54. package/dist/components/ic/VectorInputController.svelte +29 -10
  55. package/dist/components/ic/VectorInputController.svelte.d.ts +33 -18
  56. package/dist/components/icons/locked.svelte +5 -1
  57. package/dist/components/icons/locked.svelte.d.ts +11 -24
  58. package/dist/components/icons/unlocked.svelte +5 -1
  59. package/dist/components/icons/unlocked.svelte.d.ts +11 -24
  60. package/dist/index.d.ts +1 -0
  61. package/dist/schemaViews.d.ts +105 -0
  62. package/dist/schemaViews.js +11 -0
  63. package/dist/utils/FontManager.js +11 -4
  64. package/package.json +16 -12
@@ -1,3 +1,5 @@
1
+ <svelte:options runes={false} />
2
+
1
3
  <script lang="ts">
2
4
  /**
3
5
  * PropsEditor Component
@@ -13,7 +15,16 @@
13
15
  // Svelte imports
14
16
  import { onMount, onDestroy, createEventDispatcher, afterUpdate } from 'svelte';
15
17
  const dispatch = createEventDispatcher();
16
- import { writable, get } from 'svelte/store';
18
+ import { writable, get, type Writable } from 'svelte/store';
19
+ import type {
20
+ ContentSchemaItemView,
21
+ ContentSchemaPropertyView,
22
+ ContentSchemaView,
23
+ DesignSchemaComponentView,
24
+ DesignSchemaPropertyGroupView,
25
+ DesignSchemaPropertyView,
26
+ DesignSchemaView,
27
+ } from '../../schemaViews.js';
17
28
 
18
29
  /**
19
30
  * COMPONENTS IMPORTS
@@ -59,10 +70,12 @@
59
70
  TagsInputController,
60
71
  DateFieldInputController
61
72
  } from '../ic';
73
+ import type { InputControllerConfig } from '../ic';
62
74
 
63
75
  /**
64
76
  * UTILS IMPORTS
65
77
  */
78
+ import InputControllerRenderer, { type ControllerDescriptor } from '../ic/InputControllerRenderer.svelte';
66
79
  import MediaManager from '../../utils/MediaManager';
67
80
  import FontManager from '../../utils/FontManager';
68
81
  import { config as appConfig } from '../../config';
@@ -81,15 +94,6 @@
81
94
  */
82
95
  type InputValue = string | number | boolean | any | null;
83
96
 
84
- interface PropertyGroup {
85
- name: string;
86
- properties: Array<Property>;
87
- visibilityCondition?: {
88
- dependsOn: string | string[];
89
- condition: string;
90
- };
91
- }
92
-
93
97
  // Add this interface near the other interfaces at the top of the file
94
98
  interface ContentProperty {
95
99
  id: string;
@@ -114,9 +118,10 @@
114
118
  // Dimensions
115
119
  export let width: string = "450px";
116
120
  export let height: string = "720px";
117
- // Schemas
118
- export let designSchema: any = null;
119
- export let contentSchema: any | undefined = undefined;
121
+ // Schemas — typed against the package's canonical schema views
122
+ // (`$lib/schemaViews`), the same shapes the consumers normalize to.
123
+ export let designSchema: DesignSchemaView | null = null;
124
+ export let contentSchema: ContentSchemaView | undefined = undefined;
120
125
  export let itemTemplate: any = null; // Store template for new items
121
126
  // Tab titles
122
127
  export let designTabTitle: string = 'Design';
@@ -135,6 +140,23 @@
135
140
  // Enable built-in drag (component manages its own absolute positioning)
136
141
  export let enableDrag = true;
137
142
 
143
+ /** Show Save status control in the top drag/header bar (composition page). */
144
+ export let showSaveButton = false;
145
+ /** True when local edits are not yet persisted. */
146
+ export let saveDirty = false;
147
+ /** True while a save request is in flight. */
148
+ export let saveSaving = false;
149
+
150
+ /** Optional content below the shared toolbar, before property fields. */
151
+ export let headerContent = undefined;
152
+ export let headerClass = '';
153
+ export let dragHandleClass = 'text-grey2';
154
+ export let title = 'Edit Properties';
155
+ /** Hide the default missing-schema message for header-only editors. */
156
+ export let showSchemaError = true;
157
+ /** Optional layout wrapper moved by this editor's built-in drag and position API. */
158
+ export let positionTarget = null;
159
+
138
160
  // Initial values from saved composition (merged over schema defaults)
139
161
  export let initialDesignValues: Record<string, any> = {};
140
162
  export let initialContentValues: Record<string, any> = {};
@@ -144,8 +166,11 @@
144
166
  */
145
167
  const CONTENT_PADDING = 16; // Outer padding for both tabs
146
168
  const CONTENT_INNER_PADDING = 16; // Inner padding for cards in Content tab
147
- const HEADER_VERTICAL_PADDING = 24; // Padding for top and bottom of header
148
- const HEADER_HEIGHT = 85; // 96px for header + 1px for separator
169
+ const DRAG_BAR_HEIGHT = 28; // Drag handle / Save row (absolute, overlays the header top)
170
+ const TABS_VERTICAL_PADDING = 14; // Breathing room above and below the tab list
171
+ const TABS_LIST_HEIGHT = 56; // Tabs.List height
172
+ // Drag bar band + gap + tabs + gap + 1px separator
173
+ const HEADER_HEIGHT = DRAG_BAR_HEIGHT + TABS_VERTICAL_PADDING * 2 + TABS_LIST_HEIGHT + 1;
149
174
  const EDITOR_STROKE_COLOR = '#e5e7eb'; // Color for editor stroke
150
175
  const EDITOR_STROKE_WIDTH = '1px'; // Thickness of editor stroke
151
176
 
@@ -191,7 +216,7 @@
191
216
  const DRAG_SHIFT_X = 24; // shift mirror left so cursor is over drag handle
192
217
 
193
218
  // Helper function to get visible groups
194
- function getVisibleGroups(components: Array<{ propertyGroups: PropertyGroup[] }>): PropertyGroup[] {
219
+ function getVisibleGroups(components: DesignSchemaComponentView[]): PropertyGroup[] {
195
220
  return components?.reduce((acc: PropertyGroup[], component) => {
196
221
  const visibleGroups = component.propertyGroups.filter((group: PropertyGroup) =>
197
222
  shouldShowPropertyGroup(group, component.propertyGroups[0].properties, 'design')
@@ -205,7 +230,7 @@
205
230
 
206
231
  // Update the function with proper type annotations
207
232
  function getAccordionItemPosition(
208
- component: { propertyGroups: PropertyGroup[] },
233
+ component: DesignSchemaComponentView,
209
234
  propertyGroup: PropertyGroup,
210
235
  groupIndex: number
211
236
  ): 'first' | 'middle' | 'last' | 'only' {
@@ -928,8 +953,11 @@
928
953
  */
929
954
  onDestroy(() => {
930
955
  // Clean up drag listeners
931
- document.removeEventListener('mousemove', handleDragMove);
932
- document.removeEventListener('mouseup', handleDragEnd);
956
+ if (browser) {
957
+ document.removeEventListener('mousemove', handleDragMove);
958
+ document.removeEventListener('mouseup', handleDragEnd);
959
+ if (dragState.active) document.body.style.cursor = '';
960
+ }
933
961
 
934
962
  // Clean up scroll listeners
935
963
  if (designScrollViewport) {
@@ -1331,7 +1359,44 @@
1331
1359
  return '';
1332
1360
  }
1333
1361
 
1334
- function renderInputController(schema: string, property: any, propertyIndex: number, isFirstGroup: boolean, contentItemId?: string) {
1362
+ /**
1363
+ * The bag `renderInputController()` builds for one controller instance.
1364
+ * Declared keys are what PropsEditor itself always supplies; the index
1365
+ * signature carries the schema-driven constraint keys (`min`, `max`,
1366
+ * `options`, …) whose set depends on the property definition.
1367
+ * `describeInputController` picks from this bag per controller kind, so
1368
+ * anything a controller never reads never reaches it.
1369
+ */
1370
+ interface InputControllerProps {
1371
+ id: string;
1372
+ value: InputValue;
1373
+ opacity?: number;
1374
+ onChange: (value: InputValue) => void;
1375
+ onOpacityChange: (opacity: number) => void;
1376
+ portalTarget: string | HTMLElement;
1377
+ header?: { label: string; helpText?: string };
1378
+ config: InputControllerConfig;
1379
+ controllerId: string;
1380
+ openDropdowns: Writable<Set<string>>;
1381
+ toggleDropdown: (id: string) => void;
1382
+ onFocus: () => void;
1383
+ [schemaKey: string]: any;
1384
+ }
1385
+
1386
+ /**
1387
+ * What `renderInputController()` hands the template.
1388
+ * `component` is a 20-member union of controller component types — typing
1389
+ * it as the union makes `<svelte:component this={...}>` collapse attribute
1390
+ * checking to `never` (every prop errors); `any` restores the spread's
1391
+ * intended looseness.
1392
+ */
1393
+ interface RenderedInputController {
1394
+ component: any; // union of controller components — see comment above
1395
+ props: InputControllerProps;
1396
+ header: { label: string; helpText: string };
1397
+ }
1398
+
1399
+ function renderInputController(schema: string, property: any, propertyIndex: number, isFirstGroup: boolean, contentItemId?: string): RenderedInputController {
1335
1400
  const basePropertyId = property.id || property.name;
1336
1401
  const propertyId = contentItemId ? `${contentItemId}__${basePropertyId}` : basePropertyId;
1337
1402
  const controllerId = `${schema}-${propertyId}`;
@@ -1342,7 +1407,7 @@
1342
1407
  component: null,
1343
1408
  props: { id: controllerId },
1344
1409
  header: { label: basePropertyId, helpText: '' }
1345
- };
1410
+ } as RenderedInputController;
1346
1411
  }
1347
1412
  const InputControllerComponent = getInputControllerComponent(inputController.type.toLowerCase());
1348
1413
 
@@ -1366,17 +1431,11 @@
1366
1431
 
1367
1432
  const { description, label, ...restInputController } = inputController;
1368
1433
 
1369
- // Only set shouldFocus on initial mount or explicit focus request
1370
- const shouldFocus = activeControllerId === controllerId;
1371
-
1372
1434
  // Add special handling for position controller
1373
1435
  const additionalProps = inputController.type.toLowerCase() === 'position'
1374
1436
  ? { onDragStateChange: (dragging: boolean) => isDragging = dragging }
1375
1437
  : {};
1376
1438
 
1377
- // Add visibility dependency tracking
1378
- const hasVisibilityCondition = property.visibilityCondition !== undefined;
1379
-
1380
1439
  return {
1381
1440
  component: InputControllerComponent,
1382
1441
  props: {
@@ -1405,7 +1464,6 @@
1405
1464
  openDropdowns.set(new Set([id]));
1406
1465
  }
1407
1466
  },
1408
- shouldFocus,
1409
1467
  onFocus: () => {
1410
1468
  const newControllerId = `${schema}-${propertyId}`;
1411
1469
  if (schema === 'design' && lastFocusedDesignController !== newControllerId) {
@@ -1427,8 +1485,7 @@
1427
1485
  groupName: schema
1428
1486
  });
1429
1487
  }
1430
- },
1431
- 'data-visibility-dependent': hasVisibilityCondition
1488
+ }
1432
1489
  },
1433
1490
  header: {
1434
1491
  label: String(inputController.type || '').toLowerCase() === 'font'
@@ -1475,6 +1532,106 @@
1475
1532
  }
1476
1533
  }
1477
1534
 
1535
+ /**
1536
+ * Narrows the render bag down to exactly the props the chosen controller
1537
+ * declares. Each pick literal is contextually typed against that
1538
+ * controller's declared props interface, so passing a key the controller
1539
+ * never reads — or forgetting a required one — is a compile error here
1540
+ * instead of a silent no-op at runtime.
1541
+ */
1542
+ function describeInputController(component: ReturnType<typeof getInputControllerComponent> | null, props: InputControllerProps): ControllerDescriptor | null {
1543
+ switch (component) {
1544
+ case TextInputController: return { kind: 'textinput', props: {
1545
+ label: props.label, value: props.value, onChange: props.onChange,
1546
+ config: props.config, type: props.type, placeholder: props.placeholder,
1547
+ description: props.description, onFocus: props.onFocus } };
1548
+ case TextAreaPreviewInputController: return { kind: 'textarea', props: {
1549
+ label: props.label, value: props.value, onChange: props.onChange,
1550
+ config: props.config, description: props.description, portalTarget: props.portalTarget } };
1551
+ case NumberInputController: return { kind: 'number', props: {
1552
+ label: props.label, value: props.value, onChange: props.onChange, config: props.config,
1553
+ helpText: props.helpText, description: props.description, onFocus: props.onFocus } };
1554
+ case SliderInputController: return { kind: 'slider', props: {
1555
+ height: props.height, elementsGap: props.elementsGap, thumbSize: props.thumbSize,
1556
+ sliderType: props.sliderType, barHeight: props.barHeight, label: props.label,
1557
+ value: props.value, onChange: props.onChange, config: props.config,
1558
+ helpText: props.helpText, min: props.min, max: props.max, step: props.step,
1559
+ onFocus: props.onFocus, autofocus: props.autofocus } };
1560
+ case SliderRangeInputController: return { kind: 'sliderrange', props: {
1561
+ label: props.label, value: props.value, onChange: props.onChange, config: props.config,
1562
+ helpText: props.helpText, min: props.min, max: props.max, step: props.step,
1563
+ onFocus: props.onFocus } };
1564
+ case DropdownInputController: return { kind: 'dropdown', props: {
1565
+ label: props.label, defaultSelectedLabel: props.defaultSelectedLabel, value: props.value,
1566
+ onChange: props.onChange, config: props.config, options: props.options,
1567
+ description: props.description, controllerId: props.controllerId,
1568
+ openDropdowns: props.openDropdowns, toggleDropdown: props.toggleDropdown,
1569
+ onFocus: props.onFocus } };
1570
+ case DatePickerInputController: return { kind: 'datepicker', props: {
1571
+ label: props.label, value: props.value, onChange: props.onChange,
1572
+ description: props.description, controllerId: props.controllerId,
1573
+ openDropdowns: props.openDropdowns, toggleDropdown: props.toggleDropdown,
1574
+ onFocus: props.onFocus } };
1575
+ case DateFieldInputController: return { kind: 'datefield', props: {
1576
+ label: props.label, value: props.value, onChange: props.onChange, config: props.config,
1577
+ helpText: props.helpText, onFocus: props.onFocus } };
1578
+ case RangeCalendarInputController: return { kind: 'rangecalendar', props: {
1579
+ label: props.label, value: props.value, onChange: props.onChange,
1580
+ helpText: props.helpText, controllerId: props.controllerId,
1581
+ openDropdowns: props.openDropdowns, toggleDropdown: props.toggleDropdown,
1582
+ onFocus: props.onFocus } };
1583
+ case SwitchInputController: return { kind: 'switch', props: {
1584
+ label: props.label, value: props.value, onChange: props.onChange,
1585
+ description: props.description, onFocus: props.onFocus } };
1586
+ case ColorPickerInputController: return { kind: 'colorpicker', props: {
1587
+ label: props.label, value: props.value, onChange: props.onChange,
1588
+ controllerId: props.controllerId, id: props.id, config: props.config,
1589
+ helpText: props.helpText, saveRecentColors: props.saveRecentColors,
1590
+ showOpacityControls: props.showOpacityControls, autofocus: props.autofocus,
1591
+ portalTarget: props.portalTarget, pickerSize: props.pickerSize,
1592
+ sliderWidth: props.sliderWidth, opacity: props.opacity, showRing: props.showRing,
1593
+ onFocus: props.onFocus, onOpacityChange: props.onOpacityChange } };
1594
+ case CheckboxInputController: return { kind: 'checkbox', props: {
1595
+ value: props.value, onChange: props.onChange, config: props.config,
1596
+ description: props.description, onFocus: props.onFocus, options: props.options } };
1597
+ case RadioGroupInputController: return { kind: 'radiogroup', props: {
1598
+ value: props.value, onChange: props.onChange, config: props.config,
1599
+ description: props.description, onFocus: props.onFocus, options: props.options } };
1600
+ case FontInputController: return { kind: 'font', props: {
1601
+ label: props.label, style: props.style, value: props.value, onChange: props.onChange,
1602
+ description: props.description, controllerId: props.controllerId,
1603
+ openDropdowns: props.openDropdowns, toggleDropdown: props.toggleDropdown,
1604
+ hideRecentFromList: props.hideRecentFromList, hideTrashIcon: props.hideTrashIcon } };
1605
+ case PositionInputController: return { kind: 'position', props: {
1606
+ value: props.value, onChange: props.onChange, onFocus: props.onFocus,
1607
+ maskThumb: props.maskThumb, onDragStateChange: props.onDragStateChange } };
1608
+ case BrowserInputController: return { kind: 'browser', props: {
1609
+ header: props.header, label: props.label, value: props.value,
1610
+ onChange: props.onChange, portalTarget: props.portalTarget, config: props.config } };
1611
+ case FixedPositionInputController: return { kind: 'fixedposition', props: {
1612
+ value: props.value, onChange: props.onChange, onFocus: props.onFocus, id: props.id } };
1613
+ case VectorInputController: return { kind: 'vector', props: {
1614
+ label: props.label, value: props.value, onChange: props.onChange,
1615
+ description: props.description, controllerId: props.controllerId,
1616
+ openDropdowns: props.openDropdowns, toggleDropdown: props.toggleDropdown,
1617
+ onFocus: props.onFocus, hideRecentFromList: props.hideRecentFromList,
1618
+ hideTrashIcon: props.hideTrashIcon } };
1619
+ case AngleInputController: return { kind: 'angle', props: {
1620
+ label: props.label, value: props.value, onChange: props.onChange, config: props.config,
1621
+ helpText: props.helpText, onFocus: props.onFocus } };
1622
+ case TimeInputController: return { kind: 'time', props: {
1623
+ label: props.label, value: props.value, onChange: props.onChange,
1624
+ helpText: props.helpText, onFocus: props.onFocus } };
1625
+ case RoundedCornersInputController: return { kind: 'roundedcorners', props: {
1626
+ value: props.value, onChange: props.onChange, helpText: props.helpText,
1627
+ onFocus: props.onFocus } };
1628
+ case TagsInputController: return { kind: 'tags', props: {
1629
+ label: props.label, value: props.value, onChange: props.onChange, config: props.config,
1630
+ helpText: props.helpText, onFocus: props.onFocus } };
1631
+ default: return null;
1632
+ }
1633
+ }
1634
+
1478
1635
  // Create a store for managing open dropdowns (only one should be open at a time)
1479
1636
  const openDropdowns = writable<Set<string>>(new Set());
1480
1637
 
@@ -1581,37 +1738,16 @@
1581
1738
  }
1582
1739
 
1583
1740
  /**
1584
- * Interface definitions for content structure
1741
+ * SCHEMA SHAPES — aliases of the package's canonical schema views
1742
+ * (`$lib/schemaViews`, exported at the package root). PropsEditor used to
1743
+ * declare stricter local duplicates, which made every array flowing from
1744
+ * the open view props unassignable to them; the canonical names are the
1745
+ * same types now, so the contract holds across the package boundary.
1585
1746
  */
1586
- interface Property {
1587
- id: string;
1588
- name?: string;
1589
- type: string;
1590
- defaultValue?: any;
1591
- value?: any; // Add value property to interface
1592
- constraints?: Record<string, any>;
1593
- inputController: {
1594
- type: string;
1595
- description?: string;
1596
- helpText?: string;
1597
- config?: Record<string, any>;
1598
- };
1599
- }
1600
-
1601
- interface ContentItem {
1602
- id: string;
1603
- properties: Array<Property>;
1604
- orderNumber?: number;
1605
- title?: string;
1606
- }
1607
-
1608
- interface ContentSchema {
1609
- genericContent: {
1610
- title: string;
1611
- properties: Array<Property>;
1612
- };
1613
- contentItems: ContentItem[];
1614
- }
1747
+ type Property = DesignSchemaPropertyView;
1748
+ type PropertyGroup = DesignSchemaPropertyGroupView;
1749
+ type ContentItem = ContentSchemaItemView;
1750
+ type ContentSchema = ContentSchemaView;
1615
1751
 
1616
1752
  onMount(() => {
1617
1753
  const resizeObserver = new ResizeObserver(() => {
@@ -2034,7 +2170,13 @@
2034
2170
  const key = p.id || p.name;
2035
2171
  // Canonical state first; schema mirror/defaultValue are legacy
2036
2172
  // seeds for keys the editor never touched.
2037
- const liveOnItem = key != null ? (item as Record<string, unknown>)[key] : undefined;
2173
+ const liveOnItem =
2174
+ key != null
2175
+ ? // Items legitimately carry canonical per-key state alongside
2176
+ // `properties`; route through unknown to the index signature
2177
+ // honestly.
2178
+ (item as unknown as Record<string, unknown>)[key]
2179
+ : undefined;
2038
2180
  props[key] = normalizeContentPropertyValue(
2039
2181
  pickLiveContentValue(
2040
2182
  contentState[`${item.id}__${key}`],
@@ -2252,28 +2394,43 @@
2252
2394
  let dragState = { active: false, startX: 0, startY: 0, initialLeft: 0, initialTop: 0 };
2253
2395
 
2254
2396
  function handleDragStart(e: MouseEvent) {
2255
- if (!enableDrag || !rootElement) return;
2397
+ const target = positionTarget || rootElement;
2398
+ if (!enableDrag || !target || e.button !== 0) return;
2256
2399
  e.preventDefault();
2257
-
2400
+ e.stopPropagation();
2401
+
2402
+ // Wrapped editors keep the measured size, including the parent's height clamp.
2403
+ // Resolve percentage/centered placement before leaving flow, never parse it as pixels.
2404
+ if (positionTarget) {
2405
+ const rectangle = target.getBoundingClientRect();
2406
+ const parent = target.offsetParent;
2407
+ const parentRectangle = parent?.getBoundingClientRect() || { left: 0, top: 0 };
2408
+ const left = rectangle.left - parentRectangle.left - (parent?.clientLeft || 0) + (parent?.scrollLeft || 0);
2409
+ const top = rectangle.top - parentRectangle.top - (parent?.clientTop || 0) + (parent?.scrollTop || 0);
2410
+ Object.assign(target.style, {
2411
+ position: 'absolute', margin: '0', transform: 'none',
2412
+ boxSizing: 'border-box', width: `${rectangle.width}px`, height: `${rectangle.height}px`,
2413
+ left: `${left}px`, top: `${top}px`
2414
+ });
2415
+ }
2258
2416
  dragState = {
2259
2417
  active: true,
2260
2418
  startX: e.clientX,
2261
2419
  startY: e.clientY,
2262
- initialLeft: parseFloat(rootElement.style.left) || 0,
2263
- initialTop: parseFloat(rootElement.style.top) || 0
2420
+ initialLeft: parseFloat(target.style.left) || 0,
2421
+ initialTop: parseFloat(target.style.top) || 0
2264
2422
  };
2265
- rootElement.style.zIndex = '50';
2423
+ target.style.zIndex = '50';
2266
2424
  document.addEventListener('mousemove', handleDragMove);
2267
2425
  document.addEventListener('mouseup', handleDragEnd);
2268
2426
  document.body.style.cursor = 'move';
2269
2427
  }
2270
2428
 
2271
2429
  function handleDragMove(e: MouseEvent) {
2272
- if (!dragState.active || !rootElement) return;
2430
+ if (!dragState.active) return;
2273
2431
  const left = dragState.initialLeft + (e.clientX - dragState.startX);
2274
2432
  const top = dragState.initialTop + (e.clientY - dragState.startY);
2275
- rootElement.style.left = `${left}px`;
2276
- rootElement.style.top = `${top}px`;
2433
+ setPosition(left, top);
2277
2434
  dispatch('move', { left, top });
2278
2435
  }
2279
2436
 
@@ -2283,10 +2440,7 @@
2283
2440
  document.removeEventListener('mousemove', handleDragMove);
2284
2441
  document.removeEventListener('mouseup', handleDragEnd);
2285
2442
  document.body.style.cursor = '';
2286
- dispatch('moveEnd', {
2287
- left: parseFloat(rootElement.style.left) || 0,
2288
- top: parseFloat(rootElement.style.top) || 0
2289
- });
2443
+ dispatch('moveEnd', getPosition());
2290
2444
  }
2291
2445
 
2292
2446
  /** Public: update a design state value externally (used by EmbedEditor for dimension sync) */
@@ -2296,17 +2450,19 @@
2296
2450
 
2297
2451
  /** Public: set absolute position programmatically (used by parent for layout) */
2298
2452
  export function setPosition(left: number, top: number) {
2299
- if (!rootElement) return;
2300
- rootElement.style.left = `${left}px`;
2301
- rootElement.style.top = `${top}px`;
2453
+ const target = positionTarget || rootElement;
2454
+ if (!target) return;
2455
+ target.style.left = `${left}px`;
2456
+ target.style.top = `${top}px`;
2302
2457
  }
2303
2458
 
2304
2459
  /** Public: get current position */
2305
2460
  export function getPosition() {
2306
- if (!rootElement) return { left: 0, top: 0 };
2461
+ const target = positionTarget || rootElement;
2462
+ if (!target) return { left: 0, top: 0 };
2307
2463
  return {
2308
- left: parseFloat(rootElement.style.left) || 0,
2309
- top: parseFloat(rootElement.style.top) || 0
2464
+ left: parseFloat(target.style.left) || 0,
2465
+ top: parseFloat(target.style.top) || 0
2310
2466
  };
2311
2467
  }
2312
2468
 
@@ -2353,47 +2509,64 @@
2353
2509
  onValueChange={handleTabChange}
2354
2510
  >
2355
2511
 
2356
- {#if !designSchema && !contentSchema}
2357
- <div class="flex flex-col items-center justify-center h-full">
2358
- <div class="text-error text-lg font-medium">No schema configuration available</div>
2359
- <div class="text-grey5 subtext mt-1">Error code: CONFIG_NOT_FOUND</div>
2360
- </div>
2361
- {:else}
2362
- <!-- Drag Handle - Show outside header when no header exists -->
2363
- {#if showDragHandle && (!designSchema || !contentSchema)}
2512
+ <!-- A single toolbar owns drag and Save for all editor variants. -->
2513
+ {#if showDragHandle && (designSchema || contentSchema || headerContent)}
2364
2514
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2365
- <div class="absolute top-0 left-0 w-full flex justify-center z-[999]">
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'}">
2366
2516
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2367
2517
  <div
2368
- class="flex items-center justify-center h-6 cursor-move transition-all duration-200 widgetic-colors opacity-0 hover:opacity-100 drag-handle-container"
2518
+ class="flex items-center justify-center h-7 cursor-move transition-all duration-200 widgetic-colors opacity-0 hover:opacity-100 drag-handle-container"
2369
2519
  onmousedown={handleDragStart}
2370
2520
  >
2371
- <DragHandle class="w-[66px] h-[24px] text-grey2 hover:text-grey3 transition-colors" />
2521
+ <DragHandle class="w-16.5 h-6 {dragHandleClass} hover:text-grey3 transition-colors" />
2372
2522
  </div>
2523
+ {#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}
2525
+ <button
2526
+ 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
2528
+ {saveSaving
2529
+ ? 'text-[hsl(var(--deep-forest-green))] bg-[hsl(var(--light-green))]'
2530
+ : saveDirty
2531
+ ? 'text-white bg-[hsl(var(--deep-forest-green))] hover:bg-[hsl(var(--dark))] hover:text-white'
2532
+ : 'text-[hsl(var(--grey5))] bg-grey2 inset-ring inset-ring-grey3/20 hover:inset-ring-deep-forest-green hover:bg-[hsl(var(--deep-forest-green))] hover:text-white'}"
2533
+ disabled={saveSaving}
2534
+ onclick={(e) => {
2535
+ e.stopPropagation();
2536
+ if (!saveSaving && saveDirty) dispatch('save');
2537
+ }}
2538
+ onmousedown={(e) => e.stopPropagation()}
2539
+ >
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">
2541
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
2542
+ <polyline points="17 21 17 13 7 13 7 21" />
2543
+ <polyline points="7 3 7 8 15 8" />
2544
+ </svg>
2545
+ <span class="props-editor-save-label">{saveSaving ? 'Saving' : 'Save'}</span>
2546
+ </button>
2547
+ {/if}
2373
2548
  </div>
2374
2549
  {/if}
2375
2550
 
2551
+ {#if headerContent}
2552
+ <div class="props-editor-header-content shrink-0 {headerClass}">{@render headerContent()}</div>
2553
+ {/if}
2554
+
2555
+ {#if !designSchema && !contentSchema}
2556
+ {#if showSchemaError}
2557
+ <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>
2560
+ </div>
2561
+ {/if}
2562
+ {:else}
2376
2563
  <!-- Tabs header with buttons and separator - only show if both schemas exist -->
2377
2564
  {#if designSchema && contentSchema}
2378
2565
  <div
2379
- class="tabs-header h-[96px] flex items-start pt-6 px-6 pb-4 rounded-t-[var(--radius-large)] header-hover-area relative"
2566
+ class="tabs-header h-28 shrink-0 flex items-start pt-10.5 px-6 pb-3.5 rounded-t-large header-hover-area relative"
2380
2567
  >
2381
- <!-- Drag Handle - Show inside header when header exists -->
2382
- {#if showDragHandle}
2383
- <!-- svelte-ignore a11y_no_static_element_interactions -->
2384
- <div class="absolute top-0 left-0 w-full flex justify-center z-[999]">
2385
- <!-- svelte-ignore a11y_no_static_element_interactions -->
2386
- <div
2387
- class="flex items-center justify-center h-6 cursor-move transition-all duration-200 widgetic-colors opacity-0 hover:opacity-100 drag-handle-container"
2388
- onmousedown={handleDragStart}
2389
- >
2390
- <DragHandle class="w-[66px] h-[24px] text-grey2 hover:text-grey3 transition-colors" />
2391
- </div>
2392
- </div>
2393
- {/if}
2394
-
2395
2568
  <Tabs.List
2396
- class="tabs-list w-full h-[56px] rounded-large bg-grey2 p-1"
2569
+ class="tabs-list w-full h-14 rounded-large bg-grey2 p-1"
2397
2570
  >
2398
2571
  <Tabs.Trigger
2399
2572
  value="design"
@@ -2421,7 +2594,7 @@
2421
2594
  {/if}
2422
2595
 
2423
2596
  <!-- Design Tab Content -->
2424
- <div class="tab-design-container flex-grow overflow-y-auto scroll-area widgetic-colors hidden"
2597
+ <div class="tab-design-container grow scroll-area widgetic-colors hidden {headerContent ? 'min-h-0 flex-1 overflow-hidden' : 'overflow-y-auto'}"
2425
2598
  class:hidden={currentTab !== 'design'}
2426
2599
  style="max-height: {!designSchema || !contentSchema ? '100%' : `calc(100% - ${HEADER_HEIGHT}px)`}"
2427
2600
  data-scrollable={$designScrollState.shouldShowScroll}
@@ -2430,13 +2603,13 @@
2430
2603
  class="h-full"
2431
2604
  type="hover"
2432
2605
  scrollHideDelay={0}
2433
- scrollbarYClasses="flex touch-none select-none transition-colors w-[24px] px-2 bg-white {!designSchema || !contentSchema ? 'pt-6' : 'pt-4'} pb-6 [&>div]:w-[8px] [&>div]:bg-grey1 [&>div]:rounded-pill [&>div]:cursor-pointer [&>div]:transition-colors [&>div]:duration-250 [&>div]:hover:bg-grey2"
2606
+ scrollbarYClasses="flex touch-none select-none transition-colors w-6 px-2 {headerContent ? 'bg-transparent' : 'bg-white'} {!designSchema || !contentSchema ? 'pt-7' : 'pt-4'} pb-6 [&>div]:w-2 [&>div]:bg-grey1 [&>div]:rounded-pill [&>div]:cursor-pointer [&>div]:transition-colors [&>div]:duration-250 [&>div]:hover:bg-grey2"
2434
2607
  >
2435
2608
  <div class="scroll-viewport" data-radix-scroll-area-viewport bind:this={designScrollViewport}>
2436
- <div class="px-6 {!designSchema || !contentSchema ? 'py-6' : 'pt-4 pb-6'}" bind:this={designContentWrapper}>
2609
+ <div class="px-6 {!designSchema || !contentSchema ? 'pt-7 pb-6' : 'pt-4 pb-6'}" bind:this={designContentWrapper}>
2437
2610
  <Tabs.Content
2438
2611
  value="design"
2439
- class="design-tab-content !mt-0 tabs-content-container w-full outline-none {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-left' : ''}"
2612
+ class="design-tab-content mt-0! tabs-content-container w-full outline-none {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-left' : ''}"
2440
2613
  data-tab="design"
2441
2614
  style="animation-duration: 300ms; transform: none;"
2442
2615
  >
@@ -2495,12 +2668,12 @@
2495
2668
  </div>
2496
2669
  {/if}
2497
2670
 
2498
- <div class="ic-input-ct {isFontIc ? 'w-full' : 'flex-shrink-0 ml-auto'}" style={isFontIc ? '' : 'width: 185px;'}>
2499
- <svelte:component
2500
- this={InputComponent}
2501
- {...props}
2502
- value={reactiveValue !== undefined ? reactiveValue : props.value}
2503
- shouldFocus={activeControllerId === props.id}
2671
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0 ml-auto'}" style={isFontIc ? '' : 'width: 185px;'}>
2672
+ <InputControllerRenderer
2673
+ controller={describeInputController(InputComponent, {
2674
+ ...props,
2675
+ value: reactiveValue !== undefined ? reactiveValue : props.value
2676
+ })}
2504
2677
  />
2505
2678
  </div>
2506
2679
  </div>
@@ -2533,7 +2706,7 @@
2533
2706
 
2534
2707
  <!-- Content Tab Content -->
2535
2708
  {#if contentSchema}
2536
- <div class="tab-content-container flex-grow overflow-y-auto scroll-area widgetic-colors hidden"
2709
+ <div class="tab-content-container grow overflow-y-auto scroll-area widgetic-colors hidden"
2537
2710
  class:hidden={currentTab !== 'content'}
2538
2711
  style="max-height: {!contentSchema || !designSchema ? '100%' : `calc(100% - ${HEADER_HEIGHT}px)`}"
2539
2712
  data-scrollable={$contentScrollState.shouldShowScroll}
@@ -2542,13 +2715,13 @@
2542
2715
  class="h-full"
2543
2716
  type="hover"
2544
2717
  scrollHideDelay={0}
2545
- scrollbarYClasses="flex touch-none select-none transition-colors w-[24px] px-2 bg-white {!designSchema || !contentSchema ? 'pt-6' : 'pt-4'} pb-6 [&>div]:w-[8px] [&>div]:bg-grey1 [&>div]:rounded-pill [&>div]:cursor-pointer [&>div]:transition-colors [&>div]:duration-250 [&>div]:hover:bg-grey2"
2718
+ scrollbarYClasses="flex touch-none select-none transition-colors w-6 px-2 {headerContent ? 'bg-transparent' : 'bg-white'} {!designSchema || !contentSchema ? 'pt-7' : 'pt-4'} pb-6 [&>div]:w-2 [&>div]:bg-grey1 [&>div]:rounded-pill [&>div]:cursor-pointer [&>div]:transition-colors [&>div]:duration-250 [&>div]:hover:bg-grey2"
2546
2719
  >
2547
2720
  <div class="scroll-viewport" data-radix-scroll-area-viewport bind:this={contentScrollViewport}>
2548
- <div class="px-6 {!designSchema || !contentSchema ? 'pt-6 pb-6' : 'pt-4 pb-6'}" bind:this={contentWrapper}>
2721
+ <div class="px-6 {!designSchema || !contentSchema ? 'pt-7 pb-6' : 'pt-4 pb-6'}" bind:this={contentWrapper}>
2549
2722
  <Tabs.Content
2550
2723
  value="content"
2551
- class="content-tab-content !mt-0 tabs-content-container w-full {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-right' : ''}"
2724
+ class="content-tab-content mt-0! tabs-content-container w-full {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-right' : ''}"
2552
2725
  data-tab="content"
2553
2726
  style="transform: none; position: relative; z-index: 1;"
2554
2727
  >
@@ -2561,7 +2734,7 @@
2561
2734
  onOpenChange={() => isGenericContentOpen = !isGenericContentOpen}
2562
2735
  >
2563
2736
  <Collapsible.Trigger class="generic-content-trigger w-full">
2564
- <div class="flex items-center h-[52px]" style="padding-left: {CONTENT_INNER_PADDING}px; padding-right: {CONTENT_INNER_PADDING}px;">
2737
+ <div class="flex items-center h-13" style="padding-left: {CONTENT_INNER_PADDING}px; padding-right: {CONTENT_INNER_PADDING}px;">
2565
2738
  <h3 class="body-medium text-grey7 flex-1 content-title">{contentSchema?.genericContent?.title || 'Content'}</h3>
2566
2739
  <ChevronDown class="h-6 w-6 text-grey5 transition-transform chevron" />
2567
2740
  </div>
@@ -2574,7 +2747,7 @@
2574
2747
  <Collapsible.Content>
2575
2748
  {#if isGenericContentOpen}
2576
2749
  <Card.Content class="p-0 px-4">
2577
- <div class="pt-[14px] pb-[16px]">
2750
+ <div class="pt-3.5 pb-4">
2578
2751
  <div class="properties-list flex flex-col" style="gap: 8px;">
2579
2752
  {#each contentSchema?.genericContent?.properties || [] as property, propertyIndex}
2580
2753
  {@const result = renderInputController(
@@ -2607,12 +2780,9 @@
2607
2780
  </div>
2608
2781
  {/if}
2609
2782
 
2610
- <div class="ic-input-ct {isFontIc ? 'w-full' : 'flex-shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2611
- <svelte:component
2612
- this={InputComponent}
2613
- {...props}
2614
- {header}
2615
- shouldFocus={activeControllerId === props.id}
2783
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2784
+ <InputControllerRenderer
2785
+ controller={describeInputController(InputComponent, { ...props, header })}
2616
2786
  />
2617
2787
  </div>
2618
2788
  </div>
@@ -2650,8 +2820,8 @@
2650
2820
  onOpenChange={() => toggleItem(item.id)}
2651
2821
  >
2652
2822
  <Collapsible.Trigger class="content-item-trigger w-full">
2653
- <div class="flex items-center h-[52px] cursor-pointer widgetic-colors">
2654
- <div class="drag-handle flex items-center justify-center w-[56px] cursor-move hover:text-grey5">
2823
+ <div class="flex items-center h-13 cursor-pointer widgetic-colors">
2824
+ <div class="drag-handle flex items-center justify-center w-14 cursor-move hover:text-grey5">
2655
2825
  <IconGripVertical class="w-6 h-6 text-grey2 transition-colors duration-200" />
2656
2826
  </div>
2657
2827
 
@@ -2674,7 +2844,7 @@
2674
2844
  <Collapsible.Content>
2675
2845
  {#if isItemOpen(item.id)}
2676
2846
  <Card.Content class="p-0 px-4">
2677
- <div class="pt-[14px] pb-[16px]">
2847
+ <div class="pt-3.5 pb-4">
2678
2848
  <div class="properties-list flex flex-col" style="gap: 8px;">
2679
2849
  {#each item.properties as property, propertyIndex}
2680
2850
  {@const result = renderInputController(
@@ -2708,12 +2878,9 @@
2708
2878
  </div>
2709
2879
  {/if}
2710
2880
 
2711
- <div class="ic-input-ct {isFontIc ? 'w-full' : 'flex-shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2712
- <svelte:component
2713
- this={InputComponent}
2714
- {...props}
2715
- {header}
2716
- shouldFocus={activeControllerId === props.id}
2881
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2882
+ <InputControllerRenderer
2883
+ controller={describeInputController(InputComponent, { ...props, header })}
2717
2884
  />
2718
2885
  </div>
2719
2886
  </div>
@@ -2722,11 +2889,11 @@
2722
2889
  </div>
2723
2890
 
2724
2891
  <!-- Delete Item Button -->
2725
- <div class="mt-[16px] flex flex-col gap-2 relative z-10">
2892
+ <div class="mt-4 flex flex-col gap-2 relative z-10">
2726
2893
  <!-- svelte-ignore a11y_mouse_events_have_key_events -->
2727
2894
  <button
2728
2895
  type="button"
2729
- class="widgetic-colors h-[34px] w-full flex items-center justify-center relative z-10 group"
2896
+ class="widgetic-colors h-8.5 w-full flex items-center justify-center relative z-10 group"
2730
2897
  style="
2731
2898
  background-color: hsl(var(--error)) !important;
2732
2899
  border-radius: var(--radius-small) !important;
@@ -2764,7 +2931,7 @@
2764
2931
  <div class="content-add-item-ct mt-4">
2765
2932
  <Button
2766
2933
  variant="ghost"
2767
- class="group w-full h-[52px] bg-grey1 hover:bg-grey2 active:scale-[0.98] border-2 border-grey2 rounded-medium"
2934
+ class="group w-full h-13 bg-grey1 hover:bg-grey2 active:scale-[0.98] border-2 border-grey2 rounded-medium"
2768
2935
  style="transition: background-color 0.2s ease, transform 0.15s ease !important;"
2769
2936
  onclick={handleAddNewItem}
2770
2937
  >
@@ -2788,7 +2955,7 @@
2788
2955
  <div
2789
2956
  bind:this={fileBrowserPortalHost}
2790
2957
  id="props-editor-file-browser-portal"
2791
- class="props-editor-file-browser-portal pointer-events-none absolute inset-0 z-[999998]"
2958
+ class="props-editor-file-browser-portal pointer-events-none absolute inset-0 z-999998"
2792
2959
  ></div>
2793
2960
  </Tabs.Root>
2794
2961
  </div>
@@ -2806,7 +2973,9 @@
2806
2973
  .props-editor-container:not(:has(.header-hover-area)) {
2807
2974
  position: relative;
2808
2975
  }
2809
- .props-editor-container:not(:has(.header-hover-area)):hover :global(.drag-handle-container) {
2976
+ .props-editor-container:not(:has(.header-hover-area)):hover :global(.drag-handle-container),
2977
+ .props-editor-container:has(.header-hover-area:hover) :global(.props-editor-drag-bar .drag-handle-container),
2978
+ .props-editor-container :global(.props-editor-drag-bar:hover .drag-handle-container) {
2810
2979
  opacity: 1 !important;
2811
2980
  }
2812
2981
  :global(.drag-handle-container) {
@@ -2844,6 +3013,23 @@
2844
3013
  .props-editor-container:has(.tabs-header) {
2845
3014
  --has-both-schemas: 1rem;
2846
3015
  }
3016
+
3017
+ /* ── Save button (composition drag bar) ── */
3018
+ /* Icon stays put next to the text; label is centered in the remaining space
3019
+ with a small left bias so "Save" and "Saving" never shift the icon. */
3020
+ .props-editor-save-label {
3021
+ padding-left: 14px;
3022
+ line-height: 1;
3023
+ }
3024
+ .props-editor-save-btn .props-editor-save-icon {
3025
+ color: currentColor !important;
3026
+ stroke: currentColor !important;
3027
+ }
3028
+ .props-editor-save-label,
3029
+ :global(.props-editor-save-label) {
3030
+ color: currentColor !important;
3031
+ line-height: 1;
3032
+ }
2847
3033
  :global(.content-items-wrapper) {
2848
3034
  padding-top: 1rem;
2849
3035
  }