@ptengine/inline-editor 0.1.4 → 0.1.6

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 (51) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/InlineEditor.d.ts +15 -1
  3. package/dist/InlineEditor.js +85 -54
  4. package/dist/canvas-ui/tree/TreePanel.d.ts +2 -1
  5. package/dist/canvas-ui/tree/TreePanel.js +79 -39
  6. package/dist/chrome/EditorChrome.d.ts +2 -0
  7. package/dist/chrome/EditorChrome.js +1 -1
  8. package/dist/chrome/VersionMenu.d.ts +3 -1
  9. package/dist/chrome/VersionMenu.js +24 -12
  10. package/dist/chrome/topbarPrimitives.d.ts +5 -52
  11. package/dist/chrome/topbarPrimitives.js +13 -31
  12. package/dist/dialogs/ClickActionDialog.js +4 -5
  13. package/dist/dialogs/CodeField.d.ts +4 -0
  14. package/dist/dialogs/CodeField.js +10 -0
  15. package/dist/dialogs/HtmlEditDialog.js +3 -2
  16. package/dist/dialogs/ImageEditDialog.js +53 -15
  17. package/dist/dialogs/ImageSettingDialog.js +3 -3
  18. package/dist/dialogs/InsertElementDialog.js +6 -6
  19. package/dist/dialogs/MediaPreview.d.ts +5 -0
  20. package/dist/dialogs/MediaPreview.js +34 -0
  21. package/dist/dialogs/ReplaceVideoDialog.js +8 -4
  22. package/dist/index.d.ts +1 -0
  23. package/dist/kernel/blocks/BlockOverlay.js +1 -1
  24. package/dist/kernel/blocks/resolveBlocks.d.ts +16 -0
  25. package/dist/kernel/blocks/resolveBlocks.js +39 -6
  26. package/dist/kernel/canvasCommands.d.ts +7 -1
  27. package/dist/kernel/canvasCommands.js +3 -0
  28. package/dist/kernel/changelist/ChangeListPanel.js +11 -9
  29. package/dist/kernel/changelist/useChangeList.d.ts +4 -0
  30. package/dist/kernel/changelist/useChangeList.js +84 -10
  31. package/dist/kernel/selection/SelectionOverlay.js +1 -1
  32. package/dist/kernel/selection/SelectionToolbar.js +26 -8
  33. package/dist/kernel/selection/buildRootShelf.d.ts +49 -2
  34. package/dist/kernel/selection/buildRootShelf.js +156 -41
  35. package/dist/kernel/selection/elementAiContext.d.ts +11 -3
  36. package/dist/kernel/selection/resolveAnchors.d.ts +17 -0
  37. package/dist/kernel/selection/resolveAnchors.js +59 -11
  38. package/dist/locales.d.ts +186 -48
  39. package/dist/locales.js +10 -9
  40. package/dist/panels/EditorStatus.d.ts +5 -0
  41. package/dist/panels/EditorStatus.js +4 -0
  42. package/dist/panels/ScreenshotCard.d.ts +7 -0
  43. package/dist/panels/ScreenshotCard.js +57 -0
  44. package/dist/panels/StylePanel.d.ts +7 -1
  45. package/dist/panels/StylePanel.js +105 -12
  46. package/dist/reviewLocales.d.ts +140 -0
  47. package/dist/reviewLocales.js +134 -0
  48. package/dist/tree/LayersPanel.js +4 -2
  49. package/dist/useInlineEditor.d.ts +12 -1
  50. package/dist/useInlineEditor.js +21 -4
  51. package/package.json +1 -1
@@ -6,5 +6,5 @@ import { ChromeBackButton, ChromeDivider, ChromeIconButton, ChromePrimaryButton,
6
6
  export function EditorChrome(props) {
7
7
  const { t } = useTranslation();
8
8
  const full = props.variant === 'full';
9
- return (_jsxs("header", { "data-testid": full ? 'editor-chrome-full' : 'editor-chrome-minimal', className: "relative flex h-[42px] shrink-0 items-center justify-between gap-2\n border-b border-[#ececef] bg-white px-3", children: [_jsxs("div", { className: "flex min-w-0 items-center", children: [_jsx(ChromeBackButton, { "data-testid": "chrome-exit", label: t(full ? 'inlineCanvas.chrome.exit' : 'inlineCanvas.chrome.close', full ? '退出' : '关闭'), onClick: props.onExit, children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsx(ChromeDivider, {}), _jsx("span", { className: "max-w-[240px] truncate rounded px-2 py-1 text-[13px] font-medium\n text-[#1a1a1a]", children: props.title }), full && (_jsx(VersionMenu, { versions: props.versions, activeVersionId: props.activeVersionId, onVersionChange: props.onVersionChange, onAddVersion: props.onAddVersion, onRenameVersion: props.onRenameVersion, switching: props.versionSwitching, fallbackLabel: props.versionLabel }))] }), _jsxs("div", { className: "flex shrink-0 items-center", children: [full && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(ChromeIconButton, { "data-testid": "chrome-undo", label: t('inlineCanvas.chrome.undo', '撤销'), disabled: !props.canUndo, onClick: props.onUndo, children: _jsx(Undo2, { className: "size-4" }) }), _jsx(ChromeIconButton, { "data-testid": "chrome-redo", label: t('inlineCanvas.chrome.redo', '重做'), disabled: !props.canRedo, onClick: props.onRedo, children: _jsx(Redo2, { className: "size-4" }) })] }), _jsx(ChromeDivider, {})] })), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(ChromeTextButton, { "data-testid": "chrome-changelist", "data-changelist-trigger": true, active: props.changeListOpen, onClick: props.onToggleChangeList, children: [_jsx(List, { className: "size-3.5" }), t('inlineCanvas.chrome.changeList', '改动清单'), props.changeCount > 0 && (_jsx("span", { className: "tabular-nums text-[#8a8f98]", children: props.changeCount }))] }), props.onPreview && (_jsx(ChromeTextButton, { "data-testid": "chrome-preview", onClick: props.onPreview, children: t('inlineCanvas.chrome.preview', '预览') })), full && (_jsx(ChromePrimaryButton, { "data-testid": "chrome-save", disabled: props.versionSwitching, onClick: props.onSave, children: t('inlineCanvas.chrome.save', '保存') })), props.onToggleAi && (_jsx(ChromeIconButton, { "data-testid": "chrome-ai", label: t('inlineCanvas.chrome.ai', 'AI 助手'), active: props.aiOpen, onClick: props.onToggleAi, children: _jsx(Sparkles, { className: "size-4" }) })), props.slotRight] })] })] }));
9
+ return (_jsxs("header", { "data-testid": full ? 'editor-chrome-full' : 'editor-chrome-minimal', className: "relative flex h-[42px] shrink-0 items-center justify-between gap-2\n border-b border-[#ececef] bg-white px-3", children: [_jsxs("div", { className: "flex min-w-0 items-center", children: [_jsx(ChromeBackButton, { "data-testid": "chrome-exit", label: t(full ? 'inlineCanvas.chrome.exit' : 'inlineCanvas.chrome.close', full ? '退出' : '关闭'), onClick: props.onExit, children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsx(ChromeDivider, {}), _jsx("span", { className: "max-w-[240px] truncate rounded px-2 py-1 text-[13px] font-medium\n text-[#1a1a1a]", children: props.title }), full && (_jsx(VersionMenu, { versions: props.versions, activeVersionId: props.activeVersionId, onVersionChange: props.onVersionChange, onAddVersion: props.onAddVersion, onRenameVersion: props.onRenameVersion, removableVersionIds: props.removableVersionIds, onRemoveVersion: props.onRemoveVersion, switching: props.versionSwitching, fallbackLabel: props.versionLabel }))] }), _jsxs("div", { className: "flex shrink-0 items-center", children: [full && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(ChromeIconButton, { "data-testid": "chrome-undo", label: t('inlineCanvas.chrome.undo', '撤销'), disabled: !props.canUndo, onClick: props.onUndo, children: _jsx(Undo2, { className: "size-4" }) }), _jsx(ChromeIconButton, { "data-testid": "chrome-redo", label: t('inlineCanvas.chrome.redo', '重做'), disabled: !props.canRedo, onClick: props.onRedo, children: _jsx(Redo2, { className: "size-4" }) })] }), _jsx(ChromeDivider, {})] })), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(ChromeTextButton, { "data-testid": "chrome-changelist", "data-changelist-trigger": true, active: props.changeListOpen, onClick: props.onToggleChangeList, children: [_jsx(List, { className: "size-3.5" }), t('inlineCanvas.chrome.changeList', '改动清单'), props.changeCount > 0 && (_jsx("span", { className: "tabular-nums text-[#8a8f98]", children: props.changeCount }))] }), props.onPreview && (_jsx(ChromeTextButton, { "data-testid": "chrome-preview", onClick: props.onPreview, children: t('inlineCanvas.chrome.preview', '预览') })), full && (_jsx(ChromePrimaryButton, { "data-testid": "chrome-save", disabled: props.versionSwitching, onClick: props.onSave, children: t('inlineCanvas.review.saveDraft', '保存草稿') })), props.onToggleAi && (_jsx(ChromeIconButton, { "data-testid": "chrome-ai", label: t('inlineCanvas.chrome.ai', 'AI 助手'), active: props.aiOpen, onClick: props.onToggleAi, children: _jsx(Sparkles, { className: "size-4" }) })), props.slotRight] })] })] }));
10
10
  }
@@ -15,6 +15,8 @@ export interface VersionMenuProps {
15
15
  * 所以这里只做校验与交互,改不改得成由宿主说了算。
16
16
  */
17
17
  onRenameVersion?: (id: string, name: string) => void;
18
+ removableVersionIds?: string[];
19
+ onRemoveVersion?: (id: string) => void;
18
20
  /**
19
21
  * 正在切换版本(撤光当前版本的改动 + 回放另一版本的)。为真时整个入口禁用 ——
20
22
  * 切换是一串跨域命令,中途再选一次会让两个版本的改动交错落在同一个页面上。
@@ -23,4 +25,4 @@ export interface VersionMenuProps {
23
25
  /** 没有 `versions` 时展示的静态字符串(旧调用方、单版本入口)。 */
24
26
  fallbackLabel: string;
25
27
  }
26
- export declare function VersionMenu({ versions, activeVersionId, onVersionChange, onAddVersion, onRenameVersion, switching, fallbackLabel }: VersionMenuProps): import("react").JSX.Element;
28
+ export declare function VersionMenu({ versions, activeVersionId, onVersionChange, onAddVersion, onRenameVersion, removableVersionIds, onRemoveVersion, switching, fallbackLabel }: VersionMenuProps): import("react").JSX.Element;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useCallback, useRef, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Check, ChevronDown, Pencil, Plus } from 'lucide-react';
5
- import { useDismissOnOutside } from './useDismissOnOutside.js';
5
+ import { Button, Input, Popover, PopoverTrigger, PopoverContent } from '@ptengine/design-components';
6
6
  const MAX_VERSION_NAME = 40;
7
7
  function versionLetter(index) {
8
8
  return index < 26 ? String.fromCharCode(65 + index) : String(index + 1);
@@ -12,7 +12,7 @@ function descriptiveName(name, label) {
12
12
  const trimmed = name.trim();
13
13
  return trimmed && trimmed !== label.trim() ? trimmed : null;
14
14
  }
15
- export function VersionMenu({ versions, activeVersionId, onVersionChange, onAddVersion, onRenameVersion, switching = false, fallbackLabel }) {
15
+ export function VersionMenu({ versions, activeVersionId, onVersionChange, onAddVersion, onRenameVersion, removableVersionIds = [], onRemoveVersion, switching = false, fallbackLabel }) {
16
16
  const { t } = useTranslation();
17
17
  const [open, setOpen] = useState(false);
18
18
  const ref = useRef(null);
@@ -21,7 +21,6 @@ export function VersionMenu({ versions, activeVersionId, onVersionChange, onAddV
21
21
  setOpen(false);
22
22
  setEditing(null);
23
23
  }, []);
24
- useDismissOnOutside(open, ref, close, 'version-trigger');
25
24
  if (!versions?.length) {
26
25
  return (_jsx("span", { "data-testid": "chrome-version-static", className: "shrink-0 rounded bg-[#f3f4f4] px-2.5 py-1 text-xs text-[#1a1a1a]", children: fallbackLabel }));
27
26
  }
@@ -56,10 +55,18 @@ export function VersionMenu({ versions, activeVersionId, onVersionChange, onAddV
56
55
  const active = activeIndex >= 0 ? versions[activeIndex] : undefined;
57
56
  const label = (i) => t(VARIANT_KEY, { name: versionLetter(i), defaultValue: `版本 ${versionLetter(i)}` });
58
57
  const activeLabel = activeIndex >= 0 ? label(activeIndex) : '';
59
- return (_jsxs("div", { className: "relative shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "chrome-version-trigger", "data-version-trigger": true, "aria-haspopup": "listbox", "aria-expanded": open, disabled: switching, onClick: () => setOpen(v => !v), className: "inline-flex h-[30px] max-w-[260px] items-center gap-1.5 rounded-lg\n border border-[#ececef] bg-white pl-2.5 pr-2 text-xs transition-colors\n enabled:hover:bg-[rgba(16,28,31,0.06)] disabled:cursor-default\n disabled:opacity-50", children: [active ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-[#12262b]", children: activeLabel }), descriptiveName(active.name, activeLabel) && (_jsxs("span", { className: "truncate text-[#12262b]", children: ["\uFF1A", active.name] }))] })) : (_jsx("span", { className: "truncate text-[#52525b]", children: fallbackLabel })), _jsx(ChevronDown, { className: "size-3.5 shrink-0 text-[#8a8f98]" })] }), open && (_jsxs("div", { ref: ref, "data-testid": "chrome-version-menu", role: "listbox", className: "absolute left-0 top-[calc(100%+6px)] z-40 w-[300px] rounded-lg\n border border-[#ececef] bg-white p-[5px]\n shadow-[0_8px_24px_rgba(16,28,31,0.14),0_2px_8px_rgba(16,28,31,0.08)]", children: [versions.map((v, i) => {
58
+ return (_jsxs(Popover, { open: open, onOpenChange: v => {
59
+ setOpen(v);
60
+ if (!v)
61
+ setEditing(null);
62
+ }, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", "data-testid": "chrome-version-trigger", "data-version-trigger": true, "aria-haspopup": "dialog", "aria-expanded": open, disabled: switching, variant: "secondary", className: "max-w-[260px]", children: [active ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-[#12262b]", children: activeLabel }), descriptiveName(active.name, activeLabel) && (_jsxs("span", { className: "truncate text-[#12262b]", children: ["\uFF1A", active.name] }))] })) : (_jsx("span", { className: "truncate text-[#52525b]", children: fallbackLabel })), _jsx(ChevronDown, { className: "size-3.5 shrink-0 text-[#8a8f98]" })] }) }), open && (_jsxs(PopoverContent, { align: "start", ref: ref, "data-testid": "chrome-version-menu", role: "dialog", "aria-label": t('inlineCanvas.review.versions', '版本管理'), className: "z-[1050] w-[320px] p-2", children: [versions.map((v, i) => {
60
63
  const on = v.id === activeVersionId;
61
64
  if (editing?.id === v.id) {
62
- return (_jsxs("div", { className: "px-2.5 py-1.5", children: [_jsx("input", { autoFocus: true, "data-testid": `chrome-version-rename-input-${v.id}`, value: editing.draft, onChange: e => setEditing({ id: v.id, draft: e.target.value, error: null }), onKeyDown: e => {
65
+ return (_jsxs("div", { className: "px-2.5 py-1.5", children: [_jsx(Input, { autoFocus: true, "data-testid": `chrome-version-rename-input-${v.id}`, value: editing.draft, onChange: e => setEditing({
66
+ id: v.id,
67
+ draft: e.target.value,
68
+ error: null
69
+ }), onKeyDown: e => {
63
70
  if (e.key === 'Escape') {
64
71
  e.stopPropagation();
65
72
  setEditing(null);
@@ -69,18 +76,23 @@ export function VersionMenu({ versions, activeVersionId, onVersionChange, onAddV
69
76
  e.stopPropagation();
70
77
  commitRename(v);
71
78
  }
72
- }, onBlur: () => commitRename(v), className: "w-full rounded-md border border-[#00A92E] px-2 py-1\n text-[12.5px] text-[#12262b] outline-none" }), editing.error && (_jsx("div", { "data-testid": "chrome-version-rename-error", className: "mt-1 px-0.5 text-[11.5px] text-[#d4380d]", children: editing.error }))] }, v.id));
79
+ }, "aria-label": t('inlineCanvas.chrome.renameVersion', '重命名') }), _jsxs("div", { className: "mt-2 flex justify-end gap-2", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setEditing(null), children: t('common.cancel', '取消') }), _jsx(Button, { size: "sm", onClick: () => commitRename(v), children: t('common.apply', '应用') })] }), editing.error && (_jsx("div", { "data-testid": "chrome-version-rename-error", className: "mt-1 px-0.5 text-xs text-[#d4380d]", children: editing.error }))] }, v.id));
73
80
  }
74
- return (_jsxs("div", { className: "group flex w-full items-center gap-1", children: [_jsxs("button", { type: "button", role: "option", "aria-selected": on, "data-testid": `chrome-version-option-${v.id}`, onClick: () => {
81
+ return (_jsxs("div", { className: "group flex w-full items-center gap-1", children: [_jsxs(Button, { type: "button", variant: "ghost", "aria-pressed": on, "data-testid": `chrome-version-option-${v.id}`, onClick: () => {
75
82
  setOpen(false);
76
83
  if (!on)
77
84
  onVersionChange?.(v.id);
78
- }, className: `flex min-w-0 flex-1 items-center gap-2 rounded-md px-2.5 py-2
79
- text-left text-[12.5px] transition-colors
85
+ }, className: `flex min-w-0 flex-1 items-center justify-start gap-2 rounded-md px-2.5 py-2
86
+ text-left text-[13px] transition-colors
80
87
  hover:bg-[rgba(16,28,31,0.06)]
81
- ${on ? 'bg-[rgba(16,28,31,0.06)]' : ''}`, children: [_jsx("span", { className: "shrink-0 text-[#12262b]", children: label(i) }), descriptiveName(v.name, label(i)) && (_jsxs("span", { className: "truncate text-[#12262b]", children: ["\uFF1A", v.name] })), on && _jsx(Check, { className: "ml-auto size-3.5 shrink-0 text-[#12262b]" })] }), onRenameVersion && (_jsx("button", { type: "button", "data-testid": `chrome-version-rename-${v.id}`, title: t('inlineCanvas.chrome.renameVersion', '重命名'), "aria-label": t('inlineCanvas.chrome.renameVersion', '重命名'), onClick: () => setEditing({ id: v.id, draft: v.name, error: null }), className: "shrink-0 rounded-md p-1.5 text-[#6b7477] opacity-0\n transition-opacity hover:bg-[rgba(16,28,31,0.06)]\n focus-visible:opacity-100 group-hover:opacity-100", children: _jsx(Pencil, { className: "size-3.5" }) }))] }, v.id));
82
- }), onAddVersion && (_jsxs("button", { type: "button", "data-testid": "chrome-add-version", onClick: () => {
88
+ ${on ? 'bg-[rgba(16,28,31,0.06)]' : ''}`, children: [_jsx("span", { className: "shrink-0 text-[#12262b]", children: label(i) }), descriptiveName(v.name, label(i)) && (_jsxs("span", { className: "truncate text-[#12262b]", children: ["\uFF1A", v.name] })), on && (_jsx(Check, { className: "ml-auto size-3.5 shrink-0 text-[#12262b]" }))] }), removableVersionIds.includes(v.id) && onRemoveVersion && (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `${v.name} · ${t('inlineCanvas.review.removeEmptyVersion', '移除空版本')}`, onClick: () => {
89
+ onRemoveVersion(v.id);
90
+ close();
91
+ }, children: t('inlineCanvas.review.removeEmptyVersion', '移除空版本') })), onRenameVersion && (_jsx(Button, { type: "button", variant: "link", size: "icon-sm", "data-testid": `chrome-version-rename-${v.id}`, title: t('inlineCanvas.chrome.renameVersion', '重命名'), "aria-label": t('inlineCanvas.chrome.renameVersion', '重命名'), onClick: () => setEditing({ id: v.id, draft: v.name, error: null }), className: "shrink-0 rounded-md p-1.5 text-[#6b7477] opacity-0\n transition-opacity\n focus-visible:opacity-100 group-hover:opacity-100", children: _jsx(Pencil, { className: "size-3.5" }) }))] }, v.id));
92
+ }), onAddVersion && (_jsxs(Button, { type: "button", variant: "ghost", "data-testid": "chrome-add-version", disabled: versions.length >= 4, onClick: () => {
83
93
  setOpen(false);
84
94
  onAddVersion();
85
- }, className: "mt-1 flex w-full items-center gap-1.5 border-t\n border-[#f1f2f3] px-2.5 pb-1 pt-2.5 text-left text-[12.5px]\n text-[#00A92E]", children: [_jsx(Plus, { className: "size-3.5" }), t('inlineCanvas.chrome.addVersion', '新增版本')] }))] }))] }));
95
+ }, className: "mt-1 flex w-full items-center justify-start gap-1.5 border-t\n border-[#f1f2f3] px-2.5 pb-1 pt-2.5 text-left text-[13px]\n text-foreground", children: [_jsx(Plus, { className: "size-3.5" }), versions.length >= 4
96
+ ? t('inlineCanvas.review.versionLimit', '最多 4 个版本')
97
+ : t('inlineCanvas.chrome.addVersion', '新增版本')] }))] }))] }));
86
98
  }
@@ -1,69 +1,22 @@
1
1
  import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
2
- /**
3
- * 顶栏原语 —— 尺寸、间距、色值**逐条对齐三代落地页编辑器的顶栏**
4
- * (`apps/x/src/components/landing-pages/editor/topbar/topbar.css` 的 `lpx-tb-*`,
5
- * 以及 `landing-page-editor-v3/styles/editor.css` 的 `.editor-toolbar` 系列)。
6
- *
7
- * ## 为什么是抄取值,不是复用组件
8
- *
9
- * 那套原语住在 app 里(它依赖 app 的 `SimpleTooltip` 与 `--lpv2-*` token),而本包
10
- * **不能反向依赖 app** —— 同 `renderNodeIcon` 由宿主注入的理由。
11
- *
12
- * ## 色值也跟着换了
13
- *
14
- * 早先这里只搬几何、色值留在本包原有的 #091E42 档,理由是「顶栏与它下面的三栏不同色」。
15
- * 实际做出来两套灰在同一屏上更别扭 —— 顶栏现在整条走 lpx 的中性档(#1a1a1a / #52525b /
16
- * #ececef / #12262b),下面的图层树、样式面板、清单不动。顶栏是一条独立的横带,
17
- * 它和落地页编辑器一致比和自己脚下的面板一致更重要。
18
- *
19
- * ## 为什么用 `title` 而不是 tooltip 组件
20
- *
21
- * 图标按钮没有可见文字,必须有悬浮提示。本包里没有 tooltip 原语,而 Radix 那套在
22
- * jsdom 里点不开(`useDismissOnOutside` 的头注释有同一条理由)—— 原生 `title`
23
- * 加 `aria-label` 是这里唯一既能用又守得住的做法。
24
- */
25
- /** 顶栏分组分隔线:1×18 `#e7e8e9`,左右各 8px(= lpx-tb-divider)。 */
26
2
  export declare function ChromeDivider(): import("react").JSX.Element;
27
3
  export interface ChromeIconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
28
- /** 悬浮提示 + 无障碍名。图标按钮没有可见文字,**必填**。 */
29
4
  label: string;
30
- /**
31
- * 选中 / 面板展开中。
32
- *
33
- * ⚠️ **不传就不写 `aria-pressed`**(不是写 false)。`aria-pressed` 的含义是「这是个
34
- * 双态开关」—— 撤销、重做、预览都是一次性动作,给它们写上 false 等于告诉读屏软件
35
- * 「这个开关现在是关的」,那是假的。只有 AI 开关、改动清单这类真开关才带。
36
- */
37
5
  active?: boolean;
38
6
  children: ReactNode;
39
7
  }
40
- /** 顶栏 30×30 图标按钮(= lpx-tb-btn)。顶栏里除退出和主动作外的图标入口都用它。 */
41
- export declare function ChromeIconButton({ label, active, className, children, ...rest }: ChromeIconButtonProps): import("react").JSX.Element;
42
- /**
43
- * 退出按钮(= .toolbar-back-btn)——**带边框、radius 6**,与顶栏其它图标按钮刻意不同。
44
- *
45
- * 它是这条顶栏上唯一一个「离开当前上下文」的动作,落地页编辑器也是这么区分的:
46
- * 其余图标按钮是透明底 radius 8,只有它有边框。
47
- */
48
- export declare function ChromeBackButton({ label, className, children, ...rest }: ChromeIconButtonProps): import("react").JSX.Element;
49
- /** 顶栏文字按钮(= .editor-checklist-btn):h30 / radius 8 / 描边白底。 */
50
- export declare function ChromeTextButton({ active, className, children, ...rest }: {
8
+ export declare function ChromeIconButton({ label, active, children, ...rest }: ChromeIconButtonProps): import("react").JSX.Element;
9
+ export declare const ChromeBackButton: typeof ChromeIconButton;
10
+ export declare function ChromeTextButton({ active, children, ...rest }: {
51
11
  active?: boolean;
52
12
  children: ReactNode;
53
13
  } & ButtonHTMLAttributes<HTMLButtonElement>): import("react").JSX.Element;
54
- /** 顶栏主动作按钮(= .toolbar-action-btn):h30 / radius 6 / `#12262b`。一条顶栏只该有一个。 */
55
- export declare function ChromePrimaryButton({ className, children, ...rest }: {
14
+ export declare function ChromePrimaryButton({ children, ...rest }: {
56
15
  children: ReactNode;
57
16
  } & ButtonHTMLAttributes<HTMLButtonElement>): import("react").JSX.Element;
58
- /**
59
- * 分段 tab 的轨道(= lpx-tb-tabs):灰底 + 2px 内边距,当前档是浮起的白卡片。
60
- *
61
- * 当前档靠「白卡片」区分而不是靠彩色底 —— 与落地页编辑器一致。
62
- */
63
17
  export declare function ChromeSegmented({ children, className, ...rest }: HTMLAttributes<HTMLDivElement>): import("react").JSX.Element;
64
18
  export interface ChromeSegmentProps extends ButtonHTMLAttributes<HTMLButtonElement> {
65
19
  active: boolean;
66
20
  children: ReactNode;
67
21
  }
68
- /** 分段 tab 的一档(= lpx-tb-tab):高 26、radius 6,当前档白卡片 + 1px 阴影。 */
69
- export declare function ChromeSegment({ active, className, children, ...rest }: ChromeSegmentProps): import("react").JSX.Element;
22
+ export declare function ChromeSegment({ active, children, ...rest }: ChromeSegmentProps): import("react").JSX.Element;
@@ -1,39 +1,21 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@ptengine/design-components';
2
3
  export function ChromeDivider() {
3
- return _jsx("span", { "aria-hidden": true, className: "mx-2 h-[18px] w-px shrink-0 bg-[#e7e8e9]" });
4
+ return _jsx("span", { "aria-hidden": true, className: "mx-2 h-4 w-px shrink-0 bg-border" });
4
5
  }
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 }));
6
+ export function ChromeIconButton({ label, active, children, ...rest }) {
7
+ return (_jsx(TooltipProvider, { delayDuration: 200, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", size: "icon", variant: "ghost", "aria-label": label, "aria-pressed": active, ...rest, children: children }) }), _jsx(TooltipContent, { children: label })] }) }));
11
8
  }
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 }));
9
+ export const ChromeBackButton = ChromeIconButton;
10
+ export function ChromeTextButton({ active, children, ...rest }) {
11
+ return (_jsx(Button, { type: "button", variant: "secondary", "aria-pressed": active, ...rest, children: children }));
16
12
  }
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 }));
13
+ export function ChromePrimaryButton({ children, ...rest }) {
14
+ return (_jsx(Button, { type: "button", ...rest, children: children }));
30
15
  }
31
16
  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 }));
17
+ return (_jsx("div", { role: "group", className: `inline-flex items-center gap-1 rounded-lg bg-muted p-1 ${className}`, ...rest, children: children }));
33
18
  }
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 }));
19
+ export function ChromeSegment({ active, children, ...rest }) {
20
+ return (_jsx(Button, { type: "button", size: "sm", variant: active ? 'secondary' : 'ghost', "aria-pressed": active, ...rest, children: children }));
39
21
  }
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CodeField } from './CodeField.js';
2
3
  import { useEffect, useMemo, useState } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
4
- import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Switch, Textarea } from '@ptengine/design-components';
5
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Switch, RadioGroup, RadioGroupItem } from '@ptengine/design-components';
5
6
  import { INLINE_ACTION_TYPES, buildActionObject, isActionComplete } from './actionTypes.js';
6
7
  function seedToConfigs(seed) {
7
8
  const configs = {};
@@ -25,11 +26,9 @@ export function ClickActionDialog({ open, seed, onOpenChange, onApply }) {
25
26
  const config = configs[type] ?? {};
26
27
  const complete = isActionComplete(type, config);
27
28
  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: () => {
29
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { closeLabel: t('inlineCanvas.chrome.close', '关闭'), className: "max-h-[calc(100dvh-32px)] overflow-y-auto", "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(RadioGroup, { value: type, onValueChange: value => setType(value), "aria-label": t('inlineCanvas.action.type'), className: "grid grid-cols-2 gap-x-4 gap-y-3", children: INLINE_ACTION_TYPES.map(item => (_jsxs("label", { className: "flex cursor-pointer items-center gap-2 text-sm", children: [_jsx(RadioGroupItem, { value: item.key, "data-testid": `action-type-${item.key}`, "aria-label": t(item.labelKey) }), t(item.labelKey)] }, item.key))) })] }), spec?.fields.map(field => (_jsxs("div", { children: [_jsx("label", { htmlFor: `action-${field.key}`, className: "mb-1.5 block text-[13px] text-foreground", children: t(field.labelKey) }), field.kind === 'switch' ? (_jsx(Switch, { id: `action-${field.key}`, "aria-label": t(field.labelKey), "data-testid": `action-field-${field.key}`, checked: Boolean(config[field.key]), onCheckedChange: checked => setField(field.key, checked) })) : field.kind === 'textarea' ? (_jsx(CodeField, { id: `action-${field.key}`, "aria-label": t(field.labelKey), "data-testid": `action-field-${field.key}`, rows: 4, value: String(config[field.key] ?? ''), onChange: e => setField(field.key, e.target.value) })) : (_jsx(Input, { id: `action-${field.key}`, "aria-label": t(field.labelKey), "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: "secondary", onClick: () => onOpenChange(false), children: t('common.cancel', '取消') }), _jsx(Button, { "data-testid": "action-apply", disabled: !complete, onClick: () => {
31
30
  const { [type]: current, ...others } = configs;
32
31
  void onApply(buildActionObject({ type, config: current, otherConfigs: others }));
33
32
  onOpenChange(false);
34
- }, children: t('inlineCanvas.chrome.save') })] })] }) }));
33
+ }, children: t('common.apply', '应用') })] })] }) }));
35
34
  }
@@ -0,0 +1,4 @@
1
+ import type { ComponentProps } from 'react';
2
+ import { Textarea } from '@ptengine/design-components';
3
+ /** Shared lightweight code input. Inline font prevents host typography overriding monospace. */
4
+ export declare function CodeField(props: ComponentProps<typeof Textarea>): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Textarea } from '@ptengine/design-components';
3
+ export function CodeField(props) {
4
+ return (_jsx(Textarea, { ...props, spellCheck: false, autoCapitalize: "off", autoCorrect: "off", style: {
5
+ ...props.style,
6
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
7
+ fontSize: 13,
8
+ lineHeight: '20px'
9
+ } }));
10
+ }
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CodeField } from './CodeField.js';
2
3
  import { useEffect, useMemo, useState } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
4
5
  import { AlertTriangle } from 'lucide-react';
5
- import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Textarea } from '@ptengine/design-components';
6
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@ptengine/design-components';
6
7
  import { validateHtml } from './htmlValidation.js';
7
8
  export function HtmlEditDialog({ open, seed, onOpenChange, onApply }) {
8
9
  const { t } = useTranslation();
@@ -13,7 +14,7 @@ export function HtmlEditDialog({ open, seed, onOpenChange, onApply }) {
13
14
  }, [open, seed]);
14
15
  const result = useMemo(() => validateHtml(html), [html]);
15
16
  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
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { closeLabel: t('inlineCanvas.chrome.close', '关闭'), size: "md", className: "max-h-[calc(100dvh-32px)] overflow-y-auto", "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(CodeField, { "aria-label": t('inlineCanvas.html.title'), "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
18
  count: result.strippedScriptCount,
18
19
  defaultValue: '其中 {{count}} 段 script/noscript 会被移除'
19
20
  })] })), 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: () => {
@@ -1,57 +1,95 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MediaPreview } from './MediaPreview.js';
2
3
  import { useEffect, useMemo, useRef, useState } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
4
- import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input } from '@ptengine/design-components';
5
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Tabs, TabsList, TabsTrigger, Label } from '@ptengine/design-components';
5
6
  import { pasteModes } from '../kernel/selection/toolbarButtons.js';
6
7
  import { resolveImageBranch } from './imageEdit.js';
7
8
  export function ImageEditDialog({ open, capabilities, seed = '', onOpenChange, onApply, uploadImage }) {
8
9
  const { t } = useTranslation();
9
10
  const [url, setUrl] = useState(seed);
11
+ const [source, setSource] = useState('link');
12
+ const [uploaded, setUploaded] = useState('');
13
+ const [ready, setReady] = useState(false);
14
+ const epoch = useRef(0);
10
15
  const [uploading, setUploading] = useState(false);
11
16
  const [error, setError] = useState(null);
17
+ const lastFileRef = useRef(undefined);
12
18
  const fileRef = useRef(null);
13
19
  const branch = capabilities ? resolveImageBranch(capabilities) : 'insert';
14
20
  const modes = useMemo(() => (capabilities ? pasteModes(capabilities) : []), [capabilities]);
21
+ const modeKey = modes.join(',');
15
22
  const [mode, setMode] = useState('append');
16
23
  useEffect(() => {
17
24
  if (!open)
18
25
  return;
19
26
  setUrl(seed);
27
+ setUploaded('');
28
+ setSource('link');
29
+ setReady(false);
30
+ epoch.current++;
20
31
  setError(null);
32
+ setUploading(false);
21
33
  if (modes.length > 0)
22
34
  setMode(modes[0]);
23
- }, [open, seed, modes]);
35
+ return () => {
36
+ epoch.current++;
37
+ };
38
+ }, [open, seed, modeKey]);
24
39
  const onPick = async (file) => {
25
40
  if (!file || !uploadImage)
26
41
  return;
42
+ if (!file.type.startsWith('image/') || file.size > 10 * 1024 * 1024) {
43
+ setError(t('inlineCanvas.review.imageLimit', '请选择不超过 10 MB 的图片'));
44
+ return;
45
+ }
46
+ lastFileRef.current = file;
47
+ const request = ++epoch.current;
27
48
  setUploading(true);
28
49
  setError(null);
29
50
  try {
30
- setUrl(await uploadImage(file));
51
+ const next = await uploadImage(file);
52
+ if (request === epoch.current) {
53
+ setUploaded(next);
54
+ setReady(false);
55
+ }
31
56
  }
32
57
  catch (e) {
33
- setError(String(e?.message ?? e));
58
+ if (request === epoch.current)
59
+ setError(String(e?.message ?? e));
34
60
  }
35
61
  finally {
36
- setUploading(false);
62
+ if (request === epoch.current)
63
+ setUploading(false);
37
64
  }
38
65
  };
39
66
  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'
67
+ const selectedUrl = source === 'upload' ? uploaded : url.trim();
68
+ const canApply = !!selectedUrl && ready && !uploading && !noPlace;
69
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { closeLabel: t('inlineCanvas.chrome.close', '关闭'), className: "max-h-[calc(100dvh-32px)] overflow-y-auto", "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: [seed && (_jsxs("details", { children: [_jsx("summary", { className: "text-xs", children: t('inlineCanvas.review.currentMedia', '当前媒体') }), _jsx(MediaPreview, { url: seed })] })), uploadImage && (_jsx(Tabs, { value: source, onValueChange: v => {
70
+ setSource(v);
71
+ setReady(false);
72
+ }, children: _jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "upload", children: t('inlineCanvas.review.uploadFile', '上传文件') }), _jsx(TabsTrigger, { value: "link", children: t('inlineCanvas.review.useLink', '使用链接') })] }) })), _jsx("p", { "data-testid": "image-edit-branch", className: "rounded bg-[#F4F5F7] px-2 py-1 text-xs leading-4 text-[#6B778C]", children: branch === 'img-src'
73
+ ? t('inlineCanvas.review.replaceImage', '替换当前图片')
43
74
  : 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
75
+ ? t('inlineCanvas.review.replaceBackground', '替换背景图片')
76
+ : t('inlineCanvas.review.insertImage', '插入新图片') }), _jsxs("div", { hidden: source !== 'link', children: [_jsx(Label, { htmlFor: "image-edit-url", children: t('inlineCanvas.image.url') }), _jsx(Input, { id: "image-edit-url", "aria-label": t('inlineCanvas.image.url'), "data-testid": "image-edit-url", value: url, onChange: e => {
77
+ setReady(false);
78
+ setUrl(e.target.value);
79
+ } })] }), uploadImage && (_jsxs("div", { hidden: source !== 'upload', onDragOver: e => e.preventDefault(), onDrop: e => {
80
+ e.preventDefault();
81
+ void onPick(e.dataTransfer.files[0]);
82
+ }, className: "rounded-lg border border-dashed border-border p-4", children: [_jsx("p", { className: "mb-2 text-xs text-muted-foreground", children: t('inlineCanvas.review.dropImage', '拖入图片或选择文件,支持图片格式,最大 10 MB。上传只准备素材,应用后才修改画布。') }), _jsx("input", { ref: fileRef, type: "file", accept: "image/*", "data-testid": "image-edit-file", className: "hidden", onChange: e => {
83
+ void onPick(e.target.files?.[0]);
84
+ e.target.value = '';
85
+ } }), _jsx(Button, { variant: "secondary", size: "sm", "data-testid": "image-edit-upload", disabled: uploading, onClick: () => fileRef.current?.click(), children: uploading
46
86
  ? 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: () => {
87
+ : t('inlineCanvas.image.upload') })] })), selectedUrl && (_jsxs("div", { children: [_jsx("p", { className: "mb-2 text-xs", children: t('inlineCanvas.review.pendingMedia', '待应用媒体') }), _jsx(MediaPreview, { url: selectedUrl, onReady: setReady }, source + selectedUrl)] })), 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), size: "sm", variant: mode === m ? 'secondary' : 'ghost', children: t(`inlineCanvas.clipboard.${m}`) }, m))) })] })), error && (_jsxs("p", { "data-testid": "image-edit-error", className: "text-xs text-[#DE350B]", children: [error, source === 'upload' && lastFileRef.current && (_jsx(Button, { variant: "secondary", size: "sm", disabled: uploading, onClick: () => void onPick(lastFileRef.current), children: t('inlineCanvas.review.retry', '重试') }))] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "secondary", onClick: () => onOpenChange(false), children: t('common.cancel', '取消') }), _jsx(Button, { "data-testid": "image-edit-apply", disabled: !canApply, onClick: () => {
50
88
  void onApply({
51
89
  branch,
52
- url: url.trim(),
90
+ url: selectedUrl,
53
91
  mode: branch === 'insert' ? mode : undefined
54
92
  });
55
93
  onOpenChange(false);
56
- }, children: t('inlineCanvas.chrome.save') })] })] }) }));
94
+ }, children: t('common.apply', '应用') })] })] }) }));
57
95
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input } from '@ptengine/design-components';
4
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label } from '@ptengine/design-components';
5
5
  export function ImageSettingDialog({ open, seed = '', onOpenChange, onApply }) {
6
6
  const { t } = useTranslation();
7
7
  const [alt, setAlt] = useState(seed);
@@ -10,8 +10,8 @@ export function ImageSettingDialog({ open, seed = '', onOpenChange, onApply }) {
10
10
  setAlt(seed);
11
11
  }, [open, seed]);
12
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: () => {
13
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { closeLabel: t('inlineCanvas.chrome.close', '关闭'), className: "max-h-[calc(100dvh-32px)] overflow-y-auto", "data-testid": "image-setting-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.image.settingTitle') }) }), _jsxs("div", { children: [_jsx(Label, { htmlFor: "image-setting-alt", children: t('inlineCanvas.image.alt') }), _jsx(Input, { id: "image-setting-alt", "aria-label": t('inlineCanvas.image.alt'), "data-testid": "image-setting-alt", value: alt, onChange: e => setAlt(e.target.value) }), _jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: t('inlineCanvas.review.altHelp', '替代文本用于描述图片内容。装饰图片可留空。') })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "secondary", onClick: () => onOpenChange(false), children: t('common.cancel', '取消') }), _jsx(Button, { "data-testid": "image-setting-apply", disabled: !changed, onClick: () => {
14
14
  void onApply(alt);
15
15
  onOpenChange(false);
16
- }, children: t('inlineCanvas.chrome.save') })] })] }) }));
16
+ }, children: t('common.apply', '应用') })] })] }) }));
17
17
  }
@@ -1,14 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CodeField } from './CodeField.js';
2
3
  import { useEffect, useMemo, useState } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
4
5
  import { AlertTriangle } from 'lucide-react';
5
- import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Textarea } from '@ptengine/design-components';
6
+ import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@ptengine/design-components';
6
7
  import { pasteModes } from '../kernel/selection/toolbarButtons.js';
7
8
  import { validateHtml } from './htmlValidation.js';
8
9
  export function InsertElementDialog({ open, capabilities, onOpenChange, onApply }) {
9
10
  const { t } = useTranslation();
10
11
  const [html, setHtml] = useState('');
11
12
  const modes = useMemo(() => (capabilities ? pasteModes(capabilities) : []), [capabilities]);
13
+ const modeKey = modes.join(',');
12
14
  const [mode, setMode] = useState('append');
13
15
  useEffect(() => {
14
16
  if (!open)
@@ -16,13 +18,11 @@ export function InsertElementDialog({ open, capabilities, onOpenChange, onApply
16
18
  setHtml('');
17
19
  if (modes.length > 0)
18
20
  setMode(modes[0]);
19
- }, [open, modes]);
21
+ }, [open, modeKey]);
20
22
  const result = useMemo(() => validateHtml(html), [html]);
21
23
  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: () => {
24
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { closeLabel: t('inlineCanvas.chrome.close', '关闭'), size: "md", className: "max-h-[calc(100dvh-32px)] overflow-y-auto", "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(CodeField, { "aria-label": t('inlineCanvas.insert.html'), "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), size: "sm", variant: mode === m ? 'secondary' : 'ghost', 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-xs leading-4 text-[#DE350B]", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3 shrink-0" }), t(result.error)] }))] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "secondary", onClick: () => onOpenChange(false), children: t('common.cancel', '取消') }), _jsx(Button, { "data-testid": "insert-apply", disabled: !canApply, onClick: () => {
25
25
  void onApply({ html: html.trim(), mode });
26
26
  onOpenChange(false);
27
- }, children: t('inlineCanvas.chrome.save') })] })] }) }));
27
+ }, children: t('common.apply', '应用') })] })] }) }));
28
28
  }
@@ -0,0 +1,5 @@
1
+ export declare function MediaPreview({ url, kind, onReady }: {
2
+ url: string;
3
+ kind?: 'image' | 'video';
4
+ onReady?: (ready: boolean) => void;
5
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Button } from '@ptengine/design-components';
4
+ import { useTranslation } from 'react-i18next';
5
+ export function MediaPreview({ url, kind = 'image', onReady }) {
6
+ const mediaRef = useRef(null);
7
+ const { t } = useTranslation();
8
+ const [state, setState] = useState('loading');
9
+ const [retry, setRetry] = useState(0);
10
+ useEffect(() => {
11
+ const media = mediaRef.current;
12
+ const loaded = media instanceof HTMLImageElement
13
+ ? media.complete && media.naturalWidth > 0
14
+ : media instanceof HTMLVideoElement && media.readyState >= 1;
15
+ setState(loaded ? 'ready' : 'loading');
16
+ onReady?.(!!loaded);
17
+ }, [url, retry]);
18
+ const ready = () => {
19
+ setState('ready');
20
+ onReady?.(true);
21
+ };
22
+ const fail = () => {
23
+ setState('error');
24
+ onReady?.(false);
25
+ };
26
+ if (!url)
27
+ return null;
28
+ const youtube = /^https:\/\/(www\.)?youtube(-nocookie)?\.com\/embed\//i.test(url);
29
+ return (_jsxs("div", { className: "min-w-0 space-y-2", children: [kind === 'image' ? (_jsx("img", { ref: el => {
30
+ mediaRef.current = el;
31
+ }, src: url, alt: t('inlineCanvas.review.mediaPreview', '媒体预览'), onLoad: ready, onError: fail, className: "max-h-48 w-full rounded-lg object-contain" })) : youtube ? (_jsx("iframe", { title: t('inlineCanvas.review.mediaPreview', '媒体预览'), src: url, onLoad: ready, onError: fail, className: "aspect-video w-full", allowFullScreen: true })) : (_jsx("video", { ref: el => {
32
+ mediaRef.current = el;
33
+ }, src: url, controls: true, preload: "metadata", onLoadedMetadata: ready, onError: fail, className: "max-h-48 w-full" })), _jsx("div", { role: "status", className: "text-xs text-muted-foreground", children: state === 'loading' ? (t('inlineCanvas.review.loading', '正在加载预览…')) : state === 'error' ? (_jsxs(_Fragment, { children: [t('inlineCanvas.review.loadError', '预览加载失败'), ' ', _jsx(Button, { size: "sm", variant: "secondary", onClick: () => setRetry(x => x + 1), children: t('inlineCanvas.review.retry', '重试') })] })) : (t('inlineCanvas.review.ready', '媒体已就绪,应用后修改画布')) })] }, `${url}-${retry}`));
34
+ }