@aortl/admin-css 0.16.2 → 0.18.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.
@@ -96,4 +96,42 @@
96
96
 
97
97
  /* No `.card-warning .card-title` rule — the yellow-400 accent fails AA on
98
98
  the muted yellow surface. */
99
+
100
+ /* Full-bleed media. Sibling of .card-body; first/last placement picks up the
101
+ card's corner rounding without overflow on the root. */
102
+ .card-media {
103
+ @apply block w-full shrink-0 overflow-hidden;
104
+ }
105
+ .card-media:first-child {
106
+ border-top-left-radius: inherit;
107
+ border-top-right-radius: inherit;
108
+ }
109
+ .card-media:last-child {
110
+ border-bottom-left-radius: inherit;
111
+ border-bottom-right-radius: inherit;
112
+ }
113
+ .card-media > :is(img, video) {
114
+ @apply block w-full h-full object-cover;
115
+ }
116
+
117
+ /* Scroll region — header/actions pin, body scrolls; consumer sets the height.
118
+ Direct-child header/actions gain padding + a divider since inside
119
+ .card-body they carry none. */
120
+ .card-scroll > .card-header {
121
+ @apply shrink-0 px-5 py-3 border-b border-border;
122
+ }
123
+ .card-scroll > .card-body {
124
+ @apply flex-1 min-h-0 overflow-y-auto;
125
+ }
126
+ .card-scroll > .card-body:last-child {
127
+ /* Keep the scrollbar inside the rounded corners when there's no footer. */
128
+ border-bottom-left-radius: inherit;
129
+ border-bottom-right-radius: inherit;
130
+ }
131
+ .card-scroll > .card-actions {
132
+ @apply shrink-0 px-5 py-3 border-t border-border;
133
+ }
134
+ .card-compact.card-scroll > :is(.card-header, .card-actions) {
135
+ @apply px-3 py-2;
136
+ }
99
137
  }
@@ -0,0 +1,81 @@
1
+ @layer components {
2
+ /* A Dialog variant: the same native <dialog> machinery (showModal, ::backdrop,
3
+ @starting-style, allow-discrete transitions), edge-anchored instead of
4
+ centred. `.drawer` overrides `.dialog`'s centring; it imports after dialog.css
5
+ so equal-specificity rules win. The base opacity fade still applies. */
6
+ .drawer {
7
+ margin: 0;
8
+ max-width: none;
9
+ max-height: none;
10
+ border-radius: 0;
11
+ height: 100dvh;
12
+ width: min(28rem, 100vw);
13
+ /* Default: anchored to the inline-end (right in LTR) edge. */
14
+ margin-inline-start: auto;
15
+ transform: translateX(100%);
16
+ }
17
+ .drawer[open] {
18
+ transform: translateX(0);
19
+ }
20
+ @starting-style {
21
+ .drawer[open] {
22
+ transform: translateX(100%);
23
+ }
24
+ }
25
+
26
+ /* Inline-start (left in LTR) edge. */
27
+ .drawer-start {
28
+ margin-inline-start: 0;
29
+ margin-inline-end: auto;
30
+ transform: translateX(-100%);
31
+ }
32
+ @starting-style {
33
+ .drawer-start[open] {
34
+ transform: translateX(-100%);
35
+ }
36
+ }
37
+
38
+ /* Bottom sheet. */
39
+ .drawer-bottom {
40
+ width: 100vw;
41
+ height: auto;
42
+ max-height: 85dvh;
43
+ margin-block-start: auto;
44
+ margin-inline: 0;
45
+ transform: translateY(100%);
46
+ }
47
+ .drawer-bottom[open] {
48
+ transform: translateY(0);
49
+ }
50
+ @starting-style {
51
+ .drawer-bottom[open] {
52
+ transform: translateY(100%);
53
+ }
54
+ }
55
+
56
+ /* Sizes adjust the cross-axis extent. */
57
+ .drawer-sm {
58
+ width: min(20rem, 100vw);
59
+ }
60
+ .drawer-lg {
61
+ width: min(36rem, 100vw);
62
+ }
63
+ .drawer-bottom.drawer-sm {
64
+ width: 100vw;
65
+ max-height: 50dvh;
66
+ }
67
+ .drawer-bottom.drawer-lg {
68
+ width: 100vw;
69
+ max-height: 95dvh;
70
+ }
71
+
72
+ /* Slide is decorative — drop it under reduced motion, keep the opacity fade. */
73
+ @media (prefers-reduced-motion: reduce) {
74
+ .drawer {
75
+ transition:
76
+ display 150ms allow-discrete,
77
+ overlay 150ms allow-discrete,
78
+ opacity 150ms ease-out;
79
+ }
80
+ }
81
+ }
@@ -4,6 +4,7 @@
4
4
  @import "./container.css";
5
5
  @import "./badge.css";
6
6
  @import "./brand-tile.css";
7
+ @import "./avatar.css";
7
8
  @import "./kbd.css";
8
9
  @import "./indicator.css";
9
10
  @import "./spinner.css";
@@ -11,11 +12,14 @@
11
12
  @import "./breadcrumbs.css";
12
13
  @import "./pagination.css";
13
14
  @import "./property-list.css";
15
+ @import "./separator.css";
14
16
  @import "./button.css";
15
17
  @import "./button-group.css";
16
18
  @import "./link.css";
17
19
  @import "./input.css";
18
20
  @import "./input-group.css";
21
+ @import "./input-icon.css";
22
+ @import "./number-input.css";
19
23
  @import "./textarea.css";
20
24
  @import "./checkbox.css";
21
25
  @import "./radio.css";
@@ -23,7 +27,10 @@
23
27
  @import "./select.css";
24
28
  @import "./card.css";
25
29
  @import "./stat-card.css";
30
+ @import "./item.css";
31
+ @import "./timeline.css";
26
32
  @import "./dialog.css";
33
+ @import "./drawer.css";
27
34
  @import "./field.css";
28
35
  @import "./file-input.css";
29
36
  @import "./footer.css";
@@ -15,6 +15,18 @@
15
15
  .indicator:has(> .card) {
16
16
  --indicator-offset: 6px;
17
17
  }
18
+ .indicator:has(> .avatar) {
19
+ --indicator-offset: 5px;
20
+ }
21
+ .indicator:has(> .avatar-sm) {
22
+ --indicator-offset: 3px;
23
+ }
24
+ .indicator:has(> .avatar-lg) {
25
+ --indicator-offset: 6px;
26
+ }
27
+ .indicator:has(> .avatar-square) {
28
+ --indicator-offset: 2px;
29
+ }
18
30
 
19
31
  .indicator-item {
20
32
  @apply absolute z-[1] whitespace-nowrap;
@@ -0,0 +1,65 @@
1
+ @layer components {
2
+ /* Overlay wrapper — the .input child keeps its own border, background,
3
+ and focus outline; icons float above it, :has() padding clears them. */
4
+ .input-icon {
5
+ @apply relative inline-flex w-full items-center text-sm text-text-muted;
6
+ --input-icon-inset: calc(var(--spacing) * 3); /* matches .input px-3 */
7
+ --input-icon-pad: calc(var(--spacing) * 9);
8
+ }
9
+
10
+ .input-icon:has(> .input-sm) {
11
+ @apply text-xs;
12
+ --input-icon-inset: calc(var(--spacing) * 2.5);
13
+ --input-icon-pad: calc(var(--spacing) * 8);
14
+ }
15
+
16
+ .input-icon:has(> .input-lg) {
17
+ @apply text-base;
18
+ --input-icon-inset: calc(var(--spacing) * 4);
19
+ --input-icon-pad: calc(var(--spacing) * 10);
20
+ }
21
+
22
+ /* Decorative glyphs pass clicks through; an interactive trailing button doesn't. */
23
+ .input-icon > :where(i, svg) {
24
+ @apply absolute top-1/2 -translate-y-1/2 pointer-events-none;
25
+ }
26
+
27
+ .input-icon > :where(i, svg):first-child {
28
+ left: var(--input-icon-inset);
29
+ }
30
+
31
+ /* Trailing slot — a glyph or an interactive `.input-action` button after the input. */
32
+ .input-icon > .input ~ :where(i, svg, .input-action) {
33
+ right: var(--input-icon-inset);
34
+ }
35
+
36
+ .input-icon:has(> :where(i, svg):first-child) > .input {
37
+ padding-left: var(--input-icon-pad);
38
+ }
39
+
40
+ .input-icon:has(> .input ~ :where(i, svg, .input-action)) > .input {
41
+ padding-right: var(--input-icon-pad);
42
+ }
43
+
44
+ .input-icon:has(> .input:disabled) > :where(i, svg) {
45
+ @apply opacity-50;
46
+ }
47
+
48
+ /* Interactive trailing control (clear, password reveal). Positioned like the
49
+ trailing glyph but clickable, with a currentColor hover wash. */
50
+ .input-action {
51
+ @apply absolute top-1/2 -translate-y-1/2
52
+ inline-flex items-center justify-center
53
+ size-5 rounded
54
+ text-text-muted cursor-pointer
55
+ bg-transparent border-0
56
+ hover:text-text
57
+ focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-focus;
58
+ }
59
+ .input-action:hover {
60
+ background-color: color-mix(in oklab, currentColor 12%, transparent);
61
+ }
62
+ .input-icon:has(> .input:disabled) > .input-action {
63
+ @apply opacity-50 pointer-events-none;
64
+ }
65
+ }
@@ -0,0 +1,76 @@
1
+ @layer components {
2
+ /* Compact low-chrome row between Table and Card: media | content | actions.
3
+ For settings rows, member lists, notifications, search results. */
4
+ .item {
5
+ @apply flex items-center gap-3 w-full px-3 py-2 text-sm text-text;
6
+ }
7
+
8
+ .item-media {
9
+ @apply flex items-center shrink-0 text-text-muted;
10
+ }
11
+ .item-media > :is(i, svg) {
12
+ font-size: 1.25rem;
13
+ }
14
+
15
+ /* min-w-0 lets the text column shrink so long values wrap instead of pushing
16
+ the actions off the row. */
17
+ .item-content {
18
+ @apply flex flex-col gap-0.5 flex-1 min-w-0;
19
+ }
20
+ .item-title {
21
+ @apply font-medium leading-tight;
22
+ overflow-wrap: break-word;
23
+ }
24
+ .item-description {
25
+ @apply text-text-muted leading-snug;
26
+ text-wrap: pretty;
27
+ }
28
+
29
+ .item-actions {
30
+ @apply flex items-center gap-2 shrink-0 relative z-[1];
31
+ margin-inline-start: auto;
32
+ }
33
+
34
+ /* Variants — keep it flat: default / outline / muted only. */
35
+ .item-outline {
36
+ @apply border border-border rounded-lg;
37
+ }
38
+ .item-muted {
39
+ @apply bg-surface-muted rounded-lg;
40
+ }
41
+
42
+ /* Sizes */
43
+ .item-sm {
44
+ @apply gap-2 px-2 py-1.5 text-xs;
45
+ }
46
+ .item-lg {
47
+ @apply gap-4 px-4 py-3 text-base;
48
+ }
49
+
50
+ /* Divided stack. */
51
+ .item-group {
52
+ @apply flex flex-col;
53
+ }
54
+ .item-group > .item:not(:last-child) {
55
+ @apply border-b border-border;
56
+ }
57
+ .item-group-bordered {
58
+ @apply border border-border rounded-lg overflow-hidden;
59
+ }
60
+
61
+ /* Clickable row — the first nested <a> fills the row via ::after; actions keep
62
+ their own stacking context so they stay clickable (same trick as table-row-link). */
63
+ .item-link {
64
+ @apply relative cursor-pointer;
65
+ }
66
+ .item-link:hover {
67
+ @apply bg-surface-muted;
68
+ }
69
+ .item-link a:first-of-type::after {
70
+ content: "";
71
+ @apply absolute inset-0;
72
+ }
73
+ .item-link:focus-within {
74
+ @apply outline-2 -outline-offset-2 outline-focus;
75
+ }
76
+ }
@@ -102,6 +102,21 @@
102
102
  flex-shrink: 0;
103
103
  }
104
104
 
105
+ /* Checkbox/radio items — a leading indicator column whose check shows only when
106
+ aria-checked. In vanilla, set role=menuitemcheckbox/menuitemradio + aria-checked;
107
+ the gutter is always reserved so labels stay aligned across the group. */
108
+ .menu-item-indicator {
109
+ @apply inline-flex items-center justify-center shrink-0;
110
+ inline-size: 1rem;
111
+ block-size: 1rem;
112
+ }
113
+ .menu-item-indicator > :is(i, svg) {
114
+ opacity: 0;
115
+ }
116
+ .menu-item[aria-checked="true"] .menu-item-indicator > :is(i, svg) {
117
+ opacity: 1;
118
+ }
119
+
105
120
  .menu-separator {
106
121
  @apply h-px my-1 bg-border border-0;
107
122
  }
@@ -0,0 +1,64 @@
1
+ @layer components {
2
+ /* React's Base UI Root is a transparent wrapper; the visual group is `.number-input`. */
3
+ .number-input-root {
4
+ display: contents;
5
+ }
6
+
7
+ /* Connected group: − | input | +, mirroring .input-group's look. */
8
+ .number-input {
9
+ @apply inline-flex w-full items-stretch
10
+ rounded-lg border border-border bg-surface
11
+ transition-colors duration-150
12
+ focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-focus;
13
+ }
14
+ .number-input:hover {
15
+ @apply border-border-strong;
16
+ }
17
+
18
+ /* Borderless field: right-aligned tabular digits, native spinners hidden. */
19
+ .number-input-field {
20
+ @apply flex-1 min-w-0 w-full px-3 py-2
21
+ bg-transparent text-sm leading-none text-text text-right tabular-nums
22
+ border-0 outline-none
23
+ placeholder:text-text-muted
24
+ disabled:opacity-50 disabled:cursor-not-allowed;
25
+ -moz-appearance: textfield;
26
+ }
27
+ .number-input-field::-webkit-inner-spin-button,
28
+ .number-input-field::-webkit-outer-spin-button {
29
+ -webkit-appearance: none;
30
+ margin: 0;
31
+ }
32
+
33
+ /* Stepper buttons. */
34
+ .number-input-step {
35
+ @apply inline-flex items-center justify-center shrink-0
36
+ w-8 text-text-muted bg-transparent
37
+ cursor-pointer select-none border-0
38
+ hover:bg-surface-muted hover:text-text
39
+ disabled:opacity-50 disabled:cursor-not-allowed;
40
+ }
41
+ .number-input-step:first-child {
42
+ @apply border-r border-border rounded-l-lg;
43
+ }
44
+ .number-input-step:last-child {
45
+ @apply border-l border-border rounded-r-lg;
46
+ }
47
+ .number-input-step > :is(i, svg) {
48
+ font-size: 0.875rem;
49
+ }
50
+
51
+ /* Sizes */
52
+ .number-input-sm .number-input-field {
53
+ @apply text-xs px-2.5 py-1.5;
54
+ }
55
+ .number-input-sm .number-input-step {
56
+ @apply w-7;
57
+ }
58
+ .number-input-lg .number-input-field {
59
+ @apply text-base px-4 py-2.5;
60
+ }
61
+ .number-input-lg .number-input-step {
62
+ @apply w-9;
63
+ }
64
+ }
@@ -0,0 +1,13 @@
1
+ @layer components {
2
+ /* Works on <hr> (border reset) and any block element. Margins are zeroed —
3
+ spacing comes from the parent's gap or margin utilities. */
4
+ .separator {
5
+ @apply block h-px w-full m-0 shrink-0 border-0 bg-border;
6
+ }
7
+
8
+ .separator-vertical {
9
+ @apply w-px h-auto self-stretch;
10
+ /* Visible outside a stretching flex/grid cross axis. */
11
+ min-height: 1lh;
12
+ }
13
+ }
@@ -21,6 +21,39 @@
21
21
  @apply text-sm text-text-muted;
22
22
  }
23
23
 
24
+ /* Trend delta — caret drawn from borders (no icon dependency) plus a value.
25
+ `data-trend` rotates the caret; `data-intent` colors it, kept independent
26
+ because up is not always good (a rising error rate is bad). */
27
+ .stat-card-trend {
28
+ @apply inline-flex items-center gap-1 text-sm font-medium tabular-nums text-text-muted;
29
+ }
30
+ .stat-card-trend::before {
31
+ content: "";
32
+ width: 0;
33
+ height: 0;
34
+ border-inline: 0.3em solid transparent;
35
+ border-block-end: 0.42em solid currentColor;
36
+ }
37
+ .stat-card-trend[data-trend="down"]::before {
38
+ border-block-end: 0;
39
+ border-block-start: 0.42em solid currentColor;
40
+ }
41
+ .stat-card-trend[data-trend="flat"]::before {
42
+ width: 0.55em;
43
+ border-inline: 0;
44
+ border-block-end: 0;
45
+ border-block-start: 0.16em solid currentColor;
46
+ }
47
+ .stat-card-trend[data-intent="positive"] {
48
+ @apply text-success;
49
+ }
50
+ .stat-card-trend[data-intent="negative"] {
51
+ @apply text-danger;
52
+ }
53
+ .stat-card-trend[data-intent="neutral"] {
54
+ @apply text-text-muted;
55
+ }
56
+
24
57
  /* `.card-compact` targets `.card-body`; with no body, step the root instead. */
25
58
  .card-compact.stat-card {
26
59
  @apply p-3 gap-0.5;
@@ -16,6 +16,16 @@
16
16
  @apply font-medium text-text-muted bg-surface-muted border-b-border-strong whitespace-nowrap;
17
17
  }
18
18
 
19
+ /* Totals/footer — all tfoot rows are emphasized; the first row carries a
20
+ strong divider against the body. 2px so border-collapse picks it over the
21
+ tbody last row's 1px border-b (equal widths resolve to the upper cell). */
22
+ .table :where(tfoot :is(td, th)) {
23
+ @apply font-semibold;
24
+ }
25
+ .table :where(tfoot tr:first-child :is(td, th)) {
26
+ @apply border-t-2 border-t-border-strong;
27
+ }
28
+
19
29
  /* Body cells only — break long unbreakable values (IDs, hashes, URLs) instead
20
30
  of forcing the table into horizontal overflow. */
21
31
  .table :where(td),
@@ -111,4 +121,48 @@
111
121
  .table-row-link:focus-within {
112
122
  @apply outline-2 -outline-offset-2 outline-focus;
113
123
  }
124
+
125
+ /* Compact density — mirrors table-relaxed at the tight end. */
126
+ .table-compact :where(th, td),
127
+ .table-compact .table-cell,
128
+ .table-compact .table-header-cell {
129
+ @apply px-2 py-1 text-xs;
130
+ }
131
+
132
+ /* Empty state — a centered, muted message row (author sets colspan). */
133
+ .table :where(td).table-empty,
134
+ .table-empty {
135
+ @apply text-center text-text-muted py-8;
136
+ }
137
+ /* No hover affordance on the empty-state row. */
138
+ .table tbody tr:has(> .table-empty):hover :where(td) {
139
+ background-color: transparent;
140
+ }
141
+
142
+ /* Pinned first column — sticky against horizontal scroll. Requires an
143
+ overflow-x ancestor (the table adds no wrapper, same stance as table-sticky).
144
+ Each row state repaints the cell so scrolled content doesn't bleed through. */
145
+ .table-pin-col :where(thead th, tbody td, tfoot :is(td, th)):first-child {
146
+ position: sticky;
147
+ inset-inline-start: 0;
148
+ }
149
+ .table-pin-col tbody td:first-child {
150
+ z-index: 1;
151
+ background-color: var(--color-surface);
152
+ }
153
+ /* Header corner sits above the body column and the sticky header row. */
154
+ .table-pin-col :where(thead th):first-child {
155
+ z-index: 11;
156
+ }
157
+ .table-pin-col.table-striped tbody tr:nth-child(even) td:first-child {
158
+ background-color: var(--color-surface-muted);
159
+ }
160
+ .table-pin-col tbody tr:hover td:first-child {
161
+ background-color: var(--color-surface-muted);
162
+ }
163
+ .table-pin-col tbody tr:has(input[type="checkbox"]:checked) td:first-child,
164
+ .table-pin-col tbody tr:has(.checkbox[data-checked]) td:first-child,
165
+ .table-pin-col tbody tr[data-selected] td:first-child {
166
+ background-color: var(--color-primary-muted);
167
+ }
114
168
  }
@@ -0,0 +1,113 @@
1
+ @layer components {
2
+ /* Vertical event rail: an indicator gutter and a content column per <li>, with
3
+ a connector line drawn down the gutter that stops at the last item. */
4
+ .timeline {
5
+ list-style: none;
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ .timeline-item {
11
+ display: grid;
12
+ grid-template-columns: 1.5rem 1fr;
13
+ column-gap: 0.75rem;
14
+ padding-bottom: 1rem;
15
+ position: relative;
16
+ }
17
+ .timeline-item:last-child {
18
+ padding-bottom: 0;
19
+ }
20
+
21
+ /* Connector — vertical line centred in the gutter, hidden on the last item. */
22
+ .timeline-item:not(:last-child)::before {
23
+ content: "";
24
+ position: absolute;
25
+ inset-block: 0.75rem 0;
26
+ inset-inline-start: 0.75rem;
27
+ width: 2px;
28
+ transform: translateX(-50%);
29
+ background-color: var(--color-border);
30
+ }
31
+
32
+ .timeline-indicator {
33
+ grid-column: 1;
34
+ align-self: start;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ height: 1.5rem;
39
+ position: relative;
40
+ z-index: 1;
41
+ }
42
+ .timeline-dot {
43
+ display: inline-block;
44
+ width: 0.625rem;
45
+ height: 0.625rem;
46
+ border-radius: 9999px;
47
+ background-color: var(--color-border-strong);
48
+ box-shadow: 0 0 0 3px var(--color-surface);
49
+ }
50
+ /* Surface backing masks the connector running behind an icon indicator. */
51
+ .timeline-indicator > :is(i, svg) {
52
+ font-size: 1rem;
53
+ color: var(--color-text-muted);
54
+ background-color: var(--color-surface);
55
+ border-radius: 9999px;
56
+ }
57
+
58
+ .timeline-content {
59
+ grid-column: 2;
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 0.125rem;
63
+ min-width: 0;
64
+ padding-block: 0.125rem 0;
65
+ }
66
+ .timeline-title {
67
+ @apply text-sm font-medium text-text leading-tight;
68
+ }
69
+ .timeline-time {
70
+ @apply text-xs text-text-muted tabular-nums;
71
+ }
72
+ .timeline-description {
73
+ @apply text-sm text-text-muted;
74
+ text-wrap: pretty;
75
+ }
76
+
77
+ /* Status accents on the dot. */
78
+ .timeline-item-info .timeline-dot {
79
+ background-color: var(--color-info);
80
+ }
81
+ .timeline-item-success .timeline-dot {
82
+ background-color: var(--color-success);
83
+ }
84
+ .timeline-item-warning .timeline-dot {
85
+ background-color: var(--color-warning);
86
+ }
87
+ .timeline-item-danger .timeline-dot {
88
+ background-color: var(--color-danger);
89
+ }
90
+
91
+ /* Numbered variant — the gutter holds a numbered/checked marker (steps rail). */
92
+ .timeline-numbered .timeline-item {
93
+ grid-template-columns: 1.75rem 1fr;
94
+ }
95
+ .timeline-numbered .timeline-item:not(:last-child)::before {
96
+ inset-inline-start: 0.875rem;
97
+ inset-block: 1.75rem 0;
98
+ }
99
+ .timeline-numbered .timeline-indicator {
100
+ height: 1.75rem;
101
+ }
102
+ .timeline-marker {
103
+ @apply inline-flex items-center justify-center
104
+ size-7 rounded-full text-xs font-semibold tabular-nums
105
+ bg-surface-strong text-text-muted;
106
+ box-shadow: 0 0 0 3px var(--color-surface);
107
+ }
108
+ /* Completed and current steps fill with the primary ink. */
109
+ .timeline-item-success .timeline-marker,
110
+ .timeline-item-current .timeline-marker {
111
+ @apply bg-primary text-primary-content;
112
+ }
113
+ }