@arronqzy/react-view 1.0.13 → 1.0.15

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.
Files changed (29) hide show
  1. package/dist/styles.css +96 -4
  2. package/package.json +11 -10
  3. package/src/panel/ReactViewOnlinePreview.tsx +31 -2
  4. package/src/panel/ReactViewPanel.tsx +186 -124
  5. package/src/panel/ai/applyAssistantAction.ts +1 -0
  6. package/src/panel/components/BusyOverlay.tsx +25 -0
  7. package/src/panel/components/ElementsLayer.tsx +110 -68
  8. package/src/panel/components/MaterialSidebar.tsx +15 -0
  9. package/src/panel/components/MoveableLayer.tsx +8 -0
  10. package/src/panel/components/PanelCanvas.tsx +3 -0
  11. package/src/panel/components/PanelConfigSidebar.tsx +140 -91
  12. package/src/panel/components/SelectLayer.tsx +5 -0
  13. package/src/panel/components/WorkspaceConfigSidebar.tsx +1 -0
  14. package/src/panel/components/WorkspaceProjectNav.tsx +27 -4
  15. package/src/panel/components/config/PanelConfigScene3dSection.tsx +874 -0
  16. package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +1 -2
  17. package/src/panel/components/table/PanelConfigTableSection.tsx +82 -50
  18. package/src/panel/components/table/TableNodeContent.tsx +40 -15
  19. package/src/panel/hooks/usePanelElements.ts +13 -1
  20. package/src/panel/hooks/useWorkspaceProjects.ts +133 -66
  21. package/src/panel/library/workspace-project-cache.ts +8 -2
  22. package/src/panel/library/workspace-project-db.ts +29 -11
  23. package/src/panel/library/workspace-snapshot.ts +21 -4
  24. package/src/panel/scope/view-scope-store.ts +8 -0
  25. package/src/panel/types.ts +4 -3
  26. package/src/panel/utils/async-work.ts +178 -0
  27. package/src/panel/utils/gridPlacement.ts +29 -0
  28. package/src/panel/utils/panelElementDefaults.ts +37 -1
  29. package/src/panel/utils/scope-autocomplete.ts +0 -114
@@ -7,6 +7,7 @@ import { usePanelElements } from "./hooks/usePanelElements";
7
7
  import { useWorkspaceProjects } from "./hooks/useWorkspaceProjects";
8
8
  import type { WorkspaceProjectRecord } from "./library/workspace-project-db";
9
9
  import { WorkspaceProjectNav } from "./components/WorkspaceProjectNav";
10
+ import { BusyOverlay } from "./components/BusyOverlay";
10
11
  import { PanelCanvas } from "./components/PanelCanvas";
11
12
  import { PanelRulers } from "./components/PanelRulers";
12
13
  import { type ViewportZoom } from "./viewportZoom";
@@ -15,6 +16,12 @@ import { SelectLayer } from "./components/SelectLayer";
15
16
  import { MoveableLayer } from "./components/MoveableLayer";
16
17
  import { buildChartOption, CHART_TYPES } from "./utils/chartOptionBuilder";
17
18
  import { readOutputScale, writeOutputScale } from "./utils/outputScale";
19
+ import {
20
+ assertFileSize,
21
+ parseJsonText,
22
+ readFileAsDataUrl,
23
+ runBusyTask,
24
+ } from "./utils/async-work";
18
25
  import { MaterialSidebar } from "./components/MaterialSidebar";
19
26
  import { AssistantChatPanel } from "./ai/AssistantChatPanel";
20
27
  import {
@@ -45,13 +52,15 @@ import {
45
52
  useBlueprintNodeSelectionGuard,
46
53
  useBlueprintPageLifecycle,
47
54
  stopAllClockSchedules,
55
+ resolveRunnableNodeType,
56
+ resolveViewElementIds,
48
57
  type BlueprintDocument,
49
58
  type BlueprintGraphNode,
50
59
  type BlueprintLibraryListItem,
51
60
  type BlueprintMetaDraft,
52
61
  } from "@arronqzy/react-blueprint";
53
62
  import type { LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
54
- import { abortClockNode } from "@arronqzy/blueprint-dsl";
63
+ import { abortClockNode, collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE } from "@arronqzy/blueprint-dsl";
55
64
  import { WorkspaceStageSplit } from "./components/WorkspaceStageSplit";
56
65
  import {
57
66
  WorkspaceConfigSidebar,
@@ -736,6 +745,7 @@ export function ReactViewPanel({
736
745
  | "jsonConfig"
737
746
  | "logicConfig"
738
747
  | "clockConfig"
748
+ | "eventConfig"
739
749
  >
740
750
  >
741
751
  ) => {
@@ -987,18 +997,20 @@ export function ReactViewPanel({
987
997
  void refreshBlueprintLibrary();
988
998
  return;
989
999
  }
990
- setBlueprintGraph(BlueprintGraph.fromDocument(record.document));
991
- setBlueprintMeta({
992
- name: record.name,
993
- remark: record.remark ?? "",
1000
+ await runBusyTask(t("common.loadingBlueprint"), async () => {
1001
+ setBlueprintGraph(BlueprintGraph.fromDocument(record.document));
1002
+ setBlueprintMeta({
1003
+ name: record.name,
1004
+ remark: record.remark ?? "",
1005
+ });
1006
+ setActiveBlueprintLibraryId(record.id);
1007
+ setBlueprintSyncedDocument(record.document);
1008
+ setSelectedBlueprintNodeId(null);
1009
+ setConfigFocus("blueprint");
1010
+ setBlueprintOpen(true);
994
1011
  });
995
- setActiveBlueprintLibraryId(record.id);
996
- setBlueprintSyncedDocument(record.document);
997
- setSelectedBlueprintNodeId(null);
998
- setConfigFocus("blueprint");
999
- setBlueprintOpen(true);
1000
1012
  },
1001
- [refreshBlueprintLibrary]
1013
+ [refreshBlueprintLibrary, t]
1002
1014
  );
1003
1015
 
1004
1016
  const snapshotWorkspaceBlueprint = useCallback(() => {
@@ -1052,66 +1064,70 @@ export function ReactViewPanel({
1052
1064
 
1053
1065
  const syncBlueprintToLibrary = useCallback(async () => {
1054
1066
  if (!activeBlueprintLibraryId) return;
1067
+ await runBusyTask(t("common.syncingBlueprint"), async () => {
1068
+ const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1069
+ if (!existing) {
1070
+ toast({ title: t("panel.messages.blueprintNotFound") });
1071
+ void refreshBlueprintLibrary();
1072
+ setActiveBlueprintLibraryId(null);
1073
+ setBlueprintSyncedDocument(null);
1074
+ return;
1075
+ }
1055
1076
 
1056
- const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1057
- if (!existing) {
1058
- toast({ title: t("panel.messages.blueprintNotFound") });
1059
- void refreshBlueprintLibrary();
1060
- setActiveBlueprintLibraryId(null);
1061
- setBlueprintSyncedDocument(null);
1062
- return;
1063
- }
1064
-
1065
- const record = buildLibraryRecord({
1066
- id: existing.id,
1067
- createdAt: existing.createdAt,
1068
- document: blueprintGraph.document,
1069
- meta: blueprintMeta,
1070
- source: existing.source,
1077
+ const record = buildLibraryRecord({
1078
+ id: existing.id,
1079
+ createdAt: existing.createdAt,
1080
+ document: blueprintGraph.document,
1081
+ meta: blueprintMeta,
1082
+ source: existing.source,
1083
+ });
1084
+ await putBlueprintLibraryRecord(record);
1085
+ setBlueprintSyncedDocument(blueprintGraph.document);
1086
+ await refreshBlueprintLibrary();
1087
+ toast({ title: messages.blueprintSynced(record.name) });
1071
1088
  });
1072
- await putBlueprintLibraryRecord(record);
1073
- setBlueprintSyncedDocument(blueprintGraph.document);
1074
- await refreshBlueprintLibrary();
1075
- toast({ title: messages.blueprintSynced(record.name) });
1076
1089
  }, [
1077
1090
  activeBlueprintLibraryId,
1078
1091
  blueprintGraph.document,
1079
1092
  blueprintMeta,
1080
1093
  refreshBlueprintLibrary,
1094
+ t,
1081
1095
  ]);
1082
1096
 
1083
1097
  const saveBlueprintToLibrary = useCallback(
1084
1098
  async (meta: BlueprintMetaDraft) => {
1085
- setBlueprintMeta(meta);
1086
-
1087
- let recordId = activeBlueprintLibraryId ?? undefined;
1088
- let createdAt: number | undefined;
1089
- if (activeBlueprintLibraryId) {
1090
- const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1091
- if (existing?.source === "saved") {
1092
- recordId = existing.id;
1093
- createdAt = existing.createdAt;
1099
+ await runBusyTask(t("common.savingBlueprint"), async () => {
1100
+ setBlueprintMeta(meta);
1101
+
1102
+ let recordId = activeBlueprintLibraryId ?? undefined;
1103
+ let createdAt: number | undefined;
1104
+ if (activeBlueprintLibraryId) {
1105
+ const existing = await getBlueprintLibraryRecord(activeBlueprintLibraryId);
1106
+ if (existing?.source === "saved") {
1107
+ recordId = existing.id;
1108
+ createdAt = existing.createdAt;
1109
+ } else {
1110
+ recordId = createLibraryBlueprintId();
1111
+ }
1094
1112
  } else {
1095
1113
  recordId = createLibraryBlueprintId();
1096
1114
  }
1097
- } else {
1098
- recordId = createLibraryBlueprintId();
1099
- }
1100
1115
 
1101
- const record = buildLibraryRecord({
1102
- id: recordId,
1103
- createdAt,
1104
- document: blueprintGraph.document,
1105
- meta,
1106
- source: "saved",
1116
+ const record = buildLibraryRecord({
1117
+ id: recordId,
1118
+ createdAt,
1119
+ document: blueprintGraph.document,
1120
+ meta,
1121
+ source: "saved",
1122
+ });
1123
+ await putBlueprintLibraryRecord(record);
1124
+ setActiveBlueprintLibraryId(record.id);
1125
+ setBlueprintSyncedDocument(blueprintGraph.document);
1126
+ await refreshBlueprintLibrary();
1127
+ toast({ title: t("panel.messages.blueprintSavedLocal") });
1107
1128
  });
1108
- await putBlueprintLibraryRecord(record);
1109
- setActiveBlueprintLibraryId(record.id);
1110
- setBlueprintSyncedDocument(blueprintGraph.document);
1111
- await refreshBlueprintLibrary();
1112
- toast({ title: t("panel.messages.blueprintSavedLocal") });
1113
1129
  },
1114
- [activeBlueprintLibraryId, blueprintGraph.document, refreshBlueprintLibrary]
1130
+ [activeBlueprintLibraryId, blueprintGraph.document, refreshBlueprintLibrary, t]
1115
1131
  );
1116
1132
 
1117
1133
  const openBlueprintMetaDialog = useCallback((mode: "export" | "save") => {
@@ -1122,16 +1138,18 @@ export function ReactViewPanel({
1122
1138
  const handleBlueprintMetaConfirm = useCallback(
1123
1139
  async (meta: BlueprintMetaDraft) => {
1124
1140
  if (blueprintMetaDialogMode === "export") {
1125
- setBlueprintMeta(meta);
1126
- downloadBlueprintExport(
1127
- buildBlueprintExportPayload(blueprintGraph.document, meta)
1128
- );
1129
- toast({ title: t("panel.messages.blueprintExported") });
1141
+ await runBusyTask(t("common.exportingPanel"), async () => {
1142
+ setBlueprintMeta(meta);
1143
+ downloadBlueprintExport(
1144
+ buildBlueprintExportPayload(blueprintGraph.document, meta)
1145
+ );
1146
+ toast({ title: t("panel.messages.blueprintExported") });
1147
+ });
1130
1148
  return;
1131
1149
  }
1132
1150
  await saveBlueprintToLibrary(meta);
1133
1151
  },
1134
- [blueprintGraph.document, blueprintMetaDialogMode, saveBlueprintToLibrary]
1152
+ [blueprintGraph.document, blueprintMetaDialogMode, saveBlueprintToLibrary, t]
1135
1153
  );
1136
1154
 
1137
1155
  const handleRenameBlueprintLibraryItem = useCallback(
@@ -1170,22 +1188,32 @@ export function ReactViewPanel({
1170
1188
 
1171
1189
  const handleBlueprintImportFile = useCallback(
1172
1190
  async (file: File) => {
1173
- const text = await file.text();
1174
- const payload = parseBlueprintImportFile(JSON.parse(text) as unknown);
1175
- const record = libraryRecordFromImport(payload);
1176
- await putBlueprintLibraryRecord(record);
1177
- await refreshBlueprintLibrary();
1178
- if (!activeBlueprintLibraryId) {
1179
- snapshotWorkspaceBlueprint();
1191
+ try {
1192
+ await runBusyTask(t("common.importingBlueprint"), async () => {
1193
+ assertFileSize(file, "json");
1194
+ const text = await file.text();
1195
+ const payload = parseBlueprintImportFile(await parseJsonText(text));
1196
+ const record = libraryRecordFromImport(payload);
1197
+ await putBlueprintLibraryRecord(record);
1198
+ await refreshBlueprintLibrary();
1199
+ if (!activeBlueprintLibraryId) {
1200
+ snapshotWorkspaceBlueprint();
1201
+ }
1202
+ await loadBlueprintFromLibrary(record.id);
1203
+ toast({ title: t("panel.messages.blueprintImported") });
1204
+ });
1205
+ } catch (error) {
1206
+ toast({
1207
+ title: error instanceof Error ? error.message : t("panel.messages.importInvalidFormat"),
1208
+ });
1180
1209
  }
1181
- await loadBlueprintFromLibrary(record.id);
1182
- toast({ title: t("panel.messages.blueprintImported") });
1183
1210
  },
1184
1211
  [
1185
1212
  activeBlueprintLibraryId,
1186
1213
  loadBlueprintFromLibrary,
1187
1214
  refreshBlueprintLibrary,
1188
1215
  snapshotWorkspaceBlueprint,
1216
+ t,
1189
1217
  ]
1190
1218
  );
1191
1219
 
@@ -1227,7 +1255,7 @@ export function ReactViewPanel({
1227
1255
  onProjectApplied: handleWorkspaceProjectApplied,
1228
1256
  });
1229
1257
 
1230
- const { triggerBlueprintNode } = useBlueprintPageLifecycle({
1258
+ const { triggerBlueprintNode, emitViewEvent, firedLifecyclePhases } = useBlueprintPageLifecycle({
1231
1259
  graph: blueprintGraph,
1232
1260
  active: blueprintOpen,
1233
1261
  bootKey: workspaceProjects.activeProjectId ?? undefined,
@@ -1239,6 +1267,24 @@ export function ReactViewPanel({
1239
1267
  onViewScopeUpdate: handleViewScopeUpdate,
1240
1268
  });
1241
1269
 
1270
+ const boundViewEventTypes = useMemo(
1271
+ () =>
1272
+ collectArmedViewEventBindings(
1273
+ blueprintGraph.document.nodes.map((node) => ({
1274
+ id: node.id,
1275
+ nodeType: resolveRunnableNodeType(node),
1276
+ lifecyclePhase: node.lifecyclePhase,
1277
+ viewElementIds: resolveViewElementIds(node),
1278
+ eventConfig: node.eventConfig,
1279
+ })),
1280
+ blueprintGraph.document.edges,
1281
+ EVENT_NODE_TYPE,
1282
+ LIFECYCLE_NODE_TYPE,
1283
+ firedLifecyclePhases
1284
+ ),
1285
+ [blueprintGraph.document.nodes, blueprintGraph.document.edges, firedLifecyclePhases]
1286
+ );
1287
+
1242
1288
  const handleWorkspaceCreateProject = useCallback(async () => {
1243
1289
  const result = await workspaceProjects.handleCreateProject();
1244
1290
  if (result?.name) {
@@ -1250,6 +1296,7 @@ export function ReactViewPanel({
1250
1296
  const handleWorkspaceOpenProject = useCallback(
1251
1297
  async (id: string) => {
1252
1298
  try {
1299
+ stopAllClockSchedules();
1253
1300
  clearViewElementScopes();
1254
1301
  await workspaceProjects.handleOpenProject(id);
1255
1302
  setSelectedIds([]);
@@ -1563,70 +1610,72 @@ export function ReactViewPanel({
1563
1610
  );
1564
1611
 
1565
1612
  const handleExport = useCallback(() => {
1566
- const data = exportPanelData();
1567
- const blob = new Blob([JSON.stringify(data, null, 2)], {
1568
- type: "application/json",
1613
+ void runBusyTask(t("common.exportingPanel"), async () => {
1614
+ const data = exportPanelData();
1615
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
1616
+ type: "application/json",
1617
+ });
1618
+ const url = URL.createObjectURL(blob);
1619
+ const a = document.createElement("a");
1620
+ a.href = url;
1621
+ const safeName = (productName.trim() || "panel")
1622
+ .replace(/[\\/:*?"<>|]/g, "-")
1623
+ .replace(/\s+/g, "-");
1624
+ a.download = `${safeName}-${Date.now()}.json`;
1625
+ a.click();
1626
+ URL.revokeObjectURL(url);
1569
1627
  });
1570
- const url = URL.createObjectURL(blob);
1571
- const a = document.createElement("a");
1572
- a.href = url;
1573
- const safeName = (productName.trim() || "panel")
1574
- .replace(/[\\/:*?"<>|]/g, "-")
1575
- .replace(/\s+/g, "-");
1576
- a.download = `${safeName}-${Date.now()}.json`;
1577
- a.click();
1578
- URL.revokeObjectURL(url);
1579
- }, [exportPanelData, productName]);
1628
+ }, [exportPanelData, productName, t]);
1580
1629
 
1581
1630
  const handleImportFile = useCallback(
1582
1631
  async (file: File) => {
1583
- const text = await file.text();
1584
- const parsed = JSON.parse(text) as State;
1585
- const ok = importPanelData(parsed);
1586
- if (!ok) {
1587
- window.alert(messages.importInvalidFormat);
1588
- } else {
1589
- setSelectedIds([]);
1632
+ try {
1633
+ await runBusyTask(t("common.importingPanel"), async () => {
1634
+ assertFileSize(file, "json");
1635
+ const text = await file.text();
1636
+ const parsed = await parseJsonText<State>(text);
1637
+ const ok = importPanelData(parsed);
1638
+ if (!ok) {
1639
+ window.alert(messages.importInvalidFormat);
1640
+ } else {
1641
+ setSelectedIds([]);
1642
+ }
1643
+ });
1644
+ } catch (error) {
1645
+ window.alert(
1646
+ error instanceof Error ? error.message : messages.importInvalidFormat
1647
+ );
1590
1648
  }
1591
1649
  },
1592
- [importPanelData]
1650
+ [importPanelData, t]
1593
1651
  );
1594
1652
 
1595
- const normalizeTitleIconFile = useCallback((file: File) => {
1653
+ const normalizeTitleIconFile = useCallback(async (file: File) => {
1654
+ const src = await readFileAsDataUrl(file, "read-failed", "image");
1596
1655
  return new Promise<string>((resolve, reject) => {
1597
- const reader = new FileReader();
1598
- reader.onerror = () => reject(new Error("read-failed"));
1599
- reader.onload = () => {
1600
- const src = typeof reader.result === "string" ? reader.result : "";
1601
- if (!src) {
1602
- reject(new Error("empty-data"));
1656
+ const img = new Image();
1657
+ img.onload = () => {
1658
+ const size = 64;
1659
+ const canvas = document.createElement("canvas");
1660
+ canvas.width = size;
1661
+ canvas.height = size;
1662
+ const ctx = canvas.getContext("2d");
1663
+ if (!ctx) {
1664
+ reject(new Error("canvas-context-failed"));
1603
1665
  return;
1604
1666
  }
1605
- const img = new Image();
1606
- img.onload = () => {
1607
- const size = 64;
1608
- const canvas = document.createElement("canvas");
1609
- canvas.width = size;
1610
- canvas.height = size;
1611
- const ctx = canvas.getContext("2d");
1612
- if (!ctx) {
1613
- reject(new Error("canvas-context-failed"));
1614
- return;
1615
- }
1616
- ctx.clearRect(0, 0, size, size);
1617
- // 保持长宽比居中绘制,统一导出 PNG,提升 favicon 兼容性
1618
- const scale = Math.min(size / img.width, size / img.height);
1619
- const drawW = img.width * scale;
1620
- const drawH = img.height * scale;
1621
- const dx = (size - drawW) / 2;
1622
- const dy = (size - drawH) / 2;
1623
- ctx.drawImage(img, dx, dy, drawW, drawH);
1624
- resolve(canvas.toDataURL("image/png"));
1625
- };
1626
- img.onerror = () => reject(new Error("image-decode-failed"));
1627
- img.src = src;
1667
+ ctx.clearRect(0, 0, size, size);
1668
+ const scale = Math.min(size / img.width, size / img.height);
1669
+ const drawW = img.width * scale;
1670
+ const drawH = img.height * scale;
1671
+ const dx = (size - drawW) / 2;
1672
+ const dy = (size - drawH) / 2;
1673
+ ctx.drawImage(img, dx, dy, drawW, drawH);
1674
+ img.src = "";
1675
+ resolve(canvas.toDataURL("image/png"));
1628
1676
  };
1629
- reader.readAsDataURL(file);
1677
+ img.onerror = () => reject(new Error("image-decode-failed"));
1678
+ img.src = src;
1630
1679
  });
1631
1680
  }, []);
1632
1681
 
@@ -1935,6 +1984,7 @@ export function ReactViewPanel({
1935
1984
  ].join(" ")}
1936
1985
  style={{ ["--panel-font-px" as string]: `${panelFontPx}px` }}
1937
1986
  >
1987
+ <BusyOverlay />
1938
1988
  <TooltipProvider delayDuration={120}>
1939
1989
  <Tooltip open={ellipsisTooltip.open}>
1940
1990
  <TooltipTrigger asChild>
@@ -1953,6 +2003,11 @@ export function ReactViewPanel({
1953
2003
  .panel-font-root :is(span, label, p, strong, button, input, textarea, select, option, a, li) {
1954
2004
  font-size: var(--panel-font-px) !important;
1955
2005
  }
2006
+ /* 画布节点、富文本内容使用各自的字号,不被编辑器 UI 字号强制覆盖 */
2007
+ .panel-font-root [data-workspace-region="view"] :is(span, label, p, strong, button, input, textarea, select, option, a, li),
2008
+ .panel-font-root [data-panel-user-text] :is(span, label, p, strong, button, input, textarea, select, option, a, li) {
2009
+ font-size: inherit !important;
2010
+ }
1956
2011
  .panel-font-root :is(input, textarea):focus,
1957
2012
  .panel-font-root :is(input, textarea):focus-visible {
1958
2013
  outline: none !important;
@@ -2069,6 +2124,7 @@ export function ReactViewPanel({
2069
2124
  activeProjectId={workspaceProjects.activeProjectId}
2070
2125
  activeProjectName={workspaceProjects.activeProjectName}
2071
2126
  dirty={workspaceProjects.dirty}
2127
+ previewingProjectIds={workspaceProjects.previewingProjectIds}
2072
2128
  onCreateProject={handleWorkspaceCreateProject}
2073
2129
  onOpenProject={handleWorkspaceOpenProject}
2074
2130
  onSyncProject={handleWorkspaceSyncProject}
@@ -2558,8 +2614,10 @@ export function ReactViewPanel({
2558
2614
  try {
2559
2615
  const normalizedDataUrl = await normalizeTitleIconFile(file);
2560
2616
  setTitleIconDataUrl(normalizedDataUrl);
2561
- } catch {
2562
- window.alert(messages.iconReadFailed);
2617
+ } catch (error) {
2618
+ window.alert(
2619
+ error instanceof Error ? error.message : messages.iconReadFailed
2620
+ );
2563
2621
  }
2564
2622
  }}
2565
2623
  />
@@ -2692,6 +2750,10 @@ export function ReactViewPanel({
2692
2750
  onTableCellAction={(payload) => {
2693
2751
  void triggerBlueprintNode(payload.blueprintNodeId, payload);
2694
2752
  }}
2753
+ boundViewEventTypes={boundViewEventTypes}
2754
+ onViewUiEvent={(payload) => {
2755
+ void emitViewEvent(payload);
2756
+ }}
2695
2757
  />
2696
2758
 
2697
2759
  <SelectLayer
@@ -202,6 +202,7 @@ function applyBlueprintAddNode(
202
202
  fetch: "fetch",
203
203
  json: "json",
204
204
  and: "and",
205
+ event: "event",
205
206
  };
206
207
  const configSource = configSourceMap[alias];
207
208
  if (configSource && configSource !== "logic") {
@@ -0,0 +1,25 @@
1
+ import { createPortal } from "react-dom";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import { useBusyOverlayMessage } from "../utils/async-work";
4
+
5
+ export function BusyOverlay() {
6
+ const { t } = useI18n();
7
+ const message = useBusyOverlayMessage();
8
+ if (!message || typeof document === "undefined") return null;
9
+
10
+ return createPortal(
11
+ <div
12
+ className="fixed inset-0 flex flex-col items-center justify-center gap-3 bg-black/45 px-6"
13
+ style={{ zIndex: 40000 }}
14
+ role="status"
15
+ aria-live="polite"
16
+ aria-busy="true"
17
+ >
18
+ <div className="h-9 w-9 animate-spin rounded-full border-2 border-white/30 border-t-white" />
19
+ <div className="max-w-sm text-center text-sm text-white">
20
+ {message || t("common.loading")}
21
+ </div>
22
+ </div>,
23
+ document.body
24
+ );
25
+ }