@wocart/theme 1.6.0 → 2.0.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/CHANGELOG.md +96 -0
- package/README.md +211 -9
- package/dist/astro/Block.astro +29 -0
- package/dist/astro/Blocks.astro +23 -0
- package/dist/astro/InstanceView.astro +34 -10
- package/dist/astro/ThemeStyles.astro +34 -0
- package/dist/astro/index.d.ts +49 -1
- package/dist/astro/index.js +43 -5
- package/dist/blocks.d.ts +61 -0
- package/dist/blocks.js +144 -0
- package/dist/conditions.d.ts +21 -0
- package/dist/conditions.js +65 -0
- package/dist/definitions.js +399 -29
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -0
- package/dist/instances.d.ts +4 -0
- package/dist/instances.js +72 -4
- package/dist/manifest-diff.js +63 -2
- package/dist/manifest.d.ts +245 -13
- package/dist/manifest.js +64 -1
- package/dist/operations.d.ts +39 -13
- package/dist/operations.js +253 -55
- package/dist/page-data.js +63 -27
- package/dist/preview-bridge.js +10 -7
- package/dist/preview-protocol.d.ts +8 -3
- package/dist/rich-text.d.ts +2 -0
- package/dist/rich-text.js +4 -0
- package/dist/schemas.d.ts +2 -1
- package/dist/schemas.js +3 -1
- package/dist/settings.js +20 -1
- package/dist/style.d.ts +45 -0
- package/dist/style.js +248 -0
- package/dist/surface-document.d.ts +49 -0
- package/dist/surface-document.js +162 -0
- package/dist/types.d.ts +217 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ComponentInstance, PageData, PageDataIssue, ResolutionScope, ThemeDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The document with everything it must hold: the surface's required sections,
|
|
4
|
+
* each component's fixed blocks (stored, with an id derived from its owner and
|
|
5
|
+
* name) and each slot's required blocks — a hidden required or fixed block is
|
|
6
|
+
* shown again. Added ids are unique in the document. What was added or
|
|
7
|
+
* corrected is reported.
|
|
8
|
+
*/
|
|
9
|
+
export declare function completePage(theme: ThemeDefinition, page: PageData): {
|
|
10
|
+
page: PageData;
|
|
11
|
+
issues: PageDataIssue[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* An instance tree with its fixed and required blocks, as an editor inserts
|
|
15
|
+
* it: added ids are unique against `taken`, which receives every id of the
|
|
16
|
+
* tree. Never changes `instance`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function completeInstance(theme: ThemeDefinition, instance: ComponentInstance, taken: Set<string>): ComponentInstance;
|
|
19
|
+
/** Why the theme's default document rendered instead of the saved one. */
|
|
20
|
+
export type SurfaceDocumentReason =
|
|
21
|
+
/** The store saved none. */
|
|
22
|
+
"absent"
|
|
23
|
+
/** What was saved is not a page document this SDK reads. */
|
|
24
|
+
| "invalid"
|
|
25
|
+
/** The saved document is for another surface. */
|
|
26
|
+
| "other-surface"
|
|
27
|
+
/** The saved document was composed for another theme. */
|
|
28
|
+
| "other-theme"
|
|
29
|
+
/** None of the saved sections is in this theme's catalog. */
|
|
30
|
+
| "no-overlap";
|
|
31
|
+
export interface SurfaceDocument {
|
|
32
|
+
/** The document to render, completed. */
|
|
33
|
+
page: PageData;
|
|
34
|
+
source: "saved" | "default";
|
|
35
|
+
/** `null` when the saved document rendered. */
|
|
36
|
+
reason: SurfaceDocumentReason | null;
|
|
37
|
+
/** The saved document's issues, then the default's when it rendered instead. */
|
|
38
|
+
issues: PageDataIssue[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The page a surface or area renders: the saved document when this theme can
|
|
42
|
+
* render it, else the theme's default (`SurfaceSpec.document` or
|
|
43
|
+
* `AreaSpec.document`, or an empty page). A saved document that names another theme, or whose sections
|
|
44
|
+
* this catalog knows none of, renders the default — a theme switch must not
|
|
45
|
+
* leave an empty page. A deliberately emptied document renders empty. The
|
|
46
|
+
* result is completed (`completePage`), so required and fixed blocks render
|
|
47
|
+
* whatever was saved.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveSurfaceDocument(theme: ThemeDefinition, surface: string, saved: unknown, scope?: ResolutionScope): SurfaceDocument;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* From what a store saved to the page that renders, for any surface or area
|
|
3
|
+
* (an area's document is the surface `area:<key>`):
|
|
4
|
+
* which document wins (the saved one, or the theme's default), and that
|
|
5
|
+
* document completed with every fixed and required block. A theme
|
|
6
|
+
* renders `resolveSurfaceDocument(...).page`; an editor edits
|
|
7
|
+
* `completePage(...).page`. Both are pure and never change their input.
|
|
8
|
+
*/
|
|
9
|
+
import { THEME_LIMITS, completeRequired, fixedBlockId, shippedDocument, slotCap, surfaceRules } from "./blocks.js";
|
|
10
|
+
import { parsePageData, validatePageData } from "./page-data.js";
|
|
11
|
+
/**
|
|
12
|
+
* The document with everything it must hold: the surface's required sections,
|
|
13
|
+
* each component's fixed blocks (stored, with an id derived from its owner and
|
|
14
|
+
* name) and each slot's required blocks — a hidden required or fixed block is
|
|
15
|
+
* shown again. Added ids are unique in the document. What was added or
|
|
16
|
+
* corrected is reported.
|
|
17
|
+
*/
|
|
18
|
+
export function completePage(theme, page) {
|
|
19
|
+
const next = structuredClone(page);
|
|
20
|
+
const issues = [];
|
|
21
|
+
const taken = new Set();
|
|
22
|
+
const collect = (node) => {
|
|
23
|
+
taken.add(node.id);
|
|
24
|
+
for (const child of node.children ?? [])
|
|
25
|
+
collect(child);
|
|
26
|
+
for (const list of Object.values(node.slots ?? {}))
|
|
27
|
+
for (const child of list)
|
|
28
|
+
collect(child);
|
|
29
|
+
for (const block of Object.values(node.fixed ?? {}))
|
|
30
|
+
collect(block);
|
|
31
|
+
};
|
|
32
|
+
next.sections.forEach(collect);
|
|
33
|
+
const spec = surfaceRules(theme, next.surface);
|
|
34
|
+
next.sections = completeRequired(next.sections, spec?.required, { id: next.surface, slot: "sections" }, "sections", issues, taken);
|
|
35
|
+
next.sections.forEach((section, index) => completeTree(theme, section, `sections[${index}]`, issues, taken, 0));
|
|
36
|
+
return { page: next, issues };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* An instance tree with its fixed and required blocks, as an editor inserts
|
|
40
|
+
* it: added ids are unique against `taken`, which receives every id of the
|
|
41
|
+
* tree. Never changes `instance`.
|
|
42
|
+
*/
|
|
43
|
+
export function completeInstance(theme, instance, taken) {
|
|
44
|
+
const next = structuredClone(instance);
|
|
45
|
+
const collect = (node) => {
|
|
46
|
+
taken.add(node.id);
|
|
47
|
+
for (const child of node.children ?? [])
|
|
48
|
+
collect(child);
|
|
49
|
+
for (const list of Object.values(node.slots ?? {}))
|
|
50
|
+
for (const child of list)
|
|
51
|
+
collect(child);
|
|
52
|
+
for (const block of Object.values(node.fixed ?? {}))
|
|
53
|
+
collect(block);
|
|
54
|
+
};
|
|
55
|
+
collect(next);
|
|
56
|
+
completeTree(theme, next, "instance", [], taken, 0);
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
59
|
+
function uniqueId(base, taken) {
|
|
60
|
+
let id = base;
|
|
61
|
+
for (let n = 2; taken.has(id); n += 1)
|
|
62
|
+
id = `${base}~${n}`;
|
|
63
|
+
taken.add(id);
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
function completeTree(theme, node, path, issues, taken, depth) {
|
|
67
|
+
const definition = Object.hasOwn(theme.components, node.type) ? theme.components[node.type] : undefined;
|
|
68
|
+
if (definition === undefined || depth > THEME_LIMITS.depth)
|
|
69
|
+
return;
|
|
70
|
+
const declared = definition.fixed ?? [];
|
|
71
|
+
const stored = node.fixed ?? {};
|
|
72
|
+
for (const name of Object.keys(stored)) {
|
|
73
|
+
if (!declared.some((block) => block.name === name)) {
|
|
74
|
+
issues.push({ path: `${path}.fixed.${name}`, code: "unknown-fixed", message: `"${node.type}" places no fixed block "${name}"; removed` });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (declared.length > 0) {
|
|
78
|
+
const fixed = {};
|
|
79
|
+
for (const { name, type } of declared) {
|
|
80
|
+
const entry = Object.hasOwn(stored, name) ? stored[name] : undefined;
|
|
81
|
+
if (entry !== undefined && entry.type === type) {
|
|
82
|
+
delete entry.hidden;
|
|
83
|
+
fixed[name] = entry;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (entry !== undefined) {
|
|
87
|
+
issues.push({ path: `${path}.fixed.${name}.type`, code: "fixed-type-mismatch", message: `the fixed block "${name}" is a "${type}", not a "${entry.type}"; replaced by one with its defaults` });
|
|
88
|
+
}
|
|
89
|
+
fixed[name] = { id: uniqueId(fixedBlockId(node.id, name), taken), type };
|
|
90
|
+
}
|
|
91
|
+
node.fixed = fixed;
|
|
92
|
+
}
|
|
93
|
+
else if (node.fixed !== undefined) {
|
|
94
|
+
delete node.fixed;
|
|
95
|
+
}
|
|
96
|
+
for (const slot of definition.slots ?? []) {
|
|
97
|
+
if ((slot.required ?? []).length === 0)
|
|
98
|
+
continue;
|
|
99
|
+
const before = issues.length;
|
|
100
|
+
if (slot.name === "default") {
|
|
101
|
+
const list = [...(node.children ?? []), ...(node.slots?.default ?? [])];
|
|
102
|
+
const done = completeRequired(list, slot.required, { id: node.id, slot: "default" }, `${path}.children`, issues, taken);
|
|
103
|
+
if (issues.length === before)
|
|
104
|
+
continue;
|
|
105
|
+
node.children = done;
|
|
106
|
+
if (node.slots?.default !== undefined) {
|
|
107
|
+
delete node.slots.default;
|
|
108
|
+
if (Object.keys(node.slots).length === 0)
|
|
109
|
+
delete node.slots;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const list = node.slots !== undefined && Object.hasOwn(node.slots, slot.name) ? node.slots[slot.name] : [];
|
|
114
|
+
const done = completeRequired(list, slot.required, { id: node.id, slot: slot.name }, `${path}.slots.${slot.name}`, issues, taken);
|
|
115
|
+
if (issues.length === before)
|
|
116
|
+
continue;
|
|
117
|
+
node.slots = { ...node.slots, [slot.name]: done };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
(node.children ?? []).forEach((child, index) => completeTree(theme, child, `${path}.children[${index}]`, issues, taken, depth + 1));
|
|
121
|
+
for (const [name, list] of Object.entries(node.slots ?? {})) {
|
|
122
|
+
list.forEach((child, index) => completeTree(theme, child, `${path}.slots.${name}[${index}]`, issues, taken, depth + 1));
|
|
123
|
+
}
|
|
124
|
+
for (const [name, block] of Object.entries(node.fixed ?? {}))
|
|
125
|
+
completeTree(theme, block, `${path}.fixed.${name}`, issues, taken, depth + 1);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The page a surface or area renders: the saved document when this theme can
|
|
129
|
+
* render it, else the theme's default (`SurfaceSpec.document` or
|
|
130
|
+
* `AreaSpec.document`, or an empty page). A saved document that names another theme, or whose sections
|
|
131
|
+
* this catalog knows none of, renders the default — a theme switch must not
|
|
132
|
+
* leave an empty page. A deliberately emptied document renders empty. The
|
|
133
|
+
* result is completed (`completePage`), so required and fixed blocks render
|
|
134
|
+
* whatever was saved.
|
|
135
|
+
*/
|
|
136
|
+
export function resolveSurfaceDocument(theme, surface, saved, scope = {}) {
|
|
137
|
+
const fallback = (reason, savedIssues) => {
|
|
138
|
+
const shipped = parsePageData(shippedDocument(theme, surface) ?? { version: 1, surface, sections: [] });
|
|
139
|
+
const page = shipped.page ?? { version: 1, surface, sections: [] };
|
|
140
|
+
return { page: completePage(theme, page).page, source: "default", reason, issues: [...savedIssues, ...shipped.issues] };
|
|
141
|
+
};
|
|
142
|
+
if (saved === null || saved === undefined)
|
|
143
|
+
return fallback("absent", []);
|
|
144
|
+
const checked = validatePageData(theme, saved, scope);
|
|
145
|
+
if (checked.page === null)
|
|
146
|
+
return fallback("invalid", checked.issues);
|
|
147
|
+
if (checked.page.surface !== surface) {
|
|
148
|
+
return fallback("other-surface", [
|
|
149
|
+
...checked.issues,
|
|
150
|
+
{ path: "surface", code: "envelope-invalid", message: `the saved document is for "${checked.page.surface}", not "${surface}"` },
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
if (checked.page.theme !== undefined && checked.page.theme.key !== theme.key)
|
|
154
|
+
return fallback("other-theme", checked.issues);
|
|
155
|
+
const sections = checked.page.sections;
|
|
156
|
+
if (sections.length > 0 && sections.every((section) => !Object.hasOwn(theme.components, section.type))) {
|
|
157
|
+
return fallback("no-overlap", checked.issues);
|
|
158
|
+
}
|
|
159
|
+
const cap = slotCap(surfaceRules(theme, surface), THEME_LIMITS.sections);
|
|
160
|
+
const renderable = { ...checked.page, sections: sections.slice(0, cap) };
|
|
161
|
+
return { page: completePage(theme, renderable).page, source: "saved", reason: null, issues: checked.issues };
|
|
162
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -41,7 +41,28 @@ export interface ResolutionScope {
|
|
|
41
41
|
* reader that meets a kind it does not know rejects the manifest rather than
|
|
42
42
|
* render a value it cannot check.
|
|
43
43
|
*/
|
|
44
|
-
export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url" | "color" | "menu" | "collection" | "image" | "richtext" | "list" | "product" | "category";
|
|
44
|
+
export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url" | "color" | "menu" | "collection" | "image" | "richtext" | "list" | "product" | "category" | "style" | "colorScheme" | "colorSchemes" | "productList" | "collectionList";
|
|
45
|
+
/** A value a condition compares against: a field's value is one of these when it is set. */
|
|
46
|
+
export type ConditionValue = string | number | boolean | null;
|
|
47
|
+
/**
|
|
48
|
+
* When a field applies, from the values of the fields beside it (the same
|
|
49
|
+
* component's, list item's or settings'): equal to a value, one of several,
|
|
50
|
+
* all or any of other conditions, or not one. A closed format, never an
|
|
51
|
+
* expression; `isFieldVisible` evaluates it. A hidden field keeps its value.
|
|
52
|
+
*/
|
|
53
|
+
export type FieldCondition = {
|
|
54
|
+
field: string;
|
|
55
|
+
equals: ConditionValue;
|
|
56
|
+
} | {
|
|
57
|
+
field: string;
|
|
58
|
+
in: readonly ConditionValue[];
|
|
59
|
+
} | {
|
|
60
|
+
all: readonly FieldCondition[];
|
|
61
|
+
} | {
|
|
62
|
+
any: readonly FieldCondition[];
|
|
63
|
+
} | {
|
|
64
|
+
not: FieldCondition;
|
|
65
|
+
};
|
|
45
66
|
interface FieldDefinitionBase {
|
|
46
67
|
/** The exact prop name the component expects. */
|
|
47
68
|
key: string;
|
|
@@ -50,6 +71,8 @@ interface FieldDefinitionBase {
|
|
|
50
71
|
/** Editor-facing help text. */
|
|
51
72
|
description?: LocalizedText;
|
|
52
73
|
required?: boolean;
|
|
74
|
+
/** When an editor shows the field; absent: always. The renderer ignores it. */
|
|
75
|
+
visibleIf?: FieldCondition;
|
|
53
76
|
}
|
|
54
77
|
/**
|
|
55
78
|
* `text` / `textarea`: a string that defaults from the content pack
|
|
@@ -83,6 +106,8 @@ export interface NumberFieldDefinition extends FieldDefinitionBase {
|
|
|
83
106
|
min?: number;
|
|
84
107
|
max?: number;
|
|
85
108
|
step?: number;
|
|
109
|
+
/** How an editor shows it: a box to type in (default) or a slider, which needs `min`, `max` and `step`. */
|
|
110
|
+
display?: "input" | "slider";
|
|
86
111
|
}
|
|
87
112
|
/** `boolean`. */
|
|
88
113
|
export interface BooleanFieldDefinition extends FieldDefinitionBase {
|
|
@@ -99,6 +124,8 @@ export interface SelectFieldDefinition extends FieldDefinitionBase {
|
|
|
99
124
|
options: readonly string[];
|
|
100
125
|
optionLabels?: Readonly<Record<string, LocalizedText>>;
|
|
101
126
|
default?: string;
|
|
127
|
+
/** How an editor shows the options: a dropdown (default), radio buttons, or one segmented control. */
|
|
128
|
+
display?: "dropdown" | "radio" | "segmented";
|
|
102
129
|
}
|
|
103
130
|
/**
|
|
104
131
|
* `url`: a link target an editor may set, safe to render as an `href`. Only a
|
|
@@ -178,9 +205,102 @@ export interface RichTextFieldDefinition extends FieldDefinitionBase {
|
|
|
178
205
|
kind: "richtext";
|
|
179
206
|
formats?: readonly RichTextFormat[];
|
|
180
207
|
default?: RichTextValue;
|
|
208
|
+
/**
|
|
209
|
+
* `true`: one line of text — a single paragraph with marks and links, no
|
|
210
|
+
* headings or lists — rendered without a block element
|
|
211
|
+
* (`richTextToHtml(doc, { inline: true })`), for a title with one bold word.
|
|
212
|
+
*/
|
|
213
|
+
inline?: boolean;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* `productList`: hand-picked products, in order — product handles, each read
|
|
217
|
+
* with the storefront client's `products.get(handle)`. A renamed or deleted
|
|
218
|
+
* product reads as absent. At most `max` (at most 50).
|
|
219
|
+
*/
|
|
220
|
+
export interface ProductListFieldDefinition extends FieldDefinitionBase {
|
|
221
|
+
kind: "productList";
|
|
222
|
+
min?: number;
|
|
223
|
+
max?: number;
|
|
224
|
+
default?: readonly string[];
|
|
225
|
+
}
|
|
226
|
+
/** `collectionList`: hand-picked collections, in order — collection handles. At most `max` (at most 50). */
|
|
227
|
+
export interface CollectionListFieldDefinition extends FieldDefinitionBase {
|
|
228
|
+
kind: "collectionList";
|
|
229
|
+
min?: number;
|
|
230
|
+
max?: number;
|
|
231
|
+
default?: readonly string[];
|
|
232
|
+
}
|
|
233
|
+
/** The platform's breakpoints, phone first: `base` everywhere, `md` from 768 px, `lg` from 1024 px. */
|
|
234
|
+
export type Breakpoint = "base" | "md" | "lg";
|
|
235
|
+
/** A value per breakpoint; a wider one inherits from the narrower. */
|
|
236
|
+
export type Responsive<T> = {
|
|
237
|
+
base: T;
|
|
238
|
+
} & Partial<Record<Exclude<Breakpoint, "base">, T>>;
|
|
239
|
+
/** The platform's step names; a theme gives the ones it offers a size (`ThemeStyle.spacing`). */
|
|
240
|
+
export type StyleStep = "s0" | "s1" | "s2" | "s3" | "s4" | "s5" | "s6" | "s7" | "s8" | "s9" | "s10" | "s11" | "s12";
|
|
241
|
+
/** The widths a `size` panel offers besides `full`; a theme gives each a maximum (`ThemeStyle.widths`). */
|
|
242
|
+
export type StyleWidth = "wide" | "content" | "narrow";
|
|
243
|
+
/** What a `style` field may offer, each a closed set of CSS logical properties. */
|
|
244
|
+
export type StylePanel = "spacing" | "layout" | "size";
|
|
245
|
+
export interface StyleValue {
|
|
246
|
+
spacing?: {
|
|
247
|
+
paddingBlockStart?: Responsive<StyleStep>;
|
|
248
|
+
paddingBlockEnd?: Responsive<StyleStep>;
|
|
249
|
+
paddingInline?: Responsive<StyleStep>;
|
|
250
|
+
marginBlockStart?: Responsive<StyleStep>;
|
|
251
|
+
marginBlockEnd?: Responsive<StyleStep>;
|
|
252
|
+
gap?: Responsive<StyleStep>;
|
|
253
|
+
};
|
|
254
|
+
layout?: {
|
|
255
|
+
direction?: Responsive<"row" | "column">;
|
|
256
|
+
align?: Responsive<"start" | "center" | "end" | "stretch">;
|
|
257
|
+
justify?: Responsive<"start" | "center" | "end" | "between">;
|
|
258
|
+
wrap?: Responsive<boolean>;
|
|
259
|
+
};
|
|
260
|
+
size?: {
|
|
261
|
+
width?: Responsive<"full" | StyleWidth>;
|
|
262
|
+
minHeight?: Responsive<StyleStep>;
|
|
263
|
+
};
|
|
181
264
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
265
|
+
/**
|
|
266
|
+
* `style`: how a section or block sits on the page — spacing, layout, size —
|
|
267
|
+
* as steps and choices, never CSS. `styleClasses(value)` turns a value into
|
|
268
|
+
* the classes of the stylesheet the SDK makes from the theme's scales.
|
|
269
|
+
*/
|
|
270
|
+
export interface StyleFieldDefinition extends FieldDefinitionBase {
|
|
271
|
+
kind: "style";
|
|
272
|
+
panels: readonly StylePanel[];
|
|
273
|
+
default?: StyleValue;
|
|
274
|
+
}
|
|
275
|
+
/** The roles every color scheme gives a color, the same in every theme. */
|
|
276
|
+
export type ColorRole = "background" | "text" | "heading" | "accent" | "button" | "buttonText" | "border";
|
|
277
|
+
/** One named set of colors a section or block may wear. */
|
|
278
|
+
export interface ColorScheme {
|
|
279
|
+
/** Lowercase letters, digits and dashes, at most 32; what a `colorScheme` value names. */
|
|
280
|
+
id: string;
|
|
281
|
+
name: LocalizedText;
|
|
282
|
+
colors: Readonly<Record<ColorRole, string>>;
|
|
283
|
+
}
|
|
284
|
+
/** `colorScheme`: which of the store's color schemes a section or block wears — a scheme's id. */
|
|
285
|
+
export interface ColorSchemeFieldDefinition extends FieldDefinitionBase {
|
|
286
|
+
kind: "colorScheme";
|
|
287
|
+
default?: string;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* `colorSchemes`: the store's color schemes, in order — a theme setting, at
|
|
291
|
+
* most one per theme. Its default is the theme's own schemes.
|
|
292
|
+
*/
|
|
293
|
+
export interface ColorSchemesFieldDefinition extends FieldDefinitionBase {
|
|
294
|
+
kind: "colorSchemes";
|
|
295
|
+
default: readonly ColorScheme[];
|
|
296
|
+
}
|
|
297
|
+
/** The sizes a theme gives the platform's style steps, in pixels. */
|
|
298
|
+
export interface ThemeStyle {
|
|
299
|
+
spacing?: Readonly<Partial<Record<StyleStep, number>>>;
|
|
300
|
+
widths?: Readonly<Partial<Record<StyleWidth, number>>>;
|
|
301
|
+
}
|
|
302
|
+
/** A field a list item may hold: any kind but `list` itself and the store's color schemes. */
|
|
303
|
+
export type ListItemFieldDefinition = Exclude<FieldDefinition, ListFieldDefinition | ColorSchemesFieldDefinition>;
|
|
184
304
|
/**
|
|
185
305
|
* `list`: repeatable items — trust badges, questions and answers, slides. Each
|
|
186
306
|
* stored item is `{ id, props }`: the id is unique within the list, so
|
|
@@ -197,7 +317,7 @@ export interface ListFieldDefinition extends FieldDefinitionBase {
|
|
|
197
317
|
/** The item field (`text` or `textarea`) an editor shows as each item's title. */
|
|
198
318
|
itemLabel?: string;
|
|
199
319
|
}
|
|
200
|
-
export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition | ColorFieldDefinition | MenuFieldDefinition | CollectionFieldDefinition | ProductFieldDefinition | CategoryFieldDefinition | ImageFieldDefinition | RichTextFieldDefinition | ListFieldDefinition;
|
|
320
|
+
export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition | ColorFieldDefinition | MenuFieldDefinition | CollectionFieldDefinition | ProductFieldDefinition | CategoryFieldDefinition | ImageFieldDefinition | RichTextFieldDefinition | ListFieldDefinition | StyleFieldDefinition | ColorSchemeFieldDefinition | ColorSchemesFieldDefinition | ProductListFieldDefinition | CollectionListFieldDefinition;
|
|
201
321
|
/**
|
|
202
322
|
* A stored image: the object's key in the store's media bucket
|
|
203
323
|
* (`products/3f2a….webp`), never a URL, plus what the editor knows about it.
|
|
@@ -303,7 +423,45 @@ export interface SlotDefinition {
|
|
|
303
423
|
* a slot.
|
|
304
424
|
*/
|
|
305
425
|
allow?: readonly string[];
|
|
426
|
+
/** At most this many instances (1–50). Absent: the platform's cap (`THEME_LIMITS.slotInstances`). */
|
|
427
|
+
max?: number;
|
|
428
|
+
/**
|
|
429
|
+
* Types that must be present: an editor cannot remove, hide or move out the
|
|
430
|
+
* last `min` of them, nor insert past `max`, and a document that lacks them
|
|
431
|
+
* renders them anyway (`completePage`). Each type must be one `allow` takes.
|
|
432
|
+
*/
|
|
433
|
+
required?: readonly RequiredBlock[];
|
|
306
434
|
}
|
|
435
|
+
/** A type a slot or surface must hold: at least `min` (default 1), at most `max`. */
|
|
436
|
+
export interface RequiredBlock {
|
|
437
|
+
type: string;
|
|
438
|
+
min?: number;
|
|
439
|
+
max?: number;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Where an editor may place a component: `"section"` at the top level of a
|
|
443
|
+
* surface or area, `"block"` only inside a slot, `"both"` anywhere. Absent:
|
|
444
|
+
* anywhere. Enforced by the document operations, never at render.
|
|
445
|
+
*/
|
|
446
|
+
export type ComponentPlacement = "section" | "block" | "both";
|
|
447
|
+
/**
|
|
448
|
+
* A block the component's own code places with `<Block of={blocks} name>`:
|
|
449
|
+
* an editor changes its fields and fills its slots, but never removes, moves,
|
|
450
|
+
* duplicates or hides it. It renders even when the document does not store it.
|
|
451
|
+
*/
|
|
452
|
+
export interface FixedBlockDefinition {
|
|
453
|
+
/** Lowercase kebab-case, unique among the component's fixed blocks. */
|
|
454
|
+
name: string;
|
|
455
|
+
/** The component type rendered there (a catalog key). */
|
|
456
|
+
type: string;
|
|
457
|
+
/** Editor-facing name; the type's label when absent. */
|
|
458
|
+
label?: LocalizedText;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* What a component is to the platform. `"order-form"`: it places orders —
|
|
462
|
+
* every product surface must guarantee one (`validateThemeDefinition`).
|
|
463
|
+
*/
|
|
464
|
+
export type ComponentRole = "order-form";
|
|
307
465
|
/**
|
|
308
466
|
* What a component permits.
|
|
309
467
|
*
|
|
@@ -420,6 +578,9 @@ export interface ComponentDefinition {
|
|
|
420
578
|
load?: LoadFn;
|
|
421
579
|
capabilities?: ComponentCapabilities;
|
|
422
580
|
presets?: readonly ComponentPreset[];
|
|
581
|
+
placement?: ComponentPlacement;
|
|
582
|
+
fixed?: readonly FixedBlockDefinition[];
|
|
583
|
+
role?: ComponentRole;
|
|
423
584
|
}
|
|
424
585
|
/**
|
|
425
586
|
* The value type one field resolves to. A select narrows to its option
|
|
@@ -435,6 +596,12 @@ export type FieldValue<F extends FieldDefinition> = F extends {
|
|
|
435
596
|
} ? Option : F extends {
|
|
436
597
|
kind: "image";
|
|
437
598
|
} ? ResolvedImage : F extends {
|
|
599
|
+
kind: "style";
|
|
600
|
+
} ? StyleValue : F extends {
|
|
601
|
+
kind: "colorSchemes";
|
|
602
|
+
} ? ColorScheme[] : F extends {
|
|
603
|
+
kind: "productList" | "collectionList";
|
|
604
|
+
} ? string[] : F extends {
|
|
438
605
|
kind: "richtext";
|
|
439
606
|
} ? RichTextDocument : F extends {
|
|
440
607
|
kind: "list";
|
|
@@ -494,6 +661,8 @@ export interface ThemeDefinition {
|
|
|
494
661
|
* offer only valid surfaces.
|
|
495
662
|
*/
|
|
496
663
|
surfaces?: readonly SurfaceSpec[];
|
|
664
|
+
/** The parts every page shares, each a document of sections (`area:<key>`). */
|
|
665
|
+
areas?: readonly AreaSpec[];
|
|
497
666
|
/**
|
|
498
667
|
* The languages this theme's content packs cover (BCP 47), e.g.
|
|
499
668
|
* `["ar", "fr", "en"]`. Declared, the validator requires every translated
|
|
@@ -509,6 +678,12 @@ export interface ThemeDefinition {
|
|
|
509
678
|
* with `checkThemeSettings`. Setting keys are unique across all groups.
|
|
510
679
|
*/
|
|
511
680
|
settings?: readonly SettingsGroup[];
|
|
681
|
+
/**
|
|
682
|
+
* The sizes this theme gives the platform's style steps and widths. A
|
|
683
|
+
* `style` field offers only what is given here; the SDK's stylesheet
|
|
684
|
+
* (`themeStylesEndpoint`) is made from it.
|
|
685
|
+
*/
|
|
686
|
+
style?: ThemeStyle;
|
|
512
687
|
/**
|
|
513
688
|
* The theme's own text, one pack per language: what a `contentKey` field
|
|
514
689
|
* shows until the merchant edits it, and the store-wide labels a merchant
|
|
@@ -548,6 +723,26 @@ export interface SurfaceSpec {
|
|
|
548
723
|
pageType?: SurfacePageType;
|
|
549
724
|
/** The theme's shipped document for this surface: what renders before the merchant saves one. */
|
|
550
725
|
document?: PageData;
|
|
726
|
+
/** Top-level sections the surface must hold, as a slot's `required`. Alternates inherit their base's. */
|
|
727
|
+
required?: readonly RequiredBlock[];
|
|
728
|
+
/** At most this many top-level sections (1–50). Absent: `THEME_LIMITS.sections`. */
|
|
729
|
+
max?: number;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* A part every page shares — a header, a footer — composed of sections like a
|
|
733
|
+
* page and edited once for the whole store. Its document is stored as the
|
|
734
|
+
* surface `area:<key>`.
|
|
735
|
+
*/
|
|
736
|
+
export interface AreaSpec {
|
|
737
|
+
/** Lowercase kebab-case, unique among the theme's areas. */
|
|
738
|
+
key: string;
|
|
739
|
+
label: LocalizedText;
|
|
740
|
+
/** The theme's shipped document for the area (surface `area:<key>`). */
|
|
741
|
+
document?: PageData;
|
|
742
|
+
/** Sections the area must hold, as a surface's `required`. */
|
|
743
|
+
required?: readonly RequiredBlock[];
|
|
744
|
+
/** At most this many sections (1–50). */
|
|
745
|
+
max?: number;
|
|
551
746
|
}
|
|
552
747
|
/**
|
|
553
748
|
* The storefront's page types — what a URL means, with the paths the platform
|
|
@@ -574,6 +769,9 @@ export interface ComponentSpec {
|
|
|
574
769
|
slots: readonly SlotDefinition[];
|
|
575
770
|
capabilities?: ComponentCapabilities;
|
|
576
771
|
presets: readonly ComponentPreset[];
|
|
772
|
+
placement?: ComponentPlacement;
|
|
773
|
+
fixed?: readonly FixedBlockDefinition[];
|
|
774
|
+
role?: ComponentRole;
|
|
577
775
|
}
|
|
578
776
|
/**
|
|
579
777
|
* The serializable theme contract.
|
|
@@ -599,10 +797,14 @@ export interface ThemeManifest {
|
|
|
599
797
|
*/
|
|
600
798
|
sdkVersion?: string;
|
|
601
799
|
surfaces: readonly SurfaceSpec[];
|
|
800
|
+
/** The parts every page shares (possibly none). */
|
|
801
|
+
areas: readonly AreaSpec[];
|
|
602
802
|
/** The languages the theme's packs cover (possibly none declared). */
|
|
603
803
|
locales: readonly LocaleCode[];
|
|
604
804
|
/** The theme's global settings (possibly none). */
|
|
605
805
|
settings: readonly SettingsGroup[];
|
|
806
|
+
/** The sizes the theme gives the style steps (absent: none). */
|
|
807
|
+
style?: ThemeStyle;
|
|
606
808
|
/** The theme's content packs, by language (possibly none). */
|
|
607
809
|
content: Readonly<Record<LocaleCode, Readonly<Record<string, string>>>>;
|
|
608
810
|
/** Whether the theme serves a preview route (`THEME_PREVIEW_PATH`). */
|
|
@@ -635,9 +837,15 @@ export interface ComponentInstance {
|
|
|
635
837
|
props?: JsonObject;
|
|
636
838
|
children?: ComponentInstance[];
|
|
637
839
|
slots?: Record<string, ComponentInstance[]>;
|
|
840
|
+
/**
|
|
841
|
+
* The values of the component's fixed blocks, by the name its definition
|
|
842
|
+
* gives each. Absent entries render with their defaults.
|
|
843
|
+
*/
|
|
844
|
+
fixed?: Record<string, ComponentInstance>;
|
|
638
845
|
/**
|
|
639
846
|
* `true`: kept in the document but not rendered — the merchant switched the
|
|
640
|
-
* section off. Its children are not rendered either.
|
|
847
|
+
* section off. Its children are not rendered either. Ignored on a fixed
|
|
848
|
+
* block and on a required block a slot would otherwise lack.
|
|
641
849
|
*/
|
|
642
850
|
hidden?: boolean;
|
|
643
851
|
}
|
|
@@ -669,7 +877,7 @@ export interface PageData {
|
|
|
669
877
|
version: string;
|
|
670
878
|
};
|
|
671
879
|
}
|
|
672
|
-
export type PageDataIssueCode = "envelope-invalid" | "instance-dropped" | "duplicate-id" | "unknown-type" | "unknown-prop" | "invalid-prop" | "missing-required-prop" | "undeclared-children" | "unknown-slot" | "slot-not-multiple" | "disallowed-child" | "invalid-provenance" | "theme-mismatch";
|
|
880
|
+
export type PageDataIssueCode = "envelope-invalid" | "instance-dropped" | "duplicate-id" | "unknown-type" | "unknown-prop" | "invalid-prop" | "missing-required-prop" | "undeclared-children" | "unknown-slot" | "slot-not-multiple" | "disallowed-child" | "invalid-provenance" | "theme-mismatch" | "unknown-fixed" | "fixed-type-mismatch" | "missing-required-block" | "limit-exceeded";
|
|
673
881
|
/** One reported degradation; `path` points into the document (e.g. `sections[0].props.limit`). */
|
|
674
882
|
export interface PageDataIssue {
|
|
675
883
|
path: string;
|
|
@@ -686,9 +894,11 @@ export interface ResolvedInstance {
|
|
|
686
894
|
props: Record<string, unknown>;
|
|
687
895
|
children: ComponentInstance[];
|
|
688
896
|
slots: Record<string, ComponentInstance[]>;
|
|
897
|
+
/** Every fixed block the definition declares, stored or with its defaults, by name. */
|
|
898
|
+
fixed: Record<string, ComponentInstance>;
|
|
689
899
|
issues: PageDataIssue[];
|
|
690
900
|
}
|
|
691
|
-
export type ThemeDefinitionIssueCode = "invalid-theme" | "invalid-settings" | "invalid-definition" | "invalid-type" | "type-mismatch" | "invalid-field" | "duplicate-field-key" | "unknown-content-key" | "invalid-slot" | "duplicate-slot" | "invalid-capability" | "invalid-load" | "invalid-preset" | "invalid-document" | "invalid-content";
|
|
901
|
+
export type ThemeDefinitionIssueCode = "invalid-theme" | "invalid-settings" | "invalid-definition" | "invalid-type" | "type-mismatch" | "invalid-field" | "duplicate-field-key" | "unknown-content-key" | "invalid-slot" | "duplicate-slot" | "invalid-capability" | "invalid-load" | "invalid-preset" | "invalid-document" | "invalid-content" | "invalid-placement" | "invalid-fixed" | "invalid-role" | "order-form-not-guaranteed";
|
|
692
902
|
export interface ThemeDefinitionIssue {
|
|
693
903
|
path: string;
|
|
694
904
|
code: ThemeDefinitionIssueCode;
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wocart/theme",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "The Wocart theme contract: code-owned component definitions, JSON page documents, validation and resolution, and a reference Astro renderer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wocart",
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
"default": "./dist/astro/index.js"
|
|
39
39
|
},
|
|
40
40
|
"./astro/InstanceView.astro": "./dist/astro/InstanceView.astro",
|
|
41
|
+
"./astro/Blocks.astro": "./dist/astro/Blocks.astro",
|
|
42
|
+
"./astro/Block.astro": "./dist/astro/Block.astro",
|
|
43
|
+
"./astro/ThemeStyles.astro": "./dist/astro/ThemeStyles.astro",
|
|
41
44
|
"./image-service": {
|
|
42
45
|
"types": "./dist/image-service.d.ts",
|
|
43
46
|
"default": "./dist/image-service.js"
|