@wocart/theme 1.5.0 → 2.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 +110 -0
- package/README.md +226 -11
- package/dist/astro/Block.astro +29 -0
- package/dist/astro/Blocks.astro +23 -0
- package/dist/astro/InstanceView.astro +34 -10
- package/dist/astro/ThemeStyles.astro +34 -0
- package/dist/astro/index.d.ts +49 -1
- package/dist/astro/index.js +43 -5
- package/dist/blocks.d.ts +61 -0
- package/dist/blocks.js +144 -0
- package/dist/conditions.d.ts +21 -0
- package/dist/conditions.js +65 -0
- package/dist/definitions.js +399 -29
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -0
- package/dist/instances.d.ts +4 -0
- package/dist/instances.js +72 -4
- package/dist/manifest-diff.js +63 -2
- package/dist/manifest.d.ts +245 -13
- package/dist/manifest.js +64 -1
- package/dist/operations.d.ts +39 -13
- package/dist/operations.js +253 -55
- package/dist/page-data.js +63 -27
- package/dist/preview-bridge.js +162 -1
- package/dist/preview-protocol.d.ts +53 -1
- package/dist/rich-text.d.ts +2 -0
- package/dist/rich-text.js +4 -0
- package/dist/schemas.d.ts +2 -1
- package/dist/schemas.js +3 -1
- package/dist/settings.js +20 -1
- package/dist/style.d.ts +45 -0
- package/dist/style.js +248 -0
- package/dist/surface-document.d.ts +49 -0
- package/dist/surface-document.js +162 -0
- package/dist/types.d.ts +217 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,116 @@ 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.0.0 — 2026-10-20
|
|
9
|
+
|
|
10
|
+
Blocks: components place other components inside them, some fixed by the
|
|
11
|
+
theme's code and some required, and every product page keeps an order form.
|
|
12
|
+
Style: a merchant changes a section's spacing, layout, size and colors, as
|
|
13
|
+
steps and color schemes the SDK turns into CSS. Areas: a header and a footer
|
|
14
|
+
edited once for every page. Fields: hand-picked products and collections,
|
|
15
|
+
display hints, and fields that show only when they apply.
|
|
16
|
+
|
|
17
|
+
### Breaking
|
|
18
|
+
|
|
19
|
+
What a 1.x theme or editor may need to change:
|
|
20
|
+
|
|
21
|
+
- A surface whose `pageType` is `product` must guarantee an order form:
|
|
22
|
+
require a section that places a component with `role: "order-form"`
|
|
23
|
+
through fixed or required blocks. A theme without a product surface is
|
|
24
|
+
unaffected.
|
|
25
|
+
- `blocks` is a reserved prop: no component field may use that key.
|
|
26
|
+
- The reader drops what goes past `THEME_LIMITS` (50 per slot, 50 sections,
|
|
27
|
+
1,000 instances, 6 levels) and reports `limit-exceeded`.
|
|
28
|
+
- An editor on 1.x rejects a manifest that uses the new field kinds
|
|
29
|
+
(`style`, `colorScheme`, `colorSchemes`, `productList`, `collectionList`):
|
|
30
|
+
move an editor to 2.0 before a theme uses them.
|
|
31
|
+
- `ThemeManifest.areas` is always present (a manifest without it reads as
|
|
32
|
+
none), `InstanceLocation` carries `depth`, and a list item cannot hold
|
|
33
|
+
`colorSchemes`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Field kinds `style` (panels `spacing`, `layout`, `size`; a value per
|
|
38
|
+
breakpoint), `colorScheme` and `colorSchemes` (the store's schemes, a theme
|
|
39
|
+
setting). `ThemeDefinition.style` gives the platform's steps (`s0`–`s12`)
|
|
40
|
+
and widths their sizes; the manifest carries them.
|
|
41
|
+
- `styleClasses`, `schemeClass`, `themeStyleSheet`, `colorSchemeCss`,
|
|
42
|
+
`contrastRatio`, `STYLE_BREAKPOINTS`, `STYLE_STEPS`, `STYLE_WIDTHS`,
|
|
43
|
+
`STYLE_PANELS`, `COLOR_ROLES`, `COLOR_SCHEMES_MAX`, `THEME_STYLES_PATH`;
|
|
44
|
+
`themeStylesEndpoint` and `@wocart/theme/astro/ThemeStyles.astro`, which
|
|
45
|
+
adds its inline sheet's hash to a page's Content-Security-Policy.
|
|
46
|
+
- `checkThemeSettings` refuses a scheme picker naming a scheme the store does
|
|
47
|
+
not have, and a style the theme gives no size; the manifest diff reports a
|
|
48
|
+
size or panel a theme stops offering as breaking.
|
|
49
|
+
- Areas (`ThemeDefinition.areas`): the parts every page shares, each a
|
|
50
|
+
document stored as the surface `area:<key>` (`areaSurface`), with
|
|
51
|
+
`required` and `max`; the manifest carries them and its diff reports an
|
|
52
|
+
area removed as breaking.
|
|
53
|
+
- Field kinds `productList` and `collectionList`; `display` on `select`
|
|
54
|
+
(dropdown, radio, segmented) and `number` (input, slider); `inline` on
|
|
55
|
+
`richtext`, and `richTextToHtml`'s `inline` option.
|
|
56
|
+
- `visibleIf` on any field and `isFieldVisible`, `CONDITION_DEPTH`.
|
|
57
|
+
- `isPreview(blocks)`, for a placeholder a section draws in an editor's
|
|
58
|
+
preview only.
|
|
59
|
+
- The preview `move` and `restore` patches take `parent`, so reordering
|
|
60
|
+
blocks inside a component shows at once.
|
|
61
|
+
|
|
62
|
+
- `placement` on a component: `"section"`, `"block"` or `"both"`, enforced by
|
|
63
|
+
the document operations.
|
|
64
|
+
- `blocks`, given by the renderer to every component, and the components
|
|
65
|
+
`@wocart/theme/astro/Blocks.astro` and `@wocart/theme/astro/Block.astro`
|
|
66
|
+
that place a slot's blocks and a fixed block where the component wants them
|
|
67
|
+
— any number of slots, where pushed content can fill only one named slot.
|
|
68
|
+
`hasBlocks(blocks, slot)` and the `BlocksHandle` type.
|
|
69
|
+
- Fixed blocks (`fixed` on a definition, `fixed` on an instance): placed by
|
|
70
|
+
code, never removed, moved, copied or hidden, rendered with their defaults
|
|
71
|
+
when a document does not store them. `fixedBlockId`.
|
|
72
|
+
- Required blocks (`required` on a slot or a surface, with `min` and `max`)
|
|
73
|
+
and `max` on a slot or surface: a required block may move but is never lost,
|
|
74
|
+
and a document that lacks one renders it anyway.
|
|
75
|
+
- `role: "order-form"` and the rule that every product surface guarantees an
|
|
76
|
+
order form (`order-form-not-guaranteed`).
|
|
77
|
+
- `THEME_LIMITS`: 50 instances per slot, 50 sections per surface, 1,000
|
|
78
|
+
instances per document, 6 levels of nesting.
|
|
79
|
+
- `completePage(theme, page)`: a document with everything it must hold, for
|
|
80
|
+
an editor to open.
|
|
81
|
+
- `resolveSurfaceDocument(theme, surface, saved)`: the saved document or the
|
|
82
|
+
theme's default for any surface, completed — the policy a theme used to
|
|
83
|
+
write itself.
|
|
84
|
+
- `removeInstance(theme, page, id)` and `setInstanceHidden(theme, page, id,
|
|
85
|
+
hidden)`, which keep required blocks.
|
|
86
|
+
- Issue codes `unknown-fixed`, `fixed-type-mismatch`,
|
|
87
|
+
`missing-required-block`, `limit-exceeded` (reading); `fixed-block`,
|
|
88
|
+
`required-block`, `not-placeable`, `limit-exceeded` (writing);
|
|
89
|
+
`invalid-placement`, `invalid-fixed`, `invalid-role`,
|
|
90
|
+
`order-form-not-guaranteed` (assembly).
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- The reader drops what goes past the limits, reporting it; documents
|
|
95
|
+
written so far are far below them.
|
|
96
|
+
- `InstanceLocation` carries `depth`, and `fixed` for a fixed block.
|
|
97
|
+
- A default document written for another surface reports only that.
|
|
98
|
+
|
|
99
|
+
### Deprecated
|
|
100
|
+
|
|
101
|
+
- `removeInstance(page, id)` and `setInstanceHidden(page, id, hidden)`: pass
|
|
102
|
+
the theme first.
|
|
103
|
+
|
|
104
|
+
## 1.6.0 — 2026-10-19
|
|
105
|
+
|
|
106
|
+
An editor's change shows on the preview page at once.
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- The preview protocol's `patch` message (`PreviewPatch`): a change the
|
|
111
|
+
editor has already made — move, duplicate, remove and restore a section,
|
|
112
|
+
hide or show one, a field's text — shown by the preview bridge on the page
|
|
113
|
+
as it stands, before the render that follows it. Patches are hints; the
|
|
114
|
+
render stays the page's truth, and a render already on its way when a
|
|
115
|
+
patch arrives is dropped so it cannot undo it. A page that does not know
|
|
116
|
+
patches ignores them. The protocol version stays 1.
|
|
117
|
+
|
|
8
118
|
## 1.5.0 — 2026-10-18
|
|
9
119
|
|
|
10
120
|
The editor-facing contract: everything a visual editor needs to discover and
|
package/README.md
CHANGED
|
@@ -39,8 +39,10 @@ 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), the preview markers and the `RendererIssue`
|
|
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 |
|
|
43
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)) |
|
|
44
46
|
| `@wocart/theme/image-service` | `mediaImageService` — an Astro image service that resizes media-domain images on the media domain (any storefront host) |
|
|
45
47
|
| `@wocart/theme/preview` | `startPreviewBridge` — the browser module of a theme's preview page (see [Preview](#preview)) |
|
|
46
48
|
|
|
@@ -129,7 +131,32 @@ defineComponent({
|
|
|
129
131
|
| `category` | a category slug | found in `@wocart/storefront`'s `categories.list()` |
|
|
130
132
|
| `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 |
|
|
131
133
|
| `richtext` | a rich-text document, or one per language | a closed JSON format; `formats` narrows what it may contain |
|
|
132
|
-
| `list` | `[{ id, props }]` | item `fields` of any kind but `list`; `min`/`max`; `itemLabel` names the item field an editor shows; default items carry their own ids |
|
|
134
|
+
| `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 |
|
|
135
|
+
| `style` | `{ spacing?, layout?, size? }`, a value per breakpoint | `panels` names what it offers; steps and choices only, never CSS (see [Style](#style)) |
|
|
136
|
+
| `colorScheme` | a color scheme's id | one of the store's schemes; the theme needs a `colorSchemes` setting |
|
|
137
|
+
| `colorSchemes` | `[{ id, name, colors }]` | a theme setting, at most one; every role a hex color; at most 24 |
|
|
138
|
+
| `productList` | product handles, in order | distinct; `min`/`max` (at most 50); each read with `products.get(handle)`, a missing one reads as absent |
|
|
139
|
+
| `collectionList` | collection handles, in order | distinct; `min`/`max` (at most 50) |
|
|
140
|
+
|
|
141
|
+
**How an editor shows a field** never changes what is stored:
|
|
142
|
+
`display: "dropdown" | "radio" | "segmented"` on a `select` (alignment is a
|
|
143
|
+
segmented `select` of `start`, `center`, `end`), `display: "input" | "slider"`
|
|
144
|
+
on a `number` (a slider needs `min`, `max` and `step`), and `inline: true` on
|
|
145
|
+
a `richtext` — one line, a single paragraph with marks and links, rendered
|
|
146
|
+
without a block element by `richTextToHtml(doc, { inline: true })`.
|
|
147
|
+
|
|
148
|
+
**A field may show only when it applies.** `visibleIf` is a closed JSON
|
|
149
|
+
condition on the fields beside it (the same component's, list item's, or
|
|
150
|
+
theme settings'): `{ field, equals }`, `{ field, in: [...] }`, `{ all: [...] }`,
|
|
151
|
+
`{ any: [...] }`, `{ not: ... }`, nested at most `CONDITION_DEPTH` (4) deep.
|
|
152
|
+
`isFieldVisible(field, values)` evaluates it for an editor — an unset field
|
|
153
|
+
compares as `null` — and the renderer ignores it; a hidden field keeps its
|
|
154
|
+
value.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
{ key: "source", kind: "select", label: "Products", options: ["latest", "picked"], default: "latest", display: "segmented" },
|
|
158
|
+
{ key: "picked", kind: "productList", label: "Picked products", max: 12, visibleIf: { field: "source", equals: "picked" } },
|
|
159
|
+
```
|
|
133
160
|
|
|
134
161
|
Use `url` for every editor-settable `href`, never `text`: the reader drops an
|
|
135
162
|
unsafe saved link (`invalid-prop`) and falls back to the field's default, so a
|
|
@@ -269,6 +296,144 @@ A component declares where children may go; the document says what is there.
|
|
|
269
296
|
displaces an allowed one. The rules travel in the manifest, so an editor can
|
|
270
297
|
offer only valid insertions.
|
|
271
298
|
|
|
299
|
+
## Blocks
|
|
300
|
+
|
|
301
|
+
A section is often made of parts a merchant arranges — a product section's
|
|
302
|
+
gallery, price, order form, a countdown, "Add to cart" — and some of them must
|
|
303
|
+
always be there. Blocks are components placed inside other components.
|
|
304
|
+
|
|
305
|
+
**Where a component may go.** `placement: "section"` (the top level of a page),
|
|
306
|
+
`"block"` (only inside a slot) or `"both"`; absent means anywhere. The document
|
|
307
|
+
operations enforce it; the renderer never does.
|
|
308
|
+
|
|
309
|
+
**A component places its own blocks.** The renderer gives every component a
|
|
310
|
+
`blocks` prop. Put each slot's blocks, and each fixed block, where the design
|
|
311
|
+
wants them — as many slots as the component declares:
|
|
312
|
+
|
|
313
|
+
```astro
|
|
314
|
+
---
|
|
315
|
+
import Blocks from "@wocart/theme/astro/Blocks.astro";
|
|
316
|
+
import Block from "@wocart/theme/astro/Block.astro";
|
|
317
|
+
import { hasBlocks, type BlocksHandle } from "@wocart/theme/astro";
|
|
318
|
+
|
|
319
|
+
const { blocks, title } = Astro.props as { blocks: BlocksHandle; title: string };
|
|
320
|
+
---
|
|
321
|
+
<section>
|
|
322
|
+
{hasBlocks(blocks, "media") && <div class="media"><Blocks of={blocks} slot="media" /></div>}
|
|
323
|
+
<h1>{title}</h1>
|
|
324
|
+
<Block of={blocks} name="buy-box" />
|
|
325
|
+
</section>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`<slot />` keeps working for children, as before; a component that pulls its
|
|
329
|
+
blocks never pays for content pushed into a slot it does not render. `blocks`
|
|
330
|
+
is a reserved prop: no field may use that key.
|
|
331
|
+
|
|
332
|
+
`isPreview(blocks)` says whether the component renders in an editor's
|
|
333
|
+
preview. A section with nothing to show yet — no products picked, no text
|
|
334
|
+
written — draws a placeholder there, so a merchant can see and select it; a
|
|
335
|
+
shopper's page never shows one.
|
|
336
|
+
|
|
337
|
+
**Fixed blocks** are placed by the component's code, like Shopify's static
|
|
338
|
+
blocks. Declare them so an editor knows them:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
fixed: [{ name: "buy-box", type: "buy-box", label: "Buy box" }],
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
An editor changes a fixed block's fields and fills its slots, but cannot
|
|
345
|
+
remove, move, copy or hide it. It renders even when the document does not
|
|
346
|
+
store it, with its defaults and the id `<owner id>~<name>` (`fixedBlockId`).
|
|
347
|
+
|
|
348
|
+
**Required blocks** may move but never disappear. A slot — or a surface, for
|
|
349
|
+
its top-level sections — lists them:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
slots: [{ name: "default", allow: ["order-form", "countdown", "add-to-cart"], max: 4,
|
|
353
|
+
required: [{ type: "order-form", max: 1 }] }],
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The operations refuse to remove, hide or move out the last `min` (default 1)
|
|
357
|
+
of a required type, or to insert past its `max`. A document that lacks one —
|
|
358
|
+
an old document, a theme update — renders it anyway, added at the end of its
|
|
359
|
+
slot, and the reader reports `missing-required-block`.
|
|
360
|
+
|
|
361
|
+
**Every product page keeps an order form.** A component with
|
|
362
|
+
`role: "order-form"` places orders. Every surface whose `pageType` is
|
|
363
|
+
`product` must guarantee at least one through required sections and fixed or
|
|
364
|
+
required blocks; assembly refuses it otherwise (`order-form-not-guaranteed`).
|
|
365
|
+
Give the order form's entry `max: 1` for exactly one. Its fields are not the
|
|
366
|
+
theme's: a store's checkout form is set in the merchant dashboard and enforced
|
|
367
|
+
by the server.
|
|
368
|
+
|
|
369
|
+
**Limits** (`THEME_LIMITS`): 50 instances per slot, 50 sections per surface,
|
|
370
|
+
1,000 instances per document, 6 levels of nesting below a section. A slot or
|
|
371
|
+
surface `max` may set fewer. The reader renders up to a limit and reports
|
|
372
|
+
`limit-exceeded`; the operations refuse to go past one. Assembly refuses fixed
|
|
373
|
+
and required blocks that contain themselves or nest deeper than the limit.
|
|
374
|
+
|
|
375
|
+
## Style
|
|
376
|
+
|
|
377
|
+
A merchant changes how a section sits on the page and which colors it wears
|
|
378
|
+
— never by writing CSS. A theme gives sizes to the platform's style steps,
|
|
379
|
+
offers style fields, and the SDK writes the CSS.
|
|
380
|
+
|
|
381
|
+
**Sizes.** The step names are the platform's (`s0` to `s12`); a theme maps the
|
|
382
|
+
ones it offers to pixels, and the widths a section may take:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
style: { spacing: { s0: 0, s2: 8, s4: 16, s6: 32, s8: 64 }, widths: { wide: 1280, content: 960, narrow: 640 } },
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Style fields.** A `style` field names its panels — `spacing` (padding and
|
|
389
|
+
margin on the block axis, inline padding, gap), `layout` (direction, align,
|
|
390
|
+
justify, wrap) and `size` (width, minimum height). A value holds a choice per
|
|
391
|
+
breakpoint — `base` everywhere, `md` from 768 px, `lg` from 1024 px — phone
|
|
392
|
+
first, on CSS logical properties so right-to-left stores mirror on their own:
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
{ key: "style", kind: "style", label: "Style", panels: ["spacing", "size"],
|
|
396
|
+
default: { spacing: { paddingBlockStart: { base: "s4", lg: "s8" } } } }
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
A writer refuses a step or width the theme gives no size; a reader renders one
|
|
400
|
+
as no style.
|
|
401
|
+
|
|
402
|
+
**Colors.** The store's color schemes are one theme setting, `colorSchemes`:
|
|
403
|
+
named schemes, each a hex color for every role (`background`, `text`,
|
|
404
|
+
`heading`, `accent`, `button`, `buttonText`, `border`). A `colorScheme` field
|
|
405
|
+
picks one by id. A page document never holds a color.
|
|
406
|
+
|
|
407
|
+
**In the page.** Give the element its classes, serve the stylesheet, and put
|
|
408
|
+
the schemes in the `<head>`:
|
|
409
|
+
|
|
410
|
+
```astro
|
|
411
|
+
---
|
|
412
|
+
import { schemeClass, styleClasses } from "@wocart/theme";
|
|
413
|
+
const { style, scheme } = Astro.props;
|
|
414
|
+
---
|
|
415
|
+
<section class={`${schemeClass(scheme)} ${styleClasses(style)}`}>…</section>
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
// src/pages/wocart-styles.css.ts
|
|
420
|
+
import { themeStylesEndpoint } from "@wocart/theme/astro";
|
|
421
|
+
export const prerender = true;
|
|
422
|
+
export const GET = themeStylesEndpoint(theme);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
```astro
|
|
426
|
+
<!-- the layout's <head> -->
|
|
427
|
+
<ThemeStyles schemes={settings.colorSchemes} /> <!-- @wocart/theme/astro/ThemeStyles.astro -->
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
The schemes become custom properties on `.wc-scheme-<id>`
|
|
431
|
+
(`--wc-color-background`, `--wc-color-button-text`, …) with the background
|
|
432
|
+
and text applied. On a page with a Content-Security-Policy, `ThemeStyles` adds
|
|
433
|
+
its inline sheet's hash to the policy, so `style-src` needs no
|
|
434
|
+
`'unsafe-inline'`. `contrastRatio(a, b)` gives the WCAG ratio an editor warns
|
|
435
|
+
on (below 4.5 for text).
|
|
436
|
+
|
|
272
437
|
## Pages and templates
|
|
273
438
|
|
|
274
439
|
A surface is a document a theme can render. Declare the platform page type it
|
|
@@ -297,6 +462,26 @@ surfaces: [
|
|
|
297
462
|
it can serve (alternates included); `isInsertableOn(component, surface)`
|
|
298
463
|
applies the rule.
|
|
299
464
|
|
|
465
|
+
**Areas** are the parts every page shares — a header, a footer — composed of
|
|
466
|
+
sections like a page and edited once for the whole store:
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
areas: [
|
|
470
|
+
{ key: "header", label: "Header", document: headerDocument, required: [{ type: "menu-bar", max: 1 }], max: 4 },
|
|
471
|
+
{ key: "footer", label: "Footer" },
|
|
472
|
+
],
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
An area's document is the surface `area:<key>` (`areaSurface("header")`):
|
|
476
|
+
read, completed, checked and edited like a page's, with `required` and `max`
|
|
477
|
+
as a surface has them. A component names `area:header` in
|
|
478
|
+
`capabilities.surfaces` to stay in the header. The layout renders it like a
|
|
479
|
+
page: `resolveSurfaceDocument(theme, "area:header", await store.pageData.get("area:header"))`.
|
|
480
|
+
|
|
481
|
+
A template's preview names the resource it shows: the preview draft's `path`
|
|
482
|
+
(`/products/robe-kabyle`), which `@wocart/storefront`'s `matchStorefrontPath`
|
|
483
|
+
turns into the page type and the route's params.
|
|
484
|
+
|
|
300
485
|
## Presets
|
|
301
486
|
|
|
302
487
|
A preset is a ready-made configuration an editor offers when inserting a
|
|
@@ -337,11 +522,21 @@ if (!edit.ok) console.log(edit.issue.code, edit.issue.message); // "not-availabl
|
|
|
337
522
|
|
|
338
523
|
`insertInstance`, `removeInstance`, `moveInstance`, `duplicateInstance`,
|
|
339
524
|
`setInstanceProps` and `setInstanceHidden` never change their input. They
|
|
340
|
-
refuse a component that is `insertable: false
|
|
341
|
-
document's surface, a slot that does not take it
|
|
342
|
-
cycle, an id already in the document,
|
|
343
|
-
|
|
344
|
-
|
|
525
|
+
refuse a component that is `insertable: false`, not available on the
|
|
526
|
+
document's surface or not placeable there, a slot that does not take it or is
|
|
527
|
+
full, an index out of range, a cycle, an id already in the document, a value
|
|
528
|
+
its field refuses, any change to a fixed block but its fields
|
|
529
|
+
(`fixed-block`), losing a required block (`required-block`) and going past a
|
|
530
|
+
limit (`limit-exceeded`). `removeInstance(theme, page, id)` and
|
|
531
|
+
`setInstanceHidden(theme, page, id, hidden)` take the theme first; their older
|
|
532
|
+
forms without it still refuse a fixed block but cannot know a slot's required
|
|
533
|
+
types. `checkPageData(theme, page)` is the strict check of a whole document
|
|
534
|
+
before saving it; `findInstance` locates an instance, a fixed block included.
|
|
535
|
+
|
|
536
|
+
Open a document with `completePage(theme, page)`: it adds what the document
|
|
537
|
+
must hold — required sections, fixed blocks, required blocks, each with an id
|
|
538
|
+
unique in the document — so what an editor shows is what renders. Inserting a
|
|
539
|
+
component completes it the same way.
|
|
345
540
|
|
|
346
541
|
An editor holds a theme's manifest, not its code:
|
|
347
542
|
`manifestCatalog(manifest)` makes the catalog these functions take, with no
|
|
@@ -406,15 +601,28 @@ framing window, with `source: "wocart-editor"` and the protocol's `version`
|
|
|
406
601
|
| The editor sends | The page answers |
|
|
407
602
|
| --- | --- |
|
|
408
603
|
| — | `ready` with every instance: `{ id, type, parent, hidden, rect }` |
|
|
409
|
-
| `render` `{ draft, revision? }` | `rendered` `{ revision, instances }`, or `error` `{ revision, reason }`; an answer overtaken by a newer render is dropped |
|
|
604
|
+
| `render` `{ draft, revision? }` | `rendered` `{ revision, instances }`, or `error` `{ revision, reason }`; an answer overtaken by a newer render or a patch is dropped |
|
|
605
|
+
| `patch` `{ patches }` | the change shown at once, on the page as it stands, then `layout` (see below) |
|
|
410
606
|
| `mode` `"edit"` \| `"interactive"` | in `edit`, links and forms do nothing and a click is `select` `{ id }` of the innermost instance |
|
|
411
607
|
| `scroll-to` `{ id }` | the instance scrolls into view |
|
|
412
608
|
| `token` `{ token }` | the next render uses it |
|
|
413
609
|
| — | `hover` `{ id \| null }` as the pointer moves; `layout` after a scroll or resize |
|
|
414
610
|
|
|
611
|
+
A `patch` shows a change the editor has already made to its draft before the
|
|
612
|
+
render that follows it arrives — so moving, copying, removing or hiding a
|
|
613
|
+
section, and typing a field's text, show on the page at once. Each patch
|
|
614
|
+
(`PreviewPatch`) is a hint: the page applies what it finds — `move` a section
|
|
615
|
+
to an index, `duplicate` an instance under new ids, `remove` one and
|
|
616
|
+
`restore` it, `hide` one or show it again, replace `text` that reads `from`
|
|
617
|
+
inside an instance with `to` — and leaves the rest to the render, which is
|
|
618
|
+
the page's truth. An editor therefore always sends the render too; a page
|
|
619
|
+
from before 1.6.0 ignores patches and still shows every change, a moment
|
|
620
|
+
later.
|
|
621
|
+
|
|
415
622
|
A swapped section's scripts do not run again. A section with browser behavior
|
|
416
623
|
binds it again on `PREVIEW_SECTION_LOAD_EVENT` (`wocart:section-load`,
|
|
417
|
-
dispatched on `document` with `detail.id`)
|
|
624
|
+
dispatched on `document` with `detail.id`) — also for a section a patch
|
|
625
|
+
copied, restored or showed again. When the bridge replaces the whole
|
|
418
626
|
page instead, it does what Astro's router does after a navigation — a new
|
|
419
627
|
`<body>`, the root element's attributes, the title — and dispatches
|
|
420
628
|
`astro:page-load`, so a script written for the router mounts again as it does
|
|
@@ -576,8 +784,15 @@ policy is:
|
|
|
576
784
|
reported through `onIssue`.
|
|
577
785
|
5. **An empty saved document** → honor it. A page the merchant emptied renders empty.
|
|
578
786
|
|
|
579
|
-
|
|
580
|
-
document
|
|
787
|
+
`resolveSurfaceDocument(theme, surface, saved)` applies this policy — and
|
|
788
|
+
also renders the default for a document saved for another surface or another
|
|
789
|
+
theme — then completes the result (`completePage`). The shipped default is
|
|
790
|
+
the surface's `document`:
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
const { page, source, reason, issues } = resolveSurfaceDocument(theme, "home", await store.pageData.get("home"));
|
|
794
|
+
// render page.sections; log reason and issues when source is "default"
|
|
795
|
+
```
|
|
581
796
|
|
|
582
797
|
## Theme settings
|
|
583
798
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* One fixed block of the component that received `blocks`, where its code
|
|
4
|
+
* places it: the stored block, or one with its defaults when the document
|
|
5
|
+
* does not store it. `name` is one the definition's `fixed` lists.
|
|
6
|
+
*
|
|
7
|
+
* <Block of={blocks} name="buy-box" />
|
|
8
|
+
*/
|
|
9
|
+
import InstanceView from "./InstanceView.astro";
|
|
10
|
+
import type { BlocksHandle } from "./index.js";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
of: BlocksHandle;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { of, name } = Astro.props as Props;
|
|
18
|
+
const instance = Object.hasOwn(of.fixed, name) ? of.fixed[name] : undefined;
|
|
19
|
+
if (instance === undefined) {
|
|
20
|
+
const issue = {
|
|
21
|
+
code: "unknown-fixed" as const,
|
|
22
|
+
message: `"${of.owner.type}" places no fixed block "${name}"; declare it in the definition's \`fixed\``,
|
|
23
|
+
instanceId: of.owner.id,
|
|
24
|
+
};
|
|
25
|
+
if (of.render.onIssue) of.render.onIssue(issue);
|
|
26
|
+
else console.warn(`[@wocart/theme/astro] ${issue.code}: ${issue.message}`);
|
|
27
|
+
}
|
|
28
|
+
---
|
|
29
|
+
{instance !== undefined && <InstanceView instance={instance} {...of.render} />}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The blocks of one slot of the component that received `blocks`, in order,
|
|
4
|
+
* each through `InstanceView` — so `load`, issues, hidden blocks and preview
|
|
5
|
+
* markers work the same at every depth. A component may place any
|
|
6
|
+
* number of its slots this way.
|
|
7
|
+
*
|
|
8
|
+
* <Blocks of={blocks} slot="media" />
|
|
9
|
+
* <Blocks of={blocks} /> (the default slot)
|
|
10
|
+
*/
|
|
11
|
+
import InstanceView from "./InstanceView.astro";
|
|
12
|
+
import type { BlocksHandle } from "./index.js";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
of: BlocksHandle;
|
|
16
|
+
/** Absent: the default slot. */
|
|
17
|
+
slot?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { of, slot = "default" } = Astro.props as Props;
|
|
21
|
+
const list = Object.hasOwn(of.slots, slot) ? of.slots[slot]! : [];
|
|
22
|
+
---
|
|
23
|
+
{list.map((instance) => <InstanceView instance={instance} {...of.render} />)}
|
|
@@ -5,17 +5,23 @@
|
|
|
5
5
|
*
|
|
6
6
|
* instance (skipped when `hidden`) → definition (catalog) → resolveInstance (chain + gating)
|
|
7
7
|
* → definition.load (optional, server-only, degrading)
|
|
8
|
-
* → <Component {...props}> with children + one named slot
|
|
8
|
+
* → <Component {...props} blocks> with children + one named slot
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Blocks: every component receives `blocks`, and places any slot's
|
|
11
|
+
* blocks with `<Blocks of={blocks} slot>` and its fixed blocks with
|
|
12
|
+
* `<Block of={blocks} name>` — as many slots as it declares.
|
|
13
|
+
*
|
|
14
|
+
* Pushed slots, for components that use `<slot />`: the Astro compiler lifts
|
|
15
|
+
* the first slot-attribute expression of a dynamic child expression into the
|
|
16
|
+
* slots-object key, evaluated in component scope — so exactly ONE named slot
|
|
17
|
+
* per instance can be pushed generically. Extra named slots with content are
|
|
18
|
+
* reported, never silently dropped. Slot content renders lazily, so a
|
|
19
|
+
* component that pulls its blocks never renders what was pushed.
|
|
14
20
|
*
|
|
15
21
|
* Server-only. No hydration, no directives, no second component system —
|
|
16
22
|
* the theme's own `.astro` files do all rendering.
|
|
17
23
|
*/
|
|
18
|
-
import { previewEndMarker, previewStartMarker, resolveInstance, type RendererIssue } from "./index.js";
|
|
24
|
+
import { THEME_LIMITS, previewEndMarker, previewStartMarker, resolveInstance, type BlocksHandle, type RendererIssue } from "./index.js";
|
|
19
25
|
import type {
|
|
20
26
|
ComponentInstance,
|
|
21
27
|
ReadCache,
|
|
@@ -37,9 +43,11 @@ interface Props {
|
|
|
37
43
|
* comments an editor's bridge reads. Never set for a shopper's page.
|
|
38
44
|
*/
|
|
39
45
|
preview?: boolean | undefined;
|
|
46
|
+
/** Levels below the page's sections; past `THEME_LIMITS.depth` nothing renders. */
|
|
47
|
+
depth?: number | undefined;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
const { instance, theme, context, storefront, reads, onIssue, preview = false } = Astro.props as Props;
|
|
50
|
+
const { instance, theme, context, storefront, reads, onIssue, preview = false, depth = 0 } = Astro.props as Props;
|
|
43
51
|
|
|
44
52
|
const report = (issue: RendererIssue): void => {
|
|
45
53
|
if (onIssue) {
|
|
@@ -52,10 +60,18 @@ const report = (issue: RendererIssue): void => {
|
|
|
52
60
|
// A hidden instance stays in the document and renders nothing: no lookup,
|
|
53
61
|
// no `load`, no children.
|
|
54
62
|
const hidden = instance.hidden === true;
|
|
55
|
-
const
|
|
63
|
+
const tooDeep = depth > THEME_LIMITS.depth;
|
|
64
|
+
if (tooDeep && !hidden) {
|
|
65
|
+
report({
|
|
66
|
+
code: "limit-exceeded",
|
|
67
|
+
message: `"${instance.type}" is nested more than ${THEME_LIMITS.depth} levels below a section; nothing rendered`,
|
|
68
|
+
instanceId: instance.id,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const definition = !hidden && !tooDeep && Object.hasOwn(theme.components, instance.type)
|
|
56
72
|
? theme.components[instance.type]
|
|
57
73
|
: undefined;
|
|
58
|
-
if (definition === undefined && !hidden) {
|
|
74
|
+
if (definition === undefined && !hidden && !tooDeep) {
|
|
59
75
|
report({
|
|
60
76
|
code: "unknown-type",
|
|
61
77
|
message: `"${instance.type}" is not in the theme catalog; nothing rendered`,
|
|
@@ -123,8 +139,14 @@ if (definition !== undefined) {
|
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
141
|
|
|
142
|
+
const blocks: BlocksHandle = {
|
|
143
|
+
owner: { id: instance.id, type: instance.type },
|
|
144
|
+
slots: { ...resolved.slots, default: resolved.children },
|
|
145
|
+
fixed: resolved.fixed,
|
|
146
|
+
render: { theme, context, storefront, reads, onIssue, preview, depth: depth + 1 },
|
|
147
|
+
};
|
|
126
148
|
Component = definition.component as RenderableComponent;
|
|
127
|
-
renderProps = { ...resolved.props, ...loaded };
|
|
149
|
+
renderProps = { ...resolved.props, ...loaded, blocks };
|
|
128
150
|
children = resolved.children;
|
|
129
151
|
|
|
130
152
|
const namedEntries = Object.entries(resolved.slots);
|
|
@@ -160,6 +182,7 @@ const endMarker = preview ? `<!--${previewEndMarker(instance)}-->` : "";
|
|
|
160
182
|
reads={reads}
|
|
161
183
|
onIssue={onIssue}
|
|
162
184
|
preview={preview}
|
|
185
|
+
depth={depth + 1}
|
|
163
186
|
/>
|
|
164
187
|
))}
|
|
165
188
|
{firstNamedSlotName !== undefined && (
|
|
@@ -173,6 +196,7 @@ const endMarker = preview ? `<!--${previewEndMarker(instance)}-->` : "";
|
|
|
173
196
|
reads={reads}
|
|
174
197
|
onIssue={onIssue}
|
|
175
198
|
preview={preview}
|
|
199
|
+
depth={depth + 1}
|
|
176
200
|
/>
|
|
177
201
|
))}
|
|
178
202
|
</Fragment>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The theme's style in a page's <head>: the stylesheet of style classes
|
|
4
|
+
* (served at `THEME_STYLES_PATH` by `themeStylesEndpoint`) and the store's
|
|
5
|
+
* color schemes as one small inline sheet. On a page with a
|
|
6
|
+
* Content-Security-Policy, the inline sheet's hash joins the policy, so
|
|
7
|
+
* `style-src` needs no 'unsafe-inline'.
|
|
8
|
+
*
|
|
9
|
+
* <ThemeStyles schemes={settings.colorSchemes} />
|
|
10
|
+
*/
|
|
11
|
+
import { THEME_STYLES_PATH, colorSchemeCss } from "./index.js";
|
|
12
|
+
import type { ColorScheme } from "../types.js";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
schemes?: readonly ColorScheme[] | undefined;
|
|
16
|
+
/** `false` when the theme bundles the style classes itself. */
|
|
17
|
+
stylesheet?: boolean | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { schemes, stylesheet = true } = Astro.props as Props;
|
|
21
|
+
const css = colorSchemeCss(schemes);
|
|
22
|
+
if (css.length > 0) {
|
|
23
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(css)));
|
|
24
|
+
const hash = `sha256-${btoa(String.fromCharCode(...digest))}`;
|
|
25
|
+
const csp = (Astro as unknown as { csp?: { insertStyleHash?: (hash: string) => void } }).csp;
|
|
26
|
+
try {
|
|
27
|
+
csp?.insertStyleHash?.(hash);
|
|
28
|
+
} catch {
|
|
29
|
+
// The page renders without a policy.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
---
|
|
33
|
+
{stylesheet && <link rel="stylesheet" href={THEME_STYLES_PATH} />}
|
|
34
|
+
{css.length > 0 && <style is:inline set:html={css} />}
|
package/dist/astro/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { PageDataIssueCode, ReadCache, ThemeDefinition } from "../types.js";
|
|
1
|
+
import type { ComponentInstance, PageDataIssueCode, ReadCache, ThemeDefinition, ThemeRenderContext } from "../types.js";
|
|
2
|
+
import type { StoreClient } from "@wocart/storefront/server";
|
|
2
3
|
export { resolveInstance } from "../instances.js";
|
|
4
|
+
export { THEME_LIMITS } from "../blocks.js";
|
|
5
|
+
export { THEME_STYLES_PATH, colorSchemeCss } from "../style.js";
|
|
3
6
|
export { previewEndMarker, previewStartMarker } from "../preview-protocol.js";
|
|
4
7
|
/**
|
|
5
8
|
* The theme's manifest as a prerendered endpoint, the file a release and an
|
|
@@ -17,6 +20,51 @@ export { previewEndMarker, previewStartMarker } from "../preview-protocol.js";
|
|
|
17
20
|
* whose manifest is invalid fails its build instead of shipping it.
|
|
18
21
|
*/
|
|
19
22
|
export declare function themeManifestEndpoint(theme: ThemeDefinition): () => Response;
|
|
23
|
+
/**
|
|
24
|
+
* The stylesheet of the theme's style classes as a prerendered endpoint, the
|
|
25
|
+
* file `<ThemeStyles>` links (`THEME_STYLES_PATH`):
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* // src/pages/wocart-styles.css.ts
|
|
29
|
+
* import { themeStylesEndpoint } from "@wocart/theme/astro";
|
|
30
|
+
* import { theme } from "../theme/theme";
|
|
31
|
+
* export const prerender = true;
|
|
32
|
+
* export const GET = themeStylesEndpoint(theme);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function themeStylesEndpoint(theme: ThemeDefinition): () => Response;
|
|
36
|
+
/**
|
|
37
|
+
* What the renderer gives every component as `blocks`: its instance's slots
|
|
38
|
+
* (the default slot under `"default"`), its fixed blocks by name — both
|
|
39
|
+
* already gated and completed — and what rendering them needs. A component
|
|
40
|
+
* passes it to `<Blocks>` and `<Block>`; it never needs to read it.
|
|
41
|
+
*/
|
|
42
|
+
export interface BlocksHandle {
|
|
43
|
+
owner: {
|
|
44
|
+
id: string;
|
|
45
|
+
type: string;
|
|
46
|
+
};
|
|
47
|
+
slots: Readonly<Record<string, readonly ComponentInstance[]>>;
|
|
48
|
+
fixed: Readonly<Record<string, ComponentInstance>>;
|
|
49
|
+
render: {
|
|
50
|
+
theme: ThemeDefinition;
|
|
51
|
+
context: ThemeRenderContext;
|
|
52
|
+
storefront: StoreClient;
|
|
53
|
+
reads: ReadCache;
|
|
54
|
+
onIssue: ((issue: RendererIssue) => void) | undefined;
|
|
55
|
+
preview: boolean;
|
|
56
|
+
depth: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether the component renders in an editor's preview. A section with
|
|
61
|
+
* nothing to show yet — no products picked, no text written — draws a
|
|
62
|
+
* placeholder there, so a merchant can see and select it; a shopper's page
|
|
63
|
+
* never shows one.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isPreview(blocks: BlocksHandle | undefined): boolean;
|
|
66
|
+
/** Whether a slot holds a block that renders (not hidden), so a component can leave out an empty wrapper. */
|
|
67
|
+
export declare function hasBlocks(blocks: BlocksHandle | undefined, slot?: string): boolean;
|
|
20
68
|
/** One renderer-specific degradation, reported through `onIssue`. */
|
|
21
69
|
export interface RendererIssue {
|
|
22
70
|
code: PageDataIssueCode | "missing-component" | "load-failed" | "multiple-slots-unsupported";
|