torch-glare 2.2.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/lib/components/Badge.tsx +6 -0
- package/apps/lib/components/ContextMenu.tsx +14 -11
- package/apps/lib/components/DataViews/DataViewRadio.tsx +9 -2
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +8 -8
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +13 -32
- package/apps/lib/components/DataViews/FilterPanel.tsx +26 -9
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +80 -0
- package/apps/lib/components/DataViews/types.ts +8 -0
- package/apps/lib/components/DatePicker.tsx +6 -1
- package/apps/lib/components/DropdownMenu.tsx +14 -11
- package/apps/lib/components/HeaderBar.tsx +127 -0
- package/apps/lib/components/SearchableSelect.tsx +42 -42
- package/apps/lib/components/SearchableTable.tsx +167 -177
- package/apps/lib/components/TabSwitch.tsx +181 -0
- package/docs/components/context-menu.md +30 -0
- package/docs/components/data-views-config-panel.md +3 -3
- package/docs/components/data-views-layout.md +9 -2
- package/docs/components/dropdown-menu.md +28 -0
- package/docs/components/header-bar.md +181 -0
- package/docs/components/searchable-table.md +44 -30
- package/docs/components/section-block.md +118 -0
- package/docs/components/tab-switch.md +163 -0
- package/docs/how-to/data-views-from-backend-response.md +1 -0
- package/package.json +1 -1
|
@@ -251,53 +251,53 @@ export function SearchableSelect({
|
|
|
251
251
|
<div
|
|
252
252
|
ref={scrollRef}
|
|
253
253
|
onScroll={handleScroll}
|
|
254
|
-
className="overflow-y-auto overflow-x-hidden scrollbar-hide"
|
|
254
|
+
className="overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide"
|
|
255
255
|
style={{ maxHeight: maxVisibleItems * ROW_HEIGHT }}
|
|
256
256
|
>
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
)}
|
|
274
|
-
>
|
|
275
|
-
<div>
|
|
276
|
-
{option.icon}
|
|
277
|
-
<span className="flex-1 text-start">{option.label}</span>
|
|
278
|
-
{isSelected && (
|
|
279
|
-
<i className="ri-check-line text-[16px] shrink-0" />
|
|
257
|
+
{filteredOptions.length > 0 && (
|
|
258
|
+
// Boxed group container — matches the DropdownMenu's auto-grouped look.
|
|
259
|
+
<div className="flex flex-col gap-[1px] rounded-[10px] overflow-hidden">
|
|
260
|
+
{filteredOptions.map((option) => {
|
|
261
|
+
const isSelected = option.value === value;
|
|
262
|
+
return (
|
|
263
|
+
// Same structure as DropdownMenuItem: MenuItemStyles on the
|
|
264
|
+
// element + a single inner <div> the styles target via [&>div].
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
key={option.value}
|
|
268
|
+
onClick={() => handleSelect(option)}
|
|
269
|
+
data-highlighted={isSelected ? "" : undefined}
|
|
270
|
+
className={cn(
|
|
271
|
+
MenuItemStyles({ variant: "Default", size: "M" }),
|
|
272
|
+
"shrink-0" // keep full row height; the list scrolls instead of squishing
|
|
280
273
|
)}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
274
|
+
>
|
|
275
|
+
<div>
|
|
276
|
+
{option.icon}
|
|
277
|
+
<span className="flex-1 text-start">{option.label}</span>
|
|
278
|
+
{isSelected && (
|
|
279
|
+
<i className="ri-check-line text-[16px] shrink-0" />
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</button>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
287
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
{/* Empty state — only when nothing is loading. */}
|
|
289
|
+
{filteredOptions.length === 0 && !loading && (
|
|
290
|
+
<div className="px-3 py-2 typography-body-small-regular text-white-alpha-75">
|
|
291
|
+
No results found
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
295
|
+
{/* Loading row (initial load or fetching the next page). */}
|
|
296
|
+
{loading && (
|
|
297
|
+
<div className="flex items-center justify-center py-2">
|
|
298
|
+
<LoadingIcon size="M" />
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
301
|
|
|
302
302
|
</div>
|
|
303
303
|
</PopoverContent>
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
InputHTMLAttributes,
|
|
6
|
+
ReactNode,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
5
12
|
import { cn } from "../utils/cn";
|
|
6
13
|
import { Themes } from "../utils/types";
|
|
7
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogTrigger,
|
|
17
|
+
DialogContent,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
} from "./Dialog";
|
|
8
20
|
import { Icon, Input, Group } from "./Input";
|
|
9
|
-
import {
|
|
10
|
-
import { useClickOutside } from "../hooks/useClickOutside";
|
|
21
|
+
import { LoadingIcon } from "./Button";
|
|
11
22
|
import {
|
|
12
23
|
Table,
|
|
13
24
|
TableHeader,
|
|
@@ -18,12 +29,12 @@ import {
|
|
|
18
29
|
} from "./Table";
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
|
-
* SearchableTable — a
|
|
32
|
+
* SearchableTable — a field that opens a modal Dialog to pick a row from a Table.
|
|
22
33
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
34
|
+
* The trigger shows a placeholder until something is selected, then shows the
|
|
35
|
+
* selected row's label. Clicking it opens a dialog containing a search input and
|
|
36
|
+
* the data Table; clicking a row selects it, sets the value, and closes the
|
|
37
|
+
* dialog. Supports client- or server-side search and infinite-scroll pagination.
|
|
27
38
|
*/
|
|
28
39
|
|
|
29
40
|
export interface SearchableTableColumn<T> {
|
|
@@ -40,13 +51,18 @@ interface Props<T> {
|
|
|
40
51
|
/** Controlled selected row (optional). */
|
|
41
52
|
value?: T | null;
|
|
42
53
|
onSelect?: (row: T) => void;
|
|
43
|
-
/** Text shown
|
|
54
|
+
/** Text shown on the trigger after selection; defaults to the first column's value. */
|
|
44
55
|
getLabel?: (row: T) => string;
|
|
45
56
|
/** Stable id/key per row; defaults to JSON of the row. */
|
|
46
57
|
getRowId?: (row: T) => string;
|
|
47
58
|
/** Which fields the search matches; defaults to every column key. */
|
|
48
59
|
searchKeys?: (keyof T & string)[];
|
|
60
|
+
/** Trigger placeholder shown until a row is selected. */
|
|
49
61
|
placeholder?: string;
|
|
62
|
+
/** Placeholder for the search input inside the dialog. */
|
|
63
|
+
searchPlaceholder?: string;
|
|
64
|
+
/** Label shown on the dialog's search field. */
|
|
65
|
+
title?: string;
|
|
50
66
|
size?: "XS" | "S" | "M";
|
|
51
67
|
variant?: "SystemStyle" | "PresentationStyle";
|
|
52
68
|
icon?: ReactNode;
|
|
@@ -71,44 +87,8 @@ interface Props<T> {
|
|
|
71
87
|
loading?: boolean;
|
|
72
88
|
/** Called when the list nears the bottom and `hasMore && !loading`. */
|
|
73
89
|
onLoadMore?: () => void;
|
|
74
|
-
/** Max rows visible before the list scrolls (default 6). */
|
|
75
|
-
maxVisibleRows?: number;
|
|
76
90
|
}
|
|
77
91
|
|
|
78
|
-
// One M-size table row ≈ 40px + the 44px sticky header.
|
|
79
|
-
const ROW_HEIGHT = 40;
|
|
80
|
-
const HEADER_HEIGHT = 44;
|
|
81
|
-
|
|
82
|
-
// Popover surface styled like the DropdownMenu (kept local — self-contained).
|
|
83
|
-
const tableDropdownStyles = cva(
|
|
84
|
-
[
|
|
85
|
-
"p-1",
|
|
86
|
-
"rounded-[14px]",
|
|
87
|
-
"border-0", // neutralize PopoverContent's base border (the card provides its own)
|
|
88
|
-
"outline-none",
|
|
89
|
-
"overflow-y-auto",
|
|
90
|
-
"scrollbar-hide",
|
|
91
|
-
"backdrop-blur-[21px]",
|
|
92
|
-
"data-[state=open]:animate-in",
|
|
93
|
-
"data-[state=open]:fade-in-0",
|
|
94
|
-
],
|
|
95
|
-
{
|
|
96
|
-
variants: {
|
|
97
|
-
variant: {
|
|
98
|
-
PresentationStyle: [
|
|
99
|
-
"bg-[rgba(61,64,69,0.72)]",
|
|
100
|
-
"shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
|
|
101
|
-
],
|
|
102
|
-
SystemStyle: [
|
|
103
|
-
"bg-background-system-body-primary",
|
|
104
|
-
"shadow-[0px_0px_18px_0px_rgba(0,0,0,0.75)]",
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
defaultVariants: { variant: "PresentationStyle" },
|
|
109
|
-
}
|
|
110
|
-
);
|
|
111
|
-
|
|
112
92
|
export function SearchableTable<T extends Record<string, unknown>>({
|
|
113
93
|
columns,
|
|
114
94
|
rows,
|
|
@@ -117,7 +97,9 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
117
97
|
getLabel,
|
|
118
98
|
getRowId,
|
|
119
99
|
searchKeys,
|
|
120
|
-
placeholder = "
|
|
100
|
+
placeholder = "Select…",
|
|
101
|
+
searchPlaceholder = "Search…",
|
|
102
|
+
title = "Select an item",
|
|
121
103
|
size = "M",
|
|
122
104
|
variant = "PresentationStyle",
|
|
123
105
|
icon,
|
|
@@ -130,14 +112,18 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
130
112
|
hasMore = false,
|
|
131
113
|
loading = false,
|
|
132
114
|
onLoadMore,
|
|
133
|
-
maxVisibleRows = 6,
|
|
134
115
|
}: Props<T>) {
|
|
135
116
|
const [open, setOpen] = useState(false);
|
|
136
117
|
const [search, setSearch] = useState("");
|
|
137
|
-
const
|
|
138
|
-
const popoverContentRef = useRef<HTMLDivElement>(null);
|
|
118
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
139
119
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
140
120
|
|
|
121
|
+
const rowId = (row: T) => getRowId?.(row) ?? JSON.stringify(row);
|
|
122
|
+
const label = (row: T) =>
|
|
123
|
+
getLabel?.(row) ?? String(row[columns[0]?.key] ?? "");
|
|
124
|
+
|
|
125
|
+
const keysToSearch = searchKeys ?? columns.map((c) => c.key);
|
|
126
|
+
|
|
141
127
|
// Debounced server search: notify the consumer to refetch when the query
|
|
142
128
|
// settles. Skipped entirely if no onSearchChange is provided (static mode).
|
|
143
129
|
useEffect(() => {
|
|
@@ -146,8 +132,13 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
146
132
|
return () => clearTimeout(id);
|
|
147
133
|
}, [search, onSearchChange, searchDebounceMs]);
|
|
148
134
|
|
|
149
|
-
//
|
|
150
|
-
|
|
135
|
+
// Reset the query whenever the dialog closes so it reopens clean.
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!open) setSearch("");
|
|
138
|
+
}, [open]);
|
|
139
|
+
|
|
140
|
+
// Infinite scroll: when the scroll viewport nears the bottom, ask the consumer
|
|
141
|
+
// to load the next page.
|
|
151
142
|
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
152
143
|
if (!onLoadMore || !hasMore || loading) return;
|
|
153
144
|
const el = e.currentTarget;
|
|
@@ -156,22 +147,6 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
156
147
|
}
|
|
157
148
|
};
|
|
158
149
|
|
|
159
|
-
const groupRef = useClickOutside<HTMLDivElement>((e) => {
|
|
160
|
-
const target = e?.target as Node;
|
|
161
|
-
if (
|
|
162
|
-
!groupRef.current?.contains(target) &&
|
|
163
|
-
!popoverContentRef.current?.contains(target)
|
|
164
|
-
) {
|
|
165
|
-
setOpen(false);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const rowId = (row: T) => getRowId?.(row) ?? JSON.stringify(row);
|
|
170
|
-
const label = (row: T) =>
|
|
171
|
-
getLabel?.(row) ?? String(row[columns[0]?.key] ?? "");
|
|
172
|
-
|
|
173
|
-
const keysToSearch = searchKeys ?? columns.map((c) => c.key);
|
|
174
|
-
|
|
175
150
|
const filteredRows = useMemo(() => {
|
|
176
151
|
// Server-side search mode: render rows as-is (already filtered upstream).
|
|
177
152
|
if (!filterClientSide) return rows;
|
|
@@ -185,124 +160,87 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
185
160
|
}, [rows, search, keysToSearch, filterClientSide]);
|
|
186
161
|
|
|
187
162
|
const selectedId = value ? rowId(value) : undefined;
|
|
188
|
-
// True while the user is actively typing a query. When false, the input shows
|
|
189
|
-
// the selected row's label as its real value (solid text, not placeholder).
|
|
190
|
-
const [searching, setSearching] = useState(false);
|
|
191
163
|
|
|
192
164
|
const handleSelect = (row: T) => {
|
|
193
165
|
onSelect?.(row);
|
|
194
|
-
setSearch("");
|
|
195
|
-
setSearching(false);
|
|
196
166
|
setOpen(false);
|
|
197
167
|
};
|
|
198
168
|
|
|
199
|
-
// Solid value: search text while typing, otherwise the selected label.
|
|
200
|
-
const displayValue = searching ? search : value ? label(value) : "";
|
|
201
|
-
|
|
202
169
|
return (
|
|
203
|
-
<
|
|
204
|
-
<
|
|
170
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
171
|
+
<DialogTrigger asChild>
|
|
172
|
+
{/* Trigger: shows placeholder until a row is selected, then its label. */}
|
|
205
173
|
<Group
|
|
206
174
|
dir={dir}
|
|
207
175
|
data-theme={theme}
|
|
208
176
|
variant={variant}
|
|
209
177
|
size={size === "XS" ? "S" : size}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
178
|
+
role="button"
|
|
179
|
+
tabIndex={0}
|
|
180
|
+
className={cn(
|
|
181
|
+
"flex w-full items-center gap-1 p-1 cursor-pointer",
|
|
182
|
+
className
|
|
183
|
+
)}
|
|
215
184
|
>
|
|
216
185
|
{icon && <Icon>{icon}</Icon>}
|
|
217
|
-
<
|
|
218
|
-
ref={inputRef}
|
|
219
|
-
placeholder={placeholder}
|
|
220
|
-
value={displayValue}
|
|
221
|
-
onChange={(e) => {
|
|
222
|
-
setSearch(e.target.value);
|
|
223
|
-
setSearching(true);
|
|
224
|
-
}}
|
|
225
|
-
onFocus={() => {
|
|
226
|
-
// Start a fresh search; the selected label clears so the user can type.
|
|
227
|
-
setSearching(true);
|
|
228
|
-
setSearch("");
|
|
229
|
-
setOpen(true);
|
|
230
|
-
}}
|
|
231
|
-
onBlur={() => setSearching(false)}
|
|
232
|
-
className={cn("min-w-[100px] flex-1", {
|
|
233
|
-
"!h-[18px]": size === "XS",
|
|
234
|
-
"!h-[22px]": size === "S",
|
|
235
|
-
"!h-[24px]": size === "M",
|
|
236
|
-
})}
|
|
237
|
-
/>
|
|
238
|
-
{/* Chevron toggle — boxed icon button matching the Select component. */}
|
|
239
|
-
<Button
|
|
240
|
-
as="span"
|
|
241
|
-
buttonType="icon"
|
|
242
|
-
tabIndex={-1}
|
|
243
|
-
aria-label={open ? "Close" : "Open"}
|
|
244
|
-
onMouseDown={(e: React.MouseEvent) => {
|
|
245
|
-
// Prevent the input's blur/focus race; toggle the dropdown.
|
|
246
|
-
e.preventDefault();
|
|
247
|
-
if (open) {
|
|
248
|
-
setOpen(false);
|
|
249
|
-
} else {
|
|
250
|
-
inputRef.current?.focus();
|
|
251
|
-
setOpen(true);
|
|
252
|
-
}
|
|
253
|
-
}}
|
|
186
|
+
<span
|
|
254
187
|
className={cn(
|
|
255
|
-
"
|
|
256
|
-
|
|
188
|
+
"flex-1 px-1 truncate typography-body-medium-regular",
|
|
189
|
+
value
|
|
190
|
+
? "text-content-presentation-action-light-primary"
|
|
191
|
+
: "text-content-presentation-action-light-secondary"
|
|
257
192
|
)}
|
|
258
193
|
>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
open && "rotate-180"
|
|
263
|
-
)}
|
|
264
|
-
/>
|
|
265
|
-
</Button>
|
|
194
|
+
{value ? label(value) : placeholder}
|
|
195
|
+
</span>
|
|
196
|
+
<i className="ri-arrow-down-s-line text-[16px] shrink-0 text-content-presentation-action-light-primary" />
|
|
266
197
|
</Group>
|
|
267
|
-
</
|
|
198
|
+
</DialogTrigger>
|
|
268
199
|
|
|
269
|
-
<
|
|
200
|
+
<DialogContent
|
|
270
201
|
dir={dir}
|
|
271
202
|
data-theme={theme}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// inside it. Cap height to ~maxVisibleRows (+ header) so the rest scrolls.
|
|
277
|
-
style={{
|
|
278
|
-
width: dropdownWidth || undefined,
|
|
279
|
-
maxHeight: HEADER_HEIGHT + maxVisibleRows * ROW_HEIGHT + 8,
|
|
203
|
+
onOpenAutoFocus={(e) => {
|
|
204
|
+
// Focus the search input instead of the first row.
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
inputRef.current?.focus();
|
|
280
207
|
}}
|
|
281
|
-
|
|
282
|
-
|
|
208
|
+
className={cn(
|
|
209
|
+
"w-[min(640px,90vw)] bg-transparent !items-stretch rounded-[14px] gap-3 shadow-none",
|
|
210
|
+
)}
|
|
283
211
|
>
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
212
|
+
{/* Visually-hidden title for accessibility (Radix requires a DialogTitle). */}
|
|
213
|
+
<DialogTitle className="sr-only">{title ?? "Select an item"}</DialogTitle>
|
|
214
|
+
|
|
215
|
+
{/* Search input — local inline-label field (full control over colors). */}
|
|
216
|
+
<SearchInput
|
|
217
|
+
ref={inputRef}
|
|
218
|
+
variant={variant}
|
|
219
|
+
theme={theme}
|
|
220
|
+
dir={dir}
|
|
221
|
+
label={title}
|
|
222
|
+
value={search}
|
|
223
|
+
placeholder={searchPlaceholder}
|
|
224
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
225
|
+
/>
|
|
226
|
+
|
|
227
|
+
{/* Scrollable table area */}
|
|
228
|
+
<div
|
|
229
|
+
ref={scrollRef}
|
|
230
|
+
onScroll={handleScroll}
|
|
231
|
+
className="max-h-[55vh] overflow-auto scrollbar-hide rounded-[10px] border border-white-alpha-40 bg-[rgba(184,192,204,0.5)]"
|
|
232
|
+
>
|
|
233
|
+
{filteredRows.length > 0 && (
|
|
290
234
|
<Table
|
|
291
235
|
theme={theme as never}
|
|
292
236
|
data-theme="dark"
|
|
293
|
-
|
|
294
|
-
// scrolls horizontally instead of squeezing them into the input width.
|
|
295
|
-
className="w-max rounded-[10px] overflow-hidden"
|
|
237
|
+
className="w-full rounded-[10px] overflow-hidden"
|
|
296
238
|
>
|
|
297
|
-
|
|
298
|
-
(which fill the row); bg/height on a <tr> doesn't render
|
|
299
|
-
reliably because cells paint over it. */}
|
|
300
|
-
<TableHeader className=" bg-black-alpha-20 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]">
|
|
239
|
+
<TableHeader className="bg-black-alpha-20 shadow-[0px_4px_8px_0px_rgba(0,0,0,0.15)]">
|
|
301
240
|
<TableRow className="h-[44px] [&_button]:bg-white-alpha-40 [&>th]:border-white-alpha-20">
|
|
302
241
|
{columns.map((col) => (
|
|
303
242
|
<TableHead
|
|
304
243
|
key={col.key}
|
|
305
|
-
// White semibold header text from the design.
|
|
306
244
|
className="cursor-default typography-body-medium-medium text-white"
|
|
307
245
|
>
|
|
308
246
|
{col.header}
|
|
@@ -318,15 +256,14 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
318
256
|
key={id}
|
|
319
257
|
state={selectedId === id ? "selected" : undefined}
|
|
320
258
|
onClick={() => handleSelect(row)}
|
|
321
|
-
|
|
322
|
-
|
|
259
|
+
className={cn(
|
|
260
|
+
"cursor-pointer h-[40px]",
|
|
261
|
+
selectedId === id && "bg-white-alpha-50"
|
|
262
|
+
)}
|
|
323
263
|
>
|
|
324
264
|
{columns.map((col) => (
|
|
325
265
|
<TableCell
|
|
326
266
|
key={col.key}
|
|
327
|
-
// White body text; whitespace-nowrap (overriding the
|
|
328
|
-
// component's break-all) keeps columns wide so the
|
|
329
|
-
// table overflows horizontally instead of wrapping.
|
|
330
267
|
className="border-b border-white-alpha-20 text-white px-[8px] !whitespace-nowrap !break-normal"
|
|
331
268
|
>
|
|
332
269
|
{col.render
|
|
@@ -339,25 +276,78 @@ export function SearchableTable<T extends Record<string, unknown>>({
|
|
|
339
276
|
})}
|
|
340
277
|
</TableBody>
|
|
341
278
|
</Table>
|
|
342
|
-
|
|
343
|
-
)}
|
|
279
|
+
)}
|
|
344
280
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
281
|
+
{/* Empty state — only when nothing is loading. */}
|
|
282
|
+
{filteredRows.length === 0 && !loading && (
|
|
283
|
+
<div className="px-3 py-6 text-center typography-body-small-regular text-white-alpha-75">
|
|
284
|
+
No results found
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
351
287
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
288
|
+
{/* Loading row (initial load or fetching the next page). */}
|
|
289
|
+
{loading && (
|
|
290
|
+
<div className="flex items-center justify-center py-3">
|
|
291
|
+
<LoadingIcon size="M" />
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
</DialogContent>
|
|
296
|
+
</Dialog>
|
|
360
297
|
);
|
|
361
298
|
}
|
|
362
299
|
|
|
363
300
|
SearchableTable.displayName = "SearchableTable";
|
|
301
|
+
|
|
302
|
+
/* -------------------------------------------------------------------------- */
|
|
303
|
+
/* SearchInput — local inline-label search field (replaces InnerLabelField). */
|
|
304
|
+
/* The whole field + its text turn white on hover/focus, fully controlled here. */
|
|
305
|
+
/* -------------------------------------------------------------------------- */
|
|
306
|
+
|
|
307
|
+
interface SearchInputProps
|
|
308
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
309
|
+
variant?: "SystemStyle" | "PresentationStyle";
|
|
310
|
+
theme?: Themes;
|
|
311
|
+
label?: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
|
315
|
+
({ variant = "PresentationStyle", theme, label, className, ...props }, ref) => (
|
|
316
|
+
<Group
|
|
317
|
+
data-theme={theme}
|
|
318
|
+
variant={variant}
|
|
319
|
+
size="M"
|
|
320
|
+
className={cn(
|
|
321
|
+
"group flex w-full items-center gap-2 p-1 rounded-[10px] transition-colors",
|
|
322
|
+
"border border-white-alpha-40 bg-[rgba(184,192,204,0.5)]",
|
|
323
|
+
"hover:bg-background-presentation-table-row-hover",
|
|
324
|
+
"focus-within:bg-background-presentation-table-row-hover",
|
|
325
|
+
className
|
|
326
|
+
)}
|
|
327
|
+
>
|
|
328
|
+
<Icon className="shrink-0">
|
|
329
|
+
<i className="ri-search-line text-content-presentation-global-secondary transition-colors group-hover:text-white group-focus-within:text-white" />
|
|
330
|
+
</Icon>
|
|
331
|
+
|
|
332
|
+
{label && (
|
|
333
|
+
<>
|
|
334
|
+
<span className="shrink-0 px-1 typography-labels-small-regular text-content-presentation-global-secondary text-[14px] text-white transition-colors group-hover:text-white group-focus-within:text-white">
|
|
335
|
+
{label}
|
|
336
|
+
</span>
|
|
337
|
+
<span className="h-[14px] w-px shrink-0 rounded-full bg-border-presentation-action-labelless-divider transition-colors group-hover:bg-white-alpha-40" />
|
|
338
|
+
</>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
<Input
|
|
342
|
+
ref={ref}
|
|
343
|
+
{...props}
|
|
344
|
+
className={cn(
|
|
345
|
+
"min-w-[100px] flex-1 !h-[24px] bg-transparent",
|
|
346
|
+
"text-content-presentation-action-light-primary",
|
|
347
|
+
"transition-colors group-hover:!text-white group-focus-within:!text-white"
|
|
348
|
+
)}
|
|
349
|
+
/>
|
|
350
|
+
</Group>
|
|
351
|
+
)
|
|
352
|
+
);
|
|
353
|
+
SearchInput.displayName = "SearchInput";
|