@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.
@@ -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
+ }