@arronqzy/react-view 1.0.22 → 1.0.23
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/package.json +11 -11
- package/src/panel/ReactViewPanel.tsx +109 -965
- package/src/panel/components/PanelConfigSidebar.tsx +297 -3712
- package/src/panel/components/PanelHistoryPopover.tsx +163 -0
- package/src/panel/components/PanelLayerDock.tsx +412 -0
- package/src/panel/components/PanelMenubar.tsx +149 -0
- package/src/panel/components/PanelWorkspaceDialogs.tsx +271 -0
- package/src/panel/components/config/PanelConfigAudioSection.tsx +334 -0
- package/src/panel/components/config/PanelConfigChartSection.tsx +799 -0
- package/src/panel/components/config/PanelConfigGeometrySection.tsx +233 -0
- package/src/panel/components/config/PanelConfigGridChildSpan.tsx +70 -0
- package/src/panel/components/config/PanelConfigGridSection.tsx +118 -0
- package/src/panel/components/config/PanelConfigMultiSelect.tsx +1291 -0
- package/src/panel/components/config/PanelConfigNodeInfo.tsx +169 -0
- package/src/panel/components/config/PanelConfigReferenceSection.tsx +99 -0
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +6 -2
- package/src/panel/components/config/PanelConfigStyleSections.tsx +249 -0
- package/src/panel/components/config/PanelConfigTextSection.tsx +268 -0
- package/src/panel/components/config/PanelConfigVideoSection.tsx +133 -0
- package/src/panel/components/config/PanelConfigViewportSection.tsx +139 -0
- package/src/panel/components/config/helpers.ts +85 -0
- package/src/panel/icons.tsx +160 -0
- package/src/panel/utils/panelSelection.ts +57 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Menubar,
|
|
4
|
+
MenubarCheckboxItem,
|
|
5
|
+
MenubarContent,
|
|
6
|
+
MenubarItem,
|
|
7
|
+
MenubarMenu,
|
|
8
|
+
MenubarRadioGroup,
|
|
9
|
+
MenubarRadioItem,
|
|
10
|
+
MenubarSeparator,
|
|
11
|
+
MenubarTrigger,
|
|
12
|
+
} from "@arronqzy/ui";
|
|
13
|
+
import type { Locale } from "@arronqzy/i18n";
|
|
14
|
+
|
|
15
|
+
export type PanelMenubarProps = {
|
|
16
|
+
t: (key: string, params?: Record<string, string | number>) => string;
|
|
17
|
+
handleExport: () => void;
|
|
18
|
+
importInputRef: React.RefObject<HTMLInputElement | null>;
|
|
19
|
+
blueprintImportInputRef: React.RefObject<HTMLInputElement | null>;
|
|
20
|
+
canUndo: boolean;
|
|
21
|
+
canRedo: boolean;
|
|
22
|
+
undo: () => void;
|
|
23
|
+
redo: () => void;
|
|
24
|
+
hasUnlockedSelection: boolean;
|
|
25
|
+
selectedIds: string[];
|
|
26
|
+
bringElementsForward: (ids: string[]) => void;
|
|
27
|
+
sendElementsBackward: (ids: string[]) => void;
|
|
28
|
+
bringElementsToFront: (ids: string[]) => void;
|
|
29
|
+
sendElementsToBack: (ids: string[]) => void;
|
|
30
|
+
adjustUniformZoom: (updater: (value: number) => number) => void;
|
|
31
|
+
applyTheme: (dark: boolean) => void;
|
|
32
|
+
isDark: boolean;
|
|
33
|
+
openBlueprintExportDialog: () => void;
|
|
34
|
+
panelFontSize: "sm" | "md" | "lg";
|
|
35
|
+
setPanelFontSize: (value: "sm" | "md" | "lg") => void;
|
|
36
|
+
locale: Locale;
|
|
37
|
+
setLocale: (locale: Locale) => void;
|
|
38
|
+
outputScale: boolean;
|
|
39
|
+
setOutputScale: (value: boolean) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function PanelMenubar({
|
|
43
|
+
t,
|
|
44
|
+
handleExport,
|
|
45
|
+
importInputRef,
|
|
46
|
+
blueprintImportInputRef,
|
|
47
|
+
canUndo,
|
|
48
|
+
canRedo,
|
|
49
|
+
undo,
|
|
50
|
+
redo,
|
|
51
|
+
hasUnlockedSelection,
|
|
52
|
+
selectedIds,
|
|
53
|
+
bringElementsForward,
|
|
54
|
+
sendElementsBackward,
|
|
55
|
+
bringElementsToFront,
|
|
56
|
+
sendElementsToBack,
|
|
57
|
+
adjustUniformZoom,
|
|
58
|
+
applyTheme,
|
|
59
|
+
isDark,
|
|
60
|
+
openBlueprintExportDialog,
|
|
61
|
+
panelFontSize,
|
|
62
|
+
setPanelFontSize,
|
|
63
|
+
locale,
|
|
64
|
+
setLocale,
|
|
65
|
+
outputScale,
|
|
66
|
+
setOutputScale,
|
|
67
|
+
}: PanelMenubarProps) {
|
|
68
|
+
return (
|
|
69
|
+
<Menubar className="h-8 border-0 bg-transparent p-0 shadow-none">
|
|
70
|
+
<MenubarMenu>
|
|
71
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.file")}</MenubarTrigger>
|
|
72
|
+
<MenubarContent className="z-[10100]">
|
|
73
|
+
{/* <MenubarItem onClick={handlePreviewLayer}>{t("panel.workspace.previewDocTitle")}</MenubarItem> */}
|
|
74
|
+
<MenubarItem onClick={handleExport}>{t("panel.menubar.export")}</MenubarItem>
|
|
75
|
+
<MenubarItem onClick={() => importInputRef.current?.click()}>{t("panel.menubar.import")}</MenubarItem>
|
|
76
|
+
</MenubarContent>
|
|
77
|
+
</MenubarMenu>
|
|
78
|
+
<MenubarMenu>
|
|
79
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.edit")}</MenubarTrigger>
|
|
80
|
+
<MenubarContent className="z-[10100]">
|
|
81
|
+
<MenubarItem disabled={!canUndo} onClick={undo}>{t("panel.menubar.undo")}</MenubarItem>
|
|
82
|
+
<MenubarItem disabled={!canRedo} onClick={redo}>{t("panel.menubar.redo")}</MenubarItem>
|
|
83
|
+
<MenubarSeparator />
|
|
84
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsForward(selectedIds)}>{t("panel.menubar.bringForward")}</MenubarItem>
|
|
85
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsBackward(selectedIds)}>{t("panel.menubar.sendBackward")}</MenubarItem>
|
|
86
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsToFront(selectedIds)}>{t("panel.menubar.bringToFront")}</MenubarItem>
|
|
87
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsToBack(selectedIds)}>{t("panel.menubar.sendToBack")}</MenubarItem>
|
|
88
|
+
</MenubarContent>
|
|
89
|
+
</MenubarMenu>
|
|
90
|
+
<MenubarMenu>
|
|
91
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.view")}</MenubarTrigger>
|
|
92
|
+
<MenubarContent className="z-[10100]">
|
|
93
|
+
<MenubarItem onClick={() => adjustUniformZoom((z) => z - 0.1)}>{t("panel.menubar.zoomOut")}</MenubarItem>
|
|
94
|
+
<MenubarItem onClick={() => adjustUniformZoom((z) => z + 0.1)}>{t("panel.menubar.zoomIn")}</MenubarItem>
|
|
95
|
+
<MenubarSeparator />
|
|
96
|
+
<MenubarItem
|
|
97
|
+
onClick={() => {
|
|
98
|
+
applyTheme(!isDark);
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{isDark ? t("panel.menubar.switchToLight") : t("panel.menubar.switchToDark")}
|
|
102
|
+
</MenubarItem>
|
|
103
|
+
</MenubarContent>
|
|
104
|
+
</MenubarMenu>
|
|
105
|
+
<MenubarMenu>
|
|
106
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.blueprint")}</MenubarTrigger>
|
|
107
|
+
<MenubarContent className="z-[10100]">
|
|
108
|
+
<MenubarItem onClick={openBlueprintExportDialog}>{t("panel.menubar.export")}</MenubarItem>
|
|
109
|
+
<MenubarItem onClick={() => blueprintImportInputRef.current?.click()}>
|
|
110
|
+
{t("panel.menubar.import")}
|
|
111
|
+
</MenubarItem>
|
|
112
|
+
</MenubarContent>
|
|
113
|
+
</MenubarMenu>
|
|
114
|
+
<MenubarMenu>
|
|
115
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.settings")}</MenubarTrigger>
|
|
116
|
+
<MenubarContent className="z-[10100]">
|
|
117
|
+
<MenubarRadioGroup
|
|
118
|
+
value={panelFontSize}
|
|
119
|
+
onValueChange={(value) => setPanelFontSize(value as "sm" | "md" | "lg")}
|
|
120
|
+
>
|
|
121
|
+
<MenubarRadioItem value="sm">{t("panel.menubar.fontSmall")}</MenubarRadioItem>
|
|
122
|
+
<MenubarRadioItem value="md">{t("panel.menubar.fontMedium")}</MenubarRadioItem>
|
|
123
|
+
<MenubarRadioItem value="lg">{t("panel.menubar.fontLarge")}</MenubarRadioItem>
|
|
124
|
+
</MenubarRadioGroup>
|
|
125
|
+
<MenubarSeparator />
|
|
126
|
+
<div className="px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
127
|
+
{t("panel.theme.language")}
|
|
128
|
+
</div>
|
|
129
|
+
<MenubarRadioGroup
|
|
130
|
+
value={locale}
|
|
131
|
+
onValueChange={(value) => setLocale(value as "zh-CN" | "en-US")}
|
|
132
|
+
>
|
|
133
|
+
<MenubarRadioItem value="zh-CN">{t("panel.theme.zhCN")}</MenubarRadioItem>
|
|
134
|
+
<MenubarRadioItem value="en-US">{t("panel.theme.enUS")}</MenubarRadioItem>
|
|
135
|
+
</MenubarRadioGroup>
|
|
136
|
+
<MenubarSeparator />
|
|
137
|
+
<MenubarCheckboxItem
|
|
138
|
+
checked={outputScale}
|
|
139
|
+
onCheckedChange={(checked) => setOutputScale(checked === true)}
|
|
140
|
+
title={t("panel.menubar.outputScaleHint")}
|
|
141
|
+
>
|
|
142
|
+
{t("panel.menubar.outputScale")}
|
|
143
|
+
</MenubarCheckboxItem>
|
|
144
|
+
</MenubarContent>
|
|
145
|
+
</MenubarMenu>
|
|
146
|
+
</Menubar>
|
|
147
|
+
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
AlertDialog,
|
|
4
|
+
AlertDialogAction,
|
|
5
|
+
AlertDialogCancel,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogDescription,
|
|
14
|
+
DialogFooter,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
RadioGroup,
|
|
18
|
+
RadioGroupItem,
|
|
19
|
+
Select,
|
|
20
|
+
SelectContent,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectTrigger,
|
|
23
|
+
SelectValue,
|
|
24
|
+
} from "@arronqzy/ui";
|
|
25
|
+
import type { PanelActionResult, PanelLayer } from "../types";
|
|
26
|
+
|
|
27
|
+
export type PanelWorkspaceDialogsProps = {
|
|
28
|
+
t: (key: string, params?: Record<string, string | number>) => string;
|
|
29
|
+
mappingDeleteConfirmOpen: boolean;
|
|
30
|
+
setMappingDeleteConfirmOpen: (open: boolean) => void;
|
|
31
|
+
mappingDeleteProceedRef: React.MutableRefObject<(() => void) | null>;
|
|
32
|
+
mappingDeleteImpactCount: number;
|
|
33
|
+
deletingLayer: PanelLayer | null;
|
|
34
|
+
deletingLayerMode: "move" | "remove";
|
|
35
|
+
setConfirmDeleteLayerId: (id: string | null) => void;
|
|
36
|
+
setDeleteMode: (mode: "move" | "remove") => void;
|
|
37
|
+
setDeleteTargetLayerId: (id: string) => void;
|
|
38
|
+
deleteTargetLayerId: string;
|
|
39
|
+
deleteTargetCandidates: PanelLayer[];
|
|
40
|
+
deleteLayer: (
|
|
41
|
+
id: string,
|
|
42
|
+
options: { mode: "move" | "remove"; targetLayerId?: string }
|
|
43
|
+
) => PanelActionResult;
|
|
44
|
+
showActionHint: (message: string) => void;
|
|
45
|
+
titleIconPreviewOpen: boolean;
|
|
46
|
+
setTitleIconPreviewOpen: (open: boolean) => void;
|
|
47
|
+
titleIconZoom: number;
|
|
48
|
+
setTitleIconZoom: React.Dispatch<React.SetStateAction<number>>;
|
|
49
|
+
titleIconDataUrl: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function PanelWorkspaceDialogs({
|
|
53
|
+
t,
|
|
54
|
+
mappingDeleteConfirmOpen,
|
|
55
|
+
setMappingDeleteConfirmOpen,
|
|
56
|
+
mappingDeleteProceedRef,
|
|
57
|
+
mappingDeleteImpactCount,
|
|
58
|
+
deletingLayer,
|
|
59
|
+
deletingLayerMode,
|
|
60
|
+
setConfirmDeleteLayerId,
|
|
61
|
+
setDeleteMode,
|
|
62
|
+
setDeleteTargetLayerId,
|
|
63
|
+
deleteTargetLayerId,
|
|
64
|
+
deleteTargetCandidates,
|
|
65
|
+
deleteLayer,
|
|
66
|
+
showActionHint,
|
|
67
|
+
titleIconPreviewOpen,
|
|
68
|
+
setTitleIconPreviewOpen,
|
|
69
|
+
titleIconZoom,
|
|
70
|
+
setTitleIconZoom,
|
|
71
|
+
titleIconDataUrl,
|
|
72
|
+
}: PanelWorkspaceDialogsProps) {
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<AlertDialog
|
|
76
|
+
open={mappingDeleteConfirmOpen}
|
|
77
|
+
onOpenChange={(open) => {
|
|
78
|
+
setMappingDeleteConfirmOpen(open);
|
|
79
|
+
if (!open) mappingDeleteProceedRef.current = null;
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<AlertDialogContent overlayClassName="bg-transparent pointer-events-none">
|
|
83
|
+
<AlertDialogHeader>
|
|
84
|
+
<AlertDialogTitle>{t("panel.layers.confirmDeleteTitle")}</AlertDialogTitle>
|
|
85
|
+
<AlertDialogDescription>
|
|
86
|
+
{t("panel.layers.confirmDeleteMappingBody", {
|
|
87
|
+
count: mappingDeleteImpactCount,
|
|
88
|
+
})}
|
|
89
|
+
</AlertDialogDescription>
|
|
90
|
+
</AlertDialogHeader>
|
|
91
|
+
<AlertDialogFooter>
|
|
92
|
+
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
93
|
+
<AlertDialogAction
|
|
94
|
+
onClick={() => {
|
|
95
|
+
const fn = mappingDeleteProceedRef.current;
|
|
96
|
+
mappingDeleteProceedRef.current = null;
|
|
97
|
+
setMappingDeleteConfirmOpen(false);
|
|
98
|
+
fn?.();
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{t("panel.layers.continueDelete")}
|
|
102
|
+
</AlertDialogAction>
|
|
103
|
+
</AlertDialogFooter>
|
|
104
|
+
</AlertDialogContent>
|
|
105
|
+
</AlertDialog>
|
|
106
|
+
<Dialog
|
|
107
|
+
open={Boolean(deletingLayer)}
|
|
108
|
+
onOpenChange={(open) => {
|
|
109
|
+
if (open) return;
|
|
110
|
+
setConfirmDeleteLayerId(null);
|
|
111
|
+
setDeleteMode("move");
|
|
112
|
+
setDeleteTargetLayerId("");
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<DialogContent
|
|
116
|
+
className="sm:max-w-[520px]"
|
|
117
|
+
overlayClassName="bg-transparent pointer-events-none"
|
|
118
|
+
>
|
|
119
|
+
<DialogHeader>
|
|
120
|
+
<DialogTitle>{t("panel.layers.confirmDeleteLayerTitle")}</DialogTitle>
|
|
121
|
+
<DialogDescription>
|
|
122
|
+
{t("panel.layers.aboutToDeleteLayer", {
|
|
123
|
+
name: deletingLayer?.name ?? "-",
|
|
124
|
+
})}
|
|
125
|
+
</DialogDescription>
|
|
126
|
+
</DialogHeader>
|
|
127
|
+
{deletingLayer ? (
|
|
128
|
+
<RadioGroup
|
|
129
|
+
value={deletingLayerMode}
|
|
130
|
+
onValueChange={(value) => setDeleteMode(value as "move" | "remove")}
|
|
131
|
+
className="space-y-2 text-sm"
|
|
132
|
+
>
|
|
133
|
+
{!deletingLayer.isMapping ? (
|
|
134
|
+
<div className="flex items-center gap-3">
|
|
135
|
+
<label className="flex items-center gap-1.5">
|
|
136
|
+
<RadioGroupItem value="move" />
|
|
137
|
+
{t("panel.layers.migrateNodesTo")}
|
|
138
|
+
</label>
|
|
139
|
+
<Select
|
|
140
|
+
value={deleteTargetLayerId || "__none__"}
|
|
141
|
+
onValueChange={(value) =>
|
|
142
|
+
setDeleteTargetLayerId(value === "__none__" ? "" : value)
|
|
143
|
+
}
|
|
144
|
+
disabled={deletingLayerMode !== "move" || deleteTargetCandidates.length === 0}
|
|
145
|
+
>
|
|
146
|
+
<SelectTrigger className="h-8 w-[220px]">
|
|
147
|
+
<SelectValue placeholder={t("panel.layers.selectTargetLayer")} />
|
|
148
|
+
</SelectTrigger>
|
|
149
|
+
<SelectContent>
|
|
150
|
+
<SelectItem value="__none__">{t("panel.layers.pleaseSelectLayer")}</SelectItem>
|
|
151
|
+
{deleteTargetCandidates.map((l) => (
|
|
152
|
+
<SelectItem key={l.id} value={l.id}>
|
|
153
|
+
{l.name}
|
|
154
|
+
</SelectItem>
|
|
155
|
+
))}
|
|
156
|
+
</SelectContent>
|
|
157
|
+
</Select>
|
|
158
|
+
</div>
|
|
159
|
+
) : null}
|
|
160
|
+
<label className="flex items-center gap-1.5">
|
|
161
|
+
<RadioGroupItem value="remove" />
|
|
162
|
+
{deletingLayer.isMapping ? t("panel.layers.deleteMappingWithNodes") : t("panel.layers.deleteAllNodesToo")}
|
|
163
|
+
</label>
|
|
164
|
+
</RadioGroup>
|
|
165
|
+
) : null}
|
|
166
|
+
<DialogFooter>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={() => {
|
|
170
|
+
setConfirmDeleteLayerId(null);
|
|
171
|
+
setDeleteMode("move");
|
|
172
|
+
setDeleteTargetLayerId("");
|
|
173
|
+
}}
|
|
174
|
+
className="rounded border border-border px-3 py-1.5 text-sm hover:bg-accent"
|
|
175
|
+
>
|
|
176
|
+
{t("common.cancel")}
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={() => {
|
|
181
|
+
if (!deletingLayer) return;
|
|
182
|
+
const result = deleteLayer(deletingLayer.id, {
|
|
183
|
+
mode: deletingLayerMode,
|
|
184
|
+
targetLayerId: deleteTargetLayerId || undefined,
|
|
185
|
+
});
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
showActionHint(result.reason);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
setConfirmDeleteLayerId(null);
|
|
191
|
+
setDeleteMode("move");
|
|
192
|
+
setDeleteTargetLayerId("");
|
|
193
|
+
}}
|
|
194
|
+
className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
|
|
195
|
+
>
|
|
196
|
+
{t("panel.layers.confirmDeleteTitle")}
|
|
197
|
+
</button>
|
|
198
|
+
</DialogFooter>
|
|
199
|
+
</DialogContent>
|
|
200
|
+
</Dialog>
|
|
201
|
+
<Dialog
|
|
202
|
+
open={titleIconPreviewOpen}
|
|
203
|
+
onOpenChange={setTitleIconPreviewOpen}
|
|
204
|
+
>
|
|
205
|
+
<DialogContent
|
|
206
|
+
className="sm:max-w-[860px]"
|
|
207
|
+
overlayClassName="bg-transparent pointer-events-none"
|
|
208
|
+
>
|
|
209
|
+
<DialogHeader>
|
|
210
|
+
<DialogTitle>{t("panel.menubar.titleIconPreviewTitle")}</DialogTitle>
|
|
211
|
+
<DialogDescription>{t("panel.menubar.titleIconPreviewDesc")}</DialogDescription>
|
|
212
|
+
</DialogHeader>
|
|
213
|
+
<div className="flex items-center justify-end gap-2">
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
onClick={() => setTitleIconZoom((z) => Math.max(0.5, Number((z - 0.1).toFixed(2))))}
|
|
217
|
+
className="rounded border border-border px-2 py-1 text-xs hover:bg-accent"
|
|
218
|
+
>
|
|
219
|
+
-
|
|
220
|
+
</button>
|
|
221
|
+
<span className="w-14 text-center text-xs text-muted-foreground">
|
|
222
|
+
{(titleIconZoom * 100).toFixed(0)}%
|
|
223
|
+
</span>
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={() => setTitleIconZoom((z) => Math.min(8, Number((z + 0.1).toFixed(2))))}
|
|
227
|
+
className="rounded border border-border px-2 py-1 text-xs hover:bg-accent"
|
|
228
|
+
>
|
|
229
|
+
+
|
|
230
|
+
</button>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => setTitleIconZoom(1.6)}
|
|
234
|
+
className="rounded border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
|
|
235
|
+
>
|
|
236
|
+
{t("common.reset")}
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
{titleIconDataUrl ? (
|
|
240
|
+
<div
|
|
241
|
+
className="max-h-[78vh] overflow-auto rounded border border-border bg-muted/20 p-4"
|
|
242
|
+
onWheel={(e) => {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
245
|
+
setTitleIconZoom((z) => {
|
|
246
|
+
const next = Number((z + delta).toFixed(2));
|
|
247
|
+
return Math.min(8, Math.max(0.5, next));
|
|
248
|
+
});
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
<img
|
|
252
|
+
src={titleIconDataUrl}
|
|
253
|
+
alt={t("panel.menubar.titleIconPreviewAlt")}
|
|
254
|
+
className="mx-auto rounded border border-border/70 object-contain"
|
|
255
|
+
style={{
|
|
256
|
+
width: 200,
|
|
257
|
+
height: 200,
|
|
258
|
+
maxWidth: "none",
|
|
259
|
+
maxHeight: "none",
|
|
260
|
+
transform: `scale(${titleIconZoom})`,
|
|
261
|
+
transformOrigin: "center center",
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
) : null}
|
|
266
|
+
</DialogContent>
|
|
267
|
+
</Dialog>
|
|
268
|
+
|
|
269
|
+
</>
|
|
270
|
+
);
|
|
271
|
+
}
|