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.
Files changed (73) hide show
  1. package/apps/lib/components/Avatar.tsx +1 -1
  2. package/apps/lib/components/BadgeField.tsx +2 -2
  3. package/apps/lib/components/Card.tsx +68 -54
  4. package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
  5. package/apps/lib/components/DataViews/DataViewRadio.tsx +47 -0
  6. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +427 -0
  7. package/apps/lib/components/DataViews/DataViewsHeader.tsx +228 -0
  8. package/apps/lib/components/DataViews/DataViewsLayout.tsx +330 -0
  9. package/apps/lib/components/DataViews/FilterPanel.tsx +469 -0
  10. package/apps/lib/components/DataViews/HeaderSearch.tsx +97 -0
  11. package/apps/lib/components/DataViews/InboxView.tsx +495 -0
  12. package/apps/lib/components/DataViews/InboxViewCard.tsx +136 -0
  13. package/apps/lib/components/DataViews/KanbanView.tsx +353 -0
  14. package/apps/lib/components/DataViews/PanelControls.tsx +49 -0
  15. package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
  16. package/apps/lib/components/DataViews/TableView.tsx +232 -0
  17. package/apps/lib/components/DataViews/TreeView.tsx +392 -0
  18. package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
  19. package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
  20. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
  21. package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
  22. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
  23. package/apps/lib/components/DataViews/index.ts +36 -0
  24. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
  25. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
  26. package/apps/lib/components/DataViews/types.ts +206 -0
  27. package/apps/lib/components/Radio.tsx +18 -21
  28. package/apps/lib/components/Switch.tsx +3 -1
  29. package/apps/lib/components/Table.tsx +1 -1
  30. package/apps/lib/components/TreeFolder/TreeFolder.tsx +410 -0
  31. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
  32. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +363 -0
  33. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
  34. package/apps/lib/components/TreeFolder/icons.tsx +63 -0
  35. package/apps/lib/components/TreeFolder/index.ts +17 -0
  36. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
  37. package/apps/lib/components/TreeFolder/types.ts +77 -0
  38. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
  39. package/apps/lib/hooks/useDataViewsState.ts +169 -0
  40. package/apps/lib/hooks/useIsMobile.ts +21 -0
  41. package/apps/lib/layouts/DataViewCard.tsx +76 -0
  42. package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
  43. package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
  44. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
  45. package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
  46. package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
  47. package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
  48. package/dist/bin/index.js +3 -3
  49. package/dist/bin/index.js.map +1 -1
  50. package/dist/src/commands/add.d.ts.map +1 -1
  51. package/dist/src/commands/add.js +29 -6
  52. package/dist/src/commands/add.js.map +1 -1
  53. package/dist/src/commands/utils.d.ts.map +1 -1
  54. package/dist/src/commands/utils.js +22 -2
  55. package/dist/src/commands/utils.js.map +1 -1
  56. package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
  57. package/dist/src/shared/copyComponentsRecursively.js +17 -2
  58. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  59. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
  60. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
  61. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
  62. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
  63. package/docs/components/data-views-config-panel.md +204 -0
  64. package/docs/components/data-views-layout.md +270 -0
  65. package/docs/components/form-stepper.md +244 -0
  66. package/docs/components/stepper.md +215 -0
  67. package/docs/components/timeline.md +248 -0
  68. package/package.json +6 -6
  69. package/apps/lib/components/Charts-dev.tsx +0 -365
  70. package/apps/lib/components/Command-dev.tsx +0 -151
  71. package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
  72. /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
  73. /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
+ }