@arronqzy/react-view 1.0.22 → 1.0.24

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,163 @@
1
+ import React from "react";
2
+ import {
3
+ Empty,
4
+ EmptyDescription,
5
+ EmptyIcon,
6
+ EmptyTitle,
7
+ Input,
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipTrigger,
11
+ } from "@arronqzy/ui";
12
+ import { PANEL_Z_INDEX } from "../constants/zIndex";
13
+ import { IconHistory } from "../icons";
14
+ import type { PanelHistoryItem } from "../types";
15
+ import { formatRelativeTime } from "../utils/panelSelection";
16
+
17
+ export type PanelHistoryPopoverProps = {
18
+ t: (key: string, params?: Record<string, string | number>) => string;
19
+ isHistoryPanelExpanded: boolean;
20
+ setIsHistoryPanelExpanded: React.Dispatch<React.SetStateAction<boolean>>;
21
+ history: PanelHistoryItem[];
22
+ historyCursor: number;
23
+ historyKeyword: string;
24
+ setHistoryKeyword: React.Dispatch<React.SetStateAction<string>>;
25
+ normalizedHistoryKeyword: string;
26
+ goToHistory: (index: number) => void;
27
+ historyNow: number;
28
+ themedScrollbarClass: string;
29
+ };
30
+
31
+ export function PanelHistoryPopover({
32
+ t,
33
+ isHistoryPanelExpanded,
34
+ setIsHistoryPanelExpanded,
35
+ history,
36
+ historyCursor,
37
+ historyKeyword,
38
+ setHistoryKeyword,
39
+ normalizedHistoryKeyword,
40
+ goToHistory,
41
+ historyNow,
42
+ themedScrollbarClass,
43
+ }: PanelHistoryPopoverProps) {
44
+ return (
45
+ <Tooltip>
46
+ <TooltipTrigger asChild>
47
+ <div className="relative">
48
+ <button
49
+ type="button"
50
+ onClick={() => setIsHistoryPanelExpanded((prev) => !prev)}
51
+ className={[
52
+ "rounded border border-border p-1 hover:bg-accent",
53
+ isHistoryPanelExpanded ? "bg-accent/60" : "",
54
+ ].join(" ")}
55
+ aria-label={isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
56
+ >
57
+ <IconHistory />
58
+ </button>
59
+ {isHistoryPanelExpanded ? (
60
+ <div
61
+ className="absolute left-1/2 top-[calc(100%+6px)] w-[280px] -translate-x-1/2 rounded-lg border border-border bg-card/95 p-2 shadow-lg backdrop-blur"
62
+ style={{ zIndex: PANEL_Z_INDEX.historyPopover }}
63
+ >
64
+ <div className="mb-1 flex items-center justify-between">
65
+ <span className="text-[11px] font-semibold text-muted-foreground">{t("panel.history.title")}</span>
66
+ <span className="text-[11px] text-muted-foreground">
67
+ {history.length > 0 ? `${historyCursor + 1}/${history.length}` : "0/0"}
68
+ </span>
69
+ </div>
70
+ <Input
71
+ value={historyKeyword}
72
+ onChange={(e) => setHistoryKeyword(e.target.value)}
73
+ placeholder={t("panel.history.searchPlaceholder")}
74
+ className="mb-2 h-7 text-xs"
75
+ />
76
+ <div
77
+ className={`max-h-52 space-y-1 overflow-auto pr-1 text-[11px] ${themedScrollbarClass}`}
78
+ >
79
+ {history.length === 0 ? (
80
+ <Empty className="py-4">
81
+ <EmptyIcon className="h-8 w-8">
82
+ <IconHistory />
83
+ </EmptyIcon>
84
+ <EmptyTitle className="text-xs">{t("panel.history.emptyTitle")}</EmptyTitle>
85
+ <EmptyDescription className="text-[11px]">
86
+ {t("panel.history.emptyDesc")}
87
+ </EmptyDescription>
88
+ </Empty>
89
+ ) : (
90
+ history
91
+ .slice(Math.max(0, history.length - 20))
92
+ .reverse()
93
+ .filter((item) =>
94
+ !normalizedHistoryKeyword
95
+ ? true
96
+ : item.label.toLowerCase().includes(normalizedHistoryKeyword)
97
+ )
98
+ .map((item) => (
99
+ <button
100
+ key={`${item.index}-${item.timestamp}`}
101
+ type="button"
102
+ className={[
103
+ "w-full rounded border px-2 py-1 text-left transition-colors",
104
+ item.active
105
+ ? "border-primary/50 bg-primary/10 text-foreground"
106
+ : "border-border/60 bg-background/70 text-muted-foreground hover:bg-accent/50",
107
+ ].join(" ")}
108
+ title={new Date(item.timestamp).toLocaleString()}
109
+ onClick={() => {
110
+ if (item.active) return;
111
+ goToHistory(item.index);
112
+ }}
113
+ >
114
+ <div className="flex items-center justify-between gap-2">
115
+ <span className="truncate">{item.label}</span>
116
+ <span className="shrink-0 text-[10px] text-muted-foreground">
117
+ {formatRelativeTime(item.timestamp, historyNow, t)}
118
+ </span>
119
+ </div>
120
+ </button>
121
+ ))
122
+ )}
123
+ {history.length > 0 &&
124
+ history
125
+ .slice(Math.max(0, history.length - 20))
126
+ .reverse()
127
+ .filter((item) =>
128
+ !normalizedHistoryKeyword
129
+ ? true
130
+ : item.label.toLowerCase().includes(normalizedHistoryKeyword)
131
+ ).length === 0 ? (
132
+ <Empty className="py-4">
133
+ <EmptyIcon className="h-8 w-8">
134
+ <svg
135
+ viewBox="0 0 24 24"
136
+ className="h-4 w-4"
137
+ fill="none"
138
+ stroke="currentColor"
139
+ strokeWidth="1.8"
140
+ aria-hidden="true"
141
+ >
142
+ <circle cx="11" cy="11" r="7" />
143
+ <path d="m20 20-3.5-3.5" />
144
+ </svg>
145
+ </EmptyIcon>
146
+ <EmptyTitle className="text-xs">{t("panel.history.noMatchTitle")}</EmptyTitle>
147
+ <EmptyDescription className="text-[11px]">
148
+ {t("panel.history.noMatchDesc")}
149
+ </EmptyDescription>
150
+ </Empty>
151
+ ) : null}
152
+ </div>
153
+ </div>
154
+ ) : null}
155
+ </div>
156
+ </TooltipTrigger>
157
+ <TooltipContent className="z-[10000]">
158
+ {isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
159
+ </TooltipContent>
160
+ </Tooltip>
161
+
162
+ );
163
+ }
@@ -0,0 +1,412 @@
1
+ import React from "react";
2
+ import {
3
+ Checkbox,
4
+ Input,
5
+ Tabs,
6
+ TabsContent,
7
+ TabsList,
8
+ TabsTrigger,
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipProvider,
12
+ TooltipTrigger,
13
+ } from "@arronqzy/ui";
14
+ import {
15
+ IconCheck,
16
+ IconChevron,
17
+ IconClose,
18
+ IconEdit,
19
+ IconLock,
20
+ IconMerge,
21
+ IconPlus,
22
+ IconTrash,
23
+ } from "../icons";
24
+ import type { PanelLayer } from "../types";
25
+
26
+ export type PanelLayerDockProps = {
27
+ t: (key: string, params?: Record<string, string | number>) => string;
28
+ isLayerPanelExpanded: boolean;
29
+ setIsLayerPanelExpanded: React.Dispatch<React.SetStateAction<boolean>>;
30
+ addLayer: () => void;
31
+ canMergeLayers: boolean;
32
+ setIsMergingLayers: React.Dispatch<React.SetStateAction<boolean>>;
33
+ isMergingLayers: boolean;
34
+ layers: PanelLayer[];
35
+ activeLayerId: string;
36
+ setActiveLayer: (layerId: string) => void;
37
+ themedScrollbarClass: string;
38
+ activeLayer: PanelLayer | null;
39
+ editingLayerId: string | null;
40
+ setEditingLayerId: React.Dispatch<React.SetStateAction<string | null>>;
41
+ editingLayerName: string;
42
+ setEditingLayerName: React.Dispatch<React.SetStateAction<string>>;
43
+ renameLayer: (layerId: string, name: string) => void;
44
+ setPrimaryLayer: (layerId: string) => void;
45
+ toggleLayerLock: (layerId: string) => void;
46
+ getLayerDeleteBlockReason: (layerId: string) => string | null;
47
+ showActionHint: (message: string) => void;
48
+ setConfirmDeleteLayerId: (id: string | null) => void;
49
+ setDeleteTargetLayerId: (id: string) => void;
50
+ setDeleteMode: (mode: "remove" | "move") => void;
51
+ toggleLayerMergeSelected: (layerId: string) => void;
52
+ mergeLayerName: string;
53
+ setMergeLayerName: React.Dispatch<React.SetStateAction<string>>;
54
+ mergeSelectedLayers: (name?: string) => void;
55
+ };
56
+
57
+ export function PanelLayerDock({
58
+ t,
59
+ isLayerPanelExpanded,
60
+ setIsLayerPanelExpanded,
61
+ addLayer,
62
+ canMergeLayers,
63
+ setIsMergingLayers,
64
+ isMergingLayers,
65
+ layers,
66
+ activeLayerId,
67
+ setActiveLayer,
68
+ themedScrollbarClass,
69
+ activeLayer,
70
+ editingLayerId,
71
+ setEditingLayerId,
72
+ editingLayerName,
73
+ setEditingLayerName,
74
+ renameLayer,
75
+ setPrimaryLayer,
76
+ toggleLayerLock,
77
+ getLayerDeleteBlockReason,
78
+ showActionHint,
79
+ setConfirmDeleteLayerId,
80
+ setDeleteTargetLayerId,
81
+ setDeleteMode,
82
+ toggleLayerMergeSelected,
83
+ mergeLayerName,
84
+ setMergeLayerName,
85
+ mergeSelectedLayers,
86
+ }: PanelLayerDockProps) {
87
+ return (
88
+ <div className="border-t border-border bg-background/95 px-2 py-1.5">
89
+ <TooltipProvider delayDuration={120}>
90
+ <Tabs value={activeLayerId} onValueChange={setActiveLayer}>
91
+ <div className="mb-1 flex items-center gap-2">
92
+ <div className="text-[11px] font-semibold text-muted-foreground">{t("panel.layers.title")}</div>
93
+ <Tooltip>
94
+ <TooltipTrigger asChild>
95
+ <button
96
+ type="button"
97
+ onClick={() => setIsLayerPanelExpanded((v) => !v)}
98
+ aria-label={isLayerPanelExpanded ? t("panel.layers.collapseInputAria") : t("panel.layers.expandInputAria")}
99
+ className="rounded border border-border p-1 hover:bg-accent"
100
+ >
101
+ <IconChevron expanded={isLayerPanelExpanded} />
102
+ </button>
103
+ </TooltipTrigger>
104
+ <TooltipContent className="z-[10000]">
105
+ {isLayerPanelExpanded ? t("panel.layers.collapseInput") : t("panel.layers.expandInput")}
106
+ </TooltipContent>
107
+ </Tooltip>
108
+ <Tooltip>
109
+ <TooltipTrigger asChild>
110
+ <button
111
+ type="button"
112
+ onClick={addLayer}
113
+ aria-label={t("panel.layers.add")}
114
+ className="rounded border border-border p-1 hover:bg-accent"
115
+ >
116
+ <IconPlus />
117
+ </button>
118
+ </TooltipTrigger>
119
+ <TooltipContent className="z-[10000]">{t("panel.layers.add")}</TooltipContent>
120
+ </Tooltip>
121
+ <Tooltip>
122
+ <TooltipTrigger asChild>
123
+ <button
124
+ type="button"
125
+ onClick={() => {
126
+ if (!canMergeLayers) return;
127
+ setIsMergingLayers(true);
128
+ }}
129
+ aria-label={t("panel.layers.merge")}
130
+ disabled={!canMergeLayers}
131
+ className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
132
+ >
133
+ <IconMerge />
134
+ </button>
135
+ </TooltipTrigger>
136
+ <TooltipContent className="z-[10000]">
137
+ {canMergeLayers ? t("panel.layers.merge") : t("panel.layers.mergeNeedTwo")}
138
+ </TooltipContent>
139
+ </Tooltip>
140
+ </div>
141
+
142
+ <TabsList
143
+ className={`h-auto w-full justify-start gap-1 overflow-x-auto bg-muted/40 p-1 ${themedScrollbarClass}`}
144
+ >
145
+ {layers.map((layer) => (
146
+ <TabsTrigger
147
+ key={layer.id}
148
+ value={layer.id}
149
+ className="flex min-w-[120px] items-center gap-1 px-2 py-1 text-xs"
150
+ >
151
+ <span className="truncate">{layer.name}</span>
152
+ {layer.isMapping ? (
153
+ <span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
154
+ {t("panel.layers.mapping")}
155
+ </span>
156
+ ) : null}
157
+ {layer.isPrimary ? (
158
+ <span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
159
+ {t("panel.layers.primary")}
160
+ </span>
161
+ ) : null}
162
+ {layer.locked ? <IconLock locked /> : null}
163
+ </TabsTrigger>
164
+ ))}
165
+ </TabsList>
166
+
167
+ <TabsContent value={activeLayerId} className="mt-2 space-y-2">
168
+ {activeLayer ? (
169
+ <div className="flex items-center gap-2 rounded border border-border bg-card px-2 py-1.5 text-xs">
170
+ {editingLayerId === activeLayer.id ? (
171
+ <Input
172
+ value={editingLayerName}
173
+ onChange={(e) => setEditingLayerName(e.target.value)}
174
+ onKeyDown={(e) => {
175
+ if (e.key === "Enter") {
176
+ e.preventDefault();
177
+ renameLayer(editingLayerId, editingLayerName);
178
+ setEditingLayerId(null);
179
+ setEditingLayerName("");
180
+ return;
181
+ }
182
+ if (e.key === "Escape") {
183
+ e.preventDefault();
184
+ setEditingLayerId(null);
185
+ setEditingLayerName("");
186
+ }
187
+ }}
188
+ className="h-7 min-w-0 flex-1"
189
+ placeholder={t("panel.layers.namePlaceholder")}
190
+ autoFocus
191
+ />
192
+ ) : (
193
+ <span className="truncate font-medium">{activeLayer.name}</span>
194
+ )}
195
+ {activeLayer.isMapping ? (
196
+ <span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
197
+ {t("panel.layers.mappingLayer")}
198
+ </span>
199
+ ) : null}
200
+ {activeLayer.isPrimary ? (
201
+ <span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
202
+ {t("panel.layers.primaryLayer")}
203
+ </span>
204
+ ) : null}
205
+ <div className="flex-1" />
206
+ <Tooltip>
207
+ <TooltipTrigger asChild>
208
+ <button
209
+ type="button"
210
+ onClick={() => setPrimaryLayer(activeLayer.id)}
211
+ disabled={activeLayer.isPrimary}
212
+ aria-label={activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
213
+ className={[
214
+ "rounded border px-2 py-1 text-[11px] disabled:cursor-not-allowed disabled:opacity-60",
215
+ activeLayer.isPrimary
216
+ ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-600"
217
+ : "border-border hover:bg-accent",
218
+ ].join(" ")}
219
+ >
220
+ {activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
221
+ </button>
222
+ </TooltipTrigger>
223
+ <TooltipContent className="z-[10000]">
224
+ {activeLayer.isPrimary ? t("panel.layers.isPrimaryHint") : t("panel.layers.setPrimaryHint")}
225
+ </TooltipContent>
226
+ </Tooltip>
227
+ <Tooltip>
228
+ <TooltipTrigger asChild>
229
+ <button
230
+ type="button"
231
+ onClick={() => toggleLayerLock(activeLayer.id)}
232
+ disabled={!activeLayer.editable}
233
+ aria-label={activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
234
+ className="rounded border border-border p-1 disabled:opacity-40"
235
+ >
236
+ <IconLock locked={activeLayer.locked} />
237
+ </button>
238
+ </TooltipTrigger>
239
+ <TooltipContent className="z-[10000]">
240
+ {activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
241
+ </TooltipContent>
242
+ </Tooltip>
243
+ <Tooltip>
244
+ <TooltipTrigger asChild>
245
+ {editingLayerId === activeLayer.id ? (
246
+ <button
247
+ type="button"
248
+ onClick={() => {
249
+ renameLayer(editingLayerId, editingLayerName);
250
+ setEditingLayerId(null);
251
+ setEditingLayerName("");
252
+ }}
253
+ aria-label={t("panel.layers.saveName")}
254
+ className="rounded border border-border p-1 hover:bg-accent"
255
+ >
256
+ <IconCheck />
257
+ </button>
258
+ ) : (
259
+ <button
260
+ type="button"
261
+ onClick={() => {
262
+ setEditingLayerId(activeLayer.id);
263
+ setEditingLayerName(activeLayer.name);
264
+ }}
265
+ disabled={!activeLayer.editable}
266
+ aria-label={t("panel.layers.rename")}
267
+ className="rounded border border-border p-1 disabled:opacity-40"
268
+ >
269
+ <IconEdit />
270
+ </button>
271
+ )}
272
+ </TooltipTrigger>
273
+ <TooltipContent className="z-[10000]">
274
+ {editingLayerId === activeLayer.id ? t("panel.layers.saveName") : t("panel.layers.rename")}
275
+ </TooltipContent>
276
+ </Tooltip>
277
+ {editingLayerId === activeLayer.id ? (
278
+ <Tooltip>
279
+ <TooltipTrigger asChild>
280
+ <button
281
+ type="button"
282
+ onClick={() => {
283
+ setEditingLayerId(null);
284
+ setEditingLayerName("");
285
+ }}
286
+ aria-label={t("panel.layers.cancelEditName")}
287
+ className="rounded border border-border p-1 hover:bg-accent"
288
+ >
289
+ <IconClose />
290
+ </button>
291
+ </TooltipTrigger>
292
+ <TooltipContent className="z-[10000]">{t("panel.layers.cancelEdit")}</TooltipContent>
293
+ </Tooltip>
294
+ ) : null}
295
+ <Tooltip>
296
+ <TooltipTrigger asChild>
297
+ <button
298
+ type="button"
299
+ onClick={() => {
300
+ const blockReason = getLayerDeleteBlockReason(activeLayer.id);
301
+ if (blockReason) {
302
+ showActionHint(blockReason);
303
+ return;
304
+ }
305
+ setConfirmDeleteLayerId(activeLayer.id);
306
+ const firstTarget = layers.find((l) => l.id !== activeLayer.id);
307
+ setDeleteTargetLayerId(firstTarget?.id ?? "");
308
+ setDeleteMode(activeLayer.isMapping ? "remove" : "move");
309
+ }}
310
+ disabled={!activeLayer.editable || activeLayer.locked}
311
+ aria-label={
312
+ !activeLayer.editable
313
+ ? t("panel.layers.cannotDeleteDefault")
314
+ : activeLayer.locked
315
+ ? t("panel.layers.cannotDeleteLocked")
316
+ : t("panel.layers.delete")
317
+ }
318
+ className="rounded border border-border p-1 disabled:opacity-40"
319
+ >
320
+ <IconTrash />
321
+ </button>
322
+ </TooltipTrigger>
323
+ <TooltipContent className="z-[10000]">
324
+ {!activeLayer.editable
325
+ ? t("panel.layers.cannotDeleteDefault")
326
+ : activeLayer.locked
327
+ ? t("panel.layers.cannotDeleteLocked")
328
+ : t("panel.layers.delete")}
329
+ </TooltipContent>
330
+ </Tooltip>
331
+ </div>
332
+ ) : null}
333
+
334
+ {isLayerPanelExpanded ? (
335
+ <div className="rounded border border-border bg-card px-2 py-1.5 text-xs">
336
+ <div className="mb-1 text-muted-foreground">{t("panel.layers.selectMergeLayers")}</div>
337
+ <div className="flex flex-wrap gap-2">
338
+ {layers.map((layer) => (
339
+ <label key={layer.id} className="flex items-center gap-1.5">
340
+ <Checkbox
341
+ checked={Boolean(layer.mergeSelected)}
342
+ disabled={layer.isMapping}
343
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
344
+ onCheckedChange={() => toggleLayerMergeSelected(layer.id)}
345
+ />
346
+ <span
347
+ className="max-w-[140px] truncate"
348
+ title={layer.isMapping ? t("panel.layers.mappingCannotMerge") : layer.name}
349
+ >
350
+ {layer.name}
351
+ {layer.isMapping ? t("panel.layers.cannotMergeSuffix") : ""}
352
+ </span>
353
+ </label>
354
+ ))}
355
+ </div>
356
+ </div>
357
+ ) : null}
358
+ </TabsContent>
359
+ </Tabs>
360
+
361
+ {isLayerPanelExpanded && isMergingLayers ? (
362
+ <div className="mt-2 flex items-center gap-2 rounded border border-border bg-card px-2 py-1.5 text-xs">
363
+ <span className="text-muted-foreground">{t("panel.layers.mergedName")}</span>
364
+ <Input
365
+ value={mergeLayerName}
366
+ onChange={(e) => setMergeLayerName(e.target.value)}
367
+ className="h-7 min-w-0 flex-1"
368
+ placeholder={t("panel.layers.mergedNamePlaceholder")}
369
+ autoFocus
370
+ />
371
+ <Tooltip>
372
+ <TooltipTrigger asChild>
373
+ <button
374
+ type="button"
375
+ onClick={() => {
376
+ if (!canMergeLayers) return;
377
+ mergeSelectedLayers(mergeLayerName || undefined);
378
+ setIsMergingLayers(false);
379
+ setMergeLayerName("");
380
+ }}
381
+ aria-label={t("panel.layers.confirmMergeAria")}
382
+ disabled={!canMergeLayers}
383
+ className="rounded border border-border p-1 hover:bg-accent"
384
+ >
385
+ <IconCheck />
386
+ </button>
387
+ </TooltipTrigger>
388
+ <TooltipContent className="z-[10000]">{t("panel.layers.confirmMerge")}</TooltipContent>
389
+ </Tooltip>
390
+ <Tooltip>
391
+ <TooltipTrigger asChild>
392
+ <button
393
+ type="button"
394
+ onClick={() => {
395
+ setIsMergingLayers(false);
396
+ setMergeLayerName("");
397
+ }}
398
+ aria-label={t("panel.layers.cancelMergeAria")}
399
+ className="rounded border border-border p-1 hover:bg-accent"
400
+ >
401
+ <IconClose />
402
+ </button>
403
+ </TooltipTrigger>
404
+ <TooltipContent className="z-[10000]">{t("panel.layers.cancelMerge")}</TooltipContent>
405
+ </Tooltip>
406
+ </div>
407
+ ) : null}
408
+ </TooltipProvider>
409
+ </div>
410
+
411
+ );
412
+ }