@ptengine/inline-editor 0.1.4 → 0.1.6
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 +54 -0
- package/dist/InlineEditor.d.ts +15 -1
- package/dist/InlineEditor.js +85 -54
- package/dist/canvas-ui/tree/TreePanel.d.ts +2 -1
- package/dist/canvas-ui/tree/TreePanel.js +79 -39
- package/dist/chrome/EditorChrome.d.ts +2 -0
- package/dist/chrome/EditorChrome.js +1 -1
- package/dist/chrome/VersionMenu.d.ts +3 -1
- package/dist/chrome/VersionMenu.js +24 -12
- package/dist/chrome/topbarPrimitives.d.ts +5 -52
- package/dist/chrome/topbarPrimitives.js +13 -31
- package/dist/dialogs/ClickActionDialog.js +4 -5
- package/dist/dialogs/CodeField.d.ts +4 -0
- package/dist/dialogs/CodeField.js +10 -0
- package/dist/dialogs/HtmlEditDialog.js +3 -2
- package/dist/dialogs/ImageEditDialog.js +53 -15
- package/dist/dialogs/ImageSettingDialog.js +3 -3
- package/dist/dialogs/InsertElementDialog.js +6 -6
- package/dist/dialogs/MediaPreview.d.ts +5 -0
- package/dist/dialogs/MediaPreview.js +34 -0
- package/dist/dialogs/ReplaceVideoDialog.js +8 -4
- package/dist/index.d.ts +1 -0
- package/dist/kernel/blocks/BlockOverlay.js +1 -1
- package/dist/kernel/blocks/resolveBlocks.d.ts +16 -0
- package/dist/kernel/blocks/resolveBlocks.js +39 -6
- package/dist/kernel/canvasCommands.d.ts +7 -1
- package/dist/kernel/canvasCommands.js +3 -0
- package/dist/kernel/changelist/ChangeListPanel.js +11 -9
- package/dist/kernel/changelist/useChangeList.d.ts +4 -0
- package/dist/kernel/changelist/useChangeList.js +84 -10
- package/dist/kernel/selection/SelectionOverlay.js +1 -1
- package/dist/kernel/selection/SelectionToolbar.js +26 -8
- package/dist/kernel/selection/buildRootShelf.d.ts +49 -2
- package/dist/kernel/selection/buildRootShelf.js +156 -41
- package/dist/kernel/selection/elementAiContext.d.ts +11 -3
- package/dist/kernel/selection/resolveAnchors.d.ts +17 -0
- package/dist/kernel/selection/resolveAnchors.js +59 -11
- package/dist/locales.d.ts +186 -48
- package/dist/locales.js +10 -9
- package/dist/panels/EditorStatus.d.ts +5 -0
- package/dist/panels/EditorStatus.js +4 -0
- package/dist/panels/ScreenshotCard.d.ts +7 -0
- package/dist/panels/ScreenshotCard.js +57 -0
- package/dist/panels/StylePanel.d.ts +7 -1
- package/dist/panels/StylePanel.js +105 -12
- package/dist/reviewLocales.d.ts +140 -0
- package/dist/reviewLocales.js +134 -0
- package/dist/tree/LayersPanel.js +4 -2
- package/dist/useInlineEditor.d.ts +12 -1
- package/dist/useInlineEditor.js +21 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
**变更**
|
|
6
|
+
|
|
7
|
+
- 整页货架(没选中元素时交给 AI 的那份)的「区块」改为**只装着这一块的最外层容器**。此前「内层带 heading 就赢」,而 heading 是子树里
|
|
8
|
+
第一个标题,section / 轮播轨道 / 每张幻灯片拿到同一句 —— 首屏只剩轮播的几张幻灯片,「首屏下面插入」落进了轮播(真机 vivaia 首页)。
|
|
9
|
+
现在:父级横排里的几个大项(轮播的每一张、并列的列)不单独成块;装着 ≥2 个区块的是外壳、拆开;只装着 1 个的外层就是这一块;
|
|
10
|
+
占页高 ≥60% 的单链节点(main / 页面包装层)让给内层。区块很多时每块少给几行叶子,块根行一个不落。
|
|
11
|
+
- 分析路径的区块成员(`resolveBlocks` → `<page-block>` → 方案锚点)同口径上提到块根:热图成员是轮播第 1 张时,成员与方案锚点
|
|
12
|
+
改为整个首屏 section,首版不再照锚点把「首屏下方」的新模块插进轮播。整页树拉不到、或容器还装着别的区块时,成员原样不动。
|
|
13
|
+
- 编辑器交互与媒体流程统一:替换图片 / 视频、插入元素的对话框加上加载中与失败可见的媒体预览;元素截图改为结果卡片(放大预览、
|
|
14
|
+
下载、上传后复制链接,超出可视区或缩放时注明);操作反馈统一成一种状态条;代码输入框统一等宽字体;版本菜单与顶栏重排,
|
|
15
|
+
图层树搜索补上无结果与清除。
|
|
16
|
+
- 方案锚点可以带 MOVE 的目标:编辑器打开时,目标与锚点一样在活页面上重新解析(selector 唯一命中优先,失效按 `targetLabel` 找回),
|
|
17
|
+
找到的目标并进整页货架与祖先表 —— 目标不是块根时整页 MOVE 也不再因「不在货架上」被拒;找不到就按没解析到报、不授权。
|
|
18
|
+
|
|
19
|
+
**修复**
|
|
20
|
+
|
|
21
|
+
- 接通撤销和重做的操作历史;覆盖同一属性后可逐步还原,新增编辑会清空重做,切版本或会话会重置历史。回放保留 changeId,并同步插入元素的 AI 作用域。
|
|
22
|
+
- 样式面板支持标题栏拖拽与视口边界约束;连续尺寸变化不再累加偏移,顶层浮层与全屏宿主正确叠放,保留后续弹窗的遮盖关系。
|
|
23
|
+
- 修复改动清单的默认深色按钮及多行内容高度;全部撤销的确认文案与可恢复历史保持一致。
|
|
24
|
+
- 版本菜单统一左对齐,新增版本使用中性样式,重命名图标 hover 不再出现背景块。
|
|
25
|
+
- 点击动作类型改用 `@ptengine/design-components` 的 `RadioGroup / RadioGroupItem`,保留按类型切换字段和已填内容。
|
|
26
|
+
|
|
27
|
+
**新增**
|
|
28
|
+
|
|
29
|
+
- `InlineEditor` 的 `pageBlocks` prop(页面模型区块 `{ id, members }`):传了之后块根行带 `AiDescendant.blockId` —— 热图成员是轮播第 1 张时,
|
|
30
|
+
id 落在整个首屏 section 上。宿主据此打 `[B1 …]`,不再按 selector 逐字匹配成员。每个会话读一次,与 `anchors` 同一条纪律。
|
|
31
|
+
- 导出类型 `ShelfBlockHint`。
|
|
32
|
+
- `resolveBlocks` 的 `opts.viewportWidth`(块根判据的宽度门槛,缺省 375);`PageModelBlockMember.inRow`(成员在整页树上的横排位,
|
|
33
|
+
`<page-block>` 成员行据此打 `[横排 i/n]`,与整页货架同一份)。
|
|
34
|
+
- `InlinePlanAnchor` 的 `targetSelector` / `targetLabel`(只给 MOVE_ELEMENT);`ResolvedAnchor` 的 `targetResolved` / `targetVia` /
|
|
35
|
+
`resolvedTargetSelector`,宿主据此打印目标。
|
|
36
|
+
|
|
37
|
+
## 0.1.5
|
|
38
|
+
|
|
39
|
+
**变更**
|
|
40
|
+
|
|
41
|
+
- 截图之后把画布滚回截图前的位置。截图只能拍可视区(元素截图先滚过去、整页图一屏屏滚到底),此前 agent 拍完不滚回来 ——
|
|
42
|
+
进编辑器首版自动拍整页图 + 锚点近景后,页面停在最后一张近景那里。`captureSelectedElement`、`InlineEditorHandle.capturePage`、
|
|
43
|
+
`PagePreviewHandle.capturePage` 都会滚回;锚点近景几张连拍,只在最后滚回一次。
|
|
44
|
+
|
|
45
|
+
**新增**
|
|
46
|
+
|
|
47
|
+
- `useInlineEditor().commands.preserveScroll(run)`:跑完 `run` 把画布滚回开始前的位置(取几何帧的 `scrollY`)。
|
|
48
|
+
嵌套时只有最外层还原;几何帧还没到(不知道原位置)就不滚;滚回失败不抛。
|
|
49
|
+
|
|
3
50
|
## 0.1.4
|
|
4
51
|
|
|
5
52
|
**变更**
|
|
53
|
+
|
|
6
54
|
- AI 改动里的图片地址(`CHANGE_IMAGE`、`EDIT_HTML` 与 `INSERT_ELEMENT` 里的 `img[src]`)**不再按域名放行**,只要求 http(s) 绝对地址。
|
|
7
55
|
此前只认宿主注入的 origin 表 + 本会话读到过的地址,生图域名随部署环境变(测试区产图在 npstaticprod),漏配一个就整处改动被拒。
|
|
8
56
|
`javascript:` / `data:` / 相对地址 / 没有 `src` 仍以 `UNSAFE_IMAGE_SRC` 拒;`srcset` 照旧剥掉,`style` 里的 `url()` 照旧禁止。
|
|
@@ -11,6 +59,7 @@
|
|
|
11
59
|
## 0.1.3
|
|
12
60
|
|
|
13
61
|
**变更**
|
|
62
|
+
|
|
14
63
|
- 整页作用域(没选中元素)里 AI 的 `MOVE_ELEMENT`:`targetSelector` 除了被移动元素的祖先,现在也可以是**整页货架里的任意一行**
|
|
15
64
|
—— 「把 FAQ 整块挪到另一个区块后面」这类调换区块顺序的改动终于能落地(此前一律 `INVALID_MOVE_TARGET`)。
|
|
16
65
|
仍然拒绝:不在货架上的(编出来的)target、被移动元素自己、在被移动元素里面的行(防环,靠建货架时记的祖先链判)、
|
|
@@ -23,6 +72,7 @@
|
|
|
23
72
|
全部是新增:扩展不支持契约 v11 `feature:node-fields`(取树不回 `facts`)时,货架行与原来逐字一致。
|
|
24
73
|
|
|
25
74
|
**新增**
|
|
75
|
+
|
|
26
76
|
- AI 货架行标出父级横排:`AiDescendant.inRow`(`{ index, count }`),`formatShelfRow` 在引用文本之后、区块身份之前写
|
|
27
77
|
` [横排 i/n]`(与 frontend-base `<page-block>` 的行尾逐字一致)。父级是 flex 行(不换行;换行的要孩子同一行)或多列 grid
|
|
28
78
|
时才写 —— `after` 这一行会排到它旁边而不是下方。对整棵树判:父级常是货架跳过的包装层(PageFly `.pf-c`)。
|
|
@@ -37,6 +87,7 @@
|
|
|
37
87
|
全部是新增,不传新 props 的宿主行为不变。
|
|
38
88
|
|
|
39
89
|
**新增**
|
|
90
|
+
|
|
40
91
|
- `InlineEditor` 可选 props `onAiInsertedRemoved` / `onAiInsertedRestored`(`useInlineEditor` 的同名输入、
|
|
41
92
|
`useChangeList(sessionId, { onInsertedRemoved, onInsertedRestored })` 同步透传):撤销(单条 / 整批 / 换版本)
|
|
42
93
|
拿掉 AI 插入的元素时回报其 selector,并从包内的 AI 作用域剔除;回放(切版本 / 切设备)把它们放回画布时回报,
|
|
@@ -50,6 +101,7 @@
|
|
|
50
101
|
首个发布版。自 frontend-base(`cecb6de77`)迁入 `inline-editor`,其内部依赖的 `canvas-ui` 并入本包的内部目录(不单独发布)。
|
|
51
102
|
|
|
52
103
|
**产物**
|
|
104
|
+
|
|
53
105
|
- 仅 ESM,**逐模块输出**:每个源文件各出一个 `.js` 与 `.d.ts`,目录结构与源码一致,入口 `dist/index.js` / `dist/index.d.ts`;
|
|
54
106
|
相对导入均带 `.js` 扩展名(Node ESM 可解析);无 sourcemap。消费方打包器能像吃源码时一样按模块摇树、分 chunk
|
|
55
107
|
(单文件产物会被主应用对 `inlineEditorLocales` 等的静态引用整个拉进主 chunk)。
|
|
@@ -61,12 +113,14 @@
|
|
|
61
113
|
`lucide-react` / `@ptengine/inline-core`(dependencies)。
|
|
62
114
|
|
|
63
115
|
**配置入口(不兼容变更,相对迁入前的源码级用法)**
|
|
116
|
+
|
|
64
117
|
- 新增 `configureInlineEditor({ extensionId, disabledCapabilities })`,以及 `getInlineEditorConfig()` / `resetInlineEditorConfig()`。
|
|
65
118
|
预构建产物不再读 `import.meta.env`,扩展 id 与禁用能力改由宿主在应用启动时显式传入。
|
|
66
119
|
不配 `extensionId` = 内测模式(回退读 `<html data-ptengine-kit-id>`);配了 = 严格模式,只认这一个 id。
|
|
67
120
|
- 删除 `EXTENSION_ID` 常量导出;需要当前 id 请用 `getExtensionId()`。
|
|
68
121
|
|
|
69
122
|
**发布前校验**
|
|
123
|
+
|
|
70
124
|
- `pnpm build && pnpm test:build`:断言逐模块产物齐全、无测试文件、相对导入带扩展名且可解析、
|
|
71
125
|
canvas-ui 的 CSS import 与 CSS 文件在位且被 `sideEffects` 命中、外部依赖未内联、d.ts 不引 Node 类型;
|
|
72
126
|
在临时「装包方」目录里以 `--skipLibCheck false` 编译类型夹具(含 `DomTreeNode.href` 模块增强),
|
package/dist/InlineEditor.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ChromeVariant } from './chrome/EditorChrome.js';
|
|
|
3
3
|
import type { PageModelBlock } from './kernel/blocks/resolveBlocks.js';
|
|
4
4
|
import type { AdaptDomTreeOptions } from './tree/domTreeAdapter.js';
|
|
5
5
|
import { type AnchorShot, type ElementAiContext } from './kernel/selection/elementAiContext.js';
|
|
6
|
+
import { type ShelfBlockHint } from './kernel/selection/buildRootShelf.js';
|
|
6
7
|
import { type InlinePlanAnchor } from './kernel/selection/resolveAnchors.js';
|
|
7
8
|
import { type AiApplyOutcome, type AiScopeGrant, type AiWireChange } from './kernel/ai/aiInlineApply.js';
|
|
8
9
|
import { type LibraryComponent } from './assets/componentLibrary.js';
|
|
@@ -139,6 +140,17 @@ export interface InlineEditorProps {
|
|
|
139
140
|
* 不传 = 与今天完全一致。
|
|
140
141
|
*/
|
|
141
142
|
anchors?: InlinePlanAnchor[];
|
|
143
|
+
/**
|
|
144
|
+
* **页面模型的区块**(热图区块配置解析出来的 `{ id, members }`,宿主的 `PageModelBlock` 原样传)。
|
|
145
|
+
*
|
|
146
|
+
* 传了之后整页货架会按成员归属校正「哪一层算一块」,并在块根行(只装着这一个区块的最外层
|
|
147
|
+
* 容器)写 `blockId` —— 宿主据此打 `[B1 …]`。热图成员常是轮播的第 1 张:没有这一步,标签
|
|
148
|
+
* 打在那一张上,「首屏下面」就插进了轮播(真机 2026-10-20)。
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ 与 `anchors` 同一条纪律:只在 `onPageContextReady` 那条路上生效,**每个会话读一次**,
|
|
151
|
+
* 用 ref 存最新值、不进采集 effect 的依赖。不传 = 不打 `blockId`,块根照结构判。
|
|
152
|
+
*/
|
|
153
|
+
pageBlocks?: ReadonlyArray<ShelfBlockHint>;
|
|
142
154
|
/**
|
|
143
155
|
* 编辑器可用了(画布就绪、命令能发了)——**每个会话交一次**把手。
|
|
144
156
|
*
|
|
@@ -222,6 +234,8 @@ export interface InlineEditorProps {
|
|
|
222
234
|
* 名字是 experience 级状态(保存时作为 `summary` 落库),改不改得成由宿主说了算。
|
|
223
235
|
*/
|
|
224
236
|
onRenameVersion?: (id: string, name: string) => void;
|
|
237
|
+
removableVersionIds?: string[];
|
|
238
|
+
onRemoveVersion?: (id: string) => void;
|
|
225
239
|
/**
|
|
226
240
|
* 真实页面预览。**不传则顶栏不渲染那个入口** —— 预览链接由后端按 experience + 版本
|
|
227
241
|
* 签发(同详情页的「真实页面预览」),包里没有 experience 的概念,也不知道
|
|
@@ -340,4 +354,4 @@ export interface AiApplyApi {
|
|
|
340
354
|
/** 当前版本 —— 宿主把 agent 给的版本**名**映射成 id 时要知道现在在哪一版。 */
|
|
341
355
|
readonly activeVersionId?: string;
|
|
342
356
|
}
|
|
343
|
-
export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay }: InlineEditorProps): import("react").JSX.Element;
|
|
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;
|
package/dist/InlineEditor.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { EditorStatus } from './panels/EditorStatus.js';
|
|
3
|
+
import { ScreenshotCard } from './panels/ScreenshotCard.js';
|
|
2
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { createPortal } from 'react-dom';
|
|
4
7
|
import { CANVAS_UI_SCOPE } from './canvas-ui/index.js';
|
|
5
8
|
import { EditorChrome } from './chrome/EditorChrome.js';
|
|
6
9
|
import { CanvasFrame, TARGET_VIEWPORT_WIDTH, computeZoom } from './kernel/CanvasFrame.js';
|
|
@@ -16,14 +19,14 @@ import { buildElementAiContext, extractPageLinks, linksFragment, ROOT_SHELF_LIMI
|
|
|
16
19
|
import { getAncestorChain, getDomTree, getElementCapabilities, getElementImages, getOuterHtml, resolveCanvasSelector } from './kernel/canvasCommands.js';
|
|
17
20
|
import { useAiSelectionContext } from './kernel/selection/useAiSelectionContext.js';
|
|
18
21
|
import { buildDescendantShelf } from './kernel/selection/buildDescendantShelf.js';
|
|
19
|
-
import { buildRootShelf } from './kernel/selection/buildRootShelf.js';
|
|
22
|
+
import { ROOT_SHELF_TREE_PARAMS, buildRootShelf } from './kernel/selection/buildRootShelf.js';
|
|
20
23
|
import { SHELF_LAYOUT_FIELDS } from './kernel/selection/rowLayout.js';
|
|
21
24
|
import { backgroundImageAncestorFor, blockContainerFor, resolveInlinePlanAnchors, withImageRows } from './kernel/selection/resolveAnchors.js';
|
|
22
25
|
import { elementGeometryForAi } from './kernel/selection/elementGeometryForAi.js';
|
|
23
26
|
import { applyAiChanges } from './kernel/ai/aiInlineApply.js';
|
|
24
27
|
import { APPLY_CHECK_SETTLE_MS } from './kernel/ai/postApplyCheck.js';
|
|
25
28
|
import { deleteElement, editHtml, editStyle, insertElement, replaceImage, replaceVideo, setClickAction, setImageAlt } from './kernel/changelist/useChangeList.js';
|
|
26
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@ptengine/design-components';
|
|
29
|
+
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@ptengine/design-components';
|
|
27
30
|
import { pasteModes } from './kernel/selection/toolbarButtons.js';
|
|
28
31
|
import { ClickActionDialog } from './dialogs/ClickActionDialog.js';
|
|
29
32
|
import { ImageEditDialog } from './dialogs/ImageEditDialog.js';
|
|
@@ -51,7 +54,7 @@ function imageTargetOf(caps) {
|
|
|
51
54
|
return branch === 'background' ? 'background' : undefined;
|
|
52
55
|
}
|
|
53
56
|
const AI_ELEMENT_IMAGE_LIMIT = 4;
|
|
54
|
-
export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay }) {
|
|
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 }) {
|
|
55
58
|
const { t } = useTranslation();
|
|
56
59
|
const canvasAreaRef = useRef(null);
|
|
57
60
|
const canvasIframeRef = useRef(null);
|
|
@@ -73,7 +76,6 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
73
76
|
const [commandError, setCommandError] = useState(null);
|
|
74
77
|
const [aiApplyNote, setAiApplyNote] = useState(null);
|
|
75
78
|
const [shot, setShot] = useState(null);
|
|
76
|
-
const [shotUrl, setShotUrl] = useState(null);
|
|
77
79
|
const [panelWidth, setPanelWidth] = useState(0);
|
|
78
80
|
const [showChangeList, setShowChangeList] = useState(false);
|
|
79
81
|
const [stylePanelOpen, setStylePanelOpen] = useState(false);
|
|
@@ -349,7 +351,10 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
349
351
|
tag: self.tag,
|
|
350
352
|
imageTarget: imageTargetOf(selection?.capabilities),
|
|
351
353
|
images: elementImages?.selector === self.selector ? elementImages.images : undefined,
|
|
352
|
-
ancestors: (selection?.chain ?? [])
|
|
354
|
+
ancestors: (selection?.chain ?? [])
|
|
355
|
+
.map(n => n.selector)
|
|
356
|
+
.slice()
|
|
357
|
+
.reverse(),
|
|
353
358
|
outerHtml,
|
|
354
359
|
descendants: descendantShelfRef.current,
|
|
355
360
|
hasChildren: (selection?.capabilities?.childElementCount ?? 0) > 0,
|
|
@@ -426,6 +431,10 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
426
431
|
useEffect(() => {
|
|
427
432
|
anchorsRef.current = anchors;
|
|
428
433
|
}, [anchors]);
|
|
434
|
+
const pageBlocksRef = useRef(pageBlocks);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
pageBlocksRef.current = pageBlocks;
|
|
437
|
+
}, [pageBlocks]);
|
|
429
438
|
useEffect(() => {
|
|
430
439
|
rootShelfRef.current = [];
|
|
431
440
|
rootShelfAncestorsRef.current = new Map();
|
|
@@ -442,21 +451,14 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
442
451
|
pageContextSessionRef.current = sessionIdForAiContext;
|
|
443
452
|
let stale = false;
|
|
444
453
|
void (async () => {
|
|
445
|
-
const root = await getDomTree(sessionIdForAiContext,
|
|
446
|
-
rootSelector: 'body',
|
|
447
|
-
maxDepth: 16,
|
|
448
|
-
maxChildren: 40,
|
|
449
|
-
maxNodes: 1500,
|
|
450
|
-
visibleOnly: true,
|
|
451
|
-
includeRect: true,
|
|
452
|
-
fields: [...SHELF_LAYOUT_FIELDS]
|
|
453
|
-
});
|
|
454
|
+
const root = await getDomTree(sessionIdForAiContext, ROOT_SHELF_TREE_PARAMS);
|
|
454
455
|
const { html } = await getOuterHtml(sessionIdForAiContext, 'body');
|
|
455
456
|
if (stale)
|
|
456
457
|
return;
|
|
457
458
|
const { shelf, ancestorsOf } = buildRootShelf(root, {
|
|
458
459
|
limit: ROOT_SHELF_LIMIT,
|
|
459
|
-
viewportWidth: geometryRef.current?.viewportWidth
|
|
460
|
+
viewportWidth: geometryRef.current?.viewportWidth,
|
|
461
|
+
blocks: pageBlocksRef.current
|
|
460
462
|
});
|
|
461
463
|
const planAnchors = anchorsRef.current ?? [];
|
|
462
464
|
const anchorOut = planAnchors.length
|
|
@@ -480,7 +482,11 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
480
482
|
: null;
|
|
481
483
|
if (stale)
|
|
482
484
|
return;
|
|
483
|
-
const
|
|
485
|
+
const blockIdBySelector = new Map(shelf.flatMap(r => (r.blockId ? [[r.selector, r.blockId]] : [])));
|
|
486
|
+
const anchorRows = (anchorOut?.shelf ?? []).map(r => {
|
|
487
|
+
const blockId = blockIdBySelector.get(r.selector);
|
|
488
|
+
return blockId && !r.blockId ? { ...r, blockId } : r;
|
|
489
|
+
});
|
|
484
490
|
const anchorSelectors = new Set(anchorRows.map(r => r.selector));
|
|
485
491
|
const mergedShelf = [
|
|
486
492
|
...anchorRows,
|
|
@@ -505,14 +511,22 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
505
511
|
for (const { anchor, selector } of anchorBlocks) {
|
|
506
512
|
try {
|
|
507
513
|
const { html: blockHtml } = await getOuterHtml(sessionIdForAiContext, selector);
|
|
508
|
-
fragments.push({
|
|
514
|
+
fragments.push({
|
|
515
|
+
label: anchor.blockId ?? anchor.label,
|
|
516
|
+
selector,
|
|
517
|
+
html: blockHtml
|
|
518
|
+
});
|
|
509
519
|
}
|
|
510
520
|
catch {
|
|
511
521
|
}
|
|
512
522
|
const backgroundUp = anchor.blockSelectors?.length
|
|
513
523
|
? null
|
|
514
524
|
: backgroundImageAncestorFor(anchor.selector, mergedShelf, mergedAncestors);
|
|
515
|
-
const imageRoots = Array.from(new Set([
|
|
525
|
+
const imageRoots = Array.from(new Set([
|
|
526
|
+
...(anchor.blockSelectors ?? []),
|
|
527
|
+
selector,
|
|
528
|
+
...(backgroundUp ? [backgroundUp] : [])
|
|
529
|
+
])).slice(0, 3);
|
|
516
530
|
const seenUrls = new Set(images.map(i => i.url));
|
|
517
531
|
for (const root of imageRoots) {
|
|
518
532
|
try {
|
|
@@ -561,20 +575,23 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
561
575
|
fragments.push(links);
|
|
562
576
|
}
|
|
563
577
|
const captureAnchors = anchorBlocks.length
|
|
564
|
-
?
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
+
?
|
|
579
|
+
() => commandsRef.current.preserveScroll(async () => {
|
|
580
|
+
const shots = [];
|
|
581
|
+
for (const { anchor, selector } of anchorBlocks) {
|
|
582
|
+
const shotSelector = anchor.blockSelectors?.[0] ?? selector;
|
|
583
|
+
const shot = await captureAiShot(shotSelector);
|
|
584
|
+
const base = {
|
|
585
|
+
...(anchor.blockId ? { blockId: anchor.blockId } : {}),
|
|
586
|
+
label: anchor.label,
|
|
587
|
+
selector: shotSelector
|
|
588
|
+
};
|
|
589
|
+
shots.push('url' in shot
|
|
590
|
+
? { ...base, url: shot.url }
|
|
591
|
+
: { ...base, error: shot.error });
|
|
592
|
+
}
|
|
593
|
+
return shots;
|
|
594
|
+
})
|
|
578
595
|
: undefined;
|
|
579
596
|
const frame = geometryRef.current;
|
|
580
597
|
onPageContextReady(buildElementAiContext({
|
|
@@ -648,7 +665,10 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
648
665
|
: {
|
|
649
666
|
selector: 'body',
|
|
650
667
|
identifier: 'body',
|
|
651
|
-
descendants: () => [
|
|
668
|
+
descendants: () => [
|
|
669
|
+
...rootShelfRef.current.map(d => d.selector),
|
|
670
|
+
...aiInsertedRef.current
|
|
671
|
+
],
|
|
652
672
|
ancestors: [],
|
|
653
673
|
ancestorsOf: (selector) => rootShelfAncestorsRef.current.get(selector)
|
|
654
674
|
};
|
|
@@ -682,7 +702,11 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
682
702
|
scope: opts?.grant ? { ...scope, grant: opts.grant } : scope,
|
|
683
703
|
resolve: async (selector) => {
|
|
684
704
|
const r = await resolveCanvasSelector(sessionIdForAiContext, selector);
|
|
685
|
-
return {
|
|
705
|
+
return {
|
|
706
|
+
matched: r.matched,
|
|
707
|
+
matchCount: r.matchCount,
|
|
708
|
+
viewportRect: r.viewportRect
|
|
709
|
+
};
|
|
686
710
|
},
|
|
687
711
|
insertedSelectorsOf: (changeId) => changeListRef.current.peekList().find(r => r.changeId === changeId)
|
|
688
712
|
?.insertedSelectors ?? [],
|
|
@@ -821,15 +845,10 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
821
845
|
if (buttonId === 'screenshot') {
|
|
822
846
|
setCommandError(null);
|
|
823
847
|
setShot(null);
|
|
824
|
-
setShotUrl(null);
|
|
825
848
|
commands
|
|
826
849
|
.captureSelectedElement(self.selector)
|
|
827
850
|
.then(async (outcome) => {
|
|
828
851
|
setShot(outcome);
|
|
829
|
-
if (!uploadImage)
|
|
830
|
-
return;
|
|
831
|
-
const blob = await (await fetch(outcome.dataUrl)).blob();
|
|
832
|
-
setShotUrl(await uploadImage(blob));
|
|
833
852
|
})
|
|
834
853
|
.catch(error => setCommandError(describeCanvasError(error)));
|
|
835
854
|
return;
|
|
@@ -843,17 +862,16 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
843
862
|
}
|
|
844
863
|
}, [clipboard, commands, depsOf, frameProps?.sessionId, selection, t, uploadImage]);
|
|
845
864
|
const self = selection?.chain[0];
|
|
846
|
-
return (_jsxs("div", { className: `${CANVAS_UI_SCOPE} flex h-full min-h-0 flex-col bg-[#F5F7FC]`, children: [_jsx(EditorChrome, { variant: chrome, title: title, versionLabel: versionLabel, versions: versions, activeVersionId: activeVersionId, versionSwitching: switching, onVersionChange: onVersionChange, changeCount: changeList.list.length, onExit: onExit, onSave: () => {
|
|
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: () => {
|
|
847
866
|
if (switchingRef.current) {
|
|
848
867
|
setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
|
|
849
868
|
return;
|
|
850
869
|
}
|
|
851
870
|
onSave?.(editor);
|
|
852
|
-
}, onAddVersion: onAddVersion, onRenameVersion: onRenameVersion, 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 flex h-full min-h-0 flex-col bg-white", children: [!!libraryComponents && (_jsx("div", { className: "flex h-9 shrink-0 items-center gap-1 border-b\n border-[#EBECF0] px-2", children: [
|
|
871
|
+
}, 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 flex h-full min-h-0 flex-col bg-white", children: [!!libraryComponents && (_jsx("div", { className: "flex h-9 shrink-0 items-center gap-1 border-b\n border-[#EBECF0] px-2", children: [
|
|
853
872
|
['layers', '图层'],
|
|
854
873
|
['assets', '组件']
|
|
855
|
-
]
|
|
856
|
-
.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
|
|
874
|
+
].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
|
|
857
875
|
? 'bg-[#F4F5F7] font-semibold text-[#091E42]'
|
|
858
876
|
: 'text-[#6B778C]'}`, children: label }, tab))) })), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: leftTab === 'assets' && libraryComponents ? (_jsx(AssetsPanel, { components: libraryComponents, loading: libraryLoading, canInsert: !!selection?.chain[0] && !switching, onInsert: component => void insertComponent(component) })) : (_jsx(LayersPanel, { tree: tree, selectedSelector: selection?.selector ?? null, onSelect: s => void selectAndReveal(s), onHover: s => void commands.hover(s), onExpand: id => void commands.expandTreeNode(id), onMove: (dragSelector, targetSelector, pos) => uiWrite(() => writes.addMove({
|
|
859
877
|
identifier: dragSelector,
|
|
@@ -861,25 +879,38 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
|
|
|
861
879
|
targetIdentifier: targetSelector,
|
|
862
880
|
targetSelector,
|
|
863
881
|
mode: moveModeFromDropPos(pos)
|
|
864
|
-
})) })) })] }) }), _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: onDeviceChange }), _jsx("div", { ref: canvasAreaRef, className: "relative min-h-0 flex-1 overflow-hidden p-3", children: 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 && (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}, children: "\u5173\u95ED" })] }), _jsx("img", { src: shot.dataUrl, alt: "", className: "max-h-40 w-full rounded border border-[#EBECF0] object-contain" }), shot.truncated && (_jsx("p", { "data-testid": "inline-editor-shot-truncated", className: "mt-1.5 text-[11px] leading-4 text-[#DE350B]", children: "\u5143\u7D20\u8D85\u51FA\u53EF\u89C6\u533A\uFF0C\u53EA\u622A\u5230\u53EF\u89C1\u90E8\u5206" })), shot.scale < 1 && (_jsxs("p", { "data-testid": "inline-editor-shot-scale", className: "mt-1.5 text-[11px] leading-4 text-[#6B778C]", children: ["\u5F53\u524D\u7A97\u53E3\u8F83\u7A84\uFF0C\u622A\u56FE\u5206\u8FA8\u7387\u7EA6\u4E3A\u539F\u56FE\u7684", ` ${Math.round(shot.scale * 100)}%`] })), shotUrl && (_jsx("a", { "data-testid": "inline-editor-shot-url", href: shotUrl, target: "_blank", rel: "noopener noreferrer", className: "mt-1.5 block truncate text-[11px] text-[#0052CC] underline", children: shotUrl }))] })), _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 && (_jsx("div", { "data-testid": "inline-editor-command-error", className: "fixed z-[2147483003] -translate-x-1/2 rounded bg-[#DE350B] px-2.5\n py-1 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, onClick: () => setCommandError(null), children: commandError })), !capturing && aiApplyNote && (_jsx("div", { "data-testid": "inline-editor-ai-apply-note", className: "fixed z-[2147483003] max-w-[520px] -translate-x-1/2 rounded\n bg-[#253858] px-2.5 py-1 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, onClick: () => setAiApplyNote(null), children: aiApplyNote })), !capturing && move && (_jsxs("div", { "data-testid": "inline-editor-move-bar", className: "fixed z-[2147483003] flex -translate-x-1/2 items-center gap-2\n rounded bg-[#091E42] px-2.5 py-1.5 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [move.target === null ? (_jsx(_Fragment, { children: _jsx("span", { children: move.rejected === 'same-element'
|
|
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: onDeviceChange }), _jsx("div", { ref: canvasAreaRef, className: "relative min-h-0 flex-1 overflow-hidden p-3", children: 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
|
+
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 && (_jsxs(EditorStatus, { "data-testid": "inline-editor-move-bar", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [move.target === null ? (_jsx(_Fragment, { children: _jsx("span", { children: move.rejected === 'same-element'
|
|
868
885
|
? '不能移到它自己里面,请另选一个目标'
|
|
869
|
-
: t('inlineCanvas.move.pickTarget') }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: t('inlineCanvas.move.position') }), ['top', 'bottom', 'before', 'after'].map(pos => (_jsx("
|
|
886
|
+
: t('inlineCanvas.move.pickTarget') }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: t('inlineCanvas.move.position') }), ['top', 'bottom', 'before', 'after'].map(pos => (_jsx(Button, { size: "sm", variant: "secondary", type: "button", "data-testid": `move-position-${pos}`, onClick: () => void commands
|
|
870
887
|
.confirmMove(pos)
|
|
871
|
-
.catch(e => setCommandError(describeCanvasError(e))), children: t(`inlineCanvas.move.${pos}`) }, pos)))] })), _jsx("
|
|
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: {
|
|
872
889
|
left: `${area.x + area.width / 2}px`,
|
|
873
890
|
top: `${area.y + 8}px`
|
|
874
|
-
}, children: "\u6B63\u5728\u7F16\u8F91\u6587\u5B57 \u00B7 \u56DE\u8F66\u63D0\u4EA4 \u00B7 Esc \u53D6\u6D88" })), !capturing && !switching && (_jsx(SelectionToolbar, { selectedRect: selectedRect, area: area, capabilities: selection?.capabilities ?? null, isScrolling: geometry.isScrolling, onAction: onToolbarAction, host: { askAi: !!onAskAi } })), stylePanelOpen &&
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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 &&
|
|
892
|
+
!switching &&
|
|
893
|
+
selectedRect &&
|
|
894
|
+
self &&
|
|
895
|
+
createPortal(_jsx("div", { className: `${CANVAS_UI_SCOPE} fixed z-[1000]`, children: _jsx(StylePanel, { anchor: {
|
|
896
|
+
x: Math.min(selectedRect.x + selectedRect.width + 12, area.x + area.width - 332),
|
|
897
|
+
y: Math.max(area.y + 8, selectedRect.y)
|
|
898
|
+
}, tag: self.tag, capabilities: selection?.capabilities ?? null, declaredStyles: selection?.declaredStyles, onReset: property => {
|
|
899
|
+
void uiWrite(async () => {
|
|
900
|
+
for (const record of changeList
|
|
901
|
+
.peekList()
|
|
902
|
+
.filter(r => r.change.type === 'elementStyle' &&
|
|
903
|
+
r.change.selector === self.selector &&
|
|
904
|
+
r.change.property === property)
|
|
905
|
+
.reverse())
|
|
906
|
+
await writes.revert(record.changeId);
|
|
907
|
+
});
|
|
908
|
+
}, onClose: () => setStylePanelOpen(false), onApply: (property, value) => uiWrite(() => editStyle(depsOf(self), property, value)) }, self.selector) }), document.body), _jsx(Dialog, { open: pasteDialog, onOpenChange: setPasteDialog, children: _jsxs(DialogContent, { className: "max-w-[480px]", "data-testid": "paste-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.clipboard.pastePosition') }) }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: (selection?.capabilities ? pasteModes(selection.capabilities) : []).map(mode => (_jsxs(Button, { variant: "secondary", type: "button", "data-testid": `paste-mode-${mode}`, onClick: () => {
|
|
878
909
|
if (!self)
|
|
879
910
|
return;
|
|
880
911
|
setPasteDialog(false);
|
|
881
912
|
void commands
|
|
882
913
|
.pasteInto(self.selector, mode)
|
|
883
914
|
.catch(e => setCommandError(describeCanvasError(e)));
|
|
884
|
-
}, children: t(`inlineCanvas.clipboard.${mode}`) }, mode))) })] }) }), _jsx(ImageEditDialog, { open: imageDialog.open, capabilities: selection?.capabilities ?? null, seed: imageDialog.seed, uploadImage: uploadImage, onOpenChange: open => setImageDialog(prev => ({ ...prev, open })), onApply: (input) => self && uiWrite(() => replaceImage(depsOf(self), input)) }), _jsx(ReplaceVideoDialog, { open: videoDialog.open, seed: videoDialog.seed, onOpenChange: open => setVideoDialog(prev => ({ ...prev, open })), onApply: url => self && uiWrite(() => replaceVideo(depsOf(self), url)) }), _jsx(InsertElementDialog, { open: insertDialog, capabilities: selection?.capabilities ?? null, onOpenChange: setInsertDialog, onApply: input => self && uiWrite(() => insertElement(depsOf(self), input)) }), _jsx(ImageSettingDialog, { open: imageSettingDialog.open, seed: imageSettingDialog.seed, onOpenChange: open => setImageSettingDialog(prev => ({ ...prev, open })), onApply: alt => self && uiWrite(() => setImageAlt(depsOf(self), alt)) }), _jsx(ClickActionDialog, { open: actionDialog.open, seed: actionDialog.seed, onOpenChange: open => setActionDialog(prev => ({ ...prev, open })), onApply: action => self && uiWrite(() => setClickAction(depsOf(self), action)) }), _jsx(HtmlEditDialog, { open: htmlDialog.open, seed: htmlDialog.seed, onOpenChange: open => setHtmlDialog(prev => ({ ...prev, open })), onApply: html => self && uiWrite(() => editHtml(depsOf(self), html)) })] }));
|
|
915
|
+
}, children: [t('inlineCanvas.clipboard.paste', '粘贴'), " \u00B7", ' ', t(`inlineCanvas.clipboard.${mode}`)] }, mode))) })] }) }), _jsx(ImageEditDialog, { open: imageDialog.open, capabilities: selection?.capabilities ?? null, seed: imageDialog.seed, uploadImage: uploadImage, onOpenChange: open => setImageDialog(prev => ({ ...prev, open })), onApply: (input) => self && uiWrite(() => replaceImage(depsOf(self), input)) }), _jsx(ReplaceVideoDialog, { open: videoDialog.open, seed: videoDialog.seed, onOpenChange: open => setVideoDialog(prev => ({ ...prev, open })), onApply: url => self && uiWrite(() => replaceVideo(depsOf(self), url)) }), _jsx(InsertElementDialog, { open: insertDialog, capabilities: selection?.capabilities ?? null, onOpenChange: setInsertDialog, onApply: input => self && uiWrite(() => insertElement(depsOf(self), input)) }), _jsx(ImageSettingDialog, { open: imageSettingDialog.open, seed: imageSettingDialog.seed, onOpenChange: open => setImageSettingDialog(prev => ({ ...prev, open })), onApply: alt => self && uiWrite(() => setImageAlt(depsOf(self), alt)) }), _jsx(ClickActionDialog, { open: actionDialog.open, seed: actionDialog.seed, onOpenChange: open => setActionDialog(prev => ({ ...prev, open })), onApply: action => self && uiWrite(() => setClickAction(depsOf(self), action)) }), _jsx(HtmlEditDialog, { open: htmlDialog.open, seed: htmlDialog.seed, onOpenChange: open => setHtmlDialog(prev => ({ ...prev, open })), onApply: html => self && uiWrite(() => editHtml(depsOf(self), html)) })] }));
|
|
885
916
|
}
|
|
@@ -26,6 +26,7 @@ export interface TreePanelNode extends TreeNode {
|
|
|
26
26
|
truncated?: boolean;
|
|
27
27
|
}
|
|
28
28
|
export interface TreePanelProps {
|
|
29
|
+
emptyLabel?: string;
|
|
29
30
|
nodes: Record<string, TreePanelNode>;
|
|
30
31
|
rootId: string;
|
|
31
32
|
selectedId: string | null;
|
|
@@ -44,4 +45,4 @@ export interface TreePanelProps {
|
|
|
44
45
|
*/
|
|
45
46
|
onExpand?: (id: string) => void;
|
|
46
47
|
}
|
|
47
|
-
export declare function TreePanel({ nodes, rootId, selectedId, hoverId, query, onSelect, onShiftSelect, onHover, onMove, onExpand }: TreePanelProps): React.JSX.Element;
|
|
48
|
+
export declare function TreePanel({ emptyLabel, nodes, rootId, selectedId, hoverId, query, onSelect, onShiftSelect, onHover, onMove, onExpand }: TreePanelProps): React.JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
|
4
4
|
import { TREE_CLASS, TREE_ROW_HEIGHT } from './classNames.js';
|
|
5
5
|
import { dropPosition, rangeBetween, visibleOrder } from './order.js';
|
|
6
6
|
const DRAG_MIME = 'text/x-tree-id';
|
|
7
|
-
export function TreePanel({ nodes, rootId, selectedId, hoverId, query, onSelect, onShiftSelect, onHover, onMove, onExpand }) {
|
|
7
|
+
export function TreePanel({ emptyLabel = 'No matching layers', nodes, rootId, selectedId, hoverId, query, onSelect, onShiftSelect, onHover, onMove, onExpand }) {
|
|
8
8
|
const [collapsed, setCollapsed] = useState(new Set());
|
|
9
9
|
const [drop, setDrop] = useState(null);
|
|
10
10
|
const searching = !!query?.trim();
|
|
@@ -88,45 +88,85 @@ export function TreePanel({ nodes, rootId, selectedId, hoverId, query, onSelect,
|
|
|
88
88
|
next.add(id);
|
|
89
89
|
return next;
|
|
90
90
|
});
|
|
91
|
-
return (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const dragId = e.dataTransfer.getData(DRAG_MIME);
|
|
119
|
-
setDrop(null);
|
|
120
|
-
if (!dragId || dragId === id || !onMove)
|
|
121
|
-
return;
|
|
122
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
123
|
-
onMove(dragId, id, dropPosition(e.clientY - rect.top, TREE_ROW_HEIGHT, node.canContain));
|
|
124
|
-
}, children: [hasChildren ? (_jsx("button", { type: "button", "data-testid": `tree-toggle-${id}`, className: TREE_CLASS.toggle, onClick: e => {
|
|
125
|
-
e.stopPropagation();
|
|
126
|
-
if (needsLoad)
|
|
91
|
+
return (_jsxs("div", { ref: scrollRef, role: "tree", "aria-label": "\u56FE\u5C42", className: TREE_CLASS.tree, "data-testid": "tree-panel", onMouseLeave: () => onHover(null), children: [rows.length === 0 && (_jsx("p", { role: "status", className: "p-3 text-xs text-muted-foreground", children: emptyLabel })), rows.map(id => {
|
|
92
|
+
const node = nodes[id];
|
|
93
|
+
if (!node)
|
|
94
|
+
return null;
|
|
95
|
+
const depth = depthOf.get(id) ?? 0;
|
|
96
|
+
const needsLoad = node.children.length === 0 && !!node.truncated;
|
|
97
|
+
const hasChildren = node.children.length > 0 || (needsLoad && !!onExpand);
|
|
98
|
+
const dropHere = drop?.targetId === id ? drop.pos : null;
|
|
99
|
+
return (_jsxs("div", { role: "treeitem", tabIndex: id === selectedId ||
|
|
100
|
+
(!rows.includes(selectedId ?? '') && id === rows[0])
|
|
101
|
+
? 0
|
|
102
|
+
: -1, "aria-selected": id === selectedId, "aria-level": depth + 1, "aria-expanded": hasChildren ? !collapsed.has(id) : undefined, onKeyDown: e => {
|
|
103
|
+
if (e.target !== e.currentTarget)
|
|
104
|
+
return;
|
|
105
|
+
let next;
|
|
106
|
+
if (e.key === 'ArrowDown')
|
|
107
|
+
next = rows[rows.indexOf(id) + 1];
|
|
108
|
+
else if (e.key === 'ArrowUp')
|
|
109
|
+
next = rows[rows.indexOf(id) - 1];
|
|
110
|
+
else if (e.key === 'Home')
|
|
111
|
+
next = rows[0];
|
|
112
|
+
else if (e.key === 'End')
|
|
113
|
+
next = rows[rows.length - 1];
|
|
114
|
+
else if (e.key === 'ArrowRight') {
|
|
115
|
+
if (hasChildren && collapsed.has(id))
|
|
116
|
+
toggle(id);
|
|
117
|
+
else if (needsLoad)
|
|
127
118
|
onExpand?.(id);
|
|
128
119
|
else
|
|
120
|
+
next = node.children[0];
|
|
121
|
+
}
|
|
122
|
+
else if (e.key === 'ArrowLeft') {
|
|
123
|
+
if (hasChildren && !collapsed.has(id))
|
|
129
124
|
toggle(id);
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
else
|
|
126
|
+
next = parentOf.get(id);
|
|
127
|
+
}
|
|
128
|
+
else if (e.key === 'Enter' || e.key === ' ')
|
|
129
|
+
onSelect(id);
|
|
130
|
+
else
|
|
131
|
+
return;
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
if (next) {
|
|
134
|
+
onSelect(next);
|
|
135
|
+
Array.from(scrollRef.current?.querySelectorAll('[role="treeitem"]') ?? [])
|
|
136
|
+
.find(row => row.dataset.id === next)
|
|
137
|
+
?.focus();
|
|
138
|
+
}
|
|
139
|
+
}, "data-testid": "tree-row", "data-row-id": `tree-row-${id}`, "data-id": id, "data-depth": depth, className: `${TREE_CLASS.row}${selectedId === id ? ' is-selected' : ''}${hoverId === id ? ' is-hovered' : ''}${dropHere ? ` drop-${dropHere}` : ''}`, style: { paddingLeft: `${8 + depth * 12}px` }, draggable: onMove ? true : undefined, onMouseEnter: () => onHover(id), onClick: e => {
|
|
140
|
+
if (e.shiftKey && selectedId && onShiftSelect) {
|
|
141
|
+
onShiftSelect(rangeBetween(order, selectedId, id));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
onSelect(id);
|
|
145
|
+
}, onDragStart: e => e.dataTransfer.setData(DRAG_MIME, id), onDragOver: e => {
|
|
146
|
+
if (!onMove)
|
|
147
|
+
return;
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
150
|
+
const pos = dropPosition(e.clientY - rect.top, TREE_ROW_HEIGHT, node.canContain);
|
|
151
|
+
if (drop?.targetId !== id || drop.pos !== pos) {
|
|
152
|
+
setDrop({ targetId: id, pos });
|
|
153
|
+
}
|
|
154
|
+
}, onDragLeave: () => {
|
|
155
|
+
if (drop?.targetId === id)
|
|
156
|
+
setDrop(null);
|
|
157
|
+
}, onDrop: e => {
|
|
158
|
+
const dragId = e.dataTransfer.getData(DRAG_MIME);
|
|
159
|
+
setDrop(null);
|
|
160
|
+
if (!dragId || dragId === id || !onMove)
|
|
161
|
+
return;
|
|
162
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
163
|
+
onMove(dragId, id, dropPosition(e.clientY - rect.top, TREE_ROW_HEIGHT, node.canContain));
|
|
164
|
+
}, children: [hasChildren ? (_jsx("button", { type: "button", tabIndex: -1, "aria-label": node.label, "aria-expanded": !collapsed.has(id), "data-testid": `tree-toggle-${id}`, className: TREE_CLASS.toggle, onClick: e => {
|
|
165
|
+
e.stopPropagation();
|
|
166
|
+
if (needsLoad)
|
|
167
|
+
onExpand?.(id);
|
|
168
|
+
else
|
|
169
|
+
toggle(id);
|
|
170
|
+
}, children: collapsed.has(id) && !searching ? (_jsx(ChevronRight, { className: "size-3" })) : (_jsx(ChevronDown, { className: "size-3" })) })) : (_jsx("span", { className: "layer-chevron-spacer" })), node.icon && _jsx("span", { className: TREE_CLASS.icon, children: node.icon }), _jsx("span", { className: TREE_CLASS.label, title: node.label, children: node.label })] }, id));
|
|
171
|
+
})] }));
|
|
132
172
|
}
|
|
@@ -50,6 +50,8 @@ export interface EditorChromeProps {
|
|
|
50
50
|
onAddVersion?: () => void;
|
|
51
51
|
/** 版本改名。**不传则下拉行里没有那支铅笔** —— 同上。 */
|
|
52
52
|
onRenameVersion?: (id: string, name: string) => void;
|
|
53
|
+
removableVersionIds?: string[];
|
|
54
|
+
onRemoveVersion?: (id: string) => void;
|
|
53
55
|
changeCount: number;
|
|
54
56
|
onExit: () => void;
|
|
55
57
|
onSave?: () => void;
|