@widgetic/editor 4.0.1 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -769,7 +769,7 @@
769
769
  {/if}
770
770
 
771
771
  {#if mode === 'controlled' && isGeneratingCode}
772
- <GenerateLoader overlay title="Generating code…" size="lg" />
772
+ <GenerateLoader overlay title="Generating code…" subtitle="" size="lg" />
773
773
  {/if}
774
774
  </div>
775
775
  </div>
@@ -783,7 +783,7 @@
783
783
  {:else}
784
784
  <div class="flex h-full flex-col items-center justify-center p-6 text-center text-sm text-gray-600">
785
785
  {#if isGeneratingCode}
786
- <GenerateLoader title="Generating code…" size="lg" />
786
+ <GenerateLoader title="Generating code…" subtitle="" size="lg" />
787
787
  {:else if previewError === 'NO_CODE_YET'}
788
788
  <div class="font-semibold text-lg text-gray-700 mb-2">No Code Generated Yet</div>
789
789
  {:else if previewError}
@@ -855,9 +855,11 @@
855
855
  };
856
856
 
857
857
  // Add this near other reactive declarations at the top level
858
- $: if (Array.isArray(contentSchema?.contentItems) && contentSchema.contentItems.length > 0 && !itemTemplate) {
859
- itemTemplate = JSON.parse(JSON.stringify(contentSchema.contentItems[0]));
860
- // console.log('Template initialized:', itemTemplate);
858
+ $: if (Array.isArray(contentSchema?.contentItems)) {
859
+ const templateSource = contentSchema.contentItems.find((item: ContentItem) => Array.isArray(item.properties) && item.properties.length > 0);
860
+ if (templateSource) {
861
+ itemTemplate = JSON.parse(JSON.stringify(templateSource));
862
+ }
861
863
  }
862
864
 
863
865
  // Populate contentState with per-item values keyed as "itemId__propName".
@@ -872,10 +874,12 @@
872
874
  if (contentState[key] === undefined) {
873
875
  // Prefer the parent's canonical saved-value map over the
874
876
  // schema mirror, which older compositions may not fill.
875
- const parentValue = parentItemValues?.[item.id]?.[prop.id || prop.name];
876
- const seed = parentValue !== undefined ? parentValue : prop.value;
877
- if (seed !== undefined) {
878
- patch[key] = seed;
877
+ const propKey = prop.id || prop.name;
878
+ const parentValue = parentItemValues?.[item.id]?.[propKey];
879
+ const liveOnItem = propKey != null ? (item as Record<string, unknown>)[propKey] : undefined;
880
+ const seed = pickLiveContentValue(parentValue, liveOnItem, prop.value);
881
+ if (seed !== undefined && seed !== '') {
882
+ patch[key] = seed as InputValue;
879
883
  needsUpdate = true;
880
884
  }
881
885
  }
@@ -1328,9 +1332,17 @@
1328
1332
  function renderInputController(schema: string, property: any, propertyIndex: number, isFirstGroup: boolean, contentItemId?: string) {
1329
1333
  const basePropertyId = property.id || property.name;
1330
1334
  const propertyId = contentItemId ? `${contentItemId}__${basePropertyId}` : basePropertyId;
1335
+ const controllerId = `${schema}-${propertyId}`;
1331
1336
  const { type, inputController } = property;
1337
+ if (!inputController?.type) {
1338
+ console.warn('[PropsEditor] Skipping property without inputController.type:', propertyId, property);
1339
+ return {
1340
+ component: null,
1341
+ props: { id: controllerId },
1342
+ header: { label: basePropertyId, helpText: '' }
1343
+ };
1344
+ }
1332
1345
  const InputControllerComponent = getInputControllerComponent(inputController.type.toLowerCase());
1333
- const controllerId = `${schema}-${propertyId}`;
1334
1346
 
1335
1347
  const stateValue = schema === 'design' ? designState[propertyId] : contentState[propertyId];
1336
1348
  let initialValue = stateValue !== undefined ? stateValue : (property.value !== undefined ? property.value : property.defaultValue);
@@ -1373,6 +1385,9 @@
1373
1385
  onOpacityChange: (opacity: number) => updateState(schema, propertyId, initialValue, opacity),
1374
1386
  portalTarget,
1375
1387
  ...restInputController,
1388
+ ...(String(inputController.type || '').toLowerCase() === 'font'
1389
+ ? { label: description || propertyId || '' }
1390
+ : {}),
1376
1391
  ...property.constraints,
1377
1392
  ...additionalProps,
1378
1393
  config: inputController.config,
@@ -1712,74 +1727,75 @@
1712
1727
  * and appends it to the content items list
1713
1728
  */
1714
1729
  function formatDefaultItemTitle(itemNumber: number): string {
1715
- if (itemNumber < 10) return `Item 0${itemNumber}`;
1716
- return `Item ${itemNumber}`;
1730
+ return formatNumberedTitle('Item', itemNumber);
1717
1731
  }
1718
1732
 
1719
- function formatSlideTitle(slideNumber: number): string {
1720
- return `Slide ${slideNumber}`;
1733
+ /** "Tile 1", "Photo 12", "Item 03" → prefix used for the next auto title. */
1734
+ function getNumberedTitlePrefix(title: string | undefined): string | null {
1735
+ if (!title?.trim()) return null;
1736
+ const match = title.trim().match(/^(.+?)\s+(\d+)$/);
1737
+ return match?.[1] ?? null;
1721
1738
  }
1722
1739
 
1723
- function formatPhotoTitle(photoNumber: number): string {
1724
- return `Photo ${photoNumber}`;
1725
- }
1726
-
1727
- function isAutoGeneratedItemTitle(title: string | undefined): boolean {
1728
- if (!title?.trim()) return true;
1729
- const trimmed = title.trim();
1740
+ function isKnownGenericAutoTitle(title: string): boolean {
1730
1741
  return (
1731
- /^Item 0?\d+$/i.test(trimmed) ||
1732
- /^Slide \d+$/i.test(trimmed) ||
1733
- /^Photo \d+$/i.test(trimmed)
1742
+ /^Item 0?\d+$/i.test(title) ||
1743
+ /^Slide \d+$/i.test(title) ||
1744
+ /^Photo \d+$/i.test(title) ||
1745
+ /^Image \d+$/i.test(title)
1734
1746
  );
1735
1747
  }
1736
1748
 
1737
- function isSlideAutoTitle(title: string | undefined): boolean {
1738
- return !!title?.trim() && /^Slide \d+$/i.test(title.trim());
1739
- }
1740
-
1741
- function isPhotoAutoTitle(title: string | undefined): boolean {
1742
- return !!title?.trim() && /^Photo \d+$/i.test(title.trim());
1749
+ /** Most common "Name N" prefix in the list (Tile / Photo / Slide / Item / …). */
1750
+ function detectDominantTitlePrefix(items: ContentItem[]): string {
1751
+ const counts = new Map<string, number>();
1752
+ for (const item of items) {
1753
+ const prefix = getNumberedTitlePrefix(item.title);
1754
+ if (!prefix) continue;
1755
+ counts.set(prefix, (counts.get(prefix) || 0) + 1);
1756
+ }
1757
+ let bestPrefix = 'Item';
1758
+ let bestCount = 0;
1759
+ for (const [prefix, count] of counts) {
1760
+ if (count > bestCount) {
1761
+ bestPrefix = prefix;
1762
+ bestCount = count;
1763
+ }
1764
+ }
1765
+ return bestPrefix;
1743
1766
  }
1744
1767
 
1745
- function formatAutoItemTitle(title: string | undefined, position: number): string {
1746
- if (isSlideAutoTitle(title)) return formatSlideTitle(position);
1747
- if (isPhotoAutoTitle(title)) return formatPhotoTitle(position);
1748
- return formatDefaultItemTitle(position);
1768
+ function formatNumberedTitle(prefix: string, itemNumber: number): string {
1769
+ if (/^item$/i.test(prefix) && itemNumber < 10) return `Item 0${itemNumber}`;
1770
+ return `${prefix} ${itemNumber}`;
1749
1771
  }
1750
1772
 
1751
- type AutoTitlePattern = 'photo' | 'slide' | 'item';
1752
-
1753
- /** Pick Photo / Slide / Item naming from the dominant pattern in existing items. */
1754
- function detectDominantAutoTitlePattern(items: ContentItem[]): AutoTitlePattern {
1755
- let photoCount = 0;
1756
- let slideCount = 0;
1757
- let itemCount = 0;
1758
-
1759
- for (const item of items) {
1760
- const title = item.title?.trim() || '';
1761
- if (/^Photo \d+$/i.test(title)) photoCount++;
1762
- else if (/^Slide \d+$/i.test(title)) slideCount++;
1763
- else if (/^Item 0?\d+$/i.test(title) || !title) itemCount++;
1773
+ function isAutoGeneratedItemTitle(title: string | undefined, items?: ContentItem[]): boolean {
1774
+ if (!title?.trim()) return true;
1775
+ const trimmed = title.trim();
1776
+ if (isKnownGenericAutoTitle(trimmed)) return true;
1777
+ if (items && items.length) {
1778
+ const prefix = getNumberedTitlePrefix(trimmed);
1779
+ const dominant = detectDominantTitlePrefix(items);
1780
+ return !!prefix && prefix.toLowerCase() === dominant.toLowerCase();
1764
1781
  }
1782
+ return false;
1783
+ }
1765
1784
 
1766
- if (photoCount > 0 && photoCount >= slideCount && photoCount >= itemCount) return 'photo';
1767
- if (slideCount > 0 && slideCount >= itemCount) return 'slide';
1768
- return 'item';
1785
+ function formatAutoItemTitle(title: string | undefined, position: number): string {
1786
+ const prefix = getNumberedTitlePrefix(title);
1787
+ if (prefix) return formatNumberedTitle(prefix, position);
1788
+ return formatDefaultItemTitle(position);
1769
1789
  }
1770
1790
 
1771
1791
  function formatNewItemTitle(items: ContentItem[]): string {
1772
- const nextNumber = items.length + 1;
1773
- const pattern = detectDominantAutoTitlePattern(items);
1774
- if (pattern === 'photo') return formatPhotoTitle(nextNumber);
1775
- if (pattern === 'slide') return formatSlideTitle(nextNumber);
1776
- return formatDefaultItemTitle(nextNumber);
1792
+ return formatNumberedTitle(detectDominantTitlePrefix(items), items.length + 1);
1777
1793
  }
1778
1794
 
1779
1795
  function renumberAutoItemTitles(items: ContentItem[]): ContentItem[] {
1780
1796
  return items.map((item, index) => {
1781
1797
  const position = index + 1;
1782
- if (!isAutoGeneratedItemTitle(item.title)) {
1798
+ if (!isAutoGeneratedItemTitle(item.title, items)) {
1783
1799
  return { ...item, orderNumber: position };
1784
1800
  }
1785
1801
  const nextTitle = formatAutoItemTitle(item.title, position);
@@ -1801,10 +1817,11 @@
1801
1817
  }
1802
1818
  }
1803
1819
  }
1804
- if (item.title && item.title.trim() && !isAutoGeneratedItemTitle(item.title)) {
1820
+ const itemList = Array.isArray(contentSchema?.contentItems) ? contentSchema.contentItems : [];
1821
+ if (item.title && item.title.trim() && !isAutoGeneratedItemTitle(item.title, itemList)) {
1805
1822
  return item.title;
1806
1823
  }
1807
- if (isAutoGeneratedItemTitle(item.title)) {
1824
+ if (isAutoGeneratedItemTitle(item.title, itemList)) {
1808
1825
  return formatAutoItemTitle(item.title, index + 1);
1809
1826
  }
1810
1827
  return formatDefaultItemTitle(index + 1);
@@ -1868,13 +1885,20 @@
1868
1885
  } while (contentSchema.contentItems.some((item: ContentItem) => item.id === uniqueId));
1869
1886
  const newTitle = formatNewItemTitle(contentSchema.contentItems);
1870
1887
  // Create the new item (copy template or last item structure)
1871
- const template = contentSchema.contentItems[0];
1888
+ const template =
1889
+ (Array.isArray(contentSchema.contentItems) &&
1890
+ contentSchema.contentItems.find((item: ContentItem) => Array.isArray(item.properties) && item.properties.length > 0)) ||
1891
+ itemTemplate;
1892
+ if (!template || !Array.isArray(template.properties) || template.properties.length === 0) {
1893
+ console.warn('[Add] Cannot add item: template has no properties');
1894
+ return;
1895
+ }
1872
1896
  const newItem: ContentItem = {
1873
1897
  ...template,
1874
1898
  id: uniqueId,
1875
1899
  title: newTitle,
1876
1900
  orderNumber: contentSchema.contentItems.length + 1,
1877
- properties: template.properties.map((p: any) => ({ ...p })),
1901
+ properties: cloneProperties(template.properties),
1878
1902
  };
1879
1903
  // Remove the new item if it exists elsewhere (shouldn't, but for safety)
1880
1904
  const nextItems = [...contentSchema.contentItems.filter((i: ContentItem) => i.id !== uniqueId), newItem];
@@ -1934,20 +1958,47 @@
1934
1958
  }
1935
1959
  }
1936
1960
 
1937
- // Helper function to clone properties
1961
+ function unwrapContentMediaValue(raw: unknown): unknown {
1962
+ if (raw && typeof raw === 'object' && 'url' in (raw as Record<string, unknown>)) {
1963
+ return (raw as { url?: string }).url ?? '';
1964
+ }
1965
+ return raw;
1966
+ }
1967
+
1968
+ function pickLiveContentValue(...candidates: unknown[]): unknown {
1969
+ for (const candidate of candidates) {
1970
+ const unwrapped = unwrapContentMediaValue(candidate);
1971
+ if (unwrapped === undefined || unwrapped === null || unwrapped === '') continue;
1972
+ return unwrapped;
1973
+ }
1974
+ return '';
1975
+ }
1976
+
1977
+ function emptyValueForNewItemProperty(prop: any): unknown {
1978
+ const icType = String(prop?.inputController?.type || '').toLowerCase();
1979
+ if (['browser', 'image', 'file', 'video', 'audio'].includes(icType)) {
1980
+ return '';
1981
+ }
1982
+ if (typeof prop?.defaultValue === 'boolean' || typeof prop?.defaultValue === 'number') {
1983
+ return prop.defaultValue;
1984
+ }
1985
+ return '';
1986
+ }
1987
+
1988
+ // Clone controller schema for a new item. Do not copy Unsplash/sample defaults
1989
+ // into value — those overwrite live gallery URLs in the preview overlay.
1938
1990
  function cloneProperties(properties: Array<any>): Property[] {
1939
1991
  if (!properties || !Array.isArray(properties)) {
1940
1992
  return [];
1941
1993
  }
1942
1994
 
1943
1995
  return properties.map(prop => {
1944
- // Create a deep clone of each property
1945
1996
  const clonedProp: Property = {
1946
1997
  id: prop.id || prop.name,
1947
1998
  name: prop.name || prop.id,
1948
1999
  type: prop.type || 'string', // Ensure type is set
1949
2000
  defaultValue: prop.defaultValue,
1950
- value: prop.defaultValue, // Reset to default value
2001
+ value: emptyValueForNewItemProperty(prop),
1951
2002
  inputController: {
1952
2003
  type: prop.inputController?.type || 'text', // Ensure inputController.type is set
1953
2004
  description: prop.inputController?.description,
@@ -1979,8 +2030,13 @@
1979
2030
  const key = p.id || p.name;
1980
2031
  // Canonical state first; schema mirror/defaultValue are legacy
1981
2032
  // seeds for keys the editor never touched.
2033
+ const liveOnItem = key != null ? (item as Record<string, unknown>)[key] : undefined;
1982
2034
  props[key] = normalizeContentPropertyValue(
1983
- contentState[`${item.id}__${key}`] ?? p.value ?? p.defaultValue
2035
+ pickLiveContentValue(
2036
+ contentState[`${item.id}__${key}`],
2037
+ p.value,
2038
+ liveOnItem
2039
+ )
1984
2040
  );
1985
2041
  });
1986
2042
  // Carry opacity state so parent saves don't lose per-property
@@ -2415,24 +2471,27 @@
2415
2471
  {@const reactiveValue = designState[stateKey]}
2416
2472
 
2417
2473
  {#if isVisible}
2474
+ {@const isFontIc = property.inputController.type.toLowerCase() === 'font'}
2418
2475
  <div
2419
- class="ictrl input-controller flex items-center justify-between w-full p-0"
2476
+ class="ictrl input-controller flex {isFontIc ? 'flex-col items-stretch' : 'items-center justify-between'} w-full p-0"
2420
2477
  data-state={$openDropdowns?.has(props.controllerId) ? 'open' : 'closed'}
2421
- class:double-height={property.inputController.type.toLowerCase() === 'font'}
2478
+ class:double-height={isFontIc}
2422
2479
  class:position-height={property.inputController.type.toLowerCase() === 'position'}
2423
2480
  class:fixed-position-height={property.inputController.type.toLowerCase() === 'fixedposition'}
2424
2481
  class:text-area-preview-height={property.inputController.type.toLowerCase() === 'textarea' || property.inputController.type.toLowerCase() === 'textArea'}
2425
2482
  class:position-controller={property.inputController.type.toLowerCase() === 'position'}
2426
2483
  class:position-dragging={property.inputController.type.toLowerCase() === 'position' && isDragging}
2427
2484
  >
2485
+ {#if !isFontIc}
2428
2486
  <div class="ic-header-ct flex-1 min-w-0">
2429
2487
  <InputControllerHeader
2430
2488
  label={header.label}
2431
2489
  helpText={header.helpText}
2432
2490
  />
2433
2491
  </div>
2492
+ {/if}
2434
2493
 
2435
- <div class="ic-input-ct flex-shrink-0 ml-auto" style="width: 185px;">
2494
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'flex-shrink-0 ml-auto'}" style={isFontIc ? '' : 'width: 185px;'}>
2436
2495
  <svelte:component
2437
2496
  this={InputComponent}
2438
2497
  {...props}
@@ -2525,17 +2584,16 @@
2525
2584
  {@const isVisible = shouldShowProperty(property, contentSchema?.genericContent?.properties || [], 'content')}
2526
2585
 
2527
2586
  {#if isVisible}
2528
- <div class="ictrl input-controller flex items-center justify-between w-full p-0 browser-input-wrapper"
2529
- class:double-height={property.inputController.type.toLowerCase() === 'font'}
2587
+ {@const isFontIc = property.inputController.type.toLowerCase() === 'font'}
2588
+ <div class="ictrl input-controller flex {isFontIc ? 'flex-col items-stretch' : 'items-center justify-between'} w-full p-0 browser-input-wrapper"
2589
+ class:double-height={isFontIc}
2530
2590
  >
2531
- <!-- Left side - Label -->
2532
- {#if InputComponent !== BrowserInputController}
2591
+ {#if !isFontIc && InputComponent !== BrowserInputController}
2533
2592
  <InputControllerHeader
2534
2593
  label={header.label}
2535
2594
  helpText={header.helpText}
2536
2595
  />
2537
- {:else}
2538
- <!-- Special case for BrowserInputController -->
2596
+ {:else if !isFontIc}
2539
2597
  <div class="flex-1 min-w-0 pr-4">
2540
2598
  <InputControllerHeader
2541
2599
  label={header.label}
@@ -2544,8 +2602,7 @@
2544
2602
  </div>
2545
2603
  {/if}
2546
2604
 
2547
- <!-- Right side - Input Component -->
2548
- <div class="ic-input-ct flex-shrink-0" style="width: 185px;">
2605
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'flex-shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2549
2606
  <svelte:component
2550
2607
  this={InputComponent}
2551
2608
  {...props}
@@ -2625,18 +2682,17 @@
2625
2682
  {@const header = result.header}
2626
2683
  {@const isVisible = shouldShowProperty(property, item.properties, 'content')}
2627
2684
 
2628
- {#if isVisible}
2629
- <div class="ictrl input-controller flex items-center justify-between w-full p-0 browser-input-wrapper"
2630
- class:double-height={property.inputController.type.toLowerCase() === 'font'}
2685
+ {#if isVisible && InputComponent}
2686
+ {@const isFontIc = (property.inputController?.type || '').toLowerCase() === 'font'}
2687
+ <div class="ictrl input-controller flex {isFontIc ? 'flex-col items-stretch' : 'items-center justify-between'} w-full p-0 browser-input-wrapper"
2688
+ class:double-height={isFontIc}
2631
2689
  >
2632
- <!-- Left side - Label -->
2633
- {#if InputComponent !== BrowserInputController}
2690
+ {#if !isFontIc && InputComponent !== BrowserInputController}
2634
2691
  <InputControllerHeader
2635
2692
  label={header.label}
2636
2693
  helpText={header.helpText}
2637
2694
  />
2638
- {:else}
2639
- <!-- Special case for BrowserInputController -->
2695
+ {:else if !isFontIc}
2640
2696
  <div class="flex-1 min-w-0 pr-4">
2641
2697
  <InputControllerHeader
2642
2698
  label={header.label}
@@ -2645,8 +2701,7 @@
2645
2701
  </div>
2646
2702
  {/if}
2647
2703
 
2648
- <!-- Right side - Input Component -->
2649
- <div class="ic-input-ct flex-shrink-0" style="width: 185px;">
2704
+ <div class="ic-input-ct {isFontIc ? 'w-full' : 'flex-shrink-0'}" style={isFontIc ? '' : 'width: 185px;'}>
2650
2705
  <svelte:component
2651
2706
  this={InputComponent}
2652
2707
  {...props}
@@ -3370,7 +3425,9 @@
3370
3425
 
3371
3426
  /* Specific style for font controllers */
3372
3427
  :global(.input-controller.double-height) {
3373
- height: 104px !important;
3428
+ height: auto !important;
3429
+ min-height: 104px;
3430
+ align-items: flex-start !important;
3374
3431
  }
3375
3432
 
3376
3433
  /* Ensure content containers maintain width */
@@ -650,8 +650,7 @@
650
650
  {#if isFileBrowserOpen}
651
651
  <Portal target={portalTarget}>
652
652
  <div
653
- class="browser-input-file-browser-overlay pointer-events-auto absolute inset-0 z-[999999] flex flex-col overflow-hidden bg-black/40"
654
- class:p-3={!fillEditorPanel}
653
+ class="browser-input-file-browser-overlay pointer-events-auto absolute inset-0 z-[999999] flex flex-col overflow-hidden bg-black/40 p-0"
655
654
  >
656
655
  <!-- svelte-ignore a11y_click_events_have_key_events -->
657
656
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -667,20 +666,22 @@
667
666
  class:shadow-none={fillEditorPanel}
668
667
  class:max-w-[520px]={!fillEditorPanel}
669
668
  class:flex-1={!fillEditorPanel}
669
+ class:min-h-[550px]={fillEditorPanel}
670
670
  class:self-center={!fillEditorPanel}
671
671
  class:mx-auto={!fillEditorPanel}
672
672
  class:rounded-large={!fillEditorPanel}
673
673
  class:shadow-2xl={!fillEditorPanel}
674
674
  >
675
- <div class="browser-input-file-browser-header flex h-[57px] shrink-0 items-center justify-between border-b border-grey2 px-4 py-3">
675
+ <div class="browser-input-file-browser-header relative flex h-[48px] shrink-0 items-center border-b border-grey2 pl-4 pr-8">
676
676
  <span class="body-medium text-grey7">Select file</span>
677
677
  <button
678
678
  type="button"
679
- class="browser-input-file-browser-close inline-flex h-8 w-8 items-center justify-center rounded-full hover:bg-grey2"
679
+ class="browser-input-file-browser-close inline-flex h-6 w-6 items-center justify-center p-0"
680
+ style="position:absolute;top:4px;right:4px;"
680
681
  on:click={closeFileBrowser}
681
682
  aria-label="Close file browser"
682
683
  >
683
- <X class="h-4! w-4!" />
684
+ <X class="h-6! w-6!" />
684
685
  </button>
685
686
  </div>
686
687
 
@@ -705,6 +706,12 @@
705
706
  {/if}
706
707
 
707
708
  <style>
709
+ .browser-input-file-browser-close {
710
+ position: absolute;
711
+ top: 4px;
712
+ right: 4px;
713
+ }
714
+
708
715
  .browser-input-file-browser-dialog-animate {
709
716
  transform-origin: center center;
710
717
  animation: browser-input-fb-scale-in 0.22s cubic-bezier(0.4, 0, 0.2, 1);