@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +286 -2
  3. package/dist/InlineEditor.d.ts +342 -0
  4. package/dist/InlineEditor.js +867 -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 +326 -0
  58. package/dist/kernel/ai/aiInlineApply.js +461 -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 +185 -0
  104. package/dist/kernel/changelist/useChangeList.js +259 -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 +313 -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 +19 -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 +158 -0
  168. package/dist/useInlineEditor.js +343 -0
  169. package/package.json +58 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ 首个发布版。自 frontend-base(`cecb6de77`)迁入 `inline-editor`,其内部依赖的 `canvas-ui` 并入本包的内部目录(不单独发布)。
6
+
7
+ **产物**
8
+ - 仅 ESM,**逐模块输出**:每个源文件各出一个 `.js` 与 `.d.ts`,目录结构与源码一致,入口 `dist/index.js` / `dist/index.d.ts`;
9
+ 相对导入均带 `.js` 扩展名(Node ESM 可解析);无 sourcemap。消费方打包器能像吃源码时一样按模块摇树、分 chunk
10
+ (单文件产物会被主应用对 `inlineEditorLocales` 等的静态引用整个拉进主 chunk)。
11
+ - `exports` 只开放 `.` 与 `./package.json`,不开放深层子路径。
12
+ - 样式随 canvas-ui 模块带出:`dist/canvas-ui/index.js` 里 `import './selection/tokens.css'`、`import './tree/tree.css'`,
13
+ 两份 CSS 原样位于 `dist/canvas-ui/` 下。`sideEffects: ["*.css", "./dist/canvas-ui/index.js"]` —— 后一项不能少:
14
+ 该文件是纯再导出的桶文件,不列进去打包器会判它无副作用、连同 CSS import 一起摇掉。不做运行时 `<style>` 注入。
15
+ - 外部依赖(不打进包):`react` / `react-dom` / `react-i18next` / `@ptengine/design-components`(peer)、
16
+ `lucide-react` / `@ptengine/inline-core`(dependencies)。
17
+
18
+ **配置入口(不兼容变更,相对迁入前的源码级用法)**
19
+ - 新增 `configureInlineEditor({ extensionId, disabledCapabilities })`,以及 `getInlineEditorConfig()` / `resetInlineEditorConfig()`。
20
+ 预构建产物不再读 `import.meta.env`,扩展 id 与禁用能力改由宿主在应用启动时显式传入。
21
+ 不配 `extensionId` = 内测模式(回退读 `<html data-ptengine-kit-id>`);配了 = 严格模式,只认这一个 id。
22
+ - 删除 `EXTENSION_ID` 常量导出;需要当前 id 请用 `getExtensionId()`。
23
+
24
+ **发布前校验**
25
+ - `pnpm build && pnpm test:build`:断言逐模块产物齐全、无测试文件、相对导入带扩展名且可解析、
26
+ canvas-ui 的 CSS import 与 CSS 文件在位且被 `sideEffects` 命中、外部依赖未内联、d.ts 不引 Node 类型;
27
+ 在临时「装包方」目录里以 `--skipLibCheck false` 编译类型夹具(含 `DomTreeNode.href` 模块增强),
28
+ 并用 vite 真打一次只 import `InlineEditor` 的入口,断言产出 CSS 含 canvas-ui 作用域选择器。
package/README.md CHANGED
@@ -1,3 +1,287 @@
1
- # Temporary Holding Version
1
+ # @ptengine/inline-editor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 页面内联编辑器:把客户页在产品内 iframe 里渲染出来,选中元素、可视化编辑,
4
+ 产出与 engage-editor 结构一致的 change list。
5
+
6
+ 依赖 Ptengine 浏览器插件(ptengine-kit,不在本仓)提供跨域画布能力:插件负责在客户页里
7
+ 执行画布命令、上报扩展 id,本包通过 externally_connectable 与它通信(见下文「配置扩展 ID」)。
8
+
9
+ ## 两层 API
10
+
11
+ **按宿主的外壳需求选,不要只用成品层。**
12
+
13
+ ```tsx
14
+ // ① headless —— 宿主完全自定义 UI
15
+ const editor = useInlineEditor({ url, device, onChangesChange });
16
+ editor.session // gating / opening / ready / failed
17
+ editor.selection // { selector, identifier, tag, chain, capabilities } | null
18
+ editor.geometry // 选中/hover 矩形 + 滚动态
19
+ editor.changeList // 明文 change list + addStyle/addContent/revert
20
+ editor.commands // select / hover / readOuterHtml / startTextEdit
21
+
22
+ // ② 成品部件 —— 按需拼装
23
+ <EditorChrome variant="full" ... /> // 或 "minimal"
24
+ <CanvasFrame {...editor.frameProps} />
25
+ <SelectorPathBar chain={editor.selection.chain} ... />
26
+ <SelectionOverlay geometry={editor.geometry.latest} ... />
27
+ <SelectionToolbar capabilities={...} ... />
28
+ <StylePanel ... /> <HtmlEditDialog ... />
29
+ <ChangeListPanel ... />
30
+ ```
31
+
32
+ **为什么必须有 headless 层**:编辑器在不同入口下**外壳差异极大** ——
33
+ experience 创建流程要版本管理/设备切换/保存预览的双层顶栏,从 AI 对话打开则只要
34
+ 一条极简顶栏。只提供成品组件的话,后者要么被迫塞进不需要的 UI,要么就得加一堆
35
+ `showXxx` 布尔开关 —— 那是组件复杂度失控的典型开端。
36
+
37
+ ### `<PagePreview>` —— 上面那条主张的样板
38
+
39
+ 分析阶段(AI「创建 experience」的左侧客户页)要的是一块**被动预览板**:
40
+ 顶栏 + 画布 + 编号区块框,**没有**图层面板 / 改动清单 / 选中框 / 选中工具条 /
41
+ 属性面板 / 任何弹框。
42
+
43
+ 它是用 headless 层拼出来的第二个外壳,**不是** `<InlineEditor>` 的一个模式 ——
44
+ `chrome="minimal"` 只换顶栏,上面那一串部件照样全在渲染;而给 `<InlineEditor>`
45
+ 加 `showLayers` / `showToolbar` 之类开关正是本节明令不走的那条路。
46
+
47
+ ```tsx
48
+ <PagePreview
49
+ url={page.url}
50
+ device="mobile"
51
+ viewportWidthOverride={{ mobile: 375 }} // 热图区块是按 375 量的
52
+ blocks={blocks} // resolveBlocks 的产物
53
+ activeBlockId={hoveredInChat}
54
+ onHoverBlock={setHoveredInChat}
55
+ title={hostAndPath}
56
+ subtitle={page.reason}
57
+ onClose={close}
58
+ onDeviceChange={setDevice}
59
+ onSessionReady={sid => void resolveBlocksFor(sid)}
60
+ onReady={setHandle} // capturePage / scrollToSelector / getSessionId
61
+ />
62
+ ```
63
+
64
+ ⚠️ **拾取(pick mode)在预览板里照样开着** —— 那正是这块板子「惰性」的成因:
65
+ `picker.ts` 在客户页上 `preventDefault`,用户点链接不会真的跳走。关掉它,嵌进来的
66
+ 客户页就活了:点一下就导航到别的页,而 sessionId 不变,于是区块坐标/几何/截图全部
67
+ 对着一个已经不在那儿的页面,且零报错。
68
+
69
+ ## 安装与接入
70
+
71
+ ```sh
72
+ pnpm add @ptengine/inline-editor
73
+ ```
74
+
75
+ **peer 依赖**由宿主提供(不会被打进包):`react >=18`、`react-dom >=18`、`react-i18next >=13`、
76
+ `@ptengine/design-components >=0.5.3 <1`。`lucide-react` 与 `@ptengine/inline-core` 是本包的
77
+ dependencies,随包安装。包只发 ESM,逐模块输出(每个源文件一个 `.js` + `.d.ts`,入口 `dist/index.js`),
78
+ 不带 sourcemap;宿主打包器会按模块摇树、分 chunk,编辑器代码可以留在宿主的懒加载 chunk 里。
79
+ 只从包根 `@ptengine/inline-editor` 导入,不开放深层子路径。
80
+
81
+ **样式随 canvas-ui 模块自动带出**:`dist/canvas-ui/index.js` 里 `import './selection/tokens.css'` 与
82
+ `import './tree/tree.css'`,宿主只要用到编辑器(`InlineEditor` / `PagePreview` 等),画布选中框 / 图层树等样式就一并加载,
83
+ 不需要(也不应)单独引 CSS;没有运行时 `<style>` 注入,CSS 由宿主打包器正常抽取(会跟着编辑器进懒加载 chunk)。
84
+ 前提是宿主的打包器遵守 `package.json` 的 `sideEffects: ["*.css", "./dist/canvas-ui/index.js"]`(Vite / webpack 默认都遵守):
85
+ `canvas-ui/index.js` 是纯再导出的桶文件,CSS import 是它唯一的副作用,所以要显式列出,否则会被连同 CSS 一起摇掉。
86
+ 这份 CSS 全部挂在 `.inline-editor-canvas` 作用域下,不会漏进宿主的全局样式。
87
+
88
+ **vitest**:`dist/canvas-ui/index.js` import 了 `.css`,Node 原生 ESM 不认这个扩展名,所以宿主的 vitest
89
+ 要把本包内联交给 Vite 处理,否则凡是引到本包的测试文件都会报 `Unknown file extension ".css"`:
90
+
91
+ ```ts
92
+ // vitest.config.ts
93
+ test: { server: { deps: { inline: ['@ptengine/inline-editor'] } } }
94
+ ```
95
+
96
+ **Tailwind**:包里的组件用了 Tailwind 工具类,宿主的 `content` 必须覆盖产物,否则这些类不会生成:
97
+
98
+ ```js
99
+ // tailwind.config.js
100
+ content: [
101
+ './src/**/*.{ts,tsx}',
102
+ './node_modules/@ptengine/inline-editor/dist/**/*.js'
103
+ ]
104
+ ```
105
+
106
+ **启动时配置**:应用入口处调用一次 `configureInlineEditor`(扩展 id、禁用能力),见下面
107
+ 「宿主接入」第 2 步。i18n 合并见第 1 步。
108
+
109
+ ## 宿主接入
110
+
111
+ **1. 合并 i18n**(必须做,否则满屏 raw key 且不会报错):
112
+
113
+ ```ts
114
+ import { inlineEditorLocales } from '@ptengine/inline-editor';
115
+ // 把 inlineEditorLocales['zh-CN'] 等 merge 进宿主自己的 i18n 资源
116
+ ```
117
+
118
+ **2. 配置扩展 ID**:本包是预构建 ESM,**不再读 `import.meta.env`**,扩展 id 与禁用能力
119
+ 由宿主在应用启动时显式传入。Vite 宿主在入口处调用一次:
120
+
121
+ ```ts
122
+ import { configureInlineEditor } from '@ptengine/inline-editor';
123
+
124
+ configureInlineEditor({
125
+ extensionId: import.meta.env.VITE_PTENGINE_KIT_EXTENSION_ID,
126
+ disabledCapabilities: import.meta.env.VITE_CANVAS_DISABLED_CAPABILITIES
127
+ });
128
+ ```
129
+
130
+ - `extensionId` 非空 = 严格模式,只认这一个 id(正式上线填商店 id)。
131
+ - 不配(或空串、`undefined`)= 内测模式:回退读插件在 `<html data-ptengine-kit-id>`
132
+ 上报的 id。读不到就是没装插件 / 插件没在本页生效,以 `NO_EXTENSION` 失败并走安装引导
133
+ (不配 id 是合法配置,不会出 `EXTENSION_ID_MISSING`)。
134
+ - `disabledCapabilities` 接受逗号分隔字符串或字符串数组(trim、去空),列出的能力即使
135
+ 插件支持也不启用;CORE 能力不能被关掉。
136
+ - 多次调用是浅合并;`getInlineEditorConfig()` 读当前配置,`resetInlineEditorConfig()`
137
+ 清空(仅测试用)。
138
+ - ⚠️ 旧的 `EXTENSION_ID` 常量导出已删除,需要当前 id 请用 `getExtensionId()`。
139
+
140
+ ## 目录
141
+
142
+ | | |
143
+ |---|---|
144
+ | `kernel/` | 会话编排 / 画布 iframe / 几何通道 / 选中层 / change list 生成 |
145
+ | `tree/` | 图层树:DOM 树 → 面板形状的适配器 + 面板外壳 |
146
+ | `assets/` | 组件库:库组件 → 插入 HTML + 资产,面板外壳 |
147
+ | `panels/` `dialogs/` `chrome/` | 属性面板、编辑弹框、两档外壳 |
148
+ | `locales.ts` | 自带三语文案 |
149
+
150
+ `kernel/` 里的东西原先放在产品前端的某个 view 目录下 —— 那是错的位置
151
+ (它不是一个 view,是基础设施),已随本包成型搬到这里。
152
+
153
+ ## 与 v2 编辑器的一致性
154
+
155
+ 图层树的**纯逻辑与 class 名**、选中框/hover 框/工具条的**观感**,都来自本包内部目录
156
+ `canvas-ui/`(不单独发布,随本包一起打进 `dist`)—— 与落地页 v2 编辑器同一套。
157
+ **不要在本包里另写 class 名或改颜色**,要改就改 `canvas-ui/`,别处不要再另起一份。
158
+
159
+ ⚠️ 目前 v2 仍保留它自己那份实现(本期不动线上),所以同一套语义**暂时有两份**。
160
+ 改 `canvas-ui/` 的 `tree/order.ts` 或 `selection/tokens.css` 时,同步确认 v2 侧是否要跟。
161
+
162
+ **本期有意不做的**:缩放手柄(客户页尺寸多由 flex/grid 决定,`elementStyle` 写死 px 会在
163
+ 其它视口坏掉;画出手柄却拖不动比没有更让人困惑)、同屏多设备(每个设备是独立跨域 iframe,
164
+ 而 UA 覆盖靠 DNR,DNR 的规则条件里**没有帧级维度**)。
165
+
166
+ ## 多版本(AI 创建 experience 那条路)
167
+
168
+ **宿主持有数据,包只负责切换机制。** 包里不存多版本状态:它只知道「当前清单」这一份。
169
+
170
+ ```tsx
171
+ <InlineEditor
172
+ versions={[{ id: 'v-a', name: '原始版本' }, { id: 'v-b', name: '版本 B' }]}
173
+ activeVersionId={active}
174
+ versionChanges={changesByVersion} // 宿主持有的明文 change list
175
+ onVersionChange={setActive} // 用户选了 / AI 那批指名了另一版
176
+ onVersionLeave={(id, records) => save(id, records)} // ⚠️ 不接 = 那一版的改动丢了
177
+ onVersionEnter={(id, idMap) => remapCardIds(id, idMap)}
178
+ resolveChangeId={id => aliasTable.get(id) ?? id}
179
+ />
180
+ ```
181
+
182
+ - **不传 `versions` 时行为与今天完全一致**(顶栏仍是 `versionLabel` 那个静态标签,
183
+ 上面这组回调一个都不会触发)。旧调用方不用改。
184
+ - 切换 = `changeList.replaceAll()`:撤光当前的、回放目标版本的。**回放会铸新
185
+ `changeId`**(撤销闭包活在 agent 侧、随 iframe 重建而丢)—— 所以 `onVersionEnter`
186
+ 会交出一张 `idMap`(旧 id → 新 id)。**不拿它维护 `resolveChangeId` 的话**,
187
+ 切走再切回来之后,AI 卡片上的「撤销这一批」会一条都找不到,而且不报错。
188
+ - 首次挂载时 `versionChanges[activeVersionId]` **压过** `initialChanges`,只回放一次
189
+ —— 两边都放的话页面上会出现两遍同样的改动。
190
+ - `AiApplyApi.apply(changes, { versionId })` 指名了另一版时会**先切过去再应用**
191
+ (与切换共用一条串行队列);切换失败则整批 `VERSION_SWITCH_FAILED`,停在当前版本。
192
+ - **切换进行中,编辑器里一切手工写入都被拒**(工具条收起、属性面板关掉、组件插入禁用,
193
+ 清单撤销与切换前就打开的弹框点了会提示「正在切换版本」)。这不是体验取舍:
194
+ 回放那一步有「清单非空就不放」的守卫,切换途中落进一条手改就会让**整个目标版本回放不了**,
195
+ 接着被 `onVersionLeave` 覆盖掉 —— 那一版就此消失,而且不报任何错。
196
+ - 首次回放与切换走**同一条串行队列**,回放也会交出 `onVersionEnter(id, idMap)`
197
+ —— 换设备/重载会重铸全部 changeId,没有这张表,宿主手上卡片的「撤销这一批」全失效。
198
+ - 用户选的版本切换失败时,包会**回推** `onVersionChange(画布上真正装着的那一版)`,
199
+ 宿主据此把顶栏改回去;不回推的话两边永久分叉,而且连重试都不会发生。
200
+ - `revert()` 返回 `{ ok:false, reason:'VERSION_NOT_ACTIVE' }` 表示这一批属于另一个版本
201
+ (包判,宿主判不了)——宿主据此把按钮禁掉,别硬撤:那是静默 no-op。
202
+
203
+ ### `onEditorReady`:宿主拿得到的三件事
204
+
205
+ ```tsx
206
+ <InlineEditor onEditorReady={h => store.setEditorHandle(h)} />
207
+ // h.clearSelection() 清空选中 → AI 的作用域退回整页档(首版生成前必须做)
208
+ // h.peekList() 同步读当前改动清单(不是渲染期 state,不会漏刚写的那条)
209
+ // h.hasUnsavedChanges() 离开确认 / 保存按钮禁用态
210
+ ```
211
+
212
+ **每个画布会话交一次**(切设备 / 重载会再交一次,旧把手发的命令没人应答)。
213
+
214
+ ### AI apply 的两档作用域
215
+
216
+ | | 根 | 货架(授权清单) | 什么时候 |
217
+ |---|---|---|---|
218
+ | 元素 | 用户选中的那个元素 | 它的子树,30 项 | 有选中时 |
219
+ | 整页 | `body` | 整页货架,60 项(`ROOT_SHELF_LIMIT`) | **没有选中**且宿主接了 `onPageContextReady` |
220
+
221
+ 整页档是给首版生成(AI 自己挑改哪儿)的。**它不是把闸放开**:agent 仍然只能逐字抄
222
+ 我们交给它的那份货架,自己拼的 selector 照样 `OUT_OF_SCOPE`。
223
+
224
+ - **没接 `onPageContextReady` 的宿主行为不变**:没选中时 `onAiApplyReady(null)`。
225
+ - 选中一个元素 → 自动收窄成元素档;选中没了 → 退回整页档。
226
+ - 整页货架**每个会话算一次**并缓存(与交给 `onPageContextReady` 的是同一份);
227
+ 换会话(切设备 / 重载)立刻作废,重新采集之前整页档只授权 `body` 一个。
228
+ - `MOVE_ELEMENT` 的 target 仍然只能是**被移动那个元素**的祖先 —— 整页档下这条链取自
229
+ 建货架那次遍历顺带记的路径,不是 body 的祖先(body 没有)。
230
+
231
+ ## AI 改动的八个 op
232
+
233
+ `AI_SAFE_OPS` 与 mastra 的 `EDITOR_INLINE_OPS` **逐字一致**(顺序也一样)。
234
+ 其中换图、改跳转与整块重写多一道**来源闸**,依据由宿主注入:
235
+
236
+ - `allowedImageOrigins` —— `CHANGE_IMAGE` 的 `src` 允许的 origin(图片上传域等;
237
+ 客户页自己的 origin 由包自动加上)。不传 = 只允许同源的图,**这是有意的 fail closed**:
238
+ 模型凭记忆敲出来的域名会被每一个真实访客加载。
239
+ - `allowedLinkUrls` —— `SET_LINK` 允许的**站外**地址,用户在对话里逐字给过的那些
240
+ (**整串**相等,不是 origin 相等)。同源地址不用进这张表。
241
+
242
+ `EDIT_HTML`(整块重写)与 `INSERT_ELEMENT` 是**两道不同的闸**,别复用:
243
+ `insertHtmlGate` 审的是「加一个小元素」,`replaceHtmlGate` 审的是「顶替一个既有元素」——
244
+ 后者强制**单根**(多根会让其后所有 `:nth-of-type` selector 错位,且上线才暴露)、
245
+ 放行 `img`(含图的区块否则永远重写不了,`src` 走 `allowedImageOrigins`,`srcset` 剥掉)、
246
+ 预算 8KB。
247
+
248
+ ## 几条容易踩的约束
249
+
250
+ - **change list 用明文形态**(DEC-22)。change list **跟随 experience 配置一起存库**,
251
+ 包本身不碰存储 —— 宿主在 `onSave` 里拿 `editor.changeList.changes`,经
252
+ **`exportChangesForSave()`** 转成数值形态再放进配置。
253
+ ⚠️ 直接存明文不会报任何错,要等发布后 SDK 读到 `type: 'elementStyle'` 而不是 `53`
254
+ 才暴露。**任何地方都不要手搓 `{type:1,string:...}`**。
255
+ - **样式面板的门控只用能力位**(那一次批量往返拿到的),不发新的探测命令 ——
256
+ 现役 `usesStyleProperty` 往客户页插临时元素测默认值,会让同层 `nth-of-type` 偏移、
257
+ 生成的 selector 上线后指错元素。
258
+ - **length 类样式值必须补单位**:`font-size: 18` 是非法 CSS,浏览器直接忽略,
259
+ 画布上看着"没反应"而 change 已经记下了。
260
+ - **图层树的 id 就是 selector**,会原样交回 `setSelection` / `elementMove`。换成别的
261
+ (下标之类)就要维护映射表,而表一旦与树不同步,点某一行会选中另一个元素**且不报错**。
262
+ selector 撞号时给 **id** 加 `#dup2` 后缀,**绝不能污染 selector** —— 带上后缀发下去
263
+ 在客户页里什么都匹配不到。
264
+ - **树的落点档位与 `elementMove` 的 mode 是两套词表**:树是 before/after/**inside**,
265
+ mode 是 before/after/**top**/**bottom**。用 `moveModeFromDropPos()` 转,别直接传。
266
+ - **组件库插入必须同时注入 css/js**,否则是个没样式的空壳而用户以为插入失败。
267
+ 且 `setCode` 是**全量替换**语义 —— 必须累积已注入的资产、每次发完整集合,
268
+ 只发当前组件会把前面插入的组件样式全清掉;换会话时那份记账要清空。
269
+ - **带 JS 的库组件在画布里不会运行**(`setCode` 的 javascript 条目 agent 只存不执行 ——
270
+ 客户页很可能没装 SDK)。Shadow DOM 组件因此不会升级,看到的是未渲染的自定义标签。
271
+ 这是能力边界,要明确告知用户。**线上生效还需要发布链路带上组件资产**,不在本包职责内。
272
+ - **`isTextChange` 是「编辑文字」与 `</>` 的唯一区别**,两者共用 `mode: 'replace'`。
273
+ 传错会让锚点元素一侧存在一侧不存在,后续 selector 全部错位。
274
+ - **改文字是页面内原位实时编辑**(`commands.startTextEdit`),不是弹框 —— 弹框里看不到
275
+ 换行、容器撑开、撞到相邻元素这些真实排版后果。原来的 `TextEditDialog` 已删除。
276
+ - **开始原位编辑前必须 `iframe.focus()`**(产品侧做)。agent 在客户页里调 `el.focus()`
277
+ 只是在那个文档内部选中元素,浏览器的活动 frame 仍停在产品页 —— 用户敲的字会打到产品页上,
278
+ 表现是「点了编辑文字,什么都没发生,也不报错」。
279
+ - **agent 提交时会先把 innerHTML 还原成编辑前的值**,新 html 只经 `textEditCommit` 事件
280
+ 回来走正常 `applyChange`。不还原的话 `applyChange` 记的 `previousHtml` 就是编辑后的值,
281
+ 撤销会把元素「还原」成编辑后的样子 —— 静默,且看起来像撤销没生效。
282
+
283
+ ## 授权 / 公开可见
284
+
285
+ `UNLICENSED`,Ptmind 内部使用。包发布在公共 npm registry 上,**产物公开可见**:JS 是可读的
286
+ 逐模块 ESM(去掉了注释),`.d.ts` 保留 JSDoc。所以导出类型上的注释与源码里都不要写入机密信息
287
+ (密钥、内部地址、未公开的客户信息等)。
@@ -0,0 +1,342 @@
1
+ import type { CanvasDevice } from '@ptengine/inline-core/protocol';
2
+ import { type ChromeVariant } from './chrome/EditorChrome.js';
3
+ import type { PageModelBlock } from './kernel/blocks/resolveBlocks.js';
4
+ import type { AdaptDomTreeOptions } from './tree/domTreeAdapter.js';
5
+ import { type AnchorShot, type ElementAiContext } from './kernel/selection/elementAiContext.js';
6
+ import { type InlinePlanAnchor } from './kernel/selection/resolveAnchors.js';
7
+ import { type AiApplyOutcome, type AiScopeGrant, type AiWireChange } from './kernel/ai/aiInlineApply.js';
8
+ import { type LibraryComponent } from './assets/componentLibrary.js';
9
+ import { type UseInlineEditorResult } from './useInlineEditor.js';
10
+ import type { ChangeRecord, HydrateOutcome } from './kernel/changelist/useChangeList.js';
11
+ import type { InlineChange } from '@ptengine/inline-core/change-format';
12
+ /**
13
+ * 成品编辑器 —— 外壳 + 画布 + 选中层 + 编辑入口的**默认拼装**。
14
+ *
15
+ * 需要不一样的外壳时**不要改这里加开关**,直接用 `useInlineEditor()` 自己拼
16
+ * (见 README 的两层 API)。这个组件存在的意义是「大多数入口不用从零拼」,
17
+ * 不是「所有入口都必须用它」。
18
+ *
19
+ * 目标视口宽度按设备固定:客户页按 1440 设计,画布用 `zoom` 缩放去适应面板宽度,
20
+ * 而不是把 iframe 做窄 —— 做窄会触发客户页的响应式断点,看到的就不是 PC 版了。
21
+ */
22
+ export interface InlineEditorProps {
23
+ url: string;
24
+ device: CanvasDevice;
25
+ onDeviceChange: (device: CanvasDevice) => void;
26
+ title: string;
27
+ /** 单版本入口的静态版本名。传了 `versions` 时不再展示它(顶栏换成选择器)。 */
28
+ versionLabel?: string;
29
+ /**
30
+ * 可切换的版本列表。**宿主持有数据,包只负责切换机制** —— 包里不存多版本状态,
31
+ * 每次切换都是「把当前清单整体换成另一份」。
32
+ *
33
+ * 不传(或空数组)= 与今天完全一致:顶栏还是 `versionLabel` 那个静态标签,
34
+ * 下面这一组回调一个都不会触发。
35
+ */
36
+ versions?: {
37
+ id: string;
38
+ name: string;
39
+ }[];
40
+ /** 当前版本。**由宿主决定**:包切换成功后只是回调通知,不自己改这个值。 */
41
+ activeVersionId?: string;
42
+ /**
43
+ * 各版本的**明文**改动,key = versionId。宿主持有(它才知道哪些版本、存过什么)。
44
+ *
45
+ * ⚠️ 切到某个版本时包读的是这里的**当前值**,不是切换那一刻的快照 ——
46
+ * 宿主在 `onVersionLeave` 里把 live 清单写回来之后,下次切回去拿到的才是最新那份。
47
+ */
48
+ versionChanges?: Record<string, InlineChange[]>;
49
+ /**
50
+ * 版本变了(用户在顶栏选了另一个,或 AI 的这一批指名了另一个版本)。
51
+ * 宿主据此更新 `activeVersionId` —— 包不自己改,单一数据源仍在宿主手里。
52
+ */
53
+ onVersionChange?: (id: string) => void;
54
+ /**
55
+ * **离开某个版本前**把它的 live 清单交还宿主。⚠️ 不接这个回调 = 用户在这个版本上
56
+ * 做的改动会在切走时被撤光且再也拿不回来(包不留副本)。
57
+ */
58
+ onVersionLeave?: (id: string, records: ChangeRecord[]) => void;
59
+ /**
60
+ * 进入某个版本、改动回放完之后的**别名表**(旧 changeId → 本次新铸的 id)。
61
+ *
62
+ * 为什么必须有这个出口:回放每次都铸新 changeId(撤销闭包活在 agent 侧、随 iframe
63
+ * 重建而丢,见 `hydrate` 的注释),而 AI 卡片上记的是**旧** id。宿主拿这张表维护
64
+ * `resolveChangeId`,否则切走再切回来之后,卡片上的「撤销这一批」会一条都找不到。
65
+ */
66
+ onVersionEnter?: (id: string, idMap: Map<string, string>) => void;
67
+ /**
68
+ * 卡片上的旧 changeId → 当前会话里的新 id(宿主用 `onVersionEnter` 的 `idMap` 维护)。
69
+ * 不传 = 不做映射(单版本入口本来就不需要)。
70
+ */
71
+ resolveChangeId?: (id: string) => string;
72
+ /**
73
+ * `CHANGE_IMAGE` 的 origin 白名单(图片上传域等)。客户页自身的 origin 由包自动加上。
74
+ *
75
+ * 不传 = 只允许换成与客户页同源的图。**这是有意的 fail closed**:模型凭记忆敲出来的
76
+ * 域名会被每一个真实访客加载,等于我们替客户往他页面上挂了个第三方请求。
77
+ */
78
+ allowedImageOrigins?: string[];
79
+ /**
80
+ * 本会话在这个页面上读到过的图片地址(宿主从发给 agent 的 `anchor-image:` / `element-image:`
81
+ * 行与 html 里的 `<img src>` 攒)。origin 白名单之外的第二条放行路,整串相等才算 ——
82
+ * 换图、整块重写、插入模块三处共用(见 `kernel/ai/imageSource.ts`)。不传 = 这一路不放行。
83
+ */
84
+ allowedImageUrls?: string[];
85
+ /**
86
+ * `SET_LINK` 允许的**站外**地址 —— 用户在对话里逐字给过的那些(宿主从会话里攒)。
87
+ * 同源地址不需要进这张表。整串逐字相等,不是 origin 相等。
88
+ */
89
+ allowedLinkUrls?: string[];
90
+ chrome?: ChromeVariant;
91
+ onExit: () => void;
92
+ onSave?: (editor: UseInlineEditorResult) => void;
93
+ /**
94
+ * 打开**已有实验**时要回显的改动(明文形态;存库的数值 tagData 由宿主经
95
+ * `fromWireFormat` 转过来)。画布就绪后逐条回放,清单也一并填好。
96
+ *
97
+ * 不传 = 空白新建。
98
+ */
99
+ initialChanges?: InlineChange[];
100
+ /**
101
+ * 回显结果。**有失败项时宿主必须告诉用户** —— 客户页会改版,selector 不再命中是
102
+ * 常态;失败的条目仍留在清单里(不丢用户的数据),但页面上看不见它们的效果,
103
+ * 不说明的话用户会以为编辑器坏了。
104
+ */
105
+ onHydrated?: (outcome: HydrateOutcome) => void;
106
+ /**
107
+ * 「问 AI」入口。**不传则工具条上不出现这个按钮** —— 包不知道产品里有没有 AI 面板,
108
+ * 渲染出来点了没反应比没有更让人困惑。
109
+ *
110
+ * 上下文里**不含截图**。第二个参数是绑定到该元素的**延迟**截图函数,宿主在用户真的
111
+ * 发送时才调它 —— 点按钮那刻就拍的话,用户如果接着又改了这个元素,图就对不上了。
112
+ *
113
+ * `capture()` 内部串起「抓图 → 宿主 uploadImage → URL」。返回 `null` 的三种情况:
114
+ * 宿主没注入 uploadImage、画布会话已失效(编辑器被关掉)、抓图或上传失败。
115
+ * **调用方不该因为它返回 null 就阻断发送** —— 一次网络抖动不该让用户发不出消息。
116
+ */
117
+ onAskAi?: (ctx: ElementAiContext, capture: () => Promise<string | null>) => void;
118
+ /**
119
+ * **整页**上下文(以 `body` 为根)—— 给「AI 自己挑改哪儿」的首版生成用,
120
+ * 与 `onAskAi` 的元素级上下文是两件事:那份是用户已经选好了一个元素。
121
+ *
122
+ * `domReady` 且图层树拉到之后算一次。**只在传了这个 prop 时才算** ——
123
+ * 它要多一次跨域往返(整页 DOM 树 + body 的 outerHTML),没人用的话是纯浪费。
124
+ *
125
+ * ⚠️ **不改选中态**:这一步是背景采集,用户可能正看着自己刚点中的元素。
126
+ * `capture()` 与元素级同一支(对 body 就是整屏),失败回 `null`,别因此阻断发送。
127
+ *
128
+ * `captureAnchors()`(有 resolved 锚点时才给):每个锚点区块一张裁切截图。整页图告诉 agent
129
+ * 区块在哪,裁切图才看得清区块内文字的颜色、字重、按钮圆角 —— 首版要"融进去"的东西。
130
+ * 同样在发送那一刻才拍,失败的那块就少一张,不阻断。
131
+ */
132
+ onPageContextReady?: (ctx: ElementAiContext, capture: () => Promise<string | null>, captureAnchors?: () => Promise<AnchorShot[]>) => void;
133
+ /**
134
+ * **方案锚点** —— 这条实验诞生时那份方案点名的页面区块(`InlinePlan.anchors`)。
135
+ *
136
+ * 存在的理由:假设是「B2 首屏的徽章」这么写的,而包交给 agent 的整页货架是一张扁平的
137
+ * selector 表,上面没有区块身份 —— agent 只能按关键词猜,猜错了也没有任何一道闸会拦
138
+ * (真机 trace:首屏那条被静默重锚到了页底另一个提到同样关键词的段落上)。
139
+ *
140
+ * 传了之后包会在**活页面上重新解析**它们(selector 优先,失效按 `label` 找回来),
141
+ * 把命中的锚点连同**它们的子树**排在整页货架最前 —— 于是 agent 既知道该改哪个容器,
142
+ * 也真的能改到容器里的孩子(货架是授权清单,不在货架里的 selector 一律 `OUT_OF_SCOPE`)。
143
+ *
144
+ * ⚠️ 只在 `onPageContextReady` 那条路上生效,且**每个会话读一次**(与整页货架同一次采集)。
145
+ * 不传 = 与今天完全一致。
146
+ */
147
+ anchors?: InlinePlanAnchor[];
148
+ /**
149
+ * 编辑器可用了(画布就绪、命令能发了)——**每个会话交一次**把手。
150
+ *
151
+ * 换会话(切设备 / 重载)会再交一次:新会话里那些命令要带新的 sessionId,
152
+ * 宿主留着上一次那份也不会报错,只是发出去的命令没人应答。
153
+ */
154
+ onEditorReady?: (handle: InlineEditorHandle) => void;
155
+ /**
156
+ * 画布会话就绪 → 交出 sessionId。**每个会话一次**,时机与 `onEditorReady` 相同
157
+ * (agent 报了 domReady):这是 frame 命令(`resolveHeatmapSelector` 等)保证有人
158
+ * 应答的最早时刻 —— 早于它发命令会 `FRAME_NOT_READY`。切设备 / 重载开新会话时再交一次。
159
+ *
160
+ * 宿主要它是为了调 `resolveBlocks(sessionId, …)`(`useInlineEditor()` 的内部状态不外露,
161
+ * 此前只能从 iframe 的 `name` 里反解 —— 那是 hack)。
162
+ */
163
+ onSessionReady?: (sessionId: string) => void;
164
+ /**
165
+ * 按设备覆盖目标视口宽(只对**这一个实例**生效,合在 `TARGET_VIEWPORT_WIDTH` 之上)。
166
+ *
167
+ * 分析画布用:热图区块的 `yStart/yEnd` 是按 375 宽(mobile)量的,画布默认 390 ——
168
+ * 差 15px 足以让响应式断点换一套布局,区块框与页面对不上。
169
+ */
170
+ viewportWidthOverride?: Partial<Record<CanvasDevice, number>>;
171
+ /**
172
+ * 分析画布的区块框层(`resolveBlocks` 的产出)。传了就画在画布之上,随几何帧走;
173
+ * `activeId` 那块高亮;`onHover` 报芯片的悬浮(宿主据此高亮聊天里对应的段)。
174
+ * 不传 = 没有这一层(普通编辑器)。
175
+ */
176
+ blockOverlay?: {
177
+ blocks: PageModelBlock[];
178
+ activeId?: string | null;
179
+ onHover?: (id: string | null) => void;
180
+ };
181
+ /**
182
+ * 选中变化 → 新的 AI 上下文。`null` = 选中被清空。
183
+ *
184
+ * **只在 `trackAiSelection` 为真时触发**,因为算一次上下文要多一次跨域往返
185
+ * (取 outerHTML)—— AI 面板没开着的时候那次往返是纯浪费。
186
+ *
187
+ * 与 `onAskAi` 的分工:这个只**更新**上下文,宿主不该顺手把面板打开;
188
+ * `onAskAi` 是用户点了 ✨,那时才开面板。
189
+ */
190
+ onAiContextChange?: (ctx: ElementAiContext | null, capture: () => Promise<string | null>) => void;
191
+ /**
192
+ * 把「应用 AI 改动」的入口交给宿主。`null` = 画布会话不可用(此时不该也无法应用)。
193
+ *
194
+ * 为什么是回调而不是宿主直接调:AI 面板在**另一棵组件树**里(dock 挂在 AppShell 上),
195
+ * 拿不到编辑器内部的 change list —— 与 `capture` 同一套分工,宿主把它存进 store。
196
+ *
197
+ * ⚠️ 这个 apply **自带作用域**:只允许改「注册这一刻选中的那个元素」。宿主不需要、
198
+ * 也不应该自己传 selector 进来 —— agent 编的 selector 正是这道闸要拦的东西。
199
+ */
200
+ onAiApplyReady?: (api: AiApplyApi | null) => void;
201
+ /**
202
+ * 是否跟着选中实时刷新 AI 上下文。宿主在**右侧 AI 面板已打开**时给 true。
203
+ *
204
+ * 面板关着还刷的话,每点一个元素都白花一次往返;而面板开着不刷的话,用户以为
205
+ * 自己换了选中、AI 却还在聊上一个元素 —— 这种错位不会有任何提示。
206
+ */
207
+ trackAiSelection?: boolean;
208
+ /**
209
+ * 组件库数据。宿主注入(包里不碰后端)—— **不传则左侧不出现「组件」页签**,
210
+ * 而不是出现一个空页签让用户以为库是空的。Task 6 填充。
211
+ */
212
+ libraryComponents?: LibraryComponent[];
213
+ libraryLoading?: boolean;
214
+ /**
215
+ * 新增版本。**不传则顶栏不渲染那个入口** —— 版本是 experience 级状态
216
+ * (每个版本一份 change list),不归编辑器管。
217
+ */
218
+ onAddVersion?: () => void;
219
+ /**
220
+ * 版本改名。**不传则下拉行里没有铅笔**(同 `onAddVersion`)。包只做交互与校验,
221
+ * 名字是 experience 级状态(保存时作为 `summary` 落库),改不改得成由宿主说了算。
222
+ */
223
+ onRenameVersion?: (id: string, name: string) => void;
224
+ /**
225
+ * 真实页面预览。**不传则顶栏不渲染那个入口** —— 预览链接由后端按 experience + 版本
226
+ * 签发(同详情页的「真实页面预览」),包里没有 experience 的概念,也不知道
227
+ * uid / sid,所以整件事归宿主。创建流程里 experience 还没落库,就别渲染。
228
+ */
229
+ onPreview?: () => void;
230
+ /**
231
+ * 开/关宿主的右侧 AI 面板。**不传则顶栏不渲染那个按钮** —— 同 `onPreview` 的处理。
232
+ *
233
+ * 包里没有 AI 面板,它是宿主布局的一部分(apps/x 的 `layout` store `aiVisible`)。
234
+ * apps/x 的宿主(`InlineEditorHost`,详情页与创建空白体验共用)接了;宿主没有 dock 可召唤时就别接。
235
+ *
236
+ * ⚠️ 纯开关:宿主实现里**不要**顺手切 agent 或开新会话(那是 `onAskAi` 的语义)。
237
+ */
238
+ onToggleAi?: () => void;
239
+ /** 宿主的右侧 AI 面板当前开着 —— 顶栏按钮据此高亮。 */
240
+ aiOpen?: boolean;
241
+ /**
242
+ * 把截图字节换成一条 URL。
243
+ *
244
+ * **包里不碰后端端点** —— 包不知道 `VITE_BACKEND_URL`、不知道 `sid`,那些是宿主的事
245
+ * (与 README 的两层 API 分工一致)。不传则只在界面上显示截图结果,不上传。
246
+ */
247
+ uploadImage?: (blob: Blob) => Promise<string>;
248
+ /**
249
+ * 图层树行首图标渲染器。**不传则树里不出图标**(不是出个占位方块)。
250
+ *
251
+ * 为什么由宿主给:lucide 字形表是「三代编辑器共用的唯一真值表」,住在
252
+ * `apps/x/src/components/landing-pages/editor/icons/` —— 包不能反向依赖 app。
253
+ * 包只交出事实(标签名 + 有没有子节点),挑哪个字形是宿主的事。
254
+ */
255
+ renderNodeIcon?: AdaptDomTreeOptions['renderIcon'];
256
+ }
257
+ /**
258
+ * 交给宿主的编辑器把手 —— **每个画布会话交一次**(`onEditorReady`)。
259
+ *
260
+ * 为什么不是「把 `useInlineEditor()` 的结果整个交出去」:那是内核的全部内部状态,
261
+ * 交出去等于把包的实现细节变成契约。这里只开三件宿主真的做不到的事。
262
+ */
263
+ export interface InlineEditorHandle {
264
+ /**
265
+ * 清空选中(等价于 `commands.select('')`)。
266
+ *
267
+ * 宿主要它是为了**回到整页作用域**:有选中时 AI 的 apply 闸只认那一个元素与它的货架,
268
+ * 而首版生成(AI 自己挑改哪儿)必须在没有选中的状态下跑 —— 用户点过一个元素之后,
269
+ * 只有宿主知道「现在要换成整页那档了」。
270
+ */
271
+ clearSelection: () => Promise<void>;
272
+ /**
273
+ * **同步**读当前改动清单(不是渲染期那份 state)。
274
+ *
275
+ * 宿主保存时要的就是这一刻的清单;读渲染期那份会漏掉刚写进去、还没重渲染的那条。
276
+ */
277
+ peekList: () => ChangeRecord[];
278
+ /** 有没有未保存的改动 —— 离开确认、保存按钮的禁用态用它。 */
279
+ hasUnsavedChanges: () => boolean;
280
+ /**
281
+ * 整页截图 → PNG Blob(分析画布:标注区块后切瓦片给模型)。
282
+ *
283
+ * = `captureSelectedElement('body')`:走现成的编排(先滚动测量 → 藏 fixed/sticky 与
284
+ * 产品页浮层 → 分屏抓 → 恢复),**不另写一套**。fixed/sticky 由既有的 `setFixedHidden`
285
+ * 全部藏掉 —— 吸顶导航在长图里重复三四次比少一条导航更误导模型。
286
+ */
287
+ capturePage: () => Promise<Blob>;
288
+ /**
289
+ * 「定位」:把 selector 指向的元素滚进视野(= `commands.reveal`)。
290
+ * 分析画布点聊天里的某块 → 画布滚到那块。未命中不抛(agent 回 found:false)。
291
+ */
292
+ scrollToSelector: (selector: string) => Promise<void>;
293
+ /**
294
+ * 当前**就绪**的画布会话 id;`null` = 画布还没起来(含切设备后新 frame 尚未报 domReady
295
+ * 的窗口 —— 此刻发 frame 命令没人应答,所以不给旧 id)。
296
+ */
297
+ getSessionId: () => string | null;
298
+ /** 画布当前施加在 `<html>` 上的缩放比例(移动端超宽缩小;不缩为 1)。语义同 `PagePreviewHandle`。 */
299
+ getMobileFitScale: () => number;
300
+ }
301
+ /** 宿主拿到的 AI 改动入口。 */
302
+ export interface AiApplyApi {
303
+ /**
304
+ * 应用一批改动。
305
+ *
306
+ * `opts.versionId` 指名了另一个版本时**先切过去再应用**(切换与应用共用同一个串行
307
+ * 队列,所以 agent 连着给两个版本的两批不会交错)。切换失败则整批 reject,
308
+ * reason `VERSION_SWITCH_FAILED`,并停留在当前版本 —— 半批落在错版本上比不落更糟。
309
+ *
310
+ * 返回里的 `versionId` 是这一批**实际落在**哪个版本上,宿主拿它给卡片记账。
311
+ *
312
+ * `opts.grant` 是**发这条消息时交给 agent 的那份作用域**(见 `AiScopeGrant`):一轮要
313
+ * 60-120 秒,中途选中丢了的话包这边的实时作用域已经不是当时那个了。宿主自己记得当时
314
+ * 给出去的是什么,所以由它交回来 —— 与实时作用域取并集,不传则行为与今天逐字一致。
315
+ */
316
+ apply: (changes: AiWireChange[], opts?: {
317
+ versionId?: string;
318
+ grant?: AiScopeGrant;
319
+ }) => Promise<AiApplyOutcome & {
320
+ versionId: string;
321
+ }>;
322
+ /**
323
+ * 撤销**指定的**几条(卡片上的「撤销这一批」)。
324
+ *
325
+ * ⚠️ 不能用 `revertAll` —— 那会把用户自己手改的一起撤掉。
326
+ * ⚠️ **倒序撤**:后面的 change 可能作用在前面那条造出来的现场上,顺序撤会让它基于
327
+ * 已被撤掉的状态还原(与 `revertAll` 同一个理由)。
328
+ *
329
+ * 先经 `resolveChangeId` 映射(切过版本之后 id 都换过了);映射完仍不在当前清单里,
330
+ * 说明这一批属于另一个版本 —— 返回 `VERSION_NOT_ACTIVE` 而不是硬撤:那些 id 在
331
+ * agent 侧早就没有闭包了,硬撤是静默的 no-op,用户只会看到「点了没反应」。
332
+ */
333
+ revert: (changeIds: string[]) => Promise<{
334
+ ok: true;
335
+ } | {
336
+ ok: false;
337
+ reason: 'VERSION_NOT_ACTIVE' | 'EDITOR_CLOSED';
338
+ }>;
339
+ /** 当前版本 —— 宿主把 agent 给的版本**名**映射成 id 时要知道现在在哪一版。 */
340
+ readonly activeVersionId?: string;
341
+ }
342
+ export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedImageOrigins, allowedImageUrls, allowedLinkUrls, viewportWidthOverride, blockOverlay }: InlineEditorProps): import("react").JSX.Element;