@dashforge/tw 0.5.0-beta → 0.7.0-beta

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/THEME-AUDIT.md +175 -0
  3. package/dist/index.esm.js +2504 -51
  4. package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -1
  5. package/dist/src/components/Box/box.variants.d.ts.map +1 -1
  6. package/dist/src/components/DataGrid/DataGrid.d.ts +29 -0
  7. package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -0
  8. package/dist/src/components/DataGrid/dataGrid.types.d.ts +171 -0
  9. package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -0
  10. package/dist/src/components/DataGrid/dataGrid.variants.d.ts +216 -0
  11. package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -0
  12. package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -1
  13. package/dist/src/components/Divider/divider.variants.d.ts.map +1 -1
  14. package/dist/src/components/Pagination/Pagination.d.ts.map +1 -1
  15. package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -1
  16. package/dist/src/components/Popover/popover.variants.d.ts.map +1 -1
  17. package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -1
  18. package/dist/src/components/Table/Table.d.ts +24 -0
  19. package/dist/src/components/Table/Table.d.ts.map +1 -0
  20. package/dist/src/components/Table/cells/RenderButton.d.ts +13 -0
  21. package/dist/src/components/Table/cells/RenderButton.d.ts.map +1 -0
  22. package/dist/src/components/Table/cells/RenderChip.d.ts +80 -0
  23. package/dist/src/components/Table/cells/RenderChip.d.ts.map +1 -0
  24. package/dist/src/components/Table/cells/RenderText.d.ts +21 -0
  25. package/dist/src/components/Table/cells/RenderText.d.ts.map +1 -0
  26. package/dist/src/components/Table/cells/RenderTwoLine.d.ts +20 -0
  27. package/dist/src/components/Table/cells/RenderTwoLine.d.ts.map +1 -0
  28. package/dist/src/components/Table/cells/RowActionsMenu.d.ts +36 -0
  29. package/dist/src/components/Table/cells/RowActionsMenu.d.ts.map +1 -0
  30. package/dist/src/components/Table/cells/index.d.ts +11 -0
  31. package/dist/src/components/Table/cells/index.d.ts.map +1 -0
  32. package/dist/src/components/Table/table.types.d.ts +312 -0
  33. package/dist/src/components/Table/table.types.d.ts.map +1 -0
  34. package/dist/src/components/Table/table.variants.d.ts +285 -0
  35. package/dist/src/components/Table/table.variants.d.ts.map +1 -0
  36. package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -1
  37. package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -1
  38. package/dist/src/components/Typography/typography.variants.d.ts.map +1 -1
  39. package/dist/src/components/_shared/data/getNestedValue.d.ts +20 -0
  40. package/dist/src/components/_shared/data/getNestedValue.d.ts.map +1 -0
  41. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts +45 -0
  42. package/dist/src/components/_shared/data/useColumnAutoDetect.d.ts.map +1 -0
  43. package/dist/src/components/_shared/data/useControllableState.d.ts +23 -0
  44. package/dist/src/components/_shared/data/useControllableState.d.ts.map +1 -0
  45. package/dist/src/components/_shared/data/useDebouncedValue.d.ts +10 -0
  46. package/dist/src/components/_shared/data/useDebouncedValue.d.ts.map +1 -0
  47. package/dist/src/components/_shared/data/useTableFilter.d.ts +13 -0
  48. package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -0
  49. package/dist/src/components/_shared/data/useTableSearch.d.ts +23 -0
  50. package/dist/src/components/_shared/data/useTableSearch.d.ts.map +1 -0
  51. package/dist/src/components/_shared/data/useTableSelection.d.ts +25 -0
  52. package/dist/src/components/_shared/data/useTableSelection.d.ts.map +1 -0
  53. package/dist/src/components/_shared/data/useTableSort.d.ts +20 -0
  54. package/dist/src/components/_shared/data/useTableSort.d.ts.map +1 -0
  55. package/dist/src/components/_shared/data/useVirtualizer.d.ts +107 -0
  56. package/dist/src/components/_shared/data/useVirtualizer.d.ts.map +1 -0
  57. package/dist/src/index.d.ts +10 -1
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/package.json +3 -3
  60. package/src/components/Accordion/accordion.variants.ts +5 -3
  61. package/src/components/Box/Box.test.tsx +32 -11
  62. package/src/components/Box/box.variants.ts +23 -4
  63. package/src/components/DataGrid/DataGrid.test.tsx +496 -0
  64. package/src/components/DataGrid/DataGrid.tsx +831 -0
  65. package/src/components/DataGrid/dataGrid.types.ts +189 -0
  66. package/src/components/DataGrid/dataGrid.variants.ts +144 -0
  67. package/src/components/Dialog/dialog.variants.ts +10 -6
  68. package/src/components/Divider/Divider.test.tsx +3 -1
  69. package/src/components/Divider/Divider.tsx +1 -1
  70. package/src/components/Divider/divider.variants.ts +2 -1
  71. package/src/components/Pagination/Pagination.tsx +3 -1
  72. package/src/components/Pagination/pagination.variants.ts +19 -13
  73. package/src/components/Popover/popover.variants.ts +10 -4
  74. package/src/components/Skeleton/skeleton.variants.ts +2 -1
  75. package/src/components/Table/Table.test.tsx +603 -0
  76. package/src/components/Table/Table.tsx +816 -0
  77. package/src/components/Table/cells/RenderButton.tsx +23 -0
  78. package/src/components/Table/cells/RenderChip.tsx +103 -0
  79. package/src/components/Table/cells/RenderText.tsx +37 -0
  80. package/src/components/Table/cells/RenderTwoLine.tsx +34 -0
  81. package/src/components/Table/cells/RowActionsMenu.tsx +147 -0
  82. package/src/components/Table/cells/index.ts +14 -0
  83. package/src/components/Table/table.types.ts +346 -0
  84. package/src/components/Table/table.variants.ts +207 -0
  85. package/src/components/Tabs/tabs.variants.ts +19 -8
  86. package/src/components/Tooltip/tooltip.variants.ts +12 -2
  87. package/src/components/Typography/Typography.test.tsx +3 -1
  88. package/src/components/Typography/typography.variants.ts +6 -2
  89. package/src/components/_shared/data/getNestedValue.test.ts +50 -0
  90. package/src/components/_shared/data/getNestedValue.ts +32 -0
  91. package/src/components/_shared/data/useColumnAutoDetect.test.ts +123 -0
  92. package/src/components/_shared/data/useColumnAutoDetect.ts +100 -0
  93. package/src/components/_shared/data/useControllableState.ts +48 -0
  94. package/src/components/_shared/data/useDebouncedValue.ts +24 -0
  95. package/src/components/_shared/data/useTableFilter.ts +37 -0
  96. package/src/components/_shared/data/useTableSearch.test.ts +115 -0
  97. package/src/components/_shared/data/useTableSearch.ts +66 -0
  98. package/src/components/_shared/data/useTableSelection.test.ts +100 -0
  99. package/src/components/_shared/data/useTableSelection.ts +100 -0
  100. package/src/components/_shared/data/useTableSort.test.ts +150 -0
  101. package/src/components/_shared/data/useTableSort.ts +141 -0
  102. package/src/components/_shared/data/useVirtualizer.test.ts +158 -0
  103. package/src/components/_shared/data/useVirtualizer.ts +199 -0
  104. package/src/components/_shared/themeIdentity.test.ts +113 -0
  105. package/src/index.ts +59 -1
@@ -0,0 +1,189 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AccessRequirement } from '@dashforge/rbac';
3
+ import type { DataGridVariants } from './dataGrid.variants.js';
4
+ import type {
5
+ TableColumn,
6
+ TableSortModel,
7
+ TableFilterModel,
8
+ TableRowSelectionMode,
9
+ TableLabels,
10
+ } from '../Table/table.types.js';
11
+
12
+ // Re-export the column type unchanged — DataGrid uses the SAME shape
13
+ // as Table. Consumers writing `cols: TableColumn<T>[]` get autocomplete
14
+ // and don't need a parallel `DataGridColumn<T>` type.
15
+
16
+ // ───── Selection scope ─────
17
+
18
+ /**
19
+ * Determines what the "select all" header checkbox covers when
20
+ * `rowSelection="multiple"` is enabled.
21
+ *
22
+ * - `'visible'` — only the rows currently in the viewport (after
23
+ * virtualization windowing) are toggled. Useful when the
24
+ * underlying data is paginated server-side and "all" semantically
25
+ * refers to the screen.
26
+ * - `'allLoaded'` (default) — all rows currently in the `rows` prop
27
+ * are toggled. The standard "select everything I can see in this
28
+ * dataset" pattern.
29
+ *
30
+ * For "select all in server-side dataset", the consumer wires it
31
+ * themselves (e.g. fetch all ids + setSelectedRowIds).
32
+ */
33
+ export type DataGridSelectAllScope = 'visible' | 'allLoaded';
34
+
35
+ // ───── Server-side mode flags ─────
36
+
37
+ /**
38
+ * Independent opt-in flags that delegate the corresponding pipeline
39
+ * step to the consumer. With each flag enabled, DataGrid emits the
40
+ * `onXChange` callback but does NOT perform the operation locally
41
+ * — the consumer is expected to provide the resulting `rows` array.
42
+ */
43
+ export interface DataGridServerSideFlags {
44
+ serverSideSort?: boolean;
45
+ serverSideFilter?: boolean;
46
+ serverSideSearch?: boolean;
47
+ /**
48
+ * When true, DataGrid does NOT slice `rows` for pagination. The
49
+ * virtualization math uses `totalCount` (required) to size the
50
+ * scroll container.
51
+ */
52
+ serverSidePagination?: boolean;
53
+ }
54
+
55
+ // ───── Pagination (DataGrid-internal, opt-in) ─────
56
+
57
+ export interface DataGridPaginationConfig {
58
+ page: number;
59
+ pageSize: number;
60
+ onPageChange: (page: number) => void;
61
+ onPageSizeChange?: (pageSize: number) => void;
62
+ pageSizeOptions?: number[];
63
+ }
64
+
65
+ // ───── Slot props ─────
66
+
67
+ export interface DataGridSlotProps {
68
+ root?: { className?: string };
69
+ toolbar?: { className?: string };
70
+ search?: { className?: string };
71
+ scroll?: { className?: string };
72
+ table?: { className?: string };
73
+ thead?: { className?: string };
74
+ tbody?: { className?: string };
75
+ headerRow?: { className?: string };
76
+ headerCell?: { className?: string };
77
+ row?: { className?: string };
78
+ cell?: { className?: string };
79
+ emptyState?: { className?: string };
80
+ loadingOverlay?: { className?: string };
81
+ selectionCell?: { className?: string };
82
+ rowActionsCell?: { className?: string };
83
+ bulkActionFooter?: { className?: string };
84
+ /** Optional Pagination wrapper below the virtualized scroll. */
85
+ pagination?: { className?: string };
86
+ }
87
+
88
+ // ───── Main props ─────
89
+
90
+ /**
91
+ * Props for `<DataGrid>`.
92
+ *
93
+ * Companion to `<Table>` for large data sets. Where Table renders
94
+ * every row in DOM, DataGrid uses **homemade virtualization**
95
+ * (`IntersectionObserver` + scroll-event + `requestAnimationFrame`
96
+ * — no external libs) to render only a window of visible rows.
97
+ *
98
+ * Required:
99
+ * - `rows`, `cols`, `getRowId` — same as Table
100
+ * - `rowHeight` (fixed in v1; variable-height deferred to v1-bis)
101
+ * - `height` — explicit container height in CSS units, OR
102
+ * consumer sets it via `sx` / parent layout (the scroll container
103
+ * needs a bounded height for virtualization to work)
104
+ *
105
+ * Optional:
106
+ * - `overscan` — extra rows above/below viewport (default 5)
107
+ * - Server-side mode flags (4 independent opt-ins)
108
+ * - Internal pagination (opt-in via `pagination` config)
109
+ * - `selectAllScope` — `'visible'` vs `'allLoaded'` for header
110
+ * select-all checkbox
111
+ * - Everything Table accepts: sort / filter / search / selection /
112
+ * row actions / RBAC / variants / i18n labels / sx / slotProps
113
+ */
114
+ export interface DataGridProps<T extends object>
115
+ extends Pick<DataGridVariants, 'variant' | 'size' | 'density'>,
116
+ DataGridServerSideFlags {
117
+ // ───── Data ─────
118
+ rows: T[];
119
+ cols: TableColumn<T>[];
120
+ getRowId: (row: T, index: number) => string;
121
+
122
+ /** Required: total dataset count. In client-side mode equals
123
+ * `rows.length`; in `serverSidePagination` mode the consumer
124
+ * provides the full count so the virtual scroll height matches. */
125
+ totalCount?: number;
126
+
127
+ // ───── Virtualization (REQUIRED) ─────
128
+ /** Fixed row height in pixels. Required for windowing math. */
129
+ rowHeight: number;
130
+ /** Rows rendered above/below the viewport. @default 5 */
131
+ overscan?: number;
132
+ /** Container height in CSS units (e.g. `"600px"`, `"100vh"`).
133
+ * Required for the scroll container to bound the virtualization. */
134
+ height?: string;
135
+
136
+ // ───── Sort ─────
137
+ sortModel?: TableSortModel;
138
+ onSortChange?: (model: TableSortModel) => void;
139
+ defaultSortModel?: TableSortModel;
140
+
141
+ // ───── Search ─────
142
+ enableSearch?: boolean;
143
+ searchQuery?: string;
144
+ onSearchQueryChange?: (q: string) => void;
145
+ searchPlaceholder?: string;
146
+ searchDebounceMs?: number;
147
+
148
+ // ───── Filter (model only; per-column UI deferred to v1-bis) ─────
149
+ filterModel?: TableFilterModel;
150
+ onFilterChange?: (model: TableFilterModel) => void;
151
+
152
+ // ───── Selection ─────
153
+ rowSelection?: TableRowSelectionMode;
154
+ selectedRowIds?: string[];
155
+ onSelectionChange?: (ids: string[]) => void;
156
+ /** Bulk action sticky footer (visible only when selection > 0). */
157
+ bulkActions?: (selectedRows: T[]) => ReactNode;
158
+ /**
159
+ * Scope of the "select all" header checkbox.
160
+ * @default 'allLoaded'
161
+ */
162
+ selectAllScope?: DataGridSelectAllScope;
163
+
164
+ // ───── Internal pagination (opt-in) ─────
165
+ pagination?: DataGridPaginationConfig;
166
+
167
+ // ───── Row actions ─────
168
+ rowActions?: (row: T) => ReactNode;
169
+
170
+ // ───── States ─────
171
+ loading?: boolean;
172
+ loadingRowCount?: number;
173
+ emptyState?: ReactNode;
174
+
175
+ // ───── A11Y ─────
176
+ stickyHeader?: boolean;
177
+ caption?: ReactNode;
178
+ showCaption?: boolean;
179
+
180
+ // ───── RBAC ─────
181
+ access?: AccessRequirement;
182
+
183
+ // ───── i18n ─────
184
+ labels?: TableLabels;
185
+
186
+ // ───── Customization ─────
187
+ sx?: string;
188
+ slotProps?: DataGridSlotProps;
189
+ }
@@ -0,0 +1,144 @@
1
+ import { tv, type VariantProps } from 'tailwind-variants';
2
+
3
+ /**
4
+ * `<DataGrid>` variant recipe.
5
+ *
6
+ * Sibling of `tableVariants` — shares the same visual axes (variant /
7
+ * size / density) and the same identity-rule patterns:
8
+ * - No `dark:` variants on neutral palette (auto-invert via the
9
+ * dashforgePreset() CSS var swap).
10
+ * - Selected row uses LeftNav `itemActive` pattern
11
+ * (`bg-primary-100 text-primary-900`) — guaranteed contrast.
12
+ * - Hover uses one-tier elevation (`bg-neutral-100`).
13
+ *
14
+ * DataGrid-specific slot additions vs Table:
15
+ * - `stickyLeftCell` — column that uses `cols[i].sticky === 'left'`.
16
+ * Z-index ordering: `thead` (z-10) < `stickyLeftCell` (z-11) <
17
+ * `header × stickyLeft top-left corner cell` (z-20).
18
+ * - `paginationFooter` — wraps the optional `<Pagination>`.
19
+ */
20
+ export const dataGridVariants = tv({
21
+ slots: {
22
+ root: 'w-full',
23
+ toolbar: 'flex flex-wrap items-center gap-3 mb-3',
24
+ search: [
25
+ 'inline-flex items-center gap-2',
26
+ 'w-full max-w-sm rounded-md',
27
+ 'border border-neutral-300',
28
+ 'bg-neutral-50',
29
+ 'focus-within:ring-2 focus-within:ring-primary-500',
30
+ 'transition-colors motion-reduce:transition-none',
31
+ ],
32
+ // The scroll container needs an explicit bounded height for
33
+ // virtualization to compute the window. The consumer passes
34
+ // `height` prop (inline style) or sets it via `sx` / parent
35
+ // layout — DataGrid does NOT default a height (would be wrong
36
+ // for most layouts).
37
+ scroll: 'relative w-full overflow-auto rounded-md',
38
+ table: 'w-full caption-bottom text-left',
39
+ thead: 'bg-neutral-50 sticky top-0 z-10',
40
+ tbody: '',
41
+ headerRow: '',
42
+ headerCell: [
43
+ 'text-neutral-700',
44
+ 'font-semibold whitespace-nowrap select-none',
45
+ 'border-b border-neutral-200',
46
+ 'bg-neutral-50',
47
+ ],
48
+ headerCellButton: [
49
+ 'group inline-flex items-center gap-1 select-none',
50
+ 'hover:text-neutral-900',
51
+ 'focus:outline-none focus-visible:underline',
52
+ 'transition-colors motion-reduce:transition-none',
53
+ ],
54
+ row: [
55
+ 'group/row transition-colors motion-reduce:transition-none',
56
+ 'hover:bg-neutral-100',
57
+ 'data-[selected=true]:bg-primary-100',
58
+ 'data-[selected=true]:text-primary-900',
59
+ 'data-[selected=true]:font-medium',
60
+ ],
61
+ cell: [
62
+ 'text-neutral-900',
63
+ 'border-b border-neutral-200',
64
+ 'align-middle',
65
+ ],
66
+ selectionCell: 'w-10 text-center',
67
+ rowActionsCell: [
68
+ 'w-12 text-right',
69
+ 'opacity-0 group-hover/row:opacity-100 group-focus-within/row:opacity-100',
70
+ 'transition-opacity motion-reduce:transition-none',
71
+ ],
72
+ emptyState: 'py-12 text-center text-neutral-500',
73
+ loadingOverlay: 'pointer-events-none',
74
+ bulkActionFooter: [
75
+ 'sticky bottom-0 z-20',
76
+ 'flex flex-wrap items-center justify-between gap-3',
77
+ 'px-3 py-2',
78
+ 'border-t border-neutral-200',
79
+ 'bg-neutral-50 shadow-md',
80
+ ],
81
+ // Sticky left column — uses `position: sticky; left: 0` + z-index
82
+ // so the column stays pinned during horizontal scroll. The
83
+ // `bg-neutral-50` ensures the column doesn't bleed when content
84
+ // scrolls underneath.
85
+ stickyLeftCell: 'sticky left-0 z-[1] bg-neutral-50',
86
+ // Sticky left × header intersection (top-left corner): higher
87
+ // z-index than both header (`z-10`) and sticky col (`z-[1]`).
88
+ stickyLeftHeaderCell: 'sticky left-0 z-20 bg-neutral-50',
89
+ paginationFooter: 'flex justify-end mt-3',
90
+ },
91
+ variants: {
92
+ variant: {
93
+ plain: {},
94
+ lines: {},
95
+ striped: {
96
+ row: 'odd:bg-neutral-100/60',
97
+ },
98
+ bordered: {
99
+ table: 'border border-neutral-200',
100
+ cell: 'border-r last:border-r-0 border-neutral-200',
101
+ headerCell:
102
+ 'border-r last:border-r-0 border-neutral-200',
103
+ },
104
+ },
105
+ size: {
106
+ sm: {
107
+ headerCell: 'text-xs',
108
+ cell: 'text-xs',
109
+ search: 'text-xs',
110
+ },
111
+ md: {
112
+ headerCell: 'text-sm',
113
+ cell: 'text-sm',
114
+ search: 'text-sm',
115
+ },
116
+ lg: {
117
+ headerCell: 'text-base',
118
+ cell: 'text-base',
119
+ search: 'text-base',
120
+ },
121
+ },
122
+ density: {
123
+ compact: {
124
+ headerCell: 'px-2 py-1',
125
+ cell: 'px-2 py-1',
126
+ },
127
+ comfortable: {
128
+ headerCell: 'px-3 py-2',
129
+ cell: 'px-3 py-2',
130
+ },
131
+ spacious: {
132
+ headerCell: 'px-4 py-3',
133
+ cell: 'px-4 py-3',
134
+ },
135
+ },
136
+ },
137
+ defaultVariants: {
138
+ variant: 'lines',
139
+ size: 'md',
140
+ density: 'comfortable',
141
+ },
142
+ });
143
+
144
+ export type DataGridVariants = VariantProps<typeof dataGridVariants>;
@@ -26,21 +26,25 @@ export const dialogVariants = tv({
26
26
  ],
27
27
  content: [
28
28
  'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
29
- 'w-full bg-white dark:bg-neutral-900 rounded-lg shadow-xl',
30
- 'border border-neutral-200 dark:border-neutral-800',
29
+ // bg-white hardcoded (doesn't auto-invert) so `dark:` is needed.
30
+ // Target `dark:bg-neutral-100` = #171717 in dark mode (one
31
+ // elevation tier above page surface).
32
+ 'w-full bg-white dark:bg-neutral-100 rounded-lg shadow-xl',
33
+ // border-neutral-200 auto-inverts — no `dark:` needed.
34
+ 'border border-neutral-200',
31
35
  'p-6 flex flex-col gap-4 max-h-[90vh]',
32
36
  'focus:outline-none',
33
37
  'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
34
38
  'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
35
39
  'motion-reduce:transition-none motion-reduce:duration-0',
36
40
  ],
37
- title: 'text-lg font-semibold leading-tight text-neutral-900 dark:text-neutral-50',
38
- description: 'text-sm text-neutral-600 dark:text-neutral-400',
41
+ // All neutral text classes auto-invert via CSS-var swap.
42
+ title: 'text-lg font-semibold leading-tight text-neutral-900',
43
+ description: 'text-sm text-neutral-600',
39
44
  closeButton: [
40
45
  'absolute top-4 right-4 inline-flex items-center justify-center',
41
46
  'h-8 w-8 rounded-md text-neutral-500 hover:text-neutral-900',
42
- 'dark:text-neutral-400 dark:hover:text-neutral-50',
43
- 'hover:bg-neutral-100 dark:hover:bg-neutral-800',
47
+ 'hover:bg-neutral-100',
44
48
  'focus:outline-none focus:ring-2 focus:ring-primary-500',
45
49
  'transition-colors',
46
50
  ],
@@ -66,8 +66,10 @@ describe('<Divider>', () => {
66
66
  it('default neutral', () => {
67
67
  const { container } = render(<Divider />);
68
68
  const cls = container.firstElementChild?.className ?? '';
69
+ // Sprint 4.3 identity sweep: neutral palette auto-inverts via
70
+ // the dashforgePreset() CSS-var swap — no `dark:` needed.
69
71
  expect(cls).toContain('border-neutral-200');
70
- expect(cls).toContain('dark:border-neutral-800');
72
+ expect(cls).not.toContain('dark:border-neutral-');
71
73
  });
72
74
 
73
75
  it('color="primary"', () => {
@@ -128,7 +128,7 @@ export const Divider = forwardRef<HTMLElement, DividerProps>(
128
128
  {...rest}
129
129
  >
130
130
  <span aria-hidden="true" className={cn(lineCommon, leftStubClass)} />
131
- <span className={cn('shrink-0 text-sm text-neutral-500 dark:text-neutral-400', labelPadding)}>
131
+ <span className={cn('shrink-0 text-sm text-neutral-500', labelPadding)}>
132
132
  {children}
133
133
  </span>
134
134
  <span aria-hidden="true" className={cn(lineCommon, rightStubClass)} />
@@ -85,7 +85,8 @@ export const dividerLineVariants = tv({
85
85
  },
86
86
 
87
87
  color: {
88
- neutral: 'border-neutral-200 dark:border-neutral-800',
88
+ // Neutral palette auto-inverts via CSS-var swap (no `dark:`).
89
+ neutral: 'border-neutral-200',
89
90
  primary: 'border-primary-300 dark:border-primary-700',
90
91
  secondary: 'border-secondary-300 dark:border-secondary-700',
91
92
  success: 'border-success-300 dark:border-success-700',
@@ -238,7 +238,9 @@ export function Pagination(props: PaginationProps) {
238
238
  disabled={disabled}
239
239
  aria-label={labels.perPage}
240
240
  className={cn(
241
- 'rounded-md border border-neutral-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-2 py-1',
241
+ // Auto-invert via CSS-var swap — no `dark:` variants needed
242
+ // on neutral palette.
243
+ 'rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1',
242
244
  'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
243
245
  )}
244
246
  >
@@ -23,28 +23,34 @@ import { tv, type VariantProps } from 'tailwind-variants';
23
23
  export const paginationVariants = tv({
24
24
  slots: {
25
25
  root: 'flex flex-wrap items-center gap-3',
26
- summary: 'text-neutral-600 dark:text-neutral-400',
26
+ // All neutral classes auto-invert via the dashforgePreset() CSS-var
27
+ // swap. `bg-neutral-50` provides the button surface (matches page
28
+ // surface; the border gives the separation). `hover:bg-neutral-100`
29
+ // is the one-tier elevation that works in both modes.
30
+ summary: 'text-neutral-600',
27
31
  list: 'inline-flex items-center gap-1',
28
32
  pageButton: [
29
33
  'inline-flex items-center justify-center rounded-md',
30
- 'border border-neutral-300 dark:border-neutral-700',
31
- 'bg-white dark:bg-neutral-900',
32
- 'text-neutral-700 dark:text-neutral-300',
33
- 'hover:bg-neutral-50 dark:hover:bg-neutral-800',
34
+ 'border border-neutral-300',
35
+ 'bg-neutral-50',
36
+ 'text-neutral-700',
37
+ 'hover:bg-neutral-100',
34
38
  'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
35
39
  'transition-colors motion-reduce:transition-none',
36
40
  'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
37
41
  ],
38
42
  activeButton: [
43
+ // Primary palette does NOT auto-invert — `dark:` shift is a
44
+ // design choice for tone refinement (kept).
39
45
  'bg-primary-500 text-white border-primary-500',
40
46
  'hover:bg-primary-600 dark:hover:bg-primary-400',
41
47
  ],
42
48
  navButton: [
43
49
  'inline-flex items-center justify-center rounded-md',
44
- 'border border-neutral-300 dark:border-neutral-700',
45
- 'bg-white dark:bg-neutral-900',
46
- 'text-neutral-700 dark:text-neutral-300',
47
- 'hover:bg-neutral-50 dark:hover:bg-neutral-800',
50
+ 'border border-neutral-300',
51
+ 'bg-neutral-50',
52
+ 'text-neutral-700',
53
+ 'hover:bg-neutral-100',
48
54
  'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
49
55
  'transition-colors motion-reduce:transition-none',
50
56
  'disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none',
@@ -52,12 +58,12 @@ export const paginationVariants = tv({
52
58
  ellipsis: 'inline-flex items-center justify-center text-neutral-500 px-1',
53
59
  pageSizeSelector: [
54
60
  'inline-flex items-center gap-2',
55
- 'text-neutral-700 dark:text-neutral-300',
61
+ 'text-neutral-700',
56
62
  ],
57
63
  jumpInput: [
58
- 'rounded-md border border-neutral-300 dark:border-neutral-700',
59
- 'bg-white dark:bg-neutral-900',
60
- 'text-neutral-900 dark:text-neutral-50',
64
+ 'rounded-md border border-neutral-300',
65
+ 'bg-neutral-50',
66
+ 'text-neutral-900',
61
67
  'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
62
68
  'transition-colors motion-reduce:transition-none',
63
69
  ],
@@ -3,15 +3,21 @@ import { tv, type VariantProps } from 'tailwind-variants';
3
3
  export const popoverVariants = tv({
4
4
  slots: {
5
5
  content: [
6
- 'z-50 rounded-md border border-neutral-200 dark:border-neutral-800',
7
- 'bg-white dark:bg-neutral-900 p-4 shadow-lg',
8
- 'text-sm text-neutral-900 dark:text-neutral-50',
6
+ // Neutral border auto-inverts via CSS-var swap.
7
+ 'z-50 rounded-md border border-neutral-200',
8
+ // bg-white hardcoded (no auto-invert) so `dark:` is required.
9
+ // Target `dark:bg-neutral-100` resolves to #171717 in dark mode
10
+ // (one elevation tier above the page surface).
11
+ 'bg-white dark:bg-neutral-100 p-4 shadow-lg',
12
+ // Text auto-inverts.
13
+ 'text-sm text-neutral-900',
9
14
  'focus:outline-none',
10
15
  'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
11
16
  'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
12
17
  'motion-reduce:transition-none motion-reduce:duration-0',
13
18
  ],
14
- arrow: 'fill-white dark:fill-neutral-900',
19
+ // Arrow follows content bg: `fill-white` in light + matching dark target.
20
+ arrow: 'fill-white dark:fill-neutral-100',
15
21
  },
16
22
  });
17
23
 
@@ -35,7 +35,8 @@ export const skeletonVariants = tv({
35
35
  slots: {
36
36
  root: [
37
37
  'block',
38
- 'bg-neutral-200 dark:bg-neutral-800',
38
+ // Neutral palette auto-inverts via CSS-var swap (no `dark:`).
39
+ 'bg-neutral-200',
39
40
  'motion-reduce:animate-none',
40
41
  ],
41
42
  },