@payglocal_ui/flux-ui 0.2.3 → 0.2.5
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/dist/index.cjs +189 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +200 -92
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/data-table.tsx +253 -29
- package/src/dropdown-menu.tsx +4 -1
- package/src/index.ts +2 -1
- package/src/select.tsx +20 -3
package/package.json
CHANGED
package/src/data-table.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "./utils";
|
|
5
5
|
import { TableRowSkeleton } from "./skeleton";
|
|
6
6
|
import { EmptyState } from "./empty-state";
|
|
7
|
-
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
7
|
+
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
|
|
10
10
|
export type DataTableDensity = "default" | "comfortable" | "compact";
|
|
@@ -24,6 +24,38 @@ function getPageRange(current: number, total: number): (number | "…")[] {
|
|
|
24
24
|
return pages;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Row expansion: a disclosure column plus a full-width panel rendered directly
|
|
29
|
+
* beneath the expanded row. Use it when the detail belongs *with* the row in
|
|
30
|
+
* the flow of the table (a request's headers, a payload, a breakdown) rather
|
|
31
|
+
* than in a drawer that covers it.
|
|
32
|
+
*
|
|
33
|
+
* Leave `expandedKeys` unset for uncontrolled behaviour (the table remembers
|
|
34
|
+
* which rows are open). Pass `expandedKeys` + `onExpandedChange` to drive it
|
|
35
|
+
* from outside — needed when opening a row triggers a fetch.
|
|
36
|
+
*/
|
|
37
|
+
export type DataTableExpandable<T> = {
|
|
38
|
+
/** The panel shown under an expanded row. */
|
|
39
|
+
render: (row: T, index: number) => ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Which rows can open at all. Rows that cannot get no toggle and no chevron,
|
|
42
|
+
* keeping the column's width without implying an affordance that isn't there.
|
|
43
|
+
* Defaults to every row.
|
|
44
|
+
*/
|
|
45
|
+
isExpandable?: (row: T, index: number) => boolean;
|
|
46
|
+
/** Controlled open rows, as `rowKey` values. Omit for uncontrolled. */
|
|
47
|
+
expandedKeys?: string[];
|
|
48
|
+
/** Fires on every open/close in controlled mode. */
|
|
49
|
+
onExpandedChange?: (keys: string[]) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Fires only when a row opens, in both modes — the hook for lazily fetching
|
|
52
|
+
* that row's detail. Not called on close.
|
|
53
|
+
*/
|
|
54
|
+
onExpand?: (row: T, index: number) => void;
|
|
55
|
+
/** Accessible name for the toggle. Default "Toggle row details". */
|
|
56
|
+
toggleLabel?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
27
59
|
export type Column<T> = {
|
|
28
60
|
key: string;
|
|
29
61
|
header: ReactNode;
|
|
@@ -72,6 +104,22 @@ interface DataTableProps<T> {
|
|
|
72
104
|
* flush with nothing trailing it. Takes precedence over `rowCta`.
|
|
73
105
|
*/
|
|
74
106
|
rowAction?: ReactNode | ((row: T, index: number) => ReactNode);
|
|
107
|
+
/**
|
|
108
|
+
* Makes the whole row a click target — the row itself opens a drawer, a
|
|
109
|
+
* detail page, whatever the table drills into — instead of that living in a
|
|
110
|
+
* per-cell wrapper or a hover-revealed button.
|
|
111
|
+
*
|
|
112
|
+
* The handler sits on the `<tr>`, so the entire row including cell padding
|
|
113
|
+
* and the empty space between columns is clickable, and the row gets
|
|
114
|
+
* `cursor-pointer` plus keyboard access (focusable, Enter / Space).
|
|
115
|
+
*
|
|
116
|
+
* Clicks that originate inside something interactive — a `<button>`, `<a>`,
|
|
117
|
+
* a form control, a Radix trigger, or anything marked
|
|
118
|
+
* `data-row-click-ignore` — do NOT fire this. Copy buttons, per-row menus
|
|
119
|
+
* and the `rowAction` overlay therefore keep doing only their own job
|
|
120
|
+
* without each having to stop propagation.
|
|
121
|
+
*/
|
|
122
|
+
onRowClick?: (row: T, index: number) => void;
|
|
75
123
|
/** Row / cell vertical rhythm and horizontal gutters */
|
|
76
124
|
density?: DataTableDensity;
|
|
77
125
|
/**
|
|
@@ -93,6 +141,17 @@ interface DataTableProps<T> {
|
|
|
93
141
|
footerCountLabels?: { singular: string; plural: string };
|
|
94
142
|
/** With `density="compact"`, use tighter cell gutters (`pl-1.5 pr-2.5` vs `px-3`). Footer keeps normal horizontal padding. */
|
|
95
143
|
snug?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Extra control at the far left of the built-in footer, before the
|
|
146
|
+
* "Showing x–y of N" summary — a rows-per-page picker, typically.
|
|
147
|
+
*
|
|
148
|
+
* Without it a grid that needs a page-size control has to abandon the
|
|
149
|
+
* built-in footer and hand-roll one, which is how two different pagers end up
|
|
150
|
+
* in the same app.
|
|
151
|
+
*/
|
|
152
|
+
footerLeading?: ReactNode;
|
|
153
|
+
/** Per-row disclosure panel rendered beneath the row. See `DataTableExpandable`. */
|
|
154
|
+
expandable?: DataTableExpandable<T>;
|
|
96
155
|
}
|
|
97
156
|
|
|
98
157
|
export function DataTable<T>({
|
|
@@ -110,6 +169,7 @@ export function DataTable<T>({
|
|
|
110
169
|
rowKey,
|
|
111
170
|
rowCta,
|
|
112
171
|
rowAction,
|
|
172
|
+
onRowClick,
|
|
113
173
|
density = "default",
|
|
114
174
|
tableLayout = "fixed",
|
|
115
175
|
theadClassName,
|
|
@@ -117,6 +177,8 @@ export function DataTable<T>({
|
|
|
117
177
|
footerSummary = "range",
|
|
118
178
|
footerCountLabels = { singular: "item", plural: "items" },
|
|
119
179
|
snug = false,
|
|
180
|
+
expandable,
|
|
181
|
+
footerLeading,
|
|
120
182
|
}: DataTableProps<T>) {
|
|
121
183
|
const isControlled = controlledPage !== undefined;
|
|
122
184
|
const [internalPage, setInternalPage] = useState(1);
|
|
@@ -136,6 +198,31 @@ export function DataTable<T>({
|
|
|
136
198
|
// width while the table still spans the full container width.
|
|
137
199
|
const hasSpacer = tableLayout === "content";
|
|
138
200
|
|
|
201
|
+
// Row expansion. Uncontrolled by default; `expandedKeys` hands control to the
|
|
202
|
+
// caller, which is what a row whose panel fetches its own data needs.
|
|
203
|
+
// `onExpand` fires only on open, in both modes.
|
|
204
|
+
const [internalExpanded, setInternalExpanded] = useState<string[]>([]);
|
|
205
|
+
const isExpandControlled = expandable?.expandedKeys !== undefined;
|
|
206
|
+
const expandedKeys = isExpandControlled ? expandable!.expandedKeys! : internalExpanded;
|
|
207
|
+
const hasExpand = expandable != null;
|
|
208
|
+
/** Every column the expansion panel has to span. */
|
|
209
|
+
const totalColSpan =
|
|
210
|
+
columns.length + (hasExpand ? 1 : 0) + (hasSpacer ? 1 : 0) + (hasAction ? 1 : 0);
|
|
211
|
+
|
|
212
|
+
/** Whether this row is open — drives both its own styling and the panel. */
|
|
213
|
+
const rowExpanded = (row: T, index: number) =>
|
|
214
|
+
hasExpand &&
|
|
215
|
+
(expandable!.isExpandable?.(row, index) ?? true) &&
|
|
216
|
+
expandedKeys.includes(rowKeys[index]);
|
|
217
|
+
|
|
218
|
+
const toggleExpanded = (key: string, row: T, index: number) => {
|
|
219
|
+
const isOpen = expandedKeys.includes(key);
|
|
220
|
+
const next = isOpen ? expandedKeys.filter((k) => k !== key) : [...expandedKeys, key];
|
|
221
|
+
if (isExpandControlled) expandable!.onExpandedChange?.(next);
|
|
222
|
+
else setInternalExpanded(next);
|
|
223
|
+
if (!isOpen) expandable!.onExpand?.(row, index);
|
|
224
|
+
};
|
|
225
|
+
|
|
139
226
|
const total = totalRows ?? data.length;
|
|
140
227
|
const totalPages = Math.ceil(total / pageSize);
|
|
141
228
|
const paginated = isControlled ? data : data.slice((page - 1) * pageSize, page * pageSize);
|
|
@@ -172,10 +259,51 @@ export function DataTable<T>({
|
|
|
172
259
|
: compact
|
|
173
260
|
? "text-[11px] font-semibold text-muted-foreground"
|
|
174
261
|
: "text-[11px] font-semibold text-foreground/75 dark:text-foreground/85";
|
|
262
|
+
/**
|
|
263
|
+
* Expansion geometry, in px, derived from the same padding the cells use:
|
|
264
|
+
*
|
|
265
|
+
* - `expandIndent` lines the panel's content up with the first DATA column
|
|
266
|
+
* (past the 40px disclosure column), so the detail reads as hanging off the
|
|
267
|
+
* row rather than starting outside it.
|
|
268
|
+
* - `expandGuideLeft` is the centre of the chevron, where the vertical
|
|
269
|
+
* connector runs — the cue that the panel belongs to the row above.
|
|
270
|
+
*
|
|
271
|
+
* Both live here rather than in each consumer's panel: a hardcoded indent in
|
|
272
|
+
* a feature silently drifts the moment this padding or the column width
|
|
273
|
+
* changes.
|
|
274
|
+
*/
|
|
275
|
+
const cellPadLeft = comfortable ? 20 : compact ? (snug ? 6 : 12) : 16;
|
|
276
|
+
const expandIndent = 40 + cellPadLeft;
|
|
277
|
+
const expandGuideLeft = cellPadLeft + 10;
|
|
278
|
+
|
|
175
279
|
// Action overlay geometry: the action floats this many px in from the right
|
|
176
280
|
// edge of the viewport (it has no reserved column — it overlays the row).
|
|
177
281
|
const actionGutter = comfortable ? 20 : compact ? 12 : 16;
|
|
178
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Selector for everything a row-level click must keep its hands off. A click
|
|
285
|
+
* landing inside one of these belongs to that control alone — a copy button,
|
|
286
|
+
* a per-row menu, a link, a checkbox, the `rowAction` overlay — so the row
|
|
287
|
+
* handler ignores it rather than firing as well.
|
|
288
|
+
*
|
|
289
|
+
* `data-row-click-ignore` is the escape hatch for anything not covered here
|
|
290
|
+
* (a custom widget in a cell, a drag handle) without it needing to stop
|
|
291
|
+
* propagation itself.
|
|
292
|
+
*/
|
|
293
|
+
const ROW_CLICK_IGNORE =
|
|
294
|
+
'button, a, input, select, textarea, label, [role="button"], [role="link"], ' +
|
|
295
|
+
'[role="checkbox"], [role="menuitem"], [role="menu"], [role="dialog"], ' +
|
|
296
|
+
"[data-row-click-ignore]";
|
|
297
|
+
|
|
298
|
+
/** Whether a click/keypress inside a row should reach `onRowClick`. */
|
|
299
|
+
const isRowClickTarget = (target: EventTarget | null, rowEl: HTMLElement) => {
|
|
300
|
+
if (!(target instanceof Element)) return false;
|
|
301
|
+
const interactive = target.closest(ROW_CLICK_IGNORE);
|
|
302
|
+
// `closest` can walk out of the row entirely (a portalled menu, say); only
|
|
303
|
+
// a match inside THIS row means the click belonged to that control.
|
|
304
|
+
return !(interactive && rowEl.contains(interactive));
|
|
305
|
+
};
|
|
306
|
+
|
|
179
307
|
return (
|
|
180
308
|
<div
|
|
181
309
|
className={cn(
|
|
@@ -205,6 +333,7 @@ export function DataTable<T>({
|
|
|
205
333
|
>
|
|
206
334
|
{tableLayout === "fixed" && (
|
|
207
335
|
<colgroup>
|
|
336
|
+
{hasExpand ? <col style={{ width: 40 }} /> : null}
|
|
208
337
|
{columns.map((col) => (
|
|
209
338
|
<col
|
|
210
339
|
key={col.key}
|
|
@@ -234,6 +363,7 @@ export function DataTable<T>({
|
|
|
234
363
|
headerStyle === "surface" ? "border-border" : "border-border/70"
|
|
235
364
|
)}
|
|
236
365
|
>
|
|
366
|
+
{hasExpand ? <th className={cn(headPad, "w-10 p-0")} aria-hidden /> : null}
|
|
237
367
|
{columns.map((col) => (
|
|
238
368
|
<th
|
|
239
369
|
key={col.key}
|
|
@@ -264,19 +394,19 @@ export function DataTable<T>({
|
|
|
264
394
|
Array.from({ length: skeletonRows }).map((_, i) => (
|
|
265
395
|
<TableRowSkeleton
|
|
266
396
|
key={i}
|
|
267
|
-
cols={columns.length}
|
|
397
|
+
cols={columns.length + (hasExpand ? 1 : 0)}
|
|
268
398
|
density={density}
|
|
269
399
|
snug={snug}
|
|
270
400
|
/>
|
|
271
401
|
))
|
|
272
402
|
) : paginated.length === 0 ? (
|
|
273
403
|
<tr>
|
|
274
|
-
<td colSpan={
|
|
404
|
+
<td colSpan={totalColSpan}>
|
|
275
405
|
<EmptyState title={emptyTitle} description={emptyDescription} />
|
|
276
406
|
</td>
|
|
277
407
|
</tr>
|
|
278
408
|
) : (
|
|
279
|
-
paginated.
|
|
409
|
+
paginated.flatMap((row, i) => [
|
|
280
410
|
<tr
|
|
281
411
|
key={rowKeys[i]}
|
|
282
412
|
className={cn(
|
|
@@ -285,9 +415,67 @@ export function DataTable<T>({
|
|
|
285
415
|
compact && "min-h-[44px]",
|
|
286
416
|
"hover:bg-muted/40 dark:hover:bg-muted/25",
|
|
287
417
|
hasAction &&
|
|
288
|
-
"hover:shadow-[0_1px_0_rgba(0,0,0,0.04)] dark:hover:shadow-none"
|
|
418
|
+
"hover:shadow-[0_1px_0_rgba(0,0,0,0.04)] dark:hover:shadow-none",
|
|
419
|
+
// Clickable rows read as clickable, and show a focus ring
|
|
420
|
+
// when reached by keyboard. `focus-visible` only, so a
|
|
421
|
+
// mouse click does not leave a ring behind on the row.
|
|
422
|
+
onRowClick &&
|
|
423
|
+
"cursor-pointer focus-visible:outline-none focus-visible:bg-muted/40 dark:focus-visible:bg-muted/25",
|
|
424
|
+
// An open row takes its panel's background and drops the
|
|
425
|
+
// divider beneath it, so the row and its detail read as one
|
|
426
|
+
// block. Hover is pinned to the same value, or moving the
|
|
427
|
+
// mouse over an open row would make it flicker away from
|
|
428
|
+
// the panel it belongs to.
|
|
429
|
+
rowExpanded(row, i) &&
|
|
430
|
+
"border-b-0 bg-muted/40 hover:bg-muted/40 dark:bg-muted/25 dark:hover:bg-muted/25"
|
|
289
431
|
)}
|
|
432
|
+
// Keyboard parity with the mouse: the row is reachable by
|
|
433
|
+
// Tab and activated by Enter / Space, which a bare <tr> with
|
|
434
|
+
// an onClick would not be. No role override — the row stays a
|
|
435
|
+
// row for assistive tech rather than claiming to be a button.
|
|
436
|
+
tabIndex={onRowClick ? 0 : undefined}
|
|
437
|
+
onClick={
|
|
438
|
+
onRowClick
|
|
439
|
+
? (e) => {
|
|
440
|
+
if (!isRowClickTarget(e.target, e.currentTarget)) return;
|
|
441
|
+
onRowClick(row, i);
|
|
442
|
+
}
|
|
443
|
+
: undefined
|
|
444
|
+
}
|
|
445
|
+
onKeyDown={
|
|
446
|
+
onRowClick
|
|
447
|
+
? (e) => {
|
|
448
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
449
|
+
// Only the row's own focus activates it; a keypress
|
|
450
|
+
// inside a control in the row belongs to that control.
|
|
451
|
+
if (e.target !== e.currentTarget) return;
|
|
452
|
+
// Space scrolls the page by default.
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
onRowClick(row, i);
|
|
455
|
+
}
|
|
456
|
+
: undefined
|
|
457
|
+
}
|
|
290
458
|
>
|
|
459
|
+
{hasExpand ? (
|
|
460
|
+
<td className={cn(cellPad, "w-10 align-middle")}>
|
|
461
|
+
{(expandable!.isExpandable?.(row, i) ?? true) ? (
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
aria-expanded={rowExpanded(row, i)}
|
|
465
|
+
aria-label={expandable!.toggleLabel ?? "Toggle row details"}
|
|
466
|
+
onClick={() => toggleExpanded(rowKeys[i], row, i)}
|
|
467
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
468
|
+
>
|
|
469
|
+
<ChevronDown
|
|
470
|
+
className={cn(
|
|
471
|
+
"h-3.5 w-3.5 transition-transform duration-150",
|
|
472
|
+
rowExpanded(row, i) && "rotate-180"
|
|
473
|
+
)}
|
|
474
|
+
/>
|
|
475
|
+
</button>
|
|
476
|
+
) : null}
|
|
477
|
+
</td>
|
|
478
|
+
) : null}
|
|
291
479
|
{columns.map((col) => (
|
|
292
480
|
<td
|
|
293
481
|
key={col.key}
|
|
@@ -358,8 +546,38 @@ export function DataTable<T>({
|
|
|
358
546
|
</span>
|
|
359
547
|
</td>
|
|
360
548
|
) : null}
|
|
361
|
-
</tr
|
|
362
|
-
|
|
549
|
+
</tr>,
|
|
550
|
+
|
|
551
|
+
// The panel spans every column, including the toggle, spacer and
|
|
552
|
+
// action cells, so it reads as one band under its row rather
|
|
553
|
+
// than as a cell inside the grid.
|
|
554
|
+
rowExpanded(row, i) ? (
|
|
555
|
+
<tr
|
|
556
|
+
key={`${rowKeys[i]}__panel`}
|
|
557
|
+
className="border-b border-border/60 bg-muted/40 last:border-b-0 dark:bg-muted/25"
|
|
558
|
+
>
|
|
559
|
+
<td colSpan={totalColSpan} className="p-0 align-top">
|
|
560
|
+
<div
|
|
561
|
+
className="relative"
|
|
562
|
+
style={{ paddingLeft: expandIndent, paddingRight: cellPadLeft }}
|
|
563
|
+
>
|
|
564
|
+
{/* Connector: a hairline dropping from the chevron down
|
|
565
|
+
the panel. The row and its detail share a background
|
|
566
|
+
so they read as one block, which on its own leaves
|
|
567
|
+
nothing to say the lower half is derived rather than
|
|
568
|
+
more row content — this is that cue. Stops short of
|
|
569
|
+
the bottom so it reads as hanging, not as a border. */}
|
|
570
|
+
<span
|
|
571
|
+
aria-hidden
|
|
572
|
+
className="absolute top-0 bottom-4 w-px bg-border"
|
|
573
|
+
style={{ left: expandGuideLeft }}
|
|
574
|
+
/>
|
|
575
|
+
{expandable!.render(row, i)}
|
|
576
|
+
</div>
|
|
577
|
+
</td>
|
|
578
|
+
</tr>
|
|
579
|
+
) : null,
|
|
580
|
+
])
|
|
363
581
|
)}
|
|
364
582
|
</tbody>
|
|
365
583
|
</table>
|
|
@@ -370,32 +588,38 @@ export function DataTable<T>({
|
|
|
370
588
|
className={cn(
|
|
371
589
|
"flex items-center gap-4 flex-wrap border-t border-border",
|
|
372
590
|
footerPad,
|
|
373
|
-
footerSummary === "count" && totalPages <= 1
|
|
591
|
+
footerSummary === "count" && totalPages <= 1 && !footerLeading
|
|
374
592
|
? "justify-start"
|
|
375
593
|
: "justify-between"
|
|
376
594
|
)}
|
|
377
595
|
>
|
|
378
|
-
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
596
|
+
{/* `footerLeading` (a rows-per-page picker, typically) groups with the
|
|
597
|
+
summary on the left rather than becoming a third item the
|
|
598
|
+
justify-between would fling to its own corner. */}
|
|
599
|
+
<div className="flex items-center gap-3">
|
|
600
|
+
{footerLeading}
|
|
601
|
+
{footerSummary === "count" ? (
|
|
602
|
+
<span className="text-[12px] text-muted-foreground tabular-nums">
|
|
603
|
+
<span className="font-medium text-foreground">{total}</span>{" "}
|
|
604
|
+
{total === 1
|
|
605
|
+
? footerCountLabels.singular
|
|
606
|
+
: footerCountLabels.plural}
|
|
607
|
+
</span>
|
|
608
|
+
) : (
|
|
609
|
+
<span className="text-[12px] text-muted-foreground tabular-nums">
|
|
610
|
+
Showing{" "}
|
|
611
|
+
<span className="text-foreground font-medium">
|
|
612
|
+
{Math.min((page - 1) * pageSize + 1, total)}–
|
|
613
|
+
{Math.min(page * pageSize, total)}
|
|
614
|
+
</span>{" "}
|
|
615
|
+
of{" "}
|
|
616
|
+
<span className="text-foreground font-medium">
|
|
617
|
+
{total.toLocaleString()}
|
|
618
|
+
</span>{" "}
|
|
619
|
+
{total !== 1 ? "results" : "result"}
|
|
620
|
+
</span>
|
|
621
|
+
)}
|
|
622
|
+
</div>
|
|
399
623
|
|
|
400
624
|
{(footerSummary === "range" || footerSummary === "count") &&
|
|
401
625
|
totalPages > 1 && (
|
package/src/dropdown-menu.tsx
CHANGED
|
@@ -74,7 +74,10 @@ const DropdownMenuItem = React.forwardRef<
|
|
|
74
74
|
<DropdownMenuPrimitive.Item
|
|
75
75
|
ref={ref}
|
|
76
76
|
className={cn(
|
|
77
|
-
|
|
77
|
+
// 13px / tighter padding: a menu drops out of a toolbar button or a
|
|
78
|
+
// compact table control, and at 15px with 10px vertical padding it read
|
|
79
|
+
// as a larger, separate UI than the control that opened it.
|
|
80
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-lg px-2.5 py-1.5 text-[13px] outline-none transition-colors",
|
|
78
81
|
"focus:bg-muted focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
79
82
|
inset && "pl-8",
|
|
80
83
|
className
|
package/src/index.ts
CHANGED
|
@@ -194,6 +194,7 @@ export {
|
|
|
194
194
|
SelectScrollUpButton,
|
|
195
195
|
SelectScrollDownButton,
|
|
196
196
|
} from "./select";
|
|
197
|
+
export type { SelectTriggerSize } from "./select";
|
|
197
198
|
export { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator, CommandShortcut } from "./command";
|
|
198
199
|
export type {
|
|
199
200
|
CommandProps,
|
|
@@ -232,7 +233,7 @@ export { Shimmer, StatCardSkeleton, TableRowSkeleton, ChartSkeleton } from "./sk
|
|
|
232
233
|
|
|
233
234
|
// Data display
|
|
234
235
|
export { DataTable } from "./data-table";
|
|
235
|
-
export type { Column, DataTableDensity, DataTableFooterSummary, DataTableHeaderStyle } from "./data-table";
|
|
236
|
+
export type { Column, DataTableDensity, DataTableExpandable, DataTableFooterSummary, DataTableHeaderStyle } from "./data-table";
|
|
236
237
|
export { EmptyState } from "./empty-state";
|
|
237
238
|
export { PageHeader } from "./page-header";
|
|
238
239
|
export { Code, CodeBlock } from "./code";
|
package/src/select.tsx
CHANGED
|
@@ -9,14 +9,31 @@ const Select = SelectPrimitive.Root;
|
|
|
9
9
|
const SelectGroup = SelectPrimitive.Group;
|
|
10
10
|
const SelectValue = SelectPrimitive.Value;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* `md` (default) is the form-field trigger. `sm` is for dense furniture — a
|
|
14
|
+
* rows-per-page picker in a table footer, a control inside a toolbar — where
|
|
15
|
+
* the full-height field towers over everything beside it.
|
|
16
|
+
*
|
|
17
|
+
* This is a size prop rather than a job for `className` because the base sets
|
|
18
|
+
* `min-h-11`, which beats an `h-8` utility: every caller wanting a short
|
|
19
|
+
* trigger had to override min-height, padding, gap and text size together.
|
|
20
|
+
*/
|
|
21
|
+
export type SelectTriggerSize = "sm" | "md";
|
|
22
|
+
|
|
23
|
+
const selectTriggerSizes: Record<SelectTriggerSize, string> = {
|
|
24
|
+
md: "h-11 min-h-11 gap-2.5 px-4 py-2 text-[15px]",
|
|
25
|
+
sm: "h-7 min-h-7 w-auto gap-1 px-2 py-0 text-[12px]",
|
|
26
|
+
};
|
|
27
|
+
|
|
12
28
|
const SelectTrigger = React.forwardRef<
|
|
13
29
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
14
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
15
|
-
>(({ className, children, ...props }, ref) => (
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & { size?: SelectTriggerSize }
|
|
31
|
+
>(({ className, children, size = "md", ...props }, ref) => (
|
|
16
32
|
<SelectPrimitive.Trigger
|
|
17
33
|
ref={ref}
|
|
18
34
|
className={cn(
|
|
19
|
-
"flex
|
|
35
|
+
"flex w-full items-center justify-between rounded-lg border border-border bg-card text-foreground shadow-sm outline-none",
|
|
36
|
+
selectTriggerSizes[size],
|
|
20
37
|
"ring-ring/50 focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
21
38
|
"[&>span]:line-clamp-1",
|
|
22
39
|
className
|