torch-glare 2.1.1 → 2.1.2

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.
Files changed (62) hide show
  1. package/apps/lib/components/BadgeField.tsx +2 -2
  2. package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
  3. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
  4. package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
  5. package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
  6. package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
  7. package/apps/lib/components/DataViews/InboxView.tsx +514 -0
  8. package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
  9. package/apps/lib/components/DataViews/PanelControls.tsx +80 -0
  10. package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
  11. package/apps/lib/components/DataViews/TableView.tsx +232 -0
  12. package/apps/lib/components/DataViews/TreeView.tsx +363 -0
  13. package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
  14. package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
  15. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
  16. package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
  17. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
  18. package/apps/lib/components/DataViews/index.ts +30 -0
  19. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
  20. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
  21. package/apps/lib/components/DataViews/types.ts +177 -0
  22. package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
  23. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
  24. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -0
  25. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
  26. package/apps/lib/components/TreeFolder/icons.tsx +63 -0
  27. package/apps/lib/components/TreeFolder/index.ts +17 -0
  28. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
  29. package/apps/lib/components/TreeFolder/types.ts +68 -0
  30. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
  31. package/apps/lib/hooks/useDataViewsState.ts +169 -0
  32. package/apps/lib/hooks/useIsMobile.ts +21 -0
  33. package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
  34. package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
  35. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
  36. package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
  37. package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
  38. package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
  39. package/dist/bin/index.js +3 -3
  40. package/dist/bin/index.js.map +1 -1
  41. package/dist/src/commands/add.d.ts.map +1 -1
  42. package/dist/src/commands/add.js +29 -6
  43. package/dist/src/commands/add.js.map +1 -1
  44. package/dist/src/commands/utils.d.ts.map +1 -1
  45. package/dist/src/commands/utils.js +22 -2
  46. package/dist/src/commands/utils.js.map +1 -1
  47. package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
  48. package/dist/src/shared/copyComponentsRecursively.js +8 -1
  49. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  50. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
  51. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
  52. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
  53. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
  54. package/docs/components/form-stepper.md +244 -0
  55. package/docs/components/stepper.md +215 -0
  56. package/docs/components/timeline.md +248 -0
  57. package/package.json +6 -6
  58. package/apps/lib/components/Charts-dev.tsx +0 -365
  59. package/apps/lib/components/Command-dev.tsx +0 -151
  60. package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
  61. /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
  62. /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
@@ -0,0 +1,232 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { FilterPanel } from "./FilterPanel";
5
+ import type {
6
+ DynamicRecord,
7
+ ViewConfig,
8
+ DynamicColumnConfig,
9
+ DynamicFilterConfig,
10
+ FilterState,
11
+ FilterValue,
12
+ FieldConfig,
13
+ } from "./types";
14
+ import { Card, CardContent, CardHeader } from "../Card";
15
+ import { Checkbox } from "../Checkbox";
16
+ import {
17
+ Table,
18
+ TableHeader,
19
+ TableBody,
20
+ TableHead,
21
+ TableRow,
22
+ TableCell,
23
+ TableCheckbox,
24
+ } from "../Table";
25
+ import {
26
+ getByPath,
27
+ matchesFilterValues,
28
+ } from "../../utils/dataViews/pathUtils";
29
+ import { renderField } from "./fieldRenderers";
30
+ import { visibleFields } from "../../utils/dataViews/fieldUtils";
31
+ import { useIsMobile } from "../../hooks/useIsMobile";
32
+
33
+ export type TableViewProps = {
34
+ data: DynamicRecord[];
35
+ columns?: DynamicColumnConfig[];
36
+ fields: FieldConfig[];
37
+ config: ViewConfig;
38
+ onDataUpdate?: (data: DynamicRecord[]) => void;
39
+ onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void;
40
+ filters?: DynamicFilterConfig[];
41
+ filterState?: FilterState;
42
+ onFilterChange?: (filters: FilterState) => void;
43
+ showFilters?: boolean;
44
+ };
45
+
46
+ export function TableView({
47
+ data,
48
+ fields,
49
+ config,
50
+ onSortChange,
51
+ filters: filterConfig,
52
+ filterState: externalFilterState,
53
+ onFilterChange,
54
+ showFilters = true,
55
+ }: TableViewProps) {
56
+ const isMobile = useIsMobile();
57
+ const [internalFilters, setInternalFilters] = useState<FilterState>({});
58
+
59
+ const activeFilters = externalFilterState ?? internalFilters;
60
+
61
+ const sortPath = config.sortBy || null;
62
+ const sortDirection: "asc" | "desc" = config.sortOrder ?? "asc";
63
+
64
+ const handleSort = (path: string) => {
65
+ if (!onSortChange) return;
66
+ if (sortPath === path) {
67
+ onSortChange(path, sortDirection === "asc" ? "desc" : "asc");
68
+ } else {
69
+ onSortChange(path, "asc");
70
+ }
71
+ };
72
+
73
+ const handleFilterChange = (path: string, value: FilterValue) => {
74
+ const newFilters: FilterState = { ...activeFilters, [path]: value };
75
+ if (onFilterChange) onFilterChange(newFilters);
76
+ else setInternalFilters(newFilters);
77
+ };
78
+
79
+ const clearAllFilters = () => {
80
+ if (onFilterChange) onFilterChange({});
81
+ else setInternalFilters({});
82
+ };
83
+
84
+ const filteredAndSortedData = useMemo(() => {
85
+ let filtered = data.filter((item) =>
86
+ Object.entries(activeFilters).every(([path, filterValues]) =>
87
+ matchesFilterValues(item, path, filterValues),
88
+ ),
89
+ );
90
+
91
+ if (sortPath) {
92
+ filtered = [...filtered].sort((a, b) => {
93
+ const aVal = getByPath(a, sortPath);
94
+ const bVal = getByPath(b, sortPath);
95
+ const modifier = sortDirection === "asc" ? 1 : -1;
96
+
97
+ if (aVal == null && bVal == null) return 0;
98
+ if (aVal == null) return 1;
99
+ if (bVal == null) return -1;
100
+
101
+ if (typeof aVal === "string" && typeof bVal === "string") {
102
+ return aVal.localeCompare(bVal) * modifier;
103
+ }
104
+ if (typeof aVal === "number" && typeof bVal === "number") {
105
+ return (aVal - bVal) * modifier;
106
+ }
107
+ return String(aVal).localeCompare(String(bVal)) * modifier;
108
+ });
109
+ }
110
+
111
+ return filtered;
112
+ }, [data, activeFilters, sortPath, sortDirection]);
113
+
114
+ const displayFields = useMemo(
115
+ () => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
116
+ [fields],
117
+ );
118
+
119
+ const filtersEnabled = showFilters && config.showFilters !== false;
120
+
121
+ return (
122
+ <div className="flex h-full bg-background-presentation-form-base">
123
+ {filtersEnabled && !isMobile && (
124
+ <FilterPanel
125
+ data={data}
126
+ fields={fields}
127
+ filters={activeFilters}
128
+ onFilterChange={handleFilterChange}
129
+ onClearAll={clearAllFilters}
130
+ filterConfig={filterConfig}
131
+ />
132
+ )}
133
+
134
+ <div className="flex flex-1 flex-col gap-4 overflow-hidden">
135
+ {!isMobile ? (
136
+ <div className="flex-1 overflow-auto rounded-lg">
137
+ <Table className="w-full">
138
+ <TableHeader>
139
+ <TableRow>
140
+ <TableHead isDummy className="w-12">
141
+ <Checkbox />
142
+ </TableHead>
143
+ {displayFields.map((field) => (
144
+ <TableHead
145
+ key={field.path}
146
+ size="M"
147
+ sortType={
148
+ sortPath === field.path ? sortDirection : undefined
149
+ }
150
+ onSort={() => handleSort(field.path)}
151
+ >
152
+ {field.label}
153
+ </TableHead>
154
+ ))}
155
+ </TableRow>
156
+ </TableHeader>
157
+ <TableBody>
158
+ {filteredAndSortedData.map((item, idx) => (
159
+ <TableRow key={item.id ?? idx}>
160
+ <TableCell isDummy className="w-12">
161
+ <TableCheckbox id={`row-${item.id ?? idx}`} />
162
+ </TableCell>
163
+ {displayFields.map((field) => (
164
+ <TableCell key={field.path}>
165
+ {/* `isolate` confines the Badge's mix-blend-luminosity
166
+ to a local stacking context and `transform-gpu`
167
+ promotes it to its own layer, so the table's
168
+ post-mount column reflow repaints cleanly instead
169
+ of leaving a ghosted/doubled badge frame. */}
170
+ <span className="isolate inline-flex transform-gpu">
171
+ {renderField(
172
+ getByPath(item, field.path),
173
+ field,
174
+ item,
175
+ )}
176
+ </span>
177
+ </TableCell>
178
+ ))}
179
+ </TableRow>
180
+ ))}
181
+ </TableBody>
182
+ </Table>
183
+ </div>
184
+ ) : (
185
+ <div className="flex-1 overflow-auto">
186
+ <div className="grid gap-3">
187
+ {filteredAndSortedData.map((item, idx) => (
188
+ <Card key={item.id ?? idx} className="overflow-hidden">
189
+ <CardHeader className="pb-3">
190
+ <div className="flex items-start justify-between gap-3">
191
+ <div className="flex items-start gap-3 flex-1">
192
+ <Checkbox className="mt-1" />
193
+ <div className="flex-1">
194
+ {displayFields[0] && (
195
+ <p className="font-medium">
196
+ {String(
197
+ getByPath(item, displayFields[0].path) ?? "",
198
+ )}
199
+ </p>
200
+ )}
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </CardHeader>
205
+ <CardContent className="space-y-2 pt-0">
206
+ {displayFields.slice(1).map((field) => (
207
+ <div
208
+ key={field.path}
209
+ className="flex items-center justify-between text-sm"
210
+ >
211
+ <span className="text-content-presentation-global-tertiary">
212
+ {field.label}:
213
+ </span>
214
+ <span>
215
+ {renderField(
216
+ getByPath(item, field.path),
217
+ field,
218
+ item,
219
+ )}
220
+ </span>
221
+ </div>
222
+ ))}
223
+ </CardContent>
224
+ </Card>
225
+ ))}
226
+ </div>
227
+ </div>
228
+ )}
229
+ </div>
230
+ </div>
231
+ );
232
+ }
@@ -0,0 +1,363 @@
1
+ "use client"
2
+
3
+ import { useEffect, useMemo, useState } from "react"
4
+ import type {
5
+ DynamicRecord,
6
+ ViewConfig,
7
+ DynamicColumnConfig,
8
+ DynamicFilterConfig,
9
+ FieldConfig,
10
+ FilterState,
11
+ FilterValue,
12
+ TreeConfig,
13
+ } from "./types"
14
+ import {
15
+ applyMove,
16
+ autoDetectTreeShape,
17
+ buildTree,
18
+ findNodeById,
19
+ flatten,
20
+ initialExpansion,
21
+ pruneTree,
22
+ type TreeNode,
23
+ } from "../../utils/dataViews/treeUtils"
24
+ import { getByPath, matchesFilterValues } from "../../utils/dataViews/pathUtils"
25
+ import { visibleFields } from "../../utils/dataViews/fieldUtils"
26
+ import { renderField } from "./fieldRenderers"
27
+ import { renderDetailView } from "../../utils/dataViews/nestedDataUtils"
28
+ import { useIsMobile } from "../../hooks/useIsMobile"
29
+ import { TableView } from "./TableView"
30
+ import { FilterPanel } from "./FilterPanel"
31
+ import { TreeSidebar } from "./tree/TreeSidebar"
32
+ import { TreeDrawer, TreeDrawerTrigger } from "./tree/TreeDrawer"
33
+ import { Table2, FileText } from "lucide-react"
34
+ import { cn } from "../../utils/cn"
35
+
36
+ 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
+ }
48
+
49
+ export function TreeView({
50
+ data,
51
+ columns,
52
+ fields,
53
+ config,
54
+ treeConfig,
55
+ onDataUpdate,
56
+ filters: filterConfig,
57
+ filterState: externalFilterState,
58
+ onFilterChange,
59
+ showFilters = true,
60
+ }: TreeViewProps) {
61
+ const isMobile = useIsMobile()
62
+ const [internalFilters, setInternalFilters] = useState<FilterState>({})
63
+ const activeFilters = externalFilterState ?? internalFilters
64
+
65
+ const resolvedTree = useMemo(
66
+ () => autoDetectTreeShape(data, treeConfig ?? {}),
67
+ [data, treeConfig],
68
+ )
69
+
70
+ const display = useMemo(
71
+ () => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
72
+ [fields],
73
+ )
74
+
75
+ const labelField: FieldConfig = useMemo(() => {
76
+ const path = resolvedTree.nodeLabel
77
+ if (path) {
78
+ const f = fields.find((x) => x.path === path)
79
+ if (f) return f
80
+ return { path, label: path, type: "text" }
81
+ }
82
+ return display[0] ?? { path: resolvedTree.idField, type: "text" }
83
+ }, [resolvedTree, fields, display])
84
+
85
+ const fullForest = useMemo(
86
+ () => buildTree(data, resolvedTree),
87
+ [data, resolvedTree],
88
+ )
89
+
90
+ const filterEntries = useMemo(() => Object.entries(activeFilters), [activeFilters])
91
+
92
+ const visibleForest: TreeNode[] = useMemo(() => {
93
+ if (filterEntries.length === 0) return fullForest
94
+ return pruneTree(fullForest, (record) =>
95
+ filterEntries.every(([path, value]) => matchesFilterValues(record, path, value)),
96
+ )
97
+ }, [fullForest, filterEntries])
98
+
99
+ const [expanded, setExpanded] = useState<Set<string>>(() =>
100
+ initialExpansion(fullForest, resolvedTree.defaultExpanded),
101
+ )
102
+
103
+ useEffect(() => {
104
+ setExpanded(initialExpansion(fullForest, resolvedTree.defaultExpanded))
105
+ }, [fullForest, resolvedTree.defaultExpanded])
106
+
107
+ const [selectedId, setSelectedId] = useState<string | null>(() =>
108
+ fullForest[0]?.id ?? null,
109
+ )
110
+
111
+ useEffect(() => {
112
+ if (selectedId && !findNodeById(visibleForest, selectedId)) {
113
+ setSelectedId(visibleForest[0]?.id ?? null)
114
+ }
115
+ }, [visibleForest, selectedId])
116
+
117
+ const selectedNode = selectedId ? findNodeById(visibleForest, selectedId) : null
118
+ const recordsForRightPane = useMemo(
119
+ () => (selectedNode ? flatten(selectedNode) : []),
120
+ [selectedNode],
121
+ )
122
+
123
+ const toggle = (id: string) =>
124
+ 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
+ })
130
+
131
+ const handleFilterChange = (path: string, value: FilterValue) => {
132
+ const next: FilterState = { ...activeFilters, [path]: value }
133
+ if (onFilterChange) onFilterChange(next)
134
+ else setInternalFilters(next)
135
+ }
136
+ const clearAllFilters = () => {
137
+ if (onFilterChange) onFilterChange({})
138
+ else setInternalFilters({})
139
+ }
140
+
141
+ const [drawerOpen, setDrawerOpen] = useState(false)
142
+
143
+ type RightPaneMode = "table" | "details"
144
+ const [rightPaneMode, setRightPaneMode] = useState<RightPaneMode>(
145
+ treeConfig?.defaultRightPane ?? "table",
146
+ )
147
+
148
+ const dndEnabled = treeConfig?.dndEnabled !== false && !!onDataUpdate
149
+
150
+ const handleMove = ({
151
+ dragIds,
152
+ parentId,
153
+ index,
154
+ }: {
155
+ dragIds: string[]
156
+ parentId: string | null
157
+ index: number
158
+ }) => {
159
+ if (!onDataUpdate) return
160
+ const next = applyMove(data, resolvedTree, { dragIds, parentId, index })
161
+ onDataUpdate(next)
162
+ }
163
+
164
+ const treeContent = (
165
+ <TreeSidebar
166
+ roots={visibleForest}
167
+ expanded={expanded}
168
+ selectedId={selectedId}
169
+ labelField={labelField}
170
+ dndEnabled={dndEnabled}
171
+ onToggle={toggle}
172
+ onSelect={(id) => {
173
+ setSelectedId(id)
174
+ if (isMobile) setDrawerOpen(false)
175
+ }}
176
+ onMove={handleMove}
177
+ />
178
+ )
179
+
180
+ const filtersEnabled = showFilters && config.showFilters !== false
181
+
182
+ const fallbackColumns: DynamicColumnConfig[] = columns ?? fields.map((f, i) => ({
183
+ id: f.path,
184
+ label: f.label ?? f.path,
185
+ visible: f.visible !== false && f.type !== "hidden",
186
+ order: f.order ?? i,
187
+ }))
188
+
189
+ return (
190
+ <div className="flex h-full bg-background-presentation-body-primary">
191
+ {!isMobile && (
192
+ <div className="w-64 border-r border-border-presentation-global-primary bg-background-presentation-body-overlay-primary flex flex-col">
193
+ <div className="px-3 py-2 border-b border-border-presentation-global-primary">
194
+ <span className="text-xs font-semibold text-content-presentation-global-secondary uppercase tracking-wide">
195
+ Tree
196
+ </span>
197
+ </div>
198
+ <div className="flex-1 overflow-hidden">{treeContent}</div>
199
+ </div>
200
+ )}
201
+
202
+ {!isMobile && filtersEnabled && (
203
+ <FilterPanel
204
+ data={data}
205
+ fields={fields}
206
+ filters={activeFilters}
207
+ onFilterChange={handleFilterChange}
208
+ onClearAll={clearAllFilters}
209
+ filterConfig={filterConfig}
210
+ />
211
+ )}
212
+
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-body-primary">
215
+ {isMobile && <TreeDrawerTrigger onClick={() => setDrawerOpen(true)} />}
216
+
217
+ <div className="flex items-center gap-1 rounded-md border border-border-presentation-global-primary bg-background-presentation-body-overlay-primary p-0.5">
218
+ <button
219
+ type="button"
220
+ onClick={() => setRightPaneMode("table")}
221
+ aria-label="Table mode"
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
+ )}
229
+ >
230
+ <Table2 className="h-3.5 w-3.5" />
231
+ Table
232
+ </button>
233
+ <button
234
+ type="button"
235
+ onClick={() => setRightPaneMode("details")}
236
+ aria-label="Details mode"
237
+ aria-pressed={rightPaneMode === "details"}
238
+ className={cn(
239
+ "inline-flex items-center gap-1 px-2 py-1 rounded text-xs",
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>
248
+ </div>
249
+
250
+ <span className="ml-auto text-sm text-content-presentation-global-secondary truncate">
251
+ {selectedNode
252
+ ? rightPaneMode === "table"
253
+ ? `${recordsForRightPane.length} record${recordsForRightPane.length === 1 ? "" : "s"}`
254
+ : `1 record`
255
+ : "Select an item"}
256
+ </span>
257
+ </div>
258
+
259
+ <div className="flex-1 overflow-hidden">
260
+ {selectedNode ? (
261
+ rightPaneMode === "table" ? (
262
+ <TableView
263
+ data={recordsForRightPane}
264
+ columns={columns}
265
+ fields={fields}
266
+ config={{ ...config, showFilters: false }}
267
+ onDataUpdate={onDataUpdate}
268
+ filters={filterConfig}
269
+ filterState={activeFilters}
270
+ onFilterChange={(next) => {
271
+ if (onFilterChange) onFilterChange(next)
272
+ else setInternalFilters(next)
273
+ }}
274
+ showFilters={false}
275
+ />
276
+ ) : (
277
+ <DetailsBody
278
+ node={selectedNode}
279
+ fields={fields}
280
+ columns={fallbackColumns}
281
+ labelField={labelField}
282
+ />
283
+ )
284
+ ) : (
285
+ <div className="h-full flex items-center justify-center text-sm text-content-presentation-global-tertiary">
286
+ No node selected.
287
+ </div>
288
+ )}
289
+ </div>
290
+ </div>
291
+
292
+ {isMobile && (
293
+ <TreeDrawer open={drawerOpen} onOpenChange={setDrawerOpen}>
294
+ {treeContent}
295
+ </TreeDrawer>
296
+ )}
297
+ </div>
298
+ )
299
+ }
300
+
301
+ function DetailsBody({
302
+ node,
303
+ fields,
304
+ columns,
305
+ labelField,
306
+ }: {
307
+ node: TreeNode
308
+ fields: FieldConfig[]
309
+ columns: DynamicColumnConfig[]
310
+ labelField: FieldConfig
311
+ }) {
312
+ const record = node.record
313
+ const labelValue = getByPath(record, labelField.path)
314
+ const displayFields = visibleFields(fields)
315
+ .filter((f) => f.path !== labelField.path)
316
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
317
+
318
+ return (
319
+ <div className="h-full overflow-y-auto p-6 bg-background-presentation-body-primary">
320
+ <div className="max-w-3xl mx-auto space-y-6">
321
+ <div className="space-y-1">
322
+ <div className="text-xs uppercase tracking-wide text-content-presentation-global-tertiary">
323
+ {labelField.label ?? labelField.path}
324
+ </div>
325
+ <h2 className="text-2xl font-semibold text-content-presentation-global-primary">
326
+ {renderField(labelValue, labelField, record)}
327
+ </h2>
328
+ </div>
329
+
330
+ {displayFields.length > 0 && (
331
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 pb-2 border-b border-border-presentation-global-primary">
332
+ {displayFields.map((f) => {
333
+ const value = getByPath(record, f.path)
334
+ if (value == null) return null
335
+ return (
336
+ <div key={f.path} className="space-y-1">
337
+ <dt className="text-xs font-medium uppercase tracking-wide text-content-presentation-global-tertiary">
338
+ {f.label ?? f.path}
339
+ </dt>
340
+ <dd className="text-sm text-content-presentation-global-primary">
341
+ {renderField(value, f, record)}
342
+ </dd>
343
+ </div>
344
+ )
345
+ })}
346
+ </div>
347
+ )}
348
+
349
+ <div>
350
+ {renderDetailView(
351
+ record,
352
+ columns.filter((c) => c.visible),
353
+ (value, column, row) => {
354
+ const f = fields.find((x) => x.path === column.id)
355
+ if (f) return renderField(value, f, row)
356
+ return <span>{String(value ?? "")}</span>
357
+ },
358
+ )}
359
+ </div>
360
+ </div>
361
+ </div>
362
+ )
363
+ }
@@ -0,0 +1,45 @@
1
+ import type { BadgeVariant } from "./types"
2
+
3
+ type TorchBadgeColor =
4
+ | "gray"
5
+ | "slate"
6
+ | "red"
7
+ | "orange"
8
+ | "yellow"
9
+ | "green"
10
+ | "ocean"
11
+ | "blue"
12
+ | "purple"
13
+ | "rose"
14
+
15
+ type TorchBadgeStyle = "solid" | "subtle"
16
+
17
+ export type ResolvedBadgeProps = {
18
+ color: TorchBadgeColor
19
+ badgeStyle: TorchBadgeStyle
20
+ }
21
+
22
+ /**
23
+ * Map the dataviews BadgeVariant set to torch-glare Badge props.
24
+ * Defaults to `subtle` style (glare's native default — colored dot + subtle bg).
25
+ * The legacy "Light" / "navy" / "bluePurple" variants explicitly request solid
26
+ * fills, kept distinct for backwards compat with the data-views examples.
27
+ */
28
+ export function resolveBadgeVariant(variant?: BadgeVariant): ResolvedBadgeProps {
29
+ switch (variant) {
30
+ case "green": return { color: "green", badgeStyle: "subtle" }
31
+ case "greenLight": return { color: "green", badgeStyle: "subtle" }
32
+ case "cocktailGreen": return { color: "green", badgeStyle: "solid" }
33
+ case "yellow": return { color: "yellow", badgeStyle: "subtle" }
34
+ case "redOrange": return { color: "orange", badgeStyle: "subtle" }
35
+ case "redLight": return { color: "red", badgeStyle: "subtle" }
36
+ case "rose": return { color: "rose", badgeStyle: "subtle" }
37
+ case "purple": return { color: "purple", badgeStyle: "subtle" }
38
+ case "bluePurple": return { color: "purple", badgeStyle: "solid" }
39
+ case "blue": return { color: "blue", badgeStyle: "subtle" }
40
+ case "navy": return { color: "blue", badgeStyle: "solid" }
41
+ case "highlight": return { color: "yellow", badgeStyle: "subtle" }
42
+ case "gray":
43
+ default: return { color: "gray", badgeStyle: "subtle" }
44
+ }
45
+ }