@wocart/theme 2.2.0 → 3.0.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 +24 -0
- package/README.md +24 -1077
- package/dist/image-service.d.ts +16 -19
- package/dist/image-service.js +20 -23
- package/dist/manifest.d.ts +2 -2
- package/dist/manifest.js +2 -2
- package/dist/patterns.d.ts +1 -1
- package/dist/patterns.js +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,26 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
The theme contract for [Wocart](https://github.com/bighadj22/codflow)
|
|
4
4
|
storefronts. A theme declares its components in code — real `.astro`
|
|
5
|
-
components
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Astro renderer.
|
|
5
|
+
components with typed, editable fields — and a store's pages are saved as JSON
|
|
6
|
+
documents that reference them. This package validates both, resolves a saved
|
|
7
|
+
document against the theme, and renders it with a reference Astro renderer.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
break the rest of the page.
|
|
15
|
-
- **A serializable manifest.** `themeManifest` turns a theme into plain JSON
|
|
16
|
-
that editors and tools can read without running theme code.
|
|
17
|
-
- **Editor-ready.** Images, rich text, lists and product references as
|
|
18
|
-
fields; page types and reusable templates; presets; hidden sections; and the
|
|
19
|
-
document operations every editor applies — all discoverable from the
|
|
20
|
-
manifest, which a build emits as a file.
|
|
21
|
-
- **Many languages.** Text values may be translated per language, and
|
|
22
|
-
`formatMessage` handles placeholders and CLDR plurals.
|
|
23
|
-
- **A reference renderer.** `InstanceView.astro` renders a document section by
|
|
24
|
-
section through your own components.
|
|
9
|
+
**Documentation: [docs.wocart.store/sdk/theme](https://docs.wocart.store/sdk/theme)**
|
|
10
|
+
— also in [French](https://docs.wocart.store/fr/sdk/theme) and
|
|
11
|
+
[Arabic](https://docs.wocart.store/ar/sdk/theme). Start with the
|
|
12
|
+
[tutorial](https://docs.wocart.store/sdk/theme/tutorial).
|
|
25
13
|
|
|
26
14
|
## Install
|
|
27
15
|
|
|
@@ -29,1078 +17,37 @@ Astro renderer.
|
|
|
29
17
|
npm install @wocart/theme @wocart/storefront
|
|
30
18
|
```
|
|
31
19
|
|
|
32
|
-
`@wocart/storefront` is a required peer
|
|
33
|
-
`
|
|
34
|
-
optional peer, needed only for the `./astro` renderer. ESM only; tooling needs
|
|
35
|
-
Node 22.12 or newer.
|
|
20
|
+
`@wocart/storefront` 2 is a required peer; `astro` 7 is an optional peer, for
|
|
21
|
+
the `./astro` renderer. ESM only; Node 22.12 or newer for tooling.
|
|
36
22
|
|
|
37
|
-
##
|
|
38
|
-
|
|
39
|
-
| Import | What it holds |
|
|
40
|
-
| --- | --- |
|
|
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`, `previewTarget`, the preview markers and the `BlocksHandle` and `RendererIssue` types |
|
|
43
|
-
| `@wocart/theme/astro/InstanceView.astro` | the reference renderer component |
|
|
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
|
-
| `@wocart/theme/astro/ThemeStyles.astro` | the stylesheet link and the store's color schemes, for a layout's `<head>` (see [Style](#style)) |
|
|
46
|
-
| `@wocart/theme/image-service` | `mediaImageService` — an Astro image service that resizes media-domain images on the media domain (any storefront host) |
|
|
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.
|
|
55
|
-
|
|
56
|
-
## Use
|
|
23
|
+
## Quick start
|
|
57
24
|
|
|
58
25
|
```ts
|
|
59
|
-
import {
|
|
60
|
-
defineComponent,
|
|
61
|
-
defineTheme,
|
|
62
|
-
parsePageData,
|
|
63
|
-
validateThemeDefinition,
|
|
64
|
-
} from "@wocart/theme";
|
|
26
|
+
import { defineComponent, defineTheme } from "@wocart/theme";
|
|
65
27
|
import Hero from "./components/Hero.astro";
|
|
66
28
|
|
|
67
|
-
const hero = defineComponent({
|
|
29
|
+
export const hero = defineComponent({
|
|
68
30
|
type: "hero",
|
|
69
|
-
component: Hero,
|
|
31
|
+
component: Hero,
|
|
70
32
|
label: "Hero",
|
|
71
|
-
fields: [{ key: "
|
|
72
|
-
capabilities: { insertable: true },
|
|
33
|
+
fields: [{ key: "title", kind: "text", label: "Title", default: "Fast delivery" }],
|
|
73
34
|
});
|
|
74
35
|
|
|
75
36
|
export const theme = defineTheme({ key: "my-theme", version: "1.0.0", components: { hero } });
|
|
76
|
-
|
|
77
|
-
const definitionIssues = validateThemeDefinition(theme); // [] when the catalog is coherent
|
|
78
|
-
const { page, issues } = parsePageData(savedDocument); // parse-and-degrade
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Then render the page in an `.astro` file:
|
|
82
|
-
|
|
83
|
-
```astro
|
|
84
|
-
---
|
|
85
|
-
import InstanceView from "@wocart/theme/astro/InstanceView.astro";
|
|
86
|
-
import { createReadCache } from "@wocart/theme/astro";
|
|
87
|
-
|
|
88
|
-
const reads = createReadCache(); // one per render, shared by every section
|
|
89
|
-
---
|
|
90
|
-
{page?.sections.map((section) => (
|
|
91
|
-
<InstanceView instance={section} theme={theme} context={context}
|
|
92
|
-
storefront={store} reads={reads} />
|
|
93
|
-
))}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`context` is the request's `ThemeRenderContext` (the store config, your
|
|
97
|
-
content pack for the language, `locale`, `isRTL`, `url`), and `store` is the
|
|
98
|
-
server-side `StoreClient` from `@wocart/storefront/server`.
|
|
99
|
-
|
|
100
|
-
Renderer behavior: `resolveInstance` fills defaults and gates composition;
|
|
101
|
-
`load` (optional, server-only) may enrich props and degrades to base props on
|
|
102
|
-
failure; unknown types render nothing — every degradation, including invalid
|
|
103
|
-
props and undeclared children/slots, is reported through `onIssue` (default:
|
|
104
|
-
`console.warn`). The issue contains a stable `code`, `message`, `instanceId`,
|
|
105
|
-
and, for page-data resolution issues, `path`.
|
|
106
|
-
|
|
107
|
-
`load` receives the instance's resolved props typed from the component's own
|
|
108
|
-
fields, so a section reads its settings without a cast:
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
defineComponent({
|
|
112
|
-
type: "best-sellers",
|
|
113
|
-
component: BestSellers,
|
|
114
|
-
label: { ar: "الأكثر مبيعاً", fr: "Meilleures ventes" },
|
|
115
|
-
fields: [
|
|
116
|
-
{ key: "collection", kind: "collection", label: { ar: "المجموعة", fr: "Collection" }, default: "best-sellers" },
|
|
117
|
-
{ key: "limit", kind: "number", label: { ar: "العدد", fr: "Nombre" }, default: 8, min: 1, max: 24 },
|
|
118
|
-
],
|
|
119
|
-
load: async ({ storefront, props }) => ({
|
|
120
|
-
products: await storefront.products.list({ collection: props.collection, limit: props.limit }),
|
|
121
|
-
}),
|
|
122
|
-
});
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
## Field kinds
|
|
126
|
-
|
|
127
|
-
| Kind | Value | Rules |
|
|
128
|
-
|---|---|---|
|
|
129
|
-
| `text`, `textarea` | string | `default` or `contentKey` (from the theme's content pack), never both |
|
|
130
|
-
| `number` | finite number | optional `min`/`max`; a `default` must sit inside them; with `step`, values sit on the grid that starts at `min` (or 0) |
|
|
131
|
-
| `boolean` | boolean | |
|
|
132
|
-
| `select` | one of `options` | `default` must be one of the options; `optionLabels` gives each option the words an editor shows, per language |
|
|
133
|
-
| `color` | `#rgb` or `#rrggbb` | anything else (a color name, `url(…)`) is rejected at assembly and reported on read |
|
|
134
|
-
| `collection` | a collection handle (`best-sellers`) | lowercase letters, digits and hyphens; its products are listed with `@wocart/storefront`'s `products.list({ collection })`, in the merchant's order |
|
|
135
|
-
| `menu` | a menu handle (`main-menu`) | lowercase letters, digits and hyphens; the store's menu is read with `@wocart/storefront`'s `menus.get(handle)` |
|
|
136
|
-
| `url` | a link target | a site path (`/products`), `#fragment`, `?query`, `http(s)://`, `mailto:` or `tel:`; `javascript:`, `data:`, `//host` and backslash tricks are rejected at assembly and reported on read |
|
|
137
|
-
| `product` | a product handle | read with `@wocart/storefront`'s `products.get(handle)`; a renamed or deleted product reads as no product |
|
|
138
|
-
| `category` | a category slug | found in `@wocart/storefront`'s `categories.list()` |
|
|
139
|
-
| `image` | `{ key, alt?, width?, height?, focal? }` | `key` is the object's key in the store's media (`products/3f2a9c.webp`), never a URL; no default; `alt` per language |
|
|
140
|
-
| `richtext` | a rich-text document, or one per language | a closed JSON format; `formats` narrows what it may contain |
|
|
141
|
-
| `list` | `[{ id, props }]` | item `fields` of any kind but `list` and `colorSchemes`; `min`/`max`; `itemLabel` names the item field an editor shows; default items carry their own ids |
|
|
142
|
-
| `style` | `{ spacing?, layout?, size? }`, a value per breakpoint | `panels` names what it offers; steps and choices only, never CSS (see [Style](#style)) |
|
|
143
|
-
| `colorScheme` | a color scheme's id | one of the store's schemes; the theme needs a `colorSchemes` setting |
|
|
144
|
-
| `colorSchemes` | `[{ id, name, colors }]` | a theme setting, at most one; every role a hex color; at most 24 |
|
|
145
|
-
| `productList` | product handles, in order | distinct; `min`/`max` (at most 50); each read with `products.get(handle)`, a missing one reads as absent |
|
|
146
|
-
| `collectionList` | collection handles, in order | distinct; `min`/`max` (at most 50) |
|
|
147
|
-
|
|
148
|
-
**How an editor shows a field** never changes what is stored:
|
|
149
|
-
`display: "dropdown" | "radio" | "segmented"` on a `select` (alignment is a
|
|
150
|
-
segmented `select` of `start`, `center`, `end`), `display: "input" | "slider"`
|
|
151
|
-
on a `number` (a slider needs `min`, `max` and `step`), and `inline: true` on
|
|
152
|
-
a `richtext` — one line, a single paragraph with marks and links, rendered
|
|
153
|
-
without a block element by `richTextToHtml(doc, { inline: true })`.
|
|
154
|
-
|
|
155
|
-
**A field may show only when it applies.** `visibleIf` is a closed JSON
|
|
156
|
-
condition on the fields beside it (the same component's, list item's, or
|
|
157
|
-
theme settings'): `{ field, equals }`, `{ field, in: [...] }`, `{ all: [...] }`,
|
|
158
|
-
`{ any: [...] }`, `{ not: ... }`, nested at most `CONDITION_DEPTH` (4) deep.
|
|
159
|
-
`isFieldVisible(field, values)` evaluates it for an editor — an unset field
|
|
160
|
-
compares as `null` — and the renderer ignores it; a hidden field keeps its
|
|
161
|
-
value.
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
{ key: "source", kind: "select", label: "Products", options: ["latest", "picked"], default: "latest", display: "segmented" },
|
|
165
|
-
{ key: "picked", kind: "productList", label: "Picked products", max: 12, visibleIf: { field: "source", equals: "picked" } },
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Use `url` for every editor-settable `href`, never `text`: the reader drops an
|
|
169
|
-
unsafe saved link (`invalid-prop`) and falls back to the field's default, so a
|
|
170
|
-
document can never inject a script URL.
|
|
171
|
-
|
|
172
|
-
The SDK checks a value as a writer must before saving it:
|
|
173
|
-
`checkFieldValue(field, value)` returns `{ ok: true }` or the reason. It is
|
|
174
|
-
stricter than reading: an image reference or a list item with a key the
|
|
175
|
-
contract does not define is refused, where the reader ignores it.
|
|
176
|
-
|
|
177
|
-
### Images
|
|
178
|
-
|
|
179
|
-
An image value names the object in the store's own media bucket; the URL is
|
|
180
|
-
derived at render, so a domain or host change never rewrites a document.
|
|
181
|
-
Resolve it with `@wocart/storefront`'s `mediaUrl`:
|
|
182
|
-
|
|
183
|
-
```astro
|
|
184
|
-
---
|
|
185
|
-
import { mediaUrl } from "@wocart/storefront";
|
|
186
|
-
import { MEDIA_DOMAIN } from "astro:env/server";
|
|
187
|
-
const { image } = Astro.props as InferProps<typeof banner>; // ResolvedImage: alt in the rendered language
|
|
188
|
-
const src = image && mediaUrl(image.key, MEDIA_DOMAIN);
|
|
189
|
-
---
|
|
190
|
-
{src && <img src={src} alt={image.alt ?? ""} width={image.width} height={image.height} />}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
The SDK never uploads: the editor places files in the store's media and
|
|
194
|
-
stores the key. An image field has no default — a theme cannot know a store's
|
|
195
|
-
media — so the component renders its own fallback.
|
|
196
|
-
|
|
197
|
-
### Rich text
|
|
198
|
-
|
|
199
|
-
A rich-text value is `{ type: "doc", version: 1, content }`: paragraphs,
|
|
200
|
-
headings 2–4, bullet and ordered lists, line breaks, bold, italic, underline,
|
|
201
|
-
strike and links that follow the `url` rule. Nothing else is representable, so
|
|
202
|
-
a stored value cannot carry markup or scripts. Render it with the SDK:
|
|
203
|
-
|
|
204
|
-
```astro
|
|
205
|
-
---
|
|
206
|
-
import { richTextToHtml } from "@wocart/theme";
|
|
207
|
-
---
|
|
208
|
-
{body && <div set:html={richTextToHtml(body, { href: store.href })} />}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
`richTextToHtml` escapes every text and attribute and drops what is not in the
|
|
212
|
-
format even for a value that was never validated; `href` lets the theme
|
|
213
|
-
localize site links. `richTextToPlainText` gives the words for a meta
|
|
214
|
-
description or an `aria-label`. A field's `formats` (`["bold", "link"]`)
|
|
215
|
-
narrows what a merchant may use.
|
|
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
|
-
|
|
220
|
-
### Lists
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
{
|
|
224
|
-
key: "badges",
|
|
225
|
-
kind: "list",
|
|
226
|
-
label: "Badges",
|
|
227
|
-
max: 4,
|
|
228
|
-
itemLabel: "title",
|
|
229
|
-
fields: [
|
|
230
|
-
{ key: "title", kind: "text", label: "Title" },
|
|
231
|
-
{ key: "icon", kind: "select", label: "Icon", options: ["truck", "cash"], default: "truck" },
|
|
232
|
-
],
|
|
233
|
-
default: [{ id: "cod", props: { title: { ar: "الدفع عند الاستلام", fr: "Paiement à la livraison" }, icon: "cash" } }],
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
A component receives `[{ id, title, icon }]`. Each item's id is unique in its
|
|
238
|
-
list, so reordering never changes which item is which; the reader drops a
|
|
239
|
-
broken or repeated item and keeps the others, and keeps the first `max`. Item
|
|
240
|
-
fields cannot default from the content pack: every item would read the same
|
|
241
|
-
key.
|
|
242
|
-
|
|
243
|
-
## Languages
|
|
244
|
-
|
|
245
|
-
The contract is built for many languages; a store serves the ones it enables.
|
|
246
|
-
A single-language store is the same contract with one language.
|
|
247
|
-
|
|
248
|
-
- **The render context says which language to render**: `locale` (a BCP 47
|
|
249
|
-
tag: `ar`, `fr`, `ar-DZ`) and optionally `fallbackLocales`, tried in order.
|
|
250
|
-
`content` is the theme's pack for that language.
|
|
251
|
-
- **`text`, `textarea` and `url` values may be translated.** A saved value is
|
|
252
|
-
either one string, the same in every language (a brand name), or one value
|
|
253
|
-
per language: `{ "ar": "توصيل مجاني", "fr": "Livraison gratuite" }`.
|
|
254
|
-
Resolution takes the requested language, then the fallback chain, then
|
|
255
|
-
`content[contentKey]`, then `default`. A missing translation is not an error;
|
|
256
|
-
a malformed map (`FR`, a number, an empty map) is reported as `invalid-prop`.
|
|
257
|
-
Numbers, toggles and choices are shared by every language.
|
|
258
|
-
- **The writer checks every language.** `createInstance` refuses a map with an
|
|
259
|
-
unsafe link or a wrong type in any language, naming that language.
|
|
260
|
-
- **Editor-facing text can be translated**: a component's `label` and `group`,
|
|
261
|
-
a field's `label`, `description` and text `default`, a surface's `label`.
|
|
262
|
-
- **A theme declares the languages its packs cover**:
|
|
263
|
-
`defineTheme({ locales: ["ar", "fr", "en"], … })`. The validator then fails
|
|
264
|
-
any translated label or default that misses one of them, and the manifest
|
|
265
|
-
carries the list so an editor knows what the theme can serve.
|
|
266
|
-
- **`localize(value, locales)`** picks the text for the first preferred
|
|
267
|
-
language; use it wherever a theme or tool shows a translated label.
|
|
268
|
-
|
|
269
|
-
A page-document writer must save translatable copy as a per-language map.
|
|
270
|
-
Saving a plain string declares it the same in every language.
|
|
271
|
-
|
|
272
|
-
## Typed props
|
|
273
|
-
|
|
274
|
-
A component's props come from its definition, so derive them instead of
|
|
275
|
-
writing them twice. `InferProps` types each field by kind (a `select` narrows
|
|
276
|
-
to its options), makes a field with a `default` always present and every other
|
|
277
|
-
field optional, and merges in whatever `load` returns, exactly as the renderer
|
|
278
|
-
does:
|
|
279
|
-
|
|
280
|
-
```astro
|
|
281
|
-
---
|
|
282
|
-
import type { InferProps } from "@wocart/theme";
|
|
283
|
-
import type { productGrid } from "../catalog";
|
|
284
|
-
|
|
285
|
-
const { products = [], limit } = Astro.props as InferProps<typeof productGrid>;
|
|
286
|
-
---
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
Define components with `defineComponent` so keys, kinds and options keep their
|
|
290
|
-
literal types. Reusable fields are plain values: declare one once and list it
|
|
291
|
-
in several definitions.
|
|
292
|
-
|
|
293
|
-
## Composition rules (slots)
|
|
294
|
-
|
|
295
|
-
A component declares where children may go; the document says what is there.
|
|
296
|
-
|
|
297
|
-
- `slots: [{ name: "default" }]` accepts children. A component with no
|
|
298
|
-
`default` slot drops children with `undeclared-children`.
|
|
299
|
-
- `multiple: false` keeps the first child and reports the rest
|
|
300
|
-
(`slot-not-multiple`).
|
|
301
|
-
- `allow: ["product-grid", "note"]` limits a slot to those component types.
|
|
302
|
-
Assembly reports a type that is not in the catalog; the reader drops any other
|
|
303
|
-
child with `disallowed-child`. Omit `allow` to accept any type.
|
|
304
|
-
|
|
305
|
-
`allow` is applied before `multiple: false`, so a disallowed child never
|
|
306
|
-
displaces an allowed one. The rules travel in the manifest, so an editor can
|
|
307
|
-
offer only valid insertions.
|
|
308
|
-
|
|
309
|
-
## Blocks
|
|
310
|
-
|
|
311
|
-
A section is often made of parts a merchant arranges — a product section's
|
|
312
|
-
gallery, price, order form, a countdown, "Add to cart" — and some of them must
|
|
313
|
-
always be there. Blocks are components placed inside other components.
|
|
314
|
-
|
|
315
|
-
**Where a component may go.** `placement: "section"` (the top level of a page),
|
|
316
|
-
`"block"` (only inside a slot) or `"both"`; absent means anywhere. The document
|
|
317
|
-
operations enforce it; the renderer never does.
|
|
318
|
-
|
|
319
|
-
**A component places its own blocks.** The renderer gives every component a
|
|
320
|
-
`blocks` prop. Put each slot's blocks, and each fixed block, where the design
|
|
321
|
-
wants them — as many slots as the component declares:
|
|
322
|
-
|
|
323
|
-
```astro
|
|
324
|
-
---
|
|
325
|
-
import Blocks from "@wocart/theme/astro/Blocks.astro";
|
|
326
|
-
import Block from "@wocart/theme/astro/Block.astro";
|
|
327
|
-
import { hasBlocks, type BlocksHandle } from "@wocart/theme/astro";
|
|
328
|
-
|
|
329
|
-
const { blocks, title } = Astro.props as { blocks: BlocksHandle; title: string };
|
|
330
|
-
---
|
|
331
|
-
<section>
|
|
332
|
-
{hasBlocks(blocks, "media") && <div class="media"><Blocks of={blocks} slot="media" /></div>}
|
|
333
|
-
<h1>{title}</h1>
|
|
334
|
-
<Block of={blocks} name="buy-box" />
|
|
335
|
-
</section>
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
`<slot />` keeps working for children, as before; a component that pulls its
|
|
339
|
-
blocks never pays for content pushed into a slot it does not render. `blocks`
|
|
340
|
-
is a reserved prop: no field may use that key.
|
|
341
|
-
|
|
342
|
-
`isPreview(blocks)` says whether the component renders in an editor's
|
|
343
|
-
preview. A section with nothing to show yet — no products picked, no text
|
|
344
|
-
written — draws a placeholder there, so a merchant can see and select it; a
|
|
345
|
-
shopper's page never shows one.
|
|
346
|
-
|
|
347
|
-
**Fixed blocks** are placed by the component's code, like Shopify's static
|
|
348
|
-
blocks. Declare them so an editor knows them:
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
fixed: [{ name: "buy-box", type: "buy-box", label: "Buy box" }],
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
An editor changes a fixed block's fields and fills its slots, but cannot
|
|
355
|
-
remove, move, copy or hide it. It renders even when the document does not
|
|
356
|
-
store it, with its defaults and the id `<owner id>~<name>` (`fixedBlockId`).
|
|
357
|
-
|
|
358
|
-
**Required blocks** may move but never disappear. A slot — or a surface, for
|
|
359
|
-
its top-level sections — lists them:
|
|
360
|
-
|
|
361
|
-
```ts
|
|
362
|
-
slots: [{ name: "default", allow: ["order-form", "countdown", "add-to-cart"], max: 4,
|
|
363
|
-
required: [{ type: "order-form", max: 1 }] }],
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
The operations refuse to remove, hide or move out the last `min` (default 1)
|
|
367
|
-
of a required type, or to insert past its `max`. A document that lacks one —
|
|
368
|
-
an old document, a theme update — renders it anyway, added at the end of its
|
|
369
|
-
slot, and the reader reports `missing-required-block`.
|
|
370
|
-
|
|
371
|
-
**Every product page keeps an order form.** A component with
|
|
372
|
-
`role: "order-form"` places orders. Every surface whose `pageType` is
|
|
373
|
-
`product` must guarantee at least one through required sections and fixed or
|
|
374
|
-
required blocks; assembly refuses it otherwise (`order-form-not-guaranteed`).
|
|
375
|
-
Give the order form's entry `max: 1` for exactly one. Its fields are not the
|
|
376
|
-
theme's: a store's checkout form is set in the merchant dashboard and enforced
|
|
377
|
-
by the server.
|
|
378
|
-
|
|
379
|
-
**Limits** (`THEME_LIMITS`): 50 instances per slot, 50 sections per surface,
|
|
380
|
-
1,000 instances per document, 6 levels of nesting below a section. A slot or
|
|
381
|
-
surface `max` may set fewer. The reader renders up to a limit and reports
|
|
382
|
-
`limit-exceeded`; the operations refuse to go past one. Assembly refuses fixed
|
|
383
|
-
and required blocks that contain themselves or nest deeper than the limit.
|
|
384
|
-
|
|
385
|
-
## Style
|
|
386
|
-
|
|
387
|
-
A merchant changes how a section sits on the page and which colors it wears
|
|
388
|
-
— never by writing CSS. A theme gives sizes to the platform's style steps,
|
|
389
|
-
offers style fields, and the SDK writes the CSS.
|
|
390
|
-
|
|
391
|
-
**Sizes.** The step names are the platform's (`s0` to `s12`); a theme maps the
|
|
392
|
-
ones it offers to pixels, and the widths a section may take:
|
|
393
|
-
|
|
394
|
-
```ts
|
|
395
|
-
style: { spacing: { s0: 0, s2: 8, s4: 16, s6: 32, s8: 64 }, widths: { wide: 1280, content: 960, narrow: 640 } },
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
**Style fields.** A `style` field names its panels — `spacing` (padding and
|
|
399
|
-
margin on the block axis, inline padding, gap), `layout` (direction, align,
|
|
400
|
-
justify, wrap) and `size` (width, minimum height). A value holds a choice per
|
|
401
|
-
breakpoint — `base` everywhere, `md` from 768 px, `lg` from 1024 px — phone
|
|
402
|
-
first, on CSS logical properties so right-to-left stores mirror on their own:
|
|
403
|
-
|
|
404
|
-
```ts
|
|
405
|
-
{ key: "style", kind: "style", label: "Style", panels: ["spacing", "size"],
|
|
406
|
-
default: { spacing: { paddingBlockStart: { base: "s4", lg: "s8" } } } }
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
A writer refuses a step or width the theme gives no size; a reader renders one
|
|
410
|
-
as no style.
|
|
411
|
-
|
|
412
|
-
**Colors.** The store's color schemes are one theme setting, `colorSchemes`:
|
|
413
|
-
named schemes, each a hex color for every role (`background`, `text`,
|
|
414
|
-
`heading`, `accent`, `button`, `buttonText`, `border`). A `colorScheme` field
|
|
415
|
-
picks one by id. A page document never holds a color.
|
|
416
|
-
|
|
417
|
-
**In the page.** Give the element its classes, serve the stylesheet, and put
|
|
418
|
-
the schemes in the `<head>`:
|
|
419
|
-
|
|
420
|
-
```astro
|
|
421
|
-
---
|
|
422
|
-
import { schemeClass, styleClasses } from "@wocart/theme";
|
|
423
|
-
const { style, scheme } = Astro.props;
|
|
424
|
-
---
|
|
425
|
-
<section class={`${schemeClass(scheme)} ${styleClasses(style)}`}>…</section>
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
```ts
|
|
429
|
-
// src/pages/wocart-styles.css.ts
|
|
430
|
-
import { themeStylesEndpoint } from "@wocart/theme/astro";
|
|
431
|
-
export const prerender = true;
|
|
432
|
-
export const GET = themeStylesEndpoint(theme);
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
```astro
|
|
436
|
-
<!-- the layout's <head> -->
|
|
437
|
-
<ThemeStyles schemes={settings.colorSchemes} /> <!-- @wocart/theme/astro/ThemeStyles.astro -->
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
The schemes become custom properties on `.wc-scheme-<id>`
|
|
441
|
-
(`--wc-color-background`, `--wc-color-button-text`, …) with the background
|
|
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
|
|
457
|
-
its inline sheet's hash to the policy, so `style-src` needs no
|
|
458
|
-
`'unsafe-inline'`. `contrastRatio(a, b)` gives the WCAG ratio an editor warns
|
|
459
|
-
on (below 4.5 for text).
|
|
460
|
-
|
|
461
|
-
## Pages and templates
|
|
462
|
-
|
|
463
|
-
A surface is a document a theme can render. Declare the platform page type it
|
|
464
|
-
renders and, optionally, its default document:
|
|
465
|
-
|
|
466
|
-
```ts
|
|
467
|
-
import homeDocument from "./documents/home.json";
|
|
468
|
-
|
|
469
|
-
surfaces: [
|
|
470
|
-
{ key: "home", label: "Home", pageType: "home", document: homeDocument },
|
|
471
|
-
{ key: "product", label: "Product", pageType: "product" },
|
|
472
|
-
{ key: "product.compact", label: "Compact product", pageType: "product" },
|
|
473
|
-
],
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
- A page type with a resource — `product`, `category`, `collection`, `page`,
|
|
477
|
-
`landing` — makes the surface a **template**: one document renders every
|
|
478
|
-
product (or category…), and its sections read the resource from
|
|
479
|
-
`context.params` (see the route example under Ownership rules). There is no
|
|
480
|
-
document per product.
|
|
481
|
-
- `product.compact` is an **alternate** of `product`: the base must be
|
|
482
|
-
declared and render the same page type.
|
|
483
|
-
- A surface's `document` is checked at assembly against the surface and the
|
|
484
|
-
catalog, and travels in the manifest as the theme's default.
|
|
485
|
-
- `capabilities: { surfaces: ["product"] }` keeps a component to the surfaces
|
|
486
|
-
it can serve (alternates included); `isInsertableOn(component, surface)`
|
|
487
|
-
applies the rule.
|
|
488
|
-
|
|
489
|
-
**Areas** are the parts every page shares — a header, a footer — composed of
|
|
490
|
-
sections like a page and edited once for the whole store:
|
|
491
|
-
|
|
492
|
-
```ts
|
|
493
|
-
areas: [
|
|
494
|
-
{ key: "header", label: "Header", document: headerDocument, required: [{ type: "menu-bar", max: 1 }], max: 4 },
|
|
495
|
-
{ key: "footer", label: "Footer" },
|
|
496
|
-
],
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
An area's document is the surface `area:<key>` (`areaSurface("header")`):
|
|
500
|
-
read, completed, checked and edited like a page's, with `required` and `max`
|
|
501
|
-
as a surface has them. A component names `area:header` in
|
|
502
|
-
`capabilities.surfaces` to stay in the header. The layout renders it like a
|
|
503
|
-
page: `resolveSurfaceDocument(theme, "area:header", await store.pageData.get("area:header"))`.
|
|
504
|
-
|
|
505
|
-
A template's preview names the resource it shows: the preview draft's `path`
|
|
506
|
-
(`/products/robe-kabyle`), which `@wocart/storefront`'s `matchStorefrontPath`
|
|
507
|
-
turns into the page type and the route's params.
|
|
508
|
-
|
|
509
|
-
## Presets
|
|
510
|
-
|
|
511
|
-
A preset is a ready-made configuration an editor offers when inserting a
|
|
512
|
-
component — props over the field defaults, and child components for its slots:
|
|
513
|
-
|
|
514
|
-
```ts
|
|
515
|
-
presets: [
|
|
516
|
-
{
|
|
517
|
-
key: "with-grid",
|
|
518
|
-
label: "Hero with products",
|
|
519
|
-
props: { layout: "columns" },
|
|
520
|
-
children: [{ type: "hero", props: { align: "center" } }],
|
|
521
|
-
slots: { aside: [{ type: "product-grid", props: { limit: 2 } }] },
|
|
522
|
-
},
|
|
523
|
-
],
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
`instantiatePreset(theme, type, key, newId)` builds the instances, each with an
|
|
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
|
-
```
|
|
534
|
-
|
|
535
|
-
## Hidden sections
|
|
536
|
-
|
|
537
|
-
`{ "id": "…", "type": "hero", "hidden": true }` keeps a section in the
|
|
538
|
-
document without rendering it — a merchant switched it off. The renderer skips
|
|
539
|
-
it, its `load` and its children; the reader still checks it.
|
|
540
|
-
|
|
541
|
-
## Editing documents
|
|
542
|
-
|
|
543
|
-
Every writer — an editor, a CLI, an assistant — applies the same rules
|
|
544
|
-
through pure operations that return a new document or say why they refused:
|
|
545
|
-
|
|
546
|
-
```ts
|
|
547
|
-
import { createInstance, insertInstance, moveInstance, newInstanceId, checkPageData } from "@wocart/theme";
|
|
548
|
-
|
|
549
|
-
const edit = insertInstance(theme, page, createInstance(theme.components.hero, newInstanceId()), { index: 0 });
|
|
550
|
-
if (!edit.ok) console.log(edit.issue.code, edit.issue.message); // "not-available", "slot-full", …
|
|
551
37
|
```
|
|
552
38
|
|
|
553
|
-
|
|
554
|
-
`setInstanceProps` and `setInstanceHidden` never change their input. They
|
|
555
|
-
refuse a component that is `insertable: false`, not available on the
|
|
556
|
-
document's surface or not placeable there, a slot that does not take it or is
|
|
557
|
-
full, an index out of range, a cycle, an id already in the document, a value
|
|
558
|
-
its field refuses, any change to a fixed block but its fields
|
|
559
|
-
(`fixed-block`), losing a required block (`required-block`) and going past a
|
|
560
|
-
limit (`limit-exceeded`). `removeInstance(theme, page, id)` and
|
|
561
|
-
`setInstanceHidden(theme, page, id, hidden)` take the theme first; their older
|
|
562
|
-
forms without it still refuse a fixed block but cannot know a slot's required
|
|
563
|
-
types. `checkPageData(theme, page)` is the strict check of a whole document
|
|
564
|
-
before saving it; `findInstance` locates an instance, a fixed block included.
|
|
565
|
-
|
|
566
|
-
Open a document with `completePage(theme, page)`: it adds what the document
|
|
567
|
-
must hold — required sections, fixed blocks, required blocks, each with an id
|
|
568
|
-
unique in the document — so what an editor shows is what renders. Inserting a
|
|
569
|
-
component completes it the same way.
|
|
570
|
-
|
|
571
|
-
An editor holds a theme's manifest, not its code:
|
|
572
|
-
`manifestCatalog(manifest)` makes the catalog these functions take, with no
|
|
573
|
-
component implementations.
|
|
574
|
-
|
|
575
|
-
## Preview
|
|
576
|
-
|
|
577
|
-
An editor shows a merchant's unsaved draft on the live storefront, rendered by
|
|
578
|
-
the theme itself — the same components, data and CSS a shopper gets. A theme
|
|
579
|
-
that does this sets `preview: true` (the manifest says so) and serves a route
|
|
580
|
-
at `THEME_PREVIEW_PATH` (`/wocart-preview`). The editor frames it with
|
|
581
|
-
`GET /wocart-preview?token=…&surface=home&locale=fr`, and the framed page
|
|
582
|
-
re-renders each draft the editor sends by posting it back to the route as
|
|
583
|
-
JSON — never a cross-site form, so Astro's `checkOrigin` stays on. Three parts
|
|
584
|
-
meet there:
|
|
585
|
-
|
|
586
|
-
1. **The request.** `readPreviewRequest` from `@wocart/storefront/server`
|
|
587
|
-
checks the editor's signed token and returns the draft — which surface and
|
|
588
|
-
language to show and, once the editor sends one, the document, settings
|
|
589
|
-
and text edits (its guide: "Previewing a draft for an editor"). What a
|
|
590
|
-
draft leaves out renders as saved. The route answers with
|
|
591
|
-
`previewResponseHeaders`, so only that editor can frame it and nothing
|
|
592
|
-
stores it; no route rule may cache it.
|
|
593
|
-
2. **The markers.** `<InstanceView preview …>` brackets each instance's nodes
|
|
594
|
-
with two comments — `wocart:start id=… type=…[ hidden]` and
|
|
595
|
-
`wocart:end id=…` (`previewStartMarker`, `previewEndMarker`,
|
|
596
|
-
`parsePreviewMarker`). Comments, not wrapper elements, so a grid or a
|
|
597
|
-
`> *` selector lays out exactly as it does for a shopper; a hidden instance
|
|
598
|
-
keeps its markers with nothing between them. A shopper's page has none.
|
|
599
|
-
3. **The bridge.** `startPreviewBridge({ editorOrigin, token })` from
|
|
600
|
-
`@wocart/theme/preview` runs on the preview page. It reports every marked
|
|
601
|
-
instance to the editor, turns clicks into selections, and re-renders the
|
|
602
|
-
drafts the editor sends — swapping in only the sections whose markup
|
|
603
|
-
changed, or the whole page when sections were added, moved or hidden, or
|
|
604
|
-
the page around them changed.
|
|
605
|
-
|
|
606
|
-
```astro
|
|
607
|
-
---
|
|
608
|
-
// src/pages/wocart-preview.astro — the middleware read the request
|
|
609
|
-
export const prerender = false;
|
|
610
|
-
const { claims, token } = Astro.locals.preview;
|
|
611
|
-
for (const [name, value] of Object.entries(previewResponseHeaders(claims))) Astro.response.headers.set(name, value);
|
|
612
|
-
---
|
|
613
|
-
<Layout>
|
|
614
|
-
{sections.map((instance) => (
|
|
615
|
-
<InstanceView preview {instance} {theme} {context} {storefront} {reads} />
|
|
616
|
-
))}
|
|
617
|
-
<wocart-preview-bridge data-editor={claims.editor} data-token={token} hidden />
|
|
618
|
-
</Layout>
|
|
619
|
-
<script>
|
|
620
|
-
import { startPreviewBridge } from "@wocart/theme/preview";
|
|
621
|
-
const host = document.querySelector<HTMLElement>("wocart-preview-bridge")!;
|
|
622
|
-
startPreviewBridge({ editorOrigin: host.dataset.editor!, token: host.dataset.token! });
|
|
623
|
-
</script>
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
The bridge draws nothing — outlines, toolbars and panels are the editor's. It
|
|
627
|
-
posts only to `editorOrigin` and reads only messages from it, sent by the
|
|
628
|
-
framing window, with `source: "wocart-editor"` and the protocol's `version`
|
|
629
|
-
(`PREVIEW_PROTOCOL_VERSION`):
|
|
39
|
+
## Entry points
|
|
630
40
|
|
|
631
|
-
|
|
|
41
|
+
| Import | Holds |
|
|
632
42
|
| --- | --- |
|
|
633
|
-
|
|
|
634
|
-
| `
|
|
635
|
-
| `
|
|
636
|
-
| `
|
|
637
|
-
| `
|
|
638
|
-
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
A `patch` shows a change the editor has already made to its draft before the
|
|
642
|
-
render that follows it arrives — so moving, copying, removing or hiding a
|
|
643
|
-
section, and typing a field's text, show on the page at once. Each patch
|
|
644
|
-
(`PreviewPatch`) is a hint: the page applies what it finds — `move` a section
|
|
645
|
-
to an index, `duplicate` an instance under new ids, `remove` one and
|
|
646
|
-
`restore` it, `hide` one or show it again, replace `text` that reads `from`
|
|
647
|
-
inside an instance with `to` — and leaves the rest to the render, which is
|
|
648
|
-
the page's truth. An editor therefore always sends the render too; a page
|
|
649
|
-
from before 1.6.0 ignores patches and still shows every change, a moment
|
|
650
|
-
later.
|
|
651
|
-
|
|
652
|
-
A swapped section's scripts do not run again. A section with browser behavior
|
|
653
|
-
binds it again on `PREVIEW_SECTION_LOAD_EVENT` (`wocart:section-load`,
|
|
654
|
-
dispatched on `document` with `detail.id`) — also for a section a patch
|
|
655
|
-
copied, restored or showed again. `bindEach` (see [Browser scripts](#browser-scripts))
|
|
656
|
-
does it for you. When the bridge replaces the whole
|
|
657
|
-
page instead, it does what Astro's router does after a navigation — a new
|
|
658
|
-
`<body>`, the root element's attributes, the title — and dispatches
|
|
659
|
-
`astro:page-load`, so a script written for the router mounts again as it does
|
|
660
|
-
there. Changed stylesheets are replaced in place; unchanged ones never reload.
|
|
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
|
-
|
|
711
|
-
## Theme manifest (serializable catalogue)
|
|
712
|
-
|
|
713
|
-
A `ThemeDefinition` mixes data (types, fields, defaults) with runtime (the live
|
|
714
|
-
component reference, the server-only `load` hook), so tooling cannot read it
|
|
715
|
-
without executing theme modules. `themeManifest(theme)` derives the data-only
|
|
716
|
-
half — a JSON-serializable `ThemeManifest` — for editors, CLIs, and marketplaces:
|
|
717
|
-
|
|
718
|
-
```ts
|
|
719
|
-
import { themeManifest, validateThemeManifest } from "@wocart/theme";
|
|
720
|
-
|
|
721
|
-
const manifest = themeManifest(theme);
|
|
722
|
-
// {
|
|
723
|
-
// contractVersion: 1,
|
|
724
|
-
// key: "my-theme", version: "1.0.0",
|
|
725
|
-
// sdkVersion: "1.5.0", // the @wocart/theme that generated it
|
|
726
|
-
// surfaces: [], // declared surfaces (possibly none)
|
|
727
|
-
// locales: [], settings: [], content: {},
|
|
728
|
-
// components: { hero: { type, label, group, fields, slots, capabilities, presets } },
|
|
729
|
-
// } — no `component`, no `load`, JSON-round-trip stable.
|
|
730
|
-
|
|
731
|
-
const { manifest: checked, issues } = validateThemeManifest(
|
|
732
|
-
JSON.parse(JSON.stringify(manifest)),
|
|
733
|
-
);
|
|
734
|
-
// `checked` is non-null only for a structurally sound manifest; a non-semver
|
|
735
|
-
// theme `version` is the one advisory case (manifest returned with an
|
|
736
|
-
// `invalid-version` issue). Declare extra surfaces on the theme when it can
|
|
737
|
-
// render them: `defineTheme({ key, version, surfaces: [{ key: "home",
|
|
738
|
-
// label: "Home" }], components })` — declaration only, never enforcement:
|
|
739
|
-
// page parsing and resolution never consult the list.
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
### Manifest guarantees
|
|
743
|
-
|
|
744
|
-
What a validated manifest promises a consumer:
|
|
745
|
-
|
|
746
|
-
- **Derived, not authored.** The manifest is generated from the theme's
|
|
747
|
-
`ThemeDefinition` by `themeManifest()` — there is exactly one source of truth.
|
|
748
|
-
A hand-written manifest is validated strictly, but never treated as the
|
|
749
|
-
catalogue; the theme's code is.
|
|
750
|
-
- **Data-only.** No component implementations, loaders, functions, symbols,
|
|
751
|
-
bigints, non-finite numbers, or circular references. `validateThemeManifest`
|
|
752
|
-
rejects all of these as `non-serializable`, including `component`/`load` keys
|
|
753
|
-
whose values would otherwise serialize.
|
|
754
|
-
- **Normalized shapes.** `fields` and `slots` are always arrays (absent means
|
|
755
|
-
`[]`); every record key equals its component's `type`. Unknown keys are
|
|
756
|
-
**stripped, not carried** — so an additive field introduced by a newer producer
|
|
757
|
-
under the same `contractVersion` is ignored by this reader rather than
|
|
758
|
-
failing it. That is the forward-compat tradeoff: old readers stay green, but
|
|
759
|
-
they cannot see new metadata until updated.
|
|
760
|
-
- **Validation mirrors assembly.** Relational rules (`contentKey`/`default`
|
|
761
|
-
exclusivity, number `default` within `min`/`max`, select `default` in
|
|
762
|
-
`options`, slot-name shape, key/type agreement) are checked identically by
|
|
763
|
-
`validateThemeDefinition` and `validateThemeManifest`, so a manifest derived
|
|
764
|
-
from a valid theme always validates clean.
|
|
765
|
-
|
|
766
|
-
What it deliberately excludes: implementations and loaders (runtime-only).
|
|
767
|
-
Content packs travel only when the theme declares them (`defineTheme({
|
|
768
|
-
content })`).
|
|
769
|
-
|
|
770
|
-
### The manifest as a build artifact
|
|
771
|
-
|
|
772
|
-
A theme's definition imports its `.astro` components, so it cannot be loaded
|
|
773
|
-
outside Astro's build. Emit the manifest as a prerendered page at
|
|
774
|
-
`THEME_MANIFEST_PATH` (`/wocart-theme.json`):
|
|
775
|
-
|
|
776
|
-
```ts
|
|
777
|
-
// src/pages/wocart-theme.json.ts
|
|
778
|
-
import { themeManifestEndpoint } from "@wocart/theme/astro";
|
|
779
|
-
import { theme } from "../theme/theme";
|
|
780
|
-
export const prerender = true;
|
|
781
|
-
export const GET = themeManifestEndpoint(theme);
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
The manifest is validated when the module loads: an invalid one fails the
|
|
785
|
-
build instead of shipping.
|
|
786
|
-
|
|
787
|
-
### What changes between versions
|
|
788
|
-
|
|
789
|
-
`diffThemeManifests(previous, next)` lists the changes saved documents,
|
|
790
|
-
settings and text edits may not survive (`breaking`) — a removed component,
|
|
791
|
-
field, slot, option, surface or content key; a changed kind or page type; a
|
|
792
|
-
narrowed range, list, `allow` list or rich-text formats; a slot made single; a
|
|
793
|
-
field newly required with no default — and those that only add. A breaking
|
|
794
|
-
change belongs in a new major version of the theme.
|
|
795
|
-
|
|
796
|
-
### Versioning policy
|
|
797
|
-
|
|
798
|
-
Three versioned contracts coexist — keep them separate:
|
|
799
|
-
|
|
800
|
-
- `contractVersion` identifies the **manifest schema and interpretation
|
|
801
|
-
contract**. It starts at `1` and increments only on a breaking change to the
|
|
802
|
-
manifest shape or how consumers must interpret it. Ordinary additive,
|
|
803
|
-
backward-compatible metadata does not bump it. An unsupported version fails
|
|
804
|
-
clearly (`unsupported-contract`), never silently.
|
|
805
|
-
- The theme's own `version` (e.g. `"1.0.0"`) versions the **theme**. The runtime
|
|
806
|
-
`ThemeDefinition` contract accepts any non-empty string (unchanged); the
|
|
807
|
-
manifest validator additionally *reports* (never rejects) a non-semver value
|
|
808
|
-
so version hygiene is visible without breaking registration.
|
|
809
|
-
- `PageData.version` (currently `1`) versions the **page-document envelope** —
|
|
810
|
-
untouched by the manifest contract.
|
|
811
|
-
|
|
812
|
-
**One named slot per instance.** The reference renderer fills at most one named
|
|
813
|
-
slot; a second named slot *with content* is reported as
|
|
814
|
-
`multiple-slots-unsupported` and is never silently dropped. This is an Astro
|
|
815
|
-
compiler boundary (a dynamic child expression can lift only one slot attribute
|
|
816
|
-
into the slots-object key), not a temporary limitation — see `SlotDefinition`
|
|
817
|
-
for the escape hatch.
|
|
818
|
-
|
|
819
|
-
## Ownership rules
|
|
820
|
-
|
|
821
|
-
The Theme SDK owns composition mechanics: code-owned catalogs, JSON page data,
|
|
822
|
-
field validation, defaults, slot gating, server-only loads, per-render read
|
|
823
|
-
sharing, and structured degradation reports. A theme owns its Astro markup,
|
|
824
|
-
layout, styles, copy packs, route adapters, and browser interaction wiring.
|
|
825
|
-
The Storefront SDK owns commerce data, browser state machines, server adapters,
|
|
826
|
-
screen-value projections, and platform error contracts.
|
|
827
|
-
|
|
828
|
-
`InstanceView` does not write page documents, mutate catalog definitions, call
|
|
829
|
-
the dashboard, or create themes. Editors, CLIs, publishing, and persistence
|
|
830
|
-
are outside this package.
|
|
831
|
-
|
|
832
|
-
For a real Astro theme, create one `ReadCache` outside the section map, pass a
|
|
833
|
-
request-built `ThemeRenderContext`, and pass the server-only `StoreClient`
|
|
834
|
-
explicitly. A `load` function must read through `reads.get(key, loader)` so
|
|
835
|
-
siblings share a resource within that render. Validate the catalog during the
|
|
836
|
-
theme's build/test path and treat non-empty issue arrays as actionable
|
|
837
|
-
diagnostics.
|
|
838
|
-
|
|
839
|
-
On a templated surface (`/products/[slug]`, `/category/[slug]`), pass
|
|
840
|
-
`params: Astro.params` in the render context. A section reads its route
|
|
841
|
-
resource from `context.params`, under a `reads` key that includes the
|
|
842
|
-
parameter, so the page and every section share one read:
|
|
843
|
-
|
|
844
|
-
```ts
|
|
845
|
-
load: async ({ storefront, reads, context }) => ({
|
|
846
|
-
product: await reads.get(`product:${context.params?.slug}`, () =>
|
|
847
|
-
storefront.products.get(context.params?.slug ?? ""),
|
|
848
|
-
),
|
|
849
|
-
}),
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
## Saved documents
|
|
853
|
-
|
|
854
|
-
A stored page document can be missing, malformed, or built for a catalog that
|
|
855
|
-
has since changed. The SDK reports every degradation and never throws the page
|
|
856
|
-
away. Which document to render instead is a product decision; the recommended
|
|
857
|
-
policy is:
|
|
858
|
-
|
|
859
|
-
1. **No saved document** → render the theme's shipped default document.
|
|
860
|
-
2. **Envelope invalid** (`parsePageData` returns `page: null`) → render the shipped default.
|
|
861
|
-
3. **Saved sections map to no catalog type at all** (a theme switch left nothing
|
|
862
|
-
renderable) → render the shipped default.
|
|
863
|
-
4. **Partial overlap** → render what maps. Unknown types render nothing and are
|
|
864
|
-
reported through `onIssue`.
|
|
865
|
-
5. **An empty saved document** → honor it. A page the merchant emptied renders empty.
|
|
866
|
-
|
|
867
|
-
`resolveSurfaceDocument(theme, surface, saved)` applies this policy — and
|
|
868
|
-
also renders the default for a document saved for another surface or another
|
|
869
|
-
theme — then completes the result (`completePage`). The shipped default is
|
|
870
|
-
the surface's `document`:
|
|
871
|
-
|
|
872
|
-
```ts
|
|
873
|
-
const { page, source, reason, issues } = resolveSurfaceDocument(theme, "home", await store.pageData.get("home"));
|
|
874
|
-
// render page.sections; log reason and issues when source is "default"
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
## Theme settings
|
|
878
|
-
|
|
879
|
-
A theme's global choices — its font, which menu the header shows, a header
|
|
880
|
-
color — are **settings**: declared once on the theme, in groups, with the same
|
|
881
|
-
field kinds as a section. The merchant's values are stored per store and per
|
|
882
|
-
theme (a value only means something against the theme that declared it) and
|
|
883
|
-
reach the theme as `StoreConfig.themeSettings` from `@wocart/storefront`.
|
|
884
|
-
|
|
885
|
-
```ts
|
|
886
|
-
export const theme = defineTheme({
|
|
887
|
-
key: "my-theme",
|
|
888
|
-
version: "1.0.0",
|
|
889
|
-
locales: ["ar", "fr"],
|
|
890
|
-
settings: [
|
|
891
|
-
{
|
|
892
|
-
key: "header",
|
|
893
|
-
label: { ar: "الرأس", fr: "En-tête" },
|
|
894
|
-
fields: [
|
|
895
|
-
{ key: "headerMenu", kind: "menu", label: { ar: "القائمة", fr: "Menu" }, default: "main-menu" },
|
|
896
|
-
{ key: "font", kind: "select", label: { ar: "الخط", fr: "Police" }, options: ["Cairo", "Tajawal"], default: "Cairo" },
|
|
897
|
-
],
|
|
898
|
-
},
|
|
899
|
-
],
|
|
900
|
-
components: { … },
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
const { values, issues } = resolveThemeSettings(theme, config?.themeSettings, { locales: [locale] });
|
|
904
|
-
values.headerMenu; // "main-menu" — values: InferSettings<typeof theme>, from 1.1.1
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
- **Reading never fails.** Each setting takes the merchant's value when the
|
|
908
|
-
declaration accepts it, else the default (or the content pack, for text),
|
|
909
|
-
else it is absent. A value the theme no longer accepts — a removed option, a
|
|
910
|
-
color that is not one, a setting since deleted — is reported in `issues`
|
|
911
|
-
(`invalid-setting`, `unknown-setting`, `invalid-settings`) and falls back.
|
|
912
|
-
- **Keys are unique across groups**; groups are for the editor, values are flat.
|
|
913
|
-
- **A writer checks strictly** with `checkThemeSettings(theme, values)` before
|
|
914
|
-
saving: every key declared, every value accepted, in every language.
|
|
915
|
-
- Settings travel in the manifest (`manifest.settings`), so an editor lists
|
|
916
|
-
them without running the theme.
|
|
917
|
-
- Store identity — the logo, the brand colors — is not a theme setting: it
|
|
918
|
-
belongs to the store and survives a theme change.
|
|
919
|
-
|
|
920
|
-
## Content and copy
|
|
921
|
-
|
|
922
|
-
The theme owns its content. The SDK does not model a content pack, a taxonomy,
|
|
923
|
-
or a copy system — it only reads the resolved pack the request hands it.
|
|
924
|
-
|
|
925
|
-
- **`ThemeRenderContext.content`** is an opaque, flat `Record<string, string>` —
|
|
926
|
-
the theme's pack with store overrides already merged.
|
|
927
|
-
- **`FieldDefinition.contentKey`** names a key *in that map*. It is the only
|
|
928
|
-
bridge between the contract and the theme's copy.
|
|
929
|
-
- **The precedence chain** is: instance value → `content[contentKey]` →
|
|
930
|
-
`field.default` → omit (the component's own default applies last). A field
|
|
931
|
-
with neither `contentKey` nor `default` is simply absent from `props`.
|
|
932
|
-
- **Declare the packs on the theme** — `defineTheme({ content: { ar, fr } })`
|
|
933
|
-
— and every `contentKey` must exist in every pack, each declared language
|
|
934
|
-
needs its pack, and the manifest carries them, so an editor shows the text a
|
|
935
|
-
field inherits and lists the labels a merchant may override.
|
|
936
|
-
- **Or check against a key set.** `validateThemeDefinition(theme, {
|
|
937
|
-
contentKeys })` reports every `contentKey` missing from it as
|
|
938
|
-
`unknown-content-key`; an explicit set wins over declared packs, and an empty
|
|
939
|
-
array reports every declared key.
|
|
940
|
-
|
|
941
|
-
Both ways of consuming copy are legitimate, and the theme picks: a component can
|
|
942
|
-
take per-field props, or read the whole pack and overlay the resolved values
|
|
943
|
-
itself. The SDK deliberately ships neither helper — that would promote one
|
|
944
|
-
theme's implementation choice into platform API.
|
|
945
|
-
|
|
946
|
-
### Messages: placeholders and plurals
|
|
947
|
-
|
|
948
|
-
`formatMessage(pack, key, values, { locale, formatNumber? })` is the SDK's one
|
|
949
|
-
message rule — Shopify's locale-file model on the platform's own CLDR data
|
|
950
|
-
(`Intl.PluralRules`), no message-format dependency:
|
|
951
|
-
|
|
952
|
-
- `{name}` placeholders: every occurrence is filled, numbers are formatted for
|
|
953
|
-
the language (or by `formatNumber`), an unknown placeholder stays visible.
|
|
954
|
-
- A plural message is flat keys, one per CLDR form — `cartItems_one`,
|
|
955
|
-
`cartItems_few`, `cartItems_other` — chosen by `values.count`; `_other` is
|
|
956
|
-
the fallback, and an explicit `_zero` is used for 0 in any language.
|
|
957
|
-
- `pluralFormIssues(pack, locale)` lists the forms a pack is missing for its
|
|
958
|
-
language, or carries but never uses — run it over every pack in a test.
|
|
959
|
-
- `messageForms(pack, key)` collects one message's keys to hand a browser
|
|
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.
|
|
964
|
-
|
|
965
|
-
Packs stay `Record<string, string>`, so store overrides and the editor treat
|
|
966
|
-
plural forms like any other key.
|
|
967
|
-
|
|
968
|
-
## Capabilities are advisory
|
|
969
|
-
|
|
970
|
-
`ComponentDefinition.capabilities` (`insertable`, `surfaces`) is metadata for an **editor/CLI insertion
|
|
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
|
|
982
|
-
`resolveInstance` consults it: a saved document containing a component marked
|
|
983
|
-
`insertable: false` still renders, because the render path never second-guesses
|
|
984
|
-
a document that was already saved. Any future enforcement belongs at insertion
|
|
985
|
-
time, in the writer.
|
|
986
|
-
|
|
987
|
-
## Compatibility
|
|
988
|
-
|
|
989
|
-
Page data is a persisted format, and the reader is deliberately forgiving — but
|
|
990
|
-
forgiveness is degradation with a report, not silent migration. There is no
|
|
991
|
-
migration engine, by design; what follows is the whole policy:
|
|
992
|
-
|
|
993
|
-
- **Envelope version.** `PageData.version` is `1`, and only `1` parses. Unknown
|
|
994
|
-
*top-level* keys are carried through verbatim (a writer may record metadata
|
|
995
|
-
without a version bump); unknown keys *inside* an instance are stripped.
|
|
996
|
-
- **Removed or renamed component type.** The section reports `unknown-type` and
|
|
997
|
-
renders nothing; siblings survive. Never rename a `type` once documents
|
|
998
|
-
reference it — treat it as a breaking change for saved pages.
|
|
999
|
-
- **Renamed, removed, or retyped field.** A stored value for an undeclared key
|
|
1000
|
-
reports `unknown-prop` and is ignored; a value that fails the field's kind or
|
|
1001
|
-
bounds reports `invalid-prop` and falls through the defaults chain
|
|
1002
|
-
(instance → `content[contentKey]` → `field.default` → omit). A `required`
|
|
1003
|
-
field with no resolvable value reports `missing-required-prop`.
|
|
1004
|
-
- **Removed slot or narrowed `allow` list.** Saved children that no longer fit
|
|
1005
|
-
report `unknown-slot` or `disallowed-child` and are dropped. Widen a slot's
|
|
1006
|
-
rules freely, but narrow them only as a breaking change for saved pages.
|
|
1007
|
-
- **Duplicate instance ids.** First wins depth-first; later duplicates are
|
|
1008
|
-
dropped with `duplicate-id`. Generate stable unique ids at creation.
|
|
1009
|
-
- **What never breaks the page.** Any section-level failure preserves the rest
|
|
1010
|
-
of the page. Only an envelope failure (`version`, `surface`, `sections`
|
|
1011
|
-
shape) returns `page: null`.
|
|
1012
|
-
- **Theme versions.** The runtime accepts any non-empty `version` string;
|
|
1013
|
-
stick to semver so the manifest validator stays quiet, and compare releases
|
|
1014
|
-
with `diffThemeManifests`.
|
|
1015
|
-
- **Provenance.** A document may name the theme that wrote it,
|
|
1016
|
-
`"theme": { "key", "version" }`; `validatePageData` reports `theme-mismatch`
|
|
1017
|
-
for another theme's document, and a malformed one is dropped with
|
|
1018
|
-
`invalid-provenance`.
|
|
1019
|
-
- **Features and renderers.** An older renderer strips instance keys it does
|
|
1020
|
-
not know, so it would render a `hidden` section. The manifest's `sdkVersion`
|
|
1021
|
-
says which renderer a theme ships; an editor writes `hidden` only for 1.5.0
|
|
1022
|
-
or later.
|
|
1023
|
-
|
|
1024
|
-
## Images
|
|
1025
|
-
|
|
1026
|
-
`@wocart/theme/image-service` resizes every `<Image>` of a media-domain image
|
|
1027
|
-
with Cloudflare Image Resizing, on the media domain itself
|
|
1028
|
-
(`https://<media>/cdn-cgi/image/…`). The media domain is a Cloudflare zone
|
|
1029
|
-
wherever the storefront runs, so a storefront on Cloudflare and one on Vercel
|
|
1030
|
-
share the same URLs and edge cache. Any other image is served as it is; with no
|
|
1031
|
-
media domain, every image passes through.
|
|
1032
|
-
|
|
1033
|
-
The domain is read on each image, not at build — one build serves every store.
|
|
1034
|
-
Bind it once:
|
|
1035
|
-
|
|
1036
|
-
```ts
|
|
1037
|
-
// src/image-service.ts
|
|
1038
|
-
import { MEDIA_DOMAIN } from "astro:env/server";
|
|
1039
|
-
import { mediaImageService } from "@wocart/theme/image-service";
|
|
1040
|
-
export default mediaImageService(() => MEDIA_DOMAIN);
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
```js
|
|
1044
|
-
// astro.config.mjs
|
|
1045
|
-
image: { service: { entrypoint: "./src/image-service.ts" } },
|
|
1046
|
-
// @astrojs/cloudflare: imageService: "custom" · @astrojs/vercel: leave imageService off
|
|
1047
|
-
```
|
|
1048
|
-
|
|
1049
|
-
The media domain's zone needs Image Transformations on (Images →
|
|
1050
|
-
Transformations; included in the Free plan).
|
|
1051
|
-
|
|
1052
|
-
## Testing your theme
|
|
1053
|
-
|
|
1054
|
-
A theme proves it honors the contract with four checks in its own test suite:
|
|
1055
|
-
|
|
1056
|
-
1. **The catalog assembles clean** — with its packs declared on the theme,
|
|
1057
|
-
its default documents and presets are checked too:
|
|
1058
|
-
`expect(validateThemeDefinition(theme)).toEqual([])`. To check something
|
|
1059
|
-
of every component, iterate `componentsOf(theme)`, which lists them as
|
|
1060
|
-
`ComponentDefinition`s.
|
|
1061
|
-
2. **The manifest survives a JSON round trip**:
|
|
1062
|
-
`validateThemeManifest(JSON.parse(JSON.stringify(themeManifest(theme))))`
|
|
1063
|
-
returns no issues.
|
|
1064
|
-
3. **Every shipped document validates**:
|
|
1065
|
-
`validatePageData(theme, document, { content: pack, locales: [locale] })`
|
|
1066
|
-
returns no issues for each default page the theme ships.
|
|
1067
|
-
4. **Every shipped document renders**: render each section through
|
|
1068
|
-
`InstanceView` with Astro's Container API, one `createReadCache()` per page
|
|
1069
|
-
and a stand-in `StoreClient`, and assert that `onIssue` receives nothing
|
|
1070
|
-
unexpected:
|
|
1071
|
-
|
|
1072
|
-
```ts
|
|
1073
|
-
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
|
1074
|
-
import { createReadCache, type RendererIssue } from "@wocart/theme/astro";
|
|
1075
|
-
import InstanceView from "@wocart/theme/astro/InstanceView.astro";
|
|
1076
|
-
import type { StoreClient } from "@wocart/storefront/server";
|
|
1077
|
-
|
|
1078
|
-
const container = await AstroContainer.create();
|
|
1079
|
-
const reads = createReadCache();
|
|
1080
|
-
const issues: RendererIssue[] = [];
|
|
1081
|
-
const storefront = { products: { list: async () => products } } as unknown as StoreClient;
|
|
1082
|
-
|
|
1083
|
-
for (const section of page.sections) {
|
|
1084
|
-
await container.renderToString(InstanceView, {
|
|
1085
|
-
props: { instance: section, theme, context, storefront, reads, onIssue: (i) => issues.push(i) },
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
1088
|
-
expect(issues).toEqual([]);
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
Also test the degradations you rely on, such as a failing `load` still
|
|
1092
|
-
rendering base props.
|
|
1093
|
-
|
|
1094
|
-
`createInstance` refuses to build an instance the reader would flag: wrong-typed
|
|
1095
|
-
values, unknown keys, and a `required` field with no value, default, or
|
|
1096
|
-
`contentKey`.
|
|
1097
|
-
|
|
1098
|
-
## Versioning
|
|
1099
|
-
|
|
1100
|
-
This package follows [semantic versioning](https://semver.org/): breaking
|
|
1101
|
-
changes ship only in a new major version and are listed in the
|
|
1102
|
-
[changelog](./CHANGELOG.md). The manifest's `contractVersion` and the page
|
|
1103
|
-
document's `version` are versioned separately, as described above.
|
|
43
|
+
| `@wocart/theme` | definitions, validation, page documents, resolution, rich text, the manifest, languages and messages |
|
|
44
|
+
| `@wocart/theme/astro` | the read cache, the manifest and styles endpoints, the preview target |
|
|
45
|
+
| `@wocart/theme/astro/InstanceView.astro`, `Blocks.astro`, `Block.astro`, `ThemeStyles.astro` | the reference renderer |
|
|
46
|
+
| `@wocart/theme/image-service` | `mediaImageService` — resizes store images through the store's media delivery |
|
|
47
|
+
| `@wocart/theme/preview` | the preview bridge, for a theme's preview page |
|
|
48
|
+
| `@wocart/theme/lifecycle`, `@wocart/theme/messages` | small browser modules: binding behaviour across page swaps, messages and plurals |
|
|
49
|
+
|
|
50
|
+
[CHANGELOG.md](./CHANGELOG.md) lists every release.
|
|
1104
51
|
|
|
1105
52
|
## License
|
|
1106
53
|
|