@wocart/theme 1.1.0 → 1.2.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 CHANGED
@@ -5,6 +5,24 @@ All notable changes to `@wocart/theme` are recorded here. The package follows
5
5
  major version, and an export is marked `@deprecated` for at least one minor
6
6
  release before a major removes it.
7
7
 
8
+ ## 1.2.0 — 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - Field kind `collection`: a collection handle (`best-sellers`), so the
13
+ merchant picks which curated list a section shows — Shopify's `collection`
14
+ setting. Type `CollectionFieldDefinition`.
15
+
16
+ ## 1.1.1 — 2026-10-17
17
+
18
+ ### Fixed
19
+
20
+ - `resolveThemeSettings` returns its values typed from the theme's
21
+ declaration (`InferSettings<typeof theme>`) instead of
22
+ `Record<string, unknown>`, as the README promised: a setting with a default
23
+ is a value, any other may be absent. `ThemeSettingsResult` takes the values'
24
+ type as an optional parameter. No runtime change.
25
+
8
26
  ## 1.1.0 — 2026-10-17
9
27
 
10
28
  ### Added
package/README.md CHANGED
@@ -98,6 +98,7 @@ and, for page-data resolution issues, `path`.
98
98
  | `boolean` | boolean | |
99
99
  | `select` | one of `options` | `default` must be one of the options |
100
100
  | `color` | `#rgb` or `#rrggbb` | anything else (a color name, `url(…)`) is rejected at assembly and reported on read |
101
+ | `collection` | a collection handle (`best-sellers`) | lowercase letters, digits and hyphens; its products are listed with `@wocart/storefront`'s `products.list({ collection })`, in the merchant's order |
101
102
  | `menu` | a menu handle (`main-menu`) | lowercase letters, digits and hyphens; the store's menu is read with `@wocart/storefront`'s `menus.get(handle)` |
102
103
  | `url` | a link target | a site path (`/products`), `#fragment`, `?query`, `http(s)://`, `mailto:` or `tel:`; `javascript:`, `data:`, `//host` and backslash tricks are rejected at assembly and reported on read |
103
104
 
@@ -329,7 +330,7 @@ export const theme = defineTheme({
329
330
  });
330
331
 
331
332
  const { values, issues } = resolveThemeSettings(theme, config?.themeSettings, { locales: [locale] });
332
- values.headerMenu; // "main-menu" — typed: InferSettings<typeof theme>
333
+ values.headerMenu; // "main-menu" — values: InferSettings<typeof theme>, from 1.1.1
333
334
  ```
334
335
 
335
336
  - **Reading never fails.** Each setting takes the merchant's value when the
@@ -4,7 +4,7 @@
4
4
  * data — the validator exists so a theme's test suite fails loudly on catalog
5
5
  * bugs when the catalog is assembled.
6
6
  */
7
- import { COLOR_PATTERN, MENU_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, isLocaleMap, isLocaleTag, isSafeLinkTarget, } from "./schemas.js";
7
+ import { COLOR_PATTERN, MENU_HANDLE_PATTERN, COLLECTION_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, isLocaleMap, isLocaleTag, isSafeLinkTarget, } from "./schemas.js";
8
8
  /** Identity helper: returns the definition unchanged so literal types survive. */
9
9
  export function defineComponent(definition) {
10
10
  return definition;
@@ -358,6 +358,12 @@ function inspectFields(fields, path, issues, rules) {
358
358
  }
359
359
  break;
360
360
  }
361
+ case "collection": {
362
+ if (field.default !== undefined && (typeof field.default !== "string" || !COLLECTION_HANDLE_PATTERN.test(field.default))) {
363
+ addIssue(issues, `${fieldPath}.default`, "invalid-field", "`default` must be a collection handle: lowercase letters, digits and dashes");
364
+ }
365
+ break;
366
+ }
361
367
  case "url": {
362
368
  if (field.default !== undefined &&
363
369
  (typeof field.default !== "string" || !isSafeLinkTarget(field.default))) {
package/dist/index.d.ts CHANGED
@@ -17,4 +17,4 @@ export { localize } from "./locales.js";
17
17
  export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
18
18
  export type { MessageOptions, MessageValues, PluralCategory } from "./messages.js";
19
19
  export { THEME_MANIFEST_CONTRACT_VERSION, themeManifest, validateThemeManifest, } from "./manifest.js";
20
- export type { JsonValue, JsonObject, LocaleCode, LocalizedText, ResolutionScope, FieldKind, FieldDefinition, FieldValue, InferProps, SlotDefinition, ComponentCapabilities, ComponentDefinition, ComponentSpec, SurfaceSpec, ThemeDefinition, SettingsGroup, InferSettings, ThemeSettingsIssue, ThemeSettingsIssueCode, ThemeSettingsResult, ColorFieldDefinition, MenuFieldDefinition, ThemeManifest, ThemeManifestIssue, ThemeManifestIssueCode, ThemeManifestResult, ComponentInstance, PageData, ThemeRenderContext, LoadContext, LoadFn, ReadCache, PageDataIssue, PageDataIssueCode, PageDataResult, ResolvedInstance, ThemeDefinitionIssue, ThemeDefinitionIssueCode, ThemeValidationOptions, } from "./types.js";
20
+ export type { JsonValue, JsonObject, LocaleCode, LocalizedText, ResolutionScope, FieldKind, FieldDefinition, FieldValue, InferProps, SlotDefinition, ComponentCapabilities, ComponentDefinition, ComponentSpec, SurfaceSpec, ThemeDefinition, SettingsGroup, InferSettings, ThemeSettingsIssue, ThemeSettingsIssueCode, ThemeSettingsResult, ColorFieldDefinition, MenuFieldDefinition, CollectionFieldDefinition, ThemeManifest, ThemeManifestIssue, ThemeManifestIssueCode, ThemeManifestResult, ComponentInstance, PageData, ThemeRenderContext, LoadContext, LoadFn, ReadCache, PageDataIssue, PageDataIssueCode, PageDataResult, ResolvedInstance, ThemeDefinitionIssue, ThemeDefinitionIssueCode, ThemeValidationOptions, } from "./types.js";
package/dist/instances.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * default applies last).
10
10
  */
11
11
  import { localize } from "./locales.js";
12
- import { COLOR_PATTERN, MENU_HANDLE_PATTERN, isLocaleMap, isSafeLinkTarget } from "./schemas.js";
12
+ import { COLLECTION_HANDLE_PATTERN, COLOR_PATTERN, MENU_HANDLE_PATTERN, isLocaleMap, isSafeLinkTarget } from "./schemas.js";
13
13
  /** Field kinds whose values may differ per language. Numbers, toggles and choices are shared. */
14
14
  const TRANSLATABLE_KINDS = new Set(["text", "textarea", "url"]);
15
15
  const isTranslatable = (field) => TRANSLATABLE_KINDS.has(field.kind);
@@ -211,6 +211,10 @@ function checkFieldValue(field, value) {
211
211
  return typeof value === "string" && MENU_HANDLE_PATTERN.test(value)
212
212
  ? { ok: true, value }
213
213
  : { ok: false, message: "must be a menu handle" };
214
+ case "collection":
215
+ return typeof value === "string" && COLLECTION_HANDLE_PATTERN.test(value)
216
+ ? { ok: true, value }
217
+ : { ok: false, message: "must be a collection handle" };
214
218
  case "select":
215
219
  return typeof value === "string" && field.options.includes(value)
216
220
  ? { ok: true, value }
@@ -92,6 +92,13 @@ export declare const ThemeManifestSchema: z.ZodObject<{
92
92
  required: z.ZodExactOptional<z.ZodBoolean>;
93
93
  kind: z.ZodLiteral<"menu">;
94
94
  default: z.ZodExactOptional<z.ZodString>;
95
+ }, z.core.$strip>, z.ZodObject<{
96
+ key: z.ZodString;
97
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
98
+ description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
99
+ required: z.ZodExactOptional<z.ZodBoolean>;
100
+ kind: z.ZodLiteral<"collection">;
101
+ default: z.ZodExactOptional<z.ZodString>;
95
102
  }, z.core.$strip>], "kind">>;
96
103
  }, z.core.$strip>>>;
97
104
  components: z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -159,6 +166,13 @@ export declare const ThemeManifestSchema: z.ZodObject<{
159
166
  required: z.ZodExactOptional<z.ZodBoolean>;
160
167
  kind: z.ZodLiteral<"menu">;
161
168
  default: z.ZodExactOptional<z.ZodString>;
169
+ }, z.core.$strip>, z.ZodObject<{
170
+ key: z.ZodString;
171
+ label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
172
+ description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
173
+ required: z.ZodExactOptional<z.ZodBoolean>;
174
+ kind: z.ZodLiteral<"collection">;
175
+ default: z.ZodExactOptional<z.ZodString>;
162
176
  }, z.core.$strip>], "kind">>>;
163
177
  slots: z.ZodDefault<z.ZodArray<z.ZodObject<{
164
178
  name: z.ZodString;
package/dist/manifest.js CHANGED
@@ -103,6 +103,11 @@ const FieldSchema = z.discriminatedUnion("kind", [
103
103
  kind: z.literal("menu"),
104
104
  default: z.exactOptional(z.string()),
105
105
  }),
106
+ z.object({
107
+ ...FieldCommonSchema,
108
+ kind: z.literal("collection"),
109
+ default: z.exactOptional(z.string()),
110
+ }),
106
111
  ]);
107
112
  const ComponentSpecSchema = z.object({
108
113
  type: z.string(),
package/dist/schemas.d.ts CHANGED
@@ -29,6 +29,7 @@ export declare const COLOR_PATTERN: RegExp;
29
29
  * handles; a menu that does not exist reads as no menu.
30
30
  */
31
31
  export declare const MENU_HANDLE_PATTERN: RegExp;
32
+ export declare const COLLECTION_HANDLE_PATTERN: RegExp;
32
33
  export declare function isLocaleTag(value: string): boolean;
33
34
  /**
34
35
  * Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
package/dist/schemas.js CHANGED
@@ -28,6 +28,7 @@ export const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
28
28
  * handles; a menu that does not exist reads as no menu.
29
29
  */
30
30
  export const MENU_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
31
+ export const COLLECTION_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
31
32
  /**
32
33
  * A BCP 47 language tag in canonical case: a 2–3 letter language, an optional
33
34
  * 4-letter script, an optional 2-letter or 3-digit region (`ar`, `zgh`,
@@ -1,12 +1,14 @@
1
- import type { ResolutionScope, ThemeDefinition, ThemeSettingsIssue, ThemeSettingsResult } from "./types.js";
1
+ import type { InferSettings, ResolutionScope, ThemeDefinition, ThemeSettingsIssue, ThemeSettingsResult } from "./types.js";
2
2
  /**
3
3
  * The effective settings for rendering: every declared setting, from the
4
4
  * store's value when the declaration accepts it, else from the theme's
5
5
  * default. `stored` is the persisted object as it arrived (anything else —
6
6
  * null, an array — reads as no values and is reported). Translated text uses
7
- * `scope.locales`; text settings may default from `scope.content`.
7
+ * `scope.locales`; text settings may default from `scope.content`. The
8
+ * values are typed from the declaration (`InferSettings<typeof theme>`): a
9
+ * setting with a default is always there, any other may be absent.
8
10
  */
9
- export declare function resolveThemeSettings(theme: ThemeDefinition, stored: unknown, scope?: ResolutionScope): ThemeSettingsResult;
11
+ export declare function resolveThemeSettings<T extends ThemeDefinition>(theme: T, stored: unknown, scope?: ResolutionScope): ThemeSettingsResult<InferSettings<T>>;
10
12
  /**
11
13
  * What a writer must check before saving a store's settings for a theme:
12
14
  * every key declared, every value accepted by its setting — in every language
package/dist/settings.js CHANGED
@@ -29,7 +29,9 @@ const PAGE_TO_SETTINGS = {
29
29
  * store's value when the declaration accepts it, else from the theme's
30
30
  * default. `stored` is the persisted object as it arrived (anything else —
31
31
  * null, an array — reads as no values and is reported). Translated text uses
32
- * `scope.locales`; text settings may default from `scope.content`.
32
+ * `scope.locales`; text settings may default from `scope.content`. The
33
+ * values are typed from the declaration (`InferSettings<typeof theme>`): a
34
+ * setting with a default is always there, any other may be absent.
33
35
  */
34
36
  export function resolveThemeSettings(theme, stored, scope = {}) {
35
37
  const issues = [];
package/dist/types.d.ts CHANGED
@@ -36,8 +36,8 @@ export interface ResolutionScope {
36
36
  content?: Record<string, string>;
37
37
  locales?: readonly LocaleCode[];
38
38
  }
39
- /** The field kinds. `color` and `menu` were added after V1, additively. */
40
- export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url" | "color" | "menu";
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";
41
41
  interface FieldDefinitionBase {
42
42
  /** The exact prop name the component expects. */
43
43
  key: string;
@@ -112,7 +112,18 @@ export interface MenuFieldDefinition extends FieldDefinitionBase {
112
112
  kind: "menu";
113
113
  default?: string;
114
114
  }
115
- export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition | ColorFieldDefinition | MenuFieldDefinition;
115
+ /**
116
+ * `collection`: which of the store's collections to show here — Shopify's
117
+ * `collection` setting. The value is a collection handle (`best-sellers`); the
118
+ * theme lists its products with the storefront client's
119
+ * `products.list({ collection })`, in the merchant's order. An unknown handle
120
+ * lists nothing.
121
+ */
122
+ export interface CollectionFieldDefinition extends FieldDefinitionBase {
123
+ kind: "collection";
124
+ default?: string;
125
+ }
126
+ export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition | ColorFieldDefinition | MenuFieldDefinition | CollectionFieldDefinition;
116
127
  /**
117
128
  * One composition point a component declares (`"default"` = children).
118
129
  *
@@ -449,8 +460,8 @@ export interface ThemeSettingsIssue {
449
460
  message: string;
450
461
  }
451
462
  /** The effective settings: every value checked, the rest from the theme's defaults. */
452
- export interface ThemeSettingsResult {
453
- values: Record<string, unknown>;
463
+ export interface ThemeSettingsResult<V = Record<string, unknown>> {
464
+ values: V;
454
465
  issues: ThemeSettingsIssue[];
455
466
  }
456
467
  type SettingsFieldsOf<T> = T extends {
@@ -463,8 +474,7 @@ type SettingsFieldsOf<T> = T extends {
463
474
  * with a `default` is always present, any other may be absent.
464
475
  *
465
476
  * ```ts
466
- * const { values } = resolveThemeSettings(theme, stored);
467
- * const settings = values as InferSettings<typeof theme>;
477
+ * const { values } = resolveThemeSettings(theme, stored); // values: InferSettings<typeof theme>
468
478
  * ```
469
479
  */
470
480
  export type InferSettings<T extends ThemeDefinition> = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/theme",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",