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
@@ -1,4 +1,4 @@
1
- import type { BadgeVariant } from "./types";
1
+ import type { BadgeVariant } from "../../utils/dataViews/types";
2
2
 
3
3
  type TorchBadgeColor =
4
4
  "gray" | "slate" | "red" | "orange" | "yellow" | "green" | "ocean" | "blue" | "purple" | "rose";
@@ -11,7 +11,7 @@ export type ResolvedBadgeProps = {
11
11
  };
12
12
 
13
13
  /**
14
- * Map the dataviews BadgeVariant set to torch-glare Badge props.
14
+ * Map the DataViews `BadgeVariant` set onto Glare `Badge` props.
15
15
  * Defaults to `subtle` style (glare's native default — colored dot + subtle bg).
16
16
  * The legacy "Light" / "navy" / "bluePurple" variants explicitly request solid
17
17
  * fills, kept distinct for backwards compat with the data-views examples.
@@ -0,0 +1,324 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { cn } from "../../utils/cn";
5
+ import { getByPath } from "../../utils/dataViews/path";
6
+ import type { CurrencyOptions, FieldConfig, Row } from "../../utils/dataViews/types";
7
+ import { Avatar, AvatarFallback, AvatarImage } from "../Avatar";
8
+ import { Badge } from "../Badge";
9
+ import { resolveBadgeVariant } from "./badge";
10
+
11
+ /**
12
+ * Paints one field of one row.
13
+ *
14
+ * This is the whole of DataViews' relationship with your data: read the value at `field.path` and
15
+ * turn it into pixels. Every view — table cell, board card, inbox row, tree node — goes through
16
+ * here, so a field looks the same wherever it appears and there is one place to add a type.
17
+ *
18
+ * `field.render` wins over `field.type`, so anything not covered below is a one-line escape hatch
19
+ * rather than a reason to extend `FieldType`.
20
+ */
21
+ export function Cell({
22
+ field,
23
+ row,
24
+ className,
25
+ }: {
26
+ field: FieldConfig;
27
+ row: Row;
28
+ className?: string;
29
+ }) {
30
+ const value = getByPath(row, field.path);
31
+
32
+ if (field.render) return <>{field.render(value, row)}</>;
33
+
34
+ const type = field.type ?? "text";
35
+ if (type === "hidden") return null;
36
+
37
+ // `boolean` and `progress-bar` render something meaningful for a missing value — false, and an
38
+ // empty bar — so they are the two types that never show the placeholder.
39
+ if (isBlank(value) && type !== "boolean" && type !== "progress-bar") {
40
+ return className ? <span className={className}>{NULL_PLACEHOLDER}</span> : NULL_PLACEHOLDER;
41
+ }
42
+
43
+ const content = paint(type, field, row, value);
44
+ if (content === null) return null;
45
+ return className ? <span className={className}>{content}</span> : <>{content}</>;
46
+ }
47
+
48
+ const NULL_PLACEHOLDER = <span className="text-content-presentation-global-tertiary">-</span>;
49
+
50
+ /**
51
+ * An empty array counts as blank. It has to: `String([])` is `""`, so without this an `image`
52
+ * field handed `[]` renders `<img src="">`, which the browser resolves to the current page and
53
+ * re-downloads it. `badge-array` checks `Array.isArray` before this ever applies.
54
+ */
55
+ function isBlank(value: unknown) {
56
+ if (Array.isArray(value)) return value.length === 0;
57
+ return value === undefined || value === null || value === "";
58
+ }
59
+
60
+ // ─── Formatting helpers ───────────────────────────────────────────────────────
61
+
62
+ function formatWithToken(d: Date, token: string): string {
63
+ const pad = (n: number) => String(n).padStart(2, "0");
64
+ return token
65
+ .replace(/YYYY/g, String(d.getFullYear()))
66
+ .replace(/MM/g, pad(d.getMonth() + 1))
67
+ .replace(/DD/g, pad(d.getDate()))
68
+ .replace(/HH/g, pad(d.getHours()))
69
+ .replace(/mm/g, pad(d.getMinutes()))
70
+ .replace(/ss/g, pad(d.getSeconds()));
71
+ }
72
+
73
+ function toInitials(s: string): string {
74
+ if (!s) return "?";
75
+ const parts = String(s).trim().split(/\s+/).slice(0, 2);
76
+ return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
77
+ }
78
+
79
+ function IconNode({ icon }: { icon: string }) {
80
+ if (/^ri-/.test(icon)) return <i className={icon} />;
81
+ return <span aria-hidden>{icon}</span>;
82
+ }
83
+
84
+ // ─── The type switch ──────────────────────────────────────────────────────────
85
+
86
+ function paint(
87
+ type: NonNullable<FieldConfig["type"]>,
88
+ field: FieldConfig,
89
+ row: Row,
90
+ value: unknown,
91
+ ): React.ReactNode {
92
+ switch (type) {
93
+ case "number": {
94
+ return (
95
+ <span className="font-mono text-content-presentation-global-primary">
96
+ {typeof value === "number" ? value.toLocaleString() : String(value)}
97
+ </span>
98
+ );
99
+ }
100
+
101
+ case "date":
102
+ return <span className="text-content-presentation-global-primary">{String(value)}</span>;
103
+
104
+ case "date-format": {
105
+ const opts = field.dateFormat;
106
+ let formatted = String(value);
107
+ try {
108
+ const d = value instanceof Date ? value : new Date(value as string | number);
109
+ if (!Number.isNaN(d.getTime())) {
110
+ if (typeof opts === "object" && opts) {
111
+ formatted = new Intl.DateTimeFormat(undefined, opts).format(d);
112
+ } else if (typeof opts === "string") {
113
+ formatted = formatWithToken(d, opts);
114
+ } else {
115
+ formatted = new Intl.DateTimeFormat(undefined, {
116
+ year: "numeric",
117
+ month: "short",
118
+ day: "numeric",
119
+ }).format(d);
120
+ }
121
+ }
122
+ } catch {
123
+ /* fall through to the raw string */
124
+ }
125
+ return <span className="text-content-presentation-global-primary">{formatted}</span>;
126
+ }
127
+
128
+ case "boolean": {
129
+ const isTrue = !!value;
130
+ const variant = isTrue ? (field.trueVariant ?? "green") : (field.falseVariant ?? "gray");
131
+ return (
132
+ <Badge
133
+ {...resolveBadgeVariant(variant)}
134
+ label={isTrue ? (field.trueLabel ?? "Yes") : (field.falseLabel ?? "No")}
135
+ size="S"
136
+ />
137
+ );
138
+ }
139
+
140
+ case "enum-badge": {
141
+ const key = String(value);
142
+ const variant = field.variants?.[key] ?? field.defaultVariant ?? "gray";
143
+ return <Badge {...resolveBadgeVariant(variant)} label={key} size="S" />;
144
+ }
145
+
146
+ case "badge-array": {
147
+ const badgeProps = resolveBadgeVariant(field.variant ?? "blue");
148
+ if (!Array.isArray(value)) {
149
+ return <Badge {...badgeProps} label={String(value)} size="XS" />;
150
+ }
151
+ const limit = field.limit ?? value.length;
152
+ const head = value.slice(0, limit);
153
+ const overflow = value.length - head.length;
154
+ return (
155
+ <div className="flex flex-wrap gap-1">
156
+ {/* Keyed by position, not by value — a badge-array is free to repeat one. */}
157
+ {head.map((v, i) => (
158
+ <Badge key={i} {...badgeProps} label={String(v)} size="XS" />
159
+ ))}
160
+ {overflow > 0 && (
161
+ <Badge {...resolveBadgeVariant("gray")} label={`+${overflow}`} size="XS" />
162
+ )}
163
+ </div>
164
+ );
165
+ }
166
+
167
+ case "currency": {
168
+ const num = Number(value);
169
+ if (Number.isNaN(num)) {
170
+ return <span className="text-content-presentation-global-primary">{String(value)}</span>;
171
+ }
172
+ const opts: CurrencyOptions =
173
+ typeof field.currency === "string" ? { code: field.currency } : (field.currency ?? {});
174
+ const plain = () =>
175
+ `${opts.symbol ?? "$"}${num.toLocaleString(opts.locale, {
176
+ minimumFractionDigits: opts.decimals,
177
+ maximumFractionDigits: opts.decimals,
178
+ })}`;
179
+
180
+ let formatted: string;
181
+ if (opts.code) {
182
+ try {
183
+ formatted = new Intl.NumberFormat(opts.locale, {
184
+ style: "currency",
185
+ currency: opts.code,
186
+ minimumFractionDigits: opts.decimals,
187
+ maximumFractionDigits: opts.decimals,
188
+ }).format(num);
189
+ } catch {
190
+ formatted = plain();
191
+ }
192
+ } else {
193
+ formatted = plain();
194
+ }
195
+ return <span className="font-semibold text-green-600">{formatted}</span>;
196
+ }
197
+
198
+ case "number-format": {
199
+ const num = Number(value);
200
+ if (Number.isNaN(num)) {
201
+ return <span className="text-content-presentation-global-primary">{String(value)}</span>;
202
+ }
203
+ return (
204
+ <span className="font-mono text-content-presentation-global-primary">
205
+ {new Intl.NumberFormat(undefined, field.format).format(num)}
206
+ </span>
207
+ );
208
+ }
209
+
210
+ case "progress-bar": {
211
+ const raw = Number(value);
212
+ const num = Number.isFinite(raw) ? raw : 0;
213
+ const pct = Math.max(0, Math.min(100, num));
214
+ // Thresholds are percentages of the bar, not values: below `warn` is red, at or above `ok`
215
+ // is green, and everything between is amber.
216
+ const [warn, ok] = field.thresholds ?? [40, 70];
217
+ const color = num >= ok ? "bg-green-500" : num >= warn ? "bg-yellow-500" : "bg-red-500";
218
+ return (
219
+ <div className="flex min-w-[120px] items-center gap-2">
220
+ <div className="bg-background-presentation-form-field-primary h-2 flex-1 overflow-hidden rounded-full">
221
+ <div className={cn("h-full transition-all", color)} style={{ width: `${pct}%` }} />
222
+ </div>
223
+ <span className="text-content-presentation-global-primary w-10 text-right text-xs font-medium tabular-nums">
224
+ {Math.round(num)}%
225
+ </span>
226
+ </div>
227
+ );
228
+ }
229
+
230
+ case "star-rating": {
231
+ const num = Number(value);
232
+ const max = field.max ?? 5;
233
+ const filled = Math.max(0, Math.min(max, Math.floor(num)));
234
+ return (
235
+ <div className="flex items-center gap-2">
236
+ <div className="flex">
237
+ {Array.from({ length: max }).map((_, i) => (
238
+ <span key={i} className={i < filled ? "text-yellow-500" : "text-gray-300"} aria-hidden>
239
+
240
+ </span>
241
+ ))}
242
+ </div>
243
+ <span className="text-sm font-semibold tabular-nums">
244
+ {Number.isFinite(num) ? num : "-"}
245
+ </span>
246
+ </div>
247
+ );
248
+ }
249
+
250
+ case "icon-text": {
251
+ const icon = field.icon ?? "";
252
+ const after = field.iconPosition === "after";
253
+ return (
254
+ <span className="text-content-presentation-global-primary inline-flex items-center gap-1.5">
255
+ {!after && icon && <IconNode icon={icon} />}
256
+ <span>{String(value)}</span>
257
+ {after && icon && <IconNode icon={icon} />}
258
+ </span>
259
+ );
260
+ }
261
+
262
+ case "two-line": {
263
+ const secondary = field.secondaryPath != null ? getByPath(row, field.secondaryPath) : null;
264
+ return (
265
+ <div className="leading-tight">
266
+ <div className="text-content-presentation-global-primary font-semibold">
267
+ {String(value)}
268
+ </div>
269
+ {secondary != null && (
270
+ <div className="text-content-presentation-global-secondary text-xs">
271
+ {String(secondary)}
272
+ </div>
273
+ )}
274
+ </div>
275
+ );
276
+ }
277
+
278
+ case "avatar": {
279
+ const src = typeof value === "string" ? value : null;
280
+ // Without a `fallbackPath` there is no name to derive initials from — falling back to the
281
+ // image URL would spell them out of "ht".
282
+ const fallbackSource = field.fallbackPath != null ? getByPath(row, field.fallbackPath) : null;
283
+ const initials = toInitials(String(fallbackSource ?? "?"));
284
+ return (
285
+ <Avatar>
286
+ {src && <AvatarImage src={src} alt={initials} />}
287
+ <AvatarFallback>{initials}</AvatarFallback>
288
+ </Avatar>
289
+ );
290
+ }
291
+
292
+ case "link": {
293
+ const v = String(value);
294
+ let href = v;
295
+ if (field.linkType === "mailto" && !v.startsWith("mailto:")) href = `mailto:${v}`;
296
+ else if (field.linkType === "tel" && !v.startsWith("tel:")) href = `tel:${v}`;
297
+ return (
298
+ <a
299
+ href={href}
300
+ target={field.linkType === "url" ? "_blank" : undefined}
301
+ rel={field.linkType === "url" ? "noopener noreferrer" : undefined}
302
+ className="text-blue-600 hover:underline"
303
+ onClick={(e) => e.stopPropagation()}
304
+ >
305
+ {v}
306
+ </a>
307
+ );
308
+ }
309
+
310
+ case "image":
311
+ return (
312
+ // eslint-disable-next-line @next/next/no-img-element -- the src is arbitrary consumer data
313
+ <img
314
+ src={String(value)}
315
+ alt=""
316
+ className="border-border-presentation-global-primary h-10 w-10 rounded border object-cover"
317
+ />
318
+ );
319
+
320
+ case "text":
321
+ default:
322
+ return <span className="text-content-presentation-global-primary">{String(value)}</span>;
323
+ }
324
+ }
@@ -0,0 +1,144 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type {
5
+ ColumnState,
6
+ FieldConfig,
7
+ FilterFieldDescriptor,
8
+ FilterState,
9
+ Row,
10
+ Sort,
11
+ } from "../../utils/dataViews/types";
12
+
13
+ /**
14
+ * Every context DataViews uses. As in `FormBuilder`, this is the only file that calls
15
+ * `createContext` — a part never invents its own provider, so there is one place to look when
16
+ * asking "where does this part get its data".
17
+ */
18
+
19
+ // ─── Data ─────────────────────────────────────────────────────────────────────
20
+
21
+ export interface DataContextValue {
22
+ /** The rows to paint, exactly as handed in. Already filtered and sorted by the app. */
23
+ rows: readonly Row[];
24
+ /** How to paint each field. */
25
+ fields: readonly FieldConfig[];
26
+ /** The visible fields, in the order the user arranged them. Resolved once by the root. */
27
+ visibleFields: readonly FieldConfig[];
28
+ /** Stable identity for a row — selection and drag both key off this. */
29
+ getRowId: (row: Row, index: number) => string;
30
+ loading: boolean;
31
+ /** True while the *next* page is in flight — `loading` is the first one. */
32
+ loadingMore: boolean;
33
+ /** Whether there is a next page: derived from `rows.length < total`, never passed in. */
34
+ hasMore: boolean;
35
+ /** Ask for the next page. Undefined when the caller did not opt into scroll loading. */
36
+ onLoadMore?: () => void;
37
+ }
38
+
39
+ export const DataContext = createContext<DataContextValue | null>(null);
40
+
41
+ export function useDataViewsData(): DataContextValue {
42
+ const ctx = useContext(DataContext);
43
+ if (!ctx) throw new Error("DataViews parts must be rendered inside <DataViews>.");
44
+ return ctx;
45
+ }
46
+
47
+ // ─── View ─────────────────────────────────────────────────────────────────────
48
+
49
+ /** One entry in the switcher. Registered by rendering the view, never by a visibility map. */
50
+ export interface RegisteredView {
51
+ id: string;
52
+ label: string;
53
+ icon?: React.ReactNode;
54
+ }
55
+
56
+ /** The root's interaction state — everything the user can change that is not a row. */
57
+ export interface ViewContextValue {
58
+ view: string;
59
+ setView: (id: string) => void;
60
+ /** In the order the views were rendered. */
61
+ views: readonly RegisteredView[];
62
+ search: string;
63
+ setSearch: (q: string) => void;
64
+ sort: Sort;
65
+ setSort: (sort: Sort) => void;
66
+ selection: readonly string[];
67
+ setSelection: (ids: readonly string[]) => void;
68
+ /** The row the detail pane is showing, for the views that have one. */
69
+ activeId: string | null;
70
+ setActiveId: (id: string | null) => void;
71
+ /** Column visibility and order. The panel edits these; the root paints from them. */
72
+ columns: readonly ColumnState[];
73
+ setColumns: (columns: readonly ColumnState[]) => void;
74
+ }
75
+
76
+ export const ViewContext = createContext<ViewContextValue | null>(null);
77
+
78
+ export function useDataViewsView(): ViewContextValue {
79
+ const ctx = useContext(ViewContext);
80
+ if (!ctx) throw new Error("DataViews parts must be rendered inside <DataViews>.");
81
+ return ctx;
82
+ }
83
+
84
+ // ─── Panel ────────────────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Whether the settings panel is showing. Provided by the **root**, not by `Panel` — the toggle
88
+ * lives in the header, which is `Panel`'s sibling, so neither can own the state the other needs.
89
+ */
90
+ export interface PanelContextValue {
91
+ open: boolean;
92
+ setOpen: (open: boolean) => void;
93
+ }
94
+
95
+ export const PanelContext = createContext<PanelContextValue | null>(null);
96
+
97
+ export function useDataViewsPanel(): PanelContextValue {
98
+ const ctx = useContext(PanelContext);
99
+ if (!ctx) throw new Error("DataViews parts must be rendered inside <DataViews>.");
100
+ return ctx;
101
+ }
102
+
103
+ /** One entry in the panel's tab strip, registered by rendering a `DataViews.Panel.Tab`. */
104
+ export interface PanelTab {
105
+ value: string;
106
+ label: string;
107
+ icon?: React.ReactNode;
108
+ }
109
+
110
+ /** Which tab the panel is on. Owned by `Panel`, because only its own children care. */
111
+ export interface PanelTabsContextValue {
112
+ tab: string;
113
+ setTab: (tab: string) => void;
114
+ tabs: readonly PanelTab[];
115
+ }
116
+
117
+ export const PanelTabsContext = createContext<PanelTabsContextValue | null>(null);
118
+
119
+ export function useDataViewsPanelTabs(): PanelTabsContextValue {
120
+ const ctx = useContext(PanelTabsContext);
121
+ if (!ctx) throw new Error("DataViews.Panel parts must be rendered inside <DataViews.Panel>.");
122
+ return ctx;
123
+ }
124
+
125
+ // ─── Filters ──────────────────────────────────────────────────────────────────
126
+
127
+ export interface FiltersContextValue {
128
+ filters: FilterState;
129
+ setFilters: (filters: FilterState) => void;
130
+ /**
131
+ * What each filter control is, read off the `FormBuilder` children of `DataViews.Filters` —
132
+ * never derived from the rows. The root collects them as well, so `Filters.Summary` can label a
133
+ * chip even when it is rendered outside `Filters`.
134
+ */
135
+ filterFields: readonly FilterFieldDescriptor[];
136
+ }
137
+
138
+ export const FiltersContext = createContext<FiltersContextValue | null>(null);
139
+
140
+ export function useDataViewsFilters(): FiltersContextValue {
141
+ const ctx = useContext(FiltersContext);
142
+ if (!ctx) throw new Error("DataViews.Filters parts must be rendered inside <DataViews.Filters>.");
143
+ return ctx;
144
+ }