torch-glare 2.1.2 → 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/Card.tsx +68 -54
- package/apps/lib/components/DataViews/DataViewRadio.tsx +47 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +56 -45
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +130 -28
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +32 -2
- package/apps/lib/components/DataViews/FilterPanel.tsx +148 -3
- package/apps/lib/components/DataViews/HeaderSearch.tsx +97 -0
- package/apps/lib/components/DataViews/InboxView.tsx +263 -282
- package/apps/lib/components/DataViews/InboxViewCard.tsx +136 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +264 -153
- package/apps/lib/components/DataViews/PanelControls.tsx +10 -41
- package/apps/lib/components/DataViews/TreeView.tsx +220 -191
- package/apps/lib/components/DataViews/index.ts +6 -0
- package/apps/lib/components/DataViews/types.ts +30 -1
- 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 +160 -137
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +221 -93
- package/apps/lib/components/TreeFolder/types.ts +9 -0
- package/apps/lib/layouts/DataViewCard.tsx +76 -0
- package/dist/src/shared/copyComponentsRecursively.js +9 -1
- package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
- package/docs/components/data-views-config-panel.md +204 -0
- package/docs/components/data-views-layout.md +270 -0
- package/package.json +1 -1
|
@@ -38,7 +38,7 @@ const AvatarFallback = React.forwardRef<
|
|
|
38
38
|
<AvatarPrimitive.Fallback
|
|
39
39
|
ref={ref}
|
|
40
40
|
className={cn(
|
|
41
|
-
"flex h-full w-full items-center justify-center rounded-full bg-background-
|
|
41
|
+
"flex h-full w-full items-center justify-center rounded-full bg-background-presentation-action-disabled text-content-presentation-global-primary",
|
|
42
42
|
className
|
|
43
43
|
)}
|
|
44
44
|
{...props}
|
|
@@ -1,64 +1,78 @@
|
|
|
1
|
-
import { cn } from
|
|
2
|
-
import React, { HTMLAttributes } from
|
|
1
|
+
import { cn } from "../utils/cn";
|
|
2
|
+
import React, { HTMLAttributes } from "react";
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"transition-all ease-in-out duration-200",
|
|
18
|
-
"p-[16px]",
|
|
19
|
-
"border-border-presentation-global-primary",
|
|
20
|
-
"bg-background-presentation-form-radiocard-base",
|
|
21
|
-
"hover:border-border-presentation-state-focus",
|
|
22
|
-
"focus:border-border-presentation-state-focus",
|
|
23
|
-
className
|
|
24
|
-
)} />
|
|
25
|
-
)
|
|
5
|
+
interface Props extends Omit<
|
|
6
|
+
HTMLAttributes<
|
|
7
|
+
| HTMLDivElement
|
|
8
|
+
| HTMLHeadingElement
|
|
9
|
+
| HTMLParagraphElement
|
|
10
|
+
| HTMLLabelElement
|
|
11
|
+
>,
|
|
12
|
+
"htmlFor"
|
|
13
|
+
> {
|
|
14
|
+
as?: React.ElementType;
|
|
15
|
+
asChild?: boolean;
|
|
16
|
+
htmlFor?: string;
|
|
26
17
|
}
|
|
18
|
+
export const Card = ({
|
|
19
|
+
className,
|
|
20
|
+
htmlFor,
|
|
21
|
+
asChild,
|
|
22
|
+
as: Tag = "section",
|
|
23
|
+
...props
|
|
24
|
+
}: Props) => {
|
|
25
|
+
const Component = asChild ? Slot : Tag;
|
|
26
|
+
return (
|
|
27
|
+
<Component
|
|
28
|
+
htmlFor={htmlFor}
|
|
29
|
+
{...props}
|
|
30
|
+
className={cn(
|
|
31
|
+
"flex flex-col justify-start",
|
|
32
|
+
"gap-2 rounded-[12px] border",
|
|
33
|
+
"transition-all ease-in-out duration-200",
|
|
34
|
+
"p-[16px]",
|
|
35
|
+
"border-border-presentation-global-primary",
|
|
36
|
+
"bg-background-presentation-form-radiocard-base",
|
|
37
|
+
"hover:border-border-presentation-state-focus",
|
|
38
|
+
"focus:border-border-presentation-state-focus",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
interface GeneralProps extends HTMLAttributes<HTMLHeadingElement> { }
|
|
45
|
+
interface GeneralProps extends HTMLAttributes<HTMLHeadingElement> {}
|
|
31
46
|
|
|
32
47
|
export const CardHeader = ({ className, ...props }: GeneralProps) => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
{...props}
|
|
51
|
+
className={cn(
|
|
52
|
+
"text-content-presentation-global-primary m-0 typography-headers-medium-semibold",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
></div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
44
58
|
|
|
45
59
|
export const CardDescription = ({ className, ...props }: GeneralProps) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
{...props}
|
|
63
|
+
className={cn(
|
|
64
|
+
"text-content-presentation-global-primary m-0 typography-body-medium-semibold",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
></div>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
57
70
|
|
|
58
71
|
export const CardContent = ({ className, ...props }: GeneralProps) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
return (
|
|
73
|
+
<section
|
|
74
|
+
{...props}
|
|
75
|
+
className={cn("flex gap-1 flex-col items-start flex-1", className)}
|
|
76
|
+
></section>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
|
5
|
+
import { cn } from "../../utils/cn";
|
|
6
|
+
|
|
7
|
+
export interface DataViewRadioProps {
|
|
8
|
+
value: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DataViewRadio({
|
|
15
|
+
value,
|
|
16
|
+
label,
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
}: DataViewRadioProps) {
|
|
20
|
+
return (
|
|
21
|
+
<RadioGroupPrimitive.Item
|
|
22
|
+
value={value}
|
|
23
|
+
className={cn(
|
|
24
|
+
"group flex w-full items-center gap-1.5 py-1 ps-2 h-[32px]",
|
|
25
|
+
"cursor-pointer rounded-[8px] text-left outline-none transition-colors",
|
|
26
|
+
"hover:bg-background-presentation-action-contstyle-hover focus-visible:bg-background-presentation-action-contstyle-hover",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<span
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full",
|
|
33
|
+
"border border-border-presentation-action-primary bg-background-presentation-form-field-primary transition-colors",
|
|
34
|
+
"group-data-[state=checked]:border-transparent",
|
|
35
|
+
"group-data-[state=checked]:bg-border-presentation-state-focus",
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
39
|
+
<span className="h-[6px] w-[6px] rounded-full bg-white" />
|
|
40
|
+
</RadioGroupPrimitive.Indicator>
|
|
41
|
+
</span>
|
|
42
|
+
<span className="flex-1 typography-body-medium-regular text-content-presentation-global-primary">
|
|
43
|
+
{children ?? label}
|
|
44
|
+
</span>
|
|
45
|
+
</RadioGroupPrimitive.Item>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { Fragment, useMemo, useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
X,
|
|
6
6
|
Settings as SettingsIcon,
|
|
@@ -163,25 +163,21 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
const [dragPath, setDragPath] = useState<string | null>(null);
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
166
|
+
// Insertion slot in the ordered list: 0 means before the first row, N means
|
|
167
|
+
// after the last row (count). Single source of truth — there is exactly one
|
|
168
|
+
// indicator at a time, so no double-line ambiguity between adjacent rows.
|
|
169
|
+
const [dropSlot, setDropSlot] = useState<number | null>(null);
|
|
169
170
|
|
|
170
|
-
const
|
|
171
|
-
sourcePath: string,
|
|
172
|
-
targetPath: string,
|
|
173
|
-
before: boolean,
|
|
174
|
-
) => {
|
|
175
|
-
if (sourcePath === targetPath) return;
|
|
171
|
+
const reorderColumnToSlot = (sourcePath: string, slot: number) => {
|
|
176
172
|
const ids = orderedColumns.map((c) => c.id);
|
|
177
173
|
const from = ids.indexOf(sourcePath);
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
if (from === -1) return;
|
|
175
|
+
// Dropping into the same logical position (before or after itself) is a no-op.
|
|
176
|
+
if (slot === from || slot === from + 1) return;
|
|
180
177
|
const reordered = [...ids];
|
|
181
178
|
reordered.splice(from, 1);
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
const insertAt = before ? to : to + 1;
|
|
179
|
+
// After removal, indices shift left by 1 for any slot beyond `from`.
|
|
180
|
+
const insertAt = slot > from ? slot - 1 : slot;
|
|
185
181
|
reordered.splice(insertAt, 0, sourcePath);
|
|
186
182
|
const orderByPath = new Map(reordered.map((id, i) => [id, i]));
|
|
187
183
|
const next = config.tableColumns.map((c) => {
|
|
@@ -290,14 +286,17 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
290
286
|
</p>
|
|
291
287
|
) : (
|
|
292
288
|
<div data-theme="dark" className="flex flex-col gap-2">
|
|
293
|
-
{orderedColumns.map((col) => {
|
|
289
|
+
{orderedColumns.map((col, index) => {
|
|
294
290
|
const field = fieldByPath.get(col.id);
|
|
295
291
|
const isDragging = dragPath === col.id;
|
|
296
|
-
|
|
297
|
-
|
|
292
|
+
// Slot for the cursor on this row: top half = insert at
|
|
293
|
+
// `index` (before this row); bottom half = `index + 1`
|
|
294
|
+
// (after this row, which is the SAME slot as "before next
|
|
295
|
+
// row" — the single source of truth avoids the old
|
|
296
|
+
// double-line problem in the gap between rows).
|
|
298
297
|
return (
|
|
299
298
|
<div key={col.id}>
|
|
300
|
-
{
|
|
299
|
+
{dropSlot === index && dragPath && <DropLine />}
|
|
301
300
|
<div
|
|
302
301
|
draggable
|
|
303
302
|
onDragStart={(e) => {
|
|
@@ -312,29 +311,25 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
312
311
|
e.currentTarget.getBoundingClientRect();
|
|
313
312
|
const before =
|
|
314
313
|
e.clientY < rect.top + rect.height / 2;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (dropBefore !== before) setDropBefore(before);
|
|
318
|
-
}}
|
|
319
|
-
onDragLeave={() => {
|
|
320
|
-
if (dragOverPath === col.id) setDragOverPath(null);
|
|
314
|
+
const slot = before ? index : index + 1;
|
|
315
|
+
if (dropSlot !== slot) setDropSlot(slot);
|
|
321
316
|
}}
|
|
322
317
|
onDrop={(e) => {
|
|
323
318
|
e.preventDefault();
|
|
324
|
-
if (dragPath)
|
|
325
|
-
|
|
319
|
+
if (dragPath && dropSlot != null)
|
|
320
|
+
reorderColumnToSlot(dragPath, dropSlot);
|
|
326
321
|
setDragPath(null);
|
|
327
|
-
|
|
322
|
+
setDropSlot(null);
|
|
328
323
|
}}
|
|
329
324
|
onDragEnd={() => {
|
|
330
325
|
setDragPath(null);
|
|
331
|
-
|
|
326
|
+
setDropSlot(null);
|
|
332
327
|
}}
|
|
333
328
|
className={cn(
|
|
334
329
|
// SB-Column-Item: standalone #1C1D1F pill, #252729
|
|
335
330
|
// border. Figma container spec: 8px radius, 8.8px
|
|
336
331
|
// padding, 8px gap between grip / label / switch.
|
|
337
|
-
"flex items-center gap-2 rounded-
|
|
332
|
+
"flex items-center gap-2 rounded-e-[99px] rounded-s-[60px] border border-[#252729] bg-[#1C1D1F] p-[8.8px] transition-colors cursor-grab active:cursor-grabbing",
|
|
338
333
|
isDragging ? "opacity-50" : "hover:bg-[#252729]",
|
|
339
334
|
)}
|
|
340
335
|
>
|
|
@@ -353,10 +348,14 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
353
348
|
/>
|
|
354
349
|
</span>
|
|
355
350
|
</div>
|
|
356
|
-
{isTarget && !dropBefore && <DropLine />}
|
|
357
351
|
</div>
|
|
358
352
|
);
|
|
359
353
|
})}
|
|
354
|
+
{/* Drop-at-end indicator: only ever rendered when the slot
|
|
355
|
+
points past the last row, so still exactly one line. */}
|
|
356
|
+
{dropSlot === orderedColumns.length && dragPath && (
|
|
357
|
+
<DropLine />
|
|
358
|
+
)}
|
|
360
359
|
</div>
|
|
361
360
|
)}
|
|
362
361
|
</div>
|
|
@@ -373,10 +372,21 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
373
372
|
) : (
|
|
374
373
|
// Single-choice radio list (Figma 1612:30016): selecting a
|
|
375
374
|
// column sets config.sortBy; direction keeps config.sortOrder.
|
|
375
|
+
// Rows + dividers are flat siblings so the `peer` pattern
|
|
376
|
+
// can hide the dividers immediately before AND after a
|
|
377
|
+
// hovered row.
|
|
376
378
|
<RadioGroup
|
|
377
379
|
value={config.sortBy || undefined}
|
|
378
380
|
onValueChange={(v) => onConfigChange({ sortBy: v })}
|
|
379
|
-
className=
|
|
381
|
+
className={cn(
|
|
382
|
+
"flex flex-col space-y-0 rounded-[12px] bg-[#1C1D1F] p-1",
|
|
383
|
+
// Wrapper containing the hovered row: hide its OWN
|
|
384
|
+
// divider (sits above the row).
|
|
385
|
+
"[&>div:has(>[role=radio]:hover)>.dv-divider]:opacity-0",
|
|
386
|
+
// Wrapper that directly follows the one with the hovered
|
|
387
|
+
// row: hide its divider (sits below the hovered row).
|
|
388
|
+
"[&>div:has(>[role=radio]:hover)+div>.dv-divider]:opacity-0",
|
|
389
|
+
)}
|
|
380
390
|
>
|
|
381
391
|
{sortableColumns.map((col, i) => {
|
|
382
392
|
const field = fieldByPath.get(col.id);
|
|
@@ -384,7 +394,9 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
384
394
|
<div key={col.id}>
|
|
385
395
|
{/* Edge-to-edge divider (Figma: no horizontal
|
|
386
396
|
inset). */}
|
|
387
|
-
{i > 0 &&
|
|
397
|
+
{i > 0 && (
|
|
398
|
+
<div className="dv-divider h-px bg-[#2C2D2E]" />
|
|
399
|
+
)}
|
|
388
400
|
<RadioRow
|
|
389
401
|
value={col.id}
|
|
390
402
|
label={col.label || field?.label || col.id}
|
|
@@ -397,18 +409,17 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
|
|
|
397
409
|
</div>
|
|
398
410
|
</div>
|
|
399
411
|
) : (
|
|
400
|
-
<
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
</div>
|
|
412
|
+
<FilterPanel
|
|
413
|
+
variant="panel"
|
|
414
|
+
data={data}
|
|
415
|
+
fields={fields}
|
|
416
|
+
filters={filterState}
|
|
417
|
+
onFilterChange={(path: string, value: FilterValue) =>
|
|
418
|
+
onFilterChange({ ...filterState, [path]: value })
|
|
419
|
+
}
|
|
420
|
+
onClearAll={() => onFilterChange({})}
|
|
421
|
+
filterConfig={filterConfig}
|
|
422
|
+
/>
|
|
412
423
|
)}
|
|
413
424
|
</div>
|
|
414
425
|
</div>
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { Settings } from "lucide-react"
|
|
4
|
-
import type
|
|
5
|
-
import type { ViewType } from "./types"
|
|
6
|
-
import { Button } from "../Button"
|
|
7
|
-
import { cn } from "../../utils/cn"
|
|
3
|
+
import { Search, Settings } from "lucide-react";
|
|
4
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
5
|
+
import type { ViewType } from "./types";
|
|
6
|
+
import { Button } from "../Button";
|
|
7
|
+
import { cn } from "../../utils/cn";
|
|
8
8
|
|
|
9
9
|
export type DataViewsHeaderView = {
|
|
10
|
-
id: ViewType
|
|
11
|
-
label: string
|
|
12
|
-
icon: ReactNode
|
|
13
|
-
}
|
|
10
|
+
id: ViewType;
|
|
11
|
+
label: string;
|
|
12
|
+
icon: ReactNode;
|
|
13
|
+
};
|
|
14
14
|
|
|
15
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
|
-
|
|
26
|
-
|
|
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
|
+
searchValue?: string;
|
|
26
|
+
onSearchChange?: (value: string) => void;
|
|
27
|
+
searchPlaceholder?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
};
|
|
27
30
|
|
|
28
31
|
export function DataViewsHeader({
|
|
29
32
|
title,
|
|
@@ -35,6 +38,9 @@ export function DataViewsHeader({
|
|
|
35
38
|
onToggleSettings,
|
|
36
39
|
onAddNew,
|
|
37
40
|
addNewLabel = "Add New",
|
|
41
|
+
searchValue,
|
|
42
|
+
onSearchChange,
|
|
43
|
+
searchPlaceholder = "Search...",
|
|
38
44
|
className,
|
|
39
45
|
}: DataViewsHeaderProps) {
|
|
40
46
|
return (
|
|
@@ -56,17 +62,17 @@ export function DataViewsHeader({
|
|
|
56
62
|
</div>
|
|
57
63
|
|
|
58
64
|
{/* Divider */}
|
|
59
|
-
<div className="h-
|
|
65
|
+
<div className="h-5 w-px shrink-0 bg-[#434446]" />
|
|
60
66
|
|
|
61
67
|
{/* Segmented view switcher */}
|
|
62
|
-
<div className="flex flex-1 items-center">
|
|
68
|
+
<div className="flex flex-1 items-center gap-2">
|
|
63
69
|
<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
70
|
{views.map((view, idx) => {
|
|
65
|
-
const active = view.id === currentView
|
|
66
|
-
const prevActive = idx > 0 && views[idx - 1].id === currentView
|
|
71
|
+
const active = view.id === currentView;
|
|
72
|
+
const prevActive = idx > 0 && views[idx - 1].id === currentView;
|
|
67
73
|
// Separator sits between two inactive tabs only; the active white
|
|
68
74
|
// pill never has a flanking divider (matches Figma).
|
|
69
|
-
const showDivider = idx > 0 && !active && !prevActive
|
|
75
|
+
const showDivider = idx > 0 && !active && !prevActive;
|
|
70
76
|
return (
|
|
71
77
|
<div key={view.id} className="flex items-center">
|
|
72
78
|
{showDivider && (
|
|
@@ -89,13 +95,20 @@ export function DataViewsHeader({
|
|
|
89
95
|
{view.label}
|
|
90
96
|
</button>
|
|
91
97
|
</div>
|
|
92
|
-
)
|
|
98
|
+
);
|
|
93
99
|
})}
|
|
94
100
|
</div>
|
|
95
101
|
</div>
|
|
96
102
|
|
|
97
103
|
{/* Action bar */}
|
|
98
104
|
<div className="flex shrink-0 items-center gap-2">
|
|
105
|
+
{onSearchChange && (
|
|
106
|
+
<HeaderSearch
|
|
107
|
+
value={searchValue ?? ""}
|
|
108
|
+
onChange={onSearchChange}
|
|
109
|
+
placeholder={searchPlaceholder}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
99
112
|
{onAddNew && (
|
|
100
113
|
<Button
|
|
101
114
|
variant="PrimeStyle"
|
|
@@ -122,5 +135,94 @@ export function DataViewsHeader({
|
|
|
122
135
|
)}
|
|
123
136
|
</div>
|
|
124
137
|
</div>
|
|
125
|
-
)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function HeaderSearch({
|
|
142
|
+
value,
|
|
143
|
+
onChange,
|
|
144
|
+
placeholder,
|
|
145
|
+
}: {
|
|
146
|
+
value: string;
|
|
147
|
+
onChange: (value: string) => void;
|
|
148
|
+
placeholder: string;
|
|
149
|
+
}) {
|
|
150
|
+
const [open, setOpen] = useState(false);
|
|
151
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
152
|
+
const wrapRef = useRef<HTMLDivElement>(null);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (open) inputRef.current?.focus();
|
|
156
|
+
}, [open]);
|
|
157
|
+
|
|
158
|
+
// Auto-collapse on outside click only when the input is empty — keeps the
|
|
159
|
+
// expanded state if the user has typed a query but clicks away.
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!open) return;
|
|
162
|
+
function onPointerDown(e: MouseEvent) {
|
|
163
|
+
if (!wrapRef.current) return;
|
|
164
|
+
if (wrapRef.current.contains(e.target as Node)) return;
|
|
165
|
+
if (!value) setOpen(false);
|
|
166
|
+
}
|
|
167
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
168
|
+
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
169
|
+
}, [open, value]);
|
|
170
|
+
|
|
171
|
+
function clearAndCollapse() {
|
|
172
|
+
onChange("");
|
|
173
|
+
setOpen(false);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!open) {
|
|
177
|
+
return (
|
|
178
|
+
<Button
|
|
179
|
+
variant="BluContStyle"
|
|
180
|
+
size="M"
|
|
181
|
+
buttonType="icon"
|
|
182
|
+
aria-label="Open search"
|
|
183
|
+
onClick={() => setOpen(true)}
|
|
184
|
+
className="shrink-0 rounded-[6px] border border-border-presentation-global-primary"
|
|
185
|
+
>
|
|
186
|
+
<Search className="h-[18px] w-[18px]" />
|
|
187
|
+
</Button>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div
|
|
193
|
+
ref={wrapRef}
|
|
194
|
+
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"
|
|
195
|
+
>
|
|
196
|
+
<input
|
|
197
|
+
ref={inputRef}
|
|
198
|
+
type="text"
|
|
199
|
+
value={value}
|
|
200
|
+
placeholder={placeholder}
|
|
201
|
+
onChange={(e) => onChange(e.target.value)}
|
|
202
|
+
onKeyDown={(e) => {
|
|
203
|
+
if (e.key === "Escape") clearAndCollapse();
|
|
204
|
+
}}
|
|
205
|
+
className="flex-1 bg-transparent text-[14px] leading-none text-white caret-[#1E7AFE] placeholder:text-content-presentation-global-tertiary focus:outline-none"
|
|
206
|
+
/>
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
aria-label="Clear search"
|
|
210
|
+
onClick={clearAndCollapse}
|
|
211
|
+
className="flex shrink-0 items-center justify-center self-stretch px-1"
|
|
212
|
+
>
|
|
213
|
+
<svg
|
|
214
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
215
|
+
width="16"
|
|
216
|
+
height="16"
|
|
217
|
+
viewBox="0 0 16 16"
|
|
218
|
+
fill="none"
|
|
219
|
+
>
|
|
220
|
+
<path
|
|
221
|
+
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"
|
|
222
|
+
fill="white"
|
|
223
|
+
/>
|
|
224
|
+
</svg>
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
126
228
|
}
|