@wocart/theme 1.5.0 → 1.6.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 +14 -0
- package/README.md +15 -2
- package/dist/index.d.ts +1 -1
- package/dist/preview-bridge.js +159 -1
- package/dist/preview-protocol.d.ts +48 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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
|
+
## 1.6.0 — 2026-10-19
|
|
9
|
+
|
|
10
|
+
An editor's change shows on the preview page at once.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- The preview protocol's `patch` message (`PreviewPatch`): a change the
|
|
15
|
+
editor has already made — move, duplicate, remove and restore a section,
|
|
16
|
+
hide or show one, a field's text — shown by the preview bridge on the page
|
|
17
|
+
as it stands, before the render that follows it. Patches are hints; the
|
|
18
|
+
render stays the page's truth, and a render already on its way when a
|
|
19
|
+
patch arrives is dropped so it cannot undo it. A page that does not know
|
|
20
|
+
patches ignores them. The protocol version stays 1.
|
|
21
|
+
|
|
8
22
|
## 1.5.0 — 2026-10-18
|
|
9
23
|
|
|
10
24
|
The editor-facing contract: everything a visual editor needs to discover and
|
package/README.md
CHANGED
|
@@ -406,15 +406,28 @@ framing window, with `source: "wocart-editor"` and the protocol's `version`
|
|
|
406
406
|
| The editor sends | The page answers |
|
|
407
407
|
| --- | --- |
|
|
408
408
|
| — | `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 |
|
|
409
|
+
| `render` `{ draft, revision? }` | `rendered` `{ revision, instances }`, or `error` `{ revision, reason }`; an answer overtaken by a newer render or a patch is dropped |
|
|
410
|
+
| `patch` `{ patches }` | the change shown at once, on the page as it stands, then `layout` (see below) |
|
|
410
411
|
| `mode` `"edit"` \| `"interactive"` | in `edit`, links and forms do nothing and a click is `select` `{ id }` of the innermost instance |
|
|
411
412
|
| `scroll-to` `{ id }` | the instance scrolls into view |
|
|
412
413
|
| `token` `{ token }` | the next render uses it |
|
|
413
414
|
| — | `hover` `{ id \| null }` as the pointer moves; `layout` after a scroll or resize |
|
|
414
415
|
|
|
416
|
+
A `patch` shows a change the editor has already made to its draft before the
|
|
417
|
+
render that follows it arrives — so moving, copying, removing or hiding a
|
|
418
|
+
section, and typing a field's text, show on the page at once. Each patch
|
|
419
|
+
(`PreviewPatch`) is a hint: the page applies what it finds — `move` a section
|
|
420
|
+
to an index, `duplicate` an instance under new ids, `remove` one and
|
|
421
|
+
`restore` it, `hide` one or show it again, replace `text` that reads `from`
|
|
422
|
+
inside an instance with `to` — and leaves the rest to the render, which is
|
|
423
|
+
the page's truth. An editor therefore always sends the render too; a page
|
|
424
|
+
from before 1.6.0 ignores patches and still shows every change, a moment
|
|
425
|
+
later.
|
|
426
|
+
|
|
415
427
|
A swapped section's scripts do not run again. A section with browser behavior
|
|
416
428
|
binds it again on `PREVIEW_SECTION_LOAD_EVENT` (`wocart:section-load`,
|
|
417
|
-
dispatched on `document` with `detail.id`)
|
|
429
|
+
dispatched on `document` with `detail.id`) — also for a section a patch
|
|
430
|
+
copied, restored or showed again. When the bridge replaces the whole
|
|
418
431
|
page instead, it does what Astro's router does after a navigation — a new
|
|
419
432
|
`<body>`, the root element's attributes, the title — and dispatches
|
|
420
433
|
`astro:page-load`, so a script written for the router mounts again as it does
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,6 @@ export type { MessageOptions, MessageValues, PluralCategory } from "./messages.j
|
|
|
28
28
|
export { THEME_MANIFEST_CONTRACT_VERSION, THEME_MANIFEST_PATH, manifestCatalog, themeManifest, validateThemeManifest, } from "./manifest.js";
|
|
29
29
|
export { diffThemeManifests } from "./manifest-diff.js";
|
|
30
30
|
export { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, previewEndMarker, previewStartMarker, } from "./preview-protocol.js";
|
|
31
|
-
export type { PreviewEditorMessage, PreviewInstance, PreviewMode, PreviewPageMessage, PreviewRect, } from "./preview-protocol.js";
|
|
31
|
+
export type { PreviewEditorMessage, PreviewInstance, PreviewMode, PreviewPageMessage, PreviewPatch, PreviewRect, } from "./preview-protocol.js";
|
|
32
32
|
export type { ManifestChange, ManifestDiff } from "./manifest-diff.js";
|
|
33
33
|
export type { JsonValue, JsonObject, LocaleCode, LocalizedText, ResolutionScope, FieldKind, FieldDefinition, FieldValue, InferProps, FieldValues, SlotDefinition, ComponentCapabilities, ComponentPreset, PresetInstance, SurfacePageType, ComponentDefinition, ComponentSpec, SurfaceSpec, ThemeDefinition, SettingsGroup, InferSettings, ThemeSettingsIssue, ThemeSettingsIssueCode, ThemeSettingsResult, NumberFieldDefinition, ColorFieldDefinition, MenuFieldDefinition, CollectionFieldDefinition, ProductFieldDefinition, CategoryFieldDefinition, ImageFieldDefinition, RichTextFieldDefinition, RichTextFormat, ListFieldDefinition, ListItemFieldDefinition, ListItem, MediaReference, ResolvedImage, RichTextValue, RichTextDocument, RichTextBlock, RichTextParagraph, RichTextHeading, RichTextList, RichTextListItem, RichTextInline, RichTextText, RichTextBreak, RichTextMark, ThemeManifest, ThemeManifestIssue, ThemeManifestIssueCode, ThemeManifestResult, ComponentInstance, PageData, ThemeRenderContext, LoadContext, LoadFn, ReadCache, PageDataIssue, PageDataIssueCode, PageDataResult, ResolvedInstance, ThemeDefinitionIssue, ThemeDefinitionIssueCode, ThemeValidationOptions, } from "./types.js";
|
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,132 @@ 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 sectionsOf = () => marked.filter((entry) => entry.parent === null);
|
|
264
|
+
const find = (id) => marked.find((entry) => entry.id === id);
|
|
265
|
+
/** Puts `nodes` at `index` among the document's sections other than `id`. */
|
|
266
|
+
const place = (nodes, id, index) => {
|
|
267
|
+
const others = sectionsOf().filter((entry) => entry.id !== id);
|
|
268
|
+
if (!Number.isInteger(index) || index < 0 || index > others.length)
|
|
269
|
+
return false;
|
|
270
|
+
const before = others[index]?.start ?? others.at(-1)?.end.nextSibling ?? null;
|
|
271
|
+
const parent = others[index]?.start.parentNode ?? others.at(-1)?.end.parentNode;
|
|
272
|
+
if (!parent)
|
|
273
|
+
return false;
|
|
274
|
+
for (const node of nodes)
|
|
275
|
+
parent.insertBefore(node, before);
|
|
276
|
+
return true;
|
|
277
|
+
};
|
|
278
|
+
const relabel = (marker, ids) => {
|
|
279
|
+
const parsed = parsePreviewMarker(marker.data);
|
|
280
|
+
if (parsed === null || !Object.hasOwn(ids, parsed.id))
|
|
281
|
+
return;
|
|
282
|
+
const id = ids[parsed.id];
|
|
283
|
+
marker.data = parsed.kind === "start" ? previewStartMarker({ id, type: parsed.type, hidden: parsed.hidden }) : previewEndMarker({ id });
|
|
284
|
+
};
|
|
285
|
+
/** Applies one patch to the page as it stands; returns the sections it put in. */
|
|
286
|
+
const apply = (patch) => {
|
|
287
|
+
const entry = find(patch.id);
|
|
288
|
+
switch (patch.op) {
|
|
289
|
+
case "move": {
|
|
290
|
+
if (entry === undefined || entry.parent !== null)
|
|
291
|
+
return [];
|
|
292
|
+
const nodes = withMarkers(entry);
|
|
293
|
+
if (nodes !== null)
|
|
294
|
+
place(nodes, entry.id, patch.index);
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
case "duplicate": {
|
|
298
|
+
const nodes = entry === undefined ? null : withMarkers(entry);
|
|
299
|
+
if (entry === undefined || nodes === null || !Object.hasOwn(patch.ids, entry.id))
|
|
300
|
+
return [];
|
|
301
|
+
const copies = nodes.map((node) => node.cloneNode(true));
|
|
302
|
+
for (const copy of copies) {
|
|
303
|
+
if (copy.nodeType === COMMENT_NODE)
|
|
304
|
+
relabel(copy, patch.ids);
|
|
305
|
+
else if (copy.nodeType === ELEMENT_NODE) {
|
|
306
|
+
const walker = doc.createTreeWalker(copy, SHOW_COMMENT);
|
|
307
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode())
|
|
308
|
+
relabel(node, patch.ids);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const after = entry.end.nextSibling;
|
|
312
|
+
for (const copy of copies)
|
|
313
|
+
entry.end.parentNode.insertBefore(copy, after);
|
|
314
|
+
return [patch.ids[entry.id]];
|
|
315
|
+
}
|
|
316
|
+
case "remove": {
|
|
317
|
+
const nodes = entry === undefined ? null : withMarkers(entry);
|
|
318
|
+
if (nodes === null)
|
|
319
|
+
return [];
|
|
320
|
+
for (const node of nodes)
|
|
321
|
+
node.parentNode?.removeChild(node);
|
|
322
|
+
removed.set(patch.id, nodes);
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
case "restore": {
|
|
326
|
+
const nodes = removed.get(patch.id);
|
|
327
|
+
if (entry !== undefined || nodes === undefined || !place(nodes, patch.id, patch.index))
|
|
328
|
+
return [];
|
|
329
|
+
removed.delete(patch.id);
|
|
330
|
+
return [patch.id];
|
|
331
|
+
}
|
|
332
|
+
case "hide": {
|
|
333
|
+
if (entry === undefined || entry.hidden === patch.hidden)
|
|
334
|
+
return [];
|
|
335
|
+
const inside = between(entry);
|
|
336
|
+
if (inside === null)
|
|
337
|
+
return [];
|
|
338
|
+
if (patch.hidden) {
|
|
339
|
+
for (const node of inside)
|
|
340
|
+
node.parentNode?.removeChild(node);
|
|
341
|
+
hiddenNodes.set(entry.id, inside);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const kept = hiddenNodes.get(entry.id);
|
|
345
|
+
if (kept === undefined)
|
|
346
|
+
return [];
|
|
347
|
+
for (const node of kept)
|
|
348
|
+
entry.end.parentNode.insertBefore(node, entry.end);
|
|
349
|
+
hiddenNodes.delete(entry.id);
|
|
350
|
+
}
|
|
351
|
+
entry.start.data = previewStartMarker({ id: entry.id, type: entry.type, hidden: patch.hidden });
|
|
352
|
+
return patch.hidden ? [] : [entry.id];
|
|
353
|
+
}
|
|
354
|
+
case "text": {
|
|
355
|
+
const inside = entry === undefined ? null : between(entry);
|
|
356
|
+
const from = patch.from.trim();
|
|
357
|
+
if (inside === null || from === "" || typeof patch.to !== "string")
|
|
358
|
+
return [];
|
|
359
|
+
for (const node of textNodes(inside)) {
|
|
360
|
+
if (node.data.trim() !== from)
|
|
361
|
+
continue;
|
|
362
|
+
const lead = /^\s*/.exec(node.data)[0];
|
|
363
|
+
const trail = /\s*$/.exec(node.data)[0];
|
|
364
|
+
node.data = `${lead}${patch.to}${trail}`;
|
|
365
|
+
}
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
default:
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
/** Shows `patches` at once; a render already on its way would undo them, so it is dropped. */
|
|
373
|
+
const patchPage = (patches) => {
|
|
374
|
+
latest += 1;
|
|
375
|
+
const loaded = [];
|
|
376
|
+
for (const patch of patches) {
|
|
377
|
+
if (typeof patch !== "object" || patch === null || typeof patch.id !== "string")
|
|
378
|
+
continue;
|
|
379
|
+
loaded.push(...apply(patch));
|
|
380
|
+
marked = collect(doc.body);
|
|
381
|
+
}
|
|
382
|
+
for (const id of loaded)
|
|
383
|
+
doc.dispatchEvent(new win.CustomEvent(PREVIEW_SECTION_LOAD_EVENT, { detail: { id } }));
|
|
384
|
+
post({ type: "layout", instances: instances() });
|
|
385
|
+
};
|
|
232
386
|
const render = async (draft, revision) => {
|
|
233
387
|
const ticket = ++latest;
|
|
234
388
|
try {
|
|
@@ -276,6 +430,10 @@ export function startPreviewBridge(options) {
|
|
|
276
430
|
void render(message.draft, typeof message.revision === "number" ? message.revision : null);
|
|
277
431
|
}
|
|
278
432
|
break;
|
|
433
|
+
case "patch":
|
|
434
|
+
if (Array.isArray(message.patches))
|
|
435
|
+
patchPage(message.patches);
|
|
436
|
+
break;
|
|
279
437
|
case "mode":
|
|
280
438
|
if (message.mode === "edit" || message.mode === "interactive")
|
|
281
439
|
mode = message.mode;
|
|
@@ -56,10 +56,54 @@ 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`: a section of the document to `index` among the sections, counted
|
|
68
|
+
* once it has left its place (`moveInstance`).
|
|
69
|
+
* - `duplicate`: a copy of an instance right after it; `ids` maps every
|
|
70
|
+
* instance id inside it, its own included, to its copy's.
|
|
71
|
+
* - `remove`: an instance and everything in it; `restore` puts what a
|
|
72
|
+
* `remove` took back, as a section of the document at `index`.
|
|
73
|
+
* - `hide`: an instance switched off (it shows nothing) or back on.
|
|
74
|
+
* - `text`: inside an instance, text that reads `from` now reads `to` — a
|
|
75
|
+
* field's text as the theme prints it.
|
|
76
|
+
*/
|
|
77
|
+
export type PreviewPatch = {
|
|
78
|
+
op: "move";
|
|
79
|
+
id: string;
|
|
80
|
+
index: number;
|
|
81
|
+
} | {
|
|
82
|
+
op: "duplicate";
|
|
83
|
+
id: string;
|
|
84
|
+
ids: Readonly<Record<string, string>>;
|
|
85
|
+
} | {
|
|
86
|
+
op: "remove";
|
|
87
|
+
id: string;
|
|
88
|
+
} | {
|
|
89
|
+
op: "restore";
|
|
90
|
+
id: string;
|
|
91
|
+
index: number;
|
|
92
|
+
} | {
|
|
93
|
+
op: "hide";
|
|
94
|
+
id: string;
|
|
95
|
+
hidden: boolean;
|
|
96
|
+
} | {
|
|
97
|
+
op: "text";
|
|
98
|
+
id: string;
|
|
99
|
+
from: string;
|
|
100
|
+
to: string;
|
|
101
|
+
};
|
|
59
102
|
/**
|
|
60
103
|
* What the editor tells the preview page. `draft` is a `PreviewDraft` of
|
|
61
104
|
* `@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
|
|
105
|
+
* or `error` it causes, and a render answered after a newer one is dropped —
|
|
106
|
+
* as is one answered after a `patch`, which the next render follows.
|
|
63
107
|
*/
|
|
64
108
|
export type PreviewEditorMessage = {
|
|
65
109
|
source: "wocart-editor";
|
|
@@ -68,6 +112,9 @@ export type PreviewEditorMessage = {
|
|
|
68
112
|
type: "render";
|
|
69
113
|
draft: Record<string, unknown>;
|
|
70
114
|
revision?: number;
|
|
115
|
+
} | {
|
|
116
|
+
type: "patch";
|
|
117
|
+
patches: readonly PreviewPatch[];
|
|
71
118
|
} | {
|
|
72
119
|
type: "mode";
|
|
73
120
|
mode: PreviewMode;
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED