@ptengine/inline-editor 0.0.0-stage → 0.1.0
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 +28 -0
- package/README.md +286 -2
- package/dist/InlineEditor.d.ts +342 -0
- package/dist/InlineEditor.js +867 -0
- package/dist/PagePreview.d.ts +83 -0
- package/dist/PagePreview.js +111 -0
- package/dist/assets/AssetsPanel.d.ts +19 -0
- package/dist/assets/AssetsPanel.js +23 -0
- package/dist/assets/componentLibrary.d.ts +23 -0
- package/dist/assets/componentLibrary.js +12 -0
- package/dist/canvas-ui/index.d.ts +17 -0
- package/dist/canvas-ui/index.js +7 -0
- package/dist/canvas-ui/scope.d.ts +13 -0
- package/dist/canvas-ui/scope.js +1 -0
- package/dist/canvas-ui/selection/classNames.d.ts +30 -0
- package/dist/canvas-ui/selection/classNames.js +15 -0
- package/dist/canvas-ui/selection/tokens.css +204 -0
- package/dist/canvas-ui/tree/TreePanel.d.ts +47 -0
- package/dist/canvas-ui/tree/TreePanel.js +132 -0
- package/dist/canvas-ui/tree/classNames.d.ts +21 -0
- package/dist/canvas-ui/tree/classNames.js +8 -0
- package/dist/canvas-ui/tree/order.d.ts +38 -0
- package/dist/canvas-ui/tree/order.js +38 -0
- package/dist/canvas-ui/tree/tree.css +131 -0
- package/dist/chrome/EditorChrome.d.ts +83 -0
- package/dist/chrome/EditorChrome.js +10 -0
- package/dist/chrome/VersionMenu.d.ts +26 -0
- package/dist/chrome/VersionMenu.js +86 -0
- package/dist/chrome/topbarPrimitives.d.ts +69 -0
- package/dist/chrome/topbarPrimitives.js +39 -0
- package/dist/chrome/useDismissOnOutside.d.ts +23 -0
- package/dist/chrome/useDismissOnOutside.js +27 -0
- package/dist/dialogs/ClickActionDialog.d.ts +24 -0
- package/dist/dialogs/ClickActionDialog.js +35 -0
- package/dist/dialogs/HtmlEditDialog.d.ts +23 -0
- package/dist/dialogs/HtmlEditDialog.js +23 -0
- package/dist/dialogs/ImageEditDialog.d.ts +30 -0
- package/dist/dialogs/ImageEditDialog.js +57 -0
- package/dist/dialogs/ImageSettingDialog.d.ts +16 -0
- package/dist/dialogs/ImageSettingDialog.js +17 -0
- package/dist/dialogs/InsertElementDialog.d.ts +23 -0
- package/dist/dialogs/InsertElementDialog.js +28 -0
- package/dist/dialogs/ReplaceVideoDialog.d.ts +18 -0
- package/dist/dialogs/ReplaceVideoDialog.js +16 -0
- package/dist/dialogs/actionTypes.d.ts +53 -0
- package/dist/dialogs/actionTypes.js +74 -0
- package/dist/dialogs/htmlValidation.d.ts +24 -0
- package/dist/dialogs/htmlValidation.js +37 -0
- package/dist/dialogs/imageEdit.d.ts +49 -0
- package/dist/dialogs/imageEdit.js +30 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +53 -0
- package/dist/kernel/CanvasContextBar.d.ts +23 -0
- package/dist/kernel/CanvasContextBar.js +12 -0
- package/dist/kernel/CanvasFrame.d.ts +56 -0
- package/dist/kernel/CanvasFrame.js +53 -0
- package/dist/kernel/ai/aiInlineApply.d.ts +326 -0
- package/dist/kernel/ai/aiInlineApply.js +461 -0
- package/dist/kernel/ai/imageSource.d.ts +24 -0
- package/dist/kernel/ai/imageSource.js +19 -0
- package/dist/kernel/ai/insertHtmlGate.d.ts +71 -0
- package/dist/kernel/ai/insertHtmlGate.js +123 -0
- package/dist/kernel/ai/postApplyCheck.d.ts +56 -0
- package/dist/kernel/ai/postApplyCheck.js +74 -0
- package/dist/kernel/ai/replaceHtmlGate.d.ts +71 -0
- package/dist/kernel/ai/replaceHtmlGate.js +147 -0
- package/dist/kernel/blocks/BlockOverlay.d.ts +16 -0
- package/dist/kernel/blocks/BlockOverlay.js +66 -0
- package/dist/kernel/blocks/annotateAndTile.d.ts +106 -0
- package/dist/kernel/blocks/annotateAndTile.js +122 -0
- package/dist/kernel/blocks/resolveBlocks.d.ts +153 -0
- package/dist/kernel/blocks/resolveBlocks.js +233 -0
- package/dist/kernel/canvasCapabilities.d.ts +31 -0
- package/dist/kernel/canvasCapabilities.js +39 -0
- package/dist/kernel/canvasCommands.d.ts +126 -0
- package/dist/kernel/canvasCommands.js +146 -0
- package/dist/kernel/canvasErrors.d.ts +1 -0
- package/dist/kernel/canvasErrors.js +14 -0
- package/dist/kernel/captureGeometry.d.ts +35 -0
- package/dist/kernel/captureGeometry.js +25 -0
- package/dist/kernel/captureOrchestration.d.ts +48 -0
- package/dist/kernel/captureOrchestration.js +43 -0
- package/dist/kernel/captureSlices.d.ts +36 -0
- package/dist/kernel/captureSlices.js +20 -0
- package/dist/kernel/changelist/ChangeListPanel.d.ts +13 -0
- package/dist/kernel/changelist/ChangeListPanel.js +47 -0
- package/dist/kernel/changelist/ChangeListPopover.d.ts +20 -0
- package/dist/kernel/changelist/ChangeListPopover.js +10 -0
- package/dist/kernel/changelist/builders.d.ts +89 -0
- package/dist/kernel/changelist/builders.js +81 -0
- package/dist/kernel/changelist/changeSummary.d.ts +19 -0
- package/dist/kernel/changelist/changeSummary.js +85 -0
- package/dist/kernel/changelist/clipboard.d.ts +16 -0
- package/dist/kernel/changelist/clipboard.js +4 -0
- package/dist/kernel/changelist/exportForSave.d.ts +30 -0
- package/dist/kernel/changelist/exportForSave.js +11 -0
- package/dist/kernel/changelist/mergeForSave.d.ts +79 -0
- package/dist/kernel/changelist/mergeForSave.js +40 -0
- package/dist/kernel/changelist/moveObserverSafety.d.ts +70 -0
- package/dist/kernel/changelist/moveObserverSafety.js +39 -0
- package/dist/kernel/changelist/overrides.d.ts +22 -0
- package/dist/kernel/changelist/overrides.js +63 -0
- package/dist/kernel/changelist/useChangeList.d.ts +185 -0
- package/dist/kernel/changelist/useChangeList.js +259 -0
- package/dist/kernel/config.d.ts +29 -0
- package/dist/kernel/config.js +10 -0
- package/dist/kernel/dataUrlToBlob.d.ts +8 -0
- package/dist/kernel/dataUrlToBlob.js +17 -0
- package/dist/kernel/linkHref.d.ts +25 -0
- package/dist/kernel/linkHref.js +16 -0
- package/dist/kernel/mobileFit.d.ts +46 -0
- package/dist/kernel/mobileFit.js +44 -0
- package/dist/kernel/moveState.d.ts +35 -0
- package/dist/kernel/moveState.js +14 -0
- package/dist/kernel/selection/SelectionOverlay.d.ts +10 -0
- package/dist/kernel/selection/SelectionOverlay.js +25 -0
- package/dist/kernel/selection/SelectionToolbar.d.ts +30 -0
- package/dist/kernel/selection/SelectionToolbar.js +82 -0
- package/dist/kernel/selection/SelectorPathBar.d.ts +27 -0
- package/dist/kernel/selection/SelectorPathBar.js +55 -0
- package/dist/kernel/selection/buildDescendantShelf.d.ts +40 -0
- package/dist/kernel/selection/buildDescendantShelf.js +50 -0
- package/dist/kernel/selection/buildRootShelf.d.ts +26 -0
- package/dist/kernel/selection/buildRootShelf.js +149 -0
- package/dist/kernel/selection/elementAiContext.d.ts +313 -0
- package/dist/kernel/selection/elementAiContext.js +122 -0
- package/dist/kernel/selection/elementGeometryForAi.d.ts +9 -0
- package/dist/kernel/selection/elementGeometryForAi.js +15 -0
- package/dist/kernel/selection/geometry.d.ts +55 -0
- package/dist/kernel/selection/geometry.js +18 -0
- package/dist/kernel/selection/resolveAnchors.d.ts +116 -0
- package/dist/kernel/selection/resolveAnchors.js +173 -0
- package/dist/kernel/selection/shelfRow.d.ts +33 -0
- package/dist/kernel/selection/shelfRow.js +19 -0
- package/dist/kernel/selection/tagLabels.d.ts +9 -0
- package/dist/kernel/selection/tagLabels.js +46 -0
- package/dist/kernel/selection/toolbarButtons.d.ts +60 -0
- package/dist/kernel/selection/toolbarButtons.js +105 -0
- package/dist/kernel/selection/useAiSelectionContext.d.ts +26 -0
- package/dist/kernel/selection/useAiSelectionContext.js +26 -0
- package/dist/kernel/selection/useCanvasGeometry.d.ts +32 -0
- package/dist/kernel/selection/useCanvasGeometry.js +47 -0
- package/dist/kernel/selection/useSelectionRestore.d.ts +18 -0
- package/dist/kernel/selection/useSelectionRestore.js +25 -0
- package/dist/kernel/shrinkImageForUpload.d.ts +45 -0
- package/dist/kernel/shrinkImageForUpload.js +61 -0
- package/dist/kernel/useCanvasBridge.d.ts +36 -0
- package/dist/kernel/useCanvasBridge.js +41 -0
- package/dist/kernel/useCanvasGate.d.ts +22 -0
- package/dist/kernel/useCanvasGate.js +35 -0
- package/dist/kernel/useCanvasSession.d.ts +44 -0
- package/dist/kernel/useCanvasSession.js +182 -0
- package/dist/kernel/useCanvasStream.d.ts +39 -0
- package/dist/kernel/useCanvasStream.js +45 -0
- package/dist/kernel/useMobileFit.d.ts +40 -0
- package/dist/kernel/useMobileFit.js +139 -0
- package/dist/locales.d.ts +1094 -0
- package/dist/locales.js +545 -0
- package/dist/panels/StylePanel.d.ts +24 -0
- package/dist/panels/StylePanel.js +40 -0
- package/dist/panels/styleFields.d.ts +39 -0
- package/dist/panels/styleFields.js +91 -0
- package/dist/tree/LayersPanel.d.ts +30 -0
- package/dist/tree/LayersPanel.js +15 -0
- package/dist/tree/domTreeAdapter.d.ts +63 -0
- package/dist/tree/domTreeAdapter.js +90 -0
- package/dist/useInlineEditor.d.ts +158 -0
- package/dist/useInlineEditor.js +343 -0
- package/package.json +58 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { TREE_CLASS, TREE_ROW_HEIGHT } from './classNames.js';
|
|
5
|
+
import { dropPosition, rangeBetween, visibleOrder } from './order.js';
|
|
6
|
+
const DRAG_MIME = 'text/x-tree-id';
|
|
7
|
+
export function TreePanel({ nodes, rootId, selectedId, hoverId, query, onSelect, onShiftSelect, onHover, onMove, onExpand }) {
|
|
8
|
+
const [collapsed, setCollapsed] = useState(new Set());
|
|
9
|
+
const [drop, setDrop] = useState(null);
|
|
10
|
+
const searching = !!query?.trim();
|
|
11
|
+
const order = useMemo(() => visibleOrder(nodes, searching ? new Set() : collapsed, rootId), [nodes, collapsed, rootId, searching]);
|
|
12
|
+
const depthOf = useMemo(() => {
|
|
13
|
+
const map = new Map();
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
const walk = (id, depth) => {
|
|
16
|
+
if (seen.has(id))
|
|
17
|
+
return;
|
|
18
|
+
seen.add(id);
|
|
19
|
+
map.set(id, depth);
|
|
20
|
+
for (const c of nodes[id]?.children ?? [])
|
|
21
|
+
walk(c, depth + 1);
|
|
22
|
+
};
|
|
23
|
+
for (const c of nodes[rootId]?.children ?? [])
|
|
24
|
+
walk(c, 0);
|
|
25
|
+
return map;
|
|
26
|
+
}, [nodes, rootId]);
|
|
27
|
+
const parentOf = useMemo(() => {
|
|
28
|
+
const map = new Map();
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const walk = (id) => {
|
|
31
|
+
if (seen.has(id))
|
|
32
|
+
return;
|
|
33
|
+
seen.add(id);
|
|
34
|
+
for (const c of nodes[id]?.children ?? []) {
|
|
35
|
+
map.set(c, id);
|
|
36
|
+
walk(c);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
walk(rootId);
|
|
40
|
+
return map;
|
|
41
|
+
}, [nodes, rootId]);
|
|
42
|
+
const parentOfRef = useRef(parentOf);
|
|
43
|
+
parentOfRef.current = parentOf;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!selectedId)
|
|
46
|
+
return;
|
|
47
|
+
const parentOf = parentOfRef.current;
|
|
48
|
+
setCollapsed(prev => {
|
|
49
|
+
if (prev.size === 0)
|
|
50
|
+
return prev;
|
|
51
|
+
let next = null;
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
for (let id = parentOf.get(selectedId); id && !seen.has(id); id = parentOf.get(id)) {
|
|
54
|
+
seen.add(id);
|
|
55
|
+
if (!prev.has(id))
|
|
56
|
+
continue;
|
|
57
|
+
next ?? (next = new Set(prev));
|
|
58
|
+
next.delete(id);
|
|
59
|
+
}
|
|
60
|
+
return next ?? prev;
|
|
61
|
+
});
|
|
62
|
+
}, [selectedId]);
|
|
63
|
+
const rows = searching
|
|
64
|
+
? order.filter(id => nodes[id]?.label.toLowerCase().includes(query.trim().toLowerCase()))
|
|
65
|
+
: order;
|
|
66
|
+
const scrollRef = useRef(null);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!selectedId)
|
|
69
|
+
return;
|
|
70
|
+
const raf = requestAnimationFrame(() => {
|
|
71
|
+
const rows = scrollRef.current?.querySelectorAll('[data-id]');
|
|
72
|
+
if (!rows)
|
|
73
|
+
return;
|
|
74
|
+
for (const row of Array.from(rows)) {
|
|
75
|
+
if (row.dataset.id !== selectedId)
|
|
76
|
+
continue;
|
|
77
|
+
row.scrollIntoView({ block: 'nearest' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return () => cancelAnimationFrame(raf);
|
|
82
|
+
}, [selectedId]);
|
|
83
|
+
const toggle = (id) => setCollapsed(prev => {
|
|
84
|
+
const next = new Set(prev);
|
|
85
|
+
if (next.has(id))
|
|
86
|
+
next.delete(id);
|
|
87
|
+
else
|
|
88
|
+
next.add(id);
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
return (_jsx("div", { ref: scrollRef, className: TREE_CLASS.tree, "data-testid": "tree-panel", onMouseLeave: () => onHover(null), children: 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", { "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 => {
|
|
100
|
+
if (e.shiftKey && selectedId && onShiftSelect) {
|
|
101
|
+
onShiftSelect(rangeBetween(order, selectedId, id));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
onSelect(id);
|
|
105
|
+
}, onDragStart: e => e.dataTransfer.setData(DRAG_MIME, id), onDragOver: e => {
|
|
106
|
+
if (!onMove)
|
|
107
|
+
return;
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
110
|
+
const pos = dropPosition(e.clientY - rect.top, TREE_ROW_HEIGHT, node.canContain);
|
|
111
|
+
if (drop?.targetId !== id || drop.pos !== pos) {
|
|
112
|
+
setDrop({ targetId: id, pos });
|
|
113
|
+
}
|
|
114
|
+
}, onDragLeave: () => {
|
|
115
|
+
if (drop?.targetId === id)
|
|
116
|
+
setDrop(null);
|
|
117
|
+
}, onDrop: e => {
|
|
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)
|
|
127
|
+
onExpand?.(id);
|
|
128
|
+
else
|
|
129
|
+
toggle(id);
|
|
130
|
+
}, 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));
|
|
131
|
+
}) }));
|
|
132
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图层树的 class 契约 —— 与 v2 编辑器共用同一套名字(P-C2)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 必须与 v2 `styles/editor.css` 里的 `.layer-*` 选择器逐字一致。
|
|
5
|
+
* 改名的后果是样式静默失效(行还在、只是没样式),现象像「树错乱了」。
|
|
6
|
+
*/
|
|
7
|
+
export declare const TREE_CLASS: {
|
|
8
|
+
readonly tree: "layer-tree";
|
|
9
|
+
readonly row: "layer-row";
|
|
10
|
+
readonly label: "layer-name";
|
|
11
|
+
/** 行首图标位。v2 `editor.css` 里已有同名规则(14px、text-dim)。 */
|
|
12
|
+
readonly icon: "layer-icon";
|
|
13
|
+
readonly toggle: "layer-toggle";
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* 行高,与 `tree.css` 里 `.layer-row { height }` **必须一致**。
|
|
17
|
+
*
|
|
18
|
+
* 拖拽落点的三档判定(上 1/4 / 中间 / 下 1/4)按它算 —— 两处不同步会让判定整体偏一档,
|
|
19
|
+
* 表现是「明明拖到中间却插到了后面」。有测试锁住两者相等。
|
|
20
|
+
*/
|
|
21
|
+
export declare const TREE_ROW_HEIGHT = 28;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图层树的纯逻辑 —— 图层树行为的**唯一逻辑源**(P-C2)。
|
|
3
|
+
*
|
|
4
|
+
* 从 v2 编辑器的 `apps/x/src/components/landing-page-editor-v2/panels/LeftPanel.tsx`
|
|
5
|
+
* 移植而来。那边**没有任何测试**,所以移植的第一件事是补上 —— 否则这份实现从诞生起
|
|
6
|
+
* 就没有兜底。
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ 本期 v2 仍保留它自己那份(用户决定不动线上),因此**同一套语义暂时有两份实现**。
|
|
9
|
+
* 改这个文件时要同步确认 v2 侧是否要跟;彻底解决要等 v2 迁移过来。
|
|
10
|
+
*/
|
|
11
|
+
export interface TreeNode {
|
|
12
|
+
id: string;
|
|
13
|
+
children: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 当前**肉眼可见**的行,按树序扁平化。折叠节点自己在列、子树不展开。root 自身不入列。
|
|
17
|
+
*
|
|
18
|
+
* Shift 范围选择据此取区间 —— 必须与用户看到的行顺序一致,不能按父子关系推。
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ 带 `seen` 去环:DOM 树理论上无环,但适配器出 bug 时会造出环,而死循环会把整个
|
|
21
|
+
* 编辑器卡死、且完全看不出是树的问题。
|
|
22
|
+
*/
|
|
23
|
+
export declare function visibleOrder(nodes: Record<string, TreeNode>, collapsed: Set<string>, rootId: string): string[];
|
|
24
|
+
/** 「折叠全部」的目标集合:有子节点的非 root 节点。 */
|
|
25
|
+
export declare function collapsibleIds(nodes: Record<string, TreeNode>, rootId: string): string[];
|
|
26
|
+
export type DropPos = 'before' | 'after' | 'inside';
|
|
27
|
+
/**
|
|
28
|
+
* 拖到某一行上时的落点。上下各 1/4 是「插到前/后」,中间一半是「放进去」。
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ `canContain` 为假时中间档退成 `after`,不能返回 `inside` —— 否则会产出一个
|
|
31
|
+
* 应用不上的移动,而且**是静默的**(命令成功、页面没动)。
|
|
32
|
+
*/
|
|
33
|
+
export declare function dropPosition(offsetY: number, rowHeight: number, canContain: boolean): DropPos;
|
|
34
|
+
/**
|
|
35
|
+
* Shift 范围选择:按**可见行序**取 anchor→target 的闭区间,与拖动方向无关。
|
|
36
|
+
* 任一端不在序列里(比如刚被折叠掉)时退化成只选 target —— 比什么都不选更符合预期。
|
|
37
|
+
*/
|
|
38
|
+
export declare function rangeBetween(order: string[], anchorId: string, targetId: string): string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function visibleOrder(nodes, collapsed, rootId) {
|
|
2
|
+
const out = [];
|
|
3
|
+
const seen = new Set();
|
|
4
|
+
const walk = (id) => {
|
|
5
|
+
if (seen.has(id))
|
|
6
|
+
return;
|
|
7
|
+
seen.add(id);
|
|
8
|
+
out.push(id);
|
|
9
|
+
if (collapsed.has(id))
|
|
10
|
+
return;
|
|
11
|
+
for (const child of nodes[id]?.children ?? [])
|
|
12
|
+
walk(child);
|
|
13
|
+
};
|
|
14
|
+
for (const child of nodes[rootId]?.children ?? [])
|
|
15
|
+
walk(child);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
export function collapsibleIds(nodes, rootId) {
|
|
19
|
+
return Object.values(nodes)
|
|
20
|
+
.filter(n => n.id !== rootId && n.children.length > 0)
|
|
21
|
+
.map(n => n.id);
|
|
22
|
+
}
|
|
23
|
+
export function dropPosition(offsetY, rowHeight, canContain) {
|
|
24
|
+
const quarter = rowHeight / 4;
|
|
25
|
+
if (offsetY < quarter)
|
|
26
|
+
return 'before';
|
|
27
|
+
if (offsetY > rowHeight - quarter)
|
|
28
|
+
return 'after';
|
|
29
|
+
return canContain ? 'inside' : 'after';
|
|
30
|
+
}
|
|
31
|
+
export function rangeBetween(order, anchorId, targetId) {
|
|
32
|
+
const a = order.indexOf(anchorId);
|
|
33
|
+
const b = order.indexOf(targetId);
|
|
34
|
+
if (a < 0 || b < 0)
|
|
35
|
+
return [targetId];
|
|
36
|
+
const [from, to] = a <= b ? [a, b] : [b, a];
|
|
37
|
+
return order.slice(from, to + 1);
|
|
38
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图层树的样式 —— 与 v2 编辑器**同一套 class 名与观感**(P-C2)。
|
|
3
|
+
*
|
|
4
|
+
* 从 `apps/x/src/components/landing-page-editor-v2/styles/editor.css` 逐条移植,
|
|
5
|
+
* 作用域前缀 `.lp-editor-v2` 换成本包自己的 `:where(.inline-editor-canvas)`(见 `scope.ts`
|
|
6
|
+
* 与 `selection/tokens.css` 头注)。
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ `.layer-row` 的 `height` 必须与 `TreePanel` 里的 `ROW_HEIGHT` 一致 ——
|
|
9
|
+
* 拖拽落点的三档判定按那个高度算,两处不同步会让「上 1/4 / 中间 / 下 1/4」整体偏一档,
|
|
10
|
+
* 表现是「明明拖到中间却插到了后面」。有测试锁住这两个值相等。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.inline-editor-canvas {
|
|
14
|
+
--lpv2-accent: #3b6fb0;
|
|
15
|
+
--lpv2-accent-soft: rgba(59, 111, 176, 0.1);
|
|
16
|
+
--lpv2-hover: rgba(16, 28, 31, 0.08);
|
|
17
|
+
--lpv2-press: rgba(16, 28, 31, 0.1);
|
|
18
|
+
--lpv2-selected: rgba(16, 28, 31, 0.08);
|
|
19
|
+
--lpv2-text: rgba(16, 28, 31, 0.9);
|
|
20
|
+
--lpv2-text-body: rgba(16, 28, 31, 0.7);
|
|
21
|
+
--lpv2-text-dim: #707779;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:where(.inline-editor-canvas) .layer-row {
|
|
25
|
+
/* ⚠️ flex 容器里子项默认不肯收缩到内容宽度以下,不给 min-width:0 的话
|
|
26
|
+
.layer-name 的 ellipsis 不生效 —— 长文本会把整行撑出面板,
|
|
27
|
+
现象是「标签被面板边缘裁掉」而 text-overflow 明明写着。 */
|
|
28
|
+
min-width: 0;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 6px;
|
|
32
|
+
height: 28px;
|
|
33
|
+
margin: 0 4px;
|
|
34
|
+
padding-right: 10px;
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
user-select: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* 行首图标位 —— 从 v2 `.lp-editor-v2 .layer-icon` 逐条移植,去掉作用域前缀。
|
|
41
|
+
flex-shrink:0 不能省:不给的话长标签会把图标压扁成一条线。 */
|
|
42
|
+
:where(.inline-editor-canvas) .layer-icon {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
width: 14px;
|
|
47
|
+
color: var(--lpv2-text-dim);
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:where(.inline-editor-canvas) .layer-row:hover,
|
|
52
|
+
:where(.inline-editor-canvas) .layer-row.is-hovered {
|
|
53
|
+
background: var(--lpv2-hover);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:where(.inline-editor-canvas) .layer-row.in-selected-subtree {
|
|
57
|
+
background: rgba(16, 28, 31, 0.04);
|
|
58
|
+
border-radius: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:where(.inline-editor-canvas) .layer-row.is-selected:has(+ .layer-row.in-selected-subtree),
|
|
62
|
+
:where(.inline-editor-canvas) .layer-row.is-selected:has(+ .layer-row.is-selected) {
|
|
63
|
+
border-bottom-left-radius: 0;
|
|
64
|
+
border-bottom-right-radius: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:where(.inline-editor-canvas) .layer-row.in-selected-subtree + .layer-row.is-selected,
|
|
68
|
+
:where(.inline-editor-canvas) .layer-row.is-selected + .layer-row.is-selected {
|
|
69
|
+
border-top-left-radius: 0;
|
|
70
|
+
border-top-right-radius: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:where(.inline-editor-canvas) .layer-row.in-selected-subtree:hover,
|
|
74
|
+
:where(.inline-editor-canvas) .layer-row.in-selected-subtree.is-hovered {
|
|
75
|
+
background: var(--lpv2-hover);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:where(.inline-editor-canvas) .layer-row.is-selected {
|
|
79
|
+
background: var(--lpv2-selected);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
:where(.inline-editor-canvas) .layer-row.is-selected:hover,
|
|
83
|
+
:where(.inline-editor-canvas) .layer-row.is-selected.is-hovered {
|
|
84
|
+
background: var(--lpv2-press);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
:where(.inline-editor-canvas) .layer-row-root .layer-name {
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
color: var(--lpv2-text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:where(.inline-editor-canvas) .layer-row.drop-before {
|
|
93
|
+
box-shadow: inset 0 2px 0 var(--lpv2-accent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
:where(.inline-editor-canvas) .layer-row.drop-after {
|
|
97
|
+
box-shadow: inset 0 -2px 0 var(--lpv2-accent);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:where(.inline-editor-canvas) .layer-row.drop-inside {
|
|
101
|
+
background: var(--lpv2-accent-soft);
|
|
102
|
+
box-shadow: inset 0 0 0 1.5px var(--lpv2-accent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
:where(.inline-editor-canvas) .layer-name {
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
text-overflow: ellipsis;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
font-size: 12px;
|
|
110
|
+
color: var(--lpv2-text-body);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:where(.inline-editor-canvas) .layer-name-input {
|
|
114
|
+
flex: 1;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
font-size: 12px;
|
|
117
|
+
color: var(--lpv2-text);
|
|
118
|
+
background: #ffffff;
|
|
119
|
+
border: 1px solid var(--lpv2-accent);
|
|
120
|
+
border-radius: 3px;
|
|
121
|
+
padding: 1px 4px;
|
|
122
|
+
outline: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
:where(.inline-editor-canvas) .layer-icon {
|
|
126
|
+
color: var(--lpv2-text-dim);
|
|
127
|
+
font-size: 12px;
|
|
128
|
+
width: 14px;
|
|
129
|
+
text-align: center;
|
|
130
|
+
flex-shrink: 0;
|
|
131
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 编辑器顶栏 —— **一行 42px**,尺寸/间距/色值全部对齐三代落地页编辑器的
|
|
4
|
+
* `.editor-toolbar`(见 `./topbarPrimitives`)。
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* [退出] │ 首屏人物实穿图+购买CTA [版本 A:人物实穿图+CTA ⌄] … [↶][↷] │ [改动清单 3][预览][保存][✨]
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* ## 设备切换和目标页 URL **不在这里**
|
|
11
|
+
*
|
|
12
|
+
* 它们是「我在看哪台设备上的哪一页」,属于画布的上下文,不是 experience 的动作 ——
|
|
13
|
+
* 现在住在画布列顶部那条 `<CanvasContextBar>`(URL 左起、设备切换与画布同一条中轴)。
|
|
14
|
+
* 顶栏因此从两层 96px 收成一层 42px。
|
|
15
|
+
*
|
|
16
|
+
* ## 两档
|
|
17
|
+
*
|
|
18
|
+
* | 档 | 用在 | 少了什么 |
|
|
19
|
+
* |---|---|---|
|
|
20
|
+
* | `full` | experience 创建/编辑流程 | —— |
|
|
21
|
+
* | `minimal` | AI 对话里打开、快速编辑 | 保存、撤销组、版本下拉 |
|
|
22
|
+
*
|
|
23
|
+
* 两档共用**同一个** `useInlineEditor()` —— 画布、工具条、属性面板完全一致,差的只有
|
|
24
|
+
* 外壳。这是「headless 层必须存在」的直接体现:外壳差异不该渗进内核。
|
|
25
|
+
*/
|
|
26
|
+
export type ChromeVariant = 'full' | 'minimal';
|
|
27
|
+
export interface EditorChromeProps {
|
|
28
|
+
variant: ChromeVariant;
|
|
29
|
+
title: string;
|
|
30
|
+
/** 只在**没有** `versions` 时展示的静态字符串(旧调用方、单版本入口)。 */
|
|
31
|
+
versionLabel: string;
|
|
32
|
+
/**
|
|
33
|
+
* 可切换的版本列表。**宿主持有数据,外壳只负责切换动作** —— 版本是 experience 级状态
|
|
34
|
+
* (每个版本一份 change list),包里不存它。对照组不在其中(宿主传进来前已滤掉)。
|
|
35
|
+
*
|
|
36
|
+
* 不传(或空数组)时顶栏仍显示 `versionLabel` 那个静态标签,行为与今天完全一致。
|
|
37
|
+
*/
|
|
38
|
+
versions?: {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}[];
|
|
42
|
+
activeVersionId?: string;
|
|
43
|
+
onVersionChange?: (id: string) => void;
|
|
44
|
+
/**
|
|
45
|
+
* 正在切换版本(撤光当前版本的改动 + 回放另一版本的)。为真时版本入口与保存都禁用 ——
|
|
46
|
+
* 切换是一串跨域命令,中途再选一次会让两个版本的改动交错落在同一个页面上。
|
|
47
|
+
*/
|
|
48
|
+
versionSwitching?: boolean;
|
|
49
|
+
/** 新增版本。**不传则版本下拉里没有那一行** —— 见 `VersionMenu`。 */
|
|
50
|
+
onAddVersion?: () => void;
|
|
51
|
+
/** 版本改名。**不传则下拉行里没有那支铅笔** —— 同上。 */
|
|
52
|
+
onRenameVersion?: (id: string, name: string) => void;
|
|
53
|
+
changeCount: number;
|
|
54
|
+
onExit: () => void;
|
|
55
|
+
onSave?: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* 真实页面预览。**不传则不渲染那个入口** —— 预览链接由后端按 experience + 版本
|
|
58
|
+
* 签发,创建流程里还没有 experience,签不出来。
|
|
59
|
+
*/
|
|
60
|
+
onPreview?: () => void;
|
|
61
|
+
onToggleChangeList?: () => void;
|
|
62
|
+
/** 改动清单悬浮面板当前开着 —— 按钮据此高亮(它是开关,不是「打开」)。 */
|
|
63
|
+
changeListOpen?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* 开/关宿主的右侧 AI 面板。**不传则不渲染那个按钮** —— 同 `onPreview` 的处理。
|
|
66
|
+
*
|
|
67
|
+
* 宿主没有右侧 dock 可召唤时就别传 —— 渲染出来就是个点了没反应的按钮。
|
|
68
|
+
*
|
|
69
|
+
* ⚠️ 它是**纯开关**,不该顺手切 agent 或开新会话 —— 那是选中工具条上 ✨ 的语义
|
|
70
|
+
* (`onAskAi` → 宿主 `openDockWithPrefill`)。用户可能正跟某个 agent 聊着,
|
|
71
|
+
* 点一下顶栏开关就把对话清了是不能接受的。
|
|
72
|
+
*/
|
|
73
|
+
onToggleAi?: () => void;
|
|
74
|
+
/** 右侧 AI 面板当前开着。 */
|
|
75
|
+
aiOpen?: boolean;
|
|
76
|
+
onUndo?: () => void;
|
|
77
|
+
onRedo?: () => void;
|
|
78
|
+
canUndo?: boolean;
|
|
79
|
+
canRedo?: boolean;
|
|
80
|
+
/** 宿主可插入自定义按钮,排在动作组最后。 */
|
|
81
|
+
slotRight?: ReactNode;
|
|
82
|
+
}
|
|
83
|
+
export declare function EditorChrome(props: EditorChromeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ChevronLeft, List, Redo2, Sparkles, Undo2 } from 'lucide-react';
|
|
4
|
+
import { VersionMenu } from './VersionMenu.js';
|
|
5
|
+
import { ChromeBackButton, ChromeDivider, ChromeIconButton, ChromePrimaryButton, ChromeTextButton } from './topbarPrimitives.js';
|
|
6
|
+
export function EditorChrome(props) {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const full = props.variant === 'full';
|
|
9
|
+
return (_jsxs("header", { "data-testid": full ? 'editor-chrome-full' : 'editor-chrome-minimal', className: "relative flex h-[42px] shrink-0 items-center justify-between gap-2\n border-b border-[#ececef] bg-white px-3", children: [_jsxs("div", { className: "flex min-w-0 items-center", children: [_jsx(ChromeBackButton, { "data-testid": "chrome-exit", label: t(full ? 'inlineCanvas.chrome.exit' : 'inlineCanvas.chrome.close', full ? '退出' : '关闭'), onClick: props.onExit, children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsx(ChromeDivider, {}), _jsx("span", { className: "max-w-[240px] truncate rounded px-2 py-1 text-[13px] font-medium\n text-[#1a1a1a]", children: props.title }), full && (_jsx(VersionMenu, { versions: props.versions, activeVersionId: props.activeVersionId, onVersionChange: props.onVersionChange, onAddVersion: props.onAddVersion, onRenameVersion: props.onRenameVersion, switching: props.versionSwitching, fallbackLabel: props.versionLabel }))] }), _jsxs("div", { className: "flex shrink-0 items-center", children: [full && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(ChromeIconButton, { "data-testid": "chrome-undo", label: t('inlineCanvas.chrome.undo', '撤销'), disabled: !props.canUndo, onClick: props.onUndo, children: _jsx(Undo2, { className: "size-4" }) }), _jsx(ChromeIconButton, { "data-testid": "chrome-redo", label: t('inlineCanvas.chrome.redo', '重做'), disabled: !props.canRedo, onClick: props.onRedo, children: _jsx(Redo2, { className: "size-4" }) })] }), _jsx(ChromeDivider, {})] })), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(ChromeTextButton, { "data-testid": "chrome-changelist", "data-changelist-trigger": true, active: props.changeListOpen, onClick: props.onToggleChangeList, children: [_jsx(List, { className: "size-3.5" }), t('inlineCanvas.chrome.changeList', '改动清单'), props.changeCount > 0 && (_jsx("span", { className: "tabular-nums text-[#8a8f98]", children: props.changeCount }))] }), props.onPreview && (_jsx(ChromeTextButton, { "data-testid": "chrome-preview", onClick: props.onPreview, children: t('inlineCanvas.chrome.preview', '预览') })), full && (_jsx(ChromePrimaryButton, { "data-testid": "chrome-save", disabled: props.versionSwitching, onClick: props.onSave, children: t('inlineCanvas.chrome.save', '保存') })), props.onToggleAi && (_jsx(ChromeIconButton, { "data-testid": "chrome-ai", label: t('inlineCanvas.chrome.ai', 'AI 助手'), active: props.aiOpen, onClick: props.onToggleAi, children: _jsx(Sparkles, { className: "size-4" }) })), props.slotRight] })] })] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface VersionMenuProps {
|
|
2
|
+
versions?: {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}[];
|
|
6
|
+
activeVersionId?: string;
|
|
7
|
+
onVersionChange?: (id: string) => void;
|
|
8
|
+
/** 新增版本。**不传则下拉里没有那一行** —— 同顶栏其它入口的处理。 */
|
|
9
|
+
onAddVersion?: () => void;
|
|
10
|
+
/**
|
|
11
|
+
* 版本改名。**不传则行内那支铅笔不渲染** —— 与 `onAddVersion` 同一套纪律:
|
|
12
|
+
* 宿主没接这件事就别给入口,而不是给一个点不动的铅笔。
|
|
13
|
+
*
|
|
14
|
+
* 版本名不归编辑器管(它是 experience 级状态、保存时作为 `summary` 落库),
|
|
15
|
+
* 所以这里只做校验与交互,改不改得成由宿主说了算。
|
|
16
|
+
*/
|
|
17
|
+
onRenameVersion?: (id: string, name: string) => void;
|
|
18
|
+
/**
|
|
19
|
+
* 正在切换版本(撤光当前版本的改动 + 回放另一版本的)。为真时整个入口禁用 ——
|
|
20
|
+
* 切换是一串跨域命令,中途再选一次会让两个版本的改动交错落在同一个页面上。
|
|
21
|
+
*/
|
|
22
|
+
switching?: boolean;
|
|
23
|
+
/** 没有 `versions` 时展示的静态字符串(旧调用方、单版本入口)。 */
|
|
24
|
+
fallbackLabel: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function VersionMenu({ versions, activeVersionId, onVersionChange, onAddVersion, onRenameVersion, switching, fallbackLabel }: VersionMenuProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Check, ChevronDown, Pencil, Plus } from 'lucide-react';
|
|
5
|
+
import { useDismissOnOutside } from './useDismissOnOutside.js';
|
|
6
|
+
const MAX_VERSION_NAME = 40;
|
|
7
|
+
function versionLetter(index) {
|
|
8
|
+
return index < 26 ? String.fromCharCode(65 + index) : String(index + 1);
|
|
9
|
+
}
|
|
10
|
+
const VARIANT_KEY = 'inlineCanvas.chrome.variant';
|
|
11
|
+
function descriptiveName(name, label) {
|
|
12
|
+
const trimmed = name.trim();
|
|
13
|
+
return trimmed && trimmed !== label.trim() ? trimmed : null;
|
|
14
|
+
}
|
|
15
|
+
export function VersionMenu({ versions, activeVersionId, onVersionChange, onAddVersion, onRenameVersion, switching = false, fallbackLabel }) {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const ref = useRef(null);
|
|
19
|
+
const [editing, setEditing] = useState(null);
|
|
20
|
+
const close = useCallback(() => {
|
|
21
|
+
setOpen(false);
|
|
22
|
+
setEditing(null);
|
|
23
|
+
}, []);
|
|
24
|
+
useDismissOnOutside(open, ref, close, 'version-trigger');
|
|
25
|
+
if (!versions?.length) {
|
|
26
|
+
return (_jsx("span", { "data-testid": "chrome-version-static", className: "shrink-0 rounded bg-[#f3f4f4] px-2.5 py-1 text-xs text-[#1a1a1a]", children: fallbackLabel }));
|
|
27
|
+
}
|
|
28
|
+
const renameError = (next, id) => {
|
|
29
|
+
if (!next)
|
|
30
|
+
return t('inlineCanvas.chrome.renameEmpty', '名字不能为空');
|
|
31
|
+
if (next.length > MAX_VERSION_NAME)
|
|
32
|
+
return t('inlineCanvas.chrome.renameTooLong', '名字最多 40 个字');
|
|
33
|
+
if (next.includes('|'))
|
|
34
|
+
return t('inlineCanvas.chrome.renameSeparator', '名字里不能有「|」');
|
|
35
|
+
if (versions.some(o => o.id !== id && o.name.trim() === next))
|
|
36
|
+
return t('inlineCanvas.chrome.renameDuplicate', '已有同名版本');
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
const commitRename = (v) => {
|
|
40
|
+
if (!editing || editing.id !== v.id)
|
|
41
|
+
return;
|
|
42
|
+
const next = editing.draft.trim();
|
|
43
|
+
if (next === v.name) {
|
|
44
|
+
setEditing(null);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const error = renameError(next, v.id);
|
|
48
|
+
if (error) {
|
|
49
|
+
setEditing({ ...editing, error });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setEditing(null);
|
|
53
|
+
onRenameVersion?.(v.id, next);
|
|
54
|
+
};
|
|
55
|
+
const activeIndex = versions.findIndex(v => v.id === activeVersionId);
|
|
56
|
+
const active = activeIndex >= 0 ? versions[activeIndex] : undefined;
|
|
57
|
+
const label = (i) => t(VARIANT_KEY, { name: versionLetter(i), defaultValue: `版本 ${versionLetter(i)}` });
|
|
58
|
+
const activeLabel = activeIndex >= 0 ? label(activeIndex) : '';
|
|
59
|
+
return (_jsxs("div", { className: "relative shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "chrome-version-trigger", "data-version-trigger": true, "aria-haspopup": "listbox", "aria-expanded": open, disabled: switching, onClick: () => setOpen(v => !v), className: "inline-flex h-[30px] max-w-[260px] items-center gap-1.5 rounded-lg\n border border-[#ececef] bg-white pl-2.5 pr-2 text-xs transition-colors\n enabled:hover:bg-[rgba(16,28,31,0.06)] disabled:cursor-default\n disabled:opacity-50", children: [active ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-[#12262b]", children: activeLabel }), descriptiveName(active.name, activeLabel) && (_jsxs("span", { className: "truncate text-[#12262b]", children: ["\uFF1A", active.name] }))] })) : (_jsx("span", { className: "truncate text-[#52525b]", children: fallbackLabel })), _jsx(ChevronDown, { className: "size-3.5 shrink-0 text-[#8a8f98]" })] }), open && (_jsxs("div", { ref: ref, "data-testid": "chrome-version-menu", role: "listbox", className: "absolute left-0 top-[calc(100%+6px)] z-40 w-[300px] rounded-lg\n border border-[#ececef] bg-white p-[5px]\n shadow-[0_8px_24px_rgba(16,28,31,0.14),0_2px_8px_rgba(16,28,31,0.08)]", children: [versions.map((v, i) => {
|
|
60
|
+
const on = v.id === activeVersionId;
|
|
61
|
+
if (editing?.id === v.id) {
|
|
62
|
+
return (_jsxs("div", { className: "px-2.5 py-1.5", children: [_jsx("input", { autoFocus: true, "data-testid": `chrome-version-rename-input-${v.id}`, value: editing.draft, onChange: e => setEditing({ id: v.id, draft: e.target.value, error: null }), onKeyDown: e => {
|
|
63
|
+
if (e.key === 'Escape') {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
setEditing(null);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (e.key === 'Enter') {
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
commitRename(v);
|
|
71
|
+
}
|
|
72
|
+
}, onBlur: () => commitRename(v), className: "w-full rounded-md border border-[#00A92E] px-2 py-1\n text-[12.5px] text-[#12262b] outline-none" }), editing.error && (_jsx("div", { "data-testid": "chrome-version-rename-error", className: "mt-1 px-0.5 text-[11.5px] text-[#d4380d]", children: editing.error }))] }, v.id));
|
|
73
|
+
}
|
|
74
|
+
return (_jsxs("div", { className: "group flex w-full items-center gap-1", children: [_jsxs("button", { type: "button", role: "option", "aria-selected": on, "data-testid": `chrome-version-option-${v.id}`, onClick: () => {
|
|
75
|
+
setOpen(false);
|
|
76
|
+
if (!on)
|
|
77
|
+
onVersionChange?.(v.id);
|
|
78
|
+
}, className: `flex min-w-0 flex-1 items-center gap-2 rounded-md px-2.5 py-2
|
|
79
|
+
text-left text-[12.5px] transition-colors
|
|
80
|
+
hover:bg-[rgba(16,28,31,0.06)]
|
|
81
|
+
${on ? 'bg-[rgba(16,28,31,0.06)]' : ''}`, children: [_jsx("span", { className: "shrink-0 text-[#12262b]", children: label(i) }), descriptiveName(v.name, label(i)) && (_jsxs("span", { className: "truncate text-[#12262b]", children: ["\uFF1A", v.name] })), on && _jsx(Check, { className: "ml-auto size-3.5 shrink-0 text-[#12262b]" })] }), onRenameVersion && (_jsx("button", { type: "button", "data-testid": `chrome-version-rename-${v.id}`, title: t('inlineCanvas.chrome.renameVersion', '重命名'), "aria-label": t('inlineCanvas.chrome.renameVersion', '重命名'), onClick: () => setEditing({ id: v.id, draft: v.name, error: null }), className: "shrink-0 rounded-md p-1.5 text-[#6b7477] opacity-0\n transition-opacity hover:bg-[rgba(16,28,31,0.06)]\n focus-visible:opacity-100 group-hover:opacity-100", children: _jsx(Pencil, { className: "size-3.5" }) }))] }, v.id));
|
|
82
|
+
}), onAddVersion && (_jsxs("button", { type: "button", "data-testid": "chrome-add-version", onClick: () => {
|
|
83
|
+
setOpen(false);
|
|
84
|
+
onAddVersion();
|
|
85
|
+
}, className: "mt-1 flex w-full items-center gap-1.5 border-t\n border-[#f1f2f3] px-2.5 pb-1 pt-2.5 text-left text-[12.5px]\n text-[#00A92E]", children: [_jsx(Plus, { className: "size-3.5" }), t('inlineCanvas.chrome.addVersion', '新增版本')] }))] }))] }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 顶栏原语 —— 尺寸、间距、色值**逐条对齐三代落地页编辑器的顶栏**
|
|
4
|
+
* (`apps/x/src/components/landing-pages/editor/topbar/topbar.css` 的 `lpx-tb-*`,
|
|
5
|
+
* 以及 `landing-page-editor-v3/styles/editor.css` 的 `.editor-toolbar` 系列)。
|
|
6
|
+
*
|
|
7
|
+
* ## 为什么是抄取值,不是复用组件
|
|
8
|
+
*
|
|
9
|
+
* 那套原语住在 app 里(它依赖 app 的 `SimpleTooltip` 与 `--lpv2-*` token),而本包
|
|
10
|
+
* **不能反向依赖 app** —— 同 `renderNodeIcon` 由宿主注入的理由。
|
|
11
|
+
*
|
|
12
|
+
* ## 色值也跟着换了
|
|
13
|
+
*
|
|
14
|
+
* 早先这里只搬几何、色值留在本包原有的 #091E42 档,理由是「顶栏与它下面的三栏不同色」。
|
|
15
|
+
* 实际做出来两套灰在同一屏上更别扭 —— 顶栏现在整条走 lpx 的中性档(#1a1a1a / #52525b /
|
|
16
|
+
* #ececef / #12262b),下面的图层树、样式面板、清单不动。顶栏是一条独立的横带,
|
|
17
|
+
* 它和落地页编辑器一致比和自己脚下的面板一致更重要。
|
|
18
|
+
*
|
|
19
|
+
* ## 为什么用 `title` 而不是 tooltip 组件
|
|
20
|
+
*
|
|
21
|
+
* 图标按钮没有可见文字,必须有悬浮提示。本包里没有 tooltip 原语,而 Radix 那套在
|
|
22
|
+
* jsdom 里点不开(`useDismissOnOutside` 的头注释有同一条理由)—— 原生 `title`
|
|
23
|
+
* 加 `aria-label` 是这里唯一既能用又守得住的做法。
|
|
24
|
+
*/
|
|
25
|
+
/** 顶栏分组分隔线:1×18 `#e7e8e9`,左右各 8px(= lpx-tb-divider)。 */
|
|
26
|
+
export declare function ChromeDivider(): import("react").JSX.Element;
|
|
27
|
+
export interface ChromeIconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
28
|
+
/** 悬浮提示 + 无障碍名。图标按钮没有可见文字,**必填**。 */
|
|
29
|
+
label: string;
|
|
30
|
+
/**
|
|
31
|
+
* 选中 / 面板展开中。
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ **不传就不写 `aria-pressed`**(不是写 false)。`aria-pressed` 的含义是「这是个
|
|
34
|
+
* 双态开关」—— 撤销、重做、预览都是一次性动作,给它们写上 false 等于告诉读屏软件
|
|
35
|
+
* 「这个开关现在是关的」,那是假的。只有 AI 开关、改动清单这类真开关才带。
|
|
36
|
+
*/
|
|
37
|
+
active?: boolean;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
/** 顶栏 30×30 图标按钮(= lpx-tb-btn)。顶栏里除退出和主动作外的图标入口都用它。 */
|
|
41
|
+
export declare function ChromeIconButton({ label, active, className, children, ...rest }: ChromeIconButtonProps): import("react").JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* 退出按钮(= .toolbar-back-btn)——**带边框、radius 6**,与顶栏其它图标按钮刻意不同。
|
|
44
|
+
*
|
|
45
|
+
* 它是这条顶栏上唯一一个「离开当前上下文」的动作,落地页编辑器也是这么区分的:
|
|
46
|
+
* 其余图标按钮是透明底 radius 8,只有它有边框。
|
|
47
|
+
*/
|
|
48
|
+
export declare function ChromeBackButton({ label, className, children, ...rest }: ChromeIconButtonProps): import("react").JSX.Element;
|
|
49
|
+
/** 顶栏文字按钮(= .editor-checklist-btn):h30 / radius 8 / 描边白底。 */
|
|
50
|
+
export declare function ChromeTextButton({ active, className, children, ...rest }: {
|
|
51
|
+
active?: boolean;
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
} & ButtonHTMLAttributes<HTMLButtonElement>): import("react").JSX.Element;
|
|
54
|
+
/** 顶栏主动作按钮(= .toolbar-action-btn):h30 / radius 6 / `#12262b`。一条顶栏只该有一个。 */
|
|
55
|
+
export declare function ChromePrimaryButton({ className, children, ...rest }: {
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
} & ButtonHTMLAttributes<HTMLButtonElement>): import("react").JSX.Element;
|
|
58
|
+
/**
|
|
59
|
+
* 分段 tab 的轨道(= lpx-tb-tabs):灰底 + 2px 内边距,当前档是浮起的白卡片。
|
|
60
|
+
*
|
|
61
|
+
* 当前档靠「白卡片」区分而不是靠彩色底 —— 与落地页编辑器一致。
|
|
62
|
+
*/
|
|
63
|
+
export declare function ChromeSegmented({ children, className, ...rest }: HTMLAttributes<HTMLDivElement>): import("react").JSX.Element;
|
|
64
|
+
export interface ChromeSegmentProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
65
|
+
active: boolean;
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
}
|
|
68
|
+
/** 分段 tab 的一档(= lpx-tb-tab):高 26、radius 6,当前档白卡片 + 1px 阴影。 */
|
|
69
|
+
export declare function ChromeSegment({ active, className, children, ...rest }: ChromeSegmentProps): import("react").JSX.Element;
|