@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.
- package/CHANGELOG.md +42 -0
- package/dist/InlineEditor.d.ts +10 -1
- package/dist/InlineEditor.js +219 -26
- package/dist/dialogs/InsertElementDialog.js +6 -2
- package/dist/kernel/CanvasContextBar.d.ts +14 -1
- package/dist/kernel/CanvasContextBar.js +6 -4
- package/dist/kernel/ai/aiInlineApply.d.ts +4 -3
- package/dist/kernel/changelist/useChangeList.d.ts +3 -0
- package/dist/kernel/changelist/useChangeList.js +3 -0
- package/dist/kernel/selection/buildRootShelf.d.ts +7 -0
- package/dist/kernel/selection/buildRootShelf.js +42 -9
- package/dist/kernel/useCanvasSession.d.ts +7 -0
- package/dist/kernel/useCanvasSession.js +1 -1
- package/dist/locales.d.ts +51 -0
- package/dist/locales.js +21 -0
- package/dist/panels/MovePositionControls.d.ts +6 -0
- package/dist/panels/MovePositionControls.js +35 -0
- package/dist/panels/StylePanel.d.ts +3 -1
- package/dist/panels/StylePanel.js +5 -2
- package/dist/reviewLocales.d.ts +9 -0
- package/dist/reviewLocales.js +9 -0
- package/dist/useInlineEditor.d.ts +15 -0
- package/dist/useInlineEditor.js +92 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
95
|
-
|
|
96
|
-
e = e.parent;
|
|
97
|
-
if (!e)
|
|
106
|
+
const own = entryBySelector.get(member.selector);
|
|
107
|
+
if (!own)
|
|
98
108
|
continue;
|
|
99
|
-
|
|
100
|
-
for (let a =
|
|
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:
|
|
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();
|
package/dist/reviewLocales.d.ts
CHANGED
|
@@ -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: "検索をクリア";
|
package/dist/reviewLocales.js
CHANGED
|
@@ -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>;
|
package/dist/useInlineEditor.js
CHANGED
|
@@ -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
|
|
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
|
}
|