@arronqzy/react-view 1.0.33 → 1.0.36

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/readme.md CHANGED
@@ -1,16 +1,27 @@
1
1
  # @arronqzy/react-view
2
2
 
3
- Abuilder 的**视图编辑器** React 包,提供可视化画布、图层、物料、在线预览与工作区管理能力。
3
+ 视图编辑器。物料、无限画布、图层、配置侧栏、工作区和在线预览都在这个包。蓝图分屏通过依赖的 `@arronqzy/react-blueprint` 嵌进来。
4
4
 
5
- ## 功能概览
5
+ 大多数 React 宿主应直接用 [`@arronqzy/abuilder`](../abuilder/README.md) 的 `<App />`。只有要把面板拆进已有布局时才引本包。
6
6
 
7
- - **无限画布**:平移、缩放、标尺、框选与 Moveable 拖拽/缩放/旋转
8
- - **图层系统**:多图层、映射图层、主图层、锁定与合并
9
- - **物料节点**:图表(ECharts)、文本、图片、音视频、几何、网格、引用节点等
10
- - **Scope 模版**:节点属性支持 `{scope.xxx}` 表达式与解析预览
11
- - **工作区**:IndexedDB 持久化、多项目切换、同步与在线预览
12
- - **蓝图联动**:与 `@arronqzy/react-blueprint` 分屏编辑,运行时 Scope 回写视图
13
- - **宿主集成**:工作区事件订阅、预览快照、外部工作区数据加载
7
+ ![左侧物料、中间画布、右侧配置、底部图层](https://github.com/qianzhuoyao/APanel5/raw/v5/docs/guide-assets/live-zh/01-overview.png)
8
+
9
+ ## 画布上有什么
10
+
11
+ - **无限画布**:平移、缩放、标尺、框选,Moveable 拖拽 / 缩放 / 旋转
12
+ - **物料**:ECharts 图表、可视化表格、文本、图片、音视频、几何、网格、视口、三维、引用节点
13
+ - **图层**:多图层、映射图层、主图层、锁定、显示、合并
14
+ - **配置**:按类型展开;可搜字段;展开状态按节点记住
15
+ - **Scope**:属性可写 `{scope.xxx}`,蓝图执行后展开
16
+ - **工作区**:IndexedDB 多项目、导入导出、跨标签同步
17
+
18
+ 表格是物料里配置最多的一类,列、条件样式和外观都在右侧:
19
+
20
+ ![表格节点与配置侧栏](https://github.com/qianzhuoyao/APanel5/raw/v5/docs/guide-assets/live-zh/09-table-full.png)
21
+
22
+ 打开蓝图后,本包把画布拆成上下两块,下面交给蓝图编辑器:
23
+
24
+ ![视图与蓝图分屏](https://github.com/qianzhuoyao/APanel5/raw/v5/docs/guide-assets/live-zh/17-blueprint.png)
14
25
 
15
26
  ## 安装
16
27
 
@@ -18,15 +29,13 @@ Abuilder 的**视图编辑器** React 包,提供可视化画布、图层、物
18
29
  pnpm add @arronqzy/react-view
19
30
  ```
20
31
 
21
- 依赖 React 19,并需引入样式:
22
-
23
32
  ```ts
24
33
  import "@arronqzy/react-view/styles.css";
25
34
  ```
26
35
 
27
- ## 使用
36
+ Vite 宿主还要加 `@arronqzy/abuilder/vite` 的 `webllmAssistant()`,见 abuilder 的 README。否则打包 WebLLM 会失败。
28
37
 
29
- ### 完整编辑器面板
38
+ ## 编辑面板
30
39
 
31
40
  ```tsx
32
41
  import { ReactViewPanel } from "@arronqzy/react-view";
@@ -44,15 +53,15 @@ export function Editor({ workspace }: { workspace?: WorkspaceProjectRecord | nul
44
53
  }
45
54
  ```
46
55
 
47
- | `ReactViewPanel` 参数 | 说明 | 默认值 |
48
- |----------------------|------|--------|
56
+ | 参数 | 说明 | 默认 |
57
+ |------|------|------|
49
58
  | `className` | 根节点 class | — |
50
59
  | `initialZoom` | 初始缩放 | `1` |
51
- | `initialWorkspace` | 外部传入的完整工作区,挂载后自动加载 | — |
60
+ | `initialWorkspace` | 挂载后加载的完整工作区。空则空白画布 | — |
52
61
 
53
- ### 在线预览
62
+ ## 在线预览
54
63
 
55
- 按工作区 ID 从 IndexedDB / 缓存加载:
64
+ 按 ID 从 IndexedDB / 缓存加载:
56
65
 
57
66
  ```tsx
58
67
  import { ReactViewOnlinePreview } from "@arronqzy/react-view";
@@ -60,65 +69,62 @@ import { ReactViewOnlinePreview } from "@arronqzy/react-view";
60
69
  <ReactViewOnlinePreview projectId="your-project-id" />
61
70
  ```
62
71
 
63
- 直接传入工作区数据(无需 IndexedDB):
72
+ 直接给数据(优先于 `projectId`):
64
73
 
65
74
  ```tsx
66
75
  <ReactViewOnlinePreview workspace={savedWorkspace} />
67
76
  ```
68
77
 
69
- URL 参数解析:
70
-
71
78
  ```ts
72
79
  import { parseOnlinePreviewSearchParams } from "@arronqzy/react-view";
73
80
 
74
- const params = parseOnlinePreviewSearchParams("?preview=online&projectId=xxx");
81
+ parseOnlinePreviewSearchParams("?preview=online&projectId=xxx");
75
82
  ```
76
83
 
77
- | `ReactViewOnlinePreview` 参数 | 说明 |
78
- |------------------------------|------|
79
- | `projectId` | 从 IndexedDB 加载的工作区 ID |
80
- | `workspace` | 直接传入的完整工作区数据(优先于 `projectId`) |
84
+ | 参数 | 说明 |
85
+ |------|------|
86
+ | `projectId` | 从 IndexedDB 加载 |
87
+ | `workspace` | 宿主传入的完整工作区,优先 |
81
88
  | `previewInstanceId` | 预览实例 ID(可选) |
82
89
 
83
- ### 工作区事件订阅
90
+ ## 工作区事件
84
91
 
85
- 监听工作区创建与同步,回调携带**完整工作区数据**(面板 + 蓝图),便于持久化到自有后端:
92
+ 回调带完整工作区,用来写自己的后端。IndexedDB 的增删改查不导出。
86
93
 
87
94
  ```ts
88
95
  import {
89
96
  addEventSubscription,
90
97
  AbuilderEvents,
98
+ parseWorkspaceData,
91
99
  type WorkspaceData,
92
100
  } from "@arronqzy/react-view";
93
101
 
94
- const addSub = addEventSubscription(
95
- AbuilderEvents.workspaceAdd,
96
- async (workspace: WorkspaceData) => {
97
- await saveToServer(workspace);
98
- }
99
- );
100
-
101
102
  const syncSub = addEventSubscription(
102
103
  AbuilderEvents.workspaceSync,
103
- async (workspace) => {
104
+ async (workspace: WorkspaceData) => {
104
105
  await saveToServer(workspace);
105
106
  }
106
107
  );
107
-
108
- addSub.unsubscribe();
109
108
  syncSub.unsubscribe();
110
109
  ```
111
110
 
112
- | 事件 | 常量 | 触发时机 |
111
+ | 事件 | 常量 | 何时触发 |
113
112
  |------|------|----------|
114
- | `workspace:add` | `AbuilderEvents.workspaceAdd` | 创建工作区成功 |
115
- | `workspace:sync` | `AbuilderEvents.workspaceSync` | 同步工作区成功 |
113
+ | `workspace:add` | `AbuilderEvents.workspaceAdd` | 创建成功 |
114
+ | `workspace:sync` | `AbuilderEvents.workspaceSync` | 同步成功 |
115
+
116
+ 字段:`id`、`name`、`createdAt`、`updatedAt`、`panelState`、`blueprintDocument`、`blueprintMeta`、`productName`、`titleIconDataUrl`。
117
+
118
+ 校验:
116
119
 
117
- 回调参数类型为 `WorkspaceData`(即 `WorkspaceProjectRecord`),字段包括:`id`、`name`、`createdAt`、`updatedAt`、`panelState`、`blueprintDocument`、`blueprintMeta`、`productName`、`titleIconDataUrl`。
120
+ - `validateViewData` / `parseViewData` — 视图 `panelState`
121
+ - `validateBlueprintData` / `parseBlueprintData` — 蓝图文档
122
+ - `validateWorkspaceData` / `parseWorkspaceData` — 两边都要合法;`parse` 的 `value` 可作 `initialWorkspace`
123
+ - `createEmptyWorkspace` / `createWorkspaceProjectId` — 空白记录和 ID
118
124
 
119
- ### 获取预览快照
125
+ ## 预览缩略图
120
126
 
121
- 在编辑器或在线预览页已挂载时,截取当前预览画面为 base64(data URL),可用于缩略图:
127
+ 编辑器或预览页已挂载且画布有内容时:
122
128
 
123
129
  ```ts
124
130
  import { getPreviewSnapshot } from "@arronqzy/react-view";
@@ -131,56 +137,38 @@ const dataUrl = await getPreviewSnapshot({
131
137
  });
132
138
  ```
133
139
 
134
- | 参数 | 说明 | 默认值 |
135
- |------|------|--------|
136
- | `maxWidth` / `maxHeight` | 等比缩放上限 | 不限制 |
137
- | `mimeType` | `"image/png"` 或 `"image/jpeg"` | `"image/png"` |
138
- | `quality` | JPEG 质量 0–1 | `0.92` |
140
+ | 参数 | 说明 | 默认 |
141
+ |------|------|------|
142
+ | `maxWidth` / `maxHeight` | 等比上限 | 不限制 |
143
+ | `mimeType` | png 或 jpeg | `"image/png"` |
144
+ | `quality` | JPEG 0–1 | `0.92` |
139
145
  | `backgroundColor` | 背景色 | `"#ffffff"` |
140
146
 
141
- ## 核心导出
147
+ ## 主要导出
142
148
 
143
149
  | 导出 | 说明 |
144
150
  |------|------|
145
- | `ReactViewPanel` | 主编辑面板(画布 + 配置侧栏 + 蓝图分屏) |
146
- | `ReactViewOnlinePreview` | 只读在线预览页 |
147
- | `parseOnlinePreviewSearchParams` | 解析预览 URL 参数 |
148
- | `addEventSubscription` | 订阅工作区事件,返回 `{ unsubscribe }` |
149
- | `AbuilderEvents` | 工作区事件名常量 |
150
- | `getPreviewSnapshot` | 获取预览画面 base64 缩略图(仅本包 / `@arronqzy/abuilder`) |
151
- | `parseWorkspaceData` / `validateWorkspaceData` | 解析或校验完整工作区;`parse` 的 `value` 可作 `initialWorkspace` |
152
- | `parseViewData` / `validateViewData` | 解析或校验视图 `panelState` |
153
- | `parseBlueprintData` / `validateBlueprintData` | 解析或校验蓝图文档 |
154
- | `createEmptyWorkspace` | 构造空工作区记录 |
155
- | `createWorkspaceProjectId` | 生成工作区 ID |
156
- | `WorkspaceData` / `WorkspaceProjectRecord` | 完整工作区数据类型 |
157
- | `createView` / `createAction` 等 | 视图 DSL 与指令扩展(`src/core`) |
158
-
159
- 相关类型:`AbuilderEventName`、`AbuilderEventPayloadMap`、`WorkspaceAddEventPayload`、`WorkspaceSyncEventPayload`、`GetPreviewSnapshotOptions`。
160
-
161
- ## 样式与 Tailwind
162
-
163
- 本包内置 Tailwind 配置,构建产物为 `dist/styles.css`:
151
+ | `ReactViewPanel` | 主面板(画布 + 配置 + 蓝图分屏) |
152
+ | `ReactViewOnlinePreview` | 只读预览 |
153
+ | `parseOnlinePreviewSearchParams` | 解析预览 URL |
154
+ | `addEventSubscription` / `AbuilderEvents` | 工作区事件 |
155
+ | `getPreviewSnapshot` | 预览截图 data URL |
156
+ | `parseWorkspaceData` 等 | 校验 / 解析 / 空工作区 |
157
+ | `createView` / `createAction` | 视图 DSL 扩展点(`src/core`) |
158
+
159
+ ## 样式
160
+
161
+ 产物是 `dist/styles.css`。本仓库构建:
164
162
 
165
163
  ```bash
166
164
  pnpm -C packages/react-view build:css
167
165
  ```
168
166
 
169
- 若由应用侧 Tailwind 扫描源码,请将本包加入 `content`:
170
-
171
- ```ts
172
- content: [
173
- "./src/**/*.{ts,tsx}",
174
- "./node_modules/@arronqzy/react-view/src/**/*.{ts,tsx}",
175
- ]
176
- ```
167
+ 若应用自己的 Tailwind 要扫描本包源码,把 `node_modules/@arronqzy/react-view/src/**/*.{ts,tsx}` 加进 `content`。
177
168
 
178
- ## 依赖关系
169
+ ## 依赖
179
170
 
180
- - `@arronqzy/ui` — 基础 UI 组件
181
- - `@arronqzy/rx-store` — 画布状态与历史
182
- - `@arronqzy/react-blueprint` — 蓝图编辑与执行
183
- - `@arronqzy/blueprint-dsl` — 蓝图运行时
171
+ `@arronqzy/ui`、`@arronqzy/rx-store`、`@arronqzy/react-blueprint`、`@arronqzy/blueprint-dsl`、`@arronqzy/view-table`、`@arronqzy/view-scene3d`。
184
172
 
185
173
  ## 许可证
186
174
 
@@ -33,7 +33,7 @@ export class InstructionSet {
33
33
  execute(name: string, ...args: any[]) {
34
34
  const fn = this.instructions[name];
35
35
  if (!fn) {
36
- console.warn(`指令不存在: ${name}`);
36
+ console.warn(`Instruction not found: ${name}`);
37
37
  return;
38
38
  }
39
39
  const result = fn(...args);
@@ -26,7 +26,7 @@ export const Template = (
26
26
  instructions.execute(props.id + "click", { e });
27
27
  } else {
28
28
  if (isDevMode()) {
29
- console.warn("Template 缺少 id,无法触发事件");
29
+ console.warn("Template is missing an id; events cannot be triggered");
30
30
  }
31
31
  }
32
32
  };
package/src/index.ts CHANGED
@@ -14,6 +14,8 @@ export type {
14
14
  AbuilderEventPayloadMap,
15
15
  WorkspaceAddEventPayload,
16
16
  WorkspaceSyncEventPayload,
17
+ ApiCollectionUploadStartEventPayload,
18
+ ApiCollectionUploadSuccessEventPayload,
17
19
  WorkspaceData,
18
20
  } from "./panel/library/event-subscription";
19
21
  export type { WorkspaceProjectRecord } from "./panel/library/workspace-project-db";
@@ -1,6 +1,7 @@
1
1
  import { I18nProvider } from "@arronqzy/i18n/react";
2
2
  import { LOCALE_STORAGE_KEY } from "@arronqzy/i18n";
3
3
  import { appStorageKey } from "@arronqzy/blueprint-dsl";
4
+ import { BlueprintDslLocaleSync } from "@arronqzy/react-blueprint";
4
5
  import type { Locale } from "@arronqzy/i18n";
5
6
  import type { ReactNode } from "react";
6
7
 
@@ -24,7 +25,7 @@ export function I18nRoot({
24
25
  onLocaleChange={onLocaleChange}
25
26
  storageKey={appStorageKey(LOCALE_STORAGE_KEY, nameSpace)}
26
27
  >
27
- {children}
28
+ <BlueprintDslLocaleSync>{children}</BlueprintDslLocaleSync>
28
29
  </I18nProvider>
29
30
  );
30
31
  }
@@ -17,6 +17,7 @@ import type { State } from "@arronqzy/rx-store";
17
17
  import { ElementsLayer } from "./components/ElementsLayer";
18
18
  import type { TableCellActionHandler } from "./components/table/TableNodeContent";
19
19
  import type { PanelElement } from "./types";
20
+ import { localizePanelElement } from "./utils/panelElementDefaults";
20
21
  import {
21
22
  clearViewElementScopes,
22
23
  getViewElementScope,
@@ -236,7 +237,8 @@ function ReactViewOnlinePreviewInner({
236
237
 
237
238
  const displayElements = useMemo(
238
239
  () =>
239
- scopedLayerElements.map((el) => {
240
+ scopedLayerElements.map((source) => {
241
+ const el = localizePanelElement(source, t);
240
242
  const x = el.x - sceneBounds.minX;
241
243
  const y = el.y - sceneBounds.minY;
242
244
  if (outputScale) return { ...el, x, y };
@@ -255,6 +257,7 @@ function ReactViewOnlinePreviewInner({
255
257
  sceneBounds.minX,
256
258
  sceneBounds.minY,
257
259
  scopedLayerElements,
260
+ t,
258
261
  ]
259
262
  );
260
263
 
@@ -1,6 +1,6 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useI18n } from "@arronqzy/i18n/react";
3
- import type { Locale } from "@arronqzy/i18n";
3
+ import { relocalizeMessage, type Locale } from "@arronqzy/i18n";
4
4
  import type { State } from "@arronqzy/rx-store";
5
5
  import type { PanelElement } from "./types";
6
6
 
@@ -46,6 +46,7 @@ import { revealPanelConfigFromPatch } from "./ai/revealConfigField";
46
46
  import {
47
47
  BlueprintGraph,
48
48
  BlueprintMetaDialog,
49
+ ApiCollectionManagerDialog,
49
50
  BlueprintNodeSwitchTaskDialog,
50
51
  blueprintDocumentsEqual,
51
52
  buildBlueprintExportPayload,
@@ -55,6 +56,7 @@ import {
55
56
  downloadBlueprintExport,
56
57
  documentToRunnableGraph,
57
58
  getBlueprintLibraryRecord,
59
+ hydrateApiCollections,
58
60
  libraryRecordFromImport,
59
61
  listBlueprintLibrary,
60
62
  parseBlueprintImportFile,
@@ -72,12 +74,16 @@ import {
72
74
  type BlueprintMetaDraft,
73
75
  } from "@arronqzy/react-blueprint";
74
76
  import type { LibraryBlueprintResolver } from "@arronqzy/blueprint-dsl";
75
- import { abortClockNode, appStorageKey, collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE } from "@arronqzy/blueprint-dsl";
77
+ import { abortClockNode, appStorageKey, buildApiCollectionSyncPatches, toApiCollectionSyncResult, collectArmedViewEventBindings, EVENT_NODE_TYPE, LIFECYCLE_NODE_TYPE } from "@arronqzy/blueprint-dsl";
76
78
  import { WorkspaceStageSplit } from "./components/WorkspaceStageSplit";
77
79
  import {
78
80
  WorkspaceConfigSidebar,
79
81
  type WorkspaceConfigFocus,
80
82
  } from "./components/WorkspaceConfigSidebar";
83
+ import {
84
+ AbuilderEvents,
85
+ emitAbuilderEvent,
86
+ } from "./library/event-subscription";
81
87
  import {
82
88
  getViewElementScope,
83
89
  setViewElementScopes,
@@ -277,6 +283,12 @@ function ReactViewPanelInner({
277
283
  name: t("panel.defaults.unnamedBlueprint"),
278
284
  remark: "",
279
285
  });
286
+ const displayProductName = relocalizeMessage(productName, "panel.defaults.unnamedProduct", t);
287
+ const displayBlueprintName = relocalizeMessage(
288
+ blueprintMeta.name,
289
+ ["panel.defaults.unnamedBlueprint", "blueprint.dialog.unnamedBlueprint"],
290
+ t
291
+ );
280
292
  const [blueprintLibraryItems, setBlueprintLibraryItems] = useState<
281
293
  BlueprintLibraryListItem[]
282
294
  >([]);
@@ -295,6 +307,7 @@ function ReactViewPanelInner({
295
307
  const [blueprintMetaDialogMode, setBlueprintMetaDialogMode] = useState<
296
308
  "export" | "save"
297
309
  >("save");
310
+ const [apiCollectionManagerOpen, setApiCollectionManagerOpen] = useState(false);
298
311
  const [selectedBlueprintNodeId, setSelectedBlueprintNodeId] = useState<string | null>(
299
312
  null
300
313
  );
@@ -655,6 +668,12 @@ function ReactViewPanelInner({
655
668
  [nameSpace]
656
669
  );
657
670
 
671
+ useEffect(() => {
672
+ void hydrateApiCollections(nameSpace).catch((error) => {
673
+ console.error("[abuilder] hydrate api collections failed", error);
674
+ });
675
+ }, [nameSpace]);
676
+
658
677
  const handleBlueprintExecutionBlocked = useCallback((message: string) => {
659
678
  toast({ title: message });
660
679
  }, []);
@@ -669,7 +688,7 @@ function ReactViewPanelInner({
669
688
  const blueprintDebugSession = useBlueprintDebugSession({
670
689
  graph: blueprintGraph,
671
690
  blueprintId: activeBlueprintLibraryId,
672
- blueprintName: blueprintMeta.name || t("panel.defaults.unnamedBlueprint"),
691
+ blueprintName: displayBlueprintName || t("panel.defaults.unnamedBlueprint"),
673
692
  resolveLibraryBlueprint,
674
693
  libraryNameById: blueprintLibraryNameById,
675
694
  onExecutionBlocked: handleBlueprintExecutionBlocked,
@@ -1635,7 +1654,7 @@ function ReactViewPanelInner({
1635
1654
  const previewFaviconLink = faviconHrefWithVersion
1636
1655
  ? `<link rel="icon" type="image/png" href="${faviconHrefWithVersion}" /><link rel="shortcut icon" type="image/png" href="${faviconHrefWithVersion}" />`
1637
1656
  : "";
1638
- const previewTitle = productName.trim() || t("panel.defaults.unnamedProduct");
1657
+ const previewTitle = displayProductName.trim() || t("panel.defaults.unnamedProduct");
1639
1658
  const escapedPreviewTitle = previewTitle
1640
1659
  .replace(/&/g, "&amp;")
1641
1660
  .replace(/</g, "&lt;")
@@ -1774,7 +1793,7 @@ function ReactViewPanelInner({
1774
1793
  win.document.head.appendChild(link);
1775
1794
  });
1776
1795
  }
1777
- }, [allElements, layers, productName, titleIconDataUrl]);
1796
+ }, [allElements, layers, displayProductName, t, titleIconDataUrl]);
1778
1797
 
1779
1798
  const handlePreviewLayer = useCallback(() => {
1780
1799
  void (async () => {
@@ -1904,6 +1923,7 @@ function ReactViewPanelInner({
1904
1923
  applyTheme={applyTheme}
1905
1924
  isDark={isDark}
1906
1925
  openBlueprintExportDialog={openBlueprintExportDialog}
1926
+ openApiCollectionManager={() => setApiCollectionManagerOpen(true)}
1907
1927
  panelFontSize={panelFontSize}
1908
1928
  setPanelFontSize={setPanelFontSize}
1909
1929
  locale={locale}
@@ -2034,7 +2054,7 @@ function ReactViewPanelInner({
2034
2054
  style={{ zIndex: PANEL_Z_INDEX.toolbar }}
2035
2055
  >
2036
2056
  <Input
2037
- value={productName}
2057
+ value={displayProductName}
2038
2058
  onChange={(e) => setProductName(e.target.value)}
2039
2059
  placeholder={t("panel.menubar.productNamePlaceholder")}
2040
2060
  className="h-7 w-[220px] text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none"
@@ -2318,7 +2338,7 @@ function ReactViewPanelInner({
2318
2338
  blueprintProps={blueprintCanvasProps}
2319
2339
  blueprintLibraryItems={blueprintLibraryItems}
2320
2340
  activeBlueprintLibraryId={activeBlueprintLibraryId}
2321
- currentBlueprintLabel={blueprintMeta.name}
2341
+ currentBlueprintLabel={displayBlueprintName}
2322
2342
  onSelectBlueprintLibraryItem={(id) => {
2323
2343
  void handleSelectBlueprintLibraryItem(id);
2324
2344
  }}
@@ -2768,6 +2788,43 @@ function ReactViewPanelInner({
2768
2788
  void handleBlueprintMetaConfirm(meta);
2769
2789
  }}
2770
2790
  />
2791
+ <ApiCollectionManagerDialog
2792
+ open={apiCollectionManagerOpen}
2793
+ onOpenChange={setApiCollectionManagerOpen}
2794
+ onUploadStart={(info) => {
2795
+ emitAbuilderEvent(AbuilderEvents.apiCollectionUploadStart, info);
2796
+ }}
2797
+ onUploadSuccess={(info) => {
2798
+ emitAbuilderEvent(AbuilderEvents.apiCollectionUploadSuccess, info);
2799
+ }}
2800
+ onSyncCollection={(record) => {
2801
+ const patches = buildApiCollectionSyncPatches({
2802
+ collectionId: record.id,
2803
+ document: record.document,
2804
+ nodes: blueprintGraph.document.nodes
2805
+ .filter((node) => node.role === "fetch" || Boolean(node.fetchConfig))
2806
+ .map((node) => ({
2807
+ id: node.id,
2808
+ label: node.label,
2809
+ fetchConfig: node.fetchConfig,
2810
+ })),
2811
+ });
2812
+ if (patches.length === 0) {
2813
+ return toApiCollectionSyncResult([]);
2814
+ }
2815
+ setBlueprintGraph((graph) => {
2816
+ let next = graph;
2817
+ for (const patch of patches) {
2818
+ next = next.updateNode(patch.nodeId, {
2819
+ fetchConfig:
2820
+ patch.fetchConfigPatch as import("@arronqzy/blueprint-dsl").FetchRequestConfig,
2821
+ });
2822
+ }
2823
+ return next;
2824
+ });
2825
+ return toApiCollectionSyncResult(patches);
2826
+ }}
2827
+ />
2771
2828
  {pendingBlueprintNodeSwitch ? (
2772
2829
  <BlueprintNodeSwitchTaskDialog
2773
2830
  open
@@ -31,11 +31,24 @@ export function ConfigHintIcon({
31
31
  type="button"
32
32
  className={
33
33
  className ??
34
- "inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50"
34
+ "inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
35
35
  }
36
36
  aria-label={t("common.hintAria", { label: resolvedLabel })}
37
37
  >
38
- ?
38
+ <svg
39
+ viewBox="0 0 24 24"
40
+ fill="none"
41
+ stroke="currentColor"
42
+ strokeWidth={2}
43
+ strokeLinecap="round"
44
+ strokeLinejoin="round"
45
+ className="h-3.5 w-3.5"
46
+ aria-hidden="true"
47
+ >
48
+ <circle cx="12" cy="12" r="10" />
49
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
50
+ <path d="M12 17h.01" />
51
+ </svg>
39
52
  </button>
40
53
  </TooltipTrigger>
41
54
  <TooltipContent
@@ -34,6 +34,7 @@ export type PanelMenubarProps = {
34
34
  applyTheme: (dark: boolean) => void;
35
35
  isDark: boolean;
36
36
  openBlueprintExportDialog: () => void;
37
+ openApiCollectionManager: () => void;
37
38
  panelFontSize: "sm" | "md" | "lg";
38
39
  setPanelFontSize: (value: "sm" | "md" | "lg") => void;
39
40
  locale: Locale;
@@ -65,6 +66,7 @@ export function PanelMenubar({
65
66
  applyTheme,
66
67
  isDark,
67
68
  openBlueprintExportDialog,
69
+ openApiCollectionManager,
68
70
  panelFontSize,
69
71
  setPanelFontSize,
70
72
  locale,
@@ -121,6 +123,10 @@ export function PanelMenubar({
121
123
  <MenubarItem onClick={() => blueprintImportInputRef.current?.click()}>
122
124
  {t("panel.menubar.import")}
123
125
  </MenubarItem>
126
+ <MenubarSeparator />
127
+ <MenubarItem onClick={openApiCollectionManager}>
128
+ {t("panel.menubar.apiCollections")}
129
+ </MenubarItem>
124
130
  </MenubarContent>
125
131
  </MenubarMenu>
126
132
  <MenubarMenu>
@@ -152,7 +152,12 @@ export function PanelConfigAudioSection({
152
152
  t("panel.config.groupAudioSource"),
153
153
  <>
154
154
  <label className="block space-y-1" data-config-field="audioRemoteUrl">
155
- <div>{t("panel.config.audioUrl")}</div>
155
+ <div className="flex items-center gap-1">
156
+ {t("panel.config.audioUrl")}
157
+ <ConfigHintIcon label={t("panel.config.audioUrl")}>
158
+ {t("panel.config.urlScopeHint")}
159
+ </ConfigHintIcon>
160
+ </div>
156
161
  <Input
157
162
  value={selectedElement.audioRemoteUrl ?? ""}
158
163
  onChange={(e) =>
@@ -164,9 +169,6 @@ export function PanelConfigAudioSection({
164
169
  placeholder={t("panel.config.urlScopePlaceholder")}
165
170
  className="h-7 font-mono text-[11px]"
166
171
  />
167
- <p className="text-[10px] text-muted-foreground">
168
- {t("panel.config.urlScopeHint")}
169
- </p>
170
172
  </label>
171
173
  <div className="flex items-center gap-2">
172
174
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
@@ -118,7 +118,12 @@ export function PanelConfigChartSection({
118
118
  t("panel.config.groupData"),
119
119
  <>
120
120
  <label className="block space-y-1" data-config-field="chart.labelsText">
121
- <div>{t("panel.config.labelsCsv")}</div>
121
+ <div className="flex items-center gap-1">
122
+ {t("panel.config.labelsCsv")}
123
+ <ConfigHintIcon label={t("panel.config.labelsCsv")}>
124
+ {t("panel.config.scopeTemplateHint")}
125
+ </ConfigHintIcon>
126
+ </div>
122
127
  <Input
123
128
  value={getChartLabelsDisplayText(selectedElement.chart)}
124
129
  onChange={(e) =>
@@ -129,13 +134,15 @@ export function PanelConfigChartSection({
129
134
  placeholder={t("panel.config.labelsCsvPlaceholder")}
130
135
  className="h-7"
131
136
  />
132
- <p className="text-[10px] text-muted-foreground">
133
- {t("panel.config.scopeTemplateHint")}
134
- </p>
135
137
  </label>
136
138
 
137
139
  <label className="block space-y-1" data-config-field="chart.valuesText">
138
- <div>{t("panel.config.valuesCsv")}</div>
140
+ <div className="flex items-center gap-1">
141
+ {t("panel.config.valuesCsv")}
142
+ <ConfigHintIcon label={t("panel.config.valuesCsv")}>
143
+ {t("panel.config.scopeTemplateHint")}
144
+ </ConfigHintIcon>
145
+ </div>
139
146
  <Input
140
147
  value={getChartValuesDisplayText(selectedElement.chart)}
141
148
  onChange={(e) =>
@@ -146,9 +153,6 @@ export function PanelConfigChartSection({
146
153
  placeholder={t("panel.config.valuesCsvPlaceholder")}
147
154
  className="h-7"
148
155
  />
149
- <p className="text-[10px] text-muted-foreground">
150
- {t("panel.config.scopeTemplateHint")}
151
- </p>
152
156
  </label>
153
157
  </>
154
158
  )}