@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.
@@ -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
- /** A field a list item may hold: any kind but `list` itself. */
183
- export type ListItemFieldDefinition = Exclude<FieldDefinition, ListFieldDefinition>;
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
@@ -4,4 +4,4 @@
4
4
  * reads. Always equal to `package.json`'s version; the package check refuses
5
5
  * a release where they differ.
6
6
  */
7
- export declare const THEME_SDK_VERSION = "1.6.0";
7
+ export declare const THEME_SDK_VERSION = "2.0.0";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * reads. Always equal to `package.json`'s version; the package check refuses
5
5
  * a release where they differ.
6
6
  */
7
- export const THEME_SDK_VERSION = "1.6.0";
7
+ export const THEME_SDK_VERSION = "2.0.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/theme",
3
- "version": "1.6.0",
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"