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