@avocadostudio-ai/preview-adapter 0.21.1 → 0.23.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/NOTICE +8 -0
- package/dist/bridge-controller.js +15 -0
- package/dist/bridge-functions.d.ts +64 -0
- package/dist/bridge-functions.js +137 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +5 -3
- package/src/styles.css +6 -4
package/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Avocado Studio
|
|
2
|
+
Copyright 2026 Avocado Studio Contributors
|
|
3
|
+
|
|
4
|
+
This product includes software developed by the Avocado Studio
|
|
5
|
+
Contributors (https://www.avocadostudio.dev).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0. See LICENSE for the
|
|
8
|
+
full license text.
|
|
@@ -120,6 +120,21 @@ export function attachBridge(config) {
|
|
|
120
120
|
}
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
|
+
/*
|
|
124
|
+
* Which block types the site renders at a fixed position. The editor holds
|
|
125
|
+
* the site's manifest and the page does not, so it says so — on mount (with
|
|
126
|
+
* `setSelectionMode`, since postMessage has no buffering) and whenever the
|
|
127
|
+
* manifest changes. A selected block's controls are rebuilt at once, so a
|
|
128
|
+
* message that lands after the click still takes effect.
|
|
129
|
+
*/
|
|
130
|
+
if (msg.type === "setBlockConstraints") {
|
|
131
|
+
const types = Array.isArray(msg.payload.fixedBlockTypes) ? msg.payload.fixedBlockTypes : [];
|
|
132
|
+
state.fixedBlockTypes = new Set(types.filter((type) => typeof type === "string"));
|
|
133
|
+
if (state.selectedBlockId) {
|
|
134
|
+
bridge.applyBlockFocus(state.selectedBlockId, false, state.selectedEditablePath ?? undefined, { scrollIntoView: false });
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
123
138
|
if (msg.type === "applyPatch") {
|
|
124
139
|
if (state.serverVersion !== msg.fromVersion) {
|
|
125
140
|
emitPatchAck(msg.txId, false, "version_mismatch");
|
|
@@ -157,6 +157,17 @@ export declare function parseListItemPath(editablePath?: string | null): {
|
|
|
157
157
|
listKey: string;
|
|
158
158
|
index: number;
|
|
159
159
|
} | null;
|
|
160
|
+
/**
|
|
161
|
+
* Can a text edit at this path be committed?
|
|
162
|
+
*
|
|
163
|
+
* Three shapes: a top-level field (`title`), a field of a list row
|
|
164
|
+
* (`items[1].title`), and an entry of a list of strings (`paragraphs[1]`,
|
|
165
|
+
* `kind: "stringList"`). The third was missing, so an entry marked on the page
|
|
166
|
+
* selected with the right path and then refused the double-click — the one
|
|
167
|
+
* kind of list whose rows *are* text could not be typed into. The editor writes
|
|
168
|
+
* it back as the whole list with that entry replaced, and refuses when the
|
|
169
|
+
* entry turns out to be an object rather than a string.
|
|
170
|
+
*/
|
|
160
171
|
export declare function supportsInlineEditablePath(editablePath: string): boolean;
|
|
161
172
|
export declare function readNodeText(node: HTMLElement): string;
|
|
162
173
|
export declare function placeCaretAtEnd(node: HTMLElement): void;
|
|
@@ -169,6 +180,50 @@ export declare function computeMoveAfter(blockId: string, direction: "up" | "dow
|
|
|
169
180
|
canMove: boolean;
|
|
170
181
|
afterBlockId: string | null;
|
|
171
182
|
};
|
|
183
|
+
/**
|
|
184
|
+
* Whether the blocks directly above and below position `idx` are fixed.
|
|
185
|
+
*
|
|
186
|
+
* Moving a block up swaps it with the one above; if that one is fixed — a
|
|
187
|
+
* section the site always draws in the same place — the swap moves the fixed
|
|
188
|
+
* block too, and the ops engine refuses it. So the button is disabled rather
|
|
189
|
+
* than offered and refused. Pure, over one flag per block in page order, so it
|
|
190
|
+
* can be tested without a DOM.
|
|
191
|
+
*/
|
|
192
|
+
export declare function fixedNeighborFlags(fixedFlags: readonly boolean[], idx: number): {
|
|
193
|
+
above: boolean;
|
|
194
|
+
below: boolean;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Does a click on a link here select the block rather than follow the link?
|
|
198
|
+
*
|
|
199
|
+
* In selection mode, yes — when the link sits in a block. The bridge used to
|
|
200
|
+
* route every same-origin link click through the preview's navigation before
|
|
201
|
+
* selection had a look, so on a site whose CTAs, nav items and footer contact
|
|
202
|
+
* details are all anchors, those could only be reached through the property
|
|
203
|
+
* panel, and a block made of nothing but links could not be selected by click
|
|
204
|
+
* at all. With selection mode off the preview is a page to browse, and links
|
|
205
|
+
* navigate as before.
|
|
206
|
+
*
|
|
207
|
+
* Chrome is the exception. A block marked `data-block-chrome` — the built-in
|
|
208
|
+
* site header and footer — is the site's navigation, drawn so that it keeps
|
|
209
|
+
* behaving like it in the editor; its links still navigate, as its other
|
|
210
|
+
* controls still work.
|
|
211
|
+
*/
|
|
212
|
+
export declare function linkClickSelects(target: Element, selectionModeOn: boolean): boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Is this block node fixed in place?
|
|
215
|
+
*
|
|
216
|
+
* The editor says which *types* are fixed — it holds the site's manifest, the
|
|
217
|
+
* page does not — with a `setBlockConstraints` message. A site may also mark a
|
|
218
|
+
* node itself with `data-block-fixed`, the way chrome is marked with
|
|
219
|
+
* `data-block-chrome`.
|
|
220
|
+
*/
|
|
221
|
+
export declare function isFixedBlockNode(node: HTMLElement, fixedTypes: ReadonlySet<string>): boolean;
|
|
222
|
+
/** `fixedNeighborFlags` for the block on the page, in the order the page draws it. */
|
|
223
|
+
export declare function fixedNeighbors(blockId: string, fixedTypes: ReadonlySet<string>, preferredNode?: HTMLElement | null): {
|
|
224
|
+
above: boolean;
|
|
225
|
+
below: boolean;
|
|
226
|
+
};
|
|
172
227
|
export declare function computeInsertBefore(blockId: string, preferredNode?: HTMLElement | null): {
|
|
173
228
|
afterBlockId: string | null;
|
|
174
229
|
beforeBlockId: string;
|
|
@@ -356,6 +411,8 @@ export type BridgeState = {
|
|
|
356
411
|
editablePath: string;
|
|
357
412
|
initialValue: string;
|
|
358
413
|
isMultiline: boolean;
|
|
414
|
+
/** Overlay controls lifted out of the node for the edit — see `startInlineEdit`. */
|
|
415
|
+
liftedControls: Element[];
|
|
359
416
|
} | null;
|
|
360
417
|
serverVersion: number;
|
|
361
418
|
liveDraftActiveBlockId: string | null;
|
|
@@ -371,6 +428,13 @@ export type BridgeState = {
|
|
|
371
428
|
editablePath: string;
|
|
372
429
|
scope: AiLoadingScope;
|
|
373
430
|
} | null;
|
|
431
|
+
/**
|
|
432
|
+
* Block types the site renders at a fixed position, from the editor's
|
|
433
|
+
* `setBlockConstraints`. Such a block gets no move, delete or add controls,
|
|
434
|
+
* and its neighbours cannot be moved across it. Empty until the editor says
|
|
435
|
+
* otherwise, so an older editor changes nothing.
|
|
436
|
+
*/
|
|
437
|
+
fixedBlockTypes: Set<string>;
|
|
374
438
|
};
|
|
375
439
|
export declare function createBridgeState(): BridgeState;
|
|
376
440
|
export type BridgeFunctions = ReturnType<typeof createBridgeFunctions>;
|
package/dist/bridge-functions.js
CHANGED
|
@@ -421,10 +421,21 @@ export function parseListItemPath(editablePath) {
|
|
|
421
421
|
return null;
|
|
422
422
|
return { listKey: match[1], index: Number(match[2]) };
|
|
423
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* Can a text edit at this path be committed?
|
|
426
|
+
*
|
|
427
|
+
* Three shapes: a top-level field (`title`), a field of a list row
|
|
428
|
+
* (`items[1].title`), and an entry of a list of strings (`paragraphs[1]`,
|
|
429
|
+
* `kind: "stringList"`). The third was missing, so an entry marked on the page
|
|
430
|
+
* selected with the right path and then refused the double-click — the one
|
|
431
|
+
* kind of list whose rows *are* text could not be typed into. The editor writes
|
|
432
|
+
* it back as the whole list with that entry replaced, and refuses when the
|
|
433
|
+
* entry turns out to be an object rather than a string.
|
|
434
|
+
*/
|
|
424
435
|
export function supportsInlineEditablePath(editablePath) {
|
|
425
436
|
if (!editablePath)
|
|
426
437
|
return false;
|
|
427
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\[[0-9]+\]
|
|
438
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\[[0-9]+\](?:\.[A-Za-z_][A-Za-z0-9_]*)?)?$/.test(editablePath))
|
|
428
439
|
return false;
|
|
429
440
|
if (/(^|\.)(?:ctaHref|secondaryCtaHref|imageUrl|imageAlt|href|url)$/i.test(editablePath))
|
|
430
441
|
return false;
|
|
@@ -432,6 +443,8 @@ export function supportsInlineEditablePath(editablePath) {
|
|
|
432
443
|
return false;
|
|
433
444
|
return true;
|
|
434
445
|
}
|
|
446
|
+
/** What the overlay itself mounts inside a marked element, as opposed to the site's markup. */
|
|
447
|
+
const OVERLAY_CHILD_SELECTOR = ".editor-list-item-controls, .editor-list-item-add, .editor-list-item-delete, .editor-list-item-move, .editor-delete-confirm";
|
|
435
448
|
export function readNodeText(node) {
|
|
436
449
|
/*
|
|
437
450
|
* `innerText` is the *rendered* text, and this value is what gets committed
|
|
@@ -515,6 +528,69 @@ export function computeMoveAfter(blockId, direction, preferredNode) {
|
|
|
515
528
|
return { canMove: false, afterBlockId: null };
|
|
516
529
|
return { canMove: true, afterBlockId: order[idx + 1] ?? null };
|
|
517
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Whether the blocks directly above and below position `idx` are fixed.
|
|
533
|
+
*
|
|
534
|
+
* Moving a block up swaps it with the one above; if that one is fixed — a
|
|
535
|
+
* section the site always draws in the same place — the swap moves the fixed
|
|
536
|
+
* block too, and the ops engine refuses it. So the button is disabled rather
|
|
537
|
+
* than offered and refused. Pure, over one flag per block in page order, so it
|
|
538
|
+
* can be tested without a DOM.
|
|
539
|
+
*/
|
|
540
|
+
export function fixedNeighborFlags(fixedFlags, idx) {
|
|
541
|
+
if (idx < 0 || idx >= fixedFlags.length)
|
|
542
|
+
return { above: false, below: false };
|
|
543
|
+
return {
|
|
544
|
+
above: idx > 0 && fixedFlags[idx - 1] === true,
|
|
545
|
+
below: idx < fixedFlags.length - 1 && fixedFlags[idx + 1] === true
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Does a click on a link here select the block rather than follow the link?
|
|
550
|
+
*
|
|
551
|
+
* In selection mode, yes — when the link sits in a block. The bridge used to
|
|
552
|
+
* route every same-origin link click through the preview's navigation before
|
|
553
|
+
* selection had a look, so on a site whose CTAs, nav items and footer contact
|
|
554
|
+
* details are all anchors, those could only be reached through the property
|
|
555
|
+
* panel, and a block made of nothing but links could not be selected by click
|
|
556
|
+
* at all. With selection mode off the preview is a page to browse, and links
|
|
557
|
+
* navigate as before.
|
|
558
|
+
*
|
|
559
|
+
* Chrome is the exception. A block marked `data-block-chrome` — the built-in
|
|
560
|
+
* site header and footer — is the site's navigation, drawn so that it keeps
|
|
561
|
+
* behaving like it in the editor; its links still navigate, as its other
|
|
562
|
+
* controls still work.
|
|
563
|
+
*/
|
|
564
|
+
export function linkClickSelects(target, selectionModeOn) {
|
|
565
|
+
if (!selectionModeOn)
|
|
566
|
+
return false;
|
|
567
|
+
const block = target.closest("[data-block-id]");
|
|
568
|
+
if (!block)
|
|
569
|
+
return false;
|
|
570
|
+
return !(block.matches("[data-block-chrome]") || block.querySelector("[data-block-chrome]") !== null);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Is this block node fixed in place?
|
|
574
|
+
*
|
|
575
|
+
* The editor says which *types* are fixed — it holds the site's manifest, the
|
|
576
|
+
* page does not — with a `setBlockConstraints` message. A site may also mark a
|
|
577
|
+
* node itself with `data-block-fixed`, the way chrome is marked with
|
|
578
|
+
* `data-block-chrome`.
|
|
579
|
+
*/
|
|
580
|
+
export function isFixedBlockNode(node, fixedTypes) {
|
|
581
|
+
if (node.hasAttribute("data-block-fixed"))
|
|
582
|
+
return true;
|
|
583
|
+
const type = node.getAttribute("data-block-type");
|
|
584
|
+
return type !== null && fixedTypes.has(type);
|
|
585
|
+
}
|
|
586
|
+
/** `fixedNeighborFlags` for the block on the page, in the order the page draws it. */
|
|
587
|
+
export function fixedNeighbors(blockId, fixedTypes, preferredNode) {
|
|
588
|
+
const nodes = orderedBlockNodes();
|
|
589
|
+
let idx = preferredNode && preferredNode.isConnected ? nodes.indexOf(preferredNode) : -1;
|
|
590
|
+
if (idx === -1)
|
|
591
|
+
idx = nodes.findIndex((node) => node.getAttribute("data-block-id") === blockId);
|
|
592
|
+
return fixedNeighborFlags(nodes.map((node) => isFixedBlockNode(node, fixedTypes)), idx);
|
|
593
|
+
}
|
|
518
594
|
export function computeInsertBefore(blockId, preferredNode) {
|
|
519
595
|
const { idx, order } = blockOrderIndex(blockId, preferredNode);
|
|
520
596
|
if (idx <= 0)
|
|
@@ -837,6 +913,7 @@ export function createBridgeState() {
|
|
|
837
913
|
hoveredBlockRoot: null,
|
|
838
914
|
observer: null,
|
|
839
915
|
activeShimmer: null,
|
|
916
|
+
fixedBlockTypes: new Set(),
|
|
840
917
|
};
|
|
841
918
|
}
|
|
842
919
|
/**
|
|
@@ -1089,6 +1166,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1089
1166
|
s.node.textContent = s.initialValue;
|
|
1090
1167
|
s.node.setAttribute("contenteditable", "false");
|
|
1091
1168
|
s.node.classList.remove("editor-inline-editing");
|
|
1169
|
+
s.node.append(...s.liftedControls);
|
|
1092
1170
|
state.inlineEditing = null;
|
|
1093
1171
|
};
|
|
1094
1172
|
const commitInlineEdit = () => {
|
|
@@ -1099,6 +1177,7 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1099
1177
|
s.node.classList.remove("editor-inline-editing");
|
|
1100
1178
|
state.inlineEditing = null;
|
|
1101
1179
|
const nextValue = readNodeText(s.node);
|
|
1180
|
+
s.node.append(...s.liftedControls);
|
|
1102
1181
|
if (nextValue === s.initialValue)
|
|
1103
1182
|
return;
|
|
1104
1183
|
callbacks.onInlineTextCommitted({ slug, blockId: s.blockId, blockType: s.blockType, editablePath: s.editablePath, value: nextValue });
|
|
@@ -1108,13 +1187,22 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1108
1187
|
return;
|
|
1109
1188
|
if (!isInlineEditable(args.node))
|
|
1110
1189
|
return;
|
|
1111
|
-
if (args.node.children.
|
|
1190
|
+
if (Array.from(args.node.children).some((child) => !child.matches(OVERLAY_CHILD_SELECTOR)))
|
|
1112
1191
|
return;
|
|
1113
1192
|
const existing = state.inlineEditing;
|
|
1114
1193
|
if (existing?.node === args.node)
|
|
1115
1194
|
return;
|
|
1116
1195
|
if (existing)
|
|
1117
1196
|
commitInlineEdit();
|
|
1197
|
+
/*
|
|
1198
|
+
* A `stringList` entry is both the field and its list row, so the row's
|
|
1199
|
+
* move/delete/add controls are mounted *inside* the text being edited.
|
|
1200
|
+
* Lift them out for the edit — typed into, a select-all deletes them, and
|
|
1201
|
+
* the value read back would include them — and put them back after.
|
|
1202
|
+
*/
|
|
1203
|
+
const liftedControls = Array.from(args.node.children);
|
|
1204
|
+
for (const control of liftedControls)
|
|
1205
|
+
control.remove();
|
|
1118
1206
|
const initialValue = readNodeText(args.node);
|
|
1119
1207
|
state.inlineEditing = {
|
|
1120
1208
|
node: args.node,
|
|
@@ -1122,7 +1210,8 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1122
1210
|
blockType: args.blockType,
|
|
1123
1211
|
editablePath: args.editablePath,
|
|
1124
1212
|
initialValue,
|
|
1125
|
-
isMultiline: isMultilineEditable(args.node, args.editablePath)
|
|
1213
|
+
isMultiline: isMultilineEditable(args.node, args.editablePath),
|
|
1214
|
+
liftedControls
|
|
1126
1215
|
};
|
|
1127
1216
|
args.node.setAttribute("contenteditable", "true");
|
|
1128
1217
|
args.node.classList.add("editor-inline-editing");
|
|
@@ -1142,6 +1231,9 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1142
1231
|
const blockType = selected.getAttribute("data-block-type") ?? "Block";
|
|
1143
1232
|
if (!blockId)
|
|
1144
1233
|
return;
|
|
1234
|
+
// A block the site renders in a fixed place cannot be deleted.
|
|
1235
|
+
if (isFixedBlockNode(selected, state.fixedBlockTypes))
|
|
1236
|
+
return;
|
|
1145
1237
|
const del = document.createElement("button");
|
|
1146
1238
|
del.type = "button";
|
|
1147
1239
|
del.className = "editor-selected-delete";
|
|
@@ -1487,14 +1579,17 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1487
1579
|
moveUpBtn.setAttribute("data-tooltip", "Move up");
|
|
1488
1580
|
moveUpBtn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>';
|
|
1489
1581
|
const upResult = computeMoveAfter(blockId, "up", match);
|
|
1490
|
-
|
|
1582
|
+
const fixedAround = fixedNeighbors(blockId, state.fixedBlockTypes, match);
|
|
1583
|
+
moveUpBtn.disabled = !upResult.canMove || fixedAround.above;
|
|
1491
1584
|
if (!upResult.canMove)
|
|
1492
1585
|
moveUpBtn.setAttribute("data-tooltip", "Already at the top");
|
|
1586
|
+
else if (fixedAround.above)
|
|
1587
|
+
moveUpBtn.setAttribute("data-tooltip", "The block above is fixed in place");
|
|
1493
1588
|
moveUpBtn.addEventListener("click", (event) => {
|
|
1494
1589
|
event.preventDefault();
|
|
1495
1590
|
event.stopPropagation();
|
|
1496
1591
|
const move = computeMoveAfter(blockId, "up", match);
|
|
1497
|
-
if (!move.canMove)
|
|
1592
|
+
if (!move.canMove || fixedNeighbors(blockId, state.fixedBlockTypes, match).above)
|
|
1498
1593
|
return;
|
|
1499
1594
|
state.pendingScrollAnchorY = match.getBoundingClientRect().top;
|
|
1500
1595
|
state.pendingScrollIntoView = true;
|
|
@@ -1507,14 +1602,16 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1507
1602
|
moveDownBtn.setAttribute("data-tooltip", "Move down");
|
|
1508
1603
|
moveDownBtn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m19 12-7 7-7-7"/><path d="M12 5v14"/></svg>';
|
|
1509
1604
|
const downResult = computeMoveAfter(blockId, "down", match);
|
|
1510
|
-
moveDownBtn.disabled = !downResult.canMove;
|
|
1605
|
+
moveDownBtn.disabled = !downResult.canMove || fixedAround.below;
|
|
1511
1606
|
if (!downResult.canMove)
|
|
1512
1607
|
moveDownBtn.setAttribute("data-tooltip", "Already at the bottom");
|
|
1608
|
+
else if (fixedAround.below)
|
|
1609
|
+
moveDownBtn.setAttribute("data-tooltip", "The block below is fixed in place");
|
|
1513
1610
|
moveDownBtn.addEventListener("click", (event) => {
|
|
1514
1611
|
event.preventDefault();
|
|
1515
1612
|
event.stopPropagation();
|
|
1516
1613
|
const move = computeMoveAfter(blockId, "down", match);
|
|
1517
|
-
if (!move.canMove)
|
|
1614
|
+
if (!move.canMove || fixedNeighbors(blockId, state.fixedBlockTypes, match).below)
|
|
1518
1615
|
return;
|
|
1519
1616
|
state.pendingScrollAnchorY = match.getBoundingClientRect().top;
|
|
1520
1617
|
state.pendingScrollIntoView = true;
|
|
@@ -1552,17 +1649,24 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1552
1649
|
const effectivePath = editablePath ?? (previousSelectedBlockId === blockId ? previousEditablePath ?? undefined : undefined);
|
|
1553
1650
|
const isChromeBlock = match.querySelector("[data-block-chrome]") !== null || match.matches("[data-block-chrome]");
|
|
1554
1651
|
if (!isChromeBlock) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1652
|
+
/*
|
|
1653
|
+
* A fixed block — a section the site always draws in the same place —
|
|
1654
|
+
* gets no move, delete or add controls: every one of them could only end
|
|
1655
|
+
* in a refusal. Its list rows and fields are still edited as usual.
|
|
1656
|
+
*/
|
|
1657
|
+
if (!isFixedBlockNode(match, state.fixedBlockTypes)) {
|
|
1658
|
+
// Move buttons live inside the floating badge above the block so they
|
|
1659
|
+
// don't overlap block content. mountSelectedDeleteHandle does the same.
|
|
1660
|
+
const badge = match.querySelector(":scope > .editor-block-badge");
|
|
1661
|
+
if (badge) {
|
|
1662
|
+
badge.append(moveUpBtn, moveDownBtn);
|
|
1663
|
+
}
|
|
1664
|
+
else {
|
|
1665
|
+
match.prepend(moveUpBtn, moveDownBtn);
|
|
1666
|
+
}
|
|
1667
|
+
match.prepend(addTopBtn);
|
|
1668
|
+
match.append(addBtn);
|
|
1563
1669
|
}
|
|
1564
|
-
match.prepend(addTopBtn);
|
|
1565
|
-
match.append(addBtn);
|
|
1566
1670
|
mountListItemDeleteHandles(match, blockId);
|
|
1567
1671
|
mountSelectedDeleteHandle();
|
|
1568
1672
|
}
|
|
@@ -1695,8 +1799,16 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1695
1799
|
// Only hijack plain-modifier, same-tab, same-origin, non-download clicks —
|
|
1696
1800
|
// modifier-clicks, target=_blank, downloads, mailto/tel/hash, and
|
|
1697
1801
|
// cross-origin links fall through to their default behavior.
|
|
1802
|
+
//
|
|
1803
|
+
// Unless selection mode is on and the link is in a block: then the click
|
|
1804
|
+
// is a selection, and the link must not be followed by anybody — not the
|
|
1805
|
+
// router, not the browser. See `linkClickSelects`.
|
|
1698
1806
|
const anchor = target?.closest("a");
|
|
1699
|
-
|
|
1807
|
+
const selectsInstead = !!anchor && !!target && linkClickSelects(target, document.documentElement.hasAttribute("data-editor-selection-mode"));
|
|
1808
|
+
if (selectsInstead) {
|
|
1809
|
+
event.preventDefault();
|
|
1810
|
+
}
|
|
1811
|
+
else if (anchor &&
|
|
1700
1812
|
event.button === 0 &&
|
|
1701
1813
|
!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey &&
|
|
1702
1814
|
(!anchor.target || anchor.target === "_self") &&
|
|
@@ -1928,6 +2040,13 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1928
2040
|
const result = computeMoveAfter(blockId, direction, selectedNode);
|
|
1929
2041
|
if (!result.canMove)
|
|
1930
2042
|
return;
|
|
2043
|
+
// The same rules as the buttons: a fixed block does not move, and nothing
|
|
2044
|
+
// moves across one.
|
|
2045
|
+
if (selectedNode && isFixedBlockNode(selectedNode, state.fixedBlockTypes))
|
|
2046
|
+
return;
|
|
2047
|
+
const fixedAround = fixedNeighbors(blockId, state.fixedBlockTypes, selectedNode);
|
|
2048
|
+
if (direction === "up" ? fixedAround.above : fixedAround.below)
|
|
2049
|
+
return;
|
|
1931
2050
|
event.preventDefault();
|
|
1932
2051
|
event.stopPropagation();
|
|
1933
2052
|
state.pendingScrollIntoView = true;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { PreviewBridge } from "./preview-bridge.tsx";
|
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.tsx";
|
|
3
3
|
export type { PreviewBridgeConfig, PreviewBridgeCoreProps } from "./preview-bridge-core.tsx";
|
|
4
4
|
export { getPreviewWrapperProps } from "./selectable.ts";
|
|
5
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, clearAiFieldLoading, isAiFieldLoadingApplied, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } from "./bridge-functions.ts";
|
|
5
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, linkClickSelects, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, clearAiFieldLoading, isAiFieldLoadingApplied, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } from "./bridge-functions.ts";
|
|
6
6
|
export type { AiLoadingScope, BridgeCallbacks, BridgeState, BridgeFunctions } from "./bridge-functions.ts";
|
|
7
7
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.tsx";
|
|
8
8
|
export type { LivePreviewPage, LivePreviewBridgeApi } from "./live-preview-store.tsx";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { PreviewBridge } from "./preview-bridge.js";
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.js";
|
|
3
3
|
export { getPreviewWrapperProps } from "./selectable.js";
|
|
4
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, clearAiFieldLoading, isAiFieldLoadingApplied, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } from "./bridge-functions.js";
|
|
4
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, scrollBlockIntoView, parseListItemPath, supportsInlineEditablePath, linkClickSelects, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, clearAiFieldLoading, isAiFieldLoadingApplied, cleanupOverlayElements, isImageEditable, isInlineEditable, isMultilineEditable, missingBlockWrapperWarning, previewIntegrationWarnings, } from "./bridge-functions.js";
|
|
5
5
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/preview-adapter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,10 +34,12 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"dist",
|
|
37
|
-
"src/styles.css"
|
|
37
|
+
"src/styles.css",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"NOTICE"
|
|
38
40
|
],
|
|
39
41
|
"dependencies": {
|
|
40
|
-
"@avocadostudio-ai/shared": "^0.
|
|
42
|
+
"@avocadostudio-ai/shared": "^0.23.0"
|
|
41
43
|
},
|
|
42
44
|
"peerDependencies": {
|
|
43
45
|
"next": ">=15.0.0",
|
package/src/styles.css
CHANGED
|
@@ -312,13 +312,15 @@
|
|
|
312
312
|
position: absolute;
|
|
313
313
|
inset: 0;
|
|
314
314
|
background: var(--aifx-sweep);
|
|
315
|
-
transform: translateX(120%);
|
|
315
|
+
transform: translateX(-120%);
|
|
316
316
|
animation: aifx-shimmer-slide 2.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
/* Left to right, the direction text is read in — and the direction every
|
|
320
|
+
other shimmer in the editor already ran. */
|
|
319
321
|
@keyframes aifx-shimmer-slide {
|
|
320
|
-
0% { transform: translateX(120%); }
|
|
321
|
-
100% { transform: translateX(
|
|
322
|
+
0% { transform: translateX(-120%); }
|
|
323
|
+
100% { transform: translateX(120%); }
|
|
322
324
|
}
|
|
323
325
|
|
|
324
326
|
/* Fixed, so it frames what the user is looking at wherever they scroll. */
|
|
@@ -403,7 +405,7 @@ body:has(.aifx-shimmer-overlay) > .aifx-page-shimmer::before {
|
|
|
403
405
|
animation: none !important;
|
|
404
406
|
}
|
|
405
407
|
|
|
406
|
-
/* A stopped sweep rests off-canvas at translateX(120%), which left nothing
|
|
408
|
+
/* A stopped sweep rests off-canvas at translateX(-120%), which left nothing
|
|
407
409
|
on screen at all. Hold a still tint instead; the page frame's border
|
|
408
410
|
already reads without motion. */
|
|
409
411
|
.aifx-shimmer-overlay::before {
|