torch-glare 2.1.4 → 2.1.5

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.
@@ -7,6 +7,7 @@ import {
7
7
  useMemo,
8
8
  useRef,
9
9
  useState,
10
+ type ElementType,
10
11
  type ReactNode,
11
12
  } from "react";
12
13
  import { List, LayoutGrid, Inbox as InboxIcon, Network } from "lucide-react";
@@ -61,6 +62,12 @@ export type DataViewsLayoutProps = {
61
62
  addNewLabel?: string;
62
63
 
63
64
  inboxItemHref?: (item: DynamicRecord, id: any) => string;
65
+ /**
66
+ * Component used to render inbox item links when `inboxItemHref` is set.
67
+ * Defaults to a plain `<a>` (full-page navigation). Pass your router's link
68
+ * (e.g. Next.js `Link`, React Router `Link`) for client-side navigation.
69
+ */
70
+ inboxLinkComponent?: ElementType;
64
71
  inboxSelectedId?: any;
65
72
  inboxRenderDetail?: (item: DynamicRecord | null) => ReactNode;
66
73
 
@@ -108,6 +115,7 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
108
115
  onAddNew,
109
116
  addNewLabel,
110
117
  inboxItemHref,
118
+ inboxLinkComponent,
111
119
  inboxSelectedId,
112
120
  inboxRenderDetail,
113
121
  searchValue,
@@ -276,6 +284,7 @@ export const DataViewsLayout = forwardRef<HTMLDivElement, DataViewsLayoutProps>(
276
284
  onFilterChange={setFilterState}
277
285
  showFilters={false}
278
286
  itemHref={inboxItemHref}
287
+ linkComponent={inboxLinkComponent}
279
288
  selectedItemId={inboxSelectedId}
280
289
  renderDetail={inboxRenderDetail}
281
290
  />
@@ -222,7 +222,8 @@ export function FilterPanel({
222
222
  <Badge
223
223
  {...countBadge}
224
224
  label={String(totalFilters)}
225
- className="h-5 min-w-[20px] rounded-full p-0 text-xs"
225
+ showIcon={false}
226
+ className="h-5 w-5 min-w-0 justify-center rounded-full p-0 text-xs"
226
227
  size="XS"
227
228
  />
228
229
  )}
@@ -270,9 +271,9 @@ export function FilterPanel({
270
271
  <Badge
271
272
  {...countBadge}
272
273
  label={String(totalFilters)}
273
- className="h-5 min-w-[20px] rounded-full p-0 text-xs"
274
+ showIcon={false}
275
+ className="h-5 w-5 min-w-0 justify-center rounded-full p-0 text-xs"
274
276
  size="XS"
275
-
276
277
  />
277
278
  )}
278
279
  </div>
@@ -1,6 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useMemo, useState, type ReactNode } from "react";
3
+ import {
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ type ElementType,
8
+ type ReactNode,
9
+ } from "react";
4
10
  import { Badge } from "../Badge";
5
11
  import { FilterPanel } from "./FilterPanel";
6
12
  import {
@@ -57,6 +63,12 @@ export type InboxViewProps = {
57
63
  onFilterChange?: (filters: FilterState) => void;
58
64
  showFilters?: boolean;
59
65
  itemHref?: (item: DynamicRecord, id: any) => string;
66
+ /**
67
+ * Component used to render each item's link when `itemHref` is set. Defaults
68
+ * to a plain `<a>` (full-page navigation). Pass your router's link
69
+ * (e.g. Next.js `Link`, React Router `Link`) for client-side navigation.
70
+ */
71
+ linkComponent?: ElementType;
60
72
  selectedItemId?: any;
61
73
  renderDetail?: (item: DynamicRecord | null) => ReactNode;
62
74
  };
@@ -100,6 +112,7 @@ export function InboxView({
100
112
  onFilterChange,
101
113
  showFilters = true,
102
114
  itemHref,
115
+ linkComponent,
103
116
  selectedItemId,
104
117
  renderDetail,
105
118
  }: InboxViewProps) {
@@ -350,6 +363,7 @@ export function InboxView({
350
363
  selected={selected}
351
364
  onSelect={() => handleSelectItem(item)}
352
365
  href={itemHref?.(item, itemId)}
366
+ linkComponent={linkComponent}
353
367
  />
354
368
  );
355
369
  })}
@@ -140,6 +140,8 @@ Inbox auto-detects `isRead`, `isStarred`, `hasAttachment`, `priority`. Override
140
140
  | `views` | `ViewVisibility` | all on (tree auto) | Per-view toggle: `{ table?, kanban?, inbox?, tree? }`. Omitted keys default to `true` (Tree auto-hides without hierarchy). |
141
141
  | `kanbanGroupBy` | `string` | `"status"` | Dot-path to the field used for Kanban columns. |
142
142
  | `inboxConfig` | `InboxConfig` | auto-detected | Map of starred/read/attachment/priority field paths for Inbox. |
143
+ | `inboxItemHref` | `(item, id) => string` | — | When set, each Inbox row becomes a link to the returned href. |
144
+ | `inboxLinkComponent` | `ElementType` | `"a"` | Component used to render Inbox item links when `inboxItemHref` is set. Pass your router's link (Next.js `Link`, React Router `Link`) for client-side navigation; defaults to a plain `<a>` (full-page nav). |
143
145
  | `treeConfig` | `TreeConfig` | auto-detected | `childrenField`, `parentField`, `idField`, `nodeLabel`, `defaultExpanded`. |
144
146
  | `filterState` | `FilterState` | uncontrolled | Controlled filter state. Pair with `onFilterChange`. |
145
147
  | `onFilterChange` | `(state: FilterState) => void` | — | Fires when any filter changes. When provided, the layout is controlled. |
@@ -0,0 +1,159 @@
1
+ ---
2
+ title: InboxView
3
+ description: Standalone inbox/list view for DataViews — a master list with read/starred/priority states and an optional detail pane. Use inside DataViewsLayout (tab mode) or directly in Composable Mode.
4
+ group: Data Display
5
+ keywords: [data-views, inbox-view, inbox, list, master-detail, read, starred, priority, attachment, composable, dynamic-data]
6
+ ---
7
+
8
+ # InboxView
9
+
10
+ > The inbox renderer behind `DataViewsLayout`'s "Inbox" tab. It renders records as a scannable list with read / starred / priority / attachment affordances, plus an optional detail pane. In tab mode the layout renders it for you; render it directly only in **Composable Mode**.
11
+
12
+ ## Installation
13
+
14
+ Part of `torch-glare`. Ships with the `DataViews` folder when you run `npx torch-glare add DataViews` — no separate install. It depends on the shared `Badge`, `Button`, `Avatar`, `Card`, `Divider`, and `TabFormItem` components plus `lucide-react`.
15
+
16
+ ## Import
17
+
18
+ ```tsx
19
+ import { InboxView, useDataViewsState } from "torch-glare"
20
+ import type { InboxViewProps, InboxConfig, FieldConfig } from "torch-glare"
21
+ ```
22
+
23
+ ## When to use it directly
24
+
25
+ | Situation | Use |
26
+ |---|---|
27
+ | You want the standard tabbed multi-view UI | `DataViewsLayout` with `views={{ inbox: true }}` — it mounts `InboxView` for you. |
28
+ | You want a custom master-detail layout | Render `InboxView` directly with state from `useDataViewsState`, and supply `renderDetail`. |
29
+
30
+ ## Field auto-detection
31
+
32
+ InboxView auto-detects these record fields and maps them to UI affordances.
33
+ Override any of them with `inboxConfig`.
34
+
35
+ | Detected field | Affordance |
36
+ |---|---|
37
+ | `isRead` | Read/unread weight |
38
+ | `isStarred` | Star toggle |
39
+ | `hasAttachment` | Paperclip icon |
40
+ | `priority` | Priority flag |
41
+
42
+ ## Composable Mode example
43
+
44
+ ```tsx
45
+ import { InboxView, useDataViewsState } from "torch-glare"
46
+ import type { FieldConfig, InboxConfig } from "torch-glare"
47
+
48
+ const messages = [
49
+ { id: 1, subject: "Welcome", from: { name: "Ada" }, isRead: false, isStarred: true, sentAt: "2024-06-01" },
50
+ { id: 2, subject: "Invoice", from: { name: "Billing" }, isRead: true, hasAttachment: true, sentAt: "2024-06-02" },
51
+ ]
52
+
53
+ const fields: FieldConfig[] = [
54
+ { path: "subject", type: "text" },
55
+ { path: "from.name", label: "From", type: "text" },
56
+ { path: "sentAt", type: "date-format", dateFormat: "YYYY-MM-DD" },
57
+ ]
58
+
59
+ const inboxConfig: InboxConfig = {
60
+ titlePath: "subject",
61
+ previewPath: "from.name",
62
+ dateField: "sentAt",
63
+ }
64
+
65
+ function Mailbox() {
66
+ const state = useDataViewsState({ data: messages, fields })
67
+ const [selectedId, setSelectedId] = useState<number | null>(null)
68
+ return (
69
+ <InboxView
70
+ data={state.flatItems}
71
+ fields={state.resolvedFields}
72
+ config={state.config}
73
+ inboxConfig={inboxConfig}
74
+ selectedItemId={selectedId}
75
+ renderDetail={(item) =>
76
+ item ? <MessageDetail message={item} /> : <Empty />
77
+ }
78
+ />
79
+ )
80
+ }
81
+ ```
82
+
83
+ ### Link rows to routes (framework-agnostic)
84
+
85
+ `itemHref` turns each row into a link. By default the card renders a plain `<a>`
86
+ (full-page navigation), so it works in any framework. For client-side routing,
87
+ pass your router's link via `linkComponent` — this is what makes navigation
88
+ behave consistently across environments. Without it you get a normal `<a>`.
89
+
90
+ ```tsx
91
+ // Next.js
92
+ import Link from "next/link"
93
+
94
+ <InboxView
95
+ data={state.flatItems}
96
+ fields={state.resolvedFields}
97
+ config={state.config}
98
+ itemHref={(item, id) => `/messages/${id}`}
99
+ linkComponent={Link} // React Router users pass their <Link> the same way
100
+ />
101
+ ```
102
+
103
+ > Via `DataViewsLayout` (tab mode) the same prop is named `inboxLinkComponent`.
104
+
105
+ ## API Reference
106
+
107
+ ### `InboxViewProps`
108
+
109
+ | Prop | Type | Default | Description |
110
+ |---|---|---|---|
111
+ | `data` | `DynamicRecord[]` | — (required) | Records to render as list items. Pass `state.flatItems`. |
112
+ | `fields` | `FieldConfig[]` | — (required) | Field map controlling list-item content. Pass `state.resolvedFields`. |
113
+ | `config` | `ViewConfig` | — (required) | View config from `useDataViewsState`. |
114
+ | `inboxConfig` | `InboxConfig` | auto-detected | Overrides for which record paths map to title/preview/avatar/date/read/starred/attachment/priority. |
115
+ | `columns` | `DynamicColumnConfig[]` | `undefined` | Explicit column overrides. Usually derived from `fields`. |
116
+ | `onDataUpdate` | `(data: DynamicRecord[]) => void` | `undefined` | Called when item data changes (e.g. toggling read/starred). |
117
+ | `filters` | `DynamicFilterConfig[]` | `undefined` | Explicit filter definitions. Usually inferred from `filterable` fields. |
118
+ | `filterState` | `FilterState` | uncontrolled | Controlled filter state. Pair with `onFilterChange`. |
119
+ | `onFilterChange` | `(filters: FilterState) => void` | `undefined` | Fires when a filter changes. |
120
+ | `showFilters` | `boolean` | `true` | Show the integrated filter panel. |
121
+ | `itemHref` | `(item: DynamicRecord, id: any) => string` | `undefined` | When set, each row becomes a link to the returned href. |
122
+ | `linkComponent` | `ElementType` | `"a"` | Component used to render each item's link when `itemHref` is set. Pass your router's link (Next.js `Link`, React Router `Link`) for client-side navigation. Defaults to a plain `<a>` (full-page nav). |
123
+ | `selectedItemId` | `any` | `undefined` | Id of the currently selected row (drives the detail pane + highlight). |
124
+ | `renderDetail` | `(item: DynamicRecord \| null) => ReactNode` | `undefined` | Renders the right-hand detail pane for the selected item. |
125
+
126
+ ### `InboxConfig`
127
+
128
+ ```ts
129
+ type InboxConfig = {
130
+ starredField?: string
131
+ readField?: string
132
+ attachmentField?: string
133
+ priorityField?: string
134
+ titlePath?: string
135
+ previewPath?: string
136
+ avatarPath?: string
137
+ dateField?: string
138
+ }
139
+ ```
140
+
141
+ See [`DataViewsLayout`](./data-views-layout.md#fieldconfig) for `FieldConfig`,
142
+ `FilterState`, and related shapes.
143
+
144
+ ## Accessibility
145
+
146
+ - The all/starred/priority switcher uses [`TabFormItem`](./tab-form-item.md) (full keyboard support).
147
+ - Star/archive/delete actions are real `<button>`s with accessible labels.
148
+ - Avatars fall back to initials via [`Avatar`](./avatar.md).
149
+
150
+ ## Theming
151
+
152
+ Uses only `*-presentation-*` design tokens. Control the scheme via the parent
153
+ `DataViewsLayout`'s `theme`.
154
+
155
+ ## Related
156
+
157
+ - [`DataViewsLayout`](./data-views-layout.md) — the tabbed container that renders this for you
158
+ - [`TableView`](./table-view.md) · [`KanbanView`](./kanban-view.md) · [`TreeView`](./tree-view.md) — sibling views
159
+ - [How-to: Render a backend response with DataViews](../how-to/data-views-from-backend-response.md)
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: KanbanView
3
+ description: Standalone kanban board view for DataViews — groups records into columns by a field and renders each as a card. Use inside DataViewsLayout (tab mode) or directly in Composable Mode.
4
+ group: Data Display
5
+ keywords: [data-views, kanban-view, kanban, board, columns, group-by, cards, composable, dynamic-data, fields]
6
+ ---
7
+
8
+ # KanbanView
9
+
10
+ > The board renderer behind `DataViewsLayout`'s "Board" tab. It groups records into columns by `groupByField` and renders each record as a card. In tab mode the layout renders it for you; render it directly only in **Composable Mode**.
11
+
12
+ ## Installation
13
+
14
+ Part of `torch-glare`. Ships with the `DataViews` folder when you run `npx torch-glare add DataViews` — no separate install. It depends on the shared `Button` component, the `DataViewCard` layout, and `lucide-react`.
15
+
16
+ ## Import
17
+
18
+ ```tsx
19
+ import { KanbanView, useDataViewsState } from "torch-glare"
20
+ import type { KanbanViewProps, FieldConfig } from "torch-glare"
21
+ ```
22
+
23
+ ## When to use it directly
24
+
25
+ | Situation | Use |
26
+ |---|---|
27
+ | You want the standard tabbed multi-view UI | `DataViewsLayout` with `views={{ kanban: true }}` — it mounts `KanbanView` for you. |
28
+ | You want a custom layout (e.g. kanban beside a table) | Render `KanbanView` directly with state from `useDataViewsState`. |
29
+
30
+ ## Composable Mode example
31
+
32
+ `KanbanView` groups by the `groupByField` path — every distinct value becomes a
33
+ column. Column colors are assigned deterministically, or per-value via the
34
+ field's `kanbanVariants`.
35
+
36
+ ```tsx
37
+ import { KanbanView, useDataViewsState } from "torch-glare"
38
+ import type { FieldConfig } from "torch-glare"
39
+
40
+ const tasks = [
41
+ { id: 1, title: "Spec API", status: "Todo", assignee: "Ada" },
42
+ { id: 2, title: "Build UI", status: "In Progress", assignee: "Linus" },
43
+ { id: 3, title: "Ship", status: "Done", assignee: "Grace" },
44
+ ]
45
+
46
+ const fields: FieldConfig[] = [
47
+ { path: "title", type: "text" },
48
+ {
49
+ path: "status",
50
+ type: "enum-badge",
51
+ kanbanVariants: {
52
+ Todo: { label: "To Do", color: "gray" },
53
+ "In Progress": { label: "In Progress", color: "blue" },
54
+ Done: { label: "Done", color: "green" },
55
+ },
56
+ },
57
+ { path: "assignee", type: "text" },
58
+ ]
59
+
60
+ function TaskBoard() {
61
+ const state = useDataViewsState({ data: tasks, fields })
62
+ return (
63
+ <KanbanView
64
+ data={state.flatItems}
65
+ fields={state.resolvedFields}
66
+ config={state.config}
67
+ groupByField="status"
68
+ titleField="title"
69
+ />
70
+ )
71
+ }
72
+ ```
73
+
74
+ ### Column header actions
75
+
76
+ Pass `onColumnAction` to show an overflow (⋯) button on each column header. When
77
+ omitted the button is hidden.
78
+
79
+ ```tsx
80
+ <KanbanView
81
+ data={state.flatItems}
82
+ fields={state.resolvedFields}
83
+ config={state.config}
84
+ groupByField="status"
85
+ onColumnAction={(columnId) => openColumnMenu(columnId)}
86
+ />
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### `KanbanViewProps`
92
+
93
+ | Prop | Type | Default | Description |
94
+ |---|---|---|---|
95
+ | `data` | `DynamicRecord[]` | — (required) | Records to group into columns. Pass `state.flatItems` in composable mode. |
96
+ | `fields` | `FieldConfig[]` | — (required) | Field map controlling card content. Pass `state.resolvedFields`. |
97
+ | `config` | `ViewConfig` | — (required) | View config from `useDataViewsState`. |
98
+ | `groupByField` | `string` | `"status"` | Dot-path to the field whose distinct values become columns. |
99
+ | `titleField` | `string` | first visible non-group field | Dot-path of the field rendered as the card title. |
100
+ | `columns` | `DynamicColumnConfig[]` | `undefined` | Explicit column overrides. Usually derived from `fields`. |
101
+ | `onDataUpdate` | `(data: DynamicRecord[]) => void` | `undefined` | Called when a card moves between columns (updates the group-by value). |
102
+ | `onColumnAction` | `(columnId: string) => void` | `undefined` | Click handler for the column header overflow button. When omitted, the button is hidden. |
103
+
104
+ Per-column colors come from each field's `kanbanVariants` map
105
+ (`{ [value]: { label?, color? } }`). Available `color` keys: `gray`, `purple`,
106
+ `orange`, `blue`, `green`, `red`. See
107
+ [`DataViewsLayout`](./data-views-layout.md#fieldconfig) for the full
108
+ `FieldConfig` shape.
109
+
110
+ ## Accessibility
111
+
112
+ - Cards are keyboard-focusable; the column overflow button is a real `<button>`.
113
+ - Card titles use semantic heading markup within each [`DataViewCard`](./card.md).
114
+
115
+ ## Theming
116
+
117
+ Uses `*-presentation-*` tokens plus a small set of deeply-saturated column-header
118
+ fills matched to `glare-torch-mode` raw tokens. Control the scheme via the
119
+ parent `DataViewsLayout`'s `theme`.
120
+
121
+ ## Related
122
+
123
+ - [`DataViewsLayout`](./data-views-layout.md) — the tabbed container that renders this for you
124
+ - [`TableView`](./table-view.md) · [`InboxView`](./inbox-view.md) · [`TreeView`](./tree-view.md) — sibling views
125
+ - [How-to: Render a backend response with DataViews](../how-to/data-views-from-backend-response.md)
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: TableView
3
+ description: Standalone table view for DataViews — sortable columns, row selection, and an integrated filter panel. Use inside DataViewsLayout (tab mode) or directly in Composable Mode.
4
+ group: Data Display
5
+ keywords: [data-views, table-view, table, sortable, columns, selection, filter, composable, dynamic-data, fields]
6
+ ---
7
+
8
+ # TableView
9
+
10
+ > The table renderer behind `DataViewsLayout`'s "List" tab. In tab mode the layout renders it for you. Render it directly only in **Composable Mode** (custom layouts), wiring it with `useDataViewsState`.
11
+
12
+ ## Installation
13
+
14
+ Part of `torch-glare`. Ships with the `DataViews` folder when you run `npx torch-glare add DataViews` — no separate install. It depends on the shared `Card`, `Checkbox`, and `Table` components plus the colocated `FilterPanel`.
15
+
16
+ ## Import
17
+
18
+ ```tsx
19
+ import { TableView, useDataViewsState } from "torch-glare"
20
+ import type { TableViewProps, FieldConfig } from "torch-glare"
21
+ ```
22
+
23
+ ## When to use it directly
24
+
25
+ | Situation | Use |
26
+ |---|---|
27
+ | You want the standard tabbed multi-view UI | `DataViewsLayout` — it mounts `TableView` for you. Don't render this yourself. |
28
+ | You want a custom layout (e.g. table beside a kanban) | Render `TableView` directly with state from `useDataViewsState`. |
29
+ | You only ever need a table and nothing else | Render `TableView` directly, or just use the simpler [`Table`](./table.md) / [`DataTable`](./data-table.md). |
30
+
31
+ ## Composable Mode example
32
+
33
+ `TableView` is controlled — it does not own field detection or config. Pull those from `useDataViewsState` (which auto-detects fields and columns from your data) and pass them down.
34
+
35
+ ```tsx
36
+ import { TableView, useDataViewsState } from "torch-glare"
37
+ import type { FieldConfig } from "torch-glare"
38
+
39
+ const employees = [
40
+ { id: 1, name: "Ada Lovelace", role: "Engineer", salary: 120000, joinDate: "2024-04-12" },
41
+ { id: 2, name: "Linus Torvalds", role: "Engineer", salary: 145000, joinDate: "2023-09-01" },
42
+ ]
43
+
44
+ const fields: FieldConfig[] = [
45
+ { path: "name", label: "Name", type: "text" },
46
+ { path: "role", type: "text", filterable: true },
47
+ { path: "salary", type: "currency", currency: "USD" },
48
+ { path: "joinDate", type: "date-format", dateFormat: "YYYY-MM-DD" },
49
+ ]
50
+
51
+ function EmployeesTable() {
52
+ const state = useDataViewsState({ data: employees, fields })
53
+ return (
54
+ <TableView
55
+ data={state.flatItems}
56
+ fields={state.resolvedFields}
57
+ config={state.config}
58
+ onSortChange={(sortBy, sortOrder) =>
59
+ state.setConfig({ ...state.config, sortBy, sortOrder })
60
+ }
61
+ filterState={state.filterState}
62
+ onFilterChange={state.setFilterState}
63
+ />
64
+ )
65
+ }
66
+ ```
67
+
68
+ ### Hide the inline filter panel
69
+
70
+ ```tsx
71
+ <TableView
72
+ data={state.flatItems}
73
+ fields={state.resolvedFields}
74
+ config={state.config}
75
+ showFilters={false}
76
+ />
77
+ ```
78
+
79
+ ### Controlled sorting
80
+
81
+ `TableView` does not sort internally — it calls `onSortChange` and reads the
82
+ active sort from `config.sortBy` / `config.sortOrder`. Wire it to your config
83
+ state (or your backend) to make headers interactive.
84
+
85
+ ```tsx
86
+ <TableView
87
+ data={rows}
88
+ fields={fields}
89
+ config={{ defaultView: "table", sortBy: "name", sortOrder: "asc" }}
90
+ onSortChange={(sortBy, sortOrder) => refetch({ sortBy, sortOrder })}
91
+ />
92
+ ```
93
+
94
+ ## API Reference
95
+
96
+ ### `TableViewProps`
97
+
98
+ | Prop | Type | Default | Description |
99
+ |---|---|---|---|
100
+ | `data` | `DynamicRecord[]` | — (required) | Flat array of rows to render. In composable mode pass `state.flatItems`. |
101
+ | `fields` | `FieldConfig[]` | — (required) | Field map controlling which columns render and how cells format. Pass `state.resolvedFields` for auto-detected fields. |
102
+ | `config` | `ViewConfig` | — (required) | View config. `sortBy` / `sortOrder` drive the active sort indicator. |
103
+ | `columns` | `DynamicColumnConfig[]` | `undefined` | Explicit column overrides (visibility/order). Usually derived from `fields`. |
104
+ | `onDataUpdate` | `(data: DynamicRecord[]) => void` | `undefined` | Called when row data changes (e.g. inline selection). |
105
+ | `onSortChange` | `(sortBy: string, sortOrder: "asc" \| "desc") => void` | `undefined` | Fires on header click. When omitted, headers are not sortable. |
106
+ | `filters` | `DynamicFilterConfig[]` | `undefined` | Explicit filter definitions. Usually inferred from `filterable` fields. |
107
+ | `filterState` | `FilterState` | uncontrolled | Controlled filter state. Pair with `onFilterChange`. |
108
+ | `onFilterChange` | `(filters: FilterState) => void` | `undefined` | Fires when a filter changes. When provided, the view is controlled. |
109
+ | `showFilters` | `boolean` | `true` | Show the integrated filter panel. |
110
+
111
+ `DynamicColumnConfig`, `DynamicFilterConfig`, `FilterState`, and `FieldConfig`
112
+ share the same shapes documented in
113
+ [`DataViewsLayout`](./data-views-layout.md#api-reference).
114
+
115
+ ## Accessibility
116
+
117
+ - Built on the accessible [`Table`](./table.md) primitive (semantic `<table>` markup, sortable headers).
118
+ - Row selection uses `TableCheckbox` with proper labelling.
119
+ - Filter checkboxes carry labels and `htmlFor` linkage.
120
+
121
+ ## Theming
122
+
123
+ Uses only `*-presentation-*` design tokens. Wrap with `ThemeProvider` or pass a
124
+ `theme` to the parent `DataViewsLayout` to control the color scheme.
125
+
126
+ ## Related
127
+
128
+ - [`DataViewsLayout`](./data-views-layout.md) — the tabbed multi-view container that renders this for you
129
+ - [`KanbanView`](./kanban-view.md) · [`InboxView`](./inbox-view.md) · [`TreeView`](./tree-view.md) — the sibling views
130
+ - [`Table`](./table.md) / [`DataTable`](./data-table.md) — lower-level table components
131
+ - [How-to: Render a backend response with DataViews](../how-to/data-views-from-backend-response.md)
@@ -0,0 +1,136 @@
1
+ ---
2
+ title: TreeView
3
+ description: Standalone hierarchical tree view for DataViews — a sidebar tree of nodes with a right pane (table or card) for the selected node. Use inside DataViewsLayout (tab mode) or directly in Composable Mode.
4
+ group: Data Display
5
+ keywords: [data-views, tree-view, tree, hierarchy, nested, sidebar, parent-child, children, composable, dynamic-data]
6
+ ---
7
+
8
+ # TreeView
9
+
10
+ > The tree renderer behind `DataViewsLayout`'s "Tree" tab. It builds a hierarchy from your records (via a `children[]` array or a `parentId` reference) and shows a sidebar tree with a right pane for the selected node. In tab mode the layout renders it for you — and auto-hides the Tree tab when no hierarchy is detected. Render it directly only in **Composable Mode**.
11
+
12
+ ## Installation
13
+
14
+ Part of `torch-glare`. Ships with the `DataViews` folder when you run `npx torch-glare add DataViews` — no separate install. It reuses the sibling `TableView`, the `Card` component, a colocated tree sidebar/drawer, and `lucide-react`.
15
+
16
+ ## Import
17
+
18
+ ```tsx
19
+ import { TreeView, useDataViewsState } from "torch-glare"
20
+ import type { TreeViewProps, TreeConfig, FieldConfig } from "torch-glare"
21
+ ```
22
+
23
+ ## When to use it directly
24
+
25
+ | Situation | Use |
26
+ |---|---|
27
+ | You want the standard tabbed multi-view UI | `DataViewsLayout` — the Tree tab appears automatically when hierarchy is detected. |
28
+ | You want a custom layout with an always-on tree | Render `TreeView` directly with state from `useDataViewsState`. |
29
+ | You want a file/folder tree without the data-grid pane | Use [`TreeFolder`](./tree-drop-down.md) or [`TreeSubLayout`](./tree-sub-layout.md) instead. |
30
+
31
+ ## Hierarchy detection
32
+
33
+ `TreeView` auto-detects shape from your data. Override with `treeConfig`:
34
+
35
+ - **Nested** — each record carries a `children: []` array.
36
+ - **Flat / adjacency list** — each record carries a `parentId` (or similar) pointing at its parent's id.
37
+
38
+ ```tsx
39
+ // nested
40
+ const departments = [
41
+ { id: 1, name: "Engineering", children: [
42
+ { id: 2, name: "Platform" },
43
+ { id: 3, name: "Product" },
44
+ ]},
45
+ ]
46
+
47
+ // flat
48
+ const rows = [
49
+ { id: 1, name: "Engineering", parentId: null },
50
+ { id: 2, name: "Platform", parentId: 1 },
51
+ ]
52
+ ```
53
+
54
+ ## Composable Mode example
55
+
56
+ ```tsx
57
+ import { TreeView, useDataViewsState } from "torch-glare"
58
+ import type { FieldConfig, TreeConfig } from "torch-glare"
59
+
60
+ const fields: FieldConfig[] = [
61
+ { path: "name", type: "text" },
62
+ { path: "headcount", type: "number" },
63
+ ]
64
+
65
+ const treeConfig: TreeConfig = {
66
+ childrenField: "children",
67
+ nodeLabel: "name",
68
+ defaultExpanded: "roots", // "all" | "roots" | "none"
69
+ defaultRightPane: "table", // "table" | "card"
70
+ }
71
+
72
+ function OrgTree() {
73
+ const state = useDataViewsState({ data: departments, fields, treeConfig })
74
+ return (
75
+ <TreeView
76
+ data={state.items}
77
+ fields={state.resolvedFields}
78
+ config={state.config}
79
+ treeConfig={treeConfig}
80
+ />
81
+ )
82
+ }
83
+ ```
84
+
85
+ ## API Reference
86
+
87
+ ### `TreeViewProps`
88
+
89
+ | Prop | Type | Default | Description |
90
+ |---|---|---|---|
91
+ | `data` | `DynamicRecord[]` | — (required) | Records to build the hierarchy from. Pass `state.items` (nested) in composable mode. |
92
+ | `fields` | `FieldConfig[]` | — (required) | Field map for the right-pane table/card. Pass `state.resolvedFields`. |
93
+ | `config` | `ViewConfig` | — (required) | View config from `useDataViewsState`. |
94
+ | `treeConfig` | `TreeConfig` | auto-detected | Hierarchy + expansion + right-pane config (see below). |
95
+ | `columns` | `DynamicColumnConfig[]` | `undefined` | Explicit column overrides for the right-pane table. |
96
+ | `onDataUpdate` | `(data: DynamicRecord[]) => void` | `undefined` | Called when nodes move (drag-and-drop reparent), if `dndEnabled`. |
97
+ | `filters` | `DynamicFilterConfig[]` | `undefined` | Explicit filter definitions. Usually inferred from `filterable` fields. |
98
+ | `filterState` | `FilterState` | uncontrolled | Controlled filter state. Pair with `onFilterChange`. |
99
+ | `onFilterChange` | `(filters: FilterState) => void` | `undefined` | Fires when a filter changes. |
100
+ | `showFilters` | `boolean` | `true` | Show the integrated filter panel. |
101
+
102
+ ### `TreeConfig`
103
+
104
+ ```ts
105
+ type TreeConfig = {
106
+ childrenField?: string // nested mode: array property holding children
107
+ parentField?: string // flat mode: property pointing at the parent id
108
+ idField?: string // id property (default "id")
109
+ orderField?: string // optional ordering within siblings
110
+ nodeLabel?: string // which field labels each tree node
111
+ defaultExpanded?: "all" | "roots" | "none"
112
+ defaultRightPane?: "table" | "card" // "details" accepted as a deprecated alias of "card"
113
+ dndEnabled?: boolean // enable drag-and-drop reparenting
114
+ }
115
+ ```
116
+
117
+ See [`DataViewsLayout`](./data-views-layout.md#fieldconfig) for `FieldConfig`,
118
+ `FilterState`, and related shapes.
119
+
120
+ ## Accessibility
121
+
122
+ - Tree rows expose `role="treeitem"` with `aria-expanded` and `aria-selected`.
123
+ - On mobile the sidebar collapses into a drawer with a labelled trigger.
124
+ - The right-pane table inherits [`TableView`](./table-view.md)'s accessibility.
125
+
126
+ ## Theming
127
+
128
+ Uses only `*-presentation-*` design tokens. Control the scheme via the parent
129
+ `DataViewsLayout`'s `theme`.
130
+
131
+ ## Related
132
+
133
+ - [`DataViewsLayout`](./data-views-layout.md) — the tabbed container that renders this for you
134
+ - [`TableView`](./table-view.md) · [`KanbanView`](./kanban-view.md) · [`InboxView`](./inbox-view.md) — sibling views
135
+ - [`TreeFolder`](./tree-drop-down.md) / [`TreeSubLayout`](./tree-sub-layout.md) — non-grid tree navigation
136
+ - [How-to: Render a backend response with DataViews](../how-to/data-views-from-backend-response.md)
@@ -0,0 +1,191 @@
1
+ ---
2
+ title: Render a backend response with DataViews
3
+ description: Recipes for turning common backend JSON shapes into a DataViewsLayout — flat lists, nested hierarchies, inbox/message shapes, and server-driven filtering.
4
+ group: how-to
5
+ keywords: [data-views, recipes, backend, json, api, flat, nested, hierarchy, inbox, server-side, filtering, pagination, how-to]
6
+ ---
7
+
8
+ # Render a backend response with DataViews
9
+
10
+ The goal of [`DataViewsLayout`](../components/data-views-layout.md) is "one
11
+ backend response → many UI shapes." This guide maps the JSON shapes you get
12
+ back from an API to the props that turn them into a working view.
13
+
14
+ ## TL;DR
15
+
16
+ ```tsx
17
+ import { DataViewsLayout } from "torch-glare"
18
+
19
+ // the simplest possible case — just pass the array
20
+ <DataViewsLayout title="Records" data={await api.get("/records")} />
21
+ ```
22
+
23
+ Everything below is about refining that default for specific shapes.
24
+
25
+ ## Recipe 1 — Flat list of objects
26
+
27
+ The most common API response. Pass it straight in; every primitive field
28
+ becomes a column and the Table/Kanban/Inbox tabs all work. The Tree tab
29
+ auto-hides because there's no hierarchy.
30
+
31
+ ```tsx
32
+ // GET /employees → [{ id, name, role, salary, joinDate }, ...]
33
+ <DataViewsLayout title="Employees" data={employees} />
34
+ ```
35
+
36
+ Add a declarative `fields` map when you want typed rendering (currencies,
37
+ badges, dates) and filters:
38
+
39
+ ```tsx
40
+ import type { FieldConfig } from "torch-glare"
41
+
42
+ const fields: FieldConfig[] = [
43
+ { path: "name", label: "Name", type: "text" },
44
+ { path: "role", type: "text", filterable: true },
45
+ { path: "salary", type: "currency", currency: "USD", filterable: true },
46
+ { path: "joinDate", type: "date-format", dateFormat: "YYYY-MM-DD" },
47
+ ]
48
+
49
+ <DataViewsLayout title="Employees" data={employees} fields={fields} />
50
+ ```
51
+
52
+ ## Recipe 2 — Status field → Kanban board
53
+
54
+ When a record has a status-like field, group it into a board. Use
55
+ `enum-badge` + `kanbanVariants` to color each column.
56
+
57
+ ```tsx
58
+ // GET /tasks → [{ id, title, status: "Todo" | "In Progress" | "Done" }, ...]
59
+ const fields: FieldConfig[] = [
60
+ { path: "title", type: "text" },
61
+ {
62
+ path: "status",
63
+ type: "enum-badge",
64
+ kanbanVariants: {
65
+ Todo: { label: "To Do", color: "gray" },
66
+ "In Progress": { label: "In Progress", color: "blue" },
67
+ Done: { label: "Done", color: "green" },
68
+ },
69
+ },
70
+ ]
71
+
72
+ <DataViewsLayout
73
+ title="Tasks"
74
+ data={tasks}
75
+ fields={fields}
76
+ views={{ table: true, kanban: true }}
77
+ kanbanGroupBy="status"
78
+ />
79
+ ```
80
+
81
+ ## Recipe 3 — Nested objects (dot-paths)
82
+
83
+ APIs often nest related data. Reference it with dot-paths — no flattening
84
+ needed.
85
+
86
+ ```tsx
87
+ // GET /orders → [{ id, total, customer: { name, email } }, ...]
88
+ const fields: FieldConfig[] = [
89
+ { path: "id", label: "Order #", type: "number" },
90
+ { path: "customer.name", label: "Customer", type: "text" },
91
+ { path: "customer.email", label: "Email", type: "link", linkType: "mailto" },
92
+ { path: "total", type: "currency", currency: "USD" },
93
+ ]
94
+
95
+ <DataViewsLayout title="Orders" data={orders} fields={fields} />
96
+ ```
97
+
98
+ ## Recipe 4 — Hierarchy (nested `children[]` or flat `parentId`)
99
+
100
+ When records form a tree, the Tree tab appears automatically. Both shapes work:
101
+
102
+ ```tsx
103
+ // Nested: GET /departments → [{ id, name, children: [...] }]
104
+ <DataViewsLayout
105
+ data={departments}
106
+ treeConfig={{ childrenField: "children", nodeLabel: "name", defaultExpanded: "roots" }}
107
+ />
108
+
109
+ // Flat / adjacency list: GET /nodes → [{ id, name, parentId }]
110
+ <DataViewsLayout
111
+ data={nodes}
112
+ treeConfig={{ parentField: "parentId", idField: "id", nodeLabel: "name" }}
113
+ />
114
+ ```
115
+
116
+ ## Recipe 5 — Message/inbox shape
117
+
118
+ For mailbox-like data, the Inbox view auto-detects `isRead`, `isStarred`,
119
+ `hasAttachment`, and `priority`. Map title/preview/date with `inboxConfig`.
120
+
121
+ ```tsx
122
+ // GET /messages → [{ id, subject, from: { name }, isRead, isStarred, sentAt }]
123
+ <DataViewsLayout
124
+ data={messages}
125
+ views={{ inbox: true }}
126
+ inboxConfig={{ titlePath: "subject", previewPath: "from.name", dateField: "sentAt" }}
127
+ />
128
+ ```
129
+
130
+ ## Recipe 6 — Server-driven filtering & pagination
131
+
132
+ Make the layout controlled: hold `filterState` yourself and refetch when it
133
+ changes. This keeps the URL/server as the source of truth.
134
+
135
+ ```tsx
136
+ import { useState, useEffect } from "react"
137
+ import type { FilterState } from "torch-glare"
138
+
139
+ function ServerDriven() {
140
+ const [rows, setRows] = useState([])
141
+ const [filterState, setFilterState] = useState<FilterState>({})
142
+
143
+ useEffect(() => {
144
+ api.get("/records", { params: { filters: filterState } }).then(setRows)
145
+ }, [filterState])
146
+
147
+ return (
148
+ <DataViewsLayout
149
+ data={rows}
150
+ fields={fields}
151
+ filterState={filterState}
152
+ onFilterChange={setFilterState}
153
+ />
154
+ )
155
+ }
156
+ ```
157
+
158
+ ## Recipe 7 — Custom layout (composable mode)
159
+
160
+ When tabs aren't what you want — e.g. a table beside a kanban — bypass
161
+ `DataViewsLayout` and compose the views with `useDataViewsState`.
162
+
163
+ ```tsx
164
+ import { TableView, KanbanView, useDataViewsState } from "torch-glare"
165
+
166
+ function SplitScreen({ data, fields }) {
167
+ const state = useDataViewsState({ data, fields })
168
+ return (
169
+ <div className="grid grid-cols-2 gap-4 h-screen">
170
+ <TableView data={state.flatItems} fields={state.resolvedFields} config={state.config} showFilters={false} />
171
+ <KanbanView data={state.flatItems} fields={state.resolvedFields} config={state.config} groupByField="status" />
172
+ </div>
173
+ )
174
+ }
175
+ ```
176
+
177
+ See each view's reference: [TableView](../components/table-view.md) ·
178
+ [KanbanView](../components/kanban-view.md) ·
179
+ [InboxView](../components/inbox-view.md) ·
180
+ [TreeView](../components/tree-view.md).
181
+
182
+ ## Gotchas
183
+
184
+ - **Empty `data`** → all views render their empty state; pass `isLoading` upstream if you fetch async.
185
+ - **Tree tab missing?** No hierarchy was detected. Supply `treeConfig` explicitly or check your `childrenField` / `parentField`.
186
+ - **Saved Views don't persist** in tab mode — that's a known limitation documented in [`DataViewsConfigPanel`](../components/data-views-config-panel.md). Use composable mode for real persistence.
187
+
188
+ ## Related
189
+
190
+ - [`DataViewsLayout`](../components/data-views-layout.md) — full prop reference
191
+ - [`DataViewsConfigPanel`](../components/data-views-config-panel.md) — settings/filters panel
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "torch-glare",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "files": [
7
7
  "dist",
8
8
  "apps/lib",
9
9
  "docs",
10
- "!**/*-dev.*"
10
+ "!**/*-dev.*",
11
+ "!apps/lib/components/**/*.md"
11
12
  ],
12
13
  "bin": {
13
14
  "torch-glare": "dist/bin/index.js"
@@ -1,439 +0,0 @@
1
- # DataViews — Architecture & Component Map
2
-
3
- Internal reference for the DataViews feature. Use this when changing UI, debugging, or syncing with the designer.
4
-
5
- **Demo route:** `/data-views` (in `apps/app/data-views/page.tsx`)
6
- **Library root:** `apps/lib/components/DataViews/`
7
-
8
- ---
9
-
10
- ## 1. What is DataViews?
11
-
12
- A composable multi-view layout. One backend response (array of records) renders as any combination of **Table**, **Kanban**, **Inbox**, **Tree** — chosen per-screen via a `views` prop. Filters and the settings cog are togglable.
13
-
14
- ```tsx
15
- <DataViewsLayout
16
- title="Orders"
17
- data={orders}
18
- fields={[
19
- { path: "status", type: "enum-badge", variants: { Pending: "yellow", Shipped: "blue" }, filterable: true },
20
- { path: "total", type: "currency", currency: "USD", filterable: true },
21
- ]}
22
- views={{ table: true, kanban: true }} // only these tabs render
23
- showFilters
24
- showSettings
25
- />
26
- ```
27
-
28
- ---
29
-
30
- ## 2. Layout vs. Components vs. Built-ins
31
-
32
- ### 🟦 The Layout — `DataViewsLayout`
33
-
34
- The **frame**. Orchestrator, not a UI piece on its own.
35
-
36
- Owns:
37
- - Page header (title + description)
38
- - View-switcher tab bar
39
- - Settings cog button
40
- - Decision of which view to render
41
- - Decision of whether to show filter panel / settings panel
42
-
43
- Does **not** render the filter panel or settings panel directly — it passes `showFilters` down to each view, and toggles `SettingsPanel` on the side.
44
-
45
- ### 🟩 The DataViews Components (new — 6 total)
46
-
47
- | Component | File | Role |
48
- |---|---|---|
49
- | `DataViewsLayout` | `DataViewsLayout.tsx` | The frame (header + tabs + settings toggle) |
50
- | `TableView` | `TableView.tsx` | Sortable table + search + optional filter panel |
51
- | `KanbanView` | `KanbanView.tsx` | Drag-and-drop board grouped by an enum-badge field |
52
- | `InboxView` | `InboxView.tsx` | Three-pane mail-style list (filters \| list \| detail) |
53
- | `TreeView` | `TreeView.tsx` | Two-pane explorer (tree sidebar + right pane) |
54
- | `FilterPanel` | `FilterPanel.tsx` | Left-sidebar filter panel (used by Table/Inbox/Tree) |
55
- | `SettingsPanel` | `SettingsPanel.tsx` | Right-sidebar configurator (column toggles + sort + view-specific settings) |
56
-
57
- ### 🟨 Built-ins (existing torch-glare components, reused)
58
-
59
- These already exist in glare and were not modified for this feature:
60
-
61
- - `Badge` — status/priority chips (uses `color` + `badgeStyle: "solid" | "subtle"`)
62
- - `Button` — Settings cog, action buttons, drawer trigger
63
- - `Card`, `CardHeader`, `CardContent` — Kanban cards, Inbox detail pane
64
- - `Checkbox` — table row select + filter list checkboxes
65
- - `InputField` — the search bar
66
- - `Avatar`, `AvatarFallback`, `AvatarImage` — inbox sender avatars
67
- - `Switch` — settings panel toggles (column visibility, show filters)
68
- - `Divider` — separators in panels (was `Separator` in source)
69
- - `Label` — filter / setting labels
70
- - `RadioGroup`, `Radio` — Kanban groupBy picker in settings
71
- - `Table`, `TableHeader`, `TableBody`, `TableHead`, `TableRow`, `TableCell`, `TableCheckbox` — the actual `<table>` primitive
72
- - `TabFormItem` — view-switcher tabs (top variant) + inbox sidebar chips (side variant)
73
- - `Tooltip` — *(available, not currently mounted)*
74
- - `Drawer` (from `vaul`) — mobile tree drawer
75
- - Radix `Popover` — date-range filter popover
76
- - Radix `Slider` — numeric range filter
77
-
78
- ---
79
-
80
- ## 3. The Layout Structure
81
-
82
- ```
83
- ┌──────────────────────────────────────────────────────────────────────┐
84
- │ DataViewsLayout root <div class="bg-body-primary"> (theme-aware) │
85
- ├──────────────────────────────────────────────────────────────────────┤
86
- │ Header (showTitle) │
87
- │ ├── Title + description (left) │
88
- │ └── View tabs (TabFormItem×N) + Settings button (right) │
89
- ├──────────────────────────────────────────────────────────────────────┤
90
- │ main (flex row, overflow-hidden) │
91
- │ ├── Active view (one of): │
92
- │ │ ├── TableView [FilterPanel | table+search] │
93
- │ │ ├── KanbanView [columns of cards] │
94
- │ │ ├── InboxView [filter+sidebar | list | detail pane] │
95
- │ │ └── TreeView [tree sidebar | FilterPanel | right pane] │
96
- │ │ │
97
- │ └── SettingsPanel (slides in from right when cog clicked) │
98
- └──────────────────────────────────────────────────────────────────────┘
99
- ```
100
-
101
- ### Why filters live **inside** each view, not in the layout
102
-
103
- - **Kanban** intentionally has no filter panel (boards rarely do)
104
- - **Tree** puts the filter panel **between** the tree sidebar and the right pane
105
- - **Inbox** puts the filter panel **inside** its own left sidebar (mixed with starred/unread/priority chips)
106
- - **Table** puts the filter panel at the far left, like a standard list page
107
-
108
- Each view decides where filters go in its own internal grid. The layout just passes down `showFilters` as a boolean. Per-view `showFilters` prop also exists on each view component for composable mode.
109
-
110
- ---
111
-
112
- ## 4. Composable mode (no tabs)
113
-
114
- When you want a custom layout (e.g. Table on the left, Kanban on the right), bypass `DataViewsLayout` and compose the views directly using the shared state hook:
115
-
116
- ```tsx
117
- import { TableView, KanbanView, FilterPanel, useDataViewsState } from "@/components/DataViews"
118
-
119
- const state = useDataViewsState({ data, fields })
120
-
121
- return (
122
- <div className="grid grid-cols-2 gap-4 h-screen">
123
- <TableView {...state} showFilters={false} />
124
- <KanbanView {...state} groupByField="status" />
125
- </div>
126
- )
127
- ```
128
-
129
- `useDataViewsState` (`apps/lib/hooks/useDataViewsState.ts`) owns: filter state, sort state, field detection, column visibility config, tree shape detection, enabled-views resolution.
130
-
131
- ---
132
-
133
- ## 5. Data flow
134
-
135
- ```
136
- Backend → data: DynamicRecord[] (array of plain objects)
137
-
138
-
139
- FieldConfig[] (declarative field map, optional)
140
-
141
-
142
- useDataViewsState ──► detectFields() ─► merges with user fields
143
- │ │
144
- ▼ ▼
145
- flatItems (tree → flat for non-tree views) resolvedFields
146
-
147
-
148
- active view ──► FilterPanel (if enabled)
149
- │ └─► filterState (controlled or internal)
150
-
151
- renderField() per cell → torch-glare Badge / Avatar / etc.
152
- ```
153
-
154
- ### Where state lives
155
-
156
- | State | Owner | Notes |
157
- |---|---|---|
158
- | `currentView` | `useDataViewsState` | View tab selection |
159
- | `config` (sort, columns, kanbanGroupBy) | `useDataViewsState` | Single source of truth for sort + column visibility |
160
- | `items` | `useDataViewsState` | Mutable copy of `data` (kanban drag-drop edits this) |
161
- | `filterState` | `useDataViewsState` OR consumer | Controlled when `onFilterChange` is passed |
162
- | `selectedItem` | Per view | Inbox + Tree pick their own |
163
- | `expanded`, `selectedId` | TreeView | Tree-only |
164
- | `searchQuery` | TableView / InboxView | Local |
165
- | `showSettingsPanel` | `DataViewsLayout` | Toggled by cog button |
166
-
167
- ---
168
-
169
- ## 6. Utility files
170
-
171
- Located in `apps/lib/utils/dataViews/`. Pure data — no React UI imports.
172
-
173
- | File | Purpose |
174
- |---|---|
175
- | `pathUtils.ts` | `getByPath`, `setByPath`, `matchesFilterValues`, `formatPathLabel` — dot-path traversal & filter matching |
176
- | `fieldUtils.ts` | `detectFields`, `mergeFields`, `inferFieldType`, `resolveInboxConfig`, `visibleFields` — declarative field API |
177
- | `columnUtils.ts` | `detectColumns`, `mergeColumns` — legacy column config (kept for back-compat) |
178
- | `rangeUtils.ts` | `computeNumericExtremes`, `inferStep`, `presetToFilterValue`, `resolvePresets` — numeric/date range filters |
179
- | `treeUtils.ts` | `autoDetectTreeShape`, `buildTree`, `pruneTree`, `flattenAll` — hierarchy primitives |
180
- | `nestedDataUtils.tsx` | `renderDetailView`, `renderNestedObject`, `isPlainObject`, `isCurrencyField` — nested-object detail rendering |
181
-
182
- The only non-pure one is `nestedDataUtils.tsx` because it returns JSX for the inbox detail pane.
183
-
184
- ---
185
-
186
- ## 7. Types (`types.ts`)
187
-
188
- Key types exported from `apps/lib/components/DataViews/types.ts`:
189
-
190
- | Type | Shape |
191
- |---|---|
192
- | `DynamicRecord` | `Record<string, any>` — any backend object |
193
- | `ViewType` | `"table" \| "kanban" \| "inbox" \| "tree"` |
194
- | `ViewVisibility` | `{ table?, kanban?, inbox?, tree?: boolean }` |
195
- | `FieldConfig` | The declarative field definition (path, type, variants, filterable, etc.) |
196
- | `FieldType` | 17 renderer keys (text, number, enum-badge, currency, progress-bar, …) |
197
- | `BadgeVariant` | Internal dataviews badge palette — translated by `badgeAdapter.ts` |
198
- | `FilterState` | `Record<string, FilterValue>` |
199
- | `FilterValue` | `string[] \| { kind: "number", min?, max? } \| { kind: "date", from?, to? }` |
200
- | `ViewConfig` | `defaultView, tableColumns, kanbanGroupBy, showFilters, showPreviewPane, sortBy, sortOrder` |
201
- | `InboxConfig` | `starredField, readField, attachmentField, priorityField, titlePath, previewPath` |
202
- | `TreeConfig` | `childrenField, parentField, idField, nodeLabel, defaultExpanded, defaultRightPane` |
203
-
204
- ---
205
-
206
- ## 8. Field types (renderers)
207
-
208
- Defined in `fieldRenderers.tsx`. Map of `FieldType` → JSX renderer:
209
-
210
- | `type` | What it renders | Key field config props |
211
- |---|---|---|
212
- | `text` | Plain string | — |
213
- | `number` | Tabular numeric | — |
214
- | `date` | Raw date string | — |
215
- | `date-format` | `Intl.DateTimeFormat`-style or token-based | `dateFormat: "YYYY-MM-DD"` or `Intl.DateTimeFormatOptions` |
216
- | `boolean` | Yes/No Badge | `trueVariant`, `falseVariant`, `trueLabel`, `falseLabel` |
217
- | `currency` | `Intl.NumberFormat` currency | `currency: "USD"` or `{ symbol, locale, decimals, code }` |
218
- | `number-format` | `Intl.NumberFormat` formatted | `format: Intl.NumberFormatOptions` |
219
- | `enum-badge` | Single colored badge | `variants: { Value: "green" \| "yellow" \| ... }` |
220
- | `badge-array` | Row of badges with overflow `+N` | `variant`, `limit` |
221
- | `progress-bar` | Horizontal bar with % | `thresholds: [warn, ok]` |
222
- | `star-rating` | Filled-star row | `max` (default 5) |
223
- | `icon-text` | Icon + text | `icon` (lucide name or emoji), `iconPosition` |
224
- | `two-line` | Bold primary + small secondary | `secondaryPath` (dot-path) |
225
- | `avatar` | torch-glare Avatar | `fallbackPath` (for initials) |
226
- | `link` | `<a>` with mailto/tel/url | `linkType` |
227
- | `image` | `<img>` thumbnail | — |
228
- | `hidden` | Renders nothing | — |
229
-
230
- Auto-inference rules live in `inferFieldType()` (fieldUtils.ts):
231
- - `status` / `priority` keys → `enum-badge`
232
- - `email` / `phone` / `url` / `website` → `link`
233
- - `tags` / `labels` → `badge-array`
234
- - `date` / `time` suffixes → `date-format`
235
- - `salary` / `price` / `cost` / `amount` / `pay` / `fee` (or value smells like currency) → `currency`
236
- - `rating` / `score` (and value ≤ 5) → `star-rating`
237
- - ISO date strings → `date-format`
238
- - Arrays → `badge-array`
239
- - Booleans → `boolean`
240
-
241
- The escape hatch: `field.render = (value, row) => <YourJSX />` always wins.
242
-
243
- ---
244
-
245
- ## 9. Badge adapter (`badgeAdapter.ts`)
246
-
247
- The dataviews source used its own `BadgeVariant` enum (`green`, `greenLight`, `redOrange`, `bluePurple`, `navy`, etc.) that pre-dates torch-glare's current Badge API.
248
-
249
- torch-glare Badge uses `color` (gray/slate/red/orange/yellow/green/ocean/blue/purple/rose) + `badgeStyle` (solid/subtle).
250
-
251
- `resolveBadgeVariant(variant)` translates between the two. **Default style is `subtle`** to match glare's library default. Variants ending in `Light` or `navy` / `bluePurple` / `cocktailGreen` explicitly request solid.
252
-
253
- Want to change badge defaults globally? → edit `badgeAdapter.ts`.
254
- Want per-field control? → it could be exposed on `FieldConfig` as a new `badgeStyle?: "solid" | "subtle"` field (not done yet — propose it before adding).
255
-
256
- ---
257
-
258
- ## 10. Styling & theming
259
-
260
- ### Background tokens used
261
-
262
- | Surface | Token | Dark | Light |
263
- |---|---|---|---|
264
- | Page | `bg-background-presentation-body-primary` | `#252729` | `#F0F0F0` |
265
- | Side panels / Kanban column headers | `bg-background-presentation-body-overlay-primary` | `#1C1D1F` | `#F0F0F0` |
266
- | Subtle fills (chips, drop zones) | `bg-background-presentation-form-field-primary` | `#1C1D1F` | white-ish |
267
-
268
- **Do not use `bg-background-presentation-global-*`** — those tokens don't exist in this design system. They were inherited from the original data-views source and resolve to nothing. The Tailwind classes still validate but render as transparent. We swapped them all out; if you see one come back, it's a regression.
269
-
270
- ### Borders / text
271
-
272
- | Token | Use |
273
- |---|---|
274
- | `border-border-presentation-global-primary` | Panel borders, dividers |
275
- | `text-content-presentation-global-primary` | Primary text |
276
- | `text-content-presentation-global-secondary` | Secondary text |
277
- | `text-content-presentation-global-tertiary` | Muted labels |
278
- | `text-content-presentation-action-primary` | Active state text |
279
- | `bg-content-presentation-action-primary` | Active-tab + slider track + selected-mode bg |
280
-
281
- These `content-presentation-global-*` tokens **do exist** for text/borders — only the `background-*-global-*` family is missing.
282
-
283
- ### Theme switching
284
-
285
- `DataViewsLayout` accepts `theme?: "dark" | "light" | "default"` and applies it via `data-theme={theme}`. Without it, the component inherits the nearest ancestor's `data-theme` (set by `ThemeProvider`).
286
-
287
- ---
288
-
289
- ## 11. Tree view internals
290
-
291
- `TreeView.tsx` has the most complex layout. Worth a separate note.
292
-
293
- **Three columns** in desktop mode:
294
- 1. **Tree sidebar** (`TreeSidebar`) — the tree itself
295
- 2. **Filter panel** (`FilterPanel`) — only when `showFilters && hasFilterableFields`
296
- 3. **Right pane** — toggles between two modes via the toolbar:
297
- - **Table mode** (default) — `<TableView>` over the selected node + descendants
298
- - **Details mode** — single-record detail view with nested-object rendering
299
-
300
- The tree owns the filter state; the embedded `TableView` is told `showFilters={false}` so it doesn't render its own filter panel.
301
-
302
- **Mobile** collapses the tree sidebar into a left-edge drawer (`TreeDrawer` via `vaul`). The right pane gets a hamburger trigger.
303
-
304
- **Auto-detection**: if neither `treeConfig.childrenField` nor `treeConfig.parentField` is set, the view inspects the first record:
305
- - nested: `children`, `items`, `kids`, `subItems`, `nodes`
306
- - flat: `parentId`, `parent_id`, `parent`, `managerId`, `manager`
307
-
308
- If nothing matches and no `views.tree` override, the Tree tab auto-hides.
309
-
310
- ---
311
-
312
- ## 12. Filter panel rules
313
-
314
- A field becomes filterable when any of:
315
- 1. `field.filterable === true` (explicit opt-in)
316
- 2. `type` is `enum-badge`, `boolean`, `badge-array`, or `icon-text` (auto)
317
- 3. type is text with ≤10 unique values (heuristic fallback)
318
-
319
- Numeric / date fields **require explicit `filterable: true`** — they get range sliders / date pickers respectively.
320
-
321
- The panel renders three filter kinds:
322
- - **Categorical** — checkbox list (multi-select OR semantics)
323
- - **Numeric range** — Radix slider + min/max inputs + preset chips
324
- - **Date range** — Radix popover with `react-day-picker` + preset chips (`Today`, `Last 7 days`, `Last 30 days`, `This year`)
325
-
326
- Filter state shape:
327
- ```ts
328
- type FilterValue =
329
- | string[] // categorical
330
- | { kind: "number"; min?: number; max?: number } // numeric range
331
- | { kind: "date"; from?: string; to?: string } // date range, ISO YYYY-MM-DD
332
- ```
333
-
334
- ---
335
-
336
- ## 13. Settings panel sections
337
-
338
- `SettingsPanel.tsx` renders sections conditionally based on `currentView`:
339
-
340
- | Section | When | What it controls |
341
- |---|---|---|
342
- | General → Show Filters | always | `config.showFilters` |
343
- | Table Columns | `currentView === "table"` | Column visibility (Switch) + reorder (HTML5 DnD) |
344
- | Kanban Grouping | `currentView === "kanban"` | Radio over enum-badge fields → `kanbanGroupBy` |
345
- | Inbox Layout → Show Preview Pane | `currentView === "inbox"` | `config.showPreviewPane` |
346
- | Sort | always (if columns exist) | Per-column three-state: none/asc/desc |
347
-
348
- ---
349
-
350
- ## 14. CLI distribution
351
-
352
- The DataViews folder + utils folder + hooks are **not yet** distributable via `torch-glare add <Component>` — the existing CLI scans top-level `.tsx` files only. Adding `DataViews/` as a unit requires extending the CLI to recognize folder-based components and copy:
353
- - `apps/lib/components/DataViews/` (whole folder)
354
- - `apps/lib/utils/dataViews/` (whole folder)
355
- - `apps/lib/hooks/useDataViewsState.ts`
356
- - `apps/lib/hooks/useIsMobile.ts`
357
-
358
- …plus running `getDependenciesAndInstallNestedComponents` recursively across all those files to install any missing peer deps (`@radix-ui/react-slider`, `react-day-picker`, `vaul`).
359
-
360
- ---
361
-
362
- ## 15. MCP documentation
363
-
364
- Public-facing docs live in `mcp/docs/`:
365
-
366
- - `mcp/docs/components/data-views-layout.md` — main API reference
367
- - `mcp/docs/components/table-view.md`
368
- - `mcp/docs/components/kanban-view.md`
369
- - `mcp/docs/components/inbox-view.md`
370
- - `mcp/docs/components/tree-view.md`
371
- - `mcp/docs/how-to/data-views-from-backend-response.md` — recipe guide for consumers
372
-
373
- Registered in `mcp/docs/llms-manifest.json` under `components.dataDisplay`. Update versions there when bumping.
374
-
375
- ---
376
-
377
- ## 16. Common change checklist
378
-
379
- When updating UI on this feature, touch in this order:
380
-
381
- 1. **Token swap** — never use `*-global-*` backgrounds. Check the table in §10.
382
- 2. **Badge** — use `resolveBadgeVariant()` in the adapter, don't bypass it. If you need a new variant, add it to both `BadgeVariant` type (types.ts) and the switch in `badgeAdapter.ts`.
383
- 3. **Field renderer** — add new field types in `fieldRenderers.tsx` + register in the `RENDERERS` lookup table at the bottom + extend `FieldType` in types.ts.
384
- 4. **Layout change** — if it touches all four views, update each view's root container individually; the layout doesn't own view chrome.
385
- 5. **State change** — extend `useDataViewsState` first, then thread the new field through `DataViewsLayout` props.
386
- 6. **Docs** — update the relevant `.md` in `mcp/docs/components/` AND `mcp/docs/how-to/data-views-from-backend-response.md` if it changes the consumer-facing API.
387
-
388
- ---
389
-
390
- ## 17. File inventory (quick reference)
391
-
392
- ```
393
- apps/lib/components/DataViews/
394
- ├── ARCHITECTURE.md ← this file
395
- ├── DataViewsLayout.tsx ← the frame
396
- ├── TableView.tsx
397
- ├── KanbanView.tsx
398
- ├── InboxView.tsx
399
- ├── TreeView.tsx
400
- ├── FilterPanel.tsx
401
- ├── SettingsPanel.tsx
402
- ├── fieldRenderers.tsx ← per-type JSX
403
- ├── badgeAdapter.ts ← BadgeVariant → glare Badge props
404
- ├── types.ts ← all shared types
405
- ├── index.ts ← public barrel
406
- ├── filters/
407
- │ ├── DateRangePopover.tsx
408
- │ ├── PresetChips.tsx
409
- │ └── RangeSliderWithInputs.tsx
410
- └── tree/
411
- ├── TreeDrawer.tsx
412
- ├── TreeNodeRow.tsx
413
- └── TreeSidebar.tsx
414
-
415
- apps/lib/utils/dataViews/
416
- ├── pathUtils.ts
417
- ├── fieldUtils.ts
418
- ├── columnUtils.ts
419
- ├── rangeUtils.ts
420
- ├── treeUtils.ts
421
- └── nestedDataUtils.tsx
422
-
423
- apps/lib/hooks/
424
- ├── useDataViewsState.ts ← shared state machine for composable mode
425
- └── useIsMobile.ts ← <768px detection
426
-
427
- apps/app/data-views/
428
- └── page.tsx ← demo page, /data-views route
429
-
430
- mcp/docs/components/
431
- ├── data-views-layout.md
432
- ├── table-view.md
433
- ├── kanban-view.md
434
- ├── inbox-view.md
435
- └── tree-view.md
436
-
437
- mcp/docs/how-to/
438
- └── data-views-from-backend-response.md
439
- ```