@wocart/theme 2.0.0 → 2.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.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A theme's key: lowercase kebab-case, at most 64 characters. The platform
3
+ * stores a merchant's text edits under it, so it is the theme's identity on the
4
+ * platform, and the server applies the same rule.
5
+ */
6
+ export const THEME_KEY_PATTERN = /^(?=.{1,64}$)[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
7
+ /** Type keys are lowercase kebab-case. */
8
+ export const TYPE_KEY_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
9
+ /**
10
+ * The page-document **surface** key — the address of a composed page
11
+ * (`home`, `product`, …; `area:<key>` is an area's document, and
12
+ * `page:<slug>` / `lp:<slug>` are reserved conventions).
13
+ *
14
+ * The server applies the same pattern. Module-level on purpose: it is part of
15
+ * the persisted-data contract,
16
+ * not of the package's curated public surface (`src/index.ts`).
17
+ */
18
+ export const SURFACE_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
19
+ /** The platform page types a surface can render (`SurfacePageType`), mirrored from the storefront's page types. */
20
+ export const PAGE_TYPES = [
21
+ "home",
22
+ "catalog",
23
+ "category",
24
+ "collection",
25
+ "product",
26
+ "page",
27
+ "landing",
28
+ "checkout",
29
+ "thankYou",
30
+ ];
31
+ /** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
32
+ export const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
33
+ /**
34
+ * A `menu` field's value: the handle of one of the store's menus
35
+ * (`main-menu`, `footer`). The server applies the same pattern to menu
36
+ * handles; a menu that does not exist reads as no menu.
37
+ */
38
+ export const MENU_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
39
+ export const COLLECTION_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
40
+ /**
41
+ * A `product` or `category` value: a handle or slug as the store's URLs use
42
+ * it. Merchants may type their own, so the rule only keeps out what cannot be
43
+ * one URL path segment: whitespace, control characters, `/`, `\`, `?`, `#`
44
+ * and `%`, and anything over 255 characters.
45
+ */
46
+ export const RESOURCE_HANDLE_PATTERN = /^[^\s/\\?#%\u0000-\u001f\u007f]{1,255}$/u;
47
+ /**
48
+ * A media object key in the store's bucket: a namespace and a name
49
+ * (`products/3f2a9c.webp`), segments of letters, digits, `.`, `_` and `-`
50
+ * that never start with a dot — so no `..`, no leading slash, no empty
51
+ * segment — and at most 512 characters.
52
+ */
53
+ export const MEDIA_KEY_PATTERN = /^(?=.{3,512}$)[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/;
54
+ /**
55
+ * A BCP 47 language tag in canonical case: a 2–3 letter language, an optional
56
+ * 4-letter script, an optional 2-letter or 3-digit region (`ar`, `zgh`,
57
+ * `ar-DZ`, `zh-Hant-TW`, `es-419`). Canonical case keeps map keys unique:
58
+ * `fr` and `FR` can never both be present.
59
+ */
60
+ const LOCALE_TAG_PATTERN = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?$/;
61
+ export function isLocaleTag(value) {
62
+ return LOCALE_TAG_PATTERN.test(value);
63
+ }
64
+ /**
65
+ * Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
66
+ * object with at least one entry, every key a locale tag, every value a string.
67
+ */
68
+ export function isLocaleMap(value) {
69
+ return isLocaleMapOf(value, (entry) => typeof entry === "string");
70
+ }
71
+ /**
72
+ * Whether `value` is a per-language map whose entries pass `isEntry`: a plain
73
+ * object with at least one entry, every key a locale tag.
74
+ */
75
+ export function isLocaleMapOf(value, isEntry) {
76
+ if (typeof value !== "object" || value === null || Array.isArray(value))
77
+ return false;
78
+ const entries = Object.entries(value);
79
+ return entries.length > 0 && entries.every(([key, entry]) => isLocaleTag(key) && isEntry(entry));
80
+ }
81
+ /**
82
+ * Whether a `url` field value is safe to render as an `href`: a site path,
83
+ * fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
84
+ * `javascript:`/`data:` and every other scheme, protocol-relative `//host`,
85
+ * backslashes (browsers read `/\host` as `//host`), whitespace and control
86
+ * characters, and anything over 2048 characters.
87
+ */
88
+ export function isSafeLinkTarget(value) {
89
+ if (value.length === 0 || value.length > 2048)
90
+ return false;
91
+ if (/[\s\\\u0000-\u001f\u007f]/.test(value))
92
+ return false;
93
+ if (value.startsWith("/"))
94
+ return !value.startsWith("//");
95
+ if (value.startsWith("#") || value.startsWith("?"))
96
+ return true;
97
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
98
+ if (scheme === "http" || scheme === "https") {
99
+ return /^https?:\/\/[a-z0-9.-]+(:\d{1,5})?([/?#]|$)/i.test(value);
100
+ }
101
+ if (scheme === "mailto" || scheme === "tel")
102
+ return value.length > scheme.length + 1;
103
+ return false;
104
+ }
@@ -1,3 +1,4 @@
1
+ import { PREVIEW_SECTION_LOAD_EVENT } from "./lifecycle.js";
1
2
  export interface PreviewBridgeOptions {
2
3
  /** The editor's origin: the verified token's `editor` claim. */
3
4
  editorOrigin: string;
@@ -14,11 +15,12 @@ export interface PreviewBridge {
14
15
  }
15
16
  /**
16
17
  * Dispatched on `document` for each section the bridge swapped in, with the
17
- * instance id as `detail.id`: a section with browser behavior binds it again.
18
- * When the whole page was replaced instead, the bridge dispatches Astro's own
19
- * `astro:page-load`, as Astro's router does after it swaps a page.
18
+ * instance id as `detail.id`: a section with browser behavior binds it again
19
+ * (`bindEach` from `@wocart/theme/lifecycle` does). When the whole page was
20
+ * replaced instead, the bridge dispatches Astro's own `astro:page-load`, as
21
+ * Astro's router does after it swaps a page.
20
22
  */
21
- export declare const PREVIEW_SECTION_LOAD_EVENT = "wocart:section-load";
23
+ export { PREVIEW_SECTION_LOAD_EVENT };
22
24
  /**
23
25
  * Starts the bridge on a preview page. It posts `ready` with every marked
24
26
  * instance, `layout` as the page scrolls or resizes, `hover` as the merchant
@@ -14,13 +14,15 @@
14
14
  * from the framing window.
15
15
  */
16
16
  import { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, previewEndMarker, previewStartMarker, } from "./preview-protocol.js";
17
+ import { PREVIEW_SECTION_LOAD_EVENT } from "./lifecycle.js";
17
18
  /**
18
19
  * Dispatched on `document` for each section the bridge swapped in, with the
19
- * instance id as `detail.id`: a section with browser behavior binds it again.
20
- * When the whole page was replaced instead, the bridge dispatches Astro's own
21
- * `astro:page-load`, as Astro's router does after it swaps a page.
20
+ * instance id as `detail.id`: a section with browser behavior binds it again
21
+ * (`bindEach` from `@wocart/theme/lifecycle` does). When the whole page was
22
+ * replaced instead, the bridge dispatches Astro's own `astro:page-load`, as
23
+ * Astro's router does after it swaps a page.
22
24
  */
23
- export const PREVIEW_SECTION_LOAD_EVENT = "wocart:section-load";
25
+ export { PREVIEW_SECTION_LOAD_EVENT };
24
26
  const ASTRO_PAGE_LOAD_EVENT = "astro:page-load";
25
27
  const SHOW_COMMENT = 0x80;
26
28
  const SHOW_TEXT = 0x4;
@@ -0,0 +1,41 @@
1
+ import type { PreviewDraft, StoreClient } from "@wocart/storefront/server";
2
+ import type { SurfacePageType, ThemeDefinition } from "./types.js";
3
+ /** What a preview renders. */
4
+ export interface PreviewTarget {
5
+ /** The surface the page renders: the draft's own, or for an area the page it is shown on. */
6
+ surface: string;
7
+ /** That surface's page type, when it declares one. */
8
+ pageType: SurfacePageType | undefined;
9
+ /** The area the draft edits (`area:header`), or null when it edits the page itself. */
10
+ area: string | null;
11
+ /**
12
+ * The route's parameters for the resource a template previews — `{ handle }`
13
+ * for a product or collection, `{ slug }` for a category, page or landing
14
+ * page — or none when the store has none of its kind yet.
15
+ */
16
+ params: Record<string, string>;
17
+ /** The page's document: the draft's when the page is what is edited, else what the store saved (null for none). */
18
+ document: unknown;
19
+ }
20
+ /**
21
+ * What an editor's preview of `draft` renders, or null — answer 404 — for a
22
+ * surface the theme does not declare:
23
+ *
24
+ * - A template previews the resource the draft's `path` names
25
+ * (`/products/robe`), else the store's first product, category, default
26
+ * collection or text page. A store with none still previews: `params` is
27
+ * empty and the template renders empty, so its sections can be arranged.
28
+ * - An area (the header, the footer) previews on the page its `path` names
29
+ * when the theme composes that page, else on the home page.
30
+ * - The resource is chosen by the surface's `pageType`, never its key, so an
31
+ * alternate (`product.compact`) previews like its base.
32
+ *
33
+ * ```astro
34
+ * ---
35
+ * const target = await previewTarget(theme, Astro.locals.preview.draft, Astro.locals.store.client);
36
+ * if (!target) return new Response(null, { status: 404, headers: { "Cache-Control": "no-store" } });
37
+ * ---
38
+ * <Sections surface={target.surface} params={target.params} document={target.document} preview={target.area === null} />
39
+ * ```
40
+ */
41
+ export declare function previewTarget(theme: ThemeDefinition, draft: PreviewDraft, client: StoreClient): Promise<PreviewTarget | null>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Which page an editor's preview shows, decided once for every theme: the
3
+ * surface a draft previews, the resource it previews it with, and the
4
+ * document it renders.
5
+ */
6
+ import { DEFAULT_COLLECTION_HANDLES, matchStorefrontPath } from "@wocart/storefront";
7
+ import { areaSurface } from "./blocks.js";
8
+ /** The surface a page type is shown with: the one keyed by the type itself, else the first declaring it. */
9
+ function surfaceFor(surfaces, pageType) {
10
+ return surfaces.find((surface) => surface.key === pageType) ?? surfaces.find((surface) => surface.pageType === pageType);
11
+ }
12
+ async function firstCollection(client) {
13
+ for (const handle of DEFAULT_COLLECTION_HANDLES)
14
+ if (await client.collections.get(handle))
15
+ return handle;
16
+ return undefined;
17
+ }
18
+ /** The resource a template of `pageType` previews: the one the path names, else the store's first of its kind. */
19
+ async function resourceParams(pageType, named, client) {
20
+ const only = (key, value) => (value === undefined ? {} : { [key]: value });
21
+ switch (pageType) {
22
+ case "product":
23
+ return only("handle", named.handle ?? (await client.products.list({ limit: 1 }))[0]?.handle);
24
+ case "collection":
25
+ return only("handle", named.handle ?? (await firstCollection(client)));
26
+ case "category":
27
+ return only("slug", named.slug ?? (await client.categories.list())[0]?.slug);
28
+ case "page":
29
+ return only("slug", named.slug ?? (await client.config.get())?.pages[0]?.slug);
30
+ case "landing":
31
+ return only("slug", named.slug);
32
+ default:
33
+ return {};
34
+ }
35
+ }
36
+ /**
37
+ * What an editor's preview of `draft` renders, or null — answer 404 — for a
38
+ * surface the theme does not declare:
39
+ *
40
+ * - A template previews the resource the draft's `path` names
41
+ * (`/products/robe`), else the store's first product, category, default
42
+ * collection or text page. A store with none still previews: `params` is
43
+ * empty and the template renders empty, so its sections can be arranged.
44
+ * - An area (the header, the footer) previews on the page its `path` names
45
+ * when the theme composes that page, else on the home page.
46
+ * - The resource is chosen by the surface's `pageType`, never its key, so an
47
+ * alternate (`product.compact`) previews like its base.
48
+ *
49
+ * ```astro
50
+ * ---
51
+ * const target = await previewTarget(theme, Astro.locals.preview.draft, Astro.locals.store.client);
52
+ * if (!target) return new Response(null, { status: 404, headers: { "Cache-Control": "no-store" } });
53
+ * ---
54
+ * <Sections surface={target.surface} params={target.params} document={target.document} preview={target.area === null} />
55
+ * ```
56
+ */
57
+ export async function previewTarget(theme, draft, client) {
58
+ const surfaces = theme.surfaces ?? [];
59
+ const match = draft.path ? matchStorefrontPath(draft.path) : null;
60
+ const area = (theme.areas ?? []).some((candidate) => areaSurface(candidate.key) === draft.surface) ? draft.surface : null;
61
+ const shown = area === null ? surfaces.find((surface) => surface.key === draft.surface) : (match ? surfaceFor(surfaces, match.type) : undefined) ?? surfaceFor(surfaces, "home");
62
+ if (shown === undefined)
63
+ return null;
64
+ const pageType = shown.pageType;
65
+ const named = match !== null && match.type === pageType ? { ...match.params } : {};
66
+ const [params, document] = await Promise.all([
67
+ resourceParams(pageType, named, client),
68
+ area === null && draft.document !== undefined ? draft.document : client.pageData.get(shown.key),
69
+ ]);
70
+ return { surface: shown.key, pageType, area, params, document };
71
+ }
package/dist/rich-text.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * value invalid. Rendering re-checks what it emits, so a value that skipped
8
8
  * validation still cannot produce markup.
9
9
  */
10
- import { isSafeLinkTarget } from "./schemas.js";
10
+ import { isSafeLinkTarget } from "./patterns.js";
11
11
  /** Every format a rich-text field can allow, in the order an editor lists them. */
12
12
  export const RICH_TEXT_FORMATS = [
13
13
  "bold",
package/dist/schemas.d.ts CHANGED
@@ -4,68 +4,8 @@
4
4
  */
5
5
  import { z } from "zod";
6
6
  import type { ComponentInstance, JsonValue } from "./types.js";
7
- /**
8
- * A theme's key: lowercase kebab-case, at most 64 characters. The platform
9
- * stores a merchant's text edits under it, so it is the theme's identity on the
10
- * platform, and the server applies the same rule.
11
- */
12
- export declare const THEME_KEY_PATTERN: RegExp;
13
- /** Type keys are lowercase kebab-case. */
14
- export declare const TYPE_KEY_PATTERN: RegExp;
15
- /**
16
- * The page-document **surface** key — the address of a composed page
17
- * (`home`, `product`, …; `area:<key>` is an area's document, and
18
- * `page:<slug>` / `lp:<slug>` are reserved conventions).
19
- *
20
- * The server applies the same pattern. Module-level on purpose: it is part of
21
- * the persisted-data contract,
22
- * not of the package's curated public surface (`src/index.ts`).
23
- */
24
- export declare const SURFACE_KEY_PATTERN: RegExp;
25
- /** The platform page types a surface can render (`SurfacePageType`), mirrored from the storefront's page types. */
26
- export declare const PAGE_TYPES: readonly ["home", "catalog", "category", "collection", "product", "page", "landing", "checkout", "thankYou"];
27
- /** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
28
- export declare const COLOR_PATTERN: RegExp;
29
- /**
30
- * A `menu` field's value: the handle of one of the store's menus
31
- * (`main-menu`, `footer`). The server applies the same pattern to menu
32
- * handles; a menu that does not exist reads as no menu.
33
- */
34
- export declare const MENU_HANDLE_PATTERN: RegExp;
35
- export declare const COLLECTION_HANDLE_PATTERN: RegExp;
36
- /**
37
- * A `product` or `category` value: a handle or slug as the store's URLs use
38
- * it. Merchants may type their own, so the rule only keeps out what cannot be
39
- * one URL path segment: whitespace, control characters, `/`, `\`, `?`, `#`
40
- * and `%`, and anything over 255 characters.
41
- */
42
- export declare const RESOURCE_HANDLE_PATTERN: RegExp;
43
- /**
44
- * A media object key in the store's bucket: a namespace and a name
45
- * (`products/3f2a9c.webp`), segments of letters, digits, `.`, `_` and `-`
46
- * that never start with a dot — so no `..`, no leading slash, no empty
47
- * segment — and at most 512 characters.
48
- */
49
- export declare const MEDIA_KEY_PATTERN: RegExp;
50
- export declare function isLocaleTag(value: string): boolean;
51
- /**
52
- * Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
53
- * object with at least one entry, every key a locale tag, every value a string.
54
- */
55
- export declare function isLocaleMap(value: unknown): value is Readonly<Record<string, string>>;
56
- /**
57
- * Whether `value` is a per-language map whose entries pass `isEntry`: a plain
58
- * object with at least one entry, every key a locale tag.
59
- */
60
- export declare function isLocaleMapOf<T>(value: unknown, isEntry: (entry: unknown) => entry is T): value is Readonly<Record<string, T>>;
61
- /**
62
- * Whether a `url` field value is safe to render as an `href`: a site path,
63
- * fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
64
- * `javascript:`/`data:` and every other scheme, protocol-relative `//host`,
65
- * backslashes (browsers read `/\host` as `//host`), whitespace and control
66
- * characters, and anything over 2048 characters.
67
- */
68
- export declare function isSafeLinkTarget(value: string): boolean;
7
+ import { COLLECTION_HANDLE_PATTERN, COLOR_PATTERN, isLocaleMap, isLocaleMapOf, isLocaleTag, isSafeLinkTarget, MEDIA_KEY_PATTERN, MENU_HANDLE_PATTERN, PAGE_TYPES, RESOURCE_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN } from "./patterns.js";
8
+ export { COLLECTION_HANDLE_PATTERN, COLOR_PATTERN, isLocaleMap, isLocaleMapOf, isLocaleTag, isSafeLinkTarget, MEDIA_KEY_PATTERN, MENU_HANDLE_PATTERN, PAGE_TYPES, RESOURCE_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, };
69
9
  export declare const JsonValueSchema: z.ZodType<JsonValue>;
70
10
  export declare const ComponentInstanceSchema: z.ZodType<ComponentInstance>;
71
11
  /**
package/dist/schemas.js CHANGED
@@ -3,110 +3,8 @@
3
3
  * validated. Definitions are typechecked code; page data is parsed data.
4
4
  */
5
5
  import { z } from "zod";
6
- /**
7
- * A theme's key: lowercase kebab-case, at most 64 characters. The platform
8
- * stores a merchant's text edits under it, so it is the theme's identity on the
9
- * platform, and the server applies the same rule.
10
- */
11
- export const THEME_KEY_PATTERN = /^(?=.{1,64}$)[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
12
- /** Type keys are lowercase kebab-case. */
13
- export const TYPE_KEY_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
14
- /**
15
- * The page-document **surface** key — the address of a composed page
16
- * (`home`, `product`, …; `area:<key>` is an area's document, and
17
- * `page:<slug>` / `lp:<slug>` are reserved conventions).
18
- *
19
- * The server applies the same pattern. Module-level on purpose: it is part of
20
- * the persisted-data contract,
21
- * not of the package's curated public surface (`src/index.ts`).
22
- */
23
- export const SURFACE_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,63}$/;
24
- /** The platform page types a surface can render (`SurfacePageType`), mirrored from the storefront's page types. */
25
- export const PAGE_TYPES = [
26
- "home",
27
- "catalog",
28
- "category",
29
- "collection",
30
- "product",
31
- "page",
32
- "landing",
33
- "checkout",
34
- "thankYou",
35
- ];
36
- /** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
37
- export const COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
38
- /**
39
- * A `menu` field's value: the handle of one of the store's menus
40
- * (`main-menu`, `footer`). The server applies the same pattern to menu
41
- * handles; a menu that does not exist reads as no menu.
42
- */
43
- export const MENU_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
44
- export const COLLECTION_HANDLE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
45
- /**
46
- * A `product` or `category` value: a handle or slug as the store's URLs use
47
- * it. Merchants may type their own, so the rule only keeps out what cannot be
48
- * one URL path segment: whitespace, control characters, `/`, `\`, `?`, `#`
49
- * and `%`, and anything over 255 characters.
50
- */
51
- export const RESOURCE_HANDLE_PATTERN = /^[^\s/\\?#%\u0000-\u001f\u007f]{1,255}$/u;
52
- /**
53
- * A media object key in the store's bucket: a namespace and a name
54
- * (`products/3f2a9c.webp`), segments of letters, digits, `.`, `_` and `-`
55
- * that never start with a dot — so no `..`, no leading slash, no empty
56
- * segment — and at most 512 characters.
57
- */
58
- export const MEDIA_KEY_PATTERN = /^(?=.{3,512}$)[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)+$/;
59
- /**
60
- * A BCP 47 language tag in canonical case: a 2–3 letter language, an optional
61
- * 4-letter script, an optional 2-letter or 3-digit region (`ar`, `zgh`,
62
- * `ar-DZ`, `zh-Hant-TW`, `es-419`). Canonical case keeps map keys unique:
63
- * `fr` and `FR` can never both be present.
64
- */
65
- const LOCALE_TAG_PATTERN = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-(?:[A-Z]{2}|\d{3}))?$/;
66
- export function isLocaleTag(value) {
67
- return LOCALE_TAG_PATTERN.test(value);
68
- }
69
- /**
70
- * Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
71
- * object with at least one entry, every key a locale tag, every value a string.
72
- */
73
- export function isLocaleMap(value) {
74
- return isLocaleMapOf(value, (entry) => typeof entry === "string");
75
- }
76
- /**
77
- * Whether `value` is a per-language map whose entries pass `isEntry`: a plain
78
- * object with at least one entry, every key a locale tag.
79
- */
80
- export function isLocaleMapOf(value, isEntry) {
81
- if (typeof value !== "object" || value === null || Array.isArray(value))
82
- return false;
83
- const entries = Object.entries(value);
84
- return entries.length > 0 && entries.every(([key, entry]) => isLocaleTag(key) && isEntry(entry));
85
- }
86
- /**
87
- * Whether a `url` field value is safe to render as an `href`: a site path,
88
- * fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
89
- * `javascript:`/`data:` and every other scheme, protocol-relative `//host`,
90
- * backslashes (browsers read `/\host` as `//host`), whitespace and control
91
- * characters, and anything over 2048 characters.
92
- */
93
- export function isSafeLinkTarget(value) {
94
- if (value.length === 0 || value.length > 2048)
95
- return false;
96
- if (/[\s\\\u0000-\u001f\u007f]/.test(value))
97
- return false;
98
- if (value.startsWith("/"))
99
- return !value.startsWith("//");
100
- if (value.startsWith("#") || value.startsWith("?"))
101
- return true;
102
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(value)?.[1]?.toLowerCase();
103
- if (scheme === "http" || scheme === "https") {
104
- return /^https?:\/\/[a-z0-9.-]+(:\d{1,5})?([/?#]|$)/i.test(value);
105
- }
106
- if (scheme === "mailto" || scheme === "tel")
107
- return value.length > scheme.length + 1;
108
- return false;
109
- }
6
+ import { COLLECTION_HANDLE_PATTERN, COLOR_PATTERN, isLocaleMap, isLocaleMapOf, isLocaleTag, isSafeLinkTarget, MEDIA_KEY_PATTERN, MENU_HANDLE_PATTERN, PAGE_TYPES, RESOURCE_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, } from "./patterns.js";
7
+ export { COLLECTION_HANDLE_PATTERN, COLOR_PATTERN, isLocaleMap, isLocaleMapOf, isLocaleTag, isSafeLinkTarget, MEDIA_KEY_PATTERN, MENU_HANDLE_PATTERN, PAGE_TYPES, RESOURCE_HANDLE_PATTERN, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN, };
110
8
  export const JsonValueSchema = z.lazy(() => z.union([
111
9
  z.string(),
112
10
  z
package/dist/style.d.ts CHANGED
@@ -29,7 +29,16 @@ export declare function styleClasses(value: StyleValue | undefined): string;
29
29
  export declare function themeStyleSheet(style: ThemeStyle | undefined): string;
30
30
  /** Why `value` is not a list of color schemes, or `null`. */
31
31
  export declare function colorSchemesProblem(value: unknown): string | null;
32
- /** The class that gives an element a color scheme, or `""` for what is not a scheme id. */
32
+ /**
33
+ * The class every element wearing a color scheme carries, whichever scheme it
34
+ * is. A theme derives its own tokens from the roles on `:root, .wc-scheme`,
35
+ * so they follow the scheme of the nearest element that wears one.
36
+ */
37
+ export declare const SCHEME_MARKER_CLASS = "wc-scheme";
38
+ /**
39
+ * The classes that give an element a color scheme — `wc-scheme
40
+ * wc-scheme-<id>` — or `""` for what is not a scheme id.
41
+ */
33
42
  export declare function schemeClass(id: string | undefined): string;
34
43
  /**
35
44
  * The store's color schemes as CSS: each scheme's roles as custom properties
package/dist/style.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * as custom properties on `.wc-scheme-<id>`, dropping anything that is not
12
12
  * a scheme id and a hex color.
13
13
  */
14
- import { COLOR_PATTERN, isLocaleMap } from "./schemas.js";
14
+ import { COLOR_PATTERN, isLocaleMap } from "./patterns.js";
15
15
  /** The platform's breakpoints, phone first, with the width each starts at. */
16
16
  export const STYLE_BREAKPOINTS = [
17
17
  ["base", 0],
@@ -203,9 +203,18 @@ export function colorSchemesProblem(value) {
203
203
  }
204
204
  return null;
205
205
  }
206
- /** The class that gives an element a color scheme, or `""` for what is not a scheme id. */
206
+ /**
207
+ * The class every element wearing a color scheme carries, whichever scheme it
208
+ * is. A theme derives its own tokens from the roles on `:root, .wc-scheme`,
209
+ * so they follow the scheme of the nearest element that wears one.
210
+ */
211
+ export const SCHEME_MARKER_CLASS = "wc-scheme";
212
+ /**
213
+ * The classes that give an element a color scheme — `wc-scheme
214
+ * wc-scheme-<id>` — or `""` for what is not a scheme id.
215
+ */
207
216
  export function schemeClass(id) {
208
- return typeof id === "string" && COLOR_SCHEME_ID_PATTERN.test(id) ? `wc-scheme-${id}` : "";
217
+ return typeof id === "string" && COLOR_SCHEME_ID_PATTERN.test(id) ? `${SCHEME_MARKER_CLASS} wc-scheme-${id}` : "";
209
218
  }
210
219
  const roleProperty = (role) => `--wc-color-${role.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
211
220
  /**
package/dist/types.d.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import type { StoreClient } from "@wocart/storefront/server";
9
9
  import type { StoreConfig } from "@wocart/storefront";
10
- /** Every value page data may hold. */
11
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
10
+ /** Every value page data may hold. Lists may be read-only, as the rich-text format's are. */
11
+ export type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
12
12
  [key: string]: JsonValue;
13
13
  };
14
14
  /** A JSON object, as stored in `ComponentInstance.props`. */
@@ -352,46 +352,47 @@ export interface ListItem {
352
352
  }
353
353
  export type RichTextMark = "bold" | "italic" | "underline" | "strike";
354
354
  /** A run of text. `link` is a safe link target, as a `url` field's. */
355
- export interface RichTextText {
355
+ export type RichTextText = {
356
356
  type: "text";
357
357
  text: string;
358
358
  marks?: readonly RichTextMark[];
359
359
  link?: string;
360
- }
360
+ };
361
361
  /** A line break inside a block. */
362
- export interface RichTextBreak {
362
+ export type RichTextBreak = {
363
363
  type: "break";
364
- }
364
+ };
365
365
  export type RichTextInline = RichTextText | RichTextBreak;
366
- export interface RichTextParagraph {
366
+ export type RichTextParagraph = {
367
367
  type: "paragraph";
368
368
  content?: readonly RichTextInline[];
369
- }
369
+ };
370
370
  /** Levels 2–4: the page owns its level-1 heading. */
371
- export interface RichTextHeading {
371
+ export type RichTextHeading = {
372
372
  type: "heading";
373
373
  level: 2 | 3 | 4;
374
374
  content?: readonly RichTextInline[];
375
- }
376
- export interface RichTextListItem {
375
+ };
376
+ export type RichTextListItem = {
377
377
  content?: readonly RichTextInline[];
378
- }
379
- export interface RichTextList {
378
+ };
379
+ export type RichTextList = {
380
380
  type: "list";
381
381
  style: "bullet" | "ordered";
382
382
  items: readonly RichTextListItem[];
383
- }
383
+ };
384
384
  export type RichTextBlock = RichTextParagraph | RichTextHeading | RichTextList;
385
385
  /**
386
386
  * A rich-text value. A closed format: anything not listed here is invalid, so
387
387
  * no stored value can carry markup, scripts or styles. `version` lets the
388
- * format grow; a reader rejects a version it does not know.
388
+ * format grow; a reader rejects a version it does not know. Like every
389
+ * rich-text type it is plain JSON, so a preset's props can hold one.
389
390
  */
390
- export interface RichTextDocument {
391
+ export type RichTextDocument = {
391
392
  type: "doc";
392
393
  version: 1;
393
394
  content: readonly RichTextBlock[];
394
- }
395
+ };
395
396
  /** A rich-text field's stored value: one document, or one per language. */
396
397
  export type RichTextValue = RichTextDocument | Readonly<Record<LocaleCode, RichTextDocument>>;
397
398
  /**
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * reads. Always equal to `package.json`'s version; the package check refuses
5
5
  * a release where they differ.
6
6
  */
7
- export declare const THEME_SDK_VERSION = "2.0.0";
7
+ export declare const THEME_SDK_VERSION = "2.2.0";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * reads. Always equal to `package.json`'s version; the package check refuses
5
5
  * a release where they differ.
6
6
  */
7
- export const THEME_SDK_VERSION = "2.0.0";
7
+ export const THEME_SDK_VERSION = "2.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/theme",
3
- "version": "2.0.0",
3
+ "version": "2.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",
@@ -49,6 +49,14 @@
49
49
  "types": "./dist/preview-bridge.d.ts",
50
50
  "default": "./dist/preview-bridge.js"
51
51
  },
52
+ "./lifecycle": {
53
+ "types": "./dist/lifecycle.d.ts",
54
+ "default": "./dist/lifecycle.js"
55
+ },
56
+ "./messages": {
57
+ "types": "./dist/messages-entry.d.ts",
58
+ "default": "./dist/messages-entry.js"
59
+ },
52
60
  "./package.json": "./package.json"
53
61
  },
54
62
  "files": [
@@ -74,7 +82,7 @@
74
82
  "zod": "^4.4.3"
75
83
  },
76
84
  "peerDependencies": {
77
- "@wocart/storefront": "^1.14.0",
85
+ "@wocart/storefront": "^1.20.0",
78
86
  "astro": "^7.0.0"
79
87
  },
80
88
  "peerDependenciesMeta": {