@ptengine/inline-editor 0.1.5 → 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 +43 -0
- package/dist/InlineEditor.d.ts +15 -1
- package/dist/InlineEditor.js +71 -41
- 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/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/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { frameRectToProductRect } from '../selection/geometry.js';
|
|
3
|
-
const Z_INDEX =
|
|
3
|
+
const Z_INDEX = 890;
|
|
4
4
|
const CHIP_HEIGHT = 18;
|
|
5
5
|
const COLORS = { base: '#1D4ED8', active: '#DB2777', none: '#94A3B8' };
|
|
6
6
|
function boxStyle(rect, color, active, none) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomTreeNode, ElementsInBandResult, GetDomTreeParams, ResolveHeatmapSelectorResult } from '@ptengine/inline-core/protocol';
|
|
2
|
+
import { type RowPosition } from '../selection/rowLayout.js';
|
|
2
3
|
/**
|
|
3
4
|
* 分析画布:把热图的**区块配置**(MySQL 里那份,apps/x 读来)画到客户活页面上,
|
|
4
5
|
* 并给每块找一个锚点元素 + 一份 AI 能读的摘要(标题 / 文案 / CTA)。
|
|
@@ -24,6 +25,11 @@ import type { DomTreeNode, ElementsInBandResult, GetDomTreeParams, ResolveHeatma
|
|
|
24
25
|
* 3. **none** —— 都没有:members 空。区块框照画(位置本来就来自配置,一样准),
|
|
25
26
|
* 只是点不进任何元素、也没有 selector 可以交给 agent。
|
|
26
27
|
*
|
|
28
|
+
* 三条路定下成员之后,**成员上提到块根**(`liftToBlockRoots`):轮播的一张、并列的一列、区块里的一个小节点,
|
|
29
|
+
* 提到「只装着这一块的最外层容器」—— 与编辑器整页货架打 `[Bn]` 的那一行是同一个元素(同一份判定、同一份
|
|
30
|
+
* 取树口径)。真机 vivaia.jp 首页(2026-10-21):B1 的成员是轮播第 1 张,`<page-block>` 的成员行与方案锚点
|
|
31
|
+
* 照抄它,首版 after 它把「首屏下方」的新模块插进了轮播。摘要(heading / text / cta / links)按上提后的成员取。
|
|
32
|
+
*
|
|
27
33
|
* 编号 `B1..Bn` 按输入的 `yStart` 升序,与模型看到的瓦片标注一致(`annotateAndTile`)。
|
|
28
34
|
*
|
|
29
35
|
* 三条跨域命令经 `deps` 注入:编排逻辑不需要插件也不需要浏览器就能测。任何一条失败都
|
|
@@ -46,6 +52,11 @@ export interface PageModelBlockMember {
|
|
|
46
52
|
/** 该 member 子树的文案摘要(折叠空白、≤80)。拉树失败为空串。 */
|
|
47
53
|
text: string;
|
|
48
54
|
hasChildren: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 它在父级横排里的位次(整页树上判,与整页货架行的 `inRow` 同一份)。`<page-block>` 的成员行据此打
|
|
57
|
+
* `[横排 i/n]`:成员行的父级不在它自己的子树里,只能从整页树带过来。不在横排里 / 判不了时不写这个键。
|
|
58
|
+
*/
|
|
59
|
+
inRow?: RowPosition;
|
|
49
60
|
}
|
|
50
61
|
export interface PageModelBlock {
|
|
51
62
|
/** `'B1'`,`'B2'`… 按 `yStart` 升序。 */
|
|
@@ -142,6 +153,11 @@ export interface ResolveBlocksOptions {
|
|
|
142
153
|
* 三处共用这套坐标,由各自的消费者在画的时候乘比例。
|
|
143
154
|
*/
|
|
144
155
|
scale?: number;
|
|
156
|
+
/**
|
|
157
|
+
* 画布视口宽(mobile 375 / pc 1440)。成员上提的块根判据按它算「够宽」门槛,与编辑器整页货架传的是
|
|
158
|
+
* 同一个数;缺省按 375(与 `buildRootShelf` 的缺省一致)。
|
|
159
|
+
*/
|
|
160
|
+
viewportWidth?: number;
|
|
145
161
|
}
|
|
146
162
|
export declare function resolveBlocks(sessionId: string, blocks: HeatmapBlockConfig[], deps?: ResolveBlocksDeps, opts?: ResolveBlocksOptions): Promise<PageModelBlock[]>;
|
|
147
163
|
/**
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { elementsInBand as elementsInBandCommand, getDomTree as getDomTreeCommand, resolveHeatmapSelector as resolveHeatmapSelectorCommand } from '../canvasCommands.js';
|
|
2
2
|
import { linkPath } from '../linkHref.js';
|
|
3
|
+
import { ROOT_SHELF_TREE_PARAMS, liftToBlockRoots } from '../selection/buildRootShelf.js';
|
|
3
4
|
import { SHELF_TREE_MAX_DEPTH } from '../selection/elementAiContext.js';
|
|
5
|
+
import { rowPositions } from '../selection/rowLayout.js';
|
|
4
6
|
const defaultDeps = {
|
|
5
7
|
resolveHeatmapSelector: resolveHeatmapSelectorCommand,
|
|
6
8
|
elementsInBand: elementsInBandCommand,
|
|
@@ -104,7 +106,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
104
106
|
catch {
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
const
|
|
109
|
+
const located = [];
|
|
108
110
|
for (let i = 0; i < ordered.length; i++) {
|
|
109
111
|
const cfg = ordered[i];
|
|
110
112
|
const base = { id: idOf(i), name: cfg.name };
|
|
@@ -120,7 +122,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
120
122
|
hits.push({ selector: m.kernelSelector, tag: m.tag ?? '', rect: m.rect });
|
|
121
123
|
}
|
|
122
124
|
if (hits.length > 0) {
|
|
123
|
-
|
|
125
|
+
located.push({
|
|
124
126
|
...base,
|
|
125
127
|
...geometryOf(cfg),
|
|
126
128
|
members: hits.map(h => ({
|
|
@@ -132,7 +134,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
132
134
|
ctas: [],
|
|
133
135
|
text: '',
|
|
134
136
|
resolved: 'selector'
|
|
135
|
-
})
|
|
137
|
+
});
|
|
136
138
|
continue;
|
|
137
139
|
}
|
|
138
140
|
let candidate = null;
|
|
@@ -147,7 +149,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
147
149
|
candidate = null;
|
|
148
150
|
}
|
|
149
151
|
if (candidate) {
|
|
150
|
-
|
|
152
|
+
located.push({
|
|
151
153
|
...base,
|
|
152
154
|
...geometryOf(cfg),
|
|
153
155
|
members: [
|
|
@@ -162,10 +164,10 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
162
164
|
text: '',
|
|
163
165
|
resolved: 'geometry',
|
|
164
166
|
iou: candidate.iou
|
|
165
|
-
})
|
|
167
|
+
});
|
|
166
168
|
continue;
|
|
167
169
|
}
|
|
168
|
-
|
|
170
|
+
located.push({
|
|
169
171
|
...base,
|
|
170
172
|
...geometryOf(cfg),
|
|
171
173
|
members: [],
|
|
@@ -174,8 +176,39 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
174
176
|
resolved: 'none'
|
|
175
177
|
});
|
|
176
178
|
}
|
|
179
|
+
const lifted = await liftMembers(sessionId, deps, located, opts.viewportWidth);
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const b of lifted)
|
|
182
|
+
out.push(b.members.length ? await enrich(sessionId, deps, b) : b);
|
|
177
183
|
return out;
|
|
178
184
|
}
|
|
185
|
+
async function liftMembers(sessionId, deps, blocks, viewportWidth) {
|
|
186
|
+
if (!blocks.some(b => b.members.length))
|
|
187
|
+
return blocks;
|
|
188
|
+
try {
|
|
189
|
+
const tree = await deps.getDomTree(sessionId, ROOT_SHELF_TREE_PARAMS);
|
|
190
|
+
const liftOf = liftToBlockRoots(tree, { viewportWidth, blocks });
|
|
191
|
+
const inRowOf = rowPositions(tree);
|
|
192
|
+
return blocks.map(b => {
|
|
193
|
+
if (!b.members.length)
|
|
194
|
+
return b;
|
|
195
|
+
const byMember = liftOf.get(b.id);
|
|
196
|
+
const members = [];
|
|
197
|
+
for (const m of b.members) {
|
|
198
|
+
const root = byMember?.get(m.selector);
|
|
199
|
+
const next = root ? { ...m, selector: root.selector, tag: root.tag } : m;
|
|
200
|
+
if (members.some(x => x.selector === next.selector))
|
|
201
|
+
continue;
|
|
202
|
+
const inRow = inRowOf.get(next.selector);
|
|
203
|
+
members.push(inRow ? { ...next, inRow } : next);
|
|
204
|
+
}
|
|
205
|
+
return { ...b, members };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return blocks;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
179
212
|
export function isAutoGeneratedBlockName(name) {
|
|
180
213
|
const n = name.trim();
|
|
181
214
|
if (!n)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import { toWireFormat } from '@ptengine/inline-core/change-format';
|
|
4
|
-
import { Code2, Crosshair, EyeOff, Image, Link2, Move, Plus, Trash2, Type, Undo2,
|
|
4
|
+
import { Code2, Crosshair, EyeOff, Image, Link2, Move, Plus, Trash2, Type, Undo2, Paintbrush } from 'lucide-react';
|
|
5
5
|
import { Button } from '@ptengine/design-components';
|
|
6
6
|
import { summarizeChange } from './changeSummary.js';
|
|
7
7
|
import { classifySuppression } from './mergeForSave.js';
|
|
8
8
|
const ICONS = {
|
|
9
|
-
style:
|
|
9
|
+
style: Paintbrush,
|
|
10
10
|
delete: Trash2,
|
|
11
11
|
text: Type,
|
|
12
12
|
code: Code2,
|
|
@@ -16,19 +16,21 @@ const ICONS = {
|
|
|
16
16
|
insert: Plus
|
|
17
17
|
};
|
|
18
18
|
export function ChangeListPanel({ list, onRevert, lastError, onHover, onSelect, onRevertAll }) {
|
|
19
|
+
const [confirmRevert, setConfirmRevert] = useState(false);
|
|
19
20
|
const [view, setView] = useState('summary');
|
|
20
21
|
const entries = useMemo(() => list.map(r => ({ change: r.change, ancestors: r.ancestors })), [list]);
|
|
21
22
|
const json = useMemo(() => JSON.stringify(list.map(r => toWireFormat(r.change)), null, 2), [list]);
|
|
22
|
-
return (_jsxs("section", { "data-testid": "change-list-panel", className: "flex min-h-0 flex-1 flex-col", onMouseLeave: () => onHover?.(''), children: [_jsxs("
|
|
23
|
+
return (_jsxs("section", { "data-testid": "change-list-panel", className: "flex min-h-0 flex-1 flex-col", onMouseLeave: () => onHover?.(''), children: [confirmRevert && (_jsxs("div", { role: "alert", className: "border-b p-3 text-xs", children: ["\u786E\u5B9A\u64A4\u9500\u6B64\u7248\u672C\u7684\u5168\u90E8\u6539\u52A8\uFF1F", _jsxs("div", { className: "mt-2 flex justify-end gap-2", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setConfirmRevert(false), children: "\u53D6\u6D88" }), _jsx(Button, { size: "sm", variant: "destructive", onClick: () => {
|
|
24
|
+
setConfirmRevert(false);
|
|
25
|
+
onRevertAll?.();
|
|
26
|
+
}, children: "\u64A4\u9500\u5168\u90E8\u6539\u52A8" })] })] })), _jsxs("header", { className: "flex h-10 shrink-0 items-center justify-between gap-2 border-b\n border-[#f1f2f3] pl-3 pr-2", children: [_jsxs("span", { className: "truncate text-[13px] font-medium text-[#1a1a1a]", children: ["\u6539\u52A8\u6E05\u5355", _jsx("span", { "data-testid": "change-list-count", className: "ml-1.5 text-xs font-normal tabular-nums text-[#8a8f98]", children: list.length })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [_jsx("div", { className: "flex items-center gap-px rounded-md bg-[#f3f4f4] p-0.5", children: [
|
|
23
27
|
['summary', '摘要', undefined],
|
|
24
28
|
[
|
|
25
29
|
'wire',
|
|
26
|
-
'
|
|
27
|
-
'
|
|
30
|
+
'高级数据',
|
|
31
|
+
'查看此版本的高级修改数据'
|
|
28
32
|
]
|
|
29
|
-
].map(([v, label, hint]) => (_jsx(
|
|
30
|
-
? 'bg-white font-medium text-[rgba(16,28,31,0.9)] shadow-[0_1px_2px_rgba(16,28,31,0.08)]'
|
|
31
|
-
: 'text-[#707779] hover:text-[rgba(16,28,31,0.7)]'}`, children: label }, v))) }), onRevertAll && list.length > 0 && (_jsx("button", { type: "button", "data-testid": "change-list-revert-all", onClick: onRevertAll, className: "rounded px-2 py-1 text-xs text-[#52525b] transition-colors\n hover:bg-[rgba(16,28,31,0.06)] hover:text-[rgba(16,28,31,0.9)]", children: "\u5168\u90E8\u64A4\u9500" }))] })] }), lastError && (_jsx("p", { "data-testid": "change-list-error", className: "px-3 py-2 text-xs text-[#DE350B]", children: lastError })), list.length === 0 ? (_jsx("p", { "data-testid": "change-list-empty", className: "px-3 py-8 text-center text-xs text-[#8a8f98]", children: "\u8FD8\u6CA1\u6709\u6539\u52A8\u3002\u5728\u753B\u5E03\u91CC\u9009\u4E2D\u5143\u7D20\u5F00\u59CB\u7F16\u8F91\u3002" })) : view === 'summary' ? (_jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto", children: list.map((record, index) => (_jsx(ChangeRow, { record: record, summary: summarizeChange(record.change), suppression: classifySuppression(entries, index), onRevert: onRevert, onHover: onHover, onSelect: onSelect }, record.changeId))) })) : (_jsx("pre", { "data-testid": "change-list-json", className: "min-h-0 flex-1 overflow-auto bg-[#FAFAFA] px-3 py-2 text-[11px]\n leading-4 text-[#344563]", children: json }))] }));
|
|
33
|
+
].map(([v, label, hint]) => (_jsx(Button, { type: "button", "data-testid": `change-list-view-${v}`, "aria-pressed": view === v, title: hint, onClick: () => setView(v), size: "sm", variant: view === v ? 'secondary' : 'ghost', children: label }, v))) }), onRevertAll && list.length > 0 && (_jsx(Button, { type: "button", "data-testid": "change-list-revert-all", onClick: () => setConfirmRevert(true), size: "sm", variant: "ghost", children: "\u5168\u90E8\u64A4\u9500" }))] })] }), lastError && (_jsx("p", { "data-testid": "change-list-error", className: "px-3 py-2 text-xs text-[#DE350B]", children: lastError })), list.length === 0 ? (_jsx("p", { "data-testid": "change-list-empty", className: "px-3 py-8 text-center text-xs text-[#8a8f98]", children: "\u8FD8\u6CA1\u6709\u6539\u52A8\u3002\u5728\u753B\u5E03\u91CC\u9009\u4E2D\u5143\u7D20\u5F00\u59CB\u7F16\u8F91\u3002" })) : view === 'summary' ? (_jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto", children: list.map((record, index) => (_jsx(ChangeRow, { record: record, summary: summarizeChange(record.change), suppression: classifySuppression(entries, index), onRevert: onRevert, onHover: onHover, onSelect: onSelect }, record.changeId))) })) : (_jsx("pre", { "data-testid": "change-list-json", className: "min-h-0 flex-1 overflow-auto bg-[#FAFAFA] px-3 py-2 text-[11px]\n leading-4 text-[#344563]", children: json }))] }));
|
|
32
34
|
}
|
|
33
35
|
const SUPPRESSION_TEXT = {
|
|
34
36
|
'ancestor-hidden': {
|
|
@@ -43,5 +45,5 @@ const SUPPRESSION_TEXT = {
|
|
|
43
45
|
function ChangeRow({ record, summary, suppression, onRevert, onHover, onSelect }) {
|
|
44
46
|
const Icon = ICONS[summary.icon];
|
|
45
47
|
const text = suppression ? SUPPRESSION_TEXT[suppression.reason] : null;
|
|
46
|
-
return (_jsxs("li", { "data-testid": "change-list-item", "data-suppressed": suppression ? suppression.reason : undefined, className: "group flex items-center gap-2.5 border-b border-[#f1f2f3] px-3 py-2.5\n last:border-b-0 hover:bg-[rgba(16,28,31,0.04)]", onMouseEnter: () => onHover?.(summary.selector), children: [_jsx(Icon, { className: `size-4 shrink-0 ${suppression ? 'text-[#B3BAC5]' : 'text-[#6B778C]'}` }), _jsxs(
|
|
48
|
+
return (_jsxs("li", { "data-testid": "change-list-item", "data-suppressed": suppression ? suppression.reason : undefined, className: "group flex items-center gap-2.5 border-b border-[#f1f2f3] px-3 py-2.5\n last:border-b-0 hover:bg-[rgba(16,28,31,0.04)]", onMouseEnter: () => onHover?.(summary.selector), children: [_jsx(Icon, { className: `size-4 shrink-0 ${suppression ? 'text-[#B3BAC5]' : 'text-[#6B778C]'}` }), _jsxs(Button, { type: "button", variant: "ghost", "data-testid": "change-list-locate", onClick: () => onSelect?.(summary.selector), className: "flex h-auto min-h-8 min-w-0 flex-1 flex-col items-start justify-start gap-0.5 whitespace-normal py-2 text-left", title: summary.selector, children: [_jsxs("span", { className: `flex items-center gap-1.5 text-[13px] font-medium ${suppression ? 'text-[#8993A4]' : 'text-[#1a1a1a]'}`, children: [summary.action, onSelect && (_jsx(Crosshair, { className: "size-3 text-[#8993A4] opacity-0 group-hover:opacity-100" }))] }), text && (_jsxs("span", { "data-testid": "change-list-suppressed-badge", className: "flex items-center gap-1 rounded bg-[#FFF7E6] px-1.5 py-0.5\n text-[11px] leading-4 text-[#974F0C]", title: `${text.hint}(${suppression?.by})`, children: [_jsx(EyeOff, { className: "size-3 shrink-0" }), text.badge] })), summary.detail && (_jsx("span", { className: `w-full truncate font-mono text-[11px] ${suppression ? 'text-[#8993A4] line-through' : 'text-[#344563]'}`, children: summary.detail })), _jsx("span", { className: "w-full truncate font-mono text-[11px] text-[#8993A4]", children: summary.selector })] }), _jsx(Button, { variant: "ghost", size: "icon", "data-testid": `change-list-revert-${record.changeId}`, "aria-label": "\u64A4\u9500", onClick: () => onRevert(record.changeId), children: _jsx(Undo2, { className: "size-4" }) })] }));
|
|
47
49
|
}
|
|
@@ -82,6 +82,10 @@ export interface UseChangeListResult {
|
|
|
82
82
|
outcome: HydrateOutcome;
|
|
83
83
|
idMap: Map<string, string>;
|
|
84
84
|
}>;
|
|
85
|
+
canUndo: boolean;
|
|
86
|
+
canRedo: boolean;
|
|
87
|
+
undo: () => Promise<void>;
|
|
88
|
+
redo: () => Promise<void>;
|
|
85
89
|
lastError: string | null;
|
|
86
90
|
}
|
|
87
91
|
export interface HydrateOutcome {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { applyChange, revertChange } from '../canvasCommands.js';
|
|
3
3
|
import { overrideOutcome } from './overrides.js';
|
|
4
4
|
import { backgroundImageValue, buildInsertImageHtml, randomPtElementId } from '../../dialogs/imageEdit.js';
|
|
@@ -16,6 +16,16 @@ function withResolvedStyleMode(change, applied) {
|
|
|
16
16
|
}
|
|
17
17
|
export function useChangeList(sessionId, options = {}) {
|
|
18
18
|
const [list, setList] = useState([]);
|
|
19
|
+
const past = useRef([]);
|
|
20
|
+
const future = useRef([]);
|
|
21
|
+
const [, refreshHistory] = useState(0);
|
|
22
|
+
const resetHistory = useCallback(() => {
|
|
23
|
+
past.current = [];
|
|
24
|
+
future.current = [];
|
|
25
|
+
refreshHistory(n => n + 1);
|
|
26
|
+
}, []);
|
|
27
|
+
useEffect(resetHistory, [sessionId, resetHistory]);
|
|
28
|
+
const mutationQueue = useRef(Promise.resolve());
|
|
19
29
|
const [lastError, setLastError] = useState(null);
|
|
20
30
|
const listRef = useRef([]);
|
|
21
31
|
listRef.current = list;
|
|
@@ -50,7 +60,9 @@ export function useChangeList(sessionId, options = {}) {
|
|
|
50
60
|
const stored = withResolvedStyleMode(change, applied);
|
|
51
61
|
const ancestorsByChange = new Map(prev.map(r => [r.change, r.ancestors]));
|
|
52
62
|
const insertedByChange = new Map(prev.map(r => [r.change, r.insertedSelectors]));
|
|
53
|
-
const insertedNow = applied.insertedSelectors?.length
|
|
63
|
+
const insertedNow = applied.insertedSelectors?.length
|
|
64
|
+
? applied.insertedSelectors
|
|
65
|
+
: undefined;
|
|
54
66
|
const nextRecords = nextChanges.map(c => {
|
|
55
67
|
const inserted = c === change ? insertedNow : insertedByChange.get(c);
|
|
56
68
|
return {
|
|
@@ -99,15 +111,71 @@ export function useChangeList(sessionId, options = {}) {
|
|
|
99
111
|
reportRestored(records);
|
|
100
112
|
return { applied: records.length - failed.length, failed };
|
|
101
113
|
}, [sessionId, reportRestored]);
|
|
102
|
-
const guard = useCallback(
|
|
114
|
+
const guard = useCallback((run, record = true) => {
|
|
115
|
+
const next = mutationQueue.current.then(async () => {
|
|
116
|
+
const before = listRef.current;
|
|
117
|
+
try {
|
|
118
|
+
const result = await run();
|
|
119
|
+
if (record && before !== listRef.current) {
|
|
120
|
+
past.current.push({ before, after: listRef.current });
|
|
121
|
+
future.current = [];
|
|
122
|
+
refreshHistory(n => n + 1);
|
|
123
|
+
}
|
|
124
|
+
setLastError(null);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
setLastError(String(e?.message ?? e));
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
mutationQueue.current = next.catch(() => { });
|
|
133
|
+
return next;
|
|
134
|
+
}, []);
|
|
135
|
+
const restoreSnapshot = useCallback(async (snapshot) => {
|
|
136
|
+
if (!sessionId)
|
|
137
|
+
throw new Error('no canvas session');
|
|
138
|
+
const current = listRef.current;
|
|
139
|
+
for (const record of [...current].reverse()) {
|
|
140
|
+
await revertChange(sessionId, record.changeId).catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
reportRemoved(current);
|
|
143
|
+
listRef.current = [];
|
|
144
|
+
setList([]);
|
|
145
|
+
for (const record of snapshot) {
|
|
146
|
+
const applied = await applyChange(sessionId, record.changeId, record.change);
|
|
147
|
+
const restored = {
|
|
148
|
+
...record,
|
|
149
|
+
change: withResolvedStyleMode(record.change, applied),
|
|
150
|
+
...(applied?.insertedSelectors?.length
|
|
151
|
+
? { insertedSelectors: applied.insertedSelectors }
|
|
152
|
+
: {})
|
|
153
|
+
};
|
|
154
|
+
listRef.current = [...listRef.current, restored];
|
|
155
|
+
setList(listRef.current);
|
|
156
|
+
reportRestored([restored]);
|
|
157
|
+
}
|
|
158
|
+
}, [sessionId, reportRemoved, reportRestored]);
|
|
159
|
+
const travelHistory = useCallback((direction) => guard(async () => {
|
|
160
|
+
const source = direction === 'undo' ? past.current : future.current;
|
|
161
|
+
const target = direction === 'undo' ? future.current : past.current;
|
|
162
|
+
const entry = source.at(-1);
|
|
163
|
+
if (!entry)
|
|
164
|
+
return;
|
|
165
|
+
const current = listRef.current;
|
|
103
166
|
try {
|
|
104
|
-
|
|
167
|
+
await restoreSnapshot(direction === 'undo' ? entry.before : entry.after);
|
|
105
168
|
}
|
|
106
|
-
catch (
|
|
107
|
-
|
|
108
|
-
throw
|
|
169
|
+
catch (error) {
|
|
170
|
+
await restoreSnapshot(current);
|
|
171
|
+
throw error;
|
|
109
172
|
}
|
|
110
|
-
|
|
173
|
+
source.pop();
|
|
174
|
+
target.push(entry);
|
|
175
|
+
refreshHistory(n => n + 1);
|
|
176
|
+
}, false), [guard, restoreSnapshot]);
|
|
177
|
+
const undo = useCallback(() => travelHistory('undo'), [travelHistory]);
|
|
178
|
+
const redo = useCallback(() => travelHistory('redo'), [travelHistory]);
|
|
111
179
|
const addStyle = useCallback((input) => guard(() => push(buildStyleChange(input), input.ancestors)), [guard, push]);
|
|
112
180
|
const addContent = useCallback((input) => guard(() => push(buildContentChange(input), input.ancestors)), [guard, push]);
|
|
113
181
|
const addAttribute = useCallback((input) => guard(() => push(buildAttributeChange(input), input.ancestors)), [guard, push]);
|
|
@@ -158,14 +226,16 @@ export function useChangeList(sessionId, options = {}) {
|
|
|
158
226
|
idMap.set(old, nextRecords[i].changeId);
|
|
159
227
|
}
|
|
160
228
|
}
|
|
229
|
+
resetHistory();
|
|
161
230
|
return { previous, outcome, idMap };
|
|
162
|
-
}, [hydrate, revertAll, sessionId]);
|
|
231
|
+
}, [hydrate, revertAll, sessionId, resetHistory]);
|
|
163
232
|
const peekList = useCallback(() => listRef.current, []);
|
|
164
233
|
const clear = useCallback(() => {
|
|
234
|
+
resetHistory();
|
|
165
235
|
listRef.current = [];
|
|
166
236
|
setList([]);
|
|
167
237
|
setLastError(null);
|
|
168
|
-
}, []);
|
|
238
|
+
}, [resetHistory]);
|
|
169
239
|
return {
|
|
170
240
|
list,
|
|
171
241
|
changes: list.map(r => r.change),
|
|
@@ -180,6 +250,10 @@ export function useChangeList(sessionId, options = {}) {
|
|
|
180
250
|
hydrate,
|
|
181
251
|
replaceAll,
|
|
182
252
|
peekList,
|
|
253
|
+
canUndo: past.current.length > 0,
|
|
254
|
+
canRedo: future.current.length > 0,
|
|
255
|
+
undo,
|
|
256
|
+
redo,
|
|
183
257
|
lastError
|
|
184
258
|
};
|
|
185
259
|
}
|
|
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { Camera, ClipboardPaste, Code, Copy, ImageIcon, MousePointerClick, Move, Paintbrush, Pencil, Plus, Settings2, Sparkles, Trash2, Video } from 'lucide-react';
|
|
5
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@ptengine/design-components';
|
|
5
|
+
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@ptengine/design-components';
|
|
6
6
|
import { SELECTION_CLASS } from '../../canvas-ui/index.js';
|
|
7
7
|
import { visibleButtons } from './toolbarButtons.js';
|
|
8
8
|
const TOOLBAR_HEIGHT = 34;
|
|
@@ -23,7 +23,7 @@ const TOOLBAR_ICONS = {
|
|
|
23
23
|
delete: Trash2,
|
|
24
24
|
sparkles: Sparkles
|
|
25
25
|
};
|
|
26
|
-
const ICON_SIZE =
|
|
26
|
+
const ICON_SIZE = 16;
|
|
27
27
|
const ICON_STROKE_WIDTH = 1.75;
|
|
28
28
|
const TOOLTIP_DELAY = 200;
|
|
29
29
|
export function computeToolbarPosition(rect, area, width) {
|
|
@@ -51,6 +51,7 @@ export function computeToolbarPosition(rect, area, width) {
|
|
|
51
51
|
export function SelectionToolbar({ host = {}, selectedRect, area, capabilities, isScrolling, onAction, width = 240 }) {
|
|
52
52
|
const { t } = useTranslation();
|
|
53
53
|
const barRef = useRef(null);
|
|
54
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
54
55
|
const [measuredWidth, setMeasuredWidth] = useState(null);
|
|
55
56
|
useLayoutEffect(() => {
|
|
56
57
|
const measured = barRef.current?.offsetWidth ?? 0;
|
|
@@ -63,20 +64,37 @@ export function SelectionToolbar({ host = {}, selectedRect, area, capabilities,
|
|
|
63
64
|
if (buttons.length === 0)
|
|
64
65
|
return null;
|
|
65
66
|
const { left, top } = computeToolbarPosition(selectedRect, area, measuredWidth ?? width);
|
|
66
|
-
return (_jsx("div", { ref: barRef,
|
|
67
|
+
return (_jsx("div", { ref: barRef, role: "toolbar", "aria-label": t('inlineCanvas.toolbar.label', '元素操作'), onKeyDown: e => {
|
|
68
|
+
const items = Array.from(barRef.current?.querySelectorAll('button:not(:disabled)') ??
|
|
69
|
+
[]);
|
|
70
|
+
const current = items.indexOf(document.activeElement);
|
|
71
|
+
let next = current;
|
|
72
|
+
if (e.key === 'ArrowRight')
|
|
73
|
+
next = (current + 1) % items.length;
|
|
74
|
+
else if (e.key === 'ArrowLeft')
|
|
75
|
+
next = (current - 1 + items.length) % items.length;
|
|
76
|
+
else if (e.key === 'Home')
|
|
77
|
+
next = 0;
|
|
78
|
+
else if (e.key === 'End')
|
|
79
|
+
next = items.length - 1;
|
|
80
|
+
else
|
|
81
|
+
return;
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
setFocusIndex(next);
|
|
84
|
+
items[next]?.focus();
|
|
85
|
+
}, "data-testid": "canvas-selection-toolbar", className: SELECTION_CLASS.toolbar, style: {
|
|
67
86
|
position: 'fixed',
|
|
68
87
|
left: `${left}px`,
|
|
69
88
|
top: `${top}px`,
|
|
70
89
|
transform: 'none',
|
|
71
|
-
zIndex:
|
|
72
|
-
}, children: _jsx(TooltipProvider, { delayDuration: TOOLTIP_DELAY, children: buttons.map(button => {
|
|
90
|
+
zIndex: 901
|
|
91
|
+
}, children: _jsx(TooltipProvider, { delayDuration: TOOLTIP_DELAY, children: buttons.map((button, index) => {
|
|
73
92
|
const label = t(button.labelKey);
|
|
74
93
|
const Icon = TOOLBAR_ICONS[button.icon];
|
|
75
94
|
const isAi = button.id === 'ask-ai';
|
|
76
|
-
const className = `${SELECTION_CLASS.tool}${button.id === 'delete' ? ` ${SELECTION_CLASS.toolDanger}` : ''}${isAi ? ` ${SELECTION_CLASS.toolAi}` : ''}`;
|
|
77
95
|
if (isAi) {
|
|
78
|
-
return (_jsxs(
|
|
96
|
+
return (_jsxs(Button, { type: "button", "data-testid": `canvas-toolbar-${button.id}`, size: isAi ? 'sm' : 'icon-sm', variant: button.id === 'delete' ? 'destructive' : 'ghost', tabIndex: index === Math.min(focusIndex, buttons.length - 1) ? 0 : -1, onClick: () => onAction(button.id), children: ["\u2726 ", label] }, button.id));
|
|
79
97
|
}
|
|
80
|
-
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(
|
|
98
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", "data-testid": `canvas-toolbar-${button.id}`, "aria-label": label, size: isAi ? 'sm' : 'icon-sm', variant: button.id === 'delete' ? 'destructive' : 'ghost', tabIndex: index === Math.min(focusIndex, buttons.length - 1) ? 0 : -1, onClick: () => onAction(button.id), children: _jsx(Icon, { size: ICON_SIZE, strokeWidth: ICON_STROKE_WIDTH, "aria-hidden": true }) }) }), _jsx(TooltipContent, { children: label })] }, button.id));
|
|
81
99
|
}) }) }));
|
|
82
100
|
}
|
|
@@ -7,7 +7,18 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 这里改成按**几何**挑区块:新 agent 的树带 `rect`(文档坐标)/ `hidden` / `heading`
|
|
9
9
|
* (`getDomTree` 的 `includeRect` / `visibleOnly`),于是能先找出「够高、够宽」的可见区块,
|
|
10
|
-
* 每块给 1 行容器 +
|
|
10
|
+
* 每块给 1 行容器 + 几行可改点,header/nav/footer 各封顶,隐藏节点一律不给。
|
|
11
|
+
*
|
|
12
|
+
* **哪一层算「一个区块」**(2026-10-21 重写):`after` X 落在 X 的父级里,按父级怎么排孩子
|
|
13
|
+
* 摆放。「X 下面插入」要落在 X 正下方,X 的父级就得是**上下堆叠区块**的那一层 —— 所以块根 =
|
|
14
|
+
* 「只装着这一个区块的最外层容器」:
|
|
15
|
+
* · 父级横排(轮播的每一张、并列的列)里的大项不单独成块,连同子树归外层;
|
|
16
|
+
* · 装着 ≥2 个候选区块 = 外壳,拆开;只装着 1 个 = 它自己就是这一块(外层赢,小圆点、
|
|
17
|
+
* 进度条这些附属部件都在最外层里),但占页高 ≥60% 的单链节点是整页的框,让给内层;
|
|
18
|
+
* · 宿主给了页面模型(`blocks`)时,成员归属校正拆合,并在块根行写 `blockId`。
|
|
19
|
+
* 旧规则「内层有 heading 就赢」不可用:kit 的 `heading` 是**子树里**第一个标题(没有就取第一段
|
|
20
|
+
* 文字),section / 轮播轨道 / 每张幻灯片拿到的是同一句,规则又偏向内层 —— 真机 vivaia 首页的
|
|
21
|
+
* 首屏只剩三张幻灯片,模型照「首屏下面」插进了轮播(2026-10-20)。
|
|
11
22
|
*
|
|
12
23
|
* **兼容**:树上一个 `rect` 都没有 = 老 agent,整个函数退化成老算法,逐项一致。
|
|
13
24
|
*
|
|
@@ -15,11 +26,47 @@
|
|
|
15
26
|
* 里的项。MOVE 闸靠这张表判 target 是不是被移元素的祖先,两边一旦漂移就是「同一个
|
|
16
27
|
* selector 有时能移有时不能,且不报错」(见 buildDescendantShelf 头部说明)。
|
|
17
28
|
*/
|
|
18
|
-
import type { DomTreeNode } from '@ptengine/inline-core/protocol';
|
|
29
|
+
import type { DomTreeNode, GetDomTreeParams } from '@ptengine/inline-core/protocol';
|
|
19
30
|
import { type AiDescendant } from './elementAiContext.js';
|
|
31
|
+
/**
|
|
32
|
+
* 整页货架取树的口径(`getDomTree`,以 body 为根)。编辑器的整页货架与分析路径的成员上提(`resolveBlocks`)
|
|
33
|
+
* 用**同一份**:块根判据吃的是几何与排法,两边取的树不一样,认出来的块根就可能不一样。
|
|
34
|
+
*
|
|
35
|
+
* 树要**深、宽、带几何**:真实站点的内容区块常在 `body > #root > .page > main > div > div > section`
|
|
36
|
+
* 第 6-7 层,`maxDepth: 4` 根本到不了;`visibleOnly` 把抽屉 / 隐藏菜单整棵剔掉(它们此前把货架吃光),
|
|
37
|
+
* `includeRect` 给块根判据按高度 / 宽度挑区块用。`maxNodes` 是总量安全阀(1500 个节点的 JSON 仍在一次
|
|
38
|
+
* sendMessage 的舒适范围内)。老 agent 不认识这几个字段会静默忽略 —— 树上就没有 `rect`,退回老算法。
|
|
39
|
+
*/
|
|
40
|
+
export declare const ROOT_SHELF_TREE_PARAMS: Readonly<Omit<GetDomTreeParams, 'command' | 'sessionId'>>;
|
|
41
|
+
/**
|
|
42
|
+
* 页面模型的一个区块(热图区块配置解析出来的),只要 id 与成员容器的 selector。
|
|
43
|
+
* 与宿主的 `PageModelBlock` / `InlinePageBlock` 结构兼容,宿主原样传进来即可。
|
|
44
|
+
*/
|
|
45
|
+
export interface ShelfBlockHint {
|
|
46
|
+
id: string;
|
|
47
|
+
members: ReadonlyArray<{
|
|
48
|
+
selector: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 页面模型的成员 → 它所在的块根:整页货架上写着这个区块 `blockId` 的那一行(同一份判定)。
|
|
53
|
+
* 返回 区块 id → 成员 selector → 块根节点(输入树上的那个节点)。
|
|
54
|
+
*
|
|
55
|
+
* 分析路径(`resolveBlocks` → `<page-block>` → 方案锚点 → 首版 `anchors:`)此前用热图成员原样:真机
|
|
56
|
+
* vivaia 首页 B1 的成员是轮播第 1 张,方案锚点照抄它,首版 after 它就把「首屏下方」的新模块插进了轮播 ——
|
|
57
|
+
* 而编辑器货架上 `[B1]` 已经在整个首屏 section 上(2026-10-21)。两条路认块必须是同一个结果。
|
|
58
|
+
*
|
|
59
|
+
* 只提到「是成员自己或成员祖先、且只装着这一个区块」的块根。提不了的不进表,调用方保持原成员:
|
|
60
|
+
* 成员不在树上(截断 / 另一端版式)、所在容器还装着别的区块(两张幻灯片各是一块)、老 agent 没有几何。
|
|
61
|
+
*/
|
|
62
|
+
export declare function liftToBlockRoots(root: DomTreeNode, opts: {
|
|
63
|
+
viewportWidth?: number;
|
|
64
|
+
blocks: ReadonlyArray<ShelfBlockHint>;
|
|
65
|
+
}): Map<string, Map<string, DomTreeNode>>;
|
|
20
66
|
export declare function buildRootShelf(root: DomTreeNode, opts: {
|
|
21
67
|
limit: number;
|
|
22
68
|
viewportWidth?: number;
|
|
69
|
+
blocks?: ReadonlyArray<ShelfBlockHint>;
|
|
23
70
|
}): {
|
|
24
71
|
shelf: AiDescendant[];
|
|
25
72
|
ancestorsOf: Map<string, string[]>;
|