@arronqzy/react-view 1.0.12 → 1.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/react-view",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -30,13 +30,13 @@
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/blueprint-dsl": "1.0.5",
34
33
  "@arronqzy/ui": "1.0.5",
35
- "@arronqzy/i18n": "0.1.4",
36
- "@arronqzy/webllm-assistant": "0.1.6",
37
- "@arronqzy/react-blueprint": "1.0.6",
34
+ "@arronqzy/react-blueprint": "1.0.7",
38
35
  "@arronqzy/view-table": "0.1.3",
39
- "@arronqzy/rx-store": "1.0.4"
36
+ "@arronqzy/rx-store": "1.0.4",
37
+ "@arronqzy/webllm-assistant": "0.1.7",
38
+ "@arronqzy/blueprint-dsl": "1.0.6",
39
+ "@arronqzy/i18n": "0.1.5"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/react": "^19.0.1",
@@ -35,6 +35,7 @@ import {
35
35
  parsePanelLayers,
36
36
  resolvePreviewLayerElements,
37
37
  } from "./utils/panelStateIO";
38
+ import { applyPreviewSceneFill, readOutputScale } from "./utils/outputScale";
38
39
 
39
40
  const PREVIEW_BOOT_PHASES: PageLifecyclePhase[] = ["mounted"];
40
41
 
@@ -76,6 +77,8 @@ export function ReactViewOnlinePreview({
76
77
  >([]);
77
78
  const [layoutRevision, setLayoutRevision] = useState(0);
78
79
  const [layoutReady, setLayoutReady] = useState(false);
80
+ const [outputScale, setOutputScale] = useState(() => readOutputScale());
81
+ const [fillScale, setFillScale] = useState({ scaleX: 1, scaleY: 1 });
79
82
 
80
83
  const sceneRef = useRef<HTMLDivElement | null>(null);
81
84
 
@@ -179,25 +182,38 @@ export function ReactViewOnlinePreview({
179
182
 
180
183
  const displayElements = useMemo(
181
184
  () =>
182
- scopedLayerElements.map((el) => ({
183
- ...el,
184
- x: el.x - sceneBounds.minX,
185
- y: el.y - sceneBounds.minY,
186
- })),
187
- [sceneBounds.minX, sceneBounds.minY, scopedLayerElements]
185
+ scopedLayerElements.map((el) => {
186
+ const x = el.x - sceneBounds.minX;
187
+ const y = el.y - sceneBounds.minY;
188
+ if (outputScale) return { ...el, x, y };
189
+ return {
190
+ ...el,
191
+ x: x * fillScale.scaleX,
192
+ y: y * fillScale.scaleY,
193
+ width: el.width * fillScale.scaleX,
194
+ height: el.height * fillScale.scaleY,
195
+ };
196
+ }),
197
+ [
198
+ fillScale.scaleX,
199
+ fillScale.scaleY,
200
+ outputScale,
201
+ sceneBounds.minX,
202
+ sceneBounds.minY,
203
+ scopedLayerElements,
204
+ ]
188
205
  );
189
206
 
190
207
  const applySceneFit = useCallback(() => {
191
- const scene = sceneRef.current;
192
- if (!scene) return;
193
- const vw = window.innerWidth || 1;
194
- const vh = window.innerHeight || 1;
195
- const sw = Math.max(1, sceneBounds.width);
196
- const sh = Math.max(1, sceneBounds.height);
197
- const scaleX = vw / sw;
198
- const scaleY = vh / sh;
199
- if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return;
200
- scene.style.transform = `scale(${scaleX}, ${scaleY})`;
208
+ const enabled = readOutputScale();
209
+ setOutputScale(enabled);
210
+ const fill = applyPreviewSceneFill(
211
+ sceneRef.current,
212
+ sceneBounds.width,
213
+ sceneBounds.height,
214
+ enabled
215
+ );
216
+ setFillScale({ scaleX: fill.scaleX, scaleY: fill.scaleY });
201
217
  setLayoutRevision((v) => v + 1);
202
218
  notifyPreviewLayoutChanged();
203
219
  }, [sceneBounds.height, sceneBounds.width]);
@@ -304,9 +320,16 @@ export function ReactViewOnlinePreview({
304
320
  id="preview-scene"
305
321
  className="relative shrink-0 origin-top-left"
306
322
  style={{
307
- width: sceneBounds.width,
308
- height: sceneBounds.height,
323
+ width: outputScale
324
+ ? sceneBounds.width
325
+ : sceneBounds.width * fillScale.scaleX,
326
+ height: outputScale
327
+ ? sceneBounds.height
328
+ : sceneBounds.height * fillScale.scaleY,
309
329
  transformOrigin: "left top",
330
+ transform: outputScale
331
+ ? `scale(${fillScale.scaleX}, ${fillScale.scaleY})`
332
+ : "none",
310
333
  }}
311
334
  >
312
335
  <ElementsLayer
@@ -14,6 +14,7 @@ import { ElementsLayer } from "./components/ElementsLayer";
14
14
  import { SelectLayer } from "./components/SelectLayer";
15
15
  import { MoveableLayer } from "./components/MoveableLayer";
16
16
  import { buildChartOption, CHART_TYPES } from "./utils/chartOptionBuilder";
17
+ import { readOutputScale, writeOutputScale } from "./utils/outputScale";
17
18
  import { MaterialSidebar } from "./components/MaterialSidebar";
18
19
  import { AssistantChatPanel } from "./ai/AssistantChatPanel";
19
20
  import {
@@ -94,6 +95,7 @@ import {
94
95
  EmptyTitle,
95
96
  Input,
96
97
  Menubar,
98
+ MenubarCheckboxItem,
97
99
  MenubarContent,
98
100
  MenubarItem,
99
101
  MenubarMenu,
@@ -460,6 +462,7 @@ export function ReactViewPanel({
460
462
  const [titleIconZoom, setTitleIconZoom] = useState(1.6);
461
463
  const [pendingPreviewLayerId, setPendingPreviewLayerId] = useState<string | null>(null);
462
464
  const [panelFontSize, setPanelFontSize] = useState<"sm" | "md" | "lg">("md");
465
+ const [outputScale, setOutputScale] = useState(() => readOutputScale());
463
466
  const [blueprintOpen, setBlueprintOpen] = useState(false);
464
467
  const [assistantOpen, setAssistantOpen] = useState(false);
465
468
  const [blueprintGraph, setBlueprintGraph] = useState(() => BlueprintGraph.empty());
@@ -570,6 +573,10 @@ export function ReactViewPanel({
570
573
  }
571
574
  }, [panelFontSize]);
572
575
 
576
+ useEffect(() => {
577
+ writeOutputScale(outputScale);
578
+ }, [outputScale]);
579
+
573
580
  useEffect(() => {
574
581
  try {
575
582
  const stored = localStorage.getItem(TITLE_ICON_STORAGE_KEY) ?? "";
@@ -1784,16 +1791,49 @@ export function ReactViewPanel({
1784
1791
  });
1785
1792
  }
1786
1793
  var scene = document.getElementById("preview-scene");
1794
+ var root = document.getElementById("preview-root");
1795
+ var outputScale = ${outputScale ? "true" : "false"};
1796
+ function rememberBox(node) {
1797
+ if (node.dataset.origLeft != null) return;
1798
+ node.dataset.origLeft = String(parseFloat(node.style.left) || 0);
1799
+ node.dataset.origTop = String(parseFloat(node.style.top) || 0);
1800
+ node.dataset.origWidth = String(parseFloat(node.style.width) || node.offsetWidth || 0);
1801
+ node.dataset.origHeight = String(parseFloat(node.style.height) || node.offsetHeight || 0);
1802
+ }
1787
1803
  function fitScene() {
1788
1804
  if (!scene) return;
1789
1805
  var vw = window.innerWidth || 1;
1790
1806
  var vh = window.innerHeight || 1;
1791
1807
  var sw = ${sceneWidth} || 1;
1792
1808
  var sh = ${sceneHeight} || 1;
1793
- // 铺满预览窗口:宽高分别缩放,保证内容贴住四边
1794
1809
  var scaleX = vw / sw;
1795
1810
  var scaleY = vh / sh;
1796
- scene.style.transform = "translate(0px,0px) scale(" + scaleX + "," + scaleY + ")";
1811
+ if (root) root.style.overflow = "hidden";
1812
+ var nodes = scene.querySelectorAll("[data-element-id]");
1813
+ if (outputScale) {
1814
+ scene.style.width = sw + "px";
1815
+ scene.style.height = sh + "px";
1816
+ scene.style.transform = "translate(0px,0px) scale(" + scaleX + "," + scaleY + ")";
1817
+ nodes.forEach(function (node) {
1818
+ rememberBox(node);
1819
+ node.style.left = node.dataset.origLeft + "px";
1820
+ node.style.top = node.dataset.origTop + "px";
1821
+ node.style.width = node.dataset.origWidth + "px";
1822
+ node.style.height = node.dataset.origHeight + "px";
1823
+ });
1824
+ return;
1825
+ }
1826
+ // 块仍按视口撑满,但不对整棵内容做 transform,字体保持原尺寸
1827
+ scene.style.width = vw + "px";
1828
+ scene.style.height = vh + "px";
1829
+ scene.style.transform = "none";
1830
+ nodes.forEach(function (node) {
1831
+ rememberBox(node);
1832
+ node.style.left = (Number(node.dataset.origLeft) * scaleX) + "px";
1833
+ node.style.top = (Number(node.dataset.origTop) * scaleY) + "px";
1834
+ node.style.width = (Number(node.dataset.origWidth) * scaleX) + "px";
1835
+ node.style.height = (Number(node.dataset.origHeight) * scaleY) + "px";
1836
+ });
1797
1837
  }
1798
1838
  fitScene();
1799
1839
  window.addEventListener("resize", fitScene);
@@ -2013,6 +2053,14 @@ export function ReactViewPanel({
2013
2053
  <MenubarRadioItem value="zh-CN">{t("panel.theme.zhCN")}</MenubarRadioItem>
2014
2054
  <MenubarRadioItem value="en-US">{t("panel.theme.enUS")}</MenubarRadioItem>
2015
2055
  </MenubarRadioGroup>
2056
+ <MenubarSeparator />
2057
+ <MenubarCheckboxItem
2058
+ checked={outputScale}
2059
+ onCheckedChange={(checked) => setOutputScale(checked === true)}
2060
+ title={t("panel.menubar.outputScaleHint")}
2061
+ >
2062
+ {t("panel.menubar.outputScale")}
2063
+ </MenubarCheckboxItem>
2016
2064
  </MenubarContent>
2017
2065
  </MenubarMenu>
2018
2066
  </Menubar>
@@ -3176,6 +3224,8 @@ export function ReactViewPanel({
3176
3224
  configFocus={sidebarConfigFocus}
3177
3225
  executionLog={executionLogView}
3178
3226
  selectedBlueprintNode={selectedBlueprintNode}
3227
+ blueprintGraphNodes={blueprintGraph.document.nodes}
3228
+ blueprintGraphEdges={blueprintGraph.document.edges}
3179
3229
  allowFalseSignalPropagation={
3180
3230
  blueprintGraph.document.allowFalseSignalPropagation ?? false
3181
3231
  }
@@ -104,8 +104,8 @@ export function PanelConfigSidebar({
104
104
  "scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
105
105
  const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
106
106
  nodeInfo: true,
107
- styleBackground: true,
108
- styleBorder: true,
107
+ styleBackground: false,
108
+ styleBorder: false,
109
109
  chartBasic: true,
110
110
  chartAdvanced: false,
111
111
  chartAdvancedHighFreq: true,
@@ -421,9 +421,21 @@ export function PanelConfigSidebar({
421
421
 
422
422
  useEffect(() => {
423
423
  return subscribeRevealPanelConfig((detail) => {
424
- for (const section of detail.sections) {
425
- setSectionExpanded(section, true);
426
- }
424
+ setExpandedSections((prev) => {
425
+ const next = { ...prev };
426
+ for (const section of detail.sections) next[section] = true;
427
+ next.nodeMore = true;
428
+ next.styleBgLayout = true;
429
+ next.chartDisplayMore = true;
430
+ next.chartTooltip = true;
431
+ next.chartAxes = true;
432
+ next.textInputAbility = true;
433
+ next.audioDisplayStyle = true;
434
+ next.geometryCanvasScript = true;
435
+ next.geometrySketch = true;
436
+ next.referenceCopyStrategy = true;
437
+ return next;
438
+ });
427
439
  window.requestAnimationFrame(() => {
428
440
  const field = detail.fields[0];
429
441
  if (field) highlightConfigField(field);
@@ -475,16 +487,48 @@ export function PanelConfigSidebar({
475
487
  const renderFieldGroup = (
476
488
  title: string,
477
489
  children: React.ReactNode,
478
- hint?: React.ReactNode
479
- ) => (
480
- <div className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5">
481
- <div className="flex items-center gap-1">
490
+ hint?: React.ReactNode,
491
+ options?: { groupKey?: string; defaultOpen?: boolean }
492
+ ) => {
493
+ const groupKey = options?.groupKey;
494
+ const defaultOpen = options?.defaultOpen ?? true;
495
+ const header = (
496
+ <>
482
497
  <div className="text-[11px] font-semibold text-muted-foreground">{title}</div>
483
498
  {hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
484
- </div>
485
- {children}
486
- </div>
487
- );
499
+ </>
500
+ );
501
+ if (!groupKey) {
502
+ return (
503
+ <div className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5">
504
+ <div className="flex items-center gap-1">{header}</div>
505
+ {children}
506
+ </div>
507
+ );
508
+ }
509
+ const open = hasSearch ? true : isSectionExpanded(groupKey, defaultOpen);
510
+ return (
511
+ <Collapsible
512
+ open={open}
513
+ onOpenChange={(next) => setSectionExpanded(groupKey, next)}
514
+ className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5"
515
+ >
516
+ <div className="flex items-center gap-1.5">
517
+ <CollapsibleTrigger asChild>
518
+ <button
519
+ type="button"
520
+ className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
521
+ aria-label={open ? t("panel.config.collapseGroup", { title }) : t("panel.config.expandGroup", { title })}
522
+ >
523
+ {open ? "▾" : "▸"}
524
+ </button>
525
+ </CollapsibleTrigger>
526
+ {header}
527
+ </div>
528
+ <CollapsibleContent className="space-y-2.5">{children}</CollapsibleContent>
529
+ </Collapsible>
530
+ );
531
+ };
488
532
  const isNodeCardExpanded = useCallback(
489
533
  (id: string) => expandedNodeCards[id] ?? true,
490
534
  [expandedNodeCards]
@@ -1369,15 +1413,19 @@ export function PanelConfigSidebar({
1369
1413
  className="h-7"
1370
1414
  type="number"
1371
1415
  min={1}
1372
- value={el.chart?.barWidth ?? 24}
1373
- onChange={(e) =>
1416
+ placeholder={t("panel.config.barWidthAuto")}
1417
+ value={el.chart?.barWidth ?? ""}
1418
+ onChange={(e) => {
1419
+ const raw = e.target.value.trim();
1374
1420
  updateElement(el.id, {
1375
1421
  chart: {
1376
1422
  ...(el.chart ?? {}),
1377
- barWidth: Math.max(1, Number(e.target.value) || 1),
1423
+ barWidth: raw
1424
+ ? Math.max(1, Number(raw) || 1)
1425
+ : undefined,
1378
1426
  },
1379
- })
1380
- }
1427
+ });
1428
+ }}
1381
1429
  />
1382
1430
  </label>
1383
1431
  ) : null}
@@ -1879,21 +1927,6 @@ export function PanelConfigSidebar({
1879
1927
  {readonlyReason}
1880
1928
  </div>
1881
1929
  ) : null}
1882
- <div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
1883
- <label className="flex items-center gap-2">
1884
- <Checkbox
1885
- checked={selectedElement.locked === true}
1886
- disabled={!canToggleNodeLock}
1887
- className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
1888
- onCheckedChange={(checked) =>
1889
- updateElement(selectedElement.id, {
1890
- locked: checked === true,
1891
- })
1892
- }
1893
- />
1894
- <span>{t("panel.config.lockNode")}</span>
1895
- </label>
1896
- </div>
1897
1930
  <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
1898
1931
  <div className="space-y-3.5 text-xs">
1899
1932
  {renderSection(
@@ -1913,7 +1946,7 @@ export function PanelConfigSidebar({
1913
1946
  className="h-7"
1914
1947
  />
1915
1948
  </label>
1916
- <div className="grid grid-cols-3 gap-2">
1949
+ <div className="grid grid-cols-2 gap-2">
1917
1950
  <label className="block space-y-1" data-config-field="x">
1918
1951
  <div>X</div>
1919
1952
  <Input
@@ -1941,20 +1974,7 @@ export function PanelConfigSidebar({
1941
1974
  />
1942
1975
  </label>
1943
1976
  </div>
1944
- <div className="grid grid-cols-3 gap-2">
1945
- <label className="block space-y-1" data-config-field="rotate">
1946
- <div>{t("panel.config.rotate")}</div>
1947
- <Input
1948
- type="number"
1949
- value={selectedElement.rotate ?? 0}
1950
- onChange={(e) =>
1951
- updateElement(selectedElement.id, {
1952
- rotate: Number(e.target.value) || 0,
1953
- })
1954
- }
1955
- className="h-7"
1956
- />
1957
- </label>
1977
+ <div className="grid grid-cols-2 gap-2">
1958
1978
  <label className="block space-y-1" data-config-field="width">
1959
1979
  <div>{t("panel.config.width")}</div>
1960
1980
  <Input
@@ -1985,6 +2005,24 @@ export function PanelConfigSidebar({
1985
2005
  />
1986
2006
  </label>
1987
2007
  </div>
2008
+ {renderFieldGroup(
2009
+ t("panel.config.groupNodeMore"),
2010
+ <>
2011
+ <div className="grid grid-cols-2 gap-2">
2012
+ <label className="block space-y-1" data-config-field="rotate">
2013
+ <div>{t("panel.config.rotate")}</div>
2014
+ <Input
2015
+ type="number"
2016
+ value={selectedElement.rotate ?? 0}
2017
+ onChange={(e) =>
2018
+ updateElement(selectedElement.id, {
2019
+ rotate: Number(e.target.value) || 0,
2020
+ })
2021
+ }
2022
+ className="h-7"
2023
+ />
2024
+ </label>
2025
+ </div>
1988
2026
  <div className="space-y-1.5">
1989
2027
  <div className="text-[11px] text-muted-foreground">{t("panel.config.nodeZOrder")}</div>
1990
2028
  <div className="text-[11px] text-muted-foreground/90">
@@ -2023,205 +2061,177 @@ export function PanelConfigSidebar({
2023
2061
  </div>
2024
2062
  <div className="truncate text-muted-foreground">ID: {selectedElement.id}</div>
2025
2063
  <div className="text-muted-foreground">{t("panel.config.type")}: {selectedElement.materialType ?? selectedElement.id}</div>
2064
+ </>,
2065
+ undefined,
2066
+ { groupKey: "nodeMore", defaultOpen: false }
2067
+ )}
2026
2068
  </>,
2027
2069
  true,
2028
- [t("panel.config.name"), "id", t("panel.config.type"), t("panel.layers.lockShort"), "locked", "name"]
2070
+ [t("panel.config.name"), "id", t("panel.config.type"), t("panel.config.rotate"), t("panel.config.nodeZOrder"), t("panel.layers.lockShort"), "locked", "name"]
2029
2071
  )}
2072
+ </div>
2073
+ </fieldset>
2074
+ <div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
2075
+ <label className="flex items-center gap-2">
2076
+ <Checkbox
2077
+ checked={selectedElement.locked === true}
2078
+ disabled={!canToggleNodeLock}
2079
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
2080
+ onCheckedChange={(checked) =>
2081
+ updateElement(selectedElement.id, {
2082
+ locked: checked === true,
2083
+ })
2084
+ }
2085
+ />
2086
+ <span>{t("panel.config.lockNode")}</span>
2087
+ </label>
2088
+ </div>
2089
+ <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
2090
+ <div className="space-y-3.5 text-xs">
2030
2091
 
2031
- {renderSection(
2032
- "styleBackground",
2033
- t("panel.config.sectionStyleBackground"),
2092
+ {isChartElement ? (
2034
2093
  <>
2035
- {renderFieldGroup(
2036
- t("panel.config.groupBgFill"),
2094
+ {renderSection(
2095
+ "chartBasic",
2096
+ t("panel.config.sectionChartBasic"),
2037
2097
  <>
2038
- {renderColorField(
2039
- t("panel.config.backgroundColor"),
2040
- selectedElement.style?.backgroundColor ?? "",
2041
- (next) => updateSelectedStyle({ backgroundColor: next || undefined })
2098
+ {renderFieldGroup(
2099
+ t("panel.config.groupBasicDisplay"),
2100
+ <>
2101
+ <label className="block space-y-1.5" data-config-field="chart.title">
2102
+ <div>{t("panel.config.title")}</div>
2103
+ <Input
2104
+ value={selectedElement.chart?.title ?? ""}
2105
+ onChange={(e) => updateSelectedChart({ title: e.target.value })}
2106
+ className="h-7"
2107
+ />
2108
+ </label>
2109
+
2110
+ {renderColorField(
2111
+ t("panel.config.primaryColor"),
2112
+ selectedElement.chart?.color ?? "#3b82f6",
2113
+ (next) => updateSelectedChart({ color: next || "#3b82f6" })
2114
+ )}
2115
+ </>
2042
2116
  )}
2043
- <label className="block space-y-1">
2044
- <div>{t("panel.config.backgroundImage")}</div>
2045
- <Input
2046
- value={selectedElement.style?.backgroundImage ?? ""}
2047
- onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
2048
- placeholder='url("https://...") / linear-gradient(...)'
2049
- className="h-7"
2050
- />
2051
- </label>
2052
- <div className="flex items-center gap-2">
2053
- <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
2054
- {t("panel.config.uploadImage")}
2055
- <Input
2056
- type="file"
2057
- accept="image/*"
2058
- className="hidden"
2059
- onChange={async (e) => {
2060
- const file = e.target.files?.[0];
2061
- e.currentTarget.value = "";
2062
- if (!file) return;
2063
- await handleUploadBackgroundImage(file);
2064
- }}
2065
- />
2066
- </label>
2067
- {uploadStatus ? (
2068
- <span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
2069
- ) : null}
2070
- </div>
2071
- </>
2072
- )}
2073
- {renderFieldGroup(
2074
- t("panel.config.groupBgLayout"),
2075
- <div className="grid grid-cols-2 gap-2">
2076
- <label className="block space-y-1">
2077
- <div>{t("panel.config.backgroundSize")}</div>
2078
- <Select
2079
- value={selectedElement.style?.backgroundSize ?? "__none__"}
2080
- onValueChange={(value) =>
2081
- updateSelectedStyle({
2082
- backgroundSize: value === "__none__" ? undefined : value,
2083
- })
2084
- }
2085
- >
2086
- <SelectTrigger className="h-7">
2087
- <SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
2088
- </SelectTrigger>
2089
- <SelectContent>
2090
- <SelectItem value="__none__">{t("common.default")}</SelectItem>
2091
- <SelectItem value="cover">cover</SelectItem>
2092
- <SelectItem value="contain">contain</SelectItem>
2093
- <SelectItem value="100% 100%">100% 100%</SelectItem>
2094
- <SelectItem value="auto">auto</SelectItem>
2095
- </SelectContent>
2096
- </Select>
2097
- </label>
2098
- <label className="block space-y-1">
2099
- <div>{t("panel.config.backgroundPosition")}</div>
2100
- <Select
2101
- value={selectedElement.style?.backgroundPosition ?? "__none__"}
2102
- onValueChange={(value) =>
2103
- updateSelectedStyle({
2104
- backgroundPosition: value === "__none__" ? undefined : value,
2105
- })
2106
- }
2107
- >
2108
- <SelectTrigger className="h-7">
2109
- <SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
2110
- </SelectTrigger>
2111
- <SelectContent>
2112
- <SelectItem value="__none__">{t("common.default")}</SelectItem>
2113
- <SelectItem value="center">center</SelectItem>
2114
- <SelectItem value="top">top</SelectItem>
2115
- <SelectItem value="bottom">bottom</SelectItem>
2116
- <SelectItem value="left">left</SelectItem>
2117
- <SelectItem value="right">right</SelectItem>
2118
- <SelectItem value="top left">top left</SelectItem>
2119
- <SelectItem value="top right">top right</SelectItem>
2120
- <SelectItem value="bottom left">bottom left</SelectItem>
2121
- <SelectItem value="bottom right">bottom right</SelectItem>
2122
- </SelectContent>
2123
- </Select>
2124
- </label>
2125
- </div>
2126
- )}
2127
- </>,
2128
- true,
2129
- [t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
2130
- )}
2117
+ {renderFieldGroup(
2118
+ t("panel.config.groupData"),
2119
+ <>
2120
+ <label className="block space-y-1">
2121
+ <div>{t("panel.config.labelsCsv")}</div>
2122
+ <Input
2123
+ value={getChartLabelsDisplayText(selectedElement.chart)}
2124
+ onChange={(e) =>
2125
+ updateSelectedChart({
2126
+ labelsText: e.target.value,
2127
+ })
2128
+ }
2129
+ className="h-7"
2130
+ />
2131
+ </label>
2131
2132
 
2132
- {renderSection(
2133
- "styleBorder",
2134
- t("panel.config.sectionStyleBorder"),
2135
- <>
2136
- {renderFieldGroup(
2137
- t("panel.config.groupBorderGeometry"),
2138
- <div className="grid grid-cols-2 gap-2">
2139
- <label className="block space-y-1">
2140
- <div>{t("panel.config.borderWidth")}</div>
2141
- <Input
2142
- type="number"
2143
- min={0}
2144
- value={selectedElement.style?.borderWidth ?? 0}
2145
- onChange={(e) =>
2146
- updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
2147
- }
2148
- className="h-7"
2149
- />
2150
- </label>
2151
- <label className="block space-y-1">
2152
- <div>{t("panel.config.borderRadius")}</div>
2153
- <Input
2154
- type="number"
2155
- min={0}
2156
- value={selectedElement.style?.borderRadius ?? 0}
2157
- onChange={(e) =>
2158
- updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
2159
- }
2160
- className="h-7"
2161
- />
2162
- </label>
2163
- </div>
2164
- )}
2165
- {renderFieldGroup(
2166
- t("panel.config.groupBorderVisual"),
2167
- <div className="grid grid-cols-2 gap-2">
2168
- <label className="block space-y-1">
2169
- <div>{t("panel.config.borderStyle")}</div>
2170
- <Select
2171
- value={selectedElement.style?.borderStyle ?? "solid"}
2172
- onValueChange={(value) =>
2173
- updateSelectedStyle({
2174
- borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
2175
- })
2176
- }
2177
- >
2178
- <SelectTrigger className="h-7">
2179
- <SelectValue placeholder={t("panel.config.selectBorderStyle")} />
2180
- </SelectTrigger>
2181
- <SelectContent>
2182
- <SelectItem value="none">none</SelectItem>
2183
- <SelectItem value="solid">solid</SelectItem>
2184
- <SelectItem value="dashed">dashed</SelectItem>
2185
- <SelectItem value="dotted">dotted</SelectItem>
2186
- <SelectItem value="double">double</SelectItem>
2187
- </SelectContent>
2188
- </Select>
2189
- </label>
2190
- {renderColorField(
2191
- t("panel.config.borderColor"),
2192
- selectedElement.style?.borderColor ?? "",
2193
- (next) => updateSelectedStyle({ borderColor: next || undefined })
2194
- )}
2195
- </div>
2196
- )}
2197
- </>,
2198
- true,
2199
- [t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
2200
- )}
2201
-
2202
- {isChartElement ? (
2203
- <>
2204
- {renderSection(
2205
- "chartBasic",
2206
- t("panel.config.sectionChartBasic"),
2207
- <>
2208
- {renderFieldGroup(
2209
- t("panel.config.groupBasicDisplay"),
2210
- <>
2211
- <label className="block space-y-1.5" data-config-field="chart.title">
2212
- <div>{t("panel.config.title")}</div>
2133
+ <label className="block space-y-1">
2134
+ <div>{t("panel.config.valuesCsv")}</div>
2213
2135
  <Input
2214
- value={selectedElement.chart?.title ?? ""}
2215
- onChange={(e) => updateSelectedChart({ title: e.target.value })}
2136
+ value={getChartValuesDisplayText(selectedElement.chart)}
2137
+ onChange={(e) =>
2138
+ updateSelectedChart({
2139
+ valuesText: e.target.value,
2140
+ })
2141
+ }
2216
2142
  className="h-7"
2217
2143
  />
2218
2144
  </label>
2145
+ </>
2146
+ )}
2219
2147
 
2220
- {renderColorField(
2221
- t("panel.config.primaryColor"),
2222
- selectedElement.chart?.color ?? "#3b82f6",
2223
- (next) => updateSelectedChart({ color: next || "#3b82f6" })
2224
- )}
2148
+ {selectedChartType === "bar" ||
2149
+ selectedChartType === "line" ||
2150
+ selectedChartType === "area" ||
2151
+ selectedChartType === "pie" ? (
2152
+ renderFieldGroup(
2153
+ t("panel.config.groupSeries"),
2154
+ <>
2155
+ {selectedChartType === "bar" ? (
2156
+ <label className="block space-y-1">
2157
+ <div>{t("panel.config.barWidthPx")}</div>
2158
+ <Input
2159
+ type="number"
2160
+ min={1}
2161
+ placeholder={t("panel.config.barWidthAuto")}
2162
+ value={selectedElement.chart?.barWidth ?? ""}
2163
+ onChange={(e) => {
2164
+ const raw = e.target.value.trim();
2165
+ updateSelectedChart({
2166
+ barWidth: raw
2167
+ ? Math.max(1, Number(raw) || 1)
2168
+ : undefined,
2169
+ });
2170
+ }}
2171
+ className="h-7"
2172
+ />
2173
+ </label>
2174
+ ) : null}
2175
+
2176
+ {selectedChartType === "line" || selectedChartType === "area" ? (
2177
+ <label className="flex items-center gap-2">
2178
+ <Checkbox
2179
+ checked={selectedElement.chart?.smooth ?? true}
2180
+ className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
2181
+ onCheckedChange={(checked) =>
2182
+ updateSelectedChart({ smooth: checked === true })
2183
+ }
2184
+ />
2185
+ <span>{t("panel.config.smooth")}</span>
2186
+ </label>
2187
+ ) : null}
2188
+
2189
+ {selectedChartType === "pie" ? (
2190
+ <div className="grid grid-cols-2 gap-2">
2191
+ <label className="block space-y-1">
2192
+ <div>{t("panel.config.pieInnerRadiusPct")}</div>
2193
+ <Input
2194
+ type="number"
2195
+ min={0}
2196
+ max={99}
2197
+ value={selectedElement.chart?.pieInnerRadius ?? 30}
2198
+ onChange={(e) =>
2199
+ updateSelectedChart({
2200
+ pieInnerRadius: Math.max(
2201
+ 0,
2202
+ Math.min(99, Number(e.target.value) || 0)
2203
+ ),
2204
+ })
2205
+ }
2206
+ className="h-7"
2207
+ />
2208
+ </label>
2209
+ <label className="block space-y-1">
2210
+ <div>{t("panel.config.pieOuterRadiusPct")}</div>
2211
+ <Input
2212
+ type="number"
2213
+ min={1}
2214
+ max={100}
2215
+ value={selectedElement.chart?.pieOuterRadius ?? 65}
2216
+ onChange={(e) =>
2217
+ updateSelectedChart({
2218
+ pieOuterRadius: Math.max(
2219
+ 1,
2220
+ Math.min(100, Number(e.target.value) || 1)
2221
+ ),
2222
+ })
2223
+ }
2224
+ className="h-7"
2225
+ />
2226
+ </label>
2227
+ </div>
2228
+ ) : null}
2229
+ </>
2230
+ )
2231
+ ) : null}
2232
+ {renderFieldGroup(
2233
+ t("panel.config.groupChartDisplayMore"),
2234
+ <>
2225
2235
  <label className="flex items-center gap-2">
2226
2236
  <Checkbox
2227
2237
  checked={selectedElement.chart?.colorMode === "gradient"}
@@ -2304,7 +2314,9 @@ export function PanelConfigSidebar({
2304
2314
  </SelectContent>
2305
2315
  </Select>
2306
2316
  </label>
2307
- </>
2317
+ </>,
2318
+ undefined,
2319
+ { groupKey: "chartDisplayMore", defaultOpen: false }
2308
2320
  )}
2309
2321
  {renderFieldGroup(
2310
2322
  t("panel.config.groupTooltip"),
@@ -2361,39 +2373,10 @@ export function PanelConfigSidebar({
2361
2373
  className="h-7"
2362
2374
  />
2363
2375
  </label>
2364
- </>
2376
+ </>,
2377
+ undefined,
2378
+ { groupKey: "chartTooltip", defaultOpen: false }
2365
2379
  )}
2366
- {renderFieldGroup(
2367
- t("panel.config.groupData"),
2368
- <>
2369
- <label className="block space-y-1">
2370
- <div>{t("panel.config.labelsCsv")}</div>
2371
- <Input
2372
- value={getChartLabelsDisplayText(selectedElement.chart)}
2373
- onChange={(e) =>
2374
- updateSelectedChart({
2375
- labelsText: e.target.value,
2376
- })
2377
- }
2378
- className="h-7"
2379
- />
2380
- </label>
2381
-
2382
- <label className="block space-y-1">
2383
- <div>{t("panel.config.valuesCsv")}</div>
2384
- <Input
2385
- value={getChartValuesDisplayText(selectedElement.chart)}
2386
- onChange={(e) =>
2387
- updateSelectedChart({
2388
- valuesText: e.target.value,
2389
- })
2390
- }
2391
- className="h-7"
2392
- />
2393
- </label>
2394
- </>
2395
- )}
2396
-
2397
2380
  {selectedChartType === "bar" ||
2398
2381
  selectedChartType === "line" ||
2399
2382
  selectedChartType === "area" ||
@@ -2555,88 +2538,12 @@ export function PanelConfigSidebar({
2555
2538
  <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
2556
2539
  </label>
2557
2540
  </div>
2558
- </>
2541
+ </>,
2542
+ undefined,
2543
+ { groupKey: "chartAxes", defaultOpen: false }
2559
2544
  )
2560
2545
  ) : null}
2561
2546
 
2562
- {selectedChartType === "bar" ||
2563
- selectedChartType === "line" ||
2564
- selectedChartType === "area" ||
2565
- selectedChartType === "pie" ? (
2566
- renderFieldGroup(
2567
- t("panel.config.groupSeries"),
2568
- <>
2569
- {selectedChartType === "bar" ? (
2570
- <label className="block space-y-1">
2571
- <div>{t("panel.config.barWidthPx")}</div>
2572
- <Input
2573
- type="number"
2574
- min={1}
2575
- value={selectedElement.chart?.barWidth ?? 24}
2576
- onChange={(e) =>
2577
- updateSelectedChart({ barWidth: Math.max(1, Number(e.target.value) || 1) })
2578
- }
2579
- className="h-7"
2580
- />
2581
- </label>
2582
- ) : null}
2583
-
2584
- {selectedChartType === "line" || selectedChartType === "area" ? (
2585
- <label className="flex items-center gap-2">
2586
- <Checkbox
2587
- checked={selectedElement.chart?.smooth ?? true}
2588
- className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
2589
- onCheckedChange={(checked) =>
2590
- updateSelectedChart({ smooth: checked === true })
2591
- }
2592
- />
2593
- <span>{t("panel.config.smooth")}</span>
2594
- </label>
2595
- ) : null}
2596
-
2597
- {selectedChartType === "pie" ? (
2598
- <div className="grid grid-cols-2 gap-2">
2599
- <label className="block space-y-1">
2600
- <div>{t("panel.config.pieInnerRadiusPct")}</div>
2601
- <Input
2602
- type="number"
2603
- min={0}
2604
- max={99}
2605
- value={selectedElement.chart?.pieInnerRadius ?? 30}
2606
- onChange={(e) =>
2607
- updateSelectedChart({
2608
- pieInnerRadius: Math.max(
2609
- 0,
2610
- Math.min(99, Number(e.target.value) || 0)
2611
- ),
2612
- })
2613
- }
2614
- className="h-7"
2615
- />
2616
- </label>
2617
- <label className="block space-y-1">
2618
- <div>{t("panel.config.pieOuterRadiusPct")}</div>
2619
- <Input
2620
- type="number"
2621
- min={1}
2622
- max={100}
2623
- value={selectedElement.chart?.pieOuterRadius ?? 65}
2624
- onChange={(e) =>
2625
- updateSelectedChart({
2626
- pieOuterRadius: Math.max(
2627
- 1,
2628
- Math.min(100, Number(e.target.value) || 1)
2629
- ),
2630
- })
2631
- }
2632
- className="h-7"
2633
- />
2634
- </label>
2635
- </div>
2636
- ) : null}
2637
- </>
2638
- )
2639
- ) : null}
2640
2547
  </>,
2641
2548
  true,
2642
2549
  [
@@ -3086,7 +2993,9 @@ export function PanelConfigSidebar({
3086
2993
  }
3087
2994
  />
3088
2995
  <span>{t("panel.config.allowCanvasInput")}</span>
3089
- </label>
2996
+ </label>,
2997
+ undefined,
2998
+ { groupKey: "textInputAbility", defaultOpen: false }
3090
2999
  )}
3091
3000
  </>,
3092
3001
  true,
@@ -3269,7 +3178,9 @@ export function PanelConfigSidebar({
3269
3178
  {t("panel.config.audioPosterHint")}
3270
3179
  </ConfigHintIcon>
3271
3180
  </div>
3272
- </>
3181
+ </>,
3182
+ undefined,
3183
+ { groupKey: "audioDisplayStyle", defaultOpen: false }
3273
3184
  )}
3274
3185
  </>,
3275
3186
  true,
@@ -3442,7 +3353,8 @@ export function PanelConfigSidebar({
3442
3353
  </>,
3443
3354
  <>
3444
3355
  {t("panel.config.canvasScriptHint")}
3445
- </>
3356
+ </>,
3357
+ { groupKey: "geometryCanvasScript", defaultOpen: false }
3446
3358
  )}
3447
3359
  {renderFieldGroup(
3448
3360
  t("panel.config.groupSketchOverlay"),
@@ -3539,7 +3451,9 @@ export function PanelConfigSidebar({
3539
3451
  {t("panel.config.clearSketch")}
3540
3452
  </button>
3541
3453
  </div>
3542
- </>
3454
+ </>,
3455
+ undefined,
3456
+ { groupKey: "geometrySketch", defaultOpen: false }
3543
3457
  )}
3544
3458
  </>,
3545
3459
  true,
@@ -3699,17 +3613,193 @@ export function PanelConfigSidebar({
3699
3613
  </>,
3700
3614
  <>
3701
3615
  {t("panel.config.copyStrategyHint")}
3702
- </>
3616
+ </>,
3617
+ { groupKey: "referenceCopyStrategy", defaultOpen: false }
3703
3618
  )}
3704
3619
  </>,
3705
3620
  true,
3706
3621
  [t("panel.config.searchKwRef"), "ref", t("panel.config.layer"), t("panel.material.shallowCopy"), t("panel.material.deepCopy"), "snapshot"]
3707
3622
  )
3708
- ) : (
3623
+ ) : selectedElement.materialType === "image" ? null : (
3709
3624
  <div className="text-xs leading-6 text-muted-foreground">
3710
3625
  {t("panel.config.notChartType")}
3711
3626
  </div>
3712
3627
  )}
3628
+ {renderSection(
3629
+ "styleBackground",
3630
+ t("panel.config.sectionStyleBackground"),
3631
+ <>
3632
+ {renderFieldGroup(
3633
+ t("panel.config.groupBgFill"),
3634
+ <>
3635
+ {renderColorField(
3636
+ t("panel.config.backgroundColor"),
3637
+ selectedElement.style?.backgroundColor ?? "",
3638
+ (next) => updateSelectedStyle({ backgroundColor: next || undefined })
3639
+ )}
3640
+ </>
3641
+ )}
3642
+ {renderFieldGroup(
3643
+ t("panel.config.groupBgLayout"),
3644
+ <>
3645
+ <label className="block space-y-1">
3646
+ <div>{t("panel.config.backgroundImage")}</div>
3647
+ <Input
3648
+ value={selectedElement.style?.backgroundImage ?? ""}
3649
+ onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
3650
+ placeholder='url("https://...") / linear-gradient(...)'
3651
+ className="h-7"
3652
+ />
3653
+ </label>
3654
+ <div className="flex items-center gap-2">
3655
+ <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3656
+ {t("panel.config.uploadImage")}
3657
+ <Input
3658
+ type="file"
3659
+ accept="image/*"
3660
+ className="hidden"
3661
+ onChange={async (e) => {
3662
+ const file = e.target.files?.[0];
3663
+ e.currentTarget.value = "";
3664
+ if (!file) return;
3665
+ await handleUploadBackgroundImage(file);
3666
+ }}
3667
+ />
3668
+ </label>
3669
+ {uploadStatus ? (
3670
+ <span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
3671
+ ) : null}
3672
+ </div>
3673
+ <div className="grid grid-cols-2 gap-2">
3674
+ <label className="block space-y-1">
3675
+ <div>{t("panel.config.backgroundSize")}</div>
3676
+ <Select
3677
+ value={selectedElement.style?.backgroundSize ?? "__none__"}
3678
+ onValueChange={(value) =>
3679
+ updateSelectedStyle({
3680
+ backgroundSize: value === "__none__" ? undefined : value,
3681
+ })
3682
+ }
3683
+ >
3684
+ <SelectTrigger className="h-7">
3685
+ <SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
3686
+ </SelectTrigger>
3687
+ <SelectContent>
3688
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
3689
+ <SelectItem value="cover">cover</SelectItem>
3690
+ <SelectItem value="contain">contain</SelectItem>
3691
+ <SelectItem value="100% 100%">100% 100%</SelectItem>
3692
+ <SelectItem value="auto">auto</SelectItem>
3693
+ </SelectContent>
3694
+ </Select>
3695
+ </label>
3696
+ <label className="block space-y-1">
3697
+ <div>{t("panel.config.backgroundPosition")}</div>
3698
+ <Select
3699
+ value={selectedElement.style?.backgroundPosition ?? "__none__"}
3700
+ onValueChange={(value) =>
3701
+ updateSelectedStyle({
3702
+ backgroundPosition: value === "__none__" ? undefined : value,
3703
+ })
3704
+ }
3705
+ >
3706
+ <SelectTrigger className="h-7">
3707
+ <SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
3708
+ </SelectTrigger>
3709
+ <SelectContent>
3710
+ <SelectItem value="__none__">{t("common.default")}</SelectItem>
3711
+ <SelectItem value="center">center</SelectItem>
3712
+ <SelectItem value="top">top</SelectItem>
3713
+ <SelectItem value="bottom">bottom</SelectItem>
3714
+ <SelectItem value="left">left</SelectItem>
3715
+ <SelectItem value="right">right</SelectItem>
3716
+ <SelectItem value="top left">top left</SelectItem>
3717
+ <SelectItem value="top right">top right</SelectItem>
3718
+ <SelectItem value="bottom left">bottom left</SelectItem>
3719
+ <SelectItem value="bottom right">bottom right</SelectItem>
3720
+ </SelectContent>
3721
+ </Select>
3722
+ </label>
3723
+ </div>
3724
+ </>,
3725
+ undefined,
3726
+ { groupKey: "styleBgLayout", defaultOpen: false }
3727
+ )}
3728
+ </>,
3729
+ false,
3730
+ [t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
3731
+ )}
3732
+
3733
+ {renderSection(
3734
+ "styleBorder",
3735
+ t("panel.config.sectionStyleBorder"),
3736
+ <>
3737
+ {renderFieldGroup(
3738
+ t("panel.config.groupBorderGeometry"),
3739
+ <div className="grid grid-cols-2 gap-2">
3740
+ <label className="block space-y-1">
3741
+ <div>{t("panel.config.borderWidth")}</div>
3742
+ <Input
3743
+ type="number"
3744
+ min={0}
3745
+ value={selectedElement.style?.borderWidth ?? 0}
3746
+ onChange={(e) =>
3747
+ updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
3748
+ }
3749
+ className="h-7"
3750
+ />
3751
+ </label>
3752
+ <label className="block space-y-1">
3753
+ <div>{t("panel.config.borderRadius")}</div>
3754
+ <Input
3755
+ type="number"
3756
+ min={0}
3757
+ value={selectedElement.style?.borderRadius ?? 0}
3758
+ onChange={(e) =>
3759
+ updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
3760
+ }
3761
+ className="h-7"
3762
+ />
3763
+ </label>
3764
+ </div>
3765
+ )}
3766
+ {renderFieldGroup(
3767
+ t("panel.config.groupBorderVisual"),
3768
+ <div className="grid grid-cols-2 gap-2">
3769
+ <label className="block space-y-1">
3770
+ <div>{t("panel.config.borderStyle")}</div>
3771
+ <Select
3772
+ value={selectedElement.style?.borderStyle ?? "solid"}
3773
+ onValueChange={(value) =>
3774
+ updateSelectedStyle({
3775
+ borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
3776
+ })
3777
+ }
3778
+ >
3779
+ <SelectTrigger className="h-7">
3780
+ <SelectValue placeholder={t("panel.config.selectBorderStyle")} />
3781
+ </SelectTrigger>
3782
+ <SelectContent>
3783
+ <SelectItem value="none">none</SelectItem>
3784
+ <SelectItem value="solid">solid</SelectItem>
3785
+ <SelectItem value="dashed">dashed</SelectItem>
3786
+ <SelectItem value="dotted">dotted</SelectItem>
3787
+ <SelectItem value="double">double</SelectItem>
3788
+ </SelectContent>
3789
+ </Select>
3790
+ </label>
3791
+ {renderColorField(
3792
+ t("panel.config.borderColor"),
3793
+ selectedElement.style?.borderColor ?? "",
3794
+ (next) => updateSelectedStyle({ borderColor: next || undefined })
3795
+ )}
3796
+ </div>
3797
+ )}
3798
+ </>,
3799
+ false,
3800
+ [t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
3801
+ )}
3802
+
3713
3803
  {hasSearch && renderedSectionCount === 0 ? (
3714
3804
  <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3715
3805
  {t("panel.config.noMatch")}
@@ -4,6 +4,7 @@ import { useI18n } from "@arronqzy/i18n/react";
4
4
  import {
5
5
  BlueprintExecutionLogPanel,
6
6
  BlueprintNodeConfigSidebar,
7
+ type BlueprintGraphEdge,
7
8
  type BlueprintGraphNode,
8
9
  type ExecutionLogSettings,
9
10
  type ExecutionTraceEntry,
@@ -41,6 +42,8 @@ export type WorkspaceConfigSidebarProps = Omit<
41
42
  configFocus: WorkspaceConfigFocus;
42
43
  executionLog?: BlueprintExecutionLogViewProps;
43
44
  selectedBlueprintNode: BlueprintGraphNode | null;
45
+ blueprintGraphNodes?: BlueprintGraphNode[];
46
+ blueprintGraphEdges?: BlueprintGraphEdge[];
44
47
  allowFalseSignalPropagation?: boolean;
45
48
  onUpdateAllowFalseSignalPropagation?: (value: boolean) => void;
46
49
  onUpdateBlueprintNode: (
@@ -74,6 +77,8 @@ export function WorkspaceConfigSidebar({
74
77
  configFocus,
75
78
  executionLog,
76
79
  selectedBlueprintNode,
80
+ blueprintGraphNodes = [],
81
+ blueprintGraphEdges = [],
77
82
  allowFalseSignalPropagation = false,
78
83
  onUpdateAllowFalseSignalPropagation,
79
84
  onUpdateBlueprintNode,
@@ -151,6 +156,9 @@ export function WorkspaceConfigSidebar({
151
156
  return (
152
157
  <BlueprintNodeConfigSidebar
153
158
  node={selectedBlueprintNode}
159
+ graphNodes={blueprintGraphNodes}
160
+ graphEdges={blueprintGraphEdges}
161
+ traceEntries={executionLog?.entries}
154
162
  allowFalseSignalPropagation={allowFalseSignalPropagation}
155
163
  onUpdateAllowFalseSignalPropagation={onUpdateAllowFalseSignalPropagation}
156
164
  viewElementOptions={viewElementOptions}
@@ -84,6 +84,11 @@ function deepMerge<T extends Record<string, any>>(base: T, patch?: Record<string
84
84
  return next as T;
85
85
  }
86
86
 
87
+ function resolveBarWidth(chart?: PanelChartConfig): number | undefined {
88
+ const width = chart?.barWidth;
89
+ return typeof width === "number" && Number.isFinite(width) && width > 0 ? width : undefined;
90
+ }
91
+
87
92
  function mixHex(a: string, b: string, t: number): string {
88
93
  const pa = parseHex(a);
89
94
  const pb = parseHex(b);
@@ -354,7 +359,8 @@ export function buildChartOption(element: PanelElement): EChartsOption {
354
359
  chartType === "line" || chartType === "area"
355
360
  ? (element.chart?.smooth ?? true)
356
361
  : undefined,
357
- barWidth: chartType === "bar" ? (element.chart?.barWidth ?? 24) : undefined,
362
+ barWidth:
363
+ chartType === "bar" ? resolveBarWidth(element.chart) : undefined,
358
364
  areaStyle: chartType === "area" ? { opacity: 0.25 } : undefined,
359
365
  itemStyle: { color: chartColor as any },
360
366
  lineStyle: { color: chartColor as any },
@@ -0,0 +1,69 @@
1
+ /** 产物预览是否缩放内部内容(字体等)。默认 false:块仍按视口撑满,内容保持原尺寸以免失真。 */
2
+ export const OUTPUT_SCALE_STORAGE_KEY = "panel:outputScale";
3
+
4
+ export function readOutputScale(): boolean {
5
+ try {
6
+ return localStorage.getItem(OUTPUT_SCALE_STORAGE_KEY) === "1";
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ export function writeOutputScale(enabled: boolean) {
13
+ try {
14
+ localStorage.setItem(OUTPUT_SCALE_STORAGE_KEY, enabled ? "1" : "0");
15
+ } catch {
16
+ // ignore storage errors
17
+ }
18
+ }
19
+
20
+ export type PreviewFillScale = {
21
+ vw: number;
22
+ vh: number;
23
+ scaleX: number;
24
+ scaleY: number;
25
+ };
26
+
27
+ export function getPreviewFillScale(
28
+ sceneWidth: number,
29
+ sceneHeight: number
30
+ ): PreviewFillScale {
31
+ const vw = window.innerWidth || 1;
32
+ const vh = window.innerHeight || 1;
33
+ const sw = Math.max(1, sceneWidth);
34
+ const sh = Math.max(1, sceneHeight);
35
+ return {
36
+ vw,
37
+ vh,
38
+ scaleX: vw / sw,
39
+ scaleY: vh / sh,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * 预览始终按视口宽高比把场景块撑满。
45
+ * outputScale=true:CSS transform 缩放整棵内容(含字体)。
46
+ * outputScale=false:只把场景盒拉到视口大小,节点几何由调用方按 scale 映射,字体不跟着拉伸。
47
+ */
48
+ export function applyPreviewSceneFill(
49
+ scene: HTMLElement | null,
50
+ sceneWidth: number,
51
+ sceneHeight: number,
52
+ outputScale: boolean
53
+ ): PreviewFillScale {
54
+ const fill = getPreviewFillScale(sceneWidth, sceneHeight);
55
+ if (!Number.isFinite(fill.scaleX) || !Number.isFinite(fill.scaleY)) return fill;
56
+ if (!scene) return fill;
57
+ const root = scene.parentElement;
58
+ if (root) root.style.overflow = "hidden";
59
+ if (outputScale) {
60
+ scene.style.width = `${Math.max(1, sceneWidth)}px`;
61
+ scene.style.height = `${Math.max(1, sceneHeight)}px`;
62
+ scene.style.transform = `translate(0px,0px) scale(${fill.scaleX},${fill.scaleY})`;
63
+ } else {
64
+ scene.style.width = `${fill.vw}px`;
65
+ scene.style.height = `${fill.vh}px`;
66
+ scene.style.transform = "none";
67
+ }
68
+ return fill;
69
+ }
@@ -126,7 +126,6 @@ export function getDefaultChartConfig(
126
126
  return {
127
127
  title: t("panel.defaults.bar"),
128
128
  ...common,
129
- barWidth: 24,
130
129
  };
131
130
  }
132
131
  if (materialType === "line") {