@arronqzy/react-view 1.0.22 → 1.0.24

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.
@@ -1,10 +1,11 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import React, {
2
+ useEffect,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ } from "react";
2
7
  import { useI18n } from "@arronqzy/i18n/react";
3
8
  import {
4
- Card,
5
- CardContent,
6
- CardHeader,
7
- CardTitle,
8
9
  Checkbox,
9
10
  Collapsible,
10
11
  CollapsibleContent,
@@ -14,25 +15,9 @@ import {
14
15
  EmptyIcon,
15
16
  EmptyTitle,
16
17
  Input,
17
- Select,
18
- SelectContent,
19
- SelectItem,
20
- SelectTrigger,
21
- SelectValue,
22
- Textarea,
23
18
  } from "@arronqzy/ui";
24
- import type {
25
- PanelChartConfig,
26
- PanelElement,
27
- PanelElementStyle,
28
- ReferenceCopyMode,
29
- } from "../types";
30
- import { buildChartOption, CHART_TYPES, getChartLabelsDisplayText, getChartValuesDisplayText } from "../utils/chartOptionBuilder";
31
- import {
32
- VIEWPORT_OVERFLOW_MODES,
33
- normalizeViewportOverflow,
34
- type ViewportOverflowMode,
35
- } from "../utils/viewportPlacement";
19
+ import type { PanelElement, ReferenceCopyMode } from "../types";
20
+ import { CHART_TYPES } from "../utils/chartOptionBuilder";
36
21
  import type { PanelLayer } from "../types";
37
22
  import { getPanelMessages } from "../constants/messages";
38
23
  import { ViewElementScopePanel } from "./ViewElementScopePanel";
@@ -42,16 +27,27 @@ import { collectElementScopeWarnings } from "../utils/scope-template-warnings";
42
27
  import { ScopeConfigProvider } from "./scope-config/ScopeConfigContext";
43
28
  import { ScopeTemplateWarningsPanel } from "./scope-config/ScopeTemplateWarningsPanel";
44
29
  import { PanelConfigTableSection } from "./table/PanelConfigTableSection";
45
- import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
46
30
  import { highlightConfigField, subscribeRevealPanelConfig } from "../ai/revealConfigField";
47
31
  import { PanelConfigScene3dSection } from "./config/PanelConfigScene3dSection";
48
- import { readFileAsDataUrl, runBusyTask } from "../utils/async-work";
49
-
50
- type UpdateElement = (
51
- id: string,
52
- patch: Partial<PanelElement>,
53
- options?: { batchId?: string; meta?: Record<string, unknown> }
54
- ) => void;
32
+ import { PanelConfigNodeInfo } from "./config/PanelConfigNodeInfo";
33
+ import { PanelConfigChartSection } from "./config/PanelConfigChartSection";
34
+ import { PanelConfigTextSection } from "./config/PanelConfigTextSection";
35
+ import { PanelConfigStyleSections } from "./config/PanelConfigStyleSections";
36
+ import { PanelConfigGridSection } from "./config/PanelConfigGridSection";
37
+ import { PanelConfigGridChildSpan } from "./config/PanelConfigGridChildSpan";
38
+ import { PanelConfigViewportSection } from "./config/PanelConfigViewportSection";
39
+ import { PanelConfigReferenceSection } from "./config/PanelConfigReferenceSection";
40
+ import { PanelConfigAudioSection } from "./config/PanelConfigAudioSection";
41
+ import { PanelConfigVideoSection } from "./config/PanelConfigVideoSection";
42
+ import { PanelConfigGeometrySection } from "./config/PanelConfigGeometrySection";
43
+ import { PanelConfigMultiSelect } from "./config/PanelConfigMultiSelect";
44
+ import {
45
+ type ConfigSectionHelpers,
46
+ type UpdateElement,
47
+ OPTION_CHECKBOX_CLASS,
48
+ OPTION_INPUT_CLASS,
49
+ OPTION_SELECT_TRIGGER_CLASS,
50
+ } from "./config/helpers";
55
51
 
56
52
  export type PanelConfigSidebarProps = {
57
53
  selectedElement: PanelElement | null;
@@ -65,11 +61,29 @@ export type PanelConfigSidebarProps = {
65
61
  nodeId: string,
66
62
  action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
67
63
  ) => void;
68
- /** 蓝图视图绑定节点写入的信号 scope,用于展示与模版解析 */
69
64
  viewElementScope?: unknown;
70
65
  blueprintNodeOptions?: { id: string; label: string }[];
71
66
  };
72
67
 
68
+ function ConfigSearchEmpty({
69
+ hasSearch,
70
+ matchCountRef,
71
+ label,
72
+ }: {
73
+ hasSearch: boolean;
74
+ matchCountRef: React.MutableRefObject<number>;
75
+ label: string;
76
+ }) {
77
+ if (hasSearch && matchCountRef.current === 0) {
78
+ return (
79
+ <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
80
+ {label}
81
+ </div>
82
+ );
83
+ }
84
+ return null;
85
+ }
86
+
73
87
  export function PanelConfigSidebar({
74
88
  selectedElement,
75
89
  selectedElements = [],
@@ -85,26 +99,11 @@ export function PanelConfigSidebar({
85
99
  const { t } = useI18n();
86
100
  const messages = React.useMemo(() => getPanelMessages(t), [t]);
87
101
 
88
- const [isAdvancedOptionMode, setIsAdvancedOptionMode] = useState(false);
89
- const [optionJsonText, setOptionJsonText] = useState("{}");
90
- const [optionJsonError, setOptionJsonError] = useState<string | null>(null);
91
- const [uploadStatus, setUploadStatus] = useState<string>("");
92
- const [audioStatus, setAudioStatus] = useState<string>("");
93
- const [videoStatus, setVideoStatus] = useState<string>("");
94
- const [geometryDrawPenColor, setGeometryDrawPenColor] = useState("#111827");
95
- const [geometryDrawPenWidth, setGeometryDrawPenWidth] = useState(3);
96
- const [isGeometryDrawing, setIsGeometryDrawing] = useState(false);
97
102
  const [configSearch, setConfigSearch] = useState("");
98
103
  const [isSearchCollapsed, setIsSearchCollapsed] = useState(false);
99
104
  const SEARCH_COLLAPSE_STORAGE_KEY = "panel:config-search-collapsed";
100
- const textEditorRef = useRef<HTMLDivElement | null>(null);
101
- const geometryDrawCanvasRef = useRef<HTMLCanvasElement | null>(null);
102
- const geometryLastPointRef = useRef<{ x: number; y: number } | null>(null);
103
- const recorderRef = useRef<MediaRecorder | null>(null);
104
- const recordStreamRef = useRef<MediaStream | null>(null);
105
- const audioChunksRef = useRef<BlobPart[]>([]);
106
- const [isRecordingAudio, setIsRecordingAudio] = useState(false);
107
105
  const sidebarScrollRef = useRef<HTMLElement | null>(null);
106
+ const matchCountRef = useRef(0);
108
107
  const themedScrollbarClass =
109
108
  "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";
110
109
  const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
@@ -118,7 +117,6 @@ export function PanelConfigSidebar({
118
117
  chartAdvancedAxisPointer: false,
119
118
  reference: true,
120
119
  });
121
- const [expandedNodeCards, setExpandedNodeCards] = useState<Record<string, boolean>>({});
122
120
 
123
121
  const isChartElement = !!selectedElement && CHART_TYPES.has(selectedElement.materialType ?? "");
124
122
  const selectedLayer = selectedElement
@@ -133,16 +131,6 @@ export function PanelConfigSidebar({
133
131
  : selectedLayer?.locked
134
132
  ? messages.nodeConfigLayerLocked
135
133
  : "";
136
- const selectedChartType = (selectedElement?.materialType ?? "") as
137
- | "bar"
138
- | "line"
139
- | "pie"
140
- | "area"
141
- | "scatter"
142
- | "radar"
143
- | "gauge"
144
- | "funnel"
145
- | "";
146
134
  const normalizedSearch = useMemo(() => configSearch.trim().toLowerCase(), [configSearch]);
147
135
  const hasSearch = normalizedSearch.length > 0;
148
136
  const showScopePanel =
@@ -152,27 +140,13 @@ export function PanelConfigSidebar({
152
140
  const scopeWarnings = useMemo(() => {
153
141
  if (!selectedElement || viewElementScope === undefined) return [];
154
142
  return collectElementScopeWarnings(selectedElement, viewElementScope, t);
155
- }, [selectedElement, viewElementScope]);
143
+ }, [selectedElement, viewElementScope, t]);
156
144
  const effectiveSelectedElements = useMemo(() => {
157
145
  if (selectedElements.length > 0) return selectedElements;
158
146
  return selectedElement ? [selectedElement] : [];
159
147
  }, [selectedElement, selectedElements]);
160
148
  const isMultiSelectMode = effectiveSelectedElements.length > 1;
161
149
 
162
- useEffect(() => {
163
- if (!selectedElement) {
164
- setOptionJsonText("{}");
165
- setOptionJsonError(null);
166
- return;
167
- }
168
- if (CHART_TYPES.has(selectedElement.materialType ?? "")) {
169
- setOptionJsonText(JSON.stringify(buildChartOption(selectedElement), null, 2));
170
- } else {
171
- setOptionJsonText("{}");
172
- }
173
- setOptionJsonError(null);
174
- }, [selectedElement]);
175
-
176
150
  useEffect(() => {
177
151
  if (!selectedElement) return;
178
152
  if (!CHART_TYPES.has(selectedElement.materialType ?? "")) return;
@@ -182,14 +156,6 @@ export function PanelConfigSidebar({
182
156
  }));
183
157
  }, [selectedElement?.id, selectedElement?.materialType]);
184
158
 
185
- useEffect(() => {
186
- if (!selectedElement || selectedElement.materialType !== "text") return;
187
- const nextHtml =
188
- selectedElement.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
189
- if (textEditorRef.current && textEditorRef.current.innerHTML !== nextHtml) {
190
- textEditorRef.current.innerHTML = nextHtml;
191
- }
192
- }, [selectedElement?.id, selectedElement?.materialType, selectedElement?.textHtml]);
193
159
  useEffect(() => {
194
160
  if (typeof window === "undefined") return;
195
161
  const saved = window.localStorage.getItem(SEARCH_COLLAPSE_STORAGE_KEY);
@@ -199,234 +165,6 @@ export function PanelConfigSidebar({
199
165
  if (typeof window === "undefined") return;
200
166
  window.localStorage.setItem(SEARCH_COLLAPSE_STORAGE_KEY, isSearchCollapsed ? "1" : "0");
201
167
  }, [isSearchCollapsed]);
202
- useEffect(
203
- () => () => {
204
- recordStreamRef.current?.getTracks().forEach((track) => track.stop());
205
- },
206
- []
207
- );
208
-
209
- const updateSelectedChart = useCallback(
210
- (patch: Partial<PanelChartConfig>) => {
211
- if (!selectedElement) return;
212
- updateElement(selectedElement.id, {
213
- chart: { ...(selectedElement.chart ?? {}), ...patch },
214
- });
215
- },
216
- [selectedElement, updateElement]
217
- );
218
-
219
- const updateSelectedStyle = useCallback(
220
- (patch: Partial<PanelElementStyle>) => {
221
- if (!selectedElement) return;
222
- updateElement(selectedElement.id, {
223
- style: { ...(selectedElement.style ?? {}), ...patch },
224
- });
225
- },
226
- [selectedElement, updateElement]
227
- );
228
- const updateSelectedText = useCallback(
229
- (patch: Partial<PanelElement>) => {
230
- if (!selectedElement || selectedElement.materialType !== "text") return;
231
- updateElement(selectedElement.id, patch);
232
- },
233
- [selectedElement, updateElement]
234
- );
235
- const updateSelectedGeometry = useCallback(
236
- (patch: Partial<PanelElement>) => {
237
- if (!selectedElement || selectedElement.materialType !== "geometry") return;
238
- updateElement(selectedElement.id, patch);
239
- },
240
- [selectedElement, updateElement]
241
- );
242
-
243
- const redrawGeometryPadFromElement = useCallback(() => {
244
- const canvas = geometryDrawCanvasRef.current;
245
- if (!canvas) return;
246
- const ctx = canvas.getContext("2d");
247
- if (!ctx) return;
248
- const width = canvas.width;
249
- const height = canvas.height;
250
- ctx.clearRect(0, 0, width, height);
251
- ctx.fillStyle = "#ffffff";
252
- ctx.fillRect(0, 0, width, height);
253
- const sketch = selectedElement?.geometrySketchDataUrl;
254
- if (!sketch) return;
255
- const img = new Image();
256
- img.onload = () => {
257
- ctx.drawImage(img, 0, 0, width, height);
258
- };
259
- img.src = sketch;
260
- }, [selectedElement?.geometrySketchDataUrl]);
261
-
262
- useEffect(() => {
263
- if (!selectedElement || selectedElement.materialType !== "geometry") return;
264
- redrawGeometryPadFromElement();
265
- }, [redrawGeometryPadFromElement, selectedElement?.id, selectedElement?.materialType]);
266
-
267
- const handleUploadBackgroundImage = useCallback(
268
- async (file: File) => {
269
- if (!selectedElement) return;
270
- try {
271
- await runBusyTask(t("common.uploadingFile"), async () => {
272
- const base64 = await readFileAsDataUrl(file, messages.readImageFailed, "image");
273
- updateSelectedStyle({
274
- backgroundImage: `url("${base64}")`,
275
- });
276
- setUploadStatus(t("panel.config.uploadWrittenBase64"));
277
- try {
278
- const form = new FormData();
279
- form.append("file", file);
280
- const resp = await fetch("/api/upload", { method: "POST", body: form });
281
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
282
- const data = (await resp.json()) as { url?: string };
283
- if (data.url) {
284
- updateSelectedStyle({ backgroundImageRemoteUrl: data.url });
285
- setUploadStatus(t("panel.config.uploadServerAndBase64"));
286
- }
287
- } catch {
288
- setUploadStatus(t("panel.config.uploadServerFailedKeepBase64"));
289
- }
290
- });
291
- } catch (error) {
292
- setUploadStatus(
293
- error instanceof Error ? error.message : messages.readImageFailed
294
- );
295
- }
296
- },
297
- [messages.readImageFailed, selectedElement, t, updateSelectedStyle]
298
- );
299
- const updateSelectedAudio = useCallback(
300
- (patch: Partial<PanelElement>) => {
301
- if (!selectedElement || selectedElement.materialType !== "audio") return;
302
- updateElement(selectedElement.id, patch);
303
- },
304
- [selectedElement, updateElement]
305
- );
306
- const handleUploadAudioFile = useCallback(
307
- async (file: File) => {
308
- if (!selectedElement || selectedElement.materialType !== "audio") return;
309
- try {
310
- await runBusyTask(t("common.uploadingFile"), async () => {
311
- const base64 = await readFileAsDataUrl(file, messages.readAudioFailed, "audio");
312
- updateSelectedAudio({ audioSrc: base64 });
313
- setAudioStatus(messages.audioLocalSaved);
314
- try {
315
- const form = new FormData();
316
- form.append("file", file);
317
- const resp = await fetch("/api/upload", { method: "POST", body: form });
318
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
319
- const data = (await resp.json()) as { url?: string };
320
- if (data.url) {
321
- updateSelectedAudio({ audioRemoteUrl: data.url });
322
- setAudioStatus(messages.audioRemoteUploaded);
323
- }
324
- } catch {
325
- setAudioStatus(messages.audioServerUploadFailed);
326
- }
327
- });
328
- } catch (error) {
329
- setAudioStatus(
330
- error instanceof Error ? error.message : messages.readAudioFailed
331
- );
332
- }
333
- },
334
- [messages, selectedElement, t, updateSelectedAudio]
335
- );
336
- const handleUploadAudioPoster = useCallback(
337
- async (file: File) => {
338
- if (!selectedElement || selectedElement.materialType !== "audio") return;
339
- try {
340
- const base64 = await readFileAsDataUrl(file, messages.readImageFailed, "image");
341
- updateSelectedAudio({ audioPosterImage: base64 });
342
- setAudioStatus(messages.audioPosterSet);
343
- } catch (error) {
344
- setAudioStatus(
345
- error instanceof Error ? error.message : messages.readImageFailed
346
- );
347
- }
348
- },
349
- [messages, selectedElement, updateSelectedAudio]
350
- );
351
- const stopRecordingAudio = useCallback(() => {
352
- recorderRef.current?.stop();
353
- }, []);
354
- const startRecordingAudio = useCallback(async () => {
355
- if (!selectedElement || selectedElement.materialType !== "audio") return;
356
- if (!navigator.mediaDevices?.getUserMedia) {
357
- setAudioStatus(messages.audioRecordUnsupported);
358
- return;
359
- }
360
- try {
361
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
362
- recordStreamRef.current = stream;
363
- const recorder = new MediaRecorder(stream);
364
- recorderRef.current = recorder;
365
- audioChunksRef.current = [];
366
- recorder.ondataavailable = (event) => {
367
- if (event.data.size > 0) audioChunksRef.current.push(event.data);
368
- };
369
- recorder.onstop = async () => {
370
- const blob = new Blob(audioChunksRef.current, { type: recorder.mimeType || "audio/webm" });
371
- try {
372
- const dataUrl = await readFileAsDataUrl(blob, messages.readRecordAudioFailed, "audio");
373
- updateSelectedAudio({ audioSrc: dataUrl });
374
- setAudioStatus(messages.audioRecordSaved);
375
- } catch (error) {
376
- setAudioStatus(
377
- error instanceof Error ? error.message : messages.readRecordAudioFailed
378
- );
379
- }
380
- recordStreamRef.current?.getTracks().forEach((track) => track.stop());
381
- recordStreamRef.current = null;
382
- recorderRef.current = null;
383
- setIsRecordingAudio(false);
384
- };
385
- recorder.start();
386
- setIsRecordingAudio(true);
387
- setAudioStatus(messages.audioRecording);
388
- } catch {
389
- setAudioStatus(messages.audioRecordStartFailed);
390
- setIsRecordingAudio(false);
391
- }
392
- }, [selectedElement, updateSelectedAudio]);
393
- const updateSelectedVideo = useCallback(
394
- (patch: Partial<PanelElement>) => {
395
- if (!selectedElement || selectedElement.materialType !== "video") return;
396
- updateElement(selectedElement.id, patch);
397
- },
398
- [selectedElement, updateElement]
399
- );
400
- const handleUploadVideoFile = useCallback(
401
- async (file: File) => {
402
- if (!selectedElement || selectedElement.materialType !== "video") return;
403
- try {
404
- await runBusyTask(t("common.uploadingFile"), async () => {
405
- const base64 = await readFileAsDataUrl(file, messages.readVideoFailed, "video");
406
- updateSelectedVideo({ videoSrc: base64 });
407
- setVideoStatus(messages.videoLocalSaved);
408
- try {
409
- const form = new FormData();
410
- form.append("file", file);
411
- const resp = await fetch("/api/upload", { method: "POST", body: form });
412
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
413
- const data = (await resp.json()) as { url?: string };
414
- if (data.url) {
415
- updateSelectedVideo({ videoRemoteUrl: data.url });
416
- setVideoStatus(messages.videoRemoteUploaded);
417
- }
418
- } catch {
419
- setVideoStatus(messages.videoServerUploadFailed);
420
- }
421
- });
422
- } catch (error) {
423
- setVideoStatus(
424
- error instanceof Error ? error.message : messages.readVideoFailed
425
- );
426
- }
427
- },
428
- [messages, selectedElement, t, updateSelectedVideo]
429
- );
430
168
 
431
169
  const isSectionExpanded = (key: string, defaultValue = true) =>
432
170
  expandedSections[key] ?? defaultValue;
@@ -459,6 +197,8 @@ export function PanelConfigSidebar({
459
197
  });
460
198
  }, []);
461
199
 
200
+ matchCountRef.current = 0;
201
+
462
202
  const renderSection = (
463
203
  key: string,
464
204
  title: string,
@@ -471,32 +211,32 @@ export function PanelConfigSidebar({
471
211
  !hasSearch ||
472
212
  [title, ...searchTerms].some((term) => term.toLowerCase().includes(normalizedSearch));
473
213
  if (!hit) return null;
474
- renderedSectionCount += 1;
214
+ matchCountRef.current += 1;
475
215
  return (
476
- <Collapsible
477
- open={hasSearch ? true : isSectionExpanded(key, defaultOpen)}
478
- onOpenChange={(open) => setSectionExpanded(key, open)}
479
- className="rounded-xl border border-border/70 bg-card/95 shadow-sm"
480
- data-config-section={key}
481
- >
482
- <div className="flex items-center gap-1.5 px-3 py-2">
483
- <CollapsibleTrigger asChild>
484
- <button
485
- type="button"
486
- className="flex h-6 w-6 items-center justify-center rounded-md text-xs hover:bg-accent"
487
- >
488
- {isSectionExpanded(key, defaultOpen) ? "▾" : "▸"}
489
- </button>
490
- </CollapsibleTrigger>
491
- <div className="flex min-w-0 flex-1 items-center gap-1">
492
- <div className="text-[11px] font-semibold tracking-wide text-muted-foreground">{title}</div>
493
- {hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
216
+ <Collapsible
217
+ open={hasSearch ? true : isSectionExpanded(key, defaultOpen)}
218
+ onOpenChange={(open) => setSectionExpanded(key, open)}
219
+ className="rounded-xl border border-border/70 bg-card/95 shadow-sm"
220
+ data-config-section={key}
221
+ >
222
+ <div className="flex items-center gap-1.5 px-3 py-2">
223
+ <CollapsibleTrigger asChild>
224
+ <button
225
+ type="button"
226
+ className="flex h-6 w-6 items-center justify-center rounded-md text-xs hover:bg-accent"
227
+ >
228
+ {isSectionExpanded(key, defaultOpen) ? "▾" : "▸"}
229
+ </button>
230
+ </CollapsibleTrigger>
231
+ <div className="flex min-w-0 flex-1 items-center gap-1">
232
+ <div className="text-[11px] font-semibold tracking-wide text-muted-foreground">{title}</div>
233
+ {hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
234
+ </div>
494
235
  </div>
495
- </div>
496
- <CollapsibleContent className="space-y-3 border-t border-border/60 bg-muted/[0.1] px-3 pb-3 pt-2.5">
497
- {children}
498
- </CollapsibleContent>
499
- </Collapsible>
236
+ <CollapsibleContent className="space-y-3 border-t border-border/60 bg-muted/[0.1] px-3 pb-3 pt-2.5">
237
+ {children}
238
+ </CollapsibleContent>
239
+ </Collapsible>
500
240
  );
501
241
  };
502
242
 
@@ -545,13 +285,6 @@ export function PanelConfigSidebar({
545
285
  </Collapsible>
546
286
  );
547
287
  };
548
- const isNodeCardExpanded = useCallback(
549
- (id: string) => expandedNodeCards[id] ?? true,
550
- [expandedNodeCards]
551
- );
552
- const setNodeCardExpanded = useCallback((id: string, open: boolean) => {
553
- setExpandedNodeCards((prev) => ({ ...prev, [id]: open }));
554
- }, []);
555
288
 
556
289
  const renderColorField = (
557
290
  label: string,
@@ -577,12 +310,7 @@ export function PanelConfigSidebar({
577
310
  </div>
578
311
  </label>
579
312
  );
580
- const optionCheckboxClass =
581
- "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";
582
- const optionInputClass =
583
- "h-7 border border-border/60 bg-background/90 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-border/60";
584
- const optionSelectTriggerClass =
585
- "h-7 border border-border/60 bg-muted/40 shadow-none ring-0 focus:ring-0 focus:ring-offset-0 focus:border-border/60 data-[state=open]:border-border/60";
313
+
586
314
  const optionLabelTextClass = "truncate whitespace-nowrap text-[11px] leading-none";
587
315
  const renderOptionLabel = (label: string, keyPath: string, desc: string) => (
588
316
  <div className="flex min-w-0 items-center gap-1">
@@ -607,56 +335,24 @@ export function PanelConfigSidebar({
607
335
  </ConfigHintIcon>
608
336
  </div>
609
337
  );
610
- const mergeOptionPatch = useCallback(
611
- (
612
- base: Record<string, unknown> | undefined,
613
- patch: Record<string, unknown>
614
- ): Record<string, unknown> => {
615
- const output: Record<string, unknown> = { ...(base ?? {}) };
616
- for (const [key, value] of Object.entries(patch)) {
617
- const prev = output[key];
618
- if (
619
- value &&
620
- typeof value === "object" &&
621
- !Array.isArray(value) &&
622
- prev &&
623
- typeof prev === "object" &&
624
- !Array.isArray(prev)
625
- ) {
626
- output[key] = mergeOptionPatch(
627
- prev as Record<string, unknown>,
628
- value as Record<string, unknown>
629
- );
630
- } else {
631
- output[key] = value;
632
- }
633
- }
634
- return output;
635
- },
636
- []
637
- );
638
- const updateSelectedOptionForm = useCallback(
639
- (patch: Record<string, unknown>) => {
640
- if (!selectedElement) return;
641
- updateSelectedChart({
642
- option: mergeOptionPatch(
643
- (selectedElement.chart?.option as Record<string, unknown> | undefined) ?? {},
644
- patch
645
- ),
646
- });
647
- },
648
- [mergeOptionPatch, selectedElement, updateSelectedChart]
649
- );
650
- const execTextCommand = (cmd: "bold" | "italic" | "underline") => {
651
- textEditorRef.current?.focus();
652
- document.execCommand(cmd);
653
- const next = textEditorRef.current?.innerHTML ?? "";
654
- if (selectedElement?.materialType === "text") {
655
- updateElement(selectedElement.id, { textHtml: next || "<p><br/></p>" });
656
- }
338
+
339
+ const helpers: ConfigSectionHelpers = {
340
+ renderSection,
341
+ renderFieldGroup,
342
+ renderColorField,
343
+ hasSearch,
344
+ normalizedSearch,
345
+ isSectionExpanded,
346
+ setSectionExpanded,
347
+ optionCheckboxClass: OPTION_CHECKBOX_CLASS,
348
+ optionInputClass: OPTION_INPUT_CLASS,
349
+ optionSelectTriggerClass: OPTION_SELECT_TRIGGER_CLASS,
350
+ renderOptionLabel,
351
+ renderFormatterLabel,
657
352
  };
658
353
 
659
- let renderedSectionCount = 0;
354
+ const materialType = selectedElement?.materialType ?? "";
355
+
660
356
  return (
661
357
  <ScopeConfigProvider
662
358
  scope={showScopePanel ? viewElementScope : undefined}
@@ -664,3340 +360,229 @@ export function PanelConfigSidebar({
664
360
  warnings={scopeWarnings}
665
361
  scrollContainerRef={sidebarScrollRef}
666
362
  >
667
- <aside
668
- ref={sidebarScrollRef}
669
- className={`scope-config-sidebar h-full overflow-auto border-l border-border bg-muted/[0.14] px-3 py-3 text-foreground [&_.config-field--highlight]:rounded-md [&_.config-field--highlight]:ring-2 [&_.config-field--highlight]:ring-sky-400/80 [&_.scope-field--highlight]:rounded-md [&_.scope-field--highlight]:ring-2 [&_.scope-field--highlight]:ring-amber-400/80 [&_button[role=checkbox]]:border-2 [&_button[role=checkbox]]:border-foreground/80 [&_button[role=checkbox]]:bg-background [&_button[role=checkbox]]:ring-1 [&_button[role=checkbox]]:ring-foreground/40 [&_button[role=checkbox][data-state=checked]]:border-primary [&_button[role=checkbox][data-state=checked]]:ring-primary/40 ${themedScrollbarClass}`}
670
- >
671
- <div className="sticky top-0 z-20 mb-3 rounded-lg border border-border/70 bg-card/95 px-2.5 py-2 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-card/80">
672
- <div className="flex items-center justify-between gap-2">
673
- <div className="text-xs font-semibold tracking-wide">{t("panel.config.panelTitle")}</div>
674
- </div>
675
- {showScopePanel ? (
676
- <ViewElementScopePanel scope={viewElementScope} />
677
- ) : null}
678
- <ScopeTemplateWarningsPanel />
679
- <div
680
- className={
681
- showScopePanel || scopeWarnings.length > 0
682
- ? "mt-2 border-t border-border/50 pt-2"
683
- : "mt-0"
684
- }
685
- >
686
- <div className="flex items-center justify-end gap-2">
687
- <button
688
- type="button"
689
- className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
690
- onClick={() => setIsSearchCollapsed((prev) => !prev)}
691
- >
692
- {isSearchCollapsed ? t("panel.config.expandSearch") : t("panel.config.collapseSearch")}
693
- </button>
363
+ <aside
364
+ ref={sidebarScrollRef}
365
+ className={`scope-config-sidebar h-full overflow-auto border-l border-border bg-muted/[0.14] px-3 py-3 text-foreground [&_.config-field--highlight]:rounded-md [&_.config-field--highlight]:ring-2 [&_.config-field--highlight]:ring-sky-400/80 [&_.scope-field--highlight]:rounded-md [&_.scope-field--highlight]:ring-2 [&_.scope-field--highlight]:ring-amber-400/80 [&_button[role=checkbox]]:border-2 [&_button[role=checkbox]]:border-foreground/80 [&_button[role=checkbox]]:bg-background [&_button[role=checkbox]]:ring-1 [&_button[role=checkbox]]:ring-foreground/40 [&_button[role=checkbox][data-state=checked]]:border-primary [&_button[role=checkbox][data-state=checked]]:ring-primary/40 ${themedScrollbarClass}`}
366
+ >
367
+ <div className="sticky top-0 z-20 mb-3 rounded-lg border border-border/70 bg-card/95 px-2.5 py-2 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-card/80">
368
+ <div className="flex items-center justify-between gap-2">
369
+ <div className="text-xs font-semibold tracking-wide">{t("panel.config.panelTitle")}</div>
694
370
  </div>
695
- {!isSearchCollapsed ? (
696
- <div className="mt-2">
697
- <Input
698
- value={configSearch}
699
- onChange={(e) => setConfigSearch(e.target.value)}
700
- placeholder={t("panel.config.searchPlaceholder")}
701
- className="h-7"
702
- data-scope-autocomplete="off"
703
- />
704
- {hasSearch ? (
705
- <div className="mt-1 text-[11px] text-muted-foreground">
706
- {t("panel.config.searching", { query: configSearch })}
707
- </div>
708
- ) : null}
709
- </div>
710
- ) : null}
711
- </div>
712
- </div>
713
- {isMultiSelectMode ? (
714
- <div className="space-y-3">
715
- <Card>
716
- <CardHeader className="pb-2">
717
- <CardTitle className="text-xs">{t("panel.config.batchTitleWithCount", { count: effectiveSelectedElements.length })}</CardTitle>
718
- </CardHeader>
719
- <CardContent className="space-y-2 text-xs">
720
- <div className="grid grid-cols-2 gap-2">
721
- <button
722
- type="button"
723
- className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
724
- onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: true }))}
725
- >
726
- {t("panel.config.lockAll")}
727
- </button>
728
- <button
729
- type="button"
730
- className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
731
- onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: false }))}
732
- >
733
- {t("panel.config.unlockAll")}
734
- </button>
735
- <button
736
- type="button"
737
- className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
738
- onClick={() =>
739
- effectiveSelectedElements.forEach((el) =>
740
- onAdjustNodeZOrder?.(el.id, "bringForward")
741
- )
742
- }
743
- >
744
- {t("panel.config.bringAllForward")}
745
- </button>
746
- <button
747
- type="button"
748
- className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
749
- onClick={() =>
750
- effectiveSelectedElements.forEach((el) =>
751
- onAdjustNodeZOrder?.(el.id, "sendBackward")
752
- )
753
- }
754
- >
755
- {t("panel.config.sendAllBackward")}
756
- </button>
757
- <button
758
- type="button"
759
- className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
760
- onClick={() =>
761
- effectiveSelectedElements.forEach((el) =>
762
- updateElement(el.id, { zIndex: 1 })
763
- )
764
- }
765
- >
766
- {t("panel.config.setAllZIndex1")}
767
- </button>
768
- <button
769
- type="button"
770
- className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
771
- onClick={() =>
772
- effectiveSelectedElements.forEach((el) =>
773
- updateElement(el.id, {
774
- style: {
775
- ...(el.style ?? {}),
776
- backgroundColor: "#3b82f6",
777
- },
778
- })
779
- )
780
- }
781
- >
782
- {t("panel.config.setAllBgBlue")}
783
- </button>
784
- </div>
785
- </CardContent>
786
- </Card>
787
- {effectiveSelectedElements
788
- .filter((el) => {
789
- if (!hasSearch) return true;
790
- const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
791
- .toLowerCase();
792
- return text.includes(normalizedSearch);
793
- })
794
- .map((el) => (
795
- <Card
796
- key={el.id}
797
- className={el.locked ? "border-amber-500/40 bg-amber-500/5" : ""}
371
+ {showScopePanel ? <ViewElementScopePanel scope={viewElementScope} /> : null}
372
+ <ScopeTemplateWarningsPanel />
373
+ <div
374
+ className={
375
+ showScopePanel || scopeWarnings.length > 0
376
+ ? "mt-2 border-t border-border/50 pt-2"
377
+ : "mt-0"
378
+ }
379
+ >
380
+ <div className="flex items-center justify-end gap-2">
381
+ <button
382
+ type="button"
383
+ className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
384
+ onClick={() => setIsSearchCollapsed((prev) => !prev)}
798
385
  >
799
- <CardHeader className="pb-2">
800
- <div className="flex items-center gap-2">
801
- <button
802
- type="button"
803
- className="inline-flex h-6 w-6 items-center justify-center rounded border border-border text-[11px] hover:bg-accent"
804
- onClick={() => setNodeCardExpanded(el.id, !isNodeCardExpanded(el.id))}
805
- aria-label={isNodeCardExpanded(el.id) ? t("panel.config.collapseNodeConfig") : t("panel.config.expandNodeConfig")}
806
- >
807
- {isNodeCardExpanded(el.id) ? "▾" : "▸"}
808
- </button>
809
- <CardTitle className="min-w-0 flex-1 text-xs truncate">
810
- {el.name?.trim() || el.materialType || t("common.node")} · {el.id}
811
- </CardTitle>
812
- <button
813
- type="button"
814
- className="inline-flex h-6 items-center justify-center rounded border border-border px-2 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground"
815
- onClick={() => onExcludeSelectedNode?.(el.id)}
816
- title={t("panel.config.removeFromSelectionTitle")}
817
- >
818
- {t("panel.config.removeFromSelection")}
819
- </button>
386
+ {isSearchCollapsed ? t("panel.config.expandSearch") : t("panel.config.collapseSearch")}
387
+ </button>
388
+ </div>
389
+ {!isSearchCollapsed ? (
390
+ <div className="mt-2">
391
+ <Input
392
+ value={configSearch}
393
+ onChange={(e) => setConfigSearch(e.target.value)}
394
+ placeholder={t("panel.config.searchPlaceholder")}
395
+ className="h-7"
396
+ data-scope-autocomplete="off"
397
+ />
398
+ {hasSearch ? (
399
+ <div className="mt-1 text-[11px] text-muted-foreground">
400
+ {t("panel.config.searching", { query: configSearch })}
820
401
  </div>
821
- </CardHeader>
822
- {isNodeCardExpanded(el.id) ? (
823
- <CardContent className="space-y-2 text-xs">
824
- <label className="flex items-center gap-2">
825
- <Checkbox
826
- checked={el.locked === true}
827
- onCheckedChange={(checked) => updateElement(el.id, { locked: checked === true })}
828
- />
829
- <span>{t("panel.config.lockedNode")}</span>
830
- </label>
831
- {el.locked ? (
832
- <div className="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700 dark:text-amber-300">
833
- {t("panel.config.lockedNodeHint")}
834
- </div>
835
- ) : null}
836
- <fieldset disabled={el.locked} className={el.locked ? "opacity-60" : ""}>
837
- <div className="space-y-2">
838
- <label className="block space-y-1">
839
- <div>{t("panel.config.name")}</div>
840
- <Input
841
- className="h-7"
842
- value={el.name ?? ""}
843
- onChange={(e) => updateElement(el.id, { name: e.target.value || undefined })}
844
- />
845
- </label>
846
- <div className="grid grid-cols-2 gap-2">
847
- <label className="block space-y-1">
848
- <div>zIndex</div>
849
- <Input
850
- className="h-7"
851
- type="number"
852
- value={el.zIndex ?? 1}
853
- onChange={(e) =>
854
- updateElement(el.id, { zIndex: Number(e.target.value) || 1 })
855
- }
856
- />
857
- </label>
858
- <label className="block space-y-1">
859
- <div>{t("panel.config.layer")}</div>
860
- <Select
861
- value={el.layerId}
862
- onValueChange={(value) => updateElement(el.id, { layerId: value })}
863
- >
864
- <SelectTrigger className="h-7">
865
- <SelectValue placeholder={t("panel.config.selectLayer")} />
866
- </SelectTrigger>
867
- <SelectContent>
868
- {layers.map((layer) => (
869
- <SelectItem key={layer.id} value={layer.id}>
870
- {layer.name}
871
- </SelectItem>
872
- ))}
873
- </SelectContent>
874
- </Select>
875
- </label>
876
- </div>
877
- <div className="grid grid-cols-2 gap-2">
878
- <label className="block space-y-1">
879
- <div>X</div>
880
- <Input
881
- className="h-7"
882
- type="number"
883
- value={el.x}
884
- onChange={(e) => updateElement(el.id, { x: Number(e.target.value) || 0 })}
885
- />
886
- </label>
887
- <label className="block space-y-1">
888
- <div>Y</div>
889
- <Input
890
- className="h-7"
891
- type="number"
892
- value={el.y}
893
- onChange={(e) => updateElement(el.id, { y: Number(e.target.value) || 0 })}
894
- />
895
- </label>
896
- <label className="block space-y-1">
897
- <div>{t("panel.config.rotate")}</div>
898
- <Input
899
- className="h-7"
900
- type="number"
901
- value={el.rotate ?? 0}
902
- onChange={(e) =>
903
- updateElement(el.id, { rotate: Number(e.target.value) || 0 })
904
- }
905
- />
906
- </label>
907
- <label className="block space-y-1">
908
- <div>{t("panel.config.width")}</div>
909
- <Input
910
- className="h-7"
911
- type="number"
912
- min={1}
913
- value={el.width}
914
- onChange={(e) =>
915
- updateElement(el.id, { width: Math.max(1, Number(e.target.value) || 1) })
916
- }
917
- />
918
- </label>
919
- <label className="block space-y-1">
920
- <div>{t("panel.config.height")}</div>
921
- <Input
922
- className="h-7"
923
- type="number"
924
- min={1}
925
- value={el.height}
926
- onChange={(e) =>
927
- updateElement(el.id, { height: Math.max(1, Number(e.target.value) || 1) })
928
- }
929
- />
930
- </label>
931
- </div>
932
- <div className="grid grid-cols-2 gap-2">
933
- <label className="block space-y-1">
934
- <div>{t("panel.config.backgroundColor")}</div>
935
- <Input
936
- className="h-7"
937
- value={el.style?.backgroundColor ?? ""}
938
- placeholder="#000000"
939
- onChange={(e) =>
940
- updateElement(el.id, {
941
- style: {
942
- ...(el.style ?? {}),
943
- backgroundColor: e.target.value || undefined,
944
- },
945
- })
946
- }
947
- />
948
- </label>
949
- <label className="block space-y-1">
950
- <div>{t("panel.config.borderColorShort")}</div>
951
- <Input
952
- className="h-7"
953
- value={el.style?.borderColor ?? ""}
954
- placeholder="#000000"
955
- onChange={(e) =>
956
- updateElement(el.id, {
957
- style: {
958
- ...(el.style ?? {}),
959
- borderColor: e.target.value || undefined,
960
- },
961
- })
962
- }
963
- />
964
- </label>
965
- </div>
966
- {CHART_TYPES.has(el.materialType ?? "") ? (
967
- <div className="grid grid-cols-2 gap-2">
968
- <label className="block space-y-1 col-span-2">
969
- <div>{t("panel.config.chartTitle")}</div>
970
- <Input
971
- className="h-7"
972
- value={el.chart?.title ?? ""}
973
- onChange={(e) =>
974
- updateElement(el.id, {
975
- chart: { ...(el.chart ?? {}), title: e.target.value },
976
- })
977
- }
978
- />
979
- </label>
980
- <label className="block space-y-1">
981
- <div>{t("panel.config.primaryColor")}</div>
982
- <Input
983
- className="h-7"
984
- value={el.chart?.color ?? ""}
985
- onChange={(e) =>
986
- updateElement(el.id, {
987
- chart: { ...(el.chart ?? {}), color: e.target.value || undefined },
988
- })
989
- }
990
- />
991
- </label>
992
- <label className="flex items-center gap-2">
993
- <Checkbox
994
- checked={el.chart?.colorMode === "gradient"}
995
- onCheckedChange={(checked) =>
996
- updateElement(el.id, {
997
- chart: { ...(el.chart ?? {}), colorMode: checked ? "gradient" : "solid" },
998
- })
999
- }
1000
- />
1001
- <span>{t("panel.config.primaryGradient")}</span>
1002
- </label>
1003
- {el.chart?.colorMode === "gradient" ? (
1004
- <>
1005
- <label className="block space-y-1">
1006
- <div>{t("panel.config.gradientFrom")}</div>
1007
- <Input
1008
- className="h-7"
1009
- value={el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}
1010
- onChange={(e) =>
1011
- updateElement(el.id, {
1012
- chart: { ...(el.chart ?? {}), gradientFrom: e.target.value || "#3b82f6" },
1013
- })
1014
- }
1015
- />
1016
- </label>
1017
- <label className="block space-y-1">
1018
- <div>{t("panel.config.gradientTo")}</div>
1019
- <Input
1020
- className="h-7"
1021
- value={el.chart?.gradientTo ?? "#22d3ee"}
1022
- onChange={(e) =>
1023
- updateElement(el.id, {
1024
- chart: { ...(el.chart ?? {}), gradientTo: e.target.value || "#22d3ee" },
1025
- })
1026
- }
1027
- />
1028
- </label>
1029
- <label className="block space-y-1 col-span-2">
1030
- <div>{t("panel.config.gradientDirection")}</div>
1031
- <Select
1032
- value={el.chart?.gradientDirection ?? "to-right"}
1033
- onValueChange={(value) =>
1034
- updateElement(el.id, {
1035
- chart: {
1036
- ...(el.chart ?? {}),
1037
- gradientDirection: value as
1038
- | "to-right"
1039
- | "to-bottom"
1040
- | "to-bottom-right"
1041
- | "to-top-right",
1042
- },
1043
- })
1044
- }
1045
- >
1046
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1047
- <SelectContent>
1048
- <SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
1049
- <SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
1050
- <SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
1051
- <SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
1052
- </SelectContent>
1053
- </Select>
1054
- </label>
1055
- <div className="col-span-2 space-y-1">
1056
- <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
1057
- <div
1058
- className="h-6 rounded border border-border/60"
1059
- style={{
1060
- backgroundImage: `linear-gradient(${
1061
- (el.chart?.gradientDirection ?? "to-right") === "to-bottom"
1062
- ? "to bottom"
1063
- : (el.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
1064
- ? "to bottom right"
1065
- : (el.chart?.gradientDirection ?? "to-right") === "to-top-right"
1066
- ? "to top right"
1067
- : "to right"
1068
- }, ${el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}, ${el.chart?.gradientTo ?? "#22d3ee"})`,
1069
- }}
1070
- />
1071
- </div>
1072
- </>
1073
- ) : null}
1074
- <label className="block space-y-1">
1075
- <div>{t("panel.config.render")}</div>
1076
- <Select
1077
- value={el.chart?.renderMode ?? "canvas"}
1078
- onValueChange={(value) =>
1079
- updateElement(el.id, {
1080
- chart: { ...(el.chart ?? {}), renderMode: value as "canvas" | "svg" },
1081
- })
1082
- }
1083
- >
1084
- <SelectTrigger className="h-7">
1085
- <SelectValue />
1086
- </SelectTrigger>
1087
- <SelectContent>
1088
- <SelectItem value="canvas">canvas</SelectItem>
1089
- <SelectItem value="svg">svg</SelectItem>
1090
- </SelectContent>
1091
- </Select>
1092
- </label>
1093
- <label className="block space-y-1">
1094
- <div>{t("panel.config.tooltipBg")}</div>
1095
- <Input
1096
- className="h-7"
1097
- value={el.chart?.tooltipBackgroundColor ?? ""}
1098
- placeholder="#0f172a"
1099
- onChange={(e) =>
1100
- updateElement(el.id, {
1101
- chart: {
1102
- ...(el.chart ?? {}),
1103
- tooltipBackgroundColor: e.target.value || undefined,
1104
- },
1105
- })
1106
- }
1107
- />
1108
- </label>
1109
- <label className="flex items-center gap-2">
1110
- <Checkbox
1111
- checked={el.chart?.tooltipShow ?? true}
1112
- onCheckedChange={(checked) =>
1113
- updateElement(el.id, {
1114
- chart: { ...(el.chart ?? {}), tooltipShow: checked === true },
1115
- })
1116
- }
1117
- />
1118
- <span>{t("panel.config.showTooltip")}</span>
1119
- </label>
1120
- <label className="block space-y-1">
1121
- <div>{t("panel.config.tooltipTrigger")}</div>
1122
- <Select
1123
- value={el.chart?.tooltipTrigger ?? "axis"}
1124
- onValueChange={(value) =>
1125
- updateElement(el.id, {
1126
- chart: { ...(el.chart ?? {}), tooltipTrigger: value as "axis" | "item" },
1127
- })
1128
- }
1129
- >
1130
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1131
- <SelectContent>
1132
- <SelectItem value="axis">axis</SelectItem>
1133
- <SelectItem value="item">item</SelectItem>
1134
- </SelectContent>
1135
- </Select>
1136
- </label>
1137
- <label className="block space-y-1">
1138
- <div>{t("panel.config.tooltipTextColor")}</div>
1139
- <Input
1140
- className="h-7"
1141
- value={el.chart?.tooltipTextColor ?? ""}
1142
- placeholder="#f8fafc"
1143
- onChange={(e) =>
1144
- updateElement(el.id, {
1145
- chart: {
1146
- ...(el.chart ?? {}),
1147
- tooltipTextColor: e.target.value || undefined,
1148
- },
1149
- })
1150
- }
1151
- />
1152
- </label>
1153
- <label className="block space-y-1 col-span-2">
1154
- {renderFormatterLabel("Tooltip Formatter")}
1155
- <Input
1156
- className="h-7"
1157
- value={el.chart?.tooltipFormatter ?? ""}
1158
- placeholder="{b}: {c}"
1159
- onChange={(e) =>
1160
- updateElement(el.id, {
1161
- chart: {
1162
- ...(el.chart ?? {}),
1163
- tooltipFormatter: e.target.value || undefined,
1164
- },
1165
- })
1166
- }
1167
- />
1168
- </label>
1169
- <label className="block space-y-1 col-span-2">
1170
- <div>{t("panel.config.labelsCsv")}</div>
1171
- <Input
1172
- className="h-7"
1173
- value={getChartLabelsDisplayText(el.chart)}
1174
- onChange={(e) =>
1175
- updateElement(el.id, {
1176
- chart: {
1177
- ...(el.chart ?? {}),
1178
- labelsText: e.target.value,
1179
- },
1180
- })
1181
- }
1182
- />
1183
- </label>
1184
- <label className="block space-y-1 col-span-2">
1185
- <div>{t("panel.config.valuesCsv")}</div>
1186
- <Input
1187
- className="h-7"
1188
- value={getChartValuesDisplayText(el.chart)}
1189
- onChange={(e) =>
1190
- updateElement(el.id, {
1191
- chart: {
1192
- ...(el.chart ?? {}),
1193
- valuesText: e.target.value,
1194
- },
1195
- })
1196
- }
1197
- />
1198
- </label>
1199
- {["bar", "line", "area", "scatter"].includes(el.materialType ?? "") ? (
1200
- <>
1201
- <label className="block space-y-1">
1202
- <div>{t("panel.config.xAxisName")}</div>
1203
- <Input
1204
- className="h-7"
1205
- value={el.chart?.xAxisName ?? ""}
1206
- onChange={(e) =>
1207
- updateElement(el.id, {
1208
- chart: { ...(el.chart ?? {}), xAxisName: e.target.value },
1209
- })
1210
- }
1211
- />
1212
- </label>
1213
- <label className="block space-y-1">
1214
- <div>{t("panel.config.yAxisName")}</div>
1215
- <Input
1216
- className="h-7"
1217
- value={el.chart?.yAxisName ?? ""}
1218
- onChange={(e) =>
1219
- updateElement(el.id, {
1220
- chart: { ...(el.chart ?? {}), yAxisName: e.target.value },
1221
- })
1222
- }
1223
- />
1224
- </label>
1225
- <label className="block space-y-1">
1226
- <div>{t("panel.config.xAxisLabelColor")}</div>
1227
- <Input
1228
- className="h-7"
1229
- value={el.chart?.xAxisLabelColor ?? ""}
1230
- placeholder="#64748b"
1231
- onChange={(e) =>
1232
- updateElement(el.id, {
1233
- chart: { ...(el.chart ?? {}), xAxisLabelColor: e.target.value || undefined },
1234
- })
1235
- }
1236
- />
1237
- </label>
1238
- <label className="block space-y-1">
1239
- <div>{t("panel.config.yAxisLabelColor")}</div>
1240
- <Input
1241
- className="h-7"
1242
- value={el.chart?.yAxisLabelColor ?? ""}
1243
- placeholder="#64748b"
1244
- onChange={(e) =>
1245
- updateElement(el.id, {
1246
- chart: { ...(el.chart ?? {}), yAxisLabelColor: e.target.value || undefined },
1247
- })
1248
- }
1249
- />
1250
- </label>
1251
- <label className="block space-y-1">
1252
- <div>{t("panel.config.xAxisLabelFontSize")}</div>
1253
- <Input
1254
- className="h-7"
1255
- type="number"
1256
- min={8}
1257
- max={48}
1258
- value={el.chart?.xAxisLabelFontSize ?? 10}
1259
- onChange={(e) =>
1260
- updateElement(el.id, {
1261
- chart: {
1262
- ...(el.chart ?? {}),
1263
- xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
1264
- },
1265
- })
1266
- }
1267
- />
1268
- </label>
1269
- <label className="block space-y-1">
1270
- <div>{t("panel.config.yAxisLabelFontSize")}</div>
1271
- <Input
1272
- className="h-7"
1273
- type="number"
1274
- min={8}
1275
- max={48}
1276
- value={el.chart?.yAxisLabelFontSize ?? 10}
1277
- onChange={(e) =>
1278
- updateElement(el.id, {
1279
- chart: {
1280
- ...(el.chart ?? {}),
1281
- yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
1282
- },
1283
- })
1284
- }
1285
- />
1286
- </label>
1287
- <label className="flex items-center gap-2">
1288
- <Checkbox
1289
- checked={el.chart?.xAxisLabelAutoEllipsis ?? false}
1290
- onCheckedChange={(checked) =>
1291
- updateElement(el.id, {
1292
- chart: { ...(el.chart ?? {}), xAxisLabelAutoEllipsis: checked === true },
1293
- })
1294
- }
1295
- />
1296
- <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
1297
- </label>
1298
- <label className="flex items-center gap-2">
1299
- <Checkbox
1300
- checked={el.chart?.yAxisLabelAutoEllipsis ?? false}
1301
- onCheckedChange={(checked) =>
1302
- updateElement(el.id, {
1303
- chart: { ...(el.chart ?? {}), yAxisLabelAutoEllipsis: checked === true },
1304
- })
1305
- }
1306
- />
1307
- <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
1308
- </label>
1309
- <label className="flex items-center gap-2">
1310
- <Checkbox
1311
- checked={el.chart?.xAxisTickShow ?? true}
1312
- onCheckedChange={(checked) =>
1313
- updateElement(el.id, {
1314
- chart: { ...(el.chart ?? {}), xAxisTickShow: checked === true },
1315
- })
1316
- }
1317
- />
1318
- <span>{t("panel.config.xAxisTick")}</span>
1319
- </label>
1320
- <label className="flex items-center gap-2">
1321
- <Checkbox
1322
- checked={el.chart?.yAxisTickShow ?? true}
1323
- onCheckedChange={(checked) =>
1324
- updateElement(el.id, {
1325
- chart: { ...(el.chart ?? {}), yAxisTickShow: checked === true },
1326
- })
1327
- }
1328
- />
1329
- <span>{t("panel.config.yAxisTick")}</span>
1330
- </label>
1331
- <label className="block space-y-1">
1332
- <div>{t("panel.config.xAxisTickColor")}</div>
1333
- <Input
1334
- className="h-7"
1335
- value={el.chart?.xAxisTickColor ?? ""}
1336
- placeholder="#94a3b8"
1337
- onChange={(e) =>
1338
- updateElement(el.id, {
1339
- chart: { ...(el.chart ?? {}), xAxisTickColor: e.target.value || undefined },
1340
- })
1341
- }
1342
- />
1343
- </label>
1344
- <label className="block space-y-1">
1345
- <div>{t("panel.config.yAxisTickColor")}</div>
1346
- <Input
1347
- className="h-7"
1348
- value={el.chart?.yAxisTickColor ?? ""}
1349
- placeholder="#94a3b8"
1350
- onChange={(e) =>
1351
- updateElement(el.id, {
1352
- chart: { ...(el.chart ?? {}), yAxisTickColor: e.target.value || undefined },
1353
- })
1354
- }
1355
- />
1356
- </label>
1357
- <label className="flex items-center gap-2">
1358
- <Checkbox
1359
- checked={el.chart?.xAxisSplitLineShow ?? false}
1360
- onCheckedChange={(checked) =>
1361
- updateElement(el.id, {
1362
- chart: { ...(el.chart ?? {}), xAxisSplitLineShow: checked === true },
1363
- })
1364
- }
1365
- />
1366
- <span>{t("panel.config.xAxisSplitLine")}</span>
1367
- </label>
1368
- <label className="flex items-center gap-2">
1369
- <Checkbox
1370
- checked={el.chart?.yAxisSplitLineShow ?? true}
1371
- onCheckedChange={(checked) =>
1372
- updateElement(el.id, {
1373
- chart: { ...(el.chart ?? {}), yAxisSplitLineShow: checked === true },
1374
- })
1375
- }
1376
- />
1377
- <span>{t("panel.config.yAxisSplitLine")}</span>
1378
- </label>
1379
- <label className="block space-y-1">
1380
- <div>{t("panel.config.xAxisSplitLineColor")}</div>
1381
- <Input
1382
- className="h-7"
1383
- value={el.chart?.xAxisSplitLineColor ?? ""}
1384
- placeholder="#e2e8f0"
1385
- onChange={(e) =>
1386
- updateElement(el.id, {
1387
- chart: { ...(el.chart ?? {}), xAxisSplitLineColor: e.target.value || undefined },
1388
- })
1389
- }
1390
- />
1391
- </label>
1392
- <label className="block space-y-1">
1393
- <div>{t("panel.config.yAxisSplitLineColor")}</div>
1394
- <Input
1395
- className="h-7"
1396
- value={el.chart?.yAxisSplitLineColor ?? ""}
1397
- placeholder="#e2e8f0"
1398
- onChange={(e) =>
1399
- updateElement(el.id, {
1400
- chart: { ...(el.chart ?? {}), yAxisSplitLineColor: e.target.value || undefined },
1401
- })
1402
- }
1403
- />
1404
- </label>
1405
- </>
1406
- ) : null}
1407
- {el.materialType === "gauge" ? (
1408
- <label className="block space-y-1">
1409
- <div>{t("panel.config.gaugeValue")}</div>
1410
- <Input
1411
- className="h-7"
1412
- type="number"
1413
- value={el.chart?.values?.[0] ?? 0}
1414
- onChange={(e) =>
1415
- updateElement(el.id, {
1416
- chart: {
1417
- ...(el.chart ?? {}),
1418
- values: [Number(e.target.value) || 0],
1419
- },
1420
- })
1421
- }
1422
- />
1423
- </label>
1424
- ) : null}
1425
- {el.materialType === "bar" ? (
1426
- <label className="block space-y-1">
1427
- <div>{t("panel.config.barWidth")}</div>
1428
- <Input
1429
- className="h-7"
1430
- type="number"
1431
- min={1}
1432
- placeholder={t("panel.config.barWidthAuto")}
1433
- value={el.chart?.barWidth ?? ""}
1434
- onChange={(e) => {
1435
- const raw = e.target.value.trim();
1436
- updateElement(el.id, {
1437
- chart: {
1438
- ...(el.chart ?? {}),
1439
- barWidth: raw
1440
- ? Math.max(1, Number(raw) || 1)
1441
- : undefined,
1442
- },
1443
- });
1444
- }}
1445
- />
1446
- </label>
1447
- ) : null}
1448
- {(el.materialType === "line" || el.materialType === "area") ? (
1449
- <label className="flex items-center gap-2">
1450
- <Checkbox
1451
- checked={el.chart?.smooth ?? true}
1452
- onCheckedChange={(checked) =>
1453
- updateElement(el.id, {
1454
- chart: { ...(el.chart ?? {}), smooth: checked === true },
1455
- })
1456
- }
1457
- />
1458
- <span>{t("panel.config.smooth")}</span>
1459
- </label>
1460
- ) : null}
1461
- {el.materialType === "pie" ? (
1462
- <>
1463
- <label className="block space-y-1">
1464
- <div>{t("panel.config.pieInnerRadius")}</div>
1465
- <Input
1466
- className="h-7"
1467
- type="number"
1468
- min={0}
1469
- max={99}
1470
- value={el.chart?.pieInnerRadius ?? 30}
1471
- onChange={(e) =>
1472
- updateElement(el.id, {
1473
- chart: {
1474
- ...(el.chart ?? {}),
1475
- pieInnerRadius: Math.max(0, Math.min(99, Number(e.target.value) || 0)),
1476
- },
1477
- })
1478
- }
1479
- />
1480
- </label>
1481
- <label className="block space-y-1">
1482
- <div>{t("panel.config.pieOuterRadius")}</div>
1483
- <Input
1484
- className="h-7"
1485
- type="number"
1486
- min={1}
1487
- max={100}
1488
- value={el.chart?.pieOuterRadius ?? 65}
1489
- onChange={(e) =>
1490
- updateElement(el.id, {
1491
- chart: {
1492
- ...(el.chart ?? {}),
1493
- pieOuterRadius: Math.max(1, Math.min(100, Number(e.target.value) || 1)),
1494
- },
1495
- })
1496
- }
1497
- />
1498
- </label>
1499
- </>
1500
- ) : null}
1501
- <label className="block space-y-1">
1502
- {renderOptionLabel(t("panel.config.gridLeft"), "grid.left", t("panel.config.gridLeftHint"))}
1503
- <Input
1504
- className="h-7"
1505
- type="number"
1506
- value={Number((el.chart?.option as any)?.grid?.left ?? 28)}
1507
- onChange={(e) =>
1508
- updateElement(el.id, {
1509
- chart: {
1510
- ...(el.chart ?? {}),
1511
- option: mergeOptionPatch(el.chart?.option as any, {
1512
- grid: { left: Number(e.target.value) || 0 },
1513
- }),
1514
- },
1515
- })
1516
- }
1517
- />
1518
- </label>
1519
- <label className="block space-y-1">
1520
- {renderOptionLabel(t("panel.config.gridRight"), "grid.right", t("panel.config.gridRightHint"))}
1521
- <Input
1522
- className="h-7"
1523
- type="number"
1524
- value={Number((el.chart?.option as any)?.grid?.right ?? 10)}
1525
- onChange={(e) =>
1526
- updateElement(el.id, {
1527
- chart: {
1528
- ...(el.chart ?? {}),
1529
- option: mergeOptionPatch(el.chart?.option as any, {
1530
- grid: { right: Number(e.target.value) || 0 },
1531
- }),
1532
- },
1533
- })
1534
- }
1535
- />
1536
- </label>
1537
- <label className="block space-y-1">
1538
- {renderOptionLabel(t("panel.config.gridTop"), "grid.top", t("panel.config.gridTopHint"))}
1539
- <Input
1540
- className="h-7"
1541
- type="number"
1542
- value={Number((el.chart?.option as any)?.grid?.top ?? 30)}
1543
- onChange={(e) =>
1544
- updateElement(el.id, {
1545
- chart: {
1546
- ...(el.chart ?? {}),
1547
- option: mergeOptionPatch(el.chart?.option as any, {
1548
- grid: { top: Number(e.target.value) || 0 },
1549
- }),
1550
- },
1551
- })
1552
- }
1553
- />
1554
- </label>
1555
- <label className="block space-y-1">
1556
- {renderOptionLabel(t("panel.config.gridBottom"), "grid.bottom", t("panel.config.gridBottomHint"))}
1557
- <Input
1558
- className="h-7"
1559
- type="number"
1560
- value={Number((el.chart?.option as any)?.grid?.bottom ?? 20)}
1561
- onChange={(e) =>
1562
- updateElement(el.id, {
1563
- chart: {
1564
- ...(el.chart ?? {}),
1565
- option: mergeOptionPatch(el.chart?.option as any, {
1566
- grid: { bottom: Number(e.target.value) || 0 },
1567
- }),
1568
- },
1569
- })
1570
- }
1571
- />
1572
- </label>
1573
- <label className="block space-y-1">
1574
- {renderOptionLabel(t("panel.config.legendPosition"), "legend.top", t("panel.config.legendPositionHint"))}
1575
- <Select
1576
- value={String((el.chart?.option as any)?.legend?.top ?? "top")}
1577
- onValueChange={(value) =>
1578
- updateElement(el.id, {
1579
- chart: {
1580
- ...(el.chart ?? {}),
1581
- option: mergeOptionPatch(el.chart?.option as any, {
1582
- legend: { top: value },
1583
- }),
1584
- },
1585
- })
1586
- }
1587
- >
1588
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1589
- <SelectContent>
1590
- <SelectItem value="top">top</SelectItem>
1591
- <SelectItem value="bottom">bottom</SelectItem>
1592
- <SelectItem value="left">left</SelectItem>
1593
- <SelectItem value="right">right</SelectItem>
1594
- </SelectContent>
1595
- </Select>
1596
- </label>
1597
- <label className="block space-y-1">
1598
- {renderOptionLabel(t("panel.config.legendOrient"), "legend.orient", t("panel.config.legendOrientHint"))}
1599
- <Select
1600
- value={String((el.chart?.option as any)?.legend?.orient ?? "horizontal")}
1601
- onValueChange={(value) =>
1602
- updateElement(el.id, {
1603
- chart: {
1604
- ...(el.chart ?? {}),
1605
- option: mergeOptionPatch(el.chart?.option as any, {
1606
- legend: { orient: value },
1607
- }),
1608
- },
1609
- })
1610
- }
1611
- >
1612
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1613
- <SelectContent>
1614
- <SelectItem value="horizontal">horizontal</SelectItem>
1615
- <SelectItem value="vertical">vertical</SelectItem>
1616
- </SelectContent>
1617
- </Select>
1618
- </label>
1619
- <label className="flex items-center gap-2">
1620
- <Checkbox
1621
- checked={Boolean((el.chart?.option as any)?.legend?.show ?? true)}
1622
- onCheckedChange={(checked) =>
1623
- updateElement(el.id, {
1624
- chart: {
1625
- ...(el.chart ?? {}),
1626
- option: mergeOptionPatch(el.chart?.option as any, {
1627
- legend: { show: checked === true },
1628
- }),
1629
- },
1630
- })
1631
- }
1632
- />
1633
- <span>{t("panel.config.showLegendWithKey")}</span>
1634
- </label>
1635
- <label className="flex items-center gap-2">
1636
- <Checkbox
1637
- checked={Boolean(
1638
- Array.isArray((el.chart?.option as any)?.dataZoom) &&
1639
- (el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside")
1640
- )}
1641
- onCheckedChange={(checked) => {
1642
- const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
1643
- ? [...(el.chart?.option as any).dataZoom]
1644
- : [];
1645
- const next = checked
1646
- ? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }]
1647
- : prev.filter((z: any) => z?.type !== "inside");
1648
- updateElement(el.id, {
1649
- chart: {
1650
- ...(el.chart ?? {}),
1651
- option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
1652
- },
1653
- });
1654
- }}
1655
- />
1656
- <span>{t("panel.config.zoomInsideWithKey")}</span>
1657
- </label>
1658
- <label className="flex items-center gap-2">
1659
- <Checkbox
1660
- checked={Boolean(
1661
- Array.isArray((el.chart?.option as any)?.dataZoom) &&
1662
- (el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider")
1663
- )}
1664
- onCheckedChange={(checked) => {
1665
- const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
1666
- ? [...(el.chart?.option as any).dataZoom]
1667
- : [];
1668
- const next = checked
1669
- ? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }]
1670
- : prev.filter((z: any) => z?.type !== "slider");
1671
- updateElement(el.id, {
1672
- chart: {
1673
- ...(el.chart ?? {}),
1674
- option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
1675
- },
1676
- });
1677
- }}
1678
- />
1679
- <span>{t("panel.config.zoomSliderWithKey")}</span>
1680
- </label>
1681
- <label className="flex items-center gap-2">
1682
- <Checkbox
1683
- checked={Boolean((el.chart?.option as any)?.axisPointer?.show ?? false)}
1684
- onCheckedChange={(checked) =>
1685
- updateElement(el.id, {
1686
- chart: {
1687
- ...(el.chart ?? {}),
1688
- option: mergeOptionPatch(el.chart?.option as any, {
1689
- axisPointer: { show: checked === true },
1690
- }),
1691
- },
1692
- })
1693
- }
1694
- />
1695
- <span>{t("panel.config.showAxisPointerWithKey")}</span>
1696
- </label>
1697
- <label className="block space-y-1">
1698
- <div>{t("panel.config.axisPointerTypeWithKey")}</div>
1699
- <Select
1700
- value={String((el.chart?.option as any)?.axisPointer?.type ?? "line")}
1701
- onValueChange={(value) =>
1702
- updateElement(el.id, {
1703
- chart: {
1704
- ...(el.chart ?? {}),
1705
- option: mergeOptionPatch(el.chart?.option as any, {
1706
- axisPointer: { type: value },
1707
- }),
1708
- },
1709
- })
1710
- }
1711
- >
1712
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1713
- <SelectContent>
1714
- <SelectItem value="line">line</SelectItem>
1715
- <SelectItem value="shadow">shadow</SelectItem>
1716
- <SelectItem value="cross">cross</SelectItem>
1717
- </SelectContent>
1718
- </Select>
1719
- </label>
1720
- <label className="block space-y-1 col-span-2">
1721
- <div>{t("panel.config.advancedOptionJson")}</div>
1722
- <Textarea
1723
- className="h-28 font-mono text-[11px]"
1724
- defaultValue={JSON.stringify(el.chart?.option ?? {}, null, 2)}
1725
- placeholder={t("panel.config.advancedJsonPlaceholder")}
1726
- onBlur={(e) => {
1727
- const nextText = e.target.value.trim();
1728
- if (!nextText) {
1729
- updateElement(el.id, { chart: { ...(el.chart ?? {}), option: undefined } });
1730
- return;
1731
- }
1732
- try {
1733
- const parsed = JSON.parse(nextText) as Record<string, unknown>;
1734
- updateElement(el.id, { chart: { ...(el.chart ?? {}), option: parsed } });
1735
- } catch {
1736
- // ignore invalid json input on blur
1737
- }
1738
- }}
1739
- />
1740
- </label>
1741
- </div>
1742
- ) : null}
1743
- {el.materialType === "text" ? (
1744
- <div className="grid grid-cols-2 gap-2">
1745
- <label className="block space-y-1 col-span-2">
1746
- <div>{t("panel.config.textHtml")}</div>
1747
- <Textarea
1748
- className="h-24"
1749
- value={el.textHtml ?? ""}
1750
- onChange={(e) => updateElement(el.id, { textHtml: e.target.value || "<p><br/></p>" })}
1751
- />
1752
- </label>
1753
- <label className="block space-y-1">
1754
- <div>{t("panel.config.fontSize")}</div>
1755
- <Input
1756
- className="h-7"
1757
- type="number"
1758
- min={8}
1759
- value={el.textFontSize ?? 14}
1760
- onChange={(e) => updateElement(el.id, { textFontSize: Math.max(8, Number(e.target.value) || 14) })}
1761
- />
1762
- </label>
1763
- <label className="block space-y-1">
1764
- <div>{t("panel.config.textColor")}</div>
1765
- <Input
1766
- className="h-7"
1767
- value={el.textColor ?? ""}
1768
- onChange={(e) => updateElement(el.id, { textColor: e.target.value || undefined })}
1769
- />
1770
- </label>
1771
- </div>
1772
- ) : null}
1773
- {el.materialType === "audio" ? (
1774
- <div className="grid grid-cols-2 gap-2">
1775
- <label className="block space-y-1 col-span-2">
1776
- <div>{t("panel.config.audioUrl")}</div>
1777
- <Input
1778
- className="h-7"
1779
- value={el.audioRemoteUrl ?? ""}
1780
- onChange={(e) => updateElement(el.id, { audioRemoteUrl: e.target.value || undefined })}
1781
- />
1782
- </label>
1783
- <label className="block space-y-1">
1784
- <div>{t("panel.config.effect")}</div>
1785
- <Select
1786
- value={el.audioVisualEffect ?? "pulse"}
1787
- onValueChange={(value) =>
1788
- updateElement(el.id, { audioVisualEffect: value as "none" | "pulse" | "ripple" })
1789
- }
1790
- >
1791
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1792
- <SelectContent>
1793
- <SelectItem value="none">none</SelectItem>
1794
- <SelectItem value="pulse">pulse</SelectItem>
1795
- <SelectItem value="ripple">ripple</SelectItem>
1796
- </SelectContent>
1797
- </Select>
1798
- </label>
1799
- <label className="block space-y-1">
1800
- <div>{t("panel.config.speed")}</div>
1801
- <Select
1802
- value={el.audioVisualSpeed ?? "normal"}
1803
- onValueChange={(value) =>
1804
- updateElement(el.id, { audioVisualSpeed: value as "slow" | "normal" | "fast" })
1805
- }
1806
- >
1807
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1808
- <SelectContent>
1809
- <SelectItem value="slow">slow</SelectItem>
1810
- <SelectItem value="normal">normal</SelectItem>
1811
- <SelectItem value="fast">fast</SelectItem>
1812
- </SelectContent>
1813
- </Select>
1814
- </label>
1815
- </div>
1816
- ) : null}
1817
- {el.materialType === "video" ? (
1818
- <label className="block space-y-1">
1819
- <div>{t("panel.config.videoUrl")}</div>
1820
- <Input
1821
- className="h-7"
1822
- value={el.videoRemoteUrl ?? ""}
1823
- onChange={(e) => updateElement(el.id, { videoRemoteUrl: e.target.value || undefined })}
1824
- />
1825
- </label>
1826
- ) : null}
1827
- {el.materialType === "grid" ? (
1828
- <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1829
- <label className="block space-y-1.5">
1830
- <div>{t("panel.config.rows")}</div>
1831
- <Input className="h-7" type="number" min={1} value={el.gridRows ?? 2} onChange={(e) => updateElement(el.id, { gridRows: Math.max(1, Number(e.target.value) || 2) })} />
1832
- </label>
1833
- <label className="block space-y-1.5">
1834
- <div>{t("panel.config.cols")}</div>
1835
- <Input className="h-7" type="number" min={1} value={el.gridCols ?? 3} onChange={(e) => updateElement(el.id, { gridCols: Math.max(1, Number(e.target.value) || 3) })} />
1836
- </label>
1837
- </div>
1838
- ) : null}
1839
- {el.materialType === "geometry" ? (
1840
- <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1841
- <label className="block space-y-1.5">
1842
- <div>{t("panel.config.shape")}</div>
1843
- <Select
1844
- value={el.geometryShape ?? "rect"}
1845
- onValueChange={(value) =>
1846
- updateElement(el.id, { geometryShape: value as PanelElement["geometryShape"] })
1847
- }
1848
- >
1849
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1850
- <SelectContent>
1851
- <SelectItem value="rect">{t("panel.config.shapeRect")}</SelectItem>
1852
- <SelectItem value="circle">{t("panel.config.shapeCircle")}</SelectItem>
1853
- <SelectItem value="triangle">{t("panel.config.shapeTriangle")}</SelectItem>
1854
- <SelectItem value="diamond">{t("panel.config.shapeDiamond")}</SelectItem>
1855
- <SelectItem value="hexagon">{t("panel.config.shapeHexagon")}</SelectItem>
1856
- <SelectItem value="star">{t("panel.config.shapeStar")}</SelectItem>
1857
- <SelectItem value="heart">{t("panel.config.shapeHeart")}</SelectItem>
1858
- </SelectContent>
1859
- </Select>
1860
- </label>
1861
- <label className="block space-y-1.5">
1862
- <div>{t("panel.config.color")}</div>
1863
- <Input
1864
- className="h-7"
1865
- value={el.geometryColor ?? "#3b82f6"}
1866
- onChange={(e) => updateElement(el.id, { geometryColor: e.target.value || "#3b82f6" })}
1867
- />
1868
- </label>
1869
- </div>
1870
- ) : null}
1871
- {el.materialType === "reference" || el.materialType === "viewport" ? (
1872
- <div className="grid grid-cols-2 gap-2">
1873
- <label className="block space-y-1.5">
1874
- <div>{t("panel.config.refLayer")}</div>
1875
- <Select
1876
- value={el.refLayerId ?? "__none__"}
1877
- onValueChange={(value) => updateElement(el.id, { refLayerId: value === "__none__" ? undefined : value })}
1878
- >
1879
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1880
- <SelectContent>
1881
- <SelectItem value="__none__">{t("common.none")}</SelectItem>
1882
- {layers.filter((l) => l.id !== el.layerId).map((l) => (
1883
- <SelectItem key={l.id} value={l.id}>{l.name}</SelectItem>
1884
- ))}
1885
- </SelectContent>
1886
- </Select>
1887
- </label>
1888
- <label className="block space-y-1.5">
1889
- <div>{t("panel.config.copy")}</div>
1890
- <Select
1891
- value={el.refCopyMode ?? "shallow"}
1892
- onValueChange={(value) => setReferenceCopyMode?.(el.id, value as ReferenceCopyMode)}
1893
- >
1894
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1895
- <SelectContent>
1896
- <SelectItem value="shallow">shallow</SelectItem>
1897
- <SelectItem value="deep">deep</SelectItem>
1898
- </SelectContent>
1899
- </Select>
1900
- </label>
1901
- </div>
1902
- ) : null}
1903
- {el.materialType === "viewport" ? (
1904
- <label className="block space-y-1.5">
1905
- <div>{t("panel.config.viewportOverflow")}</div>
1906
- <Select
1907
- value={normalizeViewportOverflow(el.viewportOverflow)}
1908
- onValueChange={(value) =>
1909
- updateElement(el.id, {
1910
- viewportOverflow: value as ViewportOverflowMode,
1911
- })
1912
- }
1913
- >
1914
- <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1915
- <SelectContent>
1916
- {VIEWPORT_OVERFLOW_MODES.map((mode) => (
1917
- <SelectItem key={mode} value={mode}>
1918
- {mode === "scroll-x"
1919
- ? t("panel.config.viewportOverflowScrollX")
1920
- : mode === "scroll-y"
1921
- ? t("panel.config.viewportOverflowScrollY")
1922
- : mode === "scroll"
1923
- ? t("panel.config.viewportOverflowScroll")
1924
- : t("panel.config.viewportOverflowClip")}
1925
- </SelectItem>
1926
- ))}
1927
- </SelectContent>
1928
- </Select>
1929
- </label>
1930
- ) : null}
1931
- </div>
1932
- </fieldset>
1933
- </CardContent>
1934
402
  ) : null}
1935
- </Card>
1936
- ))}
1937
- {hasSearch &&
1938
- effectiveSelectedElements.every((el) => {
1939
- const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
1940
- .toLowerCase();
1941
- return !text.includes(normalizedSearch);
1942
- }) ? (
1943
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
1944
- {t("panel.config.noEditableMatch")}
1945
- </div>
1946
- ) : null}
403
+ </div>
404
+ ) : null}
405
+ </div>
1947
406
  </div>
1948
- ) : null}
1949
- {!isMultiSelectMode && !selectedElement ? (
1950
- <Empty className="py-7">
1951
- <EmptyIcon>
1952
- <svg
1953
- viewBox="0 0 24 24"
1954
- className="h-4 w-4"
1955
- fill="none"
1956
- stroke="currentColor"
1957
- strokeWidth="1.8"
1958
- aria-hidden="true"
1959
- >
1960
- <rect x="4" y="4" width="16" height="16" rx="2.5" />
1961
- <path d="M8 9h8M8 12h8M8 15h5" />
1962
- </svg>
1963
- </EmptyIcon>
1964
- <EmptyTitle>{t("panel.config.emptyNoNodeTitle")}</EmptyTitle>
1965
- <EmptyDescription>{t("panel.config.emptyNoNodeDesc")}</EmptyDescription>
1966
- </Empty>
1967
- ) : !isMultiSelectMode && selectedElement ? (
1968
- <div className="space-y-3">
1969
- {!isNodeEditable ? (
1970
- <div className="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700 dark:text-amber-300">
1971
- {readonlyReason}
1972
- </div>
1973
- ) : null}
1974
- <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
1975
- <div className="space-y-3.5 text-xs">
1976
- {renderSection(
1977
- "nodeInfo",
1978
- t("panel.config.sectionNodeInfo"),
1979
- <>
1980
- <label className="block space-y-1" data-config-field="name">
1981
- <div>{t("panel.config.nodeName")}</div>
1982
- <Input
1983
- value={selectedElement.name ?? ""}
1984
- onChange={(e) =>
407
+ {isMultiSelectMode ? (
408
+ <PanelConfigMultiSelect
409
+ elements={effectiveSelectedElements}
410
+ helpers={helpers}
411
+ updateElement={updateElement}
412
+ layers={layers}
413
+ setReferenceCopyMode={setReferenceCopyMode}
414
+ onExcludeSelectedNode={onExcludeSelectedNode}
415
+ onAdjustNodeZOrder={onAdjustNodeZOrder}
416
+ />
417
+ ) : null}
418
+ {!isMultiSelectMode && !selectedElement ? (
419
+ <Empty className="py-7">
420
+ <EmptyIcon>
421
+ <svg
422
+ viewBox="0 0 24 24"
423
+ className="h-4 w-4"
424
+ fill="none"
425
+ stroke="currentColor"
426
+ strokeWidth="1.8"
427
+ aria-hidden="true"
428
+ >
429
+ <rect x="4" y="4" width="16" height="16" rx="2.5" />
430
+ <path d="M8 9h8M8 12h8M8 15h5" />
431
+ </svg>
432
+ </EmptyIcon>
433
+ <EmptyTitle>{t("panel.config.emptyNoNodeTitle")}</EmptyTitle>
434
+ <EmptyDescription>{t("panel.config.emptyNoNodeDesc")}</EmptyDescription>
435
+ </Empty>
436
+ ) : !isMultiSelectMode && selectedElement ? (
437
+ <div className="space-y-3">
438
+ {!isNodeEditable ? (
439
+ <div className="rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-700 dark:text-amber-300">
440
+ {readonlyReason}
441
+ </div>
442
+ ) : null}
443
+ <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
444
+ <div className="space-y-3.5 text-xs">
445
+ <PanelConfigNodeInfo
446
+ element={selectedElement}
447
+ helpers={helpers}
448
+ updateElement={updateElement}
449
+ nodeZOrderLabel={nodeZOrderLabel}
450
+ onAdjustNodeZOrder={onAdjustNodeZOrder}
451
+ />
452
+ </div>
453
+ </fieldset>
454
+ <div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
455
+ <label className="flex items-center gap-2">
456
+ <Checkbox
457
+ checked={selectedElement.locked === true}
458
+ disabled={!canToggleNodeLock}
459
+ 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"
460
+ onCheckedChange={(checked) =>
1985
461
  updateElement(selectedElement.id, {
1986
- name: e.target.value || undefined,
462
+ locked: checked === true,
1987
463
  })
1988
464
  }
1989
- placeholder={t("panel.config.nodeNamePlaceholder")}
1990
- className="h-7"
1991
465
  />
466
+ <span>{t("panel.config.lockNode")}</span>
1992
467
  </label>
1993
- <div className="grid grid-cols-2 gap-2">
1994
- <label className="block space-y-1" data-config-field="x">
1995
- <div>X</div>
1996
- <Input
1997
- type="number"
1998
- value={selectedElement.x}
1999
- onChange={(e) =>
2000
- updateElement(selectedElement.id, {
2001
- x: Number(e.target.value) || 0,
2002
- })
2003
- }
2004
- className="h-7"
468
+ </div>
469
+ <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
470
+ <div className="space-y-3.5 text-xs">
471
+ {isChartElement ? (
472
+ <PanelConfigChartSection
473
+ element={selectedElement}
474
+ helpers={helpers}
475
+ updateElement={updateElement}
2005
476
  />
2006
- </label>
2007
- <label className="block space-y-1" data-config-field="y">
2008
- <div>Y</div>
2009
- <Input
2010
- type="number"
2011
- value={selectedElement.y}
2012
- onChange={(e) =>
2013
- updateElement(selectedElement.id, {
2014
- y: Number(e.target.value) || 0,
2015
- })
2016
- }
2017
- className="h-7"
477
+ ) : materialType === "table" ? (
478
+ helpers.renderSection(
479
+ "tableConfig",
480
+ t("panel.config.sectionTable"),
481
+ <PanelConfigTableSection
482
+ element={selectedElement}
483
+ disabled={!isNodeEditable}
484
+ updateElement={updateElement}
485
+ blueprintNodeOptions={blueprintNodeOptions}
486
+ />,
487
+ true,
488
+ [
489
+ t("panel.material.table"),
490
+ "table",
491
+ t("panel.config.tableSource"),
492
+ t("panel.config.tableColumns"),
493
+ t("panel.config.tableRowsText"),
494
+ ]
495
+ )
496
+ ) : materialType === "text" ? (
497
+ <PanelConfigTextSection
498
+ element={selectedElement}
499
+ helpers={helpers}
500
+ updateElement={updateElement}
2018
501
  />
2019
- </label>
2020
- </div>
2021
- <div className="grid grid-cols-2 gap-2">
2022
- <label className="block space-y-1" data-config-field="width">
2023
- <div>{t("panel.config.width")}</div>
2024
- <Input
2025
- type="number"
2026
- min={1}
2027
- value={selectedElement?.width ?? 1}
2028
- onChange={(e) => {
2029
- if (!selectedElement) return;
2030
- updateElement(selectedElement.id, {
2031
- width: Math.max(1, Number(e.target.value) || 1),
2032
- });
2033
- }}
2034
- className="h-7"
502
+ ) : materialType === "audio" ? (
503
+ <PanelConfigAudioSection
504
+ element={selectedElement}
505
+ helpers={helpers}
506
+ updateElement={updateElement}
2035
507
  />
2036
- </label>
2037
- <label className="block space-y-1" data-config-field="height">
2038
- <div>{t("panel.config.height")}</div>
2039
- <Input
2040
- type="number"
2041
- min={1}
2042
- value={selectedElement.height}
2043
- onChange={(e) =>
2044
- updateElement(selectedElement.id, {
2045
- height: Math.max(1, Number(e.target.value) || 1),
2046
- })
2047
- }
2048
- className="h-7"
508
+ ) : materialType === "video" ? (
509
+ <PanelConfigVideoSection
510
+ element={selectedElement}
511
+ helpers={helpers}
512
+ updateElement={updateElement}
2049
513
  />
2050
- </label>
2051
- </div>
2052
- {renderFieldGroup(
2053
- t("panel.config.groupNodeMore"),
2054
- <>
2055
- <div className="grid grid-cols-2 gap-2">
2056
- <label className="block space-y-1" data-config-field="rotate">
2057
- <div>{t("panel.config.rotate")}</div>
2058
- <Input
2059
- type="number"
2060
- value={selectedElement.rotate ?? 0}
2061
- onChange={(e) =>
2062
- updateElement(selectedElement.id, {
2063
- rotate: Number(e.target.value) || 0,
2064
- })
2065
- }
2066
- className="h-7"
514
+ ) : null}
515
+ <PanelConfigGridChildSpan
516
+ element={selectedElement}
517
+ helpers={helpers}
518
+ updateElement={updateElement}
519
+ />
520
+ {materialType === "geometry" ? (
521
+ <PanelConfigGeometrySection
522
+ element={selectedElement}
523
+ helpers={helpers}
524
+ updateElement={updateElement}
2067
525
  />
2068
- </label>
2069
- </div>
2070
- <div className="space-y-1.5">
2071
- <div className="text-[11px] text-muted-foreground">{t("panel.config.nodeZOrder")}</div>
2072
- <div className="text-[11px] text-muted-foreground/90">
2073
- {t("panel.config.currentZIndex", { value: nodeZOrderLabel ?? "-" })}
2074
- </div>
2075
- <div className="grid grid-cols-2 gap-2">
2076
- <button
2077
- type="button"
2078
- className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
2079
- onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringForward")}
2080
- >
2081
- {t("panel.menubar.bringForward")}
2082
- </button>
2083
- <button
2084
- type="button"
2085
- className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
2086
- onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendBackward")}
2087
- >
2088
- {t("panel.menubar.sendBackward")}
2089
- </button>
2090
- <button
2091
- type="button"
2092
- className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
2093
- onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "bringToFront")}
2094
- >
2095
- {t("panel.menubar.bringToFront")}
2096
- </button>
2097
- <button
2098
- type="button"
2099
- className="rounded border border-border bg-background px-2 py-1 text-[11px] hover:bg-accent"
2100
- onClick={() => onAdjustNodeZOrder?.(selectedElement.id, "sendToBack")}
2101
- >
2102
- {t("panel.menubar.sendToBack")}
2103
- </button>
2104
- </div>
2105
- </div>
2106
- <div className="truncate text-muted-foreground">ID: {selectedElement.id}</div>
2107
- <div className="text-muted-foreground">{t("panel.config.type")}: {selectedElement.materialType ?? selectedElement.id}</div>
2108
- </>,
2109
- undefined,
2110
- { groupKey: "nodeMore", defaultOpen: false }
2111
- )}
2112
- </>,
2113
- true,
2114
- [t("panel.config.name"), "id", t("panel.config.type"), t("panel.config.rotate"), t("panel.config.nodeZOrder"), t("panel.layers.lockShort"), "locked", "name"]
2115
- )}
2116
- </div>
2117
- </fieldset>
2118
- <div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
2119
- <label className="flex items-center gap-2">
2120
- <Checkbox
2121
- checked={selectedElement.locked === true}
2122
- disabled={!canToggleNodeLock}
2123
- 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"
2124
- onCheckedChange={(checked) =>
2125
- updateElement(selectedElement.id, {
2126
- locked: checked === true,
2127
- })
2128
- }
2129
- />
2130
- <span>{t("panel.config.lockNode")}</span>
2131
- </label>
2132
- </div>
2133
- <fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
2134
- <div className="space-y-3.5 text-xs">
2135
-
2136
- {isChartElement ? (
2137
- <>
2138
- {renderSection(
2139
- "chartBasic",
2140
- t("panel.config.sectionChartBasic"),
2141
- <>
2142
- {renderFieldGroup(
2143
- t("panel.config.groupBasicDisplay"),
2144
- <>
2145
- <label className="block space-y-1.5" data-config-field="chart.title">
2146
- <div>{t("panel.config.title")}</div>
2147
- <Input
2148
- value={selectedElement.chart?.title ?? ""}
2149
- onChange={(e) => updateSelectedChart({ title: e.target.value })}
2150
- className="h-7"
2151
- />
2152
- </label>
2153
-
2154
- {renderColorField(
2155
- t("panel.config.primaryColor"),
2156
- selectedElement.chart?.color ?? "#3b82f6",
2157
- (next) => updateSelectedChart({ color: next || "#3b82f6" })
2158
- )}
2159
- </>
2160
- )}
2161
- {renderFieldGroup(
2162
- t("panel.config.groupData"),
2163
- <>
2164
- <label className="block space-y-1">
2165
- <div>{t("panel.config.labelsCsv")}</div>
2166
- <Input
2167
- value={getChartLabelsDisplayText(selectedElement.chart)}
2168
- onChange={(e) =>
2169
- updateSelectedChart({
2170
- labelsText: e.target.value,
2171
- })
2172
- }
2173
- className="h-7"
2174
- />
2175
- </label>
2176
-
2177
- <label className="block space-y-1">
2178
- <div>{t("panel.config.valuesCsv")}</div>
2179
- <Input
2180
- value={getChartValuesDisplayText(selectedElement.chart)}
2181
- onChange={(e) =>
2182
- updateSelectedChart({
2183
- valuesText: e.target.value,
2184
- })
2185
- }
2186
- className="h-7"
2187
- />
2188
- </label>
2189
- </>
2190
- )}
2191
-
2192
- {selectedChartType === "bar" ||
2193
- selectedChartType === "line" ||
2194
- selectedChartType === "area" ||
2195
- selectedChartType === "pie" ? (
2196
- renderFieldGroup(
2197
- t("panel.config.groupSeries"),
2198
- <>
2199
- {selectedChartType === "bar" ? (
2200
- <label className="block space-y-1">
2201
- <div>{t("panel.config.barWidthPx")}</div>
2202
- <Input
2203
- type="number"
2204
- min={1}
2205
- placeholder={t("panel.config.barWidthAuto")}
2206
- value={selectedElement.chart?.barWidth ?? ""}
2207
- onChange={(e) => {
2208
- const raw = e.target.value.trim();
2209
- updateSelectedChart({
2210
- barWidth: raw
2211
- ? Math.max(1, Number(raw) || 1)
2212
- : undefined,
2213
- });
2214
- }}
2215
- className="h-7"
2216
- />
2217
- </label>
2218
- ) : null}
2219
-
2220
- {selectedChartType === "line" || selectedChartType === "area" ? (
2221
- <label className="flex items-center gap-2">
2222
- <Checkbox
2223
- checked={selectedElement.chart?.smooth ?? true}
2224
- 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"
2225
- onCheckedChange={(checked) =>
2226
- updateSelectedChart({ smooth: checked === true })
2227
- }
2228
- />
2229
- <span>{t("panel.config.smooth")}</span>
2230
- </label>
2231
- ) : null}
2232
-
2233
- {selectedChartType === "pie" ? (
2234
- <div className="grid grid-cols-2 gap-2">
2235
- <label className="block space-y-1">
2236
- <div>{t("panel.config.pieInnerRadiusPct")}</div>
2237
- <Input
2238
- type="number"
2239
- min={0}
2240
- max={99}
2241
- value={selectedElement.chart?.pieInnerRadius ?? 30}
2242
- onChange={(e) =>
2243
- updateSelectedChart({
2244
- pieInnerRadius: Math.max(
2245
- 0,
2246
- Math.min(99, Number(e.target.value) || 0)
2247
- ),
2248
- })
2249
- }
2250
- className="h-7"
2251
- />
2252
- </label>
2253
- <label className="block space-y-1">
2254
- <div>{t("panel.config.pieOuterRadiusPct")}</div>
2255
- <Input
2256
- type="number"
2257
- min={1}
2258
- max={100}
2259
- value={selectedElement.chart?.pieOuterRadius ?? 65}
2260
- onChange={(e) =>
2261
- updateSelectedChart({
2262
- pieOuterRadius: Math.max(
2263
- 1,
2264
- Math.min(100, Number(e.target.value) || 1)
2265
- ),
2266
- })
2267
- }
2268
- className="h-7"
2269
- />
2270
- </label>
2271
- </div>
2272
- ) : null}
2273
- </>
2274
- )
2275
- ) : null}
2276
- {renderFieldGroup(
2277
- t("panel.config.groupChartDisplayMore"),
2278
- <>
2279
- <label className="flex items-center gap-2">
2280
- <Checkbox
2281
- checked={selectedElement.chart?.colorMode === "gradient"}
2282
- onCheckedChange={(checked) =>
2283
- updateSelectedChart({ colorMode: checked ? "gradient" : "solid" })
2284
- }
2285
- />
2286
- <span>{t("panel.config.usePrimaryGradient")}</span>
2287
- </label>
2288
- {selectedElement.chart?.colorMode === "gradient" ? (
2289
- <div className="grid grid-cols-2 gap-2">
2290
- {renderColorField(
2291
- t("panel.config.gradientFrom"),
2292
- selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6",
2293
- (next) => updateSelectedChart({ gradientFrom: next || "#3b82f6" })
2294
- )}
2295
- {renderColorField(
2296
- t("panel.config.gradientTo"),
2297
- selectedElement.chart?.gradientTo ?? "#22d3ee",
2298
- (next) => updateSelectedChart({ gradientTo: next || "#22d3ee" })
2299
- )}
2300
- <label className="block space-y-1 col-span-2">
2301
- <div>{t("panel.config.gradientDirection")}</div>
2302
- <Select
2303
- value={selectedElement.chart?.gradientDirection ?? "to-right"}
2304
- onValueChange={(value) =>
2305
- updateSelectedChart({
2306
- gradientDirection: value as
2307
- | "to-right"
2308
- | "to-bottom"
2309
- | "to-bottom-right"
2310
- | "to-top-right",
2311
- })
2312
- }
2313
- >
2314
- <SelectTrigger className="h-7">
2315
- <SelectValue placeholder={t("panel.config.selectGradientDirection")} />
2316
- </SelectTrigger>
2317
- <SelectContent>
2318
- <SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
2319
- <SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
2320
- <SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
2321
- <SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
2322
- </SelectContent>
2323
- </Select>
2324
- </label>
2325
- <div className="col-span-2 space-y-1">
2326
- <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
2327
- <div
2328
- className="h-6 rounded border border-border/60"
2329
- style={{
2330
- backgroundImage: `linear-gradient(${
2331
- (selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom"
2332
- ? "to bottom"
2333
- : (selectedElement.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
2334
- ? "to bottom right"
2335
- : (selectedElement.chart?.gradientDirection ?? "to-right") === "to-top-right"
2336
- ? "to top right"
2337
- : "to right"
2338
- }, ${selectedElement.chart?.gradientFrom ?? selectedElement.chart?.color ?? "#3b82f6"}, ${selectedElement.chart?.gradientTo ?? "#22d3ee"})`,
2339
- }}
2340
- />
2341
- </div>
2342
- </div>
2343
- ) : null}
2344
- <label className="block space-y-1.5">
2345
- <div>{t("panel.config.renderMode")}</div>
2346
- <Select
2347
- value={selectedElement.chart?.renderMode ?? "canvas"}
2348
- onValueChange={(value) =>
2349
- updateSelectedChart({ renderMode: value as "canvas" | "svg" })
2350
- }
2351
- >
2352
- <SelectTrigger className="h-7">
2353
- <SelectValue placeholder={t("panel.config.selectRenderMode")} />
2354
- </SelectTrigger>
2355
- <SelectContent>
2356
- <SelectItem value="canvas">Canvas</SelectItem>
2357
- <SelectItem value="svg">SVG</SelectItem>
2358
- </SelectContent>
2359
- </Select>
2360
- </label>
2361
- </>,
2362
- undefined,
2363
- { groupKey: "chartDisplayMore", defaultOpen: false }
2364
- )}
2365
- {renderFieldGroup(
2366
- t("panel.config.groupTooltip"),
2367
- <>
2368
- <div className="grid grid-cols-2 gap-2">
2369
- <label className="flex items-center gap-2">
2370
- <Checkbox
2371
- checked={selectedElement.chart?.tooltipShow ?? true}
2372
- 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"
2373
- onCheckedChange={(checked) =>
2374
- updateSelectedChart({ tooltipShow: checked === true })
2375
- }
2376
- />
2377
- <span>{t("panel.config.showTooltip")}</span>
2378
- </label>
2379
- <label className="block space-y-1">
2380
- <div>{t("panel.config.tooltipTrigger")}</div>
2381
- <Select
2382
- value={selectedElement.chart?.tooltipTrigger ?? "axis"}
2383
- onValueChange={(value) =>
2384
- updateSelectedChart({ tooltipTrigger: value as "axis" | "item" })
2385
- }
2386
- >
2387
- <SelectTrigger className="h-7">
2388
- <SelectValue placeholder={t("panel.config.selectTooltipTrigger")} />
2389
- </SelectTrigger>
2390
- <SelectContent>
2391
- <SelectItem value="axis">axis</SelectItem>
2392
- <SelectItem value="item">item</SelectItem>
2393
- </SelectContent>
2394
- </Select>
2395
- </label>
2396
- </div>
2397
- <div className="grid grid-cols-2 gap-2">
2398
- {renderColorField(
2399
- t("panel.config.tooltipBgColor"),
2400
- selectedElement.chart?.tooltipBackgroundColor ?? "#0f172a",
2401
- (next) => updateSelectedChart({ tooltipBackgroundColor: next || "#0f172a" })
2402
- )}
2403
- {renderColorField(
2404
- t("panel.config.tooltipTextColor"),
2405
- selectedElement.chart?.tooltipTextColor ?? "#f8fafc",
2406
- (next) => updateSelectedChart({ tooltipTextColor: next || "#f8fafc" })
2407
- )}
2408
- </div>
2409
- <label className="block space-y-1">
2410
- {renderFormatterLabel(t("panel.config.tooltipFormatter"))}
2411
- <Input
2412
- value={selectedElement.chart?.tooltipFormatter ?? ""}
2413
- onChange={(e) =>
2414
- updateSelectedChart({ tooltipFormatter: e.target.value || undefined })
2415
- }
2416
- placeholder={t("panel.config.tooltipFormatterPlaceholder")}
2417
- className="h-7"
2418
- />
2419
- </label>
2420
- </>,
2421
- undefined,
2422
- { groupKey: "chartTooltip", defaultOpen: false }
2423
- )}
2424
- {selectedChartType === "bar" ||
2425
- selectedChartType === "line" ||
2426
- selectedChartType === "area" ||
2427
- selectedChartType === "scatter" ? (
2428
- renderFieldGroup(
2429
- t("panel.config.groupAxes"),
2430
- <>
2431
- <div className="grid grid-cols-2 gap-2">
2432
- <label className="block">
2433
- <div className="mb-1">{t("panel.config.xAxisName")}</div>
2434
- <Input
2435
- value={selectedElement.chart?.xAxisName ?? ""}
2436
- onChange={(e) => updateSelectedChart({ xAxisName: e.target.value })}
2437
- placeholder={t("panel.config.xAxisNamePlaceholder")}
2438
- className="h-7"
2439
- />
2440
- </label>
2441
- <label className="block">
2442
- <div className="mb-1">{t("panel.config.yAxisName")}</div>
2443
- <Input
2444
- value={selectedElement.chart?.yAxisName ?? ""}
2445
- onChange={(e) => updateSelectedChart({ yAxisName: e.target.value })}
2446
- placeholder={t("panel.config.yAxisNamePlaceholder")}
2447
- className="h-7"
2448
- />
2449
- </label>
2450
- </div>
2451
- <div className="grid grid-cols-2 gap-2">
2452
- <label className="flex items-center gap-2">
2453
- <Checkbox
2454
- checked={selectedElement.chart?.xAxisTickShow ?? true}
2455
- 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"
2456
- onCheckedChange={(checked) =>
2457
- updateSelectedChart({ xAxisTickShow: checked === true })
2458
- }
2459
- />
2460
- <span>{t("panel.config.xAxisTick")}</span>
2461
- </label>
2462
- <label className="flex items-center gap-2">
2463
- <Checkbox
2464
- checked={selectedElement.chart?.yAxisTickShow ?? true}
2465
- 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"
2466
- onCheckedChange={(checked) =>
2467
- updateSelectedChart({ yAxisTickShow: checked === true })
2468
- }
2469
- />
2470
- <span>{t("panel.config.yAxisTick")}</span>
2471
- </label>
2472
- </div>
2473
- <div className="grid grid-cols-2 gap-2">
2474
- {renderColorField(
2475
- t("panel.config.xAxisTickColor"),
2476
- selectedElement.chart?.xAxisTickColor ?? "#94a3b8",
2477
- (next) => updateSelectedChart({ xAxisTickColor: next || "#94a3b8" })
2478
- )}
2479
- {renderColorField(
2480
- t("panel.config.yAxisTickColor"),
2481
- selectedElement.chart?.yAxisTickColor ?? "#94a3b8",
2482
- (next) => updateSelectedChart({ yAxisTickColor: next || "#94a3b8" })
2483
- )}
2484
- </div>
2485
- <div className="grid grid-cols-2 gap-2">
2486
- <label className="flex items-center gap-2">
2487
- <Checkbox
2488
- checked={selectedElement.chart?.xAxisSplitLineShow ?? false}
2489
- 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"
2490
- onCheckedChange={(checked) =>
2491
- updateSelectedChart({ xAxisSplitLineShow: checked === true })
2492
- }
2493
- />
2494
- <span>{t("panel.config.xAxisSplitLine")}</span>
2495
- </label>
2496
- <label className="flex items-center gap-2">
2497
- <Checkbox
2498
- checked={selectedElement.chart?.yAxisSplitLineShow ?? true}
2499
- 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"
2500
- onCheckedChange={(checked) =>
2501
- updateSelectedChart({ yAxisSplitLineShow: checked === true })
2502
- }
2503
- />
2504
- <span>{t("panel.config.yAxisSplitLine")}</span>
2505
- </label>
2506
- </div>
2507
- <div className="grid grid-cols-2 gap-2">
2508
- {renderColorField(
2509
- t("panel.config.xAxisSplitLineColor"),
2510
- selectedElement.chart?.xAxisSplitLineColor ?? "#e2e8f0",
2511
- (next) => updateSelectedChart({ xAxisSplitLineColor: next || "#e2e8f0" })
2512
- )}
2513
- {renderColorField(
2514
- t("panel.config.yAxisSplitLineColor"),
2515
- selectedElement.chart?.yAxisSplitLineColor ?? "#e2e8f0",
2516
- (next) => updateSelectedChart({ yAxisSplitLineColor: next || "#e2e8f0" })
2517
- )}
2518
- </div>
2519
- <div className="grid grid-cols-2 gap-2">
2520
- {renderColorField(
2521
- t("panel.config.xAxisLabelColor"),
2522
- selectedElement.chart?.xAxisLabelColor ?? "#64748b",
2523
- (next) => updateSelectedChart({ xAxisLabelColor: next || "#64748b" })
2524
- )}
2525
- {renderColorField(
2526
- t("panel.config.yAxisLabelColor"),
2527
- selectedElement.chart?.yAxisLabelColor ?? "#64748b",
2528
- (next) => updateSelectedChart({ yAxisLabelColor: next || "#64748b" })
2529
- )}
2530
- </div>
2531
- <div className="grid grid-cols-2 gap-2">
2532
- <label className="block">
2533
- <div className="mb-1">{t("panel.config.xAxisLabelFontSize")}</div>
2534
- <Input
2535
- type="number"
2536
- min={8}
2537
- max={48}
2538
- value={selectedElement.chart?.xAxisLabelFontSize ?? 10}
2539
- onChange={(e) =>
2540
- updateSelectedChart({
2541
- xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
2542
- })
2543
- }
2544
- className="h-7"
2545
- />
2546
- </label>
2547
- <label className="block">
2548
- <div className="mb-1">{t("panel.config.yAxisLabelFontSize")}</div>
2549
- <Input
2550
- type="number"
2551
- min={8}
2552
- max={48}
2553
- value={selectedElement.chart?.yAxisLabelFontSize ?? 10}
2554
- onChange={(e) =>
2555
- updateSelectedChart({
2556
- yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
2557
- })
2558
- }
2559
- className="h-7"
2560
- />
2561
- </label>
2562
- </div>
2563
- <div className="grid grid-cols-2 gap-2">
2564
- <label className="flex items-center gap-2">
2565
- <Checkbox
2566
- checked={selectedElement.chart?.xAxisLabelAutoEllipsis ?? false}
2567
- 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"
2568
- onCheckedChange={(checked) =>
2569
- updateSelectedChart({ xAxisLabelAutoEllipsis: checked === true })
2570
- }
2571
- />
2572
- <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
2573
- </label>
2574
- <label className="flex items-center gap-2">
2575
- <Checkbox
2576
- checked={selectedElement.chart?.yAxisLabelAutoEllipsis ?? false}
2577
- 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"
2578
- onCheckedChange={(checked) =>
2579
- updateSelectedChart({ yAxisLabelAutoEllipsis: checked === true })
2580
- }
2581
- />
2582
- <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
2583
- </label>
2584
- </div>
2585
- </>,
2586
- undefined,
2587
- { groupKey: "chartAxes", defaultOpen: false }
2588
- )
2589
- ) : null}
2590
-
2591
- </>,
2592
- true,
2593
- [
2594
- t("panel.config.searchKwChart"),
2595
- "title",
2596
- "tooltip",
2597
- t("panel.config.searchKwXAxis"),
2598
- t("panel.config.searchKwYAxis"),
2599
- "axis",
2600
- "label",
2601
- t("panel.config.searchKwTick"),
2602
- t("panel.config.searchKwSplit"),
2603
- "render",
2604
- "svg",
2605
- "canvas",
2606
- ]
2607
- )}
2608
-
2609
- {renderSection(
2610
- "chartAdvanced",
2611
- t("panel.config.sectionChartAdvanced"),
2612
- <>
2613
- {renderFieldGroup(
2614
- t("panel.config.groupCommonForm"),
2615
- <div className="grid grid-cols-1 gap-2">
2616
- <div className="rounded-lg border border-border/60 bg-background/70 p-3">
2617
- <Collapsible
2618
- open={isSectionExpanded("chartAdvancedLayout", false)}
2619
- onOpenChange={(open) => setSectionExpanded("chartAdvancedLayout", open)}
2620
- >
2621
- <div className="mb-2 flex items-center gap-1.5">
2622
- <CollapsibleTrigger asChild>
2623
- <button
2624
- type="button"
2625
- className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2626
- aria-label={isSectionExpanded("chartAdvancedLayout", false) ? t("panel.config.collapseLayoutCoord") : t("panel.config.expandLayoutCoord")}
2627
- >
2628
- {isSectionExpanded("chartAdvancedLayout", false) ? "▾" : "▸"}
2629
- </button>
2630
- </CollapsibleTrigger>
2631
- <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupLayoutAndCoord")}</div>
2632
- </div>
2633
- <CollapsibleContent>
2634
- <div className="grid grid-cols-2 gap-2">
2635
- <label className="block space-y-1.5">
2636
- <div className={optionLabelTextClass}>{t("panel.config.gridLeftForm")}</div>
2637
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.left ?? 28)} onChange={(e) => updateSelectedOptionForm({ grid: { left: Number(e.target.value) || 0 } })} />
2638
- </label>
2639
- <label className="block space-y-1.5">
2640
- <div className={optionLabelTextClass}>{t("panel.config.gridRightForm")}</div>
2641
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.right ?? 10)} onChange={(e) => updateSelectedOptionForm({ grid: { right: Number(e.target.value) || 0 } })} />
2642
- </label>
2643
- <label className="block space-y-1.5">
2644
- <div className={optionLabelTextClass}>{t("panel.config.gridTopForm")}</div>
2645
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.top ?? 30)} onChange={(e) => updateSelectedOptionForm({ grid: { top: Number(e.target.value) || 0 } })} />
2646
- </label>
2647
- <label className="block space-y-1.5">
2648
- <div className={optionLabelTextClass}>{t("panel.config.gridBottomForm")}</div>
2649
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.grid?.bottom ?? 20)} onChange={(e) => updateSelectedOptionForm({ grid: { bottom: Number(e.target.value) || 0 } })} />
2650
- </label>
2651
- <label className="block space-y-1.5">
2652
- <div className={optionLabelTextClass}>{t("panel.config.legendPositionForm")}</div>
2653
- <Select value={String((selectedElement.chart?.option as any)?.legend?.top ?? "top")} onValueChange={(value) => updateSelectedOptionForm({ legend: { top: value } })}>
2654
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendPosition")} /></SelectTrigger>
2655
- <SelectContent>
2656
- <SelectItem value="top">{t("panel.config.legendTop")}</SelectItem>
2657
- <SelectItem value="bottom">{t("panel.config.legendBottom")}</SelectItem>
2658
- <SelectItem value="left">{t("panel.config.legendLeft")}</SelectItem>
2659
- <SelectItem value="right">{t("panel.config.legendRight")}</SelectItem>
2660
- </SelectContent>
2661
- </Select>
2662
- </label>
2663
- <label className="block space-y-1.5">
2664
- <div className={optionLabelTextClass}>{t("panel.config.legendOrientForm")}</div>
2665
- <Select value={String((selectedElement.chart?.option as any)?.legend?.orient ?? "horizontal")} onValueChange={(value) => updateSelectedOptionForm({ legend: { orient: value } })}>
2666
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectLegendOrient")} /></SelectTrigger>
2667
- <SelectContent>
2668
- <SelectItem value="horizontal">{t("panel.config.legendHorizontal")}</SelectItem>
2669
- <SelectItem value="vertical">{t("panel.config.legendVertical")}</SelectItem>
2670
- </SelectContent>
2671
- </Select>
2672
- </label>
2673
- <label className="block space-y-1.5">
2674
- {renderOptionLabel(t("panel.config.xAxisMin"), "xAxis.min", t("panel.config.xAxisMinHint"))}
2675
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { min: Number(e.target.value) || 0 } })} />
2676
- </label>
2677
- <label className="block space-y-1.5">
2678
- {renderOptionLabel(t("panel.config.xAxisMax"), "xAxis.max", t("panel.config.xAxisMaxHint"))}
2679
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ xAxis: { max: Number(e.target.value) || 0 } })} />
2680
- </label>
2681
- <label className="block space-y-1.5">
2682
- {renderOptionLabel(t("panel.config.yAxisMin"), "yAxis.min", t("panel.config.yAxisMinHint"))}
2683
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.min ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { min: Number(e.target.value) || 0 } })} />
2684
- </label>
2685
- <label className="block space-y-1.5">
2686
- {renderOptionLabel(t("panel.config.yAxisMax"), "yAxis.max", t("panel.config.yAxisMaxHint"))}
2687
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.max ?? 100)} onChange={(e) => updateSelectedOptionForm({ yAxis: { max: Number(e.target.value) || 0 } })} />
2688
- </label>
2689
- <label className="block space-y-1.5">
2690
- {renderOptionLabel(t("panel.config.xAxisLabelRotate"), "xAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
2691
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.xAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ xAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
2692
- </label>
2693
- <label className="block space-y-1.5">
2694
- {renderOptionLabel(t("panel.config.yAxisLabelRotate"), "yAxis.axisLabel.rotate", t("panel.config.rotateUnitHint"))}
2695
- <Input type="number" className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.yAxis?.axisLabel?.rotate ?? 0)} onChange={(e) => updateSelectedOptionForm({ yAxis: { axisLabel: { rotate: Number(e.target.value) || 0 } } })} />
2696
- </label>
2697
- </div>
2698
- </CollapsibleContent>
2699
- </Collapsible>
2700
- </div>
2701
- <div className="rounded-lg border border-border/60 bg-background/70 p-3">
2702
- <div className="space-y-2">
2703
- <Collapsible
2704
- open={isSectionExpanded("chartAdvancedHighFreq", true)}
2705
- onOpenChange={(open) => setSectionExpanded("chartAdvancedHighFreq", open)}
2706
- >
2707
- <div className="mb-2 flex items-center gap-1.5">
2708
- <CollapsibleTrigger asChild>
2709
- <button
2710
- type="button"
2711
- className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2712
- aria-label={isSectionExpanded("chartAdvancedHighFreq", true) ? t("panel.config.collapseHighFreq") : t("panel.config.expandHighFreq")}
2713
- >
2714
- {isSectionExpanded("chartAdvancedHighFreq", true) ? "▾" : "▸"}
2715
- </button>
2716
- </CollapsibleTrigger>
2717
- <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupHighFreq")}</div>
2718
- </div>
2719
- <CollapsibleContent>
2720
- <div className="grid grid-cols-1 gap-2">
2721
- <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2722
- <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.legend?.show ?? true)} onCheckedChange={(checked) => updateSelectedOptionForm({ legend: { show: checked === true } })} />
2723
- {renderOptionLabel(t("panel.config.showLegend"), "legend.show", t("panel.config.showLegendHint"))}
2724
- </label>
2725
- <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2726
- <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.grid?.containLabel ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ grid: { containLabel: checked === true } })} />
2727
- {renderOptionLabel(t("panel.config.gridContainLabel"), "grid.containLabel", t("panel.config.gridContainLabelHint"))}
2728
- </label>
2729
- <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2730
- <Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }] : prev.filter((z: any) => z?.type !== "inside"); updateSelectedOptionForm({ dataZoom: next }); }} />
2731
- {renderOptionLabel(t("panel.config.zoomInside"), "dataZoom[type=inside]", t("panel.config.zoomInsideHint"))}
2732
- </label>
2733
- <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2734
- <Checkbox className={optionCheckboxClass} checked={Boolean(Array.isArray((selectedElement.chart?.option as any)?.dataZoom) && (selectedElement.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider"))} onCheckedChange={(checked) => { const prev = Array.isArray((selectedElement.chart?.option as any)?.dataZoom) ? [...(selectedElement.chart?.option as any).dataZoom] : []; const next = checked ? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }] : prev.filter((z: any) => z?.type !== "slider"); updateSelectedOptionForm({ dataZoom: next }); }} />
2735
- {renderOptionLabel(t("panel.config.zoomSlider"), "dataZoom[type=slider]", t("panel.config.zoomSliderHint"))}
2736
- </label>
2737
- <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2738
- <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.animation ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ animation: checked === true })} />
2739
- {renderOptionLabel(t("panel.config.enableAnimation"), "animation", t("panel.config.enableAnimationHint"))}
2740
- </label>
2741
- <label className="block space-y-1.5">
2742
- {renderOptionLabel(t("panel.config.animationDuration"), "animationDuration", t("panel.config.animationDurationHint"))}
2743
- <Input type="number" min={0} className={optionInputClass} value={Number((selectedElement.chart?.option as any)?.animationDuration ?? 300)} onChange={(e) => updateSelectedOptionForm({ animationDuration: Math.max(0, Number(e.target.value) || 0) })} />
2744
- </label>
2745
- </div>
2746
- </CollapsibleContent>
2747
- </Collapsible>
2748
- <Collapsible
2749
- open={isSectionExpanded("chartAdvancedAxisPointer", false)}
2750
- onOpenChange={(open) => setSectionExpanded("chartAdvancedAxisPointer", open)}
2751
- >
2752
- <div className="mb-2 flex items-center gap-1.5">
2753
- <CollapsibleTrigger asChild>
2754
- <button
2755
- type="button"
2756
- className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
2757
- aria-label={isSectionExpanded("chartAdvancedAxisPointer", false) ? t("panel.config.collapseAxisPointer") : t("panel.config.expandAxisPointer")}
2758
- >
2759
- {isSectionExpanded("chartAdvancedAxisPointer", false) ? "▾" : "▸"}
2760
- </button>
2761
- </CollapsibleTrigger>
2762
- <div className="text-[11px] font-medium text-muted-foreground">{t("panel.config.groupAxisPointer")}</div>
2763
- </div>
2764
- <CollapsibleContent>
2765
- <div className="grid grid-cols-1 gap-2">
2766
- <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2767
- <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.show ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { show: checked === true } })} />
2768
- {renderOptionLabel(t("panel.config.showAxisPointer"), "axisPointer.show", t("panel.config.showAxisPointerHint"))}
2769
- </label>
2770
- <label className="block space-y-1.5 rounded-md bg-muted/30 px-2 py-1.5">
2771
- {renderOptionLabel(t("panel.config.axisPointerType"), "axisPointer.type", t("panel.config.axisPointerTypeHint"))}
2772
- <Select value={String((selectedElement.chart?.option as any)?.axisPointer?.type ?? "line")} onValueChange={(value) => updateSelectedOptionForm({ axisPointer: { type: value } })}>
2773
- <SelectTrigger className={optionSelectTriggerClass}><SelectValue placeholder={t("panel.config.selectAxisPointerType")} /></SelectTrigger>
2774
- <SelectContent>
2775
- <SelectItem value="line">{t("panel.config.axisPointerLine")}</SelectItem>
2776
- <SelectItem value="shadow">{t("panel.config.axisPointerShadow")}</SelectItem>
2777
- <SelectItem value="cross">{t("panel.config.axisPointerCross")}</SelectItem>
2778
- </SelectContent>
2779
- </Select>
2780
- </label>
2781
- <label className="flex items-center gap-2 rounded-md bg-muted/30 px-2 py-1.5">
2782
- <Checkbox className={optionCheckboxClass} checked={Boolean((selectedElement.chart?.option as any)?.axisPointer?.snap ?? false)} onCheckedChange={(checked) => updateSelectedOptionForm({ axisPointer: { snap: checked === true } })} />
2783
- {renderOptionLabel(t("panel.config.axisPointerSnap"), "axisPointer.snap", t("panel.config.axisPointerSnapHint"))}
2784
- </label>
2785
- </div>
2786
- </CollapsibleContent>
2787
- </Collapsible>
2788
- </div>
2789
- </div>
2790
- </div>
2791
- )}
2792
- {renderFieldGroup(
2793
- t("panel.config.groupJsonAdvanced"),
2794
- <>
2795
- <label className="flex items-center gap-2 rounded-md bg-background/70 px-2 py-1.5">
2796
- <Checkbox
2797
- checked={isAdvancedOptionMode}
2798
- 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"
2799
- onCheckedChange={(checked) => setIsAdvancedOptionMode(checked === true)}
2800
- />
2801
- <span>{t("panel.config.enableAdvancedJson")}</span>
2802
- <ConfigHintIcon label={t("panel.config.advancedJsonHintLabel")}>
2803
- {t("panel.config.advancedJsonHint")}
2804
- </ConfigHintIcon>
2805
- </label>
2806
- {isAdvancedOptionMode ? (
2807
- <>
2808
- <Textarea
2809
- value={optionJsonText}
2810
- onChange={(e) => {
2811
- const next = e.target.value;
2812
- setOptionJsonText(next);
2813
- try {
2814
- const parsed = JSON.parse(next) as Record<string, unknown>;
2815
- updateSelectedChart({ option: parsed });
2816
- setOptionJsonError(null);
2817
- } catch {
2818
- setOptionJsonError(t("panel.config.jsonInvalid"));
2819
- }
2820
- }}
2821
- spellCheck={false}
2822
- className="h-44 font-mono text-[11px]"
2823
- />
2824
- {optionJsonError ? (
2825
- <div className="rounded border border-destructive/40 bg-destructive/10 px-2 py-1.5 text-[11px] text-destructive">
2826
- {optionJsonError}
2827
- </div>
2828
- ) : (
2829
- <div className="rounded border border-emerald-500/30 bg-emerald-500/10 px-2 py-1.5 text-[11px] text-emerald-700 dark:text-emerald-300">
2830
- {t("panel.config.jsonValidApplied")}
2831
- </div>
2832
- )}
2833
- </>
2834
- ) : null}
2835
- </>
2836
- )}
2837
- </>,
2838
- false,
2839
- ["json", "option", t("panel.config.searchKwAdvanced"), "echarts"]
2840
- )}
2841
- </>
2842
- ) : selectedElement.materialType === "table" ? (
2843
- renderSection(
2844
- "tableConfig",
2845
- t("panel.config.sectionTable"),
2846
- <PanelConfigTableSection
2847
- element={selectedElement}
2848
- disabled={!isNodeEditable}
2849
- updateElement={updateElement}
2850
- blueprintNodeOptions={blueprintNodeOptions}
2851
- />,
2852
- true,
2853
- [
2854
- t("panel.material.table"),
2855
- "table",
2856
- t("panel.config.tableSource"),
2857
- t("panel.config.tableColumns"),
2858
- t("panel.config.tableRowsText"),
2859
- ]
2860
- )
2861
- ) : selectedElement.materialType === "text" ? (
2862
- renderSection(
2863
- "textConfig",
2864
- t("panel.config.sectionText"),
2865
- <>
2866
- {renderFieldGroup(
2867
- t("panel.config.groupTextContent"),
2868
- <>
2869
- <div className="flex items-center gap-1">
2870
- <button
2871
- type="button"
2872
- className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
2873
- onClick={() => execTextCommand("bold")}
2874
- >
2875
- B
2876
- </button>
2877
- <button
2878
- type="button"
2879
- className="rounded border border-border px-2 py-1 text-[11px] italic hover:bg-accent"
2880
- onClick={() => execTextCommand("italic")}
2881
- >
2882
- I
2883
- </button>
2884
- <button
2885
- type="button"
2886
- className="rounded border border-border px-2 py-1 text-[11px] underline hover:bg-accent"
2887
- onClick={() => execTextCommand("underline")}
2888
- >
2889
- U
2890
- </button>
2891
- </div>
2892
- <div
2893
- ref={textEditorRef}
2894
- data-config-field="textHtml"
2895
- data-panel-user-text=""
2896
- className="min-h-[120px] rounded border border-border bg-background px-2 py-1.5 leading-6 outline-none"
2897
- style={{
2898
- fontFamily: selectedElement.textFontFamily || undefined,
2899
- fontSize: selectedElement.textFontSize
2900
- ? `${selectedElement.textFontSize}px`
2901
- : undefined,
2902
- fontWeight: selectedElement.textFontWeight || undefined,
2903
- color: selectedElement.textColor || undefined,
2904
- lineHeight: cssTextLineHeight(selectedElement.textLineHeight),
2905
- ...cssTextAlignStyle(selectedElement.textAlign),
2906
- }}
2907
- contentEditable
2908
- suppressContentEditableWarning
2909
- onInput={(e) => {
2910
- const nextHtml = (e.currentTarget as HTMLDivElement).innerHTML;
2911
- updateSelectedText({
2912
- textHtml: nextHtml || "<p><br/></p>",
2913
- });
2914
- }}
2915
- />
2916
- </>
2917
- )}
2918
- {renderFieldGroup(
2919
- t("panel.config.groupTextStyle"),
2920
- <>
2921
- <label className="block space-y-1">
2922
- <div>{t("panel.config.fontFamily")}</div>
2923
- <Input
2924
- value={selectedElement.textFontFamily ?? ""}
2925
- onChange={(e) =>
2926
- updateSelectedText({
2927
- textFontFamily: e.target.value || undefined,
2928
- })
2929
- }
2930
- placeholder={t("panel.config.fontFamilyPlaceholder")}
2931
- className="h-7"
2932
- />
2933
- </label>
2934
- <div className="grid grid-cols-2 gap-2">
2935
- <label className="block space-y-1">
2936
- <div>{t("panel.config.fontSizePx")}</div>
2937
- <Input
2938
- type="number"
2939
- min={8}
2940
- max={200}
2941
- step={1}
2942
- value={selectedElement.textFontSize ?? 14}
2943
- onChange={(e) =>
2944
- updateSelectedText({
2945
- textFontSize: Math.max(8, Number(e.target.value) || 14),
2946
- })
2947
- }
2948
- className="h-7"
2949
- />
2950
- </label>
2951
- <label className="block space-y-1">
2952
- <div>{t("panel.config.fontWeight")}</div>
2953
- <Select
2954
- value={selectedElement.textFontWeight ?? "400"}
2955
- onValueChange={(value) =>
2956
- updateSelectedText({
2957
- textFontWeight: value,
2958
- })
2959
- }
2960
- >
2961
- <SelectTrigger className="h-7">
2962
- <SelectValue placeholder={t("panel.config.selectFontWeight")} />
2963
- </SelectTrigger>
2964
- <SelectContent>
2965
- <SelectItem value="300">300</SelectItem>
2966
- <SelectItem value="400">400</SelectItem>
2967
- <SelectItem value="500">500</SelectItem>
2968
- <SelectItem value="600">600</SelectItem>
2969
- <SelectItem value="700">700</SelectItem>
2970
- </SelectContent>
2971
- </Select>
2972
- </label>
2973
- </div>
2974
- <div className="grid grid-cols-2 gap-2">
2975
- <label className="block space-y-1">
2976
- <div>{t("panel.config.textAlign")}</div>
2977
- <Select
2978
- value={selectedElement.textAlign ?? "left"}
2979
- onValueChange={(value) =>
2980
- updateSelectedText({
2981
- textAlign: value as PanelElement["textAlign"],
2982
- })
2983
- }
2984
- >
2985
- <SelectTrigger className="h-7">
2986
- <SelectValue placeholder={t("panel.config.selectTextAlign")} />
2987
- </SelectTrigger>
2988
- <SelectContent>
2989
- <SelectItem value="left">{t("panel.config.alignLeft")}</SelectItem>
2990
- <SelectItem value="center">{t("panel.config.alignCenter")}</SelectItem>
2991
- <SelectItem value="middle">{t("panel.config.alignMiddle")}</SelectItem>
2992
- <SelectItem value="right">{t("panel.config.alignRight")}</SelectItem>
2993
- <SelectItem value="justify">{t("panel.config.alignJustify")}</SelectItem>
2994
- </SelectContent>
2995
- </Select>
2996
- </label>
2997
- <label className="block space-y-1">
2998
- <div className="flex items-center justify-between gap-2">
2999
- <span>{t("panel.config.lineHeight")}</span>
3000
- <label className="flex items-center gap-1 font-normal">
3001
- <Checkbox
3002
- checked={selectedElement.textLineHeight === "auto"}
3003
- className="h-3.5 w-3.5 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
3004
- onCheckedChange={(checked) =>
3005
- updateSelectedText({
3006
- textLineHeight: checked === true ? "auto" : 1.6,
3007
- })
3008
- }
3009
- />
3010
- <span>{t("panel.config.lineHeightAuto")}</span>
3011
- </label>
3012
- </div>
3013
- <Input
3014
- type="number"
3015
- min={1}
3016
- max={3}
3017
- step={0.1}
3018
- disabled={selectedElement.textLineHeight === "auto"}
3019
- placeholder={t("panel.config.lineHeightAuto")}
3020
- value={
3021
- selectedElement.textLineHeight === "auto"
3022
- ? ""
3023
- : (selectedElement.textLineHeight ?? 1.6)
3024
- }
3025
- onChange={(e) =>
3026
- updateSelectedText({
3027
- textLineHeight: Math.min(
3028
- 3,
3029
- Math.max(1, Number(e.target.value) || 1.6)
3030
- ),
3031
- })
3032
- }
3033
- className="h-7"
3034
- />
3035
- </label>
3036
- </div>
3037
- {renderColorField(
3038
- t("panel.config.textColor"),
3039
- selectedElement.textColor ?? "",
3040
- (next) =>
3041
- updateSelectedText({
3042
- textColor: next || undefined,
3043
- })
3044
- )}
3045
- </>
3046
- )}
3047
- {renderFieldGroup(
3048
- t("panel.config.groupInputAbility"),
3049
- <label className="flex items-center gap-2">
3050
- <Checkbox
3051
- checked={selectedElement.textAllowInput ?? true}
3052
- 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"
3053
- onCheckedChange={(checked) =>
3054
- updateSelectedText({
3055
- textAllowInput: checked !== false,
3056
- })
3057
- }
3058
- />
3059
- <span>{t("panel.config.allowCanvasInput")}</span>
3060
- </label>,
3061
- undefined,
3062
- { groupKey: "textInputAbility", defaultOpen: false }
3063
- )}
3064
- </>,
3065
- true,
3066
- [t("panel.material.text"), t("panel.config.searchKwRichText"), t("panel.config.fontFamily"), t("panel.config.color"), t("panel.config.textAlign"), t("panel.config.lineHeight"), t("panel.config.searchKwInput")]
3067
- )
3068
- ) : selectedElement.materialType === "audio" ? (
3069
- renderSection(
3070
- "audioConfig",
3071
- t("panel.config.sectionAudio"),
3072
- <>
3073
- {renderFieldGroup(
3074
- t("panel.config.groupAudioSource"),
3075
- <>
3076
- <label className="block space-y-1">
3077
- <div>{t("panel.config.audioUrl")}</div>
3078
- <Input
3079
- value={selectedElement.audioRemoteUrl ?? ""}
3080
- onChange={(e) =>
3081
- updateSelectedAudio({
3082
- audioRemoteUrl: e.target.value || undefined,
3083
- audioSrc: e.target.value || selectedElement.audioSrc,
3084
- })
3085
- }
3086
- placeholder="https://example.com/audio.mp3"
3087
- className="h-7"
3088
- />
3089
- </label>
3090
- <div className="flex items-center gap-2">
3091
- <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3092
- {t("panel.config.uploadAudio")}
3093
- <Input
3094
- type="file"
3095
- accept="audio/*"
3096
- className="hidden"
3097
- onChange={async (e) => {
3098
- const file = e.target.files?.[0];
3099
- e.currentTarget.value = "";
3100
- if (!file) return;
3101
- await handleUploadAudioFile(file);
3102
- }}
3103
- />
3104
- </label>
3105
- <button
3106
- type="button"
3107
- className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent disabled:opacity-50"
3108
- onClick={isRecordingAudio ? stopRecordingAudio : startRecordingAudio}
3109
- >
3110
- {isRecordingAudio ? t("panel.config.stopRecord") : t("panel.config.startRecord")}
3111
- </button>
3112
- </div>
3113
- {audioStatus ? (
3114
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3115
- {audioStatus}
3116
- </div>
3117
- ) : null}
3118
- <audio
3119
- controls
3120
- className="h-8 w-full"
3121
- src={selectedElement.audioSrc || selectedElement.audioRemoteUrl || ""}
3122
- />
3123
- </>
3124
- )}
3125
- {renderFieldGroup(
3126
- t("panel.config.groupDisplayStyle"),
3127
- <>
3128
- <label className="flex items-center gap-2">
3129
- <Checkbox
3130
- checked={selectedElement.mediaAutoPauseOnEdit !== false}
3131
- 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"
3132
- onCheckedChange={(checked) =>
3133
- updateSelectedAudio({
3134
- mediaAutoPauseOnEdit: checked !== false,
3135
- })
3136
- }
3137
- />
3138
- <span>{t("panel.config.autoPauseMedia")}</span>
3139
- </label>
3140
- <label className="block space-y-1">
3141
- <div>{t("panel.config.audioIconPreset")}</div>
3142
- <Select
3143
- value={selectedElement.audioIconPreset ?? "__none__"}
3144
- onValueChange={(value) =>
3145
- updateSelectedAudio({
3146
- audioIconPreset:
3147
- value === "__none__"
3148
- ? undefined
3149
- : (value as "speaker" | "music" | "headphone" | "wave"),
3150
- })
3151
- }
3152
- >
3153
- <SelectTrigger className="h-7">
3154
- <SelectValue placeholder={t("panel.config.selectIcon")} />
3155
- </SelectTrigger>
3156
- <SelectContent>
3157
- <SelectItem value="__none__">{t("panel.config.iconDefaultProgress")}</SelectItem>
3158
- <SelectItem value="speaker">{t("panel.config.iconSpeaker")}</SelectItem>
3159
- <SelectItem value="music">{t("panel.config.iconMusic")}</SelectItem>
3160
- <SelectItem value="headphone">{t("panel.config.iconHeadphone")}</SelectItem>
3161
- <SelectItem value="wave">{t("panel.config.iconWave")}</SelectItem>
3162
- </SelectContent>
3163
- </Select>
3164
- </label>
3165
- <label className="block space-y-1">
3166
- <div>{t("panel.config.visualEffect")}</div>
3167
- <Select
3168
- value={selectedElement.audioVisualEffect ?? "pulse"}
3169
- onValueChange={(value) =>
3170
- updateSelectedAudio({
3171
- audioVisualEffect: value as "none" | "pulse" | "ripple",
3172
- })
3173
- }
3174
- >
3175
- <SelectTrigger className="h-7">
3176
- <SelectValue placeholder={t("panel.config.selectEffect")} />
3177
- </SelectTrigger>
3178
- <SelectContent>
3179
- <SelectItem value="none">{t("panel.config.effectNone")}</SelectItem>
3180
- <SelectItem value="pulse">{t("panel.config.effectPulse")}</SelectItem>
3181
- <SelectItem value="ripple">{t("panel.config.effectRipple")}</SelectItem>
3182
- </SelectContent>
3183
- </Select>
3184
- </label>
3185
- <label className="block space-y-1">
3186
- <div>{t("panel.config.effectSpeed")}</div>
3187
- <Select
3188
- value={selectedElement.audioVisualSpeed ?? "normal"}
3189
- onValueChange={(value) =>
3190
- updateSelectedAudio({
3191
- audioVisualSpeed: value as "slow" | "normal" | "fast",
3192
- })
3193
- }
3194
- >
3195
- <SelectTrigger className="h-7">
3196
- <SelectValue placeholder={t("panel.config.selectSpeed")} />
3197
- </SelectTrigger>
3198
- <SelectContent>
3199
- <SelectItem value="slow">{t("panel.config.speedSlow")}</SelectItem>
3200
- <SelectItem value="normal">{t("panel.config.speedNormal")}</SelectItem>
3201
- <SelectItem value="fast">{t("panel.config.speedFast")}</SelectItem>
3202
- </SelectContent>
3203
- </Select>
3204
- </label>
3205
- <div className="flex items-center gap-2">
3206
- <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3207
- {t("panel.config.uploadPoster")}
3208
- <Input
3209
- type="file"
3210
- accept="image/*"
3211
- className="hidden"
3212
- onChange={async (e) => {
3213
- const file = e.target.files?.[0];
3214
- e.currentTarget.value = "";
3215
- if (!file) return;
3216
- await handleUploadAudioPoster(file);
3217
- }}
3218
- />
3219
- </label>
3220
- <button
3221
- type="button"
3222
- className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
3223
- onClick={() =>
3224
- updateSelectedAudio({
3225
- audioPosterImage: undefined,
3226
- })
3227
- }
3228
- >
3229
- {t("panel.config.clearPoster")}
3230
- </button>
3231
- </div>
3232
- {selectedElement.audioPosterImage ? (
3233
- <img
3234
- src={selectedElement.audioPosterImage}
3235
- alt={t("panel.config.audioPosterAlt")}
3236
- className="h-20 w-full rounded border border-border/60 object-cover"
3237
- />
3238
- ) : null}
3239
- <div className="flex items-center gap-1">
3240
- <div className="text-[11px] text-muted-foreground">{t("panel.config.audioPoster")}</div>
3241
- <ConfigHintIcon label={t("panel.config.audioPoster")}>
3242
- {t("panel.config.audioPosterHint")}
3243
- </ConfigHintIcon>
3244
- </div>
3245
- </>,
3246
- undefined,
3247
- { groupKey: "audioDisplayStyle", defaultOpen: false }
3248
- )}
3249
- </>,
3250
- true,
3251
- [t("panel.material.audio"), "url", t("panel.config.searchKwUpload"), t("panel.config.searchKwRecord"), "icon", t("panel.config.searchKwPoster"), t("panel.config.effect"), t("panel.config.searchKwAutoPause"), "media"]
3252
- )
3253
- ) : selectedElement.materialType === "video" ? (
3254
- renderSection(
3255
- "videoConfig",
3256
- t("panel.config.sectionVideo"),
3257
- <>
3258
- {renderFieldGroup(
3259
- t("panel.config.groupVideoSource"),
3260
- <>
3261
- <label className="block space-y-1">
3262
- <div>{t("panel.config.videoUrl")}</div>
3263
- <Input
3264
- value={selectedElement.videoRemoteUrl ?? ""}
3265
- onChange={(e) =>
3266
- updateSelectedVideo({
3267
- videoRemoteUrl: e.target.value || undefined,
3268
- videoSrc: e.target.value || selectedElement.videoSrc,
3269
- })
3270
- }
3271
- placeholder="https://example.com/video.mp4"
3272
- className="h-7"
3273
- />
3274
- </label>
3275
- <div className="flex items-center gap-2">
3276
- <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3277
- {t("panel.config.uploadVideo")}
3278
- <Input
3279
- type="file"
3280
- accept="video/*"
3281
- className="hidden"
3282
- onChange={async (e) => {
3283
- const file = e.target.files?.[0];
3284
- e.currentTarget.value = "";
3285
- if (!file) return;
3286
- await handleUploadVideoFile(file);
3287
- }}
3288
- />
3289
- </label>
3290
- </div>
3291
- {videoStatus ? (
3292
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3293
- {videoStatus}
3294
- </div>
3295
- ) : null}
3296
- <video
3297
- controls
3298
- className="h-36 w-full rounded border border-border/60 bg-black/80 object-contain"
3299
- src={selectedElement.videoSrc || selectedElement.videoRemoteUrl || ""}
3300
- />
3301
- <label className="flex items-center gap-2">
3302
- <Checkbox
3303
- checked={selectedElement.mediaAutoPauseOnEdit !== false}
3304
- 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"
3305
- onCheckedChange={(checked) =>
3306
- updateSelectedVideo({
3307
- mediaAutoPauseOnEdit: checked !== false,
3308
- })
3309
- }
3310
- />
3311
- <span>{t("panel.config.autoPauseMedia")}</span>
3312
- </label>
3313
- </>
3314
- )}
3315
- </>,
3316
- true,
3317
- [t("panel.material.video"), "url", t("panel.config.searchKwUpload"), t("panel.config.searchKwPreview"), t("panel.config.searchKwAutoPause"), "media"]
3318
- )
3319
- ) : null}
3320
- {selectedElement
3321
- ? selectedElement.parentGridId
3322
- ? renderSection(
3323
- "gridChildSpan",
3324
- t("panel.config.sectionGridChildSpan"),
3325
- <>
3326
- {renderFieldGroup(
3327
- t("panel.config.groupCrossSlots"),
3328
- <div className="grid grid-cols-2 gap-2">
3329
- <label className="block space-y-1">
3330
- <div>{t("panel.config.colSpan")}</div>
3331
- <Input
3332
- type="number"
3333
- min={1}
3334
- max={12}
3335
- value={selectedElement.gridColSpan ?? 1}
3336
- onChange={(e) =>
3337
- updateElement(selectedElement.id, {
3338
- gridColSpan: Math.max(1, Math.min(12, Number(e.target.value) || 1)),
3339
- })
3340
- }
3341
- className="h-7"
3342
- />
3343
- </label>
3344
- <label className="block space-y-1">
3345
- <div>{t("panel.config.rowSpan")}</div>
3346
- <Input
3347
- type="number"
3348
- min={1}
3349
- max={12}
3350
- value={selectedElement.gridRowSpan ?? 1}
3351
- onChange={(e) =>
3352
- updateElement(selectedElement.id, {
3353
- gridRowSpan: Math.max(1, Math.min(12, Number(e.target.value) || 1)),
3354
- })
3355
- }
3356
- className="h-7"
3357
- />
3358
- </label>
3359
- </div>
3360
- )}
3361
- </>,
3362
- true,
3363
- [t("panel.config.searchKwGrid"), t("panel.config.searchKwCrossCol"), t("panel.config.searchKwCrossRow"), "span", "slot"],
3364
- <>{t("panel.config.gridChildHint")}</>
3365
- )
3366
- : null
3367
- : null}
3368
- {selectedElement.materialType === "geometry" ? (
3369
- renderSection(
3370
- "geometryConfig",
3371
- t("panel.config.sectionGeometry"),
3372
- <>
3373
- {renderFieldGroup(
3374
- t("panel.config.groupBasicShape"),
3375
- <>
3376
- <label className="block space-y-1">
3377
- <div>{t("panel.config.shape")}</div>
3378
- <Select
3379
- value={selectedElement.geometryShape ?? "rect"}
3380
- onValueChange={(value) =>
3381
- updateSelectedGeometry({
3382
- geometryShape: value as PanelElement["geometryShape"],
3383
- })
3384
- }
3385
- >
3386
- <SelectTrigger className="h-7">
3387
- <SelectValue placeholder={t("panel.config.selectShape")} />
3388
- </SelectTrigger>
3389
- <SelectContent>
3390
- <SelectItem value="rect">{t("panel.config.shapeRect")}</SelectItem>
3391
- <SelectItem value="circle">{t("panel.config.shapeCircle")}</SelectItem>
3392
- <SelectItem value="triangle">{t("panel.config.shapeTriangle")}</SelectItem>
3393
- <SelectItem value="diamond">{t("panel.config.shapeDiamond")}</SelectItem>
3394
- <SelectItem value="hexagon">{t("panel.config.shapeHexagon")}</SelectItem>
3395
- <SelectItem value="star">{t("panel.config.shapeStar")}</SelectItem>
3396
- <SelectItem value="heart">{t("panel.config.shapeHeart")}</SelectItem>
3397
- </SelectContent>
3398
- </Select>
3399
- </label>
3400
- {renderColorField(
3401
- t("panel.config.geometryColor"),
3402
- selectedElement.geometryColor ?? "#3b82f6",
3403
- (next) => updateSelectedGeometry({ geometryColor: next || "#3b82f6" })
3404
- )}
3405
- </>
3406
- )}
3407
- {renderFieldGroup(
3408
- t("panel.config.groupCanvasScript"),
3409
- <>
3410
- <Textarea
3411
- value={selectedElement.geometryScript ?? ""}
3412
- onChange={(e) => updateSelectedGeometry({ geometryScript: e.target.value || undefined })}
3413
- spellCheck={false}
3414
- className="h-36 font-mono text-[11px]"
3415
- placeholder={t("panel.config.geometryScriptPlaceholder")}
3416
- />
3417
- </>,
3418
- <>
3419
- {t("panel.config.canvasScriptHint")}
3420
- </>,
3421
- { groupKey: "geometryCanvasScript", defaultOpen: false }
3422
- )}
3423
- {renderFieldGroup(
3424
- t("panel.config.groupSketchOverlay"),
3425
- <>
3426
- <div className="flex items-center gap-2">
3427
- <label className="block space-y-1">
3428
- <div className="text-[11px]">{t("panel.config.penColor")}</div>
3429
- <Input
3430
- type="color"
3431
- value={geometryDrawPenColor}
3432
- onChange={(e) => setGeometryDrawPenColor(e.target.value)}
3433
- className="h-7 w-10 p-1"
3434
- />
3435
- </label>
3436
- <label className="block space-y-1">
3437
- <div className="text-[11px]">{t("panel.config.penWidth")}</div>
3438
- <Input
3439
- type="number"
3440
- min={1}
3441
- max={24}
3442
- value={geometryDrawPenWidth}
3443
- onChange={(e) => setGeometryDrawPenWidth(Math.max(1, Math.min(24, Number(e.target.value) || 1)))}
3444
- className="h-7 w-20"
3445
- />
3446
- </label>
3447
- </div>
3448
- <div className="rounded border border-border/60 bg-white p-2">
3449
- <canvas
3450
- ref={geometryDrawCanvasRef}
3451
- width={320}
3452
- height={180}
3453
- className="h-[180px] w-full cursor-crosshair rounded border border-border/60"
3454
- onPointerDown={(e) => {
3455
- const canvas = geometryDrawCanvasRef.current;
3456
- if (!canvas) return;
3457
- const ctx = canvas.getContext("2d");
3458
- if (!ctx) return;
3459
- const rect = canvas.getBoundingClientRect();
3460
- const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
3461
- const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
3462
- geometryLastPointRef.current = { x, y };
3463
- setIsGeometryDrawing(true);
3464
- ctx.lineCap = "round";
3465
- ctx.lineJoin = "round";
3466
- ctx.strokeStyle = geometryDrawPenColor;
3467
- ctx.lineWidth = geometryDrawPenWidth;
3468
- ctx.beginPath();
3469
- ctx.moveTo(x, y);
3470
- }}
3471
- onPointerMove={(e) => {
3472
- if (!isGeometryDrawing) return;
3473
- const canvas = geometryDrawCanvasRef.current;
3474
- const last = geometryLastPointRef.current;
3475
- if (!canvas || !last) return;
3476
- const ctx = canvas.getContext("2d");
3477
- if (!ctx) return;
3478
- const rect = canvas.getBoundingClientRect();
3479
- const x = ((e.clientX - rect.left) / rect.width) * canvas.width;
3480
- const y = ((e.clientY - rect.top) / rect.height) * canvas.height;
3481
- ctx.lineTo(x, y);
3482
- ctx.stroke();
3483
- geometryLastPointRef.current = { x, y };
3484
- }}
3485
- onPointerUp={() => {
3486
- setIsGeometryDrawing(false);
3487
- geometryLastPointRef.current = null;
3488
- }}
3489
- onPointerLeave={() => {
3490
- setIsGeometryDrawing(false);
3491
- geometryLastPointRef.current = null;
3492
- }}
3493
- />
3494
- </div>
3495
- <div className="flex items-center gap-2">
3496
- <button
3497
- type="button"
3498
- className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
3499
- onClick={() => {
3500
- const canvas = geometryDrawCanvasRef.current;
3501
- if (!canvas) return;
3502
- updateSelectedGeometry({ geometrySketchDataUrl: canvas.toDataURL("image/png") });
3503
- }}
3504
- >
3505
- {t("panel.config.applySketch")}
3506
- </button>
3507
- <button
3508
- type="button"
3509
- className="rounded border border-border px-2 py-1 text-[11px] hover:bg-accent"
3510
- onClick={() => {
3511
- updateSelectedGeometry({ geometrySketchDataUrl: undefined });
3512
- redrawGeometryPadFromElement();
3513
- }}
3514
- >
3515
- {t("panel.config.clearSketch")}
3516
- </button>
3517
- </div>
3518
- </>,
3519
- undefined,
3520
- { groupKey: "geometrySketch", defaultOpen: false }
3521
- )}
3522
- </>,
3523
- true,
3524
- [t("panel.material.geometry"), "geometry", t("panel.config.shape"), "canvas", t("panel.config.searchKwScript"), t("panel.config.searchKwSketch")]
3525
- )
3526
- ) : selectedElement.materialType === "scene3d" ? (
3527
- renderSection(
3528
- "scene3dConfig",
3529
- t("panel.config.sectionScene3d"),
3530
- <PanelConfigScene3dSection
3531
- element={selectedElement}
3532
- updateElement={updateElement}
3533
- />,
3534
- true,
3535
- [
3536
- t("panel.material.scene3d"),
3537
- "3d",
3538
- "glb",
3539
- t("panel.config.scene3dUploadModels"),
3540
- t("panel.config.groupScene3dCamera"),
3541
- t("panel.config.groupScene3dModelAnim"),
3542
- ]
3543
- )
3544
- ) : selectedElement.materialType === "grid" ? (
3545
- renderSection(
3546
- "gridConfig",
3547
- t("panel.config.sectionGrid"),
3548
- <>
3549
- {renderFieldGroup(
3550
- t("panel.config.groupGridParams"),
3551
- <div className="grid grid-cols-2 gap-2">
3552
- <label className="block space-y-1">
3553
- <div>{t("panel.config.rowCount")}</div>
3554
- <Input
3555
- type="number"
3556
- min={1}
3557
- max={12}
3558
- value={selectedElement.gridRows ?? 2}
3559
- onChange={(e) =>
3560
- updateElement(selectedElement.id, {
3561
- gridRows: Math.max(1, Math.min(12, Number(e.target.value) || 2)),
3562
- })
3563
- }
3564
- className="h-7"
3565
- />
3566
- </label>
3567
- <label className="block space-y-1">
3568
- <div>{t("panel.config.colCount")}</div>
3569
- <Input
3570
- type="number"
3571
- min={1}
3572
- max={12}
3573
- value={selectedElement.gridCols ?? 3}
3574
- onChange={(e) =>
3575
- updateElement(selectedElement.id, {
3576
- gridCols: Math.max(1, Math.min(12, Number(e.target.value) || 3)),
3577
- })
3578
- }
3579
- className="h-7"
3580
- />
3581
- </label>
3582
- <label className="block space-y-1">
3583
- <div>{t("panel.config.gapPx")}</div>
3584
- <Input
3585
- type="number"
3586
- min={0}
3587
- max={80}
3588
- value={selectedElement.gridGap ?? 8}
3589
- onChange={(e) =>
3590
- updateElement(selectedElement.id, {
3591
- gridGap: Math.max(0, Math.min(80, Number(e.target.value) || 0)),
3592
- })
3593
- }
3594
- className="h-7"
3595
- />
3596
- </label>
3597
- <label className="block space-y-1">
3598
- <div>{t("panel.config.paddingPx")}</div>
3599
- <Input
3600
- type="number"
3601
- min={0}
3602
- max={100}
3603
- value={selectedElement.gridPadding ?? 10}
3604
- onChange={(e) =>
3605
- updateElement(selectedElement.id, {
3606
- gridPadding: Math.max(0, Math.min(100, Number(e.target.value) || 0)),
3607
- })
3608
- }
3609
- className="h-7"
3610
- />
3611
- </label>
3612
- <label className="block space-y-1">
3613
- <div>{t("panel.config.snapThresholdPx")}</div>
3614
- <Input
3615
- type="number"
3616
- min={8}
3617
- max={120}
3618
- value={selectedElement.gridSnapThreshold ?? 36}
3619
- onChange={(e) =>
3620
- updateElement(selectedElement.id, {
3621
- gridSnapThreshold: Math.max(
3622
- 8,
3623
- Math.min(120, Number(e.target.value) || 36)
3624
- ),
3625
- })
3626
- }
3627
- className="h-7"
3628
- />
3629
- </label>
526
+ ) : materialType === "scene3d" ? (
527
+ helpers.renderSection(
528
+ "scene3dConfig",
529
+ t("panel.config.sectionScene3d"),
530
+ <PanelConfigScene3dSection
531
+ element={selectedElement}
532
+ updateElement={updateElement}
533
+ />,
534
+ true,
535
+ [
536
+ t("panel.material.scene3d"),
537
+ "3d",
538
+ "glb",
539
+ t("panel.config.scene3dUploadModels"),
540
+ t("panel.config.groupScene3dCamera"),
541
+ t("panel.config.groupScene3dModelAnim"),
542
+ ]
543
+ )
544
+ ) : materialType === "grid" ? (
545
+ <PanelConfigGridSection
546
+ element={selectedElement}
547
+ helpers={helpers}
548
+ updateElement={updateElement}
549
+ />
550
+ ) : materialType === "viewport" ? (
551
+ <PanelConfigViewportSection
552
+ element={selectedElement}
553
+ helpers={helpers}
554
+ updateElement={updateElement}
555
+ layers={layers}
556
+ setReferenceCopyMode={setReferenceCopyMode}
557
+ />
558
+ ) : materialType === "reference" ? (
559
+ <PanelConfigReferenceSection
560
+ element={selectedElement}
561
+ helpers={helpers}
562
+ updateElement={updateElement}
563
+ layers={layers}
564
+ setReferenceCopyMode={setReferenceCopyMode}
565
+ />
566
+ ) : materialType === "image" ? null : (
567
+ <div className="text-xs leading-6 text-muted-foreground">
568
+ {t("panel.config.notChartType")}
3630
569
  </div>
3631
570
  )}
3632
- </>,
3633
- true,
3634
- [t("panel.config.searchKwGrid"), "grid", t("panel.config.rows"), t("panel.config.cols"), t("panel.config.searchKwGap"), t("panel.config.searchKwPadding"), t("panel.config.searchKwSnap"), t("panel.config.searchKwThreshold")],
3635
- <>
3636
- {t("panel.config.gridHint")}
3637
- </>
3638
- )
3639
- ) : selectedElement.materialType === "viewport" ? (
3640
- renderSection(
3641
- "viewportConfig",
3642
- t("panel.config.sectionViewport"),
3643
- <>
3644
- {renderFieldGroup(
3645
- t("panel.config.groupRefSource"),
3646
- <label className="block space-y-1">
3647
- <div>{t("panel.config.refLayer")}</div>
3648
- <Select
3649
- value={selectedElement.refLayerId ?? "__none__"}
3650
- onValueChange={(value) =>
3651
- updateElement(selectedElement.id, {
3652
- refLayerId: value === "__none__" ? undefined : value,
3653
- })
3654
- }
3655
- >
3656
- <SelectTrigger className="h-7">
3657
- <SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
3658
- </SelectTrigger>
3659
- <SelectContent>
3660
- <SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
3661
- {layers
3662
- .filter((l) => l.id !== selectedElement.layerId)
3663
- .map((l) => (
3664
- <SelectItem key={l.id} value={l.id}>
3665
- {l.name}
3666
- </SelectItem>
3667
- ))}
3668
- </SelectContent>
3669
- </Select>
3670
- </label>
3671
- )}
3672
- {renderFieldGroup(
3673
- t("panel.config.groupCopyStrategy"),
3674
- <>
3675
- <label className="block space-y-1">
3676
- <div>{t("panel.config.copyMode")}</div>
3677
- <Select
3678
- value={selectedElement.refCopyMode ?? "shallow"}
3679
- onValueChange={(value) =>
3680
- setReferenceCopyMode?.(
3681
- selectedElement.id,
3682
- value as ReferenceCopyMode
3683
- )
3684
- }
3685
- >
3686
- <SelectTrigger className="h-7">
3687
- <SelectValue placeholder={t("panel.config.selectCopyMode")} />
3688
- </SelectTrigger>
3689
- <SelectContent>
3690
- <SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
3691
- <SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
3692
- </SelectContent>
3693
- </Select>
3694
- </label>
3695
- </>,
3696
- <>{t("panel.config.copyStrategyHint")}</>,
3697
- { groupKey: "viewportCopyStrategy", defaultOpen: false }
3698
- )}
3699
- {renderFieldGroup(
3700
- t("panel.config.groupViewportOverflow"),
3701
- <label className="block space-y-1">
3702
- <div>{t("panel.config.viewportOverflow")}</div>
3703
- <Select
3704
- value={normalizeViewportOverflow(selectedElement.viewportOverflow)}
3705
- onValueChange={(value) =>
3706
- updateElement(selectedElement.id, {
3707
- viewportOverflow: value as ViewportOverflowMode,
3708
- })
3709
- }
3710
- >
3711
- <SelectTrigger className="h-7">
3712
- <SelectValue placeholder={t("panel.config.viewportOverflow")} />
3713
- </SelectTrigger>
3714
- <SelectContent>
3715
- {VIEWPORT_OVERFLOW_MODES.map((mode) => (
3716
- <SelectItem key={mode} value={mode}>
3717
- {mode === "scroll-x"
3718
- ? t("panel.config.viewportOverflowScrollX")
3719
- : mode === "scroll-y"
3720
- ? t("panel.config.viewportOverflowScrollY")
3721
- : mode === "scroll"
3722
- ? t("panel.config.viewportOverflowScroll")
3723
- : t("panel.config.viewportOverflowClip")}
3724
- </SelectItem>
3725
- ))}
3726
- </SelectContent>
3727
- </Select>
3728
- </label>
3729
- )}
3730
- </>,
3731
- true,
3732
- [
3733
- t("panel.config.searchKwViewport"),
3734
- "viewport",
3735
- t("panel.config.refLayer"),
3736
- t("panel.config.viewportOverflow"),
3737
- t("panel.config.viewportOverflowScrollX"),
3738
- t("panel.config.viewportOverflowClip"),
3739
- t("panel.material.shallowCopy"),
3740
- t("panel.material.deepCopy"),
3741
- ],
3742
- <>{t("panel.config.viewportHint")}</>
3743
- )
3744
- ) : selectedElement.materialType === "reference" ? (
3745
- renderSection(
3746
- "reference",
3747
- t("panel.config.sectionReference"),
3748
- <>
3749
- {renderFieldGroup(
3750
- t("panel.config.groupRefSource"),
3751
- <label className="block space-y-1">
3752
- <div>{t("panel.config.refLayer")}</div>
3753
- <Select
3754
- value={selectedElement.refLayerId ?? "__none__"}
3755
- onValueChange={(value) =>
3756
- updateElement(selectedElement.id, {
3757
- refLayerId: value === "__none__" ? undefined : value,
3758
- })
3759
- }
3760
- >
3761
- <SelectTrigger className="h-7">
3762
- <SelectValue placeholder={t("panel.config.pleaseSelectLayer")} />
3763
- </SelectTrigger>
3764
- <SelectContent>
3765
- <SelectItem value="__none__">{t("panel.config.noneNoRef")}</SelectItem>
3766
- {layers
3767
- .filter((l) => l.id !== selectedElement.layerId)
3768
- .map((l) => (
3769
- <SelectItem key={l.id} value={l.id}>
3770
- {l.name}
3771
- </SelectItem>
3772
- ))}
3773
- </SelectContent>
3774
- </Select>
3775
- </label>
3776
- )}
3777
- {renderFieldGroup(
3778
- t("panel.config.groupCopyStrategy"),
3779
- <>
3780
- <label className="block space-y-1">
3781
- <div>{t("panel.config.copyMode")}</div>
3782
- <Select
3783
- value={selectedElement.refCopyMode ?? "shallow"}
3784
- onValueChange={(value) =>
3785
- setReferenceCopyMode?.(
3786
- selectedElement.id,
3787
- value as ReferenceCopyMode
3788
- )
3789
- }
3790
- >
3791
- <SelectTrigger className="h-7">
3792
- <SelectValue placeholder={t("panel.config.selectCopyMode")} />
3793
- </SelectTrigger>
3794
- <SelectContent>
3795
- <SelectItem value="shallow">{t("panel.config.shallowFollow")}</SelectItem>
3796
- <SelectItem value="deep">{t("panel.config.deepFreeze")}</SelectItem>
3797
- </SelectContent>
3798
- </Select>
3799
- </label>
3800
- </>,
3801
- <>
3802
- {t("panel.config.copyStrategyHint")}
3803
- </>,
3804
- { groupKey: "referenceCopyStrategy", defaultOpen: false }
3805
- )}
3806
- </>,
3807
- true,
3808
- [t("panel.config.searchKwRef"), "ref", t("panel.config.layer"), t("panel.material.shallowCopy"), t("panel.material.deepCopy"), "snapshot"]
3809
- )
3810
- ) : selectedElement.materialType === "image" ? null : (
3811
- <div className="text-xs leading-6 text-muted-foreground">
3812
- {t("panel.config.notChartType")}
3813
- </div>
3814
- )}
3815
- {renderSection(
3816
- "styleBackground",
3817
- t("panel.config.sectionStyleBackground"),
3818
- <>
3819
- {renderFieldGroup(
3820
- t("panel.config.groupBgFill"),
3821
- <>
3822
- {renderColorField(
3823
- t("panel.config.backgroundColor"),
3824
- selectedElement.style?.backgroundColor ?? "",
3825
- (next) => updateSelectedStyle({ backgroundColor: next || undefined })
3826
- )}
3827
- </>
3828
- )}
3829
- {renderFieldGroup(
3830
- t("panel.config.groupBgLayout"),
3831
- <>
3832
- <label className="block space-y-1">
3833
- <div>{t("panel.config.backgroundImage")}</div>
3834
- <Input
3835
- value={selectedElement.style?.backgroundImage ?? ""}
3836
- onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
3837
- placeholder='url("https://...") / linear-gradient(...)'
3838
- className="h-7"
3839
- />
3840
- </label>
3841
- <div className="flex items-center gap-2">
3842
- <label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
3843
- {t("panel.config.uploadImage")}
3844
- <Input
3845
- type="file"
3846
- accept="image/*"
3847
- className="hidden"
3848
- onChange={async (e) => {
3849
- const file = e.target.files?.[0];
3850
- e.currentTarget.value = "";
3851
- if (!file) return;
3852
- await handleUploadBackgroundImage(file);
3853
- }}
3854
- />
3855
- </label>
3856
- {uploadStatus ? (
3857
- <span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
3858
- ) : null}
3859
- </div>
3860
- <div className="grid grid-cols-2 gap-2">
3861
- <label className="block space-y-1">
3862
- <div>{t("panel.config.backgroundSize")}</div>
3863
- <Select
3864
- value={selectedElement.style?.backgroundSize ?? "__none__"}
3865
- onValueChange={(value) =>
3866
- updateSelectedStyle({
3867
- backgroundSize: value === "__none__" ? undefined : value,
3868
- })
3869
- }
3870
- >
3871
- <SelectTrigger className="h-7">
3872
- <SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
3873
- </SelectTrigger>
3874
- <SelectContent>
3875
- <SelectItem value="__none__">{t("common.default")}</SelectItem>
3876
- <SelectItem value="cover">cover</SelectItem>
3877
- <SelectItem value="contain">contain</SelectItem>
3878
- <SelectItem value="100% 100%">100% 100%</SelectItem>
3879
- <SelectItem value="auto">auto</SelectItem>
3880
- </SelectContent>
3881
- </Select>
3882
- </label>
3883
- <label className="block space-y-1">
3884
- <div>{t("panel.config.backgroundPosition")}</div>
3885
- <Select
3886
- value={selectedElement.style?.backgroundPosition ?? "__none__"}
3887
- onValueChange={(value) =>
3888
- updateSelectedStyle({
3889
- backgroundPosition: value === "__none__" ? undefined : value,
3890
- })
3891
- }
3892
- >
3893
- <SelectTrigger className="h-7">
3894
- <SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
3895
- </SelectTrigger>
3896
- <SelectContent>
3897
- <SelectItem value="__none__">{t("common.default")}</SelectItem>
3898
- <SelectItem value="center">center</SelectItem>
3899
- <SelectItem value="top">top</SelectItem>
3900
- <SelectItem value="bottom">bottom</SelectItem>
3901
- <SelectItem value="left">left</SelectItem>
3902
- <SelectItem value="right">right</SelectItem>
3903
- <SelectItem value="top left">top left</SelectItem>
3904
- <SelectItem value="top right">top right</SelectItem>
3905
- <SelectItem value="bottom left">bottom left</SelectItem>
3906
- <SelectItem value="bottom right">bottom right</SelectItem>
3907
- </SelectContent>
3908
- </Select>
3909
- </label>
3910
- </div>
3911
- </>,
3912
- undefined,
3913
- { groupKey: "styleBgLayout", defaultOpen: false }
3914
- )}
3915
- </>,
3916
- false,
3917
- [t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
3918
- )}
3919
-
3920
- {renderSection(
3921
- "styleBorder",
3922
- t("panel.config.sectionStyleBorder"),
3923
- <>
3924
- {renderFieldGroup(
3925
- t("panel.config.groupBorderGeometry"),
3926
- <div className="grid grid-cols-2 gap-2">
3927
- <label className="block space-y-1">
3928
- <div>{t("panel.config.borderWidth")}</div>
3929
- <Input
3930
- type="number"
3931
- min={0}
3932
- value={selectedElement.style?.borderWidth ?? 0}
3933
- onChange={(e) =>
3934
- updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
3935
- }
3936
- className="h-7"
3937
- />
3938
- </label>
3939
- <label className="block space-y-1">
3940
- <div>{t("panel.config.borderRadius")}</div>
3941
- <Input
3942
- type="number"
3943
- min={0}
3944
- value={selectedElement.style?.borderRadius ?? 0}
3945
- onChange={(e) =>
3946
- updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
3947
- }
3948
- className="h-7"
3949
- />
3950
- </label>
3951
- </div>
3952
- )}
3953
- {renderFieldGroup(
3954
- t("panel.config.groupBorderVisual"),
3955
- <div className="grid grid-cols-2 gap-2">
3956
- <label className="block space-y-1">
3957
- <div>{t("panel.config.borderStyle")}</div>
3958
- <Select
3959
- value={selectedElement.style?.borderStyle ?? "solid"}
3960
- onValueChange={(value) =>
3961
- updateSelectedStyle({
3962
- borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
3963
- })
3964
- }
3965
- >
3966
- <SelectTrigger className="h-7">
3967
- <SelectValue placeholder={t("panel.config.selectBorderStyle")} />
3968
- </SelectTrigger>
3969
- <SelectContent>
3970
- <SelectItem value="none">none</SelectItem>
3971
- <SelectItem value="solid">solid</SelectItem>
3972
- <SelectItem value="dashed">dashed</SelectItem>
3973
- <SelectItem value="dotted">dotted</SelectItem>
3974
- <SelectItem value="double">double</SelectItem>
3975
- </SelectContent>
3976
- </Select>
3977
- </label>
3978
- {renderColorField(
3979
- t("panel.config.borderColor"),
3980
- selectedElement.style?.borderColor ?? "",
3981
- (next) => updateSelectedStyle({ borderColor: next || undefined })
3982
- )}
3983
- </div>
3984
- )}
3985
- </>,
3986
- false,
3987
- [t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
3988
- )}
3989
-
3990
- {hasSearch && renderedSectionCount === 0 ? (
3991
- <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
3992
- {t("panel.config.noMatch")}
3993
- </div>
3994
- ) : null}
3995
- </div>
3996
- </fieldset>
3997
- </div>
3998
- ) : null}
3999
- </aside>
571
+ <PanelConfigStyleSections
572
+ element={selectedElement}
573
+ helpers={helpers}
574
+ updateElement={updateElement}
575
+ />
576
+ <ConfigSearchEmpty
577
+ hasSearch={hasSearch}
578
+ matchCountRef={matchCountRef}
579
+ label={t("panel.config.noMatch")}
580
+ />
581
+ </div>
582
+ </fieldset>
583
+ </div>
584
+ ) : null}
585
+ </aside>
4000
586
  </ScopeConfigProvider>
4001
587
  );
4002
588
  }
4003
-