@dreamtree-org/landing-kit 0.8.0 → 0.10.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 +30 -0
- package/PLATFORM-API.md +17 -13
- package/README.md +27 -1
- package/ai-skills/landing-kit.md +7 -6
- package/dist/index.cjs +323 -15
- package/dist/index.js +323 -15
- package/dist/platform.cjs +1094 -398
- package/dist/platform.js +1092 -396
- package/dist/schema.cjs +324 -3
- package/dist/schema.js +304 -3
- package/index.d.ts +15 -1
- package/package.json +1 -1
- package/platform.d.ts +37 -3
- package/schema.d.ts +50 -0
package/dist/schema.js
CHANGED
|
@@ -267,8 +267,8 @@ var MAX_SECTIONS = 20;
|
|
|
267
267
|
function sectionDefaults(type) {
|
|
268
268
|
const def = SECTION_TYPES[type];
|
|
269
269
|
if (!def) throw new Error(`Unknown section type "${type}"`);
|
|
270
|
-
const
|
|
271
|
-
return
|
|
270
|
+
const fill2 = (fields2) => Object.fromEntries(fields2.map((f) => [f.key, f.kind === "list" ? (f.default || []).map((item) => ({ ...fill2(f.of), ...item })) : f.default]));
|
|
271
|
+
return fill2(def.fields);
|
|
272
272
|
}
|
|
273
273
|
var seq = 0;
|
|
274
274
|
function newSectionId(type) {
|
|
@@ -298,7 +298,7 @@ var decode = (s) => s.replace(/&(amp|quot|#39|lt|gt|#x[0-9a-f]+|#\d+);/gi, (m, e
|
|
|
298
298
|
});
|
|
299
299
|
function safeHref(raw) {
|
|
300
300
|
const v = decode(String(raw || "")).replace(/[\u0000- \u007f-\u009f]/g, "");
|
|
301
|
-
if (!v) return null;
|
|
301
|
+
if (!v || v.includes("\\")) return null;
|
|
302
302
|
if (/^(https?:\/\/|mailto:|tel:)/i.test(v)) return v;
|
|
303
303
|
if (/^\/(?!\/)/.test(v) || v.startsWith("#")) return v;
|
|
304
304
|
return null;
|
|
@@ -422,6 +422,9 @@ function field(f, value, path, errors) {
|
|
|
422
422
|
throw new Error(`Unknown field kind "${f.kind}" (${path})`);
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
|
+
function validateFields(defs, obj, path, errors) {
|
|
426
|
+
return fields(defs, obj, path, errors);
|
|
427
|
+
}
|
|
425
428
|
function fields(defs, obj, path, errors) {
|
|
426
429
|
const src = obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {};
|
|
427
430
|
if (obj != null && src !== obj) errors.push({ path, message: "must be an object" });
|
|
@@ -645,24 +648,318 @@ function themeVars(input) {
|
|
|
645
648
|
}
|
|
646
649
|
};
|
|
647
650
|
}
|
|
651
|
+
|
|
652
|
+
// src/schema/store.js
|
|
653
|
+
var STORE_VERSION = 2;
|
|
654
|
+
var MAX_BLOCKS = 12;
|
|
655
|
+
var ID_RE2 = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
656
|
+
var opt = (...pairs) => pairs.map(([value, label]) => ({ value, label }));
|
|
657
|
+
var sel = (key, label, options, d) => ({ key, kind: "select", label, options, default: d });
|
|
658
|
+
var bool = (key, label, d) => ({ key, kind: "bool", label, default: d });
|
|
659
|
+
var heading = (d, max = 90) => ({ key: "title", kind: "text", label: "Heading", max, default: d });
|
|
660
|
+
var CONTENT_BLOCKS = ["promo_banner", "usp_strip", "trust_badges", "cta_cards", "faq", "rich_text", "testimonials", "brand_strip", "gallery"];
|
|
661
|
+
var LISTING_SLOTS = {
|
|
662
|
+
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)] },
|
|
663
|
+
toolbar: { label: "Toolbar and sort", description: "Result count, sort and the filters in use.", fields: [] },
|
|
664
|
+
filters: { label: "Filters", description: "Price, brand and spec filters.", required: true, fields: [] },
|
|
665
|
+
results: { label: "Results grid", description: "The products, with paging.", required: true, fields: [] }
|
|
666
|
+
};
|
|
667
|
+
var PRODUCT_SLOTS = {
|
|
668
|
+
gallery: { label: "Gallery", description: "Photos of the product.", required: true, fields: [] },
|
|
669
|
+
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)] },
|
|
670
|
+
highlights: { label: "Highlights", description: "The product's headline numbers and selling points.", fields: [heading("Highlights")] },
|
|
671
|
+
delivery: { label: "Delivery check", description: "PIN code delivery estimate.", fields: [] },
|
|
672
|
+
seller: { label: "Seller", description: "Who sells and services the product.", fields: [] },
|
|
673
|
+
inspection: { label: "Inspection report", description: "Checks, grade and condition notes.", fields: [heading("Inspection report"), bool("expanded", "Open by default", false)] },
|
|
674
|
+
battery: { label: "Battery", description: "Battery health, where the product has one.", fields: [] },
|
|
675
|
+
included: { label: "In the box", description: "What ships with the product.", fields: [] },
|
|
676
|
+
specs: { label: "Specifications", description: "The full specification table.", fields: [heading("Specifications")] },
|
|
677
|
+
policies: { label: "Warranty and returns", description: "The store's warranty and return terms.", fields: [heading("Warranty and returns")] },
|
|
678
|
+
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")] }
|
|
679
|
+
};
|
|
680
|
+
var SORTS = opt(["relevance", "Recommended"], ["newest", "Newest first"], ["price_asc", "Price: low to high"], ["price_desc", "Price: high to low"], ["discount", "Biggest discount"]);
|
|
681
|
+
var STORE_PAGES = {
|
|
682
|
+
listing: {
|
|
683
|
+
label: "Listing page",
|
|
684
|
+
slots: LISTING_SLOTS,
|
|
685
|
+
settings: [
|
|
686
|
+
sel("columns", "Columns on desktop", opt(["3", "3"], ["4", "4"]), "3"),
|
|
687
|
+
sel("columns_mobile", "Columns on phones", opt(["1", "1"], ["2", "2"]), "2"),
|
|
688
|
+
sel("page_size", "Products per page", opt(["12", "12"], ["24", "24"], ["36", "36"]), "12"),
|
|
689
|
+
sel("default_sort", "Default sort", SORTS, "relevance"),
|
|
690
|
+
sel("filters", "Filters show as", opt(["sidebar", "Sidebar"], ["topbar", "Bar above the results"], ["drawer", "Button that opens a panel"]), "sidebar"),
|
|
691
|
+
bool("active_chips", "Show the filters in use as chips", true),
|
|
692
|
+
sel("paging", "Paging", opt(["pages", "Page numbers"], ["load_more", "Load more button"]), "pages")
|
|
693
|
+
],
|
|
694
|
+
templates: {
|
|
695
|
+
classic: { label: "Classic", description: "Filters in a sidebar beside a product grid.", settings: {}, blocks: ["collection_header", "toolbar", "filters", "results"] },
|
|
696
|
+
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"] },
|
|
697
|
+
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"] }
|
|
698
|
+
},
|
|
699
|
+
default: "classic"
|
|
700
|
+
},
|
|
701
|
+
product: {
|
|
702
|
+
label: "Product page",
|
|
703
|
+
slots: PRODUCT_SLOTS,
|
|
704
|
+
settings: [
|
|
705
|
+
sel("gallery", "Gallery", opt(["thumbs_side", "Thumbnails beside the photo"], ["thumbs_below", "Thumbnails below the photo"], ["mosaic", "Full-width mosaic"]), "thumbs_side"),
|
|
706
|
+
bool("sticky_buy_bar", "Sticky buy bar on phones", true),
|
|
707
|
+
sel("spec_tiles", "Key spec tiles", opt(["0", "None"], ["4", "4"], ["6", "6"]), "4"),
|
|
708
|
+
bool("breadcrumbs", "Show breadcrumbs", true)
|
|
709
|
+
],
|
|
710
|
+
templates: {
|
|
711
|
+
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"] },
|
|
712
|
+
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"] },
|
|
713
|
+
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"] }
|
|
714
|
+
},
|
|
715
|
+
default: "classic"
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
var STORE_PAGE_KEYS = Object.keys(STORE_PAGES);
|
|
719
|
+
var link = [
|
|
720
|
+
{ key: "label", kind: "text", label: "Label", max: 30, required: true, default: "" },
|
|
721
|
+
{ key: "href", kind: "url", label: "Link", required: true, default: "" }
|
|
722
|
+
];
|
|
723
|
+
var CHROME_FIELDS = [
|
|
724
|
+
{
|
|
725
|
+
key: "announcement",
|
|
726
|
+
kind: "group",
|
|
727
|
+
label: "Announcement bar",
|
|
728
|
+
fields: [
|
|
729
|
+
bool("enabled", "Show the announcement bar", false),
|
|
730
|
+
{ key: "text", kind: "text", label: "Message", max: 120, default: "" },
|
|
731
|
+
{ key: "href", kind: "url", label: "Link", default: "" },
|
|
732
|
+
sel("tone", "Colour", opt(["brand", "Brand"], ["accent", "Accent"], ["dark", "Dark"]), "brand")
|
|
733
|
+
]
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
key: "header",
|
|
737
|
+
kind: "group",
|
|
738
|
+
label: "Header",
|
|
739
|
+
fields: [
|
|
740
|
+
{ key: "utility_text", kind: "text", label: "Strip above the header", max: 80, default: "" },
|
|
741
|
+
bool("show_search", "Search box", true),
|
|
742
|
+
bool("category_nav", "Menu of the categories you sell", true),
|
|
743
|
+
{ key: "nav_links", kind: "list", label: "Extra menu links", max: 8, itemLabel: "label", of: link, default: [] },
|
|
744
|
+
bool("sticky", "Keep the header on screen while scrolling", true)
|
|
745
|
+
]
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
key: "footer",
|
|
749
|
+
kind: "group",
|
|
750
|
+
label: "Footer",
|
|
751
|
+
fields: [
|
|
752
|
+
{ key: "about", kind: "textarea", label: "About the store", max: 240, default: "" },
|
|
753
|
+
{
|
|
754
|
+
key: "columns",
|
|
755
|
+
kind: "list",
|
|
756
|
+
label: "Link columns",
|
|
757
|
+
max: 4,
|
|
758
|
+
itemLabel: "title",
|
|
759
|
+
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: [] }],
|
|
760
|
+
default: []
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
key: "socials",
|
|
764
|
+
kind: "list",
|
|
765
|
+
label: "Social profiles",
|
|
766
|
+
max: 6,
|
|
767
|
+
itemLabel: "network",
|
|
768
|
+
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: "" }],
|
|
769
|
+
default: []
|
|
770
|
+
},
|
|
771
|
+
{ key: "note", kind: "text", label: "Small print (GSTIN, address)", max: 160, default: "" }
|
|
772
|
+
]
|
|
773
|
+
}
|
|
774
|
+
];
|
|
775
|
+
var CARD_FIELDS = [
|
|
776
|
+
sel("image_ratio", "Photo shape", opt(["4:3", "Landscape 4:3"], ["1:1", "Square"], ["3:4", "Portrait 3:4"]), "4:3"),
|
|
777
|
+
bool("badges", "Condition and discount badges", true),
|
|
778
|
+
bool("spec_line", "Key specs line", true),
|
|
779
|
+
bool("compare", "Compare checkbox", true),
|
|
780
|
+
bool("quick_add", "Add to cart button", true),
|
|
781
|
+
bool("mrp", "Show the MRP struck through", true),
|
|
782
|
+
sel("hover", "On hover", opt(["none", "Nothing"], ["zoom", "Zoom the photo"], ["second_image", "Show the second photo"]), "zoom")
|
|
783
|
+
];
|
|
784
|
+
function validateGroups(defs, obj, path, errors) {
|
|
785
|
+
const src = obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {};
|
|
786
|
+
if (obj != null && src !== obj) errors.push({ path, message: "must be an object" });
|
|
787
|
+
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]]));
|
|
788
|
+
}
|
|
789
|
+
var fill = (defs) => Object.fromEntries(defs.map((f) => [f.key, f.kind === "group" ? fill(f.fields) : f.kind === "list" ? [...f.default || []] : f.default]));
|
|
790
|
+
var defaultChrome = () => fill(CHROME_FIELDS);
|
|
791
|
+
var defaultCard = () => fill(CARD_FIELDS);
|
|
792
|
+
function blockDef(pageKey, type) {
|
|
793
|
+
const page = own(STORE_PAGES, pageKey) ? STORE_PAGES[pageKey] : null;
|
|
794
|
+
if (!page) return null;
|
|
795
|
+
if (own(page.slots, type)) return { ...page.slots[type], slot: true };
|
|
796
|
+
if (CONTENT_BLOCKS.includes(type) && own(SECTION_TYPES, type)) return { ...SECTION_TYPES[type], slot: false };
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
var own = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
800
|
+
var blockId = (type) => type.replace(/_/g, "-");
|
|
801
|
+
var blockDefaults = (pageKey, type) => {
|
|
802
|
+
const def = blockDef(pageKey, type);
|
|
803
|
+
return def.slot ? fill(def.fields) : sectionDefaults(type);
|
|
804
|
+
};
|
|
805
|
+
function newBlock(pageKey, type) {
|
|
806
|
+
const def = blockDef(pageKey, type);
|
|
807
|
+
if (!def) throw new Error(`Unknown block "${type}" on the ${pageKey} page`);
|
|
808
|
+
return { id: def.slot ? blockId(type) : newSectionId(type), type, enabled: true, props: blockDefaults(pageKey, type) };
|
|
809
|
+
}
|
|
810
|
+
function templatePage(pageKey, template) {
|
|
811
|
+
const page = STORE_PAGES[pageKey];
|
|
812
|
+
const key = own(page.templates, template) ? template : page.default;
|
|
813
|
+
const tpl = page.templates[key];
|
|
814
|
+
const settings = { ...fill(page.settings), ...tpl.settings };
|
|
815
|
+
const blocks = tpl.blocks.map((type) => ({ id: blockId(type), type, enabled: true, props: blockDefaults(pageKey, type) }));
|
|
816
|
+
return { template: key, settings, blocks };
|
|
817
|
+
}
|
|
818
|
+
function validateTemplatePage(pageKey, input, path, errors) {
|
|
819
|
+
var _a, _b;
|
|
820
|
+
const page = STORE_PAGES[pageKey];
|
|
821
|
+
const src = input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
822
|
+
let template = (_a = src.template) != null ? _a : page.default;
|
|
823
|
+
if (!own(page.templates, template)) {
|
|
824
|
+
errors.push({ path: `${path}.template`, message: `must be one of ${Object.keys(page.templates).join(", ")}` });
|
|
825
|
+
template = page.default;
|
|
826
|
+
}
|
|
827
|
+
const tpl = page.templates[template];
|
|
828
|
+
const settingDefs = page.settings.map((f) => tpl.settings[f.key] === void 0 ? f : { ...f, default: tpl.settings[f.key] });
|
|
829
|
+
const settings = validateFields(settingDefs, (_b = src.settings) != null ? _b : {}, `${path}.settings`, errors);
|
|
830
|
+
const list = Array.isArray(src.blocks) ? src.blocks : null;
|
|
831
|
+
if (src.blocks != null && !list) errors.push({ path: `${path}.blocks`, message: "must be a list" });
|
|
832
|
+
const raw = list != null ? list : templatePage(pageKey, template).blocks;
|
|
833
|
+
if (raw.length > MAX_BLOCKS) errors.push({ path: `${path}.blocks`, message: `can have at most ${MAX_BLOCKS}` });
|
|
834
|
+
const ids = /* @__PURE__ */ new Set();
|
|
835
|
+
const slotsSeen = /* @__PURE__ */ new Set();
|
|
836
|
+
const blocks = [];
|
|
837
|
+
raw.slice(0, MAX_BLOCKS).forEach((b, i) => {
|
|
838
|
+
var _a2, _b2;
|
|
839
|
+
const at = `${path}.blocks[${i}]`;
|
|
840
|
+
if (!b || typeof b !== "object") {
|
|
841
|
+
errors.push({ path: at, message: "must be an object" });
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const def = blockDef(pageKey, b.type);
|
|
845
|
+
if (!def) {
|
|
846
|
+
errors.push({ path: `${at}.type`, message: `unknown block "${b.type}" on the ${page.label.toLowerCase()}` });
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
const id = String((_a2 = b.id) != null ? _a2 : "");
|
|
850
|
+
if (!ID_RE2.test(id)) errors.push({ path: `${at}.id`, message: "must be 1\u201340 lowercase letters, digits or dashes" });
|
|
851
|
+
else if (ids.has(id)) errors.push({ path: `${at}.id`, message: `duplicate id "${id}"` });
|
|
852
|
+
ids.add(id);
|
|
853
|
+
if (def.slot) {
|
|
854
|
+
if (slotsSeen.has(b.type)) {
|
|
855
|
+
errors.push({ path: `${at}.type`, message: `"${def.label}" can appear only once` });
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
slotsSeen.add(b.type);
|
|
859
|
+
}
|
|
860
|
+
let enabled = b.enabled === void 0 ? true : b.enabled;
|
|
861
|
+
if (typeof enabled !== "boolean") {
|
|
862
|
+
errors.push({ path: `${at}.enabled`, message: "must be true or false" });
|
|
863
|
+
enabled = true;
|
|
864
|
+
}
|
|
865
|
+
if (def.required && !enabled) {
|
|
866
|
+
errors.push({ path: `${at}.enabled`, message: `"${def.label}" is required and cannot be hidden` });
|
|
867
|
+
enabled = true;
|
|
868
|
+
}
|
|
869
|
+
blocks.push({ id, type: b.type, enabled, props: validateFields(def.fields, (_b2 = b.props) != null ? _b2 : {}, `${at}.props`, errors) });
|
|
870
|
+
});
|
|
871
|
+
for (const [type, def] of Object.entries(page.slots)) {
|
|
872
|
+
if (def.required && !slotsSeen.has(type)) {
|
|
873
|
+
errors.push({ path: `${path}.blocks`, message: `"${def.label}" is required` });
|
|
874
|
+
blocks.push({ id: ids.has(blockId(type)) ? `${blockId(type)}-1` : blockId(type), type, enabled: true, props: blockDefaults(pageKey, type) });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return { template, settings, blocks };
|
|
878
|
+
}
|
|
879
|
+
function migrateStore(input) {
|
|
880
|
+
const home = input && typeof input === "object" && Array.isArray(input.sections) ? input : null;
|
|
881
|
+
return {
|
|
882
|
+
version: STORE_VERSION,
|
|
883
|
+
pages: { home: home ? { version: 1, sections: home.sections } : defaultPage(), listing: templatePage("listing"), product: templatePage("product") },
|
|
884
|
+
chrome: defaultChrome(),
|
|
885
|
+
card: defaultCard()
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
var isLegacyPage = (doc) => !!doc && typeof doc === "object" && doc.version !== STORE_VERSION && Array.isArray(doc.sections);
|
|
889
|
+
function validateStore(input) {
|
|
890
|
+
var _a, _b, _c;
|
|
891
|
+
const errors = [];
|
|
892
|
+
const src0 = isLegacyPage(input) ? migrateStore(input) : input;
|
|
893
|
+
const src = src0 && typeof src0 === "object" && !Array.isArray(src0) ? src0 : null;
|
|
894
|
+
if (!src) {
|
|
895
|
+
const doc2 = migrateStore(null);
|
|
896
|
+
return { ok: false, errors: [{ path: "doc", message: "must be an object" }], doc: doc2 };
|
|
897
|
+
}
|
|
898
|
+
if (src.version != null && src.version !== STORE_VERSION) errors.push({ path: "version", message: `must be ${STORE_VERSION}` });
|
|
899
|
+
const pages = src.pages && typeof src.pages === "object" ? src.pages : {};
|
|
900
|
+
const home = validatePage((_a = pages.home) != null ? _a : defaultPage());
|
|
901
|
+
home.errors.forEach((e) => errors.push({ path: `pages.home.${e.path}`, message: e.message }));
|
|
902
|
+
const doc = {
|
|
903
|
+
version: STORE_VERSION,
|
|
904
|
+
pages: {
|
|
905
|
+
home: home.page,
|
|
906
|
+
...Object.fromEntries(STORE_PAGE_KEYS.map((k) => [k, validateTemplatePage(k, pages[k], `pages.${k}`, errors)]))
|
|
907
|
+
},
|
|
908
|
+
chrome: validateGroups(CHROME_FIELDS, (_b = src.chrome) != null ? _b : {}, "chrome", errors),
|
|
909
|
+
card: validateFields(CARD_FIELDS, (_c = src.card) != null ? _c : {}, "card", errors)
|
|
910
|
+
};
|
|
911
|
+
if (doc.chrome.announcement.enabled && !doc.chrome.announcement.text) errors.push({ path: "chrome.announcement.text", message: "is required when the bar is shown" });
|
|
912
|
+
return { ok: errors.length === 0, errors, doc };
|
|
913
|
+
}
|
|
914
|
+
function defaultStore({ name, tagline } = {}) {
|
|
915
|
+
return { ...migrateStore(defaultPage({ name, tagline })) };
|
|
916
|
+
}
|
|
917
|
+
function storeMediaRefs(doc) {
|
|
918
|
+
const out = [];
|
|
919
|
+
const walk = (v) => {
|
|
920
|
+
if (Array.isArray(v)) v.forEach(walk);
|
|
921
|
+
else if (v && typeof v === "object") {
|
|
922
|
+
if (v.media_id != null && Object.keys(v).every((k) => k === "media_id" || k === "alt")) out.push(v);
|
|
923
|
+
else Object.values(v).forEach(walk);
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
walk(doc);
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
648
929
|
export {
|
|
930
|
+
CARD_FIELDS,
|
|
931
|
+
CHROME_FIELDS,
|
|
932
|
+
CONTENT_BLOCKS,
|
|
649
933
|
DEFAULT_THEME,
|
|
650
934
|
FONT_PAIRS,
|
|
651
935
|
ICON_NAMES,
|
|
936
|
+
LISTING_SLOTS,
|
|
937
|
+
MAX_BLOCKS,
|
|
652
938
|
MAX_SECTIONS,
|
|
653
939
|
MODES,
|
|
654
940
|
PAGE_VERSION,
|
|
655
941
|
PALETTE_STEPS,
|
|
942
|
+
PRODUCT_SLOTS,
|
|
656
943
|
RADII,
|
|
657
944
|
SECTION_TYPES,
|
|
658
945
|
SECTION_TYPE_KEYS,
|
|
946
|
+
STORE_PAGES,
|
|
947
|
+
STORE_PAGE_KEYS,
|
|
948
|
+
STORE_VERSION,
|
|
659
949
|
THEME_PRESETS,
|
|
950
|
+
blockDef,
|
|
660
951
|
brandPalette,
|
|
661
952
|
contrastRatio,
|
|
953
|
+
defaultCard,
|
|
954
|
+
defaultChrome,
|
|
662
955
|
defaultPage,
|
|
956
|
+
defaultStore,
|
|
663
957
|
hexToOklch,
|
|
664
958
|
isHex,
|
|
959
|
+
isLegacyPage,
|
|
665
960
|
luminance,
|
|
961
|
+
migrateStore,
|
|
962
|
+
newBlock,
|
|
666
963
|
newSection,
|
|
667
964
|
newSectionId,
|
|
668
965
|
oklchToHex,
|
|
@@ -670,7 +967,11 @@ export {
|
|
|
670
967
|
safeHref,
|
|
671
968
|
sanitizeRichText,
|
|
672
969
|
sectionDefaults,
|
|
970
|
+
storeMediaRefs,
|
|
971
|
+
templatePage,
|
|
673
972
|
themeVars,
|
|
973
|
+
validateFields,
|
|
674
974
|
validatePage,
|
|
975
|
+
validateStore,
|
|
675
976
|
validateTheme
|
|
676
977
|
};
|
package/index.d.ts
CHANGED
|
@@ -299,7 +299,7 @@ export declare function createDemoIsland(props: Omit<DemoDialogProps, 'onClose'>
|
|
|
299
299
|
// ---------------------------------------------------------------------------
|
|
300
300
|
// Tenant pages: a store's own landing page as data. Validate and build pages
|
|
301
301
|
// with `@dreamtree-org/landing-kit/schema`; render them with TenantPage.
|
|
302
|
-
import type { PageDoc, Theme, MediaRef, SectionType, ValidationError, Palette, FontPair } from './schema';
|
|
302
|
+
import type { PageDoc, Theme, MediaRef, SectionType, ValidationError, Palette, FontPair, StoreDoc, TemplateBlock } from './schema';
|
|
303
303
|
|
|
304
304
|
export interface TenantPageProps {
|
|
305
305
|
/** A page document (validated again here; invalid sections are dropped). */
|
|
@@ -321,6 +321,20 @@ export function TenantPage(props: TenantPageProps): JSX.Element;
|
|
|
321
321
|
export const TENANT_BLOCKS: Record<SectionType, (props: any) => JSX.Element | null>;
|
|
322
322
|
export function HeroSlider(props: Record<string, unknown>): JSX.Element | null;
|
|
323
323
|
export function NamedIcon(props: { name: string; size?: number; className?: string }): JSX.Element | null;
|
|
324
|
+
export interface TemplateRegionProps {
|
|
325
|
+
/** The blocks of a template page (StoreDoc.pages.listing/product.blocks), in order; hidden ones are skipped. */
|
|
326
|
+
blocks: TemplateBlock[];
|
|
327
|
+
/** App components for commerce slots, by block type; return null to place a slot yourself. */
|
|
328
|
+
slots?: Record<string, ComponentType<{ block: TemplateBlock; props: Record<string, unknown> }>>;
|
|
329
|
+
theme?: Partial<Theme> | null;
|
|
330
|
+
resolveMedia?: (ref: MediaRef, variant: string) => string | null;
|
|
331
|
+
linkComponent?: ComponentType<any> | null;
|
|
332
|
+
storeName?: string;
|
|
333
|
+
className?: string;
|
|
334
|
+
}
|
|
335
|
+
/** A listing or product page's blocks: app slots plus content sections rendered like TenantPage. */
|
|
336
|
+
export function TemplateRegion(props: TemplateRegionProps): JSX.Element;
|
|
337
|
+
export function validateStore(doc: unknown): { ok: boolean; errors: ValidationError[]; doc: StoreDoc };
|
|
324
338
|
export function validatePage(doc: unknown): { ok: boolean; errors: ValidationError[]; page: PageDoc };
|
|
325
339
|
export function validateTheme(theme: unknown): { ok: boolean; errors: ValidationError[]; warnings: ValidationError[]; theme: Theme };
|
|
326
340
|
export function themeVars(theme: unknown): { theme: Theme; font: FontPair; palette: { primary: Palette; accent: Palette }; style: Record<string, string> };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dreamtree-org/landing-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The shared marketing site, auth frame (AuthShell + brands) and Platform admin console for every Dreamtree product: content-driven landing pages, login/signup shell, platform console client, Express routes, prerender and SEO files.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Partha Preetham Krishna",
|
package/platform.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const PLATFORM_API_BASE: '/api/platform';
|
|
|
5
5
|
export const PLATFORM_LOGIN_PATH: '/platform/login';
|
|
6
6
|
export const STEP_UP_REQUIRED: 'STEP_UP_REQUIRED';
|
|
7
7
|
|
|
8
|
-
export type PlatformFeature = 'overview' | 'companies' | 'jobs' | 'plans' | 'audit' | 'support' | 'settings' | 'admins';
|
|
8
|
+
export type PlatformFeature = 'overview' | 'companies' | 'jobs' | 'plans' | 'audit' | 'support' | 'settings' | 'domains' | 'admins';
|
|
9
9
|
/** Every console feature, in nav order. */
|
|
10
10
|
export const PLATFORM_FEATURES: readonly PlatformFeature[];
|
|
11
11
|
/** Features a server without `GET /me` serves. */
|
|
@@ -91,7 +91,7 @@ export interface PlatformMe {
|
|
|
91
91
|
permissions: string[];
|
|
92
92
|
/** `console` false = enrollment-only session (enroll an authenticator first). */
|
|
93
93
|
access?: { console: boolean; stepUpFresh: boolean };
|
|
94
|
-
features: Partial<Record<PlatformFeature
|
|
94
|
+
features: Partial<Record<PlatformFeature, boolean>>;
|
|
95
95
|
app: { key: string; name: string; companyLoginPath?: string };
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -187,28 +187,35 @@ export interface Plan {
|
|
|
187
187
|
seats?: number | null;
|
|
188
188
|
limits?: Record<string, number | null>;
|
|
189
189
|
features?: string[];
|
|
190
|
+
sort?: number;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
export interface Subscription {
|
|
193
194
|
id: string;
|
|
194
195
|
companyId: string;
|
|
195
196
|
planId: string;
|
|
197
|
+
planCode?: string | null;
|
|
198
|
+
planName?: string | null;
|
|
196
199
|
status: 'trial' | 'active' | 'past_due' | 'cancelled' | 'expired';
|
|
197
200
|
startsAt: Iso;
|
|
198
201
|
endsAt: Iso | null;
|
|
199
202
|
seats?: number | null;
|
|
200
203
|
notes?: string | null;
|
|
204
|
+
createdBy?: string | null;
|
|
205
|
+
createdAt?: Iso;
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
export interface SettingsField {
|
|
204
209
|
key: string;
|
|
205
210
|
label: string;
|
|
206
211
|
type: 'text' | 'number' | 'boolean' | 'select' | 'secret' | 'email' | 'url';
|
|
207
|
-
options?: string
|
|
212
|
+
options?: Array<{ value: string; label: string }>;
|
|
208
213
|
/** Secret values come back masked (`set: true`), never in clear. */
|
|
209
214
|
value?: unknown;
|
|
210
215
|
set?: boolean;
|
|
211
216
|
help?: string;
|
|
217
|
+
/** Where the value comes from. */
|
|
218
|
+
source?: 'branch' | 'company' | 'default' | null;
|
|
212
219
|
}
|
|
213
220
|
export interface SettingsFamily {
|
|
214
221
|
family: string;
|
|
@@ -216,24 +223,37 @@ export interface SettingsFamily {
|
|
|
216
223
|
scope: 'platform' | 'company';
|
|
217
224
|
fields: SettingsField[];
|
|
218
225
|
}
|
|
226
|
+
/** GET settings?company= */
|
|
227
|
+
export interface SettingsResponse {
|
|
228
|
+
families: SettingsFamily[];
|
|
229
|
+
/** Send back with PUT settings/:family; a stale version answers 409. */
|
|
230
|
+
version: number;
|
|
231
|
+
}
|
|
219
232
|
|
|
220
233
|
export interface SupportGrant {
|
|
221
234
|
id: string;
|
|
222
235
|
companyId: string;
|
|
223
236
|
companyName?: string;
|
|
224
237
|
requestedBy: string;
|
|
238
|
+
requestedById?: string;
|
|
225
239
|
approvedBy?: string | null;
|
|
226
240
|
reason: string;
|
|
241
|
+
/** Length of the access window, counted from approval. */
|
|
242
|
+
minutes?: number | null;
|
|
227
243
|
status: 'requested' | 'active' | 'expired' | 'revoked';
|
|
228
244
|
expiresAt: Iso | null;
|
|
245
|
+
approvedAt?: Iso | null;
|
|
246
|
+
revokedAt?: Iso | null;
|
|
229
247
|
createdAt: Iso;
|
|
230
248
|
}
|
|
231
249
|
|
|
232
250
|
export interface CompanyDomain {
|
|
233
251
|
id: string;
|
|
234
252
|
host: string;
|
|
253
|
+
status?: 'pending' | 'verified';
|
|
235
254
|
primary: boolean;
|
|
236
255
|
verifiedAt: Iso | null;
|
|
256
|
+
createdAt?: Iso;
|
|
237
257
|
verification?: { type: 'TXT'; name: string; value: string };
|
|
238
258
|
}
|
|
239
259
|
|
|
@@ -327,6 +347,8 @@ export function usePlatform(): {
|
|
|
327
347
|
href(rel?: string): string;
|
|
328
348
|
navigate(href: string): void;
|
|
329
349
|
can(permission: string): boolean;
|
|
350
|
+
/** Features this console shows (requested ∩ advertised by GET /me). */
|
|
351
|
+
features: PlatformFeature[];
|
|
330
352
|
companyLoginPath(slug: string): string;
|
|
331
353
|
companyLoginUrl(slug: string): string;
|
|
332
354
|
};
|
|
@@ -340,3 +362,15 @@ export const CopyButton: ComponentType<{ value: string; label?: string }>;
|
|
|
340
362
|
export const OneTimeSecret: ComponentType<{ title?: string; loginUrl?: string; email: string; password: string; onDone?: () => void }>;
|
|
341
363
|
export const InspectResult: ComponentType<{ report: Record<string, any> }>;
|
|
342
364
|
export function describeAction(action: string): string;
|
|
365
|
+
/** "₹999.00 / month" from minor units. */
|
|
366
|
+
export function formatPrice(priceMinor: number, currency?: string, interval?: Plan['interval']): string;
|
|
367
|
+
|
|
368
|
+
/** Built-in v1.1 views, exported for products that compose their own console. They render inside <PlatformConsole>. */
|
|
369
|
+
export const PlansView: ComponentType;
|
|
370
|
+
export const SupportAccessView: ComponentType;
|
|
371
|
+
export const SupportGrants: ComponentType<{ companyId?: string; companyName?: string }>;
|
|
372
|
+
type CompanyTabProps = { company: { id: string; displayName?: string } & Record<string, any>; setFlash(flash: { kind: 'success' | 'error' | 'warning' | 'info'; msg: string } | null): void };
|
|
373
|
+
export const SubscriptionTab: ComponentType<CompanyTabProps>;
|
|
374
|
+
export const DomainsTab: ComponentType<CompanyTabProps>;
|
|
375
|
+
export const SettingsTab: ComponentType<CompanyTabProps>;
|
|
376
|
+
export const SupportTab: ComponentType<CompanyTabProps>;
|
package/schema.d.ts
CHANGED
|
@@ -94,3 +94,53 @@ export function hexToOklch(hex: string): { l: number; c: number; h: number };
|
|
|
94
94
|
export function oklchToHex(c: { l: number; c: number; h: number }): string;
|
|
95
95
|
export function isHex(v: unknown): boolean;
|
|
96
96
|
export const PALETTE_STEPS: number[];
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Store documents (version 2): the landing page plus listing and product
|
|
100
|
+
// template pages, the header/footer and the product card.
|
|
101
|
+
|
|
102
|
+
export type StorePageKey = 'listing' | 'product';
|
|
103
|
+
export type ListingSlot = 'collection_header' | 'toolbar' | 'filters' | 'results';
|
|
104
|
+
export type ProductSlot = 'gallery' | 'buy_box' | 'highlights' | 'delivery' | 'seller' | 'inspection' | 'battery' | 'included' | 'specs' | 'policies' | 'similar';
|
|
105
|
+
export type ContentBlock = 'promo_banner' | 'usp_strip' | 'trust_badges' | 'cta_cards' | 'faq' | 'rich_text' | 'testimonials' | 'brand_strip' | 'gallery';
|
|
106
|
+
export interface SlotDef { label: string; description: string; required?: boolean; fields: FieldDef[] }
|
|
107
|
+
export interface TemplateDef { label: string; description: string; /** Setting values that differ from the page's defaults. */ settings: Record<string, unknown>; /** Default block order. */ blocks: string[] }
|
|
108
|
+
export interface StorePageDef { label: string; slots: Record<string, SlotDef>; settings: FieldDef[]; templates: Record<string, TemplateDef>; default: string }
|
|
109
|
+
export interface TemplateBlock { id: string; type: string; enabled: boolean; props: Record<string, unknown> }
|
|
110
|
+
export interface TemplatePage { template: string; settings: Record<string, unknown>; blocks: TemplateBlock[] }
|
|
111
|
+
export interface GroupDef { key: string; kind: 'group'; label: string; fields: FieldDef[] }
|
|
112
|
+
export interface StoreLink { label: string; href: string }
|
|
113
|
+
export interface StoreChrome {
|
|
114
|
+
announcement: { enabled: boolean; text: string; href: string; tone: 'brand' | 'accent' | 'dark' };
|
|
115
|
+
header: { utility_text: string; show_search: boolean; category_nav: boolean; nav_links: StoreLink[]; sticky: boolean };
|
|
116
|
+
footer: { about: string; columns: { title: string; links: StoreLink[] }[]; socials: { network: string; href: string }[]; note: string };
|
|
117
|
+
}
|
|
118
|
+
export interface StoreCard { image_ratio: '4:3' | '1:1' | '3:4'; badges: boolean; spec_line: boolean; compare: boolean; quick_add: boolean; mrp: boolean; hover: 'none' | 'zoom' | 'second_image' }
|
|
119
|
+
export interface StoreDoc { version: 2; pages: { home: PageDoc; listing: TemplatePage; product: TemplatePage }; chrome: StoreChrome; card: StoreCard }
|
|
120
|
+
|
|
121
|
+
export const STORE_VERSION: 2;
|
|
122
|
+
export const MAX_BLOCKS: number;
|
|
123
|
+
export const STORE_PAGES: Record<StorePageKey, StorePageDef>;
|
|
124
|
+
export const STORE_PAGE_KEYS: StorePageKey[];
|
|
125
|
+
export const LISTING_SLOTS: Record<ListingSlot, SlotDef>;
|
|
126
|
+
export const PRODUCT_SLOTS: Record<ProductSlot, SlotDef>;
|
|
127
|
+
export const CONTENT_BLOCKS: ContentBlock[];
|
|
128
|
+
export const CHROME_FIELDS: GroupDef[];
|
|
129
|
+
export const CARD_FIELDS: FieldDef[];
|
|
130
|
+
/** Normalise a store document (a version-1 landing page is migrated first) and report every problem by path. Required slots cannot be hidden or removed. */
|
|
131
|
+
export function validateStore(doc: unknown): { ok: boolean; errors: ValidationError[]; doc: StoreDoc };
|
|
132
|
+
/** A version-1 landing page (or nothing) wrapped into a store document with defaults. */
|
|
133
|
+
export function migrateStore(page: unknown): StoreDoc;
|
|
134
|
+
export function isLegacyPage(doc: unknown): boolean;
|
|
135
|
+
export function defaultStore(store?: { name?: string; tagline?: string }): StoreDoc;
|
|
136
|
+
/** A template page with that template's settings and default blocks (unknown template → the page default). */
|
|
137
|
+
export function templatePage(page: StorePageKey, template?: string): TemplatePage;
|
|
138
|
+
export function newBlock(page: StorePageKey, type: string): TemplateBlock;
|
|
139
|
+
/** A slot or content section's definition on a page (slot: true for app-rendered blocks), or null. */
|
|
140
|
+
export function blockDef(page: StorePageKey, type: string): ((SlotDef | SectionTypeDef) & { slot: boolean }) | null;
|
|
141
|
+
export function defaultChrome(): StoreChrome;
|
|
142
|
+
export function defaultCard(): StoreCard;
|
|
143
|
+
/** Every uploaded-image reference in a document. */
|
|
144
|
+
export function storeMediaRefs(doc: unknown): MediaRef[];
|
|
145
|
+
/** Validate an object against a field list with the section rules; problems are pushed onto `errors`. */
|
|
146
|
+
export function validateFields(defs: FieldDef[], obj: unknown, path: string, errors: ValidationError[]): Record<string, unknown>;
|