@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
@@ -0,0 +1,867 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { CANVAS_UI_SCOPE } from './canvas-ui/index.js';
5
+ import { EditorChrome } from './chrome/EditorChrome.js';
6
+ import { CanvasFrame, TARGET_VIEWPORT_WIDTH, computeZoom } from './kernel/CanvasFrame.js';
7
+ import { CanvasContextBar } from './kernel/CanvasContextBar.js';
8
+ import { SelectionOverlay } from './kernel/selection/SelectionOverlay.js';
9
+ import { BlockOverlay } from './kernel/blocks/BlockOverlay.js';
10
+ import { dataUrlToBlob } from './kernel/dataUrlToBlob.js';
11
+ import { SelectionToolbar } from './kernel/selection/SelectionToolbar.js';
12
+ import { ChangeListPanel } from './kernel/changelist/ChangeListPanel.js';
13
+ import { ChangeListPopover } from './kernel/changelist/ChangeListPopover.js';
14
+ import { frameRectToProductRect, measureFrameContext } from './kernel/selection/geometry.js';
15
+ import { buildElementAiContext, extractPageLinks, linksFragment, ROOT_SHELF_LIMIT, SHELF_TREE_MAX_DEPTH, SHELF_TREE_MAX_NODES } from './kernel/selection/elementAiContext.js';
16
+ import { getAncestorChain, getDomTree, getElementCapabilities, getElementImages, getOuterHtml, resolveCanvasSelector } from './kernel/canvasCommands.js';
17
+ import { useAiSelectionContext } from './kernel/selection/useAiSelectionContext.js';
18
+ import { buildDescendantShelf } from './kernel/selection/buildDescendantShelf.js';
19
+ import { buildRootShelf } from './kernel/selection/buildRootShelf.js';
20
+ import { backgroundImageAncestorFor, blockContainerFor, resolveInlinePlanAnchors, withImageRows } from './kernel/selection/resolveAnchors.js';
21
+ import { elementGeometryForAi } from './kernel/selection/elementGeometryForAi.js';
22
+ import { applyAiChanges } from './kernel/ai/aiInlineApply.js';
23
+ import { APPLY_CHECK_SETTLE_MS } from './kernel/ai/postApplyCheck.js';
24
+ import { deleteElement, editHtml, editStyle, insertElement, replaceImage, replaceVideo, setClickAction, setImageAlt } from './kernel/changelist/useChangeList.js';
25
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@ptengine/design-components';
26
+ import { pasteModes } from './kernel/selection/toolbarButtons.js';
27
+ import { ClickActionDialog } from './dialogs/ClickActionDialog.js';
28
+ import { ImageEditDialog } from './dialogs/ImageEditDialog.js';
29
+ import { resolveImageBranch } from './dialogs/imageEdit.js';
30
+ import { ImageSettingDialog } from './dialogs/ImageSettingDialog.js';
31
+ import { InsertElementDialog } from './dialogs/InsertElementDialog.js';
32
+ import { ReplaceVideoDialog } from './dialogs/ReplaceVideoDialog.js';
33
+ import { StylePanel } from './panels/StylePanel.js';
34
+ import { HtmlEditDialog } from './dialogs/HtmlEditDialog.js';
35
+ import { setCode } from './kernel/canvasCommands.js';
36
+ import { describeCanvasError } from './kernel/canvasErrors.js';
37
+ import { LayersPanel } from './tree/LayersPanel.js';
38
+ import { AssetsPanel } from './assets/AssetsPanel.js';
39
+ import { componentInsertPayload } from './assets/componentLibrary.js';
40
+ import { moveModeFromDropPos } from './tree/domTreeAdapter.js';
41
+ import { useInlineEditor } from './useInlineEditor.js';
42
+ import { shrinkImageForUpload } from './kernel/shrinkImageForUpload.js';
43
+ const nodeSelectorOf = (n) => n.selector;
44
+ function imageTargetOf(caps) {
45
+ if (!caps)
46
+ return undefined;
47
+ const branch = resolveImageBranch(caps);
48
+ if (branch === 'img-src')
49
+ return 'img';
50
+ return branch === 'background' ? 'background' : undefined;
51
+ }
52
+ const AI_ELEMENT_IMAGE_LIMIT = 4;
53
+ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedImageOrigins, allowedImageUrls, allowedLinkUrls, viewportWidthOverride, blockOverlay }) {
54
+ const { t } = useTranslation();
55
+ const canvasAreaRef = useRef(null);
56
+ const canvasIframeRef = useRef(null);
57
+ const editor = useInlineEditor({
58
+ url,
59
+ device,
60
+ iframeRef: canvasIframeRef,
61
+ renderNodeIcon
62
+ });
63
+ const { selection, tree, clipboard, move, capturing, textEditing, geometry, changeList, commands, frameProps, domReady } = editor;
64
+ const selectAndReveal = useCallback(async (selector) => {
65
+ await commands.select(selector);
66
+ await commands.reveal(selector);
67
+ }, [commands]);
68
+ const [commandError, setCommandError] = useState(null);
69
+ const [aiApplyNote, setAiApplyNote] = useState(null);
70
+ const [shot, setShot] = useState(null);
71
+ const [shotUrl, setShotUrl] = useState(null);
72
+ const [panelWidth, setPanelWidth] = useState(0);
73
+ const [showChangeList, setShowChangeList] = useState(false);
74
+ const [stylePanelOpen, setStylePanelOpen] = useState(false);
75
+ const [htmlDialog, setHtmlDialog] = useState({ open: false, seed: '' });
76
+ const [imageDialog, setImageDialog] = useState({ open: false, seed: '' });
77
+ const [imageSettingDialog, setImageSettingDialog] = useState({ open: false, seed: '' });
78
+ const [videoDialog, setVideoDialog] = useState({ open: false, seed: '' });
79
+ const [insertDialog, setInsertDialog] = useState(false);
80
+ const [pasteDialog, setPasteDialog] = useState(false);
81
+ const [showLeftPanel, setShowLeftPanel] = useState(true);
82
+ const [leftTab, setLeftTab] = useState('layers');
83
+ const [actionDialog, setActionDialog] = useState({ open: false, seed: null });
84
+ const pageOrigin = useMemo(() => {
85
+ try {
86
+ return new URL(url).origin;
87
+ }
88
+ catch {
89
+ return '';
90
+ }
91
+ }, [url]);
92
+ const imageOrigins = useMemo(() => Array.from(new Set([pageOrigin, ...(allowedImageOrigins ?? [])].filter(Boolean))), [pageOrigin, allowedImageOrigins]);
93
+ const changeListRef = useRef(changeList);
94
+ changeListRef.current = changeList;
95
+ const versionChangesRef = useRef(versionChanges);
96
+ versionChangesRef.current = versionChanges;
97
+ const versionCallbacksRef = useRef({ onVersionChange, onVersionLeave, onVersionEnter });
98
+ versionCallbacksRef.current = { onVersionChange, onVersionLeave, onVersionEnter };
99
+ const resolveChangeIdRef = useRef(resolveChangeId);
100
+ resolveChangeIdRef.current = resolveChangeId;
101
+ const hasVersions = !!versions?.length;
102
+ const hasVersionsRef = useRef(hasVersions);
103
+ hasVersionsRef.current = hasVersions;
104
+ const loadedVersionRef = useRef(activeVersionId);
105
+ const [loadedVersion, setLoadedVersion] = useState(activeVersionId);
106
+ const leftVersionIdsRef = useRef(new Map());
107
+ const [switching, setSwitching] = useState(false);
108
+ const switchingRef = useRef(false);
109
+ const queueRef = useRef(Promise.resolve());
110
+ const enqueue = useCallback((task) => {
111
+ const run = queueRef.current.then(task, task);
112
+ queueRef.current = run.then(() => undefined, () => undefined);
113
+ return run;
114
+ }, []);
115
+ const prevIdsFor = useCallback((versionId, next) => {
116
+ const remembered = versionId ? leftVersionIdsRef.current.get(versionId) : undefined;
117
+ if (remembered?.length === next.length)
118
+ return remembered;
119
+ const live = changeListRef.current.peekList().map(r => r.changeId);
120
+ if (live.length === next.length && live.length > 0)
121
+ return live;
122
+ const fromChanges = next.map(c => c.id ?? '');
123
+ return fromChanges.length > 0 && fromChanges.every(Boolean) ? fromChanges : undefined;
124
+ }, []);
125
+ const switchToVersion = useCallback(async (nextId) => {
126
+ const prevId = loadedVersionRef.current;
127
+ if (!hasVersionsRef.current || nextId === prevId)
128
+ return;
129
+ setSwitching(true);
130
+ switchingRef.current = true;
131
+ try {
132
+ const live = changeListRef.current.peekList();
133
+ if (prevId) {
134
+ versionCallbacksRef.current.onVersionLeave?.(prevId, live);
135
+ leftVersionIdsRef.current.set(prevId, live.map(r => r.changeId));
136
+ }
137
+ const next = versionChangesRef.current?.[nextId] ?? [];
138
+ const { idMap } = await changeListRef.current.replaceAll(next, prevIdsFor(nextId, next));
139
+ loadedVersionRef.current = nextId;
140
+ setLoadedVersion(nextId);
141
+ versionCallbacksRef.current.onVersionEnter?.(nextId, idMap);
142
+ }
143
+ finally {
144
+ setSwitching(false);
145
+ switchingRef.current = false;
146
+ }
147
+ }, [prevIdsFor]);
148
+ const rejectWhileSwitching = useCallback((run) => {
149
+ if (!switchingRef.current)
150
+ return run();
151
+ return Promise.reject(new Error('VERSION_SWITCHING'));
152
+ }, []);
153
+ const writes = useMemo(() => ({
154
+ addStyle: input => rejectWhileSwitching(() => changeListRef.current.addStyle(input)),
155
+ addContent: input => rejectWhileSwitching(() => changeListRef.current.addContent(input)),
156
+ addAttribute: input => rejectWhileSwitching(() => changeListRef.current.addAttribute(input)),
157
+ addAction: input => rejectWhileSwitching(() => changeListRef.current.addAction(input)),
158
+ addMove: input => rejectWhileSwitching(() => changeListRef.current.addMove(input)),
159
+ revert: id => rejectWhileSwitching(() => changeListRef.current.revert(id)),
160
+ revertAll: () => rejectWhileSwitching(() => changeListRef.current.revertAll())
161
+ }), [rejectWhileSwitching]);
162
+ const uiWrite = useCallback((run) => {
163
+ if (switchingRef.current) {
164
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
165
+ return;
166
+ }
167
+ void run().catch(e => setCommandError(describeCanvasError(e)));
168
+ }, [t]);
169
+ useEffect(() => {
170
+ if (!hasVersions || !activeVersionId)
171
+ return;
172
+ if (activeVersionId === loadedVersionRef.current)
173
+ return;
174
+ void enqueue(() => switchToVersion(activeVersionId)).catch(e => {
175
+ setCommandError(describeCanvasError(e));
176
+ const back = loadedVersionRef.current;
177
+ if (back)
178
+ versionCallbacksRef.current.onVersionChange?.(back);
179
+ });
180
+ }, [activeVersionId, hasVersions, enqueue, switchToVersion]);
181
+ useEffect(() => {
182
+ const el = canvasAreaRef.current;
183
+ if (!el)
184
+ return;
185
+ const ro = new ResizeObserver(entries => {
186
+ const w = entries[0]?.contentRect.width;
187
+ if (w)
188
+ setPanelWidth(w);
189
+ });
190
+ ro.observe(el);
191
+ return () => ro.disconnect();
192
+ }, []);
193
+ const sessionIdForAssets = frameProps?.sessionId;
194
+ useEffect(() => {
195
+ injectedAssetsRef.current = [];
196
+ }, [sessionIdForAssets]);
197
+ const hydratedSessionRef = useRef(null);
198
+ const sessionIdForHydrate = frameProps?.sessionId;
199
+ const bootChanges = hasVersions && activeVersionId && versionChanges?.[activeVersionId]
200
+ ? versionChanges[activeVersionId]
201
+ : initialChanges;
202
+ useEffect(() => {
203
+ if (!domReady || !sessionIdForHydrate)
204
+ return;
205
+ if (hydratedSessionRef.current === sessionIdForHydrate)
206
+ return;
207
+ hydratedSessionRef.current = sessionIdForHydrate;
208
+ if (!bootChanges?.length)
209
+ return;
210
+ const versionId = hasVersions ? activeVersionId : undefined;
211
+ void enqueue(async () => {
212
+ const { outcome, idMap } = await changeListRef.current.replaceAll(bootChanges, prevIdsFor(versionId, bootChanges));
213
+ if (versionId) {
214
+ loadedVersionRef.current = versionId;
215
+ setLoadedVersion(versionId);
216
+ versionCallbacksRef.current.onVersionEnter?.(versionId, idMap);
217
+ }
218
+ onHydrated?.(outcome);
219
+ }).catch(e => setCommandError(describeCanvasError(e)));
220
+ }, [domReady, sessionIdForHydrate, bootChanges]);
221
+ const commandsRef = useRef(commands);
222
+ commandsRef.current = commands;
223
+ const sessionReady = domReady && !editor.mobileFit.pending;
224
+ const readySessionRef = useRef(null);
225
+ readySessionRef.current = sessionReady && sessionIdForHydrate ? sessionIdForHydrate : null;
226
+ const fitScaleRef = useRef(1);
227
+ fitScaleRef.current = editor.mobileFit.scale;
228
+ const sessionReadyRef = useRef(null);
229
+ useEffect(() => {
230
+ if (!onSessionReady || !sessionReady || !sessionIdForHydrate)
231
+ return;
232
+ if (sessionReadyRef.current === sessionIdForHydrate)
233
+ return;
234
+ sessionReadyRef.current = sessionIdForHydrate;
235
+ onSessionReady(sessionIdForHydrate);
236
+ }, [onSessionReady, sessionReady, sessionIdForHydrate]);
237
+ const editorHandleSessionRef = useRef(null);
238
+ useEffect(() => {
239
+ if (!onEditorReady || !sessionReady || !sessionIdForHydrate)
240
+ return;
241
+ if (editorHandleSessionRef.current === sessionIdForHydrate)
242
+ return;
243
+ editorHandleSessionRef.current = sessionIdForHydrate;
244
+ onEditorReady({
245
+ clearSelection: () => commandsRef.current.select(''),
246
+ peekList: () => changeListRef.current.peekList(),
247
+ hasUnsavedChanges: () => changeListRef.current.peekList().length > 0,
248
+ capturePage: async () => {
249
+ const outcome = await commandsRef.current.captureSelectedElement('body');
250
+ return dataUrlToBlob(outcome.dataUrl);
251
+ },
252
+ scrollToSelector: selector => commandsRef.current.reveal(selector),
253
+ getSessionId: () => readySessionRef.current,
254
+ getMobileFitScale: () => fitScaleRef.current
255
+ });
256
+ }, [onEditorReady, sessionReady, sessionIdForHydrate]);
257
+ const selectedSelector = selection?.selector;
258
+ useEffect(() => {
259
+ setStylePanelOpen(false);
260
+ }, [selectedSelector]);
261
+ const overridePc = viewportWidthOverride?.pc;
262
+ const overrideMobile = viewportWidthOverride?.mobile;
263
+ const targetViewportWidth = useMemo(() => ({
264
+ pc: overridePc ?? TARGET_VIEWPORT_WIDTH.pc,
265
+ mobile: overrideMobile ?? TARGET_VIEWPORT_WIDTH.mobile
266
+ }), [overridePc, overrideMobile]);
267
+ const zoom = computeZoom(panelWidth, targetViewportWidth[device]);
268
+ const iframeRect = canvasIframeRef.current?.getBoundingClientRect();
269
+ const overlayContext = useMemo(() => measureFrameContext(canvasIframeRef.current, zoom), [iframeRect?.left, iframeRect?.top, zoom]);
270
+ const canvasAreaRect = canvasAreaRef.current?.getBoundingClientRect();
271
+ const area = {
272
+ x: canvasAreaRect?.left ?? 0,
273
+ y: canvasAreaRect?.top ?? 0,
274
+ width: canvasAreaRect?.width ?? 0,
275
+ height: canvasAreaRect?.height ?? 0
276
+ };
277
+ const selectedRect = geometry.latest?.selectedViewportRect
278
+ ? frameRectToProductRect(geometry.latest.selectedViewportRect, overlayContext)
279
+ : null;
280
+ const depsOf = useCallback((self) => ({
281
+ identifier: self.selector,
282
+ selector: self.selector,
283
+ ancestors: (selection?.chain ?? []).slice(1).map(n => n.selector),
284
+ addStyle: writes.addStyle,
285
+ addContent: writes.addContent,
286
+ addAttribute: writes.addAttribute,
287
+ addAction: writes.addAction
288
+ }), [writes, selection]);
289
+ const injectedAssetsRef = useRef([]);
290
+ const insertComponent = useCallback(async (component) => {
291
+ const target = selection?.chain[0];
292
+ const sessionId = frameProps?.sessionId;
293
+ if (!target || !sessionId)
294
+ return;
295
+ if (switchingRef.current) {
296
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
297
+ return;
298
+ }
299
+ setCommandError(null);
300
+ try {
301
+ const { html, codeList } = componentInsertPayload(component);
302
+ if (codeList.length > 0) {
303
+ injectedAssetsRef.current = [...injectedAssetsRef.current, ...codeList];
304
+ await setCode(sessionId, injectedAssetsRef.current);
305
+ }
306
+ await writes.addContent({
307
+ identifier: target.selector,
308
+ selector: target.selector,
309
+ html,
310
+ mode: 'append'
311
+ });
312
+ if (component.js) {
313
+ setCommandError('组件已插入。它带有 JS,画布里不会执行(客户页没有 SDK 运行时),发布后才生效');
314
+ }
315
+ }
316
+ catch (error) {
317
+ setCommandError(describeCanvasError(error));
318
+ }
319
+ }, [writes, frameProps?.sessionId, selection, t]);
320
+ const captureAiShot = useCallback(async (selector) => {
321
+ if (!uploadImage)
322
+ return { error: 'host did not provide uploadImage' };
323
+ try {
324
+ const outcome = await commands.captureSelectedElement(selector);
325
+ const raw = await (await fetch(outcome.dataUrl)).blob();
326
+ const blob = await shrinkImageForUpload(raw);
327
+ const url = await uploadImage(blob);
328
+ return url ? { url } : { error: 'upload returned no url' };
329
+ }
330
+ catch (error) {
331
+ setCommandError(describeCanvasError(error));
332
+ return { error: error instanceof Error ? error.message : String(error) };
333
+ }
334
+ }, [uploadImage, commands]);
335
+ const captureAiScreenshot = useCallback(async (selector) => {
336
+ const shot = await captureAiShot(selector);
337
+ return 'url' in shot ? shot.url : null;
338
+ }, [captureAiShot]);
339
+ const descendantShelfRef = useRef([]);
340
+ const geometryRef = useRef(null);
341
+ geometryRef.current = geometry.latest;
342
+ const [elementImages, setElementImages] = useState(null);
343
+ const buildAiContext = useCallback((self, outerHtml) => buildElementAiContext({
344
+ selector: self.selector,
345
+ tag: self.tag,
346
+ imageTarget: imageTargetOf(selection?.capabilities),
347
+ images: elementImages?.selector === self.selector ? elementImages.images : undefined,
348
+ ancestors: (selection?.chain ?? []).map(n => n.selector).slice().reverse(),
349
+ outerHtml,
350
+ descendants: descendantShelfRef.current,
351
+ hasChildren: (selection?.capabilities?.childElementCount ?? 0) > 0,
352
+ geometry: elementGeometryForAi(geometryRef.current, device)
353
+ }), [selection, elementImages]);
354
+ const sessionIdForAiContext = frameProps?.sessionId;
355
+ const readOuterHtmlForAi = useCallback((selector) => getOuterHtml(sessionIdForAiContext, selector), [sessionIdForAiContext]);
356
+ const captureForAi = useCallback((selector) => () => captureAiScreenshot(selector), [captureAiScreenshot]);
357
+ useEffect(() => {
358
+ const picked = selection?.chain[0];
359
+ if (!trackAiSelection || !picked || !sessionIdForAiContext) {
360
+ descendantShelfRef.current = [];
361
+ return;
362
+ }
363
+ let stale = false;
364
+ void getDomTree(sessionIdForAiContext, {
365
+ rootSelector: picked.selector,
366
+ maxDepth: SHELF_TREE_MAX_DEPTH,
367
+ maxChildren: 20,
368
+ maxNodes: SHELF_TREE_MAX_NODES,
369
+ visibleOnly: true,
370
+ includeRect: true
371
+ })
372
+ .then(root => {
373
+ if (!stale)
374
+ descendantShelfRef.current = buildDescendantShelf(root);
375
+ })
376
+ .catch(() => {
377
+ descendantShelfRef.current = [];
378
+ });
379
+ return () => {
380
+ stale = true;
381
+ };
382
+ }, [trackAiSelection, selection, sessionIdForAiContext]);
383
+ useEffect(() => {
384
+ const picked = selection?.chain[0];
385
+ if (!trackAiSelection || !picked || !sessionIdForAiContext)
386
+ return;
387
+ let stale = false;
388
+ void getElementImages(sessionIdForAiContext, picked.selector, AI_ELEMENT_IMAGE_LIMIT)
389
+ .then(({ images }) => {
390
+ if (stale || !images.length)
391
+ return;
392
+ setElementImages({ selector: picked.selector, images });
393
+ })
394
+ .catch(() => {
395
+ });
396
+ return () => {
397
+ stale = true;
398
+ };
399
+ }, [trackAiSelection, selection, sessionIdForAiContext]);
400
+ const pageContextSessionRef = useRef(null);
401
+ const rootShelfRef = useRef([]);
402
+ const aiInsertedRef = useRef([]);
403
+ const rootShelfAncestorsRef = useRef(new Map());
404
+ const anchorsResultRef = useRef(null);
405
+ const anchorsRef = useRef(anchors);
406
+ useEffect(() => {
407
+ anchorsRef.current = anchors;
408
+ }, [anchors]);
409
+ useEffect(() => {
410
+ rootShelfRef.current = [];
411
+ rootShelfAncestorsRef.current = new Map();
412
+ aiInsertedRef.current = [];
413
+ anchorsResultRef.current = null;
414
+ pageContextSessionRef.current = null;
415
+ leftVersionIdsRef.current = new Map();
416
+ }, [sessionIdForAiContext]);
417
+ useEffect(() => {
418
+ if (!onPageContextReady || !domReady || !sessionIdForAiContext)
419
+ return;
420
+ if (pageContextSessionRef.current === sessionIdForAiContext)
421
+ return;
422
+ pageContextSessionRef.current = sessionIdForAiContext;
423
+ let stale = false;
424
+ void (async () => {
425
+ const root = await getDomTree(sessionIdForAiContext, {
426
+ rootSelector: 'body',
427
+ maxDepth: 16,
428
+ maxChildren: 40,
429
+ maxNodes: 1500,
430
+ visibleOnly: true,
431
+ includeRect: true
432
+ });
433
+ const { html } = await getOuterHtml(sessionIdForAiContext, 'body');
434
+ if (stale)
435
+ return;
436
+ const { shelf, ancestorsOf } = buildRootShelf(root, {
437
+ limit: ROOT_SHELF_LIMIT,
438
+ viewportWidth: geometryRef.current?.viewportWidth
439
+ });
440
+ const planAnchors = anchorsRef.current ?? [];
441
+ const anchorOut = planAnchors.length
442
+ ? await resolveInlinePlanAnchors({
443
+ anchors: planAnchors,
444
+ rootTree: root,
445
+ resolveSelector: async (selector) => {
446
+ const r = await resolveCanvasSelector(sessionIdForAiContext, selector);
447
+ return { matched: r.matched, matchCount: r.matchCount };
448
+ },
449
+ getSubtree: (rootSelector) => getDomTree(sessionIdForAiContext, {
450
+ rootSelector,
451
+ maxDepth: SHELF_TREE_MAX_DEPTH,
452
+ maxChildren: 20,
453
+ maxNodes: SHELF_TREE_MAX_NODES,
454
+ visibleOnly: true,
455
+ includeRect: true
456
+ })
457
+ })
458
+ : null;
459
+ if (stale)
460
+ return;
461
+ const anchorRows = anchorOut?.shelf ?? [];
462
+ const anchorSelectors = new Set(anchorRows.map(r => r.selector));
463
+ const mergedShelf = [
464
+ ...anchorRows,
465
+ ...shelf.filter(row => !anchorSelectors.has(row.selector))
466
+ ];
467
+ const mergedAncestors = new Map(ancestorsOf);
468
+ for (const [selector, chain] of anchorOut?.ancestorsOf ?? []) {
469
+ mergedAncestors.set(selector, chain);
470
+ }
471
+ rootShelfRef.current = mergedShelf;
472
+ rootShelfAncestorsRef.current = mergedAncestors;
473
+ anchorsResultRef.current = anchorOut?.anchors ?? null;
474
+ const resolvedAnchors = (anchorOut?.anchors ?? []).filter(a => a.resolved);
475
+ const anchorBlocks = [];
476
+ for (const anchor of resolvedAnchors) {
477
+ const selector = blockContainerFor(anchor, mergedShelf, mergedAncestors);
478
+ if (!anchorBlocks.some(b => b.selector === selector))
479
+ anchorBlocks.push({ anchor, selector });
480
+ }
481
+ const fragments = [];
482
+ const images = [];
483
+ for (const { anchor, selector } of anchorBlocks) {
484
+ try {
485
+ const { html: blockHtml } = await getOuterHtml(sessionIdForAiContext, selector);
486
+ fragments.push({ label: anchor.blockId ?? anchor.label, selector, html: blockHtml });
487
+ }
488
+ catch {
489
+ }
490
+ const backgroundUp = anchor.blockSelectors?.length
491
+ ? null
492
+ : backgroundImageAncestorFor(anchor.selector, mergedShelf, mergedAncestors);
493
+ const imageRoots = Array.from(new Set([...(anchor.blockSelectors ?? []), selector, ...(backgroundUp ? [backgroundUp] : [])])).slice(0, 3);
494
+ const seenUrls = new Set(images.map(i => i.url));
495
+ for (const root of imageRoots) {
496
+ try {
497
+ const { images: found } = await getElementImages(sessionIdForAiContext, root);
498
+ for (const im of found) {
499
+ if (seenUrls.has(im.url))
500
+ continue;
501
+ seenUrls.add(im.url);
502
+ images.push({
503
+ ...(anchor.blockId ? { blockId: anchor.blockId } : {}),
504
+ selector: im.selector,
505
+ kind: im.kind,
506
+ url: im.url
507
+ });
508
+ }
509
+ }
510
+ catch {
511
+ }
512
+ }
513
+ }
514
+ if (stale)
515
+ return;
516
+ const onShelf = new Set(mergedShelf.map(r => r.selector));
517
+ const imageAncestors = new Map();
518
+ for (const selector of new Set(images.map(im => im.selector))) {
519
+ if (onShelf.has(selector))
520
+ continue;
521
+ try {
522
+ const { chain } = await getAncestorChain(sessionIdForAiContext, selector);
523
+ if (chain.length)
524
+ imageAncestors.set(selector, chain.slice(1).map(c => c.selector));
525
+ }
526
+ catch {
527
+ }
528
+ }
529
+ if (stale)
530
+ return;
531
+ const { shelf: pageShelf, added: imageRowCount } = withImageRows(mergedShelf, anchorRows.length, images.filter(im => onShelf.has(im.selector) || imageAncestors.has(im.selector)));
532
+ for (const [selector, chain] of imageAncestors)
533
+ mergedAncestors.set(selector, chain);
534
+ rootShelfRef.current = pageShelf;
535
+ rootShelfAncestorsRef.current = mergedAncestors;
536
+ if (fragments.length) {
537
+ const links = linksFragment(extractPageLinks(html));
538
+ if (links)
539
+ fragments.push(links);
540
+ }
541
+ const captureAnchors = anchorBlocks.length
542
+ ? async () => {
543
+ const shots = [];
544
+ for (const { anchor, selector } of anchorBlocks) {
545
+ const shotSelector = anchor.blockSelectors?.[0] ?? selector;
546
+ const shot = await captureAiShot(shotSelector);
547
+ const base = {
548
+ ...(anchor.blockId ? { blockId: anchor.blockId } : {}),
549
+ label: anchor.label,
550
+ selector: shotSelector
551
+ };
552
+ shots.push('url' in shot ? { ...base, url: shot.url } : { ...base, error: shot.error });
553
+ }
554
+ return shots;
555
+ }
556
+ : undefined;
557
+ const frame = geometryRef.current;
558
+ onPageContextReady(buildElementAiContext({
559
+ selector: 'body',
560
+ tag: 'body',
561
+ ancestors: [],
562
+ outerHtml: html,
563
+ fragments,
564
+ images,
565
+ descendants: pageShelf,
566
+ descendantLimit: ROOT_SHELF_LIMIT + anchorRows.length + imageRowCount,
567
+ ...(anchorsResultRef.current ? { anchors: anchorsResultRef.current } : {}),
568
+ hasChildren: true,
569
+ ...(frame
570
+ ? {
571
+ geometry: {
572
+ width: Math.round(frame.viewportWidth),
573
+ height: Math.round(frame.viewportHeight),
574
+ topInPage: 0,
575
+ viewportWidth: Math.round(frame.viewportWidth),
576
+ viewportHeight: Math.round(frame.viewportHeight),
577
+ device
578
+ }
579
+ }
580
+ : {})
581
+ }), () => captureAiScreenshot('body'), captureAnchors);
582
+ })().catch(error => {
583
+ if (!stale)
584
+ setCommandError(describeCanvasError(error));
585
+ });
586
+ return () => {
587
+ stale = true;
588
+ };
589
+ }, [
590
+ onPageContextReady,
591
+ domReady,
592
+ sessionIdForAiContext,
593
+ device,
594
+ captureAiScreenshot,
595
+ captureAiShot
596
+ ]);
597
+ useAiSelectionContext({
598
+ enabled: trackAiSelection,
599
+ picked: selection?.chain[0],
600
+ sessionId: sessionIdForAiContext,
601
+ readOuterHtml: readOuterHtmlForAi,
602
+ selectorOf: nodeSelectorOf,
603
+ buildContext: buildAiContext,
604
+ captureFor: captureForAi,
605
+ onChange: onAiContextChange
606
+ });
607
+ useEffect(() => {
608
+ if (!onAiApplyReady)
609
+ return;
610
+ const picked = selection?.chain[0];
611
+ const pageMode = !!onPageContextReady;
612
+ if (!sessionIdForAiContext || (!picked && !pageMode)) {
613
+ onAiApplyReady(null);
614
+ return;
615
+ }
616
+ const scope = picked
617
+ ? {
618
+ selector: picked.selector,
619
+ identifier: picked.selector,
620
+ descendants: () => [
621
+ ...descendantShelfRef.current.map(d => d.selector),
622
+ ...aiInsertedRef.current
623
+ ],
624
+ ancestors: (selection?.chain ?? []).slice(1).map(n => n.selector)
625
+ }
626
+ : {
627
+ selector: 'body',
628
+ identifier: 'body',
629
+ descendants: () => [...rootShelfRef.current.map(d => d.selector), ...aiInsertedRef.current],
630
+ ancestors: [],
631
+ ancestorsOf: (selector) => rootShelfAncestorsRef.current.get(selector)
632
+ };
633
+ onAiApplyReady({
634
+ activeVersionId: loadedVersion,
635
+ apply: async (changes, opts) => enqueue(async () => {
636
+ const target = opts?.versionId;
637
+ const rejectAll = (detail) => ({
638
+ applied: [],
639
+ rejected: changes.map((c, index) => ({
640
+ index,
641
+ selector: typeof c?.selector === 'string' ? c.selector : '',
642
+ reason: 'VERSION_SWITCH_FAILED',
643
+ detail
644
+ })),
645
+ versionId: loadedVersionRef.current ?? ''
646
+ });
647
+ if (target && target !== loadedVersionRef.current) {
648
+ if (!hasVersionsRef.current) {
649
+ return rejectAll('host did not pass `versions`');
650
+ }
651
+ try {
652
+ await switchToVersion(target);
653
+ versionCallbacksRef.current.onVersionChange?.(target);
654
+ }
655
+ catch (e) {
656
+ return rejectAll(describeCanvasError(e));
657
+ }
658
+ }
659
+ const outcome = await applyAiChanges(changes, {
660
+ scope: opts?.grant ? { ...scope, grant: opts.grant } : scope,
661
+ resolve: async (selector) => {
662
+ const r = await resolveCanvasSelector(sessionIdForAiContext, selector);
663
+ return { matched: r.matched, matchCount: r.matchCount, viewportRect: r.viewportRect };
664
+ },
665
+ insertedSelectorsOf: (changeId) => changeListRef.current.peekList().find(r => r.changeId === changeId)
666
+ ?.insertedSelectors ?? [],
667
+ settle: () => new Promise(r => setTimeout(r, APPLY_CHECK_SETTLE_MS)),
668
+ viewportWidth: () => geometryRef.current?.viewportWidth,
669
+ getCapabilities: (selector) => getElementCapabilities(sessionIdForAiContext, selector),
670
+ addStyle: changeList.addStyle,
671
+ addContent: changeList.addContent,
672
+ addMove: changeList.addMove,
673
+ addAttribute: changeList.addAttribute,
674
+ addAction: changeList.addAction,
675
+ allowedImageOrigins: imageOrigins,
676
+ allowedImageUrls: allowedImageUrls ?? [],
677
+ allowedLinkUrls: allowedLinkUrls ?? [],
678
+ pageOrigin
679
+ });
680
+ for (const a of outcome.applied) {
681
+ for (const sel of a.insertedSelectors ?? []) {
682
+ if (!aiInsertedRef.current.includes(sel))
683
+ aiInsertedRef.current.push(sel);
684
+ }
685
+ }
686
+ const skipped = outcome.rejected
687
+ .map(r => `${r.reason}${r.detail ? `(${r.detail})` : ''}`)
688
+ .join(';');
689
+ setAiApplyNote(outcome.applied.length
690
+ ? t('inlineCanvas.ai.applyNote', 'AI 改动:{{ok}} 条已应用{{skip}}', {
691
+ ok: outcome.applied.length,
692
+ skip: skipped
693
+ ? `,${outcome.rejected.length} 条被跳过:${skipped}`
694
+ : ''
695
+ })
696
+ : t('inlineCanvas.ai.applyNoneNote', 'AI 改动全部被跳过:{{skip}}', {
697
+ skip: skipped
698
+ }));
699
+ return { ...outcome, versionId: loadedVersionRef.current ?? '' };
700
+ }),
701
+ revert: async (changeIds) => {
702
+ const mapped = changeIds.map(id => resolveChangeIdRef.current?.(id) ?? id);
703
+ const live = new Set(changeListRef.current.peekList().map(r => r.changeId));
704
+ if (mapped.some(id => !live.has(id))) {
705
+ return { ok: false, reason: 'VERSION_NOT_ACTIVE' };
706
+ }
707
+ for (const id of [...mapped].reverse()) {
708
+ await changeListRef.current.revert(id);
709
+ }
710
+ return { ok: true };
711
+ }
712
+ });
713
+ return () => onAiApplyReady(null);
714
+ }, [
715
+ onAiApplyReady,
716
+ onPageContextReady,
717
+ selection,
718
+ sessionIdForAiContext,
719
+ changeList.addStyle,
720
+ changeList.addContent,
721
+ changeList.addMove,
722
+ changeList.addAttribute,
723
+ changeList.addAction,
724
+ imageOrigins,
725
+ allowedImageUrls,
726
+ allowedLinkUrls,
727
+ pageOrigin,
728
+ enqueue,
729
+ switchToVersion,
730
+ loadedVersion,
731
+ t
732
+ ]);
733
+ const onToolbarAction = useCallback((buttonId) => {
734
+ const self = selection?.chain[0];
735
+ const sessionId = frameProps?.sessionId;
736
+ if (!self || !sessionId)
737
+ return;
738
+ if (switchingRef.current) {
739
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
740
+ return;
741
+ }
742
+ if (buttonId === 'delete') {
743
+ uiWrite(() => deleteElement(depsOf(self)));
744
+ return;
745
+ }
746
+ if (buttonId === 'copy') {
747
+ setCommandError(null);
748
+ commands
749
+ .copyElement(self.selector)
750
+ .catch(error => setCommandError(describeCanvasError(error)));
751
+ return;
752
+ }
753
+ if (buttonId === 'paste') {
754
+ if (!clipboard) {
755
+ setCommandError(t('inlineCanvas.clipboard.nothingToPaste'));
756
+ return;
757
+ }
758
+ setPasteDialog(true);
759
+ return;
760
+ }
761
+ if (buttonId === 'move') {
762
+ setCommandError(null);
763
+ commands.startMove(self.selector);
764
+ return;
765
+ }
766
+ if (buttonId === 'replace-image') {
767
+ setImageDialog({ open: true, seed: '' });
768
+ return;
769
+ }
770
+ if (buttonId === 'replace-video') {
771
+ setVideoDialog({ open: true, seed: '' });
772
+ return;
773
+ }
774
+ if (buttonId === 'insert') {
775
+ setInsertDialog(true);
776
+ return;
777
+ }
778
+ if (buttonId === 'image-setting') {
779
+ setImageSettingDialog({ open: true, seed: '' });
780
+ return;
781
+ }
782
+ if (buttonId === 'click-action') {
783
+ setActionDialog({ open: true, seed: null });
784
+ return;
785
+ }
786
+ if (buttonId === 'edit-style') {
787
+ setStylePanelOpen(true);
788
+ return;
789
+ }
790
+ if (buttonId === 'edit-html') {
791
+ void getOuterHtml(sessionId, self.selector).then(r => setHtmlDialog({ open: true, seed: r.html }));
792
+ return;
793
+ }
794
+ if (buttonId === 'ask-ai') {
795
+ if (!onAskAi)
796
+ return;
797
+ setCommandError(null);
798
+ void getOuterHtml(sessionId, self.selector)
799
+ .then(r => onAskAi(buildAiContext(self, r.html), () => captureAiScreenshot(self.selector)))
800
+ .catch(error => setCommandError(describeCanvasError(error)));
801
+ return;
802
+ }
803
+ if (buttonId === 'screenshot') {
804
+ setCommandError(null);
805
+ setShot(null);
806
+ setShotUrl(null);
807
+ commands
808
+ .captureSelectedElement(self.selector)
809
+ .then(async (outcome) => {
810
+ setShot(outcome);
811
+ if (!uploadImage)
812
+ return;
813
+ const blob = await (await fetch(outcome.dataUrl)).blob();
814
+ setShotUrl(await uploadImage(blob));
815
+ })
816
+ .catch(error => setCommandError(describeCanvasError(error)));
817
+ return;
818
+ }
819
+ if (buttonId === 'edit-text') {
820
+ canvasIframeRef.current?.focus();
821
+ setCommandError(null);
822
+ commands
823
+ .startTextEdit(self.selector)
824
+ .catch(error => setCommandError(describeCanvasError(error)));
825
+ }
826
+ }, [clipboard, commands, depsOf, frameProps?.sessionId, selection, t, uploadImage]);
827
+ const self = selection?.chain[0];
828
+ return (_jsxs("div", { className: `${CANVAS_UI_SCOPE} flex h-full min-h-0 flex-col bg-[#F5F7FC]`, children: [_jsx(EditorChrome, { variant: chrome, title: title, versionLabel: versionLabel, versions: versions, activeVersionId: activeVersionId, versionSwitching: switching, onVersionChange: onVersionChange, changeCount: changeList.list.length, onExit: onExit, onSave: () => {
829
+ if (switchingRef.current) {
830
+ setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
831
+ return;
832
+ }
833
+ onSave?.(editor);
834
+ }, onAddVersion: onAddVersion, onRenameVersion: onRenameVersion, onPreview: onPreview, onToggleChangeList: () => setShowChangeList(v => !v), changeListOpen: showChangeList, onToggleAi: onToggleAi, aiOpen: aiOpen }), _jsxs("div", { className: "relative flex min-h-0 flex-1", children: [_jsxs(ResizablePanelGroup, { direction: "horizontal", autoSaveId: "inline-editor-panes-v2", className: "flex min-h-0 w-full flex-1", children: [showLeftPanel && (_jsxs(_Fragment, { children: [_jsx(ResizablePanel, { id: "left", order: 1, defaultSize: 18, minSize: 12, maxSize: 40, className: "flex min-h-0 flex-col", children: _jsxs("aside", { "data-testid": "inline-editor-left-panel", className: "inline-editor-left flex h-full min-h-0 flex-col bg-white", children: [!!libraryComponents && (_jsx("div", { className: "flex h-9 shrink-0 items-center gap-1 border-b\n border-[#EBECF0] px-2", children: [
835
+ ['layers', '图层'],
836
+ ['assets', '组件']
837
+ ]
838
+ .map(([tab, label]) => (_jsx("button", { type: "button", "data-testid": `left-tab-${tab}`, "aria-pressed": leftTab === tab, onClick: () => setLeftTab(tab), className: `rounded px-2 py-1 text-[12px] ${leftTab === tab
839
+ ? 'bg-[#F4F5F7] font-semibold text-[#091E42]'
840
+ : 'text-[#6B778C]'}`, children: label }, tab))) })), _jsx("div", { className: "min-h-0 flex-1 overflow-hidden", children: leftTab === 'assets' && libraryComponents ? (_jsx(AssetsPanel, { components: libraryComponents, loading: libraryLoading, canInsert: !!selection?.chain[0] && !switching, onInsert: component => void insertComponent(component) })) : (_jsx(LayersPanel, { tree: tree, selectedSelector: selection?.selector ?? null, onSelect: s => void selectAndReveal(s), onHover: s => void commands.hover(s), onExpand: id => void commands.expandTreeNode(id), onMove: (dragSelector, targetSelector, pos) => uiWrite(() => writes.addMove({
841
+ identifier: dragSelector,
842
+ selector: dragSelector,
843
+ targetIdentifier: targetSelector,
844
+ targetSelector,
845
+ mode: moveModeFromDropPos(pos)
846
+ })) })) })] }) }), _jsx(ResizableHandle, { withHandle: true })] })), _jsx(ResizablePanel, { id: "canvas", order: 2, minSize: 30, children: _jsxs("div", { className: "flex h-full min-h-0 min-w-0 flex-col", children: [_jsx(CanvasContextBar, { url: url, device: device, onDeviceChange: onDeviceChange }), _jsx("div", { ref: canvasAreaRef, className: "relative min-h-0 flex-1 overflow-hidden p-3", children: frameProps && (_jsx(CanvasFrame, { url: frameProps.url, sessionId: frameProps.sessionId, device: frameProps.device, panelWidth: panelWidth, targetViewportWidth: targetViewportWidth[frameProps.device], onFrameLoad: () => { }, domReady: domReady, iframeRef: canvasIframeRef }, frameProps.sessionId)) })] }) })] }), !capturing && shot && (_jsxs("section", { "data-testid": "inline-editor-shot", className: "absolute bottom-4 right-4 z-30 w-[260px] rounded-lg border\n border-[#DFE1E6] bg-white p-3\n shadow-[0_8px_24px_rgba(9,30,66,0.14),0_2px_8px_rgba(9,30,66,0.08)]", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("span", { className: "text-[13px] font-semibold text-[#091E42]", children: "\u622A\u56FE" }), _jsx("button", { type: "button", className: "text-[11px] text-[#6B778C]", onClick: () => {
847
+ setShot(null);
848
+ setShotUrl(null);
849
+ }, children: "\u5173\u95ED" })] }), _jsx("img", { src: shot.dataUrl, alt: "", className: "max-h-40 w-full rounded border border-[#EBECF0] object-contain" }), shot.truncated && (_jsx("p", { "data-testid": "inline-editor-shot-truncated", className: "mt-1.5 text-[11px] leading-4 text-[#DE350B]", children: "\u5143\u7D20\u8D85\u51FA\u53EF\u89C6\u533A\uFF0C\u53EA\u622A\u5230\u53EF\u89C1\u90E8\u5206" })), shot.scale < 1 && (_jsxs("p", { "data-testid": "inline-editor-shot-scale", className: "mt-1.5 text-[11px] leading-4 text-[#6B778C]", children: ["\u5F53\u524D\u7A97\u53E3\u8F83\u7A84\uFF0C\u622A\u56FE\u5206\u8FA8\u7387\u7EA6\u4E3A\u539F\u56FE\u7684", ` ${Math.round(shot.scale * 100)}%`] })), shotUrl && (_jsx("a", { "data-testid": "inline-editor-shot-url", href: shotUrl, target: "_blank", rel: "noopener noreferrer", className: "mt-1.5 block truncate text-[11px] text-[#0052CC] underline", children: shotUrl }))] })), _jsx(ChangeListPopover, { open: showChangeList && !capturing, onClose: () => setShowChangeList(false), children: _jsx(ChangeListPanel, { list: changeList.list, onRevert: id => uiWrite(() => writes.revert(id)), onRevertAll: () => uiWrite(() => writes.revertAll()), lastError: changeList.lastError, onHover: s => void commands.hover(s), onSelect: s => void selectAndReveal(s) }) })] }), !capturing && (_jsx(SelectionOverlay, { geometry: geometry.latest, context: overlayContext, selectedLabel: self ? `<${self.tag}>` : undefined })), !capturing && blockOverlay && (_jsx(BlockOverlay, { blocks: blockOverlay.blocks, activeId: blockOverlay.activeId, onHover: blockOverlay.onHover, geometry: geometry.latest, context: overlayContext, scale: editor.mobileFit.scale })), !capturing && commandError && (_jsx("div", { "data-testid": "inline-editor-command-error", className: "fixed z-[2147483003] -translate-x-1/2 rounded bg-[#DE350B] px-2.5\n py-1 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, onClick: () => setCommandError(null), children: commandError })), !capturing && aiApplyNote && (_jsx("div", { "data-testid": "inline-editor-ai-apply-note", className: "fixed z-[2147483003] max-w-[520px] -translate-x-1/2 rounded\n bg-[#253858] px-2.5 py-1 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, onClick: () => setAiApplyNote(null), children: aiApplyNote })), !capturing && move && (_jsxs("div", { "data-testid": "inline-editor-move-bar", className: "fixed z-[2147483003] flex -translate-x-1/2 items-center gap-2\n rounded bg-[#091E42] px-2.5 py-1.5 text-[11px] leading-4 text-white shadow", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [move.target === null ? (_jsx(_Fragment, { children: _jsx("span", { children: move.rejected === 'same-element'
850
+ ? '不能移到它自己里面,请另选一个目标'
851
+ : t('inlineCanvas.move.pickTarget') }) })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: t('inlineCanvas.move.position') }), ['top', 'bottom', 'before', 'after'].map(pos => (_jsx("button", { type: "button", "data-testid": `move-position-${pos}`, className: "rounded bg-white/15 px-1.5 py-0.5 hover:bg-white/25", onClick: () => void commands
852
+ .confirmMove(pos)
853
+ .catch(e => setCommandError(describeCanvasError(e))), children: t(`inlineCanvas.move.${pos}`) }, pos)))] })), _jsx("button", { type: "button", "data-testid": "move-cancel", className: "ml-1 text-white/70 underline", onClick: () => commands.cancelMove(), children: t('inlineCanvas.move.cancel') })] })), !capturing && textEditing !== null && (_jsx("div", { "data-testid": "inline-editor-text-editing-hint", className: "pointer-events-none fixed z-[2147483003] -translate-x-1/2 rounded\n bg-[#091E42] px-2.5 py-1 text-[11px] leading-4 text-white shadow", style: {
854
+ left: `${area.x + area.width / 2}px`,
855
+ top: `${area.y + 8}px`
856
+ }, children: "\u6B63\u5728\u7F16\u8F91\u6587\u5B57 \u00B7 \u56DE\u8F66\u63D0\u4EA4 \u00B7 Esc \u53D6\u6D88" })), !capturing && !switching && (_jsx(SelectionToolbar, { selectedRect: selectedRect, area: area, capabilities: selection?.capabilities ?? null, isScrolling: geometry.isScrolling, onAction: onToolbarAction, host: { askAi: !!onAskAi } })), stylePanelOpen && !switching && selectedRect && self && (_jsx("div", { className: "fixed z-[2147483002]", style: {
857
+ left: `${Math.min(selectedRect.x + selectedRect.width + 12, area.x + area.width - 332)}px`,
858
+ top: `${Math.max(area.y + 8, selectedRect.y)}px`
859
+ }, children: _jsx(StylePanel, { tag: self.tag, capabilities: selection?.capabilities ?? null, declaredStyles: selection?.declaredStyles, onClose: () => setStylePanelOpen(false), onApply: (property, value) => uiWrite(() => editStyle(depsOf(self), property, value)) }) })), _jsx(Dialog, { open: pasteDialog, onOpenChange: setPasteDialog, children: _jsxs(DialogContent, { className: "max-w-[480px]", "data-testid": "paste-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.clipboard.pastePosition') }) }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: (selection?.capabilities ? pasteModes(selection.capabilities) : []).map(mode => (_jsx("button", { type: "button", "data-testid": `paste-mode-${mode}`, className: "rounded border border-[#DFE1E6] px-2 py-1 text-[12px]\n text-[#344563] hover:border-[#00A92E]", onClick: () => {
860
+ if (!self)
861
+ return;
862
+ setPasteDialog(false);
863
+ void commands
864
+ .pasteInto(self.selector, mode)
865
+ .catch(e => setCommandError(describeCanvasError(e)));
866
+ }, children: t(`inlineCanvas.clipboard.${mode}`) }, mode))) })] }) }), _jsx(ImageEditDialog, { open: imageDialog.open, capabilities: selection?.capabilities ?? null, seed: imageDialog.seed, uploadImage: uploadImage, onOpenChange: open => setImageDialog(prev => ({ ...prev, open })), onApply: (input) => self && uiWrite(() => replaceImage(depsOf(self), input)) }), _jsx(ReplaceVideoDialog, { open: videoDialog.open, seed: videoDialog.seed, onOpenChange: open => setVideoDialog(prev => ({ ...prev, open })), onApply: url => self && uiWrite(() => replaceVideo(depsOf(self), url)) }), _jsx(InsertElementDialog, { open: insertDialog, capabilities: selection?.capabilities ?? null, onOpenChange: setInsertDialog, onApply: input => self && uiWrite(() => insertElement(depsOf(self), input)) }), _jsx(ImageSettingDialog, { open: imageSettingDialog.open, seed: imageSettingDialog.seed, onOpenChange: open => setImageSettingDialog(prev => ({ ...prev, open })), onApply: alt => self && uiWrite(() => setImageAlt(depsOf(self), alt)) }), _jsx(ClickActionDialog, { open: actionDialog.open, seed: actionDialog.seed, onOpenChange: open => setActionDialog(prev => ({ ...prev, open })), onApply: action => self && uiWrite(() => setClickAction(depsOf(self), action)) }), _jsx(HtmlEditDialog, { open: htmlDialog.open, seed: htmlDialog.seed, onOpenChange: open => setHtmlDialog(prev => ({ ...prev, open })), onApply: html => self && uiWrite(() => editHtml(depsOf(self), html)) })] }));
867
+ }