@arronqzy/react-view 1.0.30 → 1.0.31

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
@@ -3026,6 +3026,16 @@ video {
3026
3026
  box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
3027
3027
  }
3028
3028
 
3029
+ .focus-visible\:ring-1:focus-visible {
3030
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
3031
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
3032
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
3033
+ }
3034
+
3035
+ .focus-visible\:ring-primary:focus-visible {
3036
+ --tw-ring-color: hsl(var(--primary));
3037
+ }
3038
+
3029
3039
  .focus-visible\:ring-offset-0:focus-visible {
3030
3040
  --tw-ring-offset-width: 0px;
3031
3041
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-view",
3
- "version": "1.0.30",
3
+ "version": "1.0.31",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -30,14 +30,14 @@
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/i18n": "0.1.13",
33
34
  "@arronqzy/ui": "1.0.9",
34
- "@arronqzy/blueprint-dsl": "1.0.14",
35
- "@arronqzy/i18n": "0.1.12",
36
- "@arronqzy/react-blueprint": "1.0.17",
37
- "@arronqzy/rx-store": "1.0.8",
35
+ "@arronqzy/blueprint-dsl": "1.0.15",
36
+ "@arronqzy/react-blueprint": "1.0.18",
38
37
  "@arronqzy/view-scene3d": "0.1.5",
39
- "@arronqzy/view-table": "0.1.8",
40
- "@arronqzy/webllm-assistant": "0.1.16"
38
+ "@arronqzy/rx-store": "1.0.8",
39
+ "@arronqzy/webllm-assistant": "0.1.16",
40
+ "@arronqzy/view-table": "0.1.8"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/react": "^19.0.1",
@@ -46,8 +46,8 @@
46
46
  "eslint": "^8.57.0",
47
47
  "typescript": "^5.5.4",
48
48
  "@arronqzy/eslint-config": "0.0.0",
49
- "@arronqzy/typescript-config": "1.0.0",
50
- "@arronqzy/tailwind": "1.0.2"
49
+ "@arronqzy/tailwind": "1.0.2",
50
+ "@arronqzy/typescript-config": "1.0.0"
51
51
  },
52
52
  "scripts": {
53
53
  "lint": "eslint \"src/**/*.{ts,tsx}\"",
@@ -13,6 +13,7 @@ import {
13
13
  viewportWindowContentScale,
14
14
  } from "../utils/viewportPlacement";
15
15
  import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
16
+ import { resolveBackgroundImageStyle } from "../utils/background-image-style";
16
17
  import { mergeScene3dConfig } from "@arronqzy/view-scene3d";
17
18
  import { Scene3dNodeContent } from "@arronqzy/view-scene3d/react";
18
19
  import {
@@ -660,7 +661,7 @@ function getNodeVisualStyle(element: PanelElement): React.CSSProperties {
660
661
  const style = element.style ?? {};
661
662
  return {
662
663
  backgroundColor: style.backgroundColor,
663
- backgroundImage: style.backgroundImage,
664
+ backgroundImage: resolveBackgroundImageStyle(style),
664
665
  backgroundSize: style.backgroundSize,
665
666
  backgroundPosition: style.backgroundPosition,
666
667
  borderWidth: style.borderWidth,
@@ -151,7 +151,7 @@ export function PanelConfigAudioSection({
151
151
  {renderFieldGroup(
152
152
  t("panel.config.groupAudioSource"),
153
153
  <>
154
- <label className="block space-y-1">
154
+ <label className="block space-y-1" data-config-field="audioRemoteUrl">
155
155
  <div>{t("panel.config.audioUrl")}</div>
156
156
  <Input
157
157
  value={selectedElement.audioRemoteUrl ?? ""}
@@ -161,9 +161,12 @@ export function PanelConfigAudioSection({
161
161
  audioSrc: e.target.value || selectedElement.audioSrc,
162
162
  })
163
163
  }
164
- placeholder="https://example.com/audio.mp3"
165
- className="h-7"
164
+ placeholder={t("panel.config.urlScopePlaceholder")}
165
+ className="h-7 font-mono text-[11px]"
166
166
  />
167
+ <p className="text-[10px] text-muted-foreground">
168
+ {t("panel.config.urlScopeHint")}
169
+ </p>
167
170
  </label>
168
171
  <div className="flex items-center gap-2">
169
172
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
@@ -117,7 +117,7 @@ export function PanelConfigChartSection({
117
117
  {renderFieldGroup(
118
118
  t("panel.config.groupData"),
119
119
  <>
120
- <label className="block space-y-1">
120
+ <label className="block space-y-1" data-config-field="chart.labelsText">
121
121
  <div>{t("panel.config.labelsCsv")}</div>
122
122
  <Input
123
123
  value={getChartLabelsDisplayText(selectedElement.chart)}
@@ -126,11 +126,15 @@ export function PanelConfigChartSection({
126
126
  labelsText: e.target.value,
127
127
  })
128
128
  }
129
+ placeholder={t("panel.config.labelsCsvPlaceholder")}
129
130
  className="h-7"
130
131
  />
132
+ <p className="text-[10px] text-muted-foreground">
133
+ {t("panel.config.scopeTemplateHint")}
134
+ </p>
131
135
  </label>
132
136
 
133
- <label className="block space-y-1">
137
+ <label className="block space-y-1" data-config-field="chart.valuesText">
134
138
  <div>{t("panel.config.valuesCsv")}</div>
135
139
  <Input
136
140
  value={getChartValuesDisplayText(selectedElement.chart)}
@@ -139,8 +143,12 @@ export function PanelConfigChartSection({
139
143
  valuesText: e.target.value,
140
144
  })
141
145
  }
146
+ placeholder={t("panel.config.valuesCsvPlaceholder")}
142
147
  className="h-7"
143
148
  />
149
+ <p className="text-[10px] text-muted-foreground">
150
+ {t("panel.config.scopeTemplateHint")}
151
+ </p>
144
152
  </label>
145
153
  </>
146
154
  )}
@@ -1113,12 +1113,18 @@ export function PanelConfigMultiSelect({
1113
1113
  ) : null}
1114
1114
  {el.materialType === "audio" ? (
1115
1115
  <div className="grid grid-cols-2 gap-2">
1116
- <label className="block space-y-1 col-span-2">
1116
+ <label className="block space-y-1 col-span-2" data-config-field="audioRemoteUrl">
1117
1117
  <div>{t("panel.config.audioUrl")}</div>
1118
1118
  <Input
1119
- className="h-7"
1119
+ className="h-7 font-mono text-[11px]"
1120
1120
  value={el.audioRemoteUrl ?? ""}
1121
- onChange={(e) => updateElement(el.id, { audioRemoteUrl: e.target.value || undefined })}
1121
+ placeholder={t("panel.config.urlScopePlaceholder")}
1122
+ onChange={(e) =>
1123
+ updateElement(el.id, {
1124
+ audioRemoteUrl: e.target.value || undefined,
1125
+ audioSrc: e.target.value || el.audioSrc,
1126
+ })
1127
+ }
1122
1128
  />
1123
1129
  </label>
1124
1130
  <label className="block space-y-1">
@@ -1156,12 +1162,18 @@ export function PanelConfigMultiSelect({
1156
1162
  </div>
1157
1163
  ) : null}
1158
1164
  {el.materialType === "video" ? (
1159
- <label className="block space-y-1">
1165
+ <label className="block space-y-1" data-config-field="videoRemoteUrl">
1160
1166
  <div>{t("panel.config.videoUrl")}</div>
1161
1167
  <Input
1162
- className="h-7"
1168
+ className="h-7 font-mono text-[11px]"
1163
1169
  value={el.videoRemoteUrl ?? ""}
1164
- onChange={(e) => updateElement(el.id, { videoRemoteUrl: e.target.value || undefined })}
1170
+ placeholder={t("panel.config.urlScopePlaceholder")}
1171
+ onChange={(e) =>
1172
+ updateElement(el.id, {
1173
+ videoRemoteUrl: e.target.value || undefined,
1174
+ videoSrc: e.target.value || el.videoSrc,
1175
+ })
1176
+ }
1165
1177
  />
1166
1178
  </label>
1167
1179
  ) : null}
@@ -69,6 +69,7 @@ export function PanelConfigScene3dSection({
69
69
  [element.scene3d]
70
70
  );
71
71
  const fileInputRef = useRef<HTMLInputElement | null>(null);
72
+ const [modelUrlDraft, setModelUrlDraft] = useState("");
72
73
  const [nameQuery, setNameQuery] = useState<Record<string, string>>({});
73
74
  const [openTagKeys, setOpenTagKeys] = useState<Record<string, boolean>>({});
74
75
  const patch = useCallback(
@@ -96,6 +97,25 @@ export function PanelConfigScene3dSection({
96
97
  patch({ models: nextModels });
97
98
  };
98
99
 
100
+ const handleAddModelByUrl = () => {
101
+ const url = modelUrlDraft.trim();
102
+ if (!url) return;
103
+ const fileName = url.split(/[/?#]/).filter(Boolean).pop() || "model";
104
+ patch({
105
+ models: [
106
+ ...config.models,
107
+ {
108
+ id: randomId("mdl"),
109
+ label: fileName.replace(/\.[^.]+$/, "") || fileName,
110
+ url,
111
+ fileName,
112
+ format: inferModelFormat(fileName),
113
+ },
114
+ ],
115
+ });
116
+ setModelUrlDraft("");
117
+ };
118
+
99
119
  const updateModelAnimation = (
100
120
  id: string,
101
121
  next: Partial<Scene3dModelAnimationRule>
@@ -382,6 +402,30 @@ export function PanelConfigScene3dSection({
382
402
  }}
383
403
  />
384
404
  </div>
405
+ <label className="block space-y-1" data-config-field="scene3d.modelUrl">
406
+ <div className="text-[10px] text-muted-foreground">
407
+ {t("panel.config.scene3dModelUrl")}
408
+ </div>
409
+ <div className="flex gap-1.5">
410
+ <Input
411
+ value={modelUrlDraft}
412
+ onChange={(e) => setModelUrlDraft(e.target.value)}
413
+ placeholder={t("panel.config.urlScopePlaceholder")}
414
+ className="h-7 flex-1 font-mono text-[11px]"
415
+ />
416
+ <Button
417
+ type="button"
418
+ size="sm"
419
+ variant="outline"
420
+ className="h-7 shrink-0 text-[11px]"
421
+ disabled={!modelUrlDraft.trim()}
422
+ onClick={handleAddModelByUrl}
423
+ >
424
+ {t("common.add")}
425
+ </Button>
426
+ </div>
427
+ <p className="text-[10px] text-muted-foreground">{t("panel.config.urlScopeHint")}</p>
428
+ </label>
385
429
 
386
430
  {config.models.length === 0 ? (
387
431
  <div className="rounded border border-dashed border-border/70 px-2 py-3 text-[11px] text-muted-foreground">
@@ -432,6 +476,34 @@ export function PanelConfigScene3dSection({
432
476
  </Button>
433
477
  </div>
434
478
  <div className="text-[10px] text-muted-foreground">{model.fileName ?? model.url.slice(0, 48)}</div>
479
+ <label className="block space-y-0.5">
480
+ <span className="text-[10px] text-muted-foreground">
481
+ {t("panel.config.scene3dModelUrl")}
482
+ </span>
483
+ <Input
484
+ value={model.url}
485
+ data-config-field="scene3d.models.url"
486
+ className="h-7 font-mono text-[10px]"
487
+ placeholder={t("panel.config.urlScopePlaceholder")}
488
+ onChange={(e) => {
489
+ const nextUrl = e.target.value;
490
+ const fileName =
491
+ nextUrl.split(/[/?#]/).filter(Boolean).pop() || model.fileName || "model";
492
+ patch({
493
+ models: config.models.map((m) =>
494
+ m.id === model.id
495
+ ? {
496
+ ...m,
497
+ url: nextUrl,
498
+ fileName,
499
+ format: inferModelFormat(fileName),
500
+ }
501
+ : m
502
+ ),
503
+ });
504
+ }}
505
+ />
506
+ </label>
435
507
  <div className="grid grid-cols-3 gap-1">
436
508
  {(["x", "y", "z"] as const).map((axis, index) => (
437
509
  <Input
@@ -10,6 +10,7 @@ import {
10
10
  } from "@arronqzy/ui";
11
11
  import type { PanelElement, PanelElementStyle } from "../../types";
12
12
  import { readFileAsDataUrl, runBusyTask } from "../../utils/async-work";
13
+ import { patchBackgroundImageFromInput } from "../../utils/background-image-style";
13
14
  import { getPanelMessages } from "../../constants/messages";
14
15
  import { type ConfigSectionHelpers, type UpdateElement } from "./helpers";
15
16
 
@@ -88,14 +89,23 @@ export function PanelConfigStyleSections({
88
89
  {renderFieldGroup(
89
90
  t("panel.config.groupBgLayout"),
90
91
  <>
91
- <label className="block space-y-1">
92
+ <label className="block space-y-1" data-config-field="style.backgroundImage">
92
93
  <div>{t("panel.config.backgroundImage")}</div>
93
94
  <Input
94
- value={selectedElement.style?.backgroundImage ?? ""}
95
- onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
96
- placeholder='url("https://...") / linear-gradient(...)'
97
- className="h-7"
95
+ value={
96
+ selectedElement.style?.backgroundImageRemoteUrl ??
97
+ selectedElement.style?.backgroundImage ??
98
+ ""
99
+ }
100
+ onChange={(e) =>
101
+ updateSelectedStyle(patchBackgroundImageFromInput(e.target.value))
102
+ }
103
+ placeholder={t("panel.config.urlScopePlaceholder")}
104
+ className="h-7 font-mono text-[11px]"
98
105
  />
106
+ <p className="text-[10px] text-muted-foreground">
107
+ {t("panel.config.urlScopeHint")}
108
+ </p>
99
109
  </label>
100
110
  <div className="flex items-center gap-2">
101
111
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
@@ -110,6 +110,25 @@ export function PanelConfigTextSection({
110
110
  });
111
111
  }}
112
112
  />
113
+ <label className="block space-y-1">
114
+ <div>{t("panel.config.textHtmlSource")}</div>
115
+ <textarea
116
+ data-config-field="textHtml"
117
+ value={selectedElement.textHtml ?? ""}
118
+ onChange={(e) =>
119
+ updateSelectedText({
120
+ textHtml: e.target.value || "<p><br/></p>",
121
+ })
122
+ }
123
+ rows={4}
124
+ spellCheck={false}
125
+ placeholder={t("panel.config.textHtmlSourcePlaceholder")}
126
+ 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
+ />
128
+ <p className="text-[10px] text-muted-foreground">
129
+ {t("panel.config.scopeTemplateHint")}
130
+ </p>
131
+ </label>
113
132
  </>
114
133
  )}
115
134
  {renderFieldGroup(
@@ -69,7 +69,7 @@ export function PanelConfigVideoSection({
69
69
  {renderFieldGroup(
70
70
  t("panel.config.groupVideoSource"),
71
71
  <>
72
- <label className="block space-y-1">
72
+ <label className="block space-y-1" data-config-field="videoRemoteUrl">
73
73
  <div>{t("panel.config.videoUrl")}</div>
74
74
  <Input
75
75
  value={selectedElement.videoRemoteUrl ?? ""}
@@ -79,9 +79,12 @@ export function PanelConfigVideoSection({
79
79
  videoSrc: e.target.value || selectedElement.videoSrc,
80
80
  })
81
81
  }
82
- placeholder="https://example.com/video.mp4"
83
- className="h-7"
82
+ placeholder={t("panel.config.urlScopePlaceholder")}
83
+ className="h-7 font-mono text-[11px]"
84
84
  />
85
+ <p className="text-[10px] text-muted-foreground">
86
+ {t("panel.config.urlScopeHint")}
87
+ </p>
85
88
  </label>
86
89
  <div className="flex items-center gap-2">
87
90
  <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
@@ -0,0 +1,39 @@
1
+ import type { PanelElementStyle } from "../types";
2
+
3
+ /** Build a CSS `url('...')` value from a plain URL / template string. */
4
+ export function cssUrlFromValue(value: string): string {
5
+ const trimmed = value.trim();
6
+ if (!trimmed) return "";
7
+ if (/^url\(/i.test(trimmed) || /gradient\(/i.test(trimmed)) return trimmed;
8
+ const escaped = trimmed.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
9
+ return `url('${escaped}')`;
10
+ }
11
+
12
+ /** Prefer explicit backgroundImage; fall back to remote URL (supports scope templates after resolve). */
13
+ export function resolveBackgroundImageStyle(
14
+ style?: PanelElementStyle | null
15
+ ): string | undefined {
16
+ if (!style) return undefined;
17
+ if (style.backgroundImage?.trim()) return style.backgroundImage;
18
+ if (style.backgroundImageRemoteUrl?.trim()) {
19
+ return cssUrlFromValue(style.backgroundImageRemoteUrl);
20
+ }
21
+ return undefined;
22
+ }
23
+
24
+ /** Normalize config input: plain URL/template → both css + remote; css/gradient → backgroundImage only. */
25
+ export function patchBackgroundImageFromInput(
26
+ value: string
27
+ ): Partial<PanelElementStyle> {
28
+ const trimmed = value.trim();
29
+ if (!trimmed) {
30
+ return { backgroundImage: undefined, backgroundImageRemoteUrl: undefined };
31
+ }
32
+ if (/^url\(/i.test(trimmed) || /gradient\(/i.test(trimmed)) {
33
+ return { backgroundImage: trimmed, backgroundImageRemoteUrl: undefined };
34
+ }
35
+ return {
36
+ backgroundImage: cssUrlFromValue(trimmed),
37
+ backgroundImageRemoteUrl: trimmed,
38
+ };
39
+ }
@@ -90,3 +90,15 @@ export function materializeChartLabelsFromScope(
90
90
  }
91
91
  return undefined;
92
92
  }
93
+
94
+ /** Prefer valuesText templates; expand [...{scope}] / {scope} into number[]. */
95
+ export function materializeChartValuesFromScope(
96
+ chart: PanelChartConfig | undefined,
97
+ scope: unknown
98
+ ): number[] | undefined {
99
+ if (!chart) return undefined;
100
+ if (chart.valuesText !== undefined) {
101
+ return materializeChartValuesText(chart.valuesText, scope);
102
+ }
103
+ return undefined;
104
+ }
@@ -1,7 +1,7 @@
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 { materializeChartLabelsFromScope } from "./scope-template-chart";
4
+ import { materializeChartLabelsFromScope, materializeChartValuesFromScope } from "./scope-template-chart";
5
5
  import {
6
6
  SCOPE_SPREAD_TEMPLATE_RE,
7
7
  evaluateSpreadScopeExpression,
@@ -11,10 +11,12 @@ import {
11
11
  resolveSpreadScopePath,
12
12
  } from "./scope-template-spread";
13
13
 
14
- const SCOPE_TEMPLATE_RE = /\{[^}]+\}|\[\.\.\.\{[^}]+\}\]/;
14
+ const SCOPE_TOKEN_RE = /\{(scope[^}]*)\}/g;
15
15
 
16
16
  export function hasScopeTemplate(value: string): boolean {
17
- return SCOPE_TEMPLATE_RE.test(value);
17
+ SCOPE_TOKEN_RE.lastIndex = 0;
18
+ SCOPE_SPREAD_TEMPLATE_RE.lastIndex = 0;
19
+ return SCOPE_TOKEN_RE.test(value) || SCOPE_SPREAD_TEMPLATE_RE.test(value);
18
20
  }
19
21
 
20
22
  export function evaluateScopeExpression(
@@ -31,6 +33,19 @@ export function evaluateScopeExpression(
31
33
  }
32
34
  }
33
35
 
36
+ function stringifyScopeValue(value: unknown): string {
37
+ if (value === null || value === undefined) return "";
38
+ if (typeof value === "string") return value;
39
+ if (typeof value === "number" || typeof value === "boolean") {
40
+ return String(value);
41
+ }
42
+ try {
43
+ return JSON.stringify(value);
44
+ } catch {
45
+ return String(value);
46
+ }
47
+ }
48
+
34
49
  export function evaluateScopeTemplate(
35
50
  template: string,
36
51
  scope: unknown
@@ -45,19 +60,10 @@ export function evaluateScopeTemplate(
45
60
  }
46
61
  );
47
62
 
48
- return withSpread.replace(/\{([^}]+)\}/g, (_: string, rawExpr: string) => {
49
- const value = evaluateScopeExpression(rawExpr, scope);
50
- if (value === null || value === undefined) return "";
51
- if (typeof value === "string") return value;
52
- if (typeof value === "number" || typeof value === "boolean") {
53
- return String(value);
54
- }
55
- try {
56
- return JSON.stringify(value);
57
- } catch {
58
- return String(value);
59
- }
60
- });
63
+ // Only `{scope...}` tokens — keep ECharts / CSS braces like `{b}` intact.
64
+ return withSpread.replace(SCOPE_TOKEN_RE, (_: string, rawExpr: string) =>
65
+ stringifyScopeValue(evaluateScopeExpression(rawExpr, scope))
66
+ );
61
67
  }
62
68
 
63
69
  function tryParseJson(text: string): unknown | undefined {
@@ -85,7 +91,7 @@ function materializeTemplateValue(template: string, scope: unknown): unknown {
85
91
  );
86
92
  }
87
93
 
88
- const singleMatch = /^\{([^}]+)\}$/.exec(trimmed);
94
+ const singleMatch = /^\{(scope[^}]*)\}$/.exec(trimmed);
89
95
  if (singleMatch) {
90
96
  return evaluateScopeExpression(singleMatch[1]!, scope);
91
97
  }
@@ -148,6 +154,11 @@ export function resolvePanelElementScope(
148
154
  resolved.chart.labels = labels;
149
155
  delete resolved.chart.labelsText;
150
156
  }
157
+ const values = materializeChartValuesFromScope(element.chart, scope);
158
+ if (values !== undefined) {
159
+ resolved.chart.values = values;
160
+ delete resolved.chart.valuesText;
161
+ }
151
162
  }
152
163
  if (resolved.table) {
153
164
  const hasDynamicSource =