torch-glare 2.1.2 → 2.1.3
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/apps/lib/components/Avatar.tsx +1 -1
- package/apps/lib/components/Card.tsx +68 -54
- package/apps/lib/components/DataViews/DataViewRadio.tsx +47 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +56 -45
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +130 -28
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +32 -2
- package/apps/lib/components/DataViews/FilterPanel.tsx +148 -3
- package/apps/lib/components/DataViews/HeaderSearch.tsx +97 -0
- package/apps/lib/components/DataViews/InboxView.tsx +263 -282
- package/apps/lib/components/DataViews/InboxViewCard.tsx +136 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +264 -153
- package/apps/lib/components/DataViews/PanelControls.tsx +10 -41
- package/apps/lib/components/DataViews/TreeView.tsx +220 -191
- package/apps/lib/components/DataViews/index.ts +6 -0
- package/apps/lib/components/DataViews/types.ts +30 -1
- package/apps/lib/components/Radio.tsx +18 -21
- package/apps/lib/components/Switch.tsx +3 -1
- package/apps/lib/components/Table.tsx +1 -1
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +160 -137
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +221 -93
- package/apps/lib/components/TreeFolder/types.ts +9 -0
- package/apps/lib/layouts/DataViewCard.tsx +76 -0
- package/dist/src/shared/copyComponentsRecursively.js +9 -1
- package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
- package/docs/components/data-views-config-panel.md +204 -0
- package/docs/components/data-views-layout.md +270 -0
- package/package.json +1 -1
|
@@ -1,57 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
4
3
|
import { Switch } from "../Switch";
|
|
5
|
-
import {
|
|
4
|
+
import { DataViewRadio } from "./DataViewRadio";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* DataViews config-panel form controls.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Figma styling stays internal to DataViews. Not re-exported from index.ts.
|
|
9
|
+
* The config panel is wrapped in `data-theme="dark"`, so theme-aware
|
|
10
|
+
* components (DataViewRadio, Switch) render in dark mode automatically. The
|
|
11
|
+
* green-checked Switch hex is the only thing this panel still hardcodes
|
|
12
|
+
* because it sits outside the theme system.
|
|
15
13
|
*/
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
|
-
* Saved View / Default Sort radio row
|
|
19
|
-
* match Figma node 1612:30021. The shared <Radio>/<Label> components impose
|
|
20
|
-
* their own circle size, color tokens, line-height reset and wrapper flex
|
|
21
|
-
* layout that fight this panel's always-dark Figma spec, so the row is
|
|
22
|
-
* hand-built here instead.
|
|
23
|
-
*
|
|
24
|
-
* The whole row IS the Radix Item, so the entire area (circle + label +
|
|
25
|
-
* padding) is one click target. The circle/label are non-interactive visuals.
|
|
16
|
+
* Saved View / Default Sort radio row.
|
|
26
17
|
*
|
|
18
|
+
* Thin wrapper around the reusable {@link DataViewRadio} so the config panel
|
|
19
|
+
* keeps a stable name. The whole row IS the Radix Item, so the entire area
|
|
20
|
+
* (circle + label + padding) is one click target.
|
|
27
21
|
*/
|
|
28
22
|
export function RadioRow({ value, label }: { value: string; label: string }) {
|
|
29
|
-
return
|
|
30
|
-
<RadioGroupPrimitive.Item
|
|
31
|
-
value={value}
|
|
32
|
-
className={cn(
|
|
33
|
-
"group flex w-full items-center gap-1.5 py-1 pl-2",
|
|
34
|
-
"cursor-pointer rounded-[8px] text-left outline-none transition-colors",
|
|
35
|
-
"hover:bg-white/[0.04] focus-visible:bg-white/[0.04]",
|
|
36
|
-
)}
|
|
37
|
-
>
|
|
38
|
-
<span
|
|
39
|
-
className={cn(
|
|
40
|
-
"flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full",
|
|
41
|
-
"border border-[#626467] bg-[rgba(255,255,255,0.05)] transition-colors",
|
|
42
|
-
"group-data-[state=checked]:border-transparent",
|
|
43
|
-
"group-data-[state=checked]:bg-[#005ECC]",
|
|
44
|
-
)}
|
|
45
|
-
>
|
|
46
|
-
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
47
|
-
<span className="h-[6px] w-[6px] rounded-full bg-white" />
|
|
48
|
-
</RadioGroupPrimitive.Indicator>
|
|
49
|
-
</span>
|
|
50
|
-
<span className="text-[14px] font-normal leading-[1.475] text-white">
|
|
51
|
-
{label}
|
|
52
|
-
</span>
|
|
53
|
-
</RadioGroupPrimitive.Item>
|
|
54
|
-
);
|
|
23
|
+
return <DataViewRadio value={value} label={label} />;
|
|
55
24
|
}
|
|
56
25
|
|
|
57
26
|
// Shared <Switch> with the bright-green checked track (#0AC713) from the Figma
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useState } from "react"
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import type {
|
|
5
5
|
DynamicRecord,
|
|
6
6
|
ViewConfig,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
FilterState,
|
|
11
11
|
FilterValue,
|
|
12
12
|
TreeConfig,
|
|
13
|
-
} from "./types"
|
|
13
|
+
} from "./types";
|
|
14
14
|
import {
|
|
15
15
|
applyMove,
|
|
16
16
|
autoDetectTreeShape,
|
|
@@ -20,31 +20,34 @@ import {
|
|
|
20
20
|
initialExpansion,
|
|
21
21
|
pruneTree,
|
|
22
22
|
type TreeNode,
|
|
23
|
-
} from "../../utils/dataViews/treeUtils"
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
23
|
+
} from "../../utils/dataViews/treeUtils";
|
|
24
|
+
import {
|
|
25
|
+
getByPath,
|
|
26
|
+
matchesFilterValues,
|
|
27
|
+
} from "../../utils/dataViews/pathUtils";
|
|
28
|
+
import { visibleFields } from "../../utils/dataViews/fieldUtils";
|
|
29
|
+
import { renderField } from "./fieldRenderers";
|
|
30
|
+
import { useIsMobile } from "../../hooks/useIsMobile";
|
|
31
|
+
import { TableView } from "./TableView";
|
|
32
|
+
import { FilterPanel } from "./FilterPanel";
|
|
33
|
+
import { TreeSidebar } from "./tree/TreeSidebar";
|
|
34
|
+
import { TreeDrawer, TreeDrawerTrigger } from "./tree/TreeDrawer";
|
|
35
|
+
import { Card, CardContent, CardHeader } from "../Card";
|
|
36
|
+
import { Table2, LayoutGrid } from "lucide-react";
|
|
37
|
+
import { cn } from "../../utils/cn";
|
|
35
38
|
|
|
36
39
|
export type TreeViewProps = {
|
|
37
|
-
data: DynamicRecord[]
|
|
38
|
-
columns?: DynamicColumnConfig[]
|
|
39
|
-
fields: FieldConfig[]
|
|
40
|
-
config: ViewConfig
|
|
41
|
-
treeConfig?: TreeConfig
|
|
42
|
-
onDataUpdate?: (data: DynamicRecord[]) => void
|
|
43
|
-
filters?: DynamicFilterConfig[]
|
|
44
|
-
filterState?: FilterState
|
|
45
|
-
onFilterChange?: (filters: FilterState) => void
|
|
46
|
-
showFilters?: boolean
|
|
47
|
-
}
|
|
40
|
+
data: DynamicRecord[];
|
|
41
|
+
columns?: DynamicColumnConfig[];
|
|
42
|
+
fields: FieldConfig[];
|
|
43
|
+
config: ViewConfig;
|
|
44
|
+
treeConfig?: TreeConfig;
|
|
45
|
+
onDataUpdate?: (data: DynamicRecord[]) => void;
|
|
46
|
+
filters?: DynamicFilterConfig[];
|
|
47
|
+
filterState?: FilterState;
|
|
48
|
+
onFilterChange?: (filters: FilterState) => void;
|
|
49
|
+
showFilters?: boolean;
|
|
50
|
+
};
|
|
48
51
|
|
|
49
52
|
export function TreeView({
|
|
50
53
|
data,
|
|
@@ -58,108 +61,118 @@ export function TreeView({
|
|
|
58
61
|
onFilterChange,
|
|
59
62
|
showFilters = true,
|
|
60
63
|
}: TreeViewProps) {
|
|
61
|
-
const isMobile = useIsMobile()
|
|
62
|
-
const [internalFilters, setInternalFilters] = useState<FilterState>({})
|
|
63
|
-
const activeFilters = externalFilterState ?? internalFilters
|
|
64
|
+
const isMobile = useIsMobile();
|
|
65
|
+
const [internalFilters, setInternalFilters] = useState<FilterState>({});
|
|
66
|
+
const activeFilters = externalFilterState ?? internalFilters;
|
|
64
67
|
|
|
65
68
|
const resolvedTree = useMemo(
|
|
66
69
|
() => autoDetectTreeShape(data, treeConfig ?? {}),
|
|
67
70
|
[data, treeConfig],
|
|
68
|
-
)
|
|
71
|
+
);
|
|
69
72
|
|
|
70
73
|
const display = useMemo(
|
|
71
74
|
() => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
|
72
75
|
[fields],
|
|
73
|
-
)
|
|
76
|
+
);
|
|
74
77
|
|
|
75
78
|
const labelField: FieldConfig = useMemo(() => {
|
|
76
|
-
const path = resolvedTree.nodeLabel
|
|
79
|
+
const path = resolvedTree.nodeLabel;
|
|
77
80
|
if (path) {
|
|
78
|
-
const f = fields.find((x) => x.path === path)
|
|
79
|
-
if (f) return f
|
|
80
|
-
return { path, label: path, type: "text" }
|
|
81
|
+
const f = fields.find((x) => x.path === path);
|
|
82
|
+
if (f) return f;
|
|
83
|
+
return { path, label: path, type: "text" };
|
|
81
84
|
}
|
|
82
|
-
return display[0] ?? { path: resolvedTree.idField, type: "text" }
|
|
83
|
-
}, [resolvedTree, fields, display])
|
|
85
|
+
return display[0] ?? { path: resolvedTree.idField, type: "text" };
|
|
86
|
+
}, [resolvedTree, fields, display]);
|
|
84
87
|
|
|
85
88
|
const fullForest = useMemo(
|
|
86
89
|
() => buildTree(data, resolvedTree),
|
|
87
90
|
[data, resolvedTree],
|
|
88
|
-
)
|
|
91
|
+
);
|
|
89
92
|
|
|
90
|
-
const filterEntries = useMemo(
|
|
93
|
+
const filterEntries = useMemo(
|
|
94
|
+
() => Object.entries(activeFilters),
|
|
95
|
+
[activeFilters],
|
|
96
|
+
);
|
|
91
97
|
|
|
92
98
|
const visibleForest: TreeNode[] = useMemo(() => {
|
|
93
|
-
if (filterEntries.length === 0) return fullForest
|
|
99
|
+
if (filterEntries.length === 0) return fullForest;
|
|
94
100
|
return pruneTree(fullForest, (record) =>
|
|
95
|
-
filterEntries.every(([path, value]) =>
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
filterEntries.every(([path, value]) =>
|
|
102
|
+
matchesFilterValues(record, path, value),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}, [fullForest, filterEntries]);
|
|
98
106
|
|
|
99
107
|
const [expanded, setExpanded] = useState<Set<string>>(() =>
|
|
100
108
|
initialExpansion(fullForest, resolvedTree.defaultExpanded),
|
|
101
|
-
)
|
|
109
|
+
);
|
|
102
110
|
|
|
103
111
|
useEffect(() => {
|
|
104
|
-
setExpanded(initialExpansion(fullForest, resolvedTree.defaultExpanded))
|
|
105
|
-
}, [fullForest, resolvedTree.defaultExpanded])
|
|
112
|
+
setExpanded(initialExpansion(fullForest, resolvedTree.defaultExpanded));
|
|
113
|
+
}, [fullForest, resolvedTree.defaultExpanded]);
|
|
106
114
|
|
|
107
|
-
const [selectedId, setSelectedId] = useState<string | null>(
|
|
108
|
-
fullForest[0]?.id ?? null,
|
|
109
|
-
)
|
|
115
|
+
const [selectedId, setSelectedId] = useState<string | null>(
|
|
116
|
+
() => fullForest[0]?.id ?? null,
|
|
117
|
+
);
|
|
110
118
|
|
|
111
119
|
useEffect(() => {
|
|
112
120
|
if (selectedId && !findNodeById(visibleForest, selectedId)) {
|
|
113
|
-
setSelectedId(visibleForest[0]?.id ?? null)
|
|
121
|
+
setSelectedId(visibleForest[0]?.id ?? null);
|
|
114
122
|
}
|
|
115
|
-
}, [visibleForest, selectedId])
|
|
123
|
+
}, [visibleForest, selectedId]);
|
|
116
124
|
|
|
117
|
-
const selectedNode = selectedId
|
|
125
|
+
const selectedNode = selectedId
|
|
126
|
+
? findNodeById(visibleForest, selectedId)
|
|
127
|
+
: null;
|
|
118
128
|
const recordsForRightPane = useMemo(
|
|
119
129
|
() => (selectedNode ? flatten(selectedNode) : []),
|
|
120
130
|
[selectedNode],
|
|
121
|
-
)
|
|
131
|
+
);
|
|
122
132
|
|
|
123
133
|
const toggle = (id: string) =>
|
|
124
134
|
setExpanded((prev) => {
|
|
125
|
-
const next = new Set(prev)
|
|
126
|
-
if (next.has(id)) next.delete(id)
|
|
127
|
-
else next.add(id)
|
|
128
|
-
return next
|
|
129
|
-
})
|
|
135
|
+
const next = new Set(prev);
|
|
136
|
+
if (next.has(id)) next.delete(id);
|
|
137
|
+
else next.add(id);
|
|
138
|
+
return next;
|
|
139
|
+
});
|
|
130
140
|
|
|
131
141
|
const handleFilterChange = (path: string, value: FilterValue) => {
|
|
132
|
-
const next: FilterState = { ...activeFilters, [path]: value }
|
|
133
|
-
if (onFilterChange) onFilterChange(next)
|
|
134
|
-
else setInternalFilters(next)
|
|
135
|
-
}
|
|
142
|
+
const next: FilterState = { ...activeFilters, [path]: value };
|
|
143
|
+
if (onFilterChange) onFilterChange(next);
|
|
144
|
+
else setInternalFilters(next);
|
|
145
|
+
};
|
|
136
146
|
const clearAllFilters = () => {
|
|
137
|
-
if (onFilterChange) onFilterChange({})
|
|
138
|
-
else setInternalFilters({})
|
|
139
|
-
}
|
|
147
|
+
if (onFilterChange) onFilterChange({});
|
|
148
|
+
else setInternalFilters({});
|
|
149
|
+
};
|
|
140
150
|
|
|
141
|
-
const [drawerOpen, setDrawerOpen] = useState(false)
|
|
151
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
142
152
|
|
|
143
|
-
type RightPaneMode = "table" | "
|
|
153
|
+
type RightPaneMode = "table" | "card";
|
|
144
154
|
const [rightPaneMode, setRightPaneMode] = useState<RightPaneMode>(
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
// "details" is a deprecated alias of "card".
|
|
156
|
+
treeConfig?.defaultRightPane === "details"
|
|
157
|
+
? "card"
|
|
158
|
+
: (treeConfig?.defaultRightPane ?? "table"),
|
|
159
|
+
);
|
|
147
160
|
|
|
148
|
-
const dndEnabled = treeConfig?.dndEnabled !== false && !!onDataUpdate
|
|
161
|
+
const dndEnabled = treeConfig?.dndEnabled !== false && !!onDataUpdate;
|
|
149
162
|
|
|
150
163
|
const handleMove = ({
|
|
151
164
|
dragIds,
|
|
152
165
|
parentId,
|
|
153
166
|
index,
|
|
154
167
|
}: {
|
|
155
|
-
dragIds: string[]
|
|
156
|
-
parentId: string | null
|
|
157
|
-
index: number
|
|
168
|
+
dragIds: string[];
|
|
169
|
+
parentId: string | null;
|
|
170
|
+
index: number;
|
|
158
171
|
}) => {
|
|
159
|
-
if (!onDataUpdate) return
|
|
160
|
-
const next = applyMove(data, resolvedTree, { dragIds, parentId, index })
|
|
161
|
-
onDataUpdate(next)
|
|
162
|
-
}
|
|
172
|
+
if (!onDataUpdate) return;
|
|
173
|
+
const next = applyMove(data, resolvedTree, { dragIds, parentId, index });
|
|
174
|
+
onDataUpdate(next);
|
|
175
|
+
};
|
|
163
176
|
|
|
164
177
|
const treeContent = (
|
|
165
178
|
<TreeSidebar
|
|
@@ -170,29 +183,25 @@ export function TreeView({
|
|
|
170
183
|
dndEnabled={dndEnabled}
|
|
171
184
|
onToggle={toggle}
|
|
172
185
|
onSelect={(id) => {
|
|
173
|
-
setSelectedId(id)
|
|
174
|
-
if (isMobile) setDrawerOpen(false)
|
|
186
|
+
setSelectedId(id);
|
|
187
|
+
if (isMobile) setDrawerOpen(false);
|
|
175
188
|
}}
|
|
176
189
|
onMove={handleMove}
|
|
177
190
|
/>
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
const filtersEnabled = showFilters && config.showFilters !== false
|
|
191
|
+
);
|
|
181
192
|
|
|
182
|
-
const
|
|
183
|
-
id: f.path,
|
|
184
|
-
label: f.label ?? f.path,
|
|
185
|
-
visible: f.visible !== false && f.type !== "hidden",
|
|
186
|
-
order: f.order ?? i,
|
|
187
|
-
}))
|
|
193
|
+
const filtersEnabled = showFilters && config.showFilters !== false;
|
|
188
194
|
|
|
189
195
|
return (
|
|
190
|
-
<div className="flex h-full
|
|
196
|
+
<div className="flex h-full gap-2">
|
|
191
197
|
{!isMobile && (
|
|
192
|
-
<div className="w-64
|
|
198
|
+
<div className="w-64 rounded-[16px] border border-border-presentation-global-primary bg-background-presentation-form-base overflow-hidden flex flex-col">
|
|
193
199
|
<div className="px-3 py-2 border-b border-border-presentation-global-primary">
|
|
194
|
-
<span
|
|
195
|
-
|
|
200
|
+
<span
|
|
201
|
+
style={{ fontFeatureSettings: "'cv05' on" }}
|
|
202
|
+
className="typography-display-medium-medium uppercase text-content-presentation-global-primary"
|
|
203
|
+
>
|
|
204
|
+
categories
|
|
196
205
|
</span>
|
|
197
206
|
</div>
|
|
198
207
|
<div className="flex-1 overflow-hidden">{treeContent}</div>
|
|
@@ -210,50 +219,67 @@ export function TreeView({
|
|
|
210
219
|
/>
|
|
211
220
|
)}
|
|
212
221
|
|
|
213
|
-
<div className="flex-1 flex flex-col overflow-hidden">
|
|
214
|
-
<div className="flex items-center gap-2 px-3 py-2 border-b border-border-presentation-global-primary bg-background-presentation-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
aria-pressed={rightPaneMode === "table"}
|
|
223
|
-
className={cn(
|
|
224
|
-
"inline-flex items-center gap-1 px-2 py-1 rounded text-xs",
|
|
225
|
-
rightPaneMode === "table"
|
|
226
|
-
? "bg-content-presentation-action-primary text-white"
|
|
227
|
-
: "text-content-presentation-global-secondary hover:text-content-presentation-global-primary",
|
|
228
|
-
)}
|
|
222
|
+
<div className="flex-1 flex flex-col overflow-hidden rounded-[16px] border border-border-presentation-global-primary bg-background-presentation-form-base">
|
|
223
|
+
<div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-border-presentation-global-primary bg-background-presentation-form-base">
|
|
224
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
225
|
+
{isMobile && (
|
|
226
|
+
<TreeDrawerTrigger onClick={() => setDrawerOpen(true)} />
|
|
227
|
+
)}
|
|
228
|
+
<span
|
|
229
|
+
style={{ fontFeatureSettings: "'cv05' on" }}
|
|
230
|
+
className="typography-display-medium-medium uppercase text-content-presentation-global-primary truncate"
|
|
229
231
|
>
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
rightPaneMode === "details"
|
|
241
|
-
? "bg-content-presentation-action-primary text-white"
|
|
242
|
-
: "text-content-presentation-global-secondary hover:text-content-presentation-global-primary",
|
|
243
|
-
)}
|
|
244
|
-
>
|
|
245
|
-
<FileText className="h-3.5 w-3.5" />
|
|
246
|
-
Details
|
|
247
|
-
</button>
|
|
232
|
+
{selectedNode
|
|
233
|
+
? String(getByPath(selectedNode.record, labelField.path) ?? "Items")
|
|
234
|
+
: "Items"}
|
|
235
|
+
</span>
|
|
236
|
+
<div className="h-6 w-px bg-border-presentation-global-primary shrink-0" />
|
|
237
|
+
<span className="text-sm text-content-presentation-global-secondary truncate">
|
|
238
|
+
{selectedNode
|
|
239
|
+
? `${recordsForRightPane.length} record${recordsForRightPane.length === 1 ? "" : "s"}`
|
|
240
|
+
: "Select an item"}
|
|
241
|
+
</span>
|
|
248
242
|
</div>
|
|
249
243
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
244
|
+
{/* Segmented switcher — same style as the main view switcher
|
|
245
|
+
(DataViewsHeader): #252729 track, white active pill, divider
|
|
246
|
+
between two inactive tabs only. */}
|
|
247
|
+
<div className="flex items-center gap-[2px] rounded-[10px] bg-background-presentation-body-primary p-[2px] shadow-[inset_0_0_4px_0_rgba(0,0,0,0.08)] shrink-0">
|
|
248
|
+
{(
|
|
249
|
+
[
|
|
250
|
+
{ id: "table", label: "List", icon: <Table2 /> },
|
|
251
|
+
{ id: "card", label: "Cards", icon: <LayoutGrid /> },
|
|
252
|
+
] as const
|
|
253
|
+
).map((tab, idx) => {
|
|
254
|
+
const active = rightPaneMode === tab.id;
|
|
255
|
+
const prevActive = idx > 0 && rightPaneMode === "table";
|
|
256
|
+
const showDivider = idx > 0 && !active && !prevActive;
|
|
257
|
+
return (
|
|
258
|
+
<div key={tab.id} className="flex items-center">
|
|
259
|
+
{showDivider && (
|
|
260
|
+
<div className="mx-[3px] h-3 w-px bg-[#434446]" />
|
|
261
|
+
)}
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
aria-label={`${tab.label} mode`}
|
|
265
|
+
aria-pressed={active}
|
|
266
|
+
onClick={() => setRightPaneMode(tab.id)}
|
|
267
|
+
className={cn(
|
|
268
|
+
"flex h-6 items-center gap-[6px] rounded-[8px] px-3 text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
|
|
269
|
+
active
|
|
270
|
+
? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
|
|
271
|
+
: "bg-transparent text-content-presentation-global-primary hover:bg-white/5",
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
274
|
+
<span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
|
|
275
|
+
{tab.icon}
|
|
276
|
+
</span>
|
|
277
|
+
<span className="max-w-[80px] truncate">{tab.label}</span>
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
})}
|
|
282
|
+
</div>
|
|
257
283
|
</div>
|
|
258
284
|
|
|
259
285
|
<div className="flex-1 overflow-hidden">
|
|
@@ -268,16 +294,15 @@ export function TreeView({
|
|
|
268
294
|
filters={filterConfig}
|
|
269
295
|
filterState={activeFilters}
|
|
270
296
|
onFilterChange={(next) => {
|
|
271
|
-
if (onFilterChange) onFilterChange(next)
|
|
272
|
-
else setInternalFilters(next)
|
|
297
|
+
if (onFilterChange) onFilterChange(next);
|
|
298
|
+
else setInternalFilters(next);
|
|
273
299
|
}}
|
|
274
300
|
showFilters={false}
|
|
275
301
|
/>
|
|
276
302
|
) : (
|
|
277
|
-
<
|
|
278
|
-
|
|
303
|
+
<CardGrid
|
|
304
|
+
records={recordsForRightPane}
|
|
279
305
|
fields={fields}
|
|
280
|
-
columns={fallbackColumns}
|
|
281
306
|
labelField={labelField}
|
|
282
307
|
/>
|
|
283
308
|
)
|
|
@@ -295,69 +320,73 @@ export function TreeView({
|
|
|
295
320
|
</TreeDrawer>
|
|
296
321
|
)}
|
|
297
322
|
</div>
|
|
298
|
-
)
|
|
323
|
+
);
|
|
299
324
|
}
|
|
300
325
|
|
|
301
|
-
|
|
302
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Card mode for the Tree right pane: renders the same record set as Table
|
|
328
|
+
* mode, one library <Card> per record. The label field is the card header;
|
|
329
|
+
* the remaining visible fields are key/value rows in the card body.
|
|
330
|
+
*/
|
|
331
|
+
function CardGrid({
|
|
332
|
+
records,
|
|
303
333
|
fields,
|
|
304
|
-
columns,
|
|
305
334
|
labelField,
|
|
306
335
|
}: {
|
|
307
|
-
|
|
308
|
-
fields: FieldConfig[]
|
|
309
|
-
|
|
310
|
-
labelField: FieldConfig
|
|
336
|
+
records: DynamicRecord[];
|
|
337
|
+
fields: FieldConfig[];
|
|
338
|
+
labelField: FieldConfig;
|
|
311
339
|
}) {
|
|
312
|
-
const
|
|
313
|
-
const labelValue = getByPath(record, labelField.path)
|
|
314
|
-
const displayFields = visibleFields(fields)
|
|
340
|
+
const bodyFields = visibleFields(fields)
|
|
315
341
|
.filter((f) => f.path !== labelField.path)
|
|
316
|
-
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
342
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
317
343
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<div className="
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
<h2 className="text-2xl font-semibold text-content-presentation-global-primary">
|
|
326
|
-
{renderField(labelValue, labelField, record)}
|
|
327
|
-
</h2>
|
|
328
|
-
</div>
|
|
344
|
+
if (records.length === 0) {
|
|
345
|
+
return (
|
|
346
|
+
<div className="h-full flex items-center justify-center text-sm text-content-presentation-global-tertiary">
|
|
347
|
+
No records.
|
|
348
|
+
</div>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
329
351
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
<dd className="text-sm text-content-presentation-global-primary">
|
|
341
|
-
{renderField(value, f, record)}
|
|
342
|
-
</dd>
|
|
352
|
+
return (
|
|
353
|
+
<div className="h-full overflow-y-auto p-4 bg-background-presentation-body-primary">
|
|
354
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
355
|
+
{records.map((record, idx) => {
|
|
356
|
+
const labelValue = getByPath(record, labelField.path);
|
|
357
|
+
return (
|
|
358
|
+
<Card key={record.id ?? idx} className="overflow-hidden">
|
|
359
|
+
<CardHeader className="pb-2">
|
|
360
|
+
<div className="text-xs uppercase tracking-wide text-content-presentation-global-tertiary">
|
|
361
|
+
{labelField.label ?? labelField.path}
|
|
343
362
|
</div>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
363
|
+
<div className="text-base font-semibold text-content-presentation-global-primary">
|
|
364
|
+
{renderField(labelValue, labelField, record)}
|
|
365
|
+
</div>
|
|
366
|
+
</CardHeader>
|
|
367
|
+
<CardContent className="space-y-2 pt-0">
|
|
368
|
+
{bodyFields.map((f) => {
|
|
369
|
+
const value = getByPath(record, f.path);
|
|
370
|
+
if (value == null) return null;
|
|
371
|
+
return (
|
|
372
|
+
<div
|
|
373
|
+
key={f.path}
|
|
374
|
+
className="flex items-center justify-between gap-3 text-sm"
|
|
375
|
+
>
|
|
376
|
+
<span className="text-content-presentation-global-tertiary">
|
|
377
|
+
{f.label ?? f.path}
|
|
378
|
+
</span>
|
|
379
|
+
<span className="text-content-presentation-global-primary text-right">
|
|
380
|
+
{renderField(value, f, record)}
|
|
381
|
+
</span>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
})}
|
|
385
|
+
</CardContent>
|
|
386
|
+
</Card>
|
|
387
|
+
);
|
|
388
|
+
})}
|
|
360
389
|
</div>
|
|
361
390
|
</div>
|
|
362
|
-
)
|
|
391
|
+
);
|
|
363
392
|
}
|
|
@@ -10,9 +10,15 @@ export type { KanbanViewProps } from "./KanbanView"
|
|
|
10
10
|
export { InboxView } from "./InboxView"
|
|
11
11
|
export type { InboxViewProps } from "./InboxView"
|
|
12
12
|
|
|
13
|
+
export { InboxViewCard } from "./InboxViewCard"
|
|
14
|
+
export type { InboxViewCardProps } from "./InboxViewCard"
|
|
15
|
+
|
|
13
16
|
export { TreeView } from "./TreeView"
|
|
14
17
|
export type { TreeViewProps } from "./TreeView"
|
|
15
18
|
|
|
19
|
+
export { DataViewRadio } from "./DataViewRadio"
|
|
20
|
+
export type { DataViewRadioProps } from "./DataViewRadio"
|
|
21
|
+
|
|
16
22
|
export { FilterPanel } from "./FilterPanel"
|
|
17
23
|
export { SettingsPanel } from "./SettingsPanel"
|
|
18
24
|
export { DataViewsHeader } from "./DataViewsHeader"
|