@ptengine/inline-editor 0.1.6 → 0.1.8

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.
@@ -19,6 +19,8 @@ const SAME_TOP_PX = 2;
19
19
  const CHROME_ROWS_CAP = 5;
20
20
  const CHROME_TAGS = new Set(['header', 'nav', 'footer']);
21
21
  const TEXT_LEAF_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'button', 'span']);
22
+ const isTextLeaf = (n) => n.tag === 'img' || (TEXT_LEAF_TAGS.has(n.tag) && n.textSummary.trim().length > 0);
23
+ const hasContent = (n) => isTextLeaf(n) || !!n.image || n.textSummary.trim().length > 0 || n.children.some(c => !c.hidden && hasContent(c));
22
24
  function analyzeBlockRoots(root, opts) {
23
25
  const viewportWidth = typeof opts.viewportWidth === 'number' && opts.viewportWidth > 0
24
26
  ? opts.viewportWidth
@@ -76,13 +78,23 @@ function analyzeBlockRoots(root, opts) {
76
78
  for (const e of entries)
77
79
  e.demoted = false;
78
80
  }
79
- const isCandidate = (e) => e.landmark && !e.demoted;
80
81
  const entryBySelector = new Map();
81
82
  for (const e of entries)
82
83
  if (!entryBySelector.has(e.node.selector))
83
84
  entryBySelector.set(e.node.selector, e);
85
+ const memberBoxes = new Set();
86
+ for (const b of opts.blocks ?? []) {
87
+ for (const member of b.members) {
88
+ const e = entryBySelector.get(member.selector);
89
+ const r = e?.node.rect;
90
+ if (e && r && r.height > 0 && r.width >= minWidth)
91
+ memberBoxes.add(e);
92
+ }
93
+ }
94
+ const isCandidate = (e) => (e.landmark || memberBoxes.has(e)) && !e.demoted;
84
95
  const claimed = new Map();
85
96
  const blocksIn = new Map();
97
+ const memberEntries = new Set();
86
98
  const addTo = (m, e, id) => {
87
99
  let set = m.get(e);
88
100
  if (!set)
@@ -91,14 +103,17 @@ function analyzeBlockRoots(root, opts) {
91
103
  };
92
104
  for (const b of opts.blocks ?? []) {
93
105
  for (const member of b.members) {
94
- let e = entryBySelector.get(member.selector) ?? null;
95
- while (e && !isCandidate(e))
96
- e = e.parent;
97
- if (!e)
106
+ const own = entryBySelector.get(member.selector);
107
+ if (!own)
98
108
  continue;
99
- addTo(claimed, e, b.id);
100
- for (let a = e; a; a = a.parent)
109
+ memberEntries.add(own);
110
+ for (let a = own; a; a = a.parent)
101
111
  addTo(blocksIn, a, b.id);
112
+ let e = own;
113
+ while (e && !isCandidate(e))
114
+ e = e.parent;
115
+ if (e)
116
+ addTo(claimed, e, b.id);
102
117
  }
103
118
  }
104
119
  const modelBlockCount = (e) => blocksIn.get(e)?.size ?? 0;
@@ -126,7 +141,7 @@ function analyzeBlockRoots(root, opts) {
126
141
  for (const e of [...candidates].reverse()) {
127
142
  const ks = kids.get(e) ?? [];
128
143
  let unit;
129
- if (isAtomic(e))
144
+ if (isAtomic(e) && (memberEntries.has(e) || ks.length < 2))
130
145
  unit = true;
131
146
  else if (modelBlockCount(e) >= 2)
132
147
  unit = ks.length === 0;
@@ -148,6 +163,25 @@ function analyzeBlockRoots(root, opts) {
148
163
  }
149
164
  };
150
165
  takeRoots(tops);
166
+ const rootSet = new Set(blockRoots);
167
+ const holdsRoot = new Set();
168
+ for (const r of blockRoots)
169
+ for (let a = r.parent; a; a = a.parent)
170
+ holdsRoot.add(a);
171
+ const layers = new Set(blockRoots.map(r => r.parent?.node ?? root));
172
+ for (const layer of layers) {
173
+ if (laysOutInRow(layer))
174
+ continue;
175
+ for (const child of layer.children) {
176
+ const e = entryOf.get(child);
177
+ if (!e || rootSet.has(e) || holdsRoot.has(e) || e.landmark || e.demoted || e.chromeKey !== null)
178
+ continue;
179
+ const r = child.rect;
180
+ if (r && r.height > 0 && r.width >= minWidth && hasContent(child))
181
+ blockRoots.push(e);
182
+ }
183
+ }
184
+ blockRoots.sort((a, b) => a.order - b.order);
151
185
  const blockIdOf = (e) => {
152
186
  const ids = blocksIn.get(e);
153
187
  return ids && ids.size === 1 ? [...ids][0] : undefined;
@@ -226,7 +260,6 @@ export function buildRootShelf(root, opts) {
226
260
  chromeRows.set(e.chromeKey, (chromeRows.get(e.chromeKey) ?? 0) + 1);
227
261
  return true;
228
262
  };
229
- const isTextLeaf = (n) => n.tag === 'img' || (TEXT_LEAF_TAGS.has(n.tag) && n.textSummary.trim().length > 0);
230
263
  const leavesPerBlock = landmarks.length
231
264
  ? Math.min(LEAVES_PER_LANDMARK, Math.max(1, Math.floor((limit - landmarks.length) / landmarks.length)))
232
265
  : LEAVES_PER_LANDMARK;
@@ -32,6 +32,13 @@ export declare function useCanvasSession(input: {
32
32
  device: CanvasDevice;
33
33
  /** agent 主动发来的流(lifecycle / geometry / elementPicked / …)转出去给 useCanvasStream 消费。 */
34
34
  onStreamEvent?: (msg: CanvasStreamMessage) => void;
35
+ /**
36
+ * 递增即按同一 url/device **重建会话**(新 sessionId、新 iframe)。
37
+ *
38
+ * 交互模式里用户点链接让画布整页跳走后,退出时要回到实验页:同会话里跳走的文档
39
+ * 不是编辑对象(拾取没开、改动没放),只能整个重开 —— 与切设备同一条路。
40
+ */
41
+ reloadKey?: number;
35
42
  }): {
36
43
  state: State;
37
44
  reason: LoadFailedReason | undefined;
@@ -177,6 +177,6 @@ export function useCanvasSession(input) {
177
177
  void sendCanvasCommand({ command: 'destroy', sessionId }).catch(() => { });
178
178
  port?.disconnect();
179
179
  };
180
- }, [input.url, input.device]);
180
+ }, [input.url, input.device, input.reloadKey]);
181
181
  return { state, reason, portError, frameProps, handshake, readyPayload, capabilities };
182
182
  }
package/dist/locales.d.ts CHANGED
@@ -54,6 +54,7 @@ export declare const zhCN: {
54
54
  title: string;
55
55
  pickTarget: string;
56
56
  cancel: string;
57
+ confirm: string;
57
58
  position: string;
58
59
  top: string;
59
60
  bottom: string;
@@ -89,6 +90,12 @@ export declare const zhCN: {
89
90
  pc: string;
90
91
  mobile: string;
91
92
  };
93
+ interactive: {
94
+ enter: string;
95
+ tips: string;
96
+ active: string;
97
+ exit: string;
98
+ };
92
99
  version: {
93
100
  switching: string;
94
101
  };
@@ -234,6 +241,7 @@ export declare const enUS: {
234
241
  title: string;
235
242
  pickTarget: string;
236
243
  cancel: string;
244
+ confirm: string;
237
245
  position: string;
238
246
  top: string;
239
247
  bottom: string;
@@ -269,6 +277,12 @@ export declare const enUS: {
269
277
  pc: string;
270
278
  mobile: string;
271
279
  };
280
+ interactive: {
281
+ enter: string;
282
+ tips: string;
283
+ active: string;
284
+ exit: string;
285
+ };
272
286
  version: {
273
287
  switching: string;
274
288
  };
@@ -414,6 +428,7 @@ export declare const jaJP: {
414
428
  title: string;
415
429
  pickTarget: string;
416
430
  cancel: string;
431
+ confirm: string;
417
432
  position: string;
418
433
  top: string;
419
434
  bottom: string;
@@ -449,6 +464,12 @@ export declare const jaJP: {
449
464
  pc: string;
450
465
  mobile: string;
451
466
  };
467
+ interactive: {
468
+ enter: string;
469
+ tips: string;
470
+ active: string;
471
+ exit: string;
472
+ };
452
473
  version: {
453
474
  switching: string;
454
475
  };
@@ -553,6 +574,9 @@ export declare const inlineEditorLocales: {
553
574
  readonly 'zh-CN': {
554
575
  readonly inlineCanvas: {
555
576
  readonly review: {
577
+ readonly pageLoadFailed: "目标页面无法加载";
578
+ readonly pageLoadHint: "请返回检查页面地址、网站访问权限和网络连接后重试。";
579
+ readonly backFromFailed: "返回修改";
556
580
  readonly generating: "正在生成截图…";
557
581
  readonly noLayers: "没有匹配的图层";
558
582
  readonly clearSearch: "清除搜索";
@@ -641,6 +665,7 @@ export declare const inlineEditorLocales: {
641
665
  title: string;
642
666
  pickTarget: string;
643
667
  cancel: string;
668
+ confirm: string;
644
669
  position: string;
645
670
  top: string;
646
671
  bottom: string;
@@ -676,6 +701,12 @@ export declare const inlineEditorLocales: {
676
701
  pc: string;
677
702
  mobile: string;
678
703
  };
704
+ readonly interactive: {
705
+ enter: string;
706
+ tips: string;
707
+ active: string;
708
+ exit: string;
709
+ };
679
710
  readonly version: {
680
711
  switching: string;
681
712
  };
@@ -779,6 +810,9 @@ export declare const inlineEditorLocales: {
779
810
  readonly 'en-US': {
780
811
  readonly inlineCanvas: {
781
812
  readonly review: {
813
+ readonly pageLoadFailed: "Unable to load the target page";
814
+ readonly pageLoadHint: "Go back to check the page URL, site permissions and network connection, then retry.";
815
+ readonly backFromFailed: "Go back";
782
816
  readonly generating: "Generating screenshot…";
783
817
  readonly noLayers: "No matching layers";
784
818
  readonly clearSearch: "Clear search";
@@ -867,6 +901,7 @@ export declare const inlineEditorLocales: {
867
901
  title: string;
868
902
  pickTarget: string;
869
903
  cancel: string;
904
+ confirm: string;
870
905
  position: string;
871
906
  top: string;
872
907
  bottom: string;
@@ -902,6 +937,12 @@ export declare const inlineEditorLocales: {
902
937
  pc: string;
903
938
  mobile: string;
904
939
  };
940
+ readonly interactive: {
941
+ enter: string;
942
+ tips: string;
943
+ active: string;
944
+ exit: string;
945
+ };
905
946
  readonly version: {
906
947
  switching: string;
907
948
  };
@@ -1005,6 +1046,9 @@ export declare const inlineEditorLocales: {
1005
1046
  readonly 'ja-JP': {
1006
1047
  readonly inlineCanvas: {
1007
1048
  readonly review: {
1049
+ readonly pageLoadFailed: "対象ページを読み込めません";
1050
+ readonly pageLoadHint: "戻ってページ URL、サイトへのアクセス権限、ネットワーク接続を確認して再試行してください。";
1051
+ readonly backFromFailed: "戻って修正";
1008
1052
  readonly generating: "スクリーンショットを生成中…";
1009
1053
  readonly noLayers: "一致するレイヤーがありません";
1010
1054
  readonly clearSearch: "検索をクリア";
@@ -1093,6 +1137,7 @@ export declare const inlineEditorLocales: {
1093
1137
  title: string;
1094
1138
  pickTarget: string;
1095
1139
  cancel: string;
1140
+ confirm: string;
1096
1141
  position: string;
1097
1142
  top: string;
1098
1143
  bottom: string;
@@ -1128,6 +1173,12 @@ export declare const inlineEditorLocales: {
1128
1173
  pc: string;
1129
1174
  mobile: string;
1130
1175
  };
1176
+ readonly interactive: {
1177
+ enter: string;
1178
+ tips: string;
1179
+ active: string;
1180
+ exit: string;
1181
+ };
1131
1182
  readonly version: {
1132
1183
  switching: string;
1133
1184
  };
package/dist/locales.js CHANGED
@@ -44,6 +44,7 @@ export const zhCN = {
44
44
  title: '移动元素',
45
45
  pickTarget: '在画布里点选目标元素',
46
46
  cancel: '取消移动',
47
+ confirm: '确认移动',
47
48
  position: '相对目标的位置',
48
49
  top: '移入开头',
49
50
  bottom: '移入末尾',
@@ -79,6 +80,12 @@ export const zhCN = {
79
80
  pc: 'PC',
80
81
  mobile: '移动'
81
82
  },
83
+ interactive: {
84
+ enter: '交互模式',
85
+ tips: '切换到交互模式可以对页面进行交互,以修改需要交互后才能看到的内容。',
86
+ active: '您当前正在交互模式',
87
+ exit: '返回编辑模式'
88
+ },
82
89
  version: {
83
90
  switching: '正在切换版本,请稍候'
84
91
  },
@@ -224,6 +231,7 @@ export const enUS = {
224
231
  title: 'Move element',
225
232
  pickTarget: 'Pick a target element on the canvas',
226
233
  cancel: 'Cancel move',
234
+ confirm: 'Confirm move',
227
235
  position: 'Position relative to target',
228
236
  top: 'Inside, first',
229
237
  bottom: 'Inside, last',
@@ -259,6 +267,12 @@ export const enUS = {
259
267
  pc: 'Desktop',
260
268
  mobile: 'Mobile'
261
269
  },
270
+ interactive: {
271
+ enter: 'Interactive mode',
272
+ tips: 'Switch to interactive mode to interact with the page like in a live website.',
273
+ active: 'You are now in interactive mode',
274
+ exit: 'Back to edit mode'
275
+ },
262
276
  version: {
263
277
  switching: 'Switching version, please wait'
264
278
  },
@@ -404,6 +418,7 @@ export const jaJP = {
404
418
  title: '要素を移動',
405
419
  pickTarget: 'キャンバスで対象要素を選択',
406
420
  cancel: '移動をキャンセル',
421
+ confirm: '移動を確定',
407
422
  position: '対象に対する位置',
408
423
  top: '内側の先頭',
409
424
  bottom: '内側の末尾',
@@ -439,6 +454,12 @@ export const jaJP = {
439
454
  pc: 'PC',
440
455
  mobile: 'モバイル'
441
456
  },
457
+ interactive: {
458
+ enter: '画面操作モード',
459
+ tips: '画面操作できるようになります。表示されていない要素を開き、その後編集モードに切り替えることで隠れた要素を変更することが可能です。',
460
+ active: '現在、画面操作モードです',
461
+ exit: '編集モードに戻る'
462
+ },
442
463
  version: {
443
464
  switching: 'バージョンを切り替えています。しばらくお待ちください'
444
465
  },
@@ -0,0 +1,6 @@
1
+ import type { MovePosition } from '../kernel/moveState.js';
2
+ export declare function MovePositionControls({ onConfirm, onCancel, onError }: {
3
+ onConfirm: (position: MovePosition) => Promise<void>;
4
+ onCancel: () => void;
5
+ onError: (error: unknown) => void;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, ToggleGroup, ToggleGroupItem } from '@ptengine/design-components';
5
+ const positions = ['top', 'bottom', 'before', 'after'];
6
+ export function MovePositionControls({ onConfirm, onCancel, onError }) {
7
+ const { t } = useTranslation();
8
+ const [position, setPosition] = useState(null);
9
+ const [pending, setPending] = useState(false);
10
+ const submitting = useRef(false);
11
+ const confirm = async () => {
12
+ if (!position || submitting.current)
13
+ return;
14
+ submitting.current = true;
15
+ setPending(true);
16
+ try {
17
+ await onConfirm(position);
18
+ }
19
+ catch (error) {
20
+ onError(error);
21
+ }
22
+ finally {
23
+ submitting.current = false;
24
+ setPending(false);
25
+ }
26
+ };
27
+ return (_jsxs("div", { className: "flex w-full min-w-0 flex-col gap-2.5", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: t('inlineCanvas.move.position') }), _jsxs("div", { className: "flex flex-col items-stretch gap-3", children: [_jsx(ToggleGroup, { type: "single", variant: "outline", value: position ?? '', disabled: pending, "aria-label": t('inlineCanvas.move.position'), onValueChange: value => {
28
+ if (positions.includes(value)) {
29
+ setPosition(value);
30
+ }
31
+ }, style: {
32
+ display: 'grid',
33
+ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))'
34
+ }, children: positions.map(pos => (_jsx(ToggleGroupItem, { value: pos, "data-testid": `move-position-${pos}`, children: t(`inlineCanvas.move.${pos}`) }, pos))) }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Button, { size: "default", variant: "secondary", type: "button", "data-testid": "move-cancel", disabled: pending, onClick: onCancel, children: t('inlineCanvas.move.cancel') }), _jsx(Button, { size: "default", type: "button", "data-testid": "move-confirm", disabled: !position || pending, onClick: () => void confirm(), children: t('inlineCanvas.move.confirm') })] })] })] }));
35
+ }
@@ -22,9 +22,11 @@ export interface StylePanelProps {
22
22
  capabilities: ElementCapabilities | null;
23
23
  /** 元素当前的 author 层声明,用作输入框初值。 */
24
24
  declaredStyles?: Record<string, string>;
25
+ /** Only these properties have an editor override that can be cleared. */
26
+ resettableProperties?: string[];
25
27
  /** 提交一条样式改动。由宿主接到 `changeList.addStyle`。 */
26
28
  onApply: (property: string, value: string) => void | Promise<unknown>;
27
29
  onClose: () => void;
28
30
  onReset?: (property: string) => void;
29
31
  }
30
- export declare function StylePanel({ anchor, tag, capabilities, declaredStyles, onApply, onReset, onClose }: StylePanelProps): import("react").JSX.Element | null;
32
+ export declare function StylePanel({ anchor, tag, capabilities, declaredStyles, resettableProperties, onApply, onReset, onClose }: StylePanelProps): import("react").JSX.Element | null;
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
4
4
  import { X } from 'lucide-react';
5
5
  import { Button, InputGroup, InputGroupInput, InputGroupAddon, InputGroupText, Label, NativeSelect, Separator } from '@ptengine/design-components';
6
6
  import { composeStyleValue, visibleSections } from './styleFields.js';
7
- export function StylePanel({ anchor, tag, capabilities, declaredStyles = {}, onApply, onReset, onClose }) {
7
+ export function StylePanel({ anchor, tag, capabilities, declaredStyles = {}, resettableProperties, onApply, onReset, onClose }) {
8
8
  const { t } = useTranslation();
9
9
  const sections = visibleSections(capabilities);
10
10
  const panelRef = useRef(null);
@@ -88,7 +88,10 @@ export function StylePanel({ anchor, tag, capabilities, declaredStyles = {}, onA
88
88
  drag.current = null;
89
89
  }, onLostPointerCapture: () => {
90
90
  drag.current = null;
91
- }, className: "flex cursor-grab touch-none select-none h-11 shrink-0 items-center justify-between border-b\n border-[#EBECF0] bg-[#F4F5F7] px-3.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("code", { className: "text-xs text-[#6B778C]", children: ["<", tag, ">"] }), _jsx("span", { className: "text-[13px] font-semibold text-[#091E42]", children: t(`taginfo.${tag.toUpperCase()}`, tag) })] }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('inlineCanvas.chrome.close', '关闭'), "data-testid": "inline-style-panel-close", onClick: onClose, children: _jsx(X, { className: "size-4" }) })] }), _jsx("div", { className: "min-h-0 flex-1 flex-col overflow-y-auto", children: sections.map((section, index) => (_jsxs("section", { "data-testid": `style-section-${section.id}`, className: "flex flex-col gap-3 px-3.5 py-3.5", children: [index > 0 && _jsx(Separator, { className: "-mt-3.5 mb-0.5" }), _jsx("p", { className: "text-xs font-semibold text-[#091E42]", children: t(section.labelKey) }), _jsx("div", { className: "grid grid-cols-2 gap-2.5", children: section.fields.map(field => (_jsx(StyleFieldInput, { field: field, initial: declaredStyles[field.property] ?? '', onApply: onApply, onReset: onReset }, field.property))) })] }, section.id))) }), _jsx("footer", { className: "shrink-0 bg-[#F7F8F9] px-3.5 py-3 text-xs leading-5 text-[#6B778C]", children: t('inlineCanvas.style.footnote', '每次改动都会记入改动清单,可单条撤销') })] }));
91
+ }, className: "flex cursor-grab touch-none select-none h-11 shrink-0 items-center justify-between border-b\n border-[#EBECF0] bg-[#F4F5F7] px-3.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("code", { className: "text-xs text-[#6B778C]", children: ["<", tag, ">"] }), _jsx("span", { className: "text-[13px] font-semibold text-[#091E42]", children: t(`taginfo.${tag.toUpperCase()}`, tag) })] }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('inlineCanvas.chrome.close', '关闭'), "data-testid": "inline-style-panel-close", onClick: onClose, children: _jsx(X, { className: "size-4" }) })] }), _jsx("div", { className: "min-h-0 flex-1 flex-col overflow-y-auto", children: sections.map((section, index) => (_jsxs("section", { "data-testid": `style-section-${section.id}`, className: "flex flex-col gap-3 px-3.5 py-3.5", children: [index > 0 && _jsx(Separator, { className: "-mt-3.5 mb-0.5" }), _jsx("p", { className: "text-xs font-semibold text-[#091E42]", children: t(section.labelKey) }), _jsx("div", { className: "grid grid-cols-2 gap-2.5", children: section.fields.map(field => (_jsx(StyleFieldInput, { field: field, initial: declaredStyles[field.property] ?? '', onApply: onApply, onReset: resettableProperties === undefined ||
92
+ resettableProperties.includes(field.property)
93
+ ? onReset
94
+ : undefined }, field.property))) })] }, section.id))) }), _jsx("footer", { className: "shrink-0 bg-[#F7F8F9] px-3.5 py-3 text-xs leading-5 text-[#6B778C]", children: t('inlineCanvas.style.footnote', '每次改动都会记入改动清单,可单条撤销') })] }));
92
95
  }
93
96
  function StyleFieldInput({ field, initial, onApply, onReset }) {
94
97
  const { t } = useTranslation();
@@ -1,5 +1,8 @@
1
1
  export declare const reviewLocales: {
2
2
  readonly 'zh-CN': {
3
+ readonly pageLoadFailed: "目标页面无法加载";
4
+ readonly pageLoadHint: "请返回检查页面地址、网站访问权限和网络连接后重试。";
5
+ readonly backFromFailed: "返回修改";
3
6
  readonly generating: "正在生成截图…";
4
7
  readonly noLayers: "没有匹配的图层";
5
8
  readonly clearSearch: "清除搜索";
@@ -46,6 +49,9 @@ export declare const reviewLocales: {
46
49
  readonly align_justify: "两端对齐";
47
50
  };
48
51
  readonly 'en-US': {
52
+ readonly pageLoadFailed: "Unable to load the target page";
53
+ readonly pageLoadHint: "Go back to check the page URL, site permissions and network connection, then retry.";
54
+ readonly backFromFailed: "Go back";
49
55
  readonly generating: "Generating screenshot…";
50
56
  readonly noLayers: "No matching layers";
51
57
  readonly clearSearch: "Clear search";
@@ -92,6 +98,9 @@ export declare const reviewLocales: {
92
98
  readonly align_justify: "Justify";
93
99
  };
94
100
  readonly 'ja-JP': {
101
+ readonly pageLoadFailed: "対象ページを読み込めません";
102
+ readonly pageLoadHint: "戻ってページ URL、サイトへのアクセス権限、ネットワーク接続を確認して再試行してください。";
103
+ readonly backFromFailed: "戻って修正";
95
104
  readonly generating: "スクリーンショットを生成中…";
96
105
  readonly noLayers: "一致するレイヤーがありません";
97
106
  readonly clearSearch: "検索をクリア";
@@ -1,5 +1,8 @@
1
1
  export const reviewLocales = {
2
2
  'zh-CN': {
3
+ pageLoadFailed: '目标页面无法加载',
4
+ pageLoadHint: '请返回检查页面地址、网站访问权限和网络连接后重试。',
5
+ backFromFailed: '返回修改',
3
6
  generating: '正在生成截图…',
4
7
  noLayers: '没有匹配的图层', clearSearch: '清除搜索', searchLayers: '搜索图层',
5
8
  linkCopied: '链接已复制',
@@ -44,6 +47,9 @@ export const reviewLocales = {
44
47
  align_justify: '两端对齐'
45
48
  },
46
49
  'en-US': {
50
+ pageLoadFailed: 'Unable to load the target page',
51
+ pageLoadHint: 'Go back to check the page URL, site permissions and network connection, then retry.',
52
+ backFromFailed: 'Go back',
47
53
  generating: 'Generating screenshot…',
48
54
  noLayers: 'No matching layers', clearSearch: 'Clear search', searchLayers: 'Search layers',
49
55
  linkCopied: 'Link copied',
@@ -88,6 +94,9 @@ export const reviewLocales = {
88
94
  align_justify: 'Justify'
89
95
  },
90
96
  'ja-JP': {
97
+ pageLoadFailed: '対象ページを読み込めません',
98
+ pageLoadHint: '戻ってページ URL、サイトへのアクセス権限、ネットワーク接続を確認して再試行してください。',
99
+ backFromFailed: '戻って修正',
91
100
  generating: 'スクリーンショットを生成中…',
92
101
  noLayers: '一致するレイヤーがありません', clearSearch: '検索をクリア', searchLayers: 'レイヤーを検索',
93
102
  linkCopied: 'リンクをコピーしました',
@@ -42,8 +42,11 @@ export interface InlineEditorSelection {
42
42
  declaredStyles: Record<string, string>;
43
43
  }
44
44
  export interface UseInlineEditorInput {
45
+ onHistoryShortcut?: (direction: 'undo' | 'redo') => void;
45
46
  url: string;
46
47
  device: CanvasDevice;
48
+ /** 递增即按同一 url/device 重建画布会话(见 `useCanvasSession` 的同名参数)。 */
49
+ reloadKey?: number;
47
50
  /** change list 变化时回调 —— 宿主自己决定存哪(本 hook 不做持久化)。 */
48
51
  onChangesChange?: (changes: InlineChange[]) => void;
49
52
  /**
@@ -109,6 +112,18 @@ export interface UseInlineEditorResult {
109
112
  */
110
113
  mobileFit: MobileFitState;
111
114
  changeList: ReturnType<typeof useChangeList>;
115
+ /**
116
+ * 交互模式:画布上的拾取关掉,点击/悬停/输入原样交给客户页(打开弹窗、切换页签、展开折叠
117
+ * 之后再改那些「交互后才看得见」的内容)。对应老编辑器(engage-editor)顶栏的「交互模式」。
118
+ */
119
+ interactive: boolean;
120
+ /**
121
+ * 进出交互模式。退出时返回 `navigated`:交互期间画布整页跳走过(点了链接 / 提交了表单)。
122
+ * 跳走的文档不是编辑对象,调用方应重建会话(`reloadKey`)回到实验页,而不是在新页上继续编辑。
123
+ */
124
+ setInteractive: (on: boolean) => Promise<{
125
+ navigated: boolean;
126
+ }>;
112
127
  commands: {
113
128
  /** 选中某个元素(点路径选择器的某一级、或程序化选中)。空串 = 清空。 */
114
129
  select: (selector: string) => Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { CANVAS_MSG_NAMESPACE } from '@ptengine/inline-core/protocol';
2
3
  import { useCanvasSession } from './kernel/useCanvasSession.js';
3
4
  import { useCanvasStream } from './kernel/useCanvasStream.js';
4
5
  import { useMobileFit } from './kernel/useMobileFit.js';
@@ -37,7 +38,12 @@ export function useInlineEditor(input) {
37
38
  });
38
39
  }, []);
39
40
  const [readySessionId, setReadySessionId] = useState(null);
41
+ const [interactive, setInteractiveState] = useState(false);
42
+ const interactiveRef = useRef(false);
43
+ const navigatedRef = useRef(false);
40
44
  const onLifecycle = useCallback((payload, eventSessionId) => {
45
+ if (payload.phase === 'navAway' && interactiveRef.current)
46
+ navigatedRef.current = true;
41
47
  if (payload.phase !== 'domReady' && payload.phase !== 'loaded')
42
48
  return;
43
49
  if (!eventSessionId)
@@ -49,10 +55,24 @@ export function useInlineEditor(input) {
49
55
  documentSizeRef.current?.(payload);
50
56
  }, []);
51
57
  const stream = useCanvasStream(undefined, geometry.onGeometry, onTextEditCommit, onLifecycle, onDocumentSize);
58
+ const historyShortcutRef = useRef(input.onHistoryShortcut);
59
+ historyShortcutRef.current = input.onHistoryShortcut;
60
+ const onStreamEvent = useCallback((msg) => {
61
+ if (msg.namespace === CANVAS_MSG_NAMESPACE && msg.event === 'historyShortcut') {
62
+ if (interactiveRef.current)
63
+ return;
64
+ const direction = msg.payload?.direction;
65
+ if (direction === 'undo' || direction === 'redo')
66
+ historyShortcutRef.current?.(direction);
67
+ }
68
+ else
69
+ stream.onStreamEvent(msg);
70
+ }, [stream.onStreamEvent]);
52
71
  const session = useCanvasSession({
53
72
  url: input.url,
54
73
  device: input.device,
55
- onStreamEvent: stream.onStreamEvent
74
+ onStreamEvent,
75
+ reloadKey: input.reloadKey
56
76
  });
57
77
  const sessionId = session.frameProps?.sessionId;
58
78
  const changeList = useChangeList(sessionId, {
@@ -109,6 +129,9 @@ export function useInlineEditor(input) {
109
129
  const geometryReset = geometry.reset;
110
130
  const streamReset = stream.reset;
111
131
  useEffect(() => {
132
+ interactiveRef.current = false;
133
+ navigatedRef.current = false;
134
+ setInteractiveState(false);
112
135
  setSelection_(null);
113
136
  setTree(null);
114
137
  treeForRef.current = null;
@@ -147,6 +170,35 @@ export function useInlineEditor(input) {
147
170
  ? graftSubtree(prev, anchorId, root, { renderIcon: renderIconRef.current })
148
171
  : prev);
149
172
  }, [sessionId]);
173
+ const treeLoaded = tree !== null;
174
+ const [treeEpoch, setTreeEpoch] = useState(0);
175
+ const contentRevision = changeList.list
176
+ .filter(record => ['elementContent', 'elementText', 'elementMove'].includes(record.change.type))
177
+ .map(record => record.changeId)
178
+ .join('|')
179
+ .concat(`#${treeEpoch}`);
180
+ const refreshedContent = useRef('#0');
181
+ useEffect(() => {
182
+ if (!wantTree ||
183
+ !domReady ||
184
+ !sessionId ||
185
+ !treeRef.current ||
186
+ refreshedContent.current === contentRevision)
187
+ return;
188
+ let cancelled = false;
189
+ const loaded = Object.values(treeRef.current.nodes);
190
+ void getDomTree(sessionId, { maxDepth: 16, maxNodes: Math.max(1500, loaded.length) })
191
+ .then(root => {
192
+ if (!cancelled) {
193
+ refreshedContent.current = contentRevision;
194
+ setTree(adaptDomTree(root, { renderIcon: renderIconRef.current }));
195
+ }
196
+ })
197
+ .catch(() => { });
198
+ return () => {
199
+ cancelled = true;
200
+ };
201
+ }, [contentRevision, domReady, sessionId, wantTree, treeLoaded]);
150
202
  const revealAttemptedRef = useRef(new Set());
151
203
  const selectedSelector = selection?.selector ?? null;
152
204
  const selectionChain = selection?.chain;
@@ -332,6 +384,41 @@ export function useInlineEditor(input) {
332
384
  }));
333
385
  }
334
386
  }), [graftAt, input.iframeRef, preserveScroll, refresh, sessionId]);
387
+ const setInteractive = useCallback(async (on) => {
388
+ if (!sessionId)
389
+ return { navigated: false };
390
+ if (on) {
391
+ if (interactiveRef.current)
392
+ return { navigated: false };
393
+ interactiveRef.current = true;
394
+ navigatedRef.current = false;
395
+ setInteractiveState(true);
396
+ setMove(null);
397
+ setSelection_(null);
398
+ await setHoverTarget(sessionId, '').catch(() => { });
399
+ await setSelection(sessionId, '').catch(() => { });
400
+ try {
401
+ await setPickMode(sessionId, false);
402
+ }
403
+ catch (e) {
404
+ interactiveRef.current = false;
405
+ setInteractiveState(false);
406
+ throw e;
407
+ }
408
+ return { navigated: false };
409
+ }
410
+ if (!interactiveRef.current)
411
+ return { navigated: false };
412
+ const navigated = navigatedRef.current;
413
+ interactiveRef.current = false;
414
+ navigatedRef.current = false;
415
+ setInteractiveState(false);
416
+ if (!navigated) {
417
+ await setPickMode(sessionId, true);
418
+ setTreeEpoch(n => n + 1);
419
+ }
420
+ return { navigated };
421
+ }, [sessionId]);
335
422
  return useMemo(() => ({
336
423
  session,
337
424
  frameProps: session.frameProps,
@@ -345,6 +432,8 @@ export function useInlineEditor(input) {
345
432
  geometry,
346
433
  mobileFit: mobileFit.state,
347
434
  changeList,
435
+ interactive,
436
+ setInteractive,
348
437
  commands
349
438
  }), [
350
439
  session,
@@ -358,6 +447,8 @@ export function useInlineEditor(input) {
358
447
  geometry,
359
448
  mobileFit.state,
360
449
  changeList,
450
+ interactive,
451
+ setInteractive,
361
452
  commands
362
453
  ]);
363
454
  }