@jxsuite/studio 0.33.0 → 0.34.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.
Files changed (85) hide show
  1. package/dist/iframe-entry.js +6230 -0
  2. package/dist/iframe-entry.js.map +35 -0
  3. package/dist/studio.js +10754 -11060
  4. package/dist/studio.js.map +83 -72
  5. package/package.json +7 -7
  6. package/src/browse/browse.ts +11 -4
  7. package/src/canvas/canvas-helpers.ts +2 -56
  8. package/src/canvas/canvas-live-render.ts +102 -435
  9. package/src/canvas/canvas-origin.ts +66 -0
  10. package/src/canvas/canvas-patcher.ts +63 -403
  11. package/src/canvas/canvas-render.ts +70 -212
  12. package/src/canvas/canvas-utils.ts +37 -65
  13. package/src/canvas/iframe-channel.ts +154 -0
  14. package/src/canvas/iframe-drop.ts +484 -0
  15. package/src/canvas/iframe-entry.ts +600 -0
  16. package/src/canvas/iframe-host.ts +1373 -0
  17. package/src/canvas/iframe-inline-edit.ts +367 -0
  18. package/src/canvas/iframe-insert.ts +164 -0
  19. package/src/canvas/iframe-interaction.ts +176 -0
  20. package/src/canvas/iframe-keys.ts +85 -0
  21. package/src/canvas/iframe-overlay.ts +218 -0
  22. package/src/canvas/iframe-patch.ts +363 -0
  23. package/src/canvas/iframe-protocol.ts +361 -0
  24. package/src/canvas/iframe-render.ts +458 -0
  25. package/src/canvas/iframe-slash.ts +114 -0
  26. package/src/canvas/iframe-subtree.ts +113 -0
  27. package/src/canvas/path-mapping.ts +86 -0
  28. package/src/canvas/serialize-scope.ts +65 -0
  29. package/src/editor/canvas-context-menu.ts +40 -0
  30. package/src/editor/canvas-slash-bridge.ts +21 -0
  31. package/src/editor/context-menu.ts +2 -1
  32. package/src/editor/inline-edit-apply.ts +183 -0
  33. package/src/editor/inline-edit.ts +99 -21
  34. package/src/editor/inline-link.ts +89 -0
  35. package/src/editor/insert-zone-action.ts +35 -0
  36. package/src/editor/merge-tags.ts +26 -2
  37. package/src/editor/repeater-scope.ts +144 -0
  38. package/src/editor/shortcuts.ts +14 -28
  39. package/src/editor/slash-menu.ts +73 -42
  40. package/src/files/files.ts +2 -1
  41. package/src/page-params.ts +383 -0
  42. package/src/panels/ai-panel.ts +5 -7
  43. package/src/panels/block-action-bar.ts +296 -138
  44. package/src/panels/canvas-dnd-bridge.ts +397 -0
  45. package/src/panels/component-preview.ts +56 -0
  46. package/src/panels/dnd.ts +41 -17
  47. package/src/panels/drag-ghost.ts +62 -0
  48. package/src/panels/editors.ts +1 -1
  49. package/src/panels/overlays.ts +10 -125
  50. package/src/panels/properties-panel.ts +210 -0
  51. package/src/panels/right-panel.ts +0 -2
  52. package/src/panels/signals-panel.ts +136 -22
  53. package/src/panels/stylebook-doc.ts +373 -0
  54. package/src/panels/stylebook-panel.ts +46 -689
  55. package/src/panels/tab-bar.ts +159 -13
  56. package/src/panels/toolbar.ts +3 -2
  57. package/src/platforms/devserver.ts +15 -0
  58. package/src/services/monaco-setup.ts +12 -0
  59. package/src/services/render-critic.ts +9 -9
  60. package/src/settings/css-vars-editor.ts +2 -2
  61. package/src/store.ts +4 -62
  62. package/src/studio.ts +90 -40
  63. package/src/tabs/doc-op-apply.ts +89 -0
  64. package/src/tabs/patch-ops.ts +6 -2
  65. package/src/tabs/tab.ts +23 -4
  66. package/src/tabs/transact.ts +2 -74
  67. package/src/types.ts +14 -18
  68. package/src/ui/jx-theme.ts +63 -0
  69. package/src/ui/media-picker.ts +6 -4
  70. package/src/ui/spectrum.ts +5 -0
  71. package/src/utils/canvas-media.ts +0 -137
  72. package/src/utils/edit-display.ts +23 -3
  73. package/src/utils/geometry.ts +43 -0
  74. package/src/utils/link-target.ts +93 -0
  75. package/src/utils/strip-events.ts +54 -0
  76. package/src/view.ts +0 -23
  77. package/src/workspace/workspace.ts +14 -1
  78. package/src/canvas/canvas-subtree-render.ts +0 -113
  79. package/src/editor/component-inline-edit.ts +0 -349
  80. package/src/editor/content-inline-edit.ts +0 -207
  81. package/src/editor/insertion-helper.ts +0 -308
  82. package/src/panels/canvas-dnd.ts +0 -329
  83. package/src/panels/panel-events.ts +0 -306
  84. package/src/panels/preview-render.ts +0 -132
  85. package/src/panels/pseudo-preview.ts +0 -75
@@ -10,6 +10,19 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
10
10
  const mediaTags = new Set(["img", "video", "source", "iframe", "audio"]);
11
11
  const TRANSPARENT_PX =
12
12
  "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
13
+ // A neutral, visible gray placeholder for image-like bindings in edit mode (e.g. a component `image`
14
+ // Prop bound to ${...}). The component renders <img src={prop}> internally, so a transparent pixel
15
+ // Would collapse to nothing — a gray SVG box reads as an intentional placeholder instead.
16
+ const PLACEHOLDER_IMG =
17
+ "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='32'%20height='32'%3E%3Crect%20width='32'%20height='32'%20fill='%23d0d0d0'/%3E%3C/svg%3E";
18
+
19
+ // Prop/attribute names that feed an <img>/background, so a ${...} binding would otherwise render a
20
+ // Broken image in edit mode. Matched by exact name or common camelCase suffix (featuredImage, heroBg).
21
+ const IMAGE_KEY_RE =
22
+ /(src|poster|image|bg|background|icon|logo|photo|avatar|thumbnail|cover|banner)$/i;
23
+ function isImageUrlKey(key: string): boolean {
24
+ return IMAGE_KEY_RE.test(key);
25
+ }
13
26
 
14
27
  const textTags = new Set([
15
28
  "p",
@@ -197,8 +210,15 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
197
210
  const propsOut: Record<string, unknown> = {};
198
211
  for (const [pk, pv] of Object.entries(v)) {
199
212
  if (typeof pv === "string" && pv.includes("${")) {
200
- const isUrlAttr = pk === "src" || pk === "href" || pk === "poster" || pk === "action";
201
- propsOut[pk] = isUrlAttr ? "" : templateToEditDisplay(pv);
213
+ // Image-like prop neutral placeholder (the component renders it as an <img src>); link
214
+ // Targets inert ""; everything else → the readable ❪ expr ❫ binding text.
215
+ if (isImageUrlKey(pk)) {
216
+ propsOut[pk] = PLACEHOLDER_IMG;
217
+ } else if (pk === "href" || pk === "action") {
218
+ propsOut[pk] = "";
219
+ } else {
220
+ propsOut[pk] = templateToEditDisplay(pv);
221
+ }
202
222
  } else if (pv && typeof pv === "object" && (pv as Record<string, unknown>).$ref) {
203
223
  const ref = (pv as Record<string, unknown>).$ref as string;
204
224
  const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
@@ -238,7 +258,7 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
238
258
  border: "1px dashed color-mix(in srgb, var(--danger) 40%, transparent)",
239
259
  borderRadius: "4px",
240
260
  color: "var(--danger)",
241
- fontFamily: "'SF Mono', 'Fira Code', monospace",
261
+ fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
242
262
  fontSize: "11px",
243
263
  fontStyle: "italic",
244
264
  padding: "6px 10px",
@@ -0,0 +1,43 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Geometry read funnel — the single place the studio reads element bounds and does point hit-tests.
4
+ *
5
+ * Centralizing these reads is a seam for the iframe-canvas migration: today every call delegates
6
+ * straight to the DOM, but routing them through one module lets later phases redirect
7
+ * CANVAS-element geometry to rects posted out of the iframe (which the parent can't measure
8
+ * directly) without touching dozens of call sites. A test (`geometry.test.ts`) asserts no raw
9
+ * `getBoundingClientRect`/`elementFromPoint`/`elementsFromPoint` calls exist elsewhere in `src`.
10
+ */
11
+
12
+ /** Bounding rect of an element, in viewport coordinates. */
13
+ export function rectOf(el: Element): DOMRect {
14
+ return el.getBoundingClientRect();
15
+ }
16
+
17
+ /**
18
+ * Bounding rect of a Range, in viewport coordinates. Lives here (the one allowlisted home for
19
+ * `getBoundingClientRect`) so the `geometry.test.ts` textual guard passes — the iframe-side
20
+ * selection snapshot funnels its caret/selection geometry through this, never calling
21
+ * `range.getBoundingClientRect()` directly.
22
+ */
23
+ export function rectOfRange(r: Range): DOMRect {
24
+ return r.getBoundingClientRect();
25
+ }
26
+
27
+ /** Topmost element at a viewport point, in `root` (defaults to the global document). */
28
+ export function elementAtPoint(
29
+ x: number,
30
+ y: number,
31
+ root: Document | ShadowRoot = document,
32
+ ): Element | null {
33
+ return root.elementFromPoint(x, y);
34
+ }
35
+
36
+ /** All elements at a viewport point, front-to-back, in `root` (defaults to the global document). */
37
+ export function elementsAtPoint(
38
+ x: number,
39
+ y: number,
40
+ root: Document | ShadowRoot = document,
41
+ ): Element[] {
42
+ return root.elementsFromPoint(x, y);
43
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Link-target.ts — pure, dependency-free helpers for classifying and composing anchor `href`
3
+ * targets in the properties panel's Link section.
4
+ *
5
+ * `classifyHref` splits a raw href string into a discriminated `{ kind, value }` pair so the UI can
6
+ * offer a target-kind selector (Internal page / External URL / #anchor / mailto: / tel:).
7
+ * `composeHref` is its inverse — the two round-trip for every kind so editing through the UI never
8
+ * corrupts an href.
9
+ *
10
+ * Both functions are total (never throw) and have no DOM or platform dependencies, so they are
11
+ * fully unit-testable in isolation.
12
+ */
13
+
14
+ /** The distinct target kinds an anchor href can address. */
15
+ export type LinkKind = "internal" | "external" | "anchor" | "mailto" | "tel";
16
+
17
+ /** A classified href: its {@link LinkKind} plus the "bare" value for that kind's input. */
18
+ export interface LinkTarget {
19
+ kind: LinkKind;
20
+ /**
21
+ * The user-facing value for the kind's input: - internal → the path (e.g. `/about/`) - external →
22
+ * the full href (e.g. `https://x.com`) - anchor → the fragment WITHOUT the leading `#` (e.g.
23
+ * `section`) - mailto → the email address WITHOUT the `mailto:` scheme - tel → the phone number
24
+ * WITHOUT the `tel:` scheme
25
+ */
26
+ value: string;
27
+ }
28
+
29
+ /** Matches an absolute URL scheme like `http://`, `https://`, `ftp://`, or protocol-relative `//`. */
30
+ const ABSOLUTE_SCHEME = /^([a-z][a-z\d+.-]*:)?\/\//i;
31
+
32
+ /**
33
+ * Classify a raw `href` string into a {@link LinkTarget}.
34
+ *
35
+ * Rules (checked in order): 1. `mailto:…` → `{ kind: "mailto", value: address }` 2. `tel:…` → `{
36
+ * kind: "tel", value: number }` 3. `#…` → `{ kind: "anchor", value: rest }` (leading `#` stripped)
37
+ * 4. absolute (`http(s)://…`) or protocol-relative (`//…`) → `{ kind: "external", value: href }` 5.
38
+ * `/…` → `{ kind: "internal", value: path }` 6. anything else → `{ kind: "external", value: href
39
+ * }`
40
+ *
41
+ * @param href The raw attribute value (may be empty)
42
+ */
43
+ export function classifyHref(href = ""): LinkTarget {
44
+ const value = href;
45
+ const lower = value.toLowerCase();
46
+
47
+ if (lower.startsWith("mailto:")) {
48
+ return { kind: "mailto", value: value.slice("mailto:".length) };
49
+ }
50
+ if (lower.startsWith("tel:")) {
51
+ return { kind: "tel", value: value.slice("tel:".length) };
52
+ }
53
+ if (value.startsWith("#")) {
54
+ return { kind: "anchor", value: value.slice(1) };
55
+ }
56
+ if (ABSOLUTE_SCHEME.test(value)) {
57
+ return { kind: "external", value };
58
+ }
59
+ if (value.startsWith("/")) {
60
+ return { kind: "internal", value };
61
+ }
62
+ return { kind: "external", value };
63
+ }
64
+
65
+ /**
66
+ * Compose an `href` string from a {@link LinkKind} and its bare value — the inverse of
67
+ * {@link classifyHref}. An empty (or whitespace-only) value yields `""` for every kind so callers
68
+ * can treat "cleared" uniformly. Round-trips with `classifyHref` for every kind.
69
+ *
70
+ * @param kind The target kind
71
+ * @param value The bare value for that kind (address/number/fragment/path/href)
72
+ */
73
+ export function composeHref(kind: LinkKind, value: string): string {
74
+ const bare = (value ?? "").trim();
75
+ if (!bare) {
76
+ return "";
77
+ }
78
+ switch (kind) {
79
+ case "mailto": {
80
+ return `mailto:${bare}`;
81
+ }
82
+ case "tel": {
83
+ return `tel:${bare}`;
84
+ }
85
+ case "anchor": {
86
+ return bare.startsWith("#") ? bare : `#${bare}`;
87
+ }
88
+ // Internal / external — the bare value IS the href.
89
+ default: {
90
+ return bare;
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Strip `on*` event-handler bindings from a Jx document tree (returns a shallow-rebuilt copy). Used
3
+ * to neutralize handlers before edit/design rendering so the canvas never wires live behavior, and
4
+ * to drop `timing: "server"` state entries the canvas can't run.
5
+ *
6
+ * Lives in its own dependency-light module (it pulls only the pure `@jxsuite/schema/guards`
7
+ * predicate) so the slim canvas-iframe subtree renderer can import it without dragging in the
8
+ * editor's reactive `store.ts`; `store.ts` re-exports it for the legacy in-realm patcher.
9
+ */
10
+
11
+ import { isEventBinding } from "@jxsuite/schema/guards";
12
+ import type { JxMutableNode } from "@jxsuite/schema/types";
13
+
14
+ /** Strip all `on*` event-handler properties (and server-timed state) from a document tree. */
15
+ export function stripEventHandlers(node: JxMutableNode): JxMutableNode {
16
+ if (!node || typeof node !== "object") {
17
+ return node;
18
+ }
19
+ if (Array.isArray(node)) {
20
+ // Arrays of nodes round-trip element-wise; the array itself is not a node.
21
+ return node.map((n: JxMutableNode) => stripEventHandlers(n)) as unknown as JxMutableNode;
22
+ }
23
+ const out: Record<string, unknown> = {};
24
+ for (const [k, v] of Object.entries(node)) {
25
+ if (k.startsWith("on") && isEventBinding(v)) {
26
+ continue;
27
+ }
28
+ if (k === "children") {
29
+ out.children = Array.isArray(v)
30
+ ? v.map((c: JxMutableNode) => stripEventHandlers(c))
31
+ : stripEventHandlers(v as JxMutableNode);
32
+ } else if (k === "cases" && typeof v === "object") {
33
+ const cases: Record<string, unknown> = {};
34
+ for (const [ck, cv] of Object.entries(v as Record<string, unknown>)) {
35
+ cases[ck] = stripEventHandlers(cv as JxMutableNode);
36
+ }
37
+ out.cases = cases;
38
+ } else if (k === "state" && typeof v === "object" && v !== null) {
39
+ const state: Record<string, unknown> = {};
40
+ for (const [sk, sv] of Object.entries(v as Record<string, unknown>)) {
41
+ if (sv && typeof sv === "object" && (sv as Record<string, unknown>).timing === "server") {
42
+ continue;
43
+ }
44
+ state[sk] = sv;
45
+ }
46
+ out.state = state;
47
+ } else if (k === "style" || k === "attributes" || k === "$media") {
48
+ out[k] = v;
49
+ } else {
50
+ out[k] = v;
51
+ }
52
+ }
53
+ return out;
54
+ }
package/src/view.ts CHANGED
@@ -28,15 +28,6 @@ interface ViewState {
28
28
  _editingTarget?: string | null;
29
29
  })
30
30
  | null;
31
- componentInlineEdit: {
32
- el: HTMLElement;
33
- path: (string | number)[];
34
- originalText: string;
35
- mediaName: string | null;
36
- _outsideHandler?: ((e: MouseEvent) => void) | null;
37
- [k: string]: unknown;
38
- } | null;
39
- inlineEditCleanup: (() => void) | null;
40
31
  blockActionBarEl: HTMLElement | null;
41
32
  selDragCleanup: (() => void) | null;
42
33
  dndCleanups: (() => void)[];
@@ -46,16 +37,9 @@ interface ViewState {
46
37
  forcedAttrEl: HTMLElement | null;
47
38
  elementsCollapsed: Set<string>;
48
39
  elementsFilter: string;
49
- lastDragInput: {
50
- clientX: number;
51
- clientY: number;
52
- [k: string]: unknown;
53
- } | null;
54
40
  _currentDropTargetRow: HTMLElement | null;
55
41
  layerDragSourceHeight: number;
56
- savedRange: Range | null;
57
42
  _completionRegistered: boolean;
58
- stylebookElToTag: WeakMap<Element, string>;
59
43
  showAddBreakpointForm: boolean;
60
44
  addBreakpointPreview: string;
61
45
  layoutSelection: unknown;
@@ -81,10 +65,6 @@ export const view: ViewState = {
81
65
  monacoEditor: null,
82
66
  functionEditor: null,
83
67
 
84
- // Inline editing
85
- componentInlineEdit: null,
86
- inlineEditCleanup: null,
87
-
88
68
  // Floating UI containers
89
69
  blockActionBarEl: null,
90
70
 
@@ -105,16 +85,13 @@ export const view: ViewState = {
105
85
  elementsFilter: "",
106
86
 
107
87
  // Drag interaction
108
- lastDragInput: null,
109
88
  _currentDropTargetRow: null,
110
89
  layerDragSourceHeight: 0,
111
90
 
112
91
  // Editor state
113
- savedRange: null,
114
92
  _completionRegistered: false,
115
93
 
116
94
  // Canvas / stylebook
117
- stylebookElToTag: new WeakMap(),
118
95
 
119
96
  // Responsive breakpoints UI
120
97
  showAddBreakpointForm: false,
@@ -1,4 +1,4 @@
1
- import { computed, reactive } from "../reactivity";
1
+ import { computed, reactive, toRaw } from "../reactivity";
2
2
  import { createTab, disposeTab } from "../tabs/tab";
3
3
  import type { Tab } from "../tabs/tab";
4
4
 
@@ -56,6 +56,19 @@ export const activeTab = computed(() =>
56
56
  workspace.activeTabId ? (workspace.tabs.get(workspace.activeTabId) ?? null) : null,
57
57
  ) as unknown as ComputedRef<Tab | null>;
58
58
 
59
+ /**
60
+ * Whether `tab` is the active tab. Identity check survives reactive-proxy wrapping (activeTab.value
61
+ * is a proxy; callers may hold either the raw tab or a proxy of it).
62
+ *
63
+ * @param {Tab | null} tab
64
+ */
65
+ export function isTabActive(tab: Tab | null): boolean {
66
+ const active = activeTab.value;
67
+ return (
68
+ tab !== null && active !== null && toRaw(active as object) === toRaw(tab as unknown as object)
69
+ );
70
+ }
71
+
59
72
  /**
60
73
  * Open a new tab and make it active.
61
74
  *
@@ -1,113 +0,0 @@
1
- /// <reference lib="dom" />
2
- /// <reference lib="dom.iterable" />
3
- /**
4
- * Isolated subtree rendering for surgical canvas patches. Re-renders a single document node into
5
- * fresh DOM using the same edit-mode transform, runtime renderer, scope, and path mapper as the
6
- * panel's last full render — so the patched DOM is indistinguishable from a full re-render.
7
- */
8
-
9
- import { elToRenderScope, elToScope, getNodeAtPath, stripEventHandlers } from "../store";
10
- import { prepareForEditMode } from "../utils/edit-display";
11
- import { renderNode as runtimeRenderNode } from "@jxsuite/runtime";
12
- import { effectScope } from "../reactivity";
13
- import { canvasPerf } from "./canvas-perf";
14
-
15
- import type { JxPath } from "../state";
16
- import type { JxMutableNode } from "@jxsuite/schema/types";
17
- import type { CanvasPanel, PanelLiveCtx } from "../types";
18
-
19
- /**
20
- * Render the document node at docPath into a detached DOM subtree for the given panel.
21
- *
22
- * The scope comes from the parent element's recorded render scope (elToScope), falling back to the
23
- * panel's root scope — so $-bindings, $media, and state references resolve exactly as they did in
24
- * the full render. Throws when the panel has no live render context.
25
- *
26
- * @param {CanvasPanel} panel
27
- * @param {JxMutableNode} doc Raw (non-reactive) document root
28
- * @param {JxPath} docPath
29
- * @param {Element} parentEl The rendered parent element (scope source)
30
- */
31
- export function renderSubtree(
32
- panel: CanvasPanel,
33
- doc: JxMutableNode,
34
- docPath: JxPath,
35
- parentEl: Element,
36
- ): HTMLElement | Text {
37
- const { liveCtx } = panel;
38
- if (!liveCtx) {
39
- throw new Error("panel-missing-live-ctx");
40
- }
41
- const node = getNodeAtPath(doc, docPath) as JxMutableNode | string | undefined;
42
- if (node === undefined) {
43
- throw new Error(`node-not-found:${docPath.join("/")}`);
44
- }
45
- const def = typeof node === "string" ? node : prepareForEditMode(stripEventHandlers(node));
46
- const scope = elToScope.get(parentEl) ?? liveCtx.scope;
47
-
48
- // Render inside an effect scope parented to the panel's render scope: disposable on its own
49
- // When the subtree is later replaced/removed, and disposed with the panel on full renders.
50
- const render = () => {
51
- const subScope = effectScope();
52
- const rendered = subScope.run(() =>
53
- runtimeRenderNode(def, scope, {
54
- _path: docPathToRenderPath(docPath, liveCtx),
55
- onNodeCreated: liveCtx.pathMapper,
56
- }),
57
- )!;
58
- if (rendered instanceof HTMLElement) {
59
- elToRenderScope.set(rendered, subScope);
60
- }
61
- return rendered;
62
- };
63
- const el = panel.renderScope ? panel.renderScope.run(render)! : render();
64
- canvasPerf.subtreeRenders += 1;
65
-
66
- // Match full-render edit display: canvas content never receives pointer events directly.
67
- if (el instanceof HTMLElement) {
68
- el.style.pointerEvents = "none";
69
- for (const child of el.querySelectorAll("*")) {
70
- (child as HTMLElement).style.pointerEvents = "none";
71
- }
72
- }
73
- return el;
74
- }
75
-
76
- /**
77
- * Inverse of makePathMapper's layout-prefix strip: document paths are relative to the page doc,
78
- * render paths to the layout-merged doc. ($map/$switch paths never reach here — the patcher
79
- * escalates them.)
80
- *
81
- * @param {JxPath} docPath
82
- * @param {PanelLiveCtx} liveCtx
83
- */
84
- function docPathToRenderPath(docPath: JxPath, liveCtx: PanelLiveCtx): (string | number)[] {
85
- if (liveCtx.layoutWrapped && liveCtx.pageContentPrefix && docPath[0] === "children") {
86
- // Re-apply the slot-container offset stripped by makePathMapper: page child index N renders at
87
- // Container index N + offset (offset = leading layout siblings before the <slot>).
88
- const [, idx] = docPath;
89
- return typeof idx === "number"
90
- ? [...liveCtx.pageContentPrefix, idx + (liveCtx.pageContentOffset ?? 0), ...docPath.slice(2)]
91
- : [...liveCtx.pageContentPrefix, ...docPath.slice(1)];
92
- }
93
- return docPath;
94
- }
95
-
96
- /**
97
- * The DOM node currently occupying children[index] of the rendered parent, or null when appending
98
- * at the end. The runtime renders an optional leading text node (from textContent) followed by one
99
- * DOM node per children entry, in order.
100
- *
101
- * @param {Element} parentEl
102
- * @param {JxMutableNode} parentNode
103
- * @param {number} index
104
- */
105
- export function domChildReference(
106
- parentEl: Element,
107
- parentNode: JxMutableNode,
108
- index: number,
109
- ): ChildNode | null {
110
- const text = parentNode.textContent;
111
- const textOffset = text != null && String(text) !== "" ? 1 : 0;
112
- return parentEl.childNodes[textOffset + index] ?? null;
113
- }