torch-glare 2.4.5 → 2.5.1

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 (197) hide show
  1. package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
  2. package/apps/lib/components/DataViews/cell.tsx +324 -0
  3. package/apps/lib/components/DataViews/context.ts +144 -0
  4. package/apps/lib/components/DataViews/data-views.tsx +383 -0
  5. package/apps/lib/components/DataViews/filters/children.tsx +98 -0
  6. package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
  7. package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
  8. package/apps/lib/components/DataViews/filters/index.ts +4 -0
  9. package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
  10. package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
  11. package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
  12. package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
  13. package/apps/lib/components/DataViews/filters/values.ts +173 -0
  14. package/apps/lib/components/DataViews/header.tsx +217 -0
  15. package/apps/lib/components/DataViews/hooks/index.ts +5 -0
  16. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
  17. package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
  18. package/apps/lib/components/DataViews/index.ts +74 -26
  19. package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
  20. package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
  21. package/apps/lib/components/DataViews/panel/index.ts +3 -0
  22. package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
  23. package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
  24. package/apps/lib/components/DataViews/panel/section.tsx +79 -0
  25. package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
  26. package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
  27. package/apps/lib/components/DataViews/slots.ts +63 -0
  28. package/apps/lib/components/DataViews/states.tsx +38 -0
  29. package/apps/lib/components/DataViews/types.ts +485 -178
  30. package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
  31. package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
  32. package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
  33. package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
  34. package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
  35. package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
  36. package/apps/lib/components/FormBuilder/context.ts +20 -6
  37. package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
  38. package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
  39. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
  40. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +7 -6
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +1 -1
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +55 -3
  45. package/apps/lib/components/FormBuilder/index.ts +3 -1
  46. package/apps/lib/components/FormBuilder/types.ts +40 -0
  47. package/apps/lib/components/TabSwitch.tsx +16 -4
  48. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  49. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  50. package/apps/lib/components/TreeFolder/index.ts +1 -1
  51. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  52. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  53. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  54. package/apps/lib/registry.json +216 -4
  55. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  56. package/apps/lib/utils/dataViews/path.ts +67 -0
  57. package/apps/lib/utils/dataViews/query.ts +73 -0
  58. package/apps/lib/utils/dataViews/types.ts +187 -0
  59. package/dist/bin/index.js +8 -4
  60. package/dist/bin/index.js.map +1 -1
  61. package/dist/src/commands/add.d.ts +3 -2
  62. package/dist/src/commands/add.d.ts.map +1 -1
  63. package/dist/src/commands/add.js +27 -35
  64. package/dist/src/commands/add.js.map +1 -1
  65. package/dist/src/commands/hook.d.ts.map +1 -1
  66. package/dist/src/commands/hook.js +23 -13
  67. package/dist/src/commands/hook.js.map +1 -1
  68. package/dist/src/commands/init.d.ts.map +1 -1
  69. package/dist/src/commands/init.js +6 -2
  70. package/dist/src/commands/init.js.map +1 -1
  71. package/dist/src/commands/layout.d.ts.map +1 -1
  72. package/dist/src/commands/layout.js +23 -13
  73. package/dist/src/commands/layout.js.map +1 -1
  74. package/dist/src/commands/provider.d.ts.map +1 -1
  75. package/dist/src/commands/provider.js +22 -12
  76. package/dist/src/commands/provider.js.map +1 -1
  77. package/dist/src/commands/utils.d.ts.map +1 -1
  78. package/dist/src/commands/utils.js +21 -28
  79. package/dist/src/commands/utils.js.map +1 -1
  80. package/dist/src/shared/copyComponentsRecursively.d.ts +5 -3
  81. package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
  82. package/dist/src/shared/copyComponentsRecursively.js +5 -6
  83. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  84. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +1 -1
  85. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
  86. package/dist/src/shared/installDependencies.d.ts +16 -1
  87. package/dist/src/shared/installDependencies.d.ts.map +1 -1
  88. package/dist/src/shared/installDependencies.js +41 -21
  89. package/dist/src/shared/installDependencies.js.map +1 -1
  90. package/dist/src/shared/installFromPlan.d.ts +23 -0
  91. package/dist/src/shared/installFromPlan.d.ts.map +1 -0
  92. package/dist/src/shared/installFromPlan.js +77 -0
  93. package/dist/src/shared/installFromPlan.js.map +1 -0
  94. package/dist/src/shared/resolveEntry.d.ts +21 -0
  95. package/dist/src/shared/resolveEntry.d.ts.map +1 -0
  96. package/dist/src/shared/resolveEntry.js +54 -0
  97. package/dist/src/shared/resolveEntry.js.map +1 -0
  98. package/dist/src/shared/suggestOtherCommand.d.ts +8 -0
  99. package/dist/src/shared/suggestOtherCommand.d.ts.map +1 -0
  100. package/dist/src/shared/suggestOtherCommand.js +34 -0
  101. package/dist/src/shared/suggestOtherCommand.js.map +1 -0
  102. package/dist/src/shared/tailwindInit.d.ts +3 -1
  103. package/dist/src/shared/tailwindInit.d.ts.map +1 -1
  104. package/dist/src/shared/tailwindInit.js +3 -1
  105. package/dist/src/shared/tailwindInit.js.map +1 -1
  106. package/dist/src/shared/wireStylesheet.d.ts +32 -0
  107. package/dist/src/shared/wireStylesheet.d.ts.map +1 -0
  108. package/dist/src/shared/wireStylesheet.js +92 -0
  109. package/dist/src/shared/wireStylesheet.js.map +1 -0
  110. package/dist/src/types/main.d.ts +6 -0
  111. package/dist/src/types/main.d.ts.map +1 -1
  112. package/docs/components/breadcrumb.md +1 -1
  113. package/docs/components/button-group.md +1 -1
  114. package/docs/components/button.md +1 -1
  115. package/docs/components/card.md +1 -1
  116. package/docs/components/checkbox.md +1 -1
  117. package/docs/components/data-views/backend-response.md +324 -0
  118. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  119. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  120. package/docs/components/data-views/examples/fields.md +362 -0
  121. package/docs/components/data-views/examples/filters.md +308 -0
  122. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  123. package/docs/components/data-views/examples/index.md +29 -0
  124. package/docs/components/data-views/examples/overview.md +244 -0
  125. package/docs/components/data-views/examples/panel.md +212 -0
  126. package/docs/components/data-views/examples/scale.md +231 -0
  127. package/docs/components/data-views/examples/server-side.md +210 -0
  128. package/docs/components/data-views/examples/state.md +250 -0
  129. package/docs/components/data-views/examples/tree-custom.md +388 -0
  130. package/docs/components/data-views/examples/view-registry.md +313 -0
  131. package/docs/components/data-views/examples/views.md +534 -0
  132. package/docs/components/data-views/guide.md +405 -0
  133. package/docs/components/data-views/index.md +1504 -0
  134. package/docs/components/data-views/migration.md +79 -0
  135. package/docs/components/date-picker.md +0 -1
  136. package/docs/components/form-builder.md +3 -2
  137. package/docs/components/form-renderer.md +2 -1
  138. package/docs/components/form.md +1 -1
  139. package/docs/components/input-field.md +1 -1
  140. package/docs/components/input-otp.md +1 -1
  141. package/docs/components/input.md +1 -1
  142. package/docs/components/labeled-check-box.md +1 -1
  143. package/docs/components/labeled-radio.md +1 -1
  144. package/docs/components/radio-card.md +1 -1
  145. package/docs/components/radio.md +1 -1
  146. package/docs/components/search-field.md +1 -1
  147. package/docs/components/section-block.md +1 -1
  148. package/docs/components/select.md +1 -1
  149. package/docs/components/simple-select.md +1 -1
  150. package/docs/components/switch.md +1 -1
  151. package/docs/components/tab-switch.md +1 -1
  152. package/docs/components/table.md +1 -1
  153. package/docs/components/text-editor.md +1 -1
  154. package/docs/components/textarea.md +1 -1
  155. package/docs/components/toggle-button.md +1 -1
  156. package/docs/components/toggle.md +1 -1
  157. package/docs/components/tree-folder.md +110 -0
  158. package/docs/how-to/forms-with-form-builder.md +2 -2
  159. package/docs/reference/cli.md +28 -3
  160. package/docs/reference/components.md +16 -6
  161. package/docs/tutorials/component-composition.md +11 -13
  162. package/docs/tutorials/getting-started.md +7 -0
  163. package/package.json +3 -2
  164. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  165. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  166. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  167. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  168. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  169. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  170. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  171. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  172. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  173. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  174. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  175. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  176. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  177. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  178. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  179. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  180. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  181. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  182. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  183. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  184. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  185. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  186. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  187. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  188. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  189. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  190. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  191. package/docs/components/data-views-config-panel.md +0 -208
  192. package/docs/components/data-views-layout.md +0 -291
  193. package/docs/components/inbox-view.md +0 -170
  194. package/docs/components/kanban-view.md +0 -135
  195. package/docs/components/table-view.md +0 -141
  196. package/docs/components/tree-view.md +0 -147
  197. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -1,212 +0,0 @@
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 { getByPath, matchesFilterValues } from "../../utils/dataViews/pathUtils";
26
- import { renderField } from "./fieldRenderers";
27
- import { visibleFields } from "../../utils/dataViews/fieldUtils";
28
- import { useIsMobile } from "../../hooks/useIsMobile";
29
-
30
- export type TableViewProps = {
31
- data: DynamicRecord[];
32
- columns?: DynamicColumnConfig[];
33
- fields: FieldConfig[];
34
- config: ViewConfig;
35
- onDataUpdate?: (data: DynamicRecord[]) => void;
36
- onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void;
37
- filters?: DynamicFilterConfig[];
38
- filterState?: FilterState;
39
- onFilterChange?: (filters: FilterState) => void;
40
- showFilters?: boolean;
41
- };
42
-
43
- export function TableView({
44
- data,
45
- fields,
46
- config,
47
- onSortChange,
48
- filters: filterConfig,
49
- filterState: externalFilterState,
50
- onFilterChange,
51
- showFilters = true,
52
- }: TableViewProps) {
53
- const isMobile = useIsMobile();
54
- const [internalFilters, setInternalFilters] = useState<FilterState>({});
55
-
56
- const activeFilters = externalFilterState ?? internalFilters;
57
-
58
- const sortPath = config.sortBy || null;
59
- const sortDirection: "asc" | "desc" = config.sortOrder ?? "asc";
60
-
61
- const handleSort = (path: string) => {
62
- if (!onSortChange) return;
63
- if (sortPath === path) {
64
- onSortChange(path, sortDirection === "asc" ? "desc" : "asc");
65
- } else {
66
- onSortChange(path, "asc");
67
- }
68
- };
69
-
70
- const handleFilterChange = (path: string, value: FilterValue) => {
71
- const newFilters: FilterState = { ...activeFilters, [path]: value };
72
- if (onFilterChange) onFilterChange(newFilters);
73
- else setInternalFilters(newFilters);
74
- };
75
-
76
- const clearAllFilters = () => {
77
- if (onFilterChange) onFilterChange({});
78
- else setInternalFilters({});
79
- };
80
-
81
- const filteredAndSortedData = useMemo(() => {
82
- let filtered = data.filter((item) =>
83
- Object.entries(activeFilters).every(([path, filterValues]) =>
84
- matchesFilterValues(item, path, filterValues),
85
- ),
86
- );
87
-
88
- if (sortPath) {
89
- filtered = [...filtered].sort((a, b) => {
90
- const aVal = getByPath(a, sortPath);
91
- const bVal = getByPath(b, sortPath);
92
- const modifier = sortDirection === "asc" ? 1 : -1;
93
-
94
- if (aVal == null && bVal == null) return 0;
95
- if (aVal == null) return 1;
96
- if (bVal == null) return -1;
97
-
98
- if (typeof aVal === "string" && typeof bVal === "string") {
99
- return aVal.localeCompare(bVal) * modifier;
100
- }
101
- if (typeof aVal === "number" && typeof bVal === "number") {
102
- return (aVal - bVal) * modifier;
103
- }
104
- return String(aVal).localeCompare(String(bVal)) * modifier;
105
- });
106
- }
107
-
108
- return filtered;
109
- }, [data, activeFilters, sortPath, sortDirection]);
110
-
111
- const displayFields = useMemo(
112
- () => visibleFields(fields).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
113
- [fields],
114
- );
115
-
116
- const filtersEnabled = showFilters && config.showFilters !== false;
117
-
118
- return (
119
- <div className="flex h-full bg-background-presentation-form-base">
120
- {filtersEnabled && !isMobile && (
121
- <FilterPanel
122
- data={data}
123
- fields={fields}
124
- filters={activeFilters}
125
- onFilterChange={handleFilterChange}
126
- onClearAll={clearAllFilters}
127
- filterConfig={filterConfig}
128
- />
129
- )}
130
-
131
- <div className="flex flex-1 flex-col gap-4 overflow-hidden">
132
- {!isMobile ? (
133
- <div className="flex-1 overflow-auto rounded-lg">
134
- <Table className="w-full">
135
- <TableHeader>
136
- <TableRow>
137
- <TableHead isDummy className="w-12">
138
- <Checkbox />
139
- </TableHead>
140
- {displayFields.map((field) => (
141
- <TableHead
142
- key={field.path}
143
- size="M"
144
- sortType={sortPath === field.path ? sortDirection : undefined}
145
- onSort={() => handleSort(field.path)}
146
- >
147
- {field.label}
148
- </TableHead>
149
- ))}
150
- </TableRow>
151
- </TableHeader>
152
- <TableBody>
153
- {filteredAndSortedData.map((item, idx) => (
154
- <TableRow key={(item.id ?? idx) as string | number}>
155
- <TableCell isDummy className="w-12">
156
- <TableCheckbox id={`row-${item.id ?? idx}`} />
157
- </TableCell>
158
- {displayFields.map((field) => (
159
- <TableCell key={field.path}>
160
- {/* `isolate` confines the Badge's mix-blend-luminosity
161
- to a local stacking context and `transform-gpu`
162
- promotes it to its own layer, so the table's
163
- post-mount column reflow repaints cleanly instead
164
- of leaving a ghosted/doubled badge frame. */}
165
- <span className="isolate inline-flex transform-gpu">
166
- {renderField(getByPath(item, field.path), field, item)}
167
- </span>
168
- </TableCell>
169
- ))}
170
- </TableRow>
171
- ))}
172
- </TableBody>
173
- </Table>
174
- </div>
175
- ) : (
176
- <div className="flex-1 overflow-auto">
177
- <div className="grid gap-3">
178
- {filteredAndSortedData.map((item, idx) => (
179
- <Card key={(item.id ?? idx) as string | number} className="overflow-hidden">
180
- <CardHeader className="pb-3">
181
- <div className="flex items-start justify-between gap-3">
182
- <div className="flex items-start gap-3 flex-1">
183
- <Checkbox className="mt-1" />
184
- <div className="flex-1">
185
- {displayFields[0] && (
186
- <p className="font-medium">
187
- {String(getByPath(item, displayFields[0].path) ?? "")}
188
- </p>
189
- )}
190
- </div>
191
- </div>
192
- </div>
193
- </CardHeader>
194
- <CardContent className="space-y-2 pt-0">
195
- {displayFields.slice(1).map((field) => (
196
- <div key={field.path} className="flex items-center justify-between text-sm">
197
- <span className="text-content-presentation-global-tertiary">
198
- {field.label}:
199
- </span>
200
- <span>{renderField(getByPath(item, field.path), field, item)}</span>
201
- </div>
202
- ))}
203
- </CardContent>
204
- </Card>
205
- ))}
206
- </div>
207
- </div>
208
- )}
209
- </div>
210
- </div>
211
- );
212
- }
@@ -1,364 +0,0 @@
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 { useIsMobile } from "../../hooks/useIsMobile";
28
- import { TableView } from "./TableView";
29
- import { FilterPanel } from "./FilterPanel";
30
- import { TreeSidebar } from "./tree/TreeSidebar";
31
- import { TreeDrawer, TreeDrawerTrigger } from "./tree/TreeDrawer";
32
- import { Card, CardContent, CardHeader } from "../Card";
33
- import { Table2, LayoutGrid } 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(() => buildTree(data, resolvedTree), [data, resolvedTree]);
86
-
87
- const filterEntries = useMemo(() => Object.entries(activeFilters), [activeFilters]);
88
-
89
- const visibleForest: TreeNode[] = useMemo(() => {
90
- if (filterEntries.length === 0) return fullForest;
91
- return pruneTree(fullForest, (record) =>
92
- filterEntries.every(([path, value]) => matchesFilterValues(record, path, value)),
93
- );
94
- }, [fullForest, filterEntries]);
95
-
96
- const [expanded, setExpanded] = useState<Set<string>>(() =>
97
- initialExpansion(fullForest, resolvedTree.defaultExpanded),
98
- );
99
-
100
- useEffect(() => {
101
- setExpanded(initialExpansion(fullForest, resolvedTree.defaultExpanded));
102
- }, [fullForest, resolvedTree.defaultExpanded]);
103
-
104
- const [selectedId, setSelectedId] = useState<string | null>(() => fullForest[0]?.id ?? null);
105
-
106
- useEffect(() => {
107
- if (selectedId && !findNodeById(visibleForest, selectedId)) {
108
- setSelectedId(visibleForest[0]?.id ?? null);
109
- }
110
- }, [visibleForest, selectedId]);
111
-
112
- const selectedNode = selectedId ? findNodeById(visibleForest, selectedId) : null;
113
- const recordsForRightPane = useMemo(
114
- () => (selectedNode ? flatten(selectedNode) : []),
115
- [selectedNode],
116
- );
117
-
118
- const toggle = (id: string) =>
119
- setExpanded((prev) => {
120
- const next = new Set(prev);
121
- if (next.has(id)) next.delete(id);
122
- else next.add(id);
123
- return next;
124
- });
125
-
126
- const handleFilterChange = (path: string, value: FilterValue) => {
127
- const next: FilterState = { ...activeFilters, [path]: value };
128
- if (onFilterChange) onFilterChange(next);
129
- else setInternalFilters(next);
130
- };
131
- const clearAllFilters = () => {
132
- if (onFilterChange) onFilterChange({});
133
- else setInternalFilters({});
134
- };
135
-
136
- const [drawerOpen, setDrawerOpen] = useState(false);
137
-
138
- type RightPaneMode = "table" | "card";
139
- const [rightPaneMode, setRightPaneMode] = useState<RightPaneMode>(
140
- // "details" is a deprecated alias of "card".
141
- treeConfig?.defaultRightPane === "details" ? "card" : (treeConfig?.defaultRightPane ?? "table"),
142
- );
143
-
144
- const dndEnabled = treeConfig?.dndEnabled !== false && !!onDataUpdate;
145
-
146
- const handleMove = ({
147
- dragIds,
148
- parentId,
149
- index,
150
- }: {
151
- dragIds: string[];
152
- parentId: string | null;
153
- index: number;
154
- }) => {
155
- if (!onDataUpdate) return;
156
- const next = applyMove(data, resolvedTree, { dragIds, parentId, index });
157
- onDataUpdate(next);
158
- };
159
-
160
- const treeContent = (
161
- <TreeSidebar
162
- roots={visibleForest}
163
- expanded={expanded}
164
- selectedId={selectedId}
165
- labelField={labelField}
166
- dndEnabled={dndEnabled}
167
- onToggle={toggle}
168
- onSelect={(id) => {
169
- setSelectedId(id);
170
- if (isMobile) setDrawerOpen(false);
171
- }}
172
- onMove={handleMove}
173
- />
174
- );
175
-
176
- const filtersEnabled = showFilters && config.showFilters !== false;
177
-
178
- return (
179
- <div className="flex h-full gap-2">
180
- {!isMobile && (
181
- <div className="w-64 rounded-[16px] border border-border-presentation-global-primary bg-background-presentation-form-base overflow-hidden flex flex-col">
182
- <div className="px-3 py-2 border-b border-border-presentation-global-primary">
183
- <span
184
- style={{ fontFeatureSettings: "'cv05' on" }}
185
- className="typography-display-medium-medium uppercase text-content-presentation-global-primary"
186
- >
187
- categories
188
- </span>
189
- </div>
190
- <div className="flex-1 overflow-hidden">{treeContent}</div>
191
- </div>
192
- )}
193
-
194
- {!isMobile && filtersEnabled && (
195
- <FilterPanel
196
- data={data}
197
- fields={fields}
198
- filters={activeFilters}
199
- onFilterChange={handleFilterChange}
200
- onClearAll={clearAllFilters}
201
- filterConfig={filterConfig}
202
- />
203
- )}
204
-
205
- <div className="flex-1 flex flex-col overflow-hidden rounded-[16px] border border-border-presentation-global-primary bg-background-presentation-form-base">
206
- <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">
207
- <div className="flex items-center gap-2 min-w-0">
208
- {isMobile && <TreeDrawerTrigger onClick={() => setDrawerOpen(true)} />}
209
- <span
210
- style={{ fontFeatureSettings: "'cv05' on" }}
211
- className="typography-display-medium-medium uppercase text-content-presentation-global-primary truncate"
212
- >
213
- {selectedNode
214
- ? String(getByPath(selectedNode.record, labelField.path) ?? "Items")
215
- : "Items"}
216
- </span>
217
- <div className="h-6 w-px bg-border-presentation-global-primary shrink-0" />
218
- <span className="text-sm text-content-presentation-global-secondary truncate">
219
- {selectedNode
220
- ? `${recordsForRightPane.length} record${recordsForRightPane.length === 1 ? "" : "s"}`
221
- : "Select an item"}
222
- </span>
223
- </div>
224
-
225
- {/* Segmented switcher — same style as the main view switcher
226
- (DataViewsHeader): #252729 track, white active pill, divider
227
- between two inactive tabs only. */}
228
- <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">
229
- {(
230
- [
231
- { id: "table", label: "List", icon: <Table2 /> },
232
- { id: "card", label: "Cards", icon: <LayoutGrid /> },
233
- ] as const
234
- ).map((tab, idx) => {
235
- const active = rightPaneMode === tab.id;
236
- const prevActive = idx > 0 && rightPaneMode === "table";
237
- const showDivider = idx > 0 && !active && !prevActive;
238
- return (
239
- <div key={tab.id} className="flex items-center">
240
- {showDivider && <div className="mx-[3px] h-3 w-px bg-[#434446]" />}
241
- <button
242
- type="button"
243
- aria-label={`${tab.label} mode`}
244
- aria-pressed={active}
245
- onClick={() => setRightPaneMode(tab.id)}
246
- className={cn(
247
- "flex h-6 items-center gap-[6px] rounded-[8px] px-3 text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
248
- active
249
- ? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
250
- : "bg-transparent text-content-presentation-global-primary hover:bg-white/5",
251
- )}
252
- >
253
- <span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
254
- {tab.icon}
255
- </span>
256
- <span className="max-w-[80px] truncate">{tab.label}</span>
257
- </button>
258
- </div>
259
- );
260
- })}
261
- </div>
262
- </div>
263
-
264
- <div className="flex-1 overflow-hidden">
265
- {selectedNode ? (
266
- rightPaneMode === "table" ? (
267
- <TableView
268
- data={recordsForRightPane}
269
- columns={columns}
270
- fields={fields}
271
- config={{ ...config, showFilters: false }}
272
- onDataUpdate={onDataUpdate}
273
- filters={filterConfig}
274
- filterState={activeFilters}
275
- onFilterChange={(next) => {
276
- if (onFilterChange) onFilterChange(next);
277
- else setInternalFilters(next);
278
- }}
279
- showFilters={false}
280
- />
281
- ) : (
282
- <CardGrid records={recordsForRightPane} fields={fields} labelField={labelField} />
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
- /**
302
- * Card mode for the Tree right pane: renders the same record set as Table
303
- * mode, one library <Card> per record. The label field is the card header;
304
- * the remaining visible fields are key/value rows in the card body.
305
- */
306
- function CardGrid({
307
- records,
308
- fields,
309
- labelField,
310
- }: {
311
- records: DynamicRecord[];
312
- fields: FieldConfig[];
313
- labelField: FieldConfig;
314
- }) {
315
- const bodyFields = visibleFields(fields)
316
- .filter((f) => f.path !== labelField.path)
317
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
318
-
319
- if (records.length === 0) {
320
- return (
321
- <div className="h-full flex items-center justify-center text-sm text-content-presentation-global-tertiary">
322
- No records.
323
- </div>
324
- );
325
- }
326
-
327
- return (
328
- <div className="h-full overflow-y-auto p-4 bg-background-presentation-body-primary">
329
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
330
- {records.map((record, idx) => {
331
- const labelValue = getByPath(record, labelField.path);
332
- return (
333
- <Card key={(record.id ?? idx) as string | number} className="overflow-hidden">
334
- <CardHeader className="pb-2">
335
- <div className="text-xs uppercase tracking-wide text-content-presentation-global-tertiary">
336
- {labelField.label ?? labelField.path}
337
- </div>
338
- <div className="text-base font-semibold text-content-presentation-global-primary">
339
- {renderField(labelValue, labelField, record)}
340
- </div>
341
- </CardHeader>
342
- <CardContent className="space-y-2 pt-0">
343
- {bodyFields.map((f) => {
344
- const value = getByPath(record, f.path);
345
- if (value == null) return null;
346
- return (
347
- <div key={f.path} className="flex items-center justify-between gap-3 text-sm">
348
- <span className="text-content-presentation-global-tertiary">
349
- {f.label ?? f.path}
350
- </span>
351
- <span className="text-content-presentation-global-primary text-right">
352
- {renderField(value, f, record)}
353
- </span>
354
- </div>
355
- );
356
- })}
357
- </CardContent>
358
- </Card>
359
- );
360
- })}
361
- </div>
362
- </div>
363
- );
364
- }