@edgedev/create-edge-app 1.2.34 → 1.2.35

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.
@@ -120,6 +120,11 @@ const BLOCK_CONTENT_SNIPPETS = [
120
120
  snippet: '{{{#text {"field": "fieldName", "value": "" }}}}',
121
121
  description: 'Simple text field placeholder',
122
122
  },
123
+ {
124
+ label: 'Text with Options',
125
+ snippet: '{{{#text {"field":"fieldName","title":"Field Label","option":{"field":"fieldName","options":[{"title":"Option 1","name":"option1"},{"title":"Option 2","name":"option2"}],"optionsKey":"title","optionsValue":"name"},"value":"option1"}}}}',
126
+ description: 'Text field with selectable options',
127
+ },
123
128
  {
124
129
  label: 'Text Area',
125
130
  snippet: '{{{#textarea {"field": "fieldName", "value": "" }}}}',
@@ -898,6 +903,7 @@ const exportCurrentBlock = () => {
898
903
  :theme="theme"
899
904
  :edit-mode="true"
900
905
  :contain-fixed="true"
906
+ :disable-interactive-preview-in-edit="false"
901
907
  :allow-delete="false"
902
908
  :viewport-mode="previewViewportMode"
903
909
  :block-id="state.previewBlock.id"
@@ -922,13 +928,16 @@ const exportCurrentBlock = () => {
922
928
  </SheetHeader>
923
929
  <div class="px-6 pb-6">
924
930
  <Tabs class="w-full" default-value="guide">
925
- <TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-4">
931
+ <TabsList class="w-full mt-3 bg-secondary rounded-sm grid grid-cols-5">
926
932
  <TabsTrigger value="guide" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
927
933
  Block Guide
928
934
  </TabsTrigger>
929
935
  <TabsTrigger value="carousel" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
930
936
  Carousel Usage
931
937
  </TabsTrigger>
938
+ <TabsTrigger value="form-helpers" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
939
+ Form Helpers
940
+ </TabsTrigger>
932
941
  <TabsTrigger value="nav-bar" class="w-full text-black data-[state=active]:bg-black data-[state=active]:text-white">
933
942
  Nav Bar
934
943
  </TabsTrigger>
@@ -959,6 +968,7 @@ const exportCurrentBlock = () => {
959
968
  <a href="#arrays-filters" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Filters</a>
960
969
  <a href="#conditionals" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Conditionals</a>
961
970
  <a href="#subarrays" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Subarrays</a>
971
+ <a href="#entries" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Entries</a>
962
972
  <a href="#rendering-rules" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Rendering</a>
963
973
  <a href="#loading-tokens" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Loading</a>
964
974
  <a href="#validation" class="px-2 py-1 rounded border border-border bg-background hover:bg-muted transition">Validation</a>
@@ -1243,6 +1253,29 @@ const exportCurrentBlock = () => {
1243
1253
  </p>
1244
1254
  </section>
1245
1255
 
1256
+ <section id="entries" class="space-y-3">
1257
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1258
+ Entries (Object Key/Value Loops)
1259
+ </h3>
1260
+ <pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code>{{{#entries:pair {"field":"settings","value":{"theme":"dark","ctaText":"Contact Us"}}}}}
1261
+ <div><strong>{{pair.key}}</strong>: {{pair.value}}</div>
1262
+ {{{/entries}}}
1263
+
1264
+ {{{#entries:group {"field":"groupedItems","value":{"featured":["One","Two"],"archive":["Three"]}}}}}
1265
+ <h4>{{group.key}}</h4>
1266
+ {{{#subarray:child {"field":"item.value","value":[]}}}}
1267
+ <div>{{child}}</div>
1268
+ {{{/subarray}}}
1269
+ {{{/entries}}}</code></pre>
1270
+ <div class="text-sm text-foreground space-y-1">
1271
+ <div><code>entries</code> loops object fields instead of arrays.</div>
1272
+ <div>Use it at the root or inside other loops; it does not need to be inside <code>subarray</code>.</div>
1273
+ <div>Each iteration exposes <code>item.key</code> and <code>item.value</code>, plus alias access like <code v-pre>{{pair.key}}</code>.</div>
1274
+ <div>If a value is an array, use nested <code>subarray</code> on <code>item.value</code>.</div>
1275
+ <div>If <code>field</code> is not an object, it renders nothing.</div>
1276
+ </div>
1277
+ </section>
1278
+
1246
1279
  <section id="rendering-rules" class="space-y-2">
1247
1280
  <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1248
1281
  Rendering Rules
@@ -1372,7 +1405,6 @@ const exportCurrentBlock = () => {
1372
1405
  </h3>
1373
1406
  <p class="text-sm text-foreground">
1374
1407
  Add <code>data-carousel</code> markup to any CMS block and the runtime auto-initializes Embla on the client.
1375
- This is initialized in <code>htmlContent.vue</code> and works inside raw block HTML.
1376
1408
  </p>
1377
1409
  </section>
1378
1410
 
@@ -1488,6 +1520,14 @@ const exportCurrentBlock = () => {
1488
1520
  <div><code>cms-nav-overlay</code>: backdrop click-to-close (optional but recommended).</div>
1489
1521
  <div><code>cms-nav-close</code>: explicit close button in panel (optional).</div>
1490
1522
  <div><code>cms-nav-link</code>: links that should close panel on click (optional).</div>
1523
+ <div><code>cms-nav-folder</code>: desktop folder wrapper for dropdown behavior (recommended).</div>
1524
+ <div><code>cms-nav-folder-toggle</code>: desktop folder trigger link/button (recommended).</div>
1525
+ <div><code>cms-nav-folder-menu</code>: desktop dropdown menu panel for folder items (recommended).</div>
1526
+ <div><code>cms-nav-main</code>: optional hook for scroll/sticky/hide classes (defaults to first <code>&lt;nav&gt;</code>).</div>
1527
+ <div><code>cms-nav-pos-right</code>, <code>cms-nav-pos-left</code>, <code>cms-nav-pos-center</code>: helper classes for menu position behavior.</div>
1528
+ <div><code>cms-nav-layout</code>, <code>cms-nav-logo</code>, <code>cms-nav-desktop</code>: optional structure hooks for precise layout mapping.</div>
1529
+ <div><code>cms-nav-sticky</code>: force sticky top behavior even if your nav did not include fixed classes.</div>
1530
+ <div><code>cms-nav-hide-on-down</code>: hide nav on scroll down, show on scroll up.</div>
1491
1531
  </div>
1492
1532
  </section>
1493
1533
 
@@ -1499,6 +1539,12 @@ const exportCurrentBlock = () => {
1499
1539
  <div><code>data-cms-nav-open="true"</code> to start open.</div>
1500
1540
  <div><code>data-cms-nav-open-class="your-class"</code> to change the root open class (default <code>is-open</code>).</div>
1501
1541
  <div><code>data-cms-nav-close-on-link="false"</code> to keep panel open after link clicks.</div>
1542
+ <div><code>data-cms-nav-position="right|left|center"</code> as an alternative to helper classes.</div>
1543
+ <div><code>data-cms-nav-scrolled-class</code> / <code>data-cms-nav-top-class</code>: classes toggled on nav main target.</div>
1544
+ <div><code>data-cms-nav-scrolled-row-class</code> / <code>data-cms-nav-top-row-class</code>: classes toggled on <code>cms-nav-layout</code> for shrink/expand.</div>
1545
+ <div><code>data-cms-nav-scroll-threshold</code>: px before “scrolled” classes apply (default 10).</div>
1546
+ <div><code>data-cms-nav-hide-on-down="true"</code>, <code>data-cms-nav-hide-threshold</code> (default 80), <code>data-cms-nav-hide-delta</code> (default 6).</div>
1547
+ <div><code>data-cms-nav-hidden-class</code> / <code>data-cms-nav-visible-class</code> / <code>data-cms-nav-transition-class</code> for hide/show animation control.</div>
1502
1548
  </div>
1503
1549
  </section>
1504
1550
 
@@ -1506,26 +1552,65 @@ const exportCurrentBlock = () => {
1506
1552
  <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1507
1553
  Nav Block Template (Copy / Paste)
1508
1554
  </h3>
1509
- <pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code>&lt;div class="cms-nav-root" data-cms-nav-root data-cms-nav-close-on-link="true"&gt;
1510
- &lt;nav class="fixed inset-x-0 top-0 z-30 w-full bg-transparent text-navText"&gt;
1511
- {{{#array {"field":"siteDoc","as":"site","collection":{"path":"sites","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
1555
+ <pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code>&lt;div class="cms-nav-root cms-nav-sticky" data-cms-nav-root data-cms-nav-position="{{{#text {"field":"navPosition","title":"Menu Position","option":{"field":"navPosition","options":[{"title":"Right","name":"right"},{"title":"Left","name":"left"},{"title":"Center","name":"center"}],"optionsKey":"title","optionsValue":"name"},"value":"right"}}}}" data-cms-nav-close-on-link="true" data-cms-nav-top-class="bg-transparent border-transparent" data-cms-nav-scrolled-class="bg-navBg/80 backdrop-blur-lg shadow-lg" data-cms-nav-top-row-class="h-[64px] md:h-[88px] py-6 md:py-8" data-cms-nav-scrolled-row-class="h-[56px] md:h-[68px] py-5 md:py-4"&gt;
1556
+ {{{#array {"field":"siteDoc","collection":{"path":"sites","uniqueKey":"{orgId}","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
1557
+ &lt;nav class="cms-nav-main fixed inset-x-0 top-0 z-30 w-full bg-transparent text-navText"&gt;
1512
1558
  &lt;div class="relative w-full px-6 md:px-12"&gt;
1513
- &lt;div class="flex h-[64px] md:h-[88px] items-center justify-between gap-6 py-6 md:py-8"&gt;
1514
- &lt;a href="/" class="cursor-pointer text-xl text-navText"&gt;
1515
- &lt;img src="{{site.logo}}" class="h-[56px] md:h-[72px] py-3" /&gt;
1559
+ &lt;div class="cms-nav-layout flex h-[64px] md:h-[88px] items-center justify-between gap-6 py-6 md:py-8"&gt;
1560
+ &lt;a href="/" class="cms-nav-logo cursor-pointer text-xl text-navText"&gt;
1561
+ {{{#if {"cond":"item.logoLight"}}}}
1562
+ &lt;img src="{{item.logoLight}}" class="h-[56px] md:h-[72px] py-3" /&gt;
1563
+ {{{#else}}}
1564
+ &lt;img src="{{item.logo}}" class="h-[56px] md:h-[72px] py-3" /&gt;
1565
+ {{{/if}}}
1516
1566
  &lt;/a&gt;
1517
1567
 
1518
- &lt;div class="ml-auto flex items-center gap-2"&gt;
1519
- &lt;ul class="hidden lg:flex items-center space-x-[20px] pt-1 text-sm uppercase tracking-widest"&gt;
1520
- {{{#subarray:navItem {"field":"item.menus.Site Root","value":[]}}}}
1521
- &lt;li class="relative group"&gt;
1522
- {{{#if {"cond":"navItem.item.type == external"}}}}
1523
- &lt;a href="{{navItem.item.url}}" class="nav-item cursor-pointer"&gt;{{navItem.name}}&lt;/a&gt;
1568
+ &lt;div class="cms-nav-desktop ml-auto flex items-center gap-2"&gt;
1569
+ &lt;ul class="hidden lg:flex items-center gap-x-[20px] pt-1 text-sm uppercase tracking-widest list-none m-0 p-0 [&amp;&gt;li]:m-0 [&amp;&gt;li&gt;a]:m-0"&gt;
1570
+ {{{#subarray:menuItem {"field":"item.menus.Site Root","limit":5,"value":[]}}}}
1571
+ &lt;li class="relative group cms-nav-folder" data-cms-nav-folder&gt;
1572
+ {{{#if {"cond":"menuItem.item.type == 'external'"}}}}
1573
+ &lt;a href="{{menuItem.item.url}}" class="cursor-pointer"&gt;{{menuItem.name}}&lt;/a&gt;
1524
1574
  {{{#else}}}
1525
- {{{#if {"cond":"navItem.name == home"}}}}
1526
- &lt;a href="/" class="nav-item cursor-pointer"&gt;{{navItem.name}}&lt;/a&gt;
1575
+ {{{#if {"cond":"menuItem.item == '[object Object]'"}}}}
1576
+ {{{#entries:folderEntry {"field":"menuItem.item","value":{}}}}}
1577
+ {{{#if {"cond":"folderEntry.key == 'home'"}}}}
1578
+ &lt;a href="/" class="cms-nav-folder-toggle cursor-pointer text-sideNavText" data-cms-nav-folder-toggle&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1527
1579
  {{{#else}}}
1528
- &lt;a href="/{{navItem.name}}" class="nav-item cursor-pointer"&gt;{{navItem.name}}&lt;/a&gt;
1580
+ &lt;a href="/{{folderEntry.key}}" class="cms-nav-folder-toggle cursor-pointer text-sideNavText" data-cms-nav-folder-toggle&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1581
+ {{{/if}}}
1582
+ &lt;div class="cms-nav-folder-menu absolute left-0 top-full z-40 hidden min-w-max whitespace-nowrap bg-sideNavBg text-sideNavText py-2 text-left px-12 normal-case tracking-normal shadow-xl" data-cms-nav-folder-menu&gt;
1583
+ &lt;ul&gt;
1584
+ {{{#subarray:folderChild {"field":"item.value","value":[]}}}}
1585
+ &lt;li class="py-1"&gt;
1586
+ {{{#if {"cond":"folderChild.item.type == 'external'"}}}}
1587
+ &lt;a href="{{folderChild.item.url}}" class="block cursor-pointer whitespace-nowrap text-sideNavText"&gt;{{folderChild.name}}&lt;/a&gt;
1588
+ {{{#else}}}
1589
+ {{{#if {"cond":"folderChild.menuTitle"}}}}
1590
+ &lt;a href="/{{folderEntry.key}}/{{folderChild.name}}" class="block cursor-pointer whitespace-nowrap text-sideNavText"&gt;{{folderChild.menuTitle}}&lt;/a&gt;
1591
+ {{{#else}}}
1592
+ &lt;a href="/{{folderEntry.key}}/{{folderChild.name}}" class="block cursor-pointer whitespace-nowrap text-sideNavText"&gt;{{folderChild.name}}&lt;/a&gt;
1593
+ {{{/if}}}
1594
+ {{{/if}}}
1595
+ &lt;/li&gt;
1596
+ {{{/subarray}}}
1597
+ &lt;/ul&gt;
1598
+ &lt;/div&gt;
1599
+ {{{/entries}}}
1600
+ {{{#else}}}
1601
+ {{{#if {"cond":"menuItem.name == 'home'"}}}}
1602
+ {{{#if {"cond":"menuItem.menuTitle"}}}}
1603
+ &lt;a href="/" class="cursor-pointer"&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1604
+ {{{#else}}}
1605
+ &lt;a href="/" class="cursor-pointer"&gt;{{menuItem.name}}&lt;/a&gt;
1606
+ {{{/if}}}
1607
+ {{{#else}}}
1608
+ {{{#if {"cond":"menuItem.menuTitle"}}}}
1609
+ &lt;a href="/{{menuItem.name}}" class="cursor-pointer"&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1610
+ {{{#else}}}
1611
+ &lt;a href="/{{menuItem.name}}" class="cursor-pointer"&gt;{{menuItem.name}}&lt;/a&gt;
1612
+ {{{/if}}}
1613
+ {{{/if}}}
1529
1614
  {{{/if}}}
1530
1615
  {{{/if}}}
1531
1616
  &lt;/li&gt;
@@ -1540,34 +1625,118 @@ const exportCurrentBlock = () => {
1540
1625
  &lt;/div&gt;
1541
1626
  &lt;/div&gt;
1542
1627
  &lt;/div&gt;
1543
- {{{/array}}}
1544
1628
  &lt;/nav&gt;
1545
1629
 
1546
1630
  &lt;div class="cms-nav-overlay fixed inset-0 z-[110] bg-black/50 transition-opacity duration-300 opacity-0 pointer-events-none"&gt;&lt;/div&gt;
1547
1631
 
1548
1632
  &lt;aside class="cms-nav-panel fixed inset-y-0 right-0 z-[120] w-full max-w-md bg-sideNavBg text-sideNavText transition-all duration-300 translate-x-full opacity-0 pointer-events-none"&gt;
1549
- &lt;div class="relative h-full overflow-y-auto px-8 py-10"&gt;
1633
+ &lt;div class="relative flex h-full flex-col overflow-y-auto px-8 py-10 text-center"&gt;
1550
1634
  &lt;button type="button" class="cms-nav-close absolute right-6 top-6 text-4xl text-sideNavText"&gt;&amp;times;&lt;/button&gt;
1551
1635
 
1552
- &lt;ul class="mt-14 space-y-4 uppercase"&gt;
1553
- {{{#array {"field":"siteDoc","as":"site","collection":{"path":"sites","query":[{"field":"docId","operator":"==","value":"{siteId}"}],"order":[]},"limit":1,"value":[]}}}}
1554
- {{{#subarray:navItem {"field":"item.menus.Site Root","value":[]}}}}
1555
- &lt;li&gt;
1556
- {{{#if {"cond":"navItem.item.type == external"}}}}
1557
- &lt;a href="{{navItem.item.url}}" class="cms-nav-link block text-sideNavText"&gt;{{navItem.name}}&lt;/a&gt;
1636
+ &lt;div class="mb-8 mt-2 flex items-center justify-center gap-4"&gt;
1637
+ &lt;a href="/" class="flex items-center gap-4 text-navText"&gt;
1638
+ &lt;img src="{{item.logo}}" class="h-[30px] w-auto max-w-full object-contain" /&gt;
1639
+ {{{#if {"cond":"item.brandLogoDark"}}}}
1640
+ &lt;span class="h-10 w-px bg-black" aria-hidden="true"&gt;&lt;/span&gt;
1641
+ &lt;img src="{{item.brandLogoDark}}" class="h-[30px] w-auto max-w-full object-contain" /&gt;
1642
+ {{{/if}}}
1643
+ &lt;/a&gt;
1644
+ &lt;/div&gt;
1645
+
1646
+ &lt;ul class="w-full space-y-4 border-b border-black pb-4 uppercase"&gt;
1647
+ {{{#subarray:menuItem {"field":"item.menus.Site Root","value":[]}}}}
1648
+ &lt;li class="border-t border-black pt-4"&gt;
1649
+ {{{#if {"cond":"menuItem.item.type == 'external'"}}}}
1650
+ &lt;a href="{{menuItem.item.url}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm"&gt;{{menuItem.name}}&lt;/a&gt;
1651
+ {{{#else}}}
1652
+ {{{#if {"cond":"menuItem.item == '[object Object]'"}}}}
1653
+ {{{#entries:folderEntry {"field":"menuItem.item","value":{}}}}}
1654
+ {{{#if {"cond":"folderEntry.key == 'home'"}}}}
1655
+ &lt;a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm"&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1558
1656
  {{{#else}}}
1559
- {{{#if {"cond":"navItem.name == home"}}}}
1560
- &lt;a href="/" class="cms-nav-link block text-sideNavText"&gt;{{navItem.name}}&lt;/a&gt;
1657
+ &lt;a href="/{{folderEntry.key}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm"&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1658
+ {{{/if}}}
1659
+ &lt;ul class="mt-2 space-y-2 border-l border-black/40 pl-4"&gt;
1660
+ {{{#subarray:folderChild {"field":"item.value","value":[]}}}}
1661
+ &lt;li&gt;
1662
+ {{{#if {"cond":"folderChild.item.type == 'external'"}}}}
1663
+ &lt;a href="{{folderChild.item.url}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs"&gt;{{folderChild.name}}&lt;/a&gt;
1664
+ {{{#else}}}
1665
+ {{{#if {"cond":"folderChild.menuTitle"}}}}
1666
+ &lt;a href="/{{folderEntry.key}}/{{folderChild.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs"&gt;{{folderChild.menuTitle}}&lt;/a&gt;
1667
+ {{{#else}}}
1668
+ &lt;a href="/{{folderEntry.key}}/{{folderChild.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-xs"&gt;{{folderChild.name}}&lt;/a&gt;
1669
+ {{{/if}}}
1670
+ {{{/if}}}
1671
+ &lt;/li&gt;
1672
+ {{{/subarray}}}
1673
+ &lt;/ul&gt;
1674
+ {{{/entries}}}
1675
+ {{{#else}}}
1676
+ {{{#if {"cond":"menuItem.name == 'home'"}}}}
1677
+ {{{#if {"cond":"menuItem.menuTitle"}}}}
1678
+ &lt;a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm"&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1679
+ {{{#else}}}
1680
+ &lt;a href="/" class="cms-nav-link block text-sideNavText tracking-widest text-sm"&gt;{{menuItem.name}}&lt;/a&gt;
1681
+ {{{/if}}}
1561
1682
  {{{#else}}}
1562
- &lt;a href="/{{navItem.name}}" class="cms-nav-link block text-sideNavText"&gt;{{navItem.name}}&lt;/a&gt;
1683
+ {{{#if {"cond":"menuItem.menuTitle"}}}}
1684
+ &lt;a href="/{{menuItem.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm"&gt;{{menuItem.menuTitle}}&lt;/a&gt;
1685
+ {{{#else}}}
1686
+ &lt;a href="/{{menuItem.name}}" class="cms-nav-link block text-sideNavText tracking-widest text-sm"&gt;{{menuItem.name}}&lt;/a&gt;
1687
+ {{{/if}}}
1688
+ {{{/if}}}
1563
1689
  {{{/if}}}
1564
1690
  {{{/if}}}
1565
1691
  &lt;/li&gt;
1566
1692
  {{{/subarray}}}
1567
- {{{/array}}}
1568
1693
  &lt;/ul&gt;
1694
+
1695
+ &lt;div class="mt-10 flex w-full items-center justify-center gap-4"&gt;
1696
+ {{{#if {"cond":"item.socialFacebook"}}}}
1697
+ &lt;a href="{{item.socialFacebook}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg"&gt;
1698
+ &lt;span class="sr-only"&gt;Facebook&lt;/span&gt;
1699
+ &lt;span class="h-5 w-5 [&amp;&gt;svg]:h-5 [&amp;&gt;svg]:w-5 [&amp;&gt;svg]:fill-current" aria-hidden="true"&gt;
1700
+ &lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"&gt;
1701
+ &lt;path d="M80 299.3V512H196V299.3h86.5l18-97.8H196V166.9c0-51.7 20.3-71.5 72.7-71.5c16.3 0 29.4 .4 37 1.2V7.9C291.4 4 256.4 0 236.2 0C129.3 0 80 50.5 80 159.4v42.1H14v97.8H80z"&gt;&lt;/path&gt;
1702
+ &lt;/svg&gt;
1703
+ &lt;/span&gt;
1704
+ &lt;/a&gt;
1705
+ {{{/if}}}
1706
+ {{{#if {"cond":"item.socialInstagram"}}}}
1707
+ &lt;a href="{{item.socialInstagram}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg"&gt;
1708
+ &lt;span class="sr-only"&gt;Instagram&lt;/span&gt;
1709
+ &lt;span class="h-5 w-5 [&amp;&gt;svg]:h-5 [&amp;&gt;svg]:w-5 [&amp;&gt;svg]:fill-current" aria-hidden="true"&gt;
1710
+ &lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"&gt;
1711
+ &lt;path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"&gt;&lt;/path&gt;
1712
+ &lt;/svg&gt;
1713
+ &lt;/span&gt;
1714
+ &lt;/a&gt;
1715
+ {{{/if}}}
1716
+ {{{#if {"cond":"item.socialLinkedIn"}}}}
1717
+ &lt;a href="{{item.socialLinkedIn}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg"&gt;
1718
+ &lt;span class="sr-only"&gt;LinkedIn&lt;/span&gt;
1719
+ &lt;span class="h-5 w-5 [&amp;&gt;svg]:h-5 [&amp;&gt;svg]:w-5 [&amp;&gt;svg]:fill-current" aria-hidden="true"&gt;
1720
+ &lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"&gt;
1721
+ &lt;path d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z"&gt;&lt;/path&gt;
1722
+ &lt;/svg&gt;
1723
+ &lt;/span&gt;
1724
+ &lt;/a&gt;
1725
+ {{{/if}}}
1726
+ {{{#if {"cond":"item.socialYouTube"}}}}
1727
+ &lt;a href="{{item.socialYouTube}}" target="_blank" rel="noopener" class="flex h-10 w-10 items-center justify-center rounded-full border border-sideNavText text-sideNavText transition-colors duration-200 hover:bg-sideNavText hover:text-sideNavBg"&gt;
1728
+ &lt;span class="sr-only"&gt;YouTube&lt;/span&gt;
1729
+ &lt;span class="h-5 w-5 [&amp;&gt;svg]:h-5 [&amp;&gt;svg]:w-5 [&amp;&gt;svg]:fill-current" aria-hidden="true"&gt;
1730
+ &lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"&gt;
1731
+ &lt;path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"&gt;&lt;/path&gt;
1732
+ &lt;/svg&gt;
1733
+ &lt;/span&gt;
1734
+ &lt;/a&gt;
1735
+ {{{/if}}}
1736
+ &lt;/div&gt;
1569
1737
  &lt;/div&gt;
1570
1738
  &lt;/aside&gt;
1739
+ {{{/array}}}
1571
1740
  &lt;/div&gt;</code></pre>
1572
1741
  </section>
1573
1742
 
@@ -1579,8 +1748,152 @@ const exportCurrentBlock = () => {
1579
1748
  <div>Clicking the nav button opens the slide-out in Block Editor preview and Page Preview mode.</div>
1580
1749
  <div>Interactive nav elements do not trigger “Edit Block”. Clicking outside them still opens the editor in edit mode.</div>
1581
1750
  <div>In CMS preview, fixed nav and panel are contained to the preview surface by the block wrapper.</div>
1751
+ <div><code>cms-nav-pos-left</code> also switches the slide-out panel to the left side.</div>
1752
+ </div>
1753
+ </section>
1754
+ </div>
1755
+ </div>
1756
+ </TabsContent>
1757
+
1758
+ <TabsContent value="form-helpers">
1759
+ <div class="h-[calc(100vh-190px)] overflow-y-auto pr-1 pb-6">
1760
+ <div class="space-y-6">
1761
+ <section class="space-y-2">
1762
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1763
+ What This Does
1764
+ </h3>
1765
+ <p class="text-sm text-foreground">
1766
+ Add helper classes or data attributes to a CMS block form, and the client runtime will submit to
1767
+ <code>/api/contact</code> with anti-bot checks and submit history tracking.
1768
+ </p>
1769
+ </section>
1770
+
1771
+ <section class="space-y-2">
1772
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1773
+ CMS Preview Scope
1774
+ </h3>
1775
+ <p class="text-sm text-foreground">
1776
+ In Block Editor, this is for structure and messaging preview only. Use it to verify markup and required-state UX,
1777
+ not to validate end-to-end delivery.
1778
+ </p>
1779
+ </section>
1780
+
1781
+ <section class="space-y-2">
1782
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1783
+ Helper Contract
1784
+ </h3>
1785
+ <div class="text-sm text-foreground space-y-1">
1786
+ <div><code>form.cms-form</code> or <code>form[data-cms-form]</code>: form root.</div>
1787
+ <div><code>.cms-form-required</code> or <code>[data-cms-required=&quot;true&quot;]</code>: required field markers.</div>
1788
+ <div><code>.cms-form-submit</code> or <code>[data-cms-form-submit]</code>: submit button.</div>
1789
+ <div><code>.cms-form-message</code> or <code>[data-cms-form-message]</code>: status/error message container.</div>
1790
+ </div>
1791
+ </section>
1792
+
1793
+ <section class="space-y-2">
1794
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1795
+ Defaults + Messages
1796
+ </h3>
1797
+ <div class="text-sm text-foreground space-y-1">
1798
+ <div>Default endpoint: <code>/api/contact</code>.</div>
1799
+ <div><code>data-cms-success-message</code>: override success copy.</div>
1800
+ <div><code>data-cms-error-message</code>: override error copy.</div>
1801
+ <div><code>data-cms-required-message</code>: override required-field copy.</div>
1582
1802
  </div>
1583
1803
  </section>
1804
+
1805
+ <section class="space-y-2">
1806
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1807
+ Context IDs
1808
+ </h3>
1809
+ <p class="text-sm text-foreground">
1810
+ Block/Page/Site/Org IDs are inherited from the CMS HTML wrapper automatically, so forms in blocks
1811
+ do not need manual context wiring.
1812
+ </p>
1813
+ </section>
1814
+
1815
+ <section class="space-y-3">
1816
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
1817
+ Contact Form Example (Block HTML)
1818
+ </h3>
1819
+ <pre v-pre class="rounded-md bg-muted p-3 text-xs overflow-auto"><code>&lt;section
1820
+ class="relative cms-block cms-block-contact-form-placeholder rounded-2xl border border-dashed border-slate-300 bg-slate-50/70 px-4 py-6 sm:px-6 sm:py-8"
1821
+ data-block-type="contact-form-placeholder"
1822
+ &gt;
1823
+ &lt;div class="mx-auto max-w-3xl pt-6"&gt;
1824
+ &lt;div class="mb-6 space-y-2 text-center sm:text-left"&gt;
1825
+ &lt;h2 class="text-xl font-semibold text-slate-900"&gt;
1826
+ {{{#text {"field":"formHeader","title":"Form Header","value":"Contact Us"}}}}
1827
+ &lt;/h2&gt;
1828
+ &lt;p class="text-sm text-slate-600"&gt;
1829
+ {{{#text {"field":"formSubheader","title":"Form Subheader","value":"Subheader content"}}}}
1830
+ &lt;/p&gt;
1831
+ &lt;/div&gt;
1832
+
1833
+ &lt;form
1834
+ class="cms-form space-y-4"
1835
+ data-cms-form
1836
+ data-cms-required-message="Please complete all required fields."
1837
+ data-cms-success-message="Thanks! Your message has been sent."
1838
+ data-cms-error-message="Sorry, we could not send your message. Please try again."
1839
+ data-cms-success-class="cms-form-message cms-form-message-success"
1840
+ data-cms-error-class="cms-form-message cms-form-message-error"
1841
+ data-cms-invalid-class="cms-form-field-invalid"
1842
+ data-cms-working-class="cms-form-submitting"
1843
+ &gt;
1844
+ &lt;!-- Honeypot (optional, used by helper if present) --&gt;
1845
+ &lt;div class="pointer-events-none absolute -left-[9999px] top-auto h-px w-px overflow-hidden opacity-0" aria-hidden="true"&gt;
1846
+ &lt;label for="cms-company"&gt;Company&lt;/label&gt;
1847
+ &lt;input id="cms-company" name="company" type="text" tabindex="-1" autocomplete="off" /&gt;
1848
+ &lt;/div&gt;
1849
+
1850
+ &lt;div class="space-y-4"&gt;
1851
+ {{{#array {"field":"formFields","schema":[{"field":"fieldName","type":"text","title":"Field Label"},{"field":"fieldType","type":"option","title":"Field Type","option":{"optionsKey":"title","optionsValue":"value","options":[{"title":"Text","value":"text"},{"title":"Email","value":"email"},{"title":"Phone","value":"tel"},{"title":"Textarea","value":"textarea"}]},"value":"text"},{"field":"fieldRequired","type":"option","title":"Required","option":{"optionsKey":"title","optionsValue":"value","options":[{"title":"Yes","value":"true"},{"title":"No","value":"false"}]},"value":"true"}],"value":[{"fieldName":"Name","fieldType":"text","fieldRequired":"true"},{"fieldName":"Email","fieldType":"email","fieldRequired":"true"},{"fieldName":"Message","fieldType":"textarea","fieldRequired":"true"}]}}}}
1852
+ &lt;div class="space-y-1"&gt;
1853
+ &lt;label class="text-xs font-medium uppercase tracking-wide text-slate-600"&gt;
1854
+ {{item.fieldName}}
1855
+ &lt;/label&gt;
1856
+
1857
+ {{{#if {"cond":"item.fieldType == 'textarea'"}}}}
1858
+ &lt;textarea
1859
+ class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
1860
+ data-cms-required="{{item.fieldRequired}}"
1861
+ name="{{item.fieldName}}"
1862
+ placeholder="{{item.fieldName}}"
1863
+ rows="6"
1864
+ &gt;&lt;/textarea&gt;
1865
+ {{{#else}}}
1866
+ &lt;input
1867
+ class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
1868
+ data-cms-required="{{item.fieldRequired}}"
1869
+ type="{{item.fieldType}}"
1870
+ name="{{item.fieldName}}"
1871
+ placeholder="{{item.fieldName}}"
1872
+ /&gt;
1873
+ {{{/if}}}
1874
+ &lt;/div&gt;
1875
+ {{{/array}}}
1876
+ &lt;/div&gt;
1877
+
1878
+ &lt;div class="mt-6"&gt;
1879
+ &lt;button
1880
+ type="submit"
1881
+ class="cms-form-submit inline-flex w-full items-center justify-center rounded-lg bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white shadow-sm disabled:cursor-not-allowed disabled:opacity-60 sm:w-auto"
1882
+ data-cms-form-submit
1883
+ &gt;
1884
+ {{{#text {"field":"buttonText","title":"Button Text","value":"Send Message"}}}}
1885
+ &lt;/button&gt;
1886
+ &lt;/div&gt;
1887
+
1888
+ &lt;p class="cms-form-message hidden text-sm" data-cms-form-message&gt;&lt;/p&gt;
1889
+ &lt;/form&gt;
1890
+
1891
+ &lt;div class="hidden"&gt;
1892
+ {{{#text {"field":"emailTo","title":"Email To","value":"test@testing.com"}}}}
1893
+ &lt;/div&gt;
1894
+ &lt;/div&gt;
1895
+ &lt;/section&gt;</code></pre>
1896
+ </section>
1584
1897
  </div>
1585
1898
  </div>
1586
1899
  </TabsContent>
@@ -334,7 +334,7 @@ const clearTagFilters = () => {
334
334
  </script>
335
335
 
336
336
  <template>
337
- <div v-if="props.blockOverride" :class="previewSurfaceClass(blockOverridePreviewType)">
337
+ <div v-if="props.blockOverride" class="pointer-events-none" :class="previewSurfaceClass(blockOverridePreviewType)">
338
338
  <edge-cms-block-api
339
339
  :content="props.blockOverride.content"
340
340
  :values="props.blockOverride.values"
@@ -386,7 +386,7 @@ const clearTagFilters = () => {
386
386
  <div class="scale-wrapper">
387
387
  <div
388
388
  :ref="el => setInnerRef(block.docId, el)"
389
- class="scale-inner scale p-4"
389
+ class="scale-inner scale p-4 pointer-events-none"
390
390
  :class="previewSurfaceClass(block.previewType)"
391
391
  :data-block-id="block.docId"
392
392
  >
@@ -463,7 +463,7 @@ const clearTagFilters = () => {
463
463
  <div class="scale-wrapper">
464
464
  <div
465
465
  :ref="el => setInnerRef(block.docId, el)"
466
- class="scale-inner scale p-4"
466
+ class="scale-inner scale p-4 pointer-events-none"
467
467
  :class="previewSurfaceClass(block.previewType)"
468
468
  :data-block-id="block.docId"
469
469
  >
@@ -43,7 +43,8 @@ const router = useRouter()
43
43
  const blockImportInputRef = ref(null)
44
44
  const blockImportDocIdResolver = ref(null)
45
45
  const blockImportConflictResolver = ref(null)
46
- const INVALID_BLOCK_IMPORT_MESSAGE = 'Invalid file. Please import a valid block file.'
46
+ const DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE = 'Failed to import block JSON.'
47
+ const OPTIONAL_BLOCK_IMPORT_KEYS = new Set(['previewType'])
47
48
 
48
49
  const seedInitialBlocks = async () => {
49
50
  console.log('Seeding initial blocks...')
@@ -415,7 +416,7 @@ const readTextFile = file => new Promise((resolve, reject) => {
415
416
 
416
417
  const normalizeImportedDoc = (payload, fallbackDocId = '') => {
417
418
  if (!payload || typeof payload !== 'object' || Array.isArray(payload))
418
- throw new Error(INVALID_BLOCK_IMPORT_MESSAGE)
419
+ throw new Error('Invalid JSON payload. Expected an object.')
419
420
 
420
421
  if (payload.document && typeof payload.document === 'object' && !Array.isArray(payload.document)) {
421
422
  const normalized = { ...payload.document }
@@ -454,30 +455,48 @@ const getBlockDocDefaults = () => getDocDefaultsFromSchema(blockNewDocSchema.val
454
455
 
455
456
  const validateImportedBlockDoc = (doc) => {
456
457
  if (!isPlainObject(doc))
457
- throw new Error(INVALID_BLOCK_IMPORT_MESSAGE)
458
+ throw new Error('Invalid block document. Expected an object.')
458
459
 
459
460
  const requiredKeys = Object.keys(blockNewDocSchema.value || {})
461
+ .filter(key => !OPTIONAL_BLOCK_IMPORT_KEYS.has(key))
460
462
  const missing = requiredKeys.filter(key => !Object.prototype.hasOwnProperty.call(doc, key))
461
463
  if (missing.length)
462
- throw new Error(INVALID_BLOCK_IMPORT_MESSAGE)
464
+ throw new Error(`Missing required block key(s): ${missing.join(', ')}`)
463
465
 
464
466
  return doc
465
467
  }
466
468
 
467
469
  const validateImportedBlockThemes = (doc) => {
470
+ if (Object.prototype.hasOwnProperty.call(doc || {}, 'themes') && !Array.isArray(doc?.themes)) {
471
+ throw new Error('Invalid "themes" value. Expected an array of theme docIds.')
472
+ }
473
+
468
474
  const importedThemes = Array.isArray(doc?.themes) ? doc.themes : []
469
475
  if (!importedThemes.length)
470
476
  return doc
471
477
 
472
478
  const orgThemes = edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`] || {}
479
+ const missingThemeIds = []
480
+ let hasEmptyThemeId = false
473
481
  const normalizedThemes = []
474
482
  for (const themeId of importedThemes) {
475
483
  const normalizedThemeId = String(themeId || '').trim()
476
- if (!normalizedThemeId || !orgThemes[normalizedThemeId])
477
- throw new Error(INVALID_BLOCK_IMPORT_MESSAGE)
484
+ if (!normalizedThemeId) {
485
+ hasEmptyThemeId = true
486
+ continue
487
+ }
488
+ if (!orgThemes[normalizedThemeId]) {
489
+ missingThemeIds.push(normalizedThemeId)
490
+ continue
491
+ }
478
492
  normalizedThemes.push(normalizedThemeId)
479
493
  }
480
494
 
495
+ if (hasEmptyThemeId)
496
+ throw new Error('Themes include an empty theme id.')
497
+ if (missingThemeIds.length)
498
+ throw new Error(`Theme id(s) not found in this organization: ${[...new Set(missingThemeIds)].join(', ')}`)
499
+
481
500
  doc.themes = [...new Set(normalizedThemes)]
482
501
  return doc
483
502
  }
@@ -553,10 +572,29 @@ const getImportDocId = async (incomingDoc, fallbackDocId = '') => {
553
572
  }
554
573
 
555
574
  const openImportErrorDialog = (message) => {
556
- state.importErrorMessage = String(message || 'Failed to import block JSON.')
575
+ state.importErrorMessage = String(message || DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE)
557
576
  state.importErrorDialogOpen = true
558
577
  }
559
578
 
579
+ const getBlockImportFailureReason = (error, message) => {
580
+ if (error instanceof SyntaxError)
581
+ return `Invalid JSON syntax: ${String(error?.message || 'Unable to parse JSON.')}`
582
+ if (/^Import canceled\./i.test(message))
583
+ return message
584
+ return message
585
+ }
586
+
587
+ const logBlockImportFailure = (file, error, message) => {
588
+ const fileName = String(file?.name || 'unknown-file')
589
+ const reason = getBlockImportFailureReason(error, message)
590
+ console.error(`[BlocksManager] Block import failed for "${fileName}". Reason: ${reason}`, {
591
+ fileName,
592
+ reason,
593
+ errorMessage: message,
594
+ error,
595
+ })
596
+ }
597
+
560
598
  const triggerBlockImport = () => {
561
599
  blockImportInputRef.value?.click()
562
600
  }
@@ -614,14 +652,11 @@ const handleBlockImport = async (event) => {
614
652
  await importSingleBlockFile(file, existingBlocks)
615
653
  }
616
654
  catch (error) {
617
- console.error('Failed to import block JSON', error)
618
- const message = error?.message || 'Failed to import block JSON.'
655
+ const message = error?.message || DEFAULT_BLOCK_IMPORT_ERROR_MESSAGE
656
+ logBlockImportFailure(file, error, message)
619
657
  if (/^Import canceled\./i.test(message))
620
658
  continue
621
- if (error instanceof SyntaxError || message === INVALID_BLOCK_IMPORT_MESSAGE)
622
- openImportErrorDialog(INVALID_BLOCK_IMPORT_MESSAGE)
623
- else
624
- openImportErrorDialog(message)
659
+ openImportErrorDialog(getBlockImportFailureReason(error, message))
625
660
  }
626
661
  }
627
662
  }