@ptengine/inline-editor 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +286 -2
  3. package/dist/InlineEditor.d.ts +349 -0
  4. package/dist/InlineEditor.js +886 -0
  5. package/dist/PagePreview.d.ts +83 -0
  6. package/dist/PagePreview.js +111 -0
  7. package/dist/assets/AssetsPanel.d.ts +19 -0
  8. package/dist/assets/AssetsPanel.js +23 -0
  9. package/dist/assets/componentLibrary.d.ts +23 -0
  10. package/dist/assets/componentLibrary.js +12 -0
  11. package/dist/canvas-ui/index.d.ts +17 -0
  12. package/dist/canvas-ui/index.js +7 -0
  13. package/dist/canvas-ui/scope.d.ts +13 -0
  14. package/dist/canvas-ui/scope.js +1 -0
  15. package/dist/canvas-ui/selection/classNames.d.ts +30 -0
  16. package/dist/canvas-ui/selection/classNames.js +15 -0
  17. package/dist/canvas-ui/selection/tokens.css +204 -0
  18. package/dist/canvas-ui/tree/TreePanel.d.ts +47 -0
  19. package/dist/canvas-ui/tree/TreePanel.js +132 -0
  20. package/dist/canvas-ui/tree/classNames.d.ts +21 -0
  21. package/dist/canvas-ui/tree/classNames.js +8 -0
  22. package/dist/canvas-ui/tree/order.d.ts +38 -0
  23. package/dist/canvas-ui/tree/order.js +38 -0
  24. package/dist/canvas-ui/tree/tree.css +131 -0
  25. package/dist/chrome/EditorChrome.d.ts +83 -0
  26. package/dist/chrome/EditorChrome.js +10 -0
  27. package/dist/chrome/VersionMenu.d.ts +26 -0
  28. package/dist/chrome/VersionMenu.js +86 -0
  29. package/dist/chrome/topbarPrimitives.d.ts +69 -0
  30. package/dist/chrome/topbarPrimitives.js +39 -0
  31. package/dist/chrome/useDismissOnOutside.d.ts +23 -0
  32. package/dist/chrome/useDismissOnOutside.js +27 -0
  33. package/dist/dialogs/ClickActionDialog.d.ts +24 -0
  34. package/dist/dialogs/ClickActionDialog.js +35 -0
  35. package/dist/dialogs/HtmlEditDialog.d.ts +23 -0
  36. package/dist/dialogs/HtmlEditDialog.js +23 -0
  37. package/dist/dialogs/ImageEditDialog.d.ts +30 -0
  38. package/dist/dialogs/ImageEditDialog.js +57 -0
  39. package/dist/dialogs/ImageSettingDialog.d.ts +16 -0
  40. package/dist/dialogs/ImageSettingDialog.js +17 -0
  41. package/dist/dialogs/InsertElementDialog.d.ts +23 -0
  42. package/dist/dialogs/InsertElementDialog.js +28 -0
  43. package/dist/dialogs/ReplaceVideoDialog.d.ts +18 -0
  44. package/dist/dialogs/ReplaceVideoDialog.js +16 -0
  45. package/dist/dialogs/actionTypes.d.ts +53 -0
  46. package/dist/dialogs/actionTypes.js +74 -0
  47. package/dist/dialogs/htmlValidation.d.ts +24 -0
  48. package/dist/dialogs/htmlValidation.js +37 -0
  49. package/dist/dialogs/imageEdit.d.ts +49 -0
  50. package/dist/dialogs/imageEdit.js +30 -0
  51. package/dist/index.d.ts +101 -0
  52. package/dist/index.js +53 -0
  53. package/dist/kernel/CanvasContextBar.d.ts +23 -0
  54. package/dist/kernel/CanvasContextBar.js +12 -0
  55. package/dist/kernel/CanvasFrame.d.ts +56 -0
  56. package/dist/kernel/CanvasFrame.js +53 -0
  57. package/dist/kernel/ai/aiInlineApply.d.ts +330 -0
  58. package/dist/kernel/ai/aiInlineApply.js +477 -0
  59. package/dist/kernel/ai/imageSource.d.ts +24 -0
  60. package/dist/kernel/ai/imageSource.js +19 -0
  61. package/dist/kernel/ai/insertHtmlGate.d.ts +71 -0
  62. package/dist/kernel/ai/insertHtmlGate.js +123 -0
  63. package/dist/kernel/ai/postApplyCheck.d.ts +56 -0
  64. package/dist/kernel/ai/postApplyCheck.js +74 -0
  65. package/dist/kernel/ai/replaceHtmlGate.d.ts +71 -0
  66. package/dist/kernel/ai/replaceHtmlGate.js +147 -0
  67. package/dist/kernel/blocks/BlockOverlay.d.ts +16 -0
  68. package/dist/kernel/blocks/BlockOverlay.js +66 -0
  69. package/dist/kernel/blocks/annotateAndTile.d.ts +106 -0
  70. package/dist/kernel/blocks/annotateAndTile.js +122 -0
  71. package/dist/kernel/blocks/resolveBlocks.d.ts +153 -0
  72. package/dist/kernel/blocks/resolveBlocks.js +233 -0
  73. package/dist/kernel/canvasCapabilities.d.ts +31 -0
  74. package/dist/kernel/canvasCapabilities.js +39 -0
  75. package/dist/kernel/canvasCommands.d.ts +126 -0
  76. package/dist/kernel/canvasCommands.js +146 -0
  77. package/dist/kernel/canvasErrors.d.ts +1 -0
  78. package/dist/kernel/canvasErrors.js +14 -0
  79. package/dist/kernel/captureGeometry.d.ts +35 -0
  80. package/dist/kernel/captureGeometry.js +25 -0
  81. package/dist/kernel/captureOrchestration.d.ts +48 -0
  82. package/dist/kernel/captureOrchestration.js +43 -0
  83. package/dist/kernel/captureSlices.d.ts +36 -0
  84. package/dist/kernel/captureSlices.js +20 -0
  85. package/dist/kernel/changelist/ChangeListPanel.d.ts +13 -0
  86. package/dist/kernel/changelist/ChangeListPanel.js +47 -0
  87. package/dist/kernel/changelist/ChangeListPopover.d.ts +20 -0
  88. package/dist/kernel/changelist/ChangeListPopover.js +10 -0
  89. package/dist/kernel/changelist/builders.d.ts +89 -0
  90. package/dist/kernel/changelist/builders.js +81 -0
  91. package/dist/kernel/changelist/changeSummary.d.ts +19 -0
  92. package/dist/kernel/changelist/changeSummary.js +85 -0
  93. package/dist/kernel/changelist/clipboard.d.ts +16 -0
  94. package/dist/kernel/changelist/clipboard.js +4 -0
  95. package/dist/kernel/changelist/exportForSave.d.ts +30 -0
  96. package/dist/kernel/changelist/exportForSave.js +11 -0
  97. package/dist/kernel/changelist/mergeForSave.d.ts +79 -0
  98. package/dist/kernel/changelist/mergeForSave.js +40 -0
  99. package/dist/kernel/changelist/moveObserverSafety.d.ts +70 -0
  100. package/dist/kernel/changelist/moveObserverSafety.js +39 -0
  101. package/dist/kernel/changelist/overrides.d.ts +22 -0
  102. package/dist/kernel/changelist/overrides.js +63 -0
  103. package/dist/kernel/changelist/useChangeList.d.ts +200 -0
  104. package/dist/kernel/changelist/useChangeList.js +282 -0
  105. package/dist/kernel/config.d.ts +29 -0
  106. package/dist/kernel/config.js +10 -0
  107. package/dist/kernel/dataUrlToBlob.d.ts +8 -0
  108. package/dist/kernel/dataUrlToBlob.js +17 -0
  109. package/dist/kernel/linkHref.d.ts +25 -0
  110. package/dist/kernel/linkHref.js +16 -0
  111. package/dist/kernel/mobileFit.d.ts +46 -0
  112. package/dist/kernel/mobileFit.js +44 -0
  113. package/dist/kernel/moveState.d.ts +35 -0
  114. package/dist/kernel/moveState.js +14 -0
  115. package/dist/kernel/selection/SelectionOverlay.d.ts +10 -0
  116. package/dist/kernel/selection/SelectionOverlay.js +25 -0
  117. package/dist/kernel/selection/SelectionToolbar.d.ts +30 -0
  118. package/dist/kernel/selection/SelectionToolbar.js +82 -0
  119. package/dist/kernel/selection/SelectorPathBar.d.ts +27 -0
  120. package/dist/kernel/selection/SelectorPathBar.js +55 -0
  121. package/dist/kernel/selection/buildDescendantShelf.d.ts +40 -0
  122. package/dist/kernel/selection/buildDescendantShelf.js +50 -0
  123. package/dist/kernel/selection/buildRootShelf.d.ts +26 -0
  124. package/dist/kernel/selection/buildRootShelf.js +149 -0
  125. package/dist/kernel/selection/elementAiContext.d.ts +324 -0
  126. package/dist/kernel/selection/elementAiContext.js +122 -0
  127. package/dist/kernel/selection/elementGeometryForAi.d.ts +9 -0
  128. package/dist/kernel/selection/elementGeometryForAi.js +15 -0
  129. package/dist/kernel/selection/geometry.d.ts +55 -0
  130. package/dist/kernel/selection/geometry.js +18 -0
  131. package/dist/kernel/selection/resolveAnchors.d.ts +116 -0
  132. package/dist/kernel/selection/resolveAnchors.js +173 -0
  133. package/dist/kernel/selection/shelfRow.d.ts +33 -0
  134. package/dist/kernel/selection/shelfRow.js +21 -0
  135. package/dist/kernel/selection/tagLabels.d.ts +9 -0
  136. package/dist/kernel/selection/tagLabels.js +46 -0
  137. package/dist/kernel/selection/toolbarButtons.d.ts +60 -0
  138. package/dist/kernel/selection/toolbarButtons.js +105 -0
  139. package/dist/kernel/selection/useAiSelectionContext.d.ts +26 -0
  140. package/dist/kernel/selection/useAiSelectionContext.js +26 -0
  141. package/dist/kernel/selection/useCanvasGeometry.d.ts +32 -0
  142. package/dist/kernel/selection/useCanvasGeometry.js +47 -0
  143. package/dist/kernel/selection/useSelectionRestore.d.ts +18 -0
  144. package/dist/kernel/selection/useSelectionRestore.js +25 -0
  145. package/dist/kernel/shrinkImageForUpload.d.ts +45 -0
  146. package/dist/kernel/shrinkImageForUpload.js +61 -0
  147. package/dist/kernel/useCanvasBridge.d.ts +36 -0
  148. package/dist/kernel/useCanvasBridge.js +41 -0
  149. package/dist/kernel/useCanvasGate.d.ts +22 -0
  150. package/dist/kernel/useCanvasGate.js +35 -0
  151. package/dist/kernel/useCanvasSession.d.ts +44 -0
  152. package/dist/kernel/useCanvasSession.js +182 -0
  153. package/dist/kernel/useCanvasStream.d.ts +39 -0
  154. package/dist/kernel/useCanvasStream.js +45 -0
  155. package/dist/kernel/useMobileFit.d.ts +40 -0
  156. package/dist/kernel/useMobileFit.js +139 -0
  157. package/dist/locales.d.ts +1094 -0
  158. package/dist/locales.js +545 -0
  159. package/dist/panels/StylePanel.d.ts +24 -0
  160. package/dist/panels/StylePanel.js +40 -0
  161. package/dist/panels/styleFields.d.ts +39 -0
  162. package/dist/panels/styleFields.js +91 -0
  163. package/dist/tree/LayersPanel.d.ts +30 -0
  164. package/dist/tree/LayersPanel.js +15 -0
  165. package/dist/tree/domTreeAdapter.d.ts +63 -0
  166. package/dist/tree/domTreeAdapter.js +90 -0
  167. package/dist/useInlineEditor.d.ts +161 -0
  168. package/dist/useInlineEditor.js +346 -0
  169. package/package.json +58 -3
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function ChromeDivider() {
3
+ return _jsx("span", { "aria-hidden": true, className: "mx-2 h-[18px] w-px shrink-0 bg-[#e7e8e9]" });
4
+ }
5
+ export function ChromeIconButton({ label, active, className = '', children, ...rest }) {
6
+ return (_jsx("button", { type: "button", title: label, "aria-label": label, "aria-pressed": active, className: `inline-flex size-[30px] shrink-0 items-center justify-center rounded-lg
7
+ text-[rgba(16,28,31,0.7)] transition-colors
8
+ enabled:hover:bg-[rgba(16,28,31,0.08)] enabled:hover:text-[rgba(16,28,31,0.9)]
9
+ disabled:cursor-default disabled:text-[rgba(16,28,31,0.4)]
10
+ ${active ? 'bg-[rgba(16,28,31,0.08)] text-[rgba(16,28,31,0.9)]' : ''} ${className}`, ...rest, children: children }));
11
+ }
12
+ export function ChromeBackButton({ label, className = '', children, ...rest }) {
13
+ return (_jsx("button", { type: "button", title: label, "aria-label": label, className: `inline-flex size-[30px] shrink-0 items-center justify-center rounded-md
14
+ border border-[#e5e7eb] bg-white text-[#666] transition-colors
15
+ hover:border-[#d0d0d0] hover:bg-[#fafafa] hover:text-[#333] ${className}`, ...rest, children: children }));
16
+ }
17
+ export function ChromeTextButton({ active, className = '', children, ...rest }) {
18
+ return (_jsx("button", { type: "button", "aria-pressed": active, className: `inline-flex h-[30px] shrink-0 items-center gap-[5px] whitespace-nowrap
19
+ rounded-lg border border-[#ececef] bg-white px-3 text-xs font-medium
20
+ text-[#52525b] transition-colors
21
+ enabled:hover:border-[rgba(16,28,31,0.2)] enabled:hover:bg-[rgba(16,28,31,0.08)]
22
+ enabled:hover:text-[rgba(16,28,31,0.9)]
23
+ disabled:cursor-default disabled:opacity-50
24
+ ${active ? 'border-[rgba(16,28,31,0.2)] bg-[rgba(16,28,31,0.08)] text-[rgba(16,28,31,0.9)]' : ''} ${className}`, ...rest, children: children }));
25
+ }
26
+ export function ChromePrimaryButton({ className = '', children, ...rest }) {
27
+ return (_jsx("button", { type: "button", className: `inline-flex h-[30px] shrink-0 items-center gap-1.5 whitespace-nowrap
28
+ rounded-md bg-[#12262b] px-3 text-xs font-medium text-white transition-colors
29
+ enabled:hover:bg-[#1c3a42] disabled:cursor-default disabled:opacity-50 ${className}`, ...rest, children: children }));
30
+ }
31
+ export function ChromeSegmented({ children, className = '', ...rest }) {
32
+ return (_jsx("div", { className: `inline-flex shrink-0 items-center gap-px rounded-lg bg-[#f3f4f4] p-0.5 ${className}`, ...rest, children: children }));
33
+ }
34
+ export function ChromeSegment({ active, className = '', children, ...rest }) {
35
+ return (_jsx("button", { type: "button", "aria-pressed": active, className: `inline-flex h-[26px] shrink-0 items-center justify-center gap-1.5
36
+ rounded-md px-2.5 text-xs transition-colors ${active
37
+ ? 'bg-white font-medium text-[rgba(16,28,31,0.9)] shadow-[0_1px_2px_rgba(16,28,31,0.08)]'
38
+ : 'text-[#707779] enabled:hover:bg-[#e7e8e9] enabled:hover:text-[rgba(16,28,31,0.7)]'} ${className}`, ...rest, children: children }));
39
+ }
@@ -0,0 +1,23 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * 「ESC 关 + 点外部关」—— 编辑器里所有手写浮层共用的一条规则。
4
+ *
5
+ * 为什么手写而不是用 Radix 的 Popover/DropdownMenu:同 `EditorChrome` 当初拒掉 Radix
6
+ * `Select` 的理由 —— 它们在 jsdom 里点不开(要真实 pointer 事件 + ResizeObserver),
7
+ * 接进来等于这些入口**没人守**。而本包所有浮层(选中框、工具条、样式面板、7 个弹框)
8
+ * 本来就是手写定位的,不该为了两个下拉再引入第二套机制。
9
+ *
10
+ * ⚠️ **触发器必须被摘出去。** 不摘的话点触发器的顺序是:pointerdown → 本浮层关 →
11
+ * click → 触发器自己的 onClick 又把它打开,净效果是浮层**永远关不上**,且零报错。
12
+ * 触发器用 `data-<triggerAttr>` 自报家门,这里按 `closest()` 把它整棵子树排除
13
+ * (点到按钮里的图标也算)。
14
+ *
15
+ * ⚠️ 画布是跨域 iframe,**点画布不会触发这里** —— 里面的 pointerdown 不冒泡到产品页
16
+ * document。这是刻意留的:改动清单里点一条要在画布中选中那个元素,用户得在两边来回点。
17
+ *
18
+ * @param open 关着时不挂任何监听(不是挂了再判断)。
19
+ * @param ref 浮层根节点;它内部的点击一律放行。
20
+ * @param onClose 关闭回调。**引用要稳定**,否则每次 render 都重挂一轮监听。
21
+ * @param triggerAttr 触发器上的 data 属性名,不含 `data-` 前缀。
22
+ */
23
+ export declare function useDismissOnOutside(open: boolean, ref: RefObject<HTMLElement | null>, onClose: () => void, triggerAttr: string): void;
@@ -0,0 +1,27 @@
1
+ import { useEffect } from 'react';
2
+ export function useDismissOnOutside(open, ref, onClose, triggerAttr) {
3
+ useEffect(() => {
4
+ if (!open)
5
+ return;
6
+ const onKeyDown = (e) => {
7
+ if (e.key === 'Escape')
8
+ onClose();
9
+ };
10
+ const onPointerDown = (e) => {
11
+ const target = e.target;
12
+ if (!target)
13
+ return;
14
+ if (ref.current?.contains(target))
15
+ return;
16
+ if (target.closest?.(`[data-${triggerAttr}]`))
17
+ return;
18
+ onClose();
19
+ };
20
+ document.addEventListener('keydown', onKeyDown);
21
+ document.addEventListener('pointerdown', onPointerDown);
22
+ return () => {
23
+ document.removeEventListener('keydown', onKeyDown);
24
+ document.removeEventListener('pointerdown', onPointerDown);
25
+ };
26
+ }, [open, ref, onClose, triggerAttr]);
27
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 「点击动作」配置弹框。
3
+ *
4
+ * 字段**按类型动态渲染**(`INLINE_ACTION_TYPES[].fields`),不是把 7 种类型的字段全渲染
5
+ * 出来再靠 CSS 隐藏 —— 后者会让用户把电话号码填进一个其实不会被采用的框里。
6
+ *
7
+ * ⚠️ 每种类型的已填内容**各存一份**(`configs` 按类型分桶),切走再切回来还在。
8
+ * 这不只是体验:产出的 `clickActionConfig` 本来就是分桶的完整对象
9
+ * (见 `buildActionObject` 的注释),只留当前类型那一份会与现役产出对不上。
10
+ */
11
+ export interface ClickActionDialogProps {
12
+ open: boolean;
13
+ /**
14
+ * 元素上已有的动作对象(`changeElementAction.action` 的形状);`null` = 还没配过。
15
+ * 有值时回填,而不是从空白开始。
16
+ */
17
+ seed: {
18
+ clickActionType?: string;
19
+ clickActionConfig?: Record<string, unknown>;
20
+ } | null;
21
+ onOpenChange: (open: boolean) => void;
22
+ onApply: (action: Record<string, unknown>) => void | Promise<unknown>;
23
+ }
24
+ export declare function ClickActionDialog({ open, seed, onOpenChange, onApply }: ClickActionDialogProps): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Switch, Textarea } from '@ptengine/design-components';
5
+ import { INLINE_ACTION_TYPES, buildActionObject, isActionComplete } from './actionTypes.js';
6
+ function seedToConfigs(seed) {
7
+ const configs = {};
8
+ for (const spec of INLINE_ACTION_TYPES) {
9
+ const fromSeed = seed?.clickActionConfig?.[spec.key];
10
+ configs[spec.key] = { ...spec.defaults, ...fromSeed };
11
+ }
12
+ return configs;
13
+ }
14
+ export function ClickActionDialog({ open, seed, onOpenChange, onApply }) {
15
+ const { t } = useTranslation();
16
+ const [type, setType] = useState('link');
17
+ const [configs, setConfigs] = useState(() => seedToConfigs(seed));
18
+ useEffect(() => {
19
+ if (!open)
20
+ return;
21
+ setType(seed?.clickActionType ?? 'link');
22
+ setConfigs(seedToConfigs(seed));
23
+ }, [open, seed]);
24
+ const spec = useMemo(() => INLINE_ACTION_TYPES.find(a => a.key === type), [type]);
25
+ const config = configs[type] ?? {};
26
+ const complete = isActionComplete(type, config);
27
+ const setField = (key, value) => setConfigs(prev => ({ ...prev, [type]: { ...prev[type], [key]: value } }));
28
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-[480px]", "data-testid": "click-action-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.action.title') }) }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.action.type') }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: INLINE_ACTION_TYPES.map(item => (_jsx("button", { type: "button", "data-testid": `action-type-${item.key}`, "aria-pressed": type === item.key, onClick: () => setType(item.key), className: `rounded border px-2 py-1 text-[12px] ${type === item.key
29
+ ? 'border-[#00A92E] bg-[#E9F9EE] text-[#091E42]'
30
+ : 'border-[#DFE1E6] text-[#6B778C]'}`, children: t(item.labelKey) }, item.key))) })] }), spec?.fields.map(field => (_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t(field.labelKey) }), field.kind === 'switch' ? (_jsx(Switch, { "data-testid": `action-field-${field.key}`, checked: Boolean(config[field.key]), onCheckedChange: checked => setField(field.key, checked) })) : field.kind === 'textarea' ? (_jsx(Textarea, { "data-testid": `action-field-${field.key}`, rows: 4, value: String(config[field.key] ?? ''), onChange: e => setField(field.key, e.target.value) })) : (_jsx(Input, { "data-testid": `action-field-${field.key}`, type: field.kind === 'tel' ? 'tel' : 'text', value: String(config[field.key] ?? ''), onChange: e => setField(field.key, e.target.value) }))] }, field.key)))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: t('inlineCanvas.chrome.close') }), _jsx(Button, { "data-testid": "action-apply", disabled: !complete, onClick: () => {
31
+ const { [type]: current, ...others } = configs;
32
+ void onApply(buildActionObject({ type, config: current, otherConfigs: others }));
33
+ onOpenChange(false);
34
+ }, children: t('inlineCanvas.chrome.save') })] })] }) }));
35
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `</>` 编辑 HTML —— 替掉画布调试页的第二个 `window.prompt`。
3
+ *
4
+ * 对齐现役 `HtmlEditDialog.vue`:
5
+ * - seed 是**整个元素的 outerHTML**(不是 innerHTML),且 seed 时就已剥 script/noscript
6
+ * - 确认后产出 `elementContent` + `mode: 'replace'`,**不带 `isTextChange`**
7
+ * (带了就变成「换 innerHTML」,语义完全不同)
8
+ *
9
+ * 与现役的两点差异,都是有意的:
10
+ * 1. 现役用 CodeMirror;这里用等宽 `Textarea`。引一个编辑器依赖只为编辑一小段
11
+ * HTML 不划算,而且 `</>` 的典型用法是微调而不是写整页。
12
+ * 2. 现役在**客户页文档**里建临时节点做校验;我们在产品页做(见 htmlValidation.ts)——
13
+ * 往客户页塞节点正是 C5 禁止的。
14
+ */
15
+ export interface HtmlEditDialogProps {
16
+ open: boolean;
17
+ /** 由 `commands.readOuterHtml()` 取到的 outerHTML(已剥 script)。 */
18
+ seed: string;
19
+ onOpenChange: (open: boolean) => void;
20
+ /** 确认。宿主接到 `changeList.addContent({ mode:'replace', html })`。 */
21
+ onApply: (html: string) => void | Promise<unknown>;
22
+ }
23
+ export declare function HtmlEditDialog({ open, seed, onOpenChange, onApply }: HtmlEditDialogProps): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { AlertTriangle } from 'lucide-react';
5
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Textarea } from '@ptengine/design-components';
6
+ import { validateHtml } from './htmlValidation.js';
7
+ export function HtmlEditDialog({ open, seed, onOpenChange, onApply }) {
8
+ const { t } = useTranslation();
9
+ const [html, setHtml] = useState(seed);
10
+ useEffect(() => {
11
+ if (open)
12
+ setHtml(seed);
13
+ }, [open, seed]);
14
+ const result = useMemo(() => validateHtml(html), [html]);
15
+ const changed = html.trim() !== seed.trim();
16
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-[640px]", "data-testid": "html-edit-dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t('inlineCanvas.html.title', '编辑 HTML') }), _jsx(DialogDescription, { children: t('inlineCanvas.html.tip', '编辑的是整个元素(outerHTML)。保存后 SDK 会用它整体替换原元素。') })] }), _jsx(Textarea, { "data-testid": "html-edit-input", value: html, onChange: e => setHtml(e.target.value), spellCheck: false, className: "h-72 font-mono text-xs leading-5" }), result.strippedScriptCount > 0 && (_jsxs("p", { "data-testid": "html-edit-script-warning", className: "flex items-center gap-1.5 text-xs text-[#8A6D00]", children: [_jsx(AlertTriangle, { className: "size-3.5" }), t('inlineCanvas.html.scriptStripped', {
17
+ count: result.strippedScriptCount,
18
+ defaultValue: '其中 {{count}} 段 script/noscript 会被移除'
19
+ })] })), result.error && (_jsx("p", { "data-testid": "html-edit-error", className: "text-xs text-[#DE350B]", children: t(result.error) })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "secondary", onClick: () => onOpenChange(false), children: t('common.cancel', '取消') }), _jsx(Button, { "data-testid": "html-edit-apply", disabled: !result.ok || !changed, onClick: () => {
20
+ void onApply(html.trim());
21
+ onOpenChange(false);
22
+ }, children: t('common.apply', '应用') })] })] }) }));
23
+ }
@@ -0,0 +1,30 @@
1
+ import type { ElementCapabilities } from '@ptengine/inline-core/protocol';
2
+ import { type ImageEditBranch } from './imageEdit.js';
3
+ /**
4
+ * 「替换图片」弹框。
5
+ *
6
+ * ⚠️ **同一个按钮,按元素类型产出三种完全不同的 change**(见 `imageEdit.ts`):
7
+ * `<img>` → 改 src;有背景图 → 改 `background-image`;其它 → 插一个新 `<img>`。
8
+ * 弹框只负责把分支和参数交出去,判分支的逻辑不在这里重写。
9
+ *
10
+ * 判错的后果不是报错,是「点了替换图片、图没变」—— 比如对一个背景图 div 产出
11
+ * `changeElementAttribute src`,属性确实被设上了,但 div 根本不吃 src。所以界面上
12
+ * **把分支显示出来**:用户能看见这次会改什么,不至于对着没反应的图猜。
13
+ */
14
+ export interface ImageEditApply {
15
+ branch: ImageEditBranch;
16
+ url: string;
17
+ /** 仅 `insert` 分支用:插到哪。 */
18
+ mode?: 'insert' | 'append' | 'before' | 'after';
19
+ }
20
+ export interface ImageEditDialogProps {
21
+ open: boolean;
22
+ capabilities: ElementCapabilities | null;
23
+ /** 当前图片地址,用于回填。 */
24
+ seed?: string;
25
+ onOpenChange: (open: boolean) => void;
26
+ onApply: (input: ImageEditApply) => void | Promise<unknown>;
27
+ /** 本地选图后换成 URL。宿主注入(包里不碰后端端点)。不传则只能填 URL。 */
28
+ uploadImage?: (blob: Blob) => Promise<string>;
29
+ }
30
+ export declare function ImageEditDialog({ open, capabilities, seed, onOpenChange, onApply, uploadImage }: ImageEditDialogProps): import("react").JSX.Element;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input } from '@ptengine/design-components';
5
+ import { pasteModes } from '../kernel/selection/toolbarButtons.js';
6
+ import { resolveImageBranch } from './imageEdit.js';
7
+ export function ImageEditDialog({ open, capabilities, seed = '', onOpenChange, onApply, uploadImage }) {
8
+ const { t } = useTranslation();
9
+ const [url, setUrl] = useState(seed);
10
+ const [uploading, setUploading] = useState(false);
11
+ const [error, setError] = useState(null);
12
+ const fileRef = useRef(null);
13
+ const branch = capabilities ? resolveImageBranch(capabilities) : 'insert';
14
+ const modes = useMemo(() => (capabilities ? pasteModes(capabilities) : []), [capabilities]);
15
+ const [mode, setMode] = useState('append');
16
+ useEffect(() => {
17
+ if (!open)
18
+ return;
19
+ setUrl(seed);
20
+ setError(null);
21
+ if (modes.length > 0)
22
+ setMode(modes[0]);
23
+ }, [open, seed, modes]);
24
+ const onPick = async (file) => {
25
+ if (!file || !uploadImage)
26
+ return;
27
+ setUploading(true);
28
+ setError(null);
29
+ try {
30
+ setUrl(await uploadImage(file));
31
+ }
32
+ catch (e) {
33
+ setError(String(e?.message ?? e));
34
+ }
35
+ finally {
36
+ setUploading(false);
37
+ }
38
+ };
39
+ const noPlace = branch === 'insert' && modes.length === 0;
40
+ const canApply = url.trim().length > 0 && !uploading && !noPlace;
41
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-[480px]", "data-testid": "image-edit-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.image.replaceTitle') }) }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { "data-testid": "image-edit-branch", className: "rounded bg-[#F4F5F7] px-2 py-1 text-[11px] leading-4 text-[#6B778C]", children: branch === 'img-src'
42
+ ? '改 <img> 的 src'
43
+ : branch === 'background'
44
+ ? '改 background-image'
45
+ : '插入一个新的 <img>' }), _jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.image.url') }), _jsx(Input, { "data-testid": "image-edit-url", value: url, onChange: e => setUrl(e.target.value) })] }), uploadImage && (_jsxs("div", { children: [_jsx("input", { ref: fileRef, type: "file", accept: "image/*", "data-testid": "image-edit-file", className: "hidden", onChange: e => void onPick(e.target.files?.[0]) }), _jsx(Button, { variant: "secondary", size: "sm", "data-testid": "image-edit-upload", disabled: uploading, onClick: () => fileRef.current?.click(), children: uploading
46
+ ? t('inlineCanvas.image.uploading')
47
+ : t('inlineCanvas.image.upload') })] })), branch === 'insert' && modes.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.image.insertPosition') }), _jsx("div", { className: "flex gap-1.5", children: modes.map(m => (_jsx("button", { type: "button", "data-testid": `image-edit-mode-${m}`, "aria-pressed": mode === m, onClick: () => setMode(m), className: `rounded border px-2 py-1 text-[12px] ${mode === m
48
+ ? 'border-[#00A92E] bg-[#E9F9EE] text-[#091E42]'
49
+ : 'border-[#DFE1E6] text-[#6B778C]'}`, children: t(`inlineCanvas.clipboard.${m}`) }, m))) })] })), error && (_jsx("p", { "data-testid": "image-edit-error", className: "text-[11px] text-[#DE350B]", children: error }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: t('inlineCanvas.chrome.close') }), _jsx(Button, { "data-testid": "image-edit-apply", disabled: !canApply, onClick: () => {
50
+ void onApply({
51
+ branch,
52
+ url: url.trim(),
53
+ mode: branch === 'insert' ? mode : undefined
54
+ });
55
+ onOpenChange(false);
56
+ }, children: t('inlineCanvas.chrome.save') })] })] }) }));
57
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 「图片设置」弹框 —— 本期**只有 alt**(与 `toolbarButtons.ts` 里那条注释一致)。
3
+ *
4
+ * 产出 `changeElementAttribute` 的 `alt`。⚠️ 清空 alt 与「不改 alt」是两回事:
5
+ * 前者要产出一条 `value` 省略的 change(= 移除属性),后者应该什么都不产出。
6
+ * 混为一谈的话,用户只是打开看一眼再关掉,也会莫名多出一条改动。
7
+ */
8
+ export interface ImageSettingDialogProps {
9
+ open: boolean;
10
+ /** 当前 alt,用于回填。 */
11
+ seed?: string;
12
+ onOpenChange: (open: boolean) => void;
13
+ /** `alt` 为空串表示**移除该属性**。 */
14
+ onApply: (alt: string) => void | Promise<unknown>;
15
+ }
16
+ export declare function ImageSettingDialog({ open, seed, onOpenChange, onApply }: ImageSettingDialogProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input } from '@ptengine/design-components';
5
+ export function ImageSettingDialog({ open, seed = '', onOpenChange, onApply }) {
6
+ const { t } = useTranslation();
7
+ const [alt, setAlt] = useState(seed);
8
+ useEffect(() => {
9
+ if (open)
10
+ setAlt(seed);
11
+ }, [open, seed]);
12
+ const changed = alt !== seed;
13
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-[480px]", "data-testid": "image-setting-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.image.settingTitle') }) }), _jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.image.alt') }), _jsx(Input, { "data-testid": "image-setting-alt", value: alt, onChange: e => setAlt(e.target.value) })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: t('inlineCanvas.chrome.close') }), _jsx(Button, { "data-testid": "image-setting-apply", disabled: !changed, onClick: () => {
14
+ void onApply(alt);
15
+ onOpenChange(false);
16
+ }, children: t('inlineCanvas.chrome.save') })] })] }) }));
17
+ }
@@ -0,0 +1,23 @@
1
+ import type { ElementCapabilities } from '@ptengine/inline-core/protocol';
2
+ /**
3
+ * 「插入元素」—— 往选中元素的内部/前后插一段自己写的 HTML。
4
+ *
5
+ * 产出 `elementContent`(54),mode 为 insert/append/before/after(**不是 replace** ——
6
+ * replace 会把目标元素整个换掉,那是 `</>` 的语义)。
7
+ *
8
+ * ⚠️ 用户写的 HTML **原样插入,不加包装**。这和「替换图片」那条分支刻意不同:
9
+ * 那里插的是我们自己生成的 `<img>`,包一层带防撞号的 `div` 是为了给它一个稳定的选择器
10
+ * 锚点;而用户手写的 HTML 该落成什么样就是什么样,凭空多一层 `div` 会把他的布局改坏,
11
+ * 且他完全不知道那层是哪来的。
12
+ */
13
+ export interface InsertElementApply {
14
+ html: string;
15
+ mode: 'insert' | 'append' | 'before' | 'after';
16
+ }
17
+ export interface InsertElementDialogProps {
18
+ open: boolean;
19
+ capabilities: ElementCapabilities | null;
20
+ onOpenChange: (open: boolean) => void;
21
+ onApply: (input: InsertElementApply) => void | Promise<unknown>;
22
+ }
23
+ export declare function InsertElementDialog({ open, capabilities, onOpenChange, onApply }: InsertElementDialogProps): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { AlertTriangle } from 'lucide-react';
5
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Textarea } from '@ptengine/design-components';
6
+ import { pasteModes } from '../kernel/selection/toolbarButtons.js';
7
+ import { validateHtml } from './htmlValidation.js';
8
+ export function InsertElementDialog({ open, capabilities, onOpenChange, onApply }) {
9
+ const { t } = useTranslation();
10
+ const [html, setHtml] = useState('');
11
+ const modes = useMemo(() => (capabilities ? pasteModes(capabilities) : []), [capabilities]);
12
+ const [mode, setMode] = useState('append');
13
+ useEffect(() => {
14
+ if (!open)
15
+ return;
16
+ setHtml('');
17
+ if (modes.length > 0)
18
+ setMode(modes[0]);
19
+ }, [open, modes]);
20
+ const result = useMemo(() => validateHtml(html), [html]);
21
+ const canApply = html.trim().length > 0 && result.ok && modes.length > 0;
22
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-[640px]", "data-testid": "insert-element-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.insert.title') }) }), _jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.insert.html') }), _jsx(Textarea, { "data-testid": "insert-html", rows: 8, className: "font-mono text-[12px]", value: html, onChange: e => setHtml(e.target.value) })] }), modes.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.insert.position') }), _jsx("div", { className: "flex gap-1.5", children: modes.map(m => (_jsx("button", { type: "button", "data-testid": `insert-mode-${m}`, "aria-pressed": mode === m, onClick: () => setMode(m), className: `rounded border px-2 py-1 text-[12px] ${mode === m
23
+ ? 'border-[#00A92E] bg-[#E9F9EE] text-[#091E42]'
24
+ : 'border-[#DFE1E6] text-[#6B778C]'}`, children: t(`inlineCanvas.clipboard.${m}`) }, m))) })] })), html.trim().length > 0 && result.error && (_jsxs("p", { "data-testid": "insert-error", className: "flex items-start gap-1.5 text-[11px] leading-4 text-[#DE350B]", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3 shrink-0" }), t(result.error)] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: t('inlineCanvas.chrome.close') }), _jsx(Button, { "data-testid": "insert-apply", disabled: !canApply, onClick: () => {
25
+ void onApply({ html: html.trim(), mode });
26
+ onOpenChange(false);
27
+ }, children: t('inlineCanvas.chrome.save') })] })] }) }));
28
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 「替换视频」—— 产出 `changeElementAttribute`(55) 的 `src`。
3
+ *
4
+ * 覆盖两种元素(能力位 `isVideo` 的判据,见 capabilities.ts):
5
+ * `<video>`,以及 src 含 YouTube 标记的 `<iframe>`。两者都是改 `src`,所以**不像
6
+ * 「替换图片」那样有三个分支** —— 那边要分 img/背景图/插入,这边只有一条路。
7
+ *
8
+ * ⚠️ `<video>` 可能把地址写在子 `<source>` 上而不是自己的 `src`。这时设 `src` 仍然有效:
9
+ * 按 HTML 规范,video 自己有 `src` 属性时,子 `<source>` 会被忽略。
10
+ */
11
+ export interface ReplaceVideoDialogProps {
12
+ open: boolean;
13
+ /** 当前地址,用于回填。 */
14
+ seed?: string;
15
+ onOpenChange: (open: boolean) => void;
16
+ onApply: (url: string) => void | Promise<unknown>;
17
+ }
18
+ export declare function ReplaceVideoDialog({ open, seed, onOpenChange, onApply }: ReplaceVideoDialogProps): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input } from '@ptengine/design-components';
5
+ export function ReplaceVideoDialog({ open, seed = '', onOpenChange, onApply }) {
6
+ const { t } = useTranslation();
7
+ const [url, setUrl] = useState(seed);
8
+ useEffect(() => {
9
+ if (open)
10
+ setUrl(seed);
11
+ }, [open, seed]);
12
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-[480px]", "data-testid": "replace-video-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.video.title') }) }), _jsxs("div", { children: [_jsx("p", { className: "mb-1.5 text-[13px] text-[#344563]", children: t('inlineCanvas.video.url') }), _jsx(Input, { "data-testid": "replace-video-url", value: url, onChange: e => setUrl(e.target.value) }), _jsx("p", { className: "mt-1.5 text-[11px] leading-4 text-[#6B778C]", children: t('inlineCanvas.video.hint') })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: t('inlineCanvas.chrome.close') }), _jsx(Button, { "data-testid": "replace-video-apply", disabled: url.trim().length === 0, onClick: () => {
13
+ void onApply(url.trim());
14
+ onOpenChange(false);
15
+ }, children: t('inlineCanvas.chrome.save') })] })] }) }));
16
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * 点击动作的类型表 —— 逐字段移植现役
3
+ * `components/context-menu/actions/actions.config.ts`。
4
+ *
5
+ * ⚠️ **只保留 `effective` 含 `inline` 的 7 种。**
6
+ * 现役共 9 种,但 `closePopup` 只对 popup/form 生效、`showMessage` 只对 form 生效
7
+ * (`actions.config.ts:84-89, 123-129` 的 `effective` 字段)。画布是 inline 场景,
8
+ * 把那两种列出来会让用户配一个 SDK 侧根本不会执行的动作 —— 保存成功、上线无效果、
9
+ * 且没有任何报错。
10
+ *
11
+ * ⚠️ config 的**字段名逐字照抄**:它们直接进 `changeElementAction.action.object
12
+ * .clickActionConfig`,SDK 按名取值。改名不会报错,只会让动作静默失效。
13
+ */
14
+ export type InlineActionKey = 'link' | 'line' | 'tel' | 'openPopup' | 'copiedText' | 'downloadFile' | 'runCode';
15
+ export interface ActionFieldSpec {
16
+ /** 直接进 clickActionConfig 的 key,**不可改名**。 */
17
+ key: string;
18
+ labelKey: string;
19
+ kind: 'text' | 'url' | 'tel' | 'textarea' | 'switch';
20
+ }
21
+ export interface ActionTypeSpec {
22
+ key: InlineActionKey;
23
+ labelKey: string;
24
+ fields: ActionFieldSpec[];
25
+ /** 该类型的默认 config(现役 `xxxConfig` 常量,逐字段一致)。 */
26
+ defaults: Record<string, unknown>;
27
+ }
28
+ export declare const INLINE_ACTION_TYPES: ActionTypeSpec[];
29
+ /**
30
+ * 组装写进 `changeElementAction.action` 的对象。
31
+ *
32
+ * ⚠️ 形状逐字段对齐 `defaultActionsConfig`:
33
+ * `clickActionConfig` 是**按类型分桶的完整对象**(每种类型一个 key),
34
+ * 不是「只放当前选中类型的那一份」。现役 `actions.config.ts:130+` 用 reduce 把 9 种
35
+ * 的默认 config 全铺开,切换类型时各自的填写内容都还在。
36
+ * 只塞当前类型的话,用户切走再切回来填过的内容就没了。
37
+ *
38
+ * ⚠️ `enableTrackEvent` 在 TS 声明 `ExperienceAction` 里**没有**,但实际组装有
39
+ * (`aiModificationService.ts:286`)。缺失是否影响 SDK 点击埋点**未确证** ——
40
+ * 这里照写,与现役产出保持一致。
41
+ */
42
+ export interface BuildActionInput {
43
+ type: InlineActionKey;
44
+ /** 当前类型的字段值。 */
45
+ config: Record<string, unknown>;
46
+ /** 其余类型已填的内容(切换类型时保留)。 */
47
+ otherConfigs?: Record<string, Record<string, unknown>>;
48
+ enableTrackEvent?: boolean;
49
+ trackEventConfig?: Record<string, unknown>;
50
+ }
51
+ export declare function buildActionObject(input: BuildActionInput): Record<string, unknown>;
52
+ /** 该类型的必填字段是否都填了。空的动作保存下去等于配了个不生效的按钮。 */
53
+ export declare function isActionComplete(type: InlineActionKey, config: Record<string, unknown>): boolean;
@@ -0,0 +1,74 @@
1
+ export const INLINE_ACTION_TYPES = [
2
+ {
3
+ key: 'link',
4
+ labelKey: 'inlineCanvas.action.link',
5
+ defaults: { linkType: 'absolute', linkURL: '', openNewTab: false, closePopup: false },
6
+ fields: [
7
+ { key: 'linkURL', labelKey: 'inlineCanvas.action.linkUrl', kind: 'url' },
8
+ { key: 'openNewTab', labelKey: 'inlineCanvas.action.openNewTab', kind: 'switch' }
9
+ ]
10
+ },
11
+ {
12
+ key: 'line',
13
+ labelKey: 'inlineCanvas.action.line',
14
+ defaults: { linkType: 'absolute', linkURL: '', openNewTab: true, closePopup: false },
15
+ fields: [
16
+ { key: 'linkURL', labelKey: 'inlineCanvas.action.lineUrl', kind: 'url' },
17
+ { key: 'openNewTab', labelKey: 'inlineCanvas.action.openNewTab', kind: 'switch' }
18
+ ]
19
+ },
20
+ {
21
+ key: 'tel',
22
+ labelKey: 'inlineCanvas.action.tel',
23
+ defaults: { phoneNumber: '' },
24
+ fields: [{ key: 'phoneNumber', labelKey: 'inlineCanvas.action.phone', kind: 'tel' }]
25
+ },
26
+ {
27
+ key: 'openPopup',
28
+ labelKey: 'inlineCanvas.action.openPopup',
29
+ defaults: { popupId: '' },
30
+ fields: [{ key: 'popupId', labelKey: 'inlineCanvas.action.popupId', kind: 'text' }]
31
+ },
32
+ {
33
+ key: 'copiedText',
34
+ labelKey: 'inlineCanvas.action.copiedText',
35
+ defaults: { text: '' },
36
+ fields: [{ key: 'text', labelKey: 'inlineCanvas.action.copyText', kind: 'textarea' }]
37
+ },
38
+ {
39
+ key: 'downloadFile',
40
+ labelKey: 'inlineCanvas.action.downloadFile',
41
+ defaults: { fileURL: '' },
42
+ fields: [{ key: 'fileURL', labelKey: 'inlineCanvas.action.fileUrl', kind: 'url' }]
43
+ },
44
+ {
45
+ key: 'runCode',
46
+ labelKey: 'inlineCanvas.action.runCode',
47
+ defaults: { code: '', codeId: '', closePopup: false },
48
+ fields: [{ key: 'code', labelKey: 'inlineCanvas.action.code', kind: 'textarea' }]
49
+ }
50
+ ];
51
+ export function buildActionObject(input) {
52
+ const clickActionConfig = {};
53
+ for (const spec of INLINE_ACTION_TYPES) {
54
+ clickActionConfig[spec.key] =
55
+ spec.key === input.type
56
+ ? { ...spec.defaults, ...input.config }
57
+ : { ...spec.defaults, ...(input.otherConfigs?.[spec.key] ?? {}) };
58
+ }
59
+ return {
60
+ enableClickAction: true,
61
+ clickActionType: input.type,
62
+ clickActionConfig,
63
+ enableTrackEvent: input.enableTrackEvent ?? false,
64
+ trackEventConfig: input.trackEventConfig ?? { eventName: '', eventId: '' }
65
+ };
66
+ }
67
+ export function isActionComplete(type, config) {
68
+ const spec = INLINE_ACTION_TYPES.find(a => a.key === type);
69
+ if (!spec)
70
+ return false;
71
+ return spec.fields
72
+ .filter(f => f.kind !== 'switch')
73
+ .every(f => String(config[f.key] ?? '').trim().length > 0);
74
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `</>`(编辑 HTML)的输入校验 —— 移植现役 `HtmlEditDialog.vue:135-150` 的做法:
3
+ * 用 `<template>` 解析一遍,看解析结果是否与输入等价。
4
+ *
5
+ * ⚠️ **为什么用 `<template>` 而不是 `innerHTML` 到一个 `<div>`**:
6
+ * `<div>` 会对 `<td>` / `<tr>` / `<option>` 这类**需要正确宿主**的标签做纠错性丢弃 ——
7
+ * 用户编辑一个表格单元格时,`<td>x</td>` 会被解析成裸文本 `x`,校验「通过」了,
8
+ * 应用后单元格结构却塌掉。`<template>` 的解析上下文是 content model 无关的,不丢。
9
+ *
10
+ * ⚠️ 校验在**产品页文档**里做,不是客户页。现役是在客户页文档里建临时节点
11
+ * (`goog$dom$getOwnerDocument(this.appliedElement).createElement('template')`)——
12
+ * 我们不那么做:那是往客户页塞节点,正是 C5 禁止的。
13
+ * 代价是极少数依赖客户页自定义元素定义的解析差异;换来的是零注入。
14
+ */
15
+ export interface HtmlValidationResult {
16
+ ok: boolean;
17
+ /** 解析后规范化的 HTML。用于提示「你的写法会被浏览器改写成这样」。 */
18
+ normalized: string;
19
+ /** 被剥掉的 `<script>`/`<noscript>` 数量 —— 应用侧无条件剥,这里提前告知。 */
20
+ strippedScriptCount: number;
21
+ /** 失败原因(i18n key 或裸文案)。`ok` 为真时为 null。 */
22
+ error: string | null;
23
+ }
24
+ export declare function validateHtml(input: string): HtmlValidationResult;
@@ -0,0 +1,37 @@
1
+ const EMPTY = {
2
+ ok: false,
3
+ normalized: '',
4
+ strippedScriptCount: 0,
5
+ error: 'inlineCanvas.html.errorEmpty'
6
+ };
7
+ export function validateHtml(input) {
8
+ const html = input.trim();
9
+ if (!html)
10
+ return EMPTY;
11
+ const template = document.createElement('template');
12
+ template.innerHTML = html;
13
+ const scriptLike = template.content.querySelectorAll('script, noscript');
14
+ const strippedScriptCount = scriptLike.length;
15
+ if (template.content.children.length === 0) {
16
+ return {
17
+ ok: false,
18
+ normalized: template.innerHTML,
19
+ strippedScriptCount,
20
+ error: 'inlineCanvas.html.errorNoElement'
21
+ };
22
+ }
23
+ if (template.content.children.length > 1) {
24
+ return {
25
+ ok: false,
26
+ normalized: template.innerHTML,
27
+ strippedScriptCount,
28
+ error: 'inlineCanvas.html.errorMultipleRoots'
29
+ };
30
+ }
31
+ return {
32
+ ok: true,
33
+ normalized: template.innerHTML,
34
+ strippedScriptCount,
35
+ error: null
36
+ };
37
+ }