@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
package/dist/locales.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reviewLocales } from './reviewLocales.js';
|
|
1
2
|
export const zhCN = {
|
|
2
3
|
inlineCanvas: {
|
|
3
4
|
action: {
|
|
@@ -32,7 +33,7 @@ export const zhCN = {
|
|
|
32
33
|
},
|
|
33
34
|
image: {
|
|
34
35
|
replaceTitle: '替换图片',
|
|
35
|
-
settingTitle: '
|
|
36
|
+
settingTitle: '图片替代文本',
|
|
36
37
|
url: '图片地址',
|
|
37
38
|
upload: '上传图片',
|
|
38
39
|
uploading: '上传中…',
|
|
@@ -43,6 +44,7 @@ export const zhCN = {
|
|
|
43
44
|
title: '移动元素',
|
|
44
45
|
pickTarget: '在画布里点选目标元素',
|
|
45
46
|
cancel: '取消移动',
|
|
47
|
+
confirm: '确认移动',
|
|
46
48
|
position: '相对目标的位置',
|
|
47
49
|
top: '移入开头',
|
|
48
50
|
bottom: '移入末尾',
|
|
@@ -90,7 +92,7 @@ export const zhCN = {
|
|
|
90
92
|
},
|
|
91
93
|
html: {
|
|
92
94
|
title: '编辑 HTML',
|
|
93
|
-
tip: '
|
|
95
|
+
tip: '编辑整个元素(outerHTML)。应用后替换画布中的元素;保存草稿后才写入体验配置。',
|
|
94
96
|
scriptStripped: '其中 {{count}} 段 script/noscript 会被移除',
|
|
95
97
|
errorEmpty: '内容不能为空',
|
|
96
98
|
errorNoElement: '需要一个完整的元素标签,纯文本请用「编辑文字」',
|
|
@@ -212,7 +214,7 @@ export const enUS = {
|
|
|
212
214
|
},
|
|
213
215
|
image: {
|
|
214
216
|
replaceTitle: 'Replace image',
|
|
215
|
-
settingTitle: 'Image
|
|
217
|
+
settingTitle: 'Image alternative text',
|
|
216
218
|
url: 'Image URL',
|
|
217
219
|
upload: 'Upload image',
|
|
218
220
|
uploading: 'Uploading…',
|
|
@@ -223,6 +225,7 @@ export const enUS = {
|
|
|
223
225
|
title: 'Move element',
|
|
224
226
|
pickTarget: 'Pick a target element on the canvas',
|
|
225
227
|
cancel: 'Cancel move',
|
|
228
|
+
confirm: 'Confirm move',
|
|
226
229
|
position: 'Position relative to target',
|
|
227
230
|
top: 'Inside, first',
|
|
228
231
|
bottom: 'Inside, last',
|
|
@@ -270,7 +273,7 @@ export const enUS = {
|
|
|
270
273
|
},
|
|
271
274
|
html: {
|
|
272
275
|
title: 'Edit HTML',
|
|
273
|
-
tip: '
|
|
276
|
+
tip: 'Edit the entire element (outerHTML). Apply updates the canvas; Save draft stores the experience configuration.',
|
|
274
277
|
scriptStripped: '{{count}} script/noscript block(s) will be removed',
|
|
275
278
|
errorEmpty: 'Content cannot be empty',
|
|
276
279
|
errorNoElement: 'A complete element tag is required; use Edit text for plain text',
|
|
@@ -392,7 +395,7 @@ export const jaJP = {
|
|
|
392
395
|
},
|
|
393
396
|
image: {
|
|
394
397
|
replaceTitle: '画像を差し替え',
|
|
395
|
-
settingTitle: '
|
|
398
|
+
settingTitle: '画像の代替テキスト',
|
|
396
399
|
url: '画像 URL',
|
|
397
400
|
upload: '画像をアップロード',
|
|
398
401
|
uploading: 'アップロード中…',
|
|
@@ -403,6 +406,7 @@ export const jaJP = {
|
|
|
403
406
|
title: '要素を移動',
|
|
404
407
|
pickTarget: 'キャンバスで対象要素を選択',
|
|
405
408
|
cancel: '移動をキャンセル',
|
|
409
|
+
confirm: '移動を確定',
|
|
406
410
|
position: '対象に対する位置',
|
|
407
411
|
top: '内側の先頭',
|
|
408
412
|
bottom: '内側の末尾',
|
|
@@ -450,7 +454,7 @@ export const jaJP = {
|
|
|
450
454
|
},
|
|
451
455
|
html: {
|
|
452
456
|
title: 'HTML を編集',
|
|
453
|
-
tip: '要素全体(outerHTML
|
|
457
|
+
tip: '要素全体(outerHTML)を編集します。適用でキャンバスを更新し、下書きを保存すると設定が保存されます。',
|
|
454
458
|
scriptStripped: 'うち {{count}} 個の script/noscript は削除されます',
|
|
455
459
|
errorEmpty: '内容を入力してください',
|
|
456
460
|
errorNoElement: '完全な要素タグが必要です。プレーンテキストは「テキストを編集」をご利用ください',
|
|
@@ -539,7 +543,7 @@ export const jaJP = {
|
|
|
539
543
|
}
|
|
540
544
|
};
|
|
541
545
|
export const inlineEditorLocales = {
|
|
542
|
-
'zh-CN': zhCN,
|
|
543
|
-
'en-US': enUS,
|
|
544
|
-
'ja-JP': jaJP
|
|
546
|
+
'zh-CN': { ...zhCN, inlineCanvas: { ...zhCN.inlineCanvas, review: reviewLocales['zh-CN'] } },
|
|
547
|
+
'en-US': { ...enUS, inlineCanvas: { ...enUS.inlineCanvas, review: reviewLocales['en-US'] } },
|
|
548
|
+
'ja-JP': { ...jaJP, inlineCanvas: { ...jaJP.inlineCanvas, review: reviewLocales['ja-JP'] } }
|
|
545
549
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
/** Shared contextual feedback. Actions remain explicit buttons supplied by the caller. */
|
|
3
|
+
export declare function EditorStatus({ tone, className, ...props }: HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
tone?: 'info' | 'error';
|
|
5
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function EditorStatus({ tone = 'info', className = '', ...props }) {
|
|
3
|
+
return (_jsx("div", { role: tone === 'error' ? 'alert' : 'status', ...props, className: `fixed z-[903] flex max-w-[min(640px,calc(100vw-32px))] -translate-x-1/2 flex-wrap items-center gap-2 rounded-lg border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md ${tone === 'error' ? 'border-destructive' : ''} ${className}` }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MovePosition } from '../kernel/moveState.js';
|
|
2
|
+
export declare function MovePositionControls({ onConfirm, onCancel, onError }: {
|
|
3
|
+
onConfirm: (position: MovePosition) => Promise<void>;
|
|
4
|
+
onCancel: () => void;
|
|
5
|
+
onError: (error: unknown) => void;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Button, ToggleGroup, ToggleGroupItem } from '@ptengine/design-components';
|
|
5
|
+
const positions = ['top', 'bottom', 'before', 'after'];
|
|
6
|
+
export function MovePositionControls({ onConfirm, onCancel, onError }) {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const [position, setPosition] = useState(null);
|
|
9
|
+
const [pending, setPending] = useState(false);
|
|
10
|
+
const submitting = useRef(false);
|
|
11
|
+
const confirm = async () => {
|
|
12
|
+
if (!position || submitting.current)
|
|
13
|
+
return;
|
|
14
|
+
submitting.current = true;
|
|
15
|
+
setPending(true);
|
|
16
|
+
try {
|
|
17
|
+
await onConfirm(position);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
onError(error);
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
submitting.current = false;
|
|
24
|
+
setPending(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 flex-col gap-2.5", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: t('inlineCanvas.move.position') }), _jsxs("div", { className: "flex flex-col items-stretch gap-3", children: [_jsx(ToggleGroup, { type: "single", variant: "outline", value: position ?? '', disabled: pending, "aria-label": t('inlineCanvas.move.position'), onValueChange: value => {
|
|
28
|
+
if (positions.includes(value)) {
|
|
29
|
+
setPosition(value);
|
|
30
|
+
}
|
|
31
|
+
}, style: {
|
|
32
|
+
display: 'grid',
|
|
33
|
+
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))'
|
|
34
|
+
}, children: positions.map(pos => (_jsx(ToggleGroupItem, { value: pos, "data-testid": `move-position-${pos}`, children: t(`inlineCanvas.move.${pos}`) }, pos))) }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(Button, { size: "default", variant: "secondary", type: "button", "data-testid": "move-cancel", disabled: pending, onClick: onCancel, children: t('inlineCanvas.move.cancel') }), _jsx(Button, { size: "default", type: "button", "data-testid": "move-confirm", disabled: !position || pending, onClick: () => void confirm(), children: t('inlineCanvas.move.confirm') })] })] })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@ptengine/design-components';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
export function ScreenshotCard({ dataUrl, truncated, scale, uploadImage, onClose }) {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const [url, setUrl] = useState('');
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
const [note, setNote] = useState('');
|
|
11
|
+
const [zoom, setZoom] = useState(false);
|
|
12
|
+
const pending = useRef(false);
|
|
13
|
+
const copyLink = async () => {
|
|
14
|
+
if (pending.current)
|
|
15
|
+
return;
|
|
16
|
+
pending.current = true;
|
|
17
|
+
setBusy(true);
|
|
18
|
+
setNote('');
|
|
19
|
+
try {
|
|
20
|
+
let link = url;
|
|
21
|
+
if (!link) {
|
|
22
|
+
link = await uploadImage(await (await fetch(dataUrl)).blob());
|
|
23
|
+
setUrl(link);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await navigator.clipboard.writeText(link);
|
|
27
|
+
setNote(t('inlineCanvas.review.linkCopied', '链接已复制'));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
setNote(t('inlineCanvas.review.copyLinkManual', '复制失败,请从下方手动复制链接'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
setNote(t('inlineCanvas.review.uploadError', '上传失败,可重试;本地图片仍可下载'));
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
pending.current = false;
|
|
38
|
+
setBusy(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
return (_jsxs("section", { "data-testid": "inline-editor-shot", className: "absolute bottom-4 right-4 z-30 w-[320px] max-w-full space-y-2 rounded-lg border border-border bg-popover p-3 text-popover-foreground shadow-lg", children: [_jsxs("header", { className: "flex items-center justify-between text-sm font-semibold", children: [t('inlineCanvas.review.screenshot', '元素截图'), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": t('inlineCanvas.chrome.close', '关闭'), onClick: onClose, children: _jsx(X, { className: "size-4" }) })] }), _jsx(Button, { variant: "ghost", className: "h-auto w-full p-0", "aria-label": t('inlineCanvas.review.enlarge', '放大预览'), onClick: () => setZoom(true), children: _jsx("img", { src: dataUrl, alt: t('inlineCanvas.review.screenshot', '元素截图'), className: "max-h-40 w-full object-contain" }) }), truncated && (_jsx("p", { className: "text-xs text-muted-foreground", children: t('inlineCanvas.review.shotTruncated', '元素超出可视区,只截到可见部分') })), scale < 1 && (_jsx("p", { className: "text-xs text-muted-foreground", children: t('inlineCanvas.review.shotScale', '截图分辨率约为原图的 {{scale}}%', {
|
|
42
|
+
scale: Math.round(scale * 100)
|
|
43
|
+
}) })), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { size: "sm", variant: "secondary", asChild: true, children: _jsx("a", { href: dataUrl, download: "element-screenshot.png", children: t('inlineCanvas.review.downloadImage', '下载图片') }) }), typeof ClipboardItem !== 'undefined' && navigator.clipboard?.write && (_jsx(Button, { size: "sm", variant: "secondary", onClick: async () => {
|
|
44
|
+
try {
|
|
45
|
+
const blob = await (await fetch(dataUrl)).blob();
|
|
46
|
+
await navigator.clipboard.write([
|
|
47
|
+
new ClipboardItem({ [blob.type]: blob })
|
|
48
|
+
]);
|
|
49
|
+
setNote(t('inlineCanvas.review.imageCopied', '图片已复制'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
setNote(t('inlineCanvas.review.copyError', '复制失败,请下载图片'));
|
|
53
|
+
}
|
|
54
|
+
}, children: t('inlineCanvas.review.copyImage', '复制图片') })), uploadImage && (_jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => void copyLink(), children: busy
|
|
55
|
+
? t('inlineCanvas.review.uploading', '正在上传…')
|
|
56
|
+
: t('inlineCanvas.review.copyLink', '复制链接') }))] }), note && (_jsx("p", { role: "status", className: "text-xs", children: note })), url && (_jsx(Input, { readOnly: true, "aria-label": t('inlineCanvas.review.shotLink', '截图链接'), value: url, onFocus: e => e.target.select() })), _jsx(Dialog, { open: zoom, onOpenChange: setZoom, children: _jsxs(DialogContent, { size: "lg", closeLabel: t('inlineCanvas.chrome.close', '关闭'), children: [_jsx(DialogTitle, { children: t('inlineCanvas.review.screenshot', '元素截图') }), _jsx("img", { src: dataUrl, alt: t('inlineCanvas.review.screenshot', '元素截图'), className: "max-h-[70vh] w-full object-contain" })] }) })] }));
|
|
57
|
+
}
|
|
@@ -13,12 +13,20 @@ import type { ElementCapabilities } from '@ptengine/inline-core/protocol';
|
|
|
13
13
|
* 真的打到了客户页 DOM 上,还各发了一次跨三跳的命令)。
|
|
14
14
|
*/
|
|
15
15
|
export interface StylePanelProps {
|
|
16
|
+
/** Initial viewport position; subsequent selection geometry must not move the panel. */
|
|
17
|
+
anchor?: {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
};
|
|
16
21
|
tag: string;
|
|
17
22
|
capabilities: ElementCapabilities | null;
|
|
18
23
|
/** 元素当前的 author 层声明,用作输入框初值。 */
|
|
19
24
|
declaredStyles?: Record<string, string>;
|
|
25
|
+
/** Only these properties have an editor override that can be cleared. */
|
|
26
|
+
resettableProperties?: string[];
|
|
20
27
|
/** 提交一条样式改动。由宿主接到 `changeList.addStyle`。 */
|
|
21
28
|
onApply: (property: string, value: string) => void | Promise<unknown>;
|
|
22
29
|
onClose: () => void;
|
|
30
|
+
onReset?: (property: string) => void;
|
|
23
31
|
}
|
|
24
|
-
export declare function StylePanel({ tag, capabilities, declaredStyles, onApply, onClose }: StylePanelProps): import("react").JSX.Element | null;
|
|
32
|
+
export declare function StylePanel({ anchor, tag, capabilities, declaredStyles, resettableProperties, onApply, onReset, onClose }: StylePanelProps): import("react").JSX.Element | null;
|
|
@@ -1,20 +1,106 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { X } from 'lucide-react';
|
|
5
|
-
import { Button,
|
|
5
|
+
import { Button, InputGroup, InputGroupInput, InputGroupAddon, InputGroupText, Label, NativeSelect, Separator } from '@ptengine/design-components';
|
|
6
6
|
import { composeStyleValue, visibleSections } from './styleFields.js';
|
|
7
|
-
export function StylePanel({ tag, capabilities, declaredStyles = {}, onApply, onClose }) {
|
|
7
|
+
export function StylePanel({ anchor, tag, capabilities, declaredStyles = {}, resettableProperties, onApply, onReset, onClose }) {
|
|
8
8
|
const { t } = useTranslation();
|
|
9
9
|
const sections = visibleSections(capabilities);
|
|
10
|
+
const panelRef = useRef(null);
|
|
11
|
+
const [offset, setOffset] = useState(() => anchor ?? { x: 0, y: 0 });
|
|
12
|
+
const renderedOffset = useRef(offset);
|
|
13
|
+
renderedOffset.current = offset;
|
|
14
|
+
const anchored = anchor !== undefined;
|
|
15
|
+
const drag = useRef(null);
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const clamp = () => {
|
|
18
|
+
if (drag.current)
|
|
19
|
+
return;
|
|
20
|
+
const rect = panelRef.current?.getBoundingClientRect();
|
|
21
|
+
if (!rect)
|
|
22
|
+
return;
|
|
23
|
+
const left = Math.max(8, Math.min(rect.left, window.innerWidth - rect.width - 8));
|
|
24
|
+
const top = Math.max(8, Math.min(rect.top, window.innerHeight - rect.height - 8));
|
|
25
|
+
if (anchored && panelRef.current) {
|
|
26
|
+
panelRef.current.style.left = `${left}px`;
|
|
27
|
+
panelRef.current.style.top = `${top}px`;
|
|
28
|
+
}
|
|
29
|
+
const next = anchored
|
|
30
|
+
? { x: left, y: top }
|
|
31
|
+
: {
|
|
32
|
+
x: renderedOffset.current.x + left - rect.left,
|
|
33
|
+
y: renderedOffset.current.y + top - rect.top
|
|
34
|
+
};
|
|
35
|
+
setOffset(prev => (prev.x === next.x && prev.y === next.y ? prev : next));
|
|
36
|
+
};
|
|
37
|
+
clamp();
|
|
38
|
+
window.addEventListener('resize', clamp);
|
|
39
|
+
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(clamp);
|
|
40
|
+
if (panelRef.current)
|
|
41
|
+
observer?.observe(panelRef.current);
|
|
42
|
+
const frame = typeof requestAnimationFrame === 'undefined' ? null : requestAnimationFrame(clamp);
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener('resize', clamp);
|
|
45
|
+
observer?.disconnect();
|
|
46
|
+
if (frame !== null)
|
|
47
|
+
cancelAnimationFrame(frame);
|
|
48
|
+
};
|
|
49
|
+
}, [anchored, sections.length]);
|
|
10
50
|
if (sections.length === 0)
|
|
11
51
|
return null;
|
|
12
|
-
return (_jsxs("div", {
|
|
52
|
+
return (_jsxs("div", { ref: panelRef, style: anchor
|
|
53
|
+
? { position: 'fixed', left: offset.x, top: offset.y }
|
|
54
|
+
: { transform: `translate(${offset.x}px, ${offset.y}px)` }, "data-testid": "inline-style-panel", className: "flex max-h-[calc(100vh-100px)] w-80 max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-lg border border-[#DFE1E6]\n bg-white shadow-lg", children: [_jsxs("header", { "data-testid": "inline-style-panel-drag", onPointerDown: event => {
|
|
55
|
+
if (event.button !== 0 || event.target.closest('button'))
|
|
56
|
+
return;
|
|
57
|
+
const rect = panelRef.current.getBoundingClientRect();
|
|
58
|
+
drag.current = {
|
|
59
|
+
pointer: event.pointerId,
|
|
60
|
+
x: event.clientX,
|
|
61
|
+
y: event.clientY,
|
|
62
|
+
left: rect.left,
|
|
63
|
+
top: rect.top,
|
|
64
|
+
width: rect.width,
|
|
65
|
+
height: rect.height,
|
|
66
|
+
offset
|
|
67
|
+
};
|
|
68
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
}, onPointerMove: event => {
|
|
71
|
+
const start = drag.current;
|
|
72
|
+
if (!start || start.pointer !== event.pointerId)
|
|
73
|
+
return;
|
|
74
|
+
const left = Math.max(8, Math.min(start.left + event.clientX - start.x, window.innerWidth - start.width - 8));
|
|
75
|
+
const top = Math.max(8, Math.min(start.top + event.clientY - start.y, window.innerHeight - start.height - 8));
|
|
76
|
+
const next = {
|
|
77
|
+
x: start.offset.x + left - start.left,
|
|
78
|
+
y: start.offset.y + top - start.top
|
|
79
|
+
};
|
|
80
|
+
if (anchored && panelRef.current) {
|
|
81
|
+
panelRef.current.style.left = `${next.x}px`;
|
|
82
|
+
panelRef.current.style.top = `${next.y}px`;
|
|
83
|
+
}
|
|
84
|
+
setOffset(next);
|
|
85
|
+
}, onPointerUp: () => {
|
|
86
|
+
drag.current = null;
|
|
87
|
+
}, onPointerCancel: () => {
|
|
88
|
+
drag.current = null;
|
|
89
|
+
}, onLostPointerCapture: () => {
|
|
90
|
+
drag.current = null;
|
|
91
|
+
}, className: "flex cursor-grab touch-none select-none h-11 shrink-0 items-center justify-between border-b\n border-[#EBECF0] bg-[#F4F5F7] px-3.5", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("code", { className: "text-xs text-[#6B778C]", children: ["<", tag, ">"] }), _jsx("span", { className: "text-[13px] font-semibold text-[#091E42]", children: t(`taginfo.${tag.toUpperCase()}`, tag) })] }), _jsx(Button, { variant: "ghost", size: "icon", "aria-label": t('inlineCanvas.chrome.close', '关闭'), "data-testid": "inline-style-panel-close", onClick: onClose, children: _jsx(X, { className: "size-4" }) })] }), _jsx("div", { className: "min-h-0 flex-1 flex-col overflow-y-auto", children: sections.map((section, index) => (_jsxs("section", { "data-testid": `style-section-${section.id}`, className: "flex flex-col gap-3 px-3.5 py-3.5", children: [index > 0 && _jsx(Separator, { className: "-mt-3.5 mb-0.5" }), _jsx("p", { className: "text-xs font-semibold text-[#091E42]", children: t(section.labelKey) }), _jsx("div", { className: "grid grid-cols-2 gap-2.5", children: section.fields.map(field => (_jsx(StyleFieldInput, { field: field, initial: declaredStyles[field.property] ?? '', onApply: onApply, onReset: resettableProperties === undefined ||
|
|
92
|
+
resettableProperties.includes(field.property)
|
|
93
|
+
? onReset
|
|
94
|
+
: undefined }, field.property))) })] }, section.id))) }), _jsx("footer", { className: "shrink-0 bg-[#F7F8F9] px-3.5 py-3 text-xs leading-5 text-[#6B778C]", children: t('inlineCanvas.style.footnote', '每次改动都会记入改动清单,可单条撤销') })] }));
|
|
13
95
|
}
|
|
14
|
-
function StyleFieldInput({ field, initial, onApply }) {
|
|
96
|
+
function StyleFieldInput({ field, initial, onApply, onReset }) {
|
|
15
97
|
const { t } = useTranslation();
|
|
16
98
|
const [value, setValue] = useState(initial);
|
|
17
99
|
const [committed, setCommitted] = useState(initial);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
setValue(initial);
|
|
102
|
+
setCommitted(initial);
|
|
103
|
+
}, [initial]);
|
|
18
104
|
const commit = () => {
|
|
19
105
|
const composed = composeStyleValue(field, value);
|
|
20
106
|
if (!composed || composed === committed)
|
|
@@ -24,17 +110,27 @@ function StyleFieldInput({ field, initial, onApply }) {
|
|
|
24
110
|
};
|
|
25
111
|
const label = t(field.labelKey);
|
|
26
112
|
const id = `style-${field.property}`;
|
|
27
|
-
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { htmlFor: id, className: "text-xs font-normal text-
|
|
113
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-1", children: [_jsx(Label, { htmlFor: id, className: "text-xs font-normal text-muted-foreground", children: label }), onReset && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": `${label} · ${t('inlineCanvas.review.reset', '清除覆盖')}`, onClick: () => {
|
|
114
|
+
setValue(initial);
|
|
115
|
+
setCommitted(initial);
|
|
116
|
+
onReset(field.property);
|
|
117
|
+
}, children: t('inlineCanvas.review.reset', '清除覆盖') }))] }), field.kind === 'select' ? (_jsxs(NativeSelect, { id: id, "data-testid": id, "aria-label": label, value: value, onChange: e => {
|
|
28
118
|
const next = e.target.value;
|
|
29
119
|
setValue(next);
|
|
30
120
|
if (next && next !== committed) {
|
|
31
121
|
setCommitted(next);
|
|
32
122
|
void onApply(field.property, next);
|
|
33
123
|
}
|
|
34
|
-
}, children: [_jsx("option", { value: "", children: "--" }), field.options?.map(o => (_jsx("option", { value: o, children:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
124
|
+
}, children: [_jsx("option", { value: "", children: "--" }), field.options?.map(o => (_jsx("option", { value: o, children: field.property === 'text-align'
|
|
125
|
+
? t(`inlineCanvas.review.align_${o}`, o)
|
|
126
|
+
: o }, o)))] })) : (_jsxs("div", { className: "flex items-center gap-1", children: [field.kind === 'color' && (_jsx("input", { type: "color", "aria-label": `${label} · ${t('inlineCanvas.review.colorSwatch', '选择颜色')}`, value: /^#[0-9a-f]{6}$/i.test(value) ? value : '#000000', className: "h-8 w-8 shrink-0 cursor-pointer rounded border border-border", onChange: e => {
|
|
127
|
+
setValue(e.target.value);
|
|
128
|
+
setCommitted(e.target.value);
|
|
129
|
+
void onApply(field.property, e.target.value);
|
|
130
|
+
} })), _jsxs(InputGroup, { children: [_jsx(InputGroupInput, { id: id, "data-testid": id, "aria-label": label, value: value, type: field.kind === 'color' ? 'text' : 'text', placeholder: field.unit ? 'auto / 100% / 1em' : undefined, onChange: e => setValue(e.target.value), onBlur: commit, onKeyDown: e => {
|
|
131
|
+
if (e.key === 'Enter') {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
commit();
|
|
134
|
+
}
|
|
135
|
+
} }), field.unit && (_jsx(InputGroupAddon, { align: "inline-end", children: _jsx(InputGroupText, { children: field.unit }) }))] })] }))] }));
|
|
40
136
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export declare const reviewLocales: {
|
|
2
|
+
readonly 'zh-CN': {
|
|
3
|
+
readonly pageLoadFailed: "目标页面无法加载";
|
|
4
|
+
readonly pageLoadHint: "请返回检查页面地址、网站访问权限和网络连接后重试。";
|
|
5
|
+
readonly backFromFailed: "返回修改";
|
|
6
|
+
readonly generating: "正在生成截图…";
|
|
7
|
+
readonly noLayers: "没有匹配的图层";
|
|
8
|
+
readonly clearSearch: "清除搜索";
|
|
9
|
+
readonly searchLayers: "搜索图层";
|
|
10
|
+
readonly linkCopied: "链接已复制";
|
|
11
|
+
readonly copyLinkManual: "复制失败,请从下方手动复制链接";
|
|
12
|
+
readonly uploadError: "上传失败,可重试;本地图片仍可下载";
|
|
13
|
+
readonly screenshot: "元素截图";
|
|
14
|
+
readonly enlarge: "放大预览";
|
|
15
|
+
readonly shotTruncated: "元素超出可视区,只截到可见部分";
|
|
16
|
+
readonly shotScale: "截图分辨率约为原图的 {{scale}}%";
|
|
17
|
+
readonly downloadImage: "下载图片";
|
|
18
|
+
readonly imageCopied: "图片已复制";
|
|
19
|
+
readonly copyError: "复制失败,请下载图片";
|
|
20
|
+
readonly copyImage: "复制图片";
|
|
21
|
+
readonly uploading: "正在上传…";
|
|
22
|
+
readonly copyLink: "复制链接";
|
|
23
|
+
readonly shotLink: "截图链接";
|
|
24
|
+
readonly reset: "清除覆盖";
|
|
25
|
+
readonly colorSwatch: "选择颜色";
|
|
26
|
+
readonly saveDraft: "保存草稿";
|
|
27
|
+
readonly versions: "版本管理";
|
|
28
|
+
readonly removeEmptyVersion: "移除空版本";
|
|
29
|
+
readonly versionLimit: "最多 4 个版本";
|
|
30
|
+
readonly imageLimit: "请选择不超过 10 MB 的图片";
|
|
31
|
+
readonly currentMedia: "当前媒体";
|
|
32
|
+
readonly uploadFile: "上传文件";
|
|
33
|
+
readonly useLink: "使用链接";
|
|
34
|
+
readonly replaceImage: "替换当前图片";
|
|
35
|
+
readonly replaceBackground: "替换背景图片";
|
|
36
|
+
readonly insertImage: "插入新图片";
|
|
37
|
+
readonly dropImage: "拖入图片或选择文件,支持图片格式,最大 10 MB。上传只准备素材,应用后才修改画布。";
|
|
38
|
+
readonly pendingMedia: "待应用媒体";
|
|
39
|
+
readonly altHelp: "替代文本用于描述图片内容。装饰图片可留空。";
|
|
40
|
+
readonly videoLinkHint: "支持视频文件链接和 YouTube 嵌入链接。加载就绪后应用到画布。";
|
|
41
|
+
readonly mediaPreview: "媒体预览";
|
|
42
|
+
readonly loading: "正在加载预览…";
|
|
43
|
+
readonly loadError: "预览加载失败";
|
|
44
|
+
readonly retry: "重试";
|
|
45
|
+
readonly ready: "媒体已就绪,应用后修改画布";
|
|
46
|
+
readonly align_left: "左对齐";
|
|
47
|
+
readonly align_center: "居中";
|
|
48
|
+
readonly align_right: "右对齐";
|
|
49
|
+
readonly align_justify: "两端对齐";
|
|
50
|
+
};
|
|
51
|
+
readonly 'en-US': {
|
|
52
|
+
readonly pageLoadFailed: "Unable to load the target page";
|
|
53
|
+
readonly pageLoadHint: "Go back to check the page URL, site permissions and network connection, then retry.";
|
|
54
|
+
readonly backFromFailed: "Go back";
|
|
55
|
+
readonly generating: "Generating screenshot…";
|
|
56
|
+
readonly noLayers: "No matching layers";
|
|
57
|
+
readonly clearSearch: "Clear search";
|
|
58
|
+
readonly searchLayers: "Search layers";
|
|
59
|
+
readonly linkCopied: "Link copied";
|
|
60
|
+
readonly copyLinkManual: "Copy failed. Copy the link below manually.";
|
|
61
|
+
readonly uploadError: "Upload failed. Retry or download the local image.";
|
|
62
|
+
readonly screenshot: "Element screenshot";
|
|
63
|
+
readonly enlarge: "Enlarge preview";
|
|
64
|
+
readonly shotTruncated: "Only the visible part of the element was captured.";
|
|
65
|
+
readonly shotScale: "Screenshot resolution is about {{scale}}% of the original.";
|
|
66
|
+
readonly downloadImage: "Download image";
|
|
67
|
+
readonly imageCopied: "Image copied";
|
|
68
|
+
readonly copyError: "Copy failed. Download the image instead.";
|
|
69
|
+
readonly copyImage: "Copy image";
|
|
70
|
+
readonly uploading: "Uploading…";
|
|
71
|
+
readonly copyLink: "Copy link";
|
|
72
|
+
readonly shotLink: "Screenshot link";
|
|
73
|
+
readonly reset: "Clear override";
|
|
74
|
+
readonly colorSwatch: "Choose color";
|
|
75
|
+
readonly saveDraft: "Save draft";
|
|
76
|
+
readonly versions: "Manage versions";
|
|
77
|
+
readonly removeEmptyVersion: "Remove empty version";
|
|
78
|
+
readonly versionLimit: "Maximum 4 versions";
|
|
79
|
+
readonly imageLimit: "Choose an image up to 10 MB.";
|
|
80
|
+
readonly currentMedia: "Current media";
|
|
81
|
+
readonly uploadFile: "Upload file";
|
|
82
|
+
readonly useLink: "Use link";
|
|
83
|
+
readonly replaceImage: "Replace current image";
|
|
84
|
+
readonly replaceBackground: "Replace background image";
|
|
85
|
+
readonly insertImage: "Insert new image";
|
|
86
|
+
readonly dropImage: "Drop an image or choose a file, up to 10 MB. Upload prepares the asset; Apply changes the canvas.";
|
|
87
|
+
readonly pendingMedia: "Media to apply";
|
|
88
|
+
readonly altHelp: "Describe the image for assistive technology. Leave blank for decorative images.";
|
|
89
|
+
readonly videoLinkHint: "Use a video file URL or YouTube embed link. Apply after the preview is ready.";
|
|
90
|
+
readonly mediaPreview: "Media preview";
|
|
91
|
+
readonly loading: "Loading preview…";
|
|
92
|
+
readonly loadError: "Preview failed to load";
|
|
93
|
+
readonly retry: "Retry";
|
|
94
|
+
readonly ready: "Ready. Apply to update the canvas.";
|
|
95
|
+
readonly align_left: "Align left";
|
|
96
|
+
readonly align_center: "Center";
|
|
97
|
+
readonly align_right: "Align right";
|
|
98
|
+
readonly align_justify: "Justify";
|
|
99
|
+
};
|
|
100
|
+
readonly 'ja-JP': {
|
|
101
|
+
readonly pageLoadFailed: "対象ページを読み込めません";
|
|
102
|
+
readonly pageLoadHint: "戻ってページ URL、サイトへのアクセス権限、ネットワーク接続を確認して再試行してください。";
|
|
103
|
+
readonly backFromFailed: "戻って修正";
|
|
104
|
+
readonly generating: "スクリーンショットを生成中…";
|
|
105
|
+
readonly noLayers: "一致するレイヤーがありません";
|
|
106
|
+
readonly clearSearch: "検索をクリア";
|
|
107
|
+
readonly searchLayers: "レイヤーを検索";
|
|
108
|
+
readonly linkCopied: "リンクをコピーしました";
|
|
109
|
+
readonly copyLinkManual: "コピーできませんでした。下のリンクを手動でコピーしてください。";
|
|
110
|
+
readonly uploadError: "アップロードできませんでした。再試行または画像をダウンロードしてください。";
|
|
111
|
+
readonly screenshot: "要素のスクリーンショット";
|
|
112
|
+
readonly enlarge: "プレビューを拡大";
|
|
113
|
+
readonly shotTruncated: "表示されている部分のみ撮影しました。";
|
|
114
|
+
readonly shotScale: "解像度は元の約 {{scale}}% です。";
|
|
115
|
+
readonly downloadImage: "画像をダウンロード";
|
|
116
|
+
readonly imageCopied: "画像をコピーしました";
|
|
117
|
+
readonly copyError: "コピーできませんでした。画像をダウンロードしてください。";
|
|
118
|
+
readonly copyImage: "画像をコピー";
|
|
119
|
+
readonly uploading: "アップロード中…";
|
|
120
|
+
readonly copyLink: "リンクをコピー";
|
|
121
|
+
readonly shotLink: "スクリーンショットのリンク";
|
|
122
|
+
readonly reset: "上書きを解除";
|
|
123
|
+
readonly colorSwatch: "色を選択";
|
|
124
|
+
readonly saveDraft: "下書きを保存";
|
|
125
|
+
readonly versions: "バージョン管理";
|
|
126
|
+
readonly removeEmptyVersion: "空のバージョンを削除";
|
|
127
|
+
readonly versionLimit: "最大4バージョン";
|
|
128
|
+
readonly imageLimit: "10 MB 以下の画像を選択してください。";
|
|
129
|
+
readonly currentMedia: "現在のメディア";
|
|
130
|
+
readonly uploadFile: "ファイルをアップロード";
|
|
131
|
+
readonly useLink: "リンクを使用";
|
|
132
|
+
readonly replaceImage: "現在の画像を置き換え";
|
|
133
|
+
readonly replaceBackground: "背景画像を置き換え";
|
|
134
|
+
readonly insertImage: "新しい画像を挿入";
|
|
135
|
+
readonly dropImage: "画像をドロップするか選択してください(最大10 MB)。アップロードで素材を準備し、適用でキャンバスを変更します。";
|
|
136
|
+
readonly pendingMedia: "適用するメディア";
|
|
137
|
+
readonly altHelp: "画像の内容を説明してください。装飾用の画像は空欄にできます。";
|
|
138
|
+
readonly videoLinkHint: "動画ファイルのURLまたはYouTube埋め込みリンクを入力し、読み込み後に適用してください。";
|
|
139
|
+
readonly mediaPreview: "メディアプレビュー";
|
|
140
|
+
readonly loading: "プレビューを読み込み中…";
|
|
141
|
+
readonly loadError: "プレビューを読み込めませんでした";
|
|
142
|
+
readonly retry: "再試行";
|
|
143
|
+
readonly ready: "準備完了。適用するとキャンバスを変更します。";
|
|
144
|
+
readonly align_left: "左揃え";
|
|
145
|
+
readonly align_center: "中央揃え";
|
|
146
|
+
readonly align_right: "右揃え";
|
|
147
|
+
readonly align_justify: "両端揃え";
|
|
148
|
+
};
|
|
149
|
+
};
|