@arronqzy/react-view 1.0.3 → 1.0.7
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/dist/styles.css +555 -15
- package/package.json +14 -7
- package/readme.md +74 -5
- package/src/core/template.tsx +12 -2
- package/src/panel/ReactViewOnlinePreview.tsx +18 -8
- package/src/panel/ReactViewPanel.tsx +403 -177
- package/src/panel/ai/AssistantChatPanel.tsx +895 -0
- package/src/panel/ai/AssistantMarkdown.tsx +256 -0
- package/src/panel/ai/applyAssistantAction.ts +652 -0
- package/src/panel/ai/revealConfigField.ts +137 -0
- package/src/panel/components/ConfigHintIcon.tsx +50 -0
- package/src/panel/components/ElementsLayer.tsx +39 -22
- package/src/panel/components/MaterialSidebar.tsx +157 -109
- package/src/panel/components/MoveableLayer.tsx +75 -61
- package/src/panel/components/PanelCanvas.tsx +81 -64
- package/src/panel/components/PanelConfigSidebar.tsx +471 -448
- package/src/panel/components/SelectLayer.tsx +1 -0
- package/src/panel/components/ViewElementScopePanel.tsx +13 -12
- package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
- package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
- package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
- package/src/panel/components/table/ConditionEditor.tsx +205 -0
- package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
- package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
- package/src/panel/components/table/TableNodeContent.tsx +544 -0
- package/src/panel/constants/messages.ts +76 -34
- package/src/panel/hooks/usePanelElements.ts +64 -52
- package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
- package/src/panel/types.ts +6 -0
- package/src/panel/utils/chartOptionBuilder.ts +12 -1
- package/src/panel/utils/panelElementDefaults.ts +69 -39
- package/src/panel/utils/scope-field-labels.ts +51 -37
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +128 -0
- package/src/panel/utils/scope-template-spread.ts +252 -0
- package/src/panel/utils/scope-template-warnings.ts +131 -11
- package/src/panel/utils/scope-template.ts +105 -3
- package/src/tailwind.css +36 -15
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
import type { State } from "@arronqzy/rx-store";
|
|
3
4
|
import type { PanelElement } from "./types";
|
|
4
5
|
|
|
@@ -14,6 +15,14 @@ import { SelectLayer } from "./components/SelectLayer";
|
|
|
14
15
|
import { MoveableLayer } from "./components/MoveableLayer";
|
|
15
16
|
import { buildChartOption, CHART_TYPES } from "./utils/chartOptionBuilder";
|
|
16
17
|
import { MaterialSidebar } from "./components/MaterialSidebar";
|
|
18
|
+
import { AssistantChatPanel } from "./ai/AssistantChatPanel";
|
|
19
|
+
import {
|
|
20
|
+
listLayersFromStore,
|
|
21
|
+
listPanelElementsFromStore,
|
|
22
|
+
getActiveLayerIdFromStore,
|
|
23
|
+
type ApplyAssistantDeps,
|
|
24
|
+
} from "./ai/applyAssistantAction";
|
|
25
|
+
import { revealPanelConfigFromPatch } from "./ai/revealConfigField";
|
|
17
26
|
import {
|
|
18
27
|
BlueprintGraph,
|
|
19
28
|
BlueprintMetaDialog,
|
|
@@ -55,8 +64,9 @@ import {
|
|
|
55
64
|
useViewScopeStoreVersion,
|
|
56
65
|
} from "./scope/view-scope-store";
|
|
57
66
|
import { resolvePanelElementScope } from "./utils/scope-template";
|
|
58
|
-
import {
|
|
67
|
+
import { getPanelMessages } from "./constants/messages";
|
|
59
68
|
import { PANEL_Z_INDEX } from "./constants/zIndex";
|
|
69
|
+
import { useRafThrottledScroll } from "./hooks/useRafThrottledScroll";
|
|
60
70
|
import {
|
|
61
71
|
AlertDialog,
|
|
62
72
|
AlertDialogAction,
|
|
@@ -66,6 +76,7 @@ import {
|
|
|
66
76
|
AlertDialogFooter,
|
|
67
77
|
AlertDialogHeader,
|
|
68
78
|
AlertDialogTitle,
|
|
79
|
+
Button,
|
|
69
80
|
Checkbox,
|
|
70
81
|
Dialog,
|
|
71
82
|
DialogContent,
|
|
@@ -111,6 +122,7 @@ import {
|
|
|
111
122
|
TooltipProvider,
|
|
112
123
|
TooltipTrigger,
|
|
113
124
|
toast,
|
|
125
|
+
useTheme,
|
|
114
126
|
} from "@arronqzy/ui";
|
|
115
127
|
|
|
116
128
|
function IconPlus() {
|
|
@@ -272,16 +284,20 @@ function IconImage() {
|
|
|
272
284
|
);
|
|
273
285
|
}
|
|
274
286
|
|
|
275
|
-
function formatRelativeTime(
|
|
287
|
+
function formatRelativeTime(
|
|
288
|
+
timestamp: number,
|
|
289
|
+
now: number,
|
|
290
|
+
t: (key: string, params?: Record<string, string | number>) => string
|
|
291
|
+
): string {
|
|
276
292
|
const diff = Math.max(0, now - timestamp);
|
|
277
293
|
const sec = Math.floor(diff / 1000);
|
|
278
|
-
if (sec < 60) return
|
|
294
|
+
if (sec < 60) return t("common.secondsAgo", { sec });
|
|
279
295
|
const min = Math.floor(sec / 60);
|
|
280
|
-
if (min < 60) return
|
|
296
|
+
if (min < 60) return t("common.minutesAgo", { min });
|
|
281
297
|
const hour = Math.floor(min / 60);
|
|
282
|
-
if (hour < 24) return
|
|
298
|
+
if (hour < 24) return t("common.hoursAgo", { hour });
|
|
283
299
|
const day = Math.floor(hour / 24);
|
|
284
|
-
return
|
|
300
|
+
return t("common.daysAgo", { day });
|
|
285
301
|
}
|
|
286
302
|
|
|
287
303
|
function getSelectedTargetsFromIds(
|
|
@@ -304,6 +320,7 @@ function shouldClearSelectionOnBlank(target: HTMLElement | null) {
|
|
|
304
320
|
if (
|
|
305
321
|
target.closest(".rv-selectable") ||
|
|
306
322
|
target.closest(".moveable-control-box") ||
|
|
323
|
+
target.closest(".moveable-group") ||
|
|
307
324
|
target.closest(".moveable-line") ||
|
|
308
325
|
target.closest(".moveable-control") ||
|
|
309
326
|
target.closest(".moveable-direction")
|
|
@@ -330,9 +347,15 @@ export type ReactViewPanelProps = {
|
|
|
330
347
|
className?: string;
|
|
331
348
|
};
|
|
332
349
|
|
|
333
|
-
export function ReactViewPanel({
|
|
350
|
+
export function ReactViewPanel({
|
|
351
|
+
initialZoom = 1,
|
|
352
|
+
className,
|
|
353
|
+
}: ReactViewPanelProps) {
|
|
354
|
+
const { t, locale, setLocale } = useI18n();
|
|
355
|
+
const messages = useMemo(() => getPanelMessages(t), [t]);
|
|
334
356
|
const THEME_STORAGE_KEY = "panel:theme";
|
|
335
357
|
const TITLE_ICON_STORAGE_KEY = "panel:titleIconDataUrl";
|
|
358
|
+
const { setTheme } = useTheme();
|
|
336
359
|
const themedScrollbarClass =
|
|
337
360
|
"scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
|
|
338
361
|
const {
|
|
@@ -396,7 +419,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
396
419
|
return { x: rounded, y: rounded };
|
|
397
420
|
});
|
|
398
421
|
}, []);
|
|
399
|
-
const
|
|
422
|
+
const { scroll, scrollRef: scrollPosRef, onScrollChange: onViewportScrollChange } =
|
|
423
|
+
useRafThrottledScroll();
|
|
424
|
+
const viewportSyncRef = useRef<(() => void) | null>(null);
|
|
400
425
|
|
|
401
426
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
402
427
|
const [selectedTargets, setSelectedTargets] = useState<HTMLElement[]>([]);
|
|
@@ -429,16 +454,19 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
429
454
|
const [isHistoryPanelExpanded, setIsHistoryPanelExpanded] = useState(false);
|
|
430
455
|
const [historyNow, setHistoryNow] = useState(Date.now());
|
|
431
456
|
const [historyKeyword, setHistoryKeyword] = useState("");
|
|
432
|
-
const [productName, setProductName] = useState("
|
|
457
|
+
const [productName, setProductName] = useState(t("panel.defaults.unnamedProduct"));
|
|
433
458
|
const [titleIconDataUrl, setTitleIconDataUrl] = useState<string>("");
|
|
434
459
|
const [titleIconPreviewOpen, setTitleIconPreviewOpen] = useState(false);
|
|
435
460
|
const [titleIconZoom, setTitleIconZoom] = useState(1.6);
|
|
436
461
|
const [pendingPreviewLayerId, setPendingPreviewLayerId] = useState<string | null>(null);
|
|
437
462
|
const [panelFontSize, setPanelFontSize] = useState<"sm" | "md" | "lg">("md");
|
|
438
463
|
const [blueprintOpen, setBlueprintOpen] = useState(false);
|
|
464
|
+
const [assistantOpen, setAssistantOpen] = useState(false);
|
|
439
465
|
const [blueprintGraph, setBlueprintGraph] = useState(() => BlueprintGraph.empty());
|
|
466
|
+
const blueprintGraphRef = useRef(blueprintGraph);
|
|
467
|
+
blueprintGraphRef.current = blueprintGraph;
|
|
440
468
|
const [blueprintMeta, setBlueprintMeta] = useState<BlueprintMetaDraft>({
|
|
441
|
-
name: "
|
|
469
|
+
name: t("panel.defaults.unnamedBlueprint"),
|
|
442
470
|
remark: "",
|
|
443
471
|
});
|
|
444
472
|
const [blueprintLibraryItems, setBlueprintLibraryItems] = useState<
|
|
@@ -487,7 +515,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
487
515
|
const applyTheme = useCallback((checked: boolean) => {
|
|
488
516
|
const root = document.documentElement;
|
|
489
517
|
root.classList.toggle("dark", checked);
|
|
518
|
+
root.classList.toggle("light", !checked);
|
|
490
519
|
root.dataset.theme = checked ? "dark" : "light";
|
|
520
|
+
setTheme(checked ? "dark" : "light");
|
|
491
521
|
try {
|
|
492
522
|
// panel 独立持久化键,避免与宿主应用的通用 theme 键互相覆盖
|
|
493
523
|
localStorage.setItem(THEME_STORAGE_KEY, checked ? "dark" : "light");
|
|
@@ -497,7 +527,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
497
527
|
// ignore storage errors
|
|
498
528
|
}
|
|
499
529
|
setIsDark(checked);
|
|
500
|
-
}, [THEME_STORAGE_KEY]);
|
|
530
|
+
}, [THEME_STORAGE_KEY, setTheme]);
|
|
501
531
|
|
|
502
532
|
useEffect(() => {
|
|
503
533
|
const root = document.documentElement;
|
|
@@ -828,7 +858,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
828
858
|
const blueprintDebugSession = useBlueprintDebugSession({
|
|
829
859
|
graph: blueprintGraph,
|
|
830
860
|
blueprintId: activeBlueprintLibraryId,
|
|
831
|
-
blueprintName: blueprintMeta.name || "
|
|
861
|
+
blueprintName: blueprintMeta.name || t("panel.defaults.unnamedBlueprint"),
|
|
832
862
|
resolveLibraryBlueprint,
|
|
833
863
|
libraryNameById: blueprintLibraryNameById,
|
|
834
864
|
onExecutionBlocked: handleBlueprintExecutionBlocked,
|
|
@@ -894,8 +924,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
894
924
|
toast({
|
|
895
925
|
title:
|
|
896
926
|
removed > 0
|
|
897
|
-
?
|
|
898
|
-
:
|
|
927
|
+
? messages.clearedBlueprintLogs(removed)
|
|
928
|
+
: messages.noSavedBlueprintLogs,
|
|
899
929
|
});
|
|
900
930
|
},
|
|
901
931
|
hasSavedRuns: blueprintDebugSession.totalSavedRunCount > 0,
|
|
@@ -946,7 +976,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
946
976
|
async (id: string) => {
|
|
947
977
|
const record = await getBlueprintLibraryRecord(id);
|
|
948
978
|
if (!record) {
|
|
949
|
-
toast({ title: "
|
|
979
|
+
toast({ title: t("panel.messages.blueprintNotFound") });
|
|
950
980
|
void refreshBlueprintLibrary();
|
|
951
981
|
return;
|
|
952
982
|
}
|
|
@@ -1018,7 +1048,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1018
1048
|
|
|
1019
1049
|
const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
|
|
1020
1050
|
if (!existing) {
|
|
1021
|
-
toast({ title: "
|
|
1051
|
+
toast({ title: t("panel.messages.blueprintNotFound") });
|
|
1022
1052
|
void refreshBlueprintLibrary();
|
|
1023
1053
|
setActiveBlueprintLibraryId(null);
|
|
1024
1054
|
setBlueprintSyncedDocument(null);
|
|
@@ -1035,7 +1065,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1035
1065
|
await putBlueprintLibraryRecord(record);
|
|
1036
1066
|
setBlueprintSyncedDocument(blueprintGraph.document);
|
|
1037
1067
|
await refreshBlueprintLibrary();
|
|
1038
|
-
toast({ title:
|
|
1068
|
+
toast({ title: messages.blueprintSynced(record.name) });
|
|
1039
1069
|
}, [
|
|
1040
1070
|
activeBlueprintLibraryId,
|
|
1041
1071
|
blueprintGraph.document,
|
|
@@ -1072,7 +1102,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1072
1102
|
setActiveBlueprintLibraryId(record.id);
|
|
1073
1103
|
setBlueprintSyncedDocument(blueprintGraph.document);
|
|
1074
1104
|
await refreshBlueprintLibrary();
|
|
1075
|
-
toast({ title: "
|
|
1105
|
+
toast({ title: t("panel.messages.blueprintSavedLocal") });
|
|
1076
1106
|
},
|
|
1077
1107
|
[activeBlueprintLibraryId, blueprintGraph.document, refreshBlueprintLibrary]
|
|
1078
1108
|
);
|
|
@@ -1089,7 +1119,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1089
1119
|
downloadBlueprintExport(
|
|
1090
1120
|
buildBlueprintExportPayload(blueprintGraph.document, meta)
|
|
1091
1121
|
);
|
|
1092
|
-
toast({ title: "
|
|
1122
|
+
toast({ title: t("panel.messages.blueprintExported") });
|
|
1093
1123
|
return;
|
|
1094
1124
|
}
|
|
1095
1125
|
await saveBlueprintToLibrary(meta);
|
|
@@ -1101,7 +1131,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1101
1131
|
async (id: string, name: string) => {
|
|
1102
1132
|
const updated = await updateBlueprintLibraryMeta(id, { name });
|
|
1103
1133
|
if (!updated) {
|
|
1104
|
-
toast({ title: "
|
|
1134
|
+
toast({ title: t("panel.messages.renameFailed") });
|
|
1105
1135
|
void refreshBlueprintLibrary();
|
|
1106
1136
|
return;
|
|
1107
1137
|
}
|
|
@@ -1109,7 +1139,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1109
1139
|
if (activeBlueprintLibraryId === id) {
|
|
1110
1140
|
setBlueprintMeta((prev) => ({ ...prev, name }));
|
|
1111
1141
|
}
|
|
1112
|
-
toast({ title: "
|
|
1142
|
+
toast({ title: t("panel.messages.blueprintRenamed") });
|
|
1113
1143
|
},
|
|
1114
1144
|
[activeBlueprintLibraryId, refreshBlueprintLibrary]
|
|
1115
1145
|
);
|
|
@@ -1122,7 +1152,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1122
1152
|
setActiveBlueprintLibraryId(null);
|
|
1123
1153
|
setBlueprintSyncedDocument(null);
|
|
1124
1154
|
}
|
|
1125
|
-
toast({ title: "
|
|
1155
|
+
toast({ title: t("panel.messages.blueprintDeletedFromLibrary") });
|
|
1126
1156
|
},
|
|
1127
1157
|
[activeBlueprintLibraryId, refreshBlueprintLibrary]
|
|
1128
1158
|
);
|
|
@@ -1142,7 +1172,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1142
1172
|
snapshotWorkspaceBlueprint();
|
|
1143
1173
|
}
|
|
1144
1174
|
await loadBlueprintFromLibrary(record.id);
|
|
1145
|
-
toast({ title: "
|
|
1175
|
+
toast({ title: t("panel.messages.blueprintImported") });
|
|
1146
1176
|
},
|
|
1147
1177
|
[
|
|
1148
1178
|
activeBlueprintLibraryId,
|
|
@@ -1162,7 +1192,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1162
1192
|
workspaceBlueprintRef.current = {
|
|
1163
1193
|
document: record.blueprintDocument,
|
|
1164
1194
|
meta: {
|
|
1165
|
-
name: record.blueprintMeta?.name ?? "
|
|
1195
|
+
name: record.blueprintMeta?.name ?? t("panel.defaults.unnamedBlueprint"),
|
|
1166
1196
|
remark: record.blueprintMeta?.remark ?? "",
|
|
1167
1197
|
},
|
|
1168
1198
|
};
|
|
@@ -1190,7 +1220,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1190
1220
|
onProjectApplied: handleWorkspaceProjectApplied,
|
|
1191
1221
|
});
|
|
1192
1222
|
|
|
1193
|
-
useBlueprintPageLifecycle({
|
|
1223
|
+
const { triggerBlueprintNode } = useBlueprintPageLifecycle({
|
|
1194
1224
|
graph: blueprintGraph,
|
|
1195
1225
|
active: blueprintOpen,
|
|
1196
1226
|
bootKey: workspaceProjects.activeProjectId ?? undefined,
|
|
@@ -1205,7 +1235,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1205
1235
|
const handleWorkspaceCreateProject = useCallback(async () => {
|
|
1206
1236
|
const result = await workspaceProjects.handleCreateProject();
|
|
1207
1237
|
if (result?.name) {
|
|
1208
|
-
toast({ title:
|
|
1238
|
+
toast({ title: messages.workspaceCreated(result.name) });
|
|
1209
1239
|
}
|
|
1210
1240
|
return result;
|
|
1211
1241
|
}, [workspaceProjects]);
|
|
@@ -1217,12 +1247,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1217
1247
|
await workspaceProjects.handleOpenProject(id);
|
|
1218
1248
|
setSelectedIds([]);
|
|
1219
1249
|
toast({
|
|
1220
|
-
title: "
|
|
1221
|
-
description: "
|
|
1250
|
+
title: t("panel.messages.workspaceLoaded"),
|
|
1251
|
+
description: t("panel.messages.workspaceLoadedDesc"),
|
|
1222
1252
|
});
|
|
1223
1253
|
} catch (error) {
|
|
1224
1254
|
toast({
|
|
1225
|
-
title: error instanceof Error ? error.message : "
|
|
1255
|
+
title: error instanceof Error ? error.message : t("panel.messages.openWorkspaceFailed"),
|
|
1226
1256
|
});
|
|
1227
1257
|
}
|
|
1228
1258
|
},
|
|
@@ -1233,11 +1263,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1233
1263
|
try {
|
|
1234
1264
|
const name = await workspaceProjects.handleSyncProject();
|
|
1235
1265
|
if (name) {
|
|
1236
|
-
toast({ title:
|
|
1266
|
+
toast({ title: messages.blueprintSynced(name) });
|
|
1237
1267
|
}
|
|
1238
1268
|
} catch (error) {
|
|
1239
1269
|
toast({
|
|
1240
|
-
title: error instanceof Error ? error.message : "
|
|
1270
|
+
title: error instanceof Error ? error.message : t("panel.messages.syncFailed"),
|
|
1241
1271
|
});
|
|
1242
1272
|
}
|
|
1243
1273
|
}, [workspaceProjects]);
|
|
@@ -1245,7 +1275,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1245
1275
|
const handleWorkspaceDeleteProject = useCallback(
|
|
1246
1276
|
async (id: string) => {
|
|
1247
1277
|
await workspaceProjects.handleDeleteProject(id);
|
|
1248
|
-
toast({ title: "
|
|
1278
|
+
toast({ title: t("panel.messages.workspaceDeleted") });
|
|
1249
1279
|
},
|
|
1250
1280
|
[workspaceProjects]
|
|
1251
1281
|
);
|
|
@@ -1256,7 +1286,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1256
1286
|
await workspaceProjects.handlePreviewProject(projectId, options);
|
|
1257
1287
|
} catch (error) {
|
|
1258
1288
|
toast({
|
|
1259
|
-
title: error instanceof Error ? error.message : "
|
|
1289
|
+
title: error instanceof Error ? error.message : t("panel.messages.openPreviewFailed"),
|
|
1260
1290
|
});
|
|
1261
1291
|
}
|
|
1262
1292
|
},
|
|
@@ -1295,42 +1325,200 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1295
1325
|
const layer = layerById.get(el.layerId);
|
|
1296
1326
|
return !layer?.locked;
|
|
1297
1327
|
});
|
|
1328
|
+
|
|
1329
|
+
const selectedIdsRef = useRef(selectedIds);
|
|
1330
|
+
selectedIdsRef.current = selectedIds;
|
|
1331
|
+
const zoomRef = useRef(zoom);
|
|
1332
|
+
zoomRef.current = zoom;
|
|
1333
|
+
const blueprintOpenRef = useRef(blueprintOpen);
|
|
1334
|
+
blueprintOpenRef.current = blueprintOpen;
|
|
1335
|
+
const workspaceLiveRef = useRef({
|
|
1336
|
+
activeProjectId: workspaceProjects.activeProjectId,
|
|
1337
|
+
activeProjectName: workspaceProjects.activeProjectName,
|
|
1338
|
+
dirty: workspaceProjects.dirty,
|
|
1339
|
+
projectCount: workspaceProjects.projects.length,
|
|
1340
|
+
});
|
|
1341
|
+
workspaceLiveRef.current = {
|
|
1342
|
+
activeProjectId: workspaceProjects.activeProjectId,
|
|
1343
|
+
activeProjectName: workspaceProjects.activeProjectName,
|
|
1344
|
+
dirty: workspaceProjects.dirty,
|
|
1345
|
+
projectCount: workspaceProjects.projects.length,
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
const assistantDeps = useMemo<ApplyAssistantDeps>(
|
|
1349
|
+
() => ({
|
|
1350
|
+
addElementFromMaterial,
|
|
1351
|
+
updateElement,
|
|
1352
|
+
deleteElement,
|
|
1353
|
+
duplicateElement,
|
|
1354
|
+
setSelectedIds: (ids) => {
|
|
1355
|
+
selectedIdsRef.current = ids;
|
|
1356
|
+
selectViewElements(ids);
|
|
1357
|
+
// Newly added nodes may not be in DOM until next paint — refresh Moveable targets.
|
|
1358
|
+
requestAnimationFrame(() => {
|
|
1359
|
+
setSelectedTargets(
|
|
1360
|
+
getSelectedTargetsFromIds(canvasRef.current, selectedIdsRef.current)
|
|
1361
|
+
);
|
|
1362
|
+
});
|
|
1363
|
+
},
|
|
1364
|
+
getSelectedIds: () => selectedIdsRef.current,
|
|
1365
|
+
revealConfigFromPatch: (patch) => {
|
|
1366
|
+
window.setTimeout(() => {
|
|
1367
|
+
revealPanelConfigFromPatch(patch);
|
|
1368
|
+
}, 60);
|
|
1369
|
+
},
|
|
1370
|
+
focusWorkspace: (area) => {
|
|
1371
|
+
if (area === "blueprint") {
|
|
1372
|
+
setConfigFocus("blueprint");
|
|
1373
|
+
setBlueprintOpen(true);
|
|
1374
|
+
} else {
|
|
1375
|
+
focusViewConfig();
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
getElementIds: () =>
|
|
1379
|
+
new Set(listPanelElementsFromStore().map((el) => el.id)),
|
|
1380
|
+
getElement: (id) => listPanelElementsFromStore().find((el) => el.id === id),
|
|
1381
|
+
listElements: () => listPanelElementsFromStore(),
|
|
1382
|
+
getLayers: () => listLayersFromStore(),
|
|
1383
|
+
getActiveLayerId: () => getActiveLayerIdFromStore(),
|
|
1384
|
+
setActiveLayer,
|
|
1385
|
+
addLayer,
|
|
1386
|
+
renameLayer,
|
|
1387
|
+
setLayerLocked: (id, locked) => {
|
|
1388
|
+
const layer = listLayersFromStore().find((l) => l.id === id);
|
|
1389
|
+
if (!layer) return;
|
|
1390
|
+
if (!!layer.locked !== locked) toggleLayerLock(id);
|
|
1391
|
+
},
|
|
1392
|
+
deleteLayer: (id) => {
|
|
1393
|
+
const result = deleteLayer(id, { mode: "remove" });
|
|
1394
|
+
return result.ok
|
|
1395
|
+
? { ok: true }
|
|
1396
|
+
: { ok: false, reason: result.reason };
|
|
1397
|
+
},
|
|
1398
|
+
bringElementsToFront,
|
|
1399
|
+
sendElementsToBack,
|
|
1400
|
+
bringElementsForward,
|
|
1401
|
+
sendElementsBackward,
|
|
1402
|
+
getZoom: () => zoomRef.current.x,
|
|
1403
|
+
setZoomAbsolute: (value) => {
|
|
1404
|
+
const next = Math.min(4, Math.max(0.25, value));
|
|
1405
|
+
const rounded = Number(next.toFixed(4));
|
|
1406
|
+
zoomRef.current = { x: rounded, y: rounded };
|
|
1407
|
+
setZoom({ x: rounded, y: rounded });
|
|
1408
|
+
},
|
|
1409
|
+
adjustZoomDelta: (delta) => {
|
|
1410
|
+
const next = Math.min(4, Math.max(0.25, zoomRef.current.x + delta));
|
|
1411
|
+
const rounded = Number(next.toFixed(4));
|
|
1412
|
+
zoomRef.current = { x: rounded, y: rounded };
|
|
1413
|
+
setZoom({ x: rounded, y: rounded });
|
|
1414
|
+
},
|
|
1415
|
+
fitViewport: () => {
|
|
1416
|
+
zoomRef.current = { x: 1, y: 1 };
|
|
1417
|
+
setZoom({ x: 1, y: 1 });
|
|
1418
|
+
},
|
|
1419
|
+
undo,
|
|
1420
|
+
redo,
|
|
1421
|
+
setBlueprintGraph,
|
|
1422
|
+
getBlueprintGraph: () => blueprintGraphRef.current,
|
|
1423
|
+
setBlueprintOpen,
|
|
1424
|
+
getBlueprintOpen: () => blueprintOpenRef.current,
|
|
1425
|
+
runBlueprintAll: () => blueprintDebugSession.runAll(),
|
|
1426
|
+
getWorkspace: () => ({ ...workspaceLiveRef.current }),
|
|
1427
|
+
workspaceSave: () => workspaceProjects.handleSaveProject(),
|
|
1428
|
+
workspaceSync: () => workspaceProjects.handleSyncProject(),
|
|
1429
|
+
workspaceCreate: async (name) => {
|
|
1430
|
+
if (name?.trim()) setProductName(name.trim());
|
|
1431
|
+
return workspaceProjects.handleCreateProject();
|
|
1432
|
+
},
|
|
1433
|
+
workspaceOpen: (id) => workspaceProjects.handleOpenProject(id),
|
|
1434
|
+
workspacePreview: async (id) => {
|
|
1435
|
+
const target =
|
|
1436
|
+
id ?? workspaceLiveRef.current.activeProjectId ?? undefined;
|
|
1437
|
+
if (!target) {
|
|
1438
|
+
const created = await workspaceProjects.handleCreateProject();
|
|
1439
|
+
await openOnlinePreviewForProject(created.id, { syncFirst: false });
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
await openOnlinePreviewForProject(target, { syncFirst: true });
|
|
1443
|
+
},
|
|
1444
|
+
exportPanelJson: () => JSON.stringify(exportPanelData(), null, 2),
|
|
1445
|
+
importPanelJson: (json) => {
|
|
1446
|
+
const parsed = JSON.parse(json) as State;
|
|
1447
|
+
const ok = importPanelData(parsed);
|
|
1448
|
+
if (ok) {
|
|
1449
|
+
selectedIdsRef.current = [];
|
|
1450
|
+
setSelectedIds([]);
|
|
1451
|
+
}
|
|
1452
|
+
return ok;
|
|
1453
|
+
},
|
|
1454
|
+
setTheme: (theme) => applyTheme(theme === "dark"),
|
|
1455
|
+
setLocale,
|
|
1456
|
+
setPanelFontSize,
|
|
1457
|
+
}),
|
|
1458
|
+
[
|
|
1459
|
+
addElementFromMaterial,
|
|
1460
|
+
updateElement,
|
|
1461
|
+
deleteElement,
|
|
1462
|
+
duplicateElement,
|
|
1463
|
+
selectViewElements,
|
|
1464
|
+
focusViewConfig,
|
|
1465
|
+
setActiveLayer,
|
|
1466
|
+
addLayer,
|
|
1467
|
+
renameLayer,
|
|
1468
|
+
toggleLayerLock,
|
|
1469
|
+
deleteLayer,
|
|
1470
|
+
bringElementsToFront,
|
|
1471
|
+
sendElementsToBack,
|
|
1472
|
+
bringElementsForward,
|
|
1473
|
+
sendElementsBackward,
|
|
1474
|
+
undo,
|
|
1475
|
+
redo,
|
|
1476
|
+
setBlueprintGraph,
|
|
1477
|
+
blueprintDebugSession,
|
|
1478
|
+
workspaceProjects,
|
|
1479
|
+
openOnlinePreviewForProject,
|
|
1480
|
+
exportPanelData,
|
|
1481
|
+
importPanelData,
|
|
1482
|
+
applyTheme,
|
|
1483
|
+
setLocale,
|
|
1484
|
+
]
|
|
1485
|
+
);
|
|
1298
1486
|
const showActionHint = useCallback((message: string) => {
|
|
1299
1487
|
toast({
|
|
1300
|
-
title: "
|
|
1488
|
+
title: t("panel.messages.operationRestricted"),
|
|
1301
1489
|
description: message,
|
|
1302
1490
|
});
|
|
1303
1491
|
}, []);
|
|
1304
1492
|
const hintUnlockNodeForDelete = useCallback(() => {
|
|
1305
|
-
showActionHint(
|
|
1493
|
+
showActionHint(messages.nodeDeleteLocked);
|
|
1306
1494
|
}, [showActionHint]);
|
|
1307
1495
|
const hintUnlockLayerForDelete = useCallback(() => {
|
|
1308
|
-
showActionHint(
|
|
1496
|
+
showActionHint(messages.nodeDeleteLayerLocked);
|
|
1309
1497
|
}, [showActionHint]);
|
|
1310
1498
|
const hintLockedNodesInBatchDelete = useCallback(() => {
|
|
1311
|
-
showActionHint(
|
|
1499
|
+
showActionHint(messages.nodeBatchDeleteContainsLocked);
|
|
1312
1500
|
}, [showActionHint]);
|
|
1313
1501
|
const hintNodeNotFound = useCallback(() => {
|
|
1314
|
-
showActionHint(
|
|
1502
|
+
showActionHint(messages.nodeNotFound);
|
|
1315
1503
|
}, [showActionHint]);
|
|
1316
1504
|
const hintUnlockNodeForMove = useCallback(() => {
|
|
1317
|
-
showActionHint(
|
|
1505
|
+
showActionHint(messages.nodeMoveLocked);
|
|
1318
1506
|
}, [showActionHint]);
|
|
1319
1507
|
const hintUnlockSourceLayerForMove = useCallback(() => {
|
|
1320
|
-
showActionHint(
|
|
1508
|
+
showActionHint(messages.nodeMoveSourceLayerLocked);
|
|
1321
1509
|
}, [showActionHint]);
|
|
1322
1510
|
const hintTargetLayerNotFound = useCallback(() => {
|
|
1323
|
-
showActionHint(
|
|
1511
|
+
showActionHint(messages.targetLayerNotFound);
|
|
1324
1512
|
}, [showActionHint]);
|
|
1325
1513
|
const hintUnlockTargetLayerForMove = useCallback(() => {
|
|
1326
|
-
showActionHint(
|
|
1514
|
+
showActionHint(messages.nodeMoveTargetLayerLocked);
|
|
1327
1515
|
}, [showActionHint]);
|
|
1328
1516
|
const getLayerDeleteBlockReason = useCallback(
|
|
1329
1517
|
(layerId: string) => {
|
|
1330
1518
|
const targetLayer = layerById.get(layerId);
|
|
1331
|
-
if (!targetLayer) return "
|
|
1332
|
-
if (!targetLayer.editable) return "
|
|
1333
|
-
if (targetLayer.locked) return "
|
|
1519
|
+
if (!targetLayer) return t("panel.messages.layerNotFound");
|
|
1520
|
+
if (!targetLayer.editable) return t("panel.layers.cannotDeleteDefault");
|
|
1521
|
+
if (targetLayer.locked) return t("panel.layers.cannotDeleteLocked");
|
|
1334
1522
|
if (targetLayer.isMapping) return null;
|
|
1335
1523
|
const hasBlockingRef = allElements.some((el) => {
|
|
1336
1524
|
if (el.layerId === layerId) return false;
|
|
@@ -1338,7 +1526,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1338
1526
|
if (el.refLayerId !== layerId) return false;
|
|
1339
1527
|
return (el.refCopyMode ?? "shallow") !== "deep";
|
|
1340
1528
|
});
|
|
1341
|
-
if (hasBlockingRef) return "
|
|
1529
|
+
if (hasBlockingRef) return t("panel.messages.shallowRefBlockingDelete");
|
|
1342
1530
|
return null;
|
|
1343
1531
|
},
|
|
1344
1532
|
[allElements, layerById]
|
|
@@ -1389,7 +1577,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1389
1577
|
const parsed = JSON.parse(text) as State;
|
|
1390
1578
|
const ok = importPanelData(parsed);
|
|
1391
1579
|
if (!ok) {
|
|
1392
|
-
window.alert(
|
|
1580
|
+
window.alert(messages.importInvalidFormat);
|
|
1393
1581
|
} else {
|
|
1394
1582
|
setSelectedIds([]);
|
|
1395
1583
|
}
|
|
@@ -1548,7 +1736,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1548
1736
|
const previewFaviconLink = faviconHrefWithVersion
|
|
1549
1737
|
? `<link rel="icon" type="image/png" href="${faviconHrefWithVersion}" /><link rel="shortcut icon" type="image/png" href="${faviconHrefWithVersion}" />`
|
|
1550
1738
|
: "";
|
|
1551
|
-
const previewTitle = productName.trim() || "
|
|
1739
|
+
const previewTitle = productName.trim() || t("panel.defaults.unnamedProduct");
|
|
1552
1740
|
const escapedPreviewTitle = previewTitle
|
|
1553
1741
|
.replace(/&/g, "&")
|
|
1554
1742
|
.replace(/</g, "<")
|
|
@@ -1670,7 +1858,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1670
1858
|
}
|
|
1671
1859
|
} catch (error) {
|
|
1672
1860
|
toast({
|
|
1673
|
-
title: error instanceof Error ? error.message : "
|
|
1861
|
+
title: error instanceof Error ? error.message : t("panel.messages.openPreviewFailed"),
|
|
1674
1862
|
});
|
|
1675
1863
|
}
|
|
1676
1864
|
})();
|
|
@@ -1760,59 +1948,70 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1760
1948
|
<nav className="panel-font-root relative z-30 flex items-center gap-2 border-b border-border bg-background/95 px-2 text-foreground">
|
|
1761
1949
|
<Menubar className="h-8 border-0 bg-transparent p-0 shadow-none">
|
|
1762
1950
|
<MenubarMenu>
|
|
1763
|
-
<MenubarTrigger className="px-2 py-1 text-xs font-normal"
|
|
1951
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.file")}</MenubarTrigger>
|
|
1764
1952
|
<MenubarContent className="z-[10100]">
|
|
1765
|
-
{/* <MenubarItem onClick={handlePreviewLayer}
|
|
1766
|
-
<MenubarItem onClick={handleExport}
|
|
1767
|
-
<MenubarItem onClick={() => importInputRef.current?.click()}
|
|
1953
|
+
{/* <MenubarItem onClick={handlePreviewLayer}>{t("panel.workspace.previewDocTitle")}</MenubarItem> */}
|
|
1954
|
+
<MenubarItem onClick={handleExport}>{t("panel.menubar.export")}</MenubarItem>
|
|
1955
|
+
<MenubarItem onClick={() => importInputRef.current?.click()}>{t("panel.menubar.import")}</MenubarItem>
|
|
1768
1956
|
</MenubarContent>
|
|
1769
1957
|
</MenubarMenu>
|
|
1770
1958
|
<MenubarMenu>
|
|
1771
|
-
<MenubarTrigger className="px-2 py-1 text-xs font-normal"
|
|
1959
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.edit")}</MenubarTrigger>
|
|
1772
1960
|
<MenubarContent className="z-[10100]">
|
|
1773
|
-
<MenubarItem disabled={!canUndo} onClick={undo}
|
|
1774
|
-
<MenubarItem disabled={!canRedo} onClick={redo}
|
|
1961
|
+
<MenubarItem disabled={!canUndo} onClick={undo}>{t("panel.menubar.undo")}</MenubarItem>
|
|
1962
|
+
<MenubarItem disabled={!canRedo} onClick={redo}>{t("panel.menubar.redo")}</MenubarItem>
|
|
1775
1963
|
<MenubarSeparator />
|
|
1776
|
-
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsForward(selectedIds)}
|
|
1777
|
-
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsBackward(selectedIds)}
|
|
1778
|
-
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsToFront(selectedIds)}
|
|
1779
|
-
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsToBack(selectedIds)}
|
|
1964
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsForward(selectedIds)}>{t("panel.menubar.bringForward")}</MenubarItem>
|
|
1965
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsBackward(selectedIds)}>{t("panel.menubar.sendBackward")}</MenubarItem>
|
|
1966
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => bringElementsToFront(selectedIds)}>{t("panel.menubar.bringToFront")}</MenubarItem>
|
|
1967
|
+
<MenubarItem disabled={!hasUnlockedSelection} onClick={() => sendElementsToBack(selectedIds)}>{t("panel.menubar.sendToBack")}</MenubarItem>
|
|
1780
1968
|
</MenubarContent>
|
|
1781
1969
|
</MenubarMenu>
|
|
1782
1970
|
<MenubarMenu>
|
|
1783
|
-
<MenubarTrigger className="px-2 py-1 text-xs font-normal"
|
|
1971
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.view")}</MenubarTrigger>
|
|
1784
1972
|
<MenubarContent className="z-[10100]">
|
|
1785
|
-
<MenubarItem onClick={() => adjustUniformZoom((z) => z - 0.1)}
|
|
1786
|
-
<MenubarItem onClick={() => adjustUniformZoom((z) => z + 0.1)}
|
|
1973
|
+
<MenubarItem onClick={() => adjustUniformZoom((z) => z - 0.1)}>{t("panel.menubar.zoomOut")}</MenubarItem>
|
|
1974
|
+
<MenubarItem onClick={() => adjustUniformZoom((z) => z + 0.1)}>{t("panel.menubar.zoomIn")}</MenubarItem>
|
|
1787
1975
|
<MenubarSeparator />
|
|
1788
1976
|
<MenubarItem
|
|
1789
1977
|
onClick={() => {
|
|
1790
1978
|
applyTheme(!isDark);
|
|
1791
1979
|
}}
|
|
1792
1980
|
>
|
|
1793
|
-
{isDark ? "
|
|
1981
|
+
{isDark ? t("panel.menubar.switchToLight") : t("panel.menubar.switchToDark")}
|
|
1794
1982
|
</MenubarItem>
|
|
1795
1983
|
</MenubarContent>
|
|
1796
1984
|
</MenubarMenu>
|
|
1797
1985
|
<MenubarMenu>
|
|
1798
|
-
<MenubarTrigger className="px-2 py-1 text-xs font-normal"
|
|
1986
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.blueprint")}</MenubarTrigger>
|
|
1799
1987
|
<MenubarContent className="z-[10100]">
|
|
1800
|
-
<MenubarItem onClick={openBlueprintExportDialog}
|
|
1988
|
+
<MenubarItem onClick={openBlueprintExportDialog}>{t("panel.menubar.export")}</MenubarItem>
|
|
1801
1989
|
<MenubarItem onClick={() => blueprintImportInputRef.current?.click()}>
|
|
1802
|
-
|
|
1990
|
+
{t("panel.menubar.import")}
|
|
1803
1991
|
</MenubarItem>
|
|
1804
1992
|
</MenubarContent>
|
|
1805
1993
|
</MenubarMenu>
|
|
1806
1994
|
<MenubarMenu>
|
|
1807
|
-
<MenubarTrigger className="px-2 py-1 text-xs font-normal"
|
|
1995
|
+
<MenubarTrigger className="px-2 py-1 text-xs font-normal">{t("panel.menubar.settings")}</MenubarTrigger>
|
|
1808
1996
|
<MenubarContent className="z-[10100]">
|
|
1809
1997
|
<MenubarRadioGroup
|
|
1810
1998
|
value={panelFontSize}
|
|
1811
1999
|
onValueChange={(value) => setPanelFontSize(value as "sm" | "md" | "lg")}
|
|
1812
2000
|
>
|
|
1813
|
-
<MenubarRadioItem value="sm"
|
|
1814
|
-
<MenubarRadioItem value="md"
|
|
1815
|
-
<MenubarRadioItem value="lg"
|
|
2001
|
+
<MenubarRadioItem value="sm">{t("panel.menubar.fontSmall")}</MenubarRadioItem>
|
|
2002
|
+
<MenubarRadioItem value="md">{t("panel.menubar.fontMedium")}</MenubarRadioItem>
|
|
2003
|
+
<MenubarRadioItem value="lg">{t("panel.menubar.fontLarge")}</MenubarRadioItem>
|
|
2004
|
+
</MenubarRadioGroup>
|
|
2005
|
+
<MenubarSeparator />
|
|
2006
|
+
<div className="px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
2007
|
+
{t("panel.theme.language")}
|
|
2008
|
+
</div>
|
|
2009
|
+
<MenubarRadioGroup
|
|
2010
|
+
value={locale}
|
|
2011
|
+
onValueChange={(value) => setLocale(value as "zh-CN" | "en-US")}
|
|
2012
|
+
>
|
|
2013
|
+
<MenubarRadioItem value="zh-CN">{t("panel.theme.zhCN")}</MenubarRadioItem>
|
|
2014
|
+
<MenubarRadioItem value="en-US">{t("panel.theme.enUS")}</MenubarRadioItem>
|
|
1816
2015
|
</MenubarRadioGroup>
|
|
1817
2016
|
</MenubarContent>
|
|
1818
2017
|
</MenubarMenu>
|
|
@@ -1829,6 +2028,15 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1829
2028
|
onPreviewProject={openOnlinePreviewForProject}
|
|
1830
2029
|
/>
|
|
1831
2030
|
<div className="flex-1" />
|
|
2031
|
+
<Button
|
|
2032
|
+
type="button"
|
|
2033
|
+
variant={assistantOpen ? "secondary" : "outline"}
|
|
2034
|
+
size="sm"
|
|
2035
|
+
className="h-7 shrink-0 px-2 text-xs"
|
|
2036
|
+
onClick={() => setAssistantOpen((v) => !v)}
|
|
2037
|
+
>
|
|
2038
|
+
{assistantOpen ? t("panel.ai.hide") : t("panel.ai.open")}
|
|
2039
|
+
</Button>
|
|
1832
2040
|
</nav>
|
|
1833
2041
|
<ResizablePanelGroup direction="horizontal" className="min-h-0 w-full flex-1">
|
|
1834
2042
|
<ResizablePanel defaultSize={15} minSize={15}>
|
|
@@ -1930,10 +2138,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1930
2138
|
<Input
|
|
1931
2139
|
value={productName}
|
|
1932
2140
|
onChange={(e) => setProductName(e.target.value)}
|
|
1933
|
-
placeholder="
|
|
2141
|
+
placeholder={t("panel.menubar.productNamePlaceholder")}
|
|
1934
2142
|
className="h-7 w-[220px] text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none"
|
|
1935
2143
|
/>
|
|
1936
|
-
<span className="text-[11px] text-muted-foreground"
|
|
2144
|
+
<span className="text-[11px] text-muted-foreground">{t("panel.menubar.productName")}</span>
|
|
1937
2145
|
<TooltipProvider delayDuration={150}>
|
|
1938
2146
|
<Tooltip>
|
|
1939
2147
|
<TooltipTrigger asChild>
|
|
@@ -1941,12 +2149,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1941
2149
|
type="button"
|
|
1942
2150
|
onClick={() => titleIconInputRef.current?.click()}
|
|
1943
2151
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
1944
|
-
aria-label="
|
|
2152
|
+
aria-label={t("panel.menubar.uploadTitleIcon")}
|
|
1945
2153
|
>
|
|
1946
2154
|
<IconImage />
|
|
1947
2155
|
</button>
|
|
1948
2156
|
</TooltipTrigger>
|
|
1949
|
-
<TooltipContent className="z-[10000]"
|
|
2157
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.uploadTitleIcon")}</TooltipContent>
|
|
1950
2158
|
</Tooltip>
|
|
1951
2159
|
{titleIconDataUrl ? (
|
|
1952
2160
|
<Tooltip>
|
|
@@ -1955,12 +2163,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1955
2163
|
type="button"
|
|
1956
2164
|
onClick={() => setTitleIconDataUrl("")}
|
|
1957
2165
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
1958
|
-
aria-label="
|
|
2166
|
+
aria-label={t("panel.menubar.clearTitleIcon")}
|
|
1959
2167
|
>
|
|
1960
2168
|
<IconClose />
|
|
1961
2169
|
</button>
|
|
1962
2170
|
</TooltipTrigger>
|
|
1963
|
-
<TooltipContent className="z-[10000]"
|
|
2171
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.clearTitleIcon")}</TooltipContent>
|
|
1964
2172
|
</Tooltip>
|
|
1965
2173
|
) : null}
|
|
1966
2174
|
{titleIconDataUrl ? (
|
|
@@ -1970,17 +2178,17 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1970
2178
|
type="button"
|
|
1971
2179
|
onClick={() => setTitleIconPreviewOpen(true)}
|
|
1972
2180
|
className="h-7 shrink-0 flex min-w-[92px] items-center gap-1.5 rounded border border-border bg-muted/30 px-2 py-1 hover:bg-accent/40"
|
|
1973
|
-
aria-label="
|
|
2181
|
+
aria-label={t("panel.menubar.viewTitleIcon")}
|
|
1974
2182
|
>
|
|
1975
2183
|
<img
|
|
1976
2184
|
src={titleIconDataUrl}
|
|
1977
|
-
alt="
|
|
2185
|
+
alt={t("panel.menubar.titleIconThumbAlt")}
|
|
1978
2186
|
className="h-5 w-5 shrink-0 rounded border border-border/60 object-cover"
|
|
1979
2187
|
/>
|
|
1980
2188
|
<span className="text-[10px] text-muted-foreground">Title Icon</span>
|
|
1981
2189
|
</button>
|
|
1982
2190
|
</TooltipTrigger>
|
|
1983
|
-
<TooltipContent className="z-[10000]"
|
|
2191
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.viewTitleIconHint")}</TooltipContent>
|
|
1984
2192
|
</Tooltip>
|
|
1985
2193
|
) : null}
|
|
1986
2194
|
<Tooltip>
|
|
@@ -1990,12 +2198,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
1990
2198
|
onClick={undo}
|
|
1991
2199
|
disabled={!canUndo}
|
|
1992
2200
|
className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
|
|
1993
|
-
aria-label="
|
|
2201
|
+
aria-label={t("panel.menubar.undo")}
|
|
1994
2202
|
>
|
|
1995
2203
|
<IconUndo />
|
|
1996
2204
|
</button>
|
|
1997
2205
|
</TooltipTrigger>
|
|
1998
|
-
<TooltipContent className="z-[10000]"
|
|
2206
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.undoShortcut")}</TooltipContent>
|
|
1999
2207
|
</Tooltip>
|
|
2000
2208
|
<Tooltip>
|
|
2001
2209
|
<TooltipTrigger asChild>
|
|
@@ -2004,12 +2212,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2004
2212
|
onClick={redo}
|
|
2005
2213
|
disabled={!canRedo}
|
|
2006
2214
|
className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
|
|
2007
|
-
aria-label="
|
|
2215
|
+
aria-label={t("panel.menubar.redo")}
|
|
2008
2216
|
>
|
|
2009
2217
|
<IconRedo />
|
|
2010
2218
|
</button>
|
|
2011
2219
|
</TooltipTrigger>
|
|
2012
|
-
<TooltipContent className="z-[10000]"
|
|
2220
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.redoShortcut")}</TooltipContent>
|
|
2013
2221
|
</Tooltip>
|
|
2014
2222
|
<Tooltip>
|
|
2015
2223
|
<TooltipTrigger asChild>
|
|
@@ -2021,7 +2229,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2021
2229
|
"rounded border border-border p-1 hover:bg-accent",
|
|
2022
2230
|
isHistoryPanelExpanded ? "bg-accent/60" : "",
|
|
2023
2231
|
].join(" ")}
|
|
2024
|
-
aria-label={isHistoryPanelExpanded ? "
|
|
2232
|
+
aria-label={isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
|
|
2025
2233
|
>
|
|
2026
2234
|
<IconHistory />
|
|
2027
2235
|
</button>
|
|
@@ -2031,7 +2239,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2031
2239
|
style={{ zIndex: PANEL_Z_INDEX.historyPopover }}
|
|
2032
2240
|
>
|
|
2033
2241
|
<div className="mb-1 flex items-center justify-between">
|
|
2034
|
-
<span className="text-[11px] font-semibold text-muted-foreground"
|
|
2242
|
+
<span className="text-[11px] font-semibold text-muted-foreground">{t("panel.history.title")}</span>
|
|
2035
2243
|
<span className="text-[11px] text-muted-foreground">
|
|
2036
2244
|
{history.length > 0 ? `${historyCursor + 1}/${history.length}` : "0/0"}
|
|
2037
2245
|
</span>
|
|
@@ -2039,7 +2247,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2039
2247
|
<Input
|
|
2040
2248
|
value={historyKeyword}
|
|
2041
2249
|
onChange={(e) => setHistoryKeyword(e.target.value)}
|
|
2042
|
-
placeholder="
|
|
2250
|
+
placeholder={t("panel.history.searchPlaceholder")}
|
|
2043
2251
|
className="mb-2 h-7 text-xs"
|
|
2044
2252
|
/>
|
|
2045
2253
|
<div
|
|
@@ -2050,9 +2258,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2050
2258
|
<EmptyIcon className="h-8 w-8">
|
|
2051
2259
|
<IconHistory />
|
|
2052
2260
|
</EmptyIcon>
|
|
2053
|
-
<EmptyTitle className="text-xs"
|
|
2261
|
+
<EmptyTitle className="text-xs">{t("panel.history.emptyTitle")}</EmptyTitle>
|
|
2054
2262
|
<EmptyDescription className="text-[11px]">
|
|
2055
|
-
|
|
2263
|
+
{t("panel.history.emptyDesc")}
|
|
2056
2264
|
</EmptyDescription>
|
|
2057
2265
|
</Empty>
|
|
2058
2266
|
) : (
|
|
@@ -2083,7 +2291,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2083
2291
|
<div className="flex items-center justify-between gap-2">
|
|
2084
2292
|
<span className="truncate">{item.label}</span>
|
|
2085
2293
|
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
2086
|
-
{formatRelativeTime(item.timestamp, historyNow)}
|
|
2294
|
+
{formatRelativeTime(item.timestamp, historyNow, t)}
|
|
2087
2295
|
</span>
|
|
2088
2296
|
</div>
|
|
2089
2297
|
</button>
|
|
@@ -2112,9 +2320,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2112
2320
|
<path d="m20 20-3.5-3.5" />
|
|
2113
2321
|
</svg>
|
|
2114
2322
|
</EmptyIcon>
|
|
2115
|
-
<EmptyTitle className="text-xs"
|
|
2323
|
+
<EmptyTitle className="text-xs">{t("panel.history.noMatchTitle")}</EmptyTitle>
|
|
2116
2324
|
<EmptyDescription className="text-[11px]">
|
|
2117
|
-
|
|
2325
|
+
{t("panel.history.noMatchDesc")}
|
|
2118
2326
|
</EmptyDescription>
|
|
2119
2327
|
</Empty>
|
|
2120
2328
|
) : null}
|
|
@@ -2124,7 +2332,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2124
2332
|
</div>
|
|
2125
2333
|
</TooltipTrigger>
|
|
2126
2334
|
<TooltipContent className="z-[10000]">
|
|
2127
|
-
{isHistoryPanelExpanded ? "
|
|
2335
|
+
{isHistoryPanelExpanded ? t("panel.history.collapse") : t("panel.history.expand")}
|
|
2128
2336
|
</TooltipContent>
|
|
2129
2337
|
</Tooltip>
|
|
2130
2338
|
</TooltipProvider>
|
|
@@ -2134,7 +2342,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2134
2342
|
type="button"
|
|
2135
2343
|
disabled={!hasUnlockedSelection}
|
|
2136
2344
|
className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
|
|
2137
|
-
aria-label="
|
|
2345
|
+
aria-label={t("panel.menubar.nodeZOrderAria")}
|
|
2138
2346
|
>
|
|
2139
2347
|
<IconLayers />
|
|
2140
2348
|
</button>
|
|
@@ -2145,28 +2353,28 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2145
2353
|
onClick={() => bringElementsForward(selectedIds)}
|
|
2146
2354
|
>
|
|
2147
2355
|
<span className="mr-2 inline-flex"><IconForward /></span>
|
|
2148
|
-
|
|
2356
|
+
{t("panel.menubar.bringForward")}
|
|
2149
2357
|
</DropdownMenuItem>
|
|
2150
2358
|
<DropdownMenuItem
|
|
2151
2359
|
disabled={!hasUnlockedSelection}
|
|
2152
2360
|
onClick={() => sendElementsBackward(selectedIds)}
|
|
2153
2361
|
>
|
|
2154
2362
|
<span className="mr-2 inline-flex"><IconBackward /></span>
|
|
2155
|
-
|
|
2363
|
+
{t("panel.menubar.sendBackward")}
|
|
2156
2364
|
</DropdownMenuItem>
|
|
2157
2365
|
<DropdownMenuItem
|
|
2158
2366
|
disabled={!hasUnlockedSelection}
|
|
2159
2367
|
onClick={() => bringElementsToFront(selectedIds)}
|
|
2160
2368
|
>
|
|
2161
2369
|
<span className="mr-2 inline-flex"><IconBringFront /></span>
|
|
2162
|
-
|
|
2370
|
+
{t("panel.menubar.bringToFront")}
|
|
2163
2371
|
</DropdownMenuItem>
|
|
2164
2372
|
<DropdownMenuItem
|
|
2165
2373
|
disabled={!hasUnlockedSelection}
|
|
2166
2374
|
onClick={() => sendElementsToBack(selectedIds)}
|
|
2167
2375
|
>
|
|
2168
2376
|
<span className="mr-2 inline-flex"><IconSendBack /></span>
|
|
2169
|
-
|
|
2377
|
+
{t("panel.menubar.sendToBack")}
|
|
2170
2378
|
</DropdownMenuItem>
|
|
2171
2379
|
</DropdownMenuContent>
|
|
2172
2380
|
</DropdownMenu>
|
|
@@ -2175,11 +2383,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2175
2383
|
data-blueprint-toggle
|
|
2176
2384
|
className="flex shrink-0 items-center gap-2 rounded-md border border-border bg-muted/30 px-2 py-1"
|
|
2177
2385
|
>
|
|
2178
|
-
<span className="text-[11px] text-muted-foreground"
|
|
2386
|
+
<span className="text-[11px] text-muted-foreground">{t("panel.menubar.blueprint")}</span>
|
|
2179
2387
|
<Switch
|
|
2180
2388
|
checked={blueprintOpen}
|
|
2181
2389
|
onCheckedChange={setBlueprintOpen}
|
|
2182
|
-
aria-label="
|
|
2390
|
+
aria-label={t("panel.menubar.showBlueprintPanel")}
|
|
2183
2391
|
/>
|
|
2184
2392
|
</div>
|
|
2185
2393
|
<TooltipProvider delayDuration={150}>
|
|
@@ -2189,7 +2397,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2189
2397
|
<Switch
|
|
2190
2398
|
checked={isDark}
|
|
2191
2399
|
onCheckedChange={applyTheme}
|
|
2192
|
-
aria-label="
|
|
2400
|
+
aria-label={t("panel.menubar.toggleTheme")}
|
|
2193
2401
|
className="data-[state=checked]:bg-primary/80 data-[state=unchecked]:bg-secondary"
|
|
2194
2402
|
/>
|
|
2195
2403
|
<div
|
|
@@ -2223,7 +2431,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2223
2431
|
</div>
|
|
2224
2432
|
</div>
|
|
2225
2433
|
</TooltipTrigger>
|
|
2226
|
-
<TooltipContent className="z-[10000]"
|
|
2434
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.toggleThemeHint")}</TooltipContent>
|
|
2227
2435
|
</Tooltip>
|
|
2228
2436
|
<Tooltip>
|
|
2229
2437
|
<TooltipTrigger asChild>
|
|
@@ -2235,7 +2443,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2235
2443
|
-
|
|
2236
2444
|
</button>
|
|
2237
2445
|
</TooltipTrigger>
|
|
2238
|
-
<TooltipContent className="z-[10000]"
|
|
2446
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.zoomOutCanvas")}</TooltipContent>
|
|
2239
2447
|
</Tooltip>
|
|
2240
2448
|
</TooltipProvider>
|
|
2241
2449
|
<span className="w-20 text-center text-xs">
|
|
@@ -2254,7 +2462,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2254
2462
|
+
|
|
2255
2463
|
</button>
|
|
2256
2464
|
</TooltipTrigger>
|
|
2257
|
-
<TooltipContent className="z-[10000]"
|
|
2465
|
+
<TooltipContent className="z-[10000]">{t("panel.menubar.zoomInCanvas")}</TooltipContent>
|
|
2258
2466
|
</Tooltip>
|
|
2259
2467
|
</TooltipProvider>
|
|
2260
2468
|
</div>
|
|
@@ -2270,7 +2478,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2270
2478
|
try {
|
|
2271
2479
|
await handleImportFile(file);
|
|
2272
2480
|
} catch {
|
|
2273
|
-
window.alert(
|
|
2481
|
+
window.alert(messages.importJsonParseError);
|
|
2274
2482
|
}
|
|
2275
2483
|
}}
|
|
2276
2484
|
/>
|
|
@@ -2286,7 +2494,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2286
2494
|
try {
|
|
2287
2495
|
await handleBlueprintImportFile(file);
|
|
2288
2496
|
} catch {
|
|
2289
|
-
window.alert(
|
|
2497
|
+
window.alert(messages.blueprintImportInvalid);
|
|
2290
2498
|
}
|
|
2291
2499
|
}}
|
|
2292
2500
|
/>
|
|
@@ -2303,7 +2511,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2303
2511
|
const normalizedDataUrl = await normalizeTitleIconFile(file);
|
|
2304
2512
|
setTitleIconDataUrl(normalizedDataUrl);
|
|
2305
2513
|
} catch {
|
|
2306
|
-
window.alert(
|
|
2514
|
+
window.alert(messages.iconReadFailed);
|
|
2307
2515
|
}
|
|
2308
2516
|
}}
|
|
2309
2517
|
/>
|
|
@@ -2369,6 +2577,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2369
2577
|
const nodeIdFromElement = nodeEl?.dataset.elementId ?? null;
|
|
2370
2578
|
const onMoveable =
|
|
2371
2579
|
!!target?.closest(".moveable-control-box") ||
|
|
2580
|
+
!!target?.closest(".moveable-group") ||
|
|
2372
2581
|
!!target?.closest(".moveable-line") ||
|
|
2373
2582
|
!!target?.closest(".moveable-control") ||
|
|
2374
2583
|
!!target?.closest(".moveable-direction");
|
|
@@ -2394,8 +2603,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2394
2603
|
ref={syncScrollRef}
|
|
2395
2604
|
zoom={zoom}
|
|
2396
2605
|
onZoomChange={setZoom}
|
|
2397
|
-
onScrollChange={
|
|
2606
|
+
onScrollChange={onViewportScrollChange}
|
|
2398
2607
|
canvasRef={syncCanvasRef}
|
|
2608
|
+
viewportSyncRef={viewportSyncRef}
|
|
2399
2609
|
viewportOverlay={
|
|
2400
2610
|
<MoveableLayer
|
|
2401
2611
|
zoomX={zoom.x}
|
|
@@ -2405,8 +2615,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2405
2615
|
selectedTargets={selectedTargets}
|
|
2406
2616
|
elementsById={byId}
|
|
2407
2617
|
updateElement={updateElement}
|
|
2408
|
-
refreshToken={
|
|
2409
|
-
|
|
2618
|
+
refreshToken={historyCursor}
|
|
2619
|
+
viewportSyncRef={viewportSyncRef}
|
|
2410
2620
|
/>
|
|
2411
2621
|
}
|
|
2412
2622
|
onCanvasMouseDownCapture={(e) => {
|
|
@@ -2417,7 +2627,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2417
2627
|
}}
|
|
2418
2628
|
onDropMaterial={({ materialId, x, y }) => {
|
|
2419
2629
|
if (activeLayer?.locked) {
|
|
2420
|
-
showActionHint(
|
|
2630
|
+
showActionHint(messages.activeLayerLockedCannotAdd);
|
|
2421
2631
|
return;
|
|
2422
2632
|
}
|
|
2423
2633
|
addElementFromMaterial(materialId, x, y);
|
|
@@ -2431,6 +2641,9 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2431
2641
|
onSelectIds={selectViewElements}
|
|
2432
2642
|
updateElement={updateElement}
|
|
2433
2643
|
layerLocked={Boolean(activeLayer?.locked)}
|
|
2644
|
+
onTableCellAction={(payload) => {
|
|
2645
|
+
void triggerBlueprintNode(payload.blueprintNodeId, payload);
|
|
2646
|
+
}}
|
|
2434
2647
|
/>
|
|
2435
2648
|
|
|
2436
2649
|
<SelectLayer
|
|
@@ -2467,13 +2680,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2467
2680
|
if (!node) return;
|
|
2468
2681
|
const layer = layerById.get(node.layerId);
|
|
2469
2682
|
if (layer?.locked) {
|
|
2470
|
-
showActionHint(
|
|
2683
|
+
showActionHint(messages.nodeLayerLockedCannotCopy);
|
|
2471
2684
|
return;
|
|
2472
2685
|
}
|
|
2473
2686
|
setCopiedNodeId(contextMenuNodeId);
|
|
2474
2687
|
}}
|
|
2475
2688
|
>
|
|
2476
|
-
|
|
2689
|
+
{t("panel.menubar.duplicateNode")}
|
|
2477
2690
|
</DropdownMenuItem>
|
|
2478
2691
|
{contextMenuNodeId &&
|
|
2479
2692
|
byId.get(contextMenuNodeId)?.mappingSourceNodeId ? (
|
|
@@ -2485,7 +2698,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2485
2698
|
if (!sourceNodeId) return;
|
|
2486
2699
|
const sourceNode = byId.get(sourceNodeId);
|
|
2487
2700
|
if (!sourceNode) {
|
|
2488
|
-
showActionHint(
|
|
2701
|
+
showActionHint(messages.sourceNodeNotFound);
|
|
2489
2702
|
return;
|
|
2490
2703
|
}
|
|
2491
2704
|
if (activeLayerId !== sourceNode.layerId) {
|
|
@@ -2495,7 +2708,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2495
2708
|
setContextMenuNodeId(null);
|
|
2496
2709
|
}}
|
|
2497
2710
|
>
|
|
2498
|
-
|
|
2711
|
+
{t("panel.menubar.locateSourceNode")}
|
|
2499
2712
|
</DropdownMenuItem>
|
|
2500
2713
|
) : null}
|
|
2501
2714
|
<DropdownMenuItem
|
|
@@ -2512,7 +2725,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2512
2725
|
setContextMenuNodeId(null);
|
|
2513
2726
|
}}
|
|
2514
2727
|
>
|
|
2515
|
-
|
|
2728
|
+
{t("panel.menubar.openInMappingLayer")}
|
|
2516
2729
|
</DropdownMenuItem>
|
|
2517
2730
|
<DropdownMenuItem
|
|
2518
2731
|
onClick={() => {
|
|
@@ -2543,7 +2756,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2543
2756
|
return !!layerById.get(el.layerId)?.locked;
|
|
2544
2757
|
});
|
|
2545
2758
|
if (hasLockedLayerNode) {
|
|
2546
|
-
showActionHint(
|
|
2759
|
+
showActionHint(messages.selectionContainsLockedLayerNode);
|
|
2547
2760
|
return;
|
|
2548
2761
|
}
|
|
2549
2762
|
deleteElements(selectedIds);
|
|
@@ -2567,8 +2780,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2567
2780
|
}}
|
|
2568
2781
|
>
|
|
2569
2782
|
{contextMenuNodeId && selectedIds.includes(contextMenuNodeId) && selectedIds.length > 1
|
|
2570
|
-
?
|
|
2571
|
-
: "
|
|
2783
|
+
? t("panel.menubar.deleteSelectedNodes", { count: selectedIds.length })
|
|
2784
|
+
: t("panel.menubar.deleteNode")}
|
|
2572
2785
|
</DropdownMenuItem>
|
|
2573
2786
|
</>
|
|
2574
2787
|
) : (
|
|
@@ -2579,7 +2792,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2579
2792
|
if (!copiedNodeId) return;
|
|
2580
2793
|
const sourceNode = byId.get(copiedNodeId);
|
|
2581
2794
|
if (sourceNode && layerById.get(sourceNode.layerId)?.locked) {
|
|
2582
|
-
showActionHint(
|
|
2795
|
+
showActionHint(messages.sourceLayerLockedCannotPaste);
|
|
2583
2796
|
return;
|
|
2584
2797
|
}
|
|
2585
2798
|
const canvasEl = canvasRef.current;
|
|
@@ -2599,10 +2812,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2599
2812
|
const centerX = rect.left + rect.width / 2;
|
|
2600
2813
|
const centerY = rect.top + rect.height / 2;
|
|
2601
2814
|
targetX =
|
|
2602
|
-
(centerX - rect.left +
|
|
2815
|
+
(centerX - rect.left + scrollPosRef.current.left) /
|
|
2603
2816
|
Math.max(0.0001, zoom.x);
|
|
2604
2817
|
targetY =
|
|
2605
|
-
(centerY - rect.top +
|
|
2818
|
+
(centerY - rect.top + scrollPosRef.current.top) /
|
|
2606
2819
|
Math.max(0.0001, zoom.y);
|
|
2607
2820
|
}
|
|
2608
2821
|
duplicateElement(
|
|
@@ -2613,7 +2826,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2613
2826
|
);
|
|
2614
2827
|
}}
|
|
2615
2828
|
>
|
|
2616
|
-
|
|
2829
|
+
{t("panel.menubar.pasteNode")}
|
|
2617
2830
|
</DropdownMenuItem>
|
|
2618
2831
|
<DropdownMenuItem
|
|
2619
2832
|
disabled={elements.length === 0}
|
|
@@ -2621,7 +2834,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2621
2834
|
selectViewElements(elements.map((el) => el.id));
|
|
2622
2835
|
}}
|
|
2623
2836
|
>
|
|
2624
|
-
|
|
2837
|
+
{t("panel.menubar.selectAllInLayer")}
|
|
2625
2838
|
</DropdownMenuItem>
|
|
2626
2839
|
</>
|
|
2627
2840
|
)}
|
|
@@ -2635,20 +2848,20 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2635
2848
|
<TooltipProvider delayDuration={120}>
|
|
2636
2849
|
<Tabs value={activeLayerId} onValueChange={setActiveLayer}>
|
|
2637
2850
|
<div className="mb-1 flex items-center gap-2">
|
|
2638
|
-
<div className="text-[11px] font-semibold text-muted-foreground"
|
|
2851
|
+
<div className="text-[11px] font-semibold text-muted-foreground">{t("panel.layers.title")}</div>
|
|
2639
2852
|
<Tooltip>
|
|
2640
2853
|
<TooltipTrigger asChild>
|
|
2641
2854
|
<button
|
|
2642
2855
|
type="button"
|
|
2643
2856
|
onClick={() => setIsLayerPanelExpanded((v) => !v)}
|
|
2644
|
-
aria-label={isLayerPanelExpanded ? "
|
|
2857
|
+
aria-label={isLayerPanelExpanded ? t("panel.layers.collapseInputAria") : t("panel.layers.expandInputAria")}
|
|
2645
2858
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
2646
2859
|
>
|
|
2647
2860
|
<IconChevron expanded={isLayerPanelExpanded} />
|
|
2648
2861
|
</button>
|
|
2649
2862
|
</TooltipTrigger>
|
|
2650
2863
|
<TooltipContent className="z-[10000]">
|
|
2651
|
-
{isLayerPanelExpanded ? "
|
|
2864
|
+
{isLayerPanelExpanded ? t("panel.layers.collapseInput") : t("panel.layers.expandInput")}
|
|
2652
2865
|
</TooltipContent>
|
|
2653
2866
|
</Tooltip>
|
|
2654
2867
|
<Tooltip>
|
|
@@ -2656,13 +2869,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2656
2869
|
<button
|
|
2657
2870
|
type="button"
|
|
2658
2871
|
onClick={addLayer}
|
|
2659
|
-
aria-label="
|
|
2872
|
+
aria-label={t("panel.layers.add")}
|
|
2660
2873
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
2661
2874
|
>
|
|
2662
2875
|
<IconPlus />
|
|
2663
2876
|
</button>
|
|
2664
2877
|
</TooltipTrigger>
|
|
2665
|
-
<TooltipContent className="z-[10000]"
|
|
2878
|
+
<TooltipContent className="z-[10000]">{t("panel.layers.add")}</TooltipContent>
|
|
2666
2879
|
</Tooltip>
|
|
2667
2880
|
<Tooltip>
|
|
2668
2881
|
<TooltipTrigger asChild>
|
|
@@ -2672,7 +2885,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2672
2885
|
if (!canMergeLayers) return;
|
|
2673
2886
|
setIsMergingLayers(true);
|
|
2674
2887
|
}}
|
|
2675
|
-
aria-label="
|
|
2888
|
+
aria-label={t("panel.layers.merge")}
|
|
2676
2889
|
disabled={!canMergeLayers}
|
|
2677
2890
|
className="rounded border border-border p-1 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-40"
|
|
2678
2891
|
>
|
|
@@ -2680,7 +2893,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2680
2893
|
</button>
|
|
2681
2894
|
</TooltipTrigger>
|
|
2682
2895
|
<TooltipContent className="z-[10000]">
|
|
2683
|
-
{canMergeLayers ? "
|
|
2896
|
+
{canMergeLayers ? t("panel.layers.merge") : t("panel.layers.mergeNeedTwo")}
|
|
2684
2897
|
</TooltipContent>
|
|
2685
2898
|
</Tooltip>
|
|
2686
2899
|
</div>
|
|
@@ -2697,12 +2910,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2697
2910
|
<span className="truncate">{layer.name}</span>
|
|
2698
2911
|
{layer.isMapping ? (
|
|
2699
2912
|
<span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
|
|
2700
|
-
|
|
2913
|
+
{t("panel.layers.mapping")}
|
|
2701
2914
|
</span>
|
|
2702
2915
|
) : null}
|
|
2703
2916
|
{layer.isPrimary ? (
|
|
2704
2917
|
<span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
|
|
2705
|
-
|
|
2918
|
+
{t("panel.layers.primary")}
|
|
2706
2919
|
</span>
|
|
2707
2920
|
) : null}
|
|
2708
2921
|
{layer.locked ? <IconLock locked /> : null}
|
|
@@ -2732,7 +2945,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2732
2945
|
}
|
|
2733
2946
|
}}
|
|
2734
2947
|
className="h-7 min-w-0 flex-1"
|
|
2735
|
-
placeholder="
|
|
2948
|
+
placeholder={t("panel.layers.namePlaceholder")}
|
|
2736
2949
|
autoFocus
|
|
2737
2950
|
/>
|
|
2738
2951
|
) : (
|
|
@@ -2740,12 +2953,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2740
2953
|
)}
|
|
2741
2954
|
{activeLayer.isMapping ? (
|
|
2742
2955
|
<span className="rounded border border-primary/40 bg-primary/10 px-1 text-[10px] text-primary">
|
|
2743
|
-
|
|
2956
|
+
{t("panel.layers.mappingLayer")}
|
|
2744
2957
|
</span>
|
|
2745
2958
|
) : null}
|
|
2746
2959
|
{activeLayer.isPrimary ? (
|
|
2747
2960
|
<span className="rounded border border-emerald-500/40 bg-emerald-500/10 px-1 text-[10px] text-emerald-600">
|
|
2748
|
-
|
|
2961
|
+
{t("panel.layers.primaryLayer")}
|
|
2749
2962
|
</span>
|
|
2750
2963
|
) : null}
|
|
2751
2964
|
<div className="flex-1" />
|
|
@@ -2755,7 +2968,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2755
2968
|
type="button"
|
|
2756
2969
|
onClick={() => setPrimaryLayer(activeLayer.id)}
|
|
2757
2970
|
disabled={activeLayer.isPrimary}
|
|
2758
|
-
aria-label={activeLayer.isPrimary ? "
|
|
2971
|
+
aria-label={activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
|
|
2759
2972
|
className={[
|
|
2760
2973
|
"rounded border px-2 py-1 text-[11px] disabled:cursor-not-allowed disabled:opacity-60",
|
|
2761
2974
|
activeLayer.isPrimary
|
|
@@ -2763,11 +2976,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2763
2976
|
: "border-border hover:bg-accent",
|
|
2764
2977
|
].join(" ")}
|
|
2765
2978
|
>
|
|
2766
|
-
{activeLayer.isPrimary ? "
|
|
2979
|
+
{activeLayer.isPrimary ? t("panel.layers.isPrimary") : t("panel.layers.setPrimary")}
|
|
2767
2980
|
</button>
|
|
2768
2981
|
</TooltipTrigger>
|
|
2769
2982
|
<TooltipContent className="z-[10000]">
|
|
2770
|
-
{activeLayer.isPrimary ? "
|
|
2983
|
+
{activeLayer.isPrimary ? t("panel.layers.isPrimaryHint") : t("panel.layers.setPrimaryHint")}
|
|
2771
2984
|
</TooltipContent>
|
|
2772
2985
|
</Tooltip>
|
|
2773
2986
|
<Tooltip>
|
|
@@ -2776,14 +2989,14 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2776
2989
|
type="button"
|
|
2777
2990
|
onClick={() => toggleLayerLock(activeLayer.id)}
|
|
2778
2991
|
disabled={!activeLayer.editable}
|
|
2779
|
-
aria-label={activeLayer.locked ? "
|
|
2992
|
+
aria-label={activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
|
|
2780
2993
|
className="rounded border border-border p-1 disabled:opacity-40"
|
|
2781
2994
|
>
|
|
2782
2995
|
<IconLock locked={activeLayer.locked} />
|
|
2783
2996
|
</button>
|
|
2784
2997
|
</TooltipTrigger>
|
|
2785
2998
|
<TooltipContent className="z-[10000]">
|
|
2786
|
-
{activeLayer.locked ? "
|
|
2999
|
+
{activeLayer.locked ? t("panel.layers.unlock") : t("panel.layers.lock")}
|
|
2787
3000
|
</TooltipContent>
|
|
2788
3001
|
</Tooltip>
|
|
2789
3002
|
<Tooltip>
|
|
@@ -2796,7 +3009,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2796
3009
|
setEditingLayerId(null);
|
|
2797
3010
|
setEditingLayerName("");
|
|
2798
3011
|
}}
|
|
2799
|
-
aria-label="
|
|
3012
|
+
aria-label={t("panel.layers.saveName")}
|
|
2800
3013
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
2801
3014
|
>
|
|
2802
3015
|
<IconCheck />
|
|
@@ -2809,7 +3022,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2809
3022
|
setEditingLayerName(activeLayer.name);
|
|
2810
3023
|
}}
|
|
2811
3024
|
disabled={!activeLayer.editable}
|
|
2812
|
-
aria-label="
|
|
3025
|
+
aria-label={t("panel.layers.rename")}
|
|
2813
3026
|
className="rounded border border-border p-1 disabled:opacity-40"
|
|
2814
3027
|
>
|
|
2815
3028
|
<IconEdit />
|
|
@@ -2817,7 +3030,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2817
3030
|
)}
|
|
2818
3031
|
</TooltipTrigger>
|
|
2819
3032
|
<TooltipContent className="z-[10000]">
|
|
2820
|
-
{editingLayerId === activeLayer.id ? "
|
|
3033
|
+
{editingLayerId === activeLayer.id ? t("panel.layers.saveName") : t("panel.layers.rename")}
|
|
2821
3034
|
</TooltipContent>
|
|
2822
3035
|
</Tooltip>
|
|
2823
3036
|
{editingLayerId === activeLayer.id ? (
|
|
@@ -2829,13 +3042,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2829
3042
|
setEditingLayerId(null);
|
|
2830
3043
|
setEditingLayerName("");
|
|
2831
3044
|
}}
|
|
2832
|
-
aria-label="
|
|
3045
|
+
aria-label={t("panel.layers.cancelEditName")}
|
|
2833
3046
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
2834
3047
|
>
|
|
2835
3048
|
<IconClose />
|
|
2836
3049
|
</button>
|
|
2837
3050
|
</TooltipTrigger>
|
|
2838
|
-
<TooltipContent className="z-[10000]"
|
|
3051
|
+
<TooltipContent className="z-[10000]">{t("panel.layers.cancelEdit")}</TooltipContent>
|
|
2839
3052
|
</Tooltip>
|
|
2840
3053
|
) : null}
|
|
2841
3054
|
<Tooltip>
|
|
@@ -2856,10 +3069,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2856
3069
|
disabled={!activeLayer.editable || activeLayer.locked}
|
|
2857
3070
|
aria-label={
|
|
2858
3071
|
!activeLayer.editable
|
|
2859
|
-
? "
|
|
3072
|
+
? t("panel.layers.cannotDeleteDefault")
|
|
2860
3073
|
: activeLayer.locked
|
|
2861
|
-
? "
|
|
2862
|
-
: "
|
|
3074
|
+
? t("panel.layers.cannotDeleteLocked")
|
|
3075
|
+
: t("panel.layers.delete")
|
|
2863
3076
|
}
|
|
2864
3077
|
className="rounded border border-border p-1 disabled:opacity-40"
|
|
2865
3078
|
>
|
|
@@ -2868,10 +3081,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2868
3081
|
</TooltipTrigger>
|
|
2869
3082
|
<TooltipContent className="z-[10000]">
|
|
2870
3083
|
{!activeLayer.editable
|
|
2871
|
-
? "
|
|
3084
|
+
? t("panel.layers.cannotDeleteDefault")
|
|
2872
3085
|
: activeLayer.locked
|
|
2873
|
-
? "
|
|
2874
|
-
: "
|
|
3086
|
+
? t("panel.layers.cannotDeleteLocked")
|
|
3087
|
+
: t("panel.layers.delete")}
|
|
2875
3088
|
</TooltipContent>
|
|
2876
3089
|
</Tooltip>
|
|
2877
3090
|
</div>
|
|
@@ -2879,7 +3092,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2879
3092
|
|
|
2880
3093
|
{isLayerPanelExpanded ? (
|
|
2881
3094
|
<div className="rounded border border-border bg-card px-2 py-1.5 text-xs">
|
|
2882
|
-
<div className="mb-1 text-muted-foreground"
|
|
3095
|
+
<div className="mb-1 text-muted-foreground">{t("panel.layers.selectMergeLayers")}</div>
|
|
2883
3096
|
<div className="flex flex-wrap gap-2">
|
|
2884
3097
|
{layers.map((layer) => (
|
|
2885
3098
|
<label key={layer.id} className="flex items-center gap-1.5">
|
|
@@ -2891,10 +3104,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2891
3104
|
/>
|
|
2892
3105
|
<span
|
|
2893
3106
|
className="max-w-[140px] truncate"
|
|
2894
|
-
title={layer.isMapping ? "
|
|
3107
|
+
title={layer.isMapping ? t("panel.layers.mappingCannotMerge") : layer.name}
|
|
2895
3108
|
>
|
|
2896
3109
|
{layer.name}
|
|
2897
|
-
{layer.isMapping ? "
|
|
3110
|
+
{layer.isMapping ? t("panel.layers.cannotMergeSuffix") : ""}
|
|
2898
3111
|
</span>
|
|
2899
3112
|
</label>
|
|
2900
3113
|
))}
|
|
@@ -2906,12 +3119,12 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2906
3119
|
|
|
2907
3120
|
{isLayerPanelExpanded && isMergingLayers ? (
|
|
2908
3121
|
<div className="mt-2 flex items-center gap-2 rounded border border-border bg-card px-2 py-1.5 text-xs">
|
|
2909
|
-
<span className="text-muted-foreground"
|
|
3122
|
+
<span className="text-muted-foreground">{t("panel.layers.mergedName")}</span>
|
|
2910
3123
|
<Input
|
|
2911
3124
|
value={mergeLayerName}
|
|
2912
3125
|
onChange={(e) => setMergeLayerName(e.target.value)}
|
|
2913
3126
|
className="h-7 min-w-0 flex-1"
|
|
2914
|
-
placeholder="
|
|
3127
|
+
placeholder={t("panel.layers.mergedNamePlaceholder")}
|
|
2915
3128
|
autoFocus
|
|
2916
3129
|
/>
|
|
2917
3130
|
<Tooltip>
|
|
@@ -2924,14 +3137,14 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2924
3137
|
setIsMergingLayers(false);
|
|
2925
3138
|
setMergeLayerName("");
|
|
2926
3139
|
}}
|
|
2927
|
-
aria-label="
|
|
3140
|
+
aria-label={t("panel.layers.confirmMergeAria")}
|
|
2928
3141
|
disabled={!canMergeLayers}
|
|
2929
3142
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
2930
3143
|
>
|
|
2931
3144
|
<IconCheck />
|
|
2932
3145
|
</button>
|
|
2933
3146
|
</TooltipTrigger>
|
|
2934
|
-
<TooltipContent className="z-[10000]"
|
|
3147
|
+
<TooltipContent className="z-[10000]">{t("panel.layers.confirmMerge")}</TooltipContent>
|
|
2935
3148
|
</Tooltip>
|
|
2936
3149
|
<Tooltip>
|
|
2937
3150
|
<TooltipTrigger asChild>
|
|
@@ -2941,13 +3154,13 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2941
3154
|
setIsMergingLayers(false);
|
|
2942
3155
|
setMergeLayerName("");
|
|
2943
3156
|
}}
|
|
2944
|
-
aria-label="
|
|
3157
|
+
aria-label={t("panel.layers.cancelMergeAria")}
|
|
2945
3158
|
className="rounded border border-border p-1 hover:bg-accent"
|
|
2946
3159
|
>
|
|
2947
3160
|
<IconClose />
|
|
2948
3161
|
</button>
|
|
2949
3162
|
</TooltipTrigger>
|
|
2950
|
-
<TooltipContent className="z-[10000]"
|
|
3163
|
+
<TooltipContent className="z-[10000]">{t("panel.layers.cancelMerge")}</TooltipContent>
|
|
2951
3164
|
</Tooltip>
|
|
2952
3165
|
</div>
|
|
2953
3166
|
) : null}
|
|
@@ -2971,6 +3184,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
2971
3184
|
}
|
|
2972
3185
|
onUpdateBlueprintNode={handleUpdateBlueprintNode}
|
|
2973
3186
|
blueprintLibraryOptions={blueprintLibraryOptions}
|
|
3187
|
+
blueprintNodeOptions={blueprintGraph.document.nodes.map((node) => ({
|
|
3188
|
+
id: node.id,
|
|
3189
|
+
label: `${node.label || node.id}${node.nodeType ? ` (${node.nodeType})` : ""}`,
|
|
3190
|
+
}))}
|
|
2974
3191
|
selectedElement={selectedElement}
|
|
2975
3192
|
selectedElements={selectedElements}
|
|
2976
3193
|
viewElementScope={selectedElementScope}
|
|
@@ -3005,6 +3222,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3005
3222
|
</div>
|
|
3006
3223
|
</ResizablePanel>
|
|
3007
3224
|
</ResizablePanelGroup>
|
|
3225
|
+
<AssistantChatPanel
|
|
3226
|
+
deps={assistantDeps}
|
|
3227
|
+
open={assistantOpen}
|
|
3228
|
+
onOpenChange={setAssistantOpen}
|
|
3229
|
+
/>
|
|
3008
3230
|
<AlertDialog
|
|
3009
3231
|
open={mappingDeleteConfirmOpen}
|
|
3010
3232
|
onOpenChange={(open) => {
|
|
@@ -3014,13 +3236,15 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3014
3236
|
>
|
|
3015
3237
|
<AlertDialogContent overlayClassName="bg-transparent pointer-events-none">
|
|
3016
3238
|
<AlertDialogHeader>
|
|
3017
|
-
<AlertDialogTitle
|
|
3239
|
+
<AlertDialogTitle>{t("panel.layers.confirmDeleteTitle")}</AlertDialogTitle>
|
|
3018
3240
|
<AlertDialogDescription>
|
|
3019
|
-
|
|
3241
|
+
{t("panel.layers.confirmDeleteMappingBody", {
|
|
3242
|
+
count: mappingDeleteImpactCount,
|
|
3243
|
+
})}
|
|
3020
3244
|
</AlertDialogDescription>
|
|
3021
3245
|
</AlertDialogHeader>
|
|
3022
3246
|
<AlertDialogFooter>
|
|
3023
|
-
<AlertDialogCancel
|
|
3247
|
+
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
3024
3248
|
<AlertDialogAction
|
|
3025
3249
|
onClick={() => {
|
|
3026
3250
|
const fn = mappingDeleteProceedRef.current;
|
|
@@ -3029,7 +3253,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3029
3253
|
fn?.();
|
|
3030
3254
|
}}
|
|
3031
3255
|
>
|
|
3032
|
-
|
|
3256
|
+
{t("panel.layers.continueDelete")}
|
|
3033
3257
|
</AlertDialogAction>
|
|
3034
3258
|
</AlertDialogFooter>
|
|
3035
3259
|
</AlertDialogContent>
|
|
@@ -3048,9 +3272,11 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3048
3272
|
overlayClassName="bg-transparent pointer-events-none"
|
|
3049
3273
|
>
|
|
3050
3274
|
<DialogHeader>
|
|
3051
|
-
<DialogTitle
|
|
3275
|
+
<DialogTitle>{t("panel.layers.confirmDeleteLayerTitle")}</DialogTitle>
|
|
3052
3276
|
<DialogDescription>
|
|
3053
|
-
|
|
3277
|
+
{t("panel.layers.aboutToDeleteLayer", {
|
|
3278
|
+
name: deletingLayer?.name ?? "-",
|
|
3279
|
+
})}
|
|
3054
3280
|
</DialogDescription>
|
|
3055
3281
|
</DialogHeader>
|
|
3056
3282
|
{deletingLayer ? (
|
|
@@ -3063,7 +3289,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3063
3289
|
<div className="flex items-center gap-3">
|
|
3064
3290
|
<label className="flex items-center gap-1.5">
|
|
3065
3291
|
<RadioGroupItem value="move" />
|
|
3066
|
-
|
|
3292
|
+
{t("panel.layers.migrateNodesTo")}
|
|
3067
3293
|
</label>
|
|
3068
3294
|
<Select
|
|
3069
3295
|
value={deleteTargetLayerId || "__none__"}
|
|
@@ -3073,10 +3299,10 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3073
3299
|
disabled={deletingLayerMode !== "move" || deleteTargetCandidates.length === 0}
|
|
3074
3300
|
>
|
|
3075
3301
|
<SelectTrigger className="h-8 w-[220px]">
|
|
3076
|
-
<SelectValue placeholder="
|
|
3302
|
+
<SelectValue placeholder={t("panel.layers.selectTargetLayer")} />
|
|
3077
3303
|
</SelectTrigger>
|
|
3078
3304
|
<SelectContent>
|
|
3079
|
-
<SelectItem value="__none__"
|
|
3305
|
+
<SelectItem value="__none__">{t("panel.layers.pleaseSelectLayer")}</SelectItem>
|
|
3080
3306
|
{deleteTargetCandidates.map((l) => (
|
|
3081
3307
|
<SelectItem key={l.id} value={l.id}>
|
|
3082
3308
|
{l.name}
|
|
@@ -3088,7 +3314,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3088
3314
|
) : null}
|
|
3089
3315
|
<label className="flex items-center gap-1.5">
|
|
3090
3316
|
<RadioGroupItem value="remove" />
|
|
3091
|
-
{deletingLayer.isMapping ? "
|
|
3317
|
+
{deletingLayer.isMapping ? t("panel.layers.deleteMappingWithNodes") : t("panel.layers.deleteAllNodesToo")}
|
|
3092
3318
|
</label>
|
|
3093
3319
|
</RadioGroup>
|
|
3094
3320
|
) : null}
|
|
@@ -3102,7 +3328,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3102
3328
|
}}
|
|
3103
3329
|
className="rounded border border-border px-3 py-1.5 text-sm hover:bg-accent"
|
|
3104
3330
|
>
|
|
3105
|
-
|
|
3331
|
+
{t("common.cancel")}
|
|
3106
3332
|
</button>
|
|
3107
3333
|
<button
|
|
3108
3334
|
type="button"
|
|
@@ -3122,7 +3348,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3122
3348
|
}}
|
|
3123
3349
|
className="rounded bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
|
|
3124
3350
|
>
|
|
3125
|
-
|
|
3351
|
+
{t("panel.layers.confirmDeleteTitle")}
|
|
3126
3352
|
</button>
|
|
3127
3353
|
</DialogFooter>
|
|
3128
3354
|
</DialogContent>
|
|
@@ -3136,8 +3362,8 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3136
3362
|
overlayClassName="bg-transparent pointer-events-none"
|
|
3137
3363
|
>
|
|
3138
3364
|
<DialogHeader>
|
|
3139
|
-
<DialogTitle>
|
|
3140
|
-
<DialogDescription
|
|
3365
|
+
<DialogTitle>{t("panel.menubar.titleIconPreviewTitle")}</DialogTitle>
|
|
3366
|
+
<DialogDescription>{t("panel.menubar.titleIconPreviewDesc")}</DialogDescription>
|
|
3141
3367
|
</DialogHeader>
|
|
3142
3368
|
<div className="flex items-center justify-end gap-2">
|
|
3143
3369
|
<button
|
|
@@ -3162,7 +3388,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3162
3388
|
onClick={() => setTitleIconZoom(1.6)}
|
|
3163
3389
|
className="rounded border border-border px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
|
|
3164
3390
|
>
|
|
3165
|
-
|
|
3391
|
+
{t("common.reset")}
|
|
3166
3392
|
</button>
|
|
3167
3393
|
</div>
|
|
3168
3394
|
{titleIconDataUrl ? (
|
|
@@ -3179,7 +3405,7 @@ export function ReactViewPanel({ initialZoom = 1, className }: ReactViewPanelPro
|
|
|
3179
3405
|
>
|
|
3180
3406
|
<img
|
|
3181
3407
|
src={titleIconDataUrl}
|
|
3182
|
-
alt="
|
|
3408
|
+
alt={t("panel.menubar.titleIconPreviewAlt")}
|
|
3183
3409
|
className="mx-auto rounded border border-border/70 object-contain"
|
|
3184
3410
|
style={{
|
|
3185
3411
|
width: 200,
|