@wocart/theme 2.1.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 +31 -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/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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ 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
|
+
## 2.2.0 — 2026-10-24
|
|
9
|
+
|
|
10
|
+
What every theme used to write itself is now the package's: binding browser
|
|
11
|
+
behaviour through page and section swaps, choosing what an editor's preview
|
|
12
|
+
shows, and a marker for color schemes. Browser code takes only what builds no
|
|
13
|
+
validator.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `@wocart/theme/lifecycle`: `bindEach(selector, bind)` binds each element
|
|
18
|
+
once — when the page is ready, after Astro swaps a page, after the editor
|
|
19
|
+
swaps a section — and runs a bind's teardown when its element leaves;
|
|
20
|
+
`onEachPage(boot)`; `PREVIEW_SECTION_LOAD_EVENT`, without the bridge.
|
|
21
|
+
- `@wocart/theme/messages`: the message functions for browser code.
|
|
22
|
+
`pluralMessageKeys` is now exported, here and from the root.
|
|
23
|
+
- `previewTarget(theme, draft, client)` in `@wocart/theme/astro`: the surface,
|
|
24
|
+
route parameters and document an editor's preview renders, chosen by page
|
|
25
|
+
type, or null for a surface the theme does not declare.
|
|
26
|
+
- `SCHEME_MARKER_CLASS`: `schemeClass(id)` gives `wc-scheme wc-scheme-<id>`,
|
|
27
|
+
so a theme derives its tokens on `:root, .wc-scheme`.
|
|
28
|
+
- `defineCapabilities(capabilities)` for capabilities several components share,
|
|
29
|
+
and `componentsOf(theme)` to iterate a theme's components as definitions.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The rich-text types are plain JSON: a preset or a default holds a document
|
|
34
|
+
without a cast. `JsonValue` accepts read-only lists.
|
|
35
|
+
- The style and rich-text helpers no longer bring Zod into a browser bundle.
|
|
36
|
+
- The `@wocart/storefront` peer is `^1.20.0`: `previewTarget` uses its route
|
|
37
|
+
matcher and default collections.
|
|
38
|
+
|
|
8
39
|
## 2.1.0 — 2026-10-21
|
|
9
40
|
|
|
10
41
|
The release check now sees where a section may be placed.
|
package/README.md
CHANGED
|
@@ -39,12 +39,19 @@ Node 22.12 or newer.
|
|
|
39
39
|
| Import | What it holds |
|
|
40
40
|
| --- | --- |
|
|
41
41
|
| `@wocart/theme` | definitions, validation, page documents, resolution, document operations, rich text output, the manifest and its diff, languages and messages — framework-neutral |
|
|
42
|
-
| `@wocart/theme/astro` | `createReadCache`, `themeManifestEndpoint` (the manifest as a build artifact), `hasBlocks`, `isPreview`, `THEME_LIMITS`, `themeStylesEndpoint`, the preview markers and the `BlocksHandle` and `RendererIssue` types |
|
|
42
|
+
| `@wocart/theme/astro` | `createReadCache`, `themeManifestEndpoint` (the manifest as a build artifact), `hasBlocks`, `isPreview`, `THEME_LIMITS`, `themeStylesEndpoint`, `previewTarget`, the preview markers and the `BlocksHandle` and `RendererIssue` types |
|
|
43
43
|
| `@wocart/theme/astro/InstanceView.astro` | the reference renderer component |
|
|
44
44
|
| `@wocart/theme/astro/Blocks.astro`, `@wocart/theme/astro/Block.astro` | a slot's blocks and a fixed block, placed by a component (see [Blocks](#blocks)) |
|
|
45
45
|
| `@wocart/theme/astro/ThemeStyles.astro` | the stylesheet link and the store's color schemes, for a layout's `<head>` (see [Style](#style)) |
|
|
46
46
|
| `@wocart/theme/image-service` | `mediaImageService` — an Astro image service that resizes media-domain images on the media domain (any storefront host) |
|
|
47
47
|
| `@wocart/theme/preview` | `startPreviewBridge` — the browser module of a theme's preview page (see [Preview](#preview)) |
|
|
48
|
+
| `@wocart/theme/lifecycle` | `bindEach`, `onEachPage`, `PREVIEW_SECTION_LOAD_EVENT` — binding browser behaviour so it survives page and section swaps; tiny, without the bridge (see [Browser scripts](#browser-scripts)) |
|
|
49
|
+
| `@wocart/theme/messages` | `formatMessage`, `messageForms`, `pluralMessageKeys`, `pluralFormIssues` for browser code (see [Messages](#messages-placeholders-and-plurals)) |
|
|
50
|
+
|
|
51
|
+
Browser code imports only what builds no validator: `./lifecycle`,
|
|
52
|
+
`./messages`, and from the root the style, rich-text and message helpers. A
|
|
53
|
+
test of this package bundles each of them and fails if Zod or the editor's
|
|
54
|
+
bridge reaches a shopper's page.
|
|
48
55
|
|
|
49
56
|
## Use
|
|
50
57
|
|
|
@@ -207,6 +214,9 @@ localize site links. `richTextToPlainText` gives the words for a meta
|
|
|
207
214
|
description or an `aria-label`. A field's `formats` (`["bold", "link"]`)
|
|
208
215
|
narrows what a merchant may use.
|
|
209
216
|
|
|
217
|
+
The rich-text types are plain JSON (`RichTextDocument` is a `JsonObject`), so
|
|
218
|
+
a preset or a default can hold a document without a cast.
|
|
219
|
+
|
|
210
220
|
### Lists
|
|
211
221
|
|
|
212
222
|
```ts
|
|
@@ -429,7 +439,21 @@ export const GET = themeStylesEndpoint(theme);
|
|
|
429
439
|
|
|
430
440
|
The schemes become custom properties on `.wc-scheme-<id>`
|
|
431
441
|
(`--wc-color-background`, `--wc-color-button-text`, …) with the background
|
|
432
|
-
and text applied.
|
|
442
|
+
and text applied. `schemeClass(id)` also gives the element `wc-scheme`
|
|
443
|
+
(`SCHEME_MARKER_CLASS`), the one class every element wearing a scheme
|
|
444
|
+
carries. Derive the theme's own tokens there and on the page, so they follow
|
|
445
|
+
the scheme of the nearest element that wears one:
|
|
446
|
+
|
|
447
|
+
```css
|
|
448
|
+
:root, .wc-scheme {
|
|
449
|
+
--surface: var(--wc-color-background, #fff);
|
|
450
|
+
--ink: var(--wc-color-text, #1d1d1f);
|
|
451
|
+
--line: color-mix(in srgb, var(--wc-color-border, #e5e5e5) 70%, transparent);
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Without that rule a token computed once on `:root` keeps the page's colors
|
|
456
|
+
inside a section that wears another scheme. On a page with a Content-Security-Policy, `ThemeStyles` adds
|
|
433
457
|
its inline sheet's hash to the policy, so `style-src` needs no
|
|
434
458
|
`'unsafe-inline'`. `contrastRatio(a, b)` gives the WCAG ratio an editor warns
|
|
435
459
|
on (below 4.5 for text).
|
|
@@ -500,7 +524,13 @@ presets: [
|
|
|
500
524
|
```
|
|
501
525
|
|
|
502
526
|
`instantiatePreset(theme, type, key, newId)` builds the instances, each with an
|
|
503
|
-
id from `newId`. Presets are checked at assembly by building them.
|
|
527
|
+
id from `newId`. Presets are checked at assembly by building them. A preset's
|
|
528
|
+
props may hold rich text as it is written:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
const welcome: RichTextDocument = { type: "doc", version: 1, content: [{ type: "paragraph", content: [{ type: "text", text: "Bienvenue" }] }] };
|
|
532
|
+
presets: [{ key: "welcome", label: "Welcome", props: { body: welcome } }],
|
|
533
|
+
```
|
|
504
534
|
|
|
505
535
|
## Hidden sections
|
|
506
536
|
|
|
@@ -622,12 +652,62 @@ later.
|
|
|
622
652
|
A swapped section's scripts do not run again. A section with browser behavior
|
|
623
653
|
binds it again on `PREVIEW_SECTION_LOAD_EVENT` (`wocart:section-load`,
|
|
624
654
|
dispatched on `document` with `detail.id`) — also for a section a patch
|
|
625
|
-
copied, restored or showed again.
|
|
655
|
+
copied, restored or showed again. `bindEach` (see [Browser scripts](#browser-scripts))
|
|
656
|
+
does it for you. When the bridge replaces the whole
|
|
626
657
|
page instead, it does what Astro's router does after a navigation — a new
|
|
627
658
|
`<body>`, the root element's attributes, the title — and dispatches
|
|
628
659
|
`astro:page-load`, so a script written for the router mounts again as it does
|
|
629
660
|
there. Changed stylesheets are replaced in place; unchanged ones never reload.
|
|
630
661
|
|
|
662
|
+
### Which page a preview shows
|
|
663
|
+
|
|
664
|
+
`previewTarget(theme, draft, client)` from `@wocart/theme/astro` decides it
|
|
665
|
+
for every theme: the surface to render, its route parameters and its
|
|
666
|
+
document — or null, answered 404, for a surface the theme does not declare.
|
|
667
|
+
|
|
668
|
+
- A template previews the resource the draft's path names, else the store's
|
|
669
|
+
first product, category, default collection or text page; a store with
|
|
670
|
+
none still previews, with empty `params`, so its sections can be arranged.
|
|
671
|
+
- An area (`area:header`) previews on the page its path names when the theme
|
|
672
|
+
composes it, else home; `area` names it, and the page's own document is
|
|
673
|
+
the saved one.
|
|
674
|
+
- The resource is chosen by the surface's `pageType`, so an alternate
|
|
675
|
+
(`product.compact`) previews like its base.
|
|
676
|
+
|
|
677
|
+
```astro
|
|
678
|
+
---
|
|
679
|
+
const target = await previewTarget(theme, Astro.locals.preview.draft, Astro.locals.store.client);
|
|
680
|
+
if (!target) return new Response(null, { status: 404, headers: { "Cache-Control": "no-store" } });
|
|
681
|
+
---
|
|
682
|
+
<Sections surface={target.surface} params={target.params} document={target.document} preview={target.area === null} />
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
## Browser scripts
|
|
686
|
+
|
|
687
|
+
A theme's browser behaviour meets markup three ways: the page loads, Astro's
|
|
688
|
+
router swaps the page (`astro:page-load`), an editor's preview swaps one
|
|
689
|
+
section (`PREVIEW_SECTION_LOAD_EVENT`). `@wocart/theme/lifecycle` binds it
|
|
690
|
+
once per element whichever way it came:
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
import { bindEach, onEachPage } from "@wocart/theme/lifecycle";
|
|
694
|
+
|
|
695
|
+
bindEach<HTMLFormElement>("[data-order-form]", (form) => {
|
|
696
|
+
const onResize = () => layout(form);
|
|
697
|
+
window.addEventListener("resize", onResize);
|
|
698
|
+
return () => window.removeEventListener("resize", onResize);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
onEachPage(() => {
|
|
702
|
+
const timer = setInterval(tick, 1000);
|
|
703
|
+
return () => clearInterval(timer);
|
|
704
|
+
});
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
A bind's teardown runs when its element leaves — before Astro swaps the page
|
|
708
|
+
out, or when a later pass finds it gone — so document and window listeners
|
|
709
|
+
never pile up. Both return a function that stops them.
|
|
710
|
+
|
|
631
711
|
## Theme manifest (serializable catalogue)
|
|
632
712
|
|
|
633
713
|
A `ThemeDefinition` mixes data (types, fields, defaults) with runtime (the live
|
|
@@ -877,7 +957,10 @@ message rule — Shopify's locale-file model on the platform's own CLDR data
|
|
|
877
957
|
- `pluralFormIssues(pack, locale)` lists the forms a pack is missing for its
|
|
878
958
|
language, or carries but never uses — run it over every pack in a test.
|
|
879
959
|
- `messageForms(pack, key)` collects one message's keys to hand a browser
|
|
880
|
-
script.
|
|
960
|
+
script; `pluralMessageKeys(pack)` names every plural message in a pack.
|
|
961
|
+
|
|
962
|
+
Browser code imports the same functions from `@wocart/theme/messages`, an
|
|
963
|
+
entry with nothing else in it.
|
|
881
964
|
|
|
882
965
|
Packs stay `Record<string, string>`, so store overrides and the editor treat
|
|
883
966
|
plural forms like any other key.
|
|
@@ -885,7 +968,17 @@ plural forms like any other key.
|
|
|
885
968
|
## Capabilities are advisory
|
|
886
969
|
|
|
887
970
|
`ComponentDefinition.capabilities` (`insertable`, `surfaces`) is metadata for an **editor/CLI insertion
|
|
888
|
-
surface**, not renderer enforcement.
|
|
971
|
+
surface**, not renderer enforcement. Capabilities several components share go
|
|
972
|
+
through `defineCapabilities`:
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
const ANY_PAGE = defineCapabilities({ surfaces: ["home", "product", "collection"] });
|
|
976
|
+
const grid = defineComponent({ type: "product-grid", capabilities: ANY_PAGE, /* … */ });
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
A plain constant inferred from a literal (`{ surfaces: [...] as const }`) can
|
|
980
|
+
make TypeScript read a definition's `fields` in two shapes when its `load`
|
|
981
|
+
types its props from them; the error then names an unrelated field. Neither `parsePageData` nor
|
|
889
982
|
`resolveInstance` consults it: a saved document containing a component marked
|
|
890
983
|
`insertable: false` still renders, because the render path never second-guesses
|
|
891
984
|
a document that was already saved. Any future enforcement belongs at insertion
|
|
@@ -962,7 +1055,9 @@ A theme proves it honors the contract with four checks in its own test suite:
|
|
|
962
1055
|
|
|
963
1056
|
1. **The catalog assembles clean** — with its packs declared on the theme,
|
|
964
1057
|
its default documents and presets are checked too:
|
|
965
|
-
`expect(validateThemeDefinition(theme)).toEqual([])`.
|
|
1058
|
+
`expect(validateThemeDefinition(theme)).toEqual([])`. To check something
|
|
1059
|
+
of every component, iterate `componentsOf(theme)`, which lists them as
|
|
1060
|
+
`ComponentDefinition`s.
|
|
966
1061
|
2. **The manifest survives a JSON round trip**:
|
|
967
1062
|
`validateThemeManifest(JSON.parse(JSON.stringify(themeManifest(theme))))`
|
|
968
1063
|
returns no issues.
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { resolveInstance } from "../instances.js";
|
|
|
4
4
|
export { THEME_LIMITS } from "../blocks.js";
|
|
5
5
|
export { THEME_STYLES_PATH, colorSchemeCss } from "../style.js";
|
|
6
6
|
export { previewEndMarker, previewStartMarker } from "../preview-protocol.js";
|
|
7
|
+
export { previewTarget } from "../preview-target.js";
|
|
8
|
+
export type { PreviewTarget } from "../preview-target.js";
|
|
7
9
|
/**
|
|
8
10
|
* The theme's manifest as a prerendered endpoint, the file a release and an
|
|
9
11
|
* editor read (`THEME_MANIFEST_PATH`):
|
package/dist/astro/index.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* `InstanceView`'s `onIssue` prop.
|
|
12
12
|
* - `BlocksHandle` and `hasBlocks` — what a component receives as `blocks`,
|
|
13
13
|
* and whether one of its slots has anything to render.
|
|
14
|
+
* - `previewTarget` — which surface, resource and document an editor's
|
|
15
|
+
* preview renders.
|
|
14
16
|
* - `resolveInstance` and `THEME_LIMITS` — re-exported so `InstanceView.astro`'s
|
|
15
17
|
* co-located import (`./index.js`) resolves identically in the dev source
|
|
16
18
|
* layout and in the published `dist/astro` layout. The same values as the
|
|
@@ -26,6 +28,7 @@ export { resolveInstance } from "../instances.js";
|
|
|
26
28
|
export { THEME_LIMITS } from "../blocks.js";
|
|
27
29
|
export { THEME_STYLES_PATH, colorSchemeCss } from "../style.js";
|
|
28
30
|
export { previewEndMarker, previewStartMarker } from "../preview-protocol.js";
|
|
31
|
+
export { previewTarget } from "../preview-target.js";
|
|
29
32
|
/**
|
|
30
33
|
* The theme's manifest as a prerendered endpoint, the file a release and an
|
|
31
34
|
* editor read (`THEME_MANIFEST_PATH`):
|
package/dist/definitions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentDefinition, FieldDefinition, FieldValues, LoadContext, ThemeDefinition, ThemeDefinitionIssue, ThemeValidationOptions } from "./types.js";
|
|
1
|
+
import type { ComponentCapabilities, ComponentDefinition, FieldDefinition, FieldValues, LoadContext, ThemeDefinition, ThemeDefinitionIssue, ThemeValidationOptions } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Identity helper: returns the definition unchanged so literal types survive.
|
|
4
4
|
* `load` receives the component's props typed from its own `fields`, so a
|
|
@@ -20,6 +20,21 @@ type ComponentBase = Omit<ComponentDefinition, "fields" | "load">;
|
|
|
20
20
|
type FieldsPart<Fields extends readonly FieldDefinition[]> = [Fields] extends [readonly []] ? {} : {
|
|
21
21
|
fields: Fields;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Identity helper for capabilities several components share:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* const ANY_PAGE = defineCapabilities({ surfaces: ["home", "product", "collection"] });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* Share them through it rather than as a plain constant. A constant inferred
|
|
31
|
+
* from a literal (`{ surfaces: [...] as const }`) can make TypeScript read a
|
|
32
|
+
* definition's `fields` twice in two shapes when its `load` types its props
|
|
33
|
+
* from them, and the error then names an unrelated field.
|
|
34
|
+
*/
|
|
35
|
+
export declare function defineCapabilities(capabilities: ComponentCapabilities): ComponentCapabilities;
|
|
36
|
+
/** A theme's components as definitions, in its catalog's order — what a test or a tool iterates. */
|
|
37
|
+
export declare function componentsOf(theme: Pick<ThemeDefinition, "components">): ComponentDefinition[];
|
|
23
38
|
/** Identity helper for the catalog. */
|
|
24
39
|
export declare function defineTheme<const T extends ThemeDefinition>(definition: T): T;
|
|
25
40
|
/** Check the whole catalog: key/type identity, field rules, slot rules. */
|
package/dist/definitions.js
CHANGED
|
@@ -15,6 +15,25 @@ import { COLOR_PATTERN, MENU_HANDLE_PATTERN, COLLECTION_HANDLE_PATTERN, PAGE_TYP
|
|
|
15
15
|
export function defineComponent(definition) {
|
|
16
16
|
return definition;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Identity helper for capabilities several components share:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* const ANY_PAGE = defineCapabilities({ surfaces: ["home", "product", "collection"] });
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Share them through it rather than as a plain constant. A constant inferred
|
|
26
|
+
* from a literal (`{ surfaces: [...] as const }`) can make TypeScript read a
|
|
27
|
+
* definition's `fields` twice in two shapes when its `load` types its props
|
|
28
|
+
* from them, and the error then names an unrelated field.
|
|
29
|
+
*/
|
|
30
|
+
export function defineCapabilities(capabilities) {
|
|
31
|
+
return capabilities;
|
|
32
|
+
}
|
|
33
|
+
/** A theme's components as definitions, in its catalog's order — what a test or a tool iterates. */
|
|
34
|
+
export function componentsOf(theme) {
|
|
35
|
+
return Object.values(theme.components);
|
|
36
|
+
}
|
|
18
37
|
/** Identity helper for the catalog. */
|
|
19
38
|
export function defineTheme(definition) {
|
|
20
39
|
return definition;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* This entry is framework-neutral. The reference Astro renderer lives in the
|
|
12
12
|
* `./astro` subpath.
|
|
13
13
|
*/
|
|
14
|
-
export { defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
14
|
+
export { componentsOf, defineCapabilities, defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
15
15
|
export { parsePageData, validatePageData } from "./page-data.js";
|
|
16
16
|
export { resolveInstance, createInstance, checkStoredValue as checkFieldValue } from "./instances.js";
|
|
17
17
|
export type { CheckedValue as FieldValueCheck } from "./instances.js";
|
|
@@ -20,7 +20,7 @@ export { instantiatePreset, isInsertableOn } from "./composition.js";
|
|
|
20
20
|
export { COMPONENT_PLACEMENTS, COMPONENT_ROLES, THEME_LIMITS, areaSurface, fixedBlockId } from "./blocks.js";
|
|
21
21
|
export { completePage, resolveSurfaceDocument } from "./surface-document.js";
|
|
22
22
|
export { CONDITION_DEPTH, isFieldVisible } from "./conditions.js";
|
|
23
|
-
export { COLOR_ROLES, COLOR_SCHEMES_MAX, STYLE_BREAKPOINTS, STYLE_PANELS, STYLE_STEPS, STYLE_WIDTHS, THEME_STYLES_PATH, colorSchemeCss, contrastRatio, schemeClass, styleClasses, themeStyleSheet, } from "./style.js";
|
|
23
|
+
export { COLOR_ROLES, COLOR_SCHEMES_MAX, STYLE_BREAKPOINTS, STYLE_PANELS, STYLE_STEPS, STYLE_WIDTHS, THEME_STYLES_PATH, SCHEME_MARKER_CLASS, colorSchemeCss, contrastRatio, schemeClass, styleClasses, themeStyleSheet, } from "./style.js";
|
|
24
24
|
export type { SurfaceDocument, SurfaceDocumentReason } from "./surface-document.js";
|
|
25
25
|
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
26
26
|
export type { DocumentEdit, DocumentIssue, DocumentIssueCode, InstanceLocation, InstancePlacement, } from "./operations.js";
|
|
@@ -28,7 +28,7 @@ export { THEME_SDK_VERSION } from "./version.js";
|
|
|
28
28
|
export type { RichTextHtmlOptions } from "./rich-text.js";
|
|
29
29
|
export { resolveThemeSettings, checkThemeSettings } from "./settings.js";
|
|
30
30
|
export { localize } from "./locales.js";
|
|
31
|
-
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
|
|
31
|
+
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues, pluralMessageKeys } from "./messages.js";
|
|
32
32
|
export type { MessageOptions, MessageValues, PluralCategory } from "./messages.js";
|
|
33
33
|
export { THEME_MANIFEST_CONTRACT_VERSION, THEME_MANIFEST_PATH, manifestCatalog, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
34
34
|
export { diffThemeManifests } from "./manifest-diff.js";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* This entry is framework-neutral. The reference Astro renderer lives in the
|
|
12
12
|
* `./astro` subpath.
|
|
13
13
|
*/
|
|
14
|
-
export { defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
14
|
+
export { componentsOf, defineCapabilities, defineComponent, defineTheme, validateThemeDefinition } from "./definitions.js";
|
|
15
15
|
export { parsePageData, validatePageData } from "./page-data.js";
|
|
16
16
|
export { resolveInstance, createInstance, checkStoredValue as checkFieldValue } from "./instances.js";
|
|
17
17
|
export { RICH_TEXT_FORMATS, richTextToHtml, richTextToPlainText } from "./rich-text.js";
|
|
@@ -19,12 +19,12 @@ export { instantiatePreset, isInsertableOn } from "./composition.js";
|
|
|
19
19
|
export { COMPONENT_PLACEMENTS, COMPONENT_ROLES, THEME_LIMITS, areaSurface, fixedBlockId } from "./blocks.js";
|
|
20
20
|
export { completePage, resolveSurfaceDocument } from "./surface-document.js";
|
|
21
21
|
export { CONDITION_DEPTH, isFieldVisible } from "./conditions.js";
|
|
22
|
-
export { COLOR_ROLES, COLOR_SCHEMES_MAX, STYLE_BREAKPOINTS, STYLE_PANELS, STYLE_STEPS, STYLE_WIDTHS, THEME_STYLES_PATH, colorSchemeCss, contrastRatio, schemeClass, styleClasses, themeStyleSheet, } from "./style.js";
|
|
22
|
+
export { COLOR_ROLES, COLOR_SCHEMES_MAX, STYLE_BREAKPOINTS, STYLE_PANELS, STYLE_STEPS, STYLE_WIDTHS, THEME_STYLES_PATH, SCHEME_MARKER_CLASS, colorSchemeCss, contrastRatio, schemeClass, styleClasses, themeStyleSheet, } from "./style.js";
|
|
23
23
|
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
24
24
|
export { THEME_SDK_VERSION } from "./version.js";
|
|
25
25
|
export { resolveThemeSettings, checkThemeSettings } from "./settings.js";
|
|
26
26
|
export { localize } from "./locales.js";
|
|
27
|
-
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues } from "./messages.js";
|
|
27
|
+
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues, pluralMessageKeys } from "./messages.js";
|
|
28
28
|
export { THEME_MANIFEST_CONTRACT_VERSION, THEME_MANIFEST_PATH, manifestCatalog, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
29
29
|
export { diffThemeManifests } from "./manifest-diff.js";
|
|
30
30
|
export { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, previewEndMarker, previewStartMarker, } from "./preview-protocol.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@wocart/theme/lifecycle` — binding a theme's browser behaviour to its
|
|
3
|
+
* markup, so it survives the page being swapped.
|
|
4
|
+
*
|
|
5
|
+
* Markup arrives three ways: the page loads; Astro's router swaps the page
|
|
6
|
+
* (`astro:page-load`); an editor's preview swaps one section in
|
|
7
|
+
* (`PREVIEW_SECTION_LOAD_EVENT`). A script that binds once on import misses
|
|
8
|
+
* the last two, and one that binds on every event binds twice. `bindEach`
|
|
9
|
+
* binds each matching element exactly once, whichever way it arrived, and
|
|
10
|
+
* `onEachPage` runs a page's behaviour once per page.
|
|
11
|
+
*
|
|
12
|
+
* This module is tiny and loads nothing else: a shopper's page imports it, and
|
|
13
|
+
* the editor's bridge (`@wocart/theme/preview`) stays on the preview route.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Dispatched on `document` for each section the editor's preview swapped in,
|
|
17
|
+
* with the instance id as `detail.id`. When the whole page was replaced, the
|
|
18
|
+
* preview dispatches Astro's own `astro:page-load` instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PREVIEW_SECTION_LOAD_EVENT = "wocart:section-load";
|
|
21
|
+
/** What a bind may return: a function that undoes it — listeners on `document` or `window`, timers. */
|
|
22
|
+
export type Teardown = void | (() => void);
|
|
23
|
+
export interface LifecycleOptions {
|
|
24
|
+
/** The document to bind in; the page's own by default. */
|
|
25
|
+
document?: Document;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Runs `boot` for the page the shopper is on, and again for each page Astro's
|
|
29
|
+
* router swaps in. Its teardown runs just before the page is swapped out, so
|
|
30
|
+
* listeners on `document` or `window` never pile up from one page to the next.
|
|
31
|
+
* Returns a function that stops it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function onEachPage(boot: () => Teardown, options?: LifecycleOptions): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Runs `bind` once on every element `selector` matches: when the page is
|
|
36
|
+
* ready, after Astro's router swaps a page in, and after an editor's preview
|
|
37
|
+
* swaps a section in. A bind may return a teardown, run when its element
|
|
38
|
+
* leaves — before Astro swaps the page out, or when a later pass finds the
|
|
39
|
+
* element gone. Returns a function that stops it and runs every teardown.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* bindEach<HTMLFormElement>("[data-order-form]", (form) => {
|
|
43
|
+
* const onResize = () => layout(form);
|
|
44
|
+
* window.addEventListener("resize", onResize);
|
|
45
|
+
* return () => window.removeEventListener("resize", onResize);
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function bindEach<T extends Element = HTMLElement>(selector: string, bind: (element: T) => Teardown, options?: LifecycleOptions): () => void;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* `@wocart/theme/lifecycle` — binding a theme's browser behaviour to its
|
|
4
|
+
* markup, so it survives the page being swapped.
|
|
5
|
+
*
|
|
6
|
+
* Markup arrives three ways: the page loads; Astro's router swaps the page
|
|
7
|
+
* (`astro:page-load`); an editor's preview swaps one section in
|
|
8
|
+
* (`PREVIEW_SECTION_LOAD_EVENT`). A script that binds once on import misses
|
|
9
|
+
* the last two, and one that binds on every event binds twice. `bindEach`
|
|
10
|
+
* binds each matching element exactly once, whichever way it arrived, and
|
|
11
|
+
* `onEachPage` runs a page's behaviour once per page.
|
|
12
|
+
*
|
|
13
|
+
* This module is tiny and loads nothing else: a shopper's page imports it, and
|
|
14
|
+
* the editor's bridge (`@wocart/theme/preview`) stays on the preview route.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Dispatched on `document` for each section the editor's preview swapped in,
|
|
18
|
+
* with the instance id as `detail.id`. When the whole page was replaced, the
|
|
19
|
+
* preview dispatches Astro's own `astro:page-load` instead.
|
|
20
|
+
*/
|
|
21
|
+
export const PREVIEW_SECTION_LOAD_EVENT = "wocart:section-load";
|
|
22
|
+
const PAGE_LOAD = "astro:page-load";
|
|
23
|
+
const BEFORE_SWAP = "astro:before-swap";
|
|
24
|
+
function whenReady(doc, run) {
|
|
25
|
+
if (doc.readyState === "loading")
|
|
26
|
+
doc.addEventListener("DOMContentLoaded", run, { once: true });
|
|
27
|
+
else
|
|
28
|
+
run();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Runs `boot` for the page the shopper is on, and again for each page Astro's
|
|
32
|
+
* router swaps in. Its teardown runs just before the page is swapped out, so
|
|
33
|
+
* listeners on `document` or `window` never pile up from one page to the next.
|
|
34
|
+
* Returns a function that stops it.
|
|
35
|
+
*/
|
|
36
|
+
export function onEachPage(boot, options = {}) {
|
|
37
|
+
const doc = options.document ?? document;
|
|
38
|
+
let mounted = null;
|
|
39
|
+
let teardown;
|
|
40
|
+
const unmount = () => {
|
|
41
|
+
const previous = teardown;
|
|
42
|
+
teardown = undefined;
|
|
43
|
+
if (typeof previous === "function")
|
|
44
|
+
previous();
|
|
45
|
+
};
|
|
46
|
+
const mount = () => {
|
|
47
|
+
mounted = doc.body;
|
|
48
|
+
teardown = boot();
|
|
49
|
+
};
|
|
50
|
+
const pageLoad = () => {
|
|
51
|
+
if (doc.body === mounted)
|
|
52
|
+
return;
|
|
53
|
+
unmount();
|
|
54
|
+
mount();
|
|
55
|
+
};
|
|
56
|
+
doc.addEventListener(BEFORE_SWAP, unmount);
|
|
57
|
+
doc.addEventListener(PAGE_LOAD, pageLoad);
|
|
58
|
+
whenReady(doc, () => {
|
|
59
|
+
if (mounted === null)
|
|
60
|
+
mount();
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
doc.removeEventListener(BEFORE_SWAP, unmount);
|
|
64
|
+
doc.removeEventListener(PAGE_LOAD, pageLoad);
|
|
65
|
+
unmount();
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Runs `bind` once on every element `selector` matches: when the page is
|
|
70
|
+
* ready, after Astro's router swaps a page in, and after an editor's preview
|
|
71
|
+
* swaps a section in. A bind may return a teardown, run when its element
|
|
72
|
+
* leaves — before Astro swaps the page out, or when a later pass finds the
|
|
73
|
+
* element gone. Returns a function that stops it and runs every teardown.
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* bindEach<HTMLFormElement>("[data-order-form]", (form) => {
|
|
77
|
+
* const onResize = () => layout(form);
|
|
78
|
+
* window.addEventListener("resize", onResize);
|
|
79
|
+
* return () => window.removeEventListener("resize", onResize);
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function bindEach(selector, bind, options = {}) {
|
|
84
|
+
const doc = options.document ?? document;
|
|
85
|
+
const bound = new WeakSet();
|
|
86
|
+
const teardowns = new Map();
|
|
87
|
+
const release = (element) => {
|
|
88
|
+
const teardown = teardowns.get(element);
|
|
89
|
+
teardowns.delete(element);
|
|
90
|
+
bound.delete(element);
|
|
91
|
+
teardown?.();
|
|
92
|
+
};
|
|
93
|
+
const run = () => {
|
|
94
|
+
for (const element of [...teardowns.keys()])
|
|
95
|
+
if (!element.isConnected)
|
|
96
|
+
release(element);
|
|
97
|
+
doc.querySelectorAll(selector).forEach((element) => {
|
|
98
|
+
if (bound.has(element))
|
|
99
|
+
return;
|
|
100
|
+
bound.add(element);
|
|
101
|
+
const teardown = bind(element);
|
|
102
|
+
if (typeof teardown === "function")
|
|
103
|
+
teardowns.set(element, teardown);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const leave = () => {
|
|
107
|
+
for (const element of [...teardowns.keys()])
|
|
108
|
+
release(element);
|
|
109
|
+
};
|
|
110
|
+
doc.addEventListener(PAGE_LOAD, run);
|
|
111
|
+
doc.addEventListener(PREVIEW_SECTION_LOAD_EVENT, run);
|
|
112
|
+
doc.addEventListener(BEFORE_SWAP, leave);
|
|
113
|
+
whenReady(doc, run);
|
|
114
|
+
return () => {
|
|
115
|
+
doc.removeEventListener(PAGE_LOAD, run);
|
|
116
|
+
doc.removeEventListener(PREVIEW_SECTION_LOAD_EVENT, run);
|
|
117
|
+
doc.removeEventListener(BEFORE_SWAP, leave);
|
|
118
|
+
leave();
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@wocart/theme/messages` — a content pack's messages, for browser code:
|
|
3
|
+
* placeholders and CLDR plurals, with nothing else from the theme contract.
|
|
4
|
+
* The same functions as the package's root entry.
|
|
5
|
+
*/
|
|
6
|
+
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues, pluralMessageKeys } from "./messages.js";
|
|
7
|
+
export type { MessageOptions, MessageValues, PluralCategory } from "./messages.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@wocart/theme/messages` — a content pack's messages, for browser code:
|
|
3
|
+
* placeholders and CLDR plurals, with nothing else from the theme contract.
|
|
4
|
+
* The same functions as the package's root entry.
|
|
5
|
+
*/
|
|
6
|
+
export { PLURAL_CATEGORIES, formatMessage, messageForms, pluralFormIssues, pluralMessageKeys } from "./messages.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
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 declare const THEME_KEY_PATTERN: RegExp;
|
|
7
|
+
/** Type keys are lowercase kebab-case. */
|
|
8
|
+
export declare const TYPE_KEY_PATTERN: RegExp;
|
|
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 declare const SURFACE_KEY_PATTERN: RegExp;
|
|
19
|
+
/** The platform page types a surface can render (`SurfacePageType`), mirrored from the storefront's page types. */
|
|
20
|
+
export declare const PAGE_TYPES: readonly ["home", "catalog", "category", "collection", "product", "page", "landing", "checkout", "thankYou"];
|
|
21
|
+
/** A `color` field's value: a hex color, `#rgb` or `#rrggbb`. */
|
|
22
|
+
export declare const COLOR_PATTERN: RegExp;
|
|
23
|
+
/**
|
|
24
|
+
* A `menu` field's value: the handle of one of the store's menus
|
|
25
|
+
* (`main-menu`, `footer`). The server applies the same pattern to menu
|
|
26
|
+
* handles; a menu that does not exist reads as no menu.
|
|
27
|
+
*/
|
|
28
|
+
export declare const MENU_HANDLE_PATTERN: RegExp;
|
|
29
|
+
export declare const COLLECTION_HANDLE_PATTERN: RegExp;
|
|
30
|
+
/**
|
|
31
|
+
* A `product` or `category` value: a handle or slug as the store's URLs use
|
|
32
|
+
* it. Merchants may type their own, so the rule only keeps out what cannot be
|
|
33
|
+
* one URL path segment: whitespace, control characters, `/`, `\`, `?`, `#`
|
|
34
|
+
* and `%`, and anything over 255 characters.
|
|
35
|
+
*/
|
|
36
|
+
export declare const RESOURCE_HANDLE_PATTERN: RegExp;
|
|
37
|
+
/**
|
|
38
|
+
* A media object key in the store's bucket: a namespace and a name
|
|
39
|
+
* (`products/3f2a9c.webp`), segments of letters, digits, `.`, `_` and `-`
|
|
40
|
+
* that never start with a dot — so no `..`, no leading slash, no empty
|
|
41
|
+
* segment — and at most 512 characters.
|
|
42
|
+
*/
|
|
43
|
+
export declare const MEDIA_KEY_PATTERN: RegExp;
|
|
44
|
+
export declare function isLocaleTag(value: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether `value` is a per-language map (`{ ar: "…", fr: "…" }`): a plain
|
|
47
|
+
* object with at least one entry, every key a locale tag, every value a string.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isLocaleMap(value: unknown): value is Readonly<Record<string, string>>;
|
|
50
|
+
/**
|
|
51
|
+
* Whether `value` is a per-language map whose entries pass `isEntry`: a plain
|
|
52
|
+
* object with at least one entry, every key a locale tag.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isLocaleMapOf<T>(value: unknown, isEntry: (entry: unknown) => entry is T): value is Readonly<Record<string, T>>;
|
|
55
|
+
/**
|
|
56
|
+
* Whether a `url` field value is safe to render as an `href`: a site path,
|
|
57
|
+
* fragment or query, or an `http(s)`, `mailto` or `tel` URL. Rejects
|
|
58
|
+
* `javascript:`/`data:` and every other scheme, protocol-relative `//host`,
|
|
59
|
+
* backslashes (browsers read `/\host` as `//host`), whitespace and control
|
|
60
|
+
* characters, and anything over 2048 characters.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isSafeLinkTarget(value: string): boolean;
|
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": {
|