@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.
@@ -84,55 +84,35 @@ import { getPanelMessages } from "./constants/messages";
84
84
  import { PANEL_Z_INDEX } from "./constants/zIndex";
85
85
  import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
86
86
  import {
87
- AlertDialog,
88
- AlertDialogAction,
89
- AlertDialogCancel,
90
- AlertDialogContent,
91
- AlertDialogDescription,
92
- AlertDialogFooter,
93
- AlertDialogHeader,
94
- AlertDialogTitle,
87
+ IconBackward,
88
+ IconBringFront,
89
+ IconClose,
90
+ IconForward,
91
+ IconImage,
92
+ IconLayers,
93
+ IconRedo,
94
+ IconSendBack,
95
+ IconUndo,
96
+ } from "./icons";
97
+ import {
98
+ getSelectedTargetsFromIds,
99
+ shouldClearSelectionOnBlank,
100
+ } from "./utils/panelSelection";
101
+ import { PanelMenubar } from "./components/PanelMenubar";
102
+ import { PanelHistoryPopover } from "./components/PanelHistoryPopover";
103
+ import { PanelLayerDock } from "./components/PanelLayerDock";
104
+ import { PanelWorkspaceDialogs } from "./components/PanelWorkspaceDialogs";
105
+ import {
95
106
  Button,
96
- Checkbox,
97
- Dialog,
98
- DialogContent,
99
- DialogDescription,
100
- DialogFooter,
101
- DialogHeader,
102
- DialogTitle,
103
107
  DropdownMenu,
104
108
  DropdownMenuContent,
105
109
  DropdownMenuItem,
106
110
  DropdownMenuTrigger,
107
- Empty,
108
- EmptyDescription,
109
- EmptyIcon,
110
- EmptyTitle,
111
111
  Input,
112
- Menubar,
113
- MenubarCheckboxItem,
114
- MenubarContent,
115
- MenubarItem,
116
- MenubarMenu,
117
- MenubarRadioGroup,
118
- MenubarRadioItem,
119
- MenubarSeparator,
120
- MenubarTrigger,
121
- RadioGroup,
122
- RadioGroupItem,
123
112
  ResizableHandle,
124
113
  ResizablePanel,
125
114
  ResizablePanelGroup,
126
- Select,
127
- SelectContent,
128
- SelectItem,
129
- SelectTrigger,
130
- SelectValue,
131
115
  Switch,
132
- Tabs,
133
- TabsContent,
134
- TabsList,
135
- TabsTrigger,
136
116
  Toaster,
137
117
  Tooltip,
138
118
  TooltipContent,
@@ -142,223 +122,6 @@ import {
142
122
  useTheme,
143
123
  } from "@arronqzy/ui";
144
124
 
145
- function IconPlus() {
146
- return (
147
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
148
- <path d="M12 5v14M5 12h14" />
149
- </svg>
150
- );
151
- }
152
-
153
- function IconMerge() {
154
- return (
155
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
156
- <path d="M7 7h4a4 4 0 0 1 4 4v6" />
157
- <path d="m12 14 3 3 3-3" />
158
- <path d="M7 17h2" />
159
- </svg>
160
- );
161
- }
162
-
163
- function IconLock({ locked }: { locked: boolean }) {
164
- return locked ? (
165
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
166
- <rect x="4" y="11" width="16" height="9" rx="2" />
167
- <path d="M8 11V8a4 4 0 0 1 8 0v3" />
168
- </svg>
169
- ) : (
170
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
171
- <rect x="4" y="11" width="16" height="9" rx="2" />
172
- <path d="M16 11V8a4 4 0 0 0-7-2.5" />
173
- </svg>
174
- );
175
- }
176
-
177
- function IconEdit() {
178
- return (
179
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
180
- <path d="M12 20h9" />
181
- <path d="m16.5 3.5 4 4L8 20l-5 1 1-5Z" />
182
- </svg>
183
- );
184
- }
185
-
186
- function IconTrash() {
187
- return (
188
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
189
- <path d="M3 6h18" />
190
- <path d="M8 6V4h8v2" />
191
- <path d="M19 6l-1 14H6L5 6" />
192
- </svg>
193
- );
194
- }
195
-
196
- function IconCheck() {
197
- return (
198
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
199
- <path d="m5 12 5 5L20 7" />
200
- </svg>
201
- );
202
- }
203
-
204
- function IconClose() {
205
- return (
206
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
207
- <path d="M6 6l12 12M18 6 6 18" />
208
- </svg>
209
- );
210
- }
211
-
212
- function IconChevron({ expanded }: { expanded: boolean }) {
213
- return (
214
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
215
- {expanded ? <path d="m6 14 6-6 6 6" /> : <path d="m6 10 6 6 6-6" />}
216
- </svg>
217
- );
218
- }
219
-
220
- function IconUndo() {
221
- return (
222
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
223
- <path d="M9 14 4 9l5-5" />
224
- <path d="M20 20a9 9 0 0 0-9-9H4" />
225
- </svg>
226
- );
227
- }
228
-
229
- function IconRedo() {
230
- return (
231
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
232
- <path d="m15 14 5-5-5-5" />
233
- <path d="M4 20a9 9 0 0 1 9-9h7" />
234
- </svg>
235
- );
236
- }
237
-
238
- function IconHistory() {
239
- return (
240
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
241
- <path d="M3 12a9 9 0 1 0 3-6.7" />
242
- <path d="M3 4v4h4" />
243
- <path d="M12 7v5l3 2" />
244
- </svg>
245
- );
246
- }
247
-
248
- function IconBringFront() {
249
- return (
250
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
251
- <path d="M7 17h10M12 17V7" />
252
- <path d="m8.5 9 3.5-3.5L15.5 9" />
253
- </svg>
254
- );
255
- }
256
-
257
- function IconSendBack() {
258
- return (
259
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
260
- <path d="M7 7h10M12 17V7" />
261
- <path d="m15.5 15-3.5 3.5L8.5 15" />
262
- </svg>
263
- );
264
- }
265
-
266
- function IconForward() {
267
- return (
268
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
269
- <path d="M12 18V6" />
270
- <path d="m8.5 9 3.5-3.5L15.5 9" />
271
- </svg>
272
- );
273
- }
274
-
275
- function IconBackward() {
276
- return (
277
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
278
- <path d="M12 6v12" />
279
- <path d="m15.5 15-3.5 3.5L8.5 15" />
280
- </svg>
281
- );
282
- }
283
-
284
- function IconLayers() {
285
- return (
286
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
287
- <path d="m12 4 8 4-8 4-8-4 8-4Z" />
288
- <path d="m4 12 8 4 8-4" />
289
- <path d="m4 16 8 4 8-4" />
290
- </svg>
291
- );
292
- }
293
-
294
- function IconImage() {
295
- return (
296
- <svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
297
- <rect x="3" y="5" width="18" height="14" rx="2" />
298
- <circle cx="9" cy="10" r="1.5" />
299
- <path d="m21 16-5.5-5.5L7 19" />
300
- </svg>
301
- );
302
- }
303
-
304
- function formatRelativeTime(
305
- timestamp: number,
306
- now: number,
307
- t: (key: string, params?: Record<string, string | number>) => string
308
- ): string {
309
- const diff = Math.max(0, now - timestamp);
310
- const sec = Math.floor(diff / 1000);
311
- if (sec < 60) return t("common.secondsAgo", { sec });
312
- const min = Math.floor(sec / 60);
313
- if (min < 60) return t("common.minutesAgo", { min });
314
- const hour = Math.floor(min / 60);
315
- if (hour < 24) return t("common.hoursAgo", { hour });
316
- const day = Math.floor(hour / 24);
317
- return t("common.daysAgo", { day });
318
- }
319
-
320
- function getSelectedTargetsFromIds(
321
- container: HTMLElement | null,
322
- ids: string[],
323
- ) {
324
- if (!container) return [];
325
- const targets: HTMLElement[] = [];
326
- for (const id of ids) {
327
- const el = container.querySelector<HTMLElement>(
328
- `[data-element-id="${id}"]`,
329
- );
330
- if (el) targets.push(el);
331
- }
332
- return targets;
333
- }
334
-
335
- function shouldClearSelectionOnBlank(target: HTMLElement | null) {
336
- if (!target) return false;
337
- if (
338
- target.closest(".rv-selectable") ||
339
- target.closest(".moveable-control-box") ||
340
- target.closest(".moveable-group") ||
341
- target.closest(".moveable-line") ||
342
- target.closest(".moveable-control") ||
343
- target.closest(".moveable-direction")
344
- ) {
345
- return false;
346
- }
347
- if (
348
- target.closest("button") ||
349
- target.closest("input") ||
350
- target.closest("select") ||
351
- target.closest("textarea") ||
352
- target.closest("label") ||
353
- target.closest("a") ||
354
- target.closest("[role='menuitem']") ||
355
- target.closest("[data-radix-popper-content-wrapper]")
356
- ) {
357
- return false;
358
- }
359
- return true;
360
- }
361
-
362
125
  export type ReactViewPanelProps = {
363
126
  initialZoom?: number;
364
127
  className?: string;
@@ -2072,84 +1835,32 @@ function ReactViewPanelInner({
2072
1835
  }
2073
1836
  `}</style>
2074
1837
  <nav className="panel-font-root relative z-30 flex items-center gap-2 border-b border-border bg-background/95 px-2 text-foreground">
2075
- <Menubar className="h-8 border-0 bg-transparent p-0 shadow-none">
2076
- <MenubarMenu>
2077
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.file")}</MenubarTrigger>
2078
- <MenubarContent className="z-[10100]">
2079
- {/* <MenubarItem onClick={handlePreviewLayer}>{t("panel.workspace.previewDocTitle")}</MenubarItem> */}
2080
- <MenubarItem onClick={handleExport}>{t("panel.menubar.export")}</MenubarItem>
2081
- <MenubarItem onClick={() => importInputRef.current?.click()}>{t("panel.menubar.import")}</MenubarItem>
2082
- </MenubarContent>
2083
- </MenubarMenu>
2084
- <MenubarMenu>
2085
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.edit")}</MenubarTrigger>
2086
- <MenubarContent className="z-[10100]">
2087
- <MenubarItem disabled={!canUndo} onClick={undo}>{t("panel.menubar.undo")}</MenubarItem>
2088
- <MenubarItem disabled={!canRedo} onClick={redo}>{t("panel.menubar.redo")}</MenubarItem>
2089
- <MenubarSeparator />
2090
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsForward(selectedIds)}>{t("panel.menubar.bringForward")}</MenubarItem>
2091
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsBackward(selectedIds)}>{t("panel.menubar.sendBackward")}</MenubarItem>
2092
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsToFront(selectedIds)}>{t("panel.menubar.bringToFront")}</MenubarItem>
2093
- <MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsToBack(selectedIds)}>{t("panel.menubar.sendToBack")}</MenubarItem>
2094
- </MenubarContent>
2095
- </MenubarMenu>
2096
- <MenubarMenu>
2097
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.view")}</MenubarTrigger>
2098
- <MenubarContent className="z-[10100]">
2099
- <MenubarItem onClick={() => adjustUniformZoom((z) => z - 0.1)}>{t("panel.menubar.zoomOut")}</MenubarItem>
2100
- <MenubarItem onClick={() => adjustUniformZoom((z) => z + 0.1)}>{t("panel.menubar.zoomIn")}</MenubarItem>
2101
- <MenubarSeparator />
2102
- <MenubarItem
2103
- onClick={() => {
2104
- applyTheme(!isDark);
2105
- }}
2106
- >
2107
- {isDark ? t("panel.menubar.switchToLight") : t("panel.menubar.switchToDark")}
2108
- </MenubarItem>
2109
- </MenubarContent>
2110
- </MenubarMenu>
2111
- <MenubarMenu>
2112
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.blueprint")}</MenubarTrigger>
2113
- <MenubarContent className="z-[10100]">
2114
- <MenubarItem onClick={openBlueprintExportDialog}>{t("panel.menubar.export")}</MenubarItem>
2115
- <MenubarItem onClick={() => blueprintImportInputRef.current?.click()}>
2116
- {t("panel.menubar.import")}
2117
- </MenubarItem>
2118
- </MenubarContent>
2119
- </MenubarMenu>
2120
- <MenubarMenu>
2121
- <MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.settings")}</MenubarTrigger>
2122
- <MenubarContent className="z-[10100]">
2123
- <MenubarRadioGroup
2124
- value={panelFontSize}
2125
- onValueChange={(value) => setPanelFontSize(value as "sm" | "md" | "lg")}
2126
- >
2127
- <MenubarRadioItem value="sm">{t("panel.menubar.fontSmall")}</MenubarRadioItem>
2128
- <MenubarRadioItem value="md">{t("panel.menubar.fontMedium")}</MenubarRadioItem>
2129
- <MenubarRadioItem value="lg">{t("panel.menubar.fontLarge")}</MenubarRadioItem>
2130
- </MenubarRadioGroup>
2131
- <MenubarSeparator />
2132
- <div className="px-2 py-1.5 text-[11px] text-muted-foreground">
2133
- {t("panel.theme.language")}
2134
- </div>
2135
- <MenubarRadioGroup
2136
- value={locale}
2137
- onValueChange={(value) => setLocale(value as "zh-CN" | "en-US")}
2138
- >
2139
- <MenubarRadioItem value="zh-CN">{t("panel.theme.zhCN")}</MenubarRadioItem>
2140
- <MenubarRadioItem value="en-US">{t("panel.theme.enUS")}</MenubarRadioItem>
2141
- </MenubarRadioGroup>
2142
- <MenubarSeparator />
2143
- <MenubarCheckboxItem
2144
- checked={outputScale}
2145
- onCheckedChange={(checked) => setOutputScale(checked === true)}
2146
- title={t("panel.menubar.outputScaleHint")}
2147
- >
2148
- {t("panel.menubar.outputScale")}
2149
- </MenubarCheckboxItem>
2150
- </MenubarContent>
2151
- </MenubarMenu>
2152
- </Menubar>
1838
+ <PanelMenubar
1839
+ t={t}
1840
+ handleExport={handleExport}
1841
+ importInputRef={importInputRef}
1842
+ blueprintImportInputRef={blueprintImportInputRef}
1843
+ canUndo={canUndo}
1844
+ canRedo={canRedo}
1845
+ undo={undo}
1846
+ redo={redo}
1847
+ hasUnlockedSelection={hasUnlockedSelection}
1848
+ selectedIds={selectedIds}
1849
+ bringElementsForward={bringElementsForward}
1850
+ sendElementsBackward={sendElementsBackward}
1851
+ bringElementsToFront={bringElementsToFront}
1852
+ sendElementsToBack={sendElementsToBack}
1853
+ adjustUniformZoom={adjustUniformZoom}
1854
+ applyTheme={applyTheme}
1855
+ isDark={isDark}
1856
+ openBlueprintExportDialog={openBlueprintExportDialog}
1857
+ panelFontSize={panelFontSize}
1858
+ setPanelFontSize={setPanelFontSize}
1859
+ locale={locale}
1860
+ setLocale={setLocale}
1861
+ outputScale={outputScale}
1862
+ setOutputScale={setOutputScale}
1863
+ />
2153
1864
  <WorkspaceProjectNav
2154
1865
  projects={workspaceProjects.projects}
2155
1866
  activeProjectId={workspaceProjects.activeProjectId}
@@ -2354,122 +2065,19 @@ function ReactViewPanelInner({
2354
2065
  </TooltipTrigger>
2355
2066
  <TooltipContent className="z-[10000]">{t("panel.menubar.redoShortcut")}</TooltipContent>
2356
2067
  </Tooltip>
2357
- <Tooltip>
2358
- <TooltipTrigger asChild>
2359
- <div className="relative">
2360
- <button
2361
- type="button"
2362
- onClick={() => setIsHistoryPanelExpanded((prev) => !prev)}
2363
- className={[
2364
- "rounded border border-border p-1 hover:bg-accent",
2365
- isHistoryPanelExpanded ? "bg-accent/60" : "",
2366
- ].join(" ")}
2367
- aria-label={isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
2368
- >
2369
- <IconHistory />
2370
- </button>
2371
- {isHistoryPanelExpanded ? (
2372
- <div
2373
- 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"
2374
- style={{ zIndex: PANEL_Z_INDEX.historyPopover }}
2375
- >
2376
- <div className="mb-1 flex items-center justify-between">
2377
- <span className="text-[11px] font-semibold text-muted-foreground">{t("panel.history.title")}</span>
2378
- <span className="text-[11px] text-muted-foreground">
2379
- {history.length > 0 ? `${historyCursor + 1}/${history.length}` : "0/0"}
2380
- </span>
2381
- </div>
2382
- <Input
2383
- value={historyKeyword}
2384
- onChange={(e) => setHistoryKeyword(e.target.value)}
2385
- placeholder={t("panel.history.searchPlaceholder")}
2386
- className="mb-2 h-7 text-xs"
2387
- />
2388
- <div
2389
- className={`max-h-52 space-y-1 overflow-auto pr-1 text-[11px] ${themedScrollbarClass}`}
2390
- >
2391
- {history.length === 0 ? (
2392
- <Empty className="py-4">
2393
- <EmptyIcon className="h-8 w-8">
2394
- <IconHistory />
2395
- </EmptyIcon>
2396
- <EmptyTitle className="text-xs">{t("panel.history.emptyTitle")}</EmptyTitle>
2397
- <EmptyDescription className="text-[11px]">
2398
- {t("panel.history.emptyDesc")}
2399
- </EmptyDescription>
2400
- </Empty>
2401
- ) : (
2402
- history
2403
- .slice(Math.max(0, history.length - 20))
2404
- .reverse()
2405
- .filter((item) =>
2406
- !normalizedHistoryKeyword
2407
- ? true
2408
- : item.label.toLowerCase().includes(normalizedHistoryKeyword)
2409
- )
2410
- .map((item) => (
2411
- <button
2412
- key={`${item.index}-${item.timestamp}`}
2413
- type="button"
2414
- className={[
2415
- "w-full rounded border px-2 py-1 text-left transition-colors",
2416
- item.active
2417
- ? "border-primary/50 bg-primary/10 text-foreground"
2418
- : "border-border/60 bg-background/70 text-muted-foreground hover:bg-accent/50",
2419
- ].join(" ")}
2420
- title={new Date(item.timestamp).toLocaleString()}
2421
- onClick={() => {
2422
- if (item.active) return;
2423
- goToHistory(item.index);
2424
- }}
2425
- >
2426
- <div className="flex items-center justify-between gap-2">
2427
- <span className="truncate">{item.label}</span>
2428
- <span className="shrink-0 text-[10px] text-muted-foreground">
2429
- {formatRelativeTime(item.timestamp, historyNow, t)}
2430
- </span>
2431
- </div>
2432
- </button>
2433
- ))
2434
- )}
2435
- {history.length > 0 &&
2436
- history
2437
- .slice(Math.max(0, history.length - 20))
2438
- .reverse()
2439
- .filter((item) =>
2440
- !normalizedHistoryKeyword
2441
- ? true
2442
- : item.label.toLowerCase().includes(normalizedHistoryKeyword)
2443
- ).length === 0 ? (
2444
- <Empty className="py-4">
2445
- <EmptyIcon className="h-8 w-8">
2446
- <svg
2447
- viewBox="0 0 24 24"
2448
- className="h-4 w-4"
2449
- fill="none"
2450
- stroke="currentColor"
2451
- strokeWidth="1.8"
2452
- aria-hidden="true"
2453
- >
2454
- <circle cx="11" cy="11" r="7" />
2455
- <path d="m20 20-3.5-3.5" />
2456
- </svg>
2457
- </EmptyIcon>
2458
- <EmptyTitle className="text-xs">{t("panel.history.noMatchTitle")}</EmptyTitle>
2459
- <EmptyDescription className="text-[11px]">
2460
- {t("panel.history.noMatchDesc")}
2461
- </EmptyDescription>
2462
- </Empty>
2463
- ) : null}
2464
- </div>
2465
- </div>
2466
- ) : null}
2467
- </div>
2468
- </TooltipTrigger>
2469
- <TooltipContent className="z-[10000]">
2470
- {isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
2471
- </TooltipContent>
2472
- </Tooltip>
2068
+ <PanelHistoryPopover
2069
+ t={t}
2070
+ isHistoryPanelExpanded={isHistoryPanelExpanded}
2071
+ setIsHistoryPanelExpanded={setIsHistoryPanelExpanded}
2072
+ history={history}
2073
+ historyCursor={historyCursor}
2074
+ historyKeyword={historyKeyword}
2075
+ setHistoryKeyword={setHistoryKeyword}
2076
+ normalizedHistoryKeyword={normalizedHistoryKeyword}
2077
+ goToHistory={goToHistory}
2078
+ historyNow={historyNow}
2079
+ themedScrollbarClass={themedScrollbarClass}
2080
+ />
2473
2081
  </TooltipProvider>
2474
2082
  <DropdownMenu>
2475
2083
  <DropdownMenuTrigger asChild>
@@ -2985,328 +2593,36 @@ function ReactViewPanelInner({
2985
2593
  }
2986
2594
  />
2987
2595
 
2988
- <div className="border-t border-border bg-background/95 px-2 py-1.5">
2989
- <TooltipProvider delayDuration={120}>
2990
- <Tabs value={activeLayerId} onValueChange={setActiveLayer}>
2991
- <div className="mb-1 flex items-center gap-2">
2992
- <div className="text-[11px] font-semibold text-muted-foreground">{t("panel.layers.title")}</div>
2993
- <Tooltip>
2994
- <TooltipTrigger asChild>
2995
- <button
2996
- type="button"
2997
- onClick={() => setIsLayerPanelExpanded((v) => !v)}
2998
- aria-label={isLayerPanelExpanded ? t("panel.layers.collapseInputAria") : t("panel.layers.expandInputAria")}
2999
- className="rounded border border-border p-1 hover:bg-accent"
3000
- >
3001
- <IconChevron expanded={isLayerPanelExpanded} />
3002
- </button>
3003
- </TooltipTrigger>
3004
- <TooltipContent className="z-[10000]">
3005
- {isLayerPanelExpanded ? t("panel.layers.collapseInput") : t("panel.layers.expandInput")}
3006
- </TooltipContent>
3007
- </Tooltip>
3008
- <Tooltip>
3009
- <TooltipTrigger asChild>
3010
- <button
3011
- type="button"
3012
- onClick={addLayer}
3013
- aria-label={t("panel.layers.add")}
3014
- className="rounded border border-border p-1 hover:bg-accent"
3015
- >
3016
- <IconPlus />
3017
- </button>
3018
- </TooltipTrigger>
3019
- <TooltipContent className="z-[10000]">{t("panel.layers.add")}</TooltipContent>
3020
- </Tooltip>
3021
- <Tooltip>
3022
- <TooltipTrigger asChild>
3023
- <button
3024
- type="button"
3025
- onClick={() => {
3026
- if (!canMergeLayers) return;
3027
- setIsMergingLayers(true);
3028
- }}
3029
- aria-label={t("panel.layers.merge")}
3030
- disabled={!canMergeLayers}
3031
- className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
3032
- >
3033
- <IconMerge />
3034
- </button>
3035
- </TooltipTrigger>
3036
- <TooltipContent className="z-[10000]">
3037
- {canMergeLayers ? t("panel.layers.merge") : t("panel.layers.mergeNeedTwo")}
3038
- </TooltipContent>
3039
- </Tooltip>
3040
- </div>
3041
-
3042
- <TabsList
3043
- className={`h-auto w-full justify-start gap-1 overflow-x-auto bg-muted/40 p-1 ${themedScrollbarClass}`}
3044
- >
3045
- {layers.map((layer) => (
3046
- <TabsTrigger
3047
- key={layer.id}
3048
- value={layer.id}
3049
- className="flex min-w-[120px] items-center gap-1 px-2 py-1 text-xs"
3050
- >
3051
- <span className="truncate">{layer.name}</span>
3052
- {layer.isMapping ? (
3053
- <span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
3054
- {t("panel.layers.mapping")}
3055
- </span>
3056
- ) : null}
3057
- {layer.isPrimary ? (
3058
- <span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
3059
- {t("panel.layers.primary")}
3060
- </span>
3061
- ) : null}
3062
- {layer.locked ? <IconLock locked /> : null}
3063
- </TabsTrigger>
3064
- ))}
3065
- </TabsList>
3066
-
3067
- <TabsContent value={activeLayerId} className="mt-2 space-y-2">
3068
- {activeLayer ? (
3069
- <div className="flex items-center gap-2 rounded border border-border bg-card px-2 py-1.5 text-xs">
3070
- {editingLayerId === activeLayer.id ? (
3071
- <Input
3072
- value={editingLayerName}
3073
- onChange={(e) => setEditingLayerName(e.target.value)}
3074
- onKeyDown={(e) => {
3075
- if (e.key === "Enter") {
3076
- e.preventDefault();
3077
- renameLayer(editingLayerId, editingLayerName);
3078
- setEditingLayerId(null);
3079
- setEditingLayerName("");
3080
- return;
3081
- }
3082
- if (e.key === "Escape") {
3083
- e.preventDefault();
3084
- setEditingLayerId(null);
3085
- setEditingLayerName("");
3086
- }
3087
- }}
3088
- className="h-7 min-w-0 flex-1"
3089
- placeholder={t("panel.layers.namePlaceholder")}
3090
- autoFocus
3091
- />
3092
- ) : (
3093
- <span className="truncate font-medium">{activeLayer.name}</span>
3094
- )}
3095
- {activeLayer.isMapping ? (
3096
- <span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
3097
- {t("panel.layers.mappingLayer")}
3098
- </span>
3099
- ) : null}
3100
- {activeLayer.isPrimary ? (
3101
- <span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
3102
- {t("panel.layers.primaryLayer")}
3103
- </span>
3104
- ) : null}
3105
- <div className="flex-1" />
3106
- <Tooltip>
3107
- <TooltipTrigger asChild>
3108
- <button
3109
- type="button"
3110
- onClick={() => setPrimaryLayer(activeLayer.id)}
3111
- disabled={activeLayer.isPrimary}
3112
- aria-label={activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
3113
- className={[
3114
- "rounded border px-2 py-1 text-[11px] disabled:cursor-not-allowed disabled:opacity-60",
3115
- activeLayer.isPrimary
3116
- ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-600"
3117
- : "border-border hover:bg-accent",
3118
- ].join(" ")}
3119
- >
3120
- {activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
3121
- </button>
3122
- </TooltipTrigger>
3123
- <TooltipContent className="z-[10000]">
3124
- {activeLayer.isPrimary ? t("panel.layers.isPrimaryHint") : t("panel.layers.setPrimaryHint")}
3125
- </TooltipContent>
3126
- </Tooltip>
3127
- <Tooltip>
3128
- <TooltipTrigger asChild>
3129
- <button
3130
- type="button"
3131
- onClick={() => toggleLayerLock(activeLayer.id)}
3132
- disabled={!activeLayer.editable}
3133
- aria-label={activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
3134
- className="rounded border border-border p-1 disabled:opacity-40"
3135
- >
3136
- <IconLock locked={activeLayer.locked} />
3137
- </button>
3138
- </TooltipTrigger>
3139
- <TooltipContent className="z-[10000]">
3140
- {activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
3141
- </TooltipContent>
3142
- </Tooltip>
3143
- <Tooltip>
3144
- <TooltipTrigger asChild>
3145
- {editingLayerId === activeLayer.id ? (
3146
- <button
3147
- type="button"
3148
- onClick={() => {
3149
- renameLayer(editingLayerId, editingLayerName);
3150
- setEditingLayerId(null);
3151
- setEditingLayerName("");
3152
- }}
3153
- aria-label={t("panel.layers.saveName")}
3154
- className="rounded border border-border p-1 hover:bg-accent"
3155
- >
3156
- <IconCheck />
3157
- </button>
3158
- ) : (
3159
- <button
3160
- type="button"
3161
- onClick={() => {
3162
- setEditingLayerId(activeLayer.id);
3163
- setEditingLayerName(activeLayer.name);
3164
- }}
3165
- disabled={!activeLayer.editable}
3166
- aria-label={t("panel.layers.rename")}
3167
- className="rounded border border-border p-1 disabled:opacity-40"
3168
- >
3169
- <IconEdit />
3170
- </button>
3171
- )}
3172
- </TooltipTrigger>
3173
- <TooltipContent className="z-[10000]">
3174
- {editingLayerId === activeLayer.id ? t("panel.layers.saveName") : t("panel.layers.rename")}
3175
- </TooltipContent>
3176
- </Tooltip>
3177
- {editingLayerId === activeLayer.id ? (
3178
- <Tooltip>
3179
- <TooltipTrigger asChild>
3180
- <button
3181
- type="button"
3182
- onClick={() => {
3183
- setEditingLayerId(null);
3184
- setEditingLayerName("");
3185
- }}
3186
- aria-label={t("panel.layers.cancelEditName")}
3187
- className="rounded border border-border p-1 hover:bg-accent"
3188
- >
3189
- <IconClose />
3190
- </button>
3191
- </TooltipTrigger>
3192
- <TooltipContent className="z-[10000]">{t("panel.layers.cancelEdit")}</TooltipContent>
3193
- </Tooltip>
3194
- ) : null}
3195
- <Tooltip>
3196
- <TooltipTrigger asChild>
3197
- <button
3198
- type="button"
3199
- onClick={() => {
3200
- const blockReason = getLayerDeleteBlockReason(activeLayer.id);
3201
- if (blockReason) {
3202
- showActionHint(blockReason);
3203
- return;
3204
- }
3205
- setConfirmDeleteLayerId(activeLayer.id);
3206
- const firstTarget = layers.find((l) => l.id !== activeLayer.id);
3207
- setDeleteTargetLayerId(firstTarget?.id ?? "");
3208
- setDeleteMode(activeLayer.isMapping ? "remove" : "move");
3209
- }}
3210
- disabled={!activeLayer.editable || activeLayer.locked}
3211
- aria-label={
3212
- !activeLayer.editable
3213
- ? t("panel.layers.cannotDeleteDefault")
3214
- : activeLayer.locked
3215
- ? t("panel.layers.cannotDeleteLocked")
3216
- : t("panel.layers.delete")
3217
- }
3218
- className="rounded border border-border p-1 disabled:opacity-40"
3219
- >
3220
- <IconTrash />
3221
- </button>
3222
- </TooltipTrigger>
3223
- <TooltipContent className="z-[10000]">
3224
- {!activeLayer.editable
3225
- ? t("panel.layers.cannotDeleteDefault")
3226
- : activeLayer.locked
3227
- ? t("panel.layers.cannotDeleteLocked")
3228
- : t("panel.layers.delete")}
3229
- </TooltipContent>
3230
- </Tooltip>
3231
- </div>
3232
- ) : null}
3233
-
3234
- {isLayerPanelExpanded ? (
3235
- <div className="rounded border border-border bg-card px-2 py-1.5 text-xs">
3236
- <div className="mb-1 text-muted-foreground">{t("panel.layers.selectMergeLayers")}</div>
3237
- <div className="flex flex-wrap gap-2">
3238
- {layers.map((layer) => (
3239
- <label key={layer.id} className="flex items-center gap-1.5">
3240
- <Checkbox
3241
- checked={Boolean(layer.mergeSelected)}
3242
- disabled={layer.isMapping}
3243
- 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"
3244
- onCheckedChange={() => toggleLayerMergeSelected(layer.id)}
3245
- />
3246
- <span
3247
- className="max-w-[140px] truncate"
3248
- title={layer.isMapping ? t("panel.layers.mappingCannotMerge") : layer.name}
3249
- >
3250
- {layer.name}
3251
- {layer.isMapping ? t("panel.layers.cannotMergeSuffix") : ""}
3252
- </span>
3253
- </label>
3254
- ))}
3255
- </div>
3256
- </div>
3257
- ) : null}
3258
- </TabsContent>
3259
- </Tabs>
3260
-
3261
- {isLayerPanelExpanded && isMergingLayers ? (
3262
- <div className="mt-2 flex items-center gap-2 rounded border border-border bg-card px-2 py-1.5 text-xs">
3263
- <span className="text-muted-foreground">{t("panel.layers.mergedName")}</span>
3264
- <Input
3265
- value={mergeLayerName}
3266
- onChange={(e) => setMergeLayerName(e.target.value)}
3267
- className="h-7 min-w-0 flex-1"
3268
- placeholder={t("panel.layers.mergedNamePlaceholder")}
3269
- autoFocus
3270
- />
3271
- <Tooltip>
3272
- <TooltipTrigger asChild>
3273
- <button
3274
- type="button"
3275
- onClick={() => {
3276
- if (!canMergeLayers) return;
3277
- mergeSelectedLayers(mergeLayerName || undefined);
3278
- setIsMergingLayers(false);
3279
- setMergeLayerName("");
3280
- }}
3281
- aria-label={t("panel.layers.confirmMergeAria")}
3282
- disabled={!canMergeLayers}
3283
- className="rounded border border-border p-1 hover:bg-accent"
3284
- >
3285
- <IconCheck />
3286
- </button>
3287
- </TooltipTrigger>
3288
- <TooltipContent className="z-[10000]">{t("panel.layers.confirmMerge")}</TooltipContent>
3289
- </Tooltip>
3290
- <Tooltip>
3291
- <TooltipTrigger asChild>
3292
- <button
3293
- type="button"
3294
- onClick={() => {
3295
- setIsMergingLayers(false);
3296
- setMergeLayerName("");
3297
- }}
3298
- aria-label={t("panel.layers.cancelMergeAria")}
3299
- className="rounded border border-border p-1 hover:bg-accent"
3300
- >
3301
- <IconClose />
3302
- </button>
3303
- </TooltipTrigger>
3304
- <TooltipContent className="z-[10000]">{t("panel.layers.cancelMerge")}</TooltipContent>
3305
- </Tooltip>
3306
- </div>
3307
- ) : null}
3308
- </TooltipProvider>
3309
- </div>
2596
+ <PanelLayerDock
2597
+ t={t}
2598
+ isLayerPanelExpanded={isLayerPanelExpanded}
2599
+ setIsLayerPanelExpanded={setIsLayerPanelExpanded}
2600
+ addLayer={addLayer}
2601
+ canMergeLayers={canMergeLayers}
2602
+ setIsMergingLayers={setIsMergingLayers}
2603
+ isMergingLayers={isMergingLayers}
2604
+ layers={layers}
2605
+ activeLayerId={activeLayerId}
2606
+ setActiveLayer={setActiveLayer}
2607
+ themedScrollbarClass={themedScrollbarClass}
2608
+ activeLayer={activeLayer}
2609
+ editingLayerId={editingLayerId}
2610
+ setEditingLayerId={setEditingLayerId}
2611
+ editingLayerName={editingLayerName}
2612
+ setEditingLayerName={setEditingLayerName}
2613
+ renameLayer={renameLayer}
2614
+ setPrimaryLayer={setPrimaryLayer}
2615
+ toggleLayerLock={toggleLayerLock}
2616
+ getLayerDeleteBlockReason={getLayerDeleteBlockReason}
2617
+ showActionHint={showActionHint}
2618
+ setConfirmDeleteLayerId={setConfirmDeleteLayerId}
2619
+ setDeleteTargetLayerId={setDeleteTargetLayerId}
2620
+ setDeleteMode={setDeleteMode}
2621
+ toggleLayerMergeSelected={toggleLayerMergeSelected}
2622
+ mergeLayerName={mergeLayerName}
2623
+ setMergeLayerName={setMergeLayerName}
2624
+ mergeSelectedLayers={mergeSelectedLayers}
2625
+ />
3310
2626
  </div>
3311
2627
  </div>
3312
2628
  </ResizablePanel>
@@ -3370,199 +2686,27 @@ function ReactViewPanelInner({
3370
2686
  open={assistantOpen}
3371
2687
  onOpenChange={setAssistantOpen}
3372
2688
  />
3373
- <AlertDialog
3374
- open={mappingDeleteConfirmOpen}
3375
- onOpenChange={(open) => {
3376
- setMappingDeleteConfirmOpen(open);
3377
- if (!open) mappingDeleteProceedRef.current = null;
3378
- }}
3379
- >
3380
- <AlertDialogContent overlayClassName="bg-transparent pointer-events-none">
3381
- <AlertDialogHeader>
3382
- <AlertDialogTitle>{t("panel.layers.confirmDeleteTitle")}</AlertDialogTitle>
3383
- <AlertDialogDescription>
3384
- {t("panel.layers.confirmDeleteMappingBody", {
3385
- count: mappingDeleteImpactCount,
3386
- })}
3387
- </AlertDialogDescription>
3388
- </AlertDialogHeader>
3389
- <AlertDialogFooter>
3390
- <AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
3391
- <AlertDialogAction
3392
- onClick={() => {
3393
- const fn = mappingDeleteProceedRef.current;
3394
- mappingDeleteProceedRef.current = null;
3395
- setMappingDeleteConfirmOpen(false);
3396
- fn?.();
3397
- }}
3398
- >
3399
- {t("panel.layers.continueDelete")}
3400
- </AlertDialogAction>
3401
- </AlertDialogFooter>
3402
- </AlertDialogContent>
3403
- </AlertDialog>
3404
- <Dialog
3405
- open={Boolean(deletingLayer)}
3406
- onOpenChange={(open) => {
3407
- if (open) return;
3408
- setConfirmDeleteLayerId(null);
3409
- setDeleteMode("move");
3410
- setDeleteTargetLayerId("");
3411
- }}
3412
- >
3413
- <DialogContent
3414
- className="sm:max-w-[520px]"
3415
- overlayClassName="bg-transparent pointer-events-none"
3416
- >
3417
- <DialogHeader>
3418
- <DialogTitle>{t("panel.layers.confirmDeleteLayerTitle")}</DialogTitle>
3419
- <DialogDescription>
3420
- {t("panel.layers.aboutToDeleteLayer", {
3421
- name: deletingLayer?.name ?? "-",
3422
- })}
3423
- </DialogDescription>
3424
- </DialogHeader>
3425
- {deletingLayer ? (
3426
- <RadioGroup
3427
- value={deletingLayerMode}
3428
- onValueChange={(value) => setDeleteMode(value as "move" | "remove")}
3429
- className="space-y-2 text-sm"
3430
- >
3431
- {!deletingLayer.isMapping ? (
3432
- <div className="flex items-center gap-3">
3433
- <label className="flex items-center gap-1.5">
3434
- <RadioGroupItem value="move" />
3435
- {t("panel.layers.migrateNodesTo")}
3436
- </label>
3437
- <Select
3438
- value={deleteTargetLayerId || "__none__"}
3439
- onValueChange={(value) =>
3440
- setDeleteTargetLayerId(value === "__none__" ? "" : value)
3441
- }
3442
- disabled={deletingLayerMode !== "move" || deleteTargetCandidates.length === 0}
3443
- >
3444
- <SelectTrigger className="h-8 w-[220px]">
3445
- <SelectValue placeholder={t("panel.layers.selectTargetLayer")} />
3446
- </SelectTrigger>
3447
- <SelectContent>
3448
- <SelectItem value="__none__">{t("panel.layers.pleaseSelectLayer")}</SelectItem>
3449
- {deleteTargetCandidates.map((l) => (
3450
- <SelectItem key={l.id} value={l.id}>
3451
- {l.name}
3452
- </SelectItem>
3453
- ))}
3454
- </SelectContent>
3455
- </Select>
3456
- </div>
3457
- ) : null}
3458
- <label className="flex items-center gap-1.5">
3459
- <RadioGroupItem value="remove" />
3460
- {deletingLayer.isMapping ? t("panel.layers.deleteMappingWithNodes") : t("panel.layers.deleteAllNodesToo")}
3461
- </label>
3462
- </RadioGroup>
3463
- ) : null}
3464
- <DialogFooter>
3465
- <button
3466
- type="button"
3467
- onClick={() => {
3468
- setConfirmDeleteLayerId(null);
3469
- setDeleteMode("move");
3470
- setDeleteTargetLayerId("");
3471
- }}
3472
- className="rounded border border-border px-3 py-1.5 text-sm hover:bg-accent"
3473
- >
3474
- {t("common.cancel")}
3475
- </button>
3476
- <button
3477
- type="button"
3478
- onClick={() => {
3479
- if (!deletingLayer) return;
3480
- const result = deleteLayer(deletingLayer.id, {
3481
- mode: deletingLayerMode,
3482
- targetLayerId: deleteTargetLayerId || undefined,
3483
- });
3484
- if (!result.ok) {
3485
- showActionHint(result.reason);
3486
- return;
3487
- }
3488
- setConfirmDeleteLayerId(null);
3489
- setDeleteMode("move");
3490
- setDeleteTargetLayerId("");
3491
- }}
3492
- className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
3493
- >
3494
- {t("panel.layers.confirmDeleteTitle")}
3495
- </button>
3496
- </DialogFooter>
3497
- </DialogContent>
3498
- </Dialog>
3499
- <Dialog
3500
- open={titleIconPreviewOpen}
3501
- onOpenChange={setTitleIconPreviewOpen}
3502
- >
3503
- <DialogContent
3504
- className="sm:max-w-[860px]"
3505
- overlayClassName="bg-transparent pointer-events-none"
3506
- >
3507
- <DialogHeader>
3508
- <DialogTitle>{t("panel.menubar.titleIconPreviewTitle")}</DialogTitle>
3509
- <DialogDescription>{t("panel.menubar.titleIconPreviewDesc")}</DialogDescription>
3510
- </DialogHeader>
3511
- <div className="flex items-center justify-end gap-2">
3512
- <button
3513
- type="button"
3514
- onClick={() => setTitleIconZoom((z) => Math.max(0.5, Number((z - 0.1).toFixed(2))))}
3515
- className="rounded border border-border px-2 py-1 text-xs hover:bg-accent"
3516
- >
3517
- -
3518
- </button>
3519
- <span className="w-14 text-center text-xs text-muted-foreground">
3520
- {(titleIconZoom * 100).toFixed(0)}%
3521
- </span>
3522
- <button
3523
- type="button"
3524
- onClick={() => setTitleIconZoom((z) => Math.min(8, Number((z + 0.1).toFixed(2))))}
3525
- className="rounded border border-border px-2 py-1 text-xs hover:bg-accent"
3526
- >
3527
- +
3528
- </button>
3529
- <button
3530
- type="button"
3531
- onClick={() => setTitleIconZoom(1.6)}
3532
- className="rounded border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
3533
- >
3534
- {t("common.reset")}
3535
- </button>
3536
- </div>
3537
- {titleIconDataUrl ? (
3538
- <div
3539
- className="max-h-[78vh] overflow-auto rounded border border-border bg-muted/20 p-4"
3540
- onWheel={(e) => {
3541
- e.preventDefault();
3542
- const delta = e.deltaY > 0 ? -0.1 : 0.1;
3543
- setTitleIconZoom((z) => {
3544
- const next = Number((z + delta).toFixed(2));
3545
- return Math.min(8, Math.max(0.5, next));
3546
- });
3547
- }}
3548
- >
3549
- <img
3550
- src={titleIconDataUrl}
3551
- alt={t("panel.menubar.titleIconPreviewAlt")}
3552
- className="mx-auto rounded border border-border/70 object-contain"
3553
- style={{
3554
- width: 200,
3555
- height: 200,
3556
- maxWidth: "none",
3557
- maxHeight: "none",
3558
- transform: `scale(${titleIconZoom})`,
3559
- transformOrigin: "center center",
3560
- }}
3561
- />
3562
- </div>
3563
- ) : null}
3564
- </DialogContent>
3565
- </Dialog>
2689
+ <PanelWorkspaceDialogs
2690
+ t={t}
2691
+ mappingDeleteConfirmOpen={mappingDeleteConfirmOpen}
2692
+ setMappingDeleteConfirmOpen={setMappingDeleteConfirmOpen}
2693
+ mappingDeleteProceedRef={mappingDeleteProceedRef}
2694
+ mappingDeleteImpactCount={mappingDeleteImpactCount}
2695
+ deletingLayer={deletingLayer}
2696
+ deletingLayerMode={deletingLayerMode}
2697
+ setConfirmDeleteLayerId={setConfirmDeleteLayerId}
2698
+ setDeleteMode={setDeleteMode}
2699
+ setDeleteTargetLayerId={setDeleteTargetLayerId}
2700
+ deleteTargetLayerId={deleteTargetLayerId}
2701
+ deleteTargetCandidates={deleteTargetCandidates}
2702
+ deleteLayer={deleteLayer}
2703
+ showActionHint={showActionHint}
2704
+ titleIconPreviewOpen={titleIconPreviewOpen}
2705
+ setTitleIconPreviewOpen={setTitleIconPreviewOpen}
2706
+ titleIconZoom={titleIconZoom}
2707
+ setTitleIconZoom={setTitleIconZoom}
2708
+ titleIconDataUrl={titleIconDataUrl}
2709
+ />
3566
2710
  <BlueprintMetaDialog
3567
2711
  open={blueprintMetaDialogOpen}
3568
2712
  mode={blueprintMetaDialogMode}