@arronqzy/react-view 1.0.39 → 1.0.40
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 +316 -241
- package/package.json +7 -7
- package/src/panel/ReactViewPanel.tsx +80 -71
- package/src/panel/components/ElementsLayer.tsx +1 -1
- package/src/panel/components/MaterialSidebar.tsx +304 -264
- package/src/panel/components/MoveableLayer.tsx +11 -2
- package/src/panel/components/PanelCanvas.tsx +1 -0
- package/src/panel/components/PanelConfigSidebar.tsx +49 -65
- package/src/panel/components/PanelLayerDock.tsx +397 -316
- package/src/panel/components/PanelRulers.tsx +169 -42
- package/src/panel/components/SelectLayer.tsx +2 -2
- package/src/panel/components/WorkspaceProjectNav.tsx +1 -1
- package/src/panel/icons.tsx +117 -0
- package/src/tailwind.css +66 -5
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import React, { useMemo, useState } from "react";
|
|
2
2
|
import { useI18n } from "@arronqzy/i18n/react";
|
|
3
3
|
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardHeader,
|
|
7
4
|
Collapsible,
|
|
8
5
|
CollapsibleContent,
|
|
9
6
|
CollapsibleTrigger,
|
|
@@ -12,7 +9,11 @@ import {
|
|
|
12
9
|
EmptyIcon,
|
|
13
10
|
EmptyTitle,
|
|
14
11
|
Input,
|
|
15
|
-
|
|
12
|
+
Select,
|
|
13
|
+
SelectContent,
|
|
14
|
+
SelectItem,
|
|
15
|
+
SelectTrigger,
|
|
16
|
+
SelectValue,
|
|
16
17
|
Tabs,
|
|
17
18
|
TabsContent,
|
|
18
19
|
TabsList,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
TooltipProvider,
|
|
23
24
|
TooltipTrigger,
|
|
24
25
|
} from "@arronqzy/ui";
|
|
26
|
+
import { IconFolder, IconGrid, IconRefFilter, IconSearch, IconTree } from "../icons";
|
|
25
27
|
import type { PanelElement, ReferenceCopyMode } from "../types";
|
|
26
28
|
import type { PanelLayer } from "../types";
|
|
27
29
|
import { getPanelMessages } from "../constants/messages";
|
|
@@ -143,25 +145,26 @@ function IconDeepCopy({ className = "h-3.5 w-3.5" }: { className?: string }) {
|
|
|
143
145
|
function MaterialPreview({ id }: { id: string }) {
|
|
144
146
|
const { t } = useI18n();
|
|
145
147
|
const common =
|
|
146
|
-
"relative aspect-[4/3] w-full shrink-0 overflow-hidden rounded-md border border-border bg-gradient-to-br from-card to-muted/
|
|
148
|
+
"relative mx-auto aspect-[4/3] w-full shrink-0 overflow-hidden rounded-md border border-border/60 bg-gradient-to-br from-card to-muted/35";
|
|
149
|
+
const previewStyle = { maxWidth: 88, maxHeight: 64 } as const;
|
|
147
150
|
|
|
148
151
|
if (id === "bar") {
|
|
149
152
|
return (
|
|
150
|
-
<div className={common}>
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
-
</
|
|
153
|
+
<div className={common} style={previewStyle}>
|
|
154
|
+
<svg viewBox="0 0 80 60" className="h-full w-full" aria-hidden="true">
|
|
155
|
+
<rect x="8" y="40" width="8" height="12" rx="1.5" className="fill-primary/55" />
|
|
156
|
+
<rect x="22" y="30" width="8" height="22" rx="1.5" className="fill-primary/65" />
|
|
157
|
+
<rect x="36" y="16" width="8" height="36" rx="1.5" className="fill-primary/80" />
|
|
158
|
+
<rect x="50" y="24" width="8" height="28" rx="1.5" className="fill-primary/70" />
|
|
159
|
+
<rect x="64" y="34" width="8" height="18" rx="1.5" className="fill-primary/60" />
|
|
160
|
+
</svg>
|
|
158
161
|
</div>
|
|
159
162
|
);
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
if (id === "line" || id === "area") {
|
|
163
166
|
return (
|
|
164
|
-
<div className={common}>
|
|
167
|
+
<div className={common} style={previewStyle}>
|
|
165
168
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
166
169
|
{id === "area" ? (
|
|
167
170
|
<polygon
|
|
@@ -184,7 +187,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
184
187
|
|
|
185
188
|
if (id === "pie") {
|
|
186
189
|
return (
|
|
187
|
-
<div className={common}>
|
|
190
|
+
<div className={common} style={previewStyle}>
|
|
188
191
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
189
192
|
<circle cx="40" cy="30" r="16" className="fill-primary/20" />
|
|
190
193
|
<path d="M40 30 L40 14 A16 16 0 0 1 55 39 Z" className="fill-primary/75" />
|
|
@@ -196,7 +199,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
196
199
|
|
|
197
200
|
if (id === "scatter") {
|
|
198
201
|
return (
|
|
199
|
-
<div className={common}>
|
|
202
|
+
<div className={common} style={previewStyle}>
|
|
200
203
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
201
204
|
<circle cx="14" cy="42" r="2.5" className="fill-primary/80" />
|
|
202
205
|
<circle cx="24" cy="34" r="2.5" className="fill-primary/70" />
|
|
@@ -210,7 +213,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
210
213
|
|
|
211
214
|
if (id === "radar") {
|
|
212
215
|
return (
|
|
213
|
-
<div className={common}>
|
|
216
|
+
<div className={common} style={previewStyle}>
|
|
214
217
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
215
218
|
<polygon points="40,10 58,20 54,40 26,40 22,20" className="fill-primary/20 stroke-primary/60" />
|
|
216
219
|
<polygon points="40,16 52,23 49,36 31,36 28,24" className="fill-primary/45 stroke-primary/80" />
|
|
@@ -221,7 +224,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
221
224
|
|
|
222
225
|
if (id === "gauge") {
|
|
223
226
|
return (
|
|
224
|
-
<div className={common}>
|
|
227
|
+
<div className={common} style={previewStyle}>
|
|
225
228
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
226
229
|
<path d="M12 44a28 28 0 0 1 56 0" className="fill-none stroke-primary/35" strokeWidth="6" />
|
|
227
230
|
<path d="M12 44a28 28 0 0 1 40-24" className="fill-none stroke-primary/80" strokeWidth="6" />
|
|
@@ -233,7 +236,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
233
236
|
|
|
234
237
|
if (id === "funnel") {
|
|
235
238
|
return (
|
|
236
|
-
<div className={common}>
|
|
239
|
+
<div className={common} style={previewStyle}>
|
|
237
240
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
238
241
|
<polygon points="10,10 70,10 58,22 22,22" className="fill-primary/80" />
|
|
239
242
|
<polygon points="22,24 58,24 50,34 30,34" className="fill-primary/65" />
|
|
@@ -245,7 +248,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
245
248
|
|
|
246
249
|
if (id === "text") {
|
|
247
250
|
return (
|
|
248
|
-
<div className={common}>
|
|
251
|
+
<div className={common} style={previewStyle}>
|
|
249
252
|
<div className="space-y-1.5 px-2 py-2">
|
|
250
253
|
<div className="h-2 w-10 rounded bg-primary/70" />
|
|
251
254
|
<div className="h-1.5 w-full rounded bg-muted-foreground/45" />
|
|
@@ -258,7 +261,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
258
261
|
|
|
259
262
|
if (id === "table") {
|
|
260
263
|
return (
|
|
261
|
-
<div className={common}>
|
|
264
|
+
<div className={common} style={previewStyle}>
|
|
262
265
|
<div className="absolute inset-0 flex flex-col gap-1 p-2">
|
|
263
266
|
<div className="grid h-3 flex-none grid-cols-3 gap-1">
|
|
264
267
|
<div className="rounded-sm bg-primary/75" />
|
|
@@ -280,7 +283,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
280
283
|
|
|
281
284
|
if (id === "rect") {
|
|
282
285
|
return (
|
|
283
|
-
<div className={common}>
|
|
286
|
+
<div className={common} style={previewStyle}>
|
|
284
287
|
<div className="absolute inset-2 rounded-md border-2 border-primary/80 bg-primary/15" />
|
|
285
288
|
</div>
|
|
286
289
|
);
|
|
@@ -288,7 +291,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
288
291
|
|
|
289
292
|
if (id === "geometry") {
|
|
290
293
|
return (
|
|
291
|
-
<div className={common}>
|
|
294
|
+
<div className={common} style={previewStyle}>
|
|
292
295
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
293
296
|
<circle cx="22" cy="18" r="8" className="fill-primary/75" />
|
|
294
297
|
<rect x="36" y="10" width="16" height="16" rx="3" className="fill-primary/55" />
|
|
@@ -302,7 +305,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
302
305
|
|
|
303
306
|
if (id === "scene3d") {
|
|
304
307
|
return (
|
|
305
|
-
<div className={common}>
|
|
308
|
+
<div className={common} style={previewStyle}>
|
|
306
309
|
<svg viewBox="0 0 80 60" className="h-full w-full">
|
|
307
310
|
<polygon points="40,8 62,22 62,44 40,56 18,44 18,22" className="fill-primary/20 stroke-primary/80" strokeWidth="1.5" />
|
|
308
311
|
<polygon points="40,8 62,22 40,32 18,22" className="fill-primary/45" />
|
|
@@ -315,7 +318,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
315
318
|
|
|
316
319
|
if (id === "reference") {
|
|
317
320
|
return (
|
|
318
|
-
<div className={common}>
|
|
321
|
+
<div className={common} style={previewStyle}>
|
|
319
322
|
<div className="absolute inset-2 rounded-md border border-dashed border-primary/70 bg-primary/10" />
|
|
320
323
|
<div className="absolute inset-0 flex items-center justify-center text-[10px] text-primary/80">
|
|
321
324
|
{t("panel.material.reference")}
|
|
@@ -326,7 +329,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
326
329
|
|
|
327
330
|
if (id === "image") {
|
|
328
331
|
return (
|
|
329
|
-
<div className={common}>
|
|
332
|
+
<div className={common} style={previewStyle}>
|
|
330
333
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/25 via-muted/60 to-card" />
|
|
331
334
|
<svg viewBox="0 0 80 60" className="absolute inset-0 h-full w-full">
|
|
332
335
|
<circle cx="16" cy="14" r="5" className="fill-primary/70" />
|
|
@@ -339,7 +342,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
339
342
|
|
|
340
343
|
if (id === "video") {
|
|
341
344
|
return (
|
|
342
|
-
<div className={common}>
|
|
345
|
+
<div className={common} style={previewStyle}>
|
|
343
346
|
<div className="absolute inset-0 bg-primary/12" />
|
|
344
347
|
<div className="absolute left-2 right-2 top-2 h-1.5 rounded bg-primary/35" />
|
|
345
348
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
@@ -353,7 +356,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
353
356
|
|
|
354
357
|
if (id === "audio") {
|
|
355
358
|
return (
|
|
356
|
-
<div className={common}>
|
|
359
|
+
<div className={common} style={previewStyle}>
|
|
357
360
|
<div className="absolute inset-0 flex items-center justify-center gap-1">
|
|
358
361
|
<div className="h-3 w-1.5 rounded bg-primary/50" />
|
|
359
362
|
<div className="h-6 w-1.5 rounded bg-primary/75" />
|
|
@@ -366,7 +369,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
366
369
|
}
|
|
367
370
|
if (id === "grid") {
|
|
368
371
|
return (
|
|
369
|
-
<div className={common}>
|
|
372
|
+
<div className={common} style={previewStyle}>
|
|
370
373
|
<div className="absolute inset-0 grid grid-cols-3 grid-rows-2 gap-1.5 p-2">
|
|
371
374
|
{Array.from({ length: 6 }).map((_, idx) => (
|
|
372
375
|
<div key={idx} className="rounded-sm border border-dashed border-primary/55 bg-primary/10" />
|
|
@@ -377,7 +380,7 @@ function MaterialPreview({ id }: { id: string }) {
|
|
|
377
380
|
}
|
|
378
381
|
if (id === "viewport") {
|
|
379
382
|
return (
|
|
380
|
-
<div className={common}>
|
|
383
|
+
<div className={common} style={previewStyle}>
|
|
381
384
|
<div className="absolute inset-1.5 overflow-hidden rounded-sm border border-primary/70 bg-primary/10">
|
|
382
385
|
<div className="absolute left-1 top-1 h-6 w-8 rounded-[2px] bg-primary/45" />
|
|
383
386
|
<div className="absolute left-6 top-4 h-7 w-10 rounded-[2px] bg-primary/30" />
|
|
@@ -466,7 +469,6 @@ export function MaterialSidebar({
|
|
|
466
469
|
const [activeCategoryId, setActiveCategoryId] =
|
|
467
470
|
useState<MaterialCategoryId>("charts");
|
|
468
471
|
const [keyword, setKeyword] = useState("");
|
|
469
|
-
const [treeKeyword, setTreeKeyword] = useState("");
|
|
470
472
|
const [referenceOnlyTree, setReferenceOnlyTree] = useState(false);
|
|
471
473
|
const [draggingTreeNodeId, setDraggingTreeNodeId] = useState<string | null>(null);
|
|
472
474
|
const [dragOverLayerId, setDragOverLayerId] = useState<string | null>(null);
|
|
@@ -474,14 +476,14 @@ export function MaterialSidebar({
|
|
|
474
476
|
root: true,
|
|
475
477
|
});
|
|
476
478
|
|
|
477
|
-
const activeCategory = useMemo(
|
|
478
|
-
() => categories.find((c) => c.id === activeCategoryId) ?? categories[0],
|
|
479
|
-
[activeCategoryId, categories]
|
|
480
|
-
);
|
|
481
479
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
|
482
|
-
const isSearching = normalizedKeyword.length > 0;
|
|
483
|
-
const
|
|
484
|
-
const
|
|
480
|
+
const isSearching = leftTab === "materials" && normalizedKeyword.length > 0;
|
|
481
|
+
const isTreeSearching = leftTab === "tree" && normalizedKeyword.length > 0;
|
|
482
|
+
const normalizedTreeKeyword = isTreeSearching ? normalizedKeyword : "";
|
|
483
|
+
const searchPlaceholder =
|
|
484
|
+
leftTab === "tree"
|
|
485
|
+
? t("panel.material.treeSearchPlaceholder")
|
|
486
|
+
: t("panel.material.searchPlaceholder");
|
|
485
487
|
|
|
486
488
|
const matchedItems = useMemo(() => {
|
|
487
489
|
if (!isSearching) return [];
|
|
@@ -497,6 +499,10 @@ export function MaterialSidebar({
|
|
|
497
499
|
return result;
|
|
498
500
|
}, [categories, isSearching, normalizedKeyword]);
|
|
499
501
|
|
|
502
|
+
const activeCategory =
|
|
503
|
+
categories.find((c) => c.id === activeCategoryId) ?? categories[0];
|
|
504
|
+
const displayItems = isSearching ? matchedItems : (activeCategory?.items ?? []);
|
|
505
|
+
|
|
500
506
|
const themedScrollbarClass =
|
|
501
507
|
"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";
|
|
502
508
|
|
|
@@ -641,26 +647,31 @@ export function MaterialSidebar({
|
|
|
641
647
|
>
|
|
642
648
|
<div
|
|
643
649
|
className={[
|
|
644
|
-
"
|
|
645
|
-
selected
|
|
650
|
+
"group/node flex min-w-0 items-center gap-0.5 rounded-sm py-0.5 pr-1",
|
|
651
|
+
selected
|
|
652
|
+
? "bg-primary/15 text-foreground"
|
|
653
|
+
: "text-foreground/85 hover:bg-accent/55",
|
|
646
654
|
].join(" ")}
|
|
647
|
-
style={{ paddingLeft:
|
|
655
|
+
style={{ paddingLeft: 2 }}
|
|
648
656
|
>
|
|
649
657
|
{hasChildren ? (
|
|
650
658
|
<CollapsibleTrigger asChild>
|
|
651
659
|
<button
|
|
652
660
|
type="button"
|
|
653
|
-
className="flex h-
|
|
661
|
+
className="flex h-5 w-4 shrink-0 items-center justify-center rounded text-[11px] text-muted-foreground hover:bg-accent"
|
|
654
662
|
>
|
|
655
663
|
{isExpanded(nodeKey, true) ? "▾" : "▸"}
|
|
656
664
|
</button>
|
|
657
665
|
</CollapsibleTrigger>
|
|
658
666
|
) : (
|
|
659
|
-
<span className="inline-flex h-
|
|
667
|
+
<span className="inline-flex h-5 w-4 shrink-0 items-center justify-center text-[10px] text-border">
|
|
668
|
+
<span className="h-px w-2 bg-border" />
|
|
669
|
+
</span>
|
|
660
670
|
)}
|
|
671
|
+
<div className="relative min-w-0 flex-1">
|
|
661
672
|
<button
|
|
662
673
|
type="button"
|
|
663
|
-
className="
|
|
674
|
+
className="block w-full truncate pr-1 text-left text-[12px] font-medium"
|
|
664
675
|
onClick={() => onSelectNode?.(node.id, node.layerId)}
|
|
665
676
|
onContextMenu={(e) => {
|
|
666
677
|
e.preventDefault();
|
|
@@ -694,9 +705,10 @@ export function MaterialSidebar({
|
|
|
694
705
|
>
|
|
695
706
|
{getNodeDisplayName(node)}
|
|
696
707
|
</button>
|
|
708
|
+
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center gap-0.5 bg-gradient-to-l from-background via-background to-transparent pl-3 opacity-0 transition-opacity group-hover/node:pointer-events-auto group-hover/node:opacity-100 group-focus-within/node:pointer-events-auto group-focus-within/node:opacity-100">
|
|
697
709
|
{nodeHomeLayer?.isMapping ? (
|
|
698
710
|
<span
|
|
699
|
-
className="inline-flex shrink-0 items-center rounded
|
|
711
|
+
className="inline-flex shrink-0 items-center rounded border border-violet-600/70 bg-violet-500/15 px-1 py-0.5 text-[10px] font-medium text-violet-900 dark:border-violet-400/70 dark:bg-violet-500/25 dark:text-violet-100"
|
|
700
712
|
title={
|
|
701
713
|
nodeHomeLayer.mappingBaseLayerId
|
|
702
714
|
? t("panel.material.mappingLayerWithBase", {
|
|
@@ -815,26 +827,29 @@ export function MaterialSidebar({
|
|
|
815
827
|
</TooltipTrigger>
|
|
816
828
|
<TooltipContent>{node.locked ? t("panel.material.lockedNodeCannotDelete") : t("panel.material.deleteNode")}</TooltipContent>
|
|
817
829
|
</Tooltip>
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
818
832
|
</div>
|
|
819
833
|
{hasChildren ? (
|
|
820
834
|
<CollapsibleContent>
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
835
|
+
<div className="ml-[7px] border-l border-border/45 pl-1.5">
|
|
836
|
+
{children.map((child) => {
|
|
837
|
+
const childPath = `${path}->${child.id}`;
|
|
838
|
+
if (nextVisited.has(child.id)) {
|
|
839
|
+
return (
|
|
840
|
+
<div
|
|
841
|
+
key={`cycle:${childPath}`}
|
|
842
|
+
className="py-0.5 pl-1 text-[10px] text-muted-foreground/80"
|
|
843
|
+
title={t("panel.material.circularRefStopped")}
|
|
844
|
+
>
|
|
845
|
+
{getNodeDisplayName(child)}
|
|
846
|
+
{t("panel.material.circularRefSuffix")}
|
|
847
|
+
</div>
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
return renderTreeNode(child, level + 1, childPath, nextVisited, node.refSnapshot);
|
|
851
|
+
})}
|
|
852
|
+
</div>
|
|
838
853
|
</CollapsibleContent>
|
|
839
854
|
) : null}
|
|
840
855
|
</Collapsible>
|
|
@@ -854,67 +869,112 @@ export function MaterialSidebar({
|
|
|
854
869
|
<TooltipProvider delayDuration={120}>
|
|
855
870
|
<aside
|
|
856
871
|
className={[
|
|
857
|
-
"grid h-full w-full border-r border-border bg-muted/
|
|
872
|
+
"rv-material-sidebar grid h-full min-h-0 w-full overflow-hidden border-r border-border/60 bg-muted/20 text-foreground",
|
|
858
873
|
className ?? "",
|
|
859
874
|
].join(" ")}
|
|
860
875
|
style={{ gridTemplateRows: "auto 1fr" }}
|
|
861
876
|
>
|
|
862
|
-
<div className="border-b border-border bg-background/
|
|
863
|
-
<
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
877
|
+
<div className="border-b border-border/50 bg-background/70 px-2.5 py-1.5 backdrop-blur-sm">
|
|
878
|
+
<div className="relative">
|
|
879
|
+
<span
|
|
880
|
+
className="pointer-events-none absolute top-1/2 z-[1] -translate-y-1/2 text-muted-foreground"
|
|
881
|
+
style={{ left: 12 }}
|
|
882
|
+
>
|
|
883
|
+
<IconSearch />
|
|
884
|
+
</span>
|
|
885
|
+
<Input
|
|
886
|
+
value={keyword}
|
|
887
|
+
onChange={(e) => setKeyword(e.target.value)}
|
|
888
|
+
placeholder={searchPlaceholder}
|
|
889
|
+
className="h-7 rounded-md border-border/60 bg-background/90 text-[12px] shadow-none"
|
|
890
|
+
style={{ paddingLeft: 34, paddingRight: 10 }}
|
|
891
|
+
aria-label={searchPlaceholder}
|
|
892
|
+
/>
|
|
893
|
+
</div>
|
|
869
894
|
</div>
|
|
870
895
|
|
|
871
896
|
<Tabs
|
|
872
897
|
value={leftTab}
|
|
873
|
-
onValueChange={(v) =>
|
|
874
|
-
|
|
898
|
+
onValueChange={(v) => {
|
|
899
|
+
setLeftTab(v as "materials" | "tree");
|
|
900
|
+
setKeyword("");
|
|
901
|
+
}}
|
|
902
|
+
className="flex h-full min-h-0 flex-col overflow-hidden"
|
|
875
903
|
>
|
|
876
|
-
<div className="border-b border-border px-2
|
|
877
|
-
<TabsList className="grid w-full grid-cols-2">
|
|
878
|
-
<TabsTrigger
|
|
879
|
-
|
|
904
|
+
<div className="border-b border-border/50 px-2 py-1.5">
|
|
905
|
+
<TabsList className="grid h-7 w-full grid-cols-2 rounded-md bg-muted/40 p-0.5">
|
|
906
|
+
<TabsTrigger
|
|
907
|
+
value="materials"
|
|
908
|
+
className="gap-1 rounded px-1 text-[11px] font-medium data-[state=active]:font-semibold"
|
|
909
|
+
title={t("panel.material.tabMaterials")}
|
|
910
|
+
>
|
|
911
|
+
<IconGrid />
|
|
912
|
+
<span className="truncate @[200px]:inline hidden">{t("panel.material.tabMaterials")}</span>
|
|
913
|
+
</TabsTrigger>
|
|
914
|
+
<TabsTrigger
|
|
915
|
+
value="tree"
|
|
916
|
+
className="gap-1 rounded px-1 text-[11px] font-medium data-[state=active]:font-semibold"
|
|
917
|
+
title={t("panel.material.tabTree")}
|
|
918
|
+
>
|
|
919
|
+
<IconTree />
|
|
920
|
+
<span className="truncate @[200px]:inline hidden">{t("panel.material.tabTree")}</span>
|
|
921
|
+
</TabsTrigger>
|
|
880
922
|
</TabsList>
|
|
881
923
|
</div>
|
|
882
|
-
<TabsContent
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
924
|
+
<TabsContent
|
|
925
|
+
value="materials"
|
|
926
|
+
forceMount
|
|
927
|
+
className="mt-0 min-h-0 flex-1 flex-col overflow-hidden"
|
|
928
|
+
style={{ display: leftTab === "materials" ? "flex" : "none" }}
|
|
929
|
+
>
|
|
930
|
+
{!isSearching ? (
|
|
931
|
+
<div className="shrink-0 border-b border-border/40 bg-background/80 px-2 py-1.5">
|
|
932
|
+
<Select
|
|
933
|
+
value={activeCategoryId}
|
|
934
|
+
onValueChange={(v) => setActiveCategoryId(v as MaterialCategoryId)}
|
|
935
|
+
>
|
|
936
|
+
<SelectTrigger
|
|
937
|
+
className="h-7 w-full flex-nowrap gap-1 whitespace-nowrap rounded-md border-border/60 bg-muted/25 px-2 text-[12px] font-semibold shadow-none [&>span]:line-clamp-none [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap"
|
|
938
|
+
aria-label={activeCategory?.title}
|
|
939
|
+
>
|
|
940
|
+
<IconFolder />
|
|
941
|
+
<SelectValue placeholder={activeCategory?.title} />
|
|
942
|
+
</SelectTrigger>
|
|
943
|
+
<SelectContent className="z-[10100]">
|
|
944
|
+
{categories.map((category) => (
|
|
945
|
+
<SelectItem
|
|
946
|
+
key={category.id}
|
|
947
|
+
value={category.id}
|
|
948
|
+
className="whitespace-nowrap text-[12px]"
|
|
949
|
+
>
|
|
950
|
+
{`${category.title} · ${category.items.length}`}
|
|
951
|
+
</SelectItem>
|
|
952
|
+
))}
|
|
953
|
+
</SelectContent>
|
|
954
|
+
</Select>
|
|
903
955
|
</div>
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
956
|
+
) : null}
|
|
957
|
+
<div className={`min-h-0 flex-1 overflow-auto ${themedScrollbarClass}`}>
|
|
958
|
+
<div className="px-2 pb-2.5 pt-1.5">
|
|
959
|
+
<div className="mb-1.5 flex items-baseline justify-between gap-2 px-0.5">
|
|
960
|
+
<div data-typo="label" className="truncate text-[12px] font-semibold text-foreground/90">
|
|
961
|
+
{isSearching
|
|
962
|
+
? t("panel.material.searchResults", { count: matchedItems.length })
|
|
963
|
+
: activeCategory?.title}
|
|
964
|
+
</div>
|
|
965
|
+
{!isSearching ? (
|
|
966
|
+
<div data-typo="caption" className="tabular-nums text-[11px] text-muted-foreground">
|
|
967
|
+
{activeCategory?.items.length ?? 0}
|
|
968
|
+
</div>
|
|
969
|
+
) : null}
|
|
910
970
|
</div>
|
|
911
|
-
<div className="
|
|
912
|
-
{
|
|
971
|
+
<div className="rv-material-grid">
|
|
972
|
+
{displayItems.map((it) => (
|
|
913
973
|
<button
|
|
914
974
|
key={isSearching ? `${it.id}-${(it as any).categoryTitle}` : it.id}
|
|
915
975
|
type="button"
|
|
916
976
|
draggable
|
|
917
|
-
className="cursor-pointer rounded-
|
|
977
|
+
className="rv-material-card cursor-pointer rounded-lg border border-border/50 bg-card px-1.5 py-1.5 text-left text-card-foreground shadow-[0_1px_1px_hsl(var(--foreground)/0.03)] transition-colors hover:border-border hover:bg-accent/40"
|
|
918
978
|
onDragStart={(e) => {
|
|
919
979
|
e.dataTransfer.setData(
|
|
920
980
|
"application/x-arronqzy-material",
|
|
@@ -924,12 +984,20 @@ export function MaterialSidebar({
|
|
|
924
984
|
onDragMaterialStart?.(it);
|
|
925
985
|
}}
|
|
926
986
|
>
|
|
927
|
-
<div className="flex flex-col
|
|
987
|
+
<div className="flex flex-col gap-1">
|
|
928
988
|
<MaterialPreview id={it.id} />
|
|
929
|
-
<div className="min-w-0">
|
|
930
|
-
<div
|
|
989
|
+
<div className="min-w-0 px-0.5">
|
|
990
|
+
<div
|
|
991
|
+
data-typo="caption"
|
|
992
|
+
className="truncate text-center text-[11px] font-normal text-muted-foreground"
|
|
993
|
+
>
|
|
994
|
+
{it.title}
|
|
995
|
+
</div>
|
|
931
996
|
{isSearching ? (
|
|
932
|
-
<div
|
|
997
|
+
<div
|
|
998
|
+
data-typo="caption"
|
|
999
|
+
className="mt-0.5 truncate text-center text-[10px] text-muted-foreground"
|
|
1000
|
+
>
|
|
933
1001
|
{(it as any).categoryTitle}
|
|
934
1002
|
</div>
|
|
935
1003
|
) : null}
|
|
@@ -940,63 +1008,70 @@ export function MaterialSidebar({
|
|
|
940
1008
|
{isSearching && matchedItems.length === 0 ? (
|
|
941
1009
|
<Empty className="py-5">
|
|
942
1010
|
<EmptyIcon className="h-8 w-8">
|
|
943
|
-
<
|
|
944
|
-
viewBox="0 0 24 24"
|
|
945
|
-
className="h-4 w-4"
|
|
946
|
-
fill="none"
|
|
947
|
-
stroke="currentColor"
|
|
948
|
-
strokeWidth="1.8"
|
|
949
|
-
aria-hidden="true"
|
|
950
|
-
>
|
|
951
|
-
<circle cx="11" cy="11" r="7" />
|
|
952
|
-
<path d="m20 20-3.5-3.5" />
|
|
953
|
-
</svg>
|
|
1011
|
+
<IconSearch />
|
|
954
1012
|
</EmptyIcon>
|
|
955
|
-
<EmptyTitle className="text-
|
|
956
|
-
|
|
1013
|
+
<EmptyTitle data-typo="label" className="text-[12px]">
|
|
1014
|
+
{t("panel.material.emptyMaterialsTitle")}
|
|
1015
|
+
</EmptyTitle>
|
|
1016
|
+
<EmptyDescription data-typo="caption" className="text-[11px]">
|
|
957
1017
|
{t("panel.material.emptyMaterialsDesc")}
|
|
958
1018
|
</EmptyDescription>
|
|
959
1019
|
</Empty>
|
|
960
1020
|
) : null}
|
|
1021
|
+
{!isSearching && (activeCategory?.items.length ?? 0) === 0 ? (
|
|
1022
|
+
<div data-typo="caption" className="px-1 py-3 text-[11px] text-muted-foreground">
|
|
1023
|
+
{t("panel.material.emptyMaterialsTitle")}
|
|
1024
|
+
</div>
|
|
1025
|
+
) : null}
|
|
961
1026
|
</div>
|
|
962
1027
|
</div>
|
|
963
1028
|
</div>
|
|
964
1029
|
</TabsContent>
|
|
965
|
-
<TabsContent
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
className="h-8 text-xs"
|
|
973
|
-
/>
|
|
974
|
-
</div>
|
|
975
|
-
<div className="mb-2 flex items-center justify-between rounded border border-border bg-card px-2 py-1.5">
|
|
976
|
-
<span className="text-[11px] text-muted-foreground">{t("panel.material.referenceOnlyTree")}</span>
|
|
977
|
-
<Switch
|
|
978
|
-
checked={referenceOnlyTree}
|
|
979
|
-
onCheckedChange={setReferenceOnlyTree}
|
|
980
|
-
aria-label={t("panel.material.referenceOnlyTree")}
|
|
981
|
-
/>
|
|
982
|
-
</div>
|
|
1030
|
+
<TabsContent
|
|
1031
|
+
value="tree"
|
|
1032
|
+
forceMount
|
|
1033
|
+
className="mt-0 min-h-0 flex-1 flex-col overflow-hidden"
|
|
1034
|
+
style={{ display: leftTab === "tree" ? "flex" : "none" }}
|
|
1035
|
+
>
|
|
1036
|
+
<div className={`min-h-0 flex-1 overflow-auto px-1.5 py-1.5 text-xs ${themedScrollbarClass}`}>
|
|
983
1037
|
<Collapsible
|
|
984
1038
|
open={isExpanded("root", true)}
|
|
985
1039
|
onOpenChange={(open) => setExpanded("root", open)}
|
|
986
|
-
className="rounded border border-border bg-card"
|
|
987
1040
|
>
|
|
988
|
-
<div className="flex items-center gap-
|
|
1041
|
+
<div className="flex items-center gap-0.5 rounded-sm px-0.5 py-0.5 font-medium hover:bg-accent/40">
|
|
989
1042
|
<CollapsibleTrigger asChild>
|
|
990
1043
|
<button
|
|
991
1044
|
type="button"
|
|
992
|
-
className="flex h-
|
|
1045
|
+
className="flex h-5 w-4 shrink-0 items-center justify-center rounded text-[11px] text-muted-foreground hover:bg-accent"
|
|
993
1046
|
>
|
|
994
1047
|
{isExpanded("root", true) ? "▾" : "▸"}
|
|
995
1048
|
</button>
|
|
996
1049
|
</CollapsibleTrigger>
|
|
997
|
-
<span>root</span>
|
|
1050
|
+
<span className="min-w-0 flex-1 truncate text-[12px]">root</span>
|
|
1051
|
+
<Tooltip>
|
|
1052
|
+
<TooltipTrigger asChild>
|
|
1053
|
+
<button
|
|
1054
|
+
type="button"
|
|
1055
|
+
onClick={() => setReferenceOnlyTree((v) => !v)}
|
|
1056
|
+
aria-label={t("panel.material.referenceOnlyTree")}
|
|
1057
|
+
aria-pressed={referenceOnlyTree}
|
|
1058
|
+
className={[
|
|
1059
|
+
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md border transition-colors",
|
|
1060
|
+
referenceOnlyTree
|
|
1061
|
+
? "border-primary/50 bg-primary/15 text-primary"
|
|
1062
|
+
: "border-border/70 bg-background/80 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
1063
|
+
].join(" ")}
|
|
1064
|
+
>
|
|
1065
|
+
<IconRefFilter active={referenceOnlyTree} />
|
|
1066
|
+
</button>
|
|
1067
|
+
</TooltipTrigger>
|
|
1068
|
+
<TooltipContent className="z-[10100]">
|
|
1069
|
+
{t("panel.material.referenceOnlyTree")}
|
|
1070
|
+
</TooltipContent>
|
|
1071
|
+
</Tooltip>
|
|
998
1072
|
</div>
|
|
999
|
-
<CollapsibleContent
|
|
1073
|
+
<CollapsibleContent>
|
|
1074
|
+
<div className="ml-[7px] border-l border-border/45 pl-1.5">
|
|
1000
1075
|
{layers.map((layer) => {
|
|
1001
1076
|
const layerNodes = elementsByLayer.get(layer.id) ?? [];
|
|
1002
1077
|
const draggingNode = draggingTreeNodeId ? elementsById.get(draggingTreeNodeId) ?? null : null;
|
|
@@ -1028,52 +1103,77 @@ export function MaterialSidebar({
|
|
|
1028
1103
|
open={isExpanded(layerKey, true)}
|
|
1029
1104
|
onOpenChange={(open) => setExpanded(layerKey, open)}
|
|
1030
1105
|
>
|
|
1031
|
-
<
|
|
1106
|
+
<div
|
|
1032
1107
|
className={[
|
|
1033
|
-
"
|
|
1034
|
-
layer.isMapping
|
|
1035
|
-
|
|
1036
|
-
|
|
1108
|
+
"flex min-w-0 items-center gap-0.5 rounded-sm py-0.5 pr-0.5 transition-colors",
|
|
1109
|
+
layer.isMapping ? "text-violet-800 dark:text-violet-200" : "text-foreground",
|
|
1110
|
+
isCurrentDropLayer
|
|
1111
|
+
? canDropIntoLayer
|
|
1112
|
+
? "bg-primary/12 ring-1 ring-inset ring-primary/35"
|
|
1113
|
+
: "bg-destructive/10 ring-1 ring-inset ring-destructive/35"
|
|
1114
|
+
: "hover:bg-accent/50",
|
|
1115
|
+
].join(" ")}
|
|
1116
|
+
onDragOver={(e) => {
|
|
1117
|
+
const hasNodeData =
|
|
1118
|
+
e.dataTransfer.types.includes("application/x-arronqzy-tree-node");
|
|
1119
|
+
if (!hasNodeData) return;
|
|
1120
|
+
if (canDropIntoLayer) {
|
|
1121
|
+
e.preventDefault();
|
|
1122
|
+
e.dataTransfer.dropEffect = "move";
|
|
1123
|
+
} else {
|
|
1124
|
+
e.dataTransfer.dropEffect = "none";
|
|
1125
|
+
}
|
|
1126
|
+
if (dragOverLayerId !== layer.id) setDragOverLayerId(layer.id);
|
|
1127
|
+
}}
|
|
1128
|
+
onDragLeave={() => {
|
|
1129
|
+
if (dragOverLayerId === layer.id) setDragOverLayerId(null);
|
|
1130
|
+
}}
|
|
1131
|
+
onDrop={(e) => {
|
|
1132
|
+
const payload = e.dataTransfer.getData("application/x-arronqzy-tree-node");
|
|
1133
|
+
if (!payload) return;
|
|
1134
|
+
e.preventDefault();
|
|
1135
|
+
try {
|
|
1136
|
+
const data = JSON.parse(payload) as {
|
|
1137
|
+
nodeId?: string;
|
|
1138
|
+
sourceLayerId?: string;
|
|
1139
|
+
};
|
|
1140
|
+
if (!data.nodeId || !layer.id) return;
|
|
1141
|
+
if (!canDropIntoLayer) return;
|
|
1142
|
+
if (data.sourceLayerId === layer.id) return;
|
|
1143
|
+
onMoveNodeToLayer?.(data.nodeId, layer.id);
|
|
1144
|
+
} catch {
|
|
1145
|
+
// ignore invalid payload
|
|
1146
|
+
} finally {
|
|
1147
|
+
setDraggingTreeNodeId(null);
|
|
1148
|
+
setDragOverLayerId(null);
|
|
1149
|
+
}
|
|
1150
|
+
}}
|
|
1151
|
+
title={
|
|
1037
1152
|
isCurrentDropLayer
|
|
1038
1153
|
? canDropIntoLayer
|
|
1039
|
-
? "
|
|
1040
|
-
:
|
|
1041
|
-
: ""
|
|
1042
|
-
|
|
1043
|
-
.filter(Boolean)
|
|
1044
|
-
.join(" ")}
|
|
1154
|
+
? t("panel.material.releaseToMove")
|
|
1155
|
+
: dropBlockReason || t("panel.material.cannotMoveToLayer")
|
|
1156
|
+
: t("panel.material.dragToThisLayer")
|
|
1157
|
+
}
|
|
1045
1158
|
>
|
|
1046
|
-
<
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
) : null}
|
|
1060
|
-
<CollapsibleTrigger asChild>
|
|
1061
|
-
<button
|
|
1062
|
-
type="button"
|
|
1063
|
-
className="flex h-7 w-7 shrink-0 items-center justify-center rounded text-sm text-muted-foreground hover:bg-accent"
|
|
1064
|
-
>
|
|
1065
|
-
{isExpanded(layerKey, true) ? "▾" : "▸"}
|
|
1066
|
-
</button>
|
|
1067
|
-
</CollapsibleTrigger>
|
|
1068
|
-
<span className="min-w-0 flex-1 truncate text-xs font-medium text-foreground">
|
|
1159
|
+
<CollapsibleTrigger asChild>
|
|
1160
|
+
<button
|
|
1161
|
+
type="button"
|
|
1162
|
+
className="flex h-5 w-4 shrink-0 items-center justify-center rounded text-[11px] text-muted-foreground hover:bg-accent"
|
|
1163
|
+
>
|
|
1164
|
+
{isExpanded(layerKey, true) ? "▾" : "▸"}
|
|
1165
|
+
</button>
|
|
1166
|
+
</CollapsibleTrigger>
|
|
1167
|
+
<span className="shrink-0 text-muted-foreground">
|
|
1168
|
+
<IconFolder />
|
|
1169
|
+
</span>
|
|
1170
|
+
<div className="flex min-w-0 flex-1 items-center gap-0.5">
|
|
1171
|
+
<span className="min-w-0 truncate text-[12px] font-medium">
|
|
1069
1172
|
{layer.name}
|
|
1070
|
-
<span className="font-normal text-muted-foreground">
|
|
1071
|
-
({rootNodes.length})
|
|
1072
|
-
</span>
|
|
1073
1173
|
</span>
|
|
1074
1174
|
{layer.isMapping ? (
|
|
1075
1175
|
<span
|
|
1076
|
-
className="shrink-0 rounded
|
|
1176
|
+
className="shrink-0 rounded border border-violet-500/60 bg-violet-500/15 px-1 text-[10px] font-medium"
|
|
1077
1177
|
title={
|
|
1078
1178
|
layer.mappingBaseLayerId
|
|
1079
1179
|
? t("panel.material.mappingLayerWithBaseShort", {
|
|
@@ -1087,85 +1187,24 @@ export function MaterialSidebar({
|
|
|
1087
1187
|
{t("panel.material.mappingLayer")}
|
|
1088
1188
|
</span>
|
|
1089
1189
|
) : null}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
>
|
|
1099
|
-
{
|
|
1100
|
-
</
|
|
1101
|
-
) :
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
? canDropIntoLayer
|
|
1109
|
-
? "border-primary/50 bg-primary/10 text-primary"
|
|
1110
|
-
: "border-destructive/50 bg-destructive/10 text-destructive"
|
|
1111
|
-
: "border-border/50 bg-muted/20 text-muted-foreground hover:border-border hover:bg-muted/35",
|
|
1112
|
-
].join(" ")}
|
|
1113
|
-
onDragOver={(e) => {
|
|
1114
|
-
const hasNodeData =
|
|
1115
|
-
e.dataTransfer.types.includes("application/x-arronqzy-tree-node");
|
|
1116
|
-
if (!hasNodeData) return;
|
|
1117
|
-
if (canDropIntoLayer) {
|
|
1118
|
-
e.preventDefault();
|
|
1119
|
-
e.dataTransfer.dropEffect = "move";
|
|
1120
|
-
} else {
|
|
1121
|
-
e.dataTransfer.dropEffect = "none";
|
|
1122
|
-
}
|
|
1123
|
-
if (dragOverLayerId !== layer.id) setDragOverLayerId(layer.id);
|
|
1124
|
-
}}
|
|
1125
|
-
onDragLeave={() => {
|
|
1126
|
-
if (dragOverLayerId === layer.id) setDragOverLayerId(null);
|
|
1127
|
-
}}
|
|
1128
|
-
onDrop={(e) => {
|
|
1129
|
-
const payload = e.dataTransfer.getData("application/x-arronqzy-tree-node");
|
|
1130
|
-
if (!payload) return;
|
|
1131
|
-
e.preventDefault();
|
|
1132
|
-
try {
|
|
1133
|
-
const data = JSON.parse(payload) as {
|
|
1134
|
-
nodeId?: string;
|
|
1135
|
-
sourceLayerId?: string;
|
|
1136
|
-
};
|
|
1137
|
-
if (!data.nodeId || !layer.id) return;
|
|
1138
|
-
if (!canDropIntoLayer) return;
|
|
1139
|
-
if (data.sourceLayerId === layer.id) return;
|
|
1140
|
-
onMoveNodeToLayer?.(data.nodeId, layer.id);
|
|
1141
|
-
} catch {
|
|
1142
|
-
// ignore invalid payload
|
|
1143
|
-
} finally {
|
|
1144
|
-
setDraggingTreeNodeId(null);
|
|
1145
|
-
setDragOverLayerId(null);
|
|
1146
|
-
}
|
|
1147
|
-
}}
|
|
1148
|
-
title={t("panel.material.dragToThisLayer")}
|
|
1149
|
-
>
|
|
1150
|
-
{draggingTreeNodeId
|
|
1151
|
-
? canDropIntoLayer
|
|
1152
|
-
? t("panel.material.releaseToMove")
|
|
1153
|
-
: dropBlockReason || t("panel.material.cannotMoveToLayer")
|
|
1154
|
-
: t("panel.material.dragToLayer")}
|
|
1155
|
-
</div>
|
|
1156
|
-
<CollapsibleContent>
|
|
1157
|
-
{rootNodes.length === 0 ? (
|
|
1158
|
-
<div className="rounded border border-border/40 bg-muted/15 py-2 pl-3 text-[11px] text-muted-foreground">
|
|
1159
|
-
{t("panel.material.emptyLayer")}
|
|
1160
|
-
</div>
|
|
1161
|
-
) : (
|
|
1162
|
-
rootNodes.map((node) =>
|
|
1163
|
-
renderTreeNode(node, 2, `${layer.id}/${node.id}`, new Set<string>())
|
|
1164
|
-
)
|
|
1165
|
-
)}
|
|
1166
|
-
</CollapsibleContent>
|
|
1167
|
-
</CardContent>
|
|
1168
|
-
</Card>
|
|
1190
|
+
</div>
|
|
1191
|
+
<span className="ml-auto shrink-0 pl-1 tabular-nums text-[11px] text-muted-foreground">
|
|
1192
|
+
{rootNodes.length}
|
|
1193
|
+
</span>
|
|
1194
|
+
</div>
|
|
1195
|
+
<CollapsibleContent>
|
|
1196
|
+
<div className="ml-[7px] border-l border-border/45 pl-1.5">
|
|
1197
|
+
{rootNodes.length === 0 ? (
|
|
1198
|
+
<div className="py-1 pl-1 text-[11px] text-muted-foreground">
|
|
1199
|
+
{t("panel.material.emptyLayer")}
|
|
1200
|
+
</div>
|
|
1201
|
+
) : (
|
|
1202
|
+
rootNodes.map((node) =>
|
|
1203
|
+
renderTreeNode(node, 0, `${layer.id}/${node.id}`, new Set<string>())
|
|
1204
|
+
)
|
|
1205
|
+
)}
|
|
1206
|
+
</div>
|
|
1207
|
+
</CollapsibleContent>
|
|
1169
1208
|
</Collapsible>
|
|
1170
1209
|
);
|
|
1171
1210
|
})}
|
|
@@ -1178,7 +1217,7 @@ export function MaterialSidebar({
|
|
|
1178
1217
|
.filter((node) => nodeMatchesTreeSearch(node, new Set<string>()));
|
|
1179
1218
|
return rootNodes.length === 0;
|
|
1180
1219
|
}) ? (
|
|
1181
|
-
<div className="
|
|
1220
|
+
<div className="my-2">
|
|
1182
1221
|
<Empty className="py-5">
|
|
1183
1222
|
<EmptyIcon className="h-8 w-8">
|
|
1184
1223
|
<svg
|
|
@@ -1200,6 +1239,7 @@ export function MaterialSidebar({
|
|
|
1200
1239
|
</Empty>
|
|
1201
1240
|
</div>
|
|
1202
1241
|
) : null}
|
|
1242
|
+
</div>
|
|
1203
1243
|
</CollapsibleContent>
|
|
1204
1244
|
</Collapsible>
|
|
1205
1245
|
</div>
|