@akinon/pz-theme 2.0.71-beta.0 → 2.0.71
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 +7 -4
- package/package.json +4 -4
- package/src/__tests__/custom-catalog-announce.test.ts +276 -0
- package/src/blocks/group-block.tsx +9 -57
- package/src/blocks/render-composed-children.tsx +104 -0
- package/src/custom/README.md +249 -0
- package/src/custom/__tests__/define.test.tsx +619 -0
- package/src/custom/__tests__/module-boundaries.test.ts +156 -0
- package/src/custom/__tests__/public-api.test.tsx +427 -0
- package/src/custom/__tests__/slot-node-wire.test.tsx +309 -0
- package/src/custom/__tests__/slot-nodes.test.ts +144 -0
- package/src/custom/__tests__/theme-editable.test.tsx +486 -0
- package/src/custom/__tests__/theme-slot.test.tsx +387 -0
- package/src/custom/__tests__/use-theme-setting.test.tsx +260 -0
- package/src/custom/block-context.ts +64 -0
- package/src/custom/block-host.tsx +162 -0
- package/src/custom/catalog.ts +97 -0
- package/src/custom/collect.ts +342 -0
- package/src/custom/define.tsx +172 -0
- package/src/custom/editable-tags.ts +120 -0
- package/src/custom/index.ts +105 -0
- package/src/custom/parts.ts +108 -0
- package/src/custom/property-types.ts +70 -0
- package/src/custom/register.ts +545 -0
- package/src/custom/slot-nodes.ts +119 -0
- package/src/custom/theme-editable.tsx +342 -0
- package/src/custom/theme-slot.tsx +210 -0
- package/src/custom/types.ts +251 -0
- package/src/custom/use-theme-setting.ts +225 -0
- package/src/index.ts +66 -0
- package/src/placeholder-registry.ts +168 -16
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Custom blocks
|
|
2
|
+
|
|
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
|
+
## The shape
|
|
13
|
+
|
|
14
|
+
A "Complete the look" row for a product page — the merchant retypes its
|
|
15
|
+
heading, chooses how many products it shows, drops a campaign banner beside it,
|
|
16
|
+
and restyles the product cards. One block, all four mechanisms.
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
// src/theme-blocks/complete-the-look.tsx
|
|
20
|
+
'use client';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
defineCustomBlock,
|
|
24
|
+
partAttrs,
|
|
25
|
+
ThemeEditable,
|
|
26
|
+
ThemeSlot,
|
|
27
|
+
useThemeSetting
|
|
28
|
+
} from '@akinon/pz-theme/src/custom';
|
|
29
|
+
|
|
30
|
+
function CompleteTheLook() {
|
|
31
|
+
const productCount = useThemeSetting('productCount', {
|
|
32
|
+
type: 'slider',
|
|
33
|
+
label: 'How many products',
|
|
34
|
+
default: 4,
|
|
35
|
+
min: 2,
|
|
36
|
+
max: 8
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const products = useRecommendations(productCount);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<section>
|
|
43
|
+
<ThemeEditable name="heading">
|
|
44
|
+
<h2>Complete the look</h2>
|
|
45
|
+
</ThemeEditable>
|
|
46
|
+
|
|
47
|
+
<div>
|
|
48
|
+
{products.map((product) => (
|
|
49
|
+
<article key={product.pk} {...partAttrs('card')}>
|
|
50
|
+
<img src={product.image} alt="" />
|
|
51
|
+
<span {...partAttrs('price')}>{product.price}</span>
|
|
52
|
+
</article>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<ThemeSlot name="campaign" label="Campaign banner" />
|
|
57
|
+
</section>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default defineCustomBlock(CompleteTheLook, {
|
|
62
|
+
label: 'Complete the look',
|
|
63
|
+
settings: [
|
|
64
|
+
{ key: 'productCount', type: 'slider', label: 'How many products',
|
|
65
|
+
defaultValue: 4, min: 2, max: 8 },
|
|
66
|
+
{ key: 'heading', type: 'text', label: 'Heading',
|
|
67
|
+
defaultValue: 'Complete the look', editable: true }
|
|
68
|
+
],
|
|
69
|
+
slots: [{ name: 'campaign', label: 'Campaign banner' }],
|
|
70
|
+
parts: { card: { states: ['hover'] }, price: {} }
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The product data is the developer's business — it is fetched in the component
|
|
75
|
+
and the merchant never sees it as a control. What the merchant gets is the
|
|
76
|
+
heading, the count, the banner region and the card styling.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/theme-blocks/index.ts
|
|
80
|
+
import { registerCustomBlocks } from '@akinon/pz-theme/src/custom';
|
|
81
|
+
import CompleteTheLook from './complete-the-look';
|
|
82
|
+
|
|
83
|
+
registerCustomBlocks('gant', { 'complete-the-look': CompleteTheLook });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// src/app/[pz]/client-root.tsx — CLIENT ROOT ONLY
|
|
88
|
+
import './configure-page-context';
|
|
89
|
+
import '../../theme-blocks';
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The block name lives in the record passed to `registerCustomBlocks`, never in
|
|
93
|
+
`Component.name`: a production build minifies function names, so a block
|
|
94
|
+
authored as `CompleteTheLook` would register as `t` on the storefront and as
|
|
95
|
+
`CompleteTheLook` in dev, and one composition would render in one build and be an
|
|
96
|
+
unknown type in the other.
|
|
97
|
+
|
|
98
|
+
## Import it from the client root only
|
|
99
|
+
|
|
100
|
+
Not from the server layout as well. `registerCustomBlocks` is a client module
|
|
101
|
+
because it reaches the renderer registry, which imports every block module — and
|
|
102
|
+
most of those call hooks without a directive of their own, because the only path
|
|
103
|
+
that has ever reached them is a client one. Importing it from a server layout
|
|
104
|
+
fails the brand's build outright.
|
|
105
|
+
|
|
106
|
+
Client-only registration loses nothing: `ThemeBlock` is the single production
|
|
107
|
+
reader of the registry and is itself a client module, the server-side CSS
|
|
108
|
+
generator never consults it, and SSR still resolves custom renderers because
|
|
109
|
+
Next renders client components on the server through the client graph.
|
|
110
|
+
|
|
111
|
+
Use the deep path `@akinon/pz-theme/src/custom`, not the package root — the root
|
|
112
|
+
also re-exports `ThemePlaceholder`, which imports `server-only`.
|
|
113
|
+
|
|
114
|
+
## `ThemeEditable`
|
|
115
|
+
|
|
116
|
+
Makes one wrapped element merchant-editable. The wrapped tag decides the control:
|
|
117
|
+
|
|
118
|
+
| Wrapped element | Control |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `<img>` | image picker |
|
|
121
|
+
| `<h1>`–`<h6>`, `<p>`, `<span>`, `<strong>` | text box |
|
|
122
|
+
| `<a>` | two controls — link text and href |
|
|
123
|
+
| `<video>` | file picker |
|
|
124
|
+
|
|
125
|
+
The authored JSX is the default value: `<ThemeEditable name="heading"><h2>Complete
|
|
126
|
+
the look</h2></ThemeEditable>` declares a text field whose default is
|
|
127
|
+
`Complete the look`, because there is nowhere else for a developer to state it.
|
|
128
|
+
|
|
129
|
+
At render the merchant's value is applied by re-rendering the child through
|
|
130
|
+
React — never by writing to the DOM. Three shapes are refused rather than
|
|
131
|
+
guessed, and the message says which: a custom component instead of a host
|
|
132
|
+
element, a bare expression with no element, and an ambiguous multi-child
|
|
133
|
+
element. `type=` is the explicit escape.
|
|
134
|
+
|
|
135
|
+
## `useThemeSetting`
|
|
136
|
+
|
|
137
|
+
For values with no on-screen representation — how many products to fetch, which
|
|
138
|
+
category, whether a carousel auto-advances, which side an image sits on. Without
|
|
139
|
+
it every variation becomes a separate block in the picker.
|
|
140
|
+
|
|
141
|
+
React's own rule that hooks are called unconditionally is doing useful work
|
|
142
|
+
here: the language enforces that the declaration set is static.
|
|
143
|
+
|
|
144
|
+
## `ThemeSlot`
|
|
145
|
+
|
|
146
|
+
A merchant-fillable region. A block may open several, and each is filled
|
|
147
|
+
independently.
|
|
148
|
+
|
|
149
|
+
**Each slot is a real child node.** A declared slot materializes as one
|
|
150
|
+
reserved-type child of the custom block (`custom-slot`, carrying its name in
|
|
151
|
+
`properties.slotName`), and `<ThemeSlot name="campaign" />` renders THAT node's
|
|
152
|
+
children. That is what makes several slots possible without touching how
|
|
153
|
+
insertion is addressed: the editor already inserts at `(placeholderId,
|
|
154
|
+
sectionId, parentBlockId, index)`, and a slot that is a node is simply the
|
|
155
|
+
parent. A block declaring three slots gets three rows in the layer tree.
|
|
156
|
+
|
|
157
|
+
`<ThemeSlot />` with no name opens the block's single slot whatever it is
|
|
158
|
+
called, so every block written against the original single-slot API keeps
|
|
159
|
+
working unchanged. A block declaring several NAMED slots has nothing for a bare
|
|
160
|
+
slot to resolve to, and it is refused with the names listed. Two
|
|
161
|
+
`<ThemeSlot name="campaign" />` in one component are a real duplicate — both resolve
|
|
162
|
+
to the same node — so the second is refused: its children would render twice and
|
|
163
|
+
two wrappers would answer to one block id.
|
|
164
|
+
|
|
165
|
+
A slot whose node does not exist yet is not an error: it draws the empty band in
|
|
166
|
+
the designer and renders nothing live. The editor creates the nodes when the
|
|
167
|
+
block is inserted and again on every theme load, so a block already sitting in a
|
|
168
|
+
composition gets the rows for a slot the component gained later — insert alone
|
|
169
|
+
would leave those regions unreachable, because the merchant's only route into a
|
|
170
|
+
region is the add-block affordance on its tree row.
|
|
171
|
+
|
|
172
|
+
Children sitting directly on the block (the pre-node shape) are the unnamed
|
|
173
|
+
slot's, and only the unnamed slot's. The node wins that lookup once it exists,
|
|
174
|
+
so the editor MOVES those children into the unnamed node when it creates it
|
|
175
|
+
rather than appending the node beside them — appending would leave live content
|
|
176
|
+
saved, listed in the tree, and drawn nowhere. A block whose type declares only
|
|
177
|
+
named slots has nothing to move them into, so they stay where they are and stop
|
|
178
|
+
rendering, which is what changing the component means.
|
|
179
|
+
|
|
180
|
+
### Narrowing what a slot takes
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
slots: [
|
|
184
|
+
{ name: 'campaign', label: 'Campaign banner', accepts: ['image', 'text'], max: 2 },
|
|
185
|
+
{ name: 'sizeGuide', label: 'Size guide' }
|
|
186
|
+
]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`accepts` limits the block types the merchant may put in the region and `max`
|
|
190
|
+
limits how many; omit either for "anything" and "as many as you like". The
|
|
191
|
+
editor enforces both where a block is WRITTEN into the composition, not only
|
|
192
|
+
where it is offered, so a drag from elsewhere in the tree obeys them too.
|
|
193
|
+
|
|
194
|
+
Neither is read by pz-theme — the renderer draws whatever the composition holds
|
|
195
|
+
— but both are checked at declare time, because they are refused on arrival and
|
|
196
|
+
the editor refuses the WHOLE catalog rather than one slot. `accepts: []` (a
|
|
197
|
+
region nothing could ever go in) and a `max` that is not a positive whole number
|
|
198
|
+
would otherwise empty a brand's entire palette, with the only explanation in a
|
|
199
|
+
repo the developer is not working in.
|
|
200
|
+
|
|
201
|
+
## Styling parts
|
|
202
|
+
|
|
203
|
+
The `field-<key>` parts come free from `ThemeEditable`. Everything else a block
|
|
204
|
+
draws is offered to the properties panel only if it is DECLARED:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
parts: ['row', 'tile'] // the cheap form
|
|
208
|
+
parts: { row: { states: ['hover', 'open'] } } // when appearance depends on state
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Declare the same name you stamp with `partAttrs('row')`, or the panel offers a
|
|
212
|
+
control nothing on the page answers to. Only states the renderer can actually
|
|
213
|
+
match are accepted (`PART_STYLE_STATES` in `utils/part-styles.ts`) — a state
|
|
214
|
+
with no selector emits CSS that can never apply, so it is refused with the
|
|
215
|
+
available list. The `field-` prefix is reserved and cannot be declared.
|
|
216
|
+
|
|
217
|
+
## Declare `settings` and `slots`
|
|
218
|
+
|
|
219
|
+
Runtime collection exists as a fallback, but it only runs on a type's first
|
|
220
|
+
render and never re-announces — so a block that declares nothing reaches the
|
|
221
|
+
editor with an empty panel until the editor is reloaded. Declare them.
|
|
222
|
+
|
|
223
|
+
## What the merchant still cannot restyle
|
|
224
|
+
|
|
225
|
+
A field's own `field-<key>` part carries no states: declaring one is refused,
|
|
226
|
+
because the `field-` prefix belongs to `ThemeEditable`. To give a field element
|
|
227
|
+
a hover appearance, wrap it in an element you stamp and declare yourself.
|
|
228
|
+
|
|
229
|
+
## Limits
|
|
230
|
+
|
|
231
|
+
Forty types per build, twenty-four settings, eight slots and thirty-two parts
|
|
232
|
+
per type, and a 256 KB catalog. The whole catalog is posted to the editor on
|
|
233
|
+
every placeholder announce, which is what the caps are protecting.
|
|
234
|
+
|
|
235
|
+
Every one of them is refused at declare time, because the editor's answer to an
|
|
236
|
+
over-cap catalog is to drop ALL of it — a single block over the parts cap costs
|
|
237
|
+
the build every block in the palette. The one cap a declaration cannot fully
|
|
238
|
+
guarantee is the parts cap: an undeclared `ThemeEditable` discovered at render
|
|
239
|
+
time adds a `field-<key>` part after registration counted them, and if that
|
|
240
|
+
would cross the cap the part is dropped with a warning rather than the catalog
|
|
241
|
+
being put over. The field keeps its control in the Content tab and loses only
|
|
242
|
+
its styling group.
|
|
243
|
+
|
|
244
|
+
## Failure behaviour
|
|
245
|
+
|
|
246
|
+
A composition naming a type this build does not register renders nothing on the
|
|
247
|
+
live site and a labelled card in the designer, and the editor shows the block as
|
|
248
|
+
an orphan row rather than dropping it — its stored properties survive a save
|
|
249
|
+
untouched, so a type that comes back finds its content where it left it.
|