@wocart/theme 1.1.1 → 1.3.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 +18 -0
- package/README.md +19 -0
- package/dist/definitions.d.ts +23 -3
- package/dist/definitions.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/instances.js +5 -1
- package/dist/manifest.d.ts +14 -0
- package/dist/manifest.js +5 -0
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.js +1 -0
- package/dist/types.d.ts +30 -7
- package/package.json +1 -1
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.3.0 — 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `load` is typed from the component's own fields: `defineComponent` hands it
|
|
13
|
+
`props` as `FieldValues<fields>` (a field with a default is always there,
|
|
14
|
+
any other may be absent), so a definition reads its settings — which
|
|
15
|
+
collection, how many products — without a cast. What `load` returns still
|
|
16
|
+
types the component's props (`InferProps`). Type `FieldValues`.
|
|
17
|
+
|
|
18
|
+
## 1.2.0 — 2026-10-17
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Field kind `collection`: a collection handle (`best-sellers`), so the
|
|
23
|
+
merchant picks which curated list a section shows — Shopify's `collection`
|
|
24
|
+
setting. Type `CollectionFieldDefinition`.
|
|
25
|
+
|
|
8
26
|
## 1.1.1 — 2026-10-17
|
|
9
27
|
|
|
10
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -89,6 +89,24 @@ props and undeclared children/slots, is reported through `onIssue` (default:
|
|
|
89
89
|
`console.warn`). The issue contains a stable `code`, `message`, `instanceId`,
|
|
90
90
|
and, for page-data resolution issues, `path`.
|
|
91
91
|
|
|
92
|
+
`load` receives the instance's resolved props typed from the component's own
|
|
93
|
+
fields, so a section reads its settings without a cast:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
defineComponent({
|
|
97
|
+
type: "best-sellers",
|
|
98
|
+
component: BestSellers,
|
|
99
|
+
label: { ar: "الأكثر مبيعاً", fr: "Meilleures ventes" },
|
|
100
|
+
fields: [
|
|
101
|
+
{ key: "collection", kind: "collection", label: { ar: "المجموعة", fr: "Collection" }, default: "best-sellers" },
|
|
102
|
+
{ key: "limit", kind: "number", label: { ar: "العدد", fr: "Nombre" }, default: 8, min: 1, max: 24 },
|
|
103
|
+
],
|
|
104
|
+
load: async ({ storefront, props }) => ({
|
|
105
|
+
products: await storefront.products.list({ collection: props.collection, limit: props.limit }),
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
92
110
|
## Field kinds
|
|
93
111
|
|
|
94
112
|
| Kind | Value | Rules |
|
|
@@ -98,6 +116,7 @@ and, for page-data resolution issues, `path`.
|
|
|
98
116
|
| `boolean` | boolean | |
|
|
99
117
|
| `select` | one of `options` | `default` must be one of the options |
|
|
100
118
|
| `color` | `#rgb` or `#rrggbb` | anything else (a color name, `url(…)`) is rejected at assembly and reported on read |
|
|
119
|
+
| `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
120
|
| `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
121
|
| `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
122
|
|
package/dist/definitions.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import type { ComponentDefinition, ThemeDefinition, ThemeDefinitionIssue, ThemeValidationOptions } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
1
|
+
import type { ComponentDefinition, FieldDefinition, FieldValues, LoadContext, ThemeDefinition, ThemeDefinitionIssue, ThemeValidationOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper: returns the definition unchanged so literal types survive.
|
|
4
|
+
* `load` receives the component's props typed from its own `fields`, so a
|
|
5
|
+
* definition reads its settings without a cast, and what it returns types the
|
|
6
|
+
* component's props (`InferProps`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function defineComponent<const T extends ComponentBase, Loaded extends Record<string, unknown>, const Fields extends readonly FieldDefinition[] = readonly []>(definition: T & {
|
|
9
|
+
fields?: Fields;
|
|
10
|
+
load: (context: LoadContext<FieldValues<Fields>>) => Promise<Loaded>;
|
|
11
|
+
}): T & FieldsPart<Fields> & {
|
|
12
|
+
load: (context: LoadContext) => Promise<Loaded>;
|
|
13
|
+
};
|
|
14
|
+
export declare function defineComponent<const T extends ComponentBase, const Fields extends readonly FieldDefinition[] = readonly []>(definition: T & {
|
|
15
|
+
fields?: Fields;
|
|
16
|
+
load?: undefined;
|
|
17
|
+
}): T & FieldsPart<Fields>;
|
|
18
|
+
type ComponentBase = Omit<ComponentDefinition, "fields" | "load">;
|
|
19
|
+
/** The field literals a definition declared, when it declared any. */
|
|
20
|
+
type FieldsPart<Fields extends readonly FieldDefinition[]> = [Fields] extends [readonly []] ? {} : {
|
|
21
|
+
fields: Fields;
|
|
22
|
+
};
|
|
4
23
|
/** Identity helper for the catalog. */
|
|
5
24
|
export declare function defineTheme<const T extends ThemeDefinition>(definition: T): T;
|
|
6
25
|
/** Check the whole catalog: key/type identity, field rules, slot rules. */
|
|
7
26
|
export declare function validateThemeDefinition(theme: ThemeDefinition, options?: ThemeValidationOptions): ThemeDefinitionIssue[];
|
|
27
|
+
export {};
|
package/dist/definitions.js
CHANGED
|
@@ -4,8 +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";
|
|
8
|
-
/** Identity helper: returns the definition unchanged so literal types survive. */
|
|
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";
|
|
9
8
|
export function defineComponent(definition) {
|
|
10
9
|
return definition;
|
|
11
10
|
}
|
|
@@ -358,6 +357,12 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
358
357
|
}
|
|
359
358
|
break;
|
|
360
359
|
}
|
|
360
|
+
case "collection": {
|
|
361
|
+
if (field.default !== undefined && (typeof field.default !== "string" || !COLLECTION_HANDLE_PATTERN.test(field.default))) {
|
|
362
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", "`default` must be a collection handle: lowercase letters, digits and dashes");
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
361
366
|
case "url": {
|
|
362
367
|
if (field.default !== undefined &&
|
|
363
368
|
(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, FieldValues, 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 }
|
package/dist/manifest.d.ts
CHANGED
|
@@ -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`,
|
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 `
|
|
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
|
-
|
|
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
|
*
|
|
@@ -193,7 +204,7 @@ export interface ReadCache {
|
|
|
193
204
|
get<T>(key: string, load: () => T | Promise<T>): Promise<T>;
|
|
194
205
|
}
|
|
195
206
|
/** What a `load` hook receives. Server-only; reads go through the storefront SDK. */
|
|
196
|
-
export interface LoadContext {
|
|
207
|
+
export interface LoadContext<P = Record<string, unknown>> {
|
|
197
208
|
storefront: StoreClient;
|
|
198
209
|
context: ThemeRenderContext;
|
|
199
210
|
/**
|
|
@@ -204,11 +215,16 @@ export interface LoadContext {
|
|
|
204
215
|
* the same name. A definition whose component reads a prop from `load` should
|
|
205
216
|
* keep that name out of `fields`.
|
|
206
217
|
*/
|
|
207
|
-
props:
|
|
218
|
+
props: P;
|
|
208
219
|
reads: ReadCache;
|
|
209
220
|
}
|
|
210
|
-
/**
|
|
211
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Server-side data fetch for one component definition. Through
|
|
223
|
+
* `defineComponent`, `props` is typed from the definition's own fields
|
|
224
|
+
* (`FieldValues`): a field with a default is always there, any other may be
|
|
225
|
+
* absent.
|
|
226
|
+
*/
|
|
227
|
+
export type LoadFn<P = Record<string, unknown>> = (context: LoadContext<P>) => Promise<Record<string, unknown>>;
|
|
212
228
|
/**
|
|
213
229
|
* Code-owned description of one reusable component. `component` is the real
|
|
214
230
|
* component reference — opaque here so the core stays framework-neutral; the
|
|
@@ -238,6 +254,13 @@ export type FieldValue<F extends FieldDefinition> = F extends {
|
|
|
238
254
|
type FieldsOf<D> = D extends {
|
|
239
255
|
fields: readonly (infer F extends FieldDefinition)[];
|
|
240
256
|
} ? F : never;
|
|
257
|
+
/**
|
|
258
|
+
* The resolved values of a list of fields, as `load` and the component receive
|
|
259
|
+
* them: a field with a `default` is always present, any other may be absent.
|
|
260
|
+
*/
|
|
261
|
+
export type FieldValues<Fields extends readonly FieldDefinition[]> = FieldProps<{
|
|
262
|
+
fields: Fields;
|
|
263
|
+
}>;
|
|
241
264
|
type FieldProps<D> = {
|
|
242
265
|
[F in FieldsOf<D> as F extends {
|
|
243
266
|
default: unknown;
|
package/package.json
CHANGED