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,393 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo, useState } from "react";
4
- import { X, Settings as SettingsIcon, Filter as FilterIcon, Plus } from "lucide-react";
5
- import type {
6
- ViewConfig,
7
- ViewType,
8
- FieldConfig,
9
- DynamicRecord,
10
- DynamicFilterConfig,
11
- FilterState,
12
- FilterValue,
13
- } from "./types";
14
-
15
- import { RadioGroup } from "../Radio";
16
- import { Button } from "../Button";
17
- import { FilterPanel } from "./FilterPanel";
18
- import { RadioRow, DataViewsSwitch } from "./PanelControls";
19
- import { cn } from "../../utils/cn";
20
-
21
- type ConfigTab = "config" | "filters";
22
-
23
- type SavedView = { id: string; label: string };
24
-
25
- export type DataViewsConfigPanelProps = {
26
- config: ViewConfig;
27
- onConfigChange: (config: Partial<ViewConfig>) => void;
28
- onClose: () => void;
29
- currentView: ViewType;
30
- fields: FieldConfig[];
31
-
32
- // Filters tab
33
- data: DynamicRecord[];
34
- filterState: FilterState;
35
- onFilterChange: (filters: FilterState) => void;
36
- filterConfig?: DynamicFilterConfig[];
37
-
38
- // Saved views (presentational shell — wire to persistence when available)
39
- savedViews?: SavedView[];
40
- activeSavedView?: string;
41
- onSavedViewChange?: (id: string) => void;
42
- onSaveNewView?: () => void;
43
-
44
- // Animation: drives slide-in/out. Parent keeps the panel mounted through
45
- // the close animation, then unmounts.
46
- state?: "open" | "closed";
47
- };
48
-
49
- const DEFAULT_SAVED_VIEWS: SavedView[] = [{ id: "default", label: "Default View" }];
50
-
51
- function SectionHeader({ title, action }: { title: string; action?: React.ReactNode }) {
52
- return (
53
- <div className="flex items-center justify-between">
54
- <h3 className="text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
55
- {title}
56
- </h3>
57
- {action}
58
- </div>
59
- );
60
- }
61
-
62
- /** 2×3 dot drag handle, matching the Figma SB-Column-Item grip (16×16 box,
63
- * compact ~1.5px dots, tight spacing — drawn as an SVG for pixel accuracy). */
64
- function GripDots() {
65
- return (
66
- <svg
67
- aria-hidden
68
- width="16"
69
- height="16"
70
- viewBox="0 0 16 16"
71
- // Panel is always-dark chrome (like the hardcoded white label text):
72
- // the grip stays white-on-dark regardless of host theme.
73
- className="text-white/60"
74
- fill="currentColor"
75
- >
76
- {[5.33, 9.33].flatMap((cx) =>
77
- [3.33, 8, 12.67].map((cy) => <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="1" />),
78
- )}
79
- </svg>
80
- );
81
- }
82
-
83
- /** 2px blue insertion line shown between rows during a drag-reorder. */
84
- function DropLine() {
85
- return (
86
- <div className="pointer-events-none relative h-0">
87
- <div className="absolute -top-[1px] left-0 right-0 h-[2px] rounded-full bg-[#005ECC]" />
88
- </div>
89
- );
90
- }
91
-
92
- export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
93
- const {
94
- config,
95
- onConfigChange,
96
- onClose,
97
- fields,
98
- data,
99
- filterState,
100
- onFilterChange,
101
- filterConfig,
102
- savedViews = DEFAULT_SAVED_VIEWS,
103
- activeSavedView,
104
- onSavedViewChange,
105
- onSaveNewView,
106
- state = "open",
107
- } = props;
108
-
109
- const [tab, setTab] = useState<ConfigTab>("config");
110
-
111
- // Saved View is controlled by the parent when `activeSavedView` /
112
- // `onSavedViewChange` are supplied; otherwise fall back to local state so the
113
- // radios are still interactive (DataViewsLayout doesn't thread these props
114
- // yet — without this the selection would snap back every click).
115
- const [internalSavedView, setInternalSavedView] = useState(() => savedViews[0]?.id);
116
- const selectedSavedView = activeSavedView ?? internalSavedView;
117
- const handleSavedViewChange = (id: string) => {
118
- if (onSavedViewChange) onSavedViewChange(id);
119
- else setInternalSavedView(id);
120
- };
121
-
122
- const visibleFields = useMemo(() => fields.filter((f) => f.type !== "hidden"), [fields]);
123
- const visiblePaths = useMemo(() => new Set(visibleFields.map((f) => f.path)), [visibleFields]);
124
- const fieldByPath = useMemo(
125
- () => new Map(visibleFields.map((f) => [f.path, f])),
126
- [visibleFields],
127
- );
128
- const orderedColumns = useMemo(
129
- () =>
130
- [...config.tableColumns]
131
- .filter((c) => visiblePaths.has(c.id))
132
- .sort((a, b) => a.order - b.order),
133
- [config.tableColumns, visiblePaths],
134
- );
135
-
136
- const toggleColumnVisibility = (path: string) => {
137
- const next = config.tableColumns.map((c) =>
138
- c.id === path ? { ...c, visible: !c.visible } : c,
139
- );
140
- onConfigChange({ tableColumns: next });
141
- };
142
-
143
- const [dragPath, setDragPath] = useState<string | null>(null);
144
- // Insertion slot in the ordered list: 0 means before the first row, N means
145
- // after the last row (count). Single source of truth — there is exactly one
146
- // indicator at a time, so no double-line ambiguity between adjacent rows.
147
- const [dropSlot, setDropSlot] = useState<number | null>(null);
148
-
149
- const reorderColumnToSlot = (sourcePath: string, slot: number) => {
150
- const ids = orderedColumns.map((c) => c.id);
151
- const from = ids.indexOf(sourcePath);
152
- if (from === -1) return;
153
- // Dropping into the same logical position (before or after itself) is a no-op.
154
- if (slot === from || slot === from + 1) return;
155
- const reordered = [...ids];
156
- reordered.splice(from, 1);
157
- // After removal, indices shift left by 1 for any slot beyond `from`.
158
- const insertAt = slot > from ? slot - 1 : slot;
159
- reordered.splice(insertAt, 0, sourcePath);
160
- const orderByPath = new Map(reordered.map((id, i) => [id, i]));
161
- const next = config.tableColumns.map((c) => {
162
- const newOrder = orderByPath.get(c.id);
163
- return newOrder == null ? c : { ...c, order: newOrder };
164
- });
165
- onConfigChange({ tableColumns: next });
166
- };
167
-
168
- const sortableColumns = orderedColumns;
169
-
170
- const tabBtn = (id: ConfigTab, icon: React.ReactNode, label: string) => {
171
- const active = tab === id;
172
- return (
173
- <button
174
- type="button"
175
- aria-pressed={active}
176
- onClick={() => setTab(id)}
177
- className={cn(
178
- "flex h-6 flex-1 items-center justify-center gap-1 rounded-[8px] px-3 text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
179
- active
180
- ? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
181
- : "bg-transparent text-white hover:bg-white/5",
182
- )}
183
- >
184
- <span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
185
- {icon}
186
- </span>
187
- {label}
188
- </button>
189
- );
190
- };
191
-
192
- return (
193
- <div
194
- data-state={state}
195
- // Panel is always dark (Figma `Cun` = #000000). data-theme="dark" makes
196
- // child themed components (Button, Switch, Radio, FilterPanel) resolve
197
- // dark tokens even when the host app runs in default/light theme.
198
- data-theme="dark"
199
- className={cn(
200
- "flex h-full w-[260px] flex-col overflow-hidden rounded-[16px] bg-black",
201
- "transition-opacity duration-200 ease-in-out",
202
- state === "open" ? "opacity-100" : "opacity-0",
203
- )}
204
- >
205
- {/* Header: tab switcher + close */}
206
- <div className="flex items-center gap-2 px-3 py-3">
207
- <div className="flex flex-1 items-center gap-[2px] rounded-[10px] bg-[#252729] p-[2px] shadow-[inset_0_0_5px_0_rgba(0,0,0,0.16)]">
208
- {tabBtn("config", <SettingsIcon />, "Config.")}
209
- {tabBtn("filters", <FilterIcon />, "Filters")}
210
- </div>
211
- <button
212
- type="button"
213
- onClick={onClose}
214
- aria-label="Close panel"
215
- className="flex h-7 w-7 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:bg-background-presentation-state-negative-primary hover:text-white"
216
- >
217
- <X className="h-[18px] w-[18px]" />
218
- </button>
219
- </div>
220
-
221
- <div className="h-px w-full bg-[#2C2D2E]" />
222
-
223
- <div className="flex-1 overflow-y-auto">
224
- {tab === "config" ? (
225
- <div className="flex flex-col gap-6 px-3 py-4">
226
- {/* Saved View */}
227
- <div className="space-y-3">
228
- <SectionHeader title="Saved View" />
229
- <RadioGroup
230
- value={selectedSavedView}
231
- onValueChange={handleSavedViewChange}
232
- className="flex flex-col gap-1 space-y-0 rounded-[12px] bg-[#1C1D1F] p-1"
233
- >
234
- {savedViews.map((sv, i) => (
235
- <div key={sv.id}>
236
- {/* Divider spans edge-to-edge (Figma: no horizontal
237
- inset). */}
238
- {i > 0 && <div className="h-px bg-[#2C2D2E]" />}
239
- <RadioRow value={sv.id} label={sv.label} />
240
- </div>
241
- ))}
242
- </RadioGroup>
243
- <Button
244
- type="button"
245
- variant="BorderStyle"
246
- size="M"
247
- onClick={onSaveNewView}
248
- className="w-full"
249
- >
250
- <Plus className="h-4 w-4" />
251
- Save a New View
252
- </Button>
253
- </div>
254
-
255
- <div className="h-px w-full bg-[#2C2D2E]" />
256
-
257
- {/* Table Columns */}
258
- <div className="space-y-3">
259
- <SectionHeader title="Table Columns" />
260
- {orderedColumns.length === 0 ? (
261
- <p className="text-xs text-content-presentation-global-tertiary">
262
- No fields detected.
263
- </p>
264
- ) : (
265
- <div data-theme="dark" className="flex flex-col gap-2">
266
- {orderedColumns.map((col, index) => {
267
- const field = fieldByPath.get(col.id);
268
- const isDragging = dragPath === col.id;
269
- // Slot for the cursor on this row: top half = insert at
270
- // `index` (before this row); bottom half = `index + 1`
271
- // (after this row, which is the SAME slot as "before next
272
- // row" — the single source of truth avoids the old
273
- // double-line problem in the gap between rows).
274
- return (
275
- <div key={col.id}>
276
- {dropSlot === index && dragPath && <DropLine />}
277
- <div
278
- draggable
279
- onDragStart={(e) => {
280
- setDragPath(col.id);
281
- e.dataTransfer.effectAllowed = "move";
282
- e.dataTransfer.setData("text/plain", col.id);
283
- }}
284
- onDragOver={(e) => {
285
- e.preventDefault();
286
- e.dataTransfer.dropEffect = "move";
287
- const rect = e.currentTarget.getBoundingClientRect();
288
- const before = e.clientY < rect.top + rect.height / 2;
289
- const slot = before ? index : index + 1;
290
- if (dropSlot !== slot) setDropSlot(slot);
291
- }}
292
- onDrop={(e) => {
293
- e.preventDefault();
294
- if (dragPath && dropSlot != null)
295
- reorderColumnToSlot(dragPath, dropSlot);
296
- setDragPath(null);
297
- setDropSlot(null);
298
- }}
299
- onDragEnd={() => {
300
- setDragPath(null);
301
- setDropSlot(null);
302
- }}
303
- className={cn(
304
- // SB-Column-Item: standalone #1C1D1F pill, #252729
305
- // border. Figma container spec: 8px radius, 8.8px
306
- // padding, 8px gap between grip / label / switch.
307
- "flex items-center gap-2 rounded-e-[99px] rounded-s-[60px] border border-[#252729] bg-[#1C1D1F] p-[8.8px] transition-colors cursor-grab active:cursor-grabbing",
308
- isDragging ? "opacity-50" : "hover:bg-[#252729]",
309
- )}
310
- >
311
- <span className="flex shrink-0 items-center justify-center">
312
- <GripDots />
313
- </span>
314
- <span className="flex-1 text-[14px] text-white">
315
- {col.label || field?.label || col.id}
316
- </span>
317
- <span className="flex shrink-0 items-center">
318
- <DataViewsSwitch
319
- checked={col.visible}
320
- onCheckedChange={() => toggleColumnVisibility(col.id)}
321
- />
322
- </span>
323
- </div>
324
- </div>
325
- );
326
- })}
327
- {/* Drop-at-end indicator: only ever rendered when the slot
328
- points past the last row, so still exactly one line. */}
329
- {dropSlot === orderedColumns.length && dragPath && <DropLine />}
330
- </div>
331
- )}
332
- </div>
333
-
334
- <div className="h-px w-full bg-[#2C2D2E]" />
335
-
336
- {/* Default Sort */}
337
- <div className="space-y-3">
338
- <SectionHeader title="Default Sort" />
339
- {sortableColumns.length === 0 ? (
340
- <p className="text-xs text-content-presentation-global-tertiary">
341
- No sortable columns.
342
- </p>
343
- ) : (
344
- // Single-choice radio list (Figma 1612:30016): selecting a
345
- // column sets config.sortBy; direction keeps config.sortOrder.
346
- // Rows + dividers are flat siblings so the `peer` pattern
347
- // can hide the dividers immediately before AND after a
348
- // hovered row.
349
- <RadioGroup
350
- value={config.sortBy || undefined}
351
- onValueChange={(v) => onConfigChange({ sortBy: v })}
352
- className={cn(
353
- "flex flex-col space-y-0 rounded-[12px] bg-[#1C1D1F] p-1",
354
- // Wrapper containing the hovered row: hide its OWN
355
- // divider (sits above the row).
356
- "[&>div:has(>[role=radio]:hover)>.dv-divider]:opacity-0",
357
- // Wrapper that directly follows the one with the hovered
358
- // row: hide its divider (sits below the hovered row).
359
- "[&>div:has(>[role=radio]:hover)+div>.dv-divider]:opacity-0",
360
- )}
361
- >
362
- {sortableColumns.map((col, i) => {
363
- const field = fieldByPath.get(col.id);
364
- return (
365
- <div key={col.id}>
366
- {/* Edge-to-edge divider (Figma: no horizontal
367
- inset). */}
368
- {i > 0 && <div className="dv-divider h-px bg-[#2C2D2E]" />}
369
- <RadioRow value={col.id} label={col.label || field?.label || col.id} />
370
- </div>
371
- );
372
- })}
373
- </RadioGroup>
374
- )}
375
- </div>
376
- </div>
377
- ) : (
378
- <FilterPanel
379
- variant="panel"
380
- data={data}
381
- fields={fields}
382
- filters={filterState}
383
- onFilterChange={(path: string, value: FilterValue) =>
384
- onFilterChange({ ...filterState, [path]: value })
385
- }
386
- onClearAll={() => onFilterChange({})}
387
- filterConfig={filterConfig}
388
- />
389
- )}
390
- </div>
391
- </div>
392
- );
393
- }
@@ -1,207 +0,0 @@
1
- "use client";
2
-
3
- import { Search, Settings } from "lucide-react";
4
- import { useEffect, useRef, useState, type ReactNode } from "react";
5
- import type { ViewType } from "./types";
6
- import { Button } from "../Button";
7
- import { TabSwitch } from "../TabSwitch";
8
- import { cn } from "../../utils/cn";
9
-
10
- export type DataViewsHeaderView = {
11
- id: ViewType;
12
- label: string;
13
- icon: ReactNode;
14
- };
15
-
16
- type DataViewsHeaderProps = {
17
- title: string;
18
- views: DataViewsHeaderView[];
19
- currentView: ViewType;
20
- onViewChange: (view: ViewType) => void;
21
- showSettings: boolean;
22
- settingsOpen: boolean;
23
- onToggleSettings: () => void;
24
- onAddNew?: () => void;
25
- addNewLabel?: string;
26
- searchValue?: string;
27
- onSearchChange?: (value: string) => void;
28
- searchPlaceholder?: string;
29
- className?: string;
30
- };
31
-
32
- export function DataViewsHeader({
33
- title,
34
- views,
35
- currentView,
36
- onViewChange,
37
- showSettings,
38
- settingsOpen,
39
- onToggleSettings,
40
- onAddNew,
41
- addNewLabel = "Add New",
42
- searchValue,
43
- onSearchChange,
44
- searchPlaceholder = "Search...",
45
- className,
46
- }: DataViewsHeaderProps) {
47
- return (
48
- <div
49
- // Header is always dark. data-theme="dark" makes the child Button
50
- // components resolve dark-theme tokens (correct against the black bar)
51
- // even when the host app runs in default/light theme.
52
- data-theme="dark"
53
- className={cn(
54
- "flex h-[52px] w-full items-center gap-2 rounded-[12px] bg-black px-2",
55
- className,
56
- )}
57
- >
58
- {/* Title pill */}
59
- <div className="flex h-9 shrink-0 items-center gap-2 rounded-[12px] border border-[#434446] bg-[#252729] px-[10px]">
60
- <span className="text-[28px] font-[510] uppercase leading-[1.19] text-white">{title}</span>
61
- </div>
62
-
63
- {/* Divider */}
64
- <div className="h-5 w-px shrink-0 bg-[#434446]" />
65
-
66
- {/* Segmented view switcher */}
67
- <div className="flex flex-1 items-center gap-2">
68
- <TabSwitch
69
- // The header bar is always dark, so the switcher resolves dark-theme
70
- // tokens regardless of the host app's theme.
71
- theme="dark"
72
- value={currentView}
73
- onValueChange={onViewChange}
74
- options={views.map((view) => ({
75
- value: view.id,
76
- label: view.label,
77
- icon: view.icon,
78
- }))}
79
- />
80
- </div>
81
-
82
- {/* Action bar */}
83
- <div className="flex shrink-0 items-center gap-2">
84
- {onSearchChange && (
85
- <HeaderSearch
86
- value={searchValue ?? ""}
87
- onChange={onSearchChange}
88
- placeholder={searchPlaceholder}
89
- />
90
- )}
91
- {onAddNew && (
92
- <Button
93
- variant="PrimeStyle"
94
- size="M"
95
- onClick={onAddNew}
96
- className="rounded-[6px] bg-[#005ECC] px-[14px] text-[16px] font-[510] text-white hover:bg-[#005ECC]/90"
97
- >
98
- {addNewLabel}
99
- </Button>
100
- )}
101
- {/* Hidden while the panel is open — the panel has its own close
102
- control, so the header trigger would be redundant. */}
103
- {showSettings && !settingsOpen && (
104
- <Button
105
- variant="BluContStyle"
106
- size="M"
107
- onClick={onToggleSettings}
108
- aria-pressed={settingsOpen}
109
- className="gap-[6px] rounded-[6px] px-[14px] text-[16px] font-[510]"
110
- >
111
- <Settings className="h-[18px] w-[18px]" />
112
- Filter &amp; Config.
113
- </Button>
114
- )}
115
- </div>
116
- </div>
117
- );
118
- }
119
-
120
- function HeaderSearch({
121
- value,
122
- onChange,
123
- placeholder,
124
- }: {
125
- value: string;
126
- onChange: (value: string) => void;
127
- placeholder: string;
128
- }) {
129
- const [open, setOpen] = useState(false);
130
- const inputRef = useRef<HTMLInputElement>(null);
131
- const wrapRef = useRef<HTMLDivElement>(null);
132
-
133
- useEffect(() => {
134
- if (open) inputRef.current?.focus();
135
- }, [open]);
136
-
137
- // Auto-collapse on outside click only when the input is empty — keeps the
138
- // expanded state if the user has typed a query but clicks away.
139
- useEffect(() => {
140
- if (!open) return;
141
- function onPointerDown(e: MouseEvent) {
142
- if (!wrapRef.current) return;
143
- if (wrapRef.current.contains(e.target as Node)) return;
144
- if (!value) setOpen(false);
145
- }
146
- document.addEventListener("mousedown", onPointerDown);
147
- return () => document.removeEventListener("mousedown", onPointerDown);
148
- }, [open, value]);
149
-
150
- function clearAndCollapse() {
151
- onChange("");
152
- setOpen(false);
153
- }
154
-
155
- if (!open) {
156
- return (
157
- <Button
158
- variant="BluContStyle"
159
- size="M"
160
- buttonType="icon"
161
- aria-label="Open search"
162
- onClick={() => setOpen(true)}
163
- className="shrink-0 rounded-[6px] border border-border-presentation-global-primary"
164
- >
165
- <Search className="h-[18px] w-[18px]" />
166
- </Button>
167
- );
168
- }
169
-
170
- return (
171
- <div
172
- ref={wrapRef}
173
- className="relative flex h-[28px] w-[260px] shrink-0 items-center justify-center rounded-[6px] border border-border-presentation-state-focus bg-background-presentation-form-field-primary px-1 shadow-[0_1px_6px_0_rgba(0,0,0,0.30)] transition-all duration-150 ease-in-out"
174
- >
175
- <input
176
- ref={inputRef}
177
- type="text"
178
- value={value}
179
- placeholder={placeholder}
180
- onChange={(e) => onChange(e.target.value)}
181
- onKeyDown={(e) => {
182
- if (e.key === "Escape") clearAndCollapse();
183
- }}
184
- className="flex-1 bg-transparent text-[14px] leading-none text-white caret-[#1E7AFE] placeholder:text-content-presentation-global-tertiary focus:outline-none"
185
- />
186
- <button
187
- type="button"
188
- aria-label="Clear search"
189
- onClick={clearAndCollapse}
190
- className="flex shrink-0 items-center justify-center self-stretch px-1"
191
- >
192
- <svg
193
- xmlns="http://www.w3.org/2000/svg"
194
- width="16"
195
- height="16"
196
- viewBox="0 0 16 16"
197
- fill="none"
198
- >
199
- <path
200
- d="M7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325C11.6818 1.33325 14.6666 4.31802 14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666ZM7.99992 7.05712L6.1143 5.17149L5.17149 6.1143L7.05712 7.99992L5.17149 9.88552L6.1143 10.8283L7.99992 8.94272L9.88552 10.8283L10.8283 9.88552L8.94272 7.99992L10.8283 6.1143L9.88552 5.17149L7.99992 7.05712Z"
201
- fill="white"
202
- />
203
- </svg>
204
- </button>
205
- </div>
206
- );
207
- }