@wocart/theme 1.4.0 → 1.5.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/dist/types.d.ts CHANGED
@@ -36,8 +36,12 @@ export interface ResolutionScope {
36
36
  content?: Record<string, string>;
37
37
  locales?: readonly LocaleCode[];
38
38
  }
39
- /** The field kinds. `color`, `menu` and `collection` were added after V1, additively. */
40
- export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url" | "color" | "menu" | "collection";
39
+ /**
40
+ * The field kinds. Every kind after `url` was added after V1, additively; a
41
+ * reader that meets a kind it does not know rejects the manifest rather than
42
+ * render a value it cannot check.
43
+ */
44
+ export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url" | "color" | "menu" | "collection" | "image" | "richtext" | "list" | "product" | "category";
41
45
  interface FieldDefinitionBase {
42
46
  /** The exact prop name the component expects. */
43
47
  key: string;
@@ -68,22 +72,32 @@ export interface TextFieldDefinition extends FieldDefinitionBase {
68
72
  */
69
73
  default?: LocalizedText;
70
74
  }
71
- /** `number`: a finite number, optionally bounded. */
75
+ /**
76
+ * `number`: a finite number, optionally bounded. With `step`, a value must lie
77
+ * on the grid that starts at `min` (or 0) — Shopify's `range`; an editor may
78
+ * show a slider.
79
+ */
72
80
  export interface NumberFieldDefinition extends FieldDefinitionBase {
73
81
  kind: "number";
74
82
  default?: number;
75
83
  min?: number;
76
84
  max?: number;
85
+ step?: number;
77
86
  }
78
87
  /** `boolean`. */
79
88
  export interface BooleanFieldDefinition extends FieldDefinitionBase {
80
89
  kind: "boolean";
81
90
  default?: boolean;
82
91
  }
83
- /** `select`: one of a closed option list. */
92
+ /**
93
+ * `select`: one of a closed option list. The stored value is the option
94
+ * itself; `optionLabels` gives an editor the words to show for each option,
95
+ * in the theme's languages (an option without a label is shown as written).
96
+ */
84
97
  export interface SelectFieldDefinition extends FieldDefinitionBase {
85
98
  kind: "select";
86
99
  options: readonly string[];
100
+ optionLabels?: Readonly<Record<string, LocalizedText>>;
87
101
  default?: string;
88
102
  }
89
103
  /**
@@ -123,7 +137,143 @@ export interface CollectionFieldDefinition extends FieldDefinitionBase {
123
137
  kind: "collection";
124
138
  default?: string;
125
139
  }
126
- export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition | ColorFieldDefinition | MenuFieldDefinition | CollectionFieldDefinition;
140
+ /**
141
+ * `product`: which of the store's products to show here — Shopify's `product`
142
+ * setting. The value is a product handle (the `/products/<handle>` segment);
143
+ * the theme reads it with the storefront client's `products.get(handle)`. A
144
+ * renamed or deleted product reads as no product.
145
+ */
146
+ export interface ProductFieldDefinition extends FieldDefinitionBase {
147
+ kind: "product";
148
+ default?: string;
149
+ }
150
+ /**
151
+ * `category`: which of the store's categories to show here. The value is a
152
+ * category slug (the `/category/<slug>` segment); the theme finds it in the
153
+ * storefront client's `categories.list()`. An unknown slug reads as no category.
154
+ */
155
+ export interface CategoryFieldDefinition extends FieldDefinitionBase {
156
+ kind: "category";
157
+ default?: string;
158
+ }
159
+ /**
160
+ * `image`: one image from the store's own media. The value is a
161
+ * `MediaReference` — the object's key in the store's media bucket is its
162
+ * identity; the URL is derived at render from the store's media domain. A
163
+ * theme cannot know a store's media, so an image field has no default: the
164
+ * component renders its own fallback when no image is chosen.
165
+ */
166
+ export interface ImageFieldDefinition extends FieldDefinitionBase {
167
+ kind: "image";
168
+ }
169
+ /** What a rich-text field may contain. Absent `formats` allows all of them. */
170
+ export type RichTextFormat = "bold" | "italic" | "underline" | "strike" | "link" | "heading" | "list";
171
+ /**
172
+ * `richtext`: formatted text, stored as a `RichTextDocument` (or one per
173
+ * language) — never as HTML. `formats` narrows what the merchant may use; a
174
+ * stored document using anything else is invalid. Render it with
175
+ * `richTextToHtml`.
176
+ */
177
+ export interface RichTextFieldDefinition extends FieldDefinitionBase {
178
+ kind: "richtext";
179
+ formats?: readonly RichTextFormat[];
180
+ default?: RichTextValue;
181
+ }
182
+ /** A field a list item may hold: any kind but `list` itself. */
183
+ export type ListItemFieldDefinition = Exclude<FieldDefinition, ListFieldDefinition>;
184
+ /**
185
+ * `list`: repeatable items — trust badges, questions and answers, slides. Each
186
+ * stored item is `{ id, props }`: the id is unique within the list, so
187
+ * reordering never makes one item take another's identity. Item fields resolve
188
+ * like a section's (per-language text included), except that they cannot
189
+ * default from the content pack — every item would read the same key.
190
+ */
191
+ export interface ListFieldDefinition extends FieldDefinitionBase {
192
+ kind: "list";
193
+ fields: readonly ListItemFieldDefinition[];
194
+ min?: number;
195
+ max?: number;
196
+ default?: readonly ListItem[];
197
+ /** The item field (`text` or `textarea`) an editor shows as each item's title. */
198
+ itemLabel?: string;
199
+ }
200
+ export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition | ColorFieldDefinition | MenuFieldDefinition | CollectionFieldDefinition | ProductFieldDefinition | CategoryFieldDefinition | ImageFieldDefinition | RichTextFieldDefinition | ListFieldDefinition;
201
+ /**
202
+ * A stored image: the object's key in the store's media bucket
203
+ * (`products/3f2a….webp`), never a URL, plus what the editor knows about it.
204
+ * `width`/`height` are the original's pixels; `focal` is the point to keep in
205
+ * view when the image is cropped, from 0 to 1 on each axis.
206
+ */
207
+ export interface MediaReference {
208
+ key: string;
209
+ alt?: LocalizedText;
210
+ width?: number;
211
+ height?: number;
212
+ focal?: {
213
+ x: number;
214
+ y: number;
215
+ };
216
+ }
217
+ /** An image as a component receives it: `alt` already in the rendered language. */
218
+ export interface ResolvedImage {
219
+ key: string;
220
+ alt?: string;
221
+ width?: number;
222
+ height?: number;
223
+ focal?: {
224
+ x: number;
225
+ y: number;
226
+ };
227
+ }
228
+ /** One stored list item. */
229
+ export interface ListItem {
230
+ id: string;
231
+ props?: JsonObject;
232
+ }
233
+ export type RichTextMark = "bold" | "italic" | "underline" | "strike";
234
+ /** A run of text. `link` is a safe link target, as a `url` field's. */
235
+ export interface RichTextText {
236
+ type: "text";
237
+ text: string;
238
+ marks?: readonly RichTextMark[];
239
+ link?: string;
240
+ }
241
+ /** A line break inside a block. */
242
+ export interface RichTextBreak {
243
+ type: "break";
244
+ }
245
+ export type RichTextInline = RichTextText | RichTextBreak;
246
+ export interface RichTextParagraph {
247
+ type: "paragraph";
248
+ content?: readonly RichTextInline[];
249
+ }
250
+ /** Levels 2–4: the page owns its level-1 heading. */
251
+ export interface RichTextHeading {
252
+ type: "heading";
253
+ level: 2 | 3 | 4;
254
+ content?: readonly RichTextInline[];
255
+ }
256
+ export interface RichTextListItem {
257
+ content?: readonly RichTextInline[];
258
+ }
259
+ export interface RichTextList {
260
+ type: "list";
261
+ style: "bullet" | "ordered";
262
+ items: readonly RichTextListItem[];
263
+ }
264
+ export type RichTextBlock = RichTextParagraph | RichTextHeading | RichTextList;
265
+ /**
266
+ * A rich-text value. A closed format: anything not listed here is invalid, so
267
+ * no stored value can carry markup, scripts or styles. `version` lets the
268
+ * format grow; a reader rejects a version it does not know.
269
+ */
270
+ export interface RichTextDocument {
271
+ type: "doc";
272
+ version: 1;
273
+ content: readonly RichTextBlock[];
274
+ }
275
+ /** A rich-text field's stored value: one document, or one per language. */
276
+ export type RichTextValue = RichTextDocument | Readonly<Record<LocaleCode, RichTextDocument>>;
127
277
  /**
128
278
  * One composition point a component declares (`"default"` = children).
129
279
  *
@@ -165,7 +315,35 @@ export interface SlotDefinition {
165
315
  * it exists, belongs at *insertion* time in the writer, not at render.
166
316
  */
167
317
  export interface ComponentCapabilities {
318
+ /** Absent or `true`: an editor may offer it. `false`: never inserted by an editor. */
168
319
  insertable?: boolean;
320
+ /**
321
+ * The surfaces it may be inserted on, by surface key. An entry also covers
322
+ * that surface's alternates (`product` covers `product.compact`). Absent:
323
+ * any surface. Like `insertable`, enforced when inserting, never at render.
324
+ */
325
+ surfaces?: readonly string[];
326
+ }
327
+ /** One component of a preset: like an instance, without an id — ids are given when it is inserted. */
328
+ export interface PresetInstance {
329
+ type: string;
330
+ props?: JsonObject;
331
+ children?: readonly PresetInstance[];
332
+ slots?: Readonly<Record<string, readonly PresetInstance[]>>;
333
+ }
334
+ /**
335
+ * A ready-made configuration of a component that an editor offers when
336
+ * inserting it ("Hero — with image", a two-column section with its columns):
337
+ * props over the field defaults, and child components for its slots.
338
+ */
339
+ export interface ComponentPreset {
340
+ /** Lowercase kebab-case, unique among the component's presets. */
341
+ key: string;
342
+ label: LocalizedText;
343
+ description?: LocalizedText;
344
+ props?: JsonObject;
345
+ children?: readonly PresetInstance[];
346
+ slots?: Readonly<Record<string, readonly PresetInstance[]>>;
169
347
  }
170
348
  /** The request facts available while rendering (formalises `Astro.locals.store` + URL). */
171
349
  export interface ThemeRenderContext {
@@ -241,8 +419,12 @@ export interface ComponentDefinition {
241
419
  slots?: readonly SlotDefinition[];
242
420
  load?: LoadFn;
243
421
  capabilities?: ComponentCapabilities;
422
+ presets?: readonly ComponentPreset[];
244
423
  }
245
- /** The value type one field resolves to. A select narrows to its option literals. */
424
+ /**
425
+ * The value type one field resolves to. A select narrows to its option
426
+ * literals; a list resolves to its items, each `{ id }` plus its own fields.
427
+ */
246
428
  export type FieldValue<F extends FieldDefinition> = F extends {
247
429
  kind: "number";
248
430
  } ? number : F extends {
@@ -250,7 +432,18 @@ export type FieldValue<F extends FieldDefinition> = F extends {
250
432
  } ? boolean : F extends {
251
433
  kind: "select";
252
434
  options: readonly (infer Option)[];
253
- } ? Option : string;
435
+ } ? Option : F extends {
436
+ kind: "image";
437
+ } ? ResolvedImage : F extends {
438
+ kind: "richtext";
439
+ } ? RichTextDocument : F extends {
440
+ kind: "list";
441
+ fields: readonly (infer Item extends FieldDefinition)[];
442
+ } ? Array<{
443
+ id: string;
444
+ } & FieldProps<{
445
+ fields: readonly Item[];
446
+ }>> : string;
254
447
  type FieldsOf<D> = D extends {
255
448
  fields: readonly (infer F extends FieldDefinition)[];
256
449
  } ? F : never;
@@ -316,6 +509,18 @@ export interface ThemeDefinition {
316
509
  * with `checkThemeSettings`. Setting keys are unique across all groups.
317
510
  */
318
511
  settings?: readonly SettingsGroup[];
512
+ /**
513
+ * The theme's own text, one pack per language: what a `contentKey` field
514
+ * shows until the merchant edits it, and the store-wide labels a merchant
515
+ * may override. Declared, every `contentKey` must exist in every pack, and
516
+ * the manifest carries the packs so an editor shows inherited text.
517
+ */
518
+ content?: Readonly<Record<LocaleCode, Readonly<Record<string, string>>>>;
519
+ /**
520
+ * `true`: the theme serves a preview route at `THEME_PREVIEW_PATH`, so an
521
+ * editor can show a draft on the live storefront. Absent: no preview.
522
+ */
523
+ preview?: boolean;
319
524
  components: Record<string, ComponentDefinition>;
320
525
  }
321
526
  /** One group of theme settings, as an editor lists them ("Typography", "Header"). */
@@ -334,7 +539,21 @@ export interface SettingsGroup {
334
539
  export interface SurfaceSpec {
335
540
  key: string;
336
541
  label: LocalizedText;
542
+ /**
543
+ * The platform page type this surface renders. A page type with a resource
544
+ * (`product`, `category`, `collection`, `page`, `landing`) makes the surface
545
+ * a template: one document renders every resource of that type, and its
546
+ * sections read the resource from `ThemeRenderContext.params`.
547
+ */
548
+ pageType?: SurfacePageType;
549
+ /** The theme's shipped document for this surface: what renders before the merchant saves one. */
550
+ document?: PageData;
337
551
  }
552
+ /**
553
+ * The storefront's page types — what a URL means, with the paths the platform
554
+ * owns. The same names as `StorefrontPageType` in `@wocart/storefront`.
555
+ */
556
+ export type SurfacePageType = "home" | "catalog" | "category" | "collection" | "product" | "page" | "landing" | "checkout" | "thankYou";
338
557
  /**
339
558
  * One component as data: a `ComponentDefinition` minus its runtime-only
340
559
  * implementation. `component` (the live component reference) and `load` (the
@@ -354,6 +573,7 @@ export interface ComponentSpec {
354
573
  fields: readonly FieldDefinition[];
355
574
  slots: readonly SlotDefinition[];
356
575
  capabilities?: ComponentCapabilities;
576
+ presets: readonly ComponentPreset[];
357
577
  }
358
578
  /**
359
579
  * The serializable theme contract.
@@ -372,14 +592,24 @@ export interface ThemeManifest {
372
592
  contractVersion: 1;
373
593
  key: string;
374
594
  version: string;
595
+ /**
596
+ * The `@wocart/theme` version that generated the manifest — the renderer the
597
+ * theme ships with. An editor writes a feature only for renderers that read
598
+ * it (a hidden section needs 1.5.0). Absent from manifests made before 1.5.0.
599
+ */
600
+ sdkVersion?: string;
375
601
  surfaces: readonly SurfaceSpec[];
376
602
  /** The languages the theme's packs cover (possibly none declared). */
377
603
  locales: readonly LocaleCode[];
378
604
  /** The theme's global settings (possibly none). */
379
605
  settings: readonly SettingsGroup[];
606
+ /** The theme's content packs, by language (possibly none). */
607
+ content: Readonly<Record<LocaleCode, Readonly<Record<string, string>>>>;
608
+ /** Whether the theme serves a preview route (`THEME_PREVIEW_PATH`). */
609
+ preview: boolean;
380
610
  components: Record<string, ComponentSpec>;
381
611
  }
382
- export type ThemeManifestIssueCode = "invalid-manifest" | "unsupported-contract" | "invalid-identity" | "invalid-version" | "invalid-surface" | "invalid-settings" | "invalid-component" | "type-mismatch" | "non-serializable";
612
+ export type ThemeManifestIssueCode = "invalid-manifest" | "unsupported-contract" | "invalid-identity" | "invalid-version" | "invalid-surface" | "invalid-settings" | "invalid-component" | "invalid-content" | "type-mismatch" | "non-serializable";
383
613
  /** One reported manifest problem; `path` points into the manifest (e.g. `components.hero.fields[0].kind`). */
384
614
  export interface ThemeManifestIssue {
385
615
  path: string;
@@ -405,17 +635,22 @@ export interface ComponentInstance {
405
635
  props?: JsonObject;
406
636
  children?: ComponentInstance[];
407
637
  slots?: Record<string, ComponentInstance[]>;
638
+ /**
639
+ * `true`: kept in the document but not rendered — the merchant switched the
640
+ * section off. Its children are not rendered either.
641
+ */
642
+ hidden?: boolean;
408
643
  }
409
644
  /**
410
645
  * One composed surface's persisted document.
411
646
  *
412
647
  * At runtime the envelope is **forward-compatible**: `parsePageData` and
413
648
  * `PageDataSchema` carry unknown *top-level* keys through verbatim, so a later
414
- * writer can record metadata (catalog provenance is the intended first use)
415
- * without a `PageData.version` bump. Those keys are never read by the renderer,
416
- * which only walks `sections`, and they are deliberately **not declared here**:
417
- * an index signature would turn a `page.sectons` typo into `unknown` instead of
418
- * a compile error. The field is added when a real reader exists.
649
+ * writer can record metadata without a `PageData.version` bump. Those keys
650
+ * are never read by the renderer, which only walks `sections`, and they are
651
+ * deliberately **not declared here**: an index signature would turn a
652
+ * `page.sectons` typo into `unknown` instead of a compile error. A key is
653
+ * declared once it has a reader — `theme`, the document's provenance.
419
654
  *
420
655
  * Unknown keys *inside* an instance are still stripped — the envelope is
421
656
  * extensible, the instance is not.
@@ -424,8 +659,17 @@ export interface PageData {
424
659
  version: 1;
425
660
  surface: string;
426
661
  sections: ComponentInstance[];
662
+ /**
663
+ * The theme and theme version that wrote the document. A document that
664
+ * names another theme is reported (`theme-mismatch`): its sections were
665
+ * composed from another catalog.
666
+ */
667
+ theme?: {
668
+ key: string;
669
+ version: string;
670
+ };
427
671
  }
428
- 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";
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";
429
673
  /** One reported degradation; `path` points into the document (e.g. `sections[0].props.limit`). */
430
674
  export interface PageDataIssue {
431
675
  path: string;
@@ -444,7 +688,7 @@ export interface ResolvedInstance {
444
688
  slots: Record<string, ComponentInstance[]>;
445
689
  issues: PageDataIssue[];
446
690
  }
447
- 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";
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";
448
692
  export interface ThemeDefinitionIssue {
449
693
  path: string;
450
694
  code: ThemeDefinitionIssueCode;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The version of this package, as published. A theme's manifest records it
3
+ * (`ThemeManifest.sdkVersion`), so an editor knows what the theme's renderer
4
+ * reads. Always equal to `package.json`'s version; the package check refuses
5
+ * a release where they differ.
6
+ */
7
+ export declare const THEME_SDK_VERSION = "1.5.0";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The version of this package, as published. A theme's manifest records it
3
+ * (`ThemeManifest.sdkVersion`), so an editor knows what the theme's renderer
4
+ * reads. Always equal to `package.json`'s version; the package check refuses
5
+ * a release where they differ.
6
+ */
7
+ export const THEME_SDK_VERSION = "1.5.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/theme",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",
@@ -42,6 +42,10 @@
42
42
  "types": "./dist/image-service.d.ts",
43
43
  "default": "./dist/image-service.js"
44
44
  },
45
+ "./preview": {
46
+ "types": "./dist/preview-bridge.d.ts",
47
+ "default": "./dist/preview-bridge.js"
48
+ },
45
49
  "./package.json": "./package.json"
46
50
  },
47
51
  "files": [
@@ -76,10 +80,11 @@
76
80
  }
77
81
  },
78
82
  "devDependencies": {
79
- "@wocart/storefront": "^1.0.0",
80
83
  "@types/node": "^26.2.0",
84
+ "@wocart/storefront": "^1.0.0",
81
85
  "astro": "^7.3.2",
82
86
  "fast-check": "^4.6.0",
87
+ "happy-dom": "^20.11.6",
83
88
  "typescript": "^7.0.2",
84
89
  "vitest": "^5.0.0"
85
90
  }