@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/dist/page-data.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* failed; a structurally invalid section is dropped with an issue; the
|
|
8
8
|
* document itself is not rewritten by `validatePageData`.
|
|
9
9
|
*/
|
|
10
|
+
import { THEME_LIMITS, completeRequired, slotCap, surfaceRules } from "./blocks.js";
|
|
10
11
|
import { resolveInstance } from "./instances.js";
|
|
11
12
|
import { ComponentInstanceSchema, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN } from "./schemas.js";
|
|
12
13
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -30,14 +31,21 @@ export function parsePageData(input) {
|
|
|
30
31
|
return envelopeFailure("`sections` must be an array of component instances");
|
|
31
32
|
}
|
|
32
33
|
const issues = [];
|
|
33
|
-
const
|
|
34
|
+
const state = { seenIds: new Set(), count: 0, overCap: 0 };
|
|
34
35
|
const sections = [];
|
|
35
36
|
input.sections.forEach((raw, index) => {
|
|
36
|
-
|
|
37
|
+
if (sections.length >= THEME_LIMITS.sections) {
|
|
38
|
+
issues.push({ path: `sections[${index}]`, code: "limit-exceeded", message: `a document holds at most ${THEME_LIMITS.sections} sections; dropped` });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const instance = sanitizeInstance(raw, `sections[${index}]`, state, issues);
|
|
37
42
|
if (instance !== null) {
|
|
38
43
|
sections.push(instance);
|
|
39
44
|
}
|
|
40
45
|
});
|
|
46
|
+
if (state.overCap > 0) {
|
|
47
|
+
issues.push({ path: "sections", code: "limit-exceeded", message: `a document holds at most ${THEME_LIMITS.documentInstances} instances; ${state.overCap} dropped` });
|
|
48
|
+
}
|
|
41
49
|
// The envelope is forward-compatible: unknown top-level keys are
|
|
42
50
|
// carried through verbatim so a later writer can record metadata without a
|
|
43
51
|
// `version` bump. `PageData` deliberately does not declare them — see its
|
|
@@ -112,13 +120,26 @@ export function validatePageData(theme, input, scope = {}) {
|
|
|
112
120
|
walk(child, `${path}.slots.${name}[${index}]`);
|
|
113
121
|
});
|
|
114
122
|
}
|
|
123
|
+
for (const [name, block] of Object.entries(resolved.fixed)) {
|
|
124
|
+
walk(block, `${path}.fixed.${name}`);
|
|
125
|
+
}
|
|
115
126
|
};
|
|
116
|
-
parsed.page.
|
|
127
|
+
const spec = surfaceRules(theme, parsed.page.surface);
|
|
128
|
+
const cap = slotCap(spec, THEME_LIMITS.sections);
|
|
129
|
+
if (parsed.page.sections.length > cap) {
|
|
130
|
+
issues.push({ path: "sections", code: "limit-exceeded", message: `surface "${parsed.page.surface}" holds at most ${cap} sections; ${parsed.page.sections.length - cap} are not rendered` });
|
|
131
|
+
}
|
|
132
|
+
const sections = completeRequired(parsed.page.sections.slice(0, cap), spec?.required, { id: parsed.page.surface, slot: "sections" }, "sections", issues);
|
|
133
|
+
sections.forEach((section, index) => {
|
|
117
134
|
walk(section, `sections[${index}]`);
|
|
118
135
|
});
|
|
119
136
|
return { page: parsed.page, issues };
|
|
120
137
|
}
|
|
121
|
-
|
|
138
|
+
const subtreeSize = (instance) => 1 +
|
|
139
|
+
(instance.children ?? []).reduce((sum, child) => sum + subtreeSize(child), 0) +
|
|
140
|
+
Object.values(instance.slots ?? {}).reduce((sum, list) => sum + list.reduce((inner, child) => inner + subtreeSize(child), 0), 0) +
|
|
141
|
+
Object.values(instance.fixed ?? {}).reduce((sum, block) => sum + subtreeSize(block), 0);
|
|
142
|
+
function sanitizeInstance(raw, path, state, issues) {
|
|
122
143
|
const parsed = ComponentInstanceSchema.safeParse(raw);
|
|
123
144
|
if (!parsed.success) {
|
|
124
145
|
const detail = parsed.error.issues[0]?.message ?? "invalid shape";
|
|
@@ -129,15 +150,25 @@ function sanitizeInstance(raw, path, seenIds, issues) {
|
|
|
129
150
|
});
|
|
130
151
|
return null;
|
|
131
152
|
}
|
|
132
|
-
return
|
|
153
|
+
return pruneInstance(parsed.data, path, state, issues, 0);
|
|
133
154
|
}
|
|
134
155
|
/**
|
|
135
|
-
* Deterministic rebuild with duplicate ids pruned (depth-first, first wins)
|
|
136
|
-
*
|
|
137
|
-
*
|
|
156
|
+
* Deterministic rebuild with duplicate ids pruned (depth-first, first wins)
|
|
157
|
+
* and the platform's caps applied: nesting deeper than `THEME_LIMITS.depth`
|
|
158
|
+
* and instances past `THEME_LIMITS.documentInstances` are dropped and
|
|
159
|
+
* reported. Property presence is preserved — a valid document round-trips
|
|
160
|
+
* unchanged; pruning to empty keeps the empty container.
|
|
138
161
|
*/
|
|
139
|
-
function
|
|
140
|
-
if (
|
|
162
|
+
function pruneInstance(instance, path, state, issues, depth) {
|
|
163
|
+
if (depth > THEME_LIMITS.depth) {
|
|
164
|
+
issues.push({ path, code: "limit-exceeded", message: `nesting goes at most ${THEME_LIMITS.depth} levels below a section; dropped` });
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
if (state.count >= THEME_LIMITS.documentInstances) {
|
|
168
|
+
state.overCap += subtreeSize(instance);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
if (state.seenIds.has(instance.id)) {
|
|
141
172
|
issues.push({
|
|
142
173
|
path: `${path}.id`,
|
|
143
174
|
code: "duplicate-id",
|
|
@@ -145,7 +176,8 @@ function pruneDuplicateIds(instance, path, seenIds, issues) {
|
|
|
145
176
|
});
|
|
146
177
|
return null;
|
|
147
178
|
}
|
|
148
|
-
seenIds.add(instance.id);
|
|
179
|
+
state.seenIds.add(instance.id);
|
|
180
|
+
state.count += 1;
|
|
149
181
|
const pruned = { id: instance.id, type: instance.type };
|
|
150
182
|
if (instance.props !== undefined) {
|
|
151
183
|
pruned.props = instance.props;
|
|
@@ -153,29 +185,33 @@ function pruneDuplicateIds(instance, path, seenIds, issues) {
|
|
|
153
185
|
if (instance.hidden !== undefined) {
|
|
154
186
|
pruned.hidden = instance.hidden;
|
|
155
187
|
}
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
}
|
|
188
|
+
const pruneList = (list, listPath) => {
|
|
189
|
+
const sane = [];
|
|
190
|
+
list.forEach((child, index) => {
|
|
191
|
+
const kept = pruneInstance(child, `${listPath}[${index}]`, state, issues, depth + 1);
|
|
192
|
+
if (kept !== null)
|
|
193
|
+
sane.push(kept);
|
|
163
194
|
});
|
|
164
|
-
|
|
195
|
+
return sane;
|
|
196
|
+
};
|
|
197
|
+
if (instance.children !== undefined) {
|
|
198
|
+
pruned.children = pruneList(instance.children, `${path}.children`);
|
|
165
199
|
}
|
|
166
200
|
if (instance.slots !== undefined) {
|
|
167
201
|
const slots = {};
|
|
168
202
|
for (const [name, list] of Object.entries(instance.slots)) {
|
|
169
|
-
|
|
170
|
-
list.forEach((child, index) => {
|
|
171
|
-
const sane = pruneDuplicateIds(child, `${path}.slots.${name}[${index}]`, seenIds, issues);
|
|
172
|
-
if (sane !== null) {
|
|
173
|
-
saneList.push(sane);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
slots[name] = saneList;
|
|
203
|
+
slots[name] = pruneList(list, `${path}.slots.${name}`);
|
|
177
204
|
}
|
|
178
205
|
pruned.slots = slots;
|
|
179
206
|
}
|
|
207
|
+
if (instance.fixed !== undefined) {
|
|
208
|
+
const fixed = {};
|
|
209
|
+
for (const [name, block] of Object.entries(instance.fixed)) {
|
|
210
|
+
const kept = pruneInstance(block, `${path}.fixed.${name}`, state, issues, depth + 1);
|
|
211
|
+
if (kept !== null)
|
|
212
|
+
fixed[name] = kept;
|
|
213
|
+
}
|
|
214
|
+
pruned.fixed = fixed;
|
|
215
|
+
}
|
|
180
216
|
return pruned;
|
|
181
217
|
}
|
package/dist/preview-bridge.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* at, and re-renders a draft the editor sends by posting it back to the
|
|
7
7
|
* storefront and swapping in only the sections that changed — or, when the
|
|
8
8
|
* page around them changed, the whole page, the way Astro's router swaps one.
|
|
9
|
+
* A change the editor patches is shown at once, on the page as it stands,
|
|
10
|
+
* until that render arrives.
|
|
9
11
|
*
|
|
10
12
|
* It draws nothing — outlines, toolbars and panels are the editor's. It posts
|
|
11
13
|
* only to the editor origin it is given and reads only that origin's messages
|
|
12
14
|
* from the framing window.
|
|
13
15
|
*/
|
|
14
|
-
import { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, } from "./preview-protocol.js";
|
|
16
|
+
import { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, previewEndMarker, previewStartMarker, } from "./preview-protocol.js";
|
|
15
17
|
/**
|
|
16
18
|
* Dispatched on `document` for each section the bridge swapped in, with the
|
|
17
19
|
* instance id as `detail.id`: a section with browser behavior binds it again.
|
|
@@ -21,7 +23,9 @@ import { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, } fro
|
|
|
21
23
|
export const PREVIEW_SECTION_LOAD_EVENT = "wocart:section-load";
|
|
22
24
|
const ASTRO_PAGE_LOAD_EVENT = "astro:page-load";
|
|
23
25
|
const SHOW_COMMENT = 0x80;
|
|
26
|
+
const SHOW_TEXT = 0x4;
|
|
24
27
|
const ELEMENT_NODE = 1;
|
|
28
|
+
const TEXT_NODE = 3;
|
|
25
29
|
const COMMENT_NODE = 8;
|
|
26
30
|
/**
|
|
27
31
|
* Every complete marker pair under `root`, in document order, each with the
|
|
@@ -74,6 +78,30 @@ function markup(nodes) {
|
|
|
74
78
|
})
|
|
75
79
|
.join("");
|
|
76
80
|
}
|
|
81
|
+
/** An instance's nodes with its markers: what moves, copies or goes with it. */
|
|
82
|
+
function withMarkers(entry) {
|
|
83
|
+
const inside = between(entry);
|
|
84
|
+
return inside === null ? null : [entry.start, ...inside, entry.end];
|
|
85
|
+
}
|
|
86
|
+
/** Text nodes under `nodes` that a merchant reads — never a script's or a style's. */
|
|
87
|
+
function textNodes(nodes) {
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const node of nodes) {
|
|
90
|
+
if (node.nodeType === TEXT_NODE) {
|
|
91
|
+
found.push(node);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (node.nodeType !== ELEMENT_NODE)
|
|
95
|
+
continue;
|
|
96
|
+
const walker = node.ownerDocument.createTreeWalker(node, SHOW_TEXT);
|
|
97
|
+
for (let text = walker.nextNode(); text !== null; text = walker.nextNode()) {
|
|
98
|
+
const tag = text.parentElement?.tagName;
|
|
99
|
+
if (tag !== "SCRIPT" && tag !== "STYLE" && tag !== "TEMPLATE")
|
|
100
|
+
found.push(text);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
77
105
|
/**
|
|
78
106
|
* The page with every section's content left out — the root's attributes and
|
|
79
107
|
* the body around the sections: what must not change for sections to be
|
|
@@ -229,6 +257,135 @@ export function startPreviewBridge(options) {
|
|
|
229
257
|
}
|
|
230
258
|
return swapped;
|
|
231
259
|
};
|
|
260
|
+
/** What a `remove` or `hide` patch took off the page, to put back on `restore` or show. */
|
|
261
|
+
const removed = new Map();
|
|
262
|
+
const hiddenNodes = new Map();
|
|
263
|
+
const find = (id) => marked.find((entry) => entry.id === id);
|
|
264
|
+
/**
|
|
265
|
+
* Puts `nodes` at `index` among the instances of `owner` (`null`: the
|
|
266
|
+
* page's sections) other than `id`. An empty slot has no place to find, so
|
|
267
|
+
* its move waits for the render.
|
|
268
|
+
*/
|
|
269
|
+
const place = (nodes, id, index, owner) => {
|
|
270
|
+
const others = marked.filter((entry) => entry.parent === owner && entry.id !== id);
|
|
271
|
+
if (!Number.isInteger(index) || index < 0 || index > others.length)
|
|
272
|
+
return false;
|
|
273
|
+
const before = others[index]?.start ?? others.at(-1)?.end.nextSibling ?? null;
|
|
274
|
+
const parent = others[index]?.start.parentNode ?? others.at(-1)?.end.parentNode;
|
|
275
|
+
if (!parent)
|
|
276
|
+
return false;
|
|
277
|
+
for (const node of nodes)
|
|
278
|
+
parent.insertBefore(node, before);
|
|
279
|
+
return true;
|
|
280
|
+
};
|
|
281
|
+
const relabel = (marker, ids) => {
|
|
282
|
+
const parsed = parsePreviewMarker(marker.data);
|
|
283
|
+
if (parsed === null || !Object.hasOwn(ids, parsed.id))
|
|
284
|
+
return;
|
|
285
|
+
const id = ids[parsed.id];
|
|
286
|
+
marker.data = parsed.kind === "start" ? previewStartMarker({ id, type: parsed.type, hidden: parsed.hidden }) : previewEndMarker({ id });
|
|
287
|
+
};
|
|
288
|
+
/** Applies one patch to the page as it stands; returns the sections it put in. */
|
|
289
|
+
const apply = (patch) => {
|
|
290
|
+
const entry = find(patch.id);
|
|
291
|
+
switch (patch.op) {
|
|
292
|
+
case "move": {
|
|
293
|
+
if (entry === undefined || entry.parent !== (patch.parent ?? null))
|
|
294
|
+
return [];
|
|
295
|
+
const nodes = withMarkers(entry);
|
|
296
|
+
if (nodes !== null)
|
|
297
|
+
place(nodes, entry.id, patch.index, entry.parent);
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
case "duplicate": {
|
|
301
|
+
const nodes = entry === undefined ? null : withMarkers(entry);
|
|
302
|
+
if (entry === undefined || nodes === null || !Object.hasOwn(patch.ids, entry.id))
|
|
303
|
+
return [];
|
|
304
|
+
const copies = nodes.map((node) => node.cloneNode(true));
|
|
305
|
+
for (const copy of copies) {
|
|
306
|
+
if (copy.nodeType === COMMENT_NODE)
|
|
307
|
+
relabel(copy, patch.ids);
|
|
308
|
+
else if (copy.nodeType === ELEMENT_NODE) {
|
|
309
|
+
const walker = doc.createTreeWalker(copy, SHOW_COMMENT);
|
|
310
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode())
|
|
311
|
+
relabel(node, patch.ids);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const after = entry.end.nextSibling;
|
|
315
|
+
for (const copy of copies)
|
|
316
|
+
entry.end.parentNode.insertBefore(copy, after);
|
|
317
|
+
return [patch.ids[entry.id]];
|
|
318
|
+
}
|
|
319
|
+
case "remove": {
|
|
320
|
+
const nodes = entry === undefined ? null : withMarkers(entry);
|
|
321
|
+
if (nodes === null)
|
|
322
|
+
return [];
|
|
323
|
+
for (const node of nodes)
|
|
324
|
+
node.parentNode?.removeChild(node);
|
|
325
|
+
removed.set(patch.id, nodes);
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
case "restore": {
|
|
329
|
+
const nodes = removed.get(patch.id);
|
|
330
|
+
if (entry !== undefined || nodes === undefined || !place(nodes, patch.id, patch.index, patch.parent ?? null))
|
|
331
|
+
return [];
|
|
332
|
+
removed.delete(patch.id);
|
|
333
|
+
return [patch.id];
|
|
334
|
+
}
|
|
335
|
+
case "hide": {
|
|
336
|
+
if (entry === undefined || entry.hidden === patch.hidden)
|
|
337
|
+
return [];
|
|
338
|
+
const inside = between(entry);
|
|
339
|
+
if (inside === null)
|
|
340
|
+
return [];
|
|
341
|
+
if (patch.hidden) {
|
|
342
|
+
for (const node of inside)
|
|
343
|
+
node.parentNode?.removeChild(node);
|
|
344
|
+
hiddenNodes.set(entry.id, inside);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const kept = hiddenNodes.get(entry.id);
|
|
348
|
+
if (kept === undefined)
|
|
349
|
+
return [];
|
|
350
|
+
for (const node of kept)
|
|
351
|
+
entry.end.parentNode.insertBefore(node, entry.end);
|
|
352
|
+
hiddenNodes.delete(entry.id);
|
|
353
|
+
}
|
|
354
|
+
entry.start.data = previewStartMarker({ id: entry.id, type: entry.type, hidden: patch.hidden });
|
|
355
|
+
return patch.hidden ? [] : [entry.id];
|
|
356
|
+
}
|
|
357
|
+
case "text": {
|
|
358
|
+
const inside = entry === undefined ? null : between(entry);
|
|
359
|
+
const from = patch.from.trim();
|
|
360
|
+
if (inside === null || from === "" || typeof patch.to !== "string")
|
|
361
|
+
return [];
|
|
362
|
+
for (const node of textNodes(inside)) {
|
|
363
|
+
if (node.data.trim() !== from)
|
|
364
|
+
continue;
|
|
365
|
+
const lead = /^\s*/.exec(node.data)[0];
|
|
366
|
+
const trail = /\s*$/.exec(node.data)[0];
|
|
367
|
+
node.data = `${lead}${patch.to}${trail}`;
|
|
368
|
+
}
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
default:
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
/** Shows `patches` at once; a render already on its way would undo them, so it is dropped. */
|
|
376
|
+
const patchPage = (patches) => {
|
|
377
|
+
latest += 1;
|
|
378
|
+
const loaded = [];
|
|
379
|
+
for (const patch of patches) {
|
|
380
|
+
if (typeof patch !== "object" || patch === null || typeof patch.id !== "string")
|
|
381
|
+
continue;
|
|
382
|
+
loaded.push(...apply(patch));
|
|
383
|
+
marked = collect(doc.body);
|
|
384
|
+
}
|
|
385
|
+
for (const id of loaded)
|
|
386
|
+
doc.dispatchEvent(new win.CustomEvent(PREVIEW_SECTION_LOAD_EVENT, { detail: { id } }));
|
|
387
|
+
post({ type: "layout", instances: instances() });
|
|
388
|
+
};
|
|
232
389
|
const render = async (draft, revision) => {
|
|
233
390
|
const ticket = ++latest;
|
|
234
391
|
try {
|
|
@@ -276,6 +433,10 @@ export function startPreviewBridge(options) {
|
|
|
276
433
|
void render(message.draft, typeof message.revision === "number" ? message.revision : null);
|
|
277
434
|
}
|
|
278
435
|
break;
|
|
436
|
+
case "patch":
|
|
437
|
+
if (Array.isArray(message.patches))
|
|
438
|
+
patchPage(message.patches);
|
|
439
|
+
break;
|
|
279
440
|
case "mode":
|
|
280
441
|
if (message.mode === "edit" || message.mode === "interactive")
|
|
281
442
|
mode = message.mode;
|
|
@@ -56,10 +56,59 @@ export type PreviewPageMessage = {
|
|
|
56
56
|
});
|
|
57
57
|
/** The interaction an editor allows: `edit` turns clicks into selections; `interactive` lets the page behave. */
|
|
58
58
|
export type PreviewMode = "edit" | "interactive";
|
|
59
|
+
/**
|
|
60
|
+
* A change the editor has already made to its draft, for the page to show at
|
|
61
|
+
* once, before the `render` that follows it. A patch is a hint: the page
|
|
62
|
+
* applies what it can find and leaves the rest to that render, which is the
|
|
63
|
+
* page's truth — so an editor always sends the render too, and a page that
|
|
64
|
+
* does not know patches (before `@wocart/theme` 1.6.0) still shows every
|
|
65
|
+
* change, a moment later.
|
|
66
|
+
*
|
|
67
|
+
* - `move`: an instance to `index` among its siblings, counted once it has
|
|
68
|
+
* left its place (`moveInstance`): the document's sections, or with
|
|
69
|
+
* `parent` the blocks of that instance. A move to another parent is left to
|
|
70
|
+
* the render.
|
|
71
|
+
* - `duplicate`: a copy of an instance right after it; `ids` maps every
|
|
72
|
+
* instance id inside it, its own included, to its copy's.
|
|
73
|
+
* - `remove`: an instance and everything in it; `restore` puts what a
|
|
74
|
+
* `remove` took back at `index` among the sections, or among `parent`'s
|
|
75
|
+
* blocks.
|
|
76
|
+
* - `hide`: an instance switched off (it shows nothing) or back on.
|
|
77
|
+
* - `text`: inside an instance, text that reads `from` now reads `to` — a
|
|
78
|
+
* field's text as the theme prints it.
|
|
79
|
+
*/
|
|
80
|
+
export type PreviewPatch = {
|
|
81
|
+
op: "move";
|
|
82
|
+
id: string;
|
|
83
|
+
index: number;
|
|
84
|
+
parent?: string;
|
|
85
|
+
} | {
|
|
86
|
+
op: "duplicate";
|
|
87
|
+
id: string;
|
|
88
|
+
ids: Readonly<Record<string, string>>;
|
|
89
|
+
} | {
|
|
90
|
+
op: "remove";
|
|
91
|
+
id: string;
|
|
92
|
+
} | {
|
|
93
|
+
op: "restore";
|
|
94
|
+
id: string;
|
|
95
|
+
index: number;
|
|
96
|
+
parent?: string;
|
|
97
|
+
} | {
|
|
98
|
+
op: "hide";
|
|
99
|
+
id: string;
|
|
100
|
+
hidden: boolean;
|
|
101
|
+
} | {
|
|
102
|
+
op: "text";
|
|
103
|
+
id: string;
|
|
104
|
+
from: string;
|
|
105
|
+
to: string;
|
|
106
|
+
};
|
|
59
107
|
/**
|
|
60
108
|
* What the editor tells the preview page. `draft` is a `PreviewDraft` of
|
|
61
109
|
* `@wocart/storefront`; `revision`, when given, comes back on the `rendered`
|
|
62
|
-
* or `error` it causes, and a render answered after a newer one is dropped
|
|
110
|
+
* or `error` it causes, and a render answered after a newer one is dropped —
|
|
111
|
+
* as is one answered after a `patch`, which the next render follows.
|
|
63
112
|
*/
|
|
64
113
|
export type PreviewEditorMessage = {
|
|
65
114
|
source: "wocart-editor";
|
|
@@ -68,6 +117,9 @@ export type PreviewEditorMessage = {
|
|
|
68
117
|
type: "render";
|
|
69
118
|
draft: Record<string, unknown>;
|
|
70
119
|
revision?: number;
|
|
120
|
+
} | {
|
|
121
|
+
type: "patch";
|
|
122
|
+
patches: readonly PreviewPatch[];
|
|
71
123
|
} | {
|
|
72
124
|
type: "mode";
|
|
73
125
|
mode: PreviewMode;
|
package/dist/rich-text.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface RichTextHtmlOptions {
|
|
|
16
16
|
* result is escaped; a target that fails the link rule never reaches it.
|
|
17
17
|
*/
|
|
18
18
|
href?: (target: string) => string;
|
|
19
|
+
/** `true`: the first paragraph's text only, without a block element — for a field with `inline: true`. */
|
|
20
|
+
inline?: boolean;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* The document as HTML: `<p>`, `<h2>`–`<h4>`, `<ul>`/`<ol>`, `<strong>`, `<em>`,
|
package/dist/rich-text.js
CHANGED
|
@@ -157,6 +157,10 @@ const escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").r
|
|
|
157
157
|
export function richTextToHtml(document, options = {}) {
|
|
158
158
|
if (!isRecord(document) || !Array.isArray(document.content))
|
|
159
159
|
return "";
|
|
160
|
+
if (options.inline === true) {
|
|
161
|
+
const first = document.content.find((block) => isRecord(block) && block.type === "paragraph");
|
|
162
|
+
return first === undefined ? "" : inlinesHtml(first.content, options);
|
|
163
|
+
}
|
|
160
164
|
return document.content.map((block) => blockHtml(block, options)).join("");
|
|
161
165
|
}
|
|
162
166
|
function blockHtml(block, options) {
|
package/dist/schemas.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export declare const THEME_KEY_PATTERN: RegExp;
|
|
|
14
14
|
export declare const TYPE_KEY_PATTERN: RegExp;
|
|
15
15
|
/**
|
|
16
16
|
* The page-document **surface** key — the address of a composed page
|
|
17
|
-
* (`home`
|
|
17
|
+
* (`home`, `product`, …; `area:<key>` is an area's document, and
|
|
18
|
+
* `page:<slug>` / `lp:<slug>` are reserved conventions).
|
|
18
19
|
*
|
|
19
20
|
* The server applies the same pattern. Module-level on purpose: it is part of
|
|
20
21
|
* the persisted-data contract,
|
package/dist/schemas.js
CHANGED
|
@@ -13,7 +13,8 @@ export const THEME_KEY_PATTERN = /^(?=.{1,64}$)[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
|
13
13
|
export const TYPE_KEY_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
14
14
|
/**
|
|
15
15
|
* The page-document **surface** key — the address of a composed page
|
|
16
|
-
* (`home`
|
|
16
|
+
* (`home`, `product`, …; `area:<key>` is an area's document, and
|
|
17
|
+
* `page:<slug>` / `lp:<slug>` are reserved conventions).
|
|
17
18
|
*
|
|
18
19
|
* The server applies the same pattern. Module-level on purpose: it is part of
|
|
19
20
|
* the persisted-data contract,
|
|
@@ -126,6 +127,7 @@ export const ComponentInstanceSchema = z.lazy(() => z.object({
|
|
|
126
127
|
props: z.exactOptional(z.record(z.string(), JsonValueSchema)),
|
|
127
128
|
children: z.exactOptional(z.array(ComponentInstanceSchema)),
|
|
128
129
|
slots: z.exactOptional(z.record(z.string(), z.array(ComponentInstanceSchema))),
|
|
130
|
+
fixed: z.exactOptional(z.record(z.string(), ComponentInstanceSchema)),
|
|
129
131
|
hidden: z.exactOptional(z.boolean()),
|
|
130
132
|
}));
|
|
131
133
|
/**
|
package/dist/settings.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* content pack, for text) → absent.
|
|
12
12
|
*/
|
|
13
13
|
import { checkStoredValue, ownValue, resolveField } from "./instances.js";
|
|
14
|
+
import { styleSizeProblem } from "./style.js";
|
|
14
15
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
16
|
function declaredSettings(theme) {
|
|
16
17
|
const fields = new Map();
|
|
@@ -75,8 +76,26 @@ export function checkThemeSettings(theme, values) {
|
|
|
75
76
|
continue;
|
|
76
77
|
}
|
|
77
78
|
const checked = checkStoredValue(field, value);
|
|
78
|
-
if (!checked.ok)
|
|
79
|
+
if (!checked.ok) {
|
|
79
80
|
issues.push({ path: `settings.${key}`, code: "invalid-setting", message: `"${key}" ${checked.message}` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (field.kind === "style") {
|
|
84
|
+
const problem = styleSizeProblem(theme.style, value);
|
|
85
|
+
if (problem !== null)
|
|
86
|
+
issues.push({ path: `settings.${key}`, code: "invalid-setting", message: `"${key}" ${problem}` });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const schemesField = [...declared.values()].find((field) => field.kind === "colorSchemes");
|
|
90
|
+
if (schemesField?.kind === "colorSchemes") {
|
|
91
|
+
const stored = ownValue(values, schemesField.key);
|
|
92
|
+
const schemes = stored !== undefined && checkStoredValue(schemesField, stored).ok ? stored : schemesField.default;
|
|
93
|
+
const ids = new Set(schemes.map((scheme) => scheme.id));
|
|
94
|
+
for (const [key, value] of Object.entries(values)) {
|
|
95
|
+
if (declared.get(key)?.kind === "colorScheme" && typeof value === "string" && !ids.has(value)) {
|
|
96
|
+
issues.push({ path: `settings.${key}`, code: "invalid-setting", message: `"${key}" names the color scheme "${value}", which the store does not have` });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
80
99
|
}
|
|
81
100
|
return issues;
|
|
82
101
|
}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ColorScheme, StylePanel, StyleValue, ThemeStyle } from "./types.js";
|
|
2
|
+
/** The platform's breakpoints, phone first, with the width each starts at. */
|
|
3
|
+
export declare const STYLE_BREAKPOINTS: readonly [readonly ["base", 0], readonly ["md", 768], readonly ["lg", 1024]];
|
|
4
|
+
export declare const STYLE_STEPS: readonly ["s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11", "s12"];
|
|
5
|
+
export declare const STYLE_WIDTHS: readonly ["wide", "content", "narrow"];
|
|
6
|
+
export declare const STYLE_PANELS: readonly ["spacing", "layout", "size"];
|
|
7
|
+
export declare const COLOR_ROLES: readonly ["background", "text", "heading", "accent", "button", "buttonText", "border"];
|
|
8
|
+
/** A color scheme's id: what a `colorScheme` value names. */
|
|
9
|
+
export declare const COLOR_SCHEME_ID_PATTERN: RegExp;
|
|
10
|
+
/** The most color schemes a store keeps. */
|
|
11
|
+
export declare const COLOR_SCHEMES_MAX = 24;
|
|
12
|
+
/** Where a built theme serves the stylesheet of its style classes (`themeStylesEndpoint`). */
|
|
13
|
+
export declare const THEME_STYLES_PATH = "/wocart-styles.css";
|
|
14
|
+
/** Why `value` is not a style value a field offering `panels` may hold, or `null`. */
|
|
15
|
+
export declare function styleValueProblem(panels: readonly StylePanel[], value: unknown): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Why a theme's sizes leave part of a style value without a style: a step or
|
|
18
|
+
* width the theme does not size. What a writer stores must be sized; a
|
|
19
|
+
* reader renders an unsized value as no style.
|
|
20
|
+
*/
|
|
21
|
+
export declare function styleSizeProblem(style: ThemeStyle | undefined, value: StyleValue): string | null;
|
|
22
|
+
/** The classes that give an element its style value, from the theme's stylesheet (`themeStyleSheet`). */
|
|
23
|
+
export declare function styleClasses(value: StyleValue | undefined): string;
|
|
24
|
+
/**
|
|
25
|
+
* Every class a theme's sizes allow, phone first: the base rules, then each
|
|
26
|
+
* wider breakpoint in its media query. A step or width the theme does not
|
|
27
|
+
* size has no class.
|
|
28
|
+
*/
|
|
29
|
+
export declare function themeStyleSheet(style: ThemeStyle | undefined): string;
|
|
30
|
+
/** Why `value` is not a list of color schemes, or `null`. */
|
|
31
|
+
export declare function colorSchemesProblem(value: unknown): string | null;
|
|
32
|
+
/** The class that gives an element a color scheme, or `""` for what is not a scheme id. */
|
|
33
|
+
export declare function schemeClass(id: string | undefined): string;
|
|
34
|
+
/**
|
|
35
|
+
* The store's color schemes as CSS: each scheme's roles as custom properties
|
|
36
|
+
* (`--wc-color-background`, `--wc-color-button-text`, …) with its background
|
|
37
|
+
* and text applied, on `.wc-scheme-<id>`. A scheme with a malformed id or
|
|
38
|
+
* color is left out whole.
|
|
39
|
+
*/
|
|
40
|
+
export declare function colorSchemeCss(schemes: readonly ColorScheme[] | undefined): string;
|
|
41
|
+
/**
|
|
42
|
+
* The WCAG contrast ratio of two hex colors, from 1 to 21. Text needs 4.5 to
|
|
43
|
+
* meet AA; an editor warns below it. `NaN` for what is not a hex color.
|
|
44
|
+
*/
|
|
45
|
+
export declare function contrastRatio(a: string, b: string): number;
|