@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.
- package/CHANGELOG.md +42 -0
- package/README.md +102 -7
- package/dist/astro/index.d.ts +2 -0
- package/dist/astro/index.js +3 -0
- package/dist/definitions.d.ts +16 -1
- package/dist/definitions.js +19 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/lifecycle.d.ts +49 -0
- package/dist/lifecycle.js +120 -0
- package/dist/manifest-diff.js +32 -0
- package/dist/messages-entry.d.ts +7 -0
- package/dist/messages-entry.js +6 -0
- package/dist/patterns.d.ts +62 -0
- package/dist/patterns.js +104 -0
- package/dist/preview-bridge.d.ts +6 -4
- package/dist/preview-bridge.js +6 -4
- package/dist/preview-target.d.ts +41 -0
- package/dist/preview-target.js +71 -0
- package/dist/rich-text.js +1 -1
- package/dist/schemas.d.ts +2 -62
- package/dist/schemas.js +2 -104
- package/dist/style.d.ts +10 -1
- package/dist/style.js +12 -3
- package/dist/types.d.ts +18 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -2
package/dist/patterns.js
ADDED
|
@@ -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
|
+
}
|
package/dist/preview-bridge.d.ts
CHANGED
|
@@ -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
|
|
19
|
-
* `astro:page-load`, as
|
|
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
|
|
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
|
package/dist/preview-bridge.js
CHANGED
|
@@ -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
|
|
21
|
-
* `astro:page-load`, as
|
|
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
|
|
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 "./
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 "./
|
|
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
|
-
/**
|
|
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) ?
|
|
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
|
|
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
|
|
362
|
+
export type RichTextBreak = {
|
|
363
363
|
type: "break";
|
|
364
|
-
}
|
|
364
|
+
};
|
|
365
365
|
export type RichTextInline = RichTextText | RichTextBreak;
|
|
366
|
-
export
|
|
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
|
|
371
|
+
export type RichTextHeading = {
|
|
372
372
|
type: "heading";
|
|
373
373
|
level: 2 | 3 | 4;
|
|
374
374
|
content?: readonly RichTextInline[];
|
|
375
|
-
}
|
|
376
|
-
export
|
|
375
|
+
};
|
|
376
|
+
export type RichTextListItem = {
|
|
377
377
|
content?: readonly RichTextInline[];
|
|
378
|
-
}
|
|
379
|
-
export
|
|
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
|
|
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
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wocart/theme",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
85
|
+
"@wocart/storefront": "^1.20.0",
|
|
78
86
|
"astro": "^7.0.0"
|
|
79
87
|
},
|
|
80
88
|
"peerDependenciesMeta": {
|