@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.8
|
|
4
|
+
|
|
5
|
+
依赖不变(`@ptengine/inline-core` ~0.2.1),可以直接发。
|
|
6
|
+
|
|
7
|
+
**新增**
|
|
8
|
+
|
|
9
|
+
- 交互模式(对应 engage-editor 画布工具条上的「交互模式」):画布条右侧新增开关,进入后关掉拾取,点击、悬停、输入都交给客户页,
|
|
10
|
+
用来打开弹窗、切换页签、展开折叠,再改那些交互之后才看得见的内容。
|
|
11
|
+
- 进入时清掉选中与悬停,收起选中框、工具条、样式面板、移动条与区块框;顶栏整条被「您当前正在交互模式 / 返回编辑模式」提示条盖住,
|
|
12
|
+
图层面板盖一层不可点的遮罩,设备切换禁用,画布发来的与宿主页的 Cmd/Ctrl+Z 都不再触发编辑器撤销;挂 beforeunload 防止页面把产品页带走。
|
|
13
|
+
- 退出时开回拾取、重拉图层树,并把当前改动按原 changeId 撤了重放一遍(页面脚本可能把改过的元素重渲染掉了;id 不变,AI 卡片的撤销仍对得上)。
|
|
14
|
+
- 交互中画布整页跳走过(点了链接 / 提交了表单,SW 派生的 `navAway`),退出时按实验地址重建会话,改动走与切设备相同的快照回放,不往跳走后的页面上放。
|
|
15
|
+
- 切版本、文字编辑、移动拾取、截图进行中时按钮禁用;关拾取失败则不进入。
|
|
16
|
+
- `InlineEditor` 新增 `interactiveMode`(默认 `true`,只读画布可传 `false`)与 `onInteractiveChange`;headless 层 `useInlineEditor` 新增
|
|
17
|
+
`interactive` / `setInteractive(on)`(退出返回 `{ navigated }`)与入参 `reloadKey`;`useChangeList` 新增 `reapplyAll()`。
|
|
18
|
+
|
|
19
|
+
## 0.1.7
|
|
20
|
+
|
|
21
|
+
依赖 `@ptengine/inline-core` ~0.2.1(新增 `historyShortcut` 流事件):**先发 inline-core 0.2.1,再发本版**。
|
|
22
|
+
|
|
23
|
+
**变更**
|
|
24
|
+
|
|
25
|
+
- 整页模式下(宿主接了 `onPageContextReady`)选中元素时,AI 作用域 = 选中元素 ∪ 它的子树货架 ∪ **整页货架**,
|
|
26
|
+
并按整页货架那些行的真实祖先链收敛落盘;MOVE 也认货架里任意一行作 target。用户点中的常不是他说的那一层 ——
|
|
27
|
+
vivaia 首屏点哪都选中幻灯片的空覆盖链接,「首屏下面加评价」要 after 整个 section、「换这张图」要改它的兄弟 img,
|
|
28
|
+
此前都不在子树里,只能请用户取消选择(真机 2026-10-21)。并进来的仍是宿主发出去的那份货架,货架外拼出来的
|
|
29
|
+
selector 照拒;宿主选中时也要发 `<page-shelf>`。没有整页模式的老宿主行为不变。
|
|
30
|
+
- 整页货架补上矮区块:页面模型的成员容器只要够宽就是候选、不看高度;块根父级(上下叠放的那一层)里不够高、但够宽、
|
|
31
|
+
有内容、不在 header / nav / footer 里的孩子补成块根,参与排序与叶子配额,也能写 `blockId`。此前低于 120px 的区块
|
|
32
|
+
(vivaia 手机首页 86px 的新闻区)等不到广度优先回填就被行数上限挤掉,标题进不了货架,改它的请求全部被拒。
|
|
33
|
+
装着 ≥2 个候选的外壳不再因「只认领了这一块」成块;分析路径的成员(`liftToBlockRoots`)同口径上提到矮块根。
|
|
34
|
+
- 插入位置与移动位置统一使用组件库的 outline 单选 ToggleGroup;移动工具栏的位置选择单独一行,取消与确认固定在下方一行靠右,统一控件高度。移动先选位置,再确认执行,支持取消,提交期间阻止重复操作。
|
|
35
|
+
|
|
36
|
+
**修复**
|
|
37
|
+
|
|
38
|
+
- I01:设备切换先等待已开始的写入完成,再保存当前 live 清单并交还版本宿主;新会话从这份快照回放,回放期间阻止编辑与保存。版本和设备会话切换仍重置操作历史。
|
|
39
|
+
- I02:宿主和画布的 Cmd/Ctrl+Z、Shift+Z / Ctrl+Y 接入已有操作历史;输入框和文字编辑期间保留原生撤销。
|
|
40
|
+
- I04:样式读取当前生效值并随编辑/撤销刷新,仅已有覆盖提供清除入口;图片/背景图、视频、alt 读取 live DOM,点击动作回填当前版本的配置。读取失败显示错误。
|
|
41
|
+
- I05:画布会话失败显示目标页加载提示、原因与返回入口,避免无反馈的空画布。
|
|
42
|
+
- I09:文字、插入、移动及撤销/重做后刷新图层标签和结构,复用图层组件以保留搜索和折叠状态;纯样式编辑不重拉图层。
|
|
43
|
+
- 复验既有 I03 浮层布局:全屏 z-index 1000 宿主、1024×480、拖拽及关闭重开,稳定后完整位于视口内。
|
|
44
|
+
|
|
3
45
|
## 0.1.6
|
|
4
46
|
|
|
5
47
|
**变更**
|
package/dist/InlineEditor.d.ts
CHANGED
|
@@ -184,6 +184,15 @@ export interface InlineEditorProps {
|
|
|
184
184
|
activeId?: string | null;
|
|
185
185
|
onHover?: (id: string | null) => void;
|
|
186
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* 「交互模式」按钮。默认 `true`。分析画布这类只看不改的宿主传 `false` 关掉。
|
|
189
|
+
*
|
|
190
|
+
* 交互模式 = 拾取关掉、页面原样可点(打开弹窗 / 切页签 / 展开折叠后再改那些交互后才
|
|
191
|
+
* 看得见的内容),对应老编辑器(engage-editor)画布工具条上的同名开关。
|
|
192
|
+
*/
|
|
193
|
+
interactiveMode?: boolean;
|
|
194
|
+
/** 进出交互模式时通知宿主(宿主自己的浮层要不要让路由它决定)。 */
|
|
195
|
+
onInteractiveChange?: (on: boolean) => void;
|
|
187
196
|
/**
|
|
188
197
|
* 选中变化 → 新的 AI 上下文。`null` = 选中被清空。
|
|
189
198
|
*
|
|
@@ -354,4 +363,4 @@ export interface AiApplyApi {
|
|
|
354
363
|
/** 当前版本 —— 宿主把 agent 给的版本**名**映射成 id 时要知道现在在哪一版。 */
|
|
355
364
|
readonly activeVersionId?: string;
|
|
356
365
|
}
|
|
357
|
-
export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay }: InlineEditorProps): import("react").JSX.Element;
|
|
366
|
+
export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay, interactiveMode, onInteractiveChange }: InlineEditorProps): import("react").JSX.Element;
|
package/dist/InlineEditor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { EditorStatus } from './panels/EditorStatus.js';
|
|
3
|
+
import { MovePositionControls } from './panels/MovePositionControls.js';
|
|
3
4
|
import { ScreenshotCard } from './panels/ScreenshotCard.js';
|
|
4
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
6
|
import { useTranslation } from 'react-i18next';
|
|
@@ -16,7 +17,7 @@ import { ChangeListPanel } from './kernel/changelist/ChangeListPanel.js';
|
|
|
16
17
|
import { ChangeListPopover } from './kernel/changelist/ChangeListPopover.js';
|
|
17
18
|
import { frameRectToProductRect, measureFrameContext } from './kernel/selection/geometry.js';
|
|
18
19
|
import { buildElementAiContext, extractPageLinks, linksFragment, ROOT_SHELF_LIMIT, SHELF_TREE_MAX_DEPTH, SHELF_TREE_MAX_NODES } from './kernel/selection/elementAiContext.js';
|
|
19
|
-
import { getAncestorChain, getDomTree, getElementCapabilities, getElementImages, getOuterHtml, resolveCanvasSelector } from './kernel/canvasCommands.js';
|
|
20
|
+
import { getAncestorChain, getDomTree, getElementCapabilities, getElementImages, getOuterHtml, resolveCanvasSelector, queryElements } from './kernel/canvasCommands.js';
|
|
20
21
|
import { useAiSelectionContext } from './kernel/selection/useAiSelectionContext.js';
|
|
21
22
|
import { buildDescendantShelf } from './kernel/selection/buildDescendantShelf.js';
|
|
22
23
|
import { ROOT_SHELF_TREE_PARAMS, buildRootShelf } from './kernel/selection/buildRootShelf.js';
|
|
@@ -35,6 +36,7 @@ import { ImageSettingDialog } from './dialogs/ImageSettingDialog.js';
|
|
|
35
36
|
import { InsertElementDialog } from './dialogs/InsertElementDialog.js';
|
|
36
37
|
import { ReplaceVideoDialog } from './dialogs/ReplaceVideoDialog.js';
|
|
37
38
|
import { StylePanel } from './panels/StylePanel.js';
|
|
39
|
+
import { STYLE_SECTIONS } from './panels/styleFields.js';
|
|
38
40
|
import { HtmlEditDialog } from './dialogs/HtmlEditDialog.js';
|
|
39
41
|
import { setCode } from './kernel/canvasCommands.js';
|
|
40
42
|
import { describeCanvasError } from './kernel/canvasErrors.js';
|
|
@@ -54,13 +56,17 @@ function imageTargetOf(caps) {
|
|
|
54
56
|
return branch === 'background' ? 'background' : undefined;
|
|
55
57
|
}
|
|
56
58
|
const AI_ELEMENT_IMAGE_LIMIT = 4;
|
|
57
|
-
export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay }) {
|
|
59
|
+
export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay, interactiveMode = true, onInteractiveChange }) {
|
|
58
60
|
const { t } = useTranslation();
|
|
59
61
|
const canvasAreaRef = useRef(null);
|
|
60
62
|
const canvasIframeRef = useRef(null);
|
|
61
63
|
const insertedRemovedRef = useRef(null);
|
|
62
64
|
const insertedRestoredRef = useRef(null);
|
|
65
|
+
const historyShortcutRef = useRef(() => { });
|
|
66
|
+
const [reloadKey, setReloadKey] = useState(0);
|
|
63
67
|
const editor = useInlineEditor({
|
|
68
|
+
reloadKey,
|
|
69
|
+
onHistoryShortcut: direction => historyShortcutRef.current(direction),
|
|
64
70
|
onInsertedRemoved: selectors => insertedRemovedRef.current?.(selectors),
|
|
65
71
|
onInsertedRestored: selectors => insertedRestoredRef.current?.(selectors),
|
|
66
72
|
url,
|
|
@@ -68,7 +74,7 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
68
74
|
iframeRef: canvasIframeRef,
|
|
69
75
|
renderNodeIcon
|
|
70
76
|
});
|
|
71
|
-
const { selection, tree, clipboard, move, capturing, textEditing, geometry, changeList, commands, frameProps, domReady } = editor;
|
|
77
|
+
const { selection, tree, clipboard, move, capturing, textEditing, geometry, changeList, commands, frameProps, domReady, interactive } = editor;
|
|
72
78
|
const selectAndReveal = useCallback(async (selector) => {
|
|
73
79
|
await commands.select(selector);
|
|
74
80
|
await commands.reveal(selector);
|
|
@@ -80,6 +86,7 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
80
86
|
const [showChangeList, setShowChangeList] = useState(false);
|
|
81
87
|
const [stylePanelOpen, setStylePanelOpen] = useState(false);
|
|
82
88
|
const [htmlDialog, setHtmlDialog] = useState({ open: false, seed: '' });
|
|
89
|
+
const [currentStyles, setCurrentStyles] = useState({});
|
|
83
90
|
const [imageDialog, setImageDialog] = useState({ open: false, seed: '' });
|
|
84
91
|
const [imageSettingDialog, setImageSettingDialog] = useState({ open: false, seed: '' });
|
|
85
92
|
const [videoDialog, setVideoDialog] = useState({ open: false, seed: '' });
|
|
@@ -96,6 +103,25 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
96
103
|
return '';
|
|
97
104
|
}
|
|
98
105
|
}, [url]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!stylePanelOpen || !selection || !frameProps?.sessionId)
|
|
108
|
+
return;
|
|
109
|
+
let cancelled = false;
|
|
110
|
+
void queryElements(frameProps.sessionId, [selection.selector], {
|
|
111
|
+
fields: STYLE_SECTIONS.flatMap(section => section.fields.map(field => `computed:${field.property}`))
|
|
112
|
+
})
|
|
113
|
+
.then(result => {
|
|
114
|
+
if (!cancelled)
|
|
115
|
+
setCurrentStyles(result.results[0]?.matches[0]?.computed ?? {});
|
|
116
|
+
})
|
|
117
|
+
.catch(error => {
|
|
118
|
+
if (!cancelled)
|
|
119
|
+
setCommandError(describeCanvasError(error));
|
|
120
|
+
});
|
|
121
|
+
return () => {
|
|
122
|
+
cancelled = true;
|
|
123
|
+
};
|
|
124
|
+
}, [stylePanelOpen, selection?.selector, frameProps?.sessionId, changeList.list]);
|
|
99
125
|
const changeListRef = useRef(changeList);
|
|
100
126
|
changeListRef.current = changeList;
|
|
101
127
|
const versionChangesRef = useRef(versionChanges);
|
|
@@ -172,6 +198,27 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
172
198
|
}
|
|
173
199
|
void run().catch(e => setCommandError(describeCanvasError(e)));
|
|
174
200
|
}, [t]);
|
|
201
|
+
historyShortcutRef.current = direction => {
|
|
202
|
+
if (!domReady || textEditing || switchingRef.current || interactive)
|
|
203
|
+
return;
|
|
204
|
+
uiWrite(() => enqueue(() => direction === 'undo' ? changeListRef.current.undo() : changeListRef.current.redo()));
|
|
205
|
+
};
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const onKey = (event) => {
|
|
208
|
+
if (!(event.metaKey || event.ctrlKey) || event.altKey)
|
|
209
|
+
return;
|
|
210
|
+
const target = event.target;
|
|
211
|
+
if (target?.closest('input, textarea, select, [contenteditable="true"], [role="textbox"]'))
|
|
212
|
+
return;
|
|
213
|
+
const key = event.key.toLowerCase();
|
|
214
|
+
if (key !== 'z' && !(key === 'y' && event.ctrlKey))
|
|
215
|
+
return;
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
historyShortcutRef.current(event.shiftKey || key === 'y' ? 'redo' : 'undo');
|
|
218
|
+
};
|
|
219
|
+
document.addEventListener('keydown', onKey);
|
|
220
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
221
|
+
}, []);
|
|
175
222
|
useEffect(() => {
|
|
176
223
|
if (!hasVersions || !activeVersionId)
|
|
177
224
|
return;
|
|
@@ -202,18 +249,81 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
202
249
|
}, [sessionIdForAssets]);
|
|
203
250
|
const hydratedSessionRef = useRef(null);
|
|
204
251
|
const sessionIdForHydrate = frameProps?.sessionId;
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
252
|
+
const deviceSnapshot = useRef(null);
|
|
253
|
+
const snapshotForRebuild = async () => {
|
|
254
|
+
await changeListRef.current.whenIdle();
|
|
255
|
+
const live = changeListRef.current.peekList();
|
|
256
|
+
deviceSnapshot.current = { version: activeVersionId, changes: live.map(r => r.change) };
|
|
257
|
+
if (activeVersionId)
|
|
258
|
+
versionCallbacksRef.current.onVersionLeave?.(activeVersionId, live);
|
|
259
|
+
};
|
|
260
|
+
const changeDevice = (next) => {
|
|
261
|
+
if (next === device || switchingRef.current || interactive)
|
|
262
|
+
return;
|
|
263
|
+
switchingRef.current = true;
|
|
264
|
+
setSwitching(true);
|
|
265
|
+
void enqueue(async () => {
|
|
266
|
+
await snapshotForRebuild();
|
|
267
|
+
onDeviceChange(next);
|
|
268
|
+
})
|
|
269
|
+
.catch(error => setCommandError(describeCanvasError(error)))
|
|
270
|
+
.finally(() => {
|
|
271
|
+
switchingRef.current = false;
|
|
272
|
+
setSwitching(false);
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
const interactiveChangeRef = useRef(onInteractiveChange);
|
|
276
|
+
interactiveChangeRef.current = onInteractiveChange;
|
|
277
|
+
const canEnterInteractive = interactiveMode && domReady && !switching && !capturing && textEditing === null && !move;
|
|
278
|
+
const enterInteractive = () => {
|
|
279
|
+
if (!canEnterInteractive || switchingRef.current)
|
|
280
|
+
return;
|
|
281
|
+
setStylePanelOpen(false);
|
|
282
|
+
setShowChangeList(false);
|
|
283
|
+
setPasteDialog(false);
|
|
284
|
+
void enqueue(() => editor.setInteractive(true))
|
|
285
|
+
.then(() => interactiveChangeRef.current?.(true))
|
|
286
|
+
.catch(error => setCommandError(describeCanvasError(error)));
|
|
287
|
+
};
|
|
288
|
+
const exitInteractive = () => {
|
|
289
|
+
void enqueue(async () => {
|
|
290
|
+
const { navigated } = await editor.setInteractive(false);
|
|
291
|
+
interactiveChangeRef.current?.(false);
|
|
292
|
+
if (navigated) {
|
|
293
|
+
await snapshotForRebuild();
|
|
294
|
+
setReloadKey(n => n + 1);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await changeListRef.current.reapplyAll();
|
|
298
|
+
}).catch(error => setCommandError(describeCanvasError(error)));
|
|
299
|
+
};
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!interactive)
|
|
302
|
+
return;
|
|
303
|
+
const onBeforeUnload = (event) => {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
event.returnValue = '';
|
|
306
|
+
};
|
|
307
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
308
|
+
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
|
309
|
+
}, [interactive]);
|
|
310
|
+
const bootChanges = deviceSnapshot.current?.version === activeVersionId && deviceSnapshot.current
|
|
311
|
+
? deviceSnapshot.current.changes
|
|
312
|
+
: hasVersions && activeVersionId && versionChanges?.[activeVersionId]
|
|
313
|
+
? versionChanges[activeVersionId]
|
|
314
|
+
: initialChanges;
|
|
208
315
|
useEffect(() => {
|
|
209
316
|
if (!domReady || !sessionIdForHydrate)
|
|
210
317
|
return;
|
|
211
318
|
if (hydratedSessionRef.current === sessionIdForHydrate)
|
|
212
319
|
return;
|
|
213
320
|
hydratedSessionRef.current = sessionIdForHydrate;
|
|
321
|
+
deviceSnapshot.current = null;
|
|
214
322
|
if (!bootChanges?.length)
|
|
215
323
|
return;
|
|
216
324
|
const versionId = hasVersions ? activeVersionId : undefined;
|
|
325
|
+
setSwitching(true);
|
|
326
|
+
switchingRef.current = true;
|
|
217
327
|
void enqueue(async () => {
|
|
218
328
|
const { outcome, idMap } = await changeListRef.current.replaceAll(bootChanges, prevIdsFor(versionId, bootChanges));
|
|
219
329
|
if (versionId) {
|
|
@@ -222,7 +332,12 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
222
332
|
versionCallbacksRef.current.onVersionEnter?.(versionId, idMap);
|
|
223
333
|
}
|
|
224
334
|
onHydrated?.(outcome);
|
|
225
|
-
})
|
|
335
|
+
})
|
|
336
|
+
.catch(e => setCommandError(describeCanvasError(e)))
|
|
337
|
+
.finally(() => {
|
|
338
|
+
switchingRef.current = false;
|
|
339
|
+
setSwitching(false);
|
|
340
|
+
});
|
|
226
341
|
}, [domReady, sessionIdForHydrate, bootChanges]);
|
|
227
342
|
const commandsRef = useRef(commands);
|
|
228
343
|
commandsRef.current = commands;
|
|
@@ -658,9 +773,15 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
658
773
|
identifier: picked.selector,
|
|
659
774
|
descendants: () => [
|
|
660
775
|
...descendantShelfRef.current.map(d => d.selector),
|
|
776
|
+
...(pageMode ? rootShelfRef.current.map(d => d.selector) : []),
|
|
661
777
|
...aiInsertedRef.current
|
|
662
778
|
],
|
|
663
|
-
ancestors: (selection?.chain ?? []).slice(1).map(n => n.selector)
|
|
779
|
+
ancestors: (selection?.chain ?? []).slice(1).map(n => n.selector),
|
|
780
|
+
...(pageMode
|
|
781
|
+
? {
|
|
782
|
+
ancestorsOf: (selector) => rootShelfAncestorsRef.current.get(selector)
|
|
783
|
+
}
|
|
784
|
+
: {})
|
|
664
785
|
}
|
|
665
786
|
: {
|
|
666
787
|
selector: 'body',
|
|
@@ -806,11 +927,40 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
806
927
|
return;
|
|
807
928
|
}
|
|
808
929
|
if (buttonId === 'replace-image') {
|
|
809
|
-
|
|
930
|
+
void Promise.all([
|
|
931
|
+
getOuterHtml(sessionId, self.selector),
|
|
932
|
+
selection?.capabilities?.hasBackgroundImage && !selection?.capabilities?.isImg
|
|
933
|
+
? queryElements(sessionId, [self.selector], {
|
|
934
|
+
fields: ['computed:background-image']
|
|
935
|
+
})
|
|
936
|
+
: Promise.resolve(null)
|
|
937
|
+
])
|
|
938
|
+
.then(([{ html }, facts]) => {
|
|
939
|
+
const node = new DOMParser().parseFromString(html, 'text/html').body
|
|
940
|
+
.firstElementChild;
|
|
941
|
+
const background = facts?.results[0]?.matches[0]?.computed?.['background-image'] ?? '';
|
|
942
|
+
const url = /^url\(["']?(.*?)["']?\)$/.exec(background)?.[1] ?? '';
|
|
943
|
+
setImageDialog({
|
|
944
|
+
open: true,
|
|
945
|
+
seed: facts ? url : (node?.getAttribute('src') ?? '')
|
|
946
|
+
});
|
|
947
|
+
})
|
|
948
|
+
.catch(error => setCommandError(describeCanvasError(error)));
|
|
810
949
|
return;
|
|
811
950
|
}
|
|
812
951
|
if (buttonId === 'replace-video') {
|
|
813
|
-
|
|
952
|
+
void getOuterHtml(sessionId, self.selector)
|
|
953
|
+
.then(({ html }) => {
|
|
954
|
+
const node = new DOMParser().parseFromString(html, 'text/html').body
|
|
955
|
+
.firstElementChild;
|
|
956
|
+
setVideoDialog({
|
|
957
|
+
open: true,
|
|
958
|
+
seed: node?.getAttribute('src') ??
|
|
959
|
+
node?.querySelector('source')?.getAttribute('src') ??
|
|
960
|
+
''
|
|
961
|
+
});
|
|
962
|
+
})
|
|
963
|
+
.catch(error => setCommandError(describeCanvasError(error)));
|
|
814
964
|
return;
|
|
815
965
|
}
|
|
816
966
|
if (buttonId === 'insert') {
|
|
@@ -818,11 +968,29 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
818
968
|
return;
|
|
819
969
|
}
|
|
820
970
|
if (buttonId === 'image-setting') {
|
|
821
|
-
|
|
971
|
+
void getOuterHtml(sessionId, self.selector)
|
|
972
|
+
.then(({ html }) => {
|
|
973
|
+
const node = new DOMParser().parseFromString(html, 'text/html').body
|
|
974
|
+
.firstElementChild;
|
|
975
|
+
setImageSettingDialog({
|
|
976
|
+
open: true,
|
|
977
|
+
seed: node?.getAttribute('alt') ?? ''
|
|
978
|
+
});
|
|
979
|
+
})
|
|
980
|
+
.catch(error => setCommandError(describeCanvasError(error)));
|
|
822
981
|
return;
|
|
823
982
|
}
|
|
824
983
|
if (buttonId === 'click-action') {
|
|
825
|
-
|
|
984
|
+
const existing = [...changeListRef.current.peekList()]
|
|
985
|
+
.reverse()
|
|
986
|
+
.find(r => r.change.type === 'changeElementAction' &&
|
|
987
|
+
r.change.selector === self.selector);
|
|
988
|
+
setActionDialog({
|
|
989
|
+
open: true,
|
|
990
|
+
seed: existing?.change.type === 'changeElementAction'
|
|
991
|
+
? existing.change.action
|
|
992
|
+
: null
|
|
993
|
+
});
|
|
826
994
|
return;
|
|
827
995
|
}
|
|
828
996
|
if (buttonId === 'edit-style') {
|
|
@@ -862,13 +1030,13 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
862
1030
|
}
|
|
863
1031
|
}, [clipboard, commands, depsOf, frameProps?.sessionId, selection, t, uploadImage]);
|
|
864
1032
|
const self = selection?.chain[0];
|
|
865
|
-
return (_jsxs("div", { className: `${CANVAS_UI_SCOPE} flex h-full min-h-0 flex-col bg-[#F5F7FC]`, children: [_jsx(EditorChrome, { slotRight: capturing ? (_jsx("span", { role: "status", className: "text-xs text-muted-foreground", children: t('inlineCanvas.review.generating', '正在生成截图…') })) : undefined, variant: chrome, title: title, versionLabel: versionLabel, versions: versions, activeVersionId: activeVersionId, versionSwitching: switching, onVersionChange: onVersionChange, canUndo: !switching && textEditing === null && changeList.canUndo, canRedo: !switching && textEditing === null && changeList.canRedo, onUndo: () => uiWrite(() => enqueue(() => changeListRef.current.undo())), onRedo: () => uiWrite(() => enqueue(() => changeListRef.current.redo())), changeCount: changeList.list.length, onExit: onExit, onSave: () => {
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1033
|
+
return (_jsxs("div", { className: `${CANVAS_UI_SCOPE} flex h-full min-h-0 flex-col bg-[#F5F7FC]`, children: [_jsxs("div", { className: "relative shrink-0", children: [interactive && (_jsxs("div", { role: "status", "data-testid": "inline-editor-interactive-bar", className: "absolute inset-0 z-20 flex items-center justify-center gap-3\n border-b border-[#ececef] bg-white text-[13px] text-[#1a1a1a]\n shadow-[0_4px_12px_0_rgba(9,30,66,0.12)]", children: [_jsx("span", { children: t('inlineCanvas.interactive.active', '您当前正在交互模式') }), _jsx(Button, { size: "sm", variant: "secondary", "data-testid": "inline-editor-interactive-exit", onClick: exitInteractive, children: t('inlineCanvas.interactive.exit', '返回编辑模式') })] })), _jsx(EditorChrome, { slotRight: capturing ? (_jsx("span", { role: "status", className: "text-xs text-muted-foreground", children: t('inlineCanvas.review.generating', '正在生成截图…') })) : undefined, variant: chrome, title: title, versionLabel: versionLabel, versions: versions, activeVersionId: activeVersionId, versionSwitching: switching, onVersionChange: onVersionChange, canUndo: !switching && textEditing === null && changeList.canUndo, canRedo: !switching && textEditing === null && changeList.canRedo, onUndo: () => uiWrite(() => enqueue(() => changeListRef.current.undo())), onRedo: () => uiWrite(() => enqueue(() => changeListRef.current.redo())), changeCount: changeList.list.length, onExit: onExit, onSave: () => {
|
|
1034
|
+
if (switchingRef.current) {
|
|
1035
|
+
setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
onSave?.(editor);
|
|
1039
|
+
}, onAddVersion: onAddVersion, onRenameVersion: onRenameVersion, removableVersionIds: removableVersionIds, onRemoveVersion: onRemoveVersion, onPreview: onPreview, onToggleChangeList: () => setShowChangeList(v => !v), changeListOpen: showChangeList, onToggleAi: onToggleAi, aiOpen: aiOpen })] }), _jsxs("div", { className: "relative flex min-h-0 flex-1", children: [_jsxs(ResizablePanelGroup, { direction: "horizontal", autoSaveId: "inline-editor-panes-v2", className: "flex min-h-0 w-full flex-1", children: [showLeftPanel && (_jsxs(_Fragment, { children: [_jsx(ResizablePanel, { id: "left", order: 1, defaultSize: 18, minSize: 12, maxSize: 40, className: "flex min-h-0 flex-col", children: _jsxs("aside", { "data-testid": "inline-editor-left-panel", className: "inline-editor-left relative flex h-full min-h-0 flex-col bg-white", children: [interactive && (_jsx("div", { "data-testid": "inline-editor-left-veil", className: "absolute inset-0 z-10 cursor-not-allowed bg-white/60" })), !!libraryComponents && (_jsx("div", { className: "flex h-9 shrink-0 items-center gap-1 border-b\n border-[#EBECF0] px-2", children: [
|
|
872
1040
|
['layers', '图层'],
|
|
873
1041
|
['assets', '组件']
|
|
874
1042
|
].map(([tab, label]) => (_jsx("button", { type: "button", "data-testid": `left-tab-${tab}`, "aria-pressed": leftTab === tab, onClick: () => setLeftTab(tab), className: `rounded px-2 py-1 text-[12px] ${leftTab === tab
|
|
@@ -879,23 +1047,48 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
879
1047
|
targetIdentifier: targetSelector,
|
|
880
1048
|
targetSelector,
|
|
881
1049
|
mode: moveModeFromDropPos(pos)
|
|
882
|
-
})) })) })] }) }), _jsx(ResizableHandle, { withHandle: true })] })), _jsx(ResizablePanel, { id: "canvas", order: 2, minSize: 30, children: _jsxs("div", { className: "flex h-full min-h-0 min-w-0 flex-col", children: [_jsx(CanvasContextBar, { url: url, device: device, onDeviceChange:
|
|
1050
|
+
})) })) })] }) }), _jsx(ResizableHandle, { withHandle: true })] })), _jsx(ResizablePanel, { id: "canvas", order: 2, minSize: 30, children: _jsxs("div", { className: "flex h-full min-h-0 min-w-0 flex-col", children: [_jsx(CanvasContextBar, { url: url, device: device, onDeviceChange: changeDevice, deviceDisabled: interactive, interactive: interactiveMode
|
|
1051
|
+
? {
|
|
1052
|
+
active: interactive,
|
|
1053
|
+
disabled: !interactive && !canEnterInteractive,
|
|
1054
|
+
onToggle: () => interactive
|
|
1055
|
+
? exitInteractive()
|
|
1056
|
+
: enterInteractive()
|
|
1057
|
+
}
|
|
1058
|
+
: undefined }), _jsxs("div", { ref: canvasAreaRef, className: "relative min-h-0 flex-1 overflow-hidden p-3", children: [editor.session.state === 'failed' && (_jsxs("div", { role: "alert", "data-testid": "inline-editor-session-failed", className: "absolute inset-3 z-20 flex flex-col items-center justify-center gap-3 rounded-lg bg-white p-6 text-center text-sm", children: [_jsx("p", { className: "font-semibold", children: t('inlineCanvas.review.pageLoadFailed', '目标页面无法加载') }), _jsx("p", { children: [
|
|
1059
|
+
'NO_EXTENSION',
|
|
1060
|
+
'EXTENSION_ID_MISSING',
|
|
1061
|
+
'CONTRACT_MISMATCH'
|
|
1062
|
+
].includes(editor.session.reason ?? '')
|
|
1063
|
+
? describeCanvasError(editor.session.reason)
|
|
1064
|
+
: t('inlineCanvas.review.pageLoadHint', '请返回检查页面地址、网站访问权限和网络连接后重试。') }), _jsx("code", { className: "text-xs text-muted-foreground", children: editor.session.reason }), _jsx(Button, { variant: "secondary", onClick: onExit, children: t('inlineCanvas.review.backFromFailed', '返回修改') })] })), frameProps && (_jsx(CanvasFrame, { url: frameProps.url, sessionId: frameProps.sessionId, device: frameProps.device, panelWidth: panelWidth, targetViewportWidth: targetViewportWidth[frameProps.device], onFrameLoad: () => { }, domReady: domReady, iframeRef: canvasIframeRef }, frameProps.sessionId))] })] }) })] }), !capturing && shot && (_jsx(ScreenshotCard, { dataUrl: shot.dataUrl, truncated: shot.truncated, scale: shot.scale, uploadImage: uploadImage, onClose: () => {
|
|
883
1065
|
setShot(null);
|
|
884
|
-
} }, shot.dataUrl)), _jsx(ChangeListPopover, { open: showChangeList && !capturing, onClose: () => setShowChangeList(false), children: _jsx(ChangeListPanel, { list: changeList.list, onRevert: id => uiWrite(() => writes.revert(id)), onRevertAll: () => uiWrite(() => writes.revertAll()), lastError: changeList.lastError, onHover: s => void commands.hover(s), onSelect: s => void selectAndReveal(s) }) })] }), !capturing && (_jsx(SelectionOverlay, { geometry: geometry.latest, context: overlayContext, selectedLabel: self ? `<${self.tag}>` : undefined })), !capturing && blockOverlay && (_jsx(BlockOverlay, { blocks: blockOverlay.blocks, activeId: blockOverlay.activeId, onHover: blockOverlay.onHover, geometry: geometry.latest, context: overlayContext, scale: editor.mobileFit.scale })), !capturing && commandError && (_jsxs(EditorStatus, { tone: "error", "data-testid": "inline-editor-command-error", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { children: commandError }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setCommandError(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && aiApplyNote && (_jsxs(EditorStatus, { "data-testid": "inline-editor-ai-apply-note", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { role: "status", children: aiApplyNote }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setAiApplyNote(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && move && (
|
|
1066
|
+
} }, shot.dataUrl)), _jsx(ChangeListPopover, { open: showChangeList && !capturing, onClose: () => setShowChangeList(false), children: _jsx(ChangeListPanel, { list: changeList.list, onRevert: id => uiWrite(() => writes.revert(id)), onRevertAll: () => uiWrite(() => writes.revertAll()), lastError: changeList.lastError, onHover: s => void commands.hover(s), onSelect: s => void selectAndReveal(s) }) })] }), !capturing && !interactive && (_jsx(SelectionOverlay, { geometry: geometry.latest, context: overlayContext, selectedLabel: self ? `<${self.tag}>` : undefined })), !capturing && !interactive && blockOverlay && (_jsx(BlockOverlay, { blocks: blockOverlay.blocks, activeId: blockOverlay.activeId, onHover: blockOverlay.onHover, geometry: geometry.latest, context: overlayContext, scale: editor.mobileFit.scale })), !capturing && commandError && (_jsxs(EditorStatus, { tone: "error", "data-testid": "inline-editor-command-error", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { children: commandError }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setCommandError(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && aiApplyNote && (_jsxs(EditorStatus, { "data-testid": "inline-editor-ai-apply-note", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { role: "status", children: aiApplyNote }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setAiApplyNote(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && !interactive && move && (_jsx("div", { role: "toolbar", "aria-label": t('inlineCanvas.move.title'), "data-testid": "inline-editor-move-bar", className: "fixed z-[903] -translate-x-1/2 rounded-lg border bg-popover p-3 text-xs text-popover-foreground shadow-md", style: {
|
|
1067
|
+
width: 'min(600px, calc(100vw - 32px))',
|
|
1068
|
+
left: `clamp(min(316px, 50vw), ${area.x + area.width / 2}px, max(calc(100vw - 316px), 50vw))`,
|
|
1069
|
+
top: `${area.y + 8}px`
|
|
1070
|
+
}, children: move.target === null ? (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { children: move.rejected === 'same-element'
|
|
885
1071
|
? '不能移到它自己里面,请另选一个目标'
|
|
886
|
-
: t('inlineCanvas.move.pickTarget') })
|
|
887
|
-
.confirmMove(pos)
|
|
888
|
-
.catch(e => setCommandError(describeCanvasError(e))), children: t(`inlineCanvas.move.${pos}`) }, pos)))] })), _jsx(Button, { size: "sm", variant: "secondary", type: "button", "data-testid": "move-cancel", onClick: () => commands.cancelMove(), children: t('inlineCanvas.move.cancel') })] })), !capturing && textEditing !== null && (_jsx(EditorStatus, { "data-testid": "inline-editor-text-editing-hint", style: {
|
|
1072
|
+
: t('inlineCanvas.move.pickTarget') }), _jsx(Button, { size: "default", variant: "secondary", type: "button", "data-testid": "move-cancel", onClick: () => commands.cancelMove(), children: t('inlineCanvas.move.cancel') })] })) : (_jsx(MovePositionControls, { onConfirm: commands.confirmMove, onCancel: commands.cancelMove, onError: e => setCommandError(describeCanvasError(e)) }, move.target)) })), !capturing && textEditing !== null && (_jsx(EditorStatus, { "data-testid": "inline-editor-text-editing-hint", style: {
|
|
889
1073
|
left: `${area.x + area.width / 2}px`,
|
|
890
1074
|
top: `${area.y + 8}px`
|
|
891
|
-
}, children: "\u6B63\u5728\u7F16\u8F91\u6587\u5B57 \u00B7 \u56DE\u8F66\u63D0\u4EA4 \u00B7 Esc \u53D6\u6D88" })), !capturing && !switching && !move && textEditing === null && (_jsx(SelectionToolbar, { selectedRect: selectedRect, area: area, capabilities: selection?.capabilities ?? null, isScrolling: geometry.isScrolling, onAction: onToolbarAction, host: { askAi: !!onAskAi } })), stylePanelOpen &&
|
|
1075
|
+
}, children: "\u6B63\u5728\u7F16\u8F91\u6587\u5B57 \u00B7 \u56DE\u8F66\u63D0\u4EA4 \u00B7 Esc \u53D6\u6D88" })), !capturing && !interactive && !switching && !move && textEditing === null && (_jsx(SelectionToolbar, { selectedRect: selectedRect, area: area, capabilities: selection?.capabilities ?? null, isScrolling: geometry.isScrolling, onAction: onToolbarAction, host: { askAi: !!onAskAi } })), stylePanelOpen &&
|
|
1076
|
+
!interactive &&
|
|
892
1077
|
!switching &&
|
|
893
1078
|
selectedRect &&
|
|
894
1079
|
self &&
|
|
895
1080
|
createPortal(_jsx("div", { className: `${CANVAS_UI_SCOPE} fixed z-[1000]`, children: _jsx(StylePanel, { anchor: {
|
|
896
1081
|
x: Math.min(selectedRect.x + selectedRect.width + 12, area.x + area.width - 332),
|
|
897
1082
|
y: Math.max(area.y + 8, selectedRect.y)
|
|
898
|
-
}, tag: self.tag, capabilities: selection?.capabilities ?? null,
|
|
1083
|
+
}, tag: self.tag, capabilities: selection?.capabilities ?? null, resettableProperties: changeList.list.flatMap(r => r.change.type === 'elementStyle' &&
|
|
1084
|
+
r.change.selector === self.selector
|
|
1085
|
+
? [r.change.property]
|
|
1086
|
+
: []), declaredStyles: Object.assign({}, currentStyles, ...changeList.list
|
|
1087
|
+
.filter(r => r.change.type === 'elementStyle' &&
|
|
1088
|
+
r.change.selector === self.selector)
|
|
1089
|
+
.map(r => r.change.type === 'elementStyle'
|
|
1090
|
+
? { [r.change.property]: r.change.value }
|
|
1091
|
+
: {})), onReset: property => {
|
|
899
1092
|
void uiWrite(async () => {
|
|
900
1093
|
for (const record of changeList
|
|
901
1094
|
.peekList()
|
|
@@ -3,7 +3,7 @@ import { CodeField } from './CodeField.js';
|
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { AlertTriangle } from 'lucide-react';
|
|
6
|
-
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@ptengine/design-components';
|
|
6
|
+
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, ToggleGroup, ToggleGroupItem } from '@ptengine/design-components';
|
|
7
7
|
import { pasteModes } from '../kernel/selection/toolbarButtons.js';
|
|
8
8
|
import { validateHtml } from './htmlValidation.js';
|
|
9
9
|
export function InsertElementDialog({ open, capabilities, onOpenChange, onApply }) {
|
|
@@ -21,7 +21,11 @@ export function InsertElementDialog({ open, capabilities, onOpenChange, onApply
|
|
|
21
21
|
}, [open, modeKey]);
|
|
22
22
|
const result = useMemo(() => validateHtml(html), [html]);
|
|
23
23
|
const canApply = html.trim().length > 0 && result.ok && modes.length > 0;
|
|
24
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { closeLabel: t('inlineCanvas.chrome.close', '关闭'), size: "md", className: "max-h-[calc(100dvh-32px)] overflow-y-auto", "data-testid": "insert-element-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.insert.title') }) }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.insert.html') }), _jsx(CodeField, { "aria-label": t('inlineCanvas.insert.html'), "data-testid": "insert-html", rows: 8, className: "font-mono text-[12px]", value: html, onChange: e => setHtml(e.target.value) })] }), modes.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.insert.position') }), _jsx(
|
|
24
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { closeLabel: t('inlineCanvas.chrome.close', '关闭'), size: "md", className: "max-h-[calc(100dvh-32px)] overflow-y-auto", "data-testid": "insert-element-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.insert.title') }) }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.insert.html') }), _jsx(CodeField, { "aria-label": t('inlineCanvas.insert.html'), "data-testid": "insert-html", rows: 8, className: "font-mono text-[12px]", value: html, onChange: e => setHtml(e.target.value) })] }), modes.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.insert.position') }), _jsx(ToggleGroup, { type: "single", variant: "outline", value: mode, onValueChange: value => {
|
|
25
|
+
if (modes.includes(value)) {
|
|
26
|
+
setMode(value);
|
|
27
|
+
}
|
|
28
|
+
}, "aria-label": t('inlineCanvas.insert.position'), style: { justifyContent: 'flex-start', flexWrap: 'wrap' }, children: modes.map(m => (_jsx(ToggleGroupItem, { value: m, "data-testid": `insert-mode-${m}`, children: t(`inlineCanvas.clipboard.${m}`) }, m))) })] })), html.trim().length > 0 && result.error && (_jsxs("p", { "data-testid": "insert-error", className: "flex items-start gap-1.5 text-xs leading-4 text-[#DE350B]", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3 shrink-0" }), t(result.error)] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "secondary", onClick: () => onOpenChange(false), children: t('common.cancel', '取消') }), _jsx(Button, { "data-testid": "insert-apply", disabled: !canApply, onClick: () => {
|
|
25
29
|
void onApply({ html: html.trim(), mode });
|
|
26
30
|
onOpenChange(false);
|
|
27
31
|
}, children: t('common.apply', '应用') })] })] }) }));
|
|
@@ -19,5 +19,18 @@ export interface CanvasContextBarProps {
|
|
|
19
19
|
url: string;
|
|
20
20
|
device: CanvasDevice;
|
|
21
21
|
onDeviceChange: (device: CanvasDevice) => void;
|
|
22
|
+
/** 交互模式下切设备会重建画布、把用户点出来的页面状态整个丢掉,所以一并禁用。 */
|
|
23
|
+
deviceDisabled?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* 「交互模式」开关,靠右。不传 = 没有这个按钮。
|
|
26
|
+
*
|
|
27
|
+
* 放在画布条而不是顶栏:它改变的是**画布**的行为(点击交给页面),与设备切换同属
|
|
28
|
+
* 「我在怎么看这一页」;老编辑器也是挨着设备切换放的。
|
|
29
|
+
*/
|
|
30
|
+
interactive?: {
|
|
31
|
+
active: boolean;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
onToggle: () => void;
|
|
34
|
+
};
|
|
22
35
|
}
|
|
23
|
-
export declare function CanvasContextBar({ url, device, onDeviceChange }: CanvasContextBarProps): import("react").JSX.Element;
|
|
36
|
+
export declare function CanvasContextBar({ url, device, onDeviceChange, deviceDisabled, interactive }: CanvasContextBarProps): import("react").JSX.Element;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { Link2, Monitor, Smartphone } from 'lucide-react';
|
|
4
|
-
import { ChromeSegment, ChromeSegmented } from '../chrome/topbarPrimitives.js';
|
|
5
|
-
export function CanvasContextBar({ url, device, onDeviceChange }) {
|
|
3
|
+
import { Link2, Monitor, MousePointerClick, Smartphone } from 'lucide-react';
|
|
4
|
+
import { ChromeSegment, ChromeSegmented, ChromeTextButton } from '../chrome/topbarPrimitives.js';
|
|
5
|
+
export function CanvasContextBar({ url, device, onDeviceChange, deviceDisabled, interactive }) {
|
|
6
6
|
const { t } = useTranslation();
|
|
7
7
|
const items = [
|
|
8
8
|
['pc', Monitor, t('inlineCanvas.chrome.pc', 'PC')],
|
|
9
9
|
['mobile', Smartphone, t('inlineCanvas.chrome.mobile', '移动')]
|
|
10
10
|
];
|
|
11
|
-
return (_jsxs("div", { "data-testid": "canvas-context-bar", className: "relative flex h-9 shrink-0 items-center border-b border-[#EBECF0]\n bg-white px-3", children: [url && (_jsxs("a", { "data-testid": "canvas-context-url", href: url, target: "_blank", rel: "noopener noreferrer", title: url, className: "flex max-w-[calc(50%-60px)] items-center gap-1.5 truncate text-xs\n text-[#8a8f98] hover:text-[#52525b] hover:underline", children: [_jsx(Link2, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: url })] })), _jsx(ChromeSegmented, { "data-testid": "device-switch", className: "absolute left-1/2 -translate-x-1/2", children: items.map(([value, Icon, label]) => (_jsxs(ChromeSegment, { "data-testid": `device-${value}`, active: device === value, onClick: () => onDeviceChange(value), children: [_jsx(Icon, { className: "size-3.5" }), label] }, value))) })
|
|
11
|
+
return (_jsxs("div", { "data-testid": "canvas-context-bar", className: "relative flex h-9 shrink-0 items-center border-b border-[#EBECF0]\n bg-white px-3", children: [url && (_jsxs("a", { "data-testid": "canvas-context-url", href: url, target: "_blank", rel: "noopener noreferrer", title: url, className: "flex max-w-[calc(50%-60px)] items-center gap-1.5 truncate text-xs\n text-[#8a8f98] hover:text-[#52525b] hover:underline", children: [_jsx(Link2, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: url })] })), _jsx(ChromeSegmented, { "data-testid": "device-switch", className: "absolute left-1/2 -translate-x-1/2", children: items.map(([value, Icon, label]) => (_jsxs(ChromeSegment, { "data-testid": `device-${value}`, active: device === value, disabled: deviceDisabled, onClick: () => onDeviceChange(value), children: [_jsx(Icon, { className: "size-3.5" }), label] }, value))) }), interactive && (_jsxs(ChromeTextButton, { "data-testid": "canvas-interactive-toggle", className: "ml-auto h-7 gap-1.5 px-2 text-xs", active: interactive.active, disabled: interactive.disabled, title: t('inlineCanvas.interactive.tips', '切换到交互模式可以对页面进行交互,以修改需要交互后才能看到的内容。'), onClick: interactive.onToggle, children: [_jsx(MousePointerClick, { className: "size-3.5" }), interactive.active
|
|
12
|
+
? t('inlineCanvas.interactive.exit', '返回编辑模式')
|
|
13
|
+
: t('inlineCanvas.interactive.enter', '交互模式')] }))] }));
|
|
12
14
|
}
|
|
@@ -248,10 +248,11 @@ export interface AiApplyDeps {
|
|
|
248
248
|
*/
|
|
249
249
|
ancestors: string[];
|
|
250
250
|
/**
|
|
251
|
-
*
|
|
251
|
+
* 货架里某一项的祖先链(由近及远,含作用域根自己)。**整页货架那些行用**。
|
|
252
252
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
253
|
+
* 纯元素作用域不传:那时 `ancestors` 就是选中元素的祖先链,而作用域里的后代都在它
|
|
254
|
+
* 底下,同一条链够用。元素作用域并上了整页货架时(宿主有整页模式)传,查的是并进来的那些行。
|
|
255
|
+
* 整页作用域不一样 —— 根是 `body`,它的 `ancestors` 是空数组,
|
|
255
256
|
* 拿它去判 `MOVE_ELEMENT` 的 target 会把**所有**移动判成非法(body 没有祖先)。
|
|
256
257
|
* 这个函数让那道闸改为对着「被移动的那个元素」的祖先链判,规则本身没变。
|
|
257
258
|
*
|
|
@@ -82,6 +82,9 @@ export interface UseChangeListResult {
|
|
|
82
82
|
outcome: HydrateOutcome;
|
|
83
83
|
idMap: Map<string, string>;
|
|
84
84
|
}>;
|
|
85
|
+
/** 当前清单原样重放(changeId 不变、不进撤销历史)。交互模式退出时用。 */
|
|
86
|
+
reapplyAll: () => Promise<void>;
|
|
87
|
+
whenIdle: () => Promise<unknown>;
|
|
85
88
|
canUndo: boolean;
|
|
86
89
|
canRedo: boolean;
|
|
87
90
|
undo: () => Promise<void>;
|
|
@@ -174,6 +174,7 @@ export function useChangeList(sessionId, options = {}) {
|
|
|
174
174
|
target.push(entry);
|
|
175
175
|
refreshHistory(n => n + 1);
|
|
176
176
|
}, false), [guard, restoreSnapshot]);
|
|
177
|
+
const reapplyAll = useCallback(() => guard(() => restoreSnapshot(listRef.current), false), [guard, restoreSnapshot]);
|
|
177
178
|
const undo = useCallback(() => travelHistory('undo'), [travelHistory]);
|
|
178
179
|
const redo = useCallback(() => travelHistory('redo'), [travelHistory]);
|
|
179
180
|
const addStyle = useCallback((input) => guard(() => push(buildStyleChange(input), input.ancestors)), [guard, push]);
|
|
@@ -249,6 +250,8 @@ export function useChangeList(sessionId, options = {}) {
|
|
|
249
250
|
clear,
|
|
250
251
|
hydrate,
|
|
251
252
|
replaceAll,
|
|
253
|
+
reapplyAll,
|
|
254
|
+
whenIdle: () => mutationQueue.current,
|
|
252
255
|
peekList,
|
|
253
256
|
canUndo: past.current.length > 0,
|
|
254
257
|
canRedo: future.current.length > 0,
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* · 装着 ≥2 个候选区块 = 外壳,拆开;只装着 1 个 = 它自己就是这一块(外层赢,小圆点、
|
|
17
17
|
* 进度条这些附属部件都在最外层里),但占页高 ≥60% 的单链节点是整页的框,让给内层;
|
|
18
18
|
* · 宿主给了页面模型(`blocks`)时,成员归属校正拆合,并在块根行写 `blockId`。
|
|
19
|
+
*
|
|
20
|
+
* **矮区块**(2026-10-21):几何门槛(高 ≥120)挡掉了真正的矮区块 —— vivaia 手机首页的新闻区只有 86px,
|
|
21
|
+
* 父级是外壳,它既不是块根也不在块根里,只能等回填,100 行用完时它的标题还没轮到,「把新闻区标题改成…」
|
|
22
|
+
* 两次被拒、0 处落地。门槛本身不降(降了,section 里的标题条也成候选,section 被拆成外壳)。补两条:
|
|
23
|
+
* · 页面模型的成员容器只要够宽就是候选,不看高度(热图说它是一块,它就是一块);
|
|
24
|
+
* · 块根的父级就是区块上下叠放的那一层:那里不够高、但够宽、有内容、不在 header/nav/footer 里的孩子,
|
|
25
|
+
* 也算一块(没有页面模型时的兜底)。
|
|
19
26
|
* 旧规则「内层有 heading 就赢」不可用:kit 的 `heading` 是**子树里**第一个标题(没有就取第一段
|
|
20
27
|
* 文字),section / 轮播轨道 / 每张幻灯片拿到的是同一句,规则又偏向内层 —— 真机 vivaia 首页的
|
|
21
28
|
* 首屏只剩三张幻灯片,模型照「首屏下面」插进了轮播(2026-10-20)。
|