@vendure-io/ui 2.0.0-beta.10 → 2.0.0-beta.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vendure-io/ui",
3
- "version": "2.0.0-beta.10",
3
+ "version": "2.0.0-beta.12",
4
4
  "description": "React component library for Vendure, built on shadcn/ui and Tailwind v4",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "homepage": "https://github.com/vendurehq/design/tree/main/packages/ui",
@@ -2,6 +2,12 @@ import * as React from "react"
2
2
 
3
3
  import { cn } from "@vendure-io/ui/lib/utils"
4
4
 
5
+ // The size variant publishes the card's spacing contract as CSS variables
6
+ // (--card-px: horizontal content padding, --card-gap: vertical rhythm shared
7
+ // by the root padding and the flex gap). Slots consume the variables instead
8
+ // of `group-data-[size=sm]` hooks so that nested cards resolve to the NEAREST
9
+ // card — a group selector matches any ancestor and leaks the outer card's
10
+ // size into an inner one.
5
11
  function Card({
6
12
  className,
7
13
  size = "default",
@@ -12,7 +18,7 @@ function Card({
12
18
  data-slot="card"
13
19
  data-size={size}
14
20
  className={cn(
15
- "bg-card text-card-foreground group/card flex flex-col gap-6 overflow-hidden rounded-xl border border-border/60 py-6 text-sm has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
21
+ "bg-card text-card-foreground group/card [--card-gap:--spacing(6)] [--card-px:--spacing(6)] data-[size=sm]:[--card-gap:--spacing(4)] data-[size=sm]:[--card-px:--spacing(4)] flex flex-col gap-(--card-gap) overflow-hidden rounded-xl border border-border/60 py-(--card-gap) text-sm has-[>img:first-child]:pt-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
22
  className
17
23
  )}
18
24
  {...props}
@@ -20,12 +26,15 @@ function Card({
20
26
  )
21
27
  }
22
28
 
29
+ // Band dividers (consumer-applied border-b / border-t on header/footer) render
30
+ // at row-separator strength (border/50) so the card outline (border/60) stays
31
+ // the strongest structural line.
23
32
  function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
33
  return (
25
34
  <div
26
35
  data-slot="card-header"
27
36
  className={cn(
28
- "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
37
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-px) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:border-border/50 [.border-b]:pb-(--card-gap)",
29
38
  className
30
39
  )}
31
40
  {...props}
@@ -73,7 +82,32 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
82
  return (
74
83
  <div
75
84
  data-slot="card-content"
76
- className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
85
+ className={cn("px-(--card-px)", className)}
86
+ {...props}
87
+ />
88
+ )
89
+ }
90
+
91
+ // Hosts a Table full-bleed inside the card: negative vertical margins cancel
92
+ // the card's flex gap against a border-b header / border-t footer, and cancel
93
+ // the card's own padding when the table is the first/last child, so rows and
94
+ // band dividers run edge to edge (the card's overflow-hidden clips the
95
+ // corners). Edge cells get the card's content padding so cell content aligns
96
+ // with CardHeader/CardFooter content. Two contract notes:
97
+ // - The gap cancellation is unconditional: an adjacent CardHeader/CardFooter
98
+ // must carry its band border (border-b / border-t), otherwise the table
99
+ // sits directly against the band's content.
100
+ // - The edge-cell selector ((0,2,1)) out-specifies a plain padding utility on
101
+ // the cell ((0,1,0)): a `pl-*`/`pr-*` on a first/last cell silently loses —
102
+ // override with the important modifier (`pl-0!`) when a cell must opt out.
103
+ function CardTable({ className, ...props }: React.ComponentProps<"div">) {
104
+ return (
105
+ <div
106
+ data-slot="card-table"
107
+ className={cn(
108
+ "-my-(--card-gap) [&_tr>*:first-child]:pl-(--card-px) [&_tr>*:last-child]:pr-(--card-px)",
109
+ className
110
+ )}
77
111
  {...props}
78
112
  />
79
113
  )
@@ -84,7 +118,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
84
118
  <div
85
119
  data-slot="card-footer"
86
120
  className={cn(
87
- "flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
121
+ "flex items-center rounded-b-xl px-(--card-px) [.border-t]:border-border/50 [.border-t]:pt-(--card-gap)",
88
122
  className
89
123
  )}
90
124
  {...props}
@@ -100,4 +134,5 @@ export {
100
134
  CardAction,
101
135
  CardDescription,
102
136
  CardContent,
137
+ CardTable,
103
138
  }
@@ -9,11 +9,12 @@ import type { DataTableBulkActionContext } from '@vendure-io/ui/components/molec
9
9
  import { cn } from '@vendure-io/ui/lib/utils';
10
10
  import * as React from 'react';
11
11
 
12
- // The selection overlay that replaces the controls row while rows are selected.
13
- // The cross-page selection cache lives here (donor parity): `rows` only ever
14
- // holds the current page, so we accumulate `row.original` by id as pages are
15
- // seen, letting `bulkActions` receive every selected original not just the
16
- // ones still on screen.
12
+ // The selection bar that replaces the controls row inside the header band
13
+ // while rows are selected (returns null with an empty selection; the core
14
+ // decides where it renders). The cross-page selection cache lives here (donor
15
+ // parity): `rows` only ever holds the current page, so we accumulate
16
+ // `row.original` by id as pages are seen, letting `bulkActions` receive every
17
+ // selected original — not just the ones still on screen.
17
18
 
18
19
  /**
19
20
  * Accumulate `row.original` keyed by row id across page changes. Populated during
@@ -53,12 +54,12 @@ function DataTableBulkActions<TData>({
53
54
  };
54
55
 
55
56
  return (
57
+ // A plain band row, not a box: it takes the controls row's place inside
58
+ // the header band, so it carries no chrome of its own. min-h-9 matches
59
+ // the controls row's h-9 inputs so the swap holds the band height steady.
56
60
  <div
57
61
  data-slot="data-table-bulk-actions"
58
- className={cn(
59
- 'bg-muted/50 flex flex-wrap items-center gap-2 rounded-md border px-3 py-2',
60
- className,
61
- )}
62
+ className={cn('flex min-h-9 flex-wrap items-center gap-2', className)}
62
63
  {...props}
63
64
  >
64
65
  {render(ctx)}
@@ -182,7 +182,11 @@ export interface DataTableProps<TData> {
182
182
  header?: React.ReactNode;
183
183
  /** Controls row (search, faceted filters, custom buttons). Render-prop gets the live table. */
184
184
  toolbar?: React.ReactNode | ((table: Table<TData>) => React.ReactNode);
185
- /** Overlay content when selection is non-empty. Only meaningful with `rowSelection` wired. */
185
+ /**
186
+ * Replaces the controls row inside the header band while selection is
187
+ * non-empty (title and applied-filter chips stay). Only meaningful with
188
+ * `rowSelection` wired.
189
+ */
186
190
  bulkActions?: (ctx: DataTableBulkActionContext<TData>) => React.ReactNode;
187
191
  /**
188
192
  * Per-row actions → appends an actions column. First arg is `row.original`
@@ -225,8 +229,31 @@ export interface DataTableProps<TData> {
225
229
  */
226
230
  setTableOptions?: (options: TableOptions<TData>) => TableOptions<TData>;
227
231
 
232
+ /**
233
+ * The table's frame. `'card'` (default) renders the band anatomy on a real
234
+ * `Card`: a `CardHeader` (border-b) hosting the header/controls/chips zones,
235
+ * `CardTable` hosting the table flush to the card edges, and a `CardFooter`
236
+ * (border-t) hosting pagination when it is wired — no footer means the last
237
+ * row sits flush against the card's bottom edge. `'plain'` renders the same
238
+ * band structure without any card chrome, for a table embedded in an
239
+ * existing card (e.g. a dashboard widget): it MUST sit inside a `Card`,
240
+ * whose spacing variables (`--card-px`/`--card-gap`) drive the bands and
241
+ * edge-cell alignment — the host card is the frame.
242
+ */
243
+ frame?: 'card' | 'plain';
244
+ /**
245
+ * Extra `<TableRow>`s appended inside `TableBody` after the data rows — e.g.
246
+ * an order table's subtotal/shipping/total rows. Rendered only alongside
247
+ * real rows, never with the skeleton or empty states, and has no interaction
248
+ * with selection, sorting, or pagination. Use the function form to receive
249
+ * `columnCount` for `colSpan`s — display columns (select, actions) are
250
+ * injected around your columns, so a hard-coded count desyncs. Rows are
251
+ * presentational: give them `hover:bg-transparent`.
252
+ */
253
+ footerRows?: React.ReactNode | ((ctx: { columnCount: number }) => React.ReactNode);
254
+
228
255
  labels?: DataTableLabels;
229
- className?: string; // outer div[data-slot="data-table"]
256
+ className?: string; // the frame root (data-slot="data-table")
230
257
  }
231
258
 
232
259
  /**
@@ -19,6 +19,7 @@ import {
19
19
  useReactTable,
20
20
  type VisibilityState,
21
21
  } from '@tanstack/react-table';
22
+ import { Card, CardFooter, CardHeader, CardTable } from '@vendure-io/ui/components/atoms/card';
22
23
  import { Checkbox } from '@vendure-io/ui/components/atoms/checkbox';
23
24
  import {
24
25
  ContextMenu,
@@ -45,6 +46,7 @@ import {
45
46
  } from '@vendure-io/ui/components/molecules/data-table/data-table-filters';
46
47
  import {
47
48
  buildDisplayColumns,
49
+ getSelectedRowIds,
48
50
  resolveSlot,
49
51
  } from '@vendure-io/ui/components/molecules/data-table/data-table-helpers';
50
52
  import type {
@@ -59,13 +61,17 @@ import {
59
61
  } from '@vendure-io/ui/components/molecules/data-table/list-header';
60
62
  import { TablePagination } from '@vendure-io/ui/components/molecules/data-table/table-pagination';
61
63
  import { cn } from '@vendure-io/ui/lib/utils';
64
+ import { AnimatePresence, motion } from 'motion/react';
62
65
  import * as React from 'react';
63
66
 
64
67
  // The composition root. Owns the single `useReactTable` instance and the
65
- // controlled/uncontrolled bridge, then lays out header controls → chips →
66
- // bulk overlayTablefooter. Every capability follows the Phase-1 rule
67
- // verbatim: a feature renders only if its config is wired no feature flags, no
68
- // disabled placeholders. Only this folder imports TanStack; the composed
68
+ // controlled/uncontrolled bridge, then lays out the card-framed table anatomy:
69
+ // a CardHeader band (header controlschips bulk overlay), a CardTable
70
+ // hosting the Table flush to the card edges, and a CardFooter band for
71
+ // pagination. `frame="plain"` keeps the band structure without the card
72
+ // chrome. Every capability follows the Phase-1 rule verbatim: a feature
73
+ // renders only if its config is wired — no feature flags, no disabled
74
+ // placeholders. Only this folder imports TanStack; the composed
69
75
  // ListHeader/TablePagination/Chip primitives stay TanStack-free.
70
76
 
71
77
  /**
@@ -98,6 +104,16 @@ function headerLabelText<TData>(column: Column<TData, unknown>): string {
98
104
  return typeof header === 'string' ? header : column.id;
99
105
  }
100
106
 
107
+ // The controls/bulk replace-on-select swap: the outgoing row drifts slightly
108
+ // up as it fades, the incoming row rises in from below — a subtle vertical
109
+ // hand-off (4px) rather than a plain crossfade.
110
+ const bandRowFade = {
111
+ initial: { opacity: 0, y: 4 },
112
+ animate: { opacity: 1, y: 0 },
113
+ exit: { opacity: 0, y: -4 },
114
+ transition: { duration: 0.1, ease: 'easeOut' },
115
+ } as const;
116
+
101
117
  function ariaSort<TData>(
102
118
  hasSorting: boolean,
103
119
  column: Column<TData, unknown>,
@@ -126,6 +142,8 @@ function DataTable<TData>({
126
142
  emptyState,
127
143
  renderRow,
128
144
  setTableOptions,
145
+ frame = 'card',
146
+ footerRows,
129
147
  labels,
130
148
  className,
131
149
  }: DataTableProps<TData>) {
@@ -307,7 +325,11 @@ function DataTable<TData>({
307
325
  ? table.getVisibleLeafColumns().find((column) => column.id !== selectColumnId)?.id
308
326
  : undefined;
309
327
  const columnClass = (id: string): string | undefined => {
310
- if (id === selectColumnId) return 'relative w-0 p-0';
328
+ // `pl-0!`: the select cell is the first child, so CardTable's edge-cell
329
+ // padding would otherwise widen the zero-width cell and push the whole
330
+ // gutter arrangement out; important because CardTable's descendant rule
331
+ // out-specifies a plain utility on the cell.
332
+ if (id === selectColumnId) return 'relative w-0 p-0 pl-0!';
311
333
  if (id === leadingColumnId) return 'pl-8';
312
334
  // Actions cells drop vertical padding so a row-action button (taller than a
313
335
  // text line) sits inside the natural row height rather than inflating every
@@ -327,47 +349,76 @@ function DataTable<TData>({
327
349
  filters.showAppliedFilters !== false &&
328
350
  table.getState().columnFilters.length > 0;
329
351
  const showHeader = header != null || showControls || showChips;
330
- const showBulk = rowSelection != null && bulkActions != null;
352
+ // Unlike the other zones, the bulk bar exists only while rows are selected,
353
+ // so it opens the header band on its own only then — a controls-less
354
+ // selectable table must not render an empty band. (Deliberate trade-off:
355
+ // for a table with no header content at all, the band pops in on first
356
+ // selection rather than reserving empty space.)
357
+ const showBulk =
358
+ rowSelection != null &&
359
+ bulkActions != null &&
360
+ getSelectedRowIds(table.getState().rowSelection).length > 0;
331
361
 
332
- return (
333
- <div data-slot="data-table" className={cn('flex flex-col gap-4', className)}>
334
- {showHeader && (
335
- <ListHeader>
336
- {header}
337
- {showControls && (
338
- <ListHeaderControls>
339
- {toolbarNode}
340
- {hasFilterMenu && filters?.columns && (
341
- <DataTableAddFilter table={table} columns={filters.columns} label={l.addFilter} />
342
- )}
343
- {showViewOptions && (
344
- <DataTableViewOptions
362
+ const bands = (
363
+ <>
364
+ {(showHeader || showBulk) && (
365
+ <CardHeader className="border-b">
366
+ <ListHeader>
367
+ {header}
368
+ {/* Replace-on-select: while rows are selected the bulk bar takes
369
+ the controls row's place (title and applied-filter chips stay).
370
+ The swap fades out then in — one --transition-duration-fast
371
+ (100ms) per phase; mode="wait" keeps the rows sequential so
372
+ they never stack. */}
373
+ {(showControls || (rowSelection != null && bulkActions != null)) && (
374
+ <AnimatePresence initial={false} mode="wait">
375
+ {showBulk && bulkActions ? (
376
+ <motion.div key="bulk-actions" {...bandRowFade}>
377
+ <DataTableBulkActions
378
+ table={table}
379
+ cache={selectionCache}
380
+ render={bulkActions}
381
+ />
382
+ </motion.div>
383
+ ) : showControls ? (
384
+ <motion.div key="controls" {...bandRowFade}>
385
+ <ListHeaderControls>
386
+ {toolbarNode}
387
+ {hasFilterMenu && filters?.columns && (
388
+ <DataTableAddFilter
389
+ table={table}
390
+ columns={filters.columns}
391
+ label={l.addFilter}
392
+ />
393
+ )}
394
+ {showViewOptions && (
395
+ <DataTableViewOptions
396
+ table={table}
397
+ triggerLabel={l.columnsTrigger}
398
+ heading={l.columnsHeading}
399
+ />
400
+ )}
401
+ </ListHeaderControls>
402
+ </motion.div>
403
+ ) : null}
404
+ </AnimatePresence>
405
+ )}
406
+ {showChips && (
407
+ <ListHeaderChips>
408
+ <DataTableAppliedFilters
345
409
  table={table}
346
- triggerLabel={l.columnsTrigger}
347
- heading={l.columnsHeading}
410
+ columns={filters?.columns}
411
+ inlineChipLimit={filters?.inlineChipLimit}
412
+ removeLabel={l.removeFilter}
413
+ collapsedLabel={l.filtersCollapsed}
348
414
  />
349
- )}
350
- </ListHeaderControls>
351
- )}
352
- {showChips && (
353
- <ListHeaderChips>
354
- <DataTableAppliedFilters
355
- table={table}
356
- columns={filters?.columns}
357
- inlineChipLimit={filters?.inlineChipLimit}
358
- removeLabel={l.removeFilter}
359
- collapsedLabel={l.filtersCollapsed}
360
- />
361
- </ListHeaderChips>
362
- )}
363
- </ListHeader>
364
- )}
365
-
366
- {showBulk && bulkActions && (
367
- <DataTableBulkActions table={table} cache={selectionCache} render={bulkActions} />
415
+ </ListHeaderChips>
416
+ )}
417
+ </ListHeader>
418
+ </CardHeader>
368
419
  )}
369
420
 
370
- <div className="bg-card overflow-hidden rounded-xl border border-border/60">
421
+ <CardTable>
371
422
  <Table>
372
423
  <TableHeader>
373
424
  {table.getHeaderGroups().map((headerGroup) => (
@@ -419,63 +470,94 @@ function DataTable<TData>({
419
470
  </TableCell>
420
471
  </TableRow>
421
472
  ) : (
422
- bodyRows.map((row) => {
423
- const rowNode = (
424
- <TableRow
425
- className="group/row"
426
- data-state={row.getIsSelected() ? 'selected' : undefined}
427
- >
428
- {row.getVisibleCells().map((cell) => (
429
- <TableCell key={cell.id} className={columnClass(cell.column.id)}>
430
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
431
- </TableCell>
432
- ))}
433
- </TableRow>
434
- );
435
- // Right-click accelerator: the row itself is the context-menu
436
- // trigger, so the consumer supplies only the items and the core
437
- // owns the menu chrome.
438
- const defaultRow = contextActions ? (
439
- <ContextMenu>
440
- <ContextMenuTrigger render={rowNode} />
441
- <ContextMenuContent>
442
- {contextActions(row.original, { row, table })}
443
- </ContextMenuContent>
444
- </ContextMenu>
445
- ) : (
446
- rowNode
447
- );
448
- // Row-render seam: consumers can swap the default row for a
449
- // full-width utility row or a per-row wrapper the cell grid can't
450
- // express. Returning `defaultRow` keeps the built-in rendering.
451
- return (
452
- <React.Fragment key={row.id}>
453
- {renderRow ? renderRow(row, { table, columnCount, defaultRow }) : defaultRow}
454
- </React.Fragment>
455
- );
456
- })
473
+ <>
474
+ {bodyRows.map((row) => {
475
+ const rowNode = (
476
+ <TableRow
477
+ className="group/row"
478
+ data-state={row.getIsSelected() ? 'selected' : undefined}
479
+ >
480
+ {row.getVisibleCells().map((cell) => (
481
+ <TableCell key={cell.id} className={columnClass(cell.column.id)}>
482
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
483
+ </TableCell>
484
+ ))}
485
+ </TableRow>
486
+ );
487
+ // Right-click accelerator: the row itself is the context-menu
488
+ // trigger, so the consumer supplies only the items and the core
489
+ // owns the menu chrome.
490
+ const defaultRow = contextActions ? (
491
+ <ContextMenu>
492
+ <ContextMenuTrigger render={rowNode} />
493
+ <ContextMenuContent>
494
+ {contextActions(row.original, { row, table })}
495
+ </ContextMenuContent>
496
+ </ContextMenu>
497
+ ) : (
498
+ rowNode
499
+ );
500
+ // Row-render seam: consumers can swap the default row for a
501
+ // full-width utility row or a per-row wrapper the cell grid can't
502
+ // express. Returning `defaultRow` keeps the built-in rendering.
503
+ return (
504
+ <React.Fragment key={row.id}>
505
+ {renderRow ? renderRow(row, { table, columnCount, defaultRow }) : defaultRow}
506
+ </React.Fragment>
507
+ );
508
+ })}
509
+ {typeof footerRows === 'function' ? footerRows({ columnCount }) : footerRows}
510
+ </>
457
511
  )}
458
512
  </TableBody>
459
513
  </Table>
460
- </div>
514
+ </CardTable>
461
515
 
462
516
  {pagination && (
463
- <TablePagination
464
- page={pagination.page}
465
- pageSize={pagination.pageSize}
466
- totalItems={pagination.totalItems ?? table.getPrePaginationRowModel().rows.length}
467
- onPageChange={pagination.onPageChange}
468
- onPageSizeChange={pagination.onPageSizeChange}
469
- pageSizeOptions={pagination.pageSizeOptions}
470
- getPageHref={pagination.getPageHref}
471
- formatRange={pagination.formatRange}
472
- navLabel={l.pagination?.navLabel}
473
- previousLabel={l.pagination?.previousLabel}
474
- nextLabel={l.pagination?.nextLabel}
475
- pageSizeLabel={l.pagination?.pageSizeLabel}
476
- />
517
+ <CardFooter className="border-t">
518
+ <TablePagination
519
+ className="w-full"
520
+ page={pagination.page}
521
+ pageSize={pagination.pageSize}
522
+ totalItems={pagination.totalItems ?? table.getPrePaginationRowModel().rows.length}
523
+ onPageChange={pagination.onPageChange}
524
+ onPageSizeChange={pagination.onPageSizeChange}
525
+ pageSizeOptions={pagination.pageSizeOptions}
526
+ getPageHref={pagination.getPageHref}
527
+ formatRange={pagination.formatRange}
528
+ navLabel={l.pagination?.navLabel}
529
+ previousLabel={l.pagination?.previousLabel}
530
+ nextLabel={l.pagination?.nextLabel}
531
+ pageSizeLabel={l.pagination?.pageSizeLabel}
532
+ />
533
+ </CardFooter>
534
+ )}
535
+ </>
536
+ );
537
+
538
+ // `plain` renders a chrome-less stack instead of a Card so the bands and
539
+ // CardTable consume the HOST card's spacing variables (--card-px /
540
+ // --card-gap): edge cells align with the host's padding regardless of its
541
+ // size, and a trailing CardTable's negative margin reaches through the
542
+ // host's bottom padding so the last row sits flush against the host's edge.
543
+ // With no header band the leading CardTable's negative margin would eat the
544
+ // host's flex gap too and press the rows against the preceding content, so
545
+ // the stack restores that one gap as padding.
546
+ return frame === 'plain' ? (
547
+ <div
548
+ data-slot="data-table"
549
+ className={cn(
550
+ 'flex flex-col gap-(--card-gap)',
551
+ !(showHeader || showBulk) && 'pt-(--card-gap)',
552
+ className,
477
553
  )}
554
+ >
555
+ {bands}
478
556
  </div>
557
+ ) : (
558
+ <Card data-slot="data-table" className={className}>
559
+ {bands}
560
+ </Card>
479
561
  );
480
562
  }
481
563
 
@@ -75,7 +75,9 @@ function DatePicker({
75
75
  variant="outline"
76
76
  disabled={disabled}
77
77
  aria-invalid={invalid || undefined}
78
- className="min-w-0 flex-1 justify-start font-normal"
78
+ // Pickers sit in forms next to Inputs, so mirror the Input/Select
79
+ // trigger look (transparent bg) instead of the outline button fill.
80
+ className="min-w-0 flex-1 justify-start border-input bg-transparent font-normal hover:bg-transparent aria-expanded:bg-transparent"
79
81
  />
80
82
  }
81
83
  >
@@ -92,7 +92,9 @@ function DateRangePicker({
92
92
  variant="outline"
93
93
  disabled={disabled}
94
94
  aria-invalid={invalid || undefined}
95
- className="min-w-0 flex-1 justify-start font-normal"
95
+ // Pickers sit in forms next to Inputs, so mirror the Input/Select
96
+ // trigger look (transparent bg) instead of the outline button fill.
97
+ className="min-w-0 flex-1 justify-start border-input bg-transparent font-normal hover:bg-transparent aria-expanded:bg-transparent"
96
98
  />
97
99
  }
98
100
  >
@@ -84,7 +84,9 @@ function DateTimePicker({
84
84
  variant="outline"
85
85
  disabled={disabled}
86
86
  aria-invalid={invalid || undefined}
87
- className="min-w-0 flex-1 justify-start font-normal"
87
+ // Pickers sit in forms next to Inputs, so mirror the Input/Select
88
+ // trigger look (transparent bg) instead of the outline button fill.
89
+ className="min-w-0 flex-1 justify-start border-input bg-transparent font-normal hover:bg-transparent aria-expanded:bg-transparent"
88
90
  />
89
91
  }
90
92
  >