@arronqzy/react-view 1.0.35 → 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/dist/styles.css CHANGED
@@ -2511,6 +2511,10 @@ video {
2511
2511
  text-align: center;
2512
2512
  }
2513
2513
 
2514
+ .align-middle {
2515
+ vertical-align: middle;
2516
+ }
2517
+
2514
2518
  .font-mono {
2515
2519
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
2516
2520
  }
@@ -3239,6 +3243,10 @@ video {
3239
3243
  --tw-ring-color: hsl(var(--primary));
3240
3244
  }
3241
3245
 
3246
+ .focus-visible\:ring-ring:focus-visible {
3247
+ --tw-ring-color: hsl(var(--ring));
3248
+ }
3249
+
3242
3250
  .focus-visible\:ring-offset-0:focus-visible {
3243
3251
  --tw-ring-offset-width: 0px;
3244
3252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-view",
3
- "version": "1.0.35",
3
+ "version": "1.0.36",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -30,12 +30,12 @@
30
30
  "@codemirror/lang-json": "^6.0.1",
31
31
  "@codemirror/theme-one-dark": "^6.1.2",
32
32
  "@codemirror/view": "^6.36.2",
33
- "@arronqzy/ui": "1.0.9",
34
- "@arronqzy/i18n": "0.1.16",
35
- "@arronqzy/react-blueprint": "1.0.21",
36
- "@arronqzy/blueprint-dsl": "1.0.16",
37
- "@arronqzy/view-scene3d": "0.1.5",
33
+ "@arronqzy/blueprint-dsl": "1.0.17",
34
+ "@arronqzy/ui": "1.0.10",
35
+ "@arronqzy/i18n": "0.1.17",
36
+ "@arronqzy/react-blueprint": "1.0.22",
38
37
  "@arronqzy/rx-store": "1.0.8",
38
+ "@arronqzy/view-scene3d": "0.1.5",
39
39
  "@arronqzy/view-table": "0.1.8",
40
40
  "@arronqzy/webllm-assistant": "0.1.16"
41
41
  },
@@ -45,9 +45,9 @@
45
45
  "@types/use-sync-external-store": "^1.5.0",
46
46
  "eslint": "^8.57.0",
47
47
  "typescript": "^5.5.4",
48
+ "@arronqzy/typescript-config": "1.0.0",
48
49
  "@arronqzy/eslint-config": "0.0.0",
49
- "@arronqzy/tailwind": "1.0.2",
50
- "@arronqzy/typescript-config": "1.0.0"
50
+ "@arronqzy/tailwind": "1.0.2"
51
51
  },
52
52
  "scripts": {
53
53
  "lint": "eslint \"src/**/*.{ts,tsx}\"",
@@ -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
  };
@@ -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
 
@@ -283,6 +283,12 @@ function ReactViewPanelInner({
283
283
  name: t("panel.defaults.unnamedBlueprint"),
284
284
  remark: "",
285
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
+ );
286
292
  const [blueprintLibraryItems, setBlueprintLibraryItems] = useState<
287
293
  BlueprintLibraryListItem[]
288
294
  >([]);
@@ -682,7 +688,7 @@ function ReactViewPanelInner({
682
688
  const blueprintDebugSession = useBlueprintDebugSession({
683
689
  graph: blueprintGraph,
684
690
  blueprintId: activeBlueprintLibraryId,
685
- blueprintName: blueprintMeta.name || t("panel.defaults.unnamedBlueprint"),
691
+ blueprintName: displayBlueprintName || t("panel.defaults.unnamedBlueprint"),
686
692
  resolveLibraryBlueprint,
687
693
  libraryNameById: blueprintLibraryNameById,
688
694
  onExecutionBlocked: handleBlueprintExecutionBlocked,
@@ -1648,7 +1654,7 @@ function ReactViewPanelInner({
1648
1654
  const previewFaviconLink = faviconHrefWithVersion
1649
1655
  ? `<link rel="icon" type="image/png" href="${faviconHrefWithVersion}" /><link rel="shortcut icon" type="image/png" href="${faviconHrefWithVersion}" />`
1650
1656
  : "";
1651
- const previewTitle = productName.trim() || t("panel.defaults.unnamedProduct");
1657
+ const previewTitle = displayProductName.trim() || t("panel.defaults.unnamedProduct");
1652
1658
  const escapedPreviewTitle = previewTitle
1653
1659
  .replace(/&/g, "&amp;")
1654
1660
  .replace(/</g, "&lt;")
@@ -1787,7 +1793,7 @@ function ReactViewPanelInner({
1787
1793
  win.document.head.appendChild(link);
1788
1794
  });
1789
1795
  }
1790
- }, [allElements, layers, productName, titleIconDataUrl]);
1796
+ }, [allElements, layers, displayProductName, t, titleIconDataUrl]);
1791
1797
 
1792
1798
  const handlePreviewLayer = useCallback(() => {
1793
1799
  void (async () => {
@@ -2048,7 +2054,7 @@ function ReactViewPanelInner({
2048
2054
  style={{ zIndex: PANEL_Z_INDEX.toolbar }}
2049
2055
  >
2050
2056
  <Input
2051
- value={productName}
2057
+ value={displayProductName}
2052
2058
  onChange={(e) => setProductName(e.target.value)}
2053
2059
  placeholder={t("panel.menubar.productNamePlaceholder")}
2054
2060
  className="h-7 w-[220px] text-xs focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:outline-none"
@@ -2332,7 +2338,7 @@ function ReactViewPanelInner({
2332
2338
  blueprintProps={blueprintCanvasProps}
2333
2339
  blueprintLibraryItems={blueprintLibraryItems}
2334
2340
  activeBlueprintLibraryId={activeBlueprintLibraryId}
2335
- currentBlueprintLabel={blueprintMeta.name}
2341
+ currentBlueprintLabel={displayBlueprintName}
2336
2342
  onSelectBlueprintLibraryItem={(id) => {
2337
2343
  void handleSelectBlueprintLibraryItem(id);
2338
2344
  }}
@@ -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
@@ -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
  )}
@@ -305,7 +305,12 @@ export function PanelConfigScene3dSection({
305
305
  return (
306
306
  <div className="space-y-3">
307
307
  <div className="space-y-1.5">
308
- <div className="text-[11px] font-medium">{t("panel.config.groupScene3dPurpose")}</div>
308
+ <div className="flex items-center gap-1 text-[11px] font-medium">
309
+ {t("panel.config.groupScene3dPurpose")}
310
+ <ConfigHintIcon label={t("panel.config.groupScene3dPurpose")}>
311
+ {t("panel.config.scene3dPurposeHint")}
312
+ </ConfigHintIcon>
313
+ </div>
309
314
  <Select
310
315
  value={config.presentation.purpose}
311
316
  onValueChange={(value) => {
@@ -322,9 +327,6 @@ export function PanelConfigScene3dSection({
322
327
  <SelectItem value="sandbox">{t("panel.config.scene3dPurposeSandbox")}</SelectItem>
323
328
  </SelectContent>
324
329
  </Select>
325
- <div className="text-[10px] text-muted-foreground">
326
- {t("panel.config.scene3dPurposeHint")}
327
- </div>
328
330
  <label className="block space-y-0.5 text-[10px]">
329
331
  <span>{t("panel.config.scene3dEnvironment")}</span>
330
332
  <Select
@@ -403,8 +405,11 @@ export function PanelConfigScene3dSection({
403
405
  />
404
406
  </div>
405
407
  <label className="block space-y-1" data-config-field="scene3d.modelUrl">
406
- <div className="text-[10px] text-muted-foreground">
408
+ <div className="flex items-center gap-1 text-[10px] text-muted-foreground">
407
409
  {t("panel.config.scene3dModelUrl")}
410
+ <ConfigHintIcon label={t("panel.config.scene3dModelUrl")}>
411
+ {t("panel.config.urlScopeHint")}
412
+ </ConfigHintIcon>
408
413
  </div>
409
414
  <div className="flex gap-1.5">
410
415
  <Input
@@ -424,7 +429,6 @@ export function PanelConfigScene3dSection({
424
429
  {t("common.add")}
425
430
  </Button>
426
431
  </div>
427
- <p className="text-[10px] text-muted-foreground">{t("panel.config.urlScopeHint")}</p>
428
432
  </label>
429
433
 
430
434
  {config.models.length === 0 ? (
@@ -688,7 +692,12 @@ export function PanelConfigScene3dSection({
688
692
  )}
689
693
 
690
694
  <div className="space-y-2">
691
- <div className="text-[11px] font-medium">{t("panel.config.groupScene3dCamera")}</div>
695
+ <div className="flex items-center gap-1 text-[11px] font-medium">
696
+ {t("panel.config.groupScene3dCamera")}
697
+ <ConfigHintIcon label={t("panel.config.groupScene3dCamera")}>
698
+ {t("panel.config.scene3dCameraEditDesc")}
699
+ </ConfigHintIcon>
700
+ </div>
692
701
  <div className="grid grid-cols-3 gap-1.5">
693
702
  {(["x", "y", "z"] as const).map((axis, index) => (
694
703
  <label key={`pos-${axis}`} className="space-y-0.5 text-[10px]">
@@ -734,7 +743,6 @@ export function PanelConfigScene3dSection({
734
743
  }
735
744
  />
736
745
  </label>
737
- <div className="text-[10px] text-muted-foreground">{t("panel.config.scene3dCameraEditDesc")}</div>
738
746
  </div>
739
747
 
740
748
  <div className="space-y-2">
@@ -12,6 +12,7 @@ import type { PanelElement, PanelElementStyle } from "../../types";
12
12
  import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
13
13
  import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
14
14
  import { getPanelMessages } from "../../constants/messages";
15
+ import { ConfigHintIcon } from "../ConfigHintIcon";
15
16
  import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
16
17
 
17
18
  export function PanelConfigStyleSections({
@@ -90,7 +91,12 @@ export function PanelConfigStyleSections({
90
91
  t("panel.config.groupBgLayout"),
91
92
  <>
92
93
  <label className="block space-y-1" data-config-field="style.backgroundImage">
93
- <div>{t("panel.config.backgroundImage")}</div>
94
+ <div className="flex items-center gap-1">
95
+ {t("panel.config.backgroundImage")}
96
+ <ConfigHintIcon label={t("panel.config.backgroundImage")}>
97
+ {t("panel.config.urlScopeHint")}
98
+ </ConfigHintIcon>
99
+ </div>
94
100
  <Input
95
101
  value={
96
102
  selectedElement.style?.backgroundImageRemoteUrl ??
@@ -103,9 +109,6 @@ export function PanelConfigStyleSections({
103
109
  placeholder={t("panel.config.urlScopePlaceholder")}
104
110
  className="h-7 font-mono text-[11px]"
105
111
  />
106
- <p className="text-[10px] text-muted-foreground">
107
- {t("panel.config.urlScopeHint")}
108
- </p>
109
112
  </label>
110
113
  <div className="flex items-center gap-2">
111
114
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
@@ -11,6 +11,7 @@ import {
11
11
  } from "@arronqzy/ui";
12
12
  import type { PanelElement } from "../../types";
13
13
  import { cssTextLineHeight, cssTextAlignStyle } from "../../utils/panelElementDefaults";
14
+ import { ConfigHintIcon } from "../ConfigHintIcon";
14
15
  import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
15
16
 
16
17
  export function PanelConfigTextSection({
@@ -111,7 +112,12 @@ export function PanelConfigTextSection({
111
112
  }}
112
113
  />
113
114
  <label className="block space-y-1">
114
- <div>{t("panel.config.textHtmlSource")}</div>
115
+ <div className="flex items-center gap-1">
116
+ {t("panel.config.textHtmlSource")}
117
+ <ConfigHintIcon label={t("panel.config.textHtmlSource")}>
118
+ {t("panel.config.scopeTemplateHint")}
119
+ </ConfigHintIcon>
120
+ </div>
115
121
  <textarea
116
122
  data-config-field="textHtml"
117
123
  value={selectedElement.textHtml ?? ""}
@@ -125,9 +131,6 @@ export function PanelConfigTextSection({
125
131
  placeholder={t("panel.config.textHtmlSourcePlaceholder")}
126
132
  className="w-full rounded border border-border bg-background px-2 py-1.5 font-mono text-[11px] leading-relaxed outline-none focus-visible:ring-1 focus-visible:ring-primary"
127
133
  />
128
- <p className="text-[10px] text-muted-foreground">
129
- {t("panel.config.scopeTemplateHint")}
130
- </p>
131
134
  </label>
132
135
  </>
133
136
  )}
@@ -4,6 +4,7 @@ import { Checkbox, Input } from "@arronqzy/ui";
4
4
  import type { PanelElement } from "../../types";
5
5
  import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
6
6
  import { getPanelMessages } from "../../constants/messages";
7
+ import { ConfigHintIcon } from "../ConfigHintIcon";
7
8
  import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
8
9
 
9
10
  export function PanelConfigVideoSection({
@@ -70,7 +71,12 @@ export function PanelConfigVideoSection({
70
71
  t("panel.config.groupVideoSource"),
71
72
  <>
72
73
  <label className="block space-y-1" data-config-field="videoRemoteUrl">
73
- <div>{t("panel.config.videoUrl")}</div>
74
+ <div className="flex items-center gap-1">
75
+ {t("panel.config.videoUrl")}
76
+ <ConfigHintIcon label={t("panel.config.videoUrl")}>
77
+ {t("panel.config.urlScopeHint")}
78
+ </ConfigHintIcon>
79
+ </div>
74
80
  <Input
75
81
  value={selectedElement.videoRemoteUrl ?? ""}
76
82
  onChange={(e) =>
@@ -82,9 +88,6 @@ export function PanelConfigVideoSection({
82
88
  placeholder={t("panel.config.urlScopePlaceholder")}
83
89
  className="h-7 font-mono text-[11px]"
84
90
  />
85
- <p className="text-[10px] text-muted-foreground">
86
- {t("panel.config.urlScopeHint")}
87
- </p>
88
91
  </label>
89
92
  <div className="flex items-center gap-2">
90
93
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
@@ -20,6 +20,7 @@ import {
20
20
  SelectValue,
21
21
  } from "@arronqzy/ui";
22
22
  import React from "react";
23
+ import { ConfigHintIcon } from "../ConfigHintIcon";
23
24
 
24
25
  const CONDITION_OPS: ConditionOp[] = [
25
26
  "eq",
@@ -90,7 +91,12 @@ export function ConditionEditor({
90
91
  <div className="space-y-2">
91
92
  <div className="flex items-center justify-between gap-2">
92
93
  <label className={`${fieldClass} min-w-0 flex-1`}>
93
- <div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionLogic")}</div>
94
+ <div className="flex items-center gap-1 text-[11px] text-muted-foreground">
95
+ {t("panel.config.tableConditionLogic")}
96
+ <ConfigHintIcon label={t("panel.config.tableConditionLogic")}>
97
+ {t("panel.config.tableConditionLogicHint")}
98
+ </ConfigHintIcon>
99
+ </div>
94
100
  <Select
95
101
  value={logic}
96
102
  onValueChange={(next) => onChange(setConditionLogic(value, next as ConditionLogic))}
@@ -115,7 +121,6 @@ export function ConditionEditor({
115
121
  {t("panel.config.tableConditionAddItem")}
116
122
  </Button>
117
123
  </div>
118
- <div className="text-[10px] text-muted-foreground">{t("panel.config.tableConditionLogicHint")}</div>
119
124
  {items.map((item, index) => {
120
125
  const leaf: ConditionLeaf = isConditionLeaf(item)
121
126
  ? item
@@ -427,10 +427,16 @@ export function PanelConfigTableSection({
427
427
  </div>
428
428
  <details className="rounded-md border border-border/50 bg-muted/10 px-2 py-1.5">
429
429
  <summary className="cursor-pointer text-[11px] font-medium text-muted-foreground">
430
- {t("panel.config.tableAdvancedData")}
430
+ <span className="inline-flex items-center gap-1 align-middle">
431
+ {t("panel.config.tableAdvancedData")}
432
+ <span className="inline-flex" onClick={(e) => e.preventDefault()}>
433
+ <ConfigHintIcon label={t("panel.config.tableAdvancedData")}>
434
+ {t("panel.config.tableAdvancedDataHint")}
435
+ </ConfigHintIcon>
436
+ </span>
437
+ </span>
431
438
  </summary>
432
439
  <div className="mt-2 space-y-2 border-t border-border/40 pt-2">
433
- <p className="text-[10px] text-muted-foreground">{t("panel.config.tableAdvancedDataHint")}</p>
434
440
  <div className="grid grid-cols-2 gap-2">
435
441
  <label className={fieldClass}>
436
442
  {fieldLabel("panel.config.tableTransformMode", "panel.config.tableTransformModeHint")}
@@ -1000,7 +1006,7 @@ export function PanelConfigTableSection({
1000
1006
  imageUrlPrefix: e.target.value || undefined,
1001
1007
  })
1002
1008
  }
1003
- placeholder="https://cdn.example.com/ 或 {scope?.cdn}/"
1009
+ placeholder={t("panel.config.tableImageUrlPrefixPlaceholder")}
1004
1010
  className={inputClass}
1005
1011
  />
1006
1012
  </label>
@@ -1292,7 +1298,7 @@ export function PanelConfigTableSection({
1292
1298
  progressStatic: e.target.value || undefined,
1293
1299
  })
1294
1300
  }
1295
- placeholder="80 或 {scope?.pct}"
1301
+ placeholder={t("panel.config.tableProgressStaticPlaceholder")}
1296
1302
  className={inputClass}
1297
1303
  />
1298
1304
  </label>
@@ -1724,7 +1730,7 @@ export function PanelConfigTableSection({
1724
1730
  displayTemplate: e.target.value.trim() ? e.target.value : undefined,
1725
1731
  })
1726
1732
  }
1727
- placeholder="{current}分"
1733
+ placeholder={t("panel.config.tableDisplayTemplatePlaceholder")}
1728
1734
  className={inputClass}
1729
1735
  />
1730
1736
  </label>
@@ -22,6 +22,8 @@ import {
22
22
  getDefaultScene3dConfigForMaterial,
23
23
  getDefaultSizeByMaterial,
24
24
  getDefaultTextContent,
25
+ localizeLayers,
26
+ localizePanelElement,
25
27
  normalizePrimaryLayer,
26
28
  randomId,
27
29
  } from "../utils/panelElementDefaults";
@@ -67,7 +69,7 @@ export function usePanelElements() {
67
69
  () => store.getState()
68
70
  ) as State;
69
71
  const rawLayers = (state.variables?.layers as PanelLayer[] | undefined) ?? [defaultLayer];
70
- const layers = useMemo(() => normalizePrimaryLayer(rawLayers), [rawLayers]);
72
+ const layers = useMemo(() => localizeLayers(normalizePrimaryLayer(rawLayers), t), [rawLayers, t]);
71
73
  const activeLayerId =
72
74
  (state.variables?.activeLayerId as string | undefined) ?? DEFAULT_LAYER_ID;
73
75
  const canUndo = store.getHistoryCursorIndex() > 0;
@@ -137,14 +139,29 @@ export function usePanelElements() {
137
139
  });
138
140
  }, []);
139
141
 
142
+ const localizedCacheRef = useRef<{ t: typeof t; map: WeakMap<PanelElement, PanelElement> }>({
143
+ t,
144
+ map: new WeakMap(),
145
+ });
140
146
  const allElements = useMemo(() => {
141
147
  const next = materializePanelElements(
142
148
  state.root.children,
143
149
  allElementsCacheRef.current
144
150
  );
145
151
  allElementsCacheRef.current = next;
146
- return next;
147
- }, [state.root.children]);
152
+ if (localizedCacheRef.current.t !== t) {
153
+ localizedCacheRef.current = { t, map: new WeakMap() };
154
+ }
155
+ const { map } = localizedCacheRef.current;
156
+ return next.map((el) => {
157
+ let localized = map.get(el);
158
+ if (!localized) {
159
+ localized = localizePanelElement(el, t);
160
+ map.set(el, localized);
161
+ }
162
+ return localized;
163
+ });
164
+ }, [state.root.children, t]);
148
165
 
149
166
  const elements = useMemo(() => {
150
167
  const next = allElements.filter((el) => el.layerId === activeLayerId);
@@ -1,5 +1,5 @@
1
1
  import type { TranslateFn } from "@arronqzy/i18n";
2
- import { tForLocale } from "@arronqzy/i18n";
2
+ import { relocalizeMessage, tForLocale } from "@arronqzy/i18n";
3
3
  import { createDefaultScene3dConfig } from "@arronqzy/view-scene3d";
4
4
  import type { PanelChartConfig, PanelLayer, PanelElement } from "../types";
5
5
  import { createDefaultTableConfig, type PanelTableConfig } from "@arronqzy/view-table";
@@ -34,6 +34,52 @@ export function getDefaultNodeName(materialType: string, t: TranslateFn = tFallb
34
34
  return key ? t(key) : materialType;
35
35
  }
36
36
 
37
+ const DEFAULT_LAYER_NAME_KEYS = [
38
+ "panel.defaults.layer1",
39
+ "panel.defaults.layerN",
40
+ "panel.defaults.mappingLayerN",
41
+ "panel.defaults.layerRandom",
42
+ ];
43
+
44
+ /** Persisted default layer names ("图层1" / "Layer 2") follow the current locale. */
45
+ export function localizeLayerName(name: string, t: TranslateFn): string {
46
+ return relocalizeMessage(name, DEFAULT_LAYER_NAME_KEYS, t);
47
+ }
48
+
49
+ export function localizeLayers(layers: PanelLayer[], t: TranslateFn): PanelLayer[] {
50
+ return layers.map((layer) => {
51
+ const name = localizeLayerName(layer.name, t);
52
+ return name === layer.name ? layer : { ...layer, name };
53
+ });
54
+ }
55
+
56
+ const DEFAULT_TEXT_HTML_RE = /^<p>(.*)<\/p>$/s;
57
+
58
+ /**
59
+ * Default node name, chart title and text content are stored in the creation locale;
60
+ * re-render them in the current one. Returns the same object when nothing changes.
61
+ */
62
+ export function localizePanelElement(element: PanelElement, t: TranslateFn): PanelElement {
63
+ const key = DEFAULT_NODE_NAME_KEYS[element.materialType ?? ""];
64
+ if (!key) return element;
65
+ const name = element.name ? relocalizeMessage(element.name, key, t) : element.name;
66
+ const title = element.chart?.title ? relocalizeMessage(element.chart.title, key, t) : undefined;
67
+ let textHtml = element.textHtml;
68
+ const textInner = textHtml ? DEFAULT_TEXT_HTML_RE.exec(textHtml)?.[1] : undefined;
69
+ if (textInner) {
70
+ const localized = relocalizeMessage(textInner, "panel.defaults.doubleClickTextHtml", t);
71
+ if (localized !== textInner) textHtml = `<p>${localized}</p>`;
72
+ }
73
+ const titleChanged = title !== undefined && title !== element.chart?.title;
74
+ if (name === element.name && !titleChanged && textHtml === element.textHtml) return element;
75
+ return {
76
+ ...element,
77
+ name,
78
+ textHtml,
79
+ ...(titleChanged && element.chart ? { chart: { ...element.chart, title } } : null),
80
+ };
81
+ }
82
+
37
83
  export function getDefaultLayer(t: TranslateFn = tFallback): PanelLayer {
38
84
  return {
39
85
  id: DEFAULT_LAYER_ID,