@akinon/pz-theme 2.0.162-rc.0 → 2.0.163-beta.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 +15 -50
- package/package.json +4 -4
- package/readme.md +22 -0
- package/src/__tests__/custom-catalog-announce.test.ts +110 -0
- package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
- package/src/blocks/__tests__/text-block.test.tsx +6 -34
- package/src/blocks/order-detail-block.tsx +95 -113
- package/src/blocks/text-block.tsx +2 -6
- package/src/chrome/composition-source.ts +7 -2
- package/src/chrome/preview-snapshot-source.ts +5 -1
- package/src/content-capabilities.ts +41 -0
- package/src/custom/README.md +219 -100
- package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
- package/src/custom/collect.ts +9 -4
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +5 -21
- package/src/editor-locale-bridge.tsx +11 -39
- package/src/load-placeholder-sections.ts +13 -3
- package/src/placeholder-registry.ts +42 -1
- package/src/region/__tests__/editor-handshake.test.ts +144 -0
- package/src/region/__tests__/region-contract.test.ts +500 -0
- package/src/region/__tests__/region-placeholder-readers.test.ts +431 -0
- package/src/region/__tests__/region-source.test.ts +621 -0
- package/src/region/editor-handshake.ts +60 -0
- package/src/region/region-contract.ts +184 -0
- package/src/region/region-source.ts +360 -0
- package/src/theme-placeholder-wrapper.tsx +0 -3
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
- package/src/utils/sanitize-rich-text.ts +0 -215
|
@@ -3,6 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { CompositionState, readCompositionState } from './composition-state';
|
|
4
4
|
import { getPlaceholderPreviewSnapshot } from './preview-snapshot-source';
|
|
5
5
|
import { getWidgetDataWithRetry } from './retrying-widget-data';
|
|
6
|
+
import { readMarketPlaceholderCopy } from '../region/region-source'; // ZERO-5391
|
|
6
7
|
|
|
7
8
|
// `React.cache` exists on the RSC-capable React this package renders under;
|
|
8
9
|
// fall back to the raw function on older cores, where correctness is identical
|
|
@@ -20,9 +21,13 @@ export type PlaceholderWidget = {
|
|
|
20
21
|
const fetchPlaceholderWidget = async (
|
|
21
22
|
slug: string
|
|
22
23
|
): Promise<PlaceholderWidget> =>
|
|
23
|
-
|
|
24
|
+
// ZERO-5391: a placeholder the request's region has its own copy of reads
|
|
25
|
+
// that copy; anything else, and a copy that does not answer, reads the
|
|
26
|
+
// shared default exactly as before.
|
|
27
|
+
((await readMarketPlaceholderCopy(slug)) as PlaceholderWidget) ??
|
|
28
|
+
((await getWidgetDataWithRetry<{
|
|
24
29
|
attributes?: { theme_editor_placeholder?: unknown };
|
|
25
|
-
}>(slug)) as PlaceholderWidget;
|
|
30
|
+
}>(slug)) as PlaceholderWidget);
|
|
26
31
|
|
|
27
32
|
/**
|
|
28
33
|
* The one fetch of a placeholder widget per request.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import * as serverData from '@akinon/next/data/server';
|
|
3
|
+
import { resolveRegionSlug } from '../region/region-source'; // ZERO-5391
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Whole-placeholder snapshot the theme editor writes for a named preview
|
|
@@ -31,7 +32,10 @@ const fetchPreviewSnapshot = async (
|
|
|
31
32
|
if (!fn) return null;
|
|
32
33
|
|
|
33
34
|
try {
|
|
34
|
-
|
|
35
|
+
// ZERO-5391: a placeholder the request's region customised previews from
|
|
36
|
+
// that region's own blob; without one the loader falls through to the
|
|
37
|
+
// region's LIVE copy (getPlaceholderWidget), never to the default's blob.
|
|
38
|
+
return (await fn({ slug: await resolveRegionSlug(slug) })) ?? null;
|
|
35
39
|
} catch {
|
|
36
40
|
// A failed preview read must fall back to the live render, never blank it.
|
|
37
41
|
return null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { REGION_CONTENT_CAPABILITY } from './region/region-contract';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What content this build can serve differently by AUDIENCE, announced to the
|
|
5
|
+
* editor (ZERO-5391).
|
|
6
|
+
*
|
|
7
|
+
* The third sibling of `action-capabilities.ts` and `signal-capabilities.ts`.
|
|
8
|
+
* Those say what a stored action will do and which moments the renderer can
|
|
9
|
+
* report; this one says whether the storefront can tell its visitors apart
|
|
10
|
+
* when it picks the content a placeholder renders.
|
|
11
|
+
*
|
|
12
|
+
* - `region-content` a placeholder renders from its country copy
|
|
13
|
+
* (`<slug>-region-<cc>`; in draft mode that copy's
|
|
14
|
+
* `-preview-<id>` blob first) for a request whose locale
|
|
15
|
+
* names the country (`en-ae` → `ae`), whenever the
|
|
16
|
+
* `theme-regions` registry lists the placeholder for it
|
|
17
|
+
* (`region/region-source.ts`). The editor offers its
|
|
18
|
+
* country picker, and with it every write to a
|
|
19
|
+
* `-region-` slug, only to a storefront that says so: on
|
|
20
|
+
* an older theme those writes would save and render
|
|
21
|
+
* nothing.
|
|
22
|
+
*
|
|
23
|
+
* Names rather than a version number, for the reason the action capabilities
|
|
24
|
+
* are: the editor must not have to know which release added what. A build that
|
|
25
|
+
* does not send the list is read as serving one content to every visitor,
|
|
26
|
+
* which is exactly what every renderer shipped before this one did.
|
|
27
|
+
*
|
|
28
|
+
* Unlike its siblings this list does not spell its string out. The name lives
|
|
29
|
+
* in the region contract (`region/region-contract.ts`), whose copy in the
|
|
30
|
+
* editor is held byte-identical to it by a parity test on both sides, so the
|
|
31
|
+
* two can only be renamed together — there and never here.
|
|
32
|
+
*
|
|
33
|
+
* Its own module rather than a constant beside the reader that implements it:
|
|
34
|
+
* the placeholder registry announces this from the client, and it must not
|
|
35
|
+
* have to pull the server-only region reader in to say one list of strings.
|
|
36
|
+
*/
|
|
37
|
+
export type ContentCapability = typeof REGION_CONTENT_CAPABILITY;
|
|
38
|
+
|
|
39
|
+
export const CONTENT_CAPABILITIES: readonly ContentCapability[] = [
|
|
40
|
+
REGION_CONTENT_CAPABILITY
|
|
41
|
+
];
|
package/src/custom/README.md
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
# Custom blocks
|
|
2
2
|
|
|
3
|
-
A brand developer writes a React component in their own repo and decides, at the
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
A brand developer writes a React component in their own repo and decides, at the
|
|
4
|
+
point of use, what a merchant may change about it. The component appears in the
|
|
5
|
+
theme editor's Add Block picker under **Brand blocks**, its declared fields are
|
|
6
|
+
editable from the properties panel, and it can open a region the merchant fills
|
|
7
|
+
with ordinary editor content.
|
|
8
|
+
|
|
9
|
+
Nothing here is a schema kept alongside the component. The declaration *is* the
|
|
10
|
+
usage.
|
|
11
|
+
|
|
12
|
+
**Writing a whole area rather than one element inside it?** See
|
|
13
|
+
[`README-sections.md`](./README-sections.md). A custom *section* owns its own
|
|
14
|
+
band on the page: it gets the section root panel (geometry and content width),
|
|
15
|
+
and it names the children that go inside it rather than opening a hole. A custom
|
|
16
|
+
*block* is the right shape for one element the merchant drops into a band they
|
|
17
|
+
composed.
|
|
18
|
+
|
|
19
|
+
**Want a complete, commented example?** See `examples/header/` at the root of
|
|
20
|
+
the pz-next repository: one header written at six levels, from basic to
|
|
21
|
+
advanced, each building on the previous one. They are reference only — not
|
|
22
|
+
shipped in the npm package and never imported by an app.
|
|
10
23
|
|
|
11
24
|
## The shape
|
|
12
25
|
|
|
13
|
-
A "Complete the look" row for a product page — the merchant retypes its
|
|
26
|
+
A "Complete the look" row for a product page — the merchant retypes its
|
|
27
|
+
heading, chooses how many products it shows, drops a campaign banner beside it,
|
|
28
|
+
and restyles the product cards. One block, all four mechanisms.
|
|
14
29
|
|
|
15
30
|
```tsx
|
|
16
31
|
// src/theme-blocks/complete-the-look.tsx
|
|
@@ -58,28 +73,19 @@ function CompleteTheLook() {
|
|
|
58
73
|
export default defineCustomBlock(CompleteTheLook, {
|
|
59
74
|
label: 'Complete the look',
|
|
60
75
|
settings: [
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
defaultValue: 4,
|
|
66
|
-
min: 2,
|
|
67
|
-
max: 8
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
key: 'heading',
|
|
71
|
-
type: 'text',
|
|
72
|
-
label: 'Heading',
|
|
73
|
-
defaultValue: 'Complete the look',
|
|
74
|
-
editable: true
|
|
75
|
-
}
|
|
76
|
+
{ key: 'productCount', type: 'slider', label: 'How many products',
|
|
77
|
+
defaultValue: 4, min: 2, max: 8 },
|
|
78
|
+
{ key: 'heading', type: 'text', label: 'Heading',
|
|
79
|
+
defaultValue: 'Complete the look', editable: true }
|
|
76
80
|
],
|
|
77
81
|
slots: [{ name: 'campaign', label: 'Campaign banner' }],
|
|
78
82
|
parts: { card: { states: ['hover'] }, price: {} }
|
|
79
83
|
});
|
|
80
84
|
```
|
|
81
85
|
|
|
82
|
-
The product data is the developer's business — it is fetched in the component
|
|
86
|
+
The product data is the developer's business — it is fetched in the component
|
|
87
|
+
and the merchant never sees it as a control. What the merchant gets is the
|
|
88
|
+
heading, the count, the banner region and the card styling.
|
|
83
89
|
|
|
84
90
|
```ts
|
|
85
91
|
// src/theme-blocks/index.ts
|
|
@@ -95,32 +101,51 @@ import './configure-page-context';
|
|
|
95
101
|
import '../../theme-blocks';
|
|
96
102
|
```
|
|
97
103
|
|
|
98
|
-
The block name lives in the record passed to `registerCustomBlocks`, never in
|
|
104
|
+
The block name lives in the record passed to `registerCustomBlocks`, never in
|
|
105
|
+
`Component.name`: a production build minifies function names, so a block
|
|
106
|
+
authored as `CompleteTheLook` would register as `t` on the storefront and as
|
|
107
|
+
`CompleteTheLook` in dev, and one composition would render in one build and be an
|
|
108
|
+
unknown type in the other.
|
|
99
109
|
|
|
100
110
|
## Import it from the client root only
|
|
101
111
|
|
|
102
|
-
Not from the server layout as well. `registerCustomBlocks` is a client module
|
|
112
|
+
Not from the server layout as well. `registerCustomBlocks` is a client module
|
|
113
|
+
because it reaches the renderer registry, which imports every block module — and
|
|
114
|
+
most of those call hooks without a directive of their own, because the only path
|
|
115
|
+
that has ever reached them is a client one. Importing it from a server layout
|
|
116
|
+
fails the brand's build outright.
|
|
103
117
|
|
|
104
|
-
Client-only registration loses nothing: `ThemeBlock` is the single production
|
|
118
|
+
Client-only registration loses nothing: `ThemeBlock` is the single production
|
|
119
|
+
reader of the registry and is itself a client module, the server-side CSS
|
|
120
|
+
generator never consults it, and SSR still resolves custom renderers because
|
|
121
|
+
Next renders client components on the server through the client graph.
|
|
105
122
|
|
|
106
|
-
Use the deep path `@akinon/pz-theme/src/custom`, not the package root — the root
|
|
123
|
+
Use the deep path `@akinon/pz-theme/src/custom`, not the package root — the root
|
|
124
|
+
also re-exports `ThemePlaceholder`, which imports `server-only`.
|
|
107
125
|
|
|
108
126
|
## `ThemeEditable`
|
|
109
127
|
|
|
110
128
|
Makes one wrapped element merchant-editable. The wrapped tag decides the control:
|
|
111
129
|
|
|
112
130
|
| Wrapped element | Control |
|
|
113
|
-
|
|
131
|
+
|---|---|
|
|
114
132
|
| `<img>` | image picker |
|
|
115
133
|
| `<h1>`–`<h6>`, `<p>`, `<span>`, `<strong>` | text box |
|
|
116
134
|
| `<a>` | two controls — link text and href |
|
|
117
135
|
| `<video>` | file picker |
|
|
118
136
|
|
|
119
|
-
The authored JSX is the default value: `<ThemeEditable name="heading"><h2>Complete
|
|
137
|
+
The authored JSX is the default value: `<ThemeEditable name="heading"><h2>Complete
|
|
138
|
+
the look</h2></ThemeEditable>` declares a text field whose default is
|
|
139
|
+
`Complete the look`, because there is nowhere else for a developer to state it.
|
|
120
140
|
|
|
121
|
-
At render the merchant's value is applied by re-rendering the child through
|
|
141
|
+
At render the merchant's value is applied by re-rendering the child through
|
|
142
|
+
React — never by writing to the DOM. Three shapes are refused rather than
|
|
143
|
+
guessed, and the message says which: a custom component instead of a host
|
|
144
|
+
element, a bare expression with no element, and an ambiguous multi-child
|
|
145
|
+
element. `type=` is the explicit escape.
|
|
122
146
|
|
|
123
|
-
The authored text is written in the store's default language. Its other
|
|
147
|
+
The authored text is written in the store's default language. Its other
|
|
148
|
+
languages go in `translations`:
|
|
124
149
|
|
|
125
150
|
```tsx
|
|
126
151
|
<ThemeEditable name="label" translations={{ en: 'MENU' }}>
|
|
@@ -128,33 +153,63 @@ The authored text is written in the store's default language. Its other language
|
|
|
128
153
|
</ThemeEditable>
|
|
129
154
|
```
|
|
130
155
|
|
|
131
|
-
The editor is told `MENÜ` only. An English page shows `MENU` for as long as
|
|
156
|
+
The editor is told `MENÜ` only. An English page shows `MENU` for as long as
|
|
157
|
+
the field still says `MENÜ`, including the copy the editor seeds when the
|
|
158
|
+
block is placed. Once the merchant types their own text, it shows as typed.
|
|
159
|
+
This is the same rule as a per-language `useThemeSetting` default (below); a
|
|
160
|
+
setting read for the same key, such as an accessible name, can share one
|
|
161
|
+
constant with it.
|
|
132
162
|
|
|
133
163
|
## Links and the page's language
|
|
134
164
|
|
|
135
|
-
Storefront paths are stored and authored without a locale: `/yatak-odasi/`,
|
|
165
|
+
Storefront paths are stored and authored without a locale: `/yatak-odasi/`,
|
|
166
|
+
never `/en/yatak-odasi/`. On an English page that bare path is the Turkish
|
|
167
|
+
site, so a link that goes out as written takes the shopper out of English. A
|
|
168
|
+
visible `next/link` also prefetches it, which makes the middleware set
|
|
169
|
+
`pz-locale=tr`, and every request after that comes back Turkish. You do not
|
|
170
|
+
write the locale yourself; there are three places it goes on for you:
|
|
136
171
|
|
|
137
172
|
| What you render | What to use |
|
|
138
|
-
|
|
173
|
+
|---|---|
|
|
139
174
|
| The `<a>` that `ThemeEditable` wraps | Nothing: the locale is added to its href, whether the merchant set it or you did |
|
|
140
175
|
| Any other link | `ThemeLink` in place of `next/link`, with the same props |
|
|
141
176
|
| A bare string: a plain `<a>`, `router.push`, a url read off commerce data | `const localize = useLocalizeHref();` then `localize(url)` |
|
|
142
177
|
|
|
143
|
-
`ThemeLink` prefetches nothing unless you pass `prefetch`. Next otherwise
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
178
|
+
`ThemeLink` prefetches nothing unless you pass `prefetch`. Next otherwise
|
|
179
|
+
fetches a link's page the moment it scrolls into view, and because the locale
|
|
180
|
+
lives in a cookie that any request to an unprefixed path rewrites, one link
|
|
181
|
+
that slips past these rules would switch the whole session's language without
|
|
182
|
+
anyone clicking it.
|
|
183
|
+
|
|
184
|
+
All three come from `@akinon/pz-theme/src/custom`. An `<a>` nested deeper
|
|
185
|
+
inside a `ThemeEditable` is not touched — that one is a link of yours, so it
|
|
186
|
+
takes the row it belongs to above.
|
|
187
|
+
|
|
188
|
+
A full URL to this same storefront counts as a path, `www.` or not: merchants
|
|
189
|
+
paste the address bar, and `https://www.zsazsazsu.com.tr/yeni-urunler/` on an
|
|
190
|
+
English page becomes `/en/yeni-urunler/` — which also lets the router take it
|
|
191
|
+
without a page load. Addresses with no localized copy stay as they are: `/api`
|
|
192
|
+
and `/_next`, and anything ending in a file (`/files/kvkk.pdf`,
|
|
193
|
+
`/manifest.json`). So do external, `#hash`, `?query` and empty hrefs, and the
|
|
194
|
+
OAuth `/{provider}/login/` routes.
|
|
195
|
+
|
|
196
|
+
No locale is added when the URL strategy hides the page's own: the default
|
|
197
|
+
locale under `hide-default-locale`, every locale under `subdomain` and
|
|
198
|
+
`hide-all-locales`. To compare an address with the page's own, localize the
|
|
199
|
+
stored side rather than stripping the locale off the current one:
|
|
200
|
+
`localize(node.url) === pathname`.
|
|
150
201
|
|
|
151
202
|
## `useThemeSetting`
|
|
152
203
|
|
|
153
|
-
For values with no on-screen representation — how many products to fetch, which
|
|
204
|
+
For values with no on-screen representation — how many products to fetch, which
|
|
205
|
+
category, whether a carousel auto-advances, which side an image sits on. Without
|
|
206
|
+
it every variation becomes a separate block in the picker.
|
|
154
207
|
|
|
155
|
-
React's own rule that hooks are called unconditionally is doing useful work
|
|
208
|
+
React's own rule that hooks are called unconditionally is doing useful work
|
|
209
|
+
here: the language enforces that the declaration set is static.
|
|
156
210
|
|
|
157
|
-
A `text` default can be written per language, and the hook still returns one
|
|
211
|
+
A `text` default can be written per language, and the hook still returns one
|
|
212
|
+
string, the entry for the page's locale:
|
|
158
213
|
|
|
159
214
|
```tsx
|
|
160
215
|
const label = useThemeSetting('label', {
|
|
@@ -164,59 +219,89 @@ const label = useThemeSetting('label', {
|
|
|
164
219
|
});
|
|
165
220
|
```
|
|
166
221
|
|
|
167
|
-
The editor is only told the default language's entry, so a static `settings`
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<div dangerouslySetInnerHTML={{ __html: html }} />;
|
|
179
|
-
```
|
|
222
|
+
The editor is only told the default language's entry, so a static `settings`
|
|
223
|
+
declaration keeps that one plain string (`defaultValue: 'MENÜ'`). The
|
|
224
|
+
translation applies for as long as the stored value is still that default,
|
|
225
|
+
including the copy the editor seeds when the block is placed. Once the
|
|
226
|
+
merchant types their own text, it shows as typed in every language. A text
|
|
227
|
+
the merchant can translate themselves needs the `localetext` control, which a
|
|
228
|
+
custom setting cannot declare yet: this package refuses the type (see
|
|
229
|
+
`collect.ts`) because the editor stores such a value as a locale map with no
|
|
230
|
+
breakpoint key, and the editor refuses it in a custom catalog for the same
|
|
231
|
+
reason.
|
|
180
232
|
|
|
181
233
|
## `ThemeSlot`
|
|
182
234
|
|
|
183
|
-
A merchant-fillable region. A block may open several, and each is filled
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
235
|
+
A merchant-fillable region. A block may open several, and each is filled
|
|
236
|
+
independently.
|
|
237
|
+
|
|
238
|
+
**Each slot is a real child node.** A declared slot materializes as one
|
|
239
|
+
reserved-type child of the custom block (`custom-slot`, carrying its name in
|
|
240
|
+
`properties.slotName`), and `<ThemeSlot name="campaign" />` renders THAT node's
|
|
241
|
+
children. That is what makes several slots possible without touching how
|
|
242
|
+
insertion is addressed: the editor already inserts at `(placeholderId,
|
|
243
|
+
sectionId, parentBlockId, index)`, and a slot that is a node is simply the
|
|
244
|
+
parent. A block declaring three slots gets three rows in the layer tree.
|
|
245
|
+
|
|
246
|
+
`<ThemeSlot />` with no name opens the block's single slot whatever it is
|
|
247
|
+
called, so every block written against the original single-slot API keeps
|
|
248
|
+
working unchanged. A block declaring several NAMED slots has nothing for a bare
|
|
249
|
+
slot to resolve to, and it is refused with the names listed. Two
|
|
250
|
+
`<ThemeSlot name="campaign" />` in one component are a real duplicate — both resolve
|
|
251
|
+
to the same node — so the second is refused: its children would render twice and
|
|
252
|
+
two wrappers would answer to one block id.
|
|
253
|
+
|
|
254
|
+
A slot whose node does not exist yet is not an error: it draws the empty band in
|
|
255
|
+
the designer and renders nothing live. The editor creates the nodes when the
|
|
256
|
+
block is inserted and again on every theme load, so a block already sitting in a
|
|
257
|
+
composition gets the rows for a slot the component gained later — insert alone
|
|
258
|
+
would leave those regions unreachable, because the merchant's only route into a
|
|
259
|
+
region is the add-block affordance on its tree row.
|
|
260
|
+
|
|
261
|
+
Children sitting directly on the block (the pre-node shape) are the unnamed
|
|
262
|
+
slot's, and only the unnamed slot's. The node wins that lookup once it exists,
|
|
263
|
+
so the editor MOVES those children into the unnamed node when it creates it
|
|
264
|
+
rather than appending the node beside them — appending would leave live content
|
|
265
|
+
saved, listed in the tree, and drawn nowhere. A block whose type declares only
|
|
266
|
+
named slots has nothing to move them into, so they stay where they are and stop
|
|
267
|
+
rendering, which is what changing the component means.
|
|
192
268
|
|
|
193
269
|
### Narrowing what a slot takes
|
|
194
270
|
|
|
195
271
|
```ts
|
|
196
272
|
slots: [
|
|
197
|
-
{
|
|
198
|
-
name: 'campaign',
|
|
199
|
-
label: 'Campaign banner',
|
|
200
|
-
accepts: ['image', 'text'],
|
|
201
|
-
max: 2
|
|
202
|
-
},
|
|
273
|
+
{ name: 'campaign', label: 'Campaign banner', accepts: ['image', 'text'], max: 2 },
|
|
203
274
|
{ name: 'sizeGuide', label: 'Size guide' }
|
|
204
|
-
]
|
|
275
|
+
]
|
|
205
276
|
```
|
|
206
277
|
|
|
207
|
-
`accepts` limits the block types the merchant may put in the region and `max`
|
|
278
|
+
`accepts` limits the block types the merchant may put in the region and `max`
|
|
279
|
+
limits how many; omit either for "anything" and "as many as you like". The
|
|
280
|
+
editor enforces both where a block is WRITTEN into the composition, not only
|
|
281
|
+
where it is offered, so a drag from elsewhere in the tree obeys them too.
|
|
208
282
|
|
|
209
|
-
Neither is read by pz-theme — the renderer draws whatever the composition holds
|
|
283
|
+
Neither is read by pz-theme — the renderer draws whatever the composition holds
|
|
284
|
+
— but both are checked at declare time, because they are refused on arrival and
|
|
285
|
+
the editor refuses the WHOLE catalog rather than one slot. `accepts: []` (a
|
|
286
|
+
region nothing could ever go in) and a `max` that is not a positive whole number
|
|
287
|
+
would otherwise empty a brand's entire palette, with the only explanation in a
|
|
288
|
+
repo the developer is not working in.
|
|
210
289
|
|
|
211
290
|
## Detecting the editor canvas
|
|
212
291
|
|
|
213
|
-
`useCustomBlockContext()` returns the context the block or section renders in,
|
|
292
|
+
`useCustomBlockContext()` returns the context the block or section renders in,
|
|
293
|
+
or `null` outside one. Its `isDesigner` is what to branch on when the canvas
|
|
294
|
+
needs something the live site must not do — keep a menu open so the merchant
|
|
295
|
+
can style it, or stop a link from navigating on click.
|
|
214
296
|
|
|
215
297
|
```ts
|
|
216
298
|
const isDesigner = useCustomBlockContext()?.isDesigner ?? false;
|
|
217
299
|
```
|
|
218
300
|
|
|
219
|
-
`isDesigner` means "the editing affordances are on", not "the editor is
|
|
301
|
+
`isDesigner` means "the editing affordances are on", not "the editor is
|
|
302
|
+
attached". The editor's **Use preview** toggle turns it off while the editor
|
|
303
|
+
stays open, so the canvas behaves like the live site: whatever you held open
|
|
304
|
+
closes and links navigate again.
|
|
220
305
|
|
|
221
306
|
## Reading the row a block is placed in
|
|
222
307
|
|
|
@@ -240,7 +325,11 @@ package root is not importable from a client module.
|
|
|
240
325
|
|
|
241
326
|
## Reporting what happened
|
|
242
327
|
|
|
243
|
-
A custom unit can report a moment — a strip was seen, a lookbook was opened, a
|
|
328
|
+
A custom unit can report a moment — a strip was seen, a lookbook was opened, a
|
|
329
|
+
tab was changed — and the brand decides in its own repo what that moment means
|
|
330
|
+
to GA4, Insider or its own service. The theme never names an analytics event and
|
|
331
|
+
never pushes to a tag manager, which is why one renderer can serve brands whose
|
|
332
|
+
schemas disagree.
|
|
244
333
|
|
|
245
334
|
```tsx
|
|
246
335
|
import { useThemeSignal } from '@akinon/pz-theme/src/custom';
|
|
@@ -258,7 +347,9 @@ const LookbookStrip = () => {
|
|
|
258
347
|
};
|
|
259
348
|
```
|
|
260
349
|
|
|
261
|
-
For the "first time it becomes true" case — a section seen, a promo shown — use
|
|
350
|
+
For the "first time it becomes true" case — a section seen, a promo shown — use
|
|
351
|
+
the latching form. A `null` key means "not ready yet" and reports nothing, which
|
|
352
|
+
is what stops a unit reporting before the data it describes has resolved:
|
|
262
353
|
|
|
263
354
|
```tsx
|
|
264
355
|
useThemeSignalOnce({
|
|
@@ -268,7 +359,8 @@ useThemeSignalOnce({
|
|
|
268
359
|
});
|
|
269
360
|
```
|
|
270
361
|
|
|
271
|
-
Outside React — a module-level bus, a delegated document listener — call the
|
|
362
|
+
Outside React — a module-level bus, a delegated document listener — call the
|
|
363
|
+
primitive directly:
|
|
272
364
|
|
|
273
365
|
```ts
|
|
274
366
|
import { emitThemeSignal } from '@akinon/pz-theme/src/custom';
|
|
@@ -293,42 +385,69 @@ useEffect(
|
|
|
293
385
|
|
|
294
386
|
Four things the emitter does so a call site does not have to:
|
|
295
387
|
|
|
296
|
-
- **It is silent while the editor is attached, browse mode included.** It reads
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
388
|
+
- **It is silent while the editor is attached, browse mode included.** It reads
|
|
389
|
+
the editor handshake flag itself, so do NOT wrap a report in an `isDesigner`
|
|
390
|
+
check — that prop means "the editing affordances are on", goes false under
|
|
391
|
+
**Use preview**, and a report guarded on it reports a designer's clicks as a
|
|
392
|
+
shopper's.
|
|
393
|
+
- **`{ key, once: true }` is the latch**, reset on client-side navigation. A
|
|
394
|
+
repeat inside the same page view is a no-op.
|
|
395
|
+
- **A ceiling** — 24 per key, 200 per page view. An autoplaying carousel would
|
|
396
|
+
otherwise emit continuously, billed to whatever the brand forwards to.
|
|
397
|
+
- **The payload is closed to flat scalars** and sanitized at emit. A product
|
|
398
|
+
object, a DOM node or shopper-typed text is dropped with a dev warning, which
|
|
399
|
+
is how the no-PII rule is enforced rather than reviewed.
|
|
400
|
+
|
|
401
|
+
A brand's own moment must be namespaced `custom.<something>`, so it can never
|
|
402
|
+
collide with a name this package adds later and a bridge can tell the two apart.
|
|
403
|
+
The package's own names are listed in `THEME_SIGNAL_NAMES`.
|
|
302
404
|
|
|
303
405
|
## Styling parts
|
|
304
406
|
|
|
305
|
-
The `field-<key>` parts come free from `ThemeEditable`. Everything else a block
|
|
407
|
+
The `field-<key>` parts come free from `ThemeEditable`. Everything else a block
|
|
408
|
+
draws is offered to the properties panel only if it is DECLARED:
|
|
306
409
|
|
|
307
410
|
```ts
|
|
308
|
-
parts: ['row', 'tile']
|
|
309
|
-
parts: {
|
|
310
|
-
row: {
|
|
311
|
-
states: ['hover', 'open'];
|
|
312
|
-
}
|
|
313
|
-
} // when appearance depends on state
|
|
411
|
+
parts: ['row', 'tile'] // the cheap form
|
|
412
|
+
parts: { row: { states: ['hover', 'open'] } } // when appearance depends on state
|
|
314
413
|
```
|
|
315
414
|
|
|
316
|
-
Declare the same name you stamp with `partAttrs('row')`, or the panel offers a
|
|
415
|
+
Declare the same name you stamp with `partAttrs('row')`, or the panel offers a
|
|
416
|
+
control nothing on the page answers to. Only states the renderer can actually
|
|
417
|
+
match are accepted (`PART_STYLE_STATES` in `utils/part-styles.ts`) — a state
|
|
418
|
+
with no selector emits CSS that can never apply, so it is refused with the
|
|
419
|
+
available list. The `field-` prefix is reserved and cannot be declared.
|
|
317
420
|
|
|
318
421
|
## Declare `settings` and `slots`
|
|
319
422
|
|
|
320
|
-
Runtime collection exists as a fallback, but it only runs on a type's first
|
|
423
|
+
Runtime collection exists as a fallback, but it only runs on a type's first
|
|
424
|
+
render and never re-announces — so a block that declares nothing reaches the
|
|
425
|
+
editor with an empty panel until the editor is reloaded. Declare them.
|
|
321
426
|
|
|
322
427
|
## What the merchant still cannot restyle
|
|
323
428
|
|
|
324
|
-
A field's own `field-<key>` part carries no states: declaring one is refused,
|
|
429
|
+
A field's own `field-<key>` part carries no states: declaring one is refused,
|
|
430
|
+
because the `field-` prefix belongs to `ThemeEditable`. To give a field element
|
|
431
|
+
a hover appearance, wrap it in an element you stamp and declare yourself.
|
|
325
432
|
|
|
326
433
|
## Limits
|
|
327
434
|
|
|
328
|
-
Forty types per build, twenty-four settings, eight slots and thirty-two parts
|
|
435
|
+
Forty types per build, twenty-four settings, eight slots and thirty-two parts
|
|
436
|
+
per type, and a 256 KB catalog. The whole catalog is posted to the editor on
|
|
437
|
+
every placeholder announce, which is what the caps are protecting.
|
|
329
438
|
|
|
330
|
-
Every one of them is refused at declare time, because the editor's answer to an
|
|
439
|
+
Every one of them is refused at declare time, because the editor's answer to an
|
|
440
|
+
over-cap catalog is to drop ALL of it — a single block over the parts cap costs
|
|
441
|
+
the build every block in the palette. The one cap a declaration cannot fully
|
|
442
|
+
guarantee is the parts cap: an undeclared `ThemeEditable` discovered at render
|
|
443
|
+
time adds a `field-<key>` part after registration counted them, and if that
|
|
444
|
+
would cross the cap the part is dropped with a warning rather than the catalog
|
|
445
|
+
being put over. The field keeps its control in the Content tab and loses only
|
|
446
|
+
its styling group.
|
|
331
447
|
|
|
332
448
|
## Failure behaviour
|
|
333
449
|
|
|
334
|
-
A composition naming a type this build does not register renders nothing on the
|
|
450
|
+
A composition naming a type this build does not register renders nothing on the
|
|
451
|
+
live site and a labelled card in the designer, and the editor shows the block as
|
|
452
|
+
an orphan row rather than dropping it — its stored properties survive a save
|
|
453
|
+
untouched, so a type that comes back finds its content where it left it.
|