@arronqzy/react-view 1.0.21 → 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/readme.md +6 -1
- package/src/panel/I18nRoot.tsx +14 -2
- package/src/panel/ReactViewOnlinePreview.tsx +29 -18
- package/src/panel/ReactViewPanel.tsx +154 -1006
- 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/hooks/useWorkspaceProjects.ts +112 -34
- package/src/panel/icons.tsx +160 -0
- package/src/panel/index.tsx +15 -1
- package/src/panel/library/parse-workspace-data.ts +142 -0
- package/src/panel/library/workspace-project-cache.ts +13 -4
- package/src/panel/library/workspace-project-db.ts +24 -13
- package/src/panel/library/workspace-project-sync.ts +17 -6
- package/src/panel/utils/outputScale.ts +9 -4
- package/src/panel/utils/panelSelection.ts +57 -0
- package/src/panel/utils/panelStateIO.ts +18 -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
|
+
}
|