torch-glare 2.1.0 → 2.1.2

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 (65) hide show
  1. package/apps/lib/components/Badge.tsx +34 -137
  2. package/apps/lib/components/BadgeField.tsx +4 -4
  3. package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
  4. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
  5. package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
  6. package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
  7. package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
  8. package/apps/lib/components/DataViews/InboxView.tsx +514 -0
  9. package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
  10. package/apps/lib/components/DataViews/PanelControls.tsx +80 -0
  11. package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
  12. package/apps/lib/components/DataViews/TableView.tsx +232 -0
  13. package/apps/lib/components/DataViews/TreeView.tsx +363 -0
  14. package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
  15. package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
  16. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
  17. package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
  18. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
  19. package/apps/lib/components/DataViews/index.ts +30 -0
  20. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
  21. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
  22. package/apps/lib/components/DataViews/types.ts +177 -0
  23. package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
  24. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
  25. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -0
  26. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
  27. package/apps/lib/components/TreeFolder/icons.tsx +63 -0
  28. package/apps/lib/components/TreeFolder/index.ts +17 -0
  29. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
  30. package/apps/lib/components/TreeFolder/types.ts +68 -0
  31. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
  32. package/apps/lib/hooks/useDataViewsState.ts +169 -0
  33. package/apps/lib/hooks/useIsMobile.ts +21 -0
  34. package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
  35. package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
  36. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
  37. package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
  38. package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
  39. package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
  40. package/dist/bin/index.js +3 -3
  41. package/dist/bin/index.js.map +1 -1
  42. package/dist/src/commands/add.d.ts.map +1 -1
  43. package/dist/src/commands/add.js +29 -6
  44. package/dist/src/commands/add.js.map +1 -1
  45. package/dist/src/commands/utils.d.ts.map +1 -1
  46. package/dist/src/commands/utils.js +22 -2
  47. package/dist/src/commands/utils.js.map +1 -1
  48. package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
  49. package/dist/src/shared/copyComponentsRecursively.js +8 -1
  50. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  51. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
  52. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
  53. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
  54. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
  55. package/docs/components/badge-field.md +21 -21
  56. package/docs/components/badge.md +156 -483
  57. package/docs/components/form-stepper.md +244 -0
  58. package/docs/components/stepper.md +215 -0
  59. package/docs/components/timeline.md +248 -0
  60. package/docs/reference/components.md +8 -7
  61. package/docs/reference/types.md +34 -26
  62. package/docs/tutorials/theming-basics.md +30 -27
  63. package/package.json +1 -1
  64. /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
  65. /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
@@ -0,0 +1,416 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import {
5
+ X,
6
+ Settings as SettingsIcon,
7
+ Filter as FilterIcon,
8
+ Plus,
9
+ } from "lucide-react";
10
+ import type {
11
+ ViewConfig,
12
+ ViewType,
13
+ FieldConfig,
14
+ DynamicRecord,
15
+ DynamicFilterConfig,
16
+ FilterState,
17
+ FilterValue,
18
+ } from "./types";
19
+
20
+ import { RadioGroup } from "../Radio";
21
+ import { FilterPanel } from "./FilterPanel";
22
+ import { RadioRow, DataViewsSwitch } from "./PanelControls";
23
+ import { cn } from "../../utils/cn";
24
+
25
+ type ConfigTab = "config" | "filters";
26
+
27
+ type SavedView = { id: string; label: string };
28
+
29
+ export type DataViewsConfigPanelProps = {
30
+ config: ViewConfig;
31
+ onConfigChange: (config: Partial<ViewConfig>) => void;
32
+ onClose: () => void;
33
+ currentView: ViewType;
34
+ fields: FieldConfig[];
35
+
36
+ // Filters tab
37
+ data: DynamicRecord[];
38
+ filterState: FilterState;
39
+ onFilterChange: (filters: FilterState) => void;
40
+ filterConfig?: DynamicFilterConfig[];
41
+
42
+ // Saved views (presentational shell — wire to persistence when available)
43
+ savedViews?: SavedView[];
44
+ activeSavedView?: string;
45
+ onSavedViewChange?: (id: string) => void;
46
+ onSaveNewView?: () => void;
47
+
48
+ // Animation: drives slide-in/out. Parent keeps the panel mounted through
49
+ // the close animation, then unmounts.
50
+ state?: "open" | "closed";
51
+ };
52
+
53
+ const DEFAULT_SAVED_VIEWS: SavedView[] = [
54
+ { id: "default", label: "Default View" },
55
+ ];
56
+
57
+ function SectionHeader({
58
+ title,
59
+ action,
60
+ }: {
61
+ title: string;
62
+ action?: React.ReactNode;
63
+ }) {
64
+ return (
65
+ <div className="flex items-center justify-between">
66
+ <h3 className="text-[18px] font-[510] leading-[1.32] tracking-[-0.01em] text-white">
67
+ {title}
68
+ </h3>
69
+ {action}
70
+ </div>
71
+ );
72
+ }
73
+
74
+ /** 2×3 dot drag handle, matching the Figma SB-Column-Item grip (16×16 box,
75
+ * compact ~1.5px dots, tight spacing — drawn as an SVG for pixel accuracy). */
76
+ function GripDots() {
77
+ return (
78
+ <svg
79
+ aria-hidden
80
+ width="16"
81
+ height="16"
82
+ viewBox="0 0 16 16"
83
+ // Panel is always-dark chrome (like the hardcoded white label text):
84
+ // the grip stays white-on-dark regardless of host theme.
85
+ className="text-white/60"
86
+ fill="currentColor"
87
+ >
88
+ {[5.33, 9.33].flatMap((cx) =>
89
+ [3.33, 8, 12.67].map((cy) => (
90
+ <circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="1" />
91
+ )),
92
+ )}
93
+ </svg>
94
+ );
95
+ }
96
+
97
+ /** 2px blue insertion line shown between rows during a drag-reorder. */
98
+ function DropLine() {
99
+ return (
100
+ <div className="pointer-events-none relative h-0">
101
+ <div className="absolute -top-[1px] left-0 right-0 h-[2px] rounded-full bg-[#005ECC]" />
102
+ </div>
103
+ );
104
+ }
105
+
106
+ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
107
+ const {
108
+ config,
109
+ onConfigChange,
110
+ onClose,
111
+ fields,
112
+ data,
113
+ filterState,
114
+ onFilterChange,
115
+ filterConfig,
116
+ savedViews = DEFAULT_SAVED_VIEWS,
117
+ activeSavedView,
118
+ onSavedViewChange,
119
+ onSaveNewView,
120
+ state = "open",
121
+ } = props;
122
+
123
+ const [tab, setTab] = useState<ConfigTab>("config");
124
+
125
+ // Saved View is controlled by the parent when `activeSavedView` /
126
+ // `onSavedViewChange` are supplied; otherwise fall back to local state so the
127
+ // radios are still interactive (DataViewsLayout doesn't thread these props
128
+ // yet — without this the selection would snap back every click).
129
+ const [internalSavedView, setInternalSavedView] = useState(
130
+ () => savedViews[0]?.id,
131
+ );
132
+ const selectedSavedView = activeSavedView ?? internalSavedView;
133
+ const handleSavedViewChange = (id: string) => {
134
+ if (onSavedViewChange) onSavedViewChange(id);
135
+ else setInternalSavedView(id);
136
+ };
137
+
138
+ const visibleFields = useMemo(
139
+ () => fields.filter((f) => f.type !== "hidden"),
140
+ [fields],
141
+ );
142
+ const visiblePaths = useMemo(
143
+ () => new Set(visibleFields.map((f) => f.path)),
144
+ [visibleFields],
145
+ );
146
+ const fieldByPath = useMemo(
147
+ () => new Map(visibleFields.map((f) => [f.path, f])),
148
+ [visibleFields],
149
+ );
150
+ const orderedColumns = useMemo(
151
+ () =>
152
+ [...config.tableColumns]
153
+ .filter((c) => visiblePaths.has(c.id))
154
+ .sort((a, b) => a.order - b.order),
155
+ [config.tableColumns, visiblePaths],
156
+ );
157
+
158
+ const toggleColumnVisibility = (path: string) => {
159
+ const next = config.tableColumns.map((c) =>
160
+ c.id === path ? { ...c, visible: !c.visible } : c,
161
+ );
162
+ onConfigChange({ tableColumns: next });
163
+ };
164
+
165
+ const [dragPath, setDragPath] = useState<string | null>(null);
166
+ const [dragOverPath, setDragOverPath] = useState<string | null>(null);
167
+ // Whether the drop will land before (true) or after (false) dragOverPath.
168
+ const [dropBefore, setDropBefore] = useState(true);
169
+
170
+ const reorderColumn = (
171
+ sourcePath: string,
172
+ targetPath: string,
173
+ before: boolean,
174
+ ) => {
175
+ if (sourcePath === targetPath) return;
176
+ const ids = orderedColumns.map((c) => c.id);
177
+ const from = ids.indexOf(sourcePath);
178
+ let to = ids.indexOf(targetPath);
179
+ if (from === -1 || to === -1) return;
180
+ const reordered = [...ids];
181
+ reordered.splice(from, 1);
182
+ // Recompute the target index after removal, then offset for before/after.
183
+ to = reordered.indexOf(targetPath);
184
+ const insertAt = before ? to : to + 1;
185
+ reordered.splice(insertAt, 0, sourcePath);
186
+ const orderByPath = new Map(reordered.map((id, i) => [id, i]));
187
+ const next = config.tableColumns.map((c) => {
188
+ const newOrder = orderByPath.get(c.id);
189
+ return newOrder == null ? c : { ...c, order: newOrder };
190
+ });
191
+ onConfigChange({ tableColumns: next });
192
+ };
193
+
194
+ const sortableColumns = orderedColumns;
195
+
196
+ const tabBtn = (id: ConfigTab, icon: React.ReactNode, label: string) => {
197
+ const active = tab === id;
198
+ return (
199
+ <button
200
+ type="button"
201
+ aria-pressed={active}
202
+ onClick={() => setTab(id)}
203
+ className={cn(
204
+ "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",
205
+ active
206
+ ? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
207
+ : "bg-transparent text-white hover:bg-white/5",
208
+ )}
209
+ >
210
+ <span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
211
+ {icon}
212
+ </span>
213
+ {label}
214
+ </button>
215
+ );
216
+ };
217
+
218
+ return (
219
+ <div
220
+ data-state={state}
221
+ // Panel is always dark (Figma `Cun` = #000000). data-theme="dark" makes
222
+ // child themed components (Button, Switch, Radio, FilterPanel) resolve
223
+ // dark tokens even when the host app runs in default/light theme.
224
+ data-theme="dark"
225
+ className={cn(
226
+ "flex h-full w-[260px] flex-col overflow-hidden rounded-[16px] bg-black",
227
+ "transition-opacity duration-200 ease-in-out",
228
+ state === "open" ? "opacity-100" : "opacity-0",
229
+ )}
230
+ >
231
+ {/* Header: tab switcher + close */}
232
+ <div className="flex items-center gap-2 px-3 py-3">
233
+ <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)]">
234
+ {tabBtn("config", <SettingsIcon />, "Config.")}
235
+ {tabBtn("filters", <FilterIcon />, "Filters")}
236
+ </div>
237
+ <button
238
+ type="button"
239
+ onClick={onClose}
240
+ aria-label="Close panel"
241
+ className="flex h-7 w-7 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:bg-white/25"
242
+ >
243
+ <X className="h-[18px] w-[18px]" />
244
+ </button>
245
+ </div>
246
+
247
+ <div className="h-px w-full bg-[#2C2D2E]" />
248
+
249
+ <div className="flex-1 overflow-y-auto">
250
+ {tab === "config" ? (
251
+ <div className="flex flex-col gap-6 px-3 py-4">
252
+ {/* Saved View */}
253
+ <div className="space-y-3">
254
+ <SectionHeader title="Saved View" />
255
+ <RadioGroup
256
+ value={selectedSavedView}
257
+ onValueChange={handleSavedViewChange}
258
+ className="flex flex-col gap-1 space-y-0 rounded-[12px] bg-[#1C1D1F] p-1"
259
+ >
260
+ {savedViews.map((sv, i) => (
261
+ <div key={sv.id}>
262
+ {/* Divider spans edge-to-edge (Figma: no horizontal
263
+ inset). */}
264
+ {i > 0 && <div className="h-px bg-[#2C2D2E]" />}
265
+ <RadioRow value={sv.id} label={sv.label} />
266
+ </div>
267
+ ))}
268
+ </RadioGroup>
269
+ <button
270
+ type="button"
271
+ onClick={onSaveNewView}
272
+ className="flex w-full items-center justify-center gap-1.5 rounded-[4px] bg-white/[0.15] px-1.5 py-0.5 text-[12px] font-[510] text-white transition-colors hover:bg-white/25"
273
+ >
274
+ <Plus className="h-3 w-3" />
275
+ Save a New View
276
+ </button>
277
+ </div>
278
+
279
+ <div className="h-px w-full bg-[#2C2D2E]" />
280
+
281
+ {/* Table Columns */}
282
+ <div className="space-y-3">
283
+ <SectionHeader title="Table Columns" />
284
+ <p className="text-[12px] leading-[1.475] text-content-presentation-global-tertiary">
285
+ Show or hide columns in table view
286
+ </p>
287
+ {orderedColumns.length === 0 ? (
288
+ <p className="text-xs text-content-presentation-global-tertiary">
289
+ No fields detected.
290
+ </p>
291
+ ) : (
292
+ <div data-theme="dark" className="flex flex-col gap-2">
293
+ {orderedColumns.map((col) => {
294
+ const field = fieldByPath.get(col.id);
295
+ const isDragging = dragPath === col.id;
296
+ const isTarget =
297
+ dragOverPath === col.id && dragPath !== col.id;
298
+ return (
299
+ <div key={col.id}>
300
+ {isTarget && dropBefore && <DropLine />}
301
+ <div
302
+ draggable
303
+ onDragStart={(e) => {
304
+ setDragPath(col.id);
305
+ e.dataTransfer.effectAllowed = "move";
306
+ e.dataTransfer.setData("text/plain", col.id);
307
+ }}
308
+ onDragOver={(e) => {
309
+ e.preventDefault();
310
+ e.dataTransfer.dropEffect = "move";
311
+ const rect =
312
+ e.currentTarget.getBoundingClientRect();
313
+ const before =
314
+ e.clientY < rect.top + rect.height / 2;
315
+ if (dragOverPath !== col.id)
316
+ setDragOverPath(col.id);
317
+ if (dropBefore !== before) setDropBefore(before);
318
+ }}
319
+ onDragLeave={() => {
320
+ if (dragOverPath === col.id) setDragOverPath(null);
321
+ }}
322
+ onDrop={(e) => {
323
+ e.preventDefault();
324
+ if (dragPath)
325
+ reorderColumn(dragPath, col.id, dropBefore);
326
+ setDragPath(null);
327
+ setDragOverPath(null);
328
+ }}
329
+ onDragEnd={() => {
330
+ setDragPath(null);
331
+ setDragOverPath(null);
332
+ }}
333
+ className={cn(
334
+ // SB-Column-Item: standalone #1C1D1F pill, #252729
335
+ // border. Figma container spec: 8px radius, 8.8px
336
+ // padding, 8px gap between grip / label / switch.
337
+ "flex items-center gap-2 rounded-r-[99px] rounded-l-[60px] border border-[#252729] bg-[#1C1D1F] p-[8.8px] transition-colors cursor-grab active:cursor-grabbing",
338
+ isDragging ? "opacity-50" : "hover:bg-[#252729]",
339
+ )}
340
+ >
341
+ <span className="flex shrink-0 items-center justify-center">
342
+ <GripDots />
343
+ </span>
344
+ <span className="flex-1 text-[14px] text-white">
345
+ {col.label || field?.label || col.id}
346
+ </span>
347
+ <span className="flex shrink-0 items-center">
348
+ <DataViewsSwitch
349
+ checked={col.visible}
350
+ onCheckedChange={() =>
351
+ toggleColumnVisibility(col.id)
352
+ }
353
+ />
354
+ </span>
355
+ </div>
356
+ {isTarget && !dropBefore && <DropLine />}
357
+ </div>
358
+ );
359
+ })}
360
+ </div>
361
+ )}
362
+ </div>
363
+
364
+ <div className="h-px w-full bg-[#2C2D2E]" />
365
+
366
+ {/* Default Sort */}
367
+ <div className="space-y-3">
368
+ <SectionHeader title="Default Sort" />
369
+ {sortableColumns.length === 0 ? (
370
+ <p className="text-xs text-content-presentation-global-tertiary">
371
+ No sortable columns.
372
+ </p>
373
+ ) : (
374
+ // Single-choice radio list (Figma 1612:30016): selecting a
375
+ // column sets config.sortBy; direction keeps config.sortOrder.
376
+ <RadioGroup
377
+ value={config.sortBy || undefined}
378
+ onValueChange={(v) => onConfigChange({ sortBy: v })}
379
+ className="flex flex-col gap-1 space-y-0 rounded-[12px] bg-[#1C1D1F] p-1"
380
+ >
381
+ {sortableColumns.map((col, i) => {
382
+ const field = fieldByPath.get(col.id);
383
+ return (
384
+ <div key={col.id}>
385
+ {/* Edge-to-edge divider (Figma: no horizontal
386
+ inset). */}
387
+ {i > 0 && <div className="h-px bg-[#2C2D2E]" />}
388
+ <RadioRow
389
+ value={col.id}
390
+ label={col.label || field?.label || col.id}
391
+ />
392
+ </div>
393
+ );
394
+ })}
395
+ </RadioGroup>
396
+ )}
397
+ </div>
398
+ </div>
399
+ ) : (
400
+ <div className="[&>div]:w-full [&>div]:border-r-0 [&>div]:bg-transparent">
401
+ <FilterPanel
402
+ data={data}
403
+ fields={fields}
404
+ filters={filterState}
405
+ onFilterChange={(path: string, value: FilterValue) =>
406
+ onFilterChange({ ...filterState, [path]: value })
407
+ }
408
+ onClearAll={() => onFilterChange({})}
409
+ filterConfig={filterConfig}
410
+ />
411
+ </div>
412
+ )}
413
+ </div>
414
+ </div>
415
+ );
416
+ }
@@ -0,0 +1,126 @@
1
+ "use client"
2
+
3
+ import { Settings } from "lucide-react"
4
+ import type { ReactNode } from "react"
5
+ import type { ViewType } from "./types"
6
+ import { Button } from "../Button"
7
+ import { cn } from "../../utils/cn"
8
+
9
+ export type DataViewsHeaderView = {
10
+ id: ViewType
11
+ label: string
12
+ icon: ReactNode
13
+ }
14
+
15
+ type DataViewsHeaderProps = {
16
+ title: string
17
+ views: DataViewsHeaderView[]
18
+ currentView: ViewType
19
+ onViewChange: (view: ViewType) => void
20
+ showSettings: boolean
21
+ settingsOpen: boolean
22
+ onToggleSettings: () => void
23
+ onAddNew?: () => void
24
+ addNewLabel?: string
25
+ className?: string
26
+ }
27
+
28
+ export function DataViewsHeader({
29
+ title,
30
+ views,
31
+ currentView,
32
+ onViewChange,
33
+ showSettings,
34
+ settingsOpen,
35
+ onToggleSettings,
36
+ onAddNew,
37
+ addNewLabel = "Add New",
38
+ className,
39
+ }: DataViewsHeaderProps) {
40
+ return (
41
+ <div
42
+ // Header is always dark. data-theme="dark" makes the child Button
43
+ // components resolve dark-theme tokens (correct against the black bar)
44
+ // even when the host app runs in default/light theme.
45
+ data-theme="dark"
46
+ className={cn(
47
+ "flex h-[52px] w-full items-center gap-2 rounded-[12px] bg-black px-2",
48
+ className,
49
+ )}
50
+ >
51
+ {/* Title pill */}
52
+ <div className="flex h-9 shrink-0 items-center gap-2 rounded-[12px] border border-[#434446] bg-[#252729] px-[10px]">
53
+ <span className="text-[28px] font-[510] uppercase leading-[1.19] text-white">
54
+ {title}
55
+ </span>
56
+ </div>
57
+
58
+ {/* Divider */}
59
+ <div className="h-7 w-px shrink-0 bg-[#434446]" />
60
+
61
+ {/* Segmented view switcher */}
62
+ <div className="flex flex-1 items-center">
63
+ <div className="flex items-center gap-[2px] rounded-[10px] bg-[#252729] p-[2px] shadow-[inset_0_0_4px_0_rgba(0,0,0,0.08)]">
64
+ {views.map((view, idx) => {
65
+ const active = view.id === currentView
66
+ const prevActive = idx > 0 && views[idx - 1].id === currentView
67
+ // Separator sits between two inactive tabs only; the active white
68
+ // pill never has a flanking divider (matches Figma).
69
+ const showDivider = idx > 0 && !active && !prevActive
70
+ return (
71
+ <div key={view.id} className="flex items-center">
72
+ {showDivider && (
73
+ <div className="mx-[3px] h-3 w-px bg-[#434446]" />
74
+ )}
75
+ <button
76
+ type="button"
77
+ aria-pressed={active}
78
+ onClick={() => onViewChange(view.id)}
79
+ className={cn(
80
+ "flex h-6 items-center gap-[6px] rounded-[8px] px-3 text-[14px] font-[510] leading-none transition-all duration-200 ease-in-out",
81
+ active
82
+ ? "bg-white text-black shadow-[0_0_10px_2px_rgba(0,0,0,0.25)]"
83
+ : "bg-transparent text-white hover:bg-white/5",
84
+ )}
85
+ >
86
+ <span className="flex h-[14px] w-[14px] items-center justify-center [&_svg]:h-[14px] [&_svg]:w-[14px]">
87
+ {view.icon}
88
+ </span>
89
+ {view.label}
90
+ </button>
91
+ </div>
92
+ )
93
+ })}
94
+ </div>
95
+ </div>
96
+
97
+ {/* Action bar */}
98
+ <div className="flex shrink-0 items-center gap-2">
99
+ {onAddNew && (
100
+ <Button
101
+ variant="PrimeStyle"
102
+ size="M"
103
+ onClick={onAddNew}
104
+ className="rounded-[6px] bg-[#005ECC] px-[14px] text-[16px] font-[510] text-white hover:bg-[#005ECC]/90"
105
+ >
106
+ {addNewLabel}
107
+ </Button>
108
+ )}
109
+ {/* Hidden while the panel is open — the panel has its own close
110
+ control, so the header trigger would be redundant. */}
111
+ {showSettings && !settingsOpen && (
112
+ <Button
113
+ variant="BluContStyle"
114
+ size="M"
115
+ onClick={onToggleSettings}
116
+ aria-pressed={settingsOpen}
117
+ className="gap-[6px] rounded-[6px] px-[14px] text-[16px] font-[510]"
118
+ >
119
+ <Settings className="h-[18px] w-[18px]" />
120
+ Filter &amp; Config.
121
+ </Button>
122
+ )}
123
+ </div>
124
+ </div>
125
+ )
126
+ }