torch-glare 2.1.5 → 2.2.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/BadgeField.tsx +131 -12
- package/apps/lib/components/ContextMenu.tsx +524 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +1 -1
- package/apps/lib/components/Drawer.tsx +23 -4
- package/apps/lib/components/DropdownMenu.tsx +254 -102
- package/apps/lib/components/SearchableSelect.tsx +308 -0
- package/apps/lib/components/SearchableTable.tsx +363 -0
- package/apps/lib/components/Table.tsx +6 -6
- package/dist/bin/index.js +2 -1
- package/dist/bin/index.js.map +1 -1
- package/docs/components/context-menu.md +455 -0
- package/docs/components/data-views-layout.md +16 -1
- package/docs/components/drawer.md +527 -668
- package/docs/components/dropdown-menu.md +37 -34
- package/docs/components/searchable-select.md +359 -0
- package/docs/components/searchable-table.md +419 -0
- package/docs/reference/tailwind-plugins.md +21 -1
- package/docs/tutorials/getting-started.md +15 -1
- package/package.json +1 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SearchableTable
|
|
3
|
+
description: A searchable combobox whose dropdown renders a real Table, with single-select, client- or server-side search, and infinite-scroll pagination
|
|
4
|
+
group: Inputs
|
|
5
|
+
keywords: [searchable-table, combobox, table, search, async, infinite-scroll, pagination, select]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SearchableTable
|
|
9
|
+
|
|
10
|
+
> A searchable combobox that renders its options as a real, multi-column `Table` inside a `Popover`. Type to filter rows (client-side) or refetch them from a backend (server-side), click a row to single-select it, and scroll to the bottom to lazy-load more pages. Generic over your row type `T`.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
`SearchableTable` is part of the TORCH Glare component library. It composes the [Popover](./popover.md) (built on Radix Popover) and the [Table](./table.md) component internally, so both must be available — they ship with the library.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @torch-ui/components @radix-ui/react-popover
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Import
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { SearchableTable } from '@torch-ui/components'
|
|
24
|
+
import type { SearchableTableColumn } from '@torch-ui/components'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Examples
|
|
28
|
+
|
|
29
|
+
### Basic (static rows)
|
|
30
|
+
|
|
31
|
+
Provide a `columns` config and a `rows` array. Single-select is controlled via `value` / `onSelect`. Use `getLabel` to control the text shown in the input after selection, and `getRowId` to give each row a stable key.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { SearchableTable } from '@torch-ui/components'
|
|
35
|
+
import type { SearchableTableColumn } from '@torch-ui/components'
|
|
36
|
+
import { useState } from 'react'
|
|
37
|
+
|
|
38
|
+
type User = { id: string; name: string; role: string; email: string }
|
|
39
|
+
|
|
40
|
+
const users: User[] = [
|
|
41
|
+
{ id: '1', name: 'Ada Lovelace', role: 'Engineer', email: 'ada@torch.dev' },
|
|
42
|
+
{ id: '2', name: 'Alan Turing', role: 'Researcher', email: 'alan@torch.dev' },
|
|
43
|
+
{ id: '3', name: 'Grace Hopper', role: 'Admiral', email: 'grace@torch.dev' },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const columns: SearchableTableColumn<User>[] = [
|
|
47
|
+
{ key: 'name', header: 'Name' },
|
|
48
|
+
{ key: 'role', header: 'Role' },
|
|
49
|
+
{ key: 'email', header: 'Email' },
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
function Example() {
|
|
53
|
+
const [selected, setSelected] = useState<User | null>(null)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<SearchableTable<User>
|
|
57
|
+
columns={columns}
|
|
58
|
+
rows={users}
|
|
59
|
+
value={selected}
|
|
60
|
+
onSelect={setSelected}
|
|
61
|
+
getLabel={(row) => row.name}
|
|
62
|
+
getRowId={(row) => row.id}
|
|
63
|
+
placeholder="Search users…"
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The input opens its dropdown on focus. As you type, rows are filtered locally by every column key. Clicking a row selects it (single-select), closes the dropdown, and shows `getLabel(row)` in the input.
|
|
70
|
+
|
|
71
|
+
### Custom cell rendering
|
|
72
|
+
|
|
73
|
+
Use a column's `render` to control how a cell is displayed. When omitted, the cell falls back to `String(row[key])`.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const columns: SearchableTableColumn<User>[] = [
|
|
77
|
+
{ key: 'name', header: 'Name' },
|
|
78
|
+
{
|
|
79
|
+
key: 'role',
|
|
80
|
+
header: 'Role',
|
|
81
|
+
render: (row) => (
|
|
82
|
+
<span className="rounded bg-white-alpha-20 px-2 py-0.5">{row.role}</span>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: 'email',
|
|
87
|
+
header: 'Contact',
|
|
88
|
+
render: (row) => <a href={`mailto:${row.email}`}>{row.email}</a>,
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Async server search + infinite scroll
|
|
94
|
+
|
|
95
|
+
Set `filterClientSide={false}` and treat `rows` as already-filtered server data. `onSearchChange` fires (debounced by `searchDebounceMs`, default 300ms) whenever the query settles — refetch there. As the list nears the bottom, `onLoadMore` fires while `hasMore && !loading`; a loading row renders whenever `loading` is true.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { SearchableTable } from '@torch-ui/components'
|
|
99
|
+
import type { SearchableTableColumn } from '@torch-ui/components'
|
|
100
|
+
import { useEffect, useState } from 'react'
|
|
101
|
+
|
|
102
|
+
type User = { id: string; name: string; role: string; email: string }
|
|
103
|
+
|
|
104
|
+
const columns: SearchableTableColumn<User>[] = [
|
|
105
|
+
{ key: 'name', header: 'Name' },
|
|
106
|
+
{ key: 'role', header: 'Role' },
|
|
107
|
+
{ key: 'email', header: 'Email' },
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
async function fetchUsers(query: string, page: number) {
|
|
111
|
+
const res = await fetch(`/api/users?q=${encodeURIComponent(query)}&page=${page}`)
|
|
112
|
+
return res.json() as Promise<{ items: User[]; hasMore: boolean }>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function AsyncExample() {
|
|
116
|
+
const [rows, setRows] = useState<User[]>([])
|
|
117
|
+
const [query, setQuery] = useState('')
|
|
118
|
+
const [page, setPage] = useState(1)
|
|
119
|
+
const [hasMore, setHasMore] = useState(true)
|
|
120
|
+
const [loading, setLoading] = useState(false)
|
|
121
|
+
const [selected, setSelected] = useState<User | null>(null)
|
|
122
|
+
|
|
123
|
+
// Refetch from page 1 whenever the debounced query changes.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
let cancelled = false
|
|
126
|
+
setLoading(true)
|
|
127
|
+
fetchUsers(query, 1).then(({ items, hasMore }) => {
|
|
128
|
+
if (cancelled) return
|
|
129
|
+
setRows(items)
|
|
130
|
+
setHasMore(hasMore)
|
|
131
|
+
setPage(1)
|
|
132
|
+
setLoading(false)
|
|
133
|
+
})
|
|
134
|
+
return () => {
|
|
135
|
+
cancelled = true
|
|
136
|
+
}
|
|
137
|
+
}, [query])
|
|
138
|
+
|
|
139
|
+
const loadMore = async () => {
|
|
140
|
+
const next = page + 1
|
|
141
|
+
setLoading(true)
|
|
142
|
+
const { items, hasMore } = await fetchUsers(query, next)
|
|
143
|
+
setRows((prev) => [...prev, ...items])
|
|
144
|
+
setHasMore(hasMore)
|
|
145
|
+
setPage(next)
|
|
146
|
+
setLoading(false)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<SearchableTable<User>
|
|
151
|
+
columns={columns}
|
|
152
|
+
rows={rows}
|
|
153
|
+
value={selected}
|
|
154
|
+
onSelect={setSelected}
|
|
155
|
+
getRowId={(row) => row.id}
|
|
156
|
+
getLabel={(row) => row.name}
|
|
157
|
+
filterClientSide={false}
|
|
158
|
+
onSearchChange={setQuery}
|
|
159
|
+
hasMore={hasMore}
|
|
160
|
+
loading={loading}
|
|
161
|
+
onLoadMore={loadMore}
|
|
162
|
+
placeholder="Search users…"
|
|
163
|
+
/>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Capping the visible height
|
|
169
|
+
|
|
170
|
+
`maxVisibleRows` (default `6`) caps how many rows are visible before the dropdown scrolls vertically. The remaining rows stay reachable via scroll, which is also what drives infinite-scroll loading.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
<SearchableTable<User>
|
|
174
|
+
columns={columns}
|
|
175
|
+
rows={users}
|
|
176
|
+
onSelect={setSelected}
|
|
177
|
+
getRowId={(row) => row.id}
|
|
178
|
+
maxVisibleRows={4}
|
|
179
|
+
/>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## API Reference
|
|
183
|
+
|
|
184
|
+
### SearchableTable<T>
|
|
185
|
+
|
|
186
|
+
| Prop | Type | Default | Description |
|
|
187
|
+
|------|------|---------|-------------|
|
|
188
|
+
| `columns` | `SearchableTableColumn<T>[]` | Required | Column config — header, the row key to read, and an optional cell renderer. |
|
|
189
|
+
| `rows` | `T[]` | Required | The data rows. In server mode these are rendered as-is (already filtered upstream). |
|
|
190
|
+
| `value` | `T \| null` | - | Controlled selected row. The matching row is highlighted in the table. |
|
|
191
|
+
| `onSelect` | `(row: T) => void` | - | Called when a row is clicked. The dropdown closes and the input shows the row's label. |
|
|
192
|
+
| `getLabel` | `(row: T) => string` | First column's value | Text shown in the input after selection. |
|
|
193
|
+
| `getRowId` | `(row: T) => string` | `JSON.stringify(row)` | Stable id/key per row, used for keys and selection matching. |
|
|
194
|
+
| `searchKeys` | `(keyof T & string)[]` | Every column `key` | Which fields client-side search matches against. |
|
|
195
|
+
| `placeholder` | `string` | `'Search…'` | Input placeholder text. |
|
|
196
|
+
| `size` | `'XS' \| 'S' \| 'M'` | `'M'` | Input size. (`XS` maps the underlying Group to `S` with a tighter input height.) |
|
|
197
|
+
| `variant` | `'SystemStyle' \| 'PresentationStyle'` | `'PresentationStyle'` | Visual style of the input and dropdown surface. |
|
|
198
|
+
| `icon` | `ReactNode` | - | Optional leading icon rendered inside the input. |
|
|
199
|
+
| `theme` | `'dark' \| 'light' \| 'default'` | - | Theme variant, applied via `data-theme`. |
|
|
200
|
+
| `dir` | `string` | - | Text direction (e.g. `'rtl'`), applied to the input group and dropdown. |
|
|
201
|
+
| `className` | `string` | - | Additional classes merged onto the input group. |
|
|
202
|
+
| `filterClientSide` | `boolean` | `true` | When `true`, filter `rows` locally by `searchKeys`. Set `false` for server-side search. |
|
|
203
|
+
| `onSearchChange` | `(query: string) => void` | - | Debounced query callback — refetch your data here in server mode. |
|
|
204
|
+
| `searchDebounceMs` | `number` | `300` | Debounce delay (ms) for `onSearchChange`. |
|
|
205
|
+
| `hasMore` | `boolean` | `false` | Whether more pages are available; gates the infinite-scroll loader. |
|
|
206
|
+
| `loading` | `boolean` | `false` | Whether a fetch is in flight; renders a loading row and blocks `onLoadMore`. |
|
|
207
|
+
| `onLoadMore` | `() => void` | - | Called when the list nears the bottom and `hasMore && !loading`. |
|
|
208
|
+
| `maxVisibleRows` | `number` | `6` | Max rows visible before the list scrolls vertically. |
|
|
209
|
+
|
|
210
|
+
### SearchableTableColumn<T>
|
|
211
|
+
|
|
212
|
+
| Prop | Type | Default | Description |
|
|
213
|
+
|------|------|---------|-------------|
|
|
214
|
+
| `key` | `keyof T & string` | Required | Property on the row used for default rendering and search. |
|
|
215
|
+
| `header` | `ReactNode` | Required | Column header content. |
|
|
216
|
+
| `render` | `(row: T) => ReactNode` | `String(row[key])` | Custom cell renderer. |
|
|
217
|
+
|
|
218
|
+
## TypeScript
|
|
219
|
+
|
|
220
|
+
### SearchableTableColumn
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
export interface SearchableTableColumn<T> {
|
|
224
|
+
/** Property on the row used for default rendering and search. */
|
|
225
|
+
key: keyof T & string
|
|
226
|
+
header: React.ReactNode
|
|
227
|
+
/** Custom cell renderer; defaults to String(row[key]). */
|
|
228
|
+
render?: (row: T) => React.ReactNode
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Component signature
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { Themes } from '@torch-ui/components'
|
|
236
|
+
|
|
237
|
+
interface SearchableTableProps<T> {
|
|
238
|
+
columns: SearchableTableColumn<T>[]
|
|
239
|
+
rows: T[]
|
|
240
|
+
value?: T | null
|
|
241
|
+
onSelect?: (row: T) => void
|
|
242
|
+
getLabel?: (row: T) => string
|
|
243
|
+
getRowId?: (row: T) => string
|
|
244
|
+
searchKeys?: (keyof T & string)[]
|
|
245
|
+
placeholder?: string
|
|
246
|
+
size?: 'XS' | 'S' | 'M'
|
|
247
|
+
variant?: 'SystemStyle' | 'PresentationStyle'
|
|
248
|
+
icon?: React.ReactNode
|
|
249
|
+
theme?: Themes
|
|
250
|
+
dir?: string
|
|
251
|
+
className?: string
|
|
252
|
+
// Async / backend pagination
|
|
253
|
+
filterClientSide?: boolean
|
|
254
|
+
onSearchChange?: (query: string) => void
|
|
255
|
+
searchDebounceMs?: number
|
|
256
|
+
hasMore?: boolean
|
|
257
|
+
loading?: boolean
|
|
258
|
+
onLoadMore?: () => void
|
|
259
|
+
maxVisibleRows?: number
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Generic function component, constrained so rows are object-shaped:
|
|
263
|
+
export function SearchableTable<T extends Record<string, unknown>>(
|
|
264
|
+
props: SearchableTableProps<T>
|
|
265
|
+
): JSX.Element
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Always pass the type argument explicitly (`<SearchableTable<User> … />`) so `columns`, `value`, and the callbacks are fully type-checked against your row shape.
|
|
269
|
+
|
|
270
|
+
## Common Patterns
|
|
271
|
+
|
|
272
|
+
### React Query — `useInfiniteQuery`
|
|
273
|
+
|
|
274
|
+
Wire pagination through `useInfiniteQuery`, flatten the pages into `rows`, and map the query's state onto `hasMore` / `loading` / `onLoadMore`.
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { SearchableTable } from '@torch-ui/components'
|
|
278
|
+
import type { SearchableTableColumn } from '@torch-ui/components'
|
|
279
|
+
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
280
|
+
import { useState } from 'react'
|
|
281
|
+
|
|
282
|
+
type User = { id: string; name: string; role: string; email: string }
|
|
283
|
+
|
|
284
|
+
const columns: SearchableTableColumn<User>[] = [
|
|
285
|
+
{ key: 'name', header: 'Name' },
|
|
286
|
+
{ key: 'role', header: 'Role' },
|
|
287
|
+
{ key: 'email', header: 'Email' },
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
function UserPicker() {
|
|
291
|
+
const [query, setQuery] = useState('')
|
|
292
|
+
const [selected, setSelected] = useState<User | null>(null)
|
|
293
|
+
|
|
294
|
+
const {
|
|
295
|
+
data,
|
|
296
|
+
fetchNextPage,
|
|
297
|
+
hasNextPage,
|
|
298
|
+
isFetching,
|
|
299
|
+
} = useInfiniteQuery({
|
|
300
|
+
queryKey: ['users', query],
|
|
301
|
+
queryFn: async ({ pageParam = 1 }) => {
|
|
302
|
+
const res = await fetch(`/api/users?q=${query}&page=${pageParam}`)
|
|
303
|
+
// API row -> T mapping happens here.
|
|
304
|
+
const json = await res.json()
|
|
305
|
+
return {
|
|
306
|
+
items: json.results.map(
|
|
307
|
+
(r: any): User => ({
|
|
308
|
+
id: String(r.user_id),
|
|
309
|
+
name: r.full_name,
|
|
310
|
+
role: r.role_title,
|
|
311
|
+
email: r.email_address,
|
|
312
|
+
})
|
|
313
|
+
),
|
|
314
|
+
nextPage: json.has_more ? pageParam + 1 : undefined,
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
initialPageParam: 1,
|
|
318
|
+
getNextPageParam: (last) => last.nextPage,
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const rows = data?.pages.flatMap((p) => p.items) ?? []
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<SearchableTable<User>
|
|
325
|
+
columns={columns}
|
|
326
|
+
rows={rows}
|
|
327
|
+
value={selected}
|
|
328
|
+
onSelect={setSelected}
|
|
329
|
+
getRowId={(row) => row.id}
|
|
330
|
+
getLabel={(row) => row.name}
|
|
331
|
+
filterClientSide={false}
|
|
332
|
+
onSearchChange={setQuery}
|
|
333
|
+
hasMore={hasNextPage}
|
|
334
|
+
loading={isFetching}
|
|
335
|
+
onLoadMore={() => fetchNextPage()}
|
|
336
|
+
/>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### SWR — `useSWRInfinite`
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { SearchableTable } from '@torch-ui/components'
|
|
345
|
+
import useSWRInfinite from 'swr/infinite'
|
|
346
|
+
import { useState } from 'react'
|
|
347
|
+
|
|
348
|
+
const PAGE_SIZE = 20
|
|
349
|
+
|
|
350
|
+
function UserPicker() {
|
|
351
|
+
const [query, setQuery] = useState('')
|
|
352
|
+
const [selected, setSelected] = useState<User | null>(null)
|
|
353
|
+
|
|
354
|
+
const getKey = (index: number, prev: { items: User[] } | null) => {
|
|
355
|
+
if (prev && prev.items.length < PAGE_SIZE) return null // reached the end
|
|
356
|
+
return `/api/users?q=${query}&page=${index + 1}`
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { data, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher)
|
|
360
|
+
|
|
361
|
+
const rows = data?.flatMap((p) => p.items) ?? []
|
|
362
|
+
const hasMore = !!data && data[data.length - 1]?.items.length === PAGE_SIZE
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<SearchableTable<User>
|
|
366
|
+
columns={columns}
|
|
367
|
+
rows={rows}
|
|
368
|
+
value={selected}
|
|
369
|
+
onSelect={setSelected}
|
|
370
|
+
getRowId={(row) => row.id}
|
|
371
|
+
filterClientSide={false}
|
|
372
|
+
onSearchChange={setQuery}
|
|
373
|
+
hasMore={hasMore}
|
|
374
|
+
loading={isValidating}
|
|
375
|
+
onLoadMore={() => setSize(size + 1)}
|
|
376
|
+
/>
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Mapping API rows to `T`
|
|
382
|
+
|
|
383
|
+
Normalize the backend shape into your row type at the fetch boundary so that `columns`, `getLabel`, `getRowId`, and `searchKeys` all operate on consistent, typed fields:
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
function toUser(api: ApiUser): User {
|
|
387
|
+
return {
|
|
388
|
+
id: String(api.user_id),
|
|
389
|
+
name: `${api.first_name} ${api.last_name}`,
|
|
390
|
+
role: api.role ?? '—',
|
|
391
|
+
email: api.email,
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## Accessibility
|
|
397
|
+
|
|
398
|
+
- The trigger is a standard text `<input>`, so it is focusable and typeable by keyboard. Focusing it opens the dropdown and starts a fresh search.
|
|
399
|
+
- The chevron is a boxed icon button with `aria-label` toggling between `"Open"` and `"Close"`; it is `tabIndex={-1}` so keyboard focus stays on the input, and it uses `onMouseDown` with `preventDefault` to avoid the input blur/focus race.
|
|
400
|
+
- Clicking outside the input group or the dropdown closes it (handled via `useClickOutside`).
|
|
401
|
+
- The dropdown surface does not steal focus on open (`onOpenAutoFocus` is prevented), keeping the caret in the input while results update.
|
|
402
|
+
- Provide a descriptive `placeholder` and a meaningful `getLabel` so screen-reader users hear a clear value after selection. Prefer human-readable headers in `columns`.
|
|
403
|
+
|
|
404
|
+
## Best Practices
|
|
405
|
+
|
|
406
|
+
1. **Always supply `getRowId`.** The default key is `JSON.stringify(row)`, which is slow and brittle for large or nested rows. A stable id keeps keys and selection matching cheap.
|
|
407
|
+
2. **Set `getLabel` for the selected display.** Otherwise the input shows the first column's raw value, which may not be the most descriptive field.
|
|
408
|
+
3. **Pick the right search mode.** Keep `filterClientSide` (default) for small, in-memory datasets. Switch to `filterClientSide={false}` + `onSearchChange` for backend-driven search so you never filter a partial page.
|
|
409
|
+
4. **Gate pagination correctly.** `onLoadMore` only fires while `hasMore && !loading` — keep `loading` accurate so a single scroll doesn't trigger duplicate fetches, and flip `hasMore` to `false` on the last page.
|
|
410
|
+
5. **The table scrolls in both directions.** Columns keep their natural width, so wide tables scroll horizontally inside the dropdown, while `maxVisibleRows` caps the vertical height and the rest scrolls vertically (which is what drives infinite scroll). Keep column count and content reasonable so horizontal scroll stays usable.
|
|
411
|
+
6. **Tune `searchDebounceMs` to your backend.** The 300ms default suits most APIs; raise it for slow or rate-limited endpoints.
|
|
412
|
+
7. **Match `searchKeys` to visible columns.** In client mode, search defaults to every column key — narrow `searchKeys` if some columns hold non-searchable data (ids, formatted dates).
|
|
413
|
+
|
|
414
|
+
## Related Components
|
|
415
|
+
|
|
416
|
+
- [SearchableSelect](./searchable-select.md) - Single-column searchable combobox
|
|
417
|
+
- [Table](./table.md) - The table primitive rendered inside the dropdown
|
|
418
|
+
- [DataTable](./data-table.md) - Full-featured data grid for page-level tables
|
|
419
|
+
- [Select](./select.md) - Standard form select field
|
|
@@ -221,8 +221,28 @@ export default {
|
|
|
221
221
|
|
|
222
222
|
### Import CSS Variables
|
|
223
223
|
|
|
224
|
+
`tailwindVars.css` ships the `@theme` block that registers every `--color-*`
|
|
225
|
+
token (badge, action, state, etc.) as a Tailwind utility. Without it, classes
|
|
226
|
+
like `bg-background-presentation-badge-gray-solid` resolve to nothing and
|
|
227
|
+
components render with no background.
|
|
228
|
+
|
|
229
|
+
**Ordering is critical.** CSS requires all `@import` statements to come before
|
|
230
|
+
any other at-rule. Place this `@import` **above** every `@plugin` and `@source`
|
|
231
|
+
rule — directly under `@import "tailwindcss"`. If it sits after them, the
|
|
232
|
+
bundler (Vite/Lightning CSS, PostCSS) treats it as a misplaced `@import` and
|
|
233
|
+
**silently drops it**, removing the entire color registration. The tell-tale
|
|
234
|
+
build warning is `@import must precede all other statements`.
|
|
235
|
+
|
|
236
|
+
```css
|
|
237
|
+
/* app.css or globals.css — Tailwind v4 */
|
|
238
|
+
@import "tailwindcss";
|
|
239
|
+
@import "mapping-color-system-v4/tailwindVars.css"; /* MUST be before @plugin/@source */
|
|
240
|
+
@plugin "glare-torch-mode";
|
|
241
|
+
@plugin "mapping-color-system-v4";
|
|
242
|
+
```
|
|
243
|
+
|
|
224
244
|
```css
|
|
225
|
-
/*
|
|
245
|
+
/* Tailwind v3 */
|
|
226
246
|
@import 'mapping-color-system-v4/tailwindVars.css';
|
|
227
247
|
|
|
228
248
|
@tailwind base;
|
|
@@ -89,14 +89,28 @@ Add the following to your `global.css` file:
|
|
|
89
89
|
|
|
90
90
|
```css
|
|
91
91
|
@import "tailwindcss";
|
|
92
|
+
/* IMPORTANT: this @import MUST come before any @plugin or @source rule.
|
|
93
|
+
CSS requires all @import statements to precede other at-rules, so if it
|
|
94
|
+
is placed after the @plugin lines the bundler (Vite/Lightning CSS,
|
|
95
|
+
PostCSS) silently drops it. That removes every --color-* registration
|
|
96
|
+
this file provides and breaks ALL bg/text/border-*-presentation-* utilities
|
|
97
|
+
(e.g. Badge renders with no background). Keep it directly under
|
|
98
|
+
@import "tailwindcss". */
|
|
99
|
+
@import "mapping-color-system-v4/tailwindVars.css";
|
|
92
100
|
@plugin "glare-torch-mode";
|
|
93
101
|
@plugin "tailwind-scrollbar-hide";
|
|
94
102
|
@plugin "tailwindcss-animate";
|
|
95
103
|
@plugin "glare-typography";
|
|
96
104
|
@plugin "mapping-color-system-v4";
|
|
97
|
-
@import "mapping-color-system-v4/tailwindVars.css";
|
|
98
105
|
```
|
|
99
106
|
|
|
107
|
+
> ⚠️ **Common failure:** if your Badge (or any component using
|
|
108
|
+
> `bg-background-presentation-*` colors) renders with no background, the
|
|
109
|
+
> `tailwindVars.css` `@import` is almost certainly positioned **after** the
|
|
110
|
+
> `@plugin` rules and is being dropped as an invalid `@import`. Move it up,
|
|
111
|
+
> directly below `@import "tailwindcss"`, and rebuild. Look for the build
|
|
112
|
+
> warning `@import must precede all other statements` — that confirms it.
|
|
113
|
+
|
|
100
114
|
### For Tailwind CSS v3
|
|
101
115
|
|
|
102
116
|
First install the mapping color system:
|