@ptengine/inline-editor 0.0.0-stage → 0.1.1

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.
Files changed (169) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +286 -2
  3. package/dist/InlineEditor.d.ts +349 -0
  4. package/dist/InlineEditor.js +886 -0
  5. package/dist/PagePreview.d.ts +83 -0
  6. package/dist/PagePreview.js +111 -0
  7. package/dist/assets/AssetsPanel.d.ts +19 -0
  8. package/dist/assets/AssetsPanel.js +23 -0
  9. package/dist/assets/componentLibrary.d.ts +23 -0
  10. package/dist/assets/componentLibrary.js +12 -0
  11. package/dist/canvas-ui/index.d.ts +17 -0
  12. package/dist/canvas-ui/index.js +7 -0
  13. package/dist/canvas-ui/scope.d.ts +13 -0
  14. package/dist/canvas-ui/scope.js +1 -0
  15. package/dist/canvas-ui/selection/classNames.d.ts +30 -0
  16. package/dist/canvas-ui/selection/classNames.js +15 -0
  17. package/dist/canvas-ui/selection/tokens.css +204 -0
  18. package/dist/canvas-ui/tree/TreePanel.d.ts +47 -0
  19. package/dist/canvas-ui/tree/TreePanel.js +132 -0
  20. package/dist/canvas-ui/tree/classNames.d.ts +21 -0
  21. package/dist/canvas-ui/tree/classNames.js +8 -0
  22. package/dist/canvas-ui/tree/order.d.ts +38 -0
  23. package/dist/canvas-ui/tree/order.js +38 -0
  24. package/dist/canvas-ui/tree/tree.css +131 -0
  25. package/dist/chrome/EditorChrome.d.ts +83 -0
  26. package/dist/chrome/EditorChrome.js +10 -0
  27. package/dist/chrome/VersionMenu.d.ts +26 -0
  28. package/dist/chrome/VersionMenu.js +86 -0
  29. package/dist/chrome/topbarPrimitives.d.ts +69 -0
  30. package/dist/chrome/topbarPrimitives.js +39 -0
  31. package/dist/chrome/useDismissOnOutside.d.ts +23 -0
  32. package/dist/chrome/useDismissOnOutside.js +27 -0
  33. package/dist/dialogs/ClickActionDialog.d.ts +24 -0
  34. package/dist/dialogs/ClickActionDialog.js +35 -0
  35. package/dist/dialogs/HtmlEditDialog.d.ts +23 -0
  36. package/dist/dialogs/HtmlEditDialog.js +23 -0
  37. package/dist/dialogs/ImageEditDialog.d.ts +30 -0
  38. package/dist/dialogs/ImageEditDialog.js +57 -0
  39. package/dist/dialogs/ImageSettingDialog.d.ts +16 -0
  40. package/dist/dialogs/ImageSettingDialog.js +17 -0
  41. package/dist/dialogs/InsertElementDialog.d.ts +23 -0
  42. package/dist/dialogs/InsertElementDialog.js +28 -0
  43. package/dist/dialogs/ReplaceVideoDialog.d.ts +18 -0
  44. package/dist/dialogs/ReplaceVideoDialog.js +16 -0
  45. package/dist/dialogs/actionTypes.d.ts +53 -0
  46. package/dist/dialogs/actionTypes.js +74 -0
  47. package/dist/dialogs/htmlValidation.d.ts +24 -0
  48. package/dist/dialogs/htmlValidation.js +37 -0
  49. package/dist/dialogs/imageEdit.d.ts +49 -0
  50. package/dist/dialogs/imageEdit.js +30 -0
  51. package/dist/index.d.ts +101 -0
  52. package/dist/index.js +53 -0
  53. package/dist/kernel/CanvasContextBar.d.ts +23 -0
  54. package/dist/kernel/CanvasContextBar.js +12 -0
  55. package/dist/kernel/CanvasFrame.d.ts +56 -0
  56. package/dist/kernel/CanvasFrame.js +53 -0
  57. package/dist/kernel/ai/aiInlineApply.d.ts +330 -0
  58. package/dist/kernel/ai/aiInlineApply.js +477 -0
  59. package/dist/kernel/ai/imageSource.d.ts +24 -0
  60. package/dist/kernel/ai/imageSource.js +19 -0
  61. package/dist/kernel/ai/insertHtmlGate.d.ts +71 -0
  62. package/dist/kernel/ai/insertHtmlGate.js +123 -0
  63. package/dist/kernel/ai/postApplyCheck.d.ts +56 -0
  64. package/dist/kernel/ai/postApplyCheck.js +74 -0
  65. package/dist/kernel/ai/replaceHtmlGate.d.ts +71 -0
  66. package/dist/kernel/ai/replaceHtmlGate.js +147 -0
  67. package/dist/kernel/blocks/BlockOverlay.d.ts +16 -0
  68. package/dist/kernel/blocks/BlockOverlay.js +66 -0
  69. package/dist/kernel/blocks/annotateAndTile.d.ts +106 -0
  70. package/dist/kernel/blocks/annotateAndTile.js +122 -0
  71. package/dist/kernel/blocks/resolveBlocks.d.ts +153 -0
  72. package/dist/kernel/blocks/resolveBlocks.js +233 -0
  73. package/dist/kernel/canvasCapabilities.d.ts +31 -0
  74. package/dist/kernel/canvasCapabilities.js +39 -0
  75. package/dist/kernel/canvasCommands.d.ts +126 -0
  76. package/dist/kernel/canvasCommands.js +146 -0
  77. package/dist/kernel/canvasErrors.d.ts +1 -0
  78. package/dist/kernel/canvasErrors.js +14 -0
  79. package/dist/kernel/captureGeometry.d.ts +35 -0
  80. package/dist/kernel/captureGeometry.js +25 -0
  81. package/dist/kernel/captureOrchestration.d.ts +48 -0
  82. package/dist/kernel/captureOrchestration.js +43 -0
  83. package/dist/kernel/captureSlices.d.ts +36 -0
  84. package/dist/kernel/captureSlices.js +20 -0
  85. package/dist/kernel/changelist/ChangeListPanel.d.ts +13 -0
  86. package/dist/kernel/changelist/ChangeListPanel.js +47 -0
  87. package/dist/kernel/changelist/ChangeListPopover.d.ts +20 -0
  88. package/dist/kernel/changelist/ChangeListPopover.js +10 -0
  89. package/dist/kernel/changelist/builders.d.ts +89 -0
  90. package/dist/kernel/changelist/builders.js +81 -0
  91. package/dist/kernel/changelist/changeSummary.d.ts +19 -0
  92. package/dist/kernel/changelist/changeSummary.js +85 -0
  93. package/dist/kernel/changelist/clipboard.d.ts +16 -0
  94. package/dist/kernel/changelist/clipboard.js +4 -0
  95. package/dist/kernel/changelist/exportForSave.d.ts +30 -0
  96. package/dist/kernel/changelist/exportForSave.js +11 -0
  97. package/dist/kernel/changelist/mergeForSave.d.ts +79 -0
  98. package/dist/kernel/changelist/mergeForSave.js +40 -0
  99. package/dist/kernel/changelist/moveObserverSafety.d.ts +70 -0
  100. package/dist/kernel/changelist/moveObserverSafety.js +39 -0
  101. package/dist/kernel/changelist/overrides.d.ts +22 -0
  102. package/dist/kernel/changelist/overrides.js +63 -0
  103. package/dist/kernel/changelist/useChangeList.d.ts +200 -0
  104. package/dist/kernel/changelist/useChangeList.js +282 -0
  105. package/dist/kernel/config.d.ts +29 -0
  106. package/dist/kernel/config.js +10 -0
  107. package/dist/kernel/dataUrlToBlob.d.ts +8 -0
  108. package/dist/kernel/dataUrlToBlob.js +17 -0
  109. package/dist/kernel/linkHref.d.ts +25 -0
  110. package/dist/kernel/linkHref.js +16 -0
  111. package/dist/kernel/mobileFit.d.ts +46 -0
  112. package/dist/kernel/mobileFit.js +44 -0
  113. package/dist/kernel/moveState.d.ts +35 -0
  114. package/dist/kernel/moveState.js +14 -0
  115. package/dist/kernel/selection/SelectionOverlay.d.ts +10 -0
  116. package/dist/kernel/selection/SelectionOverlay.js +25 -0
  117. package/dist/kernel/selection/SelectionToolbar.d.ts +30 -0
  118. package/dist/kernel/selection/SelectionToolbar.js +82 -0
  119. package/dist/kernel/selection/SelectorPathBar.d.ts +27 -0
  120. package/dist/kernel/selection/SelectorPathBar.js +55 -0
  121. package/dist/kernel/selection/buildDescendantShelf.d.ts +40 -0
  122. package/dist/kernel/selection/buildDescendantShelf.js +50 -0
  123. package/dist/kernel/selection/buildRootShelf.d.ts +26 -0
  124. package/dist/kernel/selection/buildRootShelf.js +149 -0
  125. package/dist/kernel/selection/elementAiContext.d.ts +324 -0
  126. package/dist/kernel/selection/elementAiContext.js +122 -0
  127. package/dist/kernel/selection/elementGeometryForAi.d.ts +9 -0
  128. package/dist/kernel/selection/elementGeometryForAi.js +15 -0
  129. package/dist/kernel/selection/geometry.d.ts +55 -0
  130. package/dist/kernel/selection/geometry.js +18 -0
  131. package/dist/kernel/selection/resolveAnchors.d.ts +116 -0
  132. package/dist/kernel/selection/resolveAnchors.js +173 -0
  133. package/dist/kernel/selection/shelfRow.d.ts +33 -0
  134. package/dist/kernel/selection/shelfRow.js +21 -0
  135. package/dist/kernel/selection/tagLabels.d.ts +9 -0
  136. package/dist/kernel/selection/tagLabels.js +46 -0
  137. package/dist/kernel/selection/toolbarButtons.d.ts +60 -0
  138. package/dist/kernel/selection/toolbarButtons.js +105 -0
  139. package/dist/kernel/selection/useAiSelectionContext.d.ts +26 -0
  140. package/dist/kernel/selection/useAiSelectionContext.js +26 -0
  141. package/dist/kernel/selection/useCanvasGeometry.d.ts +32 -0
  142. package/dist/kernel/selection/useCanvasGeometry.js +47 -0
  143. package/dist/kernel/selection/useSelectionRestore.d.ts +18 -0
  144. package/dist/kernel/selection/useSelectionRestore.js +25 -0
  145. package/dist/kernel/shrinkImageForUpload.d.ts +45 -0
  146. package/dist/kernel/shrinkImageForUpload.js +61 -0
  147. package/dist/kernel/useCanvasBridge.d.ts +36 -0
  148. package/dist/kernel/useCanvasBridge.js +41 -0
  149. package/dist/kernel/useCanvasGate.d.ts +22 -0
  150. package/dist/kernel/useCanvasGate.js +35 -0
  151. package/dist/kernel/useCanvasSession.d.ts +44 -0
  152. package/dist/kernel/useCanvasSession.js +182 -0
  153. package/dist/kernel/useCanvasStream.d.ts +39 -0
  154. package/dist/kernel/useCanvasStream.js +45 -0
  155. package/dist/kernel/useMobileFit.d.ts +40 -0
  156. package/dist/kernel/useMobileFit.js +139 -0
  157. package/dist/locales.d.ts +1094 -0
  158. package/dist/locales.js +545 -0
  159. package/dist/panels/StylePanel.d.ts +24 -0
  160. package/dist/panels/StylePanel.js +40 -0
  161. package/dist/panels/styleFields.d.ts +39 -0
  162. package/dist/panels/styleFields.js +91 -0
  163. package/dist/tree/LayersPanel.d.ts +30 -0
  164. package/dist/tree/LayersPanel.js +15 -0
  165. package/dist/tree/domTreeAdapter.d.ts +63 -0
  166. package/dist/tree/domTreeAdapter.js +90 -0
  167. package/dist/useInlineEditor.d.ts +161 -0
  168. package/dist/useInlineEditor.js +346 -0
  169. package/package.json +58 -3
@@ -0,0 +1,44 @@
1
+ export const MOBILE_FIT_LAYER = 'mobile-fit';
2
+ export const RENDER_FIXES_LAYER = 'render-fixes';
3
+ export const RENDER_FIXES_ORDER = 10;
4
+ export const MOBILE_FIT_ORDER = 20;
5
+ export function naturalSize(m) {
6
+ const rawWidth = Math.max(m.html.scrollWidth, m.body?.scrollWidth ?? 0);
7
+ const width = m.body && m.body.computed.overflowX === 'hidden' ? m.body.clientWidth : rawWidth;
8
+ const height = Math.max(m.html.scrollHeight, m.body?.scrollHeight ?? 0);
9
+ return { width, height, rawWidth };
10
+ }
11
+ export function computeScale(viewportWidth, naturalWidth) {
12
+ if (naturalWidth <= 0 || viewportWidth <= 0)
13
+ return 1;
14
+ return Math.min(1, viewportWidth / naturalWidth);
15
+ }
16
+ export function buildFitCss(naturalWidth, naturalHeight, scale) {
17
+ if (scale >= 1)
18
+ return '';
19
+ const scaledHeight = Math.round(naturalHeight * scale);
20
+ return (`html{transform:scale(${scale}) !important;transform-origin:0 0 !important;` +
21
+ `width:${naturalWidth}px !important;height:${scaledHeight}px !important;}`);
22
+ }
23
+ export function planMobileFit(m) {
24
+ const size = naturalSize(m);
25
+ const scale = computeScale(m.innerWidth, size.width);
26
+ return { ...size, scale, css: buildFitCss(size.width, size.height, scale) };
27
+ }
28
+ export function buildRenderFixesCss(m) {
29
+ const rules = [];
30
+ if (m.html.computed.scrollBehavior && m.html.computed.scrollBehavior !== 'auto') {
31
+ rules.push('html{scroll-behavior:auto !important;}');
32
+ }
33
+ if (m.html.computed.display === 'none' || m.body?.computed.display === 'none') {
34
+ rules.push('html,body{display:block !important;}');
35
+ }
36
+ if (m.body) {
37
+ const h = m.body.computed.height;
38
+ const fixed = h !== '' && h !== 'auto' && h !== 'unset';
39
+ if (fixed && m.body.scrollHeight > m.body.offsetHeight) {
40
+ rules.push('body{height:unset !important;}');
41
+ }
42
+ }
43
+ return rules.join('');
44
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 「移动元素」的两步拾取状态机。
3
+ *
4
+ * 两步:① 记下**源**元素(点工具条那一刻的选中元素);② 用户在画布里点**目标**元素;
5
+ * ③ 选相对位置,产出 `elementMove`。
6
+ *
7
+ * ⚠️ **选目标期间绝不能动源选中态。** 画布里的点选平时会刷新选中元素,移动模式下那次
8
+ * 点选的语义完全不同 —— 不拦的话「源」会在第二步被自己覆盖掉,用户最后把元素移到了
9
+ * 它自己旁边(或者更糟:agent 报「不能移进自己」,而用户根本不知道源什么时候变的)。
10
+ *
11
+ * 抽成纯函数是因为这套时序在组件里测不动:它跨「点工具条 → 收 elementPicked 事件 →
12
+ * 点位置按钮」三次异步交互。
13
+ */
14
+ export type MovePosition = 'top' | 'bottom' | 'before' | 'after';
15
+ export interface MoveState {
16
+ /** 要移动的元素。 */
17
+ source: string;
18
+ /** 目标元素;`null` = 还在等用户点选。 */
19
+ target: string | null;
20
+ /** 上一次拾取被拒的原因,供界面提示。 */
21
+ rejected?: 'same-element';
22
+ }
23
+ export declare function startMove(source: string): MoveState;
24
+ /**
25
+ * 收到一次画布拾取。返回新状态 —— 调用方据此决定「这次点选算选目标,还是算普通选中」。
26
+ *
27
+ * 目标 == 源时**拒绝并留在等待态**:把元素移到它自己里面是没有意义的,
28
+ * 而 agent 侧对这种情况的行为未确证(可能静默无效,也可能抛错)。在这里拦掉最省事。
29
+ */
30
+ export declare function pickTarget(state: MoveState, selector: string): MoveState;
31
+ /** 移动模式下这次拾取该被「吃掉」吗(吃掉 = 不刷新选中态)。 */
32
+ export declare function shouldCapturePick(state: MoveState | null): boolean;
33
+ export declare function isReady(state: MoveState | null): state is MoveState & {
34
+ target: string;
35
+ };
@@ -0,0 +1,14 @@
1
+ export function startMove(source) {
2
+ return { source, target: null };
3
+ }
4
+ export function pickTarget(state, selector) {
5
+ if (selector === state.source)
6
+ return { ...state, target: null, rejected: 'same-element' };
7
+ return { source: state.source, target: selector };
8
+ }
9
+ export function shouldCapturePick(state) {
10
+ return state !== null && state.target === null;
11
+ }
12
+ export function isReady(state) {
13
+ return state !== null && state.target !== null;
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { CanvasGeometryPayload } from '@ptengine/inline-core/protocol';
2
+ import { type FrameToProductContext } from './geometry.js';
3
+ export interface SelectionOverlayProps {
4
+ geometry: CanvasGeometryPayload | null;
5
+ /** iframe 在产品页里的位置与画布缩放 —— 换算所需。 */
6
+ context: FrameToProductContext;
7
+ /** 选中元素的类型标签文案(如 `<img>`)。空则不显示标签。 */
8
+ selectedLabel?: string;
9
+ }
10
+ export declare function SelectionOverlay({ geometry, context, selectedLabel }: SelectionOverlayProps): import("react").JSX.Element | null;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SELECTION_CLASS } from '../../canvas-ui/index.js';
3
+ import { frameRectToProductRect } from './geometry.js';
4
+ function rectStyle(rect) {
5
+ return {
6
+ position: 'fixed',
7
+ left: `${rect.x}px`,
8
+ top: `${rect.y}px`,
9
+ width: `${rect.width}px`,
10
+ height: `${rect.height}px`,
11
+ boxSizing: 'border-box',
12
+ zIndex: 2147483000
13
+ };
14
+ }
15
+ export function SelectionOverlay({ geometry, context, selectedLabel }) {
16
+ if (!geometry)
17
+ return null;
18
+ const selected = geometry.selectedViewportRect
19
+ ? frameRectToProductRect(geometry.selectedViewportRect, context)
20
+ : null;
21
+ const hover = geometry.hoverViewportRect
22
+ ? frameRectToProductRect(geometry.hoverViewportRect, context)
23
+ : null;
24
+ return (_jsxs("div", { style: { pointerEvents: 'none' }, "data-testid": "canvas-overlay-root", children: [hover && (_jsx("div", { "data-testid": "canvas-hover-box", className: SELECTION_CLASS.hoverBox, style: rectStyle(hover) })), selected && (_jsx("div", { "data-testid": "canvas-selected-box", className: SELECTION_CLASS.selectBox, style: rectStyle(selected), children: selectedLabel ? (_jsxs("div", { "data-testid": "canvas-selected-label", className: SELECTION_CLASS.selectLabel, style: { top: selected.y > 22 ? '-20px' : '0px' }, children: [selectedLabel, _jsxs("span", { "data-testid": "canvas-selected-size", className: SELECTION_CLASS.selectSize, children: [Math.round(selected.width), " \u00D7 ", Math.round(selected.height)] })] })) : null }))] }));
25
+ }
@@ -0,0 +1,30 @@
1
+ import type { ElementCapabilities, ViewportRect } from '@ptengine/inline-core/protocol';
2
+ import { type ToolbarHostCapabilities } from './toolbarButtons.js';
3
+ export interface ToolbarPosition {
4
+ left: number;
5
+ top: number;
6
+ }
7
+ /**
8
+ * 三段式垂直定位 + 水平钳制。抽成纯函数以便单测(组件里内联的话只能靠快照测样式)。
9
+ *
10
+ * @param rect 选中元素在**产品页**坐标系的矩形(已换算过)
11
+ * @param area 画布可视区在产品页坐标系的矩形 —— 钳制边界
12
+ * @param width 工具条实际宽度
13
+ */
14
+ export declare function computeToolbarPosition(rect: ViewportRect, area: ViewportRect, width: number): ToolbarPosition;
15
+ export interface SelectionToolbarProps {
16
+ /** 选中元素在产品页坐标系的矩形;`null` = 无选中,不渲染。 */
17
+ selectedRect: ViewportRect | null;
18
+ /** 画布可视区(产品页坐标系),用于钳制。 */
19
+ area: ViewportRect;
20
+ /** `null` = 能力位还没拿到,先不渲染按钮(避免闪一批错误按钮)。 */
21
+ capabilities: ElementCapabilities | null;
22
+ /** DEC-15:滚动期间不渲染。 */
23
+ isScrolling: boolean;
24
+ onAction: (buttonId: string) => void;
25
+ /** 量到真实宽度之前用的估算宽度;渲染后一律按实测宽度定位。 */
26
+ width?: number;
27
+ /** 宿主注入的能力,决定依赖宿主的按钮出不出现(见 ToolbarHostCapabilities)。 */
28
+ host?: ToolbarHostCapabilities;
29
+ }
30
+ export declare function SelectionToolbar({ host, selectedRect, area, capabilities, isScrolling, onAction, width }: SelectionToolbarProps): import("react").JSX.Element | null;
@@ -0,0 +1,82 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
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';
6
+ import { SELECTION_CLASS } from '../../canvas-ui/index.js';
7
+ import { visibleButtons } from './toolbarButtons.js';
8
+ const TOOLBAR_HEIGHT = 34;
9
+ const GAP = 6;
10
+ const TOOLBAR_ICONS = {
11
+ image: ImageIcon,
12
+ video: Video,
13
+ plus: Plus,
14
+ setting: Settings2,
15
+ click: MousePointerClick,
16
+ move: Move,
17
+ copy: Copy,
18
+ paste: ClipboardPaste,
19
+ text: Pencil,
20
+ style: Paintbrush,
21
+ code: Code,
22
+ camera: Camera,
23
+ delete: Trash2,
24
+ sparkles: Sparkles
25
+ };
26
+ const ICON_SIZE = 15;
27
+ const ICON_STROKE_WIDTH = 1.75;
28
+ const TOOLTIP_DELAY = 200;
29
+ export function computeToolbarPosition(rect, area, width) {
30
+ const spaceAbove = rect.y - area.y;
31
+ const spaceBelow = area.y + area.height - (rect.y + rect.height);
32
+ let top;
33
+ if (spaceAbove >= TOOLBAR_HEIGHT + GAP) {
34
+ top = rect.y - TOOLBAR_HEIGHT - GAP;
35
+ }
36
+ else if (spaceBelow >= TOOLBAR_HEIGHT + GAP) {
37
+ top = rect.y + rect.height + GAP;
38
+ }
39
+ else {
40
+ top = rect.y + GAP;
41
+ }
42
+ let left = rect.x + rect.width - width;
43
+ const minLeft = area.x;
44
+ const maxLeft = area.x + area.width - width;
45
+ if (left > maxLeft)
46
+ left = maxLeft;
47
+ if (left < minLeft)
48
+ left = minLeft;
49
+ return { left, top };
50
+ }
51
+ export function SelectionToolbar({ host = {}, selectedRect, area, capabilities, isScrolling, onAction, width = 240 }) {
52
+ const { t } = useTranslation();
53
+ const barRef = useRef(null);
54
+ const [measuredWidth, setMeasuredWidth] = useState(null);
55
+ useLayoutEffect(() => {
56
+ const measured = barRef.current?.offsetWidth ?? 0;
57
+ if (measured > 0 && measured !== measuredWidth)
58
+ setMeasuredWidth(measured);
59
+ });
60
+ if (!selectedRect || !capabilities || isScrolling)
61
+ return null;
62
+ const buttons = visibleButtons(capabilities, host);
63
+ if (buttons.length === 0)
64
+ return null;
65
+ const { left, top } = computeToolbarPosition(selectedRect, area, measuredWidth ?? width);
66
+ return (_jsx("div", { ref: barRef, "data-testid": "canvas-selection-toolbar", className: SELECTION_CLASS.toolbar, style: {
67
+ position: 'fixed',
68
+ left: `${left}px`,
69
+ top: `${top}px`,
70
+ transform: 'none',
71
+ zIndex: 2147483001
72
+ }, children: _jsx(TooltipProvider, { delayDuration: TOOLTIP_DELAY, children: buttons.map(button => {
73
+ const label = t(button.labelKey);
74
+ const Icon = TOOLBAR_ICONS[button.icon];
75
+ const isAi = button.id === 'ask-ai';
76
+ const className = `${SELECTION_CLASS.tool}${button.id === 'delete' ? ` ${SELECTION_CLASS.toolDanger}` : ''}${isAi ? ` ${SELECTION_CLASS.toolAi}` : ''}`;
77
+ if (isAi) {
78
+ return (_jsxs("button", { type: "button", "data-testid": `canvas-toolbar-${button.id}`, className: className, onClick: () => onAction(button.id), children: ["\u2726 ", label] }, button.id));
79
+ }
80
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-testid": `canvas-toolbar-${button.id}`, "aria-label": label, className: className, onClick: () => onAction(button.id), children: _jsx(Icon, { size: ICON_SIZE, strokeWidth: ICON_STROKE_WIDTH, "aria-hidden": true }) }) }), _jsx(TooltipContent, { className: "z-[2147483003]", children: label })] }, button.id));
81
+ }) }) }));
82
+ }
@@ -0,0 +1,27 @@
1
+ import type { AncestorNode } from '@ptengine/inline-core/protocol';
2
+ /**
3
+ * 路径选择器(DEC-23)—— **常驻、放大、浮在画布 iframe 之上**的主控件。
4
+ *
5
+ * 三条要求逐条对应:
6
+ * | 要求 | 实现 |
7
+ * |---|---|
8
+ * | 常驻放大,不折叠 | 不做 hover 展开(现役默认 `max-width: 0`、hover 300ms 才展开、离开 1500ms 收起) |
9
+ * | 点节点反选元素 | `onSelect(node.selector)` → 产品页下发 `setSelection` |
10
+ * | 悬浮节点高亮对应元素 | `onHover(node.selector)` → 下发 `setHoverTarget`;**移出整条时 `onHoverEnd()`** |
11
+ *
12
+ * **为什么它是主控件而不是辅助显示**:跨域画布里点击命中的总是最深的可视元素
13
+ * (`elementFromPoint` 语义),想选中包裹它的容器**只能靠路径**。现役把它折叠起来是
14
+ * 因为它只是辅助显示;在我们这里它承担导航职责,必须常驻。
15
+ *
16
+ * **不截断**:现役只渲染 4 层祖先、`...` 是静态 `<i>` 无 click handler,超 4 层完全
17
+ * 够不着。链过长时**横向滚动**,不截断 —— 截断就回到「够不着」那个问题。
18
+ */
19
+ export interface SelectorPathBarProps {
20
+ /** `[自己, 父, …, body]`,index 0 是自己。 */
21
+ chain: AncestorNode[];
22
+ onSelect: (selector: string) => void;
23
+ onHover: (selector: string) => void;
24
+ /** ⚠️ 鼠标移出整条时必须调 —— 否则画布里的高亮会**残留**。 */
25
+ onHoverEnd: () => void;
26
+ }
27
+ export declare function SelectorPathBar({ chain, onSelect, onHover, onHoverEnd }: SelectorPathBarProps): import("react").JSX.Element | null;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { tagLabelKey } from './tagLabels.js';
5
+ export function SelectorPathBar({ chain, onSelect, onHover, onHoverEnd }) {
6
+ const { t } = useTranslation();
7
+ const scrollRef = useRef(null);
8
+ const deepest = chain[0]?.selector;
9
+ useEffect(() => {
10
+ const el = scrollRef.current;
11
+ if (el)
12
+ el.scrollLeft = el.scrollWidth;
13
+ }, [deepest]);
14
+ if (chain.length === 0)
15
+ return null;
16
+ const display = chain.slice().reverse();
17
+ return (_jsx("nav", { ref: scrollRef, "data-testid": "canvas-selector-path", "aria-label": t('inlineCanvas.selectorPath.label', '元素路径'), onMouseLeave: onHoverEnd, style: {
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ gap: '2px',
21
+ overflowX: 'auto',
22
+ whiteSpace: 'nowrap',
23
+ padding: '6px 10px',
24
+ fontSize: '13px',
25
+ lineHeight: '20px',
26
+ background: 'rgba(9, 30, 66, 0.86)',
27
+ borderRadius: '4px'
28
+ }, children: display.map((node, index) => {
29
+ const key = tagLabelKey(node.tag);
30
+ const label = key ? `${t(key)} <${node.tag}>` : `<${node.tag}>`;
31
+ const isLast = index === display.length - 1;
32
+ return (_jsxs("span", { style: { display: 'inline-flex' }, children: [index > 0 && (_jsx("span", { "aria-hidden": true, style: { color: 'rgba(255,255,255,0.45)', padding: '0 4px' }, children: "\u203A" })), _jsxs("button", { type: "button", disabled: node.isBody, onClick: () => {
33
+ if (node.isBody)
34
+ return;
35
+ onSelect(node.selector);
36
+ }, onMouseEnter: () => {
37
+ if (node.isBody)
38
+ return;
39
+ onHover(node.selector);
40
+ }, style: {
41
+ border: 'none',
42
+ background: isLast ? '#00a92e' : 'transparent',
43
+ color: node.isBody ? 'rgba(255,255,255,0.4)' : '#fff',
44
+ cursor: node.isBody ? 'default' : 'pointer',
45
+ padding: '2px 8px',
46
+ borderRadius: '3px',
47
+ fontSize: '13px',
48
+ lineHeight: '20px',
49
+ maxWidth: '220px',
50
+ overflow: 'hidden',
51
+ textOverflow: 'ellipsis',
52
+ whiteSpace: 'nowrap'
53
+ }, title: node.selector, children: [label, node.elementId ? `#${node.elementId}` : ''] })] }, `${node.selector}:${index}`));
54
+ }) }));
55
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `getDomTree({rootSelector})` 的返回 → AI 上下文里的**可编辑后代货架**。
3
+ *
4
+ * 为什么用 DOM 树而不是解析 outerHTML:树里的 `selector` 是**内核生成的、可直接回传给
5
+ * 画布命令**的那一份(带 nth-of-type 消歧)。自己从 HTML 里拼 selector 就是在复制内核的
6
+ * 逻辑,而两份逻辑一旦漂移,selector 会指到另一个元素上且不报错。
7
+ */
8
+ import type { DomTreeNode } from '@ptengine/inline-core/protocol';
9
+ import '../linkHref.js';
10
+ import { type AiDescendant } from './elementAiContext.js';
11
+ /**
12
+ * 广度优先摊平子树,**跳过根**(根就是选中元素本身,它已经在上下文里了)。
13
+ *
14
+ * 广度优先而不是深度优先:预算有限时,浅层的元素(区块里的标题、按钮)比深层的
15
+ * 内联包裹更可能是用户想改的东西。
16
+ *
17
+ * **不按层数截**:树有多深走多深(取树那一步用的是扩展的硬上限 + `maxNodes` 总量闸,见
18
+ * `SHELF_TREE_MAX_DEPTH`)。此前区块货架按 `maxDepth: 4` 取树,而 PageFly 的标题在第 5 层
19
+ * (Section > div > Row > .pf-c > Column > h1)—— 出方案时 B2 的货架全是容器,模型只能锚到
20
+ * Column。预算改由「值不值得出行」控制:包装层(`isWrapper`)与 `<br>` 这类噪音不出行,
21
+ * 但照常遍历、照常记祖先链。
22
+ */
23
+ export declare function buildDescendantShelf(root: DomTreeNode, limit?: number): AiDescendant[];
24
+ /**
25
+ * 同一次遍历,**顺带把每一项的祖先链记下来**(由近及远,含根自己)。
26
+ *
27
+ * 为什么要这份表:整页作用域(`onPageContextReady` 那条路)里,作用域的「根」是 `body`,
28
+ * 而 `MOVE_ELEMENT` 的 target 必须是**被移动那个元素**的祖先 —— 不是 body 的祖先
29
+ * (body 没有)。拿 body 的祖先链去判,整页模式下所有移动都会被判成非法。
30
+ *
31
+ * ⚠️ **必须与货架同一次遍历产出**。另写一个函数再走一遍树的话,两边的截断口径
32
+ * (`limit`、BFS 顺序)一旦漂移,就会出现「货架里有这一项、祖先表里没有」——
33
+ * 表现是同一个 selector 有时能移、有时不能,且没有任何报错。
34
+ *
35
+ * 祖先链只覆盖**货架里的项**:作用域本来就只认货架,不在货架里的 selector 走不到这一步。
36
+ */
37
+ export declare function buildDescendantShelfWithPaths(root: DomTreeNode, limit?: number): {
38
+ shelf: AiDescendant[];
39
+ ancestorsOf: Map<string, string[]>;
40
+ };
@@ -0,0 +1,50 @@
1
+ import '../linkHref.js';
2
+ import { AI_DESCENDANT_LIMIT, AI_DESCENDANT_TEXT_CHARS } from './elementAiContext.js';
3
+ const NOISE_TAGS = new Set(['br', 'wbr', 'hr', 'script', 'style', 'noscript', 'template', 'link', 'meta']);
4
+ const INTERACTIVE_TAGS = new Set(['a', 'button', 'input', 'select', 'textarea', 'label', 'summary', 'details', 'form']);
5
+ function hasChildrenOf(node) {
6
+ return node.childCount > 0 || node.children.length > 0;
7
+ }
8
+ function isWrapper(node) {
9
+ if (node.children.length !== 1 || node.childCount > 1)
10
+ return false;
11
+ if (node.image)
12
+ return false;
13
+ if (node.href)
14
+ return false;
15
+ if (INTERACTIVE_TAGS.has(node.tag.toLowerCase()))
16
+ return false;
17
+ const child = node.children[0];
18
+ return node.textSummary.trim() === child.textSummary.trim();
19
+ }
20
+ export function buildDescendantShelf(root, limit = AI_DESCENDANT_LIMIT) {
21
+ return buildDescendantShelfWithPaths(root, limit).shelf;
22
+ }
23
+ export function buildDescendantShelfWithPaths(root, limit = AI_DESCENDANT_LIMIT) {
24
+ const shelf = [];
25
+ const ancestorsOf = new Map();
26
+ const queue = root.children.map(node => ({ node, ancestors: [root.selector], depth: 0 }));
27
+ while (queue.length && shelf.length < limit) {
28
+ const { node, ancestors, depth } = queue.shift();
29
+ if (NOISE_TAGS.has(node.tag.toLowerCase()))
30
+ continue;
31
+ const childAncestors = [node.selector, ...ancestors];
32
+ if (isWrapper(node)) {
33
+ queue.push({ node: node.children[0], ancestors: childAncestors, depth });
34
+ continue;
35
+ }
36
+ const text = node.textSummary.trim().slice(0, AI_DESCENDANT_TEXT_CHARS);
37
+ shelf.push({
38
+ selector: node.selector,
39
+ tag: node.tag,
40
+ text,
41
+ hasChildren: hasChildrenOf(node),
42
+ ...(node.image ? { image: node.image } : {}),
43
+ ...(node.href ? { href: node.href } : {}),
44
+ ...(depth > 0 ? { depth } : {})
45
+ });
46
+ ancestorsOf.set(node.selector, ancestors);
47
+ queue.push(...node.children.map(child => ({ node: child, ancestors: childAncestors, depth: depth + 1 })));
48
+ }
49
+ return { shelf, ancestorsOf };
50
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 整页(以 body 为根)的 AI 货架:**区块优先**。
3
+ *
4
+ * 朴素 BFS(`buildDescendantShelfWithPaths`)在真实站点上的病:header 25 个菜单项、几个
5
+ * 隐藏抽屉各带几十个子节点、footer 30 条链接——60 个槽位在到达 `main > div > div > section`
6
+ * 之前就被吃光,AI 手上的货架里根本没有要改的那个区块,只能回「请你点选」。
7
+ *
8
+ * 这里改成按**几何**挑区块:新 agent 的树带 `rect`(文档坐标)/ `hidden` / `heading`
9
+ * (`getDomTree` 的 `includeRect` / `visibleOnly`),于是能先找出「够高、够宽」的可见区块,
10
+ * 每块给 1 行容器 + 最多几行可改点,header/nav/footer 各封顶,隐藏节点一律不给。
11
+ *
12
+ * **兼容**:树上一个 `rect` 都没有 = 老 agent,整个函数退化成老算法,逐项一致。
13
+ *
14
+ * ⚠️ `ancestorsOf` 与货架**由同一个 `emit` 产出**:货架里每一项都在表里、表里也只有货架
15
+ * 里的项。MOVE 闸靠这张表判 target 是不是被移元素的祖先,两边一旦漂移就是「同一个
16
+ * selector 有时能移有时不能,且不报错」(见 buildDescendantShelf 头部说明)。
17
+ */
18
+ import type { DomTreeNode } from '@ptengine/inline-core/protocol';
19
+ import { type AiDescendant } from './elementAiContext.js';
20
+ export declare function buildRootShelf(root: DomTreeNode, opts: {
21
+ limit: number;
22
+ viewportWidth?: number;
23
+ }): {
24
+ shelf: AiDescendant[];
25
+ ancestorsOf: Map<string, string[]>;
26
+ };
@@ -0,0 +1,149 @@
1
+ import { buildDescendantShelfWithPaths } from './buildDescendantShelf.js';
2
+ import { AI_DESCENDANT_TEXT_CHARS } from './elementAiContext.js';
3
+ const LANDMARK_MIN_HEIGHT = 120;
4
+ const LANDMARK_MIN_WIDTH_RATIO = 0.5;
5
+ const DEFAULT_VIEWPORT_WIDTH = 375;
6
+ const LEAVES_PER_LANDMARK = 6;
7
+ const CHROME_ROWS_CAP = 5;
8
+ const PREFERRED_LANDMARK_TAGS = new Set(['main', 'section', 'article']);
9
+ const CHROME_TAGS = new Set(['header', 'nav', 'footer']);
10
+ const TEXT_LEAF_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'button', 'span']);
11
+ export function buildRootShelf(root, opts) {
12
+ const { limit } = opts;
13
+ const viewportWidth = typeof opts.viewportWidth === 'number' && opts.viewportWidth > 0
14
+ ? opts.viewportWidth
15
+ : DEFAULT_VIEWPORT_WIDTH;
16
+ const minWidth = viewportWidth * LANDMARK_MIN_WIDTH_RATIO;
17
+ const entries = [];
18
+ const entryOf = new Map();
19
+ let sawRect = root.rect !== undefined;
20
+ const isLandmarkNode = (n) => n.rect !== undefined && n.rect.height >= LANDMARK_MIN_HEIGHT && n.rect.width >= minWidth;
21
+ const visit = (n, ancestors, chromeKey, nearestLandmark) => {
22
+ for (const child of n.children) {
23
+ if (child.hidden)
24
+ continue;
25
+ if (child.rect)
26
+ sawRect = true;
27
+ const ownChrome = CHROME_TAGS.has(child.tag) ? child.selector : chromeKey;
28
+ const entry = {
29
+ node: child,
30
+ ancestors,
31
+ order: entries.length,
32
+ chromeKey: ownChrome,
33
+ nearestLandmark,
34
+ isLandmark: isLandmarkNode(child)
35
+ };
36
+ entries.push(entry);
37
+ entryOf.set(child, entry);
38
+ visit(child, [child.selector, ...ancestors], ownChrome, entry.isLandmark ? entry : nearestLandmark);
39
+ }
40
+ };
41
+ visit(root, [root.selector], null, null);
42
+ if (!sawRect)
43
+ return buildDescendantShelfWithPaths(root, limit);
44
+ const beaten = new Set();
45
+ const absorbed = new Set();
46
+ for (const e of entries) {
47
+ if (!e.isLandmark || !e.nearestLandmark)
48
+ continue;
49
+ const outer = e.nearestLandmark;
50
+ const wins = !!e.node.heading ||
51
+ (PREFERRED_LANDMARK_TAGS.has(e.node.tag) && !PREFERRED_LANDMARK_TAGS.has(outer.node.tag));
52
+ if (wins)
53
+ beaten.add(outer);
54
+ else
55
+ absorbed.add(e);
56
+ }
57
+ const surviving = new Set();
58
+ for (const e of entries) {
59
+ if (!e.isLandmark || beaten.has(e))
60
+ continue;
61
+ if (absorbed.has(e)) {
62
+ let a = e.nearestLandmark;
63
+ let hasSurvivingAncestor = false;
64
+ while (a) {
65
+ if (surviving.has(a)) {
66
+ hasSurvivingAncestor = true;
67
+ break;
68
+ }
69
+ a = a.nearestLandmark;
70
+ }
71
+ if (hasSurvivingAncestor)
72
+ continue;
73
+ }
74
+ surviving.add(e);
75
+ }
76
+ const landmarks = [...surviving].sort((a, b) => {
77
+ const dt = a.node.rect.top - b.node.rect.top;
78
+ return dt !== 0 ? dt : a.order - b.order;
79
+ });
80
+ const shelf = [];
81
+ const ancestorsOf = new Map();
82
+ const emittedSelectors = new Set();
83
+ const chromeRows = new Map();
84
+ const emit = (e) => {
85
+ if (shelf.length >= limit)
86
+ return false;
87
+ if (emittedSelectors.has(e.node.selector))
88
+ return false;
89
+ if (e.chromeKey !== null && (chromeRows.get(e.chromeKey) ?? 0) >= CHROME_ROWS_CAP)
90
+ return false;
91
+ const n = e.node;
92
+ const hasChildren = n.childCount > 0 || n.children.length > 0;
93
+ const row = {
94
+ selector: n.selector,
95
+ tag: n.tag,
96
+ text: n.textSummary.trim().slice(0, AI_DESCENDANT_TEXT_CHARS),
97
+ hasChildren
98
+ };
99
+ if (hasChildren && n.heading)
100
+ row.heading = n.heading;
101
+ if (n.rect)
102
+ row.top = n.rect.top;
103
+ if (n.image)
104
+ row.image = n.image;
105
+ shelf.push(row);
106
+ ancestorsOf.set(n.selector, e.ancestors);
107
+ emittedSelectors.add(n.selector);
108
+ if (e.chromeKey !== null)
109
+ chromeRows.set(e.chromeKey, (chromeRows.get(e.chromeKey) ?? 0) + 1);
110
+ return true;
111
+ };
112
+ const isTextLeaf = (n) => n.tag === 'img' || (TEXT_LEAF_TAGS.has(n.tag) && n.textSummary.trim().length > 0);
113
+ for (const lm of landmarks) {
114
+ if (shelf.length >= limit)
115
+ break;
116
+ emit(lm);
117
+ let taken = 0;
118
+ const collect = (n) => {
119
+ for (const child of n.children) {
120
+ if (taken >= LEAVES_PER_LANDMARK || shelf.length >= limit)
121
+ return;
122
+ if (child.hidden)
123
+ continue;
124
+ const ce = entryOf.get(child);
125
+ if (!ce)
126
+ continue;
127
+ if (surviving.has(ce) && ce !== lm)
128
+ continue;
129
+ if (isTextLeaf(child) && emit(ce))
130
+ taken += 1;
131
+ collect(child);
132
+ }
133
+ };
134
+ collect(lm.node);
135
+ }
136
+ if (shelf.length < limit) {
137
+ const queue = [...root.children];
138
+ while (queue.length && shelf.length < limit) {
139
+ const n = queue.shift();
140
+ if (n.hidden)
141
+ continue;
142
+ const e = entryOf.get(n);
143
+ if (e)
144
+ emit(e);
145
+ queue.push(...n.children);
146
+ }
147
+ }
148
+ return { shelf, ancestorsOf };
149
+ }