@dreamtree-org/landing-kit 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -276,9 +276,9 @@ function Wordmark({ name }) {
276
276
  /* @__PURE__ */ jsx5("span", { className: "dt-lp-wordmark-hi", children: m[2] })
277
277
  ] });
278
278
  }
279
- function BrandLockup({ brand: own, byline = "by Dreamtree-Org", href = "/" }) {
279
+ function BrandLockup({ brand: own2, byline = "by Dreamtree-Org", href = "/" }) {
280
280
  const ctx = useLanding();
281
- const brand = own || ctx.brand;
281
+ const brand = own2 || ctx.brand;
282
282
  if (!brand) return null;
283
283
  return /* @__PURE__ */ jsxs4("a", { href, className: "dt-lp-brand", "aria-label": `${brand.name}${byline ? ` ${byline}` : ""}, home`, children: [
284
284
  /* @__PURE__ */ jsx5(BrandMark2, { brand }),
@@ -405,10 +405,10 @@ function DreamtreeFooter({
405
405
  tagline,
406
406
  legal = [],
407
407
  year = (/* @__PURE__ */ new Date()).getFullYear(),
408
- brand: own
408
+ brand: own2
409
409
  }) {
410
410
  const ctx = useLanding();
411
- const brand = own || ctx.brand;
411
+ const brand = own2 || ctx.brand;
412
412
  return /* @__PURE__ */ jsxs7("footer", { className: "dt-lp-footer", children: [
413
413
  /* @__PURE__ */ jsxs7(Wrap, { className: `dt-lp-footer-grid dt-lp-footer-grid--${Math.min(columns.length, 3)}`, children: [
414
414
  /* @__PURE__ */ jsxs7("div", { className: "dt-lp-footer-about", children: [
@@ -575,7 +575,7 @@ function FeatureSection(props) {
575
575
  ] })
576
576
  );
577
577
  case "intro": {
578
- const { subtitle, detail, note, glance = [], glanceTitle = "At a glance", link } = props;
578
+ const { subtitle, detail, note, glance = [], glanceTitle = "At a glance", link: link2 } = props;
579
579
  return shell(
580
580
  /* @__PURE__ */ jsxs10("div", { className: "dt-lp-intro-grid", children: [
581
581
  /* @__PURE__ */ jsxs10("div", { children: [
@@ -589,7 +589,7 @@ function FeatureSection(props) {
589
589
  /* @__PURE__ */ jsx12("p", { id: `${hid2}-glance`, className: "dt-lp-eyebrow", children: glanceTitle }),
590
590
  /* @__PURE__ */ jsx12(Checklist, { items: glance }),
591
591
  props.glanceNote ? /* @__PURE__ */ jsx12("p", { className: "dt-lp-note", children: props.glanceNote }) : null,
592
- link ? /* @__PURE__ */ jsx12(TextLink, { href: link.href, className: "dt-lp-mt", children: link.label }) : null
592
+ link2 ? /* @__PURE__ */ jsx12(TextLink, { href: link2.href, className: "dt-lp-mt", children: link2.label }) : null
593
593
  ] }) : null
594
594
  ] })
595
595
  );
@@ -750,7 +750,7 @@ function Faq({ id = "faq", eyebrow, title: title2 = "Frequently asked questions"
750
750
 
751
751
  // src/DemoCta.jsx
752
752
  import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
753
- function DemoCta({ id = "demo", eyebrow, title: title2, body, label = "Book a demo", chips, chipsLabel = "Business types", note, link }) {
753
+ function DemoCta({ id = "demo", eyebrow, title: title2, body, label = "Book a demo", chips, chipsLabel = "Business types", note, link: link2 }) {
754
754
  return /* @__PURE__ */ jsx16("section", { id, "aria-labelledby": `${id}-title`, className: "dt-lp-section dt-lp-cta", children: /* @__PURE__ */ jsx16(Wrap, { children: /* @__PURE__ */ jsxs14("div", { className: "dt-lp-cta-panel", children: [
755
755
  eyebrow ? /* @__PURE__ */ jsx16("p", { className: "dt-lp-eyebrow dt-lp-eyebrow--inverse", children: eyebrow }) : null,
756
756
  /* @__PURE__ */ jsx16("h2", { id: `${id}-title`, className: "dt-lp-h2 dt-lp-h2--cta", children: title2 }),
@@ -761,7 +761,7 @@ function DemoCta({ id = "demo", eyebrow, title: title2, body, label = "Book a de
761
761
  note.title ? /* @__PURE__ */ jsx16("h3", { className: "dt-lp-h3", children: note.title }) : null,
762
762
  note.body ? /* @__PURE__ */ jsx16("p", { children: note.body }) : null
763
763
  ] }) : null,
764
- link ? /* @__PURE__ */ jsx16("p", { className: "dt-lp-cta-link", children: /* @__PURE__ */ jsx16(SmartLink, { href: link.href, reload: link.reload, children: link.label }) }) : null
764
+ link2 ? /* @__PURE__ */ jsx16("p", { className: "dt-lp-cta-link", children: /* @__PURE__ */ jsx16(SmartLink, { href: link2.href, reload: link2.reload, children: link2.label }) }) : null
765
765
  ] }) }) });
766
766
  }
767
767
 
@@ -824,17 +824,17 @@ function LandingPage({ brand, content, preview = null, slots = {}, linkComponent
824
824
  import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
825
825
  function PreviewFrame({
826
826
  id = "product",
827
- heading,
827
+ heading: heading2,
828
828
  caption,
829
829
  meta,
830
830
  badge = "Demo data",
831
- brand: own,
831
+ brand: own2,
832
832
  children
833
833
  }) {
834
834
  const ctx = useLanding();
835
- const brand = own || ctx.brand;
835
+ const brand = own2 || ctx.brand;
836
836
  return /* @__PURE__ */ jsxs16("section", { id, "aria-labelledby": `${id}-title`, className: "dt-lp-preview", children: [
837
- /* @__PURE__ */ jsx18("h2", { id: `${id}-title`, className: "dt-lp-sr-only", children: heading || `${brand ? brand.name : "Product"} overview` }),
837
+ /* @__PURE__ */ jsx18("h2", { id: `${id}-title`, className: "dt-lp-sr-only", children: heading2 || `${brand ? brand.name : "Product"} overview` }),
838
838
  /* @__PURE__ */ jsx18(Wrap, { children: /* @__PURE__ */ jsxs16("figure", { className: "dt-lp-preview-frame", children: [
839
839
  /* @__PURE__ */ jsxs16("div", { className: "dt-lp-preview-bar", children: [
840
840
  /* @__PURE__ */ jsxs16("div", { className: "dt-lp-preview-title", children: [
@@ -1484,6 +1484,12 @@ var SECTION_TYPES = {
1484
1484
  var SECTION_TYPE_KEYS = Object.keys(SECTION_TYPES);
1485
1485
  var PAGE_VERSION = 1;
1486
1486
  var MAX_SECTIONS = 20;
1487
+ function sectionDefaults(type) {
1488
+ const def = SECTION_TYPES[type];
1489
+ if (!def) throw new Error(`Unknown section type "${type}"`);
1490
+ const fill2 = (fields2) => Object.fromEntries(fields2.map((f) => [f.key, f.kind === "list" ? (f.default || []).map((item) => ({ ...fill2(f.of), ...item })) : f.default]));
1491
+ return fill2(def.fields);
1492
+ }
1487
1493
 
1488
1494
  // src/schema/richtext.js
1489
1495
  var ALLOWED = /* @__PURE__ */ new Set(["p", "br", "h2", "h3", "h4", "strong", "b", "em", "i", "u", "ul", "ol", "li", "a", "blockquote"]);
@@ -1504,7 +1510,7 @@ var decode = (s) => s.replace(/&(amp|quot|#39|lt|gt|#x[0-9a-f]+|#\d+);/gi, (m, e
1504
1510
  });
1505
1511
  function safeHref(raw) {
1506
1512
  const v = decode(String(raw || "")).replace(/[\u0000- \u007f-\u009f]/g, "");
1507
- if (!v) return null;
1513
+ if (!v || v.includes("\\")) return null;
1508
1514
  if (/^(https?:\/\/|mailto:|tel:)/i.test(v)) return v;
1509
1515
  if (/^\/(?!\/)/.test(v) || v.startsWith("#")) return v;
1510
1516
  return null;
@@ -1628,6 +1634,9 @@ function field(f, value, path, errors) {
1628
1634
  throw new Error(`Unknown field kind "${f.kind}" (${path})`);
1629
1635
  }
1630
1636
  }
1637
+ function validateFields(defs, obj, path, errors) {
1638
+ return fields(defs, obj, path, errors);
1639
+ }
1631
1640
  function fields(defs, obj, path, errors) {
1632
1641
  const src = obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {};
1633
1642
  if (obj != null && src !== obj) errors.push({ path, message: "must be an object" });
@@ -1665,6 +1674,18 @@ function validatePage(input) {
1665
1674
  });
1666
1675
  return { ok: errors.length === 0, errors, page: { version: PAGE_VERSION, sections } };
1667
1676
  }
1677
+ function defaultPage({ name = "Our store", tagline = "" } = {}) {
1678
+ const hero = sectionDefaults("hero_slider");
1679
+ hero.slides = [{ ...hero.slides[0], heading: String(name).slice(0, 90), text: String(tagline || "").slice(0, 220), cta_label: "Shop now", cta_href: "/" }];
1680
+ return validatePage({
1681
+ version: PAGE_VERSION,
1682
+ sections: [
1683
+ { id: "hero", type: "hero_slider", props: { ...hero, autoplay: false } },
1684
+ { id: "featured", type: "featured", props: sectionDefaults("featured") },
1685
+ { id: "categories", type: "categories", props: sectionDefaults("categories") }
1686
+ ]
1687
+ }).page;
1688
+ }
1668
1689
 
1669
1690
  // src/schema/color.js
1670
1691
  var HEX_RE = /^#([0-9a-f]{6})$/i;
@@ -2039,12 +2060,12 @@ function SlotSection({ id, title: title2, intro: intro2, tone: tone2, slot, ...p
2039
2060
  /* @__PURE__ */ jsx24("div", { className: "dt-lp-slot-body", children: body })
2040
2061
  ] });
2041
2062
  }
2042
- function PromoBanner({ id, eyebrow, heading, text, cta_label: label, cta_href: href, image, style = "brand", media }) {
2063
+ function PromoBanner({ id, eyebrow, heading: heading2, text, cta_label: label, cta_href: href, image, style = "brand", media }) {
2043
2064
  const src = media(image, "wide");
2044
2065
  return /* @__PURE__ */ jsx24("section", { id, "aria-labelledby": hid(id), className: "dt-lp-section dt-lp-promo-wrap", children: /* @__PURE__ */ jsx24("div", { className: "dt-lp-wrap", children: /* @__PURE__ */ jsxs21("div", { className: `dt-lp-promo dt-lp-promo--${style}${src ? " dt-lp-promo--image" : ""}`, children: [
2045
2066
  /* @__PURE__ */ jsxs21("div", { className: "dt-lp-promo-copy", children: [
2046
2067
  eyebrow ? /* @__PURE__ */ jsx24("p", { className: "dt-lp-eyebrow dt-lp-eyebrow--inverse", children: eyebrow }) : null,
2047
- /* @__PURE__ */ jsx24("h2", { id: hid(id), className: "dt-lp-h2 dt-lp-promo-heading", children: heading }),
2068
+ /* @__PURE__ */ jsx24("h2", { id: hid(id), className: "dt-lp-h2 dt-lp-promo-heading", children: heading2 }),
2048
2069
  text ? /* @__PURE__ */ jsx24("p", { className: "dt-lp-promo-text", children: text }) : null,
2049
2070
  label && href ? /* @__PURE__ */ jsx24(SmartLink, { href, className: "dt-lp-btn dt-lp-btn--lg dt-lp-btn--inverse", children: label }) : null
2050
2071
  ] }),
@@ -2220,6 +2241,291 @@ function TenantPage({
2220
2241
  return /* @__PURE__ */ jsx25(SectionBoundary, { children: /* @__PURE__ */ jsx25(Block, { ...s.props, id: `s-${s.id}`, first: i === 0 && headingStart === 1, media, slot: slots[s.type], onSubscribe }) }, s.id);
2221
2242
  }) }) });
2222
2243
  }
2244
+
2245
+ // src/tenant/TemplateRegion.jsx
2246
+ import { Fragment as Fragment7, useMemo as useMemo2 } from "react";
2247
+ import { jsx as jsx26 } from "react/jsx-runtime";
2248
+ var noMedia2 = () => null;
2249
+ function TemplateRegion({
2250
+ blocks = [],
2251
+ slots = {},
2252
+ theme,
2253
+ resolveMedia = noMedia2,
2254
+ linkComponent = null,
2255
+ storeName = "",
2256
+ className = ""
2257
+ }) {
2258
+ const vars = useMemo2(() => themeVars(theme), [theme]);
2259
+ const ctx = useMemo2(() => ({ brand: { id: "tenant", name: storeName, primary: vars.theme.primary }, linkComponent, onBookDemo: null, home: false }), [storeName, vars.theme.primary, linkComponent]);
2260
+ const media = (ref, variant) => ref && ref.media_id != null ? resolveMedia(ref, variant) : null;
2261
+ return /* @__PURE__ */ jsx26(Fragment7, { children: blocks.filter((b) => b && b.enabled !== false).map((b) => {
2262
+ const Slot = slots[b.type];
2263
+ if (Slot) {
2264
+ return /* @__PURE__ */ jsx26(SectionBoundary, { children: /* @__PURE__ */ jsx26(Slot, { block: b, props: b.props || {} }) }, b.id);
2265
+ }
2266
+ const Block = TENANT_BLOCKS[b.type];
2267
+ if (!Block || Block === TENANT_BLOCKS.featured) return null;
2268
+ return /* @__PURE__ */ jsx26(SectionBoundary, { children: /* @__PURE__ */ jsx26(LandingProvider, { value: ctx, children: /* @__PURE__ */ jsx26("div", { className: `dt-landing dt-lp-tenant dt-lp-region ${className}`.trim(), style: vars.style, "data-block": b.type, children: /* @__PURE__ */ jsx26(Block, { ...b.props, id: `b-${b.id}`, first: false, media, slot: null, onSubscribe: null }) }) }) }, b.id);
2269
+ }) });
2270
+ }
2271
+
2272
+ // src/schema/store.js
2273
+ var STORE_VERSION = 2;
2274
+ var MAX_BLOCKS = 12;
2275
+ var ID_RE2 = /^[a-z0-9][a-z0-9-]{0,39}$/;
2276
+ var opt = (...pairs) => pairs.map(([value, label]) => ({ value, label }));
2277
+ var sel = (key, label, options, d) => ({ key, kind: "select", label, options, default: d });
2278
+ var bool = (key, label, d) => ({ key, kind: "bool", label, default: d });
2279
+ var heading = (d, max = 90) => ({ key: "title", kind: "text", label: "Heading", max, default: d });
2280
+ var CONTENT_BLOCKS = ["promo_banner", "usp_strip", "trust_badges", "cta_cards", "faq", "rich_text", "testimonials", "brand_strip", "gallery"];
2281
+ var LISTING_SLOTS = {
2282
+ collection_header: { label: "Collection header", description: "Title, description and quick links for the category or search.", fields: [bool("show_description", "Show the description", true), bool("show_quick_links", "Show quick links", true)] },
2283
+ toolbar: { label: "Toolbar and sort", description: "Result count, sort and the filters in use.", fields: [] },
2284
+ filters: { label: "Filters", description: "Price, brand and spec filters.", required: true, fields: [] },
2285
+ results: { label: "Results grid", description: "The products, with paging.", required: true, fields: [] }
2286
+ };
2287
+ var PRODUCT_SLOTS = {
2288
+ gallery: { label: "Gallery", description: "Photos of the product.", required: true, fields: [] },
2289
+ buy_box: { label: "Buy box", description: "Title, price, key specs and the buy buttons.", required: true, fields: [bool("show_compare", "Show Add to compare", true), bool("show_quote", "Show bulk quote link", true)] },
2290
+ highlights: { label: "Highlights", description: "The product's headline numbers and selling points.", fields: [heading("Highlights")] },
2291
+ delivery: { label: "Delivery check", description: "PIN code delivery estimate.", fields: [] },
2292
+ seller: { label: "Seller", description: "Who sells and services the product.", fields: [] },
2293
+ inspection: { label: "Inspection report", description: "Checks, grade and condition notes.", fields: [heading("Inspection report"), bool("expanded", "Open by default", false)] },
2294
+ battery: { label: "Battery", description: "Battery health, where the product has one.", fields: [] },
2295
+ included: { label: "In the box", description: "What ships with the product.", fields: [] },
2296
+ specs: { label: "Specifications", description: "The full specification table.", fields: [heading("Specifications")] },
2297
+ policies: { label: "Warranty and returns", description: "The store's warranty and return terms.", fields: [heading("Warranty and returns")] },
2298
+ similar: { label: "Similar products", description: "More products like this one.", fields: [heading("Similar products"), sel("limit", "How many", opt(["3", "3"], ["4", "4"], ["6", "6"], ["8", "8"]), "4")] }
2299
+ };
2300
+ var SORTS = opt(["relevance", "Recommended"], ["newest", "Newest first"], ["price_asc", "Price: low to high"], ["price_desc", "Price: high to low"], ["discount", "Biggest discount"]);
2301
+ var STORE_PAGES = {
2302
+ listing: {
2303
+ label: "Listing page",
2304
+ slots: LISTING_SLOTS,
2305
+ settings: [
2306
+ sel("columns", "Columns on desktop", opt(["3", "3"], ["4", "4"]), "3"),
2307
+ sel("columns_mobile", "Columns on phones", opt(["1", "1"], ["2", "2"]), "2"),
2308
+ sel("page_size", "Products per page", opt(["12", "12"], ["24", "24"], ["36", "36"]), "12"),
2309
+ sel("default_sort", "Default sort", SORTS, "relevance"),
2310
+ sel("filters", "Filters show as", opt(["sidebar", "Sidebar"], ["topbar", "Bar above the results"], ["drawer", "Button that opens a panel"]), "sidebar"),
2311
+ bool("active_chips", "Show the filters in use as chips", true),
2312
+ sel("paging", "Paging", opt(["pages", "Page numbers"], ["load_more", "Load more button"]), "pages")
2313
+ ],
2314
+ templates: {
2315
+ classic: { label: "Classic", description: "Filters in a sidebar beside a product grid.", settings: {}, blocks: ["collection_header", "toolbar", "filters", "results"] },
2316
+ topbar: { label: "Top bar", description: "Filter chips above a wide grid, more room for products.", settings: { columns: "4", filters: "topbar", paging: "load_more" }, blocks: ["collection_header", "filters", "toolbar", "results"] },
2317
+ editorial: { label: "Editorial", description: "A story-led collection header, then the grid behind a filter button.", settings: { columns: "4", filters: "drawer", active_chips: false }, blocks: ["collection_header", "rich_text", "toolbar", "filters", "results"] }
2318
+ },
2319
+ default: "classic"
2320
+ },
2321
+ product: {
2322
+ label: "Product page",
2323
+ slots: PRODUCT_SLOTS,
2324
+ settings: [
2325
+ sel("gallery", "Gallery", opt(["thumbs_side", "Thumbnails beside the photo"], ["thumbs_below", "Thumbnails below the photo"], ["mosaic", "Full-width mosaic"]), "thumbs_side"),
2326
+ bool("sticky_buy_bar", "Sticky buy bar on phones", true),
2327
+ sel("spec_tiles", "Key spec tiles", opt(["0", "None"], ["4", "4"], ["6", "6"]), "4"),
2328
+ bool("breadcrumbs", "Show breadcrumbs", true)
2329
+ ],
2330
+ templates: {
2331
+ classic: { label: "Classic", description: "Photos beside the buy box, details below.", settings: { gallery: "thumbs_side" }, blocks: ["gallery", "buy_box", "delivery", "seller", "inspection", "battery", "included", "specs", "policies", "similar"] },
2332
+ immersive: { label: "Immersive", description: "A full-width photo mosaic first, a floating buy panel, the story below.", settings: { gallery: "mosaic" }, blocks: ["gallery", "buy_box", "highlights", "inspection", "specs", "policies", "similar"] },
2333
+ compact: { label: "Compact", description: "One column, phone first, with a sticky buy bar.", settings: { gallery: "thumbs_below", spec_tiles: "0", breadcrumbs: false }, blocks: ["gallery", "buy_box", "delivery", "inspection", "specs", "policies", "similar"] }
2334
+ },
2335
+ default: "classic"
2336
+ }
2337
+ };
2338
+ var STORE_PAGE_KEYS = Object.keys(STORE_PAGES);
2339
+ var link = [
2340
+ { key: "label", kind: "text", label: "Label", max: 30, required: true, default: "" },
2341
+ { key: "href", kind: "url", label: "Link", required: true, default: "" }
2342
+ ];
2343
+ var CHROME_FIELDS = [
2344
+ {
2345
+ key: "announcement",
2346
+ kind: "group",
2347
+ label: "Announcement bar",
2348
+ fields: [
2349
+ bool("enabled", "Show the announcement bar", false),
2350
+ { key: "text", kind: "text", label: "Message", max: 120, default: "" },
2351
+ { key: "href", kind: "url", label: "Link", default: "" },
2352
+ sel("tone", "Colour", opt(["brand", "Brand"], ["accent", "Accent"], ["dark", "Dark"]), "brand")
2353
+ ]
2354
+ },
2355
+ {
2356
+ key: "header",
2357
+ kind: "group",
2358
+ label: "Header",
2359
+ fields: [
2360
+ { key: "utility_text", kind: "text", label: "Strip above the header", max: 80, default: "" },
2361
+ bool("show_search", "Search box", true),
2362
+ bool("category_nav", "Menu of the categories you sell", true),
2363
+ { key: "nav_links", kind: "list", label: "Extra menu links", max: 8, itemLabel: "label", of: link, default: [] },
2364
+ bool("sticky", "Keep the header on screen while scrolling", true)
2365
+ ]
2366
+ },
2367
+ {
2368
+ key: "footer",
2369
+ kind: "group",
2370
+ label: "Footer",
2371
+ fields: [
2372
+ { key: "about", kind: "textarea", label: "About the store", max: 240, default: "" },
2373
+ {
2374
+ key: "columns",
2375
+ kind: "list",
2376
+ label: "Link columns",
2377
+ max: 4,
2378
+ itemLabel: "title",
2379
+ of: [{ key: "title", kind: "text", label: "Column heading", max: 40, required: true, default: "" }, { key: "links", kind: "list", label: "Links", max: 8, itemLabel: "label", of: link, default: [] }],
2380
+ default: []
2381
+ },
2382
+ {
2383
+ key: "socials",
2384
+ kind: "list",
2385
+ label: "Social profiles",
2386
+ max: 6,
2387
+ itemLabel: "network",
2388
+ of: [sel("network", "Network", opt(["instagram", "Instagram"], ["facebook", "Facebook"], ["youtube", "YouTube"], ["x", "X"], ["linkedin", "LinkedIn"], ["whatsapp", "WhatsApp"]), "instagram"), { key: "href", kind: "url", label: "Profile link", required: true, default: "" }],
2389
+ default: []
2390
+ },
2391
+ { key: "note", kind: "text", label: "Small print (GSTIN, address)", max: 160, default: "" }
2392
+ ]
2393
+ }
2394
+ ];
2395
+ var CARD_FIELDS = [
2396
+ sel("image_ratio", "Photo shape", opt(["4:3", "Landscape 4:3"], ["1:1", "Square"], ["3:4", "Portrait 3:4"]), "4:3"),
2397
+ bool("badges", "Condition and discount badges", true),
2398
+ bool("spec_line", "Key specs line", true),
2399
+ bool("compare", "Compare checkbox", true),
2400
+ bool("quick_add", "Add to cart button", true),
2401
+ bool("mrp", "Show the MRP struck through", true),
2402
+ sel("hover", "On hover", opt(["none", "Nothing"], ["zoom", "Zoom the photo"], ["second_image", "Show the second photo"]), "zoom")
2403
+ ];
2404
+ function validateGroups(defs, obj, path, errors) {
2405
+ const src = obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {};
2406
+ if (obj != null && src !== obj) errors.push({ path, message: "must be an object" });
2407
+ return Object.fromEntries(defs.map((g) => [g.key, g.kind === "group" ? validateFields(g.fields, src[g.key], `${path}.${g.key}`, errors) : validateFields([g], src, path, errors)[g.key]]));
2408
+ }
2409
+ var fill = (defs) => Object.fromEntries(defs.map((f) => [f.key, f.kind === "group" ? fill(f.fields) : f.kind === "list" ? [...f.default || []] : f.default]));
2410
+ var defaultChrome = () => fill(CHROME_FIELDS);
2411
+ var defaultCard = () => fill(CARD_FIELDS);
2412
+ function blockDef(pageKey, type) {
2413
+ const page = own(STORE_PAGES, pageKey) ? STORE_PAGES[pageKey] : null;
2414
+ if (!page) return null;
2415
+ if (own(page.slots, type)) return { ...page.slots[type], slot: true };
2416
+ if (CONTENT_BLOCKS.includes(type) && own(SECTION_TYPES, type)) return { ...SECTION_TYPES[type], slot: false };
2417
+ return null;
2418
+ }
2419
+ var own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
2420
+ var blockId = (type) => type.replace(/_/g, "-");
2421
+ var blockDefaults = (pageKey, type) => {
2422
+ const def = blockDef(pageKey, type);
2423
+ return def.slot ? fill(def.fields) : sectionDefaults(type);
2424
+ };
2425
+ function templatePage(pageKey, template) {
2426
+ const page = STORE_PAGES[pageKey];
2427
+ const key = own(page.templates, template) ? template : page.default;
2428
+ const tpl = page.templates[key];
2429
+ const settings = { ...fill(page.settings), ...tpl.settings };
2430
+ const blocks = tpl.blocks.map((type) => ({ id: blockId(type), type, enabled: true, props: blockDefaults(pageKey, type) }));
2431
+ return { template: key, settings, blocks };
2432
+ }
2433
+ function validateTemplatePage(pageKey, input, path, errors) {
2434
+ var _a, _b;
2435
+ const page = STORE_PAGES[pageKey];
2436
+ const src = input && typeof input === "object" && !Array.isArray(input) ? input : {};
2437
+ let template = (_a = src.template) != null ? _a : page.default;
2438
+ if (!own(page.templates, template)) {
2439
+ errors.push({ path: `${path}.template`, message: `must be one of ${Object.keys(page.templates).join(", ")}` });
2440
+ template = page.default;
2441
+ }
2442
+ const tpl = page.templates[template];
2443
+ const settingDefs = page.settings.map((f) => tpl.settings[f.key] === void 0 ? f : { ...f, default: tpl.settings[f.key] });
2444
+ const settings = validateFields(settingDefs, (_b = src.settings) != null ? _b : {}, `${path}.settings`, errors);
2445
+ const list = Array.isArray(src.blocks) ? src.blocks : null;
2446
+ if (src.blocks != null && !list) errors.push({ path: `${path}.blocks`, message: "must be a list" });
2447
+ const raw = list != null ? list : templatePage(pageKey, template).blocks;
2448
+ if (raw.length > MAX_BLOCKS) errors.push({ path: `${path}.blocks`, message: `can have at most ${MAX_BLOCKS}` });
2449
+ const ids = /* @__PURE__ */ new Set();
2450
+ const slotsSeen = /* @__PURE__ */ new Set();
2451
+ const blocks = [];
2452
+ raw.slice(0, MAX_BLOCKS).forEach((b, i) => {
2453
+ var _a2, _b2;
2454
+ const at = `${path}.blocks[${i}]`;
2455
+ if (!b || typeof b !== "object") {
2456
+ errors.push({ path: at, message: "must be an object" });
2457
+ return;
2458
+ }
2459
+ const def = blockDef(pageKey, b.type);
2460
+ if (!def) {
2461
+ errors.push({ path: `${at}.type`, message: `unknown block "${b.type}" on the ${page.label.toLowerCase()}` });
2462
+ return;
2463
+ }
2464
+ const id = String((_a2 = b.id) != null ? _a2 : "");
2465
+ if (!ID_RE2.test(id)) errors.push({ path: `${at}.id`, message: "must be 1\u201340 lowercase letters, digits or dashes" });
2466
+ else if (ids.has(id)) errors.push({ path: `${at}.id`, message: `duplicate id "${id}"` });
2467
+ ids.add(id);
2468
+ if (def.slot) {
2469
+ if (slotsSeen.has(b.type)) {
2470
+ errors.push({ path: `${at}.type`, message: `"${def.label}" can appear only once` });
2471
+ return;
2472
+ }
2473
+ slotsSeen.add(b.type);
2474
+ }
2475
+ let enabled = b.enabled === void 0 ? true : b.enabled;
2476
+ if (typeof enabled !== "boolean") {
2477
+ errors.push({ path: `${at}.enabled`, message: "must be true or false" });
2478
+ enabled = true;
2479
+ }
2480
+ if (def.required && !enabled) {
2481
+ errors.push({ path: `${at}.enabled`, message: `"${def.label}" is required and cannot be hidden` });
2482
+ enabled = true;
2483
+ }
2484
+ blocks.push({ id, type: b.type, enabled, props: validateFields(def.fields, (_b2 = b.props) != null ? _b2 : {}, `${at}.props`, errors) });
2485
+ });
2486
+ for (const [type, def] of Object.entries(page.slots)) {
2487
+ if (def.required && !slotsSeen.has(type)) {
2488
+ errors.push({ path: `${path}.blocks`, message: `"${def.label}" is required` });
2489
+ blocks.push({ id: ids.has(blockId(type)) ? `${blockId(type)}-1` : blockId(type), type, enabled: true, props: blockDefaults(pageKey, type) });
2490
+ }
2491
+ }
2492
+ return { template, settings, blocks };
2493
+ }
2494
+ function migrateStore(input) {
2495
+ const home = input && typeof input === "object" && Array.isArray(input.sections) ? input : null;
2496
+ return {
2497
+ version: STORE_VERSION,
2498
+ pages: { home: home ? { version: 1, sections: home.sections } : defaultPage(), listing: templatePage("listing"), product: templatePage("product") },
2499
+ chrome: defaultChrome(),
2500
+ card: defaultCard()
2501
+ };
2502
+ }
2503
+ var isLegacyPage = (doc) => !!doc && typeof doc === "object" && doc.version !== STORE_VERSION && Array.isArray(doc.sections);
2504
+ function validateStore(input) {
2505
+ var _a, _b, _c;
2506
+ const errors = [];
2507
+ const src0 = isLegacyPage(input) ? migrateStore(input) : input;
2508
+ const src = src0 && typeof src0 === "object" && !Array.isArray(src0) ? src0 : null;
2509
+ if (!src) {
2510
+ const doc2 = migrateStore(null);
2511
+ return { ok: false, errors: [{ path: "doc", message: "must be an object" }], doc: doc2 };
2512
+ }
2513
+ if (src.version != null && src.version !== STORE_VERSION) errors.push({ path: "version", message: `must be ${STORE_VERSION}` });
2514
+ const pages = src.pages && typeof src.pages === "object" ? src.pages : {};
2515
+ const home = validatePage((_a = pages.home) != null ? _a : defaultPage());
2516
+ home.errors.forEach((e) => errors.push({ path: `pages.home.${e.path}`, message: e.message }));
2517
+ const doc = {
2518
+ version: STORE_VERSION,
2519
+ pages: {
2520
+ home: home.page,
2521
+ ...Object.fromEntries(STORE_PAGE_KEYS.map((k) => [k, validateTemplatePage(k, pages[k], `pages.${k}`, errors)]))
2522
+ },
2523
+ chrome: validateGroups(CHROME_FIELDS, (_b = src.chrome) != null ? _b : {}, "chrome", errors),
2524
+ card: validateFields(CARD_FIELDS, (_c = src.card) != null ? _c : {}, "card", errors)
2525
+ };
2526
+ if (doc.chrome.announcement.enabled && !doc.chrome.announcement.text) errors.push({ path: "chrome.announcement.text", message: "is required when the bar is shown" });
2527
+ return { ok: errors.length === 0, errors, doc };
2528
+ }
2223
2529
  export {
2224
2530
  Audiences,
2225
2531
  AuthShell,
@@ -2252,6 +2558,7 @@ export {
2252
2558
  SectionIntro,
2253
2559
  SmartLink,
2254
2560
  TENANT_BLOCKS,
2561
+ TemplateRegion,
2255
2562
  TenantPage,
2256
2563
  TextLink,
2257
2564
  Wordmark,
@@ -2270,5 +2577,6 @@ export {
2270
2577
  useLanding,
2271
2578
  useTapReveal,
2272
2579
  validatePage,
2580
+ validateStore,
2273
2581
  validateTheme
2274
2582
  };