@stisla/style 3.0.0-beta.8

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 (63) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +16 -0
  3. package/dist/accordion/accordion.css +194 -0
  4. package/dist/alert/alert.css +138 -0
  5. package/dist/autocomplete/autocomplete.css +193 -0
  6. package/dist/avatar/avatar.css +142 -0
  7. package/dist/avatar-group/avatar-group.css +42 -0
  8. package/dist/badge/badge.css +74 -0
  9. package/dist/breadcrumb/breadcrumb.css +71 -0
  10. package/dist/button/button.css +318 -0
  11. package/dist/button/index.d.ts +1 -0
  12. package/dist/button/index.js +6 -0
  13. package/dist/button-group/button-group.css +108 -0
  14. package/dist/card/card.css +219 -0
  15. package/dist/carousel/carousel.css +170 -0
  16. package/dist/checkbox/checkbox.css +98 -0
  17. package/dist/chunk-K45KLI3Y.js +74 -0
  18. package/dist/collapsible/collapsible.css +36 -0
  19. package/dist/combobox/combobox.css +106 -0
  20. package/dist/combobox/combobox.tomselect.css +251 -0
  21. package/dist/config-CARtrJ7I.d.ts +61 -0
  22. package/dist/dialog/dialog.css +258 -0
  23. package/dist/drawer/drawer.css +318 -0
  24. package/dist/empty-state/empty-state.css +138 -0
  25. package/dist/field/field.css +70 -0
  26. package/dist/icon-box/icon-box.css +64 -0
  27. package/dist/illustration/illustration.css +103 -0
  28. package/dist/index.d.ts +14 -0
  29. package/dist/index.js +60 -0
  30. package/dist/indicator/indicator.css +84 -0
  31. package/dist/input/input.css +220 -0
  32. package/dist/input-group/input-group.css +141 -0
  33. package/dist/kbd/kbd.css +55 -0
  34. package/dist/link/link.css +28 -0
  35. package/dist/list-group/list-group.css +261 -0
  36. package/dist/media/media.css +115 -0
  37. package/dist/menu/menu.css +237 -0
  38. package/dist/meter/meter.css +124 -0
  39. package/dist/navbar/navbar.css +170 -0
  40. package/dist/page/page.css +95 -0
  41. package/dist/pagination/pagination.css +125 -0
  42. package/dist/placeholders/placeholders.css +58 -0
  43. package/dist/popover/popover.css +251 -0
  44. package/dist/progress/progress.css +139 -0
  45. package/dist/radio/radio.css +81 -0
  46. package/dist/scroll-area/scroll-area.css +25 -0
  47. package/dist/scroll-area/scroll-area.overlayscrollbars.css +42 -0
  48. package/dist/select/select.css +282 -0
  49. package/dist/separator/separator.css +26 -0
  50. package/dist/sidebar/sidebar.css +493 -0
  51. package/dist/slider/slider.css +159 -0
  52. package/dist/spinner/spinner.css +65 -0
  53. package/dist/switch/switch.css +91 -0
  54. package/dist/table/table.css +284 -0
  55. package/dist/tabs/tabs.css +137 -0
  56. package/dist/textarea/textarea.css +99 -0
  57. package/dist/timeline/timeline.css +271 -0
  58. package/dist/toast/toast.css +267 -0
  59. package/dist/toggle/toggle.css +125 -0
  60. package/dist/toggle-group/toggle-group.css +87 -0
  61. package/dist/tooltip/tooltip.css +95 -0
  62. package/package.json +46 -0
  63. package/src/theme.css +151 -0
@@ -0,0 +1,261 @@
1
+ /* @stisla/style — List group. Ported from src/scss/components/_list-group.scss. A vertical stack of
2
+ * rows on a shared rounded surface (nav lists, settings panels, contact rolls). Interactive rows
3
+ * (a/button) get hover + focus automatically; static rows stay quiet. References the @theme tokens
4
+ * (colors var(--color-*), spacing/sizes --spacing(n), radius var(--radius-*)); only no-namespace
5
+ * customs use --st-* (border-width, duration). Knobs are --list-group-*. State via attributes / native
6
+ * (data-state / aria-current / aria-disabled / :disabled), no is-*. @layer components.
7
+ * Authoring rules: ../../../../PORTING.md */
8
+
9
+ @layer components {
10
+ .list-group {
11
+ display: flex;
12
+ flex-direction: column;
13
+ background-color: var(--list-group-bg, var(--color-surface));
14
+ color: var(--list-group-color, var(--color-foreground));
15
+ border: var(--list-group-border-width, var(--st-border-width)) solid
16
+ var(--list-group-border-color, var(--color-border));
17
+ border-radius: var(--list-group-radius, var(--radius-lg));
18
+ /* Clip square inner-row corners to the rounded outer shape (inset focus halos sit inside). */
19
+ overflow: hidden;
20
+ }
21
+
22
+ /* === Item === */
23
+ .list-group__item {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: var(--list-group-item-gap, --spacing(3));
27
+ padding: var(--list-group-item-padding-block, --spacing(3))
28
+ var(--list-group-item-padding-inline, --spacing(4));
29
+ color: var(--list-group-color, var(--color-foreground));
30
+ /* Transparent base lets variant tints + state paints stack on top. */
31
+ background-color: transparent;
32
+ /* Inter-row rule — the root border owns the top/bottom edges; first item stays flush. */
33
+ border-top: var(--st-border-width) solid var(--list-group-divider-color, var(--color-border));
34
+ position: relative;
35
+ transition:
36
+ background-color var(--list-group-transition-duration, var(--transition-duration-fast)) ease,
37
+ color var(--list-group-transition-duration, var(--transition-duration-fast)) ease;
38
+
39
+ &:first-child {
40
+ border-top: 0;
41
+ }
42
+ }
43
+
44
+ /* === Interactive rows === <a>/<button> stretch + get hover/focus; static rows stay quiet. */
45
+ a.list-group__item,
46
+ button.list-group__item {
47
+ width: 100%;
48
+ text-align: inherit;
49
+ cursor: pointer;
50
+ text-decoration: none;
51
+ }
52
+
53
+ /* Hover only on a quiet row — an active or disabled row must not flash the accent surface. */
54
+ a.list-group__item:hover:not(
55
+ [data-state="active"],
56
+ [aria-current="page"],
57
+ [aria-current="true"],
58
+ [aria-disabled="true"]
59
+ ),
60
+ button.list-group__item:hover:not(
61
+ [data-state="active"],
62
+ [aria-current="page"],
63
+ [aria-current="true"],
64
+ [aria-disabled="true"]
65
+ ):not(:disabled) {
66
+ background-color: var(--list-group-item-bg-hover, var(--color-accent));
67
+ color: var(--list-group-item-color-hover, var(--color-accent-foreground));
68
+ }
69
+
70
+ a.list-group__item:focus-visible,
71
+ button.list-group__item:focus-visible {
72
+ /* Inset halo so the root overflow: hidden doesn't clip it; z-index lifts it over the next divider. */
73
+ outline: 2px solid var(--list-group-ring, var(--color-ring));
74
+ outline-offset: -2px;
75
+ z-index: 1;
76
+ }
77
+
78
+ /* === Selection states === carved out of the hover rule via :not() above. */
79
+ .list-group__item[data-state="active"],
80
+ .list-group__item[aria-current="page"],
81
+ .list-group__item[aria-current="true"] {
82
+ background-color: var(--list-group-item-bg-active, var(--color-highlight));
83
+ color: var(--list-group-item-color-active, var(--color-foreground));
84
+ }
85
+
86
+ /* === Disabled === */
87
+ .list-group__item[aria-disabled="true"],
88
+ .list-group__item:disabled {
89
+ color: var(--list-group-item-color-disabled, var(--color-muted-foreground));
90
+ pointer-events: none;
91
+ cursor: not-allowed;
92
+ background-color: transparent;
93
+ }
94
+
95
+ /* === Leading icon === first-child <svg>/<i> pins to a fixed size + currentColor. */
96
+ .list-group__item > :is(svg, i):first-child {
97
+ width: var(--list-group-item-icon-size, --spacing(4));
98
+ height: var(--list-group-item-icon-size, --spacing(4));
99
+ flex-shrink: 0;
100
+ color: currentColor;
101
+ }
102
+
103
+ /* === Contextual variants === 10% soft fill. The border mix uses `in oklab` (not oklch) because the
104
+ surface is neutral: oklch would pull every intent toward red; oklab preserves each intent's hue. */
105
+ .list-group__item--primary {
106
+ background-color: color-mix(in oklab, var(--color-primary) 10%, var(--list-group-bg, var(--color-surface)));
107
+ color: var(--color-foreground);
108
+ }
109
+ .list-group__item--success {
110
+ background-color: color-mix(in oklab, var(--color-success) 10%, var(--list-group-bg, var(--color-surface)));
111
+ color: var(--color-foreground);
112
+ }
113
+ .list-group__item--info {
114
+ background-color: color-mix(in oklab, var(--color-info) 10%, var(--list-group-bg, var(--color-surface)));
115
+ color: var(--color-foreground);
116
+ }
117
+ .list-group__item--warning {
118
+ background-color: color-mix(in oklab, var(--color-warning) 10%, var(--list-group-bg, var(--color-surface)));
119
+ color: var(--color-foreground);
120
+ }
121
+ .list-group__item--danger {
122
+ background-color: color-mix(in oklab, var(--color-danger) 10%, var(--list-group-bg, var(--color-surface)));
123
+ color: var(--color-foreground);
124
+ }
125
+ .list-group__item--neutral {
126
+ background-color: var(--color-neutral);
127
+ color: var(--color-neutral-foreground);
128
+ }
129
+
130
+ /* === Flush === drop outer border + radius (for parents that already own a frame). */
131
+ .list-group--flush {
132
+ --list-group-bg: transparent;
133
+ border-width: 0;
134
+ border-radius: 0;
135
+ }
136
+
137
+ /* === Numbered === <ol> auto-counter; Preflight strips list-style, so restore via a counter. */
138
+ .list-group--numbered {
139
+ counter-reset: list-group;
140
+
141
+ > .list-group__item {
142
+ counter-increment: list-group;
143
+
144
+ &::before {
145
+ content: counters(list-group, ".") ". ";
146
+ }
147
+ }
148
+ }
149
+
150
+ /* === Horizontal === rows side by side; the divider moves from row-top to row-start. */
151
+ .list-group--horizontal {
152
+ flex-direction: row;
153
+
154
+ > .list-group__item {
155
+ border-top: 0;
156
+ border-inline-start: var(--st-border-width) solid var(--list-group-divider-color, var(--color-border));
157
+
158
+ &:first-child {
159
+ border-inline-start: 0;
160
+ }
161
+ }
162
+ }
163
+
164
+ /* Responsive horizontal — row layout above the named breakpoint, via Tailwind's `@variant <bp>`
165
+ (resolves to the theme's breakpoint, not a hardcoded rem). */
166
+ @variant sm {
167
+ .list-group--horizontal-sm {
168
+ flex-direction: row;
169
+ > .list-group__item {
170
+ border-top: 0;
171
+ border-inline-start: var(--st-border-width) solid var(--list-group-divider-color, var(--color-border));
172
+ &:first-child {
173
+ border-inline-start: 0;
174
+ }
175
+ }
176
+ }
177
+ }
178
+ @variant md {
179
+ .list-group--horizontal-md {
180
+ flex-direction: row;
181
+ > .list-group__item {
182
+ border-top: 0;
183
+ border-inline-start: var(--st-border-width) solid var(--list-group-divider-color, var(--color-border));
184
+ &:first-child {
185
+ border-inline-start: 0;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ @variant lg {
191
+ .list-group--horizontal-lg {
192
+ flex-direction: row;
193
+ > .list-group__item {
194
+ border-top: 0;
195
+ border-inline-start: var(--st-border-width) solid var(--list-group-divider-color, var(--color-border));
196
+ &:first-child {
197
+ border-inline-start: 0;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ @variant xl {
203
+ .list-group--horizontal-xl {
204
+ flex-direction: row;
205
+ > .list-group__item {
206
+ border-top: 0;
207
+ border-inline-start: var(--st-border-width) solid var(--list-group-divider-color, var(--color-border));
208
+ &:first-child {
209
+ border-inline-start: 0;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ @variant 2xl {
215
+ .list-group--horizontal-2xl {
216
+ flex-direction: row;
217
+ > .list-group__item {
218
+ border-top: 0;
219
+ border-inline-start: var(--st-border-width) solid var(--list-group-divider-color, var(--color-border));
220
+ &:first-child {
221
+ border-inline-start: 0;
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ /* === Card integration === a direct .card > .list-group sheds its own outer chrome so the card owns
228
+ the frame; edges merge with the card's header bottom / footer top / corner. */
229
+ .card > .list-group {
230
+ border-inline-width: 0;
231
+ border-radius: 0;
232
+ }
233
+ .card > .list-group:first-child {
234
+ border-top-width: 0;
235
+ border-start-start-radius: calc(var(--card-radius, var(--radius-lg)) - var(--st-border-width));
236
+ border-start-end-radius: calc(var(--card-radius, var(--radius-lg)) - var(--st-border-width));
237
+ }
238
+ .card > .card__header + .list-group {
239
+ border-top-width: 0;
240
+ }
241
+ .card > .list-group:last-child {
242
+ border-bottom-width: 0;
243
+ border-end-start-radius: calc(var(--card-radius, var(--radius-lg)) - var(--st-border-width));
244
+ border-end-end-radius: calc(var(--card-radius, var(--radius-lg)) - var(--st-border-width));
245
+ }
246
+ /* A list-group inside a card picks up the card's inline gutter so rows align with header/footer. */
247
+ .card .list-group {
248
+ --list-group-item-padding-inline: var(--card-padding-inline, --spacing(5));
249
+ }
250
+
251
+ @media (prefers-reduced-motion: reduce) {
252
+ .list-group__item {
253
+ transition: none;
254
+ }
255
+ }
256
+
257
+ /* Block — fills its container width per SPEC §9 escape ramp. */
258
+ .list-group--block {
259
+ width: 100%;
260
+ }
261
+ }
@@ -0,0 +1,115 @@
1
+ /* @stisla/style — Media. Ported from src/scss/components/_media.scss. Two-column media-object row:
2
+ * figure (icon / avatar / image) on the inline-start, text column, action on the inline-end. A
3
+ * multipurpose row primitive (user lists, settings, notifications, files). The flush modifier strips
4
+ * the border + bg for stacks (typically inside a .card with separators). References the @theme tokens
5
+ * (colors var(--color-*), spacing/sizes --spacing(n), type var(--text-*) / var(--leading-*) /
6
+ * var(--font-weight-*), radius var(--radius-*)); only no-namespace customs use --st-* (border-width,
7
+ * duration). Knobs are --media-*. State via attributes ([data-highlighted], :hover/:focus-visible on
8
+ * an interactive host), no is-*. @layer components. Authoring rules: ../../../../PORTING.md */
9
+
10
+ @layer components {
11
+ .media {
12
+ position: relative;
13
+ display: flex;
14
+ align-items: center;
15
+ gap: var(--media-gap, --spacing(3));
16
+ padding: var(--media-padding-block, --spacing(4)) var(--media-padding-inline, --spacing(4));
17
+ color: var(--media-color, var(--color-foreground));
18
+ background: var(--media-bg, var(--color-surface));
19
+ border: var(--media-border-width, var(--st-border-width)) solid
20
+ var(--media-border-color, var(--color-border));
21
+ border-radius: var(--media-radius, var(--radius-md));
22
+ }
23
+
24
+ /* === Modifiers === */
25
+
26
+ /* Strip chrome — the host parent owns the frame, so a stack of rows reads as one surface. */
27
+ .media--flush {
28
+ --media-bg: transparent;
29
+ --media-border-width: 0;
30
+ --media-radius: 0;
31
+ }
32
+
33
+ /* Lay parts top-to-bottom; the action's inline-end pin makes no sense on a column, so clear it. */
34
+ .media--vertical {
35
+ flex-direction: column;
36
+ align-items: stretch;
37
+ }
38
+ .media--vertical > .media__action {
39
+ margin-inline-start: 0;
40
+ }
41
+
42
+ /* === Interactive === when the row's root is itself the interactive surface (<a>/<button>), it
43
+ * earns hover + focus paint. Bg-tint only — .media carries three pinned text colors, so the row
44
+ * washes to a subtle accent and the type stays as-is. [data-highlighted] is the keyboard-nav hook. */
45
+ a.media,
46
+ button.media {
47
+ cursor: pointer;
48
+ transition: background-color var(--transition-duration-fast) ease;
49
+
50
+ @media (prefers-reduced-motion: reduce) {
51
+ transition: none;
52
+ }
53
+ }
54
+
55
+ a.media:hover,
56
+ button.media:hover,
57
+ .media[data-highlighted] {
58
+ background: var(--media-bg-hover, var(--color-accent));
59
+ }
60
+
61
+ .media:focus-visible {
62
+ outline: 2px solid var(--color-ring);
63
+ outline-offset: -2px;
64
+ }
65
+
66
+ /* === Parts === */
67
+
68
+ .media__figure {
69
+ flex-shrink: 0;
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ }
74
+
75
+ /* A bare <svg>/<i> dropped into the figure (no .icon-box / .avatar wrapper) pins to a sensible
76
+ size so rows align. :only-child gate so a multi-child figure keeps its own sizing. */
77
+ .media__figure > :is(svg, i):only-child {
78
+ width: --spacing(4.5);
79
+ height: --spacing(4.5);
80
+ flex-shrink: 0;
81
+ }
82
+
83
+ .media__content {
84
+ display: flex;
85
+ flex: 1 1 auto;
86
+ flex-direction: column;
87
+ min-width: 0;
88
+ gap: --spacing(0.5);
89
+ }
90
+
91
+ .media__title {
92
+ font-weight: var(--font-weight-semibold);
93
+ line-height: var(--leading-tight);
94
+ color: var(--color-foreground);
95
+ }
96
+
97
+ .media__description {
98
+ color: var(--color-muted-foreground);
99
+ line-height: var(--leading-normal);
100
+ }
101
+
102
+ .media__meta {
103
+ font-size: var(--text-xs);
104
+ line-height: var(--leading-snug);
105
+ color: var(--color-muted-foreground);
106
+ }
107
+
108
+ .media__action {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ gap: --spacing(2);
112
+ margin-inline-start: auto;
113
+ flex-shrink: 0;
114
+ }
115
+ }
@@ -0,0 +1,237 @@
1
+ /* @stisla/style — Menu. Ported from src/scss/components/_menu.scss (Base UI anatomy: root / trigger /
2
+ * popup / item). A floating surface of rows; the trigger is just a .button (no own class). The shared
3
+ * Sass menu-* mixins are expanded inline here with the --menu-* prefix (no shared base file — each
4
+ * surface owns its knobs). State lives in [data-state]/[data-highlighted]/[aria-*] attributes.
5
+ * References the @theme tokens: colors var(--color-*), sizes/spacing --spacing(n), type var(--text-*)
6
+ * / var(--leading-*) / var(--font-weight-*), radius var(--radius-*), shadow var(--shadow-*); only
7
+ * no-namespace customs use --st-* (border-width, duration, z-index); z-index routes through the z-index scale (--z-index-dropdown).
8
+ * The scroll-lock hook html[data-menu-open] is an attribute, recast from the legacy is-* class per
9
+ * the no-is-* convention. Positioning + keyboard nav (Floating UI) ship with the JS layer. @layer
10
+ * components. Authoring rules: ../../../../PORTING.md */
11
+
12
+ @layer components {
13
+ .menu {
14
+ position: relative;
15
+ /* inline-flex (not inline-block) so the wrapper isn't pulled into inline-baseline alignment
16
+ inside a flex parent. The single trigger child drives the wrapper height; the absolutely
17
+ positioned popup adds no contribution. */
18
+ display: inline-flex;
19
+ vertical-align: middle;
20
+ }
21
+
22
+ /* position: fixed (not absolute) so the popup escapes any ancestor with overflow: hidden; the JS
23
+ layer computes top/left in viewport coordinates to match. The surface (padding / type / fill /
24
+ rim / radius / shadow) is the expanded menu-surface mixin. */
25
+ .menu__popup {
26
+ padding: var(--menu-padding-block, --spacing(1)) var(--menu-padding-inline, --spacing(1));
27
+ font-size: var(--menu-font-size, var(--text-sm));
28
+ line-height: var(--leading-normal);
29
+ color: var(--menu-color, var(--color-foreground));
30
+ background-color: var(--menu-bg, var(--color-surface));
31
+ border: var(--menu-border-width, var(--st-border-width)) solid
32
+ var(--menu-border-color, var(--color-border));
33
+ border-radius: var(--menu-radius, var(--radius-md));
34
+ box-shadow: var(--menu-shadow, var(--shadow-md));
35
+ position: fixed;
36
+ top: 0;
37
+ left: 0;
38
+ z-index: var(--menu-z-index, var(--z-index-dropdown));
39
+ display: none;
40
+ flex-direction: column;
41
+ gap: var(--menu-gap, --spacing(0.5));
42
+ min-width: var(--menu-min-width, --spacing(40));
43
+ max-width: max-content;
44
+ margin: 0;
45
+ list-style: none;
46
+ opacity: 0;
47
+ transform: translateY(calc(--spacing(1) * -1));
48
+ transition:
49
+ opacity var(--menu-transition-duration, var(--transition-duration-fast)) ease,
50
+ transform var(--menu-transition-duration, var(--transition-duration-fast)) ease;
51
+ outline: none;
52
+ }
53
+
54
+ .menu__popup[data-state="open"] {
55
+ display: flex;
56
+ opacity: 1;
57
+ transform: none;
58
+ }
59
+
60
+ /* The JS layer caps the popup max-height to the viewport; with column flex, flex-shrink would
61
+ squish children, so pin them at natural size and let overflow-y scroll instead. */
62
+ .menu__popup > * {
63
+ flex-shrink: 0;
64
+ }
65
+
66
+ /* === Item === (expanded menu-item mixin). Concentric inner radius = popup radius − inline gutter. */
67
+ .menu__item {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: var(--menu-item-gap, --spacing(2));
71
+ width: 100%;
72
+ min-height: var(--menu-item-min-height, --spacing(8));
73
+ padding: var(--menu-item-padding-block, --spacing(0.5)) var(--menu-item-padding-inline, --spacing(3));
74
+ font: inherit;
75
+ color: var(--menu-color, var(--color-foreground));
76
+ text-align: start;
77
+ text-decoration: none;
78
+ background-color: transparent;
79
+ border: 0;
80
+ border-radius: var(
81
+ --menu-item-radius,
82
+ calc(var(--menu-radius, var(--radius-md)) - var(--menu-padding-inline, --spacing(1)))
83
+ );
84
+ cursor: pointer;
85
+ user-select: none;
86
+ transition:
87
+ background-color var(--transition-duration-fast) ease,
88
+ color var(--transition-duration-fast) ease;
89
+ }
90
+
91
+ /* Hover (mouse) + [data-highlighted] (keyboard nav) both paint accent; native :focus is excluded
92
+ so two highlight indicators don't compete. */
93
+ .menu__item:hover,
94
+ .menu__item[data-highlighted] {
95
+ color: var(--menu-item-color-hover, var(--color-accent-foreground));
96
+ background-color: var(--menu-item-bg-hover, var(--color-accent));
97
+ }
98
+
99
+ .menu__item:focus-visible {
100
+ outline: 2px solid var(--color-ring);
101
+ outline-offset: -2px;
102
+ }
103
+
104
+ /* Persistent selected — the currently-applied choice. Hover does not override it, so the active row
105
+ stays highlighted even on hover. */
106
+ .menu__item[aria-current="true"],
107
+ .menu__item[data-state="active"],
108
+ .menu__item[aria-current="true"]:hover,
109
+ .menu__item[aria-current="true"][data-highlighted],
110
+ .menu__item[data-state="active"]:hover,
111
+ .menu__item[data-state="active"][data-highlighted] {
112
+ color: var(--menu-item-color-active, var(--color-highlight-foreground));
113
+ background-color: var(--menu-item-bg-active, var(--color-highlight));
114
+ }
115
+
116
+ /* Disabled — :disabled covers <button>, [aria-disabled] covers <a>; pointer-events kills hover. */
117
+ .menu__item:disabled,
118
+ .menu__item[aria-disabled="true"] {
119
+ color: var(--menu-item-color-disabled, var(--color-muted-foreground));
120
+ background-color: transparent;
121
+ pointer-events: none;
122
+ }
123
+
124
+ /* Destructive — color flips to danger; hover uses a soft danger tint so it never matches the
125
+ standard accent fill. */
126
+ .menu__item--danger {
127
+ color: var(--menu-item-color-danger, var(--color-danger));
128
+ }
129
+
130
+ .menu__item--danger:hover,
131
+ .menu__item--danger[data-highlighted] {
132
+ color: var(--menu-item-color-danger, var(--color-danger));
133
+ background-color: var(
134
+ --menu-item-bg-danger-hover,
135
+ color-mix(in oklch, var(--color-danger) 12%, transparent)
136
+ );
137
+ }
138
+
139
+ /* === Slots === leading icon, pinned size, color tracks the row text (inherit) so it follows every
140
+ state automatically. */
141
+ .menu__item > :is(svg, i),
142
+ .menu__icon {
143
+ width: var(--menu-item-icon-size, --spacing(4));
144
+ height: var(--menu-item-icon-size, --spacing(4));
145
+ flex-shrink: 0;
146
+ color: var(--menu-item-icon-color, inherit);
147
+ }
148
+
149
+ /* Indicator slot — leading check for checkbox/radio items; footprint always reserved (visibility,
150
+ not display) so mixed checked/unchecked menus stay column-aligned. */
151
+ .menu__indicator {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ width: var(--menu-item-icon-size, --spacing(4));
156
+ height: var(--menu-item-icon-size, --spacing(4));
157
+ flex-shrink: 0;
158
+ color: inherit;
159
+ visibility: hidden;
160
+ }
161
+
162
+ .menu__item[data-state="checked"] .menu__indicator,
163
+ .menu__item[aria-checked="true"] .menu__indicator {
164
+ visibility: visible;
165
+ }
166
+
167
+ /* Trailing keyboard-shortcut chip — auto-margin pins it to the row end; inheritance keeps it
168
+ readable under hover/active paint. */
169
+ .menu__shortcut {
170
+ margin-inline-start: auto;
171
+ font-size: var(--menu-shortcut-font-size, var(--text-xs));
172
+ color: var(--menu-shortcut-color, var(--color-muted-foreground));
173
+ letter-spacing: 0.02em;
174
+ }
175
+
176
+ .menu__item:hover .menu__shortcut,
177
+ .menu__item[data-highlighted] .menu__shortcut,
178
+ .menu__item[aria-current="true"] .menu__shortcut,
179
+ .menu__item[data-state="active"] .menu__shortcut {
180
+ color: inherit;
181
+ }
182
+
183
+ /* === Group label === */
184
+ .menu__group-label {
185
+ display: flex;
186
+ align-items: center;
187
+ padding: var(--menu-group-label-padding-block, --spacing(1))
188
+ var(--menu-group-label-padding-inline, --spacing(3));
189
+ margin: 0;
190
+ font-size: var(--menu-group-label-font-size, var(--text-xs));
191
+ font-weight: var(--menu-group-label-font-weight, var(--font-weight-semibold));
192
+ color: var(--menu-group-label-color, var(--color-muted-foreground));
193
+ text-transform: none;
194
+ white-space: nowrap;
195
+ }
196
+
197
+ /* === Group === optional role="group" wrapper; tightens rhythm so its label sits flush against its
198
+ items without an extra row of menu gap. */
199
+ .menu__group {
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: var(--menu-gap, --spacing(0.5));
203
+ }
204
+
205
+ /* === Separator === */
206
+ .menu__separator {
207
+ height: 0;
208
+ margin: var(--menu-separator-margin-block, --spacing(2)) 0;
209
+ border: 0;
210
+ border-top: var(--st-border-width) solid var(--menu-separator-color, var(--color-border));
211
+ opacity: 1;
212
+ }
213
+
214
+ /* === Media integration === a borrowed .media row (role="menuitem") is flush, so it zeroed its own
215
+ radius; re-round it to the same concentric token .menu__item uses so its hover wash clips to the
216
+ matching curve. Both read the same token, so a --menu-radius / padding retune flows to both. */
217
+ .menu__popup .media--flush {
218
+ --media-radius: var(
219
+ --menu-item-radius,
220
+ calc(var(--menu-radius, var(--radius-md)) - var(--menu-padding-inline, --spacing(1)))
221
+ );
222
+ }
223
+ }
224
+
225
+ /* === Scroll lock === applied to <html> while any menu is open; the popup is position: fixed, so
226
+ without this the page would scroll under it and the JS would reposition every frame. */
227
+ html[data-menu-open] {
228
+ overflow: hidden;
229
+ scrollbar-gutter: stable;
230
+ }
231
+
232
+ @media (prefers-reduced-motion: reduce) {
233
+ .menu__popup,
234
+ .menu__item {
235
+ transition: none;
236
+ }
237
+ }