@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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,48 @@ 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
|
+
|
|
39
|
+
## 2.1.0 — 2026-10-21
|
|
40
|
+
|
|
41
|
+
The release check now sees where a section may be placed.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- `diffThemeManifests` treats a component that is no longer insertable, or that
|
|
46
|
+
is limited to a narrower list of surfaces, as a breaking change. Widening a
|
|
47
|
+
component's surfaces, or making it insertable again, is additive. Before, a
|
|
48
|
+
theme could narrow where a section sits without the release check noticing.
|
|
49
|
+
|
|
8
50
|
## 2.0.0 — 2026-10-20
|
|
9
51
|
|
|
10
52
|
Blocks: components place other components inside them, some fixed by the
|
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
|
+
}
|
package/dist/manifest-diff.js
CHANGED
|
@@ -82,6 +82,7 @@ export function diffThemeManifests(previous, next) {
|
|
|
82
82
|
}
|
|
83
83
|
function compareComponents(previous, next, path, breaking, additive) {
|
|
84
84
|
compareFields(previous.fields, next.fields, `${path}.fields`, breaking, additive);
|
|
85
|
+
compareCapabilities(previous.capabilities, next.capabilities, `${path}.capabilities`, breaking, additive);
|
|
85
86
|
const nextSlots = new Map(next.slots.map((slot) => [slot.name, slot]));
|
|
86
87
|
for (const slot of previous.slots) {
|
|
87
88
|
const after = nextSlots.get(slot.name);
|
|
@@ -108,6 +109,37 @@ function compareComponents(previous, next, path, breaking, additive) {
|
|
|
108
109
|
additive(`${path}.fixed.${block.name}`, `fixed block "${block.name}" was added`);
|
|
109
110
|
}
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Where an editor may place a component. A narrowed rule is breaking: a section
|
|
114
|
+
* saved on a surface it no longer covers may stop rendering there, once a theme
|
|
115
|
+
* renders each surface from the components it covers, and it cannot be placed
|
|
116
|
+
* again. Widening only adds.
|
|
117
|
+
*/
|
|
118
|
+
function compareCapabilities(previous, next, path, breaking, additive) {
|
|
119
|
+
const wasInsertable = previous?.insertable !== false;
|
|
120
|
+
const isInsertable = next?.insertable !== false;
|
|
121
|
+
if (wasInsertable && !isInsertable)
|
|
122
|
+
breaking(`${path}.insertable`, "no longer insertable: an editor cannot place it again");
|
|
123
|
+
else if (!wasInsertable && isInsertable)
|
|
124
|
+
additive(`${path}.insertable`, "insertable again");
|
|
125
|
+
const before = previous?.surfaces;
|
|
126
|
+
const after = next?.surfaces;
|
|
127
|
+
if (after === undefined) {
|
|
128
|
+
if (before !== undefined)
|
|
129
|
+
additive(`${path}.surfaces`, "now on any surface");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (before === undefined) {
|
|
133
|
+
breaking(`${path}.surfaces`, `now only on ${after.join(", ")}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const removed = before.filter((surface) => !after.includes(surface));
|
|
137
|
+
if (removed.length > 0)
|
|
138
|
+
breaking(`${path}.surfaces`, `no longer on ${removed.join(", ")}`);
|
|
139
|
+
const added = after.filter((surface) => !before.includes(surface));
|
|
140
|
+
if (added.length > 0)
|
|
141
|
+
additive(`${path}.surfaces`, `now also on ${added.join(", ")}`);
|
|
142
|
+
}
|
|
111
143
|
/** A list's cap and required types: a lower cap hides saved instances; a new requirement only adds. */
|
|
112
144
|
function compareCaps(previous, next, path, what, breaking, additive) {
|
|
113
145
|
if (next.max !== undefined && (previous.max === undefined || next.max < previous.max)) {
|
|
@@ -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;
|