@zalify/storefront-kit 0.1.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/LICENSE.md +49 -0
- package/README.md +63 -0
- package/dist/commerce/countdown.d.ts +18 -0
- package/dist/commerce/countdown.js +28 -0
- package/dist/commerce/events.d.ts +29 -0
- package/dist/commerce/events.js +14 -0
- package/dist/commerce/facets.d.ts +27 -0
- package/dist/commerce/facets.js +71 -0
- package/dist/commerce/index.d.ts +11 -0
- package/dist/commerce/index.js +11 -0
- package/dist/commerce/money.d.ts +51 -0
- package/dist/commerce/money.js +83 -0
- package/dist/commerce/product.d.ts +102 -0
- package/dist/commerce/product.js +93 -0
- package/dist/editor/frame.d.ts +57 -0
- package/dist/editor/frame.js +241 -0
- package/dist/editor/host.d.ts +31 -0
- package/dist/editor/host.js +67 -0
- package/dist/editor/index.d.ts +9 -0
- package/dist/editor/index.js +9 -0
- package/dist/react/adapter.d.ts +49 -0
- package/dist/react/adapter.js +23 -0
- package/dist/react/blocks/_box.d.ts +10 -0
- package/dist/react/blocks/_box.js +17 -0
- package/dist/react/blocks/_slide.d.ts +24 -0
- package/dist/react/blocks/_slide.js +21 -0
- package/dist/react/blocks/_video-card.d.ts +11 -0
- package/dist/react/blocks/_video-card.js +39 -0
- package/dist/react/blocks/accordion.d.ts +12 -0
- package/dist/react/blocks/accordion.js +5 -0
- package/dist/react/blocks/button.d.ts +10 -0
- package/dist/react/blocks/button.js +15 -0
- package/dist/react/blocks/countdown.d.ts +9 -0
- package/dist/react/blocks/countdown.js +64 -0
- package/dist/react/blocks/custom-liquid.d.ts +11 -0
- package/dist/react/blocks/custom-liquid.js +4 -0
- package/dist/react/blocks/group.d.ts +9 -0
- package/dist/react/blocks/group.js +8 -0
- package/dist/react/blocks/image.d.ts +8 -0
- package/dist/react/blocks/image.js +7 -0
- package/dist/react/blocks/message.d.ts +14 -0
- package/dist/react/blocks/message.js +5 -0
- package/dist/react/blocks/quote.d.ts +9 -0
- package/dist/react/blocks/quote.js +5 -0
- package/dist/react/blocks/spacer.d.ts +8 -0
- package/dist/react/blocks/spacer.js +5 -0
- package/dist/react/blocks/story.d.ts +17 -0
- package/dist/react/blocks/story.js +7 -0
- package/dist/react/blocks/text.d.ts +9 -0
- package/dist/react/blocks/text.js +5 -0
- package/dist/react/blocks/video.d.ts +17 -0
- package/dist/react/blocks/video.js +56 -0
- package/dist/react/components/Facets.d.ts +21 -0
- package/dist/react/components/Facets.js +220 -0
- package/dist/react/components/Icon.d.ts +12 -0
- package/dist/react/components/Icon.js +66 -0
- package/dist/react/components/PaymentIcons.d.ts +5 -0
- package/dist/react/components/PaymentIcons.generated.d.ts +22 -0
- package/dist/react/components/PaymentIcons.generated.js +21 -0
- package/dist/react/components/PaymentIcons.js +18 -0
- package/dist/react/components/Price.d.ts +24 -0
- package/dist/react/components/Price.js +9 -0
- package/dist/react/components/ProductBadges.d.ts +24 -0
- package/dist/react/components/ProductBadges.js +57 -0
- package/dist/react/components/ProductCard.d.ts +17 -0
- package/dist/react/components/ProductCard.js +202 -0
- package/dist/react/components/Rating.d.ts +11 -0
- package/dist/react/components/Rating.js +35 -0
- package/dist/react/components/SwatchStyle.d.ts +23 -0
- package/dist/react/components/SwatchStyle.js +22 -0
- package/dist/react/components/VideoModal.d.ts +55 -0
- package/dist/react/components/VideoModal.js +102 -0
- package/dist/react/engine/CssVariables.d.ts +1 -0
- package/dist/react/engine/CssVariables.js +105 -0
- package/dist/react/engine/context.d.ts +43 -0
- package/dist/react/engine/context.js +45 -0
- package/dist/react/engine/fonts.d.ts +15 -0
- package/dist/react/engine/fonts.js +33 -0
- package/dist/react/engine/images.d.ts +24 -0
- package/dist/react/engine/images.js +40 -0
- package/dist/react/engine/render.d.ts +29 -0
- package/dist/react/engine/render.js +87 -0
- package/dist/react/engine/settings.d.ts +8 -0
- package/dist/react/engine/settings.js +68 -0
- package/dist/react/engine/store.d.ts +64 -0
- package/dist/react/engine/store.js +42 -0
- package/dist/react/engine/translate.d.ts +7 -0
- package/dist/react/engine/translate.js +33 -0
- package/dist/react/engine/types.d.ts +128 -0
- package/dist/react/engine/types.js +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +39 -0
- package/dist/react/registries.d.ts +10 -0
- package/dist/react/registries.js +68 -0
- package/dist/react/registries.server.d.ts +2 -0
- package/dist/react/registries.server.js +98 -0
- package/dist/react/sections/404.d.ts +10 -0
- package/dist/react/sections/404.js +10 -0
- package/dist/react/sections/announcement-bar.d.ts +14 -0
- package/dist/react/sections/announcement-bar.js +84 -0
- package/dist/react/sections/article.d.ts +17 -0
- package/dist/react/sections/article.js +31 -0
- package/dist/react/sections/contact-form.d.ts +16 -0
- package/dist/react/sections/contact-form.js +8 -0
- package/dist/react/sections/custom-liquid.d.ts +14 -0
- package/dist/react/sections/custom-liquid.js +7 -0
- package/dist/react/sections/custom-section.d.ts +14 -0
- package/dist/react/sections/custom-section.js +9 -0
- package/dist/react/sections/featured-collection.d.ts +10 -0
- package/dist/react/sections/featured-collection.js +23 -0
- package/dist/react/sections/hero.d.ts +6 -0
- package/dist/react/sections/hero.js +35 -0
- package/dist/react/sections/image-with-text.d.ts +13 -0
- package/dist/react/sections/image-with-text.js +9 -0
- package/dist/react/sections/multicolumn.d.ts +15 -0
- package/dist/react/sections/multicolumn.js +8 -0
- package/dist/react/sections/page.d.ts +13 -0
- package/dist/react/sections/page.js +11 -0
- package/dist/react/sections/password.d.ts +15 -0
- package/dist/react/sections/password.js +9 -0
- package/dist/react/sections/rich-text.d.ts +12 -0
- package/dist/react/sections/rich-text.js +7 -0
- package/dist/react/sections/stories.d.ts +18 -0
- package/dist/react/sections/stories.js +10 -0
- package/dist/react/sections/video-bubble.d.ts +13 -0
- package/dist/react/sections/video-bubble.js +54 -0
- package/dist/react/sections/video-carousel.d.ts +19 -0
- package/dist/react/sections/video-carousel.js +9 -0
- package/dist/react/server.d.ts +7 -0
- package/dist/react/server.js +42 -0
- package/dist/schemas/bridge.d.ts +98 -0
- package/dist/schemas/bridge.js +50 -0
- package/dist/schemas/data.d.ts +54 -0
- package/dist/schemas/data.js +22 -0
- package/dist/schemas/index.d.ts +14 -0
- package/dist/schemas/index.js +14 -0
- package/dist/schemas/manifest.d.ts +63 -0
- package/dist/schemas/manifest.js +32 -0
- package/dist/schemas/schema.d.ts +159 -0
- package/dist/schemas/schema.js +129 -0
- package/dist/schemas/validate.d.ts +22 -0
- package/dist/schemas/validate.js +199 -0
- package/package.json +84 -0
- package/src/commerce/countdown.ts +38 -0
- package/src/commerce/events.ts +43 -0
- package/src/commerce/facets.ts +88 -0
- package/src/commerce/index.ts +11 -0
- package/src/commerce/money.ts +120 -0
- package/src/commerce/product.ts +177 -0
- package/src/editor/frame.ts +305 -0
- package/src/editor/host.ts +101 -0
- package/src/editor/index.ts +9 -0
- package/src/react/adapter.tsx +83 -0
- package/src/react/blocks/_box.tsx +37 -0
- package/src/react/blocks/_slide.tsx +96 -0
- package/src/react/blocks/_video-card.tsx +131 -0
- package/src/react/blocks/accordion.tsx +33 -0
- package/src/react/blocks/button.tsx +36 -0
- package/src/react/blocks/countdown.tsx +105 -0
- package/src/react/blocks/custom-liquid.tsx +21 -0
- package/src/react/blocks/group.tsx +32 -0
- package/src/react/blocks/image.tsx +29 -0
- package/src/react/blocks/message.tsx +28 -0
- package/src/react/blocks/quote.tsx +24 -0
- package/src/react/blocks/spacer.tsx +17 -0
- package/src/react/blocks/story.tsx +40 -0
- package/src/react/blocks/text.tsx +24 -0
- package/src/react/blocks/video.tsx +132 -0
- package/src/react/components/Facets.tsx +424 -0
- package/src/react/components/Icon.tsx +96 -0
- package/src/react/components/PaymentIcons.generated.ts +24 -0
- package/src/react/components/PaymentIcons.tsx +27 -0
- package/src/react/components/Price.tsx +72 -0
- package/src/react/components/ProductBadges.tsx +95 -0
- package/src/react/components/ProductCard.tsx +417 -0
- package/src/react/components/Rating.tsx +59 -0
- package/src/react/components/SwatchStyle.tsx +50 -0
- package/src/react/components/VideoModal.tsx +236 -0
- package/src/react/engine/CssVariables.tsx +142 -0
- package/src/react/engine/context.tsx +108 -0
- package/src/react/engine/fonts.ts +42 -0
- package/src/react/engine/images.tsx +58 -0
- package/src/react/engine/render.tsx +190 -0
- package/src/react/engine/settings.ts +89 -0
- package/src/react/engine/store.ts +103 -0
- package/src/react/engine/translate.ts +40 -0
- package/src/react/engine/types.ts +142 -0
- package/src/react/index.ts +78 -0
- package/src/react/registries.server.ts +110 -0
- package/src/react/registries.ts +80 -0
- package/src/react/sections/404.tsx +37 -0
- package/src/react/sections/announcement-bar.tsx +170 -0
- package/src/react/sections/article.tsx +97 -0
- package/src/react/sections/contact-form.tsx +104 -0
- package/src/react/sections/custom-liquid.tsx +39 -0
- package/src/react/sections/custom-section.tsx +44 -0
- package/src/react/sections/featured-collection.tsx +89 -0
- package/src/react/sections/hero.tsx +84 -0
- package/src/react/sections/image-with-text.tsx +61 -0
- package/src/react/sections/multicolumn.tsx +46 -0
- package/src/react/sections/page.tsx +43 -0
- package/src/react/sections/password.tsx +60 -0
- package/src/react/sections/rich-text.tsx +37 -0
- package/src/react/sections/stories.tsx +51 -0
- package/src/react/sections/video-bubble.tsx +145 -0
- package/src/react/sections/video-carousel.tsx +57 -0
- package/src/react/server.ts +60 -0
- package/src/schemas/bridge.ts +143 -0
- package/src/schemas/data.ts +72 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/manifest.ts +85 -0
- package/src/schemas/schema.ts +287 -0
- package/src/schemas/validate.ts +302 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3 — Canvas-editor bridge protocol.
|
|
3
|
+
*
|
|
4
|
+
* postMessage contract between the Zalify canvas editor (host) and a
|
|
5
|
+
* theme preview running on another origin inside an iframe (frame).
|
|
6
|
+
* Both sides validate `event.origin` and the message envelope before
|
|
7
|
+
* acting; versions negotiate on CONTRACT_VERSION's major component.
|
|
8
|
+
*/
|
|
9
|
+
import type { SectionGroupData, SettingsBag, TemplateData } from './data.ts';
|
|
10
|
+
import type { ThemeEditorManifest } from './manifest.ts';
|
|
11
|
+
export declare const BRIDGE_NAMESPACE = "zalify-editor-bridge";
|
|
12
|
+
/** Query param that switches a theme preview into editor mode. */
|
|
13
|
+
export declare const EDITOR_MODE_PARAM = "zalify-editor";
|
|
14
|
+
/** DOM attribute carrying a node's theme-JSON path (wire format). */
|
|
15
|
+
export declare const DATA_PATH_ATTR = "data-z-path";
|
|
16
|
+
/** DOM attribute the bridge sets on the selected node. */
|
|
17
|
+
export declare const DATA_SELECTED_ATTR = "data-z-selected";
|
|
18
|
+
export type Device = 'desktop' | 'mobile';
|
|
19
|
+
export interface DOMRectLike {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
}
|
|
25
|
+
/** Locates a section or block inside a template or section group. */
|
|
26
|
+
export interface ThemeJsonPath {
|
|
27
|
+
scope: {
|
|
28
|
+
kind: 'template' | 'group';
|
|
29
|
+
name: string;
|
|
30
|
+
};
|
|
31
|
+
sectionKey: string;
|
|
32
|
+
/** Nested block keys, outermost first; empty = the section itself. */
|
|
33
|
+
blockKeys: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wire format: "template:index/hero/slide/box", "group:header-group/header".
|
|
37
|
+
* Every segment is URI-component-encoded so names containing "/" stay
|
|
38
|
+
* unambiguous (customer templates are named "customers/login").
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatThemePath(path: ThemeJsonPath): string;
|
|
41
|
+
export declare function parseThemePath(wire: string): ThemeJsonPath | null;
|
|
42
|
+
export interface BridgeEnvelope<TType extends string, TPayload> {
|
|
43
|
+
z: typeof BRIDGE_NAMESPACE;
|
|
44
|
+
/** CONTRACT_VERSION of the sender. */
|
|
45
|
+
v: string;
|
|
46
|
+
type: TType;
|
|
47
|
+
payload: TPayload;
|
|
48
|
+
}
|
|
49
|
+
export type HostMessage = BridgeEnvelope<'bridge:init', {
|
|
50
|
+
editorOrigin: string;
|
|
51
|
+
device: Device;
|
|
52
|
+
selectedPath: string | null;
|
|
53
|
+
}> | BridgeEnvelope<'block:select', {
|
|
54
|
+
path: string | null;
|
|
55
|
+
}> | BridgeEnvelope<'block:hover', {
|
|
56
|
+
path: string | null;
|
|
57
|
+
}> | BridgeEnvelope<'block:scroll-to', {
|
|
58
|
+
path: string;
|
|
59
|
+
}> | BridgeEnvelope<'template:apply', {
|
|
60
|
+
templateName: string;
|
|
61
|
+
template: TemplateData;
|
|
62
|
+
groups?: Record<string, SectionGroupData>;
|
|
63
|
+
settingsData?: SettingsBag;
|
|
64
|
+
}> | BridgeEnvelope<'device:set', {
|
|
65
|
+
device: Device;
|
|
66
|
+
}> | BridgeEnvelope<'manifest:request', Record<string, never>>;
|
|
67
|
+
export type FrameMessage = BridgeEnvelope<'bridge:ready', {
|
|
68
|
+
contractVersion: string;
|
|
69
|
+
templateName: string;
|
|
70
|
+
hash: string;
|
|
71
|
+
}> | BridgeEnvelope<'block:clicked', {
|
|
72
|
+
path: string;
|
|
73
|
+
rect: DOMRectLike;
|
|
74
|
+
}> | BridgeEnvelope<'block:hovered', {
|
|
75
|
+
path: string | null;
|
|
76
|
+
rect?: DOMRectLike;
|
|
77
|
+
}> | BridgeEnvelope<'block:rects', {
|
|
78
|
+
rects: Array<{
|
|
79
|
+
path: string;
|
|
80
|
+
rect: DOMRectLike;
|
|
81
|
+
}>;
|
|
82
|
+
}> | BridgeEnvelope<'manifest:response', {
|
|
83
|
+
manifest: ThemeEditorManifest;
|
|
84
|
+
}> | BridgeEnvelope<'template:synced', {
|
|
85
|
+
hash: string;
|
|
86
|
+
}> | BridgeEnvelope<'height:changed', {
|
|
87
|
+
height: number;
|
|
88
|
+
}> | BridgeEnvelope<'navigation', {
|
|
89
|
+
templateName: string;
|
|
90
|
+
url: string;
|
|
91
|
+
}> | BridgeEnvelope<'bridge:error', {
|
|
92
|
+
code: string;
|
|
93
|
+
message: string;
|
|
94
|
+
}>;
|
|
95
|
+
export type BridgeMessage = HostMessage | FrameMessage;
|
|
96
|
+
export declare function isBridgeMessage(data: unknown): data is BridgeMessage;
|
|
97
|
+
/** Same-major = compatible; anything else degrades to read-only preview. */
|
|
98
|
+
export declare function isCompatibleVersion(mine: string, theirs: string): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const BRIDGE_NAMESPACE = 'zalify-editor-bridge';
|
|
2
|
+
/** Query param that switches a theme preview into editor mode. */
|
|
3
|
+
export const EDITOR_MODE_PARAM = 'zalify-editor';
|
|
4
|
+
/** DOM attribute carrying a node's theme-JSON path (wire format). */
|
|
5
|
+
export const DATA_PATH_ATTR = 'data-z-path';
|
|
6
|
+
/** DOM attribute the bridge sets on the selected node. */
|
|
7
|
+
export const DATA_SELECTED_ATTR = 'data-z-selected';
|
|
8
|
+
/**
|
|
9
|
+
* Wire format: "template:index/hero/slide/box", "group:header-group/header".
|
|
10
|
+
* Every segment is URI-component-encoded so names containing "/" stay
|
|
11
|
+
* unambiguous (customer templates are named "customers/login").
|
|
12
|
+
*/
|
|
13
|
+
export function formatThemePath(path) {
|
|
14
|
+
return [
|
|
15
|
+
`${path.scope.kind}:${encodeURIComponent(path.scope.name)}`,
|
|
16
|
+
encodeURIComponent(path.sectionKey),
|
|
17
|
+
...path.blockKeys.map(encodeURIComponent),
|
|
18
|
+
].join('/');
|
|
19
|
+
}
|
|
20
|
+
export function parseThemePath(wire) {
|
|
21
|
+
const [scopePart, sectionKey, ...blockKeys] = wire.split('/');
|
|
22
|
+
if (!scopePart || !sectionKey)
|
|
23
|
+
return null;
|
|
24
|
+
const colon = scopePart.indexOf(':');
|
|
25
|
+
if (colon === -1)
|
|
26
|
+
return null;
|
|
27
|
+
const kind = scopePart.slice(0, colon);
|
|
28
|
+
const name = scopePart.slice(colon + 1);
|
|
29
|
+
if ((kind !== 'template' && kind !== 'group') || !name)
|
|
30
|
+
return null;
|
|
31
|
+
return {
|
|
32
|
+
scope: { kind, name: decodeURIComponent(name) },
|
|
33
|
+
sectionKey: decodeURIComponent(sectionKey),
|
|
34
|
+
blockKeys: blockKeys.map(decodeURIComponent),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/* ----------------------------- guards ------------------------------- */
|
|
38
|
+
export function isBridgeMessage(data) {
|
|
39
|
+
return (typeof data === 'object' &&
|
|
40
|
+
data !== null &&
|
|
41
|
+
data.z === BRIDGE_NAMESPACE &&
|
|
42
|
+
typeof data.v === 'string' &&
|
|
43
|
+
typeof data.type === 'string' &&
|
|
44
|
+
typeof data.payload === 'object' &&
|
|
45
|
+
data.payload !== null);
|
|
46
|
+
}
|
|
47
|
+
/** Same-major = compatible; anything else degrades to read-only preview. */
|
|
48
|
+
export function isCompatibleVersion(mine, theirs) {
|
|
49
|
+
return mine.split('.')[0] === theirs.split('.')[0];
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 — Theme data contract.
|
|
3
|
+
*
|
|
4
|
+
* The runtime JSON shapes of a Zalify theme, byte-compatible with
|
|
5
|
+
* Shopify OS 2.0 theme JSON (templates/*.json, sections/*-group.json,
|
|
6
|
+
* config/settings_data.json). These are the canonical definitions;
|
|
7
|
+
* @zalify/storefront-kit/react's engine types mirror them and will re-export from
|
|
8
|
+
* here as the packages converge.
|
|
9
|
+
*/
|
|
10
|
+
/** A section's or block's settings: setting id -> value. */
|
|
11
|
+
export type SettingsBag = Record<string, unknown>;
|
|
12
|
+
/** A block instance inside a section (or nested inside another block). */
|
|
13
|
+
export interface BlockData {
|
|
14
|
+
type: string;
|
|
15
|
+
settings?: SettingsBag;
|
|
16
|
+
/** Nested blocks keyed by their stable key. */
|
|
17
|
+
blocks?: Record<string, BlockData>;
|
|
18
|
+
/** Render order of `blocks` keys. */
|
|
19
|
+
block_order?: string[];
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** A section instance inside a template or section group. */
|
|
23
|
+
export interface SectionData {
|
|
24
|
+
type: string;
|
|
25
|
+
settings?: SettingsBag;
|
|
26
|
+
blocks?: Record<string, BlockData>;
|
|
27
|
+
block_order?: string[];
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
custom_css?: string[];
|
|
30
|
+
}
|
|
31
|
+
/** templates/*.json — a page template. */
|
|
32
|
+
export interface TemplateData {
|
|
33
|
+
layout?: string | false;
|
|
34
|
+
wrapper?: string;
|
|
35
|
+
/** Sections keyed by their stable key. */
|
|
36
|
+
sections: Record<string, SectionData>;
|
|
37
|
+
/** Render order of `sections` keys. */
|
|
38
|
+
order: string[];
|
|
39
|
+
}
|
|
40
|
+
/** sections/*-group.json — header/footer/overlay section groups. */
|
|
41
|
+
export interface SectionGroupData extends TemplateData {
|
|
42
|
+
type: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}
|
|
45
|
+
/** config/settings_data.json — current values for global theme settings. */
|
|
46
|
+
export interface SettingsData {
|
|
47
|
+
current: string | Record<string, unknown>;
|
|
48
|
+
presets?: Record<string, Record<string, unknown>>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse theme JSON that may carry Shopify's leading block comment
|
|
52
|
+
* (the theme editor writes templates/*.json with a header comment).
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseThemeJson<T>(raw: string, name?: string): T;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 — Theme data contract.
|
|
3
|
+
*
|
|
4
|
+
* The runtime JSON shapes of a Zalify theme, byte-compatible with
|
|
5
|
+
* Shopify OS 2.0 theme JSON (templates/*.json, sections/*-group.json,
|
|
6
|
+
* config/settings_data.json). These are the canonical definitions;
|
|
7
|
+
* @zalify/storefront-kit/react's engine types mirror them and will re-export from
|
|
8
|
+
* here as the packages converge.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Parse theme JSON that may carry Shopify's leading block comment
|
|
12
|
+
* (the theme editor writes templates/*.json with a header comment).
|
|
13
|
+
*/
|
|
14
|
+
export function parseThemeJson(raw, name = 'theme JSON') {
|
|
15
|
+
const stripped = raw.replace(/^\s*\/\*[\s\S]*?\*\//, '').trim();
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(stripped);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new Error(`[storefront-kit] Failed to parse ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zalify/storefront-kit/schemas — the Zalify theme contract.
|
|
3
|
+
*
|
|
4
|
+
* L1 (data.ts): runtime theme JSON shapes, = Shopify OS 2.0.
|
|
5
|
+
* L2 (schema.ts): section/block schema types, = `{% schema %}` semantics.
|
|
6
|
+
* (manifest.ts): the generated editor manifest tying L2 together.
|
|
7
|
+
* L3 (bridge.ts): the canvas-editor postMessage protocol.
|
|
8
|
+
* validate.ts: structural + schema-aware validators.
|
|
9
|
+
*/
|
|
10
|
+
export * from './data.ts';
|
|
11
|
+
export * from './schema.ts';
|
|
12
|
+
export * from './manifest.ts';
|
|
13
|
+
export * from './bridge.ts';
|
|
14
|
+
export * from './validate.ts';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zalify/storefront-kit/schemas — the Zalify theme contract.
|
|
3
|
+
*
|
|
4
|
+
* L1 (data.ts): runtime theme JSON shapes, = Shopify OS 2.0.
|
|
5
|
+
* L2 (schema.ts): section/block schema types, = `{% schema %}` semantics.
|
|
6
|
+
* (manifest.ts): the generated editor manifest tying L2 together.
|
|
7
|
+
* L3 (bridge.ts): the canvas-editor postMessage protocol.
|
|
8
|
+
* validate.ts: structural + schema-aware validators.
|
|
9
|
+
*/
|
|
10
|
+
export * from "./data.js";
|
|
11
|
+
export * from "./schema.js";
|
|
12
|
+
export * from "./manifest.js";
|
|
13
|
+
export * from "./bridge.js";
|
|
14
|
+
export * from "./validate.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editor manifest: everything the canvas editor needs to drive its
|
|
3
|
+
* Insert/Layers/Config panels without executing theme code. Generated
|
|
4
|
+
* into the theme (and every scaffolded project) by `pnpm sync` as
|
|
5
|
+
* config/editor-manifest.generated.json; served live by the editor
|
|
6
|
+
* bridge as `manifest:response`.
|
|
7
|
+
*/
|
|
8
|
+
import type { SectionSchema, SettingDefinition, ThemeBlockSchema } from './schema.ts';
|
|
9
|
+
/**
|
|
10
|
+
* Contract version spoken by this package. Bumped with the package;
|
|
11
|
+
* editors negotiate compatibility on the major component.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CONTRACT_VERSION = "0.1.0";
|
|
14
|
+
/** Repo-relative path of the generated manifest inside a theme/project. */
|
|
15
|
+
export declare const EDITOR_MANIFEST_PATH = "config/editor-manifest.generated.json";
|
|
16
|
+
export interface SectionManifestEntry extends SectionSchema {
|
|
17
|
+
/**
|
|
18
|
+
* True when the section has a server-side data loader; settings
|
|
19
|
+
* changes may render stale until the file write round-trips.
|
|
20
|
+
* Enriched at runtime by the bridge (sync.mjs cannot know).
|
|
21
|
+
*/
|
|
22
|
+
hasLoader?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ThemeEditorManifest {
|
|
25
|
+
/** Provenance note ("scripts/sync.mjs — do not edit…"). */
|
|
26
|
+
generatedBy?: string;
|
|
27
|
+
contractVersion: string;
|
|
28
|
+
theme: {
|
|
29
|
+
name: string;
|
|
30
|
+
version: string;
|
|
31
|
+
};
|
|
32
|
+
/** Section schemas keyed by section type. */
|
|
33
|
+
sections: Record<string, SectionManifestEntry>;
|
|
34
|
+
/** Theme block schemas keyed by block type (includes "_" private blocks). */
|
|
35
|
+
blocks: Record<string, ThemeBlockSchema>;
|
|
36
|
+
/** = config/settings_schema.json (global theme settings). */
|
|
37
|
+
settingsSchema: Array<{
|
|
38
|
+
name: string;
|
|
39
|
+
theme_name?: string;
|
|
40
|
+
theme_version?: string;
|
|
41
|
+
settings?: SettingDefinition[];
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}>;
|
|
44
|
+
/** Editable template names ("index", "product", "customers/login", …). */
|
|
45
|
+
templates: string[];
|
|
46
|
+
/** Section group names ("header-group", "footer-group", …). */
|
|
47
|
+
sectionGroups: string[];
|
|
48
|
+
/** Flattened "t:" key -> display copy for the editor's locale. */
|
|
49
|
+
locales: Record<string, string>;
|
|
50
|
+
/** Content hash of everything above, for snapshot/bridge consistency. */
|
|
51
|
+
hash: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a "t:"-prefixed locale key against the manifest's flattened
|
|
55
|
+
* locales; plain strings pass through.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveLocale(value: string, locales: Record<string, string>): string;
|
|
58
|
+
/**
|
|
59
|
+
* Flatten a nested locale-schema object ({labels: {text: "Text"}}) into
|
|
60
|
+
* "labels.text" -> "Text" pairs, the form `ThemeEditorManifest.locales`
|
|
61
|
+
* carries.
|
|
62
|
+
*/
|
|
63
|
+
export declare function flattenLocales(tree: Record<string, unknown>, prefix?: string, out?: Record<string, string>): Record<string, string>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract version spoken by this package. Bumped with the package;
|
|
3
|
+
* editors negotiate compatibility on the major component.
|
|
4
|
+
*/
|
|
5
|
+
export const CONTRACT_VERSION = '0.1.0';
|
|
6
|
+
/** Repo-relative path of the generated manifest inside a theme/project. */
|
|
7
|
+
export const EDITOR_MANIFEST_PATH = 'config/editor-manifest.generated.json';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a "t:"-prefixed locale key against the manifest's flattened
|
|
10
|
+
* locales; plain strings pass through.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveLocale(value, locales) {
|
|
13
|
+
if (!value.startsWith('t:'))
|
|
14
|
+
return value;
|
|
15
|
+
return locales[value.slice(2)] ?? value;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Flatten a nested locale-schema object ({labels: {text: "Text"}}) into
|
|
19
|
+
* "labels.text" -> "Text" pairs, the form `ThemeEditorManifest.locales`
|
|
20
|
+
* carries.
|
|
21
|
+
*/
|
|
22
|
+
export function flattenLocales(tree, prefix = '', out = {}) {
|
|
23
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
24
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
25
|
+
if (typeof value === 'string')
|
|
26
|
+
out[path] = value;
|
|
27
|
+
else if (value && typeof value === 'object') {
|
|
28
|
+
flattenLocales(value, path, out);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — Theme schema contract.
|
|
3
|
+
*
|
|
4
|
+
* The editor-facing description of sections and blocks: what Shopify
|
|
5
|
+
* carries in a section's `{% schema %}` tag. Field semantics follow
|
|
6
|
+
* Shopify OS 2.0 exactly; this file only gives them an explicit,
|
|
7
|
+
* React-side home (components export these objects, and sync.mjs
|
|
8
|
+
* mirrors them out of the Liquid theme's `{% schema %}` tags).
|
|
9
|
+
*/
|
|
10
|
+
import type { BlockData, SectionData, SettingsBag } from './data.ts';
|
|
11
|
+
/**
|
|
12
|
+
* Shopify input setting types, grouped by the tier in which the Zalify
|
|
13
|
+
* canvas editor implements them. Unknown/future types must degrade to a
|
|
14
|
+
* read-only display in the editor — never an error, never value loss.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SETTING_TYPE_TIERS: {
|
|
17
|
+
/** Phase 1 — must render as editable fields. */
|
|
18
|
+
readonly t0: readonly ["text", "textarea", "richtext", "inline_richtext", "number", "range", "checkbox", "select", "radio", "color", "image_picker", "url", "text_alignment"];
|
|
19
|
+
/** Phase 1.5 — theme-level pickers. */
|
|
20
|
+
readonly t1: readonly ["font_picker", "color_background", "color_scheme", "color_scheme_group", "video_url", "html", "link_list"];
|
|
21
|
+
/** Phase 2 — resource pickers backed by storefront data. */
|
|
22
|
+
readonly t2: readonly ["product", "product_list", "collection", "collection_list", "blog", "article", "page", "metaobject", "video"];
|
|
23
|
+
/** Liquid-only; React themes preserve the value but never render it. */
|
|
24
|
+
readonly notApplicable: readonly ["liquid"];
|
|
25
|
+
};
|
|
26
|
+
export type KnownInputSettingType = (typeof SETTING_TYPE_TIERS.t0)[number] | (typeof SETTING_TYPE_TIERS.t1)[number] | (typeof SETTING_TYPE_TIERS.t2)[number] | (typeof SETTING_TYPE_TIERS.notApplicable)[number];
|
|
27
|
+
export declare const SIDEBAR_SETTING_TYPES: readonly ["header", "paragraph"];
|
|
28
|
+
export type SidebarSettingType = (typeof SIDEBAR_SETTING_TYPES)[number];
|
|
29
|
+
/** Non-input schema entries (group headers / help copy). */
|
|
30
|
+
export interface SidebarSetting {
|
|
31
|
+
type: SidebarSettingType;
|
|
32
|
+
content: string;
|
|
33
|
+
/** Shopify conditional-visibility expression. */
|
|
34
|
+
visible_if?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SettingOption {
|
|
37
|
+
value: string;
|
|
38
|
+
label: string;
|
|
39
|
+
group?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* An input setting definition. One open shape rather than a closed
|
|
43
|
+
* discriminated union: this mirrors the JSON reality (Shopify adds
|
|
44
|
+
* setting types over time) and keeps older editors forward-compatible.
|
|
45
|
+
* Type-specific fields are optional and only meaningful for the types
|
|
46
|
+
* that define them (`options` for select/radio, `min`/`max`/`step`/
|
|
47
|
+
* `unit` for range, `placeholder` for text-likes, …).
|
|
48
|
+
*/
|
|
49
|
+
export interface InputSetting {
|
|
50
|
+
type: KnownInputSettingType | (string & {});
|
|
51
|
+
/** Key in the instance's settings bag. */
|
|
52
|
+
id: string;
|
|
53
|
+
/** Supports "t:" locale keys. */
|
|
54
|
+
label: string;
|
|
55
|
+
info?: string;
|
|
56
|
+
default?: unknown;
|
|
57
|
+
visible_if?: string;
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
options?: SettingOption[];
|
|
60
|
+
min?: number;
|
|
61
|
+
max?: number;
|
|
62
|
+
step?: number;
|
|
63
|
+
unit?: string;
|
|
64
|
+
/** range-style sliders on newer setting types. */
|
|
65
|
+
accept?: string[];
|
|
66
|
+
}
|
|
67
|
+
export type SettingDefinition = InputSetting | SidebarSetting;
|
|
68
|
+
export declare function isSidebarSetting(setting: SettingDefinition): setting is SidebarSetting;
|
|
69
|
+
export declare function isInputSetting(setting: SettingDefinition): setting is InputSetting;
|
|
70
|
+
/** Which block types a section (or block) accepts as children. */
|
|
71
|
+
export interface BlockRule {
|
|
72
|
+
/**
|
|
73
|
+
* A concrete block type, or a wildcard: "@theme" (all public theme
|
|
74
|
+
* blocks — types not starting with "_"), "@app" (app blocks).
|
|
75
|
+
*/
|
|
76
|
+
type: string;
|
|
77
|
+
limit?: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A section-local block defined inline in the section's schema
|
|
81
|
+
* (Shopify OS 2.0 "section blocks"): the entry carries `name` (and
|
|
82
|
+
* usually `settings`) instead of referencing a theme block. Local
|
|
83
|
+
* blocks cannot nest further blocks.
|
|
84
|
+
*/
|
|
85
|
+
export interface LocalBlockDefinition extends BlockRule {
|
|
86
|
+
name: string;
|
|
87
|
+
settings?: SettingDefinition[];
|
|
88
|
+
}
|
|
89
|
+
/** An entry in a section schema's `blocks` array: reference or inline. */
|
|
90
|
+
export type SectionBlockEntry = BlockRule | LocalBlockDefinition;
|
|
91
|
+
export declare function isLocalBlockDefinition(entry: SectionBlockEntry): entry is LocalBlockDefinition;
|
|
92
|
+
export interface PresetBlock {
|
|
93
|
+
type: string;
|
|
94
|
+
settings?: SettingsBag;
|
|
95
|
+
blocks?: PresetBlock[];
|
|
96
|
+
/** Static preset blocks may pin their key (Shopify allows both forms). */
|
|
97
|
+
id?: string;
|
|
98
|
+
}
|
|
99
|
+
/** An "add section" catalog entry in the editor. */
|
|
100
|
+
export interface SectionPreset {
|
|
101
|
+
name: string;
|
|
102
|
+
settings?: SettingsBag;
|
|
103
|
+
blocks?: PresetBlock[];
|
|
104
|
+
/** Zalify extension: insert-panel grouping. */
|
|
105
|
+
category?: string;
|
|
106
|
+
}
|
|
107
|
+
export interface TemplateAvailability {
|
|
108
|
+
templates?: string[];
|
|
109
|
+
groups?: string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* = Shopify OS 2.0 section schema, minus Liquid-rendering concerns
|
|
113
|
+
* (tag/class/javascript/stylesheet stay in the Liquid theme).
|
|
114
|
+
*/
|
|
115
|
+
export interface SectionSchema {
|
|
116
|
+
/** Supports "t:" locale keys. */
|
|
117
|
+
name: string;
|
|
118
|
+
settings?: SettingDefinition[];
|
|
119
|
+
/** Allowed child blocks (references or local definitions); absent = none. */
|
|
120
|
+
blocks?: SectionBlockEntry[];
|
|
121
|
+
/** Defaults to Shopify's cap of 50 when blocks are allowed. */
|
|
122
|
+
max_blocks?: number;
|
|
123
|
+
/** Per-template instance cap (e.g. 1 for header-like sections). */
|
|
124
|
+
limit?: number;
|
|
125
|
+
/** No presets = not manually insertable (main-* sections). */
|
|
126
|
+
presets?: SectionPreset[];
|
|
127
|
+
default?: Pick<SectionData, 'settings' | 'blocks' | 'block_order'>;
|
|
128
|
+
enabled_on?: TemplateAvailability;
|
|
129
|
+
disabled_on?: TemplateAvailability;
|
|
130
|
+
}
|
|
131
|
+
/** = Shopify OS 2.0 theme block schema (blocks/*.liquid `{% schema %}`). */
|
|
132
|
+
export interface ThemeBlockSchema {
|
|
133
|
+
name: string;
|
|
134
|
+
settings?: SettingDefinition[];
|
|
135
|
+
/** Theme blocks may nest blocks themselves. */
|
|
136
|
+
blocks?: BlockRule[];
|
|
137
|
+
presets?: Array<{
|
|
138
|
+
name: string;
|
|
139
|
+
settings?: SettingsBag;
|
|
140
|
+
blocks?: PresetBlock[];
|
|
141
|
+
}>;
|
|
142
|
+
}
|
|
143
|
+
/** Shopify's default max_blocks when a section allows blocks. */
|
|
144
|
+
export declare const DEFAULT_MAX_BLOCKS = 50;
|
|
145
|
+
/** Block types starting with "_" are private to their section. */
|
|
146
|
+
export declare function isPrivateBlockType(type: string): boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a parent's block rules against the full block registry:
|
|
149
|
+
* which concrete child types may be inserted?
|
|
150
|
+
*/
|
|
151
|
+
export declare function allowedChildTypes(rules: SectionBlockEntry[] | undefined, allBlockTypes: string[]): string[];
|
|
152
|
+
/** Instantiate a preset's block list into named-map + order form. */
|
|
153
|
+
export declare function instantiatePresetBlocks(presetBlocks: PresetBlock[] | undefined): Pick<BlockData, 'blocks' | 'block_order'>;
|
|
154
|
+
/**
|
|
155
|
+
* Generate a stable, non-colliding key for a newly inserted
|
|
156
|
+
* section/block, following Shopify's editor convention
|
|
157
|
+
* (type with private prefix stripped + counter).
|
|
158
|
+
*/
|
|
159
|
+
export declare function generateKey(type: string, siblings: string[]): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* ------------------------- setting definitions ------------------------- */
|
|
2
|
+
/**
|
|
3
|
+
* Shopify input setting types, grouped by the tier in which the Zalify
|
|
4
|
+
* canvas editor implements them. Unknown/future types must degrade to a
|
|
5
|
+
* read-only display in the editor — never an error, never value loss.
|
|
6
|
+
*/
|
|
7
|
+
export const SETTING_TYPE_TIERS = {
|
|
8
|
+
/** Phase 1 — must render as editable fields. */
|
|
9
|
+
t0: [
|
|
10
|
+
'text',
|
|
11
|
+
'textarea',
|
|
12
|
+
'richtext',
|
|
13
|
+
'inline_richtext',
|
|
14
|
+
'number',
|
|
15
|
+
'range',
|
|
16
|
+
'checkbox',
|
|
17
|
+
'select',
|
|
18
|
+
'radio',
|
|
19
|
+
'color',
|
|
20
|
+
'image_picker',
|
|
21
|
+
'url',
|
|
22
|
+
'text_alignment',
|
|
23
|
+
],
|
|
24
|
+
/** Phase 1.5 — theme-level pickers. */
|
|
25
|
+
t1: [
|
|
26
|
+
'font_picker',
|
|
27
|
+
'color_background',
|
|
28
|
+
'color_scheme',
|
|
29
|
+
'color_scheme_group',
|
|
30
|
+
'video_url',
|
|
31
|
+
'html',
|
|
32
|
+
'link_list',
|
|
33
|
+
],
|
|
34
|
+
/** Phase 2 — resource pickers backed by storefront data. */
|
|
35
|
+
t2: [
|
|
36
|
+
'product',
|
|
37
|
+
'product_list',
|
|
38
|
+
'collection',
|
|
39
|
+
'collection_list',
|
|
40
|
+
'blog',
|
|
41
|
+
'article',
|
|
42
|
+
'page',
|
|
43
|
+
'metaobject',
|
|
44
|
+
'video',
|
|
45
|
+
],
|
|
46
|
+
/** Liquid-only; React themes preserve the value but never render it. */
|
|
47
|
+
notApplicable: ['liquid'],
|
|
48
|
+
};
|
|
49
|
+
export const SIDEBAR_SETTING_TYPES = ['header', 'paragraph'];
|
|
50
|
+
export function isSidebarSetting(setting) {
|
|
51
|
+
return SIDEBAR_SETTING_TYPES.includes(setting.type);
|
|
52
|
+
}
|
|
53
|
+
export function isInputSetting(setting) {
|
|
54
|
+
return !isSidebarSetting(setting);
|
|
55
|
+
}
|
|
56
|
+
export function isLocalBlockDefinition(entry) {
|
|
57
|
+
return 'name' in entry && typeof entry.name === 'string';
|
|
58
|
+
}
|
|
59
|
+
/** Shopify's default max_blocks when a section allows blocks. */
|
|
60
|
+
export const DEFAULT_MAX_BLOCKS = 50;
|
|
61
|
+
/** Block types starting with "_" are private to their section. */
|
|
62
|
+
export function isPrivateBlockType(type) {
|
|
63
|
+
return type.startsWith('_');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a parent's block rules against the full block registry:
|
|
67
|
+
* which concrete child types may be inserted?
|
|
68
|
+
*/
|
|
69
|
+
export function allowedChildTypes(rules, allBlockTypes) {
|
|
70
|
+
if (!rules?.length)
|
|
71
|
+
return [];
|
|
72
|
+
const allowed = new Set();
|
|
73
|
+
for (const rule of rules) {
|
|
74
|
+
if (rule.type === '@theme') {
|
|
75
|
+
for (const type of allBlockTypes) {
|
|
76
|
+
if (!isPrivateBlockType(type))
|
|
77
|
+
allowed.add(type);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (rule.type !== '@app') {
|
|
81
|
+
allowed.add(rule.type);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...allowed];
|
|
85
|
+
}
|
|
86
|
+
/** Instantiate a preset's block list into named-map + order form. */
|
|
87
|
+
export function instantiatePresetBlocks(presetBlocks) {
|
|
88
|
+
if (!presetBlocks?.length)
|
|
89
|
+
return {};
|
|
90
|
+
const blocks = {};
|
|
91
|
+
const order = [];
|
|
92
|
+
const counters = new Map();
|
|
93
|
+
for (const preset of presetBlocks) {
|
|
94
|
+
let key = preset.id;
|
|
95
|
+
if (!key || key in blocks) {
|
|
96
|
+
const base = preset.type.replace(/^_/, '');
|
|
97
|
+
const next = (counters.get(base) ?? 0) + 1;
|
|
98
|
+
counters.set(base, next);
|
|
99
|
+
key = `${base}-${next}`;
|
|
100
|
+
while (key in blocks) {
|
|
101
|
+
const bumped = (counters.get(base) ?? 0) + 1;
|
|
102
|
+
counters.set(base, bumped);
|
|
103
|
+
key = `${base}-${bumped}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const nested = instantiatePresetBlocks(preset.blocks);
|
|
107
|
+
blocks[key] = {
|
|
108
|
+
type: preset.type,
|
|
109
|
+
settings: preset.settings ? { ...preset.settings } : {},
|
|
110
|
+
...nested,
|
|
111
|
+
};
|
|
112
|
+
order.push(key);
|
|
113
|
+
}
|
|
114
|
+
return { blocks, block_order: order };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Generate a stable, non-colliding key for a newly inserted
|
|
118
|
+
* section/block, following Shopify's editor convention
|
|
119
|
+
* (type with private prefix stripped + counter).
|
|
120
|
+
*/
|
|
121
|
+
export function generateKey(type, siblings) {
|
|
122
|
+
const base = type.replace(/^_/, '');
|
|
123
|
+
if (!siblings.includes(base))
|
|
124
|
+
return base;
|
|
125
|
+
let index = 1;
|
|
126
|
+
while (siblings.includes(`${base}-${index}`))
|
|
127
|
+
index += 1;
|
|
128
|
+
return `${base}-${index}`;
|
|
129
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract validators. Pure functions returning issue lists (never
|
|
3
|
+
* throwing): callers decide whether an issue is fatal. Structural
|
|
4
|
+
* invariants always run; schema-aware checks (block whitelists,
|
|
5
|
+
* max_blocks) run only when a manifest is provided, so templates can
|
|
6
|
+
* be validated before the component registry is known.
|
|
7
|
+
*/
|
|
8
|
+
import type { SectionGroupData, TemplateData } from './data.ts';
|
|
9
|
+
import type { ThemeEditorManifest } from './manifest.ts';
|
|
10
|
+
export interface ValidationIssue {
|
|
11
|
+
/** JSON-pointer-ish location, e.g. "sections.hero.blocks.slide". */
|
|
12
|
+
path: string;
|
|
13
|
+
code: 'order-unknown-key' | 'order-duplicate-key' | 'unknown-type' | 'block-not-allowed' | 'max-blocks-exceeded' | 'setting-missing-id' | 'setting-duplicate-id' | 'manifest-missing-field';
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validate a template (or section group) against the L1 invariants,
|
|
18
|
+
* and — when a manifest is given — against the section/block schemas.
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateTemplate(template: TemplateData | SectionGroupData, manifest?: ThemeEditorManifest): ValidationIssue[];
|
|
21
|
+
/** Validate a manifest's own consistency. */
|
|
22
|
+
export declare function validateManifest(manifest: ThemeEditorManifest): ValidationIssue[];
|