@ptengine/inline-editor 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/dist/InlineEditor.d.ts +15 -1
- package/dist/InlineEditor.js +226 -52
- package/dist/canvas-ui/tree/TreePanel.d.ts +2 -1
- package/dist/canvas-ui/tree/TreePanel.js +79 -39
- package/dist/chrome/EditorChrome.d.ts +2 -0
- package/dist/chrome/EditorChrome.js +1 -1
- package/dist/chrome/VersionMenu.d.ts +3 -1
- package/dist/chrome/VersionMenu.js +24 -12
- package/dist/chrome/topbarPrimitives.d.ts +5 -52
- package/dist/chrome/topbarPrimitives.js +13 -31
- package/dist/dialogs/ClickActionDialog.js +4 -5
- package/dist/dialogs/CodeField.d.ts +4 -0
- package/dist/dialogs/CodeField.js +10 -0
- package/dist/dialogs/HtmlEditDialog.js +3 -2
- package/dist/dialogs/ImageEditDialog.js +53 -15
- package/dist/dialogs/ImageSettingDialog.js +3 -3
- package/dist/dialogs/InsertElementDialog.js +10 -6
- package/dist/dialogs/MediaPreview.d.ts +5 -0
- package/dist/dialogs/MediaPreview.js +34 -0
- package/dist/dialogs/ReplaceVideoDialog.js +8 -4
- package/dist/index.d.ts +1 -0
- package/dist/kernel/ai/aiInlineApply.d.ts +4 -3
- package/dist/kernel/blocks/BlockOverlay.js +1 -1
- package/dist/kernel/blocks/resolveBlocks.d.ts +16 -0
- package/dist/kernel/blocks/resolveBlocks.js +39 -6
- package/dist/kernel/changelist/ChangeListPanel.js +11 -9
- package/dist/kernel/changelist/useChangeList.d.ts +5 -0
- package/dist/kernel/changelist/useChangeList.js +85 -10
- package/dist/kernel/selection/SelectionOverlay.js +1 -1
- package/dist/kernel/selection/SelectionToolbar.js +26 -8
- package/dist/kernel/selection/buildRootShelf.d.ts +56 -2
- package/dist/kernel/selection/buildRootShelf.js +189 -41
- package/dist/kernel/selection/elementAiContext.d.ts +11 -3
- package/dist/kernel/selection/resolveAnchors.d.ts +17 -0
- package/dist/kernel/selection/resolveAnchors.js +59 -11
- package/dist/locales.d.ts +201 -48
- package/dist/locales.js +13 -9
- package/dist/panels/EditorStatus.d.ts +5 -0
- package/dist/panels/EditorStatus.js +4 -0
- package/dist/panels/MovePositionControls.d.ts +6 -0
- package/dist/panels/MovePositionControls.js +35 -0
- package/dist/panels/ScreenshotCard.d.ts +7 -0
- package/dist/panels/ScreenshotCard.js +57 -0
- package/dist/panels/StylePanel.d.ts +9 -1
- package/dist/panels/StylePanel.js +108 -12
- package/dist/reviewLocales.d.ts +149 -0
- package/dist/reviewLocales.js +143 -0
- package/dist/tree/LayersPanel.js +4 -2
- package/dist/useInlineEditor.d.ts +1 -0
- package/dist/useInlineEditor.js +40 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
if (request === epoch.current)
|
|
59
|
+
setError(String(e?.message ?? e));
|
|
34
60
|
}
|
|
35
61
|
finally {
|
|
36
|
-
|
|
62
|
+
if (request === epoch.current)
|
|
63
|
+
setUploading(false);
|
|
37
64
|
}
|
|
38
65
|
};
|
|
39
66
|
const noPlace = branch === 'insert' && modes.length === 0;
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
? '
|
|
45
|
-
: '
|
|
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(
|
|
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:
|
|
90
|
+
url: selectedUrl,
|
|
53
91
|
mode: branch === 'insert' ? mode : undefined
|
|
54
92
|
});
|
|
55
93
|
onOpenChange(false);
|
|
56
|
-
}, children: t('
|
|
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-
|
|
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('
|
|
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,
|
|
6
|
+
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, ToggleGroup, ToggleGroupItem } 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,15 @@ export function InsertElementDialog({ open, capabilities, onOpenChange, onApply
|
|
|
16
18
|
setHtml('');
|
|
17
19
|
if (modes.length > 0)
|
|
18
20
|
setMode(modes[0]);
|
|
19
|
-
}, [open,
|
|
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-
|
|
23
|
-
|
|
24
|
-
|
|
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(ToggleGroup, { type: "single", variant: "outline", value: mode, onValueChange: value => {
|
|
25
|
+
if (modes.includes(value)) {
|
|
26
|
+
setMode(value);
|
|
27
|
+
}
|
|
28
|
+
}, "aria-label": t('inlineCanvas.insert.position'), style: { justifyContent: 'flex-start', flexWrap: 'wrap' }, children: modes.map(m => (_jsx(ToggleGroupItem, { value: m, "data-testid": `insert-mode-${m}`, 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
29
|
void onApply({ html: html.trim(), mode });
|
|
26
30
|
onOpenChange(false);
|
|
27
|
-
}, children: t('
|
|
31
|
+
}, children: t('common.apply', '应用') })] })] }) }));
|
|
28
32
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MediaPreview } from './MediaPreview.js';
|
|
2
3
|
import { useEffect, 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, Label } from '@ptengine/design-components';
|
|
5
6
|
export function ReplaceVideoDialog({ open, seed = '', onOpenChange, onApply }) {
|
|
6
7
|
const { t } = useTranslation();
|
|
8
|
+
const [ready, setReady] = useState(false);
|
|
7
9
|
const [url, setUrl] = useState(seed);
|
|
8
10
|
useEffect(() => {
|
|
9
|
-
if (open)
|
|
11
|
+
if (open) {
|
|
10
12
|
setUrl(seed);
|
|
13
|
+
setReady(false);
|
|
14
|
+
}
|
|
11
15
|
}, [open, seed]);
|
|
12
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-
|
|
16
|
+
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": "replace-video-dialog", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: t('inlineCanvas.video.title') }) }), _jsxs("div", { children: [seed && (_jsxs("details", { children: [_jsx("summary", { children: t('inlineCanvas.review.currentMedia', '当前媒体') }), _jsx(MediaPreview, { kind: "video", url: seed })] })), _jsx(Label, { htmlFor: "replace-video-url", children: t('inlineCanvas.video.url') }), _jsx(Input, { id: "replace-video-url", "aria-label": t('inlineCanvas.video.url'), "data-testid": "replace-video-url", value: url, onChange: e => setUrl(e.target.value) }), _jsx("p", { className: "mt-1.5 text-xs leading-4 text-[#6B778C]", children: t('inlineCanvas.review.videoLinkHint', '支持视频文件链接和 YouTube 嵌入链接。加载就绪后应用到画布。') })] }), _jsx(MediaPreview, { kind: "video", url: url.trim(), onReady: setReady }, url), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "secondary", onClick: () => onOpenChange(false), children: t('common.cancel', '取消') }), _jsx(Button, { "data-testid": "replace-video-apply", disabled: !url.trim() || !ready, onClick: () => {
|
|
13
17
|
void onApply(url.trim());
|
|
14
18
|
onOpenChange(false);
|
|
15
|
-
}, children: t('
|
|
19
|
+
}, children: t('common.apply', '应用') })] })] }) }));
|
|
16
20
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export { SHELF_LAYOUT_FIELDS, laysOutInRow, rowPositions } from './kernel/select
|
|
|
79
79
|
export type { RowPosition } from './kernel/selection/rowLayout.js';
|
|
80
80
|
export { linkPath } from './kernel/linkHref.js';
|
|
81
81
|
export { buildRootShelf } from './kernel/selection/buildRootShelf.js';
|
|
82
|
+
export type { ShelfBlockHint } from './kernel/selection/buildRootShelf.js';
|
|
82
83
|
export { LayersPanel } from './tree/LayersPanel.js';
|
|
83
84
|
export { adaptDomTree, moveModeFromDropPos } from './tree/domTreeAdapter.js';
|
|
84
85
|
export type { AdaptDomTreeOptions, TreeNodeIconInput } from './tree/domTreeAdapter.js';
|
|
@@ -248,10 +248,11 @@ export interface AiApplyDeps {
|
|
|
248
248
|
*/
|
|
249
249
|
ancestors: string[];
|
|
250
250
|
/**
|
|
251
|
-
*
|
|
251
|
+
* 货架里某一项的祖先链(由近及远,含作用域根自己)。**整页货架那些行用**。
|
|
252
252
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
253
|
+
* 纯元素作用域不传:那时 `ancestors` 就是选中元素的祖先链,而作用域里的后代都在它
|
|
254
|
+
* 底下,同一条链够用。元素作用域并上了整页货架时(宿主有整页模式)传,查的是并进来的那些行。
|
|
255
|
+
* 整页作用域不一样 —— 根是 `body`,它的 `ancestors` 是空数组,
|
|
255
256
|
* 拿它去判 `MOVE_ELEMENT` 的 target 会把**所有**移动判成非法(body 没有祖先)。
|
|
256
257
|
* 这个函数让那道闸改为对着「被移动的那个元素」的祖先链判,规则本身没变。
|
|
257
258
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { frameRectToProductRect } from '../selection/geometry.js';
|
|
3
|
-
const Z_INDEX =
|
|
3
|
+
const Z_INDEX = 890;
|
|
4
4
|
const CHIP_HEIGHT = 18;
|
|
5
5
|
const COLORS = { base: '#1D4ED8', active: '#DB2777', none: '#94A3B8' };
|
|
6
6
|
function boxStyle(rect, color, active, none) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomTreeNode, ElementsInBandResult, GetDomTreeParams, ResolveHeatmapSelectorResult } from '@ptengine/inline-core/protocol';
|
|
2
|
+
import { type RowPosition } from '../selection/rowLayout.js';
|
|
2
3
|
/**
|
|
3
4
|
* 分析画布:把热图的**区块配置**(MySQL 里那份,apps/x 读来)画到客户活页面上,
|
|
4
5
|
* 并给每块找一个锚点元素 + 一份 AI 能读的摘要(标题 / 文案 / CTA)。
|
|
@@ -24,6 +25,11 @@ import type { DomTreeNode, ElementsInBandResult, GetDomTreeParams, ResolveHeatma
|
|
|
24
25
|
* 3. **none** —— 都没有:members 空。区块框照画(位置本来就来自配置,一样准),
|
|
25
26
|
* 只是点不进任何元素、也没有 selector 可以交给 agent。
|
|
26
27
|
*
|
|
28
|
+
* 三条路定下成员之后,**成员上提到块根**(`liftToBlockRoots`):轮播的一张、并列的一列、区块里的一个小节点,
|
|
29
|
+
* 提到「只装着这一块的最外层容器」—— 与编辑器整页货架打 `[Bn]` 的那一行是同一个元素(同一份判定、同一份
|
|
30
|
+
* 取树口径)。真机 vivaia.jp 首页(2026-10-21):B1 的成员是轮播第 1 张,`<page-block>` 的成员行与方案锚点
|
|
31
|
+
* 照抄它,首版 after 它把「首屏下方」的新模块插进了轮播。摘要(heading / text / cta / links)按上提后的成员取。
|
|
32
|
+
*
|
|
27
33
|
* 编号 `B1..Bn` 按输入的 `yStart` 升序,与模型看到的瓦片标注一致(`annotateAndTile`)。
|
|
28
34
|
*
|
|
29
35
|
* 三条跨域命令经 `deps` 注入:编排逻辑不需要插件也不需要浏览器就能测。任何一条失败都
|
|
@@ -46,6 +52,11 @@ export interface PageModelBlockMember {
|
|
|
46
52
|
/** 该 member 子树的文案摘要(折叠空白、≤80)。拉树失败为空串。 */
|
|
47
53
|
text: string;
|
|
48
54
|
hasChildren: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 它在父级横排里的位次(整页树上判,与整页货架行的 `inRow` 同一份)。`<page-block>` 的成员行据此打
|
|
57
|
+
* `[横排 i/n]`:成员行的父级不在它自己的子树里,只能从整页树带过来。不在横排里 / 判不了时不写这个键。
|
|
58
|
+
*/
|
|
59
|
+
inRow?: RowPosition;
|
|
49
60
|
}
|
|
50
61
|
export interface PageModelBlock {
|
|
51
62
|
/** `'B1'`,`'B2'`… 按 `yStart` 升序。 */
|
|
@@ -142,6 +153,11 @@ export interface ResolveBlocksOptions {
|
|
|
142
153
|
* 三处共用这套坐标,由各自的消费者在画的时候乘比例。
|
|
143
154
|
*/
|
|
144
155
|
scale?: number;
|
|
156
|
+
/**
|
|
157
|
+
* 画布视口宽(mobile 375 / pc 1440)。成员上提的块根判据按它算「够宽」门槛,与编辑器整页货架传的是
|
|
158
|
+
* 同一个数;缺省按 375(与 `buildRootShelf` 的缺省一致)。
|
|
159
|
+
*/
|
|
160
|
+
viewportWidth?: number;
|
|
145
161
|
}
|
|
146
162
|
export declare function resolveBlocks(sessionId: string, blocks: HeatmapBlockConfig[], deps?: ResolveBlocksDeps, opts?: ResolveBlocksOptions): Promise<PageModelBlock[]>;
|
|
147
163
|
/**
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { elementsInBand as elementsInBandCommand, getDomTree as getDomTreeCommand, resolveHeatmapSelector as resolveHeatmapSelectorCommand } from '../canvasCommands.js';
|
|
2
2
|
import { linkPath } from '../linkHref.js';
|
|
3
|
+
import { ROOT_SHELF_TREE_PARAMS, liftToBlockRoots } from '../selection/buildRootShelf.js';
|
|
3
4
|
import { SHELF_TREE_MAX_DEPTH } from '../selection/elementAiContext.js';
|
|
5
|
+
import { rowPositions } from '../selection/rowLayout.js';
|
|
4
6
|
const defaultDeps = {
|
|
5
7
|
resolveHeatmapSelector: resolveHeatmapSelectorCommand,
|
|
6
8
|
elementsInBand: elementsInBandCommand,
|
|
@@ -104,7 +106,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
104
106
|
catch {
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
|
-
const
|
|
109
|
+
const located = [];
|
|
108
110
|
for (let i = 0; i < ordered.length; i++) {
|
|
109
111
|
const cfg = ordered[i];
|
|
110
112
|
const base = { id: idOf(i), name: cfg.name };
|
|
@@ -120,7 +122,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
120
122
|
hits.push({ selector: m.kernelSelector, tag: m.tag ?? '', rect: m.rect });
|
|
121
123
|
}
|
|
122
124
|
if (hits.length > 0) {
|
|
123
|
-
|
|
125
|
+
located.push({
|
|
124
126
|
...base,
|
|
125
127
|
...geometryOf(cfg),
|
|
126
128
|
members: hits.map(h => ({
|
|
@@ -132,7 +134,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
132
134
|
ctas: [],
|
|
133
135
|
text: '',
|
|
134
136
|
resolved: 'selector'
|
|
135
|
-
})
|
|
137
|
+
});
|
|
136
138
|
continue;
|
|
137
139
|
}
|
|
138
140
|
let candidate = null;
|
|
@@ -147,7 +149,7 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
147
149
|
candidate = null;
|
|
148
150
|
}
|
|
149
151
|
if (candidate) {
|
|
150
|
-
|
|
152
|
+
located.push({
|
|
151
153
|
...base,
|
|
152
154
|
...geometryOf(cfg),
|
|
153
155
|
members: [
|
|
@@ -162,10 +164,10 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
162
164
|
text: '',
|
|
163
165
|
resolved: 'geometry',
|
|
164
166
|
iou: candidate.iou
|
|
165
|
-
})
|
|
167
|
+
});
|
|
166
168
|
continue;
|
|
167
169
|
}
|
|
168
|
-
|
|
170
|
+
located.push({
|
|
169
171
|
...base,
|
|
170
172
|
...geometryOf(cfg),
|
|
171
173
|
members: [],
|
|
@@ -174,8 +176,39 @@ export async function resolveBlocks(sessionId, blocks, deps = defaultDeps, opts
|
|
|
174
176
|
resolved: 'none'
|
|
175
177
|
});
|
|
176
178
|
}
|
|
179
|
+
const lifted = await liftMembers(sessionId, deps, located, opts.viewportWidth);
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const b of lifted)
|
|
182
|
+
out.push(b.members.length ? await enrich(sessionId, deps, b) : b);
|
|
177
183
|
return out;
|
|
178
184
|
}
|
|
185
|
+
async function liftMembers(sessionId, deps, blocks, viewportWidth) {
|
|
186
|
+
if (!blocks.some(b => b.members.length))
|
|
187
|
+
return blocks;
|
|
188
|
+
try {
|
|
189
|
+
const tree = await deps.getDomTree(sessionId, ROOT_SHELF_TREE_PARAMS);
|
|
190
|
+
const liftOf = liftToBlockRoots(tree, { viewportWidth, blocks });
|
|
191
|
+
const inRowOf = rowPositions(tree);
|
|
192
|
+
return blocks.map(b => {
|
|
193
|
+
if (!b.members.length)
|
|
194
|
+
return b;
|
|
195
|
+
const byMember = liftOf.get(b.id);
|
|
196
|
+
const members = [];
|
|
197
|
+
for (const m of b.members) {
|
|
198
|
+
const root = byMember?.get(m.selector);
|
|
199
|
+
const next = root ? { ...m, selector: root.selector, tag: root.tag } : m;
|
|
200
|
+
if (members.some(x => x.selector === next.selector))
|
|
201
|
+
continue;
|
|
202
|
+
const inRow = inRowOf.get(next.selector);
|
|
203
|
+
members.push(inRow ? { ...next, inRow } : next);
|
|
204
|
+
}
|
|
205
|
+
return { ...b, members };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return blocks;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
179
212
|
export function isAutoGeneratedBlockName(name) {
|
|
180
213
|
const n = name.trim();
|
|
181
214
|
if (!n)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import { toWireFormat } from '@ptengine/inline-core/change-format';
|
|
4
|
-
import { Code2, Crosshair, EyeOff, Image, Link2, Move, Plus, Trash2, Type, Undo2,
|
|
4
|
+
import { Code2, Crosshair, EyeOff, Image, Link2, Move, Plus, Trash2, Type, Undo2, Paintbrush } from 'lucide-react';
|
|
5
5
|
import { Button } from '@ptengine/design-components';
|
|
6
6
|
import { summarizeChange } from './changeSummary.js';
|
|
7
7
|
import { classifySuppression } from './mergeForSave.js';
|
|
8
8
|
const ICONS = {
|
|
9
|
-
style:
|
|
9
|
+
style: Paintbrush,
|
|
10
10
|
delete: Trash2,
|
|
11
11
|
text: Type,
|
|
12
12
|
code: Code2,
|
|
@@ -16,19 +16,21 @@ const ICONS = {
|
|
|
16
16
|
insert: Plus
|
|
17
17
|
};
|
|
18
18
|
export function ChangeListPanel({ list, onRevert, lastError, onHover, onSelect, onRevertAll }) {
|
|
19
|
+
const [confirmRevert, setConfirmRevert] = useState(false);
|
|
19
20
|
const [view, setView] = useState('summary');
|
|
20
21
|
const entries = useMemo(() => list.map(r => ({ change: r.change, ancestors: r.ancestors })), [list]);
|
|
21
22
|
const json = useMemo(() => JSON.stringify(list.map(r => toWireFormat(r.change)), null, 2), [list]);
|
|
22
|
-
return (_jsxs("section", { "data-testid": "change-list-panel", className: "flex min-h-0 flex-1 flex-col", onMouseLeave: () => onHover?.(''), children: [_jsxs("
|
|
23
|
+
return (_jsxs("section", { "data-testid": "change-list-panel", className: "flex min-h-0 flex-1 flex-col", onMouseLeave: () => onHover?.(''), children: [confirmRevert && (_jsxs("div", { role: "alert", className: "border-b p-3 text-xs", children: ["\u786E\u5B9A\u64A4\u9500\u6B64\u7248\u672C\u7684\u5168\u90E8\u6539\u52A8\uFF1F", _jsxs("div", { className: "mt-2 flex justify-end gap-2", children: [_jsx(Button, { size: "sm", variant: "secondary", onClick: () => setConfirmRevert(false), children: "\u53D6\u6D88" }), _jsx(Button, { size: "sm", variant: "destructive", onClick: () => {
|
|
24
|
+
setConfirmRevert(false);
|
|
25
|
+
onRevertAll?.();
|
|
26
|
+
}, children: "\u64A4\u9500\u5168\u90E8\u6539\u52A8" })] })] })), _jsxs("header", { className: "flex h-10 shrink-0 items-center justify-between gap-2 border-b\n border-[#f1f2f3] pl-3 pr-2", children: [_jsxs("span", { className: "truncate text-[13px] font-medium text-[#1a1a1a]", children: ["\u6539\u52A8\u6E05\u5355", _jsx("span", { "data-testid": "change-list-count", className: "ml-1.5 text-xs font-normal tabular-nums text-[#8a8f98]", children: list.length })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [_jsx("div", { className: "flex items-center gap-px rounded-md bg-[#f3f4f4] p-0.5", children: [
|
|
23
27
|
['summary', '摘要', undefined],
|
|
24
28
|
[
|
|
25
29
|
'wire',
|
|
26
|
-
'
|
|
27
|
-
'
|
|
30
|
+
'高级数据',
|
|
31
|
+
'查看此版本的高级修改数据'
|
|
28
32
|
]
|
|
29
|
-
].map(([v, label, hint]) => (_jsx(
|
|
30
|
-
? 'bg-white font-medium text-[rgba(16,28,31,0.9)] shadow-[0_1px_2px_rgba(16,28,31,0.08)]'
|
|
31
|
-
: 'text-[#707779] hover:text-[rgba(16,28,31,0.7)]'}`, children: label }, v))) }), onRevertAll && list.length > 0 && (_jsx("button", { type: "button", "data-testid": "change-list-revert-all", onClick: onRevertAll, className: "rounded px-2 py-1 text-xs text-[#52525b] transition-colors\n hover:bg-[rgba(16,28,31,0.06)] hover:text-[rgba(16,28,31,0.9)]", children: "\u5168\u90E8\u64A4\u9500" }))] })] }), lastError && (_jsx("p", { "data-testid": "change-list-error", className: "px-3 py-2 text-xs text-[#DE350B]", children: lastError })), list.length === 0 ? (_jsx("p", { "data-testid": "change-list-empty", className: "px-3 py-8 text-center text-xs text-[#8a8f98]", children: "\u8FD8\u6CA1\u6709\u6539\u52A8\u3002\u5728\u753B\u5E03\u91CC\u9009\u4E2D\u5143\u7D20\u5F00\u59CB\u7F16\u8F91\u3002" })) : view === 'summary' ? (_jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto", children: list.map((record, index) => (_jsx(ChangeRow, { record: record, summary: summarizeChange(record.change), suppression: classifySuppression(entries, index), onRevert: onRevert, onHover: onHover, onSelect: onSelect }, record.changeId))) })) : (_jsx("pre", { "data-testid": "change-list-json", className: "min-h-0 flex-1 overflow-auto bg-[#FAFAFA] px-3 py-2 text-[11px]\n leading-4 text-[#344563]", children: json }))] }));
|
|
33
|
+
].map(([v, label, hint]) => (_jsx(Button, { type: "button", "data-testid": `change-list-view-${v}`, "aria-pressed": view === v, title: hint, onClick: () => setView(v), size: "sm", variant: view === v ? 'secondary' : 'ghost', children: label }, v))) }), onRevertAll && list.length > 0 && (_jsx(Button, { type: "button", "data-testid": "change-list-revert-all", onClick: () => setConfirmRevert(true), size: "sm", variant: "ghost", children: "\u5168\u90E8\u64A4\u9500" }))] })] }), lastError && (_jsx("p", { "data-testid": "change-list-error", className: "px-3 py-2 text-xs text-[#DE350B]", children: lastError })), list.length === 0 ? (_jsx("p", { "data-testid": "change-list-empty", className: "px-3 py-8 text-center text-xs text-[#8a8f98]", children: "\u8FD8\u6CA1\u6709\u6539\u52A8\u3002\u5728\u753B\u5E03\u91CC\u9009\u4E2D\u5143\u7D20\u5F00\u59CB\u7F16\u8F91\u3002" })) : view === 'summary' ? (_jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto", children: list.map((record, index) => (_jsx(ChangeRow, { record: record, summary: summarizeChange(record.change), suppression: classifySuppression(entries, index), onRevert: onRevert, onHover: onHover, onSelect: onSelect }, record.changeId))) })) : (_jsx("pre", { "data-testid": "change-list-json", className: "min-h-0 flex-1 overflow-auto bg-[#FAFAFA] px-3 py-2 text-[11px]\n leading-4 text-[#344563]", children: json }))] }));
|
|
32
34
|
}
|
|
33
35
|
const SUPPRESSION_TEXT = {
|
|
34
36
|
'ancestor-hidden': {
|
|
@@ -43,5 +45,5 @@ const SUPPRESSION_TEXT = {
|
|
|
43
45
|
function ChangeRow({ record, summary, suppression, onRevert, onHover, onSelect }) {
|
|
44
46
|
const Icon = ICONS[summary.icon];
|
|
45
47
|
const text = suppression ? SUPPRESSION_TEXT[suppression.reason] : null;
|
|
46
|
-
return (_jsxs("li", { "data-testid": "change-list-item", "data-suppressed": suppression ? suppression.reason : undefined, className: "group flex items-center gap-2.5 border-b border-[#f1f2f3] px-3 py-2.5\n last:border-b-0 hover:bg-[rgba(16,28,31,0.04)]", onMouseEnter: () => onHover?.(summary.selector), children: [_jsx(Icon, { className: `size-4 shrink-0 ${suppression ? 'text-[#B3BAC5]' : 'text-[#6B778C]'}` }), _jsxs(
|
|
48
|
+
return (_jsxs("li", { "data-testid": "change-list-item", "data-suppressed": suppression ? suppression.reason : undefined, className: "group flex items-center gap-2.5 border-b border-[#f1f2f3] px-3 py-2.5\n last:border-b-0 hover:bg-[rgba(16,28,31,0.04)]", onMouseEnter: () => onHover?.(summary.selector), children: [_jsx(Icon, { className: `size-4 shrink-0 ${suppression ? 'text-[#B3BAC5]' : 'text-[#6B778C]'}` }), _jsxs(Button, { type: "button", variant: "ghost", "data-testid": "change-list-locate", onClick: () => onSelect?.(summary.selector), className: "flex h-auto min-h-8 min-w-0 flex-1 flex-col items-start justify-start gap-0.5 whitespace-normal py-2 text-left", title: summary.selector, children: [_jsxs("span", { className: `flex items-center gap-1.5 text-[13px] font-medium ${suppression ? 'text-[#8993A4]' : 'text-[#1a1a1a]'}`, children: [summary.action, onSelect && (_jsx(Crosshair, { className: "size-3 text-[#8993A4] opacity-0 group-hover:opacity-100" }))] }), text && (_jsxs("span", { "data-testid": "change-list-suppressed-badge", className: "flex items-center gap-1 rounded bg-[#FFF7E6] px-1.5 py-0.5\n text-[11px] leading-4 text-[#974F0C]", title: `${text.hint}(${suppression?.by})`, children: [_jsx(EyeOff, { className: "size-3 shrink-0" }), text.badge] })), summary.detail && (_jsx("span", { className: `w-full truncate font-mono text-[11px] ${suppression ? 'text-[#8993A4] line-through' : 'text-[#344563]'}`, children: summary.detail })), _jsx("span", { className: "w-full truncate font-mono text-[11px] text-[#8993A4]", children: summary.selector })] }), _jsx(Button, { variant: "ghost", size: "icon", "data-testid": `change-list-revert-${record.changeId}`, "aria-label": "\u64A4\u9500", onClick: () => onRevert(record.changeId), children: _jsx(Undo2, { className: "size-4" }) })] }));
|
|
47
49
|
}
|
|
@@ -82,6 +82,11 @@ export interface UseChangeListResult {
|
|
|
82
82
|
outcome: HydrateOutcome;
|
|
83
83
|
idMap: Map<string, string>;
|
|
84
84
|
}>;
|
|
85
|
+
whenIdle: () => Promise<unknown>;
|
|
86
|
+
canUndo: boolean;
|
|
87
|
+
canRedo: boolean;
|
|
88
|
+
undo: () => Promise<void>;
|
|
89
|
+
redo: () => Promise<void>;
|
|
85
90
|
lastError: string | null;
|
|
86
91
|
}
|
|
87
92
|
export interface HydrateOutcome {
|