@arronqzy/react-view 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/styles.css +96 -4
- package/package.json +8 -7
- package/src/panel/ReactViewOnlinePreview.tsx +72 -20
- package/src/panel/ReactViewPanel.tsx +238 -126
- package/src/panel/ai/applyAssistantAction.ts +1 -0
- package/src/panel/components/BusyOverlay.tsx +25 -0
- package/src/panel/components/ElementsLayer.tsx +110 -68
- package/src/panel/components/MaterialSidebar.tsx +15 -0
- package/src/panel/components/MoveableLayer.tsx +8 -0
- package/src/panel/components/PanelCanvas.tsx +3 -0
- package/src/panel/components/PanelConfigSidebar.tsx +582 -443
- package/src/panel/components/SelectLayer.tsx +5 -0
- package/src/panel/components/WorkspaceConfigSidebar.tsx +9 -0
- package/src/panel/components/WorkspaceProjectNav.tsx +27 -4
- package/src/panel/components/config/PanelConfigScene3dSection.tsx +874 -0
- package/src/panel/components/scope-config/ScopeTemplateAutocompleteHost.tsx +1 -2
- package/src/panel/components/table/PanelConfigTableSection.tsx +82 -50
- package/src/panel/components/table/TableNodeContent.tsx +40 -15
- package/src/panel/hooks/usePanelElements.ts +13 -1
- package/src/panel/hooks/useWorkspaceProjects.ts +133 -66
- package/src/panel/library/workspace-project-cache.ts +8 -2
- package/src/panel/library/workspace-project-db.ts +29 -11
- package/src/panel/library/workspace-snapshot.ts +21 -4
- package/src/panel/scope/view-scope-store.ts +8 -0
- package/src/panel/types.ts +4 -3
- package/src/panel/utils/async-work.ts +178 -0
- package/src/panel/utils/chartOptionBuilder.ts +7 -1
- package/src/panel/utils/gridPlacement.ts +29 -0
- package/src/panel/utils/outputScale.ts +69 -0
- package/src/panel/utils/panelElementDefaults.ts +37 -2
- package/src/panel/utils/scope-autocomplete.ts +0 -114
|
@@ -37,10 +37,10 @@ import { collectElementScopeWarnings } from "../utils/scope-template-warnings";
|
|
|
37
37
|
import { ScopeConfigProvider } from "./scope-config/ScopeConfigContext";
|
|
38
38
|
import { ScopeTemplateWarningsPanel } from "./scope-config/ScopeTemplateWarningsPanel";
|
|
39
39
|
import { PanelConfigTableSection } from "./table/PanelConfigTableSection";
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} from "../
|
|
40
|
+
import { cssTextLineHeight, cssTextAlignStyle } from "../utils/panelElementDefaults";
|
|
41
|
+
import { highlightConfigField, subscribeRevealPanelConfig } from "../ai/revealConfigField";
|
|
42
|
+
import { PanelConfigScene3dSection } from "./config/PanelConfigScene3dSection";
|
|
43
|
+
import { readFileAsDataUrl, runBusyTask } from "../utils/async-work";
|
|
44
44
|
|
|
45
45
|
type UpdateElement = (
|
|
46
46
|
id: string,
|
|
@@ -104,8 +104,8 @@ export function PanelConfigSidebar({
|
|
|
104
104
|
"scrollbar-thin [&::-webkit-scrollbar]:h-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-muted/40 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/80 [&::-webkit-scrollbar-thumb]:hover:bg-border";
|
|
105
105
|
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
|
|
106
106
|
nodeInfo: true,
|
|
107
|
-
styleBackground:
|
|
108
|
-
styleBorder:
|
|
107
|
+
styleBackground: false,
|
|
108
|
+
styleBorder: false,
|
|
109
109
|
chartBasic: true,
|
|
110
110
|
chartAdvanced: false,
|
|
111
111
|
chartAdvancedHighFreq: true,
|
|
@@ -262,31 +262,34 @@ export function PanelConfigSidebar({
|
|
|
262
262
|
const handleUploadBackgroundImage = useCallback(
|
|
263
263
|
async (file: File) => {
|
|
264
264
|
if (!selectedElement) return;
|
|
265
|
-
const base64 = await new Promise<string>((resolve, reject) => {
|
|
266
|
-
const reader = new FileReader();
|
|
267
|
-
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
268
|
-
reader.onerror = () => reject(new Error(messages.readImageFailed));
|
|
269
|
-
reader.readAsDataURL(file);
|
|
270
|
-
});
|
|
271
|
-
updateSelectedStyle({
|
|
272
|
-
backgroundImage: `url("${base64}")`,
|
|
273
|
-
});
|
|
274
|
-
setUploadStatus(t("panel.config.uploadWrittenBase64"));
|
|
275
265
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
266
|
+
await runBusyTask(t("common.uploadingFile"), async () => {
|
|
267
|
+
const base64 = await readFileAsDataUrl(file, messages.readImageFailed, "image");
|
|
268
|
+
updateSelectedStyle({
|
|
269
|
+
backgroundImage: `url("${base64}")`,
|
|
270
|
+
});
|
|
271
|
+
setUploadStatus(t("panel.config.uploadWrittenBase64"));
|
|
272
|
+
try {
|
|
273
|
+
const form = new FormData();
|
|
274
|
+
form.append("file", file);
|
|
275
|
+
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
276
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
277
|
+
const data = (await resp.json()) as { url?: string };
|
|
278
|
+
if (data.url) {
|
|
279
|
+
updateSelectedStyle({ backgroundImageRemoteUrl: data.url });
|
|
280
|
+
setUploadStatus(t("panel.config.uploadServerAndBase64"));
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
setUploadStatus(t("panel.config.uploadServerFailedKeepBase64"));
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
setUploadStatus(
|
|
288
|
+
error instanceof Error ? error.message : messages.readImageFailed
|
|
289
|
+
);
|
|
287
290
|
}
|
|
288
291
|
},
|
|
289
|
-
[selectedElement, updateSelectedStyle]
|
|
292
|
+
[messages.readImageFailed, selectedElement, t, updateSelectedStyle]
|
|
290
293
|
);
|
|
291
294
|
const updateSelectedAudio = useCallback(
|
|
292
295
|
(patch: Partial<PanelElement>) => {
|
|
@@ -298,43 +301,47 @@ export function PanelConfigSidebar({
|
|
|
298
301
|
const handleUploadAudioFile = useCallback(
|
|
299
302
|
async (file: File) => {
|
|
300
303
|
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
301
|
-
const base64 = await new Promise<string>((resolve, reject) => {
|
|
302
|
-
const reader = new FileReader();
|
|
303
|
-
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
304
|
-
reader.onerror = () => reject(new Error(messages.readAudioFailed));
|
|
305
|
-
reader.readAsDataURL(file);
|
|
306
|
-
});
|
|
307
|
-
updateSelectedAudio({ audioSrc: base64 });
|
|
308
|
-
setAudioStatus(messages.audioLocalSaved);
|
|
309
304
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
305
|
+
await runBusyTask(t("common.uploadingFile"), async () => {
|
|
306
|
+
const base64 = await readFileAsDataUrl(file, messages.readAudioFailed, "audio");
|
|
307
|
+
updateSelectedAudio({ audioSrc: base64 });
|
|
308
|
+
setAudioStatus(messages.audioLocalSaved);
|
|
309
|
+
try {
|
|
310
|
+
const form = new FormData();
|
|
311
|
+
form.append("file", file);
|
|
312
|
+
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
313
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
314
|
+
const data = (await resp.json()) as { url?: string };
|
|
315
|
+
if (data.url) {
|
|
316
|
+
updateSelectedAudio({ audioRemoteUrl: data.url });
|
|
317
|
+
setAudioStatus(messages.audioRemoteUploaded);
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
setAudioStatus(messages.audioServerUploadFailed);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
} catch (error) {
|
|
324
|
+
setAudioStatus(
|
|
325
|
+
error instanceof Error ? error.message : messages.readAudioFailed
|
|
326
|
+
);
|
|
321
327
|
}
|
|
322
328
|
},
|
|
323
|
-
[selectedElement, updateSelectedAudio]
|
|
329
|
+
[messages, selectedElement, t, updateSelectedAudio]
|
|
324
330
|
);
|
|
325
331
|
const handleUploadAudioPoster = useCallback(
|
|
326
332
|
async (file: File) => {
|
|
327
333
|
if (!selectedElement || selectedElement.materialType !== "audio") return;
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
334
|
+
try {
|
|
335
|
+
const base64 = await readFileAsDataUrl(file, messages.readImageFailed, "image");
|
|
336
|
+
updateSelectedAudio({ audioPosterImage: base64 });
|
|
337
|
+
setAudioStatus(messages.audioPosterSet);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
setAudioStatus(
|
|
340
|
+
error instanceof Error ? error.message : messages.readImageFailed
|
|
341
|
+
);
|
|
342
|
+
}
|
|
336
343
|
},
|
|
337
|
-
[selectedElement, updateSelectedAudio]
|
|
344
|
+
[messages, selectedElement, updateSelectedAudio]
|
|
338
345
|
);
|
|
339
346
|
const stopRecordingAudio = useCallback(() => {
|
|
340
347
|
recorderRef.current?.stop();
|
|
@@ -356,14 +363,15 @@ export function PanelConfigSidebar({
|
|
|
356
363
|
};
|
|
357
364
|
recorder.onstop = async () => {
|
|
358
365
|
const blob = new Blob(audioChunksRef.current, { type: recorder.mimeType || "audio/webm" });
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
366
|
+
try {
|
|
367
|
+
const dataUrl = await readFileAsDataUrl(blob, messages.readRecordAudioFailed, "audio");
|
|
368
|
+
updateSelectedAudio({ audioSrc: dataUrl });
|
|
369
|
+
setAudioStatus(messages.audioRecordSaved);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
setAudioStatus(
|
|
372
|
+
error instanceof Error ? error.message : messages.readRecordAudioFailed
|
|
373
|
+
);
|
|
374
|
+
}
|
|
367
375
|
recordStreamRef.current?.getTracks().forEach((track) => track.stop());
|
|
368
376
|
recordStreamRef.current = null;
|
|
369
377
|
recorderRef.current = null;
|
|
@@ -387,29 +395,32 @@ export function PanelConfigSidebar({
|
|
|
387
395
|
const handleUploadVideoFile = useCallback(
|
|
388
396
|
async (file: File) => {
|
|
389
397
|
if (!selectedElement || selectedElement.materialType !== "video") return;
|
|
390
|
-
const base64 = await new Promise<string>((resolve, reject) => {
|
|
391
|
-
const reader = new FileReader();
|
|
392
|
-
reader.onload = () => resolve(String(reader.result ?? ""));
|
|
393
|
-
reader.onerror = () => reject(new Error(messages.readVideoFailed));
|
|
394
|
-
reader.readAsDataURL(file);
|
|
395
|
-
});
|
|
396
|
-
updateSelectedVideo({ videoSrc: base64 });
|
|
397
|
-
setVideoStatus(messages.videoLocalSaved);
|
|
398
398
|
try {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
399
|
+
await runBusyTask(t("common.uploadingFile"), async () => {
|
|
400
|
+
const base64 = await readFileAsDataUrl(file, messages.readVideoFailed, "video");
|
|
401
|
+
updateSelectedVideo({ videoSrc: base64 });
|
|
402
|
+
setVideoStatus(messages.videoLocalSaved);
|
|
403
|
+
try {
|
|
404
|
+
const form = new FormData();
|
|
405
|
+
form.append("file", file);
|
|
406
|
+
const resp = await fetch("/api/upload", { method: "POST", body: form });
|
|
407
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
408
|
+
const data = (await resp.json()) as { url?: string };
|
|
409
|
+
if (data.url) {
|
|
410
|
+
updateSelectedVideo({ videoRemoteUrl: data.url });
|
|
411
|
+
setVideoStatus(messages.videoRemoteUploaded);
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
setVideoStatus(messages.videoServerUploadFailed);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
setVideoStatus(
|
|
419
|
+
error instanceof Error ? error.message : messages.readVideoFailed
|
|
420
|
+
);
|
|
410
421
|
}
|
|
411
422
|
},
|
|
412
|
-
[selectedElement, updateSelectedVideo]
|
|
423
|
+
[messages, selectedElement, t, updateSelectedVideo]
|
|
413
424
|
);
|
|
414
425
|
|
|
415
426
|
const isSectionExpanded = (key: string, defaultValue = true) =>
|
|
@@ -421,9 +432,21 @@ export function PanelConfigSidebar({
|
|
|
421
432
|
|
|
422
433
|
useEffect(() => {
|
|
423
434
|
return subscribeRevealPanelConfig((detail) => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
435
|
+
setExpandedSections((prev) => {
|
|
436
|
+
const next = { ...prev };
|
|
437
|
+
for (const section of detail.sections) next[section] = true;
|
|
438
|
+
next.nodeMore = true;
|
|
439
|
+
next.styleBgLayout = true;
|
|
440
|
+
next.chartDisplayMore = true;
|
|
441
|
+
next.chartTooltip = true;
|
|
442
|
+
next.chartAxes = true;
|
|
443
|
+
next.textInputAbility = true;
|
|
444
|
+
next.audioDisplayStyle = true;
|
|
445
|
+
next.geometryCanvasScript = true;
|
|
446
|
+
next.geometrySketch = true;
|
|
447
|
+
next.referenceCopyStrategy = true;
|
|
448
|
+
return next;
|
|
449
|
+
});
|
|
427
450
|
window.requestAnimationFrame(() => {
|
|
428
451
|
const field = detail.fields[0];
|
|
429
452
|
if (field) highlightConfigField(field);
|
|
@@ -475,16 +498,48 @@ export function PanelConfigSidebar({
|
|
|
475
498
|
const renderFieldGroup = (
|
|
476
499
|
title: string,
|
|
477
500
|
children: React.ReactNode,
|
|
478
|
-
hint?: React.ReactNode
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
501
|
+
hint?: React.ReactNode,
|
|
502
|
+
options?: { groupKey?: string; defaultOpen?: boolean }
|
|
503
|
+
) => {
|
|
504
|
+
const groupKey = options?.groupKey;
|
|
505
|
+
const defaultOpen = options?.defaultOpen ?? true;
|
|
506
|
+
const header = (
|
|
507
|
+
<>
|
|
482
508
|
<div className="text-[11px] font-semibold text-muted-foreground">{title}</div>
|
|
483
509
|
{hint ? <ConfigHintIcon label={title}>{hint}</ConfigHintIcon> : null}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
510
|
+
</>
|
|
511
|
+
);
|
|
512
|
+
if (!groupKey) {
|
|
513
|
+
return (
|
|
514
|
+
<div className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5">
|
|
515
|
+
<div className="flex items-center gap-1">{header}</div>
|
|
516
|
+
{children}
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const open = hasSearch ? true : isSectionExpanded(groupKey, defaultOpen);
|
|
521
|
+
return (
|
|
522
|
+
<Collapsible
|
|
523
|
+
open={open}
|
|
524
|
+
onOpenChange={(next) => setSectionExpanded(groupKey, next)}
|
|
525
|
+
className="space-y-2.5 rounded-lg border border-border/55 bg-background/80 p-2.5"
|
|
526
|
+
>
|
|
527
|
+
<div className="flex items-center gap-1.5">
|
|
528
|
+
<CollapsibleTrigger asChild>
|
|
529
|
+
<button
|
|
530
|
+
type="button"
|
|
531
|
+
className="flex h-5 w-5 items-center justify-center rounded text-[11px] hover:bg-accent"
|
|
532
|
+
aria-label={open ? t("panel.config.collapseGroup", { title }) : t("panel.config.expandGroup", { title })}
|
|
533
|
+
>
|
|
534
|
+
{open ? "▾" : "▸"}
|
|
535
|
+
</button>
|
|
536
|
+
</CollapsibleTrigger>
|
|
537
|
+
{header}
|
|
538
|
+
</div>
|
|
539
|
+
<CollapsibleContent className="space-y-2.5">{children}</CollapsibleContent>
|
|
540
|
+
</Collapsible>
|
|
541
|
+
);
|
|
542
|
+
};
|
|
488
543
|
const isNodeCardExpanded = useCallback(
|
|
489
544
|
(id: string) => expandedNodeCards[id] ?? true,
|
|
490
545
|
[expandedNodeCards]
|
|
@@ -1369,15 +1424,19 @@ export function PanelConfigSidebar({
|
|
|
1369
1424
|
className="h-7"
|
|
1370
1425
|
type="number"
|
|
1371
1426
|
min={1}
|
|
1372
|
-
|
|
1373
|
-
|
|
1427
|
+
placeholder={t("panel.config.barWidthAuto")}
|
|
1428
|
+
value={el.chart?.barWidth ?? ""}
|
|
1429
|
+
onChange={(e) => {
|
|
1430
|
+
const raw = e.target.value.trim();
|
|
1374
1431
|
updateElement(el.id, {
|
|
1375
1432
|
chart: {
|
|
1376
1433
|
...(el.chart ?? {}),
|
|
1377
|
-
barWidth:
|
|
1434
|
+
barWidth: raw
|
|
1435
|
+
? Math.max(1, Number(raw) || 1)
|
|
1436
|
+
: undefined,
|
|
1378
1437
|
},
|
|
1379
|
-
})
|
|
1380
|
-
}
|
|
1438
|
+
});
|
|
1439
|
+
}}
|
|
1381
1440
|
/>
|
|
1382
1441
|
</label>
|
|
1383
1442
|
) : null}
|
|
@@ -1879,21 +1938,6 @@ export function PanelConfigSidebar({
|
|
|
1879
1938
|
{readonlyReason}
|
|
1880
1939
|
</div>
|
|
1881
1940
|
) : null}
|
|
1882
|
-
<div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
|
|
1883
|
-
<label className="flex items-center gap-2">
|
|
1884
|
-
<Checkbox
|
|
1885
|
-
checked={selectedElement.locked === true}
|
|
1886
|
-
disabled={!canToggleNodeLock}
|
|
1887
|
-
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
1888
|
-
onCheckedChange={(checked) =>
|
|
1889
|
-
updateElement(selectedElement.id, {
|
|
1890
|
-
locked: checked === true,
|
|
1891
|
-
})
|
|
1892
|
-
}
|
|
1893
|
-
/>
|
|
1894
|
-
<span>{t("panel.config.lockNode")}</span>
|
|
1895
|
-
</label>
|
|
1896
|
-
</div>
|
|
1897
1941
|
<fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
|
|
1898
1942
|
<div className="space-y-3.5 text-xs">
|
|
1899
1943
|
{renderSection(
|
|
@@ -1913,7 +1957,7 @@ export function PanelConfigSidebar({
|
|
|
1913
1957
|
className="h-7"
|
|
1914
1958
|
/>
|
|
1915
1959
|
</label>
|
|
1916
|
-
<div className="grid grid-cols-
|
|
1960
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1917
1961
|
<label className="block space-y-1" data-config-field="x">
|
|
1918
1962
|
<div>X</div>
|
|
1919
1963
|
<Input
|
|
@@ -1941,20 +1985,7 @@ export function PanelConfigSidebar({
|
|
|
1941
1985
|
/>
|
|
1942
1986
|
</label>
|
|
1943
1987
|
</div>
|
|
1944
|
-
<div className="grid grid-cols-
|
|
1945
|
-
<label className="block space-y-1" data-config-field="rotate">
|
|
1946
|
-
<div>{t("panel.config.rotate")}</div>
|
|
1947
|
-
<Input
|
|
1948
|
-
type="number"
|
|
1949
|
-
value={selectedElement.rotate ?? 0}
|
|
1950
|
-
onChange={(e) =>
|
|
1951
|
-
updateElement(selectedElement.id, {
|
|
1952
|
-
rotate: Number(e.target.value) || 0,
|
|
1953
|
-
})
|
|
1954
|
-
}
|
|
1955
|
-
className="h-7"
|
|
1956
|
-
/>
|
|
1957
|
-
</label>
|
|
1988
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1958
1989
|
<label className="block space-y-1" data-config-field="width">
|
|
1959
1990
|
<div>{t("panel.config.width")}</div>
|
|
1960
1991
|
<Input
|
|
@@ -1985,6 +2016,24 @@ export function PanelConfigSidebar({
|
|
|
1985
2016
|
/>
|
|
1986
2017
|
</label>
|
|
1987
2018
|
</div>
|
|
2019
|
+
{renderFieldGroup(
|
|
2020
|
+
t("panel.config.groupNodeMore"),
|
|
2021
|
+
<>
|
|
2022
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2023
|
+
<label className="block space-y-1" data-config-field="rotate">
|
|
2024
|
+
<div>{t("panel.config.rotate")}</div>
|
|
2025
|
+
<Input
|
|
2026
|
+
type="number"
|
|
2027
|
+
value={selectedElement.rotate ?? 0}
|
|
2028
|
+
onChange={(e) =>
|
|
2029
|
+
updateElement(selectedElement.id, {
|
|
2030
|
+
rotate: Number(e.target.value) || 0,
|
|
2031
|
+
})
|
|
2032
|
+
}
|
|
2033
|
+
className="h-7"
|
|
2034
|
+
/>
|
|
2035
|
+
</label>
|
|
2036
|
+
</div>
|
|
1988
2037
|
<div className="space-y-1.5">
|
|
1989
2038
|
<div className="text-[11px] text-muted-foreground">{t("panel.config.nodeZOrder")}</div>
|
|
1990
2039
|
<div className="text-[11px] text-muted-foreground/90">
|
|
@@ -2023,205 +2072,177 @@ export function PanelConfigSidebar({
|
|
|
2023
2072
|
</div>
|
|
2024
2073
|
<div className="truncate text-muted-foreground">ID: {selectedElement.id}</div>
|
|
2025
2074
|
<div className="text-muted-foreground">{t("panel.config.type")}: {selectedElement.materialType ?? selectedElement.id}</div>
|
|
2075
|
+
</>,
|
|
2076
|
+
undefined,
|
|
2077
|
+
{ groupKey: "nodeMore", defaultOpen: false }
|
|
2078
|
+
)}
|
|
2026
2079
|
</>,
|
|
2027
2080
|
true,
|
|
2028
|
-
[t("panel.config.name"), "id", t("panel.config.type"), t("panel.layers.lockShort"), "locked", "name"]
|
|
2081
|
+
[t("panel.config.name"), "id", t("panel.config.type"), t("panel.config.rotate"), t("panel.config.nodeZOrder"), t("panel.layers.lockShort"), "locked", "name"]
|
|
2029
2082
|
)}
|
|
2083
|
+
</div>
|
|
2084
|
+
</fieldset>
|
|
2085
|
+
<div className="rounded-lg border border-border/60 bg-card/80 px-2.5 py-2 text-xs">
|
|
2086
|
+
<label className="flex items-center gap-2">
|
|
2087
|
+
<Checkbox
|
|
2088
|
+
checked={selectedElement.locked === true}
|
|
2089
|
+
disabled={!canToggleNodeLock}
|
|
2090
|
+
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"
|
|
2091
|
+
onCheckedChange={(checked) =>
|
|
2092
|
+
updateElement(selectedElement.id, {
|
|
2093
|
+
locked: checked === true,
|
|
2094
|
+
})
|
|
2095
|
+
}
|
|
2096
|
+
/>
|
|
2097
|
+
<span>{t("panel.config.lockNode")}</span>
|
|
2098
|
+
</label>
|
|
2099
|
+
</div>
|
|
2100
|
+
<fieldset disabled={!isNodeEditable} className={!isNodeEditable ? "opacity-60" : ""}>
|
|
2101
|
+
<div className="space-y-3.5 text-xs">
|
|
2030
2102
|
|
|
2031
|
-
{
|
|
2032
|
-
"styleBackground",
|
|
2033
|
-
t("panel.config.sectionStyleBackground"),
|
|
2103
|
+
{isChartElement ? (
|
|
2034
2104
|
<>
|
|
2035
|
-
{
|
|
2036
|
-
|
|
2105
|
+
{renderSection(
|
|
2106
|
+
"chartBasic",
|
|
2107
|
+
t("panel.config.sectionChartBasic"),
|
|
2037
2108
|
<>
|
|
2038
|
-
{
|
|
2039
|
-
t("panel.config.
|
|
2040
|
-
|
|
2041
|
-
|
|
2109
|
+
{renderFieldGroup(
|
|
2110
|
+
t("panel.config.groupBasicDisplay"),
|
|
2111
|
+
<>
|
|
2112
|
+
<label className="block space-y-1.5" data-config-field="chart.title">
|
|
2113
|
+
<div>{t("panel.config.title")}</div>
|
|
2114
|
+
<Input
|
|
2115
|
+
value={selectedElement.chart?.title ?? ""}
|
|
2116
|
+
onChange={(e) => updateSelectedChart({ title: e.target.value })}
|
|
2117
|
+
className="h-7"
|
|
2118
|
+
/>
|
|
2119
|
+
</label>
|
|
2120
|
+
|
|
2121
|
+
{renderColorField(
|
|
2122
|
+
t("panel.config.primaryColor"),
|
|
2123
|
+
selectedElement.chart?.color ?? "#3b82f6",
|
|
2124
|
+
(next) => updateSelectedChart({ color: next || "#3b82f6" })
|
|
2125
|
+
)}
|
|
2126
|
+
</>
|
|
2042
2127
|
)}
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
className="hidden"
|
|
2059
|
-
onChange={async (e) => {
|
|
2060
|
-
const file = e.target.files?.[0];
|
|
2061
|
-
e.currentTarget.value = "";
|
|
2062
|
-
if (!file) return;
|
|
2063
|
-
await handleUploadBackgroundImage(file);
|
|
2064
|
-
}}
|
|
2065
|
-
/>
|
|
2066
|
-
</label>
|
|
2067
|
-
{uploadStatus ? (
|
|
2068
|
-
<span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
|
|
2069
|
-
) : null}
|
|
2070
|
-
</div>
|
|
2071
|
-
</>
|
|
2072
|
-
)}
|
|
2073
|
-
{renderFieldGroup(
|
|
2074
|
-
t("panel.config.groupBgLayout"),
|
|
2075
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2076
|
-
<label className="block space-y-1">
|
|
2077
|
-
<div>{t("panel.config.backgroundSize")}</div>
|
|
2078
|
-
<Select
|
|
2079
|
-
value={selectedElement.style?.backgroundSize ?? "__none__"}
|
|
2080
|
-
onValueChange={(value) =>
|
|
2081
|
-
updateSelectedStyle({
|
|
2082
|
-
backgroundSize: value === "__none__" ? undefined : value,
|
|
2083
|
-
})
|
|
2084
|
-
}
|
|
2085
|
-
>
|
|
2086
|
-
<SelectTrigger className="h-7">
|
|
2087
|
-
<SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
|
|
2088
|
-
</SelectTrigger>
|
|
2089
|
-
<SelectContent>
|
|
2090
|
-
<SelectItem value="__none__">{t("common.default")}</SelectItem>
|
|
2091
|
-
<SelectItem value="cover">cover</SelectItem>
|
|
2092
|
-
<SelectItem value="contain">contain</SelectItem>
|
|
2093
|
-
<SelectItem value="100% 100%">100% 100%</SelectItem>
|
|
2094
|
-
<SelectItem value="auto">auto</SelectItem>
|
|
2095
|
-
</SelectContent>
|
|
2096
|
-
</Select>
|
|
2097
|
-
</label>
|
|
2098
|
-
<label className="block space-y-1">
|
|
2099
|
-
<div>{t("panel.config.backgroundPosition")}</div>
|
|
2100
|
-
<Select
|
|
2101
|
-
value={selectedElement.style?.backgroundPosition ?? "__none__"}
|
|
2102
|
-
onValueChange={(value) =>
|
|
2103
|
-
updateSelectedStyle({
|
|
2104
|
-
backgroundPosition: value === "__none__" ? undefined : value,
|
|
2105
|
-
})
|
|
2106
|
-
}
|
|
2107
|
-
>
|
|
2108
|
-
<SelectTrigger className="h-7">
|
|
2109
|
-
<SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
|
|
2110
|
-
</SelectTrigger>
|
|
2111
|
-
<SelectContent>
|
|
2112
|
-
<SelectItem value="__none__">{t("common.default")}</SelectItem>
|
|
2113
|
-
<SelectItem value="center">center</SelectItem>
|
|
2114
|
-
<SelectItem value="top">top</SelectItem>
|
|
2115
|
-
<SelectItem value="bottom">bottom</SelectItem>
|
|
2116
|
-
<SelectItem value="left">left</SelectItem>
|
|
2117
|
-
<SelectItem value="right">right</SelectItem>
|
|
2118
|
-
<SelectItem value="top left">top left</SelectItem>
|
|
2119
|
-
<SelectItem value="top right">top right</SelectItem>
|
|
2120
|
-
<SelectItem value="bottom left">bottom left</SelectItem>
|
|
2121
|
-
<SelectItem value="bottom right">bottom right</SelectItem>
|
|
2122
|
-
</SelectContent>
|
|
2123
|
-
</Select>
|
|
2124
|
-
</label>
|
|
2125
|
-
</div>
|
|
2126
|
-
)}
|
|
2127
|
-
</>,
|
|
2128
|
-
true,
|
|
2129
|
-
[t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
|
|
2130
|
-
)}
|
|
2128
|
+
{renderFieldGroup(
|
|
2129
|
+
t("panel.config.groupData"),
|
|
2130
|
+
<>
|
|
2131
|
+
<label className="block space-y-1">
|
|
2132
|
+
<div>{t("panel.config.labelsCsv")}</div>
|
|
2133
|
+
<Input
|
|
2134
|
+
value={getChartLabelsDisplayText(selectedElement.chart)}
|
|
2135
|
+
onChange={(e) =>
|
|
2136
|
+
updateSelectedChart({
|
|
2137
|
+
labelsText: e.target.value,
|
|
2138
|
+
})
|
|
2139
|
+
}
|
|
2140
|
+
className="h-7"
|
|
2141
|
+
/>
|
|
2142
|
+
</label>
|
|
2131
2143
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
t("panel.config.sectionStyleBorder"),
|
|
2135
|
-
<>
|
|
2136
|
-
{renderFieldGroup(
|
|
2137
|
-
t("panel.config.groupBorderGeometry"),
|
|
2138
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2139
|
-
<label className="block space-y-1">
|
|
2140
|
-
<div>{t("panel.config.borderWidth")}</div>
|
|
2141
|
-
<Input
|
|
2142
|
-
type="number"
|
|
2143
|
-
min={0}
|
|
2144
|
-
value={selectedElement.style?.borderWidth ?? 0}
|
|
2145
|
-
onChange={(e) =>
|
|
2146
|
-
updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
|
|
2147
|
-
}
|
|
2148
|
-
className="h-7"
|
|
2149
|
-
/>
|
|
2150
|
-
</label>
|
|
2151
|
-
<label className="block space-y-1">
|
|
2152
|
-
<div>{t("panel.config.borderRadius")}</div>
|
|
2153
|
-
<Input
|
|
2154
|
-
type="number"
|
|
2155
|
-
min={0}
|
|
2156
|
-
value={selectedElement.style?.borderRadius ?? 0}
|
|
2157
|
-
onChange={(e) =>
|
|
2158
|
-
updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
|
|
2159
|
-
}
|
|
2160
|
-
className="h-7"
|
|
2161
|
-
/>
|
|
2162
|
-
</label>
|
|
2163
|
-
</div>
|
|
2164
|
-
)}
|
|
2165
|
-
{renderFieldGroup(
|
|
2166
|
-
t("panel.config.groupBorderVisual"),
|
|
2167
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2168
|
-
<label className="block space-y-1">
|
|
2169
|
-
<div>{t("panel.config.borderStyle")}</div>
|
|
2170
|
-
<Select
|
|
2171
|
-
value={selectedElement.style?.borderStyle ?? "solid"}
|
|
2172
|
-
onValueChange={(value) =>
|
|
2173
|
-
updateSelectedStyle({
|
|
2174
|
-
borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
|
|
2175
|
-
})
|
|
2176
|
-
}
|
|
2177
|
-
>
|
|
2178
|
-
<SelectTrigger className="h-7">
|
|
2179
|
-
<SelectValue placeholder={t("panel.config.selectBorderStyle")} />
|
|
2180
|
-
</SelectTrigger>
|
|
2181
|
-
<SelectContent>
|
|
2182
|
-
<SelectItem value="none">none</SelectItem>
|
|
2183
|
-
<SelectItem value="solid">solid</SelectItem>
|
|
2184
|
-
<SelectItem value="dashed">dashed</SelectItem>
|
|
2185
|
-
<SelectItem value="dotted">dotted</SelectItem>
|
|
2186
|
-
<SelectItem value="double">double</SelectItem>
|
|
2187
|
-
</SelectContent>
|
|
2188
|
-
</Select>
|
|
2189
|
-
</label>
|
|
2190
|
-
{renderColorField(
|
|
2191
|
-
t("panel.config.borderColor"),
|
|
2192
|
-
selectedElement.style?.borderColor ?? "",
|
|
2193
|
-
(next) => updateSelectedStyle({ borderColor: next || undefined })
|
|
2194
|
-
)}
|
|
2195
|
-
</div>
|
|
2196
|
-
)}
|
|
2197
|
-
</>,
|
|
2198
|
-
true,
|
|
2199
|
-
[t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
|
|
2200
|
-
)}
|
|
2201
|
-
|
|
2202
|
-
{isChartElement ? (
|
|
2203
|
-
<>
|
|
2204
|
-
{renderSection(
|
|
2205
|
-
"chartBasic",
|
|
2206
|
-
t("panel.config.sectionChartBasic"),
|
|
2207
|
-
<>
|
|
2208
|
-
{renderFieldGroup(
|
|
2209
|
-
t("panel.config.groupBasicDisplay"),
|
|
2210
|
-
<>
|
|
2211
|
-
<label className="block space-y-1.5" data-config-field="chart.title">
|
|
2212
|
-
<div>{t("panel.config.title")}</div>
|
|
2144
|
+
<label className="block space-y-1">
|
|
2145
|
+
<div>{t("panel.config.valuesCsv")}</div>
|
|
2213
2146
|
<Input
|
|
2214
|
-
value={selectedElement.chart
|
|
2215
|
-
onChange={(e) =>
|
|
2147
|
+
value={getChartValuesDisplayText(selectedElement.chart)}
|
|
2148
|
+
onChange={(e) =>
|
|
2149
|
+
updateSelectedChart({
|
|
2150
|
+
valuesText: e.target.value,
|
|
2151
|
+
})
|
|
2152
|
+
}
|
|
2216
2153
|
className="h-7"
|
|
2217
2154
|
/>
|
|
2218
2155
|
</label>
|
|
2156
|
+
</>
|
|
2157
|
+
)}
|
|
2219
2158
|
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2159
|
+
{selectedChartType === "bar" ||
|
|
2160
|
+
selectedChartType === "line" ||
|
|
2161
|
+
selectedChartType === "area" ||
|
|
2162
|
+
selectedChartType === "pie" ? (
|
|
2163
|
+
renderFieldGroup(
|
|
2164
|
+
t("panel.config.groupSeries"),
|
|
2165
|
+
<>
|
|
2166
|
+
{selectedChartType === "bar" ? (
|
|
2167
|
+
<label className="block space-y-1">
|
|
2168
|
+
<div>{t("panel.config.barWidthPx")}</div>
|
|
2169
|
+
<Input
|
|
2170
|
+
type="number"
|
|
2171
|
+
min={1}
|
|
2172
|
+
placeholder={t("panel.config.barWidthAuto")}
|
|
2173
|
+
value={selectedElement.chart?.barWidth ?? ""}
|
|
2174
|
+
onChange={(e) => {
|
|
2175
|
+
const raw = e.target.value.trim();
|
|
2176
|
+
updateSelectedChart({
|
|
2177
|
+
barWidth: raw
|
|
2178
|
+
? Math.max(1, Number(raw) || 1)
|
|
2179
|
+
: undefined,
|
|
2180
|
+
});
|
|
2181
|
+
}}
|
|
2182
|
+
className="h-7"
|
|
2183
|
+
/>
|
|
2184
|
+
</label>
|
|
2185
|
+
) : null}
|
|
2186
|
+
|
|
2187
|
+
{selectedChartType === "line" || selectedChartType === "area" ? (
|
|
2188
|
+
<label className="flex items-center gap-2">
|
|
2189
|
+
<Checkbox
|
|
2190
|
+
checked={selectedElement.chart?.smooth ?? true}
|
|
2191
|
+
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"
|
|
2192
|
+
onCheckedChange={(checked) =>
|
|
2193
|
+
updateSelectedChart({ smooth: checked === true })
|
|
2194
|
+
}
|
|
2195
|
+
/>
|
|
2196
|
+
<span>{t("panel.config.smooth")}</span>
|
|
2197
|
+
</label>
|
|
2198
|
+
) : null}
|
|
2199
|
+
|
|
2200
|
+
{selectedChartType === "pie" ? (
|
|
2201
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2202
|
+
<label className="block space-y-1">
|
|
2203
|
+
<div>{t("panel.config.pieInnerRadiusPct")}</div>
|
|
2204
|
+
<Input
|
|
2205
|
+
type="number"
|
|
2206
|
+
min={0}
|
|
2207
|
+
max={99}
|
|
2208
|
+
value={selectedElement.chart?.pieInnerRadius ?? 30}
|
|
2209
|
+
onChange={(e) =>
|
|
2210
|
+
updateSelectedChart({
|
|
2211
|
+
pieInnerRadius: Math.max(
|
|
2212
|
+
0,
|
|
2213
|
+
Math.min(99, Number(e.target.value) || 0)
|
|
2214
|
+
),
|
|
2215
|
+
})
|
|
2216
|
+
}
|
|
2217
|
+
className="h-7"
|
|
2218
|
+
/>
|
|
2219
|
+
</label>
|
|
2220
|
+
<label className="block space-y-1">
|
|
2221
|
+
<div>{t("panel.config.pieOuterRadiusPct")}</div>
|
|
2222
|
+
<Input
|
|
2223
|
+
type="number"
|
|
2224
|
+
min={1}
|
|
2225
|
+
max={100}
|
|
2226
|
+
value={selectedElement.chart?.pieOuterRadius ?? 65}
|
|
2227
|
+
onChange={(e) =>
|
|
2228
|
+
updateSelectedChart({
|
|
2229
|
+
pieOuterRadius: Math.max(
|
|
2230
|
+
1,
|
|
2231
|
+
Math.min(100, Number(e.target.value) || 1)
|
|
2232
|
+
),
|
|
2233
|
+
})
|
|
2234
|
+
}
|
|
2235
|
+
className="h-7"
|
|
2236
|
+
/>
|
|
2237
|
+
</label>
|
|
2238
|
+
</div>
|
|
2239
|
+
) : null}
|
|
2240
|
+
</>
|
|
2241
|
+
)
|
|
2242
|
+
) : null}
|
|
2243
|
+
{renderFieldGroup(
|
|
2244
|
+
t("panel.config.groupChartDisplayMore"),
|
|
2245
|
+
<>
|
|
2225
2246
|
<label className="flex items-center gap-2">
|
|
2226
2247
|
<Checkbox
|
|
2227
2248
|
checked={selectedElement.chart?.colorMode === "gradient"}
|
|
@@ -2304,7 +2325,9 @@ export function PanelConfigSidebar({
|
|
|
2304
2325
|
</SelectContent>
|
|
2305
2326
|
</Select>
|
|
2306
2327
|
</label>
|
|
2307
|
-
|
|
2328
|
+
</>,
|
|
2329
|
+
undefined,
|
|
2330
|
+
{ groupKey: "chartDisplayMore", defaultOpen: false }
|
|
2308
2331
|
)}
|
|
2309
2332
|
{renderFieldGroup(
|
|
2310
2333
|
t("panel.config.groupTooltip"),
|
|
@@ -2361,39 +2384,10 @@ export function PanelConfigSidebar({
|
|
|
2361
2384
|
className="h-7"
|
|
2362
2385
|
/>
|
|
2363
2386
|
</label>
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
t("panel.config.groupData"),
|
|
2368
|
-
<>
|
|
2369
|
-
<label className="block space-y-1">
|
|
2370
|
-
<div>{t("panel.config.labelsCsv")}</div>
|
|
2371
|
-
<Input
|
|
2372
|
-
value={getChartLabelsDisplayText(selectedElement.chart)}
|
|
2373
|
-
onChange={(e) =>
|
|
2374
|
-
updateSelectedChart({
|
|
2375
|
-
labelsText: e.target.value,
|
|
2376
|
-
})
|
|
2377
|
-
}
|
|
2378
|
-
className="h-7"
|
|
2379
|
-
/>
|
|
2380
|
-
</label>
|
|
2381
|
-
|
|
2382
|
-
<label className="block space-y-1">
|
|
2383
|
-
<div>{t("panel.config.valuesCsv")}</div>
|
|
2384
|
-
<Input
|
|
2385
|
-
value={getChartValuesDisplayText(selectedElement.chart)}
|
|
2386
|
-
onChange={(e) =>
|
|
2387
|
-
updateSelectedChart({
|
|
2388
|
-
valuesText: e.target.value,
|
|
2389
|
-
})
|
|
2390
|
-
}
|
|
2391
|
-
className="h-7"
|
|
2392
|
-
/>
|
|
2393
|
-
</label>
|
|
2394
|
-
</>
|
|
2387
|
+
</>,
|
|
2388
|
+
undefined,
|
|
2389
|
+
{ groupKey: "chartTooltip", defaultOpen: false }
|
|
2395
2390
|
)}
|
|
2396
|
-
|
|
2397
2391
|
{selectedChartType === "bar" ||
|
|
2398
2392
|
selectedChartType === "line" ||
|
|
2399
2393
|
selectedChartType === "area" ||
|
|
@@ -2555,88 +2549,12 @@ export function PanelConfigSidebar({
|
|
|
2555
2549
|
<span>{t("panel.config.yAxisLabelAutoEllipsis")}</span>
|
|
2556
2550
|
</label>
|
|
2557
2551
|
</div>
|
|
2558
|
-
|
|
2552
|
+
</>,
|
|
2553
|
+
undefined,
|
|
2554
|
+
{ groupKey: "chartAxes", defaultOpen: false }
|
|
2559
2555
|
)
|
|
2560
2556
|
) : null}
|
|
2561
2557
|
|
|
2562
|
-
{selectedChartType === "bar" ||
|
|
2563
|
-
selectedChartType === "line" ||
|
|
2564
|
-
selectedChartType === "area" ||
|
|
2565
|
-
selectedChartType === "pie" ? (
|
|
2566
|
-
renderFieldGroup(
|
|
2567
|
-
t("panel.config.groupSeries"),
|
|
2568
|
-
<>
|
|
2569
|
-
{selectedChartType === "bar" ? (
|
|
2570
|
-
<label className="block space-y-1">
|
|
2571
|
-
<div>{t("panel.config.barWidthPx")}</div>
|
|
2572
|
-
<Input
|
|
2573
|
-
type="number"
|
|
2574
|
-
min={1}
|
|
2575
|
-
value={selectedElement.chart?.barWidth ?? 24}
|
|
2576
|
-
onChange={(e) =>
|
|
2577
|
-
updateSelectedChart({ barWidth: Math.max(1, Number(e.target.value) || 1) })
|
|
2578
|
-
}
|
|
2579
|
-
className="h-7"
|
|
2580
|
-
/>
|
|
2581
|
-
</label>
|
|
2582
|
-
) : null}
|
|
2583
|
-
|
|
2584
|
-
{selectedChartType === "line" || selectedChartType === "area" ? (
|
|
2585
|
-
<label className="flex items-center gap-2">
|
|
2586
|
-
<Checkbox
|
|
2587
|
-
checked={selectedElement.chart?.smooth ?? true}
|
|
2588
|
-
className="h-4 w-4 border-2 border-foreground/80 bg-background ring-1 ring-foreground/40 data-[state=checked]:border-primary data-[state=checked]:ring-primary/40"
|
|
2589
|
-
onCheckedChange={(checked) =>
|
|
2590
|
-
updateSelectedChart({ smooth: checked === true })
|
|
2591
|
-
}
|
|
2592
|
-
/>
|
|
2593
|
-
<span>{t("panel.config.smooth")}</span>
|
|
2594
|
-
</label>
|
|
2595
|
-
) : null}
|
|
2596
|
-
|
|
2597
|
-
{selectedChartType === "pie" ? (
|
|
2598
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2599
|
-
<label className="block space-y-1">
|
|
2600
|
-
<div>{t("panel.config.pieInnerRadiusPct")}</div>
|
|
2601
|
-
<Input
|
|
2602
|
-
type="number"
|
|
2603
|
-
min={0}
|
|
2604
|
-
max={99}
|
|
2605
|
-
value={selectedElement.chart?.pieInnerRadius ?? 30}
|
|
2606
|
-
onChange={(e) =>
|
|
2607
|
-
updateSelectedChart({
|
|
2608
|
-
pieInnerRadius: Math.max(
|
|
2609
|
-
0,
|
|
2610
|
-
Math.min(99, Number(e.target.value) || 0)
|
|
2611
|
-
),
|
|
2612
|
-
})
|
|
2613
|
-
}
|
|
2614
|
-
className="h-7"
|
|
2615
|
-
/>
|
|
2616
|
-
</label>
|
|
2617
|
-
<label className="block space-y-1">
|
|
2618
|
-
<div>{t("panel.config.pieOuterRadiusPct")}</div>
|
|
2619
|
-
<Input
|
|
2620
|
-
type="number"
|
|
2621
|
-
min={1}
|
|
2622
|
-
max={100}
|
|
2623
|
-
value={selectedElement.chart?.pieOuterRadius ?? 65}
|
|
2624
|
-
onChange={(e) =>
|
|
2625
|
-
updateSelectedChart({
|
|
2626
|
-
pieOuterRadius: Math.max(
|
|
2627
|
-
1,
|
|
2628
|
-
Math.min(100, Number(e.target.value) || 1)
|
|
2629
|
-
),
|
|
2630
|
-
})
|
|
2631
|
-
}
|
|
2632
|
-
className="h-7"
|
|
2633
|
-
/>
|
|
2634
|
-
</label>
|
|
2635
|
-
</div>
|
|
2636
|
-
) : null}
|
|
2637
|
-
</>
|
|
2638
|
-
)
|
|
2639
|
-
) : null}
|
|
2640
2558
|
</>,
|
|
2641
2559
|
true,
|
|
2642
2560
|
[
|
|
@@ -2941,7 +2859,8 @@ export function PanelConfigSidebar({
|
|
|
2941
2859
|
<div
|
|
2942
2860
|
ref={textEditorRef}
|
|
2943
2861
|
data-config-field="textHtml"
|
|
2944
|
-
|
|
2862
|
+
data-panel-user-text=""
|
|
2863
|
+
className="min-h-[120px] rounded border border-border bg-background px-2 py-1.5 leading-6 outline-none"
|
|
2945
2864
|
style={{
|
|
2946
2865
|
fontFamily: selectedElement.textFontFamily || undefined,
|
|
2947
2866
|
fontSize: selectedElement.textFontSize
|
|
@@ -2949,10 +2868,8 @@ export function PanelConfigSidebar({
|
|
|
2949
2868
|
: undefined,
|
|
2950
2869
|
fontWeight: selectedElement.textFontWeight || undefined,
|
|
2951
2870
|
color: selectedElement.textColor || undefined,
|
|
2952
|
-
lineHeight: selectedElement.textLineHeight
|
|
2953
|
-
|
|
2954
|
-
: undefined,
|
|
2955
|
-
textAlign: selectedElement.textAlign ?? "left",
|
|
2871
|
+
lineHeight: cssTextLineHeight(selectedElement.textLineHeight),
|
|
2872
|
+
...cssTextAlignStyle(selectedElement.textAlign),
|
|
2956
2873
|
}}
|
|
2957
2874
|
contentEditable
|
|
2958
2875
|
suppressContentEditableWarning
|
|
@@ -3028,7 +2945,7 @@ export function PanelConfigSidebar({
|
|
|
3028
2945
|
value={selectedElement.textAlign ?? "left"}
|
|
3029
2946
|
onValueChange={(value) =>
|
|
3030
2947
|
updateSelectedText({
|
|
3031
|
-
textAlign: value as "
|
|
2948
|
+
textAlign: value as PanelElement["textAlign"],
|
|
3032
2949
|
})
|
|
3033
2950
|
}
|
|
3034
2951
|
>
|
|
@@ -3038,19 +2955,40 @@ export function PanelConfigSidebar({
|
|
|
3038
2955
|
<SelectContent>
|
|
3039
2956
|
<SelectItem value="left">{t("panel.config.alignLeft")}</SelectItem>
|
|
3040
2957
|
<SelectItem value="center">{t("panel.config.alignCenter")}</SelectItem>
|
|
2958
|
+
<SelectItem value="middle">{t("panel.config.alignMiddle")}</SelectItem>
|
|
3041
2959
|
<SelectItem value="right">{t("panel.config.alignRight")}</SelectItem>
|
|
3042
2960
|
<SelectItem value="justify">{t("panel.config.alignJustify")}</SelectItem>
|
|
3043
2961
|
</SelectContent>
|
|
3044
2962
|
</Select>
|
|
3045
2963
|
</label>
|
|
3046
2964
|
<label className="block space-y-1">
|
|
3047
|
-
<div
|
|
2965
|
+
<div className="flex items-center justify-between gap-2">
|
|
2966
|
+
<span>{t("panel.config.lineHeight")}</span>
|
|
2967
|
+
<label className="flex items-center gap-1 font-normal">
|
|
2968
|
+
<Checkbox
|
|
2969
|
+
checked={selectedElement.textLineHeight === "auto"}
|
|
2970
|
+
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"
|
|
2971
|
+
onCheckedChange={(checked) =>
|
|
2972
|
+
updateSelectedText({
|
|
2973
|
+
textLineHeight: checked === true ? "auto" : 1.6,
|
|
2974
|
+
})
|
|
2975
|
+
}
|
|
2976
|
+
/>
|
|
2977
|
+
<span>{t("panel.config.lineHeightAuto")}</span>
|
|
2978
|
+
</label>
|
|
2979
|
+
</div>
|
|
3048
2980
|
<Input
|
|
3049
2981
|
type="number"
|
|
3050
2982
|
min={1}
|
|
3051
2983
|
max={3}
|
|
3052
2984
|
step={0.1}
|
|
3053
|
-
|
|
2985
|
+
disabled={selectedElement.textLineHeight === "auto"}
|
|
2986
|
+
placeholder={t("panel.config.lineHeightAuto")}
|
|
2987
|
+
value={
|
|
2988
|
+
selectedElement.textLineHeight === "auto"
|
|
2989
|
+
? ""
|
|
2990
|
+
: (selectedElement.textLineHeight ?? 1.6)
|
|
2991
|
+
}
|
|
3054
2992
|
onChange={(e) =>
|
|
3055
2993
|
updateSelectedText({
|
|
3056
2994
|
textLineHeight: Math.min(
|
|
@@ -3086,7 +3024,9 @@ export function PanelConfigSidebar({
|
|
|
3086
3024
|
}
|
|
3087
3025
|
/>
|
|
3088
3026
|
<span>{t("panel.config.allowCanvasInput")}</span>
|
|
3089
|
-
</label
|
|
3027
|
+
</label>,
|
|
3028
|
+
undefined,
|
|
3029
|
+
{ groupKey: "textInputAbility", defaultOpen: false }
|
|
3090
3030
|
)}
|
|
3091
3031
|
</>,
|
|
3092
3032
|
true,
|
|
@@ -3269,7 +3209,9 @@ export function PanelConfigSidebar({
|
|
|
3269
3209
|
{t("panel.config.audioPosterHint")}
|
|
3270
3210
|
</ConfigHintIcon>
|
|
3271
3211
|
</div>
|
|
3272
|
-
|
|
3212
|
+
</>,
|
|
3213
|
+
undefined,
|
|
3214
|
+
{ groupKey: "audioDisplayStyle", defaultOpen: false }
|
|
3273
3215
|
)}
|
|
3274
3216
|
</>,
|
|
3275
3217
|
true,
|
|
@@ -3442,7 +3384,8 @@ export function PanelConfigSidebar({
|
|
|
3442
3384
|
</>,
|
|
3443
3385
|
<>
|
|
3444
3386
|
{t("panel.config.canvasScriptHint")}
|
|
3445
|
-
|
|
3387
|
+
</>,
|
|
3388
|
+
{ groupKey: "geometryCanvasScript", defaultOpen: false }
|
|
3446
3389
|
)}
|
|
3447
3390
|
{renderFieldGroup(
|
|
3448
3391
|
t("panel.config.groupSketchOverlay"),
|
|
@@ -3539,12 +3482,32 @@ export function PanelConfigSidebar({
|
|
|
3539
3482
|
{t("panel.config.clearSketch")}
|
|
3540
3483
|
</button>
|
|
3541
3484
|
</div>
|
|
3542
|
-
|
|
3485
|
+
</>,
|
|
3486
|
+
undefined,
|
|
3487
|
+
{ groupKey: "geometrySketch", defaultOpen: false }
|
|
3543
3488
|
)}
|
|
3544
3489
|
</>,
|
|
3545
3490
|
true,
|
|
3546
3491
|
[t("panel.material.geometry"), "geometry", t("panel.config.shape"), "canvas", t("panel.config.searchKwScript"), t("panel.config.searchKwSketch")]
|
|
3547
3492
|
)
|
|
3493
|
+
) : selectedElement.materialType === "scene3d" ? (
|
|
3494
|
+
renderSection(
|
|
3495
|
+
"scene3dConfig",
|
|
3496
|
+
t("panel.config.sectionScene3d"),
|
|
3497
|
+
<PanelConfigScene3dSection
|
|
3498
|
+
element={selectedElement}
|
|
3499
|
+
updateElement={updateElement}
|
|
3500
|
+
/>,
|
|
3501
|
+
true,
|
|
3502
|
+
[
|
|
3503
|
+
t("panel.material.scene3d"),
|
|
3504
|
+
"3d",
|
|
3505
|
+
"glb",
|
|
3506
|
+
t("panel.config.scene3dUploadModels"),
|
|
3507
|
+
t("panel.config.groupScene3dCamera"),
|
|
3508
|
+
t("panel.config.groupScene3dModelAnim"),
|
|
3509
|
+
]
|
|
3510
|
+
)
|
|
3548
3511
|
) : selectedElement.materialType === "grid" ? (
|
|
3549
3512
|
renderSection(
|
|
3550
3513
|
"gridConfig",
|
|
@@ -3699,17 +3662,193 @@ export function PanelConfigSidebar({
|
|
|
3699
3662
|
</>,
|
|
3700
3663
|
<>
|
|
3701
3664
|
{t("panel.config.copyStrategyHint")}
|
|
3702
|
-
|
|
3665
|
+
</>,
|
|
3666
|
+
{ groupKey: "referenceCopyStrategy", defaultOpen: false }
|
|
3703
3667
|
)}
|
|
3704
3668
|
</>,
|
|
3705
3669
|
true,
|
|
3706
3670
|
[t("panel.config.searchKwRef"), "ref", t("panel.config.layer"), t("panel.material.shallowCopy"), t("panel.material.deepCopy"), "snapshot"]
|
|
3707
3671
|
)
|
|
3708
|
-
) : (
|
|
3672
|
+
) : selectedElement.materialType === "image" ? null : (
|
|
3709
3673
|
<div className="text-xs leading-6 text-muted-foreground">
|
|
3710
3674
|
{t("panel.config.notChartType")}
|
|
3711
3675
|
</div>
|
|
3712
3676
|
)}
|
|
3677
|
+
{renderSection(
|
|
3678
|
+
"styleBackground",
|
|
3679
|
+
t("panel.config.sectionStyleBackground"),
|
|
3680
|
+
<>
|
|
3681
|
+
{renderFieldGroup(
|
|
3682
|
+
t("panel.config.groupBgFill"),
|
|
3683
|
+
<>
|
|
3684
|
+
{renderColorField(
|
|
3685
|
+
t("panel.config.backgroundColor"),
|
|
3686
|
+
selectedElement.style?.backgroundColor ?? "",
|
|
3687
|
+
(next) => updateSelectedStyle({ backgroundColor: next || undefined })
|
|
3688
|
+
)}
|
|
3689
|
+
</>
|
|
3690
|
+
)}
|
|
3691
|
+
{renderFieldGroup(
|
|
3692
|
+
t("panel.config.groupBgLayout"),
|
|
3693
|
+
<>
|
|
3694
|
+
<label className="block space-y-1">
|
|
3695
|
+
<div>{t("panel.config.backgroundImage")}</div>
|
|
3696
|
+
<Input
|
|
3697
|
+
value={selectedElement.style?.backgroundImage ?? ""}
|
|
3698
|
+
onChange={(e) => updateSelectedStyle({ backgroundImage: e.target.value || undefined })}
|
|
3699
|
+
placeholder='url("https://...") / linear-gradient(...)'
|
|
3700
|
+
className="h-7"
|
|
3701
|
+
/>
|
|
3702
|
+
</label>
|
|
3703
|
+
<div className="flex items-center gap-2">
|
|
3704
|
+
<label className="inline-flex cursor-pointer items-center rounded border border-border px-2 py-1 text-[11px] hover:bg-accent">
|
|
3705
|
+
{t("panel.config.uploadImage")}
|
|
3706
|
+
<Input
|
|
3707
|
+
type="file"
|
|
3708
|
+
accept="image/*"
|
|
3709
|
+
className="hidden"
|
|
3710
|
+
onChange={async (e) => {
|
|
3711
|
+
const file = e.target.files?.[0];
|
|
3712
|
+
e.currentTarget.value = "";
|
|
3713
|
+
if (!file) return;
|
|
3714
|
+
await handleUploadBackgroundImage(file);
|
|
3715
|
+
}}
|
|
3716
|
+
/>
|
|
3717
|
+
</label>
|
|
3718
|
+
{uploadStatus ? (
|
|
3719
|
+
<span className="text-[11px] text-muted-foreground">{uploadStatus}</span>
|
|
3720
|
+
) : null}
|
|
3721
|
+
</div>
|
|
3722
|
+
<div className="grid grid-cols-2 gap-2">
|
|
3723
|
+
<label className="block space-y-1">
|
|
3724
|
+
<div>{t("panel.config.backgroundSize")}</div>
|
|
3725
|
+
<Select
|
|
3726
|
+
value={selectedElement.style?.backgroundSize ?? "__none__"}
|
|
3727
|
+
onValueChange={(value) =>
|
|
3728
|
+
updateSelectedStyle({
|
|
3729
|
+
backgroundSize: value === "__none__" ? undefined : value,
|
|
3730
|
+
})
|
|
3731
|
+
}
|
|
3732
|
+
>
|
|
3733
|
+
<SelectTrigger className="h-7">
|
|
3734
|
+
<SelectValue placeholder={t("panel.config.selectBackgroundSize")} />
|
|
3735
|
+
</SelectTrigger>
|
|
3736
|
+
<SelectContent>
|
|
3737
|
+
<SelectItem value="__none__">{t("common.default")}</SelectItem>
|
|
3738
|
+
<SelectItem value="cover">cover</SelectItem>
|
|
3739
|
+
<SelectItem value="contain">contain</SelectItem>
|
|
3740
|
+
<SelectItem value="100% 100%">100% 100%</SelectItem>
|
|
3741
|
+
<SelectItem value="auto">auto</SelectItem>
|
|
3742
|
+
</SelectContent>
|
|
3743
|
+
</Select>
|
|
3744
|
+
</label>
|
|
3745
|
+
<label className="block space-y-1">
|
|
3746
|
+
<div>{t("panel.config.backgroundPosition")}</div>
|
|
3747
|
+
<Select
|
|
3748
|
+
value={selectedElement.style?.backgroundPosition ?? "__none__"}
|
|
3749
|
+
onValueChange={(value) =>
|
|
3750
|
+
updateSelectedStyle({
|
|
3751
|
+
backgroundPosition: value === "__none__" ? undefined : value,
|
|
3752
|
+
})
|
|
3753
|
+
}
|
|
3754
|
+
>
|
|
3755
|
+
<SelectTrigger className="h-7">
|
|
3756
|
+
<SelectValue placeholder={t("panel.config.selectBackgroundPosition")} />
|
|
3757
|
+
</SelectTrigger>
|
|
3758
|
+
<SelectContent>
|
|
3759
|
+
<SelectItem value="__none__">{t("common.default")}</SelectItem>
|
|
3760
|
+
<SelectItem value="center">center</SelectItem>
|
|
3761
|
+
<SelectItem value="top">top</SelectItem>
|
|
3762
|
+
<SelectItem value="bottom">bottom</SelectItem>
|
|
3763
|
+
<SelectItem value="left">left</SelectItem>
|
|
3764
|
+
<SelectItem value="right">right</SelectItem>
|
|
3765
|
+
<SelectItem value="top left">top left</SelectItem>
|
|
3766
|
+
<SelectItem value="top right">top right</SelectItem>
|
|
3767
|
+
<SelectItem value="bottom left">bottom left</SelectItem>
|
|
3768
|
+
<SelectItem value="bottom right">bottom right</SelectItem>
|
|
3769
|
+
</SelectContent>
|
|
3770
|
+
</Select>
|
|
3771
|
+
</label>
|
|
3772
|
+
</div>
|
|
3773
|
+
</>,
|
|
3774
|
+
undefined,
|
|
3775
|
+
{ groupKey: "styleBgLayout", defaultOpen: false }
|
|
3776
|
+
)}
|
|
3777
|
+
</>,
|
|
3778
|
+
false,
|
|
3779
|
+
[t("panel.config.backgroundColor"), t("panel.config.backgroundImage"), "background", "backgroundSize", "backgroundPosition", t("panel.config.searchKwLayout")]
|
|
3780
|
+
)}
|
|
3781
|
+
|
|
3782
|
+
{renderSection(
|
|
3783
|
+
"styleBorder",
|
|
3784
|
+
t("panel.config.sectionStyleBorder"),
|
|
3785
|
+
<>
|
|
3786
|
+
{renderFieldGroup(
|
|
3787
|
+
t("panel.config.groupBorderGeometry"),
|
|
3788
|
+
<div className="grid grid-cols-2 gap-2">
|
|
3789
|
+
<label className="block space-y-1">
|
|
3790
|
+
<div>{t("panel.config.borderWidth")}</div>
|
|
3791
|
+
<Input
|
|
3792
|
+
type="number"
|
|
3793
|
+
min={0}
|
|
3794
|
+
value={selectedElement.style?.borderWidth ?? 0}
|
|
3795
|
+
onChange={(e) =>
|
|
3796
|
+
updateSelectedStyle({ borderWidth: Math.max(0, Number(e.target.value) || 0) })
|
|
3797
|
+
}
|
|
3798
|
+
className="h-7"
|
|
3799
|
+
/>
|
|
3800
|
+
</label>
|
|
3801
|
+
<label className="block space-y-1">
|
|
3802
|
+
<div>{t("panel.config.borderRadius")}</div>
|
|
3803
|
+
<Input
|
|
3804
|
+
type="number"
|
|
3805
|
+
min={0}
|
|
3806
|
+
value={selectedElement.style?.borderRadius ?? 0}
|
|
3807
|
+
onChange={(e) =>
|
|
3808
|
+
updateSelectedStyle({ borderRadius: Math.max(0, Number(e.target.value) || 0) })
|
|
3809
|
+
}
|
|
3810
|
+
className="h-7"
|
|
3811
|
+
/>
|
|
3812
|
+
</label>
|
|
3813
|
+
</div>
|
|
3814
|
+
)}
|
|
3815
|
+
{renderFieldGroup(
|
|
3816
|
+
t("panel.config.groupBorderVisual"),
|
|
3817
|
+
<div className="grid grid-cols-2 gap-2">
|
|
3818
|
+
<label className="block space-y-1">
|
|
3819
|
+
<div>{t("panel.config.borderStyle")}</div>
|
|
3820
|
+
<Select
|
|
3821
|
+
value={selectedElement.style?.borderStyle ?? "solid"}
|
|
3822
|
+
onValueChange={(value) =>
|
|
3823
|
+
updateSelectedStyle({
|
|
3824
|
+
borderStyle: value as NonNullable<PanelElementStyle["borderStyle"]>,
|
|
3825
|
+
})
|
|
3826
|
+
}
|
|
3827
|
+
>
|
|
3828
|
+
<SelectTrigger className="h-7">
|
|
3829
|
+
<SelectValue placeholder={t("panel.config.selectBorderStyle")} />
|
|
3830
|
+
</SelectTrigger>
|
|
3831
|
+
<SelectContent>
|
|
3832
|
+
<SelectItem value="none">none</SelectItem>
|
|
3833
|
+
<SelectItem value="solid">solid</SelectItem>
|
|
3834
|
+
<SelectItem value="dashed">dashed</SelectItem>
|
|
3835
|
+
<SelectItem value="dotted">dotted</SelectItem>
|
|
3836
|
+
<SelectItem value="double">double</SelectItem>
|
|
3837
|
+
</SelectContent>
|
|
3838
|
+
</Select>
|
|
3839
|
+
</label>
|
|
3840
|
+
{renderColorField(
|
|
3841
|
+
t("panel.config.borderColor"),
|
|
3842
|
+
selectedElement.style?.borderColor ?? "",
|
|
3843
|
+
(next) => updateSelectedStyle({ borderColor: next || undefined })
|
|
3844
|
+
)}
|
|
3845
|
+
</div>
|
|
3846
|
+
)}
|
|
3847
|
+
</>,
|
|
3848
|
+
false,
|
|
3849
|
+
[t("panel.config.searchKwBorder"), "border", t("panel.config.searchKwWidth"), t("panel.config.searchKwRadius"), t("panel.config.color"), t("panel.config.searchKwStyle")]
|
|
3850
|
+
)}
|
|
3851
|
+
|
|
3713
3852
|
{hasSearch && renderedSectionCount === 0 ? (
|
|
3714
3853
|
<div className="rounded border border-border/60 bg-background px-2 py-1.5 text-[11px] text-muted-foreground">
|
|
3715
3854
|
{t("panel.config.noMatch")}
|