@widgetic/editor 4.0.5 → 4.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,15 +12,7 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
12
12
  z_$$bindings?: Bindings;
13
13
  }
14
14
  declare const EmbedEditor: $$__sveltets_2_IsomorphicComponent<{
15
- /**
16
- * Component Configuration Props
17
- * width/height: Control overall component dimensions
18
- * headerHeight: Space allocated for embed code display section
19
- * embedSchema: Defines customizable properties for the embedded widget
20
- *
21
- * Note: headerHeight affects the vertical space distribution between
22
- * the embed code display and property editor sections
23
- */ class?: string;
15
+ /** Component configuration — existing public props and methods are preserved. */ class?: string;
24
16
  style?: string;
25
17
  embedHeaderText?: string;
26
18
  embedCode?: string;
@@ -28,23 +20,21 @@ declare const EmbedEditor: $$__sveltets_2_IsomorphicComponent<{
28
20
  embedSchema: any;
29
21
  initialValues?: Record<string, any>;
30
22
  standalone?: boolean;
23
+ /** Show Save status control in the shared drag bar (composition embed overlay). */ showSaveButton?: boolean;
24
+ /** True when local embed option edits are not yet persisted. */ saveDirty?: boolean;
25
+ /** True while a save request is in flight. */ saveSaving?: boolean;
31
26
  updateValue?: (propertyId: string, value: any) => void;
32
27
  setPosition?: (left: number, top: number) => void;
33
- getPosition?: () => {
34
- left: number;
35
- top: number;
36
- };
28
+ getPosition?: () => any;
37
29
  }, {
30
+ save: CustomEvent<any>;
38
31
  propertyChange: CustomEvent<any>;
39
32
  } & {
40
33
  [evt: string]: CustomEvent<any>;
41
34
  }, {}, {
42
35
  updateValue: (propertyId: string, value: any) => void;
43
36
  setPosition: (left: number, top: number) => void;
44
- getPosition: () => {
45
- left: number;
46
- top: number;
47
- };
37
+ getPosition: () => any;
48
38
  }, string>;
49
39
  type EmbedEditor = InstanceType<typeof EmbedEditor>;
50
40
  export default EmbedEditor;
@@ -1,3 +1,5 @@
1
+ <svelte:options runes={false} />
2
+
1
3
  <script lang="ts">
2
4
  /**
3
5
  * PropsEditor Component
@@ -63,6 +65,7 @@
63
65
  /**
64
66
  * UTILS IMPORTS
65
67
  */
68
+ import InputControllerRenderer, { type ControllerDescriptor } from '../ic/InputControllerRenderer.svelte';
66
69
  import MediaManager from '../../utils/MediaManager';
67
70
  import FontManager from '../../utils/FontManager';
68
71
  import { config as appConfig } from '../../config';
@@ -135,6 +138,23 @@
135
138
  // Enable built-in drag (component manages its own absolute positioning)
136
139
  export let enableDrag = true;
137
140
 
141
+ /** Show Save status control in the top drag/header bar (composition page). */
142
+ export let showSaveButton = false;
143
+ /** True when local edits are not yet persisted. */
144
+ export let saveDirty = false;
145
+ /** True while a save request is in flight. */
146
+ export let saveSaving = false;
147
+
148
+ /** Optional content below the shared toolbar, before property fields. */
149
+ export let headerContent = undefined;
150
+ export let headerClass = '';
151
+ export let dragHandleClass = 'text-grey2';
152
+ export let title = 'Edit Properties';
153
+ /** Hide the default missing-schema message for header-only editors. */
154
+ export let showSchemaError = true;
155
+ /** Optional layout wrapper moved by this editor's built-in drag and position API. */
156
+ export let positionTarget = null;
157
+
138
158
  // Initial values from saved composition (merged over schema defaults)
139
159
  export let initialDesignValues: Record<string, any> = {};
140
160
  export let initialContentValues: Record<string, any> = {};
@@ -144,8 +164,11 @@
144
164
  */
145
165
  const CONTENT_PADDING = 16; // Outer padding for both tabs
146
166
  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
167
+ const DRAG_BAR_HEIGHT = 28; // Drag handle / Save row (absolute, overlays the header top)
168
+ const TABS_VERTICAL_PADDING = 14; // Breathing room above and below the tab list
169
+ const TABS_LIST_HEIGHT = 56; // Tabs.List height
170
+ // Drag bar band + gap + tabs + gap + 1px separator
171
+ const HEADER_HEIGHT = DRAG_BAR_HEIGHT + TABS_VERTICAL_PADDING * 2 + TABS_LIST_HEIGHT + 1;
149
172
  const EDITOR_STROKE_COLOR = '#e5e7eb'; // Color for editor stroke
150
173
  const EDITOR_STROKE_WIDTH = '1px'; // Thickness of editor stroke
151
174
 
@@ -928,8 +951,11 @@
928
951
  */
929
952
  onDestroy(() => {
930
953
  // Clean up drag listeners
931
- document.removeEventListener('mousemove', handleDragMove);
932
- document.removeEventListener('mouseup', handleDragEnd);
954
+ if (browser) {
955
+ document.removeEventListener('mousemove', handleDragMove);
956
+ document.removeEventListener('mouseup', handleDragEnd);
957
+ if (dragState.active) document.body.style.cursor = '';
958
+ }
933
959
 
934
960
  // Clean up scroll listeners
935
961
  if (designScrollViewport) {
@@ -1475,6 +1501,35 @@
1475
1501
  }
1476
1502
  }
1477
1503
 
1504
+ /** Preserve the schema factory's props while narrowing each concrete render branch. */
1505
+ function describeInputController(component: ReturnType<typeof getInputControllerComponent> | null, props: ReturnType<typeof renderInputController>['props']): ControllerDescriptor | null {
1506
+ switch (component) {
1507
+ case TextInputController: return { kind: 'textinput', props };
1508
+ case TextAreaPreviewInputController: return { kind: 'textarea', props };
1509
+ case NumberInputController: return { kind: 'number', props };
1510
+ case SliderInputController: return { kind: 'slider', props };
1511
+ case SliderRangeInputController: return { kind: 'sliderrange', props };
1512
+ case DropdownInputController: return { kind: 'dropdown', props };
1513
+ case DatePickerInputController: return { kind: 'datepicker', props };
1514
+ case DateFieldInputController: return { kind: 'datefield', props };
1515
+ case RangeCalendarInputController: return { kind: 'rangecalendar', props };
1516
+ case SwitchInputController: return { kind: 'switch', props };
1517
+ case ColorPickerInputController: return { kind: 'colorpicker', props };
1518
+ case CheckboxInputController: return { kind: 'checkbox', props };
1519
+ case RadioGroupInputController: return { kind: 'radiogroup', props };
1520
+ case FontInputController: return { kind: 'font', props };
1521
+ case PositionInputController: return { kind: 'position', props };
1522
+ case BrowserInputController: return { kind: 'browser', props };
1523
+ case FixedPositionInputController: return { kind: 'fixedposition', props };
1524
+ case VectorInputController: return { kind: 'vector', props };
1525
+ case AngleInputController: return { kind: 'angle', props };
1526
+ case TimeInputController: return { kind: 'time', props };
1527
+ case RoundedCornersInputController: return { kind: 'roundedcorners', props };
1528
+ case TagsInputController: return { kind: 'tags', props };
1529
+ default: return null;
1530
+ }
1531
+ }
1532
+
1478
1533
  // Create a store for managing open dropdowns (only one should be open at a time)
1479
1534
  const openDropdowns = writable<Set<string>>(new Set());
1480
1535
 
@@ -1599,6 +1654,8 @@
1599
1654
  }
1600
1655
 
1601
1656
  interface ContentItem {
1657
+ // Saved content may carry schema-defined values alongside its metadata.
1658
+ [propertyName: string]: unknown;
1602
1659
  id: string;
1603
1660
  properties: Array<Property>;
1604
1661
  orderNumber?: number;
@@ -2252,28 +2309,43 @@
2252
2309
  let dragState = { active: false, startX: 0, startY: 0, initialLeft: 0, initialTop: 0 };
2253
2310
 
2254
2311
  function handleDragStart(e: MouseEvent) {
2255
- if (!enableDrag || !rootElement) return;
2312
+ const target = positionTarget || rootElement;
2313
+ if (!enableDrag || !target || e.button !== 0) return;
2256
2314
  e.preventDefault();
2257
-
2315
+ e.stopPropagation();
2316
+
2317
+ // Wrapped editors keep the measured size, including the parent's height clamp.
2318
+ // Resolve percentage/centered placement before leaving flow, never parse it as pixels.
2319
+ if (positionTarget) {
2320
+ const rectangle = target.getBoundingClientRect();
2321
+ const parent = target.offsetParent;
2322
+ const parentRectangle = parent?.getBoundingClientRect() || { left: 0, top: 0 };
2323
+ const left = rectangle.left - parentRectangle.left - (parent?.clientLeft || 0) + (parent?.scrollLeft || 0);
2324
+ const top = rectangle.top - parentRectangle.top - (parent?.clientTop || 0) + (parent?.scrollTop || 0);
2325
+ Object.assign(target.style, {
2326
+ position: 'absolute', margin: '0', transform: 'none',
2327
+ boxSizing: 'border-box', width: `${rectangle.width}px`, height: `${rectangle.height}px`,
2328
+ left: `${left}px`, top: `${top}px`
2329
+ });
2330
+ }
2258
2331
  dragState = {
2259
2332
  active: true,
2260
2333
  startX: e.clientX,
2261
2334
  startY: e.clientY,
2262
- initialLeft: parseFloat(rootElement.style.left) || 0,
2263
- initialTop: parseFloat(rootElement.style.top) || 0
2335
+ initialLeft: parseFloat(target.style.left) || 0,
2336
+ initialTop: parseFloat(target.style.top) || 0
2264
2337
  };
2265
- rootElement.style.zIndex = '50';
2338
+ target.style.zIndex = '50';
2266
2339
  document.addEventListener('mousemove', handleDragMove);
2267
2340
  document.addEventListener('mouseup', handleDragEnd);
2268
2341
  document.body.style.cursor = 'move';
2269
2342
  }
2270
2343
 
2271
2344
  function handleDragMove(e: MouseEvent) {
2272
- if (!dragState.active || !rootElement) return;
2345
+ if (!dragState.active) return;
2273
2346
  const left = dragState.initialLeft + (e.clientX - dragState.startX);
2274
2347
  const top = dragState.initialTop + (e.clientY - dragState.startY);
2275
- rootElement.style.left = `${left}px`;
2276
- rootElement.style.top = `${top}px`;
2348
+ setPosition(left, top);
2277
2349
  dispatch('move', { left, top });
2278
2350
  }
2279
2351
 
@@ -2283,10 +2355,7 @@
2283
2355
  document.removeEventListener('mousemove', handleDragMove);
2284
2356
  document.removeEventListener('mouseup', handleDragEnd);
2285
2357
  document.body.style.cursor = '';
2286
- dispatch('moveEnd', {
2287
- left: parseFloat(rootElement.style.left) || 0,
2288
- top: parseFloat(rootElement.style.top) || 0
2289
- });
2358
+ dispatch('moveEnd', getPosition());
2290
2359
  }
2291
2360
 
2292
2361
  /** Public: update a design state value externally (used by EmbedEditor for dimension sync) */
@@ -2296,17 +2365,19 @@
2296
2365
 
2297
2366
  /** Public: set absolute position programmatically (used by parent for layout) */
2298
2367
  export function setPosition(left: number, top: number) {
2299
- if (!rootElement) return;
2300
- rootElement.style.left = `${left}px`;
2301
- rootElement.style.top = `${top}px`;
2368
+ const target = positionTarget || rootElement;
2369
+ if (!target) return;
2370
+ target.style.left = `${left}px`;
2371
+ target.style.top = `${top}px`;
2302
2372
  }
2303
2373
 
2304
2374
  /** Public: get current position */
2305
2375
  export function getPosition() {
2306
- if (!rootElement) return { left: 0, top: 0 };
2376
+ const target = positionTarget || rootElement;
2377
+ if (!target) return { left: 0, top: 0 };
2307
2378
  return {
2308
- left: parseFloat(rootElement.style.left) || 0,
2309
- top: parseFloat(rootElement.style.top) || 0
2379
+ left: parseFloat(target.style.left) || 0,
2380
+ top: parseFloat(target.style.top) || 0
2310
2381
  };
2311
2382
  }
2312
2383
 
@@ -2353,47 +2424,64 @@
2353
2424
  onValueChange={handleTabChange}
2354
2425
  >
2355
2426
 
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)}
2427
+ <!-- A single toolbar owns drag and Save for all editor variants. -->
2428
+ {#if showDragHandle && (designSchema || contentSchema || headerContent)}
2364
2429
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2365
- <div class="absolute top-0 left-0 w-full flex justify-center z-[999]">
2430
+ <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
2431
  <!-- svelte-ignore a11y_no_static_element_interactions -->
2367
2432
  <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"
2433
+ 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
2434
  onmousedown={handleDragStart}
2370
2435
  >
2371
- <DragHandle class="w-[66px] h-[24px] text-grey2 hover:text-grey3 transition-colors" />
2436
+ <DragHandle class="w-16.5 h-6 {dragHandleClass} hover:text-grey3 transition-colors" />
2372
2437
  </div>
2438
+ {#if showSaveButton}
2439
+ {#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}
2440
+ <button
2441
+ type="button"
2442
+ 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
2443
+ {saveSaving
2444
+ ? 'text-[hsl(var(--deep-forest-green))] bg-[hsl(var(--light-green))]'
2445
+ : saveDirty
2446
+ ? 'text-white bg-[hsl(var(--deep-forest-green))] hover:bg-[hsl(var(--dark))] hover:text-white'
2447
+ : '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'}"
2448
+ disabled={saveSaving}
2449
+ onclick={(e) => {
2450
+ e.stopPropagation();
2451
+ if (!saveSaving && saveDirty) dispatch('save');
2452
+ }}
2453
+ onmousedown={(e) => e.stopPropagation()}
2454
+ >
2455
+ <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">
2456
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
2457
+ <polyline points="17 21 17 13 7 13 7 21" />
2458
+ <polyline points="7 3 7 8 15 8" />
2459
+ </svg>
2460
+ <span class="props-editor-save-label">{saveSaving ? 'Saving' : 'Save'}</span>
2461
+ </button>
2462
+ {/if}
2373
2463
  </div>
2374
2464
  {/if}
2375
2465
 
2466
+ {#if headerContent}
2467
+ <div class="props-editor-header-content shrink-0 {headerClass}">{@render headerContent()}</div>
2468
+ {/if}
2469
+
2470
+ {#if !designSchema && !contentSchema}
2471
+ {#if showSchemaError}
2472
+ <div class="props-editor-schema-error flex flex-col items-center justify-center h-full">
2473
+ <div class="props-editor-error-title text-error text-lg font-medium">No schema configuration available</div>
2474
+ <div class="props-editor-error-code text-grey5 subtext mt-1">Error code: CONFIG_NOT_FOUND</div>
2475
+ </div>
2476
+ {/if}
2477
+ {:else}
2376
2478
  <!-- Tabs header with buttons and separator - only show if both schemas exist -->
2377
2479
  {#if designSchema && contentSchema}
2378
2480
  <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"
2481
+ 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
2482
  >
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
2483
  <Tabs.List
2396
- class="tabs-list w-full h-[56px] rounded-large bg-grey2 p-1"
2484
+ class="tabs-list w-full h-14 rounded-large bg-grey2 p-1"
2397
2485
  >
2398
2486
  <Tabs.Trigger
2399
2487
  value="design"
@@ -2421,7 +2509,7 @@
2421
2509
  {/if}
2422
2510
 
2423
2511
  <!-- Design Tab Content -->
2424
- <div class="tab-design-container flex-grow overflow-y-auto scroll-area widgetic-colors hidden"
2512
+ <div class="tab-design-container grow scroll-area widgetic-colors hidden {headerContent ? 'min-h-0 flex-1 overflow-hidden' : 'overflow-y-auto'}"
2425
2513
  class:hidden={currentTab !== 'design'}
2426
2514
  style="max-height: {!designSchema || !contentSchema ? '100%' : `calc(100% - ${HEADER_HEIGHT}px)`}"
2427
2515
  data-scrollable={$designScrollState.shouldShowScroll}
@@ -2430,13 +2518,13 @@
2430
2518
  class="h-full"
2431
2519
  type="hover"
2432
2520
  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"
2521
+ 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
2522
  >
2435
2523
  <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}>
2524
+ <div class="px-6 {!designSchema || !contentSchema ? 'pt-7 pb-6' : 'pt-4 pb-6'}" bind:this={designContentWrapper}>
2437
2525
  <Tabs.Content
2438
2526
  value="design"
2439
- class="design-tab-content !mt-0 tabs-content-container w-full outline-none {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-left' : ''}"
2527
+ class="design-tab-content mt-0! tabs-content-container w-full outline-none {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-left' : ''}"
2440
2528
  data-tab="design"
2441
2529
  style="animation-duration: 300ms; transform: none;"
2442
2530
  >
@@ -2495,12 +2583,13 @@
2495
2583
  </div>
2496
2584
  {/if}
2497
2585
 
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}
2586
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0 ml-auto'}" style={isFontIc ? '' : 'width: 185px;'}>
2587
+ <InputControllerRenderer
2588
+ controller={describeInputController(InputComponent, {
2589
+ ...props,
2590
+ value: reactiveValue !== undefined ? reactiveValue : props.value,
2591
+ shouldFocus: activeControllerId === props.id
2592
+ })}
2504
2593
  />
2505
2594
  </div>
2506
2595
  </div>
@@ -2533,7 +2622,7 @@
2533
2622
 
2534
2623
  <!-- Content Tab Content -->
2535
2624
  {#if contentSchema}
2536
- <div class="tab-content-container flex-grow overflow-y-auto scroll-area widgetic-colors hidden"
2625
+ <div class="tab-content-container grow overflow-y-auto scroll-area widgetic-colors hidden"
2537
2626
  class:hidden={currentTab !== 'content'}
2538
2627
  style="max-height: {!contentSchema || !designSchema ? '100%' : `calc(100% - ${HEADER_HEIGHT}px)`}"
2539
2628
  data-scrollable={$contentScrollState.shouldShowScroll}
@@ -2542,13 +2631,13 @@
2542
2631
  class="h-full"
2543
2632
  type="hover"
2544
2633
  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"
2634
+ 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
2635
  >
2547
2636
  <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}>
2637
+ <div class="px-6 {!designSchema || !contentSchema ? 'pt-7 pb-6' : 'pt-4 pb-6'}" bind:this={contentWrapper}>
2549
2638
  <Tabs.Content
2550
2639
  value="content"
2551
- class="content-tab-content !mt-0 tabs-content-container w-full {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-right' : ''}"
2640
+ class="content-tab-content mt-0! tabs-content-container w-full {isFirstLoad ? 'initial-load' : !isInitialTabLoad ? 'animate-slide-right' : ''}"
2552
2641
  data-tab="content"
2553
2642
  style="transform: none; position: relative; z-index: 1;"
2554
2643
  >
@@ -2561,7 +2650,7 @@
2561
2650
  onOpenChange={() => isGenericContentOpen = !isGenericContentOpen}
2562
2651
  >
2563
2652
  <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;">
2653
+ <div class="flex items-center h-13" style="padding-left: {CONTENT_INNER_PADDING}px; padding-right: {CONTENT_INNER_PADDING}px;">
2565
2654
  <h3 class="body-medium text-grey7 flex-1 content-title">{contentSchema?.genericContent?.title || 'Content'}</h3>
2566
2655
  <ChevronDown class="h-6 w-6 text-grey5 transition-transform chevron" />
2567
2656
  </div>
@@ -2574,7 +2663,7 @@
2574
2663
  <Collapsible.Content>
2575
2664
  {#if isGenericContentOpen}
2576
2665
  <Card.Content class="p-0 px-4">
2577
- <div class="pt-[14px] pb-[16px]">
2666
+ <div class="pt-3.5 pb-4">
2578
2667
  <div class="properties-list flex flex-col" style="gap: 8px;">
2579
2668
  {#each contentSchema?.genericContent?.properties || [] as property, propertyIndex}
2580
2669
  {@const result = renderInputController(
@@ -2607,12 +2696,9 @@
2607
2696
  </div>
2608
2697
  {/if}
2609
2698
 
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}
2699
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2700
+ <InputControllerRenderer
2701
+ controller={describeInputController(InputComponent, { ...props, header, shouldFocus: activeControllerId === props.id })}
2616
2702
  />
2617
2703
  </div>
2618
2704
  </div>
@@ -2650,8 +2736,8 @@
2650
2736
  onOpenChange={() => toggleItem(item.id)}
2651
2737
  >
2652
2738
  <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">
2739
+ <div class="flex items-center h-13 cursor-pointer widgetic-colors">
2740
+ <div class="drag-handle flex items-center justify-center w-14 cursor-move hover:text-grey5">
2655
2741
  <IconGripVertical class="w-6 h-6 text-grey2 transition-colors duration-200" />
2656
2742
  </div>
2657
2743
 
@@ -2674,7 +2760,7 @@
2674
2760
  <Collapsible.Content>
2675
2761
  {#if isItemOpen(item.id)}
2676
2762
  <Card.Content class="p-0 px-4">
2677
- <div class="pt-[14px] pb-[16px]">
2763
+ <div class="pt-3.5 pb-4">
2678
2764
  <div class="properties-list flex flex-col" style="gap: 8px;">
2679
2765
  {#each item.properties as property, propertyIndex}
2680
2766
  {@const result = renderInputController(
@@ -2708,12 +2794,9 @@
2708
2794
  </div>
2709
2795
  {/if}
2710
2796
 
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}
2797
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2798
+ <InputControllerRenderer
2799
+ controller={describeInputController(InputComponent, { ...props, header, shouldFocus: activeControllerId === props.id })}
2717
2800
  />
2718
2801
  </div>
2719
2802
  </div>
@@ -2722,11 +2805,11 @@
2722
2805
  </div>
2723
2806
 
2724
2807
  <!-- Delete Item Button -->
2725
- <div class="mt-[16px] flex flex-col gap-2 relative z-10">
2808
+ <div class="mt-4 flex flex-col gap-2 relative z-10">
2726
2809
  <!-- svelte-ignore a11y_mouse_events_have_key_events -->
2727
2810
  <button
2728
2811
  type="button"
2729
- class="widgetic-colors h-[34px] w-full flex items-center justify-center relative z-10 group"
2812
+ class="widgetic-colors h-8.5 w-full flex items-center justify-center relative z-10 group"
2730
2813
  style="
2731
2814
  background-color: hsl(var(--error)) !important;
2732
2815
  border-radius: var(--radius-small) !important;
@@ -2764,7 +2847,7 @@
2764
2847
  <div class="content-add-item-ct mt-4">
2765
2848
  <Button
2766
2849
  variant="ghost"
2767
- class="group w-full h-[52px] bg-grey1 hover:bg-grey2 active:scale-[0.98] border-2 border-grey2 rounded-medium"
2850
+ class="group w-full h-13 bg-grey1 hover:bg-grey2 active:scale-[0.98] border-2 border-grey2 rounded-medium"
2768
2851
  style="transition: background-color 0.2s ease, transform 0.15s ease !important;"
2769
2852
  onclick={handleAddNewItem}
2770
2853
  >
@@ -2788,7 +2871,7 @@
2788
2871
  <div
2789
2872
  bind:this={fileBrowserPortalHost}
2790
2873
  id="props-editor-file-browser-portal"
2791
- class="props-editor-file-browser-portal pointer-events-none absolute inset-0 z-[999998]"
2874
+ class="props-editor-file-browser-portal pointer-events-none absolute inset-0 z-999998"
2792
2875
  ></div>
2793
2876
  </Tabs.Root>
2794
2877
  </div>
@@ -2806,7 +2889,9 @@
2806
2889
  .props-editor-container:not(:has(.header-hover-area)) {
2807
2890
  position: relative;
2808
2891
  }
2809
- .props-editor-container:not(:has(.header-hover-area)):hover :global(.drag-handle-container) {
2892
+ .props-editor-container:not(:has(.header-hover-area)):hover :global(.drag-handle-container),
2893
+ .props-editor-container:has(.header-hover-area:hover) :global(.props-editor-drag-bar .drag-handle-container),
2894
+ .props-editor-container :global(.props-editor-drag-bar:hover .drag-handle-container) {
2810
2895
  opacity: 1 !important;
2811
2896
  }
2812
2897
  :global(.drag-handle-container) {
@@ -2844,6 +2929,23 @@
2844
2929
  .props-editor-container:has(.tabs-header) {
2845
2930
  --has-both-schemas: 1rem;
2846
2931
  }
2932
+
2933
+ /* ── Save button (composition drag bar) ── */
2934
+ /* Icon stays put next to the text; label is centered in the remaining space
2935
+ with a small left bias so "Save" and "Saving" never shift the icon. */
2936
+ .props-editor-save-label {
2937
+ padding-left: 14px;
2938
+ line-height: 1;
2939
+ }
2940
+ .props-editor-save-btn .props-editor-save-icon {
2941
+ color: currentColor !important;
2942
+ stroke: currentColor !important;
2943
+ }
2944
+ .props-editor-save-label,
2945
+ :global(.props-editor-save-label) {
2946
+ color: currentColor !important;
2947
+ line-height: 1;
2948
+ }
2847
2949
  :global(.content-items-wrapper) {
2848
2950
  padding-top: 1rem;
2849
2951
  }
@@ -33,6 +33,15 @@ declare const PropsEditor: $$__sveltets_2_IsomorphicComponent<{
33
33
  defaultOpen?: boolean;
34
34
  showDragHandle?: boolean;
35
35
  enableDrag?: boolean;
36
+ /** Show Save status control in the top drag/header bar (composition page). */ showSaveButton?: boolean;
37
+ /** True when local edits are not yet persisted. */ saveDirty?: boolean;
38
+ /** True while a save request is in flight. */ saveSaving?: boolean;
39
+ /** Optional content below the shared toolbar, before property fields. */ headerContent?: any;
40
+ headerClass?: string;
41
+ dragHandleClass?: string;
42
+ title?: string;
43
+ /** Hide the default missing-schema message for header-only editors. */ showSchemaError?: boolean;
44
+ /** Optional layout wrapper moved by this editor's built-in drag and position API. */ positionTarget?: any;
36
45
  initialDesignValues?: Record<string, any>;
37
46
  initialContentValues?: Record<string, any>;
38
47
  updateDesignValue?: (propertyId: string, value: any) => void;
@@ -42,6 +51,7 @@ declare const PropsEditor: $$__sveltets_2_IsomorphicComponent<{
42
51
  top: number;
43
52
  };
44
53
  }, {
54
+ save: CustomEvent<any>;
45
55
  updated: CustomEvent<any>;
46
56
  propertyChange: CustomEvent<any>;
47
57
  goToItem: CustomEvent<any>;
@@ -67,8 +67,6 @@
67
67
  export let onChange: (newValue: string) => void; // Value change callback
68
68
  export let controllerId: string = '';
69
69
  export let id: string = '';
70
- export let openDropdowns: any;
71
- export let toggleDropdown: (id: string) => void;
72
70
  export let config: InputControllerConfig = {}; // Additional configuration
73
71
  export let helpText: string | undefined = undefined; // Help text for the input
74
72
 
@@ -21,8 +21,6 @@ declare const ColorPickerInputController: $$__sveltets_2_IsomorphicComponent<{
21
21
  onChange: (newValue: string) => void;
22
22
  controllerId?: string;
23
23
  id?: string;
24
- openDropdowns: any;
25
- toggleDropdown: (id: string) => void;
26
24
  config?: InputControllerConfig;
27
25
  helpText?: string | undefined;
28
26
  saveRecentColors?: boolean;