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,493 +0,0 @@
1
- "use client";
2
-
3
- import { Fragment } from "react";
4
- import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
- import { Button } from "../Button";
6
- import { Badge } from "../Badge";
7
- import { X } from "lucide-react";
8
- import { Checkbox } from "../Checkbox";
9
- import { Divider } from "../Divider";
10
- import { Label } from "../Label";
11
- import { DataViewRadio } from "./DataViewRadio";
12
- import { cn } from "../../utils/cn";
13
- import type {
14
- DynamicRecord,
15
- DynamicFilterConfig,
16
- FieldConfig,
17
- FieldType,
18
- FilterState,
19
- FilterValue,
20
- NumericRangeFilter,
21
- DateRangeFilter,
22
- } from "./types";
23
- import { getByPath, formatPathLabel } from "../../utils/dataViews/pathUtils";
24
- import {
25
- computeNumericExtremes,
26
- countActiveFilters,
27
- inferStep,
28
- isDateRange,
29
- isNumericRange,
30
- resolvePresets,
31
- } from "../../utils/dataViews/rangeUtils";
32
- import { RangeSliderWithInputs } from "./filters/RangeSliderWithInputs";
33
- import { DatePickerRangeFilter } from "./filters/DatePickerRangeFilter";
34
- import { PresetChips } from "./filters/PresetChips";
35
- import { resolveBadgeVariant } from "./badgeAdapter";
36
- import { SearchableSelect } from "../SearchableSelect";
37
- import type { SearchableSelectOption } from "../SearchableSelect";
38
-
39
- type FilterPanelProps = {
40
- data: DynamicRecord[];
41
- fields: FieldConfig[];
42
- filters: FilterState;
43
- onFilterChange: (path: string, value: FilterValue) => void;
44
- onClearAll: () => void;
45
- filterConfig?: DynamicFilterConfig[];
46
- /**
47
- * "default": standalone left-rail style (border, padding, light surface).
48
- * "panel": matches the Config tab inside DataViewsConfigPanel — no outer
49
- * chrome, white section headers, categorical options inside a #1C1D1F
50
- * rounded container, sections separated by #2C2D2E dividers.
51
- */
52
- variant?: "default" | "panel";
53
- };
54
-
55
- const NUMERIC_TYPES: FieldType[] = [
56
- "number",
57
- "number-format",
58
- "currency",
59
- "progress-bar",
60
- "star-rating",
61
- ];
62
-
63
- const DATE_TYPES: FieldType[] = ["date", "date-format"];
64
-
65
- type FilterKind = "categorical" | "numeric-range" | "date-range";
66
-
67
- type Entry = {
68
- path: string;
69
- label: string;
70
- kind: FilterKind;
71
- field?: FieldConfig;
72
- legacy?: DynamicFilterConfig;
73
- };
74
-
75
- function buildFilterableEntries(
76
- data: DynamicRecord[],
77
- fields: FieldConfig[],
78
- legacy?: DynamicFilterConfig[],
79
- ): Entry[] {
80
- const legacyByPath = new Map(
81
- (legacy ?? []).filter((f) => f.enabled !== false).map((f) => [f.id, f]),
82
- );
83
-
84
- const entries: Entry[] = [];
85
- const seen = new Set<string>();
86
-
87
- for (const f of fields) {
88
- if (f.type === "hidden") continue;
89
- if (f.filterable === false) continue;
90
-
91
- const isExplicit = f.filterable === true;
92
- const isCategoricalAuto =
93
- f.type === "enum-badge" ||
94
- f.type === "boolean" ||
95
- f.type === "badge-array" ||
96
- f.type === "icon-text";
97
- const isNumeric = f.type != null && NUMERIC_TYPES.includes(f.type);
98
- const isDate = f.type != null && DATE_TYPES.includes(f.type);
99
-
100
- let include = isExplicit || isCategoricalAuto;
101
-
102
- if (!include) {
103
- if (f.type !== "text" && f.type !== undefined) continue;
104
- const unique = new Set<string>();
105
- for (const item of data) {
106
- const v = getByPath(item, f.path);
107
- if (v == null) continue;
108
- unique.add(String(v));
109
- if (unique.size > 10) break;
110
- }
111
- include = unique.size > 0 && unique.size <= 10;
112
- }
113
-
114
- if (!include) continue;
115
-
116
- const kind: FilterKind = isNumeric ? "numeric-range" : isDate ? "date-range" : "categorical";
117
-
118
- entries.push({
119
- path: f.path,
120
- label: f.filterLabel ?? f.label ?? formatPathLabel(f.path),
121
- kind,
122
- field: f,
123
- legacy: legacyByPath.get(f.path),
124
- });
125
- seen.add(f.path);
126
- }
127
-
128
- for (const lf of legacyByPath.values()) {
129
- if (seen.has(lf.id)) continue;
130
- entries.push({
131
- path: lf.id,
132
- label: lf.label ?? formatPathLabel(lf.id),
133
- kind: "categorical",
134
- legacy: lf,
135
- });
136
- }
137
-
138
- return entries;
139
- }
140
-
141
- function getCategoricalOptions(
142
- data: DynamicRecord[],
143
- path: string,
144
- field?: FieldConfig,
145
- legacy?: DynamicFilterConfig,
146
- ): string[] {
147
- if (field?.filterOptions && field.filterOptions.length > 0) {
148
- return normalizeOptions(field.filterOptions);
149
- }
150
- if (legacy?.options && legacy.options.length > 0) {
151
- return normalizeOptions(legacy.options);
152
- }
153
- if (field?.variants) {
154
- const fromMap = Object.keys(field.variants);
155
- const fromData = collectUnique(data, path);
156
- return Array.from(new Set([...fromMap, ...fromData]));
157
- }
158
- return collectUnique(data, path);
159
- }
160
-
161
- function normalizeOptions(opts: NonNullable<FieldConfig["filterOptions"]>): string[] {
162
- if (opts.length === 0) return [];
163
- if (typeof opts[0] === "string") return opts as string[];
164
- return (opts as { label: string; value: string }[]).map((o) => o.value);
165
- }
166
-
167
- function collectUnique(data: DynamicRecord[], path: string): string[] {
168
- const set = new Set<string>();
169
- for (const item of data) {
170
- const v = getByPath(item, path);
171
- if (v == null) continue;
172
- if (Array.isArray(v)) {
173
- for (const x of v) set.add(String(x));
174
- } else {
175
- set.add(String(v));
176
- }
177
- }
178
- return Array.from(set).sort();
179
- }
180
-
181
- export function FilterPanel({
182
- data,
183
- fields,
184
- filters,
185
- onFilterChange,
186
- onClearAll,
187
- filterConfig,
188
- variant = "default",
189
- }: FilterPanelProps) {
190
- const entries = buildFilterableEntries(data, fields, filterConfig);
191
-
192
- const setFilter = (path: string, value: FilterValue) => {
193
- onFilterChange(path, value);
194
- const field = fields.find((f) => f.path === path);
195
- field?.onFilterChange?.(value);
196
- if (Array.isArray(value)) {
197
- const legacy = filterConfig?.find((f) => f.id === path);
198
- legacy?.onChange?.(value);
199
- }
200
- };
201
-
202
- const toggleCategorical = (path: string, option: string) => {
203
- const current = filters[path];
204
- const arr = Array.isArray(current) ? current : [];
205
- const next = arr.includes(option) ? arr.filter((v) => v !== option) : [...arr, option];
206
- setFilter(path, next);
207
- };
208
-
209
- const totalFilters = countActiveFilters(filters);
210
-
211
- if (entries.length === 0) return null;
212
-
213
- const countBadge = resolveBadgeVariant("gray");
214
-
215
- if (variant === "panel") {
216
- return (
217
- <div className="flex flex-col gap-6 px-3 py-4">
218
- <div className="flex items-center justify-between">
219
- <div className="flex items-center gap-2">
220
- <h3 className="text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
221
- Filters
222
- </h3>
223
- {totalFilters > 0 && (
224
- <Badge
225
- {...countBadge}
226
- label={String(totalFilters)}
227
- showIcon={false}
228
- className="h-5 w-5 min-w-0 justify-center rounded-full p-0 text-xs"
229
- size="XS"
230
- />
231
- )}
232
- </div>
233
- {totalFilters > 0 && (
234
- <button
235
- type="button"
236
- onClick={onClearAll}
237
- className="flex items-center gap-1 rounded-[4px] bg-white/[0.15] px-1.5 py-0.5 text-[12px] font-[510] text-white transition-colors hover:bg-white/25"
238
- >
239
- <X className="h-3 w-3" />
240
- Clear
241
- </button>
242
- )}
243
- </div>
244
-
245
- {entries.map((entry, index) => (
246
- <Fragment key={entry.path}>
247
- {index > 0 && <div className="h-px w-full bg-[#2C2D2E]" />}
248
- <div className="space-y-3">
249
- <h3 className="text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
250
- {entry.label}
251
- </h3>
252
- <FilterBody
253
- entry={entry}
254
- data={data}
255
- value={filters[entry.path]}
256
- onCategoricalToggle={(opt) => toggleCategorical(entry.path, opt)}
257
- onSetFilter={(v) => setFilter(entry.path, v)}
258
- variant="panel"
259
- />
260
- </div>
261
- </Fragment>
262
- ))}
263
- </div>
264
- );
265
- }
266
-
267
- return (
268
- <div className="w-64 border-r border-border-presentation-global-primary bg-background-presentation-body-overlay-primary p-4 overflow-y-auto">
269
- <div className="flex items-center justify-between mb-4">
270
- <div className="flex items-center gap-2">
271
- <h3 className="font-semibold text-content-presentation-global-primary">Filters</h3>
272
- {totalFilters > 0 && (
273
- <Badge
274
- {...countBadge}
275
- label={String(totalFilters)}
276
- showIcon={false}
277
- className="h-5 w-5 min-w-0 justify-center rounded-full p-0 text-xs"
278
- size="XS"
279
- />
280
- )}
281
- </div>
282
- {totalFilters > 0 && (
283
- <Button variant="PrimeStyle" size="M" onClick={onClearAll} className="h-7 gap-1 text-xs">
284
- <X className="h-3 w-3" />
285
- Clear
286
- </Button>
287
- )}
288
- </div>
289
-
290
- <div className="space-y-4">
291
- {entries.map((entry, index) => (
292
- <div key={entry.path}>
293
- {index > 0 && <Divider className="mb-4" />}
294
- <div className="space-y-2">
295
- <Label className="text-content-presentation-global-primary">{entry.label}</Label>
296
- <FilterBody
297
- entry={entry}
298
- data={data}
299
- value={filters[entry.path]}
300
- onCategoricalToggle={(opt) => toggleCategorical(entry.path, opt)}
301
- onSetFilter={(v) => setFilter(entry.path, v)}
302
- />
303
- </div>
304
- </div>
305
- ))}
306
- </div>
307
- </div>
308
- );
309
- }
310
-
311
- function FilterBody({
312
- entry,
313
- data,
314
- value,
315
- onCategoricalToggle,
316
- onSetFilter,
317
- variant = "default",
318
- }: {
319
- entry: Entry;
320
- data: DynamicRecord[];
321
- value: FilterValue | undefined;
322
- onCategoricalToggle: (option: string) => void;
323
- onSetFilter: (next: FilterValue) => void;
324
- variant?: "default" | "panel";
325
- }) {
326
- if (entry.kind === "numeric-range" && entry.field) {
327
- const extremes = computeNumericExtremes(data, entry.path);
328
- if (!extremes || extremes.min === extremes.max) {
329
- return (
330
- <div className="text-xs text-content-presentation-global-tertiary">No range to filter.</div>
331
- );
332
- }
333
- const step = inferStep(entry.field, extremes);
334
- const presets = resolvePresets(entry.field);
335
- const numericValue: NumericRangeFilter | undefined = isNumericRange(value) ? value : undefined;
336
- return (
337
- <div className="space-y-3">
338
- {presets.length > 0 && (
339
- <PresetChips presets={presets} current={value} onSelect={onSetFilter} />
340
- )}
341
- <RangeSliderWithInputs
342
- field={entry.field}
343
- extremes={extremes}
344
- step={step}
345
- value={numericValue}
346
- onChange={onSetFilter}
347
- />
348
- </div>
349
- );
350
- }
351
-
352
- if (entry.kind === "date-range" && entry.field) {
353
- const dateValue: DateRangeFilter | undefined = isDateRange(value) ? value : undefined;
354
- // Glare DatePicker in range mode — pick a from→to span. (No quick-preset
355
- // chips: the calendar range selection is the single intended interaction.)
356
- return <DatePickerRangeFilter value={dateValue} onChange={onSetFilter} />;
357
- }
358
-
359
- const opts = getCategoricalOptions(data, entry.path, entry.field, entry.legacy);
360
- const selected = Array.isArray(value) ? value : [];
361
- const isSingle = entry.field?.filterMode === "single";
362
-
363
- // Searchable single-select dropdown — for fields with many options. Stores
364
- // the chosen value as a 1-element array (empty when cleared) to stay
365
- // consistent with the categorical FilterValue shape.
366
- if (entry.field?.filterVariant === "searchable-select") {
367
- const selectOptions: SearchableSelectOption[] = opts.map((opt) => ({
368
- value: opt,
369
- label: opt,
370
- }));
371
- const current = selected[0] ?? null;
372
- return (
373
- <SearchableSelect
374
- options={selectOptions}
375
- value={current}
376
- onValueChange={(v) => onSetFilter(v ? [v] : [])}
377
- placeholder={`Select ${entry.label}…`}
378
- icon={<i className="ri-search-line" />}
379
- />
380
- );
381
- }
382
-
383
- if (isSingle) {
384
- const current = selected[0] ?? "";
385
- const onSingleChange = (next: string) => onSetFilter(next ? [next] : []);
386
- return (
387
- <RadioGroupPrimitive.Root
388
- value={current}
389
- onValueChange={onSingleChange}
390
- className={cn(
391
- "flex flex-col space-y-0 rounded-[12px] bg-[#1C1D1F] p-1",
392
- // Hide the divider directly above and below the hovered row.
393
- "[&>div:has(>[role=radio]:hover)>.dv-divider]:opacity-0",
394
- "[&>div:has(>[role=radio]:hover)+div>.dv-divider]:opacity-0",
395
- )}
396
- >
397
- {opts.map((opt, i) => {
398
- const isSelected = current === opt;
399
- const badgeVariant = entry.field?.variants?.[opt];
400
- const badgeProps = badgeVariant ? resolveBadgeVariant(badgeVariant) : null;
401
- return (
402
- <div key={opt}>
403
- {i > 0 && <div className="dv-divider h-px bg-[#2C2D2E]" />}
404
- <DataViewRadio value={opt}>
405
- {entry.legacy?.render ? (
406
- entry.legacy.render(opt, isSelected)
407
- ) : badgeProps ? (
408
- <Badge {...badgeProps} label={opt} size="XS" />
409
- ) : (
410
- opt
411
- )}
412
- </DataViewRadio>
413
- </div>
414
- );
415
- })}
416
- </RadioGroupPrimitive.Root>
417
- );
418
- }
419
-
420
- if (variant === "panel") {
421
- return (
422
- <div
423
- className={cn(
424
- "flex flex-col rounded-[12px] bg-[#1C1D1F] p-1",
425
- // Hide the divider directly above and below the hovered row.
426
- "[&>div:has(>label:hover)>.dv-divider]:opacity-0",
427
- "[&>div:has(>label:hover)+div>.dv-divider]:opacity-0",
428
- )}
429
- >
430
- {opts.map((opt, i) => {
431
- const isSelected = selected.includes(opt);
432
- const badgeVariant = entry.field?.variants?.[opt];
433
- const badgeProps = badgeVariant ? resolveBadgeVariant(badgeVariant) : null;
434
- return (
435
- <div key={opt}>
436
- {i > 0 && <div className="dv-divider h-px bg-[#2C2D2E]" />}
437
- <label
438
- htmlFor={`${entry.path}-${opt}`}
439
- className="flex cursor-pointer items-center gap-2 rounded-[8px] px-2 py-2 text-[14px] text-white hover:bg-white/5"
440
- >
441
- <Checkbox
442
- id={`${entry.path}-${opt}`}
443
- checked={isSelected}
444
- onCheckedChange={() => onCategoricalToggle(opt)}
445
- />
446
- <span className="flex-1 leading-none">
447
- {entry.legacy?.render ? (
448
- entry.legacy.render(opt, isSelected)
449
- ) : badgeProps ? (
450
- <Badge {...badgeProps} label={opt} size="XS" />
451
- ) : (
452
- opt
453
- )}
454
- </span>
455
- </label>
456
- </div>
457
- );
458
- })}
459
- </div>
460
- );
461
- }
462
-
463
- return (
464
- <div className="space-y-2">
465
- {opts.map((opt) => {
466
- const isSelected = selected.includes(opt);
467
- const badgeVariant = entry.field?.variants?.[opt];
468
- const badgeProps = badgeVariant ? resolveBadgeVariant(badgeVariant) : null;
469
- return (
470
- <div key={opt} className="flex items-center space-x-2">
471
- <Checkbox
472
- id={`${entry.path}-${opt}`}
473
- checked={isSelected}
474
- onCheckedChange={() => onCategoricalToggle(opt)}
475
- />
476
- <label
477
- htmlFor={`${entry.path}-${opt}`}
478
- className="text-sm text-content-presentation-global-primary cursor-pointer leading-none flex-1"
479
- >
480
- {entry.legacy?.render ? (
481
- entry.legacy.render(opt, isSelected)
482
- ) : badgeProps ? (
483
- <Badge {...badgeProps} label={opt} size="XS" />
484
- ) : (
485
- opt
486
- )}
487
- </label>
488
- </div>
489
- );
490
- })}
491
- </div>
492
- );
493
- }
@@ -1,93 +0,0 @@
1
- "use client";
2
-
3
- import { Search } from "lucide-react";
4
- import { useEffect, useRef, useState } from "react";
5
- import { Button } from "../Button";
6
-
7
- export type HeaderSearchProps = {
8
- value: string;
9
- onChange: (value: string) => void;
10
- placeholder?: string;
11
- };
12
-
13
- export function HeaderSearch({ value, onChange, placeholder = "Search..." }: HeaderSearchProps) {
14
- const [open, setOpen] = useState(false);
15
- const inputRef = useRef<HTMLInputElement>(null);
16
- const wrapRef = useRef<HTMLDivElement>(null);
17
-
18
- useEffect(() => {
19
- if (open) inputRef.current?.focus();
20
- }, [open]);
21
-
22
- // Auto-collapse on outside click only when the input is empty — keeps the
23
- // expanded state if the user has typed a query but clicks away.
24
- useEffect(() => {
25
- if (!open) return;
26
- function onPointerDown(e: MouseEvent) {
27
- if (!wrapRef.current) return;
28
- if (wrapRef.current.contains(e.target as Node)) return;
29
- if (!value) setOpen(false);
30
- }
31
- document.addEventListener("mousedown", onPointerDown);
32
- return () => document.removeEventListener("mousedown", onPointerDown);
33
- }, [open, value]);
34
-
35
- function clearAndCollapse() {
36
- onChange("");
37
- setOpen(false);
38
- }
39
-
40
- if (!open) {
41
- return (
42
- <Button
43
- variant="BluContStyle"
44
- size="M"
45
- buttonType="icon"
46
- aria-label="Open search"
47
- onClick={() => setOpen(true)}
48
- className="shrink-0 rounded-[6px] border border-border-presentation-global-primary"
49
- >
50
- <Search className="h-[18px] w-[18px]" />
51
- </Button>
52
- );
53
- }
54
-
55
- return (
56
- <div
57
- ref={wrapRef}
58
- 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"
59
- >
60
- <input
61
- ref={inputRef}
62
- type="text"
63
- value={value}
64
- placeholder={placeholder}
65
- onChange={(e) => onChange(e.target.value)}
66
- onKeyDown={(e) => {
67
- if (e.key === "Escape") clearAndCollapse();
68
- }}
69
- size={1}
70
- className="min-w-0 flex-1 bg-transparent text-[14px] leading-none text-white caret-[#1E7AFE] placeholder:text-content-presentation-global-tertiary focus:outline-none"
71
- />
72
- <button
73
- type="button"
74
- aria-label="Clear search"
75
- onClick={clearAndCollapse}
76
- className="flex shrink-0 items-center justify-center self-stretch px-1"
77
- >
78
- <svg
79
- xmlns="http://www.w3.org/2000/svg"
80
- width="16"
81
- height="16"
82
- viewBox="0 0 16 16"
83
- fill="none"
84
- >
85
- <path
86
- 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"
87
- fill="white"
88
- />
89
- </svg>
90
- </button>
91
- </div>
92
- );
93
- }