torch-glare 2.4.4 → 2.5.0

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 (147) 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 +30 -4
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
  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/Input.tsx +3 -0
  48. package/apps/lib/components/SearchableTable.tsx +5 -4
  49. package/apps/lib/components/SectionBlock.tsx +58 -11
  50. package/apps/lib/components/Select.tsx +3 -1
  51. package/apps/lib/components/TabSwitch.tsx +16 -4
  52. package/apps/lib/components/Table.tsx +265 -67
  53. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  54. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  55. package/apps/lib/components/TreeFolder/index.ts +1 -1
  56. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  57. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  58. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  59. package/apps/lib/registry.json +159 -4
  60. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  61. package/apps/lib/utils/dataViews/path.ts +67 -0
  62. package/apps/lib/utils/dataViews/query.ts +73 -0
  63. package/apps/lib/utils/dataViews/types.ts +187 -0
  64. package/docs/components/breadcrumb.md +1 -1
  65. package/docs/components/button-group.md +1 -1
  66. package/docs/components/button.md +1 -1
  67. package/docs/components/card.md +1 -1
  68. package/docs/components/checkbox.md +1 -1
  69. package/docs/components/data-views/backend-response.md +324 -0
  70. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  71. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  72. package/docs/components/data-views/examples/fields.md +362 -0
  73. package/docs/components/data-views/examples/filters.md +308 -0
  74. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  75. package/docs/components/data-views/examples/index.md +29 -0
  76. package/docs/components/data-views/examples/overview.md +244 -0
  77. package/docs/components/data-views/examples/panel.md +212 -0
  78. package/docs/components/data-views/examples/scale.md +231 -0
  79. package/docs/components/data-views/examples/server-side.md +210 -0
  80. package/docs/components/data-views/examples/state.md +250 -0
  81. package/docs/components/data-views/examples/tree-custom.md +388 -0
  82. package/docs/components/data-views/examples/view-registry.md +313 -0
  83. package/docs/components/data-views/examples/views.md +534 -0
  84. package/docs/components/data-views/guide.md +405 -0
  85. package/docs/components/data-views/index.md +1504 -0
  86. package/docs/components/data-views/migration.md +79 -0
  87. package/docs/components/date-picker.md +0 -1
  88. package/docs/components/form-builder.md +19 -8
  89. package/docs/components/form-renderer.md +2 -1
  90. package/docs/components/form.md +1 -1
  91. package/docs/components/input-field.md +1 -1
  92. package/docs/components/input-otp.md +1 -1
  93. package/docs/components/input.md +1 -1
  94. package/docs/components/labeled-check-box.md +1 -1
  95. package/docs/components/labeled-radio.md +1 -1
  96. package/docs/components/radio-card.md +1 -1
  97. package/docs/components/radio.md +1 -1
  98. package/docs/components/search-field.md +1 -1
  99. package/docs/components/section-block.md +79 -3
  100. package/docs/components/select.md +1 -1
  101. package/docs/components/simple-select.md +1 -1
  102. package/docs/components/switch.md +1 -1
  103. package/docs/components/tab-switch.md +1 -1
  104. package/docs/components/table.md +45 -8
  105. package/docs/components/text-editor.md +1 -1
  106. package/docs/components/textarea.md +1 -1
  107. package/docs/components/toggle-button.md +1 -1
  108. package/docs/components/toggle.md +1 -1
  109. package/docs/components/tree-folder.md +110 -0
  110. package/docs/how-to/forms-with-form-builder.md +6 -4
  111. package/docs/reference/components.md +16 -6
  112. package/docs/tutorials/component-composition.md +11 -13
  113. package/package.json +3 -2
  114. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  115. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  116. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  117. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  118. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  119. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  120. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  121. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  122. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  123. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  124. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  125. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  126. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  127. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  128. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  129. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  130. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  131. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  132. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  133. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  134. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  135. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  136. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  137. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  138. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  139. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  140. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  141. package/docs/components/data-views-config-panel.md +0 -208
  142. package/docs/components/data-views-layout.md +0 -291
  143. package/docs/components/inbox-view.md +0 -170
  144. package/docs/components/kanban-view.md +0 -135
  145. package/docs/components/table-view.md +0 -141
  146. package/docs/components/tree-view.md +0 -147
  147. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -0,0 +1,218 @@
1
+ ---
2
+ title: DataViews example — Inbox → route
3
+ description: `itemHref` + `linkComponent` — the detail pane driven by the URL.
4
+ group: examples
5
+ component: DataViews
6
+ keywords: [data-views, example, examples, inbox, routing]
7
+ ---
8
+
9
+ # DataViews example — Inbox → route
10
+
11
+ `itemHref` + `linkComponent` — the detail pane driven by the URL.
12
+
13
+ Complete and runnable — this is the page itself, not an excerpt. In the monorepo it lives at `apps/app/data-views/inbox-routing/page.tsx` and `apps/app/data-views/inbox-routing/[id]/page.tsx`.
14
+
15
+ See the [component reference](../index.md) for what each prop does, or the [guide](../guide.md) for the same ground as scenarios.
16
+
17
+ ## `apps/app/data-views/inbox-routing/page.tsx`
18
+
19
+ ```tsx
20
+ "use client";
21
+
22
+ import { useMemo, useState } from "react";
23
+ import Link from "next/link";
24
+ import { useInfiniteQuery } from "@tanstack/react-query";
25
+ import { Filter, Settings } from "lucide-react";
26
+ import { DataViews, emptyQuery, queryToParams, type SavedView } from "@/components/DataViews";
27
+ import { FormBuilder } from "@/components/FormBuilder";
28
+ import type {
29
+ DataViewsQuery,
30
+ FieldConfig,
31
+ Row,
32
+ } from "@/utils/dataViews/types";
33
+
34
+ // ─── Data ─────────────────────────────────────────────────────────────────────
35
+
36
+ interface Message extends Row {
37
+ id: string;
38
+ subject: string;
39
+ from: string;
40
+ preview?: string;
41
+ receivedAt?: string;
42
+ }
43
+
44
+ const FIELDS: FieldConfig[] = [
45
+ { path: "subject", label: "Subject", type: "text" },
46
+ { path: "from", label: "From", type: "text" },
47
+ { path: "preview", label: "Preview", type: "text" },
48
+ { path: "receivedAt", label: "Received", type: "date-format", dateFormat: "DD MMM" },
49
+ ];
50
+
51
+ const FROM_OPTIONS = [
52
+ { label: "logistics@acme.test", value: "logistics@acme.test" },
53
+ { label: "ap@initech.test", value: "ap@initech.test" },
54
+ { label: "supply@umbrella.test", value: "supply@umbrella.test" },
55
+ { label: "procurement@hooli.test", value: "procurement@hooli.test" },
56
+ { label: "ops@cyberdyne.test", value: "ops@cyberdyne.test" },
57
+ ];
58
+
59
+ /**
60
+ * The request this page makes. The querying happens in `app/api/messages/route.ts`.
61
+ */
62
+ async function fetchMessages(q: DataViewsQuery): Promise<{ rows: Message[]; total: number }> {
63
+ const params = queryToParams(q);
64
+ const res = await fetch(`/api/messages?${params}`);
65
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
66
+ return res.json();
67
+ }
68
+
69
+ // ─── Page ─────────────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * `itemHref` turns each row into a link and `linkComponent` swaps the plain anchor for the
73
+ * router's, so navigation stays client-side.
74
+ *
75
+ * Following one unmounts this page — the App Router replaces pages while keeping layouts — so the
76
+ * detail route has to stand on its own. Keeping the list beside the detail would mean hoisting
77
+ * `<DataViews>` into `inbox-routing/layout.tsx` and letting each page supply only the pane.
78
+ */
79
+ export default function InboxRoutingExample() {
80
+ const [query, setQuery] = useState(emptyQuery());
81
+ const [saved, setSaved] = useState<SavedView[]>([]);
82
+
83
+
84
+ const { data, isPending, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
85
+ queryKey: ["messages", { ...query, page: undefined }],
86
+ queryFn: ({ pageParam }) => fetchMessages({ ...query, page: pageParam }),
87
+ initialPageParam: 1,
88
+ // Undefined means "no more" — which is what the component's `hasMore` resolves to.
89
+ getNextPageParam: (last, pages) => {
90
+ const loaded = pages.reduce((n, page) => n + page.rows.length, 0);
91
+ return loaded < last.total ? pages.length + 1 : undefined;
92
+ },
93
+ });
94
+
95
+ const rows = useMemo(() => data?.pages.flatMap((page) => page.rows) ?? [], [data]);
96
+ const total = data?.pages[0]?.total ?? 0;
97
+
98
+ return (
99
+ <div className="flex h-full min-h-0 flex-col p-4">
100
+ <DataViews
101
+ rows={rows}
102
+ fields={FIELDS}
103
+ total={total}
104
+ loading={isPending}
105
+ onLoadMore={fetchNextPage}
106
+ loadingMore={isFetchingNextPage}
107
+ onQueryChange={setQuery}
108
+ className="h-full"
109
+ >
110
+ <DataViews.Header title="Inbox">
111
+ <DataViews.ViewSwitch />
112
+ <DataViews.Search />
113
+ <DataViews.PanelToggle />
114
+ </DataViews.Header>
115
+
116
+ <DataViews.Inbox
117
+ titlePath="subject"
118
+ datePath="receivedAt"
119
+ itemHref={(_row, id) => `/data-views/inbox-routing/${id}`}
120
+ linkComponent={Link}
121
+ placeholder={
122
+ <div className="typography-body-medium-regular text-content-presentation-global-secondary flex h-full items-center justify-center p-8">
123
+ Click a message to navigate to its own page.
124
+ </div>
125
+ }
126
+ />
127
+
128
+ <DataViews.Panel>
129
+ <DataViews.Panel.Tab value="config" label="Config." icon={<Settings />}>
130
+ {/* Saving is the app's job — a view outlives the component. Restoring is not:
131
+ hand the snapshot back and selecting it puts everything back internally. */}
132
+ <DataViews.Panel.SavedViews
133
+ views={saved}
134
+ onSave={(snapshot) =>
135
+ setSaved((prev) => [
136
+ ...prev,
137
+ { id: `view-${prev.length + 1}`, label: `View ${prev.length + 1}`, snapshot },
138
+ ])
139
+ }
140
+ />
141
+ <DataViews.Panel.Columns />
142
+ <DataViews.Panel.Sort />
143
+ </DataViews.Panel.Tab>
144
+
145
+ <DataViews.Panel.Tab value="filters" label="Filters" icon={<Filter />}>
146
+ {/* The controls are FormBuilder fields — the same Select and Slider any form in
147
+ this library uses. The <FormBuilder> itself lives inside Filters; you write only
148
+ its fields, and Filters reads each one's name, label and bounds to learn what
149
+ it is. */}
150
+ <DataViews.Filters
151
+ title={null}
152
+ className="border-b-0 p-0"
153
+ >
154
+ {/* A message carries no enum, no numeric field and one date, so only the
155
+ searchable single-select, the date range and the text match have data to bind
156
+ to. A checkbox list over a field that does not exist would filter to nothing. */}
157
+ <FormBuilder.Text name="subject" label="Subject contains" />
158
+ <FormBuilder.SearchableSelect name="from" label="From" options={FROM_OPTIONS} />
159
+ <FormBuilder.DateRange name="receivedAt" label="Received" />
160
+ </DataViews.Filters>
161
+ </DataViews.Panel.Tab>
162
+ </DataViews.Panel>
163
+ </DataViews>
164
+ </div>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ## `apps/app/data-views/inbox-routing/[id]/page.tsx`
170
+
171
+ ```tsx
172
+ import Link from "next/link";
173
+ import { notFound } from "next/navigation";
174
+ import { MESSAGES } from "../../../api/_lib/messages";
175
+
176
+ /**
177
+ * Where `itemHref` lands. A plain server-rendered page — no `DataViews` involved, because by this
178
+ * point the inbox has unmounted.
179
+ *
180
+ * This route is scoped to the one example that needs it. A `[id]` directly under `data-views/`
181
+ * would swallow every mistyped example slug and render a "detail" page instead of a 404.
182
+ *
183
+ * It reads the same store `/api/messages` serves rather than fetching from it: this already runs
184
+ * on the server, and a server component calling its own HTTP endpoint pays for a round trip to
185
+ * itself. The list page fetches because it runs in the browser.
186
+ */
187
+
188
+ export default async function MessageDetailPage({ params }: { params: Promise<{ id: string }> }) {
189
+ const { id } = await params;
190
+ const message = MESSAGES.find((m) => m.id === id);
191
+ if (!message) notFound();
192
+
193
+ return (
194
+ <div className="flex h-full flex-col gap-4 overflow-y-auto p-8">
195
+ <Link
196
+ href="/data-views/inbox-routing"
197
+ className="typography-body-small-regular text-content-presentation-action-link flex w-fit items-center gap-1"
198
+ >
199
+ <i className="ri-arrow-left-line" aria-hidden />
200
+ Back to the inbox
201
+ </Link>
202
+
203
+ <article className="flex flex-col gap-3">
204
+ <h1 className="typography-headers-medium-medium text-content-presentation-global-primary">
205
+ {message.subject}
206
+ </h1>
207
+ <p className="typography-body-small-regular text-content-presentation-global-secondary">
208
+ {message.from}
209
+ {message.receivedAt ? ` · ${message.receivedAt}` : " · no date"}
210
+ </p>
211
+ <p className="typography-body-medium-regular text-content-presentation-global-primary max-w-[65ch]">
212
+ {message.preview ?? "This message has no preview text."}
213
+ </p>
214
+ </article>
215
+ </div>
216
+ );
217
+ }
218
+ ```
@@ -0,0 +1,29 @@
1
+ ---
2
+ title: DataViews examples
3
+ description: Every runnable DataViews example page, as shipped documentation.
4
+ group: examples
5
+ component: DataViews
6
+ keywords: [data-views, example, examples, index]
7
+ ---
8
+
9
+ # DataViews examples
10
+
11
+ Each page below is complete and runnable. They ship with the package, so the code an agent is pointed at is code it can actually read.
12
+
13
+ | Example | Shows |
14
+ | --- | --- |
15
+ | [Views](./views.md) | All four views over one dataset, with drag round-trips. |
16
+ | [Inbox → route](./inbox-routing.md) | `itemHref` + `linkComponent` — the detail pane driven by the URL. |
17
+ | [Tree → custom UI](./tree-custom.md) | Every custom-UI seam of the tree: `renderNode`, `paneRows`, a custom cell, card and tab, `paneActions`, and a whole-pane override. |
18
+ | [Fields](./fields.md) | The field types, painted. |
19
+ | [Filters](./filters.md) | Every filter control, presets, custom filters and the summary. |
20
+ | [Server-side](./server-side.md) | `queryToParams` on the way out, `parseQuery` on the way in. |
21
+ | [Panel](./panel.md) | The settings rail: saved views, columns, sort — and the pane-mode round trip. |
22
+ | [State](./state.md) | Controlled versus uncontrolled query. |
23
+ | [View registry](./view-registry.md) | A view of your own via `markView`, beside the built-in four. |
24
+ | [Scale](./scale.md) | Virtualization and scroll loading at size. |
25
+ | [Keyboard & RTL](./a11y-rtl.md) | Keyboard paths and the RTL mirror. |
26
+ | [Everything at once](./overview.md) | Every part at once — the fastest way to see the whole shape. |
27
+ | [The endpoint these examples call](./api-orders-route.md) | The route handler every DataViews example fetches from — and the only worked example of `parseQuery` on the server. |
28
+
29
+ See the [component reference](../index.md) and the [guide](../guide.md).
@@ -0,0 +1,244 @@
1
+ ---
2
+ title: DataViews example — Everything at once
3
+ description: Every part at once — the fastest way to see the whole shape.
4
+ group: examples
5
+ component: DataViews
6
+ keywords: [data-views, example, examples, overview]
7
+ ---
8
+
9
+ # DataViews example — Everything at once
10
+
11
+ Every part at once — the fastest way to see the whole shape.
12
+
13
+ Complete and runnable — this is the page itself, not an excerpt. In the monorepo it lives at `apps/app/data-views/overview/page.tsx`.
14
+
15
+ See the [component reference](../index.md) for what each prop does, or the [guide](../guide.md) for the same ground as scenarios.
16
+
17
+ ```tsx
18
+ "use client";
19
+
20
+ import { useMemo, useState } from "react";
21
+ import { useMutation, useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
22
+ import { Filter, Settings } from "lucide-react";
23
+ import { Button } from "@/components/Button";
24
+ import { DataViews, emptyQuery, queryToParams, type SavedView } from "@/components/DataViews";
25
+ import { FormBuilder } from "@/components/FormBuilder";
26
+ import type {
27
+ DataViewsQuery,
28
+ FieldConfig,
29
+ Row,
30
+ RowGroup,
31
+ TreeNode,
32
+ } from "@/utils/dataViews/types";
33
+
34
+ // ─── Data ─────────────────────────────────────────────────────────────────────
35
+
36
+ interface Order extends Row {
37
+ id: number;
38
+ customer: { name: string };
39
+ status: "Pending" | "Shipped" | "Delivered";
40
+ priority: "High" | "Medium" | "Low";
41
+ total: number;
42
+ createdAt: string;
43
+ }
44
+
45
+ const FIELDS: FieldConfig[] = [
46
+ { path: "id", label: "Order #", type: "number" },
47
+ { path: "customer.name", label: "Customer", type: "text" },
48
+ { path: "brand.name", label: "Brand", type: "text" },
49
+ { path: "status", label: "Status", type: "enum-badge", variants: { Pending: "yellow", Shipped: "blue", Delivered: "green" } },
50
+ { path: "priority", label: "Priority", type: "enum-badge", variants: { High: "redOrange", Medium: "purple", Low: "gray" } },
51
+ { path: "total", label: "Total", type: "currency", currency: "USD" },
52
+ { path: "createdAt", label: "Created", type: "date-format", dateFormat: "YYYY-MM-DD" },
53
+ ];
54
+
55
+ /** Dynamic sets — in a real app these come from the endpoint that also does the filtering. */
56
+ const CUSTOMER_OPTIONS = [
57
+ "Acme Inc.", "Globex Corp.", "Initech", "Umbrella",
58
+ "Hooli", "Stark Industries", "Wayne Enterprises", "Cyberdyne",
59
+ ].map((v) => ({ label: v, value: v }));
60
+
61
+ const BRAND_OPTIONS = ["Bosch", "Makita", "DeWalt", "Hilti"].map((v) => ({ label: v, value: v }));
62
+
63
+ const PRIORITY_OPTIONS = ["High", "Medium", "Low"].map((v) => ({ label: v, value: v }));
64
+
65
+ const STATUS_OPTIONS = [
66
+ { label: "Pending", value: "Pending" },
67
+ { label: "Shipped", value: "Shipped" },
68
+ { label: "Delivered", value: "Delivered" },
69
+ ];
70
+
71
+ /**
72
+ * The request this page makes. The querying itself happens in `app/api/orders/route.ts` —
73
+ * nothing on this page filters, sorts or pages anything, which is the split DataViews is built
74
+ * around.
75
+ */
76
+ async function fetchOrders(q: DataViewsQuery): Promise<{ rows: Order[]; total: number }> {
77
+ const params = queryToParams(q);
78
+ const res = await fetch(`/api/orders?${params}`);
79
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
80
+ return res.json();
81
+ }
82
+
83
+ /** The other half of `onRowMove`: the board emits intent, this is what persists it. */
84
+ async function moveOrder(body: { id?: number; status?: string; reset?: boolean }) {
85
+ const res = await fetch("/api/orders", {
86
+ method: "PATCH",
87
+ headers: { "Content-Type": "application/json" },
88
+ body: JSON.stringify(body),
89
+ });
90
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
91
+ return res.json();
92
+ }
93
+
94
+ const groupByStatus = (rows: readonly Order[]): RowGroup[] =>
95
+ (["Pending", "Shipped", "Delivered", "Cancelled"] as const).map((status) => ({
96
+ id: status,
97
+ label: status,
98
+ color: ({ Pending: "gray", Shipped: "blue", Delivered: "green", Cancelled: "red" } as const)[status],
99
+ rows: rows.filter((row) => row.status === status),
100
+ }));
101
+
102
+ /** Every node is a real row here, so the detail pane fills whichever one you select. */
103
+ const nodesFromRows = (rows: readonly Order[]): TreeNode[] => {
104
+ const [first, ...rest] = rows;
105
+ if (!first) return [];
106
+ return [
107
+ {
108
+ id: String(first.id),
109
+ row: first,
110
+ depth: 0,
111
+ children: rest.slice(0, 4).map((row) => ({ id: String(row.id), row, depth: 1, children: [] })),
112
+ },
113
+ ...rest.slice(4).map((row) => ({ id: String(row.id), row, depth: 0, children: [] })),
114
+ ];
115
+ };
116
+
117
+ // ─── Page ─────────────────────────────────────────────────────────────────────
118
+
119
+ /** Every part at once: four views, the rail, filters, an empty state and a drag round-trip. */
120
+ export default function OverviewExample() {
121
+ const queryClient = useQueryClient();
122
+ const [query, setQuery] = useState(emptyQuery());
123
+ const [saved, setSaved] = useState<SavedView[]>([]);
124
+
125
+
126
+
127
+ const { data, isPending, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
128
+ // The key *is* the query: touch any part of it and TanStack refetches, and a response that
129
+ // has been superseded is discarded rather than landing on top of a newer one.
130
+ queryKey: ["overview-orders", { ...query, page: undefined }],
131
+ queryFn: ({ pageParam }) => fetchOrders({ ...query, page: pageParam }),
132
+ initialPageParam: 1,
133
+ // Undefined means "no more" — which is what the component's `hasMore` resolves to.
134
+ getNextPageParam: (last, pages) => {
135
+ const loaded = pages.reduce((n, page) => n + page.rows.length, 0);
136
+ return loaded < last.total ? pages.length + 1 : undefined;
137
+ },
138
+ });
139
+
140
+ // Memoised because `data?.rows ?? []` is a new array on every render, which would make the
141
+ // `groups`/`nodes` memos below miss every time and hand the board a new array to diff.
142
+ const rows = useMemo(() => data?.pages.flatMap((page) => page.rows) ?? [], [data]);
143
+ const total = data?.pages[0]?.total ?? 0;
144
+ const groups = useMemo(() => groupByStatus(rows), [rows]);
145
+ const nodes = useMemo(() => nodesFromRows(rows), [rows]);
146
+
147
+ // Dropping a card emits intent; this is what persists it. The card settles where it landed
148
+ // only once the refetched rows agree.
149
+ const move = useMutation({
150
+ mutationFn: moveOrder,
151
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["overview-orders"] }),
152
+ });
153
+
154
+ return (
155
+ <div className="flex h-full min-h-0 flex-col p-4">
156
+ <DataViews
157
+ rows={rows}
158
+ fields={FIELDS}
159
+ total={total}
160
+ loading={isPending}
161
+ onLoadMore={fetchNextPage}
162
+ loadingMore={isFetchingNextPage}
163
+ onQueryChange={setQuery}
164
+ className="h-full"
165
+ >
166
+ <DataViews.Header title="Orders">
167
+ <DataViews.ViewSwitch />
168
+ <DataViews.Search />
169
+ <DataViews.Actions>
170
+ <Button variant="BluColStyle" size="M">New order</Button>
171
+ <Button variant="BluColStyle" size="M" onClick={() => move.mutate({ reset: true })}>
172
+ Reset
173
+ </Button>
174
+ </DataViews.Actions>
175
+ <DataViews.PanelToggle />
176
+ </DataViews.Header>
177
+
178
+ <DataViews.Table selectable />
179
+ <DataViews.Board
180
+ groups={groups}
181
+ titlePath="customer.name"
182
+ // A drop outside any column has nowhere to go, so there is nothing to persist.
183
+ onRowMove={(intent) => {
184
+ if (intent.to) move.mutate({ id: Number(intent.id), status: intent.to });
185
+ }}
186
+ />
187
+ <DataViews.Inbox
188
+ titlePath="customer.name"
189
+ datePath="createdAt"
190
+ >
191
+ <DataViews.Detail />
192
+ </DataViews.Inbox>
193
+ {/* The pane's tabs are children, like every other part: pass none and there is no pane. */}
194
+ <DataViews.Tree nodes={nodes} labelPath="customer.name">
195
+ <DataViews.Tree.Table />
196
+ <DataViews.Tree.Cards />
197
+ </DataViews.Tree>
198
+
199
+ <DataViews.Panel>
200
+ <DataViews.Panel.Tab value="config" label="Config." icon={<Settings />}>
201
+ {/* Saving is the app's job — a view outlives the component. Restoring is not:
202
+ hand the snapshot back and selecting it puts everything back internally. */}
203
+ <DataViews.Panel.SavedViews
204
+ views={saved}
205
+ onSave={(snapshot) =>
206
+ setSaved((prev) => [
207
+ ...prev,
208
+ { id: `view-${prev.length + 1}`, label: `View ${prev.length + 1}`, snapshot },
209
+ ])
210
+ }
211
+ />
212
+ <DataViews.Panel.Columns />
213
+ <DataViews.Panel.Sort />
214
+ </DataViews.Panel.Tab>
215
+
216
+ <DataViews.Panel.Tab value="filters" label="Filters" icon={<Filter />}>
217
+ {/* The controls are FormBuilder fields — the same Select and Slider any form in
218
+ this library uses. The <FormBuilder> itself lives inside Filters; you write only
219
+ its fields, and Filters reads each one's name, label and bounds to learn what
220
+ it is. */}
221
+ <DataViews.Filters
222
+ title={null}
223
+ className="border-b-0 p-0"
224
+ >
225
+ {/* One control per section type. Which one a field gets is decided by the data:
226
+ can the option set grow, and can the user pick more than one. */}
227
+ <FormBuilder.CheckboxGroup name="status" label="Status" options={STATUS_OPTIONS} />
228
+ <FormBuilder.RadioList name="priority" label="Priority" options={PRIORITY_OPTIONS} />
229
+ <FormBuilder.SearchableSelect
230
+ name="customer.name"
231
+ label="Customer"
232
+ options={CUSTOMER_OPTIONS}
233
+ />
234
+ <FormBuilder.MultiSelect name="brand.name" label="Brand" options={BRAND_OPTIONS} />
235
+ <FormBuilder.Slider name="total" label="Total" range min={0} max={15000} step={100} />
236
+ <FormBuilder.DateRange name="createdAt" label="Created" />
237
+ </DataViews.Filters>
238
+ </DataViews.Panel.Tab>
239
+ </DataViews.Panel>
240
+ </DataViews>
241
+ </div>
242
+ );
243
+ }
244
+ ```