@aortl/admin-css 0.0.1 → 0.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.
Files changed (39) hide show
  1. package/README.md +0 -22
  2. package/dist/admin.css +2918 -248
  3. package/dist/admin.min.css +1 -1
  4. package/dist/admin.scoped.css +3383 -0
  5. package/dist/admin.scoped.min.css +46 -0
  6. package/package.json +15 -3
  7. package/src/base.css +13 -7
  8. package/src/components/accordion.css +79 -0
  9. package/src/components/alert.css +83 -0
  10. package/src/components/app-shell.css +59 -0
  11. package/src/components/badge.css +44 -0
  12. package/src/components/brand-tile.css +9 -0
  13. package/src/components/breadcrumbs.css +38 -0
  14. package/src/components/button-group.css +73 -0
  15. package/src/components/button.css +50 -1
  16. package/src/components/card.css +1 -1
  17. package/src/components/checkbox.css +38 -0
  18. package/src/components/dialog.css +91 -0
  19. package/src/components/field.css +29 -2
  20. package/src/components/file-input.css +36 -0
  21. package/src/components/footer.css +26 -0
  22. package/src/components/index.css +24 -0
  23. package/src/components/input-group.css +38 -0
  24. package/src/components/input.css +7 -0
  25. package/src/components/menu.css +88 -0
  26. package/src/components/navbar.css +66 -0
  27. package/src/components/pagination.css +43 -0
  28. package/src/components/progress.css +97 -0
  29. package/src/components/radio.css +45 -0
  30. package/src/components/select.css +114 -0
  31. package/src/components/sidebar.css +225 -0
  32. package/src/components/spinner.css +40 -0
  33. package/src/components/switch.css +62 -0
  34. package/src/components/table.css +124 -0
  35. package/src/components/tabs.css +172 -0
  36. package/src/components/textarea.css +33 -0
  37. package/src/fonts.css +88 -0
  38. package/src/index.css +1 -0
  39. package/src/theme.css +122 -29
@@ -0,0 +1,97 @@
1
+ @layer components {
2
+ /* Native <progress> styled across engines. `appearance: none` strips the
3
+ platform default; the track is the element itself, the fill comes from
4
+ ::-webkit-progress-value / ::-moz-progress-bar. */
5
+ .progress {
6
+ appearance: none;
7
+ -webkit-appearance: none;
8
+ display: block;
9
+ width: 100%;
10
+ height: 0.375rem;
11
+ border: 0;
12
+ border-radius: 9999px;
13
+ overflow: hidden;
14
+ background-color: var(--color-surface-strong);
15
+ color: var(--color-primary);
16
+ }
17
+
18
+ .progress::-webkit-progress-bar {
19
+ background-color: var(--color-surface-strong);
20
+ border-radius: 9999px;
21
+ }
22
+
23
+ .progress::-webkit-progress-value {
24
+ background-color: currentColor;
25
+ border-radius: 9999px;
26
+ transition: inline-size 200ms ease;
27
+ }
28
+
29
+ .progress::-moz-progress-bar {
30
+ background-color: currentColor;
31
+ border-radius: 9999px;
32
+ transition: inline-size 200ms ease;
33
+ }
34
+
35
+ /* Sizes */
36
+ .progress-sm {
37
+ height: 0.25rem;
38
+ }
39
+
40
+ .progress-lg {
41
+ height: 0.5rem;
42
+ }
43
+
44
+ /* Variants — recolour the fill via currentColor */
45
+ .progress-success {
46
+ color: var(--color-success);
47
+ }
48
+
49
+ .progress-warning {
50
+ color: var(--color-warning);
51
+ }
52
+
53
+ .progress-danger {
54
+ color: var(--color-danger);
55
+ }
56
+
57
+ /* Indeterminate — no value attribute. WebKit hides the value pseudo, Firefox
58
+ draws it full-width; in both cases we paint an animated gradient on the
59
+ bar itself. */
60
+ .progress:indeterminate {
61
+ background-image: linear-gradient(
62
+ 90deg,
63
+ var(--color-surface-strong) 0%,
64
+ var(--color-surface-strong) 40%,
65
+ currentColor 50%,
66
+ var(--color-surface-strong) 60%,
67
+ var(--color-surface-strong) 100%
68
+ );
69
+ background-size: 250% 100%;
70
+ background-repeat: no-repeat;
71
+ animation: progress-indeterminate 1.2s linear infinite;
72
+ }
73
+
74
+ .progress:indeterminate::-webkit-progress-bar {
75
+ background-color: transparent;
76
+ }
77
+
78
+ .progress:indeterminate::-webkit-progress-value,
79
+ .progress:indeterminate::-moz-progress-bar {
80
+ background-color: transparent;
81
+ }
82
+
83
+ @media (prefers-reduced-motion: reduce) {
84
+ .progress:indeterminate {
85
+ animation-duration: 3s;
86
+ }
87
+ }
88
+
89
+ @keyframes progress-indeterminate {
90
+ from {
91
+ background-position: 100% 0;
92
+ }
93
+ to {
94
+ background-position: 0 0;
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,45 @@
1
+ @layer components {
2
+ .radio {
3
+ @apply inline-flex items-center justify-center size-4 shrink-0
4
+ rounded-full border bg-surface
5
+ cursor-pointer
6
+ transition-colors duration-150
7
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
8
+ disabled:opacity-50 disabled:cursor-not-allowed;
9
+ }
10
+
11
+ .radio[data-unchecked] {
12
+ @apply border-border-strong hover:border-text-muted;
13
+ }
14
+
15
+ .radio[data-checked] {
16
+ @apply bg-primary border-primary hover:bg-primary-hover;
17
+ }
18
+
19
+ .radio[data-disabled] {
20
+ @apply opacity-50 cursor-not-allowed;
21
+ }
22
+
23
+ .radio-indicator {
24
+ @apply inline-flex size-1.5 rounded-full bg-primary-content;
25
+ }
26
+
27
+ .radio-group {
28
+ @apply inline-flex flex-wrap gap-4;
29
+ }
30
+
31
+ .radio-group-vertical {
32
+ @apply flex-col gap-2 items-start;
33
+ }
34
+
35
+ /* A <label> wrapping a radio + text lays out inline with a small gap.
36
+ Covers both the vanilla input.radio and Base UI's span.radio. */
37
+ label:has(> .radio) {
38
+ @apply inline-flex items-center gap-2 cursor-pointer;
39
+ }
40
+
41
+ label:has(> .radio:disabled),
42
+ label:has(> .radio[data-disabled]) {
43
+ @apply opacity-60 cursor-not-allowed;
44
+ }
45
+ }
@@ -0,0 +1,114 @@
1
+ @layer components {
2
+ .select {
3
+ @apply inline-flex items-center justify-between gap-2 w-full px-3 py-2
4
+ rounded-lg text-sm leading-none text-left
5
+ bg-surface text-text
6
+ border border-transparent
7
+ cursor-pointer select-none
8
+ transition-colors duration-150
9
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
10
+ disabled:opacity-50 disabled:cursor-not-allowed;
11
+ }
12
+
13
+ .select[data-popup-open] {
14
+ @apply outline-2 outline-offset-2 outline-primary;
15
+ }
16
+
17
+ .select[data-placeholder] {
18
+ @apply text-text-muted;
19
+ }
20
+
21
+ .select-bordered {
22
+ @apply border-border hover:border-border-strong;
23
+ }
24
+
25
+ .select-ghost {
26
+ @apply bg-transparent hover:bg-surface-muted;
27
+ }
28
+
29
+ .select-danger {
30
+ @apply border-danger focus-visible:outline-danger;
31
+ }
32
+
33
+ /* Sizes */
34
+ .select-sm {
35
+ @apply text-xs px-2.5 py-1.5;
36
+ }
37
+
38
+ .select-lg {
39
+ @apply text-base px-4 py-2.5;
40
+ }
41
+
42
+ /* Native <select> usage: same class, but suppress the native chevron and
43
+ supply our own via background-image so the look matches the Base UI
44
+ trigger. Chevron stroke uses Flexoki base-500, which reads as neutral
45
+ in both light and dark mode. */
46
+ select.select {
47
+ appearance: none;
48
+ padding-right: 2rem;
49
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23878580' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
50
+ background-repeat: no-repeat;
51
+ background-position: right 0.5rem center;
52
+ background-size: 1rem;
53
+ }
54
+
55
+ select.select-sm {
56
+ padding-right: 1.75rem;
57
+ background-size: 0.875rem;
58
+ }
59
+
60
+ select.select-lg {
61
+ padding-right: 2.25rem;
62
+ }
63
+
64
+ .select-icon {
65
+ @apply inline-flex items-center justify-center size-4 shrink-0
66
+ text-text-muted transition-transform duration-150;
67
+ }
68
+
69
+ .select[data-popup-open] .select-icon {
70
+ @apply rotate-180;
71
+ }
72
+
73
+ .select-popup {
74
+ @apply min-w-[var(--anchor-width)] max-h-72 overflow-auto
75
+ py-1 outline-none
76
+ bg-surface text-text
77
+ border border-border rounded-lg shadow-md
78
+ transition-[opacity,transform] duration-100;
79
+ }
80
+
81
+ .select-popup[data-starting-style],
82
+ .select-popup[data-ending-style] {
83
+ @apply opacity-0;
84
+ }
85
+
86
+ .select-popup[data-starting-style] {
87
+ @apply -translate-y-1;
88
+ }
89
+
90
+ .select-item {
91
+ @apply flex items-center gap-2 px-3 py-1.5 text-sm
92
+ cursor-pointer select-none outline-none;
93
+ }
94
+
95
+ .select-item[data-highlighted] {
96
+ @apply bg-surface-muted;
97
+ }
98
+
99
+ .select-item[data-selected] {
100
+ @apply font-medium;
101
+ }
102
+
103
+ .select-item[data-disabled] {
104
+ @apply opacity-50 cursor-not-allowed;
105
+ }
106
+
107
+ .select-item-indicator {
108
+ @apply inline-flex items-center justify-center size-4 ml-auto text-primary;
109
+ }
110
+
111
+ .select-group-label {
112
+ @apply px-3 pt-2 pb-1 text-xs uppercase tracking-wide text-text-muted;
113
+ }
114
+ }
@@ -0,0 +1,225 @@
1
+ @layer components {
2
+ .sidebar {
3
+ @apply flex flex-col shrink-0
4
+ bg-surface-muted text-text
5
+ border-r border-border;
6
+ width: var(--app-shell-sidebar-w, 240px);
7
+ transition: width 150ms ease;
8
+ }
9
+
10
+ /* Hidden checkbox that drives the collapsed state via :has(). Lives inside
11
+ <Sidebar.CollapseToggle>, but :has() finds it from anywhere in .sidebar. */
12
+ .sidebar-toggle {
13
+ position: absolute;
14
+ width: 1px;
15
+ height: 1px;
16
+ margin: 0;
17
+ padding: 0;
18
+ border: 0;
19
+ opacity: 0;
20
+ pointer-events: none;
21
+ }
22
+
23
+ .sidebar:has(.sidebar-toggle:checked) {
24
+ width: var(--app-shell-sidebar-w-collapsed, 56px);
25
+ }
26
+
27
+ /* Hide labels, group headings, badges, and tree panels in collapsed mode. */
28
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-label,
29
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-group-label,
30
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-badge,
31
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-collapsible-panel,
32
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-collapsible-trigger::after {
33
+ display: none;
34
+ }
35
+
36
+ /* Center the icon in collapsed mode. */
37
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-item,
38
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-subitem,
39
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-collapsible-trigger {
40
+ @apply justify-center px-1;
41
+ }
42
+
43
+ .sidebar-header {
44
+ @apply flex items-center gap-2 px-3 h-12 shrink-0 border-b border-border;
45
+ }
46
+
47
+ .sidebar-nav {
48
+ @apply flex flex-col gap-0.5 px-2 py-2 flex-1 overflow-auto;
49
+ }
50
+
51
+ .sidebar-group {
52
+ @apply flex flex-col gap-0.5;
53
+ }
54
+
55
+ .sidebar-group + .sidebar-group {
56
+ @apply mt-2;
57
+ }
58
+
59
+ .sidebar-group-label {
60
+ @apply px-2 pt-2 pb-1 text-xs uppercase tracking-wide text-text-muted;
61
+ }
62
+
63
+ .sidebar-item {
64
+ @apply flex items-center gap-2 px-2 py-1.5
65
+ rounded-md text-sm leading-none text-text
66
+ bg-transparent hover:bg-surface-strong
67
+ cursor-pointer select-none no-underline
68
+ transition-colors duration-150
69
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary;
70
+ }
71
+
72
+ .sidebar-item[aria-current="page"],
73
+ .sidebar-item[data-active] {
74
+ @apply bg-primary-muted text-primary font-medium;
75
+ }
76
+
77
+ .sidebar-icon {
78
+ @apply inline-flex items-center justify-center size-4 shrink-0 text-text-muted;
79
+ }
80
+
81
+ .sidebar-item[aria-current="page"] .sidebar-icon,
82
+ .sidebar-item[data-active] .sidebar-icon {
83
+ @apply text-primary;
84
+ }
85
+
86
+ .sidebar-label {
87
+ @apply truncate min-w-0 flex-1;
88
+ }
89
+
90
+ .sidebar-badge {
91
+ @apply ml-auto inline-flex items-center justify-center min-w-5 px-1.5 h-5
92
+ rounded-full text-xs font-medium
93
+ bg-surface-strong text-text-muted;
94
+ }
95
+
96
+ /* Native <details> for tree groups. */
97
+ .sidebar-collapsible {
98
+ @apply flex flex-col;
99
+ interpolate-size: allow-keywords;
100
+ }
101
+
102
+ .sidebar-collapsible-trigger {
103
+ @apply flex items-center gap-2 w-full px-2 py-1.5
104
+ rounded-md text-sm leading-none text-text text-left
105
+ bg-transparent hover:bg-surface-strong
106
+ cursor-pointer select-none
107
+ transition-colors duration-150
108
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary;
109
+ list-style: none;
110
+ }
111
+
112
+ .sidebar-collapsible-trigger::-webkit-details-marker {
113
+ display: none;
114
+ }
115
+
116
+ /* Chevron — points right when closed, down when open. */
117
+ .sidebar-collapsible-trigger::after {
118
+ content: "";
119
+ margin-left: auto;
120
+ width: 0.375rem;
121
+ height: 0.375rem;
122
+ border-right: 2px solid currentColor;
123
+ border-bottom: 2px solid currentColor;
124
+ color: var(--color-text-muted);
125
+ transform: rotate(-45deg);
126
+ transition: transform 150ms ease;
127
+ flex-shrink: 0;
128
+ }
129
+
130
+ .sidebar-collapsible[open] > .sidebar-collapsible-trigger::after {
131
+ transform: rotate(45deg);
132
+ }
133
+
134
+ .sidebar-collapsible-panel {
135
+ @apply flex flex-col gap-0.5 pl-4 mt-0.5;
136
+ overflow: hidden;
137
+ }
138
+
139
+ /* Smooth panel expansion via ::details-content (modern browsers).
140
+ Older browsers degrade to the native instant toggle. */
141
+ .sidebar-collapsible::details-content {
142
+ opacity: 0;
143
+ height: 0;
144
+ overflow: clip;
145
+ transition:
146
+ opacity 150ms ease,
147
+ height 150ms ease,
148
+ content-visibility 150ms;
149
+ transition-behavior: allow-discrete;
150
+ }
151
+
152
+ .sidebar-collapsible[open]::details-content {
153
+ opacity: 1;
154
+ height: auto;
155
+ }
156
+
157
+ .sidebar-subitem {
158
+ @apply flex items-center gap-2 px-2 py-1
159
+ rounded-md text-sm leading-none text-text
160
+ bg-transparent hover:bg-surface-strong
161
+ cursor-pointer select-none no-underline
162
+ transition-colors duration-150
163
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary;
164
+ }
165
+
166
+ .sidebar-subitem[aria-current="page"],
167
+ .sidebar-subitem[data-active] {
168
+ @apply bg-primary-muted text-primary font-medium;
169
+ }
170
+
171
+ .sidebar-footer {
172
+ @apply flex flex-col gap-1 px-2 py-2 border-t border-border;
173
+ }
174
+
175
+ /* Label that toggles the hidden checkbox inside it. */
176
+ .sidebar-collapse-toggle {
177
+ @apply relative inline-flex items-center justify-center size-7
178
+ rounded-md text-text-muted bg-transparent
179
+ hover:bg-surface-strong hover:text-text
180
+ cursor-pointer select-none
181
+ transition-colors duration-150;
182
+ }
183
+
184
+ .sidebar-collapse-toggle:has(.sidebar-toggle:focus-visible) {
185
+ @apply outline-2 outline-offset-2 outline-primary;
186
+ }
187
+
188
+ /* Chevron-left when expanded; flips to chevron-right when collapsed. */
189
+ .sidebar-collapse-toggle::before {
190
+ content: "";
191
+ width: 0.5rem;
192
+ height: 0.5rem;
193
+ border-left: 2px solid currentColor;
194
+ border-bottom: 2px solid currentColor;
195
+ transform: rotate(45deg);
196
+ transition: transform 150ms ease;
197
+ }
198
+
199
+ .sidebar:has(.sidebar-toggle:checked) .sidebar-collapse-toggle::before {
200
+ transform: rotate(-135deg);
201
+ }
202
+
203
+ /* Mobile drawer surface (rendered inside a Base UI Dialog) */
204
+ .sidebar-drawer-backdrop {
205
+ @apply fixed inset-0 z-40 bg-black/40
206
+ transition-opacity duration-150;
207
+ }
208
+
209
+ .sidebar-drawer-backdrop[data-starting-style],
210
+ .sidebar-drawer-backdrop[data-ending-style] {
211
+ @apply opacity-0;
212
+ }
213
+
214
+ .sidebar-drawer {
215
+ @apply fixed inset-y-0 left-0 z-50 flex flex-col
216
+ bg-surface-muted text-text border-r border-border
217
+ transition-transform duration-150;
218
+ width: min(var(--app-shell-sidebar-w, 240px), 80vw);
219
+ }
220
+
221
+ .sidebar-drawer[data-starting-style],
222
+ .sidebar-drawer[data-ending-style] {
223
+ @apply -translate-x-full;
224
+ }
225
+ }
@@ -0,0 +1,40 @@
1
+ @layer components {
2
+ /* Border-driven spinner — the top edge takes the host's `currentColor` while
3
+ the rest stays muted, producing a visible rotating arc. Inline-block so it
4
+ drops into `.btn`, `.field-label`, etc., without disturbing flow layout. */
5
+ .spinner {
6
+ display: inline-block;
7
+ width: 1rem;
8
+ height: 1rem;
9
+ flex-shrink: 0;
10
+ border-radius: 9999px;
11
+ border: 2px solid color-mix(in oklab, currentColor 25%, transparent);
12
+ border-top-color: currentColor;
13
+ animation: spinner-spin 0.6s linear infinite;
14
+ }
15
+
16
+ .spinner-sm {
17
+ width: 0.75rem;
18
+ height: 0.75rem;
19
+ border-width: 1.5px;
20
+ }
21
+
22
+ .spinner-lg {
23
+ width: 1.5rem;
24
+ height: 1.5rem;
25
+ border-width: 3px;
26
+ }
27
+
28
+ /* Honour reduced-motion — pause rather than spin frantically. */
29
+ @media (prefers-reduced-motion: reduce) {
30
+ .spinner {
31
+ animation-duration: 2s;
32
+ }
33
+ }
34
+
35
+ @keyframes spinner-spin {
36
+ to {
37
+ transform: rotate(360deg);
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,62 @@
1
+ @layer components {
2
+ .switch {
3
+ @apply relative inline-flex items-center w-9 h-5 shrink-0 p-0.5
4
+ rounded-full border border-transparent
5
+ cursor-pointer
6
+ transition-colors duration-150
7
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
8
+ disabled:opacity-50 disabled:cursor-not-allowed;
9
+ }
10
+
11
+ /* Base UI button variant: state is driven by data-* attributes. */
12
+ .switch[data-unchecked] {
13
+ @apply bg-border-strong;
14
+ }
15
+
16
+ .switch[data-checked] {
17
+ @apply bg-primary;
18
+ }
19
+
20
+ .switch[data-disabled] {
21
+ @apply opacity-50 cursor-not-allowed;
22
+ }
23
+
24
+ .switch-thumb {
25
+ @apply size-4 rounded-full bg-paper shadow-sm
26
+ transition-transform duration-150;
27
+ }
28
+
29
+ .switch[data-checked] .switch-thumb {
30
+ @apply translate-x-4;
31
+ }
32
+
33
+ /* Native input variant: state is driven by :checked, thumb is ::before. */
34
+ input.switch {
35
+ @apply appearance-none bg-border-strong m-0;
36
+ }
37
+
38
+ input.switch:checked {
39
+ @apply bg-primary;
40
+ }
41
+
42
+ input.switch::before {
43
+ content: "";
44
+ @apply size-4 rounded-full bg-paper shadow-sm
45
+ transition-transform duration-150;
46
+ }
47
+
48
+ input.switch:checked::before {
49
+ @apply translate-x-4;
50
+ }
51
+
52
+ /* A <label> wrapping a switch + text lays out inline.
53
+ Switches are wider than checkboxes/radios so the gap is a touch larger. */
54
+ label:has(> .switch) {
55
+ @apply inline-flex items-center gap-3 cursor-pointer;
56
+ }
57
+
58
+ label:has(> .switch:disabled),
59
+ label:has(> .switch[data-disabled]) {
60
+ @apply opacity-60 cursor-not-allowed;
61
+ }
62
+ }
@@ -0,0 +1,124 @@
1
+ @layer components {
2
+ .table {
3
+ @apply w-full text-sm text-text border-collapse;
4
+ }
5
+
6
+ /* Default cell padding/alignment/divider — applied via descendant selectors
7
+ for hand-written markup AND via explicit classes for cases where the
8
+ consumer renders a non-<td>/<th> element. :where() keeps specificity at 0
9
+ so authored utilities and modifiers can win without !important. Covers
10
+ thead/tbody/tfoot uniformly so a hand-written `<tfoot> <td>` lands on
11
+ the same padding as the React `.table-cell`. */
12
+ .table :where(th, td),
13
+ .table-cell,
14
+ .table-header-cell {
15
+ @apply px-3 py-1.5 text-left align-middle border-b border-border;
16
+ }
17
+
18
+ .table :where(thead th),
19
+ .table-header-cell {
20
+ @apply font-medium text-text-muted bg-surface-muted border-b-border-strong whitespace-nowrap;
21
+ }
22
+
23
+ /* Drop the divider on the very last row, whether it lives in tbody (no
24
+ tfoot present) or in tfoot — so the table doesn't double-border against
25
+ a surrounding container. `.table > :last-child` resolves to the final
26
+ section element (browsers wrap a loose `<tr>` in an implicit <tbody>). */
27
+ .table > :last-child > tr:last-child :where(td),
28
+ .table > :last-child > tr:last-child .table-cell {
29
+ @apply border-b-0;
30
+ }
31
+
32
+ /* Alignment — `data-align` works equally for hand-written
33
+ `<td data-align="right">` and for React's `<Table.Cell align="right">`,
34
+ so consumers never need a Tailwind utility in their markup. */
35
+ .table :where(th, td)[data-align="right"] {
36
+ @apply text-right;
37
+ }
38
+ .table :where(th, td)[data-align="center"] {
39
+ @apply text-center;
40
+ }
41
+
42
+ /* Right-aligned with tabular-nums so currency/totals columns don't shimmy. */
43
+ .table-cell-numeric {
44
+ @apply text-right tabular-nums;
45
+ }
46
+
47
+ /* Narrow first-column gutter for row-level status icons. The inline icon
48
+ rule normalizes Tabler webfont (`<i class="ti …">`) against React's
49
+ `<IconCircleCheck size={16}>` SVG so both deliveries land at the same
50
+ 16px box and the same vertical baseline. */
51
+ .table-cell-gutter {
52
+ @apply w-6 px-0 text-center text-text-muted;
53
+ }
54
+ .table-cell-gutter > :is(i, svg) {
55
+ font-size: 1rem;
56
+ line-height: 1;
57
+ vertical-align: middle;
58
+ }
59
+
60
+ /* Modifiers compose independently — striped + sticky + relaxed is valid. */
61
+ .table-striped tbody tr:nth-child(even) :where(td) {
62
+ @apply bg-surface-muted;
63
+ }
64
+
65
+ .table-bordered {
66
+ @apply border border-border;
67
+ }
68
+ .table-bordered :where(th, td) {
69
+ @apply border border-border;
70
+ }
71
+
72
+ .table-relaxed :where(th, td) {
73
+ @apply px-4 py-3;
74
+ }
75
+
76
+ /* Sticky header — requires a scrolling ancestor (e.g.
77
+ `<div class="overflow-auto" style="max-height: …">`). The wrapper isn't
78
+ added by the component; it would have no vanilla equivalent and would
79
+ break compositions like `<details><table>…`. */
80
+ .table-sticky thead :where(th) {
81
+ @apply sticky top-0 z-10 bg-surface-muted;
82
+ }
83
+
84
+ /* Hover — always on. Admin tables expect a row-scan affordance. */
85
+ .table tbody tr {
86
+ @apply transition-colors duration-75;
87
+ }
88
+ .table tbody tr:hover :where(td) {
89
+ @apply bg-surface-muted;
90
+ }
91
+
92
+ /* Selection — pure CSS via :has(). Fires for native checkboxes,
93
+ Base UI's checkbox via [data-checked], and the explicit
94
+ [data-selected] hook for programmatic selection without a checkbox. */
95
+ .table tbody tr:has(input[type="checkbox"]:checked),
96
+ .table tbody tr:has(.checkbox[data-checked]),
97
+ .table tbody tr[data-selected] {
98
+ @apply bg-primary-muted;
99
+ }
100
+ .table tbody tr:has(input[type="checkbox"]:checked):hover :where(td),
101
+ .table tbody tr:has(.checkbox[data-checked]):hover :where(td),
102
+ .table tbody tr[data-selected]:hover :where(td) {
103
+ @apply bg-primary-muted;
104
+ }
105
+
106
+ /* Whole-row link — the first <a> in the row gets its hit-area expanded to
107
+ fill the row via ::after. Consumer still writes the actual anchor
108
+ (preserves <Link>, plain <a href>, etc.). Other in-row anchors/buttons
109
+ should sit above the spanning pseudo-element with `relative` + non-auto
110
+ z-index; `.btn` already qualifies. */
111
+ .table-row-link {
112
+ @apply relative cursor-pointer;
113
+ }
114
+ .table-row-link:hover :where(td) {
115
+ @apply bg-surface-muted;
116
+ }
117
+ .table-row-link a:first-of-type::after {
118
+ content: "";
119
+ @apply absolute inset-0;
120
+ }
121
+ .table-row-link:focus-within {
122
+ @apply outline-2 -outline-offset-2 outline-primary;
123
+ }
124
+ }