@arronqzy/react-view 1.0.22 → 1.0.23

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.
@@ -0,0 +1,1291 @@
1
+ import { useCallback, useState } from "react";
2
+ import { useI18n } from "@arronqzy/i18n/react";
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardHeader,
7
+ CardTitle,
8
+ Checkbox,
9
+ Input,
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ Textarea,
16
+ } from "@arronqzy/ui";
17
+ import type { PanelElement, PanelLayer, ReferenceCopyMode } from "../../types";
18
+ import { CHART_TYPES, getChartLabelsDisplayText, getChartValuesDisplayText } from "../../utils/chartOptionBuilder";
19
+ import { VIEWPORT_OVERFLOW_MODES, normalizeViewportOverflow, type ViewportOverflowMode } from "../../utils/viewportPlacement";
20
+ import { type ConfigSectionHelpers, type UpdateElement, mergeOptionPatch } from "./helpers";
21
+
22
+ export function PanelConfigMultiSelect({
23
+ elements,
24
+ helpers,
25
+ updateElement,
26
+ layers,
27
+ setReferenceCopyMode,
28
+ onExcludeSelectedNode,
29
+ onAdjustNodeZOrder,
30
+ }: {
31
+ elements: PanelElement[];
32
+ helpers: ConfigSectionHelpers;
33
+ updateElement: UpdateElement;
34
+ layers: PanelLayer[];
35
+ setReferenceCopyMode?: (id: string, mode: ReferenceCopyMode) => void;
36
+ onExcludeSelectedNode?: (nodeId: string) => void;
37
+ onAdjustNodeZOrder?: (
38
+ nodeId: string,
39
+ action: "bringForward" | "sendBackward" | "bringToFront" | "sendToBack"
40
+ ) => void;
41
+ }) {
42
+ const { t } = useI18n();
43
+ const { hasSearch, normalizedSearch, renderFormatterLabel, renderOptionLabel } = helpers;
44
+ const effectiveSelectedElements = elements;
45
+ const [expandedNodeCards, setExpandedNodeCards] = useState<Record<string, boolean>>({});
46
+ const isNodeCardExpanded = useCallback(
47
+ (id: string) => expandedNodeCards[id] ?? true,
48
+ [expandedNodeCards]
49
+ );
50
+ const setNodeCardExpanded = useCallback((id: string, open: boolean) => {
51
+ setExpandedNodeCards((prev) => ({ ...prev, [id]: open }));
52
+ }, []);
53
+
54
+ return (
55
+ <div className="space-y-3">
56
+ <Card>
57
+ <CardHeader className="pb-2">
58
+ <CardTitle className="text-xs">{t("panel.config.batchTitleWithCount", { count: effectiveSelectedElements.length })}</CardTitle>
59
+ </CardHeader>
60
+ <CardContent className="space-y-2 text-xs">
61
+ <div className="grid grid-cols-2 gap-2">
62
+ <button
63
+ type="button"
64
+ className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
65
+ onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: true }))}
66
+ >
67
+ {t("panel.config.lockAll")}
68
+ </button>
69
+ <button
70
+ type="button"
71
+ className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
72
+ onClick={() => effectiveSelectedElements.forEach((el) => updateElement(el.id, { locked: false }))}
73
+ >
74
+ {t("panel.config.unlockAll")}
75
+ </button>
76
+ <button
77
+ type="button"
78
+ className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
79
+ onClick={() =>
80
+ effectiveSelectedElements.forEach((el) =>
81
+ onAdjustNodeZOrder?.(el.id, "bringForward")
82
+ )
83
+ }
84
+ >
85
+ {t("panel.config.bringAllForward")}
86
+ </button>
87
+ <button
88
+ type="button"
89
+ className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
90
+ onClick={() =>
91
+ effectiveSelectedElements.forEach((el) =>
92
+ onAdjustNodeZOrder?.(el.id, "sendBackward")
93
+ )
94
+ }
95
+ >
96
+ {t("panel.config.sendAllBackward")}
97
+ </button>
98
+ <button
99
+ type="button"
100
+ className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
101
+ onClick={() =>
102
+ effectiveSelectedElements.forEach((el) =>
103
+ updateElement(el.id, { zIndex: 1 })
104
+ )
105
+ }
106
+ >
107
+ {t("panel.config.setAllZIndex1")}
108
+ </button>
109
+ <button
110
+ type="button"
111
+ className="rounded border border-border bg-background px-2 py-1 hover:bg-accent"
112
+ onClick={() =>
113
+ effectiveSelectedElements.forEach((el) =>
114
+ updateElement(el.id, {
115
+ style: {
116
+ ...(el.style ?? {}),
117
+ backgroundColor: "#3b82f6",
118
+ },
119
+ })
120
+ )
121
+ }
122
+ >
123
+ {t("panel.config.setAllBgBlue")}
124
+ </button>
125
+ </div>
126
+ </CardContent>
127
+ </Card>
128
+ {effectiveSelectedElements
129
+ .filter((el) => {
130
+ if (!hasSearch) return true;
131
+ const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
132
+ .toLowerCase();
133
+ return text.includes(normalizedSearch);
134
+ })
135
+ .map((el) => (
136
+ <Card
137
+ key={el.id}
138
+ className={el.locked ? "border-amber-500/40 bg-amber-500/5" : ""}
139
+ >
140
+ <CardHeader className="pb-2">
141
+ <div className="flex items-center gap-2">
142
+ <button
143
+ type="button"
144
+ className="inline-flex h-6 w-6 items-center justify-center rounded border border-border text-[11px] hover:bg-accent"
145
+ onClick={() => setNodeCardExpanded(el.id, !isNodeCardExpanded(el.id))}
146
+ aria-label={isNodeCardExpanded(el.id) ? t("panel.config.collapseNodeConfig") : t("panel.config.expandNodeConfig")}
147
+ >
148
+ {isNodeCardExpanded(el.id) ? "▾" : "▸"}
149
+ </button>
150
+ <CardTitle className="min-w-0 flex-1 text-xs truncate">
151
+ {el.name?.trim() || el.materialType || t("common.node")} · {el.id}
152
+ </CardTitle>
153
+ <button
154
+ type="button"
155
+ 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"
156
+ onClick={() => onExcludeSelectedNode?.(el.id)}
157
+ title={t("panel.config.removeFromSelectionTitle")}
158
+ >
159
+ {t("panel.config.removeFromSelection")}
160
+ </button>
161
+ </div>
162
+ </CardHeader>
163
+ {isNodeCardExpanded(el.id) ? (
164
+ <CardContent className="space-y-2 text-xs">
165
+ <label className="flex items-center gap-2">
166
+ <Checkbox
167
+ checked={el.locked === true}
168
+ onCheckedChange={(checked) => updateElement(el.id, { locked: checked === true })}
169
+ />
170
+ <span>{t("panel.config.lockedNode")}</span>
171
+ </label>
172
+ {el.locked ? (
173
+ <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">
174
+ {t("panel.config.lockedNodeHint")}
175
+ </div>
176
+ ) : null}
177
+ <fieldset disabled={el.locked} className={el.locked ? "opacity-60" : ""}>
178
+ <div className="space-y-2">
179
+ <label className="block space-y-1">
180
+ <div>{t("panel.config.name")}</div>
181
+ <Input
182
+ className="h-7"
183
+ value={el.name ?? ""}
184
+ onChange={(e) => updateElement(el.id, { name: e.target.value || undefined })}
185
+ />
186
+ </label>
187
+ <div className="grid grid-cols-2 gap-2">
188
+ <label className="block space-y-1">
189
+ <div>zIndex</div>
190
+ <Input
191
+ className="h-7"
192
+ type="number"
193
+ value={el.zIndex ?? 1}
194
+ onChange={(e) =>
195
+ updateElement(el.id, { zIndex: Number(e.target.value) || 1 })
196
+ }
197
+ />
198
+ </label>
199
+ <label className="block space-y-1">
200
+ <div>{t("panel.config.layer")}</div>
201
+ <Select
202
+ value={el.layerId}
203
+ onValueChange={(value) => updateElement(el.id, { layerId: value })}
204
+ >
205
+ <SelectTrigger className="h-7">
206
+ <SelectValue placeholder={t("panel.config.selectLayer")} />
207
+ </SelectTrigger>
208
+ <SelectContent>
209
+ {layers.map((layer) => (
210
+ <SelectItem key={layer.id} value={layer.id}>
211
+ {layer.name}
212
+ </SelectItem>
213
+ ))}
214
+ </SelectContent>
215
+ </Select>
216
+ </label>
217
+ </div>
218
+ <div className="grid grid-cols-2 gap-2">
219
+ <label className="block space-y-1">
220
+ <div>X</div>
221
+ <Input
222
+ className="h-7"
223
+ type="number"
224
+ value={el.x}
225
+ onChange={(e) => updateElement(el.id, { x: Number(e.target.value) || 0 })}
226
+ />
227
+ </label>
228
+ <label className="block space-y-1">
229
+ <div>Y</div>
230
+ <Input
231
+ className="h-7"
232
+ type="number"
233
+ value={el.y}
234
+ onChange={(e) => updateElement(el.id, { y: Number(e.target.value) || 0 })}
235
+ />
236
+ </label>
237
+ <label className="block space-y-1">
238
+ <div>{t("panel.config.rotate")}</div>
239
+ <Input
240
+ className="h-7"
241
+ type="number"
242
+ value={el.rotate ?? 0}
243
+ onChange={(e) =>
244
+ updateElement(el.id, { rotate: Number(e.target.value) || 0 })
245
+ }
246
+ />
247
+ </label>
248
+ <label className="block space-y-1">
249
+ <div>{t("panel.config.width")}</div>
250
+ <Input
251
+ className="h-7"
252
+ type="number"
253
+ min={1}
254
+ value={el.width}
255
+ onChange={(e) =>
256
+ updateElement(el.id, { width: Math.max(1, Number(e.target.value) || 1) })
257
+ }
258
+ />
259
+ </label>
260
+ <label className="block space-y-1">
261
+ <div>{t("panel.config.height")}</div>
262
+ <Input
263
+ className="h-7"
264
+ type="number"
265
+ min={1}
266
+ value={el.height}
267
+ onChange={(e) =>
268
+ updateElement(el.id, { height: Math.max(1, Number(e.target.value) || 1) })
269
+ }
270
+ />
271
+ </label>
272
+ </div>
273
+ <div className="grid grid-cols-2 gap-2">
274
+ <label className="block space-y-1">
275
+ <div>{t("panel.config.backgroundColor")}</div>
276
+ <Input
277
+ className="h-7"
278
+ value={el.style?.backgroundColor ?? ""}
279
+ placeholder="#000000"
280
+ onChange={(e) =>
281
+ updateElement(el.id, {
282
+ style: {
283
+ ...(el.style ?? {}),
284
+ backgroundColor: e.target.value || undefined,
285
+ },
286
+ })
287
+ }
288
+ />
289
+ </label>
290
+ <label className="block space-y-1">
291
+ <div>{t("panel.config.borderColorShort")}</div>
292
+ <Input
293
+ className="h-7"
294
+ value={el.style?.borderColor ?? ""}
295
+ placeholder="#000000"
296
+ onChange={(e) =>
297
+ updateElement(el.id, {
298
+ style: {
299
+ ...(el.style ?? {}),
300
+ borderColor: e.target.value || undefined,
301
+ },
302
+ })
303
+ }
304
+ />
305
+ </label>
306
+ </div>
307
+ {CHART_TYPES.has(el.materialType ?? "") ? (
308
+ <div className="grid grid-cols-2 gap-2">
309
+ <label className="block space-y-1 col-span-2">
310
+ <div>{t("panel.config.chartTitle")}</div>
311
+ <Input
312
+ className="h-7"
313
+ value={el.chart?.title ?? ""}
314
+ onChange={(e) =>
315
+ updateElement(el.id, {
316
+ chart: { ...(el.chart ?? {}), title: e.target.value },
317
+ })
318
+ }
319
+ />
320
+ </label>
321
+ <label className="block space-y-1">
322
+ <div>{t("panel.config.primaryColor")}</div>
323
+ <Input
324
+ className="h-7"
325
+ value={el.chart?.color ?? ""}
326
+ onChange={(e) =>
327
+ updateElement(el.id, {
328
+ chart: { ...(el.chart ?? {}), color: e.target.value || undefined },
329
+ })
330
+ }
331
+ />
332
+ </label>
333
+ <label className="flex items-center gap-2">
334
+ <Checkbox
335
+ checked={el.chart?.colorMode === "gradient"}
336
+ onCheckedChange={(checked) =>
337
+ updateElement(el.id, {
338
+ chart: { ...(el.chart ?? {}), colorMode: checked ? "gradient" : "solid" },
339
+ })
340
+ }
341
+ />
342
+ <span>{t("panel.config.primaryGradient")}</span>
343
+ </label>
344
+ {el.chart?.colorMode === "gradient" ? (
345
+ <>
346
+ <label className="block space-y-1">
347
+ <div>{t("panel.config.gradientFrom")}</div>
348
+ <Input
349
+ className="h-7"
350
+ value={el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}
351
+ onChange={(e) =>
352
+ updateElement(el.id, {
353
+ chart: { ...(el.chart ?? {}), gradientFrom: e.target.value || "#3b82f6" },
354
+ })
355
+ }
356
+ />
357
+ </label>
358
+ <label className="block space-y-1">
359
+ <div>{t("panel.config.gradientTo")}</div>
360
+ <Input
361
+ className="h-7"
362
+ value={el.chart?.gradientTo ?? "#22d3ee"}
363
+ onChange={(e) =>
364
+ updateElement(el.id, {
365
+ chart: { ...(el.chart ?? {}), gradientTo: e.target.value || "#22d3ee" },
366
+ })
367
+ }
368
+ />
369
+ </label>
370
+ <label className="block space-y-1 col-span-2">
371
+ <div>{t("panel.config.gradientDirection")}</div>
372
+ <Select
373
+ value={el.chart?.gradientDirection ?? "to-right"}
374
+ onValueChange={(value) =>
375
+ updateElement(el.id, {
376
+ chart: {
377
+ ...(el.chart ?? {}),
378
+ gradientDirection: value as
379
+ | "to-right"
380
+ | "to-bottom"
381
+ | "to-bottom-right"
382
+ | "to-top-right",
383
+ },
384
+ })
385
+ }
386
+ >
387
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
388
+ <SelectContent>
389
+ <SelectItem value="to-right">{t("panel.config.dirToRight")}</SelectItem>
390
+ <SelectItem value="to-bottom">{t("panel.config.dirToBottom")}</SelectItem>
391
+ <SelectItem value="to-bottom-right">{t("panel.config.dirToBottomRight")}</SelectItem>
392
+ <SelectItem value="to-top-right">{t("panel.config.dirToTopRight")}</SelectItem>
393
+ </SelectContent>
394
+ </Select>
395
+ </label>
396
+ <div className="col-span-2 space-y-1">
397
+ <div className="text-[11px] text-muted-foreground">{t("panel.config.gradientPreview")}</div>
398
+ <div
399
+ className="h-6 rounded border border-border/60"
400
+ style={{
401
+ backgroundImage: `linear-gradient(${
402
+ (el.chart?.gradientDirection ?? "to-right") === "to-bottom"
403
+ ? "to bottom"
404
+ : (el.chart?.gradientDirection ?? "to-right") === "to-bottom-right"
405
+ ? "to bottom right"
406
+ : (el.chart?.gradientDirection ?? "to-right") === "to-top-right"
407
+ ? "to top right"
408
+ : "to right"
409
+ }, ${el.chart?.gradientFrom ?? el.chart?.color ?? "#3b82f6"}, ${el.chart?.gradientTo ?? "#22d3ee"})`,
410
+ }}
411
+ />
412
+ </div>
413
+ </>
414
+ ) : null}
415
+ <label className="block space-y-1">
416
+ <div>{t("panel.config.render")}</div>
417
+ <Select
418
+ value={el.chart?.renderMode ?? "canvas"}
419
+ onValueChange={(value) =>
420
+ updateElement(el.id, {
421
+ chart: { ...(el.chart ?? {}), renderMode: value as "canvas" | "svg" },
422
+ })
423
+ }
424
+ >
425
+ <SelectTrigger className="h-7">
426
+ <SelectValue />
427
+ </SelectTrigger>
428
+ <SelectContent>
429
+ <SelectItem value="canvas">canvas</SelectItem>
430
+ <SelectItem value="svg">svg</SelectItem>
431
+ </SelectContent>
432
+ </Select>
433
+ </label>
434
+ <label className="block space-y-1">
435
+ <div>{t("panel.config.tooltipBg")}</div>
436
+ <Input
437
+ className="h-7"
438
+ value={el.chart?.tooltipBackgroundColor ?? ""}
439
+ placeholder="#0f172a"
440
+ onChange={(e) =>
441
+ updateElement(el.id, {
442
+ chart: {
443
+ ...(el.chart ?? {}),
444
+ tooltipBackgroundColor: e.target.value || undefined,
445
+ },
446
+ })
447
+ }
448
+ />
449
+ </label>
450
+ <label className="flex items-center gap-2">
451
+ <Checkbox
452
+ checked={el.chart?.tooltipShow ?? true}
453
+ onCheckedChange={(checked) =>
454
+ updateElement(el.id, {
455
+ chart: { ...(el.chart ?? {}), tooltipShow: checked === true },
456
+ })
457
+ }
458
+ />
459
+ <span>{t("panel.config.showTooltip")}</span>
460
+ </label>
461
+ <label className="block space-y-1">
462
+ <div>{t("panel.config.tooltipTrigger")}</div>
463
+ <Select
464
+ value={el.chart?.tooltipTrigger ?? "axis"}
465
+ onValueChange={(value) =>
466
+ updateElement(el.id, {
467
+ chart: { ...(el.chart ?? {}), tooltipTrigger: value as "axis" | "item" },
468
+ })
469
+ }
470
+ >
471
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
472
+ <SelectContent>
473
+ <SelectItem value="axis">axis</SelectItem>
474
+ <SelectItem value="item">item</SelectItem>
475
+ </SelectContent>
476
+ </Select>
477
+ </label>
478
+ <label className="block space-y-1">
479
+ <div>{t("panel.config.tooltipTextColor")}</div>
480
+ <Input
481
+ className="h-7"
482
+ value={el.chart?.tooltipTextColor ?? ""}
483
+ placeholder="#f8fafc"
484
+ onChange={(e) =>
485
+ updateElement(el.id, {
486
+ chart: {
487
+ ...(el.chart ?? {}),
488
+ tooltipTextColor: e.target.value || undefined,
489
+ },
490
+ })
491
+ }
492
+ />
493
+ </label>
494
+ <label className="block space-y-1 col-span-2">
495
+ {renderFormatterLabel("Tooltip Formatter")}
496
+ <Input
497
+ className="h-7"
498
+ value={el.chart?.tooltipFormatter ?? ""}
499
+ placeholder="{b}: {c}"
500
+ onChange={(e) =>
501
+ updateElement(el.id, {
502
+ chart: {
503
+ ...(el.chart ?? {}),
504
+ tooltipFormatter: e.target.value || undefined,
505
+ },
506
+ })
507
+ }
508
+ />
509
+ </label>
510
+ <label className="block space-y-1 col-span-2">
511
+ <div>{t("panel.config.labelsCsv")}</div>
512
+ <Input
513
+ className="h-7"
514
+ value={getChartLabelsDisplayText(el.chart)}
515
+ onChange={(e) =>
516
+ updateElement(el.id, {
517
+ chart: {
518
+ ...(el.chart ?? {}),
519
+ labelsText: e.target.value,
520
+ },
521
+ })
522
+ }
523
+ />
524
+ </label>
525
+ <label className="block space-y-1 col-span-2">
526
+ <div>{t("panel.config.valuesCsv")}</div>
527
+ <Input
528
+ className="h-7"
529
+ value={getChartValuesDisplayText(el.chart)}
530
+ onChange={(e) =>
531
+ updateElement(el.id, {
532
+ chart: {
533
+ ...(el.chart ?? {}),
534
+ valuesText: e.target.value,
535
+ },
536
+ })
537
+ }
538
+ />
539
+ </label>
540
+ {["bar", "line", "area", "scatter"].includes(el.materialType ?? "") ? (
541
+ <>
542
+ <label className="block space-y-1">
543
+ <div>{t("panel.config.xAxisName")}</div>
544
+ <Input
545
+ className="h-7"
546
+ value={el.chart?.xAxisName ?? ""}
547
+ onChange={(e) =>
548
+ updateElement(el.id, {
549
+ chart: { ...(el.chart ?? {}), xAxisName: e.target.value },
550
+ })
551
+ }
552
+ />
553
+ </label>
554
+ <label className="block space-y-1">
555
+ <div>{t("panel.config.yAxisName")}</div>
556
+ <Input
557
+ className="h-7"
558
+ value={el.chart?.yAxisName ?? ""}
559
+ onChange={(e) =>
560
+ updateElement(el.id, {
561
+ chart: { ...(el.chart ?? {}), yAxisName: e.target.value },
562
+ })
563
+ }
564
+ />
565
+ </label>
566
+ <label className="block space-y-1">
567
+ <div>{t("panel.config.xAxisLabelColor")}</div>
568
+ <Input
569
+ className="h-7"
570
+ value={el.chart?.xAxisLabelColor ?? ""}
571
+ placeholder="#64748b"
572
+ onChange={(e) =>
573
+ updateElement(el.id, {
574
+ chart: { ...(el.chart ?? {}), xAxisLabelColor: e.target.value || undefined },
575
+ })
576
+ }
577
+ />
578
+ </label>
579
+ <label className="block space-y-1">
580
+ <div>{t("panel.config.yAxisLabelColor")}</div>
581
+ <Input
582
+ className="h-7"
583
+ value={el.chart?.yAxisLabelColor ?? ""}
584
+ placeholder="#64748b"
585
+ onChange={(e) =>
586
+ updateElement(el.id, {
587
+ chart: { ...(el.chart ?? {}), yAxisLabelColor: e.target.value || undefined },
588
+ })
589
+ }
590
+ />
591
+ </label>
592
+ <label className="block space-y-1">
593
+ <div>{t("panel.config.xAxisLabelFontSize")}</div>
594
+ <Input
595
+ className="h-7"
596
+ type="number"
597
+ min={8}
598
+ max={48}
599
+ value={el.chart?.xAxisLabelFontSize ?? 10}
600
+ onChange={(e) =>
601
+ updateElement(el.id, {
602
+ chart: {
603
+ ...(el.chart ?? {}),
604
+ xAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
605
+ },
606
+ })
607
+ }
608
+ />
609
+ </label>
610
+ <label className="block space-y-1">
611
+ <div>{t("panel.config.yAxisLabelFontSize")}</div>
612
+ <Input
613
+ className="h-7"
614
+ type="number"
615
+ min={8}
616
+ max={48}
617
+ value={el.chart?.yAxisLabelFontSize ?? 10}
618
+ onChange={(e) =>
619
+ updateElement(el.id, {
620
+ chart: {
621
+ ...(el.chart ?? {}),
622
+ yAxisLabelFontSize: Math.max(8, Math.min(48, Number(e.target.value) || 10)),
623
+ },
624
+ })
625
+ }
626
+ />
627
+ </label>
628
+ <label className="flex items-center gap-2">
629
+ <Checkbox
630
+ checked={el.chart?.xAxisLabelAutoEllipsis ?? false}
631
+ onCheckedChange={(checked) =>
632
+ updateElement(el.id, {
633
+ chart: { ...(el.chart ?? {}), xAxisLabelAutoEllipsis: checked === true },
634
+ })
635
+ }
636
+ />
637
+ <span>{t("panel.config.xAxisLabelAutoEllipsis")}</span>
638
+ </label>
639
+ <label className="flex items-center gap-2">
640
+ <Checkbox
641
+ checked={el.chart?.yAxisLabelAutoEllipsis ?? false}
642
+ onCheckedChange={(checked) =>
643
+ updateElement(el.id, {
644
+ chart: { ...(el.chart ?? {}), yAxisLabelAutoEllipsis: checked === true },
645
+ })
646
+ }
647
+ />
648
+ <span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
649
+ </label>
650
+ <label className="flex items-center gap-2">
651
+ <Checkbox
652
+ checked={el.chart?.xAxisTickShow ?? true}
653
+ onCheckedChange={(checked) =>
654
+ updateElement(el.id, {
655
+ chart: { ...(el.chart ?? {}), xAxisTickShow: checked === true },
656
+ })
657
+ }
658
+ />
659
+ <span>{t("panel.config.xAxisTick")}</span>
660
+ </label>
661
+ <label className="flex items-center gap-2">
662
+ <Checkbox
663
+ checked={el.chart?.yAxisTickShow ?? true}
664
+ onCheckedChange={(checked) =>
665
+ updateElement(el.id, {
666
+ chart: { ...(el.chart ?? {}), yAxisTickShow: checked === true },
667
+ })
668
+ }
669
+ />
670
+ <span>{t("panel.config.yAxisTick")}</span>
671
+ </label>
672
+ <label className="block space-y-1">
673
+ <div>{t("panel.config.xAxisTickColor")}</div>
674
+ <Input
675
+ className="h-7"
676
+ value={el.chart?.xAxisTickColor ?? ""}
677
+ placeholder="#94a3b8"
678
+ onChange={(e) =>
679
+ updateElement(el.id, {
680
+ chart: { ...(el.chart ?? {}), xAxisTickColor: e.target.value || undefined },
681
+ })
682
+ }
683
+ />
684
+ </label>
685
+ <label className="block space-y-1">
686
+ <div>{t("panel.config.yAxisTickColor")}</div>
687
+ <Input
688
+ className="h-7"
689
+ value={el.chart?.yAxisTickColor ?? ""}
690
+ placeholder="#94a3b8"
691
+ onChange={(e) =>
692
+ updateElement(el.id, {
693
+ chart: { ...(el.chart ?? {}), yAxisTickColor: e.target.value || undefined },
694
+ })
695
+ }
696
+ />
697
+ </label>
698
+ <label className="flex items-center gap-2">
699
+ <Checkbox
700
+ checked={el.chart?.xAxisSplitLineShow ?? false}
701
+ onCheckedChange={(checked) =>
702
+ updateElement(el.id, {
703
+ chart: { ...(el.chart ?? {}), xAxisSplitLineShow: checked === true },
704
+ })
705
+ }
706
+ />
707
+ <span>{t("panel.config.xAxisSplitLine")}</span>
708
+ </label>
709
+ <label className="flex items-center gap-2">
710
+ <Checkbox
711
+ checked={el.chart?.yAxisSplitLineShow ?? true}
712
+ onCheckedChange={(checked) =>
713
+ updateElement(el.id, {
714
+ chart: { ...(el.chart ?? {}), yAxisSplitLineShow: checked === true },
715
+ })
716
+ }
717
+ />
718
+ <span>{t("panel.config.yAxisSplitLine")}</span>
719
+ </label>
720
+ <label className="block space-y-1">
721
+ <div>{t("panel.config.xAxisSplitLineColor")}</div>
722
+ <Input
723
+ className="h-7"
724
+ value={el.chart?.xAxisSplitLineColor ?? ""}
725
+ placeholder="#e2e8f0"
726
+ onChange={(e) =>
727
+ updateElement(el.id, {
728
+ chart: { ...(el.chart ?? {}), xAxisSplitLineColor: e.target.value || undefined },
729
+ })
730
+ }
731
+ />
732
+ </label>
733
+ <label className="block space-y-1">
734
+ <div>{t("panel.config.yAxisSplitLineColor")}</div>
735
+ <Input
736
+ className="h-7"
737
+ value={el.chart?.yAxisSplitLineColor ?? ""}
738
+ placeholder="#e2e8f0"
739
+ onChange={(e) =>
740
+ updateElement(el.id, {
741
+ chart: { ...(el.chart ?? {}), yAxisSplitLineColor: e.target.value || undefined },
742
+ })
743
+ }
744
+ />
745
+ </label>
746
+ </>
747
+ ) : null}
748
+ {el.materialType === "gauge" ? (
749
+ <label className="block space-y-1">
750
+ <div>{t("panel.config.gaugeValue")}</div>
751
+ <Input
752
+ className="h-7"
753
+ type="number"
754
+ value={el.chart?.values?.[0] ?? 0}
755
+ onChange={(e) =>
756
+ updateElement(el.id, {
757
+ chart: {
758
+ ...(el.chart ?? {}),
759
+ values: [Number(e.target.value) || 0],
760
+ },
761
+ })
762
+ }
763
+ />
764
+ </label>
765
+ ) : null}
766
+ {el.materialType === "bar" ? (
767
+ <label className="block space-y-1">
768
+ <div>{t("panel.config.barWidth")}</div>
769
+ <Input
770
+ className="h-7"
771
+ type="number"
772
+ min={1}
773
+ placeholder={t("panel.config.barWidthAuto")}
774
+ value={el.chart?.barWidth ?? ""}
775
+ onChange={(e) => {
776
+ const raw = e.target.value.trim();
777
+ updateElement(el.id, {
778
+ chart: {
779
+ ...(el.chart ?? {}),
780
+ barWidth: raw
781
+ ? Math.max(1, Number(raw) || 1)
782
+ : undefined,
783
+ },
784
+ });
785
+ }}
786
+ />
787
+ </label>
788
+ ) : null}
789
+ {(el.materialType === "line" || el.materialType === "area") ? (
790
+ <label className="flex items-center gap-2">
791
+ <Checkbox
792
+ checked={el.chart?.smooth ?? true}
793
+ onCheckedChange={(checked) =>
794
+ updateElement(el.id, {
795
+ chart: { ...(el.chart ?? {}), smooth: checked === true },
796
+ })
797
+ }
798
+ />
799
+ <span>{t("panel.config.smooth")}</span>
800
+ </label>
801
+ ) : null}
802
+ {el.materialType === "pie" ? (
803
+ <>
804
+ <label className="block space-y-1">
805
+ <div>{t("panel.config.pieInnerRadius")}</div>
806
+ <Input
807
+ className="h-7"
808
+ type="number"
809
+ min={0}
810
+ max={99}
811
+ value={el.chart?.pieInnerRadius ?? 30}
812
+ onChange={(e) =>
813
+ updateElement(el.id, {
814
+ chart: {
815
+ ...(el.chart ?? {}),
816
+ pieInnerRadius: Math.max(0, Math.min(99, Number(e.target.value) || 0)),
817
+ },
818
+ })
819
+ }
820
+ />
821
+ </label>
822
+ <label className="block space-y-1">
823
+ <div>{t("panel.config.pieOuterRadius")}</div>
824
+ <Input
825
+ className="h-7"
826
+ type="number"
827
+ min={1}
828
+ max={100}
829
+ value={el.chart?.pieOuterRadius ?? 65}
830
+ onChange={(e) =>
831
+ updateElement(el.id, {
832
+ chart: {
833
+ ...(el.chart ?? {}),
834
+ pieOuterRadius: Math.max(1, Math.min(100, Number(e.target.value) || 1)),
835
+ },
836
+ })
837
+ }
838
+ />
839
+ </label>
840
+ </>
841
+ ) : null}
842
+ <label className="block space-y-1">
843
+ {renderOptionLabel(t("panel.config.gridLeft"), "grid.left", t("panel.config.gridLeftHint"))}
844
+ <Input
845
+ className="h-7"
846
+ type="number"
847
+ value={Number((el.chart?.option as any)?.grid?.left ?? 28)}
848
+ onChange={(e) =>
849
+ updateElement(el.id, {
850
+ chart: {
851
+ ...(el.chart ?? {}),
852
+ option: mergeOptionPatch(el.chart?.option as any, {
853
+ grid: { left: Number(e.target.value) || 0 },
854
+ }),
855
+ },
856
+ })
857
+ }
858
+ />
859
+ </label>
860
+ <label className="block space-y-1">
861
+ {renderOptionLabel(t("panel.config.gridRight"), "grid.right", t("panel.config.gridRightHint"))}
862
+ <Input
863
+ className="h-7"
864
+ type="number"
865
+ value={Number((el.chart?.option as any)?.grid?.right ?? 10)}
866
+ onChange={(e) =>
867
+ updateElement(el.id, {
868
+ chart: {
869
+ ...(el.chart ?? {}),
870
+ option: mergeOptionPatch(el.chart?.option as any, {
871
+ grid: { right: Number(e.target.value) || 0 },
872
+ }),
873
+ },
874
+ })
875
+ }
876
+ />
877
+ </label>
878
+ <label className="block space-y-1">
879
+ {renderOptionLabel(t("panel.config.gridTop"), "grid.top", t("panel.config.gridTopHint"))}
880
+ <Input
881
+ className="h-7"
882
+ type="number"
883
+ value={Number((el.chart?.option as any)?.grid?.top ?? 30)}
884
+ onChange={(e) =>
885
+ updateElement(el.id, {
886
+ chart: {
887
+ ...(el.chart ?? {}),
888
+ option: mergeOptionPatch(el.chart?.option as any, {
889
+ grid: { top: Number(e.target.value) || 0 },
890
+ }),
891
+ },
892
+ })
893
+ }
894
+ />
895
+ </label>
896
+ <label className="block space-y-1">
897
+ {renderOptionLabel(t("panel.config.gridBottom"), "grid.bottom", t("panel.config.gridBottomHint"))}
898
+ <Input
899
+ className="h-7"
900
+ type="number"
901
+ value={Number((el.chart?.option as any)?.grid?.bottom ?? 20)}
902
+ onChange={(e) =>
903
+ updateElement(el.id, {
904
+ chart: {
905
+ ...(el.chart ?? {}),
906
+ option: mergeOptionPatch(el.chart?.option as any, {
907
+ grid: { bottom: Number(e.target.value) || 0 },
908
+ }),
909
+ },
910
+ })
911
+ }
912
+ />
913
+ </label>
914
+ <label className="block space-y-1">
915
+ {renderOptionLabel(t("panel.config.legendPosition"), "legend.top", t("panel.config.legendPositionHint"))}
916
+ <Select
917
+ value={String((el.chart?.option as any)?.legend?.top ?? "top")}
918
+ onValueChange={(value) =>
919
+ updateElement(el.id, {
920
+ chart: {
921
+ ...(el.chart ?? {}),
922
+ option: mergeOptionPatch(el.chart?.option as any, {
923
+ legend: { top: value },
924
+ }),
925
+ },
926
+ })
927
+ }
928
+ >
929
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
930
+ <SelectContent>
931
+ <SelectItem value="top">top</SelectItem>
932
+ <SelectItem value="bottom">bottom</SelectItem>
933
+ <SelectItem value="left">left</SelectItem>
934
+ <SelectItem value="right">right</SelectItem>
935
+ </SelectContent>
936
+ </Select>
937
+ </label>
938
+ <label className="block space-y-1">
939
+ {renderOptionLabel(t("panel.config.legendOrient"), "legend.orient", t("panel.config.legendOrientHint"))}
940
+ <Select
941
+ value={String((el.chart?.option as any)?.legend?.orient ?? "horizontal")}
942
+ onValueChange={(value) =>
943
+ updateElement(el.id, {
944
+ chart: {
945
+ ...(el.chart ?? {}),
946
+ option: mergeOptionPatch(el.chart?.option as any, {
947
+ legend: { orient: value },
948
+ }),
949
+ },
950
+ })
951
+ }
952
+ >
953
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
954
+ <SelectContent>
955
+ <SelectItem value="horizontal">horizontal</SelectItem>
956
+ <SelectItem value="vertical">vertical</SelectItem>
957
+ </SelectContent>
958
+ </Select>
959
+ </label>
960
+ <label className="flex items-center gap-2">
961
+ <Checkbox
962
+ checked={Boolean((el.chart?.option as any)?.legend?.show ?? true)}
963
+ onCheckedChange={(checked) =>
964
+ updateElement(el.id, {
965
+ chart: {
966
+ ...(el.chart ?? {}),
967
+ option: mergeOptionPatch(el.chart?.option as any, {
968
+ legend: { show: checked === true },
969
+ }),
970
+ },
971
+ })
972
+ }
973
+ />
974
+ <span>{t("panel.config.showLegendWithKey")}</span>
975
+ </label>
976
+ <label className="flex items-center gap-2">
977
+ <Checkbox
978
+ checked={Boolean(
979
+ Array.isArray((el.chart?.option as any)?.dataZoom) &&
980
+ (el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "inside")
981
+ )}
982
+ onCheckedChange={(checked) => {
983
+ const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
984
+ ? [...(el.chart?.option as any).dataZoom]
985
+ : [];
986
+ const next = checked
987
+ ? [...prev.filter((z: any) => z?.type !== "inside"), { type: "inside" }]
988
+ : prev.filter((z: any) => z?.type !== "inside");
989
+ updateElement(el.id, {
990
+ chart: {
991
+ ...(el.chart ?? {}),
992
+ option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
993
+ },
994
+ });
995
+ }}
996
+ />
997
+ <span>{t("panel.config.zoomInsideWithKey")}</span>
998
+ </label>
999
+ <label className="flex items-center gap-2">
1000
+ <Checkbox
1001
+ checked={Boolean(
1002
+ Array.isArray((el.chart?.option as any)?.dataZoom) &&
1003
+ (el.chart?.option as any)?.dataZoom.some((z: any) => z?.type === "slider")
1004
+ )}
1005
+ onCheckedChange={(checked) => {
1006
+ const prev = Array.isArray((el.chart?.option as any)?.dataZoom)
1007
+ ? [...(el.chart?.option as any).dataZoom]
1008
+ : [];
1009
+ const next = checked
1010
+ ? [...prev.filter((z: any) => z?.type !== "slider"), { type: "slider" }]
1011
+ : prev.filter((z: any) => z?.type !== "slider");
1012
+ updateElement(el.id, {
1013
+ chart: {
1014
+ ...(el.chart ?? {}),
1015
+ option: mergeOptionPatch(el.chart?.option as any, { dataZoom: next }),
1016
+ },
1017
+ });
1018
+ }}
1019
+ />
1020
+ <span>{t("panel.config.zoomSliderWithKey")}</span>
1021
+ </label>
1022
+ <label className="flex items-center gap-2">
1023
+ <Checkbox
1024
+ checked={Boolean((el.chart?.option as any)?.axisPointer?.show ?? false)}
1025
+ onCheckedChange={(checked) =>
1026
+ updateElement(el.id, {
1027
+ chart: {
1028
+ ...(el.chart ?? {}),
1029
+ option: mergeOptionPatch(el.chart?.option as any, {
1030
+ axisPointer: { show: checked === true },
1031
+ }),
1032
+ },
1033
+ })
1034
+ }
1035
+ />
1036
+ <span>{t("panel.config.showAxisPointerWithKey")}</span>
1037
+ </label>
1038
+ <label className="block space-y-1">
1039
+ <div>{t("panel.config.axisPointerTypeWithKey")}</div>
1040
+ <Select
1041
+ value={String((el.chart?.option as any)?.axisPointer?.type ?? "line")}
1042
+ onValueChange={(value) =>
1043
+ updateElement(el.id, {
1044
+ chart: {
1045
+ ...(el.chart ?? {}),
1046
+ option: mergeOptionPatch(el.chart?.option as any, {
1047
+ axisPointer: { type: value },
1048
+ }),
1049
+ },
1050
+ })
1051
+ }
1052
+ >
1053
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1054
+ <SelectContent>
1055
+ <SelectItem value="line">line</SelectItem>
1056
+ <SelectItem value="shadow">shadow</SelectItem>
1057
+ <SelectItem value="cross">cross</SelectItem>
1058
+ </SelectContent>
1059
+ </Select>
1060
+ </label>
1061
+ <label className="block space-y-1 col-span-2">
1062
+ <div>{t("panel.config.advancedOptionJson")}</div>
1063
+ <Textarea
1064
+ className="h-28 font-mono text-[11px]"
1065
+ defaultValue={JSON.stringify(el.chart?.option ?? {}, null, 2)}
1066
+ placeholder={t("panel.config.advancedJsonPlaceholder")}
1067
+ onBlur={(e) => {
1068
+ const nextText = e.target.value.trim();
1069
+ if (!nextText) {
1070
+ updateElement(el.id, { chart: { ...(el.chart ?? {}), option: undefined } });
1071
+ return;
1072
+ }
1073
+ try {
1074
+ const parsed = JSON.parse(nextText) as Record<string, unknown>;
1075
+ updateElement(el.id, { chart: { ...(el.chart ?? {}), option: parsed } });
1076
+ } catch {
1077
+ // ignore invalid json input on blur
1078
+ }
1079
+ }}
1080
+ />
1081
+ </label>
1082
+ </div>
1083
+ ) : null}
1084
+ {el.materialType === "text" ? (
1085
+ <div className="grid grid-cols-2 gap-2">
1086
+ <label className="block space-y-1 col-span-2">
1087
+ <div>{t("panel.config.textHtml")}</div>
1088
+ <Textarea
1089
+ className="h-24"
1090
+ value={el.textHtml ?? ""}
1091
+ onChange={(e) => updateElement(el.id, { textHtml: e.target.value || "<p><br/></p>" })}
1092
+ />
1093
+ </label>
1094
+ <label className="block space-y-1">
1095
+ <div>{t("panel.config.fontSize")}</div>
1096
+ <Input
1097
+ className="h-7"
1098
+ type="number"
1099
+ min={8}
1100
+ value={el.textFontSize ?? 14}
1101
+ onChange={(e) => updateElement(el.id, { textFontSize: Math.max(8, Number(e.target.value) || 14) })}
1102
+ />
1103
+ </label>
1104
+ <label className="block space-y-1">
1105
+ <div>{t("panel.config.textColor")}</div>
1106
+ <Input
1107
+ className="h-7"
1108
+ value={el.textColor ?? ""}
1109
+ onChange={(e) => updateElement(el.id, { textColor: e.target.value || undefined })}
1110
+ />
1111
+ </label>
1112
+ </div>
1113
+ ) : null}
1114
+ {el.materialType === "audio" ? (
1115
+ <div className="grid grid-cols-2 gap-2">
1116
+ <label className="block space-y-1 col-span-2">
1117
+ <div>{t("panel.config.audioUrl")}</div>
1118
+ <Input
1119
+ className="h-7"
1120
+ value={el.audioRemoteUrl ?? ""}
1121
+ onChange={(e) => updateElement(el.id, { audioRemoteUrl: e.target.value || undefined })}
1122
+ />
1123
+ </label>
1124
+ <label className="block space-y-1">
1125
+ <div>{t("panel.config.effect")}</div>
1126
+ <Select
1127
+ value={el.audioVisualEffect ?? "pulse"}
1128
+ onValueChange={(value) =>
1129
+ updateElement(el.id, { audioVisualEffect: value as "none" | "pulse" | "ripple" })
1130
+ }
1131
+ >
1132
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1133
+ <SelectContent>
1134
+ <SelectItem value="none">none</SelectItem>
1135
+ <SelectItem value="pulse">pulse</SelectItem>
1136
+ <SelectItem value="ripple">ripple</SelectItem>
1137
+ </SelectContent>
1138
+ </Select>
1139
+ </label>
1140
+ <label className="block space-y-1">
1141
+ <div>{t("panel.config.speed")}</div>
1142
+ <Select
1143
+ value={el.audioVisualSpeed ?? "normal"}
1144
+ onValueChange={(value) =>
1145
+ updateElement(el.id, { audioVisualSpeed: value as "slow" | "normal" | "fast" })
1146
+ }
1147
+ >
1148
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1149
+ <SelectContent>
1150
+ <SelectItem value="slow">slow</SelectItem>
1151
+ <SelectItem value="normal">normal</SelectItem>
1152
+ <SelectItem value="fast">fast</SelectItem>
1153
+ </SelectContent>
1154
+ </Select>
1155
+ </label>
1156
+ </div>
1157
+ ) : null}
1158
+ {el.materialType === "video" ? (
1159
+ <label className="block space-y-1">
1160
+ <div>{t("panel.config.videoUrl")}</div>
1161
+ <Input
1162
+ className="h-7"
1163
+ value={el.videoRemoteUrl ?? ""}
1164
+ onChange={(e) => updateElement(el.id, { videoRemoteUrl: e.target.value || undefined })}
1165
+ />
1166
+ </label>
1167
+ ) : null}
1168
+ {el.materialType === "grid" ? (
1169
+ <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1170
+ <label className="block space-y-1.5">
1171
+ <div>{t("panel.config.rows")}</div>
1172
+ <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) })} />
1173
+ </label>
1174
+ <label className="block space-y-1.5">
1175
+ <div>{t("panel.config.cols")}</div>
1176
+ <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) })} />
1177
+ </label>
1178
+ </div>
1179
+ ) : null}
1180
+ {el.materialType === "geometry" ? (
1181
+ <div className="grid grid-cols-2 gap-2 rounded-lg border border-border/60 bg-muted/20 p-3">
1182
+ <label className="block space-y-1.5">
1183
+ <div>{t("panel.config.shape")}</div>
1184
+ <Select
1185
+ value={el.geometryShape ?? "rect"}
1186
+ onValueChange={(value) =>
1187
+ updateElement(el.id, { geometryShape: value as PanelElement["geometryShape"] })
1188
+ }
1189
+ >
1190
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1191
+ <SelectContent>
1192
+ <SelectItem value="rect">{t("panel.config.shapeRect")}</SelectItem>
1193
+ <SelectItem value="circle">{t("panel.config.shapeCircle")}</SelectItem>
1194
+ <SelectItem value="triangle">{t("panel.config.shapeTriangle")}</SelectItem>
1195
+ <SelectItem value="diamond">{t("panel.config.shapeDiamond")}</SelectItem>
1196
+ <SelectItem value="hexagon">{t("panel.config.shapeHexagon")}</SelectItem>
1197
+ <SelectItem value="star">{t("panel.config.shapeStar")}</SelectItem>
1198
+ <SelectItem value="heart">{t("panel.config.shapeHeart")}</SelectItem>
1199
+ </SelectContent>
1200
+ </Select>
1201
+ </label>
1202
+ <label className="block space-y-1.5">
1203
+ <div>{t("panel.config.color")}</div>
1204
+ <Input
1205
+ className="h-7"
1206
+ value={el.geometryColor ?? "#3b82f6"}
1207
+ onChange={(e) => updateElement(el.id, { geometryColor: e.target.value || "#3b82f6" })}
1208
+ />
1209
+ </label>
1210
+ </div>
1211
+ ) : null}
1212
+ {el.materialType === "reference" || el.materialType === "viewport" ? (
1213
+ <div className="grid grid-cols-2 gap-2">
1214
+ <label className="block space-y-1.5">
1215
+ <div>{t("panel.config.refLayer")}</div>
1216
+ <Select
1217
+ value={el.refLayerId ?? "__none__"}
1218
+ onValueChange={(value) => updateElement(el.id, { refLayerId: value === "__none__" ? undefined : value })}
1219
+ >
1220
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1221
+ <SelectContent>
1222
+ <SelectItem value="__none__">{t("common.none")}</SelectItem>
1223
+ {layers.filter((l) => l.id !== el.layerId).map((l) => (
1224
+ <SelectItem key={l.id} value={l.id}>{l.name}</SelectItem>
1225
+ ))}
1226
+ </SelectContent>
1227
+ </Select>
1228
+ </label>
1229
+ <label className="block space-y-1.5">
1230
+ <div>{t("panel.config.copy")}</div>
1231
+ <Select
1232
+ value={el.refCopyMode ?? "shallow"}
1233
+ onValueChange={(value) => setReferenceCopyMode?.(el.id, value as ReferenceCopyMode)}
1234
+ >
1235
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1236
+ <SelectContent>
1237
+ <SelectItem value="shallow">shallow</SelectItem>
1238
+ <SelectItem value="deep">deep</SelectItem>
1239
+ </SelectContent>
1240
+ </Select>
1241
+ </label>
1242
+ </div>
1243
+ ) : null}
1244
+ {el.materialType === "viewport" ? (
1245
+ <label className="block space-y-1.5">
1246
+ <div>{t("panel.config.viewportOverflow")}</div>
1247
+ <Select
1248
+ value={normalizeViewportOverflow(el.viewportOverflow)}
1249
+ onValueChange={(value) =>
1250
+ updateElement(el.id, {
1251
+ viewportOverflow: value as ViewportOverflowMode,
1252
+ })
1253
+ }
1254
+ >
1255
+ <SelectTrigger className="h-7"><SelectValue /></SelectTrigger>
1256
+ <SelectContent>
1257
+ {VIEWPORT_OVERFLOW_MODES.map((mode) => (
1258
+ <SelectItem key={mode} value={mode}>
1259
+ {mode === "scroll-x"
1260
+ ? t("panel.config.viewportOverflowScrollX")
1261
+ : mode === "scroll-y"
1262
+ ? t("panel.config.viewportOverflowScrollY")
1263
+ : mode === "scroll"
1264
+ ? t("panel.config.viewportOverflowScroll")
1265
+ : t("panel.config.viewportOverflowClip")}
1266
+ </SelectItem>
1267
+ ))}
1268
+ </SelectContent>
1269
+ </Select>
1270
+ </label>
1271
+ ) : null}
1272
+ </div>
1273
+ </fieldset>
1274
+ </CardContent>
1275
+ ) : null}
1276
+ </Card>
1277
+ ))}
1278
+ {hasSearch &&
1279
+ effectiveSelectedElements.every((el) => {
1280
+ const text = `${el.name ?? ""} ${el.id} ${el.materialType ?? ""} zIndex style layer`
1281
+ .toLowerCase();
1282
+ return !text.includes(normalizedSearch);
1283
+ }) ? (
1284
+ <div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
1285
+ {t("panel.config.noEditableMatch")}
1286
+ </div>
1287
+ ) : null}
1288
+ </div>
1289
+
1290
+ );
1291
+ }