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