@arronqzy/react-view 1.0.3 → 1.0.7
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 +555 -15
- package/package.json +14 -7
- package/readme.md +74 -5
- package/src/core/template.tsx +12 -2
- package/src/panel/ReactViewOnlinePreview.tsx +18 -8
- package/src/panel/ReactViewPanel.tsx +403 -177
- package/src/panel/ai/AssistantChatPanel.tsx +895 -0
- package/src/panel/ai/AssistantMarkdown.tsx +256 -0
- package/src/panel/ai/applyAssistantAction.ts +652 -0
- package/src/panel/ai/revealConfigField.ts +137 -0
- package/src/panel/components/ConfigHintIcon.tsx +50 -0
- package/src/panel/components/ElementsLayer.tsx +39 -22
- package/src/panel/components/MaterialSidebar.tsx +157 -109
- package/src/panel/components/MoveableLayer.tsx +75 -61
- package/src/panel/components/PanelCanvas.tsx +81 -64
- package/src/panel/components/PanelConfigSidebar.tsx +471 -448
- package/src/panel/components/SelectLayer.tsx +1 -0
- package/src/panel/components/ViewElementScopePanel.tsx +13 -12
- package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
- package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
- package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
- package/src/panel/components/table/ConditionEditor.tsx +205 -0
- package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
- package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
- package/src/panel/components/table/TableNodeContent.tsx +544 -0
- package/src/panel/constants/messages.ts +76 -34
- package/src/panel/hooks/usePanelElements.ts +64 -52
- package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
- package/src/panel/types.ts +6 -0
- package/src/panel/utils/chartOptionBuilder.ts +12 -1
- package/src/panel/utils/panelElementDefaults.ts +69 -39
- package/src/panel/utils/scope-field-labels.ts +51 -37
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +128 -0
- package/src/panel/utils/scope-template-spread.ts +252 -0
- package/src/panel/utils/scope-template-warnings.ts +131 -11
- package/src/panel/utils/scope-template.ts +105 -3
- package/src/tailwind.css +36 -15
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Map panel.update patch keys → config field anchors / sidebar sections. */
|
|
2
|
+
|
|
3
|
+
export type ConfigRevealTarget = {
|
|
4
|
+
field: string;
|
|
5
|
+
section: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const FIELD_SECTION: Record<string, string> = {
|
|
9
|
+
name: "nodeInfo",
|
|
10
|
+
x: "nodeInfo",
|
|
11
|
+
y: "nodeInfo",
|
|
12
|
+
width: "nodeInfo",
|
|
13
|
+
height: "nodeInfo",
|
|
14
|
+
rotate: "nodeInfo",
|
|
15
|
+
locked: "nodeInfo",
|
|
16
|
+
zIndex: "nodeInfo",
|
|
17
|
+
layerId: "nodeInfo",
|
|
18
|
+
textHtml: "textConfig",
|
|
19
|
+
textColor: "textConfig",
|
|
20
|
+
textFontSize: "textConfig",
|
|
21
|
+
textFontFamily: "textConfig",
|
|
22
|
+
textFontWeight: "textConfig",
|
|
23
|
+
textAlign: "textConfig",
|
|
24
|
+
textLineHeight: "textConfig",
|
|
25
|
+
table: "tableConfig",
|
|
26
|
+
"table.source": "tableConfig",
|
|
27
|
+
"table.rowsText": "tableConfig",
|
|
28
|
+
"table.columns": "tableConfig",
|
|
29
|
+
"table.emptyText": "tableConfig",
|
|
30
|
+
"table.showHeader": "tableConfig",
|
|
31
|
+
"table.stripe": "tableConfig",
|
|
32
|
+
"table.rowHeight": "tableConfig",
|
|
33
|
+
chart: "chartBasic",
|
|
34
|
+
"chart.title": "chartBasic",
|
|
35
|
+
"chart.labelsText": "chartBasic",
|
|
36
|
+
"chart.valuesText": "chartBasic",
|
|
37
|
+
"chart.color": "chartBasic",
|
|
38
|
+
style: "nodeInfo",
|
|
39
|
+
"style.backgroundColor": "nodeInfo",
|
|
40
|
+
"style.borderColor": "nodeInfo",
|
|
41
|
+
geometryShape: "geometryConfig",
|
|
42
|
+
geometryColor: "geometryConfig",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function configTargetsFromPatch(
|
|
46
|
+
patch: Record<string, unknown>
|
|
47
|
+
): ConfigRevealTarget[] {
|
|
48
|
+
const out: ConfigRevealTarget[] = [];
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
|
|
51
|
+
const push = (field: string) => {
|
|
52
|
+
const section = FIELD_SECTION[field] ?? FIELD_SECTION[field.split(".")[0]!] ?? "nodeInfo";
|
|
53
|
+
if (seen.has(field)) return;
|
|
54
|
+
seen.add(field);
|
|
55
|
+
out.push({ field, section });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (const key of Object.keys(patch)) {
|
|
59
|
+
if (key === "table" && patch.table && typeof patch.table === "object") {
|
|
60
|
+
for (const sub of Object.keys(patch.table as object)) {
|
|
61
|
+
push(`table.${sub}`);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (key === "chart" && patch.chart && typeof patch.chart === "object") {
|
|
66
|
+
for (const sub of Object.keys(patch.chart as object)) {
|
|
67
|
+
push(`chart.${sub}`);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (key === "style" && patch.style && typeof patch.style === "object") {
|
|
72
|
+
for (const sub of Object.keys(patch.style as object)) {
|
|
73
|
+
push(`style.${sub}`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
push(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const REVEAL_EVENT = "abuilder:reveal-config";
|
|
84
|
+
|
|
85
|
+
export type RevealConfigDetail = {
|
|
86
|
+
fields: string[];
|
|
87
|
+
sections: string[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function revealPanelConfig(targets: ConfigRevealTarget[]) {
|
|
91
|
+
if (typeof window === "undefined" || !targets.length) return;
|
|
92
|
+
const detail: RevealConfigDetail = {
|
|
93
|
+
fields: targets.map((t) => t.field),
|
|
94
|
+
sections: [...new Set(targets.map((t) => t.section))],
|
|
95
|
+
};
|
|
96
|
+
window.dispatchEvent(new CustomEvent(REVEAL_EVENT, { detail }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function revealPanelConfigFromPatch(patch: Record<string, unknown>) {
|
|
100
|
+
revealPanelConfig(configTargetsFromPatch(patch));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function subscribeRevealPanelConfig(
|
|
104
|
+
handler: (detail: RevealConfigDetail) => void
|
|
105
|
+
): () => void {
|
|
106
|
+
const listener = (event: Event) => {
|
|
107
|
+
const detail = (event as CustomEvent<RevealConfigDetail>).detail;
|
|
108
|
+
if (!detail) return;
|
|
109
|
+
handler(detail);
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener(REVEAL_EVENT, listener);
|
|
112
|
+
return () => window.removeEventListener(REVEAL_EVENT, listener);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function highlightConfigField(field: string) {
|
|
116
|
+
const el = document.querySelector<HTMLElement>(
|
|
117
|
+
`[data-config-field="${CSS.escape(field)}"]`
|
|
118
|
+
);
|
|
119
|
+
if (!el) return;
|
|
120
|
+
|
|
121
|
+
const scrollRoot = el.closest(".scope-config-sidebar") as HTMLElement | null;
|
|
122
|
+
if (scrollRoot) {
|
|
123
|
+
const rootRect = scrollRoot.getBoundingClientRect();
|
|
124
|
+
const elRect = el.getBoundingClientRect();
|
|
125
|
+
const top =
|
|
126
|
+
elRect.top -
|
|
127
|
+
rootRect.top +
|
|
128
|
+
scrollRoot.scrollTop -
|
|
129
|
+
scrollRoot.clientHeight / 3;
|
|
130
|
+
scrollRoot.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
|
131
|
+
} else {
|
|
132
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
el.classList.add("config-field--highlight");
|
|
136
|
+
window.setTimeout(() => el.classList.remove("config-field--highlight"), 1800);
|
|
137
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
3
|
+
import {
|
|
4
|
+
Tooltip,
|
|
5
|
+
TooltipContent,
|
|
6
|
+
TooltipProvider,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from "@arronqzy/ui";
|
|
9
|
+
|
|
10
|
+
export type ConfigHintIconProps = {
|
|
11
|
+
label?: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
contentClassName?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function ConfigHintIcon({
|
|
18
|
+
label,
|
|
19
|
+
children,
|
|
20
|
+
className,
|
|
21
|
+
contentClassName,
|
|
22
|
+
}: ConfigHintIconProps) {
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
const resolvedLabel = label ?? t("common.hint");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<TooltipProvider delayDuration={120}>
|
|
28
|
+
<Tooltip>
|
|
29
|
+
<TooltipTrigger asChild>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
className={
|
|
33
|
+
className ??
|
|
34
|
+
"inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50"
|
|
35
|
+
}
|
|
36
|
+
aria-label={t("common.hintAria", { label: resolvedLabel })}
|
|
37
|
+
>
|
|
38
|
+
?
|
|
39
|
+
</button>
|
|
40
|
+
</TooltipTrigger>
|
|
41
|
+
<TooltipContent
|
|
42
|
+
side="top"
|
|
43
|
+
className={`z-[10120] max-w-[360px] text-[11px] leading-5 ${contentClassName ?? ""}`}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</TooltipContent>
|
|
47
|
+
</Tooltip>
|
|
48
|
+
</TooltipProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
import * as echarts from "echarts";
|
|
3
4
|
import type { PanelElement } from "../types";
|
|
4
5
|
import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
|
|
5
6
|
import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
|
|
7
|
+
import { TableNodeContent, type TableCellActionHandler } from "./table/TableNodeContent";
|
|
6
8
|
|
|
7
9
|
export type ElementsLayerProps = {
|
|
8
10
|
elements: PanelElement[];
|
|
@@ -19,6 +21,7 @@ export type ElementsLayerProps = {
|
|
|
19
21
|
previewMode?: boolean;
|
|
20
22
|
/** 预览布局变更时递增,用于触发图表/画布重绘 */
|
|
21
23
|
previewLayoutKey?: number;
|
|
24
|
+
onTableCellAction?: TableCellActionHandler;
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
function TextNodeContent({
|
|
@@ -31,7 +34,8 @@ function TextNodeContent({
|
|
|
31
34
|
onChange: (nextHtml: string) => void;
|
|
32
35
|
}) {
|
|
33
36
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
34
|
-
const
|
|
37
|
+
const { t } = useI18n();
|
|
38
|
+
const html = element.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
|
|
35
39
|
const textStyle: React.CSSProperties = {
|
|
36
40
|
fontFamily: element.textFontFamily || undefined,
|
|
37
41
|
fontSize: element.textFontSize ? `${element.textFontSize}px` : undefined,
|
|
@@ -71,6 +75,7 @@ function GridNodeContent({
|
|
|
71
75
|
allElements: PanelElement[];
|
|
72
76
|
previewMode?: boolean;
|
|
73
77
|
}) {
|
|
78
|
+
const { t } = useI18n();
|
|
74
79
|
const rows = Math.max(1, Math.floor(element.gridRows ?? 2));
|
|
75
80
|
const cols = Math.max(1, Math.floor(element.gridCols ?? 3));
|
|
76
81
|
const gap = Math.max(0, element.gridGap ?? 8);
|
|
@@ -142,7 +147,7 @@ function GridNodeContent({
|
|
|
142
147
|
? "border-primary/70 bg-primary/10"
|
|
143
148
|
: "border-border/60 bg-muted/20",
|
|
144
149
|
].join(" ")}
|
|
145
|
-
title={
|
|
150
|
+
title={occupied.has(idx) ? t("panel.config.slotOccupied", { n: idx + 1 }) : t("panel.config.slotEmpty", { n: idx + 1 })}
|
|
146
151
|
/>
|
|
147
152
|
))}
|
|
148
153
|
</div>
|
|
@@ -179,6 +184,7 @@ function AudioNodeContent({
|
|
|
179
184
|
element: PanelElement;
|
|
180
185
|
selected?: boolean;
|
|
181
186
|
}) {
|
|
187
|
+
const { t } = useI18n();
|
|
182
188
|
const src = element.audioSrc || element.audioRemoteUrl || "";
|
|
183
189
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
184
190
|
const [playing, setPlaying] = useState(false);
|
|
@@ -269,11 +275,11 @@ function AudioNodeContent({
|
|
|
269
275
|
el.pause();
|
|
270
276
|
}
|
|
271
277
|
}}
|
|
272
|
-
title={!src ? "
|
|
278
|
+
title={!src ? t("panel.config.configureAudioFirst") : playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
273
279
|
>
|
|
274
280
|
<img
|
|
275
281
|
src={poster}
|
|
276
|
-
alt="
|
|
282
|
+
alt={t("panel.config.audioPoster")}
|
|
277
283
|
className="h-full w-full object-cover"
|
|
278
284
|
draggable={false}
|
|
279
285
|
onDragStart={(e) => e.preventDefault()}
|
|
@@ -295,7 +301,7 @@ function AudioNodeContent({
|
|
|
295
301
|
el.pause();
|
|
296
302
|
}
|
|
297
303
|
}}
|
|
298
|
-
title={!src ? "
|
|
304
|
+
title={!src ? t("panel.config.configureAudioFirst") : playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
299
305
|
>
|
|
300
306
|
{renderIcon()}
|
|
301
307
|
</button>
|
|
@@ -313,7 +319,7 @@ function AudioNodeContent({
|
|
|
313
319
|
</>
|
|
314
320
|
) : null}
|
|
315
321
|
<span className="pointer-events-none absolute right-2 top-2 rounded bg-black/55 px-1.5 py-0.5 text-[10px] text-white">
|
|
316
|
-
{!src ? "
|
|
322
|
+
{!src ? t("panel.config.audioNotConfigured") : playing ? t("panel.config.pause") : t("panel.config.play")}
|
|
317
323
|
</span>
|
|
318
324
|
<audio ref={audioRef} src={src} preload="metadata" className="hidden" />
|
|
319
325
|
</div>
|
|
@@ -322,14 +328,14 @@ function AudioNodeContent({
|
|
|
322
328
|
if (!src) {
|
|
323
329
|
return (
|
|
324
330
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
325
|
-
|
|
331
|
+
{t("panel.config.audioPlaceholder")}
|
|
326
332
|
</div>
|
|
327
333
|
);
|
|
328
334
|
}
|
|
329
335
|
return (
|
|
330
336
|
<div className="relative flex h-full w-full items-center justify-center rounded border border-border/60 bg-muted/10 px-2">
|
|
331
337
|
<div className="pointer-events-none text-[11px] text-muted-foreground">
|
|
332
|
-
{playing ? "
|
|
338
|
+
{playing ? t("panel.config.audioPlaying") : t("panel.config.audioReady")}
|
|
333
339
|
</div>
|
|
334
340
|
<button
|
|
335
341
|
type="button"
|
|
@@ -345,9 +351,9 @@ function AudioNodeContent({
|
|
|
345
351
|
el.pause();
|
|
346
352
|
}
|
|
347
353
|
}}
|
|
348
|
-
title={playing ? "
|
|
354
|
+
title={playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
349
355
|
>
|
|
350
|
-
{playing ? "
|
|
356
|
+
{playing ? t("panel.config.pause") : t("panel.config.play")}
|
|
351
357
|
</button>
|
|
352
358
|
<audio ref={audioRef} src={src} preload="metadata" className="hidden" />
|
|
353
359
|
</div>
|
|
@@ -361,6 +367,7 @@ function VideoNodeContent({
|
|
|
361
367
|
element: PanelElement;
|
|
362
368
|
selected?: boolean;
|
|
363
369
|
}) {
|
|
370
|
+
const { t } = useI18n();
|
|
364
371
|
const src = element.videoSrc || element.videoRemoteUrl || "";
|
|
365
372
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
366
373
|
const [playing, setPlaying] = useState(false);
|
|
@@ -388,7 +395,7 @@ function VideoNodeContent({
|
|
|
388
395
|
if (!src) {
|
|
389
396
|
return (
|
|
390
397
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
391
|
-
|
|
398
|
+
{t("panel.config.videoPlaceholder")}
|
|
392
399
|
</div>
|
|
393
400
|
);
|
|
394
401
|
}
|
|
@@ -409,9 +416,9 @@ function VideoNodeContent({
|
|
|
409
416
|
el.pause();
|
|
410
417
|
}
|
|
411
418
|
}}
|
|
412
|
-
title={playing ? "
|
|
419
|
+
title={playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
413
420
|
>
|
|
414
|
-
{playing ? "
|
|
421
|
+
{playing ? t("panel.config.pause") : t("panel.config.play")}
|
|
415
422
|
</button>
|
|
416
423
|
</div>
|
|
417
424
|
);
|
|
@@ -423,20 +430,22 @@ function hasBackgroundImage(element: PanelElement) {
|
|
|
423
430
|
}
|
|
424
431
|
|
|
425
432
|
function ImageNodeContent({ element }: { element: PanelElement }) {
|
|
433
|
+
const { t } = useI18n();
|
|
426
434
|
if (hasBackgroundImage(element)) return null;
|
|
427
435
|
return (
|
|
428
436
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
429
|
-
|
|
437
|
+
{t("panel.config.imagePlaceholder")}
|
|
430
438
|
</div>
|
|
431
439
|
);
|
|
432
440
|
}
|
|
433
441
|
|
|
434
442
|
function EmptyNodePlaceholder({ element }: { element: PanelElement }) {
|
|
443
|
+
const { t } = useI18n();
|
|
435
444
|
const labelMap: Record<string, string> = {
|
|
436
|
-
video: "
|
|
437
|
-
audio: "
|
|
445
|
+
video: t("panel.config.videoPlaceholder"),
|
|
446
|
+
audio: t("panel.config.audioPlaceholder"),
|
|
438
447
|
};
|
|
439
|
-
const label = labelMap[element.materialType ?? ""] ??
|
|
448
|
+
const label = labelMap[element.materialType ?? ""] ?? t("panel.config.materialPlaceholder", { type: element.materialType ?? t("common.node") });
|
|
440
449
|
return (
|
|
441
450
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
442
451
|
{label}
|
|
@@ -607,8 +616,6 @@ function ChartNodeContent({
|
|
|
607
616
|
window.addEventListener(PREVIEW_LAYOUT_EVENT, onLayout);
|
|
608
617
|
const obs = new ResizeObserver(onLayout);
|
|
609
618
|
obs.observe(host);
|
|
610
|
-
const parent = host.parentElement;
|
|
611
|
-
if (parent) obs.observe(parent);
|
|
612
619
|
return () => {
|
|
613
620
|
window.removeEventListener(PREVIEW_LAYOUT_EVENT, onLayout);
|
|
614
621
|
obs.disconnect();
|
|
@@ -683,6 +690,7 @@ function ReferenceNodeContent({
|
|
|
683
690
|
snapshotSource?: PanelElement[];
|
|
684
691
|
visitedIds?: string[];
|
|
685
692
|
}) {
|
|
693
|
+
const { t } = useI18n();
|
|
686
694
|
const sourceNodes = useMemo(
|
|
687
695
|
() => {
|
|
688
696
|
const fromDeep =
|
|
@@ -727,7 +735,7 @@ function ReferenceNodeContent({
|
|
|
727
735
|
}, [element.height, element.width, sourceNodes]);
|
|
728
736
|
|
|
729
737
|
if (!layout || sourceNodes.length === 0) {
|
|
730
|
-
const hintText = element.refLayerId ? "
|
|
738
|
+
const hintText = element.refLayerId ? t("panel.config.refLayerEmpty") : t("panel.config.selectRefLayer");
|
|
731
739
|
return (
|
|
732
740
|
<div className="flex h-full w-full items-center justify-center">
|
|
733
741
|
<div className="rounded border border-dashed border-border/70 px-2 py-1 text-[10px] text-muted-foreground">
|
|
@@ -781,6 +789,8 @@ function ReferenceNodeContent({
|
|
|
781
789
|
/>
|
|
782
790
|
) : node.materialType === "geometry" ? (
|
|
783
791
|
<GeometryNodeContent element={node} />
|
|
792
|
+
) : node.materialType === "table" ? (
|
|
793
|
+
<TableNodeContent element={node} interactive={false} />
|
|
784
794
|
) : (
|
|
785
795
|
<div className="h-full w-full" />
|
|
786
796
|
)}
|
|
@@ -792,7 +802,7 @@ function ReferenceNodeContent({
|
|
|
792
802
|
);
|
|
793
803
|
}
|
|
794
804
|
|
|
795
|
-
export function ElementsLayer({
|
|
805
|
+
export const ElementsLayer = React.memo(function ElementsLayer({
|
|
796
806
|
elements,
|
|
797
807
|
allElements,
|
|
798
808
|
selectedIds,
|
|
@@ -801,6 +811,7 @@ export function ElementsLayer({
|
|
|
801
811
|
layerLocked = false,
|
|
802
812
|
previewMode = false,
|
|
803
813
|
previewLayoutKey,
|
|
814
|
+
onTableCellAction,
|
|
804
815
|
}: ElementsLayerProps) {
|
|
805
816
|
const sortedElements = useMemo(() => {
|
|
806
817
|
return [...elements].sort((a, b) => {
|
|
@@ -879,6 +890,12 @@ export function ElementsLayer({
|
|
|
879
890
|
<GeometryNodeContent element={el} />
|
|
880
891
|
) : el.materialType === "image" ? (
|
|
881
892
|
<ImageNodeContent element={el} />
|
|
893
|
+
) : el.materialType === "table" ? (
|
|
894
|
+
<TableNodeContent
|
|
895
|
+
element={el}
|
|
896
|
+
interactive={previewMode || Boolean(onTableCellAction)}
|
|
897
|
+
onCellAction={onTableCellAction}
|
|
898
|
+
/>
|
|
882
899
|
) : (
|
|
883
900
|
<EmptyNodePlaceholder element={el} />
|
|
884
901
|
)}
|
|
@@ -887,5 +904,5 @@ export function ElementsLayer({
|
|
|
887
904
|
})}
|
|
888
905
|
</>
|
|
889
906
|
);
|
|
890
|
-
}
|
|
907
|
+
});
|
|
891
908
|
|