@wocart/theme 1.0.0 → 1.1.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 +17 -0
- package/README.md +45 -0
- package/dist/definitions.js +60 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/instances.d.ts +22 -1
- package/dist/instances.js +20 -8
- package/dist/manifest.d.ts +80 -0
- package/dist/manifest.js +17 -0
- package/dist/schemas.d.ts +8 -0
- package/dist/schemas.js +8 -0
- package/dist/settings.d.ts +15 -0
- package/dist/settings.js +80 -0
- package/dist/types.d.ts +72 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.1.0 — 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Theme settings.** `defineTheme({ settings })` declares a theme's global
|
|
13
|
+
settings in groups, with the field system sections use; they travel in the
|
|
14
|
+
manifest (`ThemeManifest.settings`, `[]` when none).
|
|
15
|
+
`resolveThemeSettings(theme, stored, scope)` reads a store's saved values
|
|
16
|
+
against the declaration — the merchant's value, else the default, with
|
|
17
|
+
every fallback reported (`invalid-settings`, `unknown-setting`,
|
|
18
|
+
`invalid-setting`, `missing-required-setting`). `checkThemeSettings` is the
|
|
19
|
+
writer's strict check. `InferSettings<typeof theme>` types the values.
|
|
20
|
+
- Field kinds `color` (`#rgb`/`#rrggbb`) and `menu` (a menu handle), in
|
|
21
|
+
sections and settings alike.
|
|
22
|
+
- Types `SettingsGroup`, `ThemeSettingsIssue`, `ThemeSettingsIssueCode`,
|
|
23
|
+
`ThemeSettingsResult`, `ColorFieldDefinition`, `MenuFieldDefinition`.
|
|
24
|
+
|
|
8
25
|
## 1.0.0 — 2026-10-13
|
|
9
26
|
|
|
10
27
|
The first public release.
|
package/README.md
CHANGED
|
@@ -97,6 +97,8 @@ and, for page-data resolution issues, `path`.
|
|
|
97
97
|
| `number` | finite number | optional `min`/`max`; a `default` must sit inside them |
|
|
98
98
|
| `boolean` | boolean | |
|
|
99
99
|
| `select` | one of `options` | `default` must be one of the options |
|
|
100
|
+
| `color` | `#rgb` or `#rrggbb` | anything else (a color name, `url(…)`) is rejected at assembly and reported on read |
|
|
101
|
+
| `menu` | a menu handle (`main-menu`) | lowercase letters, digits and hyphens; the store's menu is read with `@wocart/storefront`'s `menus.get(handle)` |
|
|
100
102
|
| `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 |
|
|
101
103
|
|
|
102
104
|
Use `url` for every editor-settable `href`, never `text`: the reader drops an
|
|
@@ -300,6 +302,49 @@ policy is:
|
|
|
300
302
|
The SDK does not ship this policy as a function, because the shipped default
|
|
301
303
|
document is theme content.
|
|
302
304
|
|
|
305
|
+
## Theme settings
|
|
306
|
+
|
|
307
|
+
A theme's global choices — its font, which menu the header shows, a header
|
|
308
|
+
color — are **settings**: declared once on the theme, in groups, with the same
|
|
309
|
+
field kinds as a section. The merchant's values are stored per store and per
|
|
310
|
+
theme (a value only means something against the theme that declared it) and
|
|
311
|
+
reach the theme as `StoreConfig.themeSettings` from `@wocart/storefront`.
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
export const theme = defineTheme({
|
|
315
|
+
key: "my-theme",
|
|
316
|
+
version: "1.0.0",
|
|
317
|
+
locales: ["ar", "fr"],
|
|
318
|
+
settings: [
|
|
319
|
+
{
|
|
320
|
+
key: "header",
|
|
321
|
+
label: { ar: "الرأس", fr: "En-tête" },
|
|
322
|
+
fields: [
|
|
323
|
+
{ key: "headerMenu", kind: "menu", label: { ar: "القائمة", fr: "Menu" }, default: "main-menu" },
|
|
324
|
+
{ key: "font", kind: "select", label: { ar: "الخط", fr: "Police" }, options: ["Cairo", "Tajawal"], default: "Cairo" },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
components: { … },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const { values, issues } = resolveThemeSettings(theme, config?.themeSettings, { locales: [locale] });
|
|
332
|
+
values.headerMenu; // "main-menu" — typed: InferSettings<typeof theme>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
- **Reading never fails.** Each setting takes the merchant's value when the
|
|
336
|
+
declaration accepts it, else the default (or the content pack, for text),
|
|
337
|
+
else it is absent. A value the theme no longer accepts — a removed option, a
|
|
338
|
+
color that is not one, a setting since deleted — is reported in `issues`
|
|
339
|
+
(`invalid-setting`, `unknown-setting`, `invalid-settings`) and falls back.
|
|
340
|
+
- **Keys are unique across groups**; groups are for the editor, values are flat.
|
|
341
|
+
- **A writer checks strictly** with `checkThemeSettings(theme, values)` before
|
|
342
|
+
saving: every key declared, every value accepted, in every language.
|
|
343
|
+
- Settings travel in the manifest (`manifest.settings`), so an editor lists
|
|
344
|
+
them without running the theme.
|
|
345
|
+
- Store identity — the logo, the brand colors — is not a theme setting: it
|
|
346
|
+
belongs to the store and survives a theme change.
|
|
347
|
+
|
|
303
348
|
## Content and copy
|
|
304
349
|
|
|
305
350
|
The theme owns its content. The SDK does not model a content pack, a taxonomy,
|
package/dist/definitions.js
CHANGED
|
@@ -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 { SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, isLocaleMap, isLocaleTag, isSafeLinkTarget, } from "./schemas.js";
|
|
7
|
+
import { COLOR_PATTERN, MENU_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;
|
|
@@ -95,6 +95,8 @@ export function validateThemeDefinition(theme, options = {}) {
|
|
|
95
95
|
catalogKeys: new Set(Object.keys(theme.components)),
|
|
96
96
|
locales,
|
|
97
97
|
};
|
|
98
|
+
if (theme.settings !== undefined)
|
|
99
|
+
inspectSettings(theme.settings, issues, rules);
|
|
98
100
|
for (const [key, definition] of Object.entries(theme.components)) {
|
|
99
101
|
const path = `components.${key}`;
|
|
100
102
|
if (!TYPE_KEY_PATTERN.test(key)) {
|
|
@@ -109,6 +111,51 @@ export function validateThemeDefinition(theme, options = {}) {
|
|
|
109
111
|
}
|
|
110
112
|
return issues;
|
|
111
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* The theme's settings groups: an array of `{ key, label, fields }`, group keys
|
|
116
|
+
* unique and kebab-case, fields checked like a component's, and every setting
|
|
117
|
+
* key unique across all groups — values are stored flat, by setting key.
|
|
118
|
+
*/
|
|
119
|
+
function inspectSettings(settings, issues, rules) {
|
|
120
|
+
if (!Array.isArray(settings)) {
|
|
121
|
+
addIssue(issues, "settings", "invalid-settings", "`settings` must be an array of setting groups");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const groupKeys = new Set();
|
|
125
|
+
const settingKeys = new Set();
|
|
126
|
+
settings.forEach((group, index) => {
|
|
127
|
+
const path = `settings[${index}]`;
|
|
128
|
+
if (!isRecord(group)) {
|
|
129
|
+
addIssue(issues, path, "invalid-settings", "a settings group must be an object");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (typeof group.key !== "string" || !TYPE_KEY_PATTERN.test(group.key)) {
|
|
133
|
+
addIssue(issues, `${path}.key`, "invalid-settings", "`key` must be lowercase kebab-case");
|
|
134
|
+
}
|
|
135
|
+
else if (groupKeys.has(group.key)) {
|
|
136
|
+
addIssue(issues, `${path}.key`, "invalid-settings", `duplicate settings group "${group.key}"`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
groupKeys.add(group.key);
|
|
140
|
+
}
|
|
141
|
+
const labelProblem = localizedTextProblem(group.label, rules.locales, { allowEmpty: false });
|
|
142
|
+
if (labelProblem !== null)
|
|
143
|
+
addIssue(issues, `${path}.label`, "invalid-settings", `\`label\` ${labelProblem}`);
|
|
144
|
+
if (!Array.isArray(group.fields)) {
|
|
145
|
+
addIssue(issues, `${path}.fields`, "invalid-settings", "`fields` must be an array");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
inspectFields(group.fields, path, issues, rules);
|
|
149
|
+
for (const [fieldIndex, field] of group.fields.entries()) {
|
|
150
|
+
if (!isRecord(field) || typeof field.key !== "string")
|
|
151
|
+
continue;
|
|
152
|
+
if (settingKeys.has(field.key)) {
|
|
153
|
+
addIssue(issues, `${path}.fields[${fieldIndex}].key`, "duplicate-field-key", `setting "${field.key}" is declared in another group too`);
|
|
154
|
+
}
|
|
155
|
+
settingKeys.add(field.key);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
112
159
|
function inspectSurfaces(surfaces, issues, locales) {
|
|
113
160
|
const seenKeys = new Set();
|
|
114
161
|
surfaces.forEach((surface, index) => {
|
|
@@ -299,6 +346,18 @@ function inspectFields(fields, path, issues, rules) {
|
|
|
299
346
|
}
|
|
300
347
|
break;
|
|
301
348
|
}
|
|
349
|
+
case "color": {
|
|
350
|
+
if (field.default !== undefined && (typeof field.default !== "string" || !COLOR_PATTERN.test(field.default))) {
|
|
351
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", "`default` must be a hex color, #rgb or #rrggbb");
|
|
352
|
+
}
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case "menu": {
|
|
356
|
+
if (field.default !== undefined && (typeof field.default !== "string" || !MENU_HANDLE_PATTERN.test(field.default))) {
|
|
357
|
+
addIssue(issues, `${fieldPath}.default`, "invalid-field", "`default` must be a menu handle: lowercase letters, digits and dashes");
|
|
358
|
+
}
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
302
361
|
case "url": {
|
|
303
362
|
if (field.default !== undefined &&
|
|
304
363
|
(typeof field.default !== "string" || !isSafeLinkTarget(field.default))) {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
export { defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
13
13
|
export { parsePageData, validatePageData } from "./page-data.js";
|
|
14
14
|
export { resolveInstance, createInstance } from "./instances.js";
|
|
15
|
+
export { resolveThemeSettings, checkThemeSettings } from "./settings.js";
|
|
15
16
|
export { localize } from "./locales.js";
|
|
16
17
|
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
|
|
17
18
|
export type { MessageOptions, MessageValues, PluralCategory } from "./messages.js";
|
|
18
19
|
export { THEME_MANIFEST_CONTRACT_VERSION, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
19
|
-
export type { JsonValue, JsonObject, LocaleCode, LocalizedText, ResolutionScope, FieldKind, FieldDefinition, FieldValue, InferProps, SlotDefinition, ComponentCapabilities, ComponentDefinition, ComponentSpec, SurfaceSpec, ThemeDefinition, 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, ThemeManifest, ThemeManifestIssue, ThemeManifestIssueCode, ThemeManifestResult, ComponentInstance, PageData, ThemeRenderContext, LoadContext, LoadFn, ReadCache, PageDataIssue, PageDataIssueCode, PageDataResult, ResolvedInstance, ThemeDefinitionIssue, ThemeDefinitionIssueCode, ThemeValidationOptions, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
export { defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
13
13
|
export { parsePageData, validatePageData } from "./page-data.js";
|
|
14
14
|
export { resolveInstance, createInstance } from "./instances.js";
|
|
15
|
+
export { resolveThemeSettings, checkThemeSettings } from "./settings.js";
|
|
15
16
|
export { localize } from "./locales.js";
|
|
16
17
|
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
|
|
17
18
|
export { THEME_MANIFEST_CONTRACT_VERSION, themeManifest, validateThemeManifest, } from "./manifest.js";
|
package/dist/instances.d.ts
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
import type { ComponentDefinition, ComponentInstance, JsonObject, ResolutionScope, ResolvedInstance } from "./types.js";
|
|
1
|
+
import type { ComponentDefinition, ComponentInstance, FieldDefinition, JsonObject, LocaleCode, PageDataIssue, ResolutionScope, ResolvedInstance } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Gate one instance for rendering: field values through the chain, children
|
|
4
4
|
* and named slots gated against the declared slots, every degradation
|
|
5
5
|
* reported. The instance (and page data) is never modified.
|
|
6
6
|
*/
|
|
7
7
|
export declare function resolveInstance(definition: ComponentDefinition, instance: ComponentInstance, scope?: ResolutionScope): ResolvedInstance;
|
|
8
|
+
export type CheckedValue = {
|
|
9
|
+
ok: true;
|
|
10
|
+
value: unknown;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
/** An own property only: `constructor` or `toString` must never read the prototype. */
|
|
16
|
+
export declare function ownValue<T>(record: Record<string, T>, key: string): T | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* One field through the chain. `undefined` = omit the value. Issues are
|
|
19
|
+
* reported under `${prefix}.${field.key}` — a component's `props`, or a theme's
|
|
20
|
+
* `settings`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveField(field: FieldDefinition, raw: unknown, content: Record<string, string>, locales: readonly LocaleCode[], issues: PageDataIssue[], prefix: string): unknown;
|
|
23
|
+
/**
|
|
24
|
+
* What a writer may store: a value that passes the field's rules, or — for a
|
|
25
|
+
* translatable field — a per-language map whose every entry passes them, so no
|
|
26
|
+
* language can hold an unsafe link or a wrong type.
|
|
27
|
+
*/
|
|
28
|
+
export declare function checkStoredValue(field: FieldDefinition, value: unknown): CheckedValue;
|
|
8
29
|
/**
|
|
9
30
|
* Create a new instance with insertion-time defaults:
|
|
10
31
|
* materialises `field.default` values only — content-keyed fields stay unset
|
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 { isLocaleMap, isSafeLinkTarget } from "./schemas.js";
|
|
12
|
+
import { 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);
|
|
@@ -36,7 +36,7 @@ export function resolveInstance(definition, instance, scope = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
const props = {};
|
|
38
38
|
for (const field of fields) {
|
|
39
|
-
const resolved = resolveField(field, ownValue(rawProps, field.key), content, locales, issues);
|
|
39
|
+
const resolved = resolveField(field, ownValue(rawProps, field.key), content, locales, issues, "props");
|
|
40
40
|
if (resolved !== undefined) {
|
|
41
41
|
props[field.key] = resolved;
|
|
42
42
|
}
|
|
@@ -82,7 +82,7 @@ export function resolveInstance(definition, instance, scope = {}) {
|
|
|
82
82
|
return { props, children, slots, issues };
|
|
83
83
|
}
|
|
84
84
|
/** An own property only: `constructor` or `toString` must never read the prototype. */
|
|
85
|
-
function ownValue(record, key) {
|
|
85
|
+
export function ownValue(record, key) {
|
|
86
86
|
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
@@ -103,8 +103,12 @@ function storedValueFor(field, raw, locales) {
|
|
|
103
103
|
const picked = localize(raw, locales);
|
|
104
104
|
return picked === undefined ? { kind: "untranslated" } : { kind: "value", value: picked };
|
|
105
105
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* One field through the chain. `undefined` = omit the value. Issues are
|
|
108
|
+
* reported under `${prefix}.${field.key}` — a component's `props`, or a theme's
|
|
109
|
+
* `settings`.
|
|
110
|
+
*/
|
|
111
|
+
export function resolveField(field, raw, content, locales, issues, prefix) {
|
|
108
112
|
if (raw !== undefined) {
|
|
109
113
|
const stored = storedValueFor(field, raw, locales);
|
|
110
114
|
const checked = stored.kind === "value"
|
|
@@ -117,7 +121,7 @@ function resolveField(field, raw, content, locales, issues) {
|
|
|
117
121
|
}
|
|
118
122
|
if (checked !== undefined) {
|
|
119
123
|
issues.push({
|
|
120
|
-
path:
|
|
124
|
+
path: `${prefix}.${field.key}`,
|
|
121
125
|
code: "invalid-prop",
|
|
122
126
|
message: `"${field.key}" ${checked.message}; treated as absent`,
|
|
123
127
|
});
|
|
@@ -142,7 +146,7 @@ function resolveField(field, raw, content, locales, issues) {
|
|
|
142
146
|
}
|
|
143
147
|
if (field.required === true) {
|
|
144
148
|
issues.push({
|
|
145
|
-
path:
|
|
149
|
+
path: `${prefix}.${field.key}`,
|
|
146
150
|
code: "missing-required-prop",
|
|
147
151
|
message: `required field "${field.key}" has no value`,
|
|
148
152
|
});
|
|
@@ -154,7 +158,7 @@ function resolveField(field, raw, content, locales, issues) {
|
|
|
154
158
|
* translatable field — a per-language map whose every entry passes them, so no
|
|
155
159
|
* language can hold an unsafe link or a wrong type.
|
|
156
160
|
*/
|
|
157
|
-
function checkStoredValue(field, value) {
|
|
161
|
+
export function checkStoredValue(field, value) {
|
|
158
162
|
if (!isTranslatable(field) || typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
159
163
|
return checkFieldValue(field, value);
|
|
160
164
|
}
|
|
@@ -199,6 +203,14 @@ function checkFieldValue(field, value) {
|
|
|
199
203
|
ok: false,
|
|
200
204
|
message: "must be a site path, #fragment, ?query, or an http(s), mailto or tel URL",
|
|
201
205
|
};
|
|
206
|
+
case "color":
|
|
207
|
+
return typeof value === "string" && COLOR_PATTERN.test(value)
|
|
208
|
+
? { ok: true, value }
|
|
209
|
+
: { ok: false, message: "must be a hex color, #rgb or #rrggbb" };
|
|
210
|
+
case "menu":
|
|
211
|
+
return typeof value === "string" && MENU_HANDLE_PATTERN.test(value)
|
|
212
|
+
? { ok: true, value }
|
|
213
|
+
: { ok: false, message: "must be a menu handle" };
|
|
202
214
|
case "select":
|
|
203
215
|
return typeof value === "string" && field.options.includes(value)
|
|
204
216
|
? { ok: true, value }
|
package/dist/manifest.d.ts
CHANGED
|
@@ -28,6 +28,72 @@ export declare const ThemeManifestSchema: z.ZodObject<{
|
|
|
28
28
|
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
29
29
|
}, z.core.$strip>>>;
|
|
30
30
|
locales: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
31
|
+
settings: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
32
|
+
key: z.ZodString;
|
|
33
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
34
|
+
fields: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
35
|
+
key: z.ZodString;
|
|
36
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
37
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
38
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
39
|
+
contentKey: z.ZodExactOptional<z.ZodString>;
|
|
40
|
+
default: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
41
|
+
kind: z.ZodLiteral<"text">;
|
|
42
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
43
|
+
key: z.ZodString;
|
|
44
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
45
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
46
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
47
|
+
contentKey: z.ZodExactOptional<z.ZodString>;
|
|
48
|
+
default: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
49
|
+
kind: z.ZodLiteral<"textarea">;
|
|
50
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
51
|
+
key: z.ZodString;
|
|
52
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
53
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
54
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
55
|
+
kind: z.ZodLiteral<"number">;
|
|
56
|
+
default: z.ZodExactOptional<z.ZodNumber>;
|
|
57
|
+
min: z.ZodExactOptional<z.ZodNumber>;
|
|
58
|
+
max: z.ZodExactOptional<z.ZodNumber>;
|
|
59
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
60
|
+
key: z.ZodString;
|
|
61
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
62
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
63
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
64
|
+
kind: z.ZodLiteral<"boolean">;
|
|
65
|
+
default: z.ZodExactOptional<z.ZodBoolean>;
|
|
66
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
67
|
+
key: z.ZodString;
|
|
68
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
69
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
70
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
71
|
+
kind: z.ZodLiteral<"select">;
|
|
72
|
+
options: z.ZodArray<z.ZodString>;
|
|
73
|
+
default: z.ZodExactOptional<z.ZodString>;
|
|
74
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
75
|
+
key: z.ZodString;
|
|
76
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
77
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
78
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
79
|
+
kind: z.ZodLiteral<"url">;
|
|
80
|
+
default: z.ZodExactOptional<z.ZodString>;
|
|
81
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
82
|
+
key: z.ZodString;
|
|
83
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
84
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
85
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
86
|
+
kind: z.ZodLiteral<"color">;
|
|
87
|
+
default: z.ZodExactOptional<z.ZodString>;
|
|
88
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
89
|
+
key: z.ZodString;
|
|
90
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
91
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
92
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
93
|
+
kind: z.ZodLiteral<"menu">;
|
|
94
|
+
default: z.ZodExactOptional<z.ZodString>;
|
|
95
|
+
}, z.core.$strip>], "kind">>;
|
|
96
|
+
}, z.core.$strip>>>;
|
|
31
97
|
components: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
32
98
|
type: z.ZodString;
|
|
33
99
|
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
@@ -79,6 +145,20 @@ export declare const ThemeManifestSchema: z.ZodObject<{
|
|
|
79
145
|
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
80
146
|
kind: z.ZodLiteral<"url">;
|
|
81
147
|
default: z.ZodExactOptional<z.ZodString>;
|
|
148
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
149
|
+
key: z.ZodString;
|
|
150
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
151
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
152
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
153
|
+
kind: z.ZodLiteral<"color">;
|
|
154
|
+
default: z.ZodExactOptional<z.ZodString>;
|
|
155
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
156
|
+
key: z.ZodString;
|
|
157
|
+
label: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
158
|
+
description: z.ZodExactOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
159
|
+
required: z.ZodExactOptional<z.ZodBoolean>;
|
|
160
|
+
kind: z.ZodLiteral<"menu">;
|
|
161
|
+
default: z.ZodExactOptional<z.ZodString>;
|
|
82
162
|
}, z.core.$strip>], "kind">>>;
|
|
83
163
|
slots: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
84
164
|
name: z.ZodString;
|
package/dist/manifest.js
CHANGED
|
@@ -26,6 +26,7 @@ export function themeManifest(theme) {
|
|
|
26
26
|
label: copyData(surface.label),
|
|
27
27
|
})),
|
|
28
28
|
locales: [...(theme.locales ?? [])],
|
|
29
|
+
settings: (theme.settings ?? []).map((group) => copyData(group)),
|
|
29
30
|
components: Object.fromEntries(Object.entries(theme.components).map(([key, definition]) => [
|
|
30
31
|
key,
|
|
31
32
|
componentSpec(definition),
|
|
@@ -92,6 +93,16 @@ const FieldSchema = z.discriminatedUnion("kind", [
|
|
|
92
93
|
kind: z.literal("url"),
|
|
93
94
|
default: z.exactOptional(z.string()),
|
|
94
95
|
}),
|
|
96
|
+
z.object({
|
|
97
|
+
...FieldCommonSchema,
|
|
98
|
+
kind: z.literal("color"),
|
|
99
|
+
default: z.exactOptional(z.string()),
|
|
100
|
+
}),
|
|
101
|
+
z.object({
|
|
102
|
+
...FieldCommonSchema,
|
|
103
|
+
kind: z.literal("menu"),
|
|
104
|
+
default: z.exactOptional(z.string()),
|
|
105
|
+
}),
|
|
95
106
|
]);
|
|
96
107
|
const ComponentSpecSchema = z.object({
|
|
97
108
|
type: z.string(),
|
|
@@ -120,6 +131,9 @@ export const ThemeManifestSchema = z.object({
|
|
|
120
131
|
.array(z.object({ key: z.string(), label: LocalizedTextSchema }))
|
|
121
132
|
.default([]),
|
|
122
133
|
locales: z.array(z.string()).default([]),
|
|
134
|
+
settings: z
|
|
135
|
+
.array(z.object({ key: z.string(), label: LocalizedTextSchema, fields: z.array(FieldSchema) }))
|
|
136
|
+
.default([]),
|
|
123
137
|
components: z.record(z.string(), ComponentSpecSchema),
|
|
124
138
|
});
|
|
125
139
|
// ── Validation ───────────────────────────────────────────────────────────────
|
|
@@ -177,6 +191,8 @@ function codeAt(path) {
|
|
|
177
191
|
return "invalid-version";
|
|
178
192
|
if (path === "surfaces" || path.startsWith("surfaces["))
|
|
179
193
|
return "invalid-surface";
|
|
194
|
+
if (path === "settings" || path.startsWith("settings["))
|
|
195
|
+
return "invalid-settings";
|
|
180
196
|
if (path.startsWith("components."))
|
|
181
197
|
return "invalid-component";
|
|
182
198
|
return "invalid-manifest";
|
|
@@ -199,6 +215,7 @@ function probeOf(manifest) {
|
|
|
199
215
|
version: manifest.version,
|
|
200
216
|
surfaces: manifest.surfaces,
|
|
201
217
|
locales: manifest.locales,
|
|
218
|
+
settings: manifest.settings,
|
|
202
219
|
components: Object.fromEntries(Object.entries(manifest.components).map(([key, spec]) => [
|
|
203
220
|
key,
|
|
204
221
|
{ ...spec, component: null },
|
package/dist/schemas.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ export declare const TYPE_KEY_PATTERN: RegExp;
|
|
|
21
21
|
* not of the package's curated public surface (`src/index.ts`).
|
|
22
22
|
*/
|
|
23
23
|
export declare const SURFACE_KEY_PATTERN: RegExp;
|
|
24
|
+
/** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
|
|
25
|
+
export declare const COLOR_PATTERN: RegExp;
|
|
26
|
+
/**
|
|
27
|
+
* A `menu` field's value: the handle of one of the store's menus
|
|
28
|
+
* (`main-menu`, `footer`). The server applies the same pattern to menu
|
|
29
|
+
* handles; a menu that does not exist reads as no menu.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MENU_HANDLE_PATTERN: RegExp;
|
|
24
32
|
export declare function isLocaleTag(value: string): boolean;
|
|
25
33
|
/**
|
|
26
34
|
* Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
|
package/dist/schemas.js
CHANGED
|
@@ -20,6 +20,14 @@ export const TYPE_KEY_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
|
20
20
|
* not of the package's curated public surface (`src/index.ts`).
|
|
21
21
|
*/
|
|
22
22
|
export const SURFACE_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
|
|
23
|
+
/** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
|
|
24
|
+
export const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
25
|
+
/**
|
|
26
|
+
* A `menu` field's value: the handle of one of the store's menus
|
|
27
|
+
* (`main-menu`, `footer`). The server applies the same pattern to menu
|
|
28
|
+
* handles; a menu that does not exist reads as no menu.
|
|
29
|
+
*/
|
|
30
|
+
export const MENU_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
23
31
|
/**
|
|
24
32
|
* A BCP 47 language tag in canonical case: a 2–3 letter language, an optional
|
|
25
33
|
* 4-letter script, an optional 2-letter or 3-digit region (`ar`, `zgh`,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ResolutionScope, ThemeDefinition, ThemeSettingsIssue, ThemeSettingsResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The effective settings for rendering: every declared setting, from the
|
|
4
|
+
* store's value when the declaration accepts it, else from the theme's
|
|
5
|
+
* default. `stored` is the persisted object as it arrived (anything else —
|
|
6
|
+
* null, an array — reads as no values and is reported). Translated text uses
|
|
7
|
+
* `scope.locales`; text settings may default from `scope.content`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveThemeSettings(theme: ThemeDefinition, stored: unknown, scope?: ResolutionScope): ThemeSettingsResult;
|
|
10
|
+
/**
|
|
11
|
+
* What a writer must check before saving a store's settings for a theme:
|
|
12
|
+
* every key declared, every value accepted by its setting — in every language
|
|
13
|
+
* for a translated one. An empty list means the values may be saved.
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkThemeSettings(theme: ThemeDefinition, values: unknown): ThemeSettingsIssue[];
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A store's theme settings: the merchant's values for the settings a theme
|
|
3
|
+
* declares (`ThemeDefinition.settings`), read against that declaration.
|
|
4
|
+
*
|
|
5
|
+
* Values are stored flat by setting key, for one theme (a value is only
|
|
6
|
+
* meaningful against the theme that declared it). Reading never fails: a
|
|
7
|
+
* value the declaration does not accept — a setting since removed, a color
|
|
8
|
+
* that is not a color, an option no longer offered — is reported and the
|
|
9
|
+
* setting falls back to the theme's default, exactly as a page document's
|
|
10
|
+
* field does. Precedence: the merchant's value → the theme's default (or its
|
|
11
|
+
* content pack, for text) → absent.
|
|
12
|
+
*/
|
|
13
|
+
import { checkStoredValue, ownValue, resolveField } from "./instances.js";
|
|
14
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
function declaredSettings(theme) {
|
|
16
|
+
const fields = new Map();
|
|
17
|
+
for (const group of theme.settings ?? [])
|
|
18
|
+
for (const field of group.fields)
|
|
19
|
+
if (!fields.has(field.key))
|
|
20
|
+
fields.set(field.key, field);
|
|
21
|
+
return fields;
|
|
22
|
+
}
|
|
23
|
+
const PAGE_TO_SETTINGS = {
|
|
24
|
+
"invalid-prop": "invalid-setting",
|
|
25
|
+
"missing-required-prop": "missing-required-setting",
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The effective settings for rendering: every declared setting, from the
|
|
29
|
+
* store's value when the declaration accepts it, else from the theme's
|
|
30
|
+
* default. `stored` is the persisted object as it arrived (anything else —
|
|
31
|
+
* null, an array — reads as no values and is reported). Translated text uses
|
|
32
|
+
* `scope.locales`; text settings may default from `scope.content`.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveThemeSettings(theme, stored, scope = {}) {
|
|
35
|
+
const issues = [];
|
|
36
|
+
const declared = declaredSettings(theme);
|
|
37
|
+
let values = {};
|
|
38
|
+
if (stored !== undefined && stored !== null && !isRecord(stored)) {
|
|
39
|
+
issues.push({ path: "settings", code: "invalid-settings", message: "stored settings must be an object; the theme's defaults apply" });
|
|
40
|
+
}
|
|
41
|
+
else if (isRecord(stored)) {
|
|
42
|
+
values = stored;
|
|
43
|
+
}
|
|
44
|
+
for (const key of Object.keys(values)) {
|
|
45
|
+
if (!declared.has(key))
|
|
46
|
+
issues.push({ path: `settings.${key}`, code: "unknown-setting", message: `"${key}" is not a setting of "${theme.key}"; ignored` });
|
|
47
|
+
}
|
|
48
|
+
const resolved = {};
|
|
49
|
+
const fieldIssues = [];
|
|
50
|
+
for (const [key, field] of declared) {
|
|
51
|
+
const value = resolveField(field, ownValue(values, key), scope.content ?? {}, scope.locales ?? [], fieldIssues, "settings");
|
|
52
|
+
if (value !== undefined)
|
|
53
|
+
resolved[key] = value;
|
|
54
|
+
}
|
|
55
|
+
for (const issue of fieldIssues)
|
|
56
|
+
issues.push({ path: issue.path, code: PAGE_TO_SETTINGS[issue.code] ?? "invalid-setting", message: issue.message });
|
|
57
|
+
return { values: resolved, issues };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* What a writer must check before saving a store's settings for a theme:
|
|
61
|
+
* every key declared, every value accepted by its setting — in every language
|
|
62
|
+
* for a translated one. An empty list means the values may be saved.
|
|
63
|
+
*/
|
|
64
|
+
export function checkThemeSettings(theme, values) {
|
|
65
|
+
if (!isRecord(values))
|
|
66
|
+
return [{ path: "settings", code: "invalid-settings", message: "settings must be an object" }];
|
|
67
|
+
const declared = declaredSettings(theme);
|
|
68
|
+
const issues = [];
|
|
69
|
+
for (const [key, value] of Object.entries(values)) {
|
|
70
|
+
const field = declared.get(key);
|
|
71
|
+
if (field === undefined) {
|
|
72
|
+
issues.push({ path: `settings.${key}`, code: "unknown-setting", message: `"${key}" is not a setting of "${theme.key}"` });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const checked = checkStoredValue(field, value);
|
|
76
|
+
if (!checked.ok)
|
|
77
|
+
issues.push({ path: `settings.${key}`, code: "invalid-setting", message: `"${key}" ${checked.message}` });
|
|
78
|
+
}
|
|
79
|
+
return issues;
|
|
80
|
+
}
|
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
|
|
40
|
-
export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url";
|
|
39
|
+
/** The field kinds. `color` and `menu` were added after V1, additively. */
|
|
40
|
+
export type FieldKind = "text" | "textarea" | "number" | "boolean" | "select" | "url" | "color" | "menu";
|
|
41
41
|
interface FieldDefinitionBase {
|
|
42
42
|
/** The exact prop name the component expects. */
|
|
43
43
|
key: string;
|
|
@@ -97,7 +97,22 @@ export interface UrlFieldDefinition extends FieldDefinitionBase {
|
|
|
97
97
|
kind: "url";
|
|
98
98
|
default?: string;
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
/** `color`: a hex color, `#rgb` or `#rrggbb`. Not translatable. */
|
|
101
|
+
export interface ColorFieldDefinition extends FieldDefinitionBase {
|
|
102
|
+
kind: "color";
|
|
103
|
+
default?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `menu`: which of the store's menus to show here — Shopify's `link_list`.
|
|
107
|
+
* The value is a menu handle (`main-menu`); the theme reads the menu with
|
|
108
|
+
* the storefront client's `menus.get(handle)`. A theme never assumes a menu
|
|
109
|
+
* exists: an unknown handle reads as no menu.
|
|
110
|
+
*/
|
|
111
|
+
export interface MenuFieldDefinition extends FieldDefinitionBase {
|
|
112
|
+
kind: "menu";
|
|
113
|
+
default?: string;
|
|
114
|
+
}
|
|
115
|
+
export type FieldDefinition = TextFieldDefinition | NumberFieldDefinition | BooleanFieldDefinition | SelectFieldDefinition | UrlFieldDefinition | ColorFieldDefinition | MenuFieldDefinition;
|
|
101
116
|
/**
|
|
102
117
|
* One composition point a component declares (`"default"` = children).
|
|
103
118
|
*
|
|
@@ -270,8 +285,23 @@ export interface ThemeDefinition {
|
|
|
270
285
|
* languages the theme can serve. Absent means "not declared".
|
|
271
286
|
*/
|
|
272
287
|
locales?: readonly LocaleCode[];
|
|
288
|
+
/**
|
|
289
|
+
* The theme's global settings: what a merchant may change about the whole
|
|
290
|
+
* theme (its font, its colors, which menu the header shows), declared with
|
|
291
|
+
* the same fields sections use, in named groups. Optional and additive. A
|
|
292
|
+
* store's values are read with `resolveThemeSettings`; a writer checks them
|
|
293
|
+
* with `checkThemeSettings`. Setting keys are unique across all groups.
|
|
294
|
+
*/
|
|
295
|
+
settings?: readonly SettingsGroup[];
|
|
273
296
|
components: Record<string, ComponentDefinition>;
|
|
274
297
|
}
|
|
298
|
+
/** One group of theme settings, as an editor lists them ("Typography", "Header"). */
|
|
299
|
+
export interface SettingsGroup {
|
|
300
|
+
/** Lowercase kebab-case, unique in the theme. */
|
|
301
|
+
key: string;
|
|
302
|
+
label: LocalizedText;
|
|
303
|
+
fields: readonly FieldDefinition[];
|
|
304
|
+
}
|
|
275
305
|
/**
|
|
276
306
|
* One surface a theme declares it can render.
|
|
277
307
|
*
|
|
@@ -322,9 +352,11 @@ export interface ThemeManifest {
|
|
|
322
352
|
surfaces: readonly SurfaceSpec[];
|
|
323
353
|
/** The languages the theme's packs cover (possibly none declared). */
|
|
324
354
|
locales: readonly LocaleCode[];
|
|
355
|
+
/** The theme's global settings (possibly none). */
|
|
356
|
+
settings: readonly SettingsGroup[];
|
|
325
357
|
components: Record<string, ComponentSpec>;
|
|
326
358
|
}
|
|
327
|
-
export type ThemeManifestIssueCode = "invalid-manifest" | "unsupported-contract" | "invalid-identity" | "invalid-version" | "invalid-surface" | "invalid-component" | "type-mismatch" | "non-serializable";
|
|
359
|
+
export type ThemeManifestIssueCode = "invalid-manifest" | "unsupported-contract" | "invalid-identity" | "invalid-version" | "invalid-surface" | "invalid-settings" | "invalid-component" | "type-mismatch" | "non-serializable";
|
|
328
360
|
/** One reported manifest problem; `path` points into the manifest (e.g. `components.hero.fields[0].kind`). */
|
|
329
361
|
export interface ThemeManifestIssue {
|
|
330
362
|
path: string;
|
|
@@ -389,7 +421,7 @@ export interface ResolvedInstance {
|
|
|
389
421
|
slots: Record<string, ComponentInstance[]>;
|
|
390
422
|
issues: PageDataIssue[];
|
|
391
423
|
}
|
|
392
|
-
export type ThemeDefinitionIssueCode = "invalid-theme" | "invalid-definition" | "invalid-type" | "type-mismatch" | "invalid-field" | "duplicate-field-key" | "unknown-content-key" | "invalid-slot" | "duplicate-slot" | "invalid-capability" | "invalid-load";
|
|
424
|
+
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";
|
|
393
425
|
export interface ThemeDefinitionIssue {
|
|
394
426
|
path: string;
|
|
395
427
|
code: ThemeDefinitionIssueCode;
|
|
@@ -409,4 +441,39 @@ export interface ThemeDefinitionIssue {
|
|
|
409
441
|
export interface ThemeValidationOptions {
|
|
410
442
|
contentKeys?: readonly string[];
|
|
411
443
|
}
|
|
444
|
+
export type ThemeSettingsIssueCode = "invalid-settings" | "unknown-setting" | "invalid-setting" | "missing-required-setting";
|
|
445
|
+
/** One reported problem with a store's theme settings; `path` is `settings.<key>`. */
|
|
446
|
+
export interface ThemeSettingsIssue {
|
|
447
|
+
path: string;
|
|
448
|
+
code: ThemeSettingsIssueCode;
|
|
449
|
+
message: string;
|
|
450
|
+
}
|
|
451
|
+
/** The effective settings: every value checked, the rest from the theme's defaults. */
|
|
452
|
+
export interface ThemeSettingsResult {
|
|
453
|
+
values: Record<string, unknown>;
|
|
454
|
+
issues: ThemeSettingsIssue[];
|
|
455
|
+
}
|
|
456
|
+
type SettingsFieldsOf<T> = T extends {
|
|
457
|
+
settings: readonly (infer G)[];
|
|
458
|
+
} ? G extends {
|
|
459
|
+
fields: readonly (infer F extends FieldDefinition)[];
|
|
460
|
+
} ? F : never : never;
|
|
461
|
+
/**
|
|
462
|
+
* The effective settings of a theme, typed from its declaration: a setting
|
|
463
|
+
* with a `default` is always present, any other may be absent.
|
|
464
|
+
*
|
|
465
|
+
* ```ts
|
|
466
|
+
* const { values } = resolveThemeSettings(theme, stored);
|
|
467
|
+
* const settings = values as InferSettings<typeof theme>;
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
export type InferSettings<T extends ThemeDefinition> = {
|
|
471
|
+
[F in SettingsFieldsOf<T> as F extends {
|
|
472
|
+
default: unknown;
|
|
473
|
+
} ? F["key"] : never]: FieldValue<F>;
|
|
474
|
+
} & {
|
|
475
|
+
[F in SettingsFieldsOf<T> as F extends {
|
|
476
|
+
default: unknown;
|
|
477
|
+
} ? never : F["key"]]?: FieldValue<F>;
|
|
478
|
+
};
|
|
412
479
|
export {};
|
package/package.json
CHANGED