@ptengine/inline-editor 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9
4
+
5
+ 依赖不变(`@ptengine/inline-core` ~0.2.1),可以直接发。
6
+
7
+ **新增**
8
+
9
+ - 自定义代码(老编辑器的「追加代码」/ code mode):画布条新增「追加代码」按钮,打开面板给当前版本追加 CSS 与 JavaScript。
10
+ - `InlineEditor` 新增受控属性 `code`(当前版本的 `ExperienceCode[]`)与 `onCodeChange(code, versionId)`(带上代码所属版本:切版本时面板卸载补交的输入属于旧版本);不传 `onCodeChange` 则没有按钮(对照组、只读画布),已有代码照样注入。
11
+ - 改即生效:输入停顿约 400ms 后整份交给宿主,关面板 / 换版本时立即交出;空白段不写。Tab 键插两个空格。
12
+ - CSS 经 `setCode` 实时注入画布,与组件库资产合并发送(全量替换语义,只发一边会清掉另一边);会话就绪 / 重建后重新注入。
13
+ - JavaScript 只保存不在画布执行(与老编辑器一致),面板里提示通过预览 / 发布查看,并说明 ptKit 工具函数。
14
+ - 有代码时改动清单计数 +1,清单顶上显示「自定义代码」行,点它打开面板;按钮上带圆点提示。代码不进撤销历史。
15
+ - 交互模式中按钮禁用、面板收起。
16
+
3
17
  ## 0.1.8
4
18
 
5
19
  依赖不变(`@ptengine/inline-core` ~0.2.1),可以直接发。
@@ -6,6 +6,7 @@ import { type AnchorShot, type ElementAiContext } from './kernel/selection/eleme
6
6
  import { type ShelfBlockHint } from './kernel/selection/buildRootShelf.js';
7
7
  import { type InlinePlanAnchor } from './kernel/selection/resolveAnchors.js';
8
8
  import { type AiApplyOutcome, type AiScopeGrant, type AiWireChange } from './kernel/ai/aiInlineApply.js';
9
+ import type { ExperienceCode } from '@ptengine/inline-core/protocol';
9
10
  import { type LibraryComponent } from './assets/componentLibrary.js';
10
11
  import { type UseInlineEditorResult } from './useInlineEditor.js';
11
12
  import type { ChangeRecord, HydrateOutcome } from './kernel/changelist/useChangeList.js';
@@ -193,6 +194,18 @@ export interface InlineEditorProps {
193
194
  interactiveMode?: boolean;
194
195
  /** 进出交互模式时通知宿主(宿主自己的浮层要不要让路由它决定)。 */
195
196
  onInteractiveChange?: (on: boolean) => void;
197
+ /**
198
+ * 当前版本的自定义代码(老编辑器的「追加代码」/ code mode),**受控**:宿主按版本持有,
199
+ * 切版本时换成那一版的。CSS 实时注入画布,JavaScript 只存不执行(见 `CodePanel`)。
200
+ */
201
+ code?: ExperienceCode[];
202
+ /**
203
+ * 自定义代码改了(输入停顿后整份交出),带上**这份代码属于哪一版**。⚠️ 宿主要按这个 id 存,
204
+ * 不能按「当前版本」存:切版本时面板卸载会把停顿窗口里没交出的那一下补交出来,那一刻宿主的
205
+ * 当前版本已经是新的了。**不传 = 这一版不能加代码**:画布条上没有「追加代码」
206
+ * 按钮(对照组、只读画布);已有的 `code` 仍会注入画布。
207
+ */
208
+ onCodeChange?: (code: ExperienceCode[], versionId: string | undefined) => void;
196
209
  /**
197
210
  * 选中变化 → 新的 AI 上下文。`null` = 选中被清空。
198
211
  *
@@ -363,4 +376,4 @@ export interface AiApplyApi {
363
376
  /** 当前版本 —— 宿主把 agent 给的版本**名**映射成 id 时要知道现在在哪一版。 */
364
377
  readonly activeVersionId?: string;
365
378
  }
366
- export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay, interactiveMode, onInteractiveChange }: InlineEditorProps): import("react").JSX.Element;
379
+ export declare function InlineEditor({ url, device, onDeviceChange, title, versionLabel, chrome, onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay, interactiveMode, onInteractiveChange, code, onCodeChange }: InlineEditorProps): import("react").JSX.Element;
@@ -39,6 +39,7 @@ import { StylePanel } from './panels/StylePanel.js';
39
39
  import { STYLE_SECTIONS } from './panels/styleFields.js';
40
40
  import { HtmlEditDialog } from './dialogs/HtmlEditDialog.js';
41
41
  import { setCode } from './kernel/canvasCommands.js';
42
+ import { CodePanel } from './panels/CodePanel.js';
42
43
  import { describeCanvasError } from './kernel/canvasErrors.js';
43
44
  import { LayersPanel } from './tree/LayersPanel.js';
44
45
  import { AssetsPanel } from './assets/AssetsPanel.js';
@@ -56,7 +57,7 @@ function imageTargetOf(caps) {
56
57
  return branch === 'background' ? 'background' : undefined;
57
58
  }
58
59
  const AI_ELEMENT_IMAGE_LIMIT = 4;
59
- export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay, interactiveMode = true, onInteractiveChange }) {
60
+ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel = 'V1', chrome = 'full', onExit, onSave, libraryComponents, libraryLoading, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, onPreview, onToggleAi, aiOpen, uploadImage, initialChanges, onHydrated, onAskAi, onPageContextReady, anchors, pageBlocks, onEditorReady, onSessionReady, onAiContextChange, onAiApplyReady, onAiInsertedRemoved, onAiInsertedRestored, trackAiSelection, renderNodeIcon, versions, activeVersionId, versionChanges, onVersionChange, onVersionLeave, onVersionEnter, resolveChangeId, allowedLinkUrls, viewportWidthOverride, blockOverlay, interactiveMode = true, onInteractiveChange, code, onCodeChange }) {
60
61
  const { t } = useTranslation();
61
62
  const canvasAreaRef = useRef(null);
62
63
  const canvasIframeRef = useRef(null);
@@ -281,6 +282,7 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
281
282
  setStylePanelOpen(false);
282
283
  setShowChangeList(false);
283
284
  setPasteDialog(false);
285
+ setCodePanelOpen(false);
284
286
  void enqueue(() => editor.setInteractive(true))
285
287
  .then(() => interactiveChangeRef.current?.(true))
286
288
  .catch(error => setCommandError(describeCanvasError(error)));
@@ -408,6 +410,29 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
408
410
  addAction: writes.addAction
409
411
  }), [writes, selection]);
410
412
  const injectedAssetsRef = useRef([]);
413
+ const userCode = useMemo(() => (Array.isArray(code) ? code : []), [code]);
414
+ const userCodeRef = useRef(userCode);
415
+ userCodeRef.current = userCode;
416
+ const userCodeKey = JSON.stringify(userCode);
417
+ const codeSyncedRef = useRef(null);
418
+ useEffect(() => {
419
+ const sessionId = frameProps?.sessionId;
420
+ if (!domReady || !sessionId)
421
+ return;
422
+ const last = codeSyncedRef.current;
423
+ if (last && last.sessionId === sessionId && last.key === userCodeKey)
424
+ return;
425
+ if ((!last || last.sessionId !== sessionId) &&
426
+ userCode.length === 0 &&
427
+ !injectedAssetsRef.current.length) {
428
+ codeSyncedRef.current = { sessionId, key: userCodeKey };
429
+ return;
430
+ }
431
+ codeSyncedRef.current = { sessionId, key: userCodeKey };
432
+ void setCode(sessionId, [...injectedAssetsRef.current, ...userCode]).catch(error => setCommandError(describeCanvasError(error)));
433
+ }, [domReady, frameProps?.sessionId, userCodeKey]);
434
+ const hasUserCode = userCode.some(c => c.code.trim() !== '');
435
+ const [codePanelOpen, setCodePanelOpen] = useState(false);
411
436
  const insertComponent = useCallback(async (component) => {
412
437
  const target = selection?.chain[0];
413
438
  const sessionId = frameProps?.sessionId;
@@ -422,7 +447,10 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
422
447
  const { html, codeList } = componentInsertPayload(component);
423
448
  if (codeList.length > 0) {
424
449
  injectedAssetsRef.current = [...injectedAssetsRef.current, ...codeList];
425
- await setCode(sessionId, injectedAssetsRef.current);
450
+ await setCode(sessionId, [
451
+ ...injectedAssetsRef.current,
452
+ ...userCodeRef.current
453
+ ]);
426
454
  }
427
455
  await writes.addContent({
428
456
  identifier: target.selector,
@@ -1030,7 +1058,7 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
1030
1058
  }
1031
1059
  }, [clipboard, commands, depsOf, frameProps?.sessionId, selection, t, uploadImage]);
1032
1060
  const self = selection?.chain[0];
1033
- return (_jsxs("div", { className: `${CANVAS_UI_SCOPE} flex h-full min-h-0 flex-col bg-[#F5F7FC]`, children: [_jsxs("div", { className: "relative shrink-0", children: [interactive && (_jsxs("div", { role: "status", "data-testid": "inline-editor-interactive-bar", className: "absolute inset-0 z-20 flex items-center justify-center gap-3\n border-b border-[#ececef] bg-white text-[13px] text-[#1a1a1a]\n shadow-[0_4px_12px_0_rgba(9,30,66,0.12)]", children: [_jsx("span", { children: t('inlineCanvas.interactive.active', '您当前正在交互模式') }), _jsx(Button, { size: "sm", variant: "secondary", "data-testid": "inline-editor-interactive-exit", onClick: exitInteractive, children: t('inlineCanvas.interactive.exit', '返回编辑模式') })] })), _jsx(EditorChrome, { slotRight: capturing ? (_jsx("span", { role: "status", className: "text-xs text-muted-foreground", children: t('inlineCanvas.review.generating', '正在生成截图…') })) : undefined, variant: chrome, title: title, versionLabel: versionLabel, versions: versions, activeVersionId: activeVersionId, versionSwitching: switching, onVersionChange: onVersionChange, canUndo: !switching && textEditing === null && changeList.canUndo, canRedo: !switching && textEditing === null && changeList.canRedo, onUndo: () => uiWrite(() => enqueue(() => changeListRef.current.undo())), onRedo: () => uiWrite(() => enqueue(() => changeListRef.current.redo())), changeCount: changeList.list.length, onExit: onExit, onSave: () => {
1061
+ return (_jsxs("div", { className: `${CANVAS_UI_SCOPE} flex h-full min-h-0 flex-col bg-[#F5F7FC]`, children: [_jsxs("div", { className: "relative shrink-0", children: [interactive && (_jsxs("div", { role: "status", "data-testid": "inline-editor-interactive-bar", className: "absolute inset-0 z-20 flex items-center justify-center gap-3\n border-b border-[#ececef] bg-white text-[13px] text-[#1a1a1a]\n shadow-[0_4px_12px_0_rgba(9,30,66,0.12)]", children: [_jsx("span", { children: t('inlineCanvas.interactive.active', '您当前正在交互模式') }), _jsx(Button, { size: "sm", variant: "secondary", "data-testid": "inline-editor-interactive-exit", onClick: exitInteractive, children: t('inlineCanvas.interactive.exit', '返回编辑模式') })] })), _jsx(EditorChrome, { slotRight: capturing ? (_jsx("span", { role: "status", className: "text-xs text-muted-foreground", children: t('inlineCanvas.review.generating', '正在生成截图…') })) : undefined, variant: chrome, title: title, versionLabel: versionLabel, versions: versions, activeVersionId: activeVersionId, versionSwitching: switching, onVersionChange: onVersionChange, canUndo: !switching && textEditing === null && changeList.canUndo, canRedo: !switching && textEditing === null && changeList.canRedo, onUndo: () => uiWrite(() => enqueue(() => changeListRef.current.undo())), onRedo: () => uiWrite(() => enqueue(() => changeListRef.current.redo())), changeCount: changeList.list.length + (hasUserCode ? 1 : 0), onExit: onExit, onSave: () => {
1034
1062
  if (switchingRef.current) {
1035
1063
  setCommandError(t('inlineCanvas.version.switching', '正在切换版本,请稍候'));
1036
1064
  return;
@@ -1055,15 +1083,34 @@ export function InlineEditor({ url, device, onDeviceChange, title, versionLabel
1055
1083
  ? exitInteractive()
1056
1084
  : enterInteractive()
1057
1085
  }
1086
+ : undefined, code: onCodeChange
1087
+ ? {
1088
+ active: codePanelOpen,
1089
+ hasCode: hasUserCode,
1090
+ disabled: interactive,
1091
+ onToggle: () => setCodePanelOpen(v => !v)
1092
+ }
1058
1093
  : undefined }), _jsxs("div", { ref: canvasAreaRef, className: "relative min-h-0 flex-1 overflow-hidden p-3", children: [editor.session.state === 'failed' && (_jsxs("div", { role: "alert", "data-testid": "inline-editor-session-failed", className: "absolute inset-3 z-20 flex flex-col items-center justify-center gap-3 rounded-lg bg-white p-6 text-center text-sm", children: [_jsx("p", { className: "font-semibold", children: t('inlineCanvas.review.pageLoadFailed', '目标页面无法加载') }), _jsx("p", { children: [
1059
1094
  'NO_EXTENSION',
1060
1095
  'EXTENSION_ID_MISSING',
1061
1096
  'CONTRACT_MISMATCH'
1062
1097
  ].includes(editor.session.reason ?? '')
1063
1098
  ? describeCanvasError(editor.session.reason)
1064
- : t('inlineCanvas.review.pageLoadHint', '请返回检查页面地址、网站访问权限和网络连接后重试。') }), _jsx("code", { className: "text-xs text-muted-foreground", children: editor.session.reason }), _jsx(Button, { variant: "secondary", onClick: onExit, children: t('inlineCanvas.review.backFromFailed', '返回修改') })] })), 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 && (_jsx(ScreenshotCard, { dataUrl: shot.dataUrl, truncated: shot.truncated, scale: shot.scale, uploadImage: uploadImage, onClose: () => {
1099
+ : t('inlineCanvas.review.pageLoadHint', '请返回检查页面地址、网站访问权限和网络连接后重试。') }), _jsx("code", { className: "text-xs text-muted-foreground", children: editor.session.reason }), _jsx(Button, { variant: "secondary", onClick: onExit, children: t('inlineCanvas.review.backFromFailed', '返回修改') })] })), codePanelOpen && onCodeChange && !interactive && (_jsx(CodePanel, { code: userCode, onChange: next => onCodeChange(next, activeVersionId), onClose: () => setCodePanelOpen(false) }, activeVersionId ?? 'single')), 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 && (_jsx(ScreenshotCard, { dataUrl: shot.dataUrl, truncated: shot.truncated, scale: shot.scale, uploadImage: uploadImage, onClose: () => {
1065
1100
  setShot(null);
1066
- } }, shot.dataUrl)), _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 && !interactive && (_jsx(SelectionOverlay, { geometry: geometry.latest, context: overlayContext, selectedLabel: self ? `<${self.tag}>` : undefined })), !capturing && !interactive && blockOverlay && (_jsx(BlockOverlay, { blocks: blockOverlay.blocks, activeId: blockOverlay.activeId, onHover: blockOverlay.onHover, geometry: geometry.latest, context: overlayContext, scale: editor.mobileFit.scale })), !capturing && commandError && (_jsxs(EditorStatus, { tone: "error", "data-testid": "inline-editor-command-error", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { children: commandError }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setCommandError(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && aiApplyNote && (_jsxs(EditorStatus, { "data-testid": "inline-editor-ai-apply-note", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { role: "status", children: aiApplyNote }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setAiApplyNote(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && !interactive && move && (_jsx("div", { role: "toolbar", "aria-label": t('inlineCanvas.move.title'), "data-testid": "inline-editor-move-bar", className: "fixed z-[903] -translate-x-1/2 rounded-lg border bg-popover p-3 text-xs text-popover-foreground shadow-md", style: {
1101
+ } }, shot.dataUrl)), _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), customCode: hasUserCode
1102
+ ? {
1103
+ types: userCode
1104
+ .filter(c => c.code.trim() !== '')
1105
+ .map(c => c.type),
1106
+ onOpen: onCodeChange
1107
+ ? () => {
1108
+ setShowChangeList(false);
1109
+ setCodePanelOpen(true);
1110
+ }
1111
+ : undefined
1112
+ }
1113
+ : undefined }) })] }), !capturing && !interactive && (_jsx(SelectionOverlay, { geometry: geometry.latest, context: overlayContext, selectedLabel: self ? `<${self.tag}>` : undefined })), !capturing && !interactive && blockOverlay && (_jsx(BlockOverlay, { blocks: blockOverlay.blocks, activeId: blockOverlay.activeId, onHover: blockOverlay.onHover, geometry: geometry.latest, context: overlayContext, scale: editor.mobileFit.scale })), !capturing && commandError && (_jsxs(EditorStatus, { tone: "error", "data-testid": "inline-editor-command-error", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { children: commandError }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setCommandError(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && aiApplyNote && (_jsxs(EditorStatus, { "data-testid": "inline-editor-ai-apply-note", style: { left: `${area.x + area.width / 2}px`, top: `${area.y + 8}px` }, children: [_jsx("span", { role: "status", children: aiApplyNote }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setAiApplyNote(null), children: t('inlineCanvas.chrome.close', '关闭') })] })), !capturing && !interactive && move && (_jsx("div", { role: "toolbar", "aria-label": t('inlineCanvas.move.title'), "data-testid": "inline-editor-move-bar", className: "fixed z-[903] -translate-x-1/2 rounded-lg border bg-popover p-3 text-xs text-popover-foreground shadow-md", style: {
1067
1114
  width: 'min(600px, calc(100vw - 32px))',
1068
1115
  left: `clamp(min(316px, 50vw), ${area.x + area.width / 2}px, max(calc(100vw - 316px), 50vw))`,
1069
1116
  top: `${area.y + 8}px`
@@ -32,5 +32,15 @@ export interface CanvasContextBarProps {
32
32
  disabled?: boolean;
33
33
  onToggle: () => void;
34
34
  };
35
+ /**
36
+ * 「追加代码」开关(自定义 CSS / JS 面板),挨着交互模式。不传 = 没有这个按钮(对照组、只读画布)。
37
+ * `hasCode` 时按钮上带个点,提示这一版已经有代码 —— 面板关着时代码是看不见的。
38
+ */
39
+ code?: {
40
+ active: boolean;
41
+ hasCode: boolean;
42
+ disabled?: boolean;
43
+ onToggle: () => void;
44
+ };
35
45
  }
36
- export declare function CanvasContextBar({ url, device, onDeviceChange, deviceDisabled, interactive }: CanvasContextBarProps): import("react").JSX.Element;
46
+ export declare function CanvasContextBar({ url, device, onDeviceChange, deviceDisabled, interactive, code }: CanvasContextBarProps): import("react").JSX.Element;
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
- import { Link2, Monitor, MousePointerClick, Smartphone } from 'lucide-react';
3
+ import { Code2, Link2, Monitor, MousePointerClick, Smartphone } from 'lucide-react';
4
4
  import { ChromeSegment, ChromeSegmented, ChromeTextButton } from '../chrome/topbarPrimitives.js';
5
- export function CanvasContextBar({ url, device, onDeviceChange, deviceDisabled, interactive }) {
5
+ export function CanvasContextBar({ url, device, onDeviceChange, deviceDisabled, interactive, code }) {
6
6
  const { t } = useTranslation();
7
7
  const items = [
8
8
  ['pc', Monitor, t('inlineCanvas.chrome.pc', 'PC')],
9
9
  ['mobile', Smartphone, t('inlineCanvas.chrome.mobile', '移动')]
10
10
  ];
11
- return (_jsxs("div", { "data-testid": "canvas-context-bar", className: "relative flex h-9 shrink-0 items-center border-b border-[#EBECF0]\n bg-white px-3", children: [url && (_jsxs("a", { "data-testid": "canvas-context-url", href: url, target: "_blank", rel: "noopener noreferrer", title: url, className: "flex max-w-[calc(50%-60px)] items-center gap-1.5 truncate text-xs\n text-[#8a8f98] hover:text-[#52525b] hover:underline", children: [_jsx(Link2, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: url })] })), _jsx(ChromeSegmented, { "data-testid": "device-switch", className: "absolute left-1/2 -translate-x-1/2", children: items.map(([value, Icon, label]) => (_jsxs(ChromeSegment, { "data-testid": `device-${value}`, active: device === value, disabled: deviceDisabled, onClick: () => onDeviceChange(value), children: [_jsx(Icon, { className: "size-3.5" }), label] }, value))) }), interactive && (_jsxs(ChromeTextButton, { "data-testid": "canvas-interactive-toggle", className: "ml-auto h-7 gap-1.5 px-2 text-xs", active: interactive.active, disabled: interactive.disabled, title: t('inlineCanvas.interactive.tips', '切换到交互模式可以对页面进行交互,以修改需要交互后才能看到的内容。'), onClick: interactive.onToggle, children: [_jsx(MousePointerClick, { className: "size-3.5" }), interactive.active
11
+ return (_jsxs("div", { "data-testid": "canvas-context-bar", className: "relative flex h-9 shrink-0 items-center border-b border-[#EBECF0]\n bg-white px-3", children: [url && (_jsxs("a", { "data-testid": "canvas-context-url", href: url, target: "_blank", rel: "noopener noreferrer", title: url, className: "flex max-w-[calc(50%-60px)] items-center gap-1.5 truncate text-xs\n text-[#8a8f98] hover:text-[#52525b] hover:underline", children: [_jsx(Link2, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: url })] })), _jsx(ChromeSegmented, { "data-testid": "device-switch", className: "absolute left-1/2 -translate-x-1/2", children: items.map(([value, Icon, label]) => (_jsxs(ChromeSegment, { "data-testid": `device-${value}`, active: device === value, disabled: deviceDisabled, onClick: () => onDeviceChange(value), children: [_jsx(Icon, { className: "size-3.5" }), label] }, value))) }), code && (_jsxs(ChromeTextButton, { "data-testid": "canvas-code-toggle", className: "ml-auto h-7 gap-1.5 px-2 text-xs", active: code.active, disabled: code.disabled, title: t('inlineCanvas.code.desc', '可以追加 CSS / JavaScript 代码,更自由地编辑您的网页。'), onClick: code.onToggle, children: [_jsx(Code2, { className: "size-3.5" }), t('inlineCanvas.code.button', '追加代码'), code.hasCode && (_jsx("span", { "data-testid": "canvas-code-dot", "aria-hidden": true, className: "size-1.5 rounded-full bg-[#0052CC]" }))] })), interactive && (_jsxs(ChromeTextButton, { "data-testid": "canvas-interactive-toggle", className: `${code ? 'ml-1.5' : 'ml-auto'} h-7 gap-1.5 px-2 text-xs`, active: interactive.active, disabled: interactive.disabled, title: t('inlineCanvas.interactive.tips', '切换到交互模式可以对页面进行交互,以修改需要交互后才能看到的内容。'), onClick: interactive.onToggle, children: [_jsx(MousePointerClick, { className: "size-3.5" }), interactive.active
12
12
  ? t('inlineCanvas.interactive.exit', '返回编辑模式')
13
13
  : t('inlineCanvas.interactive.enter', '交互模式')] }))] }));
14
14
  }
@@ -9,5 +9,13 @@ export interface ChangeListPanelProps {
9
9
  onSelect?: (selector: string) => void;
10
10
  /** 全部撤销。省略则不显示该入口。 */
11
11
  onRevertAll?: () => void;
12
+ /**
13
+ * 这一版有自定义代码时在清单顶上占一行(老编辑器同款,计数也 +1)。它不是一条 change:
14
+ * 不能单独撤销,点它打开代码面板(`onOpen` 不传则不可点)。
15
+ */
16
+ customCode?: {
17
+ types: Array<'css' | 'javascript'>;
18
+ onOpen?: () => void;
19
+ };
12
20
  }
13
- export declare function ChangeListPanel({ list, onRevert, lastError, onHover, onSelect, onRevertAll }: ChangeListPanelProps): import("react").JSX.Element;
21
+ export declare function ChangeListPanel({ list, onRevert, lastError, onHover, onSelect, onRevertAll, customCode }: ChangeListPanelProps): import("react").JSX.Element;
@@ -15,7 +15,7 @@ const ICONS = {
15
15
  move: Move,
16
16
  insert: Plus
17
17
  };
18
- export function ChangeListPanel({ list, onRevert, lastError, onHover, onSelect, onRevertAll }) {
18
+ export function ChangeListPanel({ list, onRevert, lastError, onHover, onSelect, onRevertAll, customCode }) {
19
19
  const [confirmRevert, setConfirmRevert] = useState(false);
20
20
  const [view, setView] = useState('summary');
21
21
  const entries = useMemo(() => list.map(r => ({ change: r.change, ancestors: r.ancestors })), [list]);
@@ -23,14 +23,16 @@ export function ChangeListPanel({ list, onRevert, lastError, onHover, onSelect,
23
23
  return (_jsxs("section", { "data-testid": "change-list-panel", className: "flex min-h-0 flex-1 flex-col", onMouseLeave: () => onHover?.(''), children: [confirmRevert && (_jsxs("div", { role: "alert", className: "border-b p-3 text-xs", children: ["\u786E\u5B9A\u64A4\u9500\u6B64\u7248\u672C\u7684\u5168\u90E8\u6539\u52A8\uFF1F", _jsxs("div", { className: "mt-2 flex justify-end gap-2", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setConfirmRevert(false), children: "\u53D6\u6D88" }), _jsx(Button, { size: "sm", variant: "destructive", onClick: () => {
24
24
  setConfirmRevert(false);
25
25
  onRevertAll?.();
26
- }, children: "\u64A4\u9500\u5168\u90E8\u6539\u52A8" })] })] })), _jsxs("header", { className: "flex h-10 shrink-0 items-center justify-between gap-2 border-b\n border-[#f1f2f3] pl-3 pr-2", children: [_jsxs("span", { className: "truncate text-[13px] font-medium text-[#1a1a1a]", children: ["\u6539\u52A8\u6E05\u5355", _jsx("span", { "data-testid": "change-list-count", className: "ml-1.5 text-xs font-normal tabular-nums text-[#8a8f98]", children: list.length })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [_jsx("div", { className: "flex items-center gap-px rounded-md bg-[#f3f4f4] p-0.5", children: [
26
+ }, children: "\u64A4\u9500\u5168\u90E8\u6539\u52A8" })] })] })), _jsxs("header", { className: "flex h-10 shrink-0 items-center justify-between gap-2 border-b\n border-[#f1f2f3] pl-3 pr-2", children: [_jsxs("span", { className: "truncate text-[13px] font-medium text-[#1a1a1a]", children: ["\u6539\u52A8\u6E05\u5355", _jsx("span", { "data-testid": "change-list-count", className: "ml-1.5 text-xs font-normal tabular-nums text-[#8a8f98]", children: list.length + (customCode ? 1 : 0) })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [_jsx("div", { className: "flex items-center gap-px rounded-md bg-[#f3f4f4] p-0.5", children: [
27
27
  ['summary', '摘要', undefined],
28
28
  [
29
29
  'wire',
30
30
  '高级数据',
31
31
  '查看此版本的高级修改数据'
32
32
  ]
33
- ].map(([v, label, hint]) => (_jsx(Button, { type: "button", "data-testid": `change-list-view-${v}`, "aria-pressed": view === v, title: hint, onClick: () => setView(v), size: "sm", variant: view === v ? 'secondary' : 'ghost', children: label }, v))) }), onRevertAll && list.length > 0 && (_jsx(Button, { type: "button", "data-testid": "change-list-revert-all", onClick: () => setConfirmRevert(true), size: "sm", variant: "ghost", children: "\u5168\u90E8\u64A4\u9500" }))] })] }), lastError && (_jsx("p", { "data-testid": "change-list-error", className: "px-3 py-2 text-xs text-[#DE350B]", children: lastError })), list.length === 0 ? (_jsx("p", { "data-testid": "change-list-empty", className: "px-3 py-8 text-center text-xs text-[#8a8f98]", children: "\u8FD8\u6CA1\u6709\u6539\u52A8\u3002\u5728\u753B\u5E03\u91CC\u9009\u4E2D\u5143\u7D20\u5F00\u59CB\u7F16\u8F91\u3002" })) : view === 'summary' ? (_jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto", children: list.map((record, index) => (_jsx(ChangeRow, { record: record, summary: summarizeChange(record.change), suppression: classifySuppression(entries, index), onRevert: onRevert, onHover: onHover, onSelect: onSelect }, record.changeId))) })) : (_jsx("pre", { "data-testid": "change-list-json", className: "min-h-0 flex-1 overflow-auto bg-[#FAFAFA] px-3 py-2 text-[11px]\n leading-4 text-[#344563]", children: json }))] }));
33
+ ].map(([v, label, hint]) => (_jsx(Button, { type: "button", "data-testid": `change-list-view-${v}`, "aria-pressed": view === v, title: hint, onClick: () => setView(v), size: "sm", variant: view === v ? 'secondary' : 'ghost', children: label }, v))) }), onRevertAll && list.length > 0 && (_jsx(Button, { type: "button", "data-testid": "change-list-revert-all", onClick: () => setConfirmRevert(true), size: "sm", variant: "ghost", children: "\u5168\u90E8\u64A4\u9500" }))] })] }), lastError && (_jsx("p", { "data-testid": "change-list-error", className: "px-3 py-2 text-xs text-[#DE350B]", children: lastError })), customCode && (_jsxs("button", { type: "button", "data-testid": "change-list-custom-code", disabled: !customCode.onOpen, onClick: customCode.onOpen, className: "flex shrink-0 items-center gap-2 border-b border-[#f1f2f3] px-3 py-2 text-left\n text-xs text-[#1a1a1a] enabled:hover:bg-[#f7f8f9]", children: [_jsx("span", { className: "font-medium", children: "\u81EA\u5B9A\u4E49\u4EE3\u7801" }), _jsx("span", { className: "text-[#8a8f98]", children: customCode.types
34
+ .map(t => (t === 'css' ? 'CSS' : 'JavaScript'))
35
+ .join(' / ') })] })), list.length === 0 && customCode ? null : list.length === 0 ? (_jsx("p", { "data-testid": "change-list-empty", className: "px-3 py-8 text-center text-xs text-[#8a8f98]", children: "\u8FD8\u6CA1\u6709\u6539\u52A8\u3002\u5728\u753B\u5E03\u91CC\u9009\u4E2D\u5143\u7D20\u5F00\u59CB\u7F16\u8F91\u3002" })) : view === 'summary' ? (_jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto", children: list.map((record, index) => (_jsx(ChangeRow, { record: record, summary: summarizeChange(record.change), suppression: classifySuppression(entries, index), onRevert: onRevert, onHover: onHover, onSelect: onSelect }, record.changeId))) })) : (_jsx("pre", { "data-testid": "change-list-json", className: "min-h-0 flex-1 overflow-auto bg-[#FAFAFA] px-3 py-2 text-[11px]\n leading-4 text-[#344563]", children: json }))] }));
34
36
  }
35
37
  const SUPPRESSION_TEXT = {
36
38
  'ancestor-hidden': {
package/dist/locales.d.ts CHANGED
@@ -96,6 +96,13 @@ export declare const zhCN: {
96
96
  active: string;
97
97
  exit: string;
98
98
  };
99
+ code: {
100
+ button: string;
101
+ title: string;
102
+ desc: string;
103
+ cssHint: string;
104
+ jsHint: string;
105
+ };
99
106
  version: {
100
107
  switching: string;
101
108
  };
@@ -283,6 +290,13 @@ export declare const enUS: {
283
290
  active: string;
284
291
  exit: string;
285
292
  };
293
+ code: {
294
+ button: string;
295
+ title: string;
296
+ desc: string;
297
+ cssHint: string;
298
+ jsHint: string;
299
+ };
286
300
  version: {
287
301
  switching: string;
288
302
  };
@@ -470,6 +484,13 @@ export declare const jaJP: {
470
484
  active: string;
471
485
  exit: string;
472
486
  };
487
+ code: {
488
+ button: string;
489
+ title: string;
490
+ desc: string;
491
+ cssHint: string;
492
+ jsHint: string;
493
+ };
473
494
  version: {
474
495
  switching: string;
475
496
  };
@@ -707,6 +728,13 @@ export declare const inlineEditorLocales: {
707
728
  active: string;
708
729
  exit: string;
709
730
  };
731
+ readonly code: {
732
+ button: string;
733
+ title: string;
734
+ desc: string;
735
+ cssHint: string;
736
+ jsHint: string;
737
+ };
710
738
  readonly version: {
711
739
  switching: string;
712
740
  };
@@ -943,6 +971,13 @@ export declare const inlineEditorLocales: {
943
971
  active: string;
944
972
  exit: string;
945
973
  };
974
+ readonly code: {
975
+ button: string;
976
+ title: string;
977
+ desc: string;
978
+ cssHint: string;
979
+ jsHint: string;
980
+ };
946
981
  readonly version: {
947
982
  switching: string;
948
983
  };
@@ -1179,6 +1214,13 @@ export declare const inlineEditorLocales: {
1179
1214
  active: string;
1180
1215
  exit: string;
1181
1216
  };
1217
+ readonly code: {
1218
+ button: string;
1219
+ title: string;
1220
+ desc: string;
1221
+ cssHint: string;
1222
+ jsHint: string;
1223
+ };
1182
1224
  readonly version: {
1183
1225
  switching: string;
1184
1226
  };
package/dist/locales.js CHANGED
@@ -86,6 +86,13 @@ export const zhCN = {
86
86
  active: '您当前正在交互模式',
87
87
  exit: '返回编辑模式'
88
88
  },
89
+ code: {
90
+ button: '追加代码',
91
+ title: '自定义代码',
92
+ desc: '可以追加 CSS / JavaScript 代码,更自由地编辑您的网页。',
93
+ cssHint: 'CSS 会实时应用到画布。',
94
+ jsHint: 'JavaScript 不会在编辑器画布里执行,请通过预览或发布后查看效果。可使用 ptKit 工具函数(如 ptKit.query)等待元素出现。'
95
+ },
89
96
  version: {
90
97
  switching: '正在切换版本,请稍候'
91
98
  },
@@ -273,6 +280,13 @@ export const enUS = {
273
280
  active: 'You are now in interactive mode',
274
281
  exit: 'Back to edit mode'
275
282
  },
283
+ code: {
284
+ button: 'Custom code',
285
+ title: 'Custom code',
286
+ desc: 'Add CSS / JavaScript to customize your page more freely.',
287
+ cssHint: 'CSS is applied to the canvas in real time.',
288
+ jsHint: 'JavaScript does not run in the editor canvas. Use preview or publish to see its effect. ptKit helpers (e.g. ptKit.query) can wait for elements to appear.'
289
+ },
276
290
  version: {
277
291
  switching: 'Switching version, please wait'
278
292
  },
@@ -460,6 +474,13 @@ export const jaJP = {
460
474
  active: '現在、画面操作モードです',
461
475
  exit: '編集モードに戻る'
462
476
  },
477
+ code: {
478
+ button: 'コード追加',
479
+ title: 'カスタムコード',
480
+ desc: 'CSS / JavaScript を追加して、ページをより自由に編集できます。',
481
+ cssHint: 'CSS はキャンバスにリアルタイムで反映されます。',
482
+ jsHint: 'JavaScript はエディタのキャンバスでは実行されません。プレビューまたは公開後に効果を確認してください。ptKit のヘルパー(例: ptKit.query)で要素の表示を待つことができます。'
483
+ },
463
484
  version: {
464
485
  switching: 'バージョンを切り替えています。しばらくお待ちください'
465
486
  },
@@ -0,0 +1,20 @@
1
+ import type { ExperienceCode } from '@ptengine/inline-core/protocol';
2
+ /**
3
+ * 自定义代码面板(老编辑器的「追加代码」/ code mode):给当前版本追加 CSS 与 JavaScript。
4
+ *
5
+ * - **改即生效,没有「应用」按钮**(与老编辑器一致):输入停顿 `FLUSH_MS` 后把整份代码交给宿主,
6
+ * 宿主写回 `code` 属性,编辑器据此把 CSS 注入画布。关面板 / 换版本时立即交出未落的那一下。
7
+ * - JavaScript **只保存、画布里不执行**(老编辑器同样关掉了:编辑器里的页面环境不对、
8
+ * 客户页也没有 SDK 的 `ptKit`),预览或发布后才生效 —— 面板里说清楚。
9
+ * - 不进撤销历史(老编辑器也不进)。
10
+ *
11
+ * 编辑控件用包里现成的等宽文本框,不引代码编辑器库(体积);Tab 键插两个空格而不是跳走焦点。
12
+ */
13
+ export interface CodePanelProps {
14
+ code: ExperienceCode[];
15
+ onChange: (code: ExperienceCode[]) => void;
16
+ onClose: () => void;
17
+ }
18
+ /** 两段文本 → 存储形状。空白段不写(与后端 normalize 同口径)。 */
19
+ export declare function toExperienceCode(css: string, js: string): ExperienceCode[];
20
+ export declare function CodePanel({ code, onChange, onClose }: CodePanelProps): import("react").JSX.Element;
@@ -0,0 +1,89 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { X } from 'lucide-react';
5
+ import { Button } from '@ptengine/design-components';
6
+ import { ChromeSegment, ChromeSegmented } from '../chrome/topbarPrimitives.js';
7
+ import { CodeField } from '../dialogs/CodeField.js';
8
+ const FLUSH_MS = 400;
9
+ function textOf(code, type) {
10
+ return code
11
+ .filter(c => c.type === type)
12
+ .map(c => c.code)
13
+ .join('\n');
14
+ }
15
+ export function toExperienceCode(css, js) {
16
+ const out = [];
17
+ if (css.trim())
18
+ out.push({ type: 'css', code: css });
19
+ if (js.trim())
20
+ out.push({ type: 'javascript', code: js });
21
+ return out;
22
+ }
23
+ export function CodePanel({ code, onChange, onClose }) {
24
+ const { t } = useTranslation();
25
+ const [tab, setTab] = useState('css');
26
+ const [draft, setDraft] = useState(() => ({
27
+ css: textOf(code, 'css'),
28
+ javascript: textOf(code, 'javascript')
29
+ }));
30
+ const draftRef = useRef(draft);
31
+ draftRef.current = draft;
32
+ const onChangeRef = useRef(onChange);
33
+ onChangeRef.current = onChange;
34
+ const timerRef = useRef(null);
35
+ const dirtyRef = useRef(false);
36
+ const flush = () => {
37
+ if (timerRef.current)
38
+ clearTimeout(timerRef.current);
39
+ timerRef.current = null;
40
+ if (!dirtyRef.current)
41
+ return;
42
+ dirtyRef.current = false;
43
+ onChangeRef.current(toExperienceCode(draftRef.current.css, draftRef.current.javascript));
44
+ };
45
+ useEffect(() => flush, []);
46
+ const cssProp = textOf(code, 'css');
47
+ const jsProp = textOf(code, 'javascript');
48
+ useEffect(() => {
49
+ if (dirtyRef.current)
50
+ return;
51
+ const cur = draftRef.current;
52
+ if (cur.css.trim() === cssProp.trim() && cur.javascript.trim() === jsProp.trim())
53
+ return;
54
+ const next = { css: cssProp, javascript: jsProp };
55
+ draftRef.current = next;
56
+ setDraft(next);
57
+ }, [cssProp, jsProp]);
58
+ const update = (type, value) => {
59
+ setDraft(prev => ({ ...prev, [type]: value }));
60
+ draftRef.current = { ...draftRef.current, [type]: value };
61
+ dirtyRef.current = true;
62
+ if (timerRef.current)
63
+ clearTimeout(timerRef.current);
64
+ timerRef.current = setTimeout(flush, FLUSH_MS);
65
+ };
66
+ const onKeyDown = (event) => {
67
+ if (event.key !== 'Tab' || event.shiftKey)
68
+ return;
69
+ event.preventDefault();
70
+ const el = event.currentTarget;
71
+ const { selectionStart, selectionEnd, value } = el;
72
+ const next = `${value.slice(0, selectionStart)} ${value.slice(selectionEnd)}`;
73
+ update(tab, next);
74
+ requestAnimationFrame(() => {
75
+ el.selectionStart = el.selectionEnd = selectionStart + 2;
76
+ });
77
+ };
78
+ return (_jsxs("aside", { "data-testid": "inline-editor-code-panel", className: "absolute bottom-3 right-3 top-3 z-30 flex w-[420px] max-w-[calc(100%-24px)] flex-col\n rounded-lg border border-[#EBECF0] bg-white shadow-[0_8px_24px_rgba(9,30,66,0.16)]", children: [_jsxs("header", { className: "flex h-11 shrink-0 items-center justify-between border-b border-[#f1f2f3] pl-3 pr-1.5", children: [_jsx("span", { className: "text-[13px] font-medium text-[#1a1a1a]", children: t('inlineCanvas.code.title', '自定义代码') }), _jsx(Button, { size: "sm", variant: "ghost", "data-testid": "inline-editor-code-close", "aria-label": t('inlineCanvas.chrome.close', '关闭'), onClick: () => {
79
+ flush();
80
+ onClose();
81
+ }, children: _jsx(X, { className: "size-4" }) })] }), _jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-2 p-3", children: [_jsx("p", { className: "text-xs leading-5 text-[#6B778C]", children: t('inlineCanvas.code.desc', '可以追加 CSS / JavaScript 代码,更自由地编辑您的网页。') }), _jsx(ChromeSegmented, { "data-testid": "inline-editor-code-tabs", className: "self-start", children: [
82
+ ['css', 'CSS'],
83
+ ['javascript', 'JavaScript']
84
+ ].map(([value, label]) => (_jsx(ChromeSegment, { "data-testid": `inline-editor-code-tab-${value}`, active: tab === value, onClick: () => setTab(value), children: label }, value))) }), _jsx(CodeField, { "data-testid": `inline-editor-code-input-${tab}`, "aria-label": tab === 'css' ? 'CSS' : 'JavaScript', className: "min-h-0 flex-1 resize-none", value: draft[tab], placeholder: tab === 'css'
85
+ ? '.hero-title { color: #e60012; }'
86
+ : "ptKit.query('.cta').then(function (el) { el.textContent = '…'; });", onChange: event => update(tab, event.target.value), onKeyDown: onKeyDown, onBlur: flush }, tab), _jsx("p", { "data-testid": "inline-editor-code-hint", className: "text-xs leading-5 text-[#6B778C]", children: tab === 'css'
87
+ ? t('inlineCanvas.code.cssHint', 'CSS 会实时应用到画布。')
88
+ : t('inlineCanvas.code.jsHint', 'JavaScript 不会在编辑器画布里执行,请通过预览或发布后查看效果。可使用 ptKit 工具函数(如 ptKit.query)等待元素出现。') })] })] }));
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ptengine/inline-editor",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Ptengine Inline 编辑器:画布会话编排、图层树、属性面板与组件库(含内部 canvas-ui)",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,