@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.
@@ -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">
@@ -73,17 +73,13 @@ export function ScopeTemplateAutocompleteHost({
73
73
 
74
74
  const refreshForInput = useCallback(
75
75
  (input: HTMLInputElement | HTMLTextAreaElement) => {
76
- if (scope === undefined) {
77
- closeDropdown();
78
- return;
79
- }
80
76
  const cursor = input.selectionStart ?? input.value.length;
81
- const parsed = parseScopeAutocomplete(input.value, cursor, scope);
77
+ const parsed = parseScopeAutocomplete(input.value, cursor, scope ?? {});
82
78
  if (!parsed) {
83
79
  closeDropdown();
84
80
  return;
85
81
  }
86
- const suggestions = getScopeAutocompleteSuggestions(scope, parsed);
82
+ const suggestions = getScopeAutocompleteSuggestions(scope ?? {}, parsed);
87
83
  if (suggestions.length === 0) {
88
84
  closeDropdown();
89
85
  return;
@@ -124,7 +120,7 @@ export function ScopeTemplateAutocompleteHost({
124
120
 
125
121
  useEffect(() => {
126
122
  const container = containerRef?.current;
127
- if (!container || scope === undefined) return;
123
+ if (!container) return;
128
124
 
129
125
  const onFocusIn = (event: FocusEvent) => {
130
126
  if (!isAutocompleteTarget(event.target)) return;
@@ -213,7 +209,7 @@ export function ScopeTemplateAutocompleteHost({
213
209
  scope,
214
210
  ]);
215
211
 
216
- if (!dropdown || scope === undefined) return null;
212
+ if (!dropdown) return null;
217
213
 
218
214
  return createPortal(
219
215
  <div
@@ -7,6 +7,7 @@ export function ScopeTemplateUsageHint() {
7
7
  <div className="space-y-2">
8
8
  <p>{t("panel.scope.usageHint1")}</p>
9
9
  <p>{t("panel.scope.usageHint2")}</p>
10
+ <p>{t("panel.scope.usageHintSystem")}</p>
10
11
  </div>
11
12
  );
12
13
  }
@@ -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);
@@ -5,6 +5,10 @@ export const AbuilderEvents = {
5
5
  workspaceAdd: "workspace:add",
6
6
  /** 同步工作区 */
7
7
  workspaceSync: "workspace:sync",
8
+ /** 开始上传接口集合 */
9
+ apiCollectionUploadStart: "api-collection:upload-start",
10
+ /** 上传接口集合成功 */
11
+ apiCollectionUploadSuccess: "api-collection:upload-success",
8
12
  } as const;
9
13
 
10
14
  export type AbuilderEventName = (typeof AbuilderEvents)[keyof typeof AbuilderEvents];
@@ -15,9 +19,25 @@ export type WorkspaceData = WorkspaceProjectRecord;
15
19
  export type WorkspaceAddEventPayload = WorkspaceData;
16
20
  export type WorkspaceSyncEventPayload = WorkspaceData;
17
21
 
22
+ export type ApiCollectionUploadStartEventPayload = {
23
+ fileNames: string[];
24
+ count: number;
25
+ };
26
+
27
+ export type ApiCollectionUploadSuccessEventPayload = {
28
+ collections: Array<{
29
+ id: string;
30
+ name: string;
31
+ apiCount: number;
32
+ persistLocal: boolean;
33
+ }>;
34
+ };
35
+
18
36
  export type AbuilderEventPayloadMap = {
19
37
  [AbuilderEvents.workspaceAdd]: WorkspaceAddEventPayload;
20
38
  [AbuilderEvents.workspaceSync]: WorkspaceSyncEventPayload;
39
+ [AbuilderEvents.apiCollectionUploadStart]: ApiCollectionUploadStartEventPayload;
40
+ [AbuilderEvents.apiCollectionUploadSuccess]: ApiCollectionUploadSuccessEventPayload;
21
41
  };
22
42
 
23
43
  type EventCallback = (payload: unknown) => void;
@@ -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,
@@ -1,6 +1,10 @@
1
1
  import type { PanelElement } from "../types";
2
2
  import type { PanelTableConfig } from "@arronqzy/view-table";
3
3
  import { isUsableTableRawData } from "@arronqzy/view-table";
4
+ import {
5
+ createSystemRoot,
6
+ isDayjsValue,
7
+ } from "@arronqzy/blueprint-dsl";
4
8
  import { materializeChartLabelsFromScope, materializeChartValuesFromScope } from "./scope-template-chart";
5
9
  import {
6
10
  SCOPE_SPREAD_TEMPLATE_RE,
@@ -11,12 +15,12 @@ import {
11
15
  resolveSpreadScopePath,
12
16
  } from "./scope-template-spread";
13
17
 
14
- const SCOPE_TOKEN_RE = /\{(scope[^}]*)\}/g;
18
+ const TEMPLATE_TOKEN_RE = /\{((?:scope|system)[^}]*)\}/g;
15
19
 
16
20
  export function hasScopeTemplate(value: string): boolean {
17
- SCOPE_TOKEN_RE.lastIndex = 0;
21
+ TEMPLATE_TOKEN_RE.lastIndex = 0;
18
22
  SCOPE_SPREAD_TEMPLATE_RE.lastIndex = 0;
19
- return SCOPE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
23
+ return TEMPLATE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
20
24
  }
21
25
 
22
26
  export function evaluateScopeExpression(
@@ -26,8 +30,13 @@ export function evaluateScopeExpression(
26
30
  const trimmed = expression.trim();
27
31
  if (!trimmed) return undefined;
28
32
  try {
29
- const fn = new Function("scope", `"use strict"; return (${trimmed});`);
30
- return fn(scope);
33
+ const system = createSystemRoot();
34
+ const fn = new Function(
35
+ "scope",
36
+ "system",
37
+ `"use strict"; return (${trimmed});`
38
+ );
39
+ return fn(scope, system);
31
40
  } catch {
32
41
  return undefined;
33
42
  }
@@ -39,6 +48,9 @@ function stringifyScopeValue(value: unknown): string {
39
48
  if (typeof value === "number" || typeof value === "boolean") {
40
49
  return String(value);
41
50
  }
51
+ if (isDayjsValue(value)) {
52
+ return value.toISOString();
53
+ }
42
54
  try {
43
55
  return JSON.stringify(value);
44
56
  } catch {
@@ -60,8 +72,8 @@ export function evaluateScopeTemplate(
60
72
  }
61
73
  );
62
74
 
63
- // Only `{scope...}` tokens — keep ECharts / CSS braces like `{b}` intact.
64
- return withSpread.replace(SCOPE_TOKEN_RE, (_: string, rawExpr: string) =>
75
+ // Only `{scope...}` / `{system...}` — keep ECharts / CSS braces like `{b}` intact.
76
+ return withSpread.replace(TEMPLATE_TOKEN_RE, (_: string, rawExpr: string) =>
65
77
  stringifyScopeValue(evaluateScopeExpression(rawExpr, scope))
66
78
  );
67
79
  }
@@ -91,7 +103,7 @@ function materializeTemplateValue(template: string, scope: unknown): unknown {
91
103
  );
92
104
  }
93
105
 
94
- const singleMatch = /^\{(scope[^}]*)\}$/.exec(trimmed);
106
+ const singleMatch = /^\{((?:scope|system)[^}]*)\}$/.exec(trimmed);
95
107
  if (singleMatch) {
96
108
  return evaluateScopeExpression(singleMatch[1]!, scope);
97
109
  }