@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/MIGRATION.md +14 -0
- package/README.md +26 -0
- package/dist/index.cjs +323 -15
- package/dist/index.js +323 -15
- package/dist/schema.cjs +324 -3
- package/dist/schema.js +304 -3
- package/index.d.ts +15 -1
- package/package.json +1 -1
- package/schema.d.ts +50 -0
package/MIGRATION.md
CHANGED
|
@@ -159,3 +159,17 @@ Nothing changes for products that don't use it. The server half, `createPlatform
|
|
|
159
159
|
- Credential-custodian "Reveal URL" with a reason, audited.
|
|
160
160
|
|
|
161
161
|
**Not in the console yet:** plans and subscriptions, platform settings, support access and domains. Their server endpoints come first (tenancy). The nav shows them only when `GET /me` advertises them.
|
|
162
|
+
|
|
163
|
+
## 0.9.0 — store documents (additive)
|
|
164
|
+
|
|
165
|
+
A store's landing, listing and product pages, its header and footer, and its product card are now one
|
|
166
|
+
version-2 document (`validateStore` from `/schema`). Nothing changes for apps that keep version-1 tenant pages.
|
|
167
|
+
|
|
168
|
+
- To adopt it, run each stored v1 page through `migrateStore(page)`. It becomes `pages.home`, and the listing
|
|
169
|
+
and product pages get their default templates. `validateStore` migrates a v1 page itself, so an app can
|
|
170
|
+
switch over on its next save.
|
|
171
|
+
- Listing and product pages are templates whose blocks are either app **slots** (`filters`, `results`,
|
|
172
|
+
`gallery`, `buy_box`…, rendered by the app through `TemplateRegion`'s `slots`) or ordinary content sections.
|
|
173
|
+
Required slots cannot be hidden or removed.
|
|
174
|
+
- `safeHref` now rejects links containing a backslash, and registry lookups (`blockDef`, `templatePage`) ignore
|
|
175
|
+
inherited keys, so `__proto__` or `constructor` is an unknown type.
|
package/README.md
CHANGED
|
@@ -202,6 +202,32 @@ import '@dreamtree-org/landing-kit/styles.css';
|
|
|
202
202
|
- `newSection(type)` / `sectionDefaults(type)` start a section with valid defaults; `defaultPage({ name, tagline })`
|
|
203
203
|
is a neutral starting page.
|
|
204
204
|
|
|
205
|
+
### Store documents (0.9.0): landing, listing and product pages as one document
|
|
206
|
+
|
|
207
|
+
A store's whole storefront design is one version-2 document, validated with `validateStore` from `/schema`:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
{ version: 2,
|
|
211
|
+
pages: { home: PageDoc /* the landing page above */,
|
|
212
|
+
listing: { template: 'classic' | 'topbar' | 'editorial', settings, blocks },
|
|
213
|
+
product: { template: 'classic' | 'immersive' | 'compact', settings, blocks } },
|
|
214
|
+
chrome: { announcement, header, footer }, // CHROME_FIELDS
|
|
215
|
+
card: { image_ratio, badges, spec_line, compare, quick_add, mrp, hover } } // CARD_FIELDS
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- `STORE_PAGES[page]` lists each page's `settings` (field defs, so a builder generates the form), `templates`
|
|
219
|
+
(setting values + default block order) and `slots`: the commerce blocks the app renders (results, filters,
|
|
220
|
+
gallery, buy box…). `required` slots can be moved, never removed or hidden, so a page always sells.
|
|
221
|
+
- Between the slots a page may place content sections from `CONTENT_BLOCKS` (promo banner, FAQ, USP strip…),
|
|
222
|
+
validated with the same rules as landing-page sections. At most `MAX_BLOCKS` (12) blocks per page.
|
|
223
|
+
- A version-1 landing page passed to `validateStore` is migrated (`migrateStore`): it becomes `pages.home`
|
|
224
|
+
and everything else takes its defaults. `isLegacyPage(doc)` tells them apart.
|
|
225
|
+
- Render a template page with `<TemplateRegion blocks slots theme resolveMedia />`: `slots[type]` is the app's
|
|
226
|
+
component for a commerce block (`{ block, props }`; return null to place it yourself), content sections render
|
|
227
|
+
like `TenantPage`.
|
|
228
|
+
- Helpers: `templatePage(page, template)`, `newBlock(page, type)`, `blockDef(page, type)`, `defaultStore`,
|
|
229
|
+
`defaultChrome`, `defaultCard`, `storeMediaRefs(doc)` (every `{ media_id }` to check before storing), `validateFields`.
|
|
230
|
+
|
|
205
231
|
### `<AuthShell>` & `@dreamtree-org/landing-kit/auth`
|
|
206
232
|
|
|
207
233
|
The shared auth frame for login, signup, password reset, consent and platform-admin sign-in (`desktop [ brand panel 40% | form card ]`, `mobile [ logo · form card · footer · legal links ]`). Import from `@dreamtree-org/landing-kit` or the lightweight `@dreamtree-org/landing-kit/auth` subpath (with `@dreamtree-org/landing-kit/auth.css` or `styles.css`).
|
package/dist/index.cjs
CHANGED
|
@@ -51,6 +51,7 @@ __export(index_exports, {
|
|
|
51
51
|
SectionIntro: () => SectionIntro,
|
|
52
52
|
SmartLink: () => SmartLink,
|
|
53
53
|
TENANT_BLOCKS: () => TENANT_BLOCKS,
|
|
54
|
+
TemplateRegion: () => TemplateRegion,
|
|
54
55
|
TenantPage: () => TenantPage,
|
|
55
56
|
TextLink: () => TextLink,
|
|
56
57
|
Wordmark: () => Wordmark,
|
|
@@ -69,6 +70,7 @@ __export(index_exports, {
|
|
|
69
70
|
useLanding: () => useLanding,
|
|
70
71
|
useTapReveal: () => useTapReveal,
|
|
71
72
|
validatePage: () => validatePage,
|
|
73
|
+
validateStore: () => validateStore,
|
|
72
74
|
validateTheme: () => validateTheme
|
|
73
75
|
});
|
|
74
76
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -349,9 +351,9 @@ function Wordmark({ name }) {
|
|
|
349
351
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dt-lp-wordmark-hi", children: m[2] })
|
|
350
352
|
] });
|
|
351
353
|
}
|
|
352
|
-
function BrandLockup({ brand:
|
|
354
|
+
function BrandLockup({ brand: own2, byline = "by Dreamtree-Org", href = "/" }) {
|
|
353
355
|
const ctx = useLanding();
|
|
354
|
-
const brand =
|
|
356
|
+
const brand = own2 || ctx.brand;
|
|
355
357
|
if (!brand) return null;
|
|
356
358
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("a", { href, className: "dt-lp-brand", "aria-label": `${brand.name}${byline ? ` ${byline}` : ""}, home`, children: [
|
|
357
359
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BrandMark2, { brand }),
|
|
@@ -478,10 +480,10 @@ function DreamtreeFooter({
|
|
|
478
480
|
tagline,
|
|
479
481
|
legal = [],
|
|
480
482
|
year = (/* @__PURE__ */ new Date()).getFullYear(),
|
|
481
|
-
brand:
|
|
483
|
+
brand: own2
|
|
482
484
|
}) {
|
|
483
485
|
const ctx = useLanding();
|
|
484
|
-
const brand =
|
|
486
|
+
const brand = own2 || ctx.brand;
|
|
485
487
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("footer", { className: "dt-lp-footer", children: [
|
|
486
488
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(Wrap, { className: `dt-lp-footer-grid dt-lp-footer-grid--${Math.min(columns.length, 3)}`, children: [
|
|
487
489
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dt-lp-footer-about", children: [
|
|
@@ -648,7 +650,7 @@ function FeatureSection(props) {
|
|
|
648
650
|
] })
|
|
649
651
|
);
|
|
650
652
|
case "intro": {
|
|
651
|
-
const { subtitle, detail, note, glance = [], glanceTitle = "At a glance", link } = props;
|
|
653
|
+
const { subtitle, detail, note, glance = [], glanceTitle = "At a glance", link: link2 } = props;
|
|
652
654
|
return shell(
|
|
653
655
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "dt-lp-intro-grid", children: [
|
|
654
656
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
|
|
@@ -662,7 +664,7 @@ function FeatureSection(props) {
|
|
|
662
664
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { id: `${hid2}-glance`, className: "dt-lp-eyebrow", children: glanceTitle }),
|
|
663
665
|
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Checklist, { items: glance }),
|
|
664
666
|
props.glanceNote ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "dt-lp-note", children: props.glanceNote }) : null,
|
|
665
|
-
|
|
667
|
+
link2 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TextLink, { href: link2.href, className: "dt-lp-mt", children: link2.label }) : null
|
|
666
668
|
] }) : null
|
|
667
669
|
] })
|
|
668
670
|
);
|
|
@@ -823,7 +825,7 @@ function Faq({ id = "faq", eyebrow, title: title2 = "Frequently asked questions"
|
|
|
823
825
|
|
|
824
826
|
// src/DemoCta.jsx
|
|
825
827
|
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
826
|
-
function DemoCta({ id = "demo", eyebrow, title: title2, body, label = "Book a demo", chips, chipsLabel = "Business types", note, link }) {
|
|
828
|
+
function DemoCta({ id = "demo", eyebrow, title: title2, body, label = "Book a demo", chips, chipsLabel = "Business types", note, link: link2 }) {
|
|
827
829
|
return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("section", { id, "aria-labelledby": `${id}-title`, className: "dt-lp-section dt-lp-cta", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Wrap, { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "dt-lp-cta-panel", children: [
|
|
828
830
|
eyebrow ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "dt-lp-eyebrow dt-lp-eyebrow--inverse", children: eyebrow }) : null,
|
|
829
831
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h2", { id: `${id}-title`, className: "dt-lp-h2 dt-lp-h2--cta", children: title2 }),
|
|
@@ -834,7 +836,7 @@ function DemoCta({ id = "demo", eyebrow, title: title2, body, label = "Book a de
|
|
|
834
836
|
note.title ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "dt-lp-h3", children: note.title }) : null,
|
|
835
837
|
note.body ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: note.body }) : null
|
|
836
838
|
] }) : null,
|
|
837
|
-
|
|
839
|
+
link2 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "dt-lp-cta-link", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SmartLink, { href: link2.href, reload: link2.reload, children: link2.label }) }) : null
|
|
838
840
|
] }) }) });
|
|
839
841
|
}
|
|
840
842
|
|
|
@@ -897,17 +899,17 @@ function LandingPage({ brand, content, preview = null, slots = {}, linkComponent
|
|
|
897
899
|
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
898
900
|
function PreviewFrame({
|
|
899
901
|
id = "product",
|
|
900
|
-
heading,
|
|
902
|
+
heading: heading2,
|
|
901
903
|
caption,
|
|
902
904
|
meta,
|
|
903
905
|
badge = "Demo data",
|
|
904
|
-
brand:
|
|
906
|
+
brand: own2,
|
|
905
907
|
children
|
|
906
908
|
}) {
|
|
907
909
|
const ctx = useLanding();
|
|
908
|
-
const brand =
|
|
910
|
+
const brand = own2 || ctx.brand;
|
|
909
911
|
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("section", { id, "aria-labelledby": `${id}-title`, className: "dt-lp-preview", children: [
|
|
910
|
-
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h2", { id: `${id}-title`, className: "dt-lp-sr-only", children:
|
|
912
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h2", { id: `${id}-title`, className: "dt-lp-sr-only", children: heading2 || `${brand ? brand.name : "Product"} overview` }),
|
|
911
913
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Wrap, { children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("figure", { className: "dt-lp-preview-frame", children: [
|
|
912
914
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dt-lp-preview-bar", children: [
|
|
913
915
|
/* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "dt-lp-preview-title", children: [
|
|
@@ -1557,6 +1559,12 @@ var SECTION_TYPES = {
|
|
|
1557
1559
|
var SECTION_TYPE_KEYS = Object.keys(SECTION_TYPES);
|
|
1558
1560
|
var PAGE_VERSION = 1;
|
|
1559
1561
|
var MAX_SECTIONS = 20;
|
|
1562
|
+
function sectionDefaults(type) {
|
|
1563
|
+
const def = SECTION_TYPES[type];
|
|
1564
|
+
if (!def) throw new Error(`Unknown section type "${type}"`);
|
|
1565
|
+
const fill2 = (fields2) => Object.fromEntries(fields2.map((f) => [f.key, f.kind === "list" ? (f.default || []).map((item) => ({ ...fill2(f.of), ...item })) : f.default]));
|
|
1566
|
+
return fill2(def.fields);
|
|
1567
|
+
}
|
|
1560
1568
|
|
|
1561
1569
|
// src/schema/richtext.js
|
|
1562
1570
|
var ALLOWED = /* @__PURE__ */ new Set(["p", "br", "h2", "h3", "h4", "strong", "b", "em", "i", "u", "ul", "ol", "li", "a", "blockquote"]);
|
|
@@ -1577,7 +1585,7 @@ var decode = (s) => s.replace(/&(amp|quot|#39|lt|gt|#x[0-9a-f]+|#\d+);/gi, (m, e
|
|
|
1577
1585
|
});
|
|
1578
1586
|
function safeHref(raw) {
|
|
1579
1587
|
const v = decode(String(raw || "")).replace(/[\u0000- \u007f-\u009f]/g, "");
|
|
1580
|
-
if (!v) return null;
|
|
1588
|
+
if (!v || v.includes("\\")) return null;
|
|
1581
1589
|
if (/^(https?:\/\/|mailto:|tel:)/i.test(v)) return v;
|
|
1582
1590
|
if (/^\/(?!\/)/.test(v) || v.startsWith("#")) return v;
|
|
1583
1591
|
return null;
|
|
@@ -1701,6 +1709,9 @@ function field(f, value, path, errors) {
|
|
|
1701
1709
|
throw new Error(`Unknown field kind "${f.kind}" (${path})`);
|
|
1702
1710
|
}
|
|
1703
1711
|
}
|
|
1712
|
+
function validateFields(defs, obj, path, errors) {
|
|
1713
|
+
return fields(defs, obj, path, errors);
|
|
1714
|
+
}
|
|
1704
1715
|
function fields(defs, obj, path, errors) {
|
|
1705
1716
|
const src = obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {};
|
|
1706
1717
|
if (obj != null && src !== obj) errors.push({ path, message: "must be an object" });
|
|
@@ -1738,6 +1749,18 @@ function validatePage(input) {
|
|
|
1738
1749
|
});
|
|
1739
1750
|
return { ok: errors.length === 0, errors, page: { version: PAGE_VERSION, sections } };
|
|
1740
1751
|
}
|
|
1752
|
+
function defaultPage({ name = "Our store", tagline = "" } = {}) {
|
|
1753
|
+
const hero = sectionDefaults("hero_slider");
|
|
1754
|
+
hero.slides = [{ ...hero.slides[0], heading: String(name).slice(0, 90), text: String(tagline || "").slice(0, 220), cta_label: "Shop now", cta_href: "/" }];
|
|
1755
|
+
return validatePage({
|
|
1756
|
+
version: PAGE_VERSION,
|
|
1757
|
+
sections: [
|
|
1758
|
+
{ id: "hero", type: "hero_slider", props: { ...hero, autoplay: false } },
|
|
1759
|
+
{ id: "featured", type: "featured", props: sectionDefaults("featured") },
|
|
1760
|
+
{ id: "categories", type: "categories", props: sectionDefaults("categories") }
|
|
1761
|
+
]
|
|
1762
|
+
}).page;
|
|
1763
|
+
}
|
|
1741
1764
|
|
|
1742
1765
|
// src/schema/color.js
|
|
1743
1766
|
var HEX_RE = /^#([0-9a-f]{6})$/i;
|
|
@@ -2112,12 +2135,12 @@ function SlotSection({ id, title: title2, intro: intro2, tone: tone2, slot, ...p
|
|
|
2112
2135
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "dt-lp-slot-body", children: body })
|
|
2113
2136
|
] });
|
|
2114
2137
|
}
|
|
2115
|
-
function PromoBanner({ id, eyebrow, heading, text, cta_label: label, cta_href: href, image, style = "brand", media }) {
|
|
2138
|
+
function PromoBanner({ id, eyebrow, heading: heading2, text, cta_label: label, cta_href: href, image, style = "brand", media }) {
|
|
2116
2139
|
const src = media(image, "wide");
|
|
2117
2140
|
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("section", { id, "aria-labelledby": hid(id), className: "dt-lp-section dt-lp-promo-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { className: "dt-lp-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: `dt-lp-promo dt-lp-promo--${style}${src ? " dt-lp-promo--image" : ""}`, children: [
|
|
2118
2141
|
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "dt-lp-promo-copy", children: [
|
|
2119
2142
|
eyebrow ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "dt-lp-eyebrow dt-lp-eyebrow--inverse", children: eyebrow }) : null,
|
|
2120
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("h2", { id: hid(id), className: "dt-lp-h2 dt-lp-promo-heading", children:
|
|
2143
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("h2", { id: hid(id), className: "dt-lp-h2 dt-lp-promo-heading", children: heading2 }),
|
|
2121
2144
|
text ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("p", { className: "dt-lp-promo-text", children: text }) : null,
|
|
2122
2145
|
label && href ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SmartLink, { href, className: "dt-lp-btn dt-lp-btn--lg dt-lp-btn--inverse", children: label }) : null
|
|
2123
2146
|
] }),
|
|
@@ -2293,3 +2316,288 @@ function TenantPage({
|
|
|
2293
2316
|
return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(SectionBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Block, { ...s.props, id: `s-${s.id}`, first: i === 0 && headingStart === 1, media, slot: slots[s.type], onSubscribe }) }, s.id);
|
|
2294
2317
|
}) }) });
|
|
2295
2318
|
}
|
|
2319
|
+
|
|
2320
|
+
// src/tenant/TemplateRegion.jsx
|
|
2321
|
+
var import_react12 = require("react");
|
|
2322
|
+
var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
2323
|
+
var noMedia2 = () => null;
|
|
2324
|
+
function TemplateRegion({
|
|
2325
|
+
blocks = [],
|
|
2326
|
+
slots = {},
|
|
2327
|
+
theme,
|
|
2328
|
+
resolveMedia = noMedia2,
|
|
2329
|
+
linkComponent = null,
|
|
2330
|
+
storeName = "",
|
|
2331
|
+
className = ""
|
|
2332
|
+
}) {
|
|
2333
|
+
const vars = (0, import_react12.useMemo)(() => themeVars(theme), [theme]);
|
|
2334
|
+
const ctx = (0, import_react12.useMemo)(() => ({ brand: { id: "tenant", name: storeName, primary: vars.theme.primary }, linkComponent, onBookDemo: null, home: false }), [storeName, vars.theme.primary, linkComponent]);
|
|
2335
|
+
const media = (ref, variant) => ref && ref.media_id != null ? resolveMedia(ref, variant) : null;
|
|
2336
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react12.Fragment, { children: blocks.filter((b) => b && b.enabled !== false).map((b) => {
|
|
2337
|
+
const Slot = slots[b.type];
|
|
2338
|
+
if (Slot) {
|
|
2339
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SectionBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Slot, { block: b, props: b.props || {} }) }, b.id);
|
|
2340
|
+
}
|
|
2341
|
+
const Block = TENANT_BLOCKS[b.type];
|
|
2342
|
+
if (!Block || Block === TENANT_BLOCKS.featured) return null;
|
|
2343
|
+
return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SectionBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(LandingProvider, { value: ctx, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: `dt-landing dt-lp-tenant dt-lp-region ${className}`.trim(), style: vars.style, "data-block": b.type, children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(Block, { ...b.props, id: `b-${b.id}`, first: false, media, slot: null, onSubscribe: null }) }) }) }, b.id);
|
|
2344
|
+
}) });
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// src/schema/store.js
|
|
2348
|
+
var STORE_VERSION = 2;
|
|
2349
|
+
var MAX_BLOCKS = 12;
|
|
2350
|
+
var ID_RE2 = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
2351
|
+
var opt = (...pairs) => pairs.map(([value, label]) => ({ value, label }));
|
|
2352
|
+
var sel = (key, label, options, d) => ({ key, kind: "select", label, options, default: d });
|
|
2353
|
+
var bool = (key, label, d) => ({ key, kind: "bool", label, default: d });
|
|
2354
|
+
var heading = (d, max = 90) => ({ key: "title", kind: "text", label: "Heading", max, default: d });
|
|
2355
|
+
var CONTENT_BLOCKS = ["promo_banner", "usp_strip", "trust_badges", "cta_cards", "faq", "rich_text", "testimonials", "brand_strip", "gallery"];
|
|
2356
|
+
var LISTING_SLOTS = {
|
|
2357
|
+
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)] },
|
|
2358
|
+
toolbar: { label: "Toolbar and sort", description: "Result count, sort and the filters in use.", fields: [] },
|
|
2359
|
+
filters: { label: "Filters", description: "Price, brand and spec filters.", required: true, fields: [] },
|
|
2360
|
+
results: { label: "Results grid", description: "The products, with paging.", required: true, fields: [] }
|
|
2361
|
+
};
|
|
2362
|
+
var PRODUCT_SLOTS = {
|
|
2363
|
+
gallery: { label: "Gallery", description: "Photos of the product.", required: true, fields: [] },
|
|
2364
|
+
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)] },
|
|
2365
|
+
highlights: { label: "Highlights", description: "The product's headline numbers and selling points.", fields: [heading("Highlights")] },
|
|
2366
|
+
delivery: { label: "Delivery check", description: "PIN code delivery estimate.", fields: [] },
|
|
2367
|
+
seller: { label: "Seller", description: "Who sells and services the product.", fields: [] },
|
|
2368
|
+
inspection: { label: "Inspection report", description: "Checks, grade and condition notes.", fields: [heading("Inspection report"), bool("expanded", "Open by default", false)] },
|
|
2369
|
+
battery: { label: "Battery", description: "Battery health, where the product has one.", fields: [] },
|
|
2370
|
+
included: { label: "In the box", description: "What ships with the product.", fields: [] },
|
|
2371
|
+
specs: { label: "Specifications", description: "The full specification table.", fields: [heading("Specifications")] },
|
|
2372
|
+
policies: { label: "Warranty and returns", description: "The store's warranty and return terms.", fields: [heading("Warranty and returns")] },
|
|
2373
|
+
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")] }
|
|
2374
|
+
};
|
|
2375
|
+
var SORTS = opt(["relevance", "Recommended"], ["newest", "Newest first"], ["price_asc", "Price: low to high"], ["price_desc", "Price: high to low"], ["discount", "Biggest discount"]);
|
|
2376
|
+
var STORE_PAGES = {
|
|
2377
|
+
listing: {
|
|
2378
|
+
label: "Listing page",
|
|
2379
|
+
slots: LISTING_SLOTS,
|
|
2380
|
+
settings: [
|
|
2381
|
+
sel("columns", "Columns on desktop", opt(["3", "3"], ["4", "4"]), "3"),
|
|
2382
|
+
sel("columns_mobile", "Columns on phones", opt(["1", "1"], ["2", "2"]), "2"),
|
|
2383
|
+
sel("page_size", "Products per page", opt(["12", "12"], ["24", "24"], ["36", "36"]), "12"),
|
|
2384
|
+
sel("default_sort", "Default sort", SORTS, "relevance"),
|
|
2385
|
+
sel("filters", "Filters show as", opt(["sidebar", "Sidebar"], ["topbar", "Bar above the results"], ["drawer", "Button that opens a panel"]), "sidebar"),
|
|
2386
|
+
bool("active_chips", "Show the filters in use as chips", true),
|
|
2387
|
+
sel("paging", "Paging", opt(["pages", "Page numbers"], ["load_more", "Load more button"]), "pages")
|
|
2388
|
+
],
|
|
2389
|
+
templates: {
|
|
2390
|
+
classic: { label: "Classic", description: "Filters in a sidebar beside a product grid.", settings: {}, blocks: ["collection_header", "toolbar", "filters", "results"] },
|
|
2391
|
+
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"] },
|
|
2392
|
+
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"] }
|
|
2393
|
+
},
|
|
2394
|
+
default: "classic"
|
|
2395
|
+
},
|
|
2396
|
+
product: {
|
|
2397
|
+
label: "Product page",
|
|
2398
|
+
slots: PRODUCT_SLOTS,
|
|
2399
|
+
settings: [
|
|
2400
|
+
sel("gallery", "Gallery", opt(["thumbs_side", "Thumbnails beside the photo"], ["thumbs_below", "Thumbnails below the photo"], ["mosaic", "Full-width mosaic"]), "thumbs_side"),
|
|
2401
|
+
bool("sticky_buy_bar", "Sticky buy bar on phones", true),
|
|
2402
|
+
sel("spec_tiles", "Key spec tiles", opt(["0", "None"], ["4", "4"], ["6", "6"]), "4"),
|
|
2403
|
+
bool("breadcrumbs", "Show breadcrumbs", true)
|
|
2404
|
+
],
|
|
2405
|
+
templates: {
|
|
2406
|
+
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"] },
|
|
2407
|
+
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"] },
|
|
2408
|
+
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"] }
|
|
2409
|
+
},
|
|
2410
|
+
default: "classic"
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
var STORE_PAGE_KEYS = Object.keys(STORE_PAGES);
|
|
2414
|
+
var link = [
|
|
2415
|
+
{ key: "label", kind: "text", label: "Label", max: 30, required: true, default: "" },
|
|
2416
|
+
{ key: "href", kind: "url", label: "Link", required: true, default: "" }
|
|
2417
|
+
];
|
|
2418
|
+
var CHROME_FIELDS = [
|
|
2419
|
+
{
|
|
2420
|
+
key: "announcement",
|
|
2421
|
+
kind: "group",
|
|
2422
|
+
label: "Announcement bar",
|
|
2423
|
+
fields: [
|
|
2424
|
+
bool("enabled", "Show the announcement bar", false),
|
|
2425
|
+
{ key: "text", kind: "text", label: "Message", max: 120, default: "" },
|
|
2426
|
+
{ key: "href", kind: "url", label: "Link", default: "" },
|
|
2427
|
+
sel("tone", "Colour", opt(["brand", "Brand"], ["accent", "Accent"], ["dark", "Dark"]), "brand")
|
|
2428
|
+
]
|
|
2429
|
+
},
|
|
2430
|
+
{
|
|
2431
|
+
key: "header",
|
|
2432
|
+
kind: "group",
|
|
2433
|
+
label: "Header",
|
|
2434
|
+
fields: [
|
|
2435
|
+
{ key: "utility_text", kind: "text", label: "Strip above the header", max: 80, default: "" },
|
|
2436
|
+
bool("show_search", "Search box", true),
|
|
2437
|
+
bool("category_nav", "Menu of the categories you sell", true),
|
|
2438
|
+
{ key: "nav_links", kind: "list", label: "Extra menu links", max: 8, itemLabel: "label", of: link, default: [] },
|
|
2439
|
+
bool("sticky", "Keep the header on screen while scrolling", true)
|
|
2440
|
+
]
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
key: "footer",
|
|
2444
|
+
kind: "group",
|
|
2445
|
+
label: "Footer",
|
|
2446
|
+
fields: [
|
|
2447
|
+
{ key: "about", kind: "textarea", label: "About the store", max: 240, default: "" },
|
|
2448
|
+
{
|
|
2449
|
+
key: "columns",
|
|
2450
|
+
kind: "list",
|
|
2451
|
+
label: "Link columns",
|
|
2452
|
+
max: 4,
|
|
2453
|
+
itemLabel: "title",
|
|
2454
|
+
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: [] }],
|
|
2455
|
+
default: []
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
key: "socials",
|
|
2459
|
+
kind: "list",
|
|
2460
|
+
label: "Social profiles",
|
|
2461
|
+
max: 6,
|
|
2462
|
+
itemLabel: "network",
|
|
2463
|
+
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: "" }],
|
|
2464
|
+
default: []
|
|
2465
|
+
},
|
|
2466
|
+
{ key: "note", kind: "text", label: "Small print (GSTIN, address)", max: 160, default: "" }
|
|
2467
|
+
]
|
|
2468
|
+
}
|
|
2469
|
+
];
|
|
2470
|
+
var CARD_FIELDS = [
|
|
2471
|
+
sel("image_ratio", "Photo shape", opt(["4:3", "Landscape 4:3"], ["1:1", "Square"], ["3:4", "Portrait 3:4"]), "4:3"),
|
|
2472
|
+
bool("badges", "Condition and discount badges", true),
|
|
2473
|
+
bool("spec_line", "Key specs line", true),
|
|
2474
|
+
bool("compare", "Compare checkbox", true),
|
|
2475
|
+
bool("quick_add", "Add to cart button", true),
|
|
2476
|
+
bool("mrp", "Show the MRP struck through", true),
|
|
2477
|
+
sel("hover", "On hover", opt(["none", "Nothing"], ["zoom", "Zoom the photo"], ["second_image", "Show the second photo"]), "zoom")
|
|
2478
|
+
];
|
|
2479
|
+
function validateGroups(defs, obj, path, errors) {
|
|
2480
|
+
const src = obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {};
|
|
2481
|
+
if (obj != null && src !== obj) errors.push({ path, message: "must be an object" });
|
|
2482
|
+
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]]));
|
|
2483
|
+
}
|
|
2484
|
+
var fill = (defs) => Object.fromEntries(defs.map((f) => [f.key, f.kind === "group" ? fill(f.fields) : f.kind === "list" ? [...f.default || []] : f.default]));
|
|
2485
|
+
var defaultChrome = () => fill(CHROME_FIELDS);
|
|
2486
|
+
var defaultCard = () => fill(CARD_FIELDS);
|
|
2487
|
+
function blockDef(pageKey, type) {
|
|
2488
|
+
const page = own(STORE_PAGES, pageKey) ? STORE_PAGES[pageKey] : null;
|
|
2489
|
+
if (!page) return null;
|
|
2490
|
+
if (own(page.slots, type)) return { ...page.slots[type], slot: true };
|
|
2491
|
+
if (CONTENT_BLOCKS.includes(type) && own(SECTION_TYPES, type)) return { ...SECTION_TYPES[type], slot: false };
|
|
2492
|
+
return null;
|
|
2493
|
+
}
|
|
2494
|
+
var own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
2495
|
+
var blockId = (type) => type.replace(/_/g, "-");
|
|
2496
|
+
var blockDefaults = (pageKey, type) => {
|
|
2497
|
+
const def = blockDef(pageKey, type);
|
|
2498
|
+
return def.slot ? fill(def.fields) : sectionDefaults(type);
|
|
2499
|
+
};
|
|
2500
|
+
function templatePage(pageKey, template) {
|
|
2501
|
+
const page = STORE_PAGES[pageKey];
|
|
2502
|
+
const key = own(page.templates, template) ? template : page.default;
|
|
2503
|
+
const tpl = page.templates[key];
|
|
2504
|
+
const settings = { ...fill(page.settings), ...tpl.settings };
|
|
2505
|
+
const blocks = tpl.blocks.map((type) => ({ id: blockId(type), type, enabled: true, props: blockDefaults(pageKey, type) }));
|
|
2506
|
+
return { template: key, settings, blocks };
|
|
2507
|
+
}
|
|
2508
|
+
function validateTemplatePage(pageKey, input, path, errors) {
|
|
2509
|
+
var _a, _b;
|
|
2510
|
+
const page = STORE_PAGES[pageKey];
|
|
2511
|
+
const src = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
2512
|
+
let template = (_a = src.template) != null ? _a : page.default;
|
|
2513
|
+
if (!own(page.templates, template)) {
|
|
2514
|
+
errors.push({ path: `${path}.template`, message: `must be one of ${Object.keys(page.templates).join(", ")}` });
|
|
2515
|
+
template = page.default;
|
|
2516
|
+
}
|
|
2517
|
+
const tpl = page.templates[template];
|
|
2518
|
+
const settingDefs = page.settings.map((f) => tpl.settings[f.key] === void 0 ? f : { ...f, default: tpl.settings[f.key] });
|
|
2519
|
+
const settings = validateFields(settingDefs, (_b = src.settings) != null ? _b : {}, `${path}.settings`, errors);
|
|
2520
|
+
const list = Array.isArray(src.blocks) ? src.blocks : null;
|
|
2521
|
+
if (src.blocks != null && !list) errors.push({ path: `${path}.blocks`, message: "must be a list" });
|
|
2522
|
+
const raw = list != null ? list : templatePage(pageKey, template).blocks;
|
|
2523
|
+
if (raw.length > MAX_BLOCKS) errors.push({ path: `${path}.blocks`, message: `can have at most ${MAX_BLOCKS}` });
|
|
2524
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2525
|
+
const slotsSeen = /* @__PURE__ */ new Set();
|
|
2526
|
+
const blocks = [];
|
|
2527
|
+
raw.slice(0, MAX_BLOCKS).forEach((b, i) => {
|
|
2528
|
+
var _a2, _b2;
|
|
2529
|
+
const at = `${path}.blocks[${i}]`;
|
|
2530
|
+
if (!b || typeof b !== "object") {
|
|
2531
|
+
errors.push({ path: at, message: "must be an object" });
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
const def = blockDef(pageKey, b.type);
|
|
2535
|
+
if (!def) {
|
|
2536
|
+
errors.push({ path: `${at}.type`, message: `unknown block "${b.type}" on the ${page.label.toLowerCase()}` });
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
const id = String((_a2 = b.id) != null ? _a2 : "");
|
|
2540
|
+
if (!ID_RE2.test(id)) errors.push({ path: `${at}.id`, message: "must be 1\u201340 lowercase letters, digits or dashes" });
|
|
2541
|
+
else if (ids.has(id)) errors.push({ path: `${at}.id`, message: `duplicate id "${id}"` });
|
|
2542
|
+
ids.add(id);
|
|
2543
|
+
if (def.slot) {
|
|
2544
|
+
if (slotsSeen.has(b.type)) {
|
|
2545
|
+
errors.push({ path: `${at}.type`, message: `"${def.label}" can appear only once` });
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
slotsSeen.add(b.type);
|
|
2549
|
+
}
|
|
2550
|
+
let enabled = b.enabled === void 0 ? true : b.enabled;
|
|
2551
|
+
if (typeof enabled !== "boolean") {
|
|
2552
|
+
errors.push({ path: `${at}.enabled`, message: "must be true or false" });
|
|
2553
|
+
enabled = true;
|
|
2554
|
+
}
|
|
2555
|
+
if (def.required && !enabled) {
|
|
2556
|
+
errors.push({ path: `${at}.enabled`, message: `"${def.label}" is required and cannot be hidden` });
|
|
2557
|
+
enabled = true;
|
|
2558
|
+
}
|
|
2559
|
+
blocks.push({ id, type: b.type, enabled, props: validateFields(def.fields, (_b2 = b.props) != null ? _b2 : {}, `${at}.props`, errors) });
|
|
2560
|
+
});
|
|
2561
|
+
for (const [type, def] of Object.entries(page.slots)) {
|
|
2562
|
+
if (def.required && !slotsSeen.has(type)) {
|
|
2563
|
+
errors.push({ path: `${path}.blocks`, message: `"${def.label}" is required` });
|
|
2564
|
+
blocks.push({ id: ids.has(blockId(type)) ? `${blockId(type)}-1` : blockId(type), type, enabled: true, props: blockDefaults(pageKey, type) });
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return { template, settings, blocks };
|
|
2568
|
+
}
|
|
2569
|
+
function migrateStore(input) {
|
|
2570
|
+
const home = input && typeof input === "object" && Array.isArray(input.sections) ? input : null;
|
|
2571
|
+
return {
|
|
2572
|
+
version: STORE_VERSION,
|
|
2573
|
+
pages: { home: home ? { version: 1, sections: home.sections } : defaultPage(), listing: templatePage("listing"), product: templatePage("product") },
|
|
2574
|
+
chrome: defaultChrome(),
|
|
2575
|
+
card: defaultCard()
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
var isLegacyPage = (doc) => !!doc && typeof doc === "object" && doc.version !== STORE_VERSION && Array.isArray(doc.sections);
|
|
2579
|
+
function validateStore(input) {
|
|
2580
|
+
var _a, _b, _c;
|
|
2581
|
+
const errors = [];
|
|
2582
|
+
const src0 = isLegacyPage(input) ? migrateStore(input) : input;
|
|
2583
|
+
const src = src0 && typeof src0 === "object" && !Array.isArray(src0) ? src0 : null;
|
|
2584
|
+
if (!src) {
|
|
2585
|
+
const doc2 = migrateStore(null);
|
|
2586
|
+
return { ok: false, errors: [{ path: "doc", message: "must be an object" }], doc: doc2 };
|
|
2587
|
+
}
|
|
2588
|
+
if (src.version != null && src.version !== STORE_VERSION) errors.push({ path: "version", message: `must be ${STORE_VERSION}` });
|
|
2589
|
+
const pages = src.pages && typeof src.pages === "object" ? src.pages : {};
|
|
2590
|
+
const home = validatePage((_a = pages.home) != null ? _a : defaultPage());
|
|
2591
|
+
home.errors.forEach((e) => errors.push({ path: `pages.home.${e.path}`, message: e.message }));
|
|
2592
|
+
const doc = {
|
|
2593
|
+
version: STORE_VERSION,
|
|
2594
|
+
pages: {
|
|
2595
|
+
home: home.page,
|
|
2596
|
+
...Object.fromEntries(STORE_PAGE_KEYS.map((k) => [k, validateTemplatePage(k, pages[k], `pages.${k}`, errors)]))
|
|
2597
|
+
},
|
|
2598
|
+
chrome: validateGroups(CHROME_FIELDS, (_b = src.chrome) != null ? _b : {}, "chrome", errors),
|
|
2599
|
+
card: validateFields(CARD_FIELDS, (_c = src.card) != null ? _c : {}, "card", errors)
|
|
2600
|
+
};
|
|
2601
|
+
if (doc.chrome.announcement.enabled && !doc.chrome.announcement.text) errors.push({ path: "chrome.announcement.text", message: "is required when the bar is shown" });
|
|
2602
|
+
return { ok: errors.length === 0, errors, doc };
|
|
2603
|
+
}
|