@aortl/admin-css 0.16.0 → 0.16.2

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 (45) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/admin.css +25 -4
  3. package/dist/admin.min.css +1 -1
  4. package/dist/admin.scoped.css +25 -4
  5. package/dist/admin.scoped.min.css +18 -15
  6. package/package.json +3 -2
  7. package/src/components/accordion.css +7 -10
  8. package/src/components/alert.css +6 -21
  9. package/src/components/app-shell.css +2 -4
  10. package/src/components/badge.css +1 -5
  11. package/src/components/breadcrumbs.css +3 -8
  12. package/src/components/button-group.css +7 -19
  13. package/src/components/button.css +7 -23
  14. package/src/components/card.css +13 -20
  15. package/src/components/chart.css +26 -83
  16. package/src/components/checkbox.css +5 -7
  17. package/src/components/code-block.css +2 -4
  18. package/src/components/container.css +3 -7
  19. package/src/components/dialog.css +13 -13
  20. package/src/components/field.css +4 -4
  21. package/src/components/footer.css +2 -3
  22. package/src/components/indicator.css +9 -28
  23. package/src/components/input-group.css +3 -6
  24. package/src/components/input.css +2 -5
  25. package/src/components/kbd.css +7 -13
  26. package/src/components/link.css +3 -5
  27. package/src/components/menu.css +11 -21
  28. package/src/components/navbar.css +3 -7
  29. package/src/components/pagination.css +1 -4
  30. package/src/components/progress.css +3 -7
  31. package/src/components/property-list.css +5 -14
  32. package/src/components/prose.css +7 -21
  33. package/src/components/radio.css +5 -5
  34. package/src/components/select.css +6 -9
  35. package/src/components/sidebar.css +2 -7
  36. package/src/components/spinner.css +1 -4
  37. package/src/components/stat-card.css +5 -16
  38. package/src/components/switch.css +5 -3
  39. package/src/components/table.css +20 -30
  40. package/src/components/tabs.css +14 -32
  41. package/src/components/textarea.css +3 -7
  42. package/src/components/tooltip.css +6 -16
  43. package/src/fonts.css +8 -28
  44. package/src/theme.css +32 -100
  45. package/src/utilities.css +11 -37
@@ -1,12 +1,10 @@
1
1
  @layer components {
2
- /* Native <details> drives open state — no JS needed. The popup is a
3
- position: absolute child of the relative .menu root. */
2
+ /* Native <details> drives open state — no JS. */
4
3
  .menu {
5
4
  @apply relative inline-block;
6
5
  }
7
6
 
8
- /* Behaviour — applied always so the trigger works regardless of
9
- additional visual classes (e.g. `btn btn-primary` for a split button). */
7
+ /* Behaviour only visual classes (e.g. `btn btn-primary`) may stack on top. */
10
8
  .menu-trigger {
11
9
  @apply inline-flex items-center gap-1.5
12
10
  cursor-pointer select-none
@@ -19,17 +17,14 @@
19
17
  display: none;
20
18
  }
21
19
 
22
- /* Default appearance — bows out when `.btn` is also on the trigger so
23
- button classes (bg, hover, padding, text colour) take over cleanly. */
20
+ /* Default appearance — bows out when `.btn` supplies its own. */
24
21
  .menu-trigger:not(.btn) {
25
22
  @apply px-2.5 py-1.5 rounded-md text-sm leading-none
26
23
  text-text bg-transparent
27
24
  hover:bg-surface-strong;
28
25
  }
29
26
 
30
- /* Chevron uses currentColor so it tracks the trigger's text colour —
31
- muted text on the default trigger, content colour on a btn-primary
32
- split button, etc. Sized in em so it scales with btn-sm / btn-lg. */
27
+ /* em-sized so the chevron scales with btn-sm / btn-lg. */
33
28
  .menu-trigger::after {
34
29
  content: "";
35
30
  width: 0.5em;
@@ -41,8 +36,7 @@
41
36
  flex-shrink: 0;
42
37
  }
43
38
 
44
- /* Icon-only triggers (e.g. the dropdown half of a split button) center
45
- their chevron instead of letting it sit at the start. */
39
+ /* A text-less trigger (split-button dropdown half) centers its chevron. */
46
40
  .menu-trigger:empty {
47
41
  @apply justify-center;
48
42
  }
@@ -59,15 +53,9 @@
59
53
  border border-border rounded-lg shadow-md;
60
54
  }
61
55
 
62
- /* CSS anchor positioning makes the popup `position: fixed` so it escapes
63
- ancestor `overflow: hidden`/`overflow: auto` clipping necessary when
64
- the menu lives inside a `<dialog>` (top-layer + overflow: hidden) or a
65
- scroll-clipped surface like a card body. `anchor-scope` keeps each
66
- menu's anchor lookups isolated so multiple menus on the page don't
67
- cross-target, and `position-try-fallbacks` flips the popup above the
68
- trigger when there isn't room below. Browsers without anchor
69
- positioning (Firefox, as of early 2026) fall back to the absolute
70
- rules above and clip as before. */
56
+ /* Anchor positioning makes the popup fixed so it escapes ancestor overflow
57
+ clipping (e.g. inside a <dialog>); browsers without it (Firefox, early
58
+ 2026) fall back to the absolute rules above and clip as before. */
71
59
  @supports (anchor-name: --x) {
72
60
  .menu {
73
61
  anchor-scope: --menu-trigger;
@@ -100,13 +88,15 @@
100
88
  hover:bg-surface-muted
101
89
  focus-visible:bg-surface-muted focus-visible:outline-none
102
90
  disabled:opacity-50 disabled:cursor-not-allowed;
91
+ /* The popup has no max-width — break long labels instead of growing it. */
92
+ overflow-wrap: break-word;
93
+ min-width: 0;
103
94
  }
104
95
 
105
96
  .menu-item[aria-disabled="true"] {
106
97
  @apply opacity-50 cursor-not-allowed;
107
98
  }
108
99
 
109
- /* Don't let a constrained menu width squish a leading/trailing icon. */
110
100
  .menu-trigger > :is(i, svg),
111
101
  .menu-item > :is(i, svg) {
112
102
  flex-shrink: 0;
@@ -1,7 +1,6 @@
1
1
  @layer components {
2
- /* 2px bottom stripe is always rendered defaults to a neutral gray via
3
- `--color-system-accent` and brand-shifts when the consuming app overrides
4
- that variable at :root (see also `.footer` and `.brand-tile`). */
2
+ /* The bottom stripe brand-shifts when the consuming app overrides
3
+ `--color-system-accent` at :root (see also `.footer` and `.brand-tile`). */
5
4
  .navbar {
6
5
  @apply flex items-center gap-3 px-4 h-12
7
6
  bg-surface-muted text-text
@@ -31,19 +30,16 @@
31
30
  @apply bg-primary-muted text-primary;
32
31
  }
33
32
 
34
- /* Keep brand/item icons at their intrinsic size in a crowded bar. */
35
33
  .navbar-brand > :is(i, svg),
36
34
  .navbar-item > :is(i, svg) {
37
35
  flex-shrink: 0;
38
36
  }
39
37
 
40
- /* Right-aligned slot for actions: shop selector, user menu, etc. */
41
38
  .navbar-actions {
42
39
  @apply flex items-center gap-2 ml-auto;
43
40
  }
44
41
 
45
- /* Hamburger; hidden above the md breakpoint. The three lines are drawn
46
- with one element + two box-shadow strokes — no SVG needed. */
42
+ /* Hamburger three lines from one element + two box-shadow strokes. */
47
43
  .navbar-mobile-toggle {
48
44
  @apply inline-flex items-center justify-center size-9 -ml-2
49
45
  rounded-md text-text bg-transparent
@@ -8,7 +8,6 @@
8
8
  @apply inline-flex;
9
9
  }
10
10
 
11
- /* Square button shared by number, prev/next, and ellipsis containers. */
12
11
  .page-link {
13
12
  @apply inline-flex items-center justify-center
14
13
  min-w-8 h-8 px-2
@@ -23,8 +22,7 @@
23
22
  disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none;
24
23
  }
25
24
 
26
- /* Current page — same surface as a sidebar's active item so the visual
27
- metaphor is consistent. */
25
+ /* Same surface as a sidebar's active item. */
28
26
  .page-link.active,
29
27
  .page-link[aria-current="page"] {
30
28
  @apply bg-primary-muted text-primary border-primary-muted;
@@ -34,7 +32,6 @@
34
32
  @apply opacity-50 cursor-not-allowed pointer-events-none;
35
33
  }
36
34
 
37
- /* Ellipsis — non-interactive placeholder between groups of pages. */
38
35
  .page-ellipsis {
39
36
  @apply inline-flex items-center justify-center
40
37
  min-w-8 h-8 px-2
@@ -1,7 +1,5 @@
1
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. */
2
+ /* The track is the element itself; the fill is the engine-specific value pseudo. */
5
3
  .progress {
6
4
  appearance: none;
7
5
  -webkit-appearance: none;
@@ -32,7 +30,6 @@
32
30
  transition: inline-size 200ms ease;
33
31
  }
34
32
 
35
- /* Sizes */
36
33
  .progress-sm {
37
34
  height: 0.25rem;
38
35
  }
@@ -54,9 +51,8 @@
54
51
  color: var(--color-danger);
55
52
  }
56
53
 
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. */
54
+ /* When indeterminate, WebKit hides the value pseudo and Firefox draws it
55
+ full-width blank both and animate a gradient on the bar itself. */
60
56
  .progress:indeterminate {
61
57
  background-image: linear-gradient(
62
58
  90deg,
@@ -1,6 +1,4 @@
1
1
  @layer components {
2
- /* Outer section — owns striped, hide-if-empty modifiers. Title
3
- (optional) and the items grid stack vertically. */
4
2
  .property-list {
5
3
  @apply flex flex-col text-sm text-text;
6
4
  }
@@ -9,8 +7,7 @@
9
7
  @apply text-sm font-bold text-text mb-2;
10
8
  }
11
9
 
12
- /* The <dl> itself is the two-track grid; <dt> auto-flows into column 1
13
- and <dd> into column 2. */
10
+ /* The <dl> is the grid; <dt>/<dd> auto-flow into the two tracks. */
14
11
  .property-list-items {
15
12
  display: grid;
16
13
  grid-template-columns: max-content 1fr;
@@ -25,15 +22,12 @@
25
22
  @apply text-text-muted;
26
23
  }
27
24
 
28
- /* `min-w-0` lets the value's grid track shrink below its content; pair it
29
- with `break-words` so long unbreakable values (IDs, hashes, emails, URLs,
30
- file paths) break instead of overflowing the column. */
25
+ /* `min-w-0` lets the track shrink below its content so long unbreakable
26
+ values break instead of overflowing the column. */
31
27
  .property-list-value {
32
28
  @apply gap-2 min-w-0 break-words;
33
29
  }
34
30
 
35
- /* Compact density — tighter rows for sidebar info blocks or very many
36
- short attributes. */
37
31
  .property-list-compact .property-list-title {
38
32
  @apply mb-1;
39
33
  }
@@ -43,7 +37,6 @@
43
37
  @apply px-2 py-0.5 min-h-6;
44
38
  }
45
39
 
46
- /* Numeric — right-align + tabular-nums on the value. */
47
40
  .property-list-value-numeric {
48
41
  justify-content: flex-end;
49
42
  @apply tabular-nums;
@@ -60,8 +53,7 @@
60
53
  display: none;
61
54
  }
62
55
 
63
- /* Copy button — always emitted by the Value but kept out of layout entirely
64
- unless this specific value opts in. */
56
+ /* Always emitted by the Value; kept out of layout unless the value opts in. */
65
57
  .property-list-copy {
66
58
  @apply inline-flex items-center justify-center
67
59
  ml-auto rounded
@@ -82,8 +74,7 @@
82
74
  pointer-events: none;
83
75
  }
84
76
 
85
- /* Reveal on hover of either the label or the value of a copyable row, on
86
- button focus, or while feedback is active. */
77
+ /* Reveal on hover of the row's label or value, on focus, or during feedback. */
87
78
  .property-list-label:has(+ .property-list-value-copyable):hover
88
79
  + .property-list-value
89
80
  .property-list-copy,
@@ -1,19 +1,13 @@
1
1
  @layer components {
2
- /*
3
- * Long-form rendered HTML markdown output, CMS bodies, anything you can't
4
- * annotate with the system's semantic class names. The global reset strips
5
- * margins, list markers, and link styling from bare elements (so admin chrome
6
- * stays neutral); `.prose` re-establishes them for its descendants only, using
7
- * the semantic tokens so it follows dark mode and `--color-*` overrides.
8
- *
9
- * Descendant rules use `:where()` to stay at specificity 0 — a consumer's own
10
- * `.prose a { … }` wins without `!important`.
11
- */
2
+ /* Styles rendered HTML (markdown, CMS bodies) that can't carry the system's
3
+ class names re-establishes what the global reset strips. Descendant rules
4
+ use `:where()` so a consumer's own `.prose a { }` wins without `!important`. */
12
5
  .prose {
13
6
  @apply text-sm leading-normal text-text;
7
+ /* Break long URLs/tokens; the scrolling `<pre>` keeps its own `white-space: pre`. */
8
+ overflow-wrap: break-word;
14
9
  }
15
10
 
16
- /* Vertical rhythm — space between flow blocks, none at the container edges. */
17
11
  .prose :where(p, ul, ol, blockquote, pre, table, figure) {
18
12
  @apply my-3;
19
13
  }
@@ -24,8 +18,7 @@
24
18
  margin-bottom: 0;
25
19
  }
26
20
 
27
- /* Headings — base.css already sizes h1–h3; here we add h4–h6 and the
28
- sectioning rhythm. The edge reset above zeroes a leading heading's margin. */
21
+ /* base.css already sizes h1–h3; add h4–h6 and the sectioning rhythm. */
29
22
  .prose :where(h1, h2, h3, h4, h5, h6) {
30
23
  @apply mt-6 mb-2 font-semibold leading-tight;
31
24
  }
@@ -36,7 +29,6 @@
36
29
  @apply text-xs uppercase tracking-wide text-text-muted;
37
30
  }
38
31
 
39
- /* Lists — restore the markers the reset removes. */
40
32
  .prose :where(ul) {
41
33
  @apply list-disc ps-5;
42
34
  }
@@ -50,7 +42,6 @@
50
42
  .prose :where(li) :where(p) {
51
43
  @apply my-1;
52
44
  }
53
- /* Nested lists hug their parent item rather than taking full block spacing. */
54
45
  .prose :where(li > ul, li > ol) {
55
46
  @apply my-1;
56
47
  }
@@ -62,15 +53,13 @@
62
53
  focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus;
63
54
  }
64
55
 
65
- /* Inline code — a tinted chip. */
66
56
  .prose :where(code) {
67
57
  @apply rounded border border-border bg-code-surface px-1 py-0.5
68
58
  font-mono text-code-text;
69
59
  font-size: 0.85em;
70
60
  }
71
61
 
72
- /* Code blocks — the .code-block surface; strip the inline chip off nested
73
- <code> so a highlighter's own tokens show through. */
62
+ /* Mirrors .code-block; nested <code> sheds the chip so highlighter tokens show. */
74
63
  .prose :where(pre) {
75
64
  @apply rounded-lg bg-code-surface p-3 font-mono text-sm text-code-text;
76
65
  white-space: pre;
@@ -81,12 +70,10 @@
81
70
  font-size: inherit;
82
71
  }
83
72
 
84
- /* Blockquote — a quiet inline-start rule. */
85
73
  .prose :where(blockquote) {
86
74
  @apply border-s-2 border-border-strong ps-3 text-text-muted;
87
75
  }
88
76
 
89
- /* Thematic break. */
90
77
  .prose :where(hr) {
91
78
  @apply my-6 border-0 border-t border-border;
92
79
  }
@@ -102,7 +89,6 @@
102
89
  @apply font-semibold text-text-muted;
103
90
  }
104
91
 
105
- /* Media + emphasis. */
106
92
  .prose :where(img) {
107
93
  @apply h-auto max-w-full rounded-md;
108
94
  }
@@ -24,9 +24,8 @@
24
24
  @apply inline-flex size-1.5 rounded-full bg-primary-content;
25
25
  }
26
26
 
27
- /* Native input variant: state is driven by :checked, the dot is ::after.
28
- Mirrors the Base UI span variant above so a vanilla <input class="radio">
29
- renders identically (the dot matches .radio-indicator's size + colour). */
27
+ /* Mirrors the Base UI span variant above the ::after dot must match
28
+ .radio-indicator's size and colour. */
30
29
  input.radio {
31
30
  @apply appearance-none border-border-strong hover:border-text-muted;
32
31
  }
@@ -49,10 +48,11 @@
49
48
  @apply flex-col gap-2 items-start;
50
49
  }
51
50
 
52
- /* A <label> wrapping a radio + text lays out inline with a small gap.
53
- Covers both the vanilla input.radio and Base UI's span.radio. */
54
51
  label:has(> .radio) {
55
52
  @apply inline-flex items-center gap-2 cursor-pointer;
53
+ /* Long labels break beside the `shrink-0` control instead of overflowing. */
54
+ overflow-wrap: break-word;
55
+ min-width: 0;
56
56
  }
57
57
 
58
58
  label:has(> .radio:disabled),
@@ -26,7 +26,6 @@
26
26
  @apply border-danger hover:border-danger focus-visible:outline-danger;
27
27
  }
28
28
 
29
- /* Sizes */
30
29
  .select-sm {
31
30
  @apply text-xs px-2.5 py-1.5;
32
31
  }
@@ -35,14 +34,9 @@
35
34
  @apply text-base px-4 py-2.5;
36
35
  }
37
36
 
38
- /* Native <select> usage: same class, but suppress the native chevron and
39
- supply our own via background-image so the look matches the Base UI
40
- trigger. A bare native <select> is a replaced element and can't host a
41
- tinted `::after` (the React trigger uses a real `.select-icon` SVG), and
42
- data URIs can't read CSS variables — so the chevron can't ride the
43
- `--color-text-muted` token directly. The stroke is Flexoki `base-500`
44
- (#848484), the same mid-gray as the token's dark-mode value, which reads
45
- as a neutral chevron in both light and dark mode. */
37
+ /* A replaced <select> can't host a tinted ::after and data URIs can't read
38
+ CSS variables, so the chevron is a background-image with a hard-coded
39
+ stroke: Flexoki base-500 (#848484), which reads neutral in both modes. */
46
40
  select.select {
47
41
  appearance: none;
48
42
  padding-right: 2rem;
@@ -90,6 +84,9 @@
90
84
  .select-item {
91
85
  @apply flex items-center gap-2 px-3 py-1.5 text-sm
92
86
  cursor-pointer select-none outline-none;
87
+ /* Break long option labels rather than grow the popup horizontally. */
88
+ overflow-wrap: break-word;
89
+ min-width: 0;
93
90
  }
94
91
 
95
92
  .select-item[data-highlighted] {
@@ -7,8 +7,7 @@
7
7
  transition: width 150ms ease;
8
8
  }
9
9
 
10
- /* Hidden checkbox that drives the collapsed state via :has(). Lives inside
11
- <Sidebar.CollapseToggle>, but :has() finds it from anywhere in .sidebar. */
10
+ /* Hidden checkbox; :has() reads the collapsed state from anywhere in .sidebar. */
12
11
  .sidebar-toggle {
13
12
  position: absolute;
14
13
  width: 1px;
@@ -24,7 +23,6 @@
24
23
  width: var(--app-shell-sidebar-w-collapsed, 56px);
25
24
  }
26
25
 
27
- /* Hide labels, group headings, badges, and tree panels in collapsed mode. */
28
26
  .sidebar:has(.sidebar-toggle:checked) .sidebar-label,
29
27
  .sidebar:has(.sidebar-toggle:checked) .sidebar-group-label,
30
28
  .sidebar:has(.sidebar-toggle:checked) .sidebar-badge,
@@ -33,7 +31,6 @@
33
31
  display: none;
34
32
  }
35
33
 
36
- /* Center the icon in collapsed mode. */
37
34
  .sidebar:has(.sidebar-toggle:checked) .sidebar-item,
38
35
  .sidebar:has(.sidebar-toggle:checked) .sidebar-subitem,
39
36
  .sidebar:has(.sidebar-toggle:checked) .sidebar-collapsible-trigger {
@@ -93,7 +90,6 @@
93
90
  bg-surface-strong text-text-muted;
94
91
  }
95
92
 
96
- /* Native <details> for tree groups. */
97
93
  .sidebar-collapsible {
98
94
  @apply flex flex-col;
99
95
  interpolate-size: allow-keywords;
@@ -136,8 +132,7 @@
136
132
  overflow: hidden;
137
133
  }
138
134
 
139
- /* Smooth panel expansion via ::details-content (modern browsers).
140
- Older browsers degrade to the native instant toggle. */
135
+ /* Browsers without ::details-content degrade to the instant native toggle. */
141
136
  .sidebar-collapsible::details-content {
142
137
  opacity: 0;
143
138
  height: 0;
@@ -1,7 +1,5 @@
1
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. */
2
+ /* The currentColor top edge over a muted ring reads as a rotating arc. */
5
3
  .spinner {
6
4
  display: inline-block;
7
5
  width: 1rem;
@@ -25,7 +23,6 @@
25
23
  border-width: 3px;
26
24
  }
27
25
 
28
- /* Honour reduced-motion — pause rather than spin frantically. */
29
26
  @media (prefers-reduced-motion: reduce) {
30
27
  .spinner {
31
28
  animation-duration: 2s;
@@ -1,11 +1,6 @@
1
1
  @layer components {
2
- /* A KPI tile is a `.card` shell with no inner `.card-body` — label / value /
3
- detail stack directly on the card. Pair `.stat-card` with `.card` in the
4
- markup so it inherits the surface, border, radius, and shadow, plus every
5
- card modifier: `.card-bordered`, `.card-compact`, and the color variants
6
- (`.card-primary`, `.card-muted`, …). `.stat-card` only adds the flat inner
7
- padding/gap and the inverted hierarchy (the value dominates, the label is a
8
- small annotation). */
2
+ /* Pairs with `.card` in the markup (no inner `.card-body`)inherits its
3
+ surface and modifiers; this class only adds the flat padding/gap. */
9
4
  .stat-card {
10
5
  @apply gap-1 p-4;
11
6
  }
@@ -14,7 +9,6 @@
14
9
  @apply flex items-center gap-2 text-sm text-text-muted font-medium;
15
10
  }
16
11
 
17
- /* Keep the label's leading icon at its intrinsic size. */
18
12
  .stat-card-label > :is(i, svg) {
19
13
  flex-shrink: 0;
20
14
  }
@@ -27,18 +21,13 @@
27
21
  @apply text-sm text-text-muted;
28
22
  }
29
23
 
30
- /* `.card-compact` tightens `.card-body`, but a stat card has no body — apply
31
- the same padding step to the stat-card root, with a tighter gap to match. */
24
+ /* `.card-compact` targets `.card-body`; with no body, step the root instead. */
32
25
  .card-compact.stat-card {
33
26
  @apply p-3 gap-0.5;
34
27
  }
35
28
 
36
- /* Color variants (`.card-primary`, `.card-success`, …) supply the tinted
37
- surface + border via `.card`. Here we tint the value to the matching accent
38
- — the headline metric is a stat card's analog of `.card-title`, so a status
39
- KPI reads its colour at a glance. `warning` is skipped for the same reason
40
- `.card-warning` skips its title: yellow-400 fails AA on the muted yellow
41
- surface. The tinted surface still carries the warning signal. */
29
+ /* Tint the value to the card variant's accent. `warning` is skipped like
30
+ `.card-warning`'s title: yellow-400 fails AA on the muted yellow surface. */
42
31
  .card-primary .stat-card-value {
43
32
  @apply text-primary;
44
33
  }
@@ -30,7 +30,7 @@
30
30
  @apply translate-x-4;
31
31
  }
32
32
 
33
- /* Native input variant: state is driven by :checked, thumb is ::before. */
33
+ /* Native input variant. */
34
34
  input.switch {
35
35
  @apply appearance-none bg-border-strong m-0;
36
36
  }
@@ -49,10 +49,12 @@
49
49
  @apply translate-x-4;
50
50
  }
51
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. */
52
+ /* Switches are wider than checkboxes/radios, so the label gap is larger. */
54
53
  label:has(> .switch) {
55
54
  @apply inline-flex items-center gap-3 cursor-pointer;
55
+ /* Let a long label break and shrink beside the shrink-0 track instead of overflowing. */
56
+ overflow-wrap: break-word;
57
+ min-width: 0;
56
58
  }
57
59
 
58
60
  label:has(> .switch:disabled),
@@ -3,12 +3,8 @@
3
3
  @apply w-full text-sm text-text border-collapse;
4
4
  }
5
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`. */
6
+ /* Descendant selectors cover hand-written markup; explicit classes cover
7
+ non-<td>/<th> renders. :where() keeps specificity 0 so authored utilities win. */
12
8
  .table :where(th, td),
13
9
  .table-cell,
14
10
  .table-header-cell {
@@ -20,18 +16,21 @@
20
16
  @apply font-medium text-text-muted bg-surface-muted border-b-border-strong whitespace-nowrap;
21
17
  }
22
18
 
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>). */
19
+ /* Body cells only break long unbreakable values (IDs, hashes, URLs) instead
20
+ of forcing the table into horizontal overflow. */
21
+ .table :where(td),
22
+ .table-cell {
23
+ overflow-wrap: break-word;
24
+ }
25
+
26
+ /* No divider on the final row — `> :last-child` is the last section, tbody or
27
+ tfoot (browsers wrap a loose <tr> in an implicit tbody). */
27
28
  .table > :last-child > tr:last-child :where(td),
28
29
  .table > :last-child > tr:last-child .table-cell {
29
30
  @apply border-b-0;
30
31
  }
31
32
 
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. */
33
+ /* `data-align` serves both hand-written markup and React's `align` prop. */
35
34
  .table :where(th, td)[data-align="right"] {
36
35
  @apply text-right;
37
36
  }
@@ -39,15 +38,12 @@
39
38
  @apply text-center;
40
39
  }
41
40
 
42
- /* Right-aligned with tabular-nums so currency/totals columns don't shimmy. */
43
41
  .table-cell-numeric {
44
42
  @apply text-right tabular-nums;
45
43
  }
46
44
 
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. */
45
+ /* Status-icon gutter; the child rule normalizes Tabler webfont <i> and React
46
+ SVG icons to the same 16px box and baseline. */
51
47
  .table-cell-gutter {
52
48
  @apply w-6 px-0 text-center text-text-muted;
53
49
  }
@@ -73,10 +69,8 @@
73
69
  @apply px-4 py-3;
74
70
  }
75
71
 
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>…`. */
72
+ /* Sticky header — requires a scrolling ancestor. The component adds no wrapper:
73
+ it'd have no vanilla equivalent and breaks compositions like <details><table>. */
80
74
  .table-sticky thead :where(th) {
81
75
  @apply sticky top-0 z-10 bg-surface-muted;
82
76
  }
@@ -89,9 +83,7 @@
89
83
  @apply bg-surface-muted;
90
84
  }
91
85
 
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. */
86
+ /* [data-selected] is the hook for programmatic selection without a checkbox. */
95
87
  .table tbody tr:has(input[type="checkbox"]:checked),
96
88
  .table tbody tr:has(.checkbox[data-checked]),
97
89
  .table tbody tr[data-selected] {
@@ -103,11 +95,9 @@
103
95
  @apply bg-primary-muted;
104
96
  }
105
97
 
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. */
98
+ /* The row's first <a> gets its hit-area expanded over the row via ::after;
99
+ other in-row anchors/buttons need `relative` + non-auto z-index to stay
100
+ clickable (`.btn` already qualifies). */
111
101
  .table-row-link {
112
102
  @apply relative cursor-pointer;
113
103
  }