@akonwi/mica 0.1.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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/combobox.js +138 -0
- package/field.js +111 -0
- package/mica.css +1536 -0
- package/package.json +32 -0
- package/select.js +30 -0
- package/tabs.js +82 -0
- package/toast.js +100 -0
package/mica.css
ADDED
|
@@ -0,0 +1,1536 @@
|
|
|
1
|
+
/*! mica — custom elements, native behavior, nearly no JavaScript. */
|
|
2
|
+
|
|
3
|
+
/* Layer order. User's unlayered CSS always wins over all of it. */
|
|
4
|
+
@layer mica.tokens, mica.preset, mica.elements, mica.layout;
|
|
5
|
+
|
|
6
|
+
/* ------------------------------------------------------------------ *
|
|
7
|
+
* Tokens — mica's only opinion. Spacing and size scales.
|
|
8
|
+
* ------------------------------------------------------------------ */
|
|
9
|
+
@layer mica.tokens {
|
|
10
|
+
:root {
|
|
11
|
+
--space-2xs: 0.25rem;
|
|
12
|
+
--space-xs: 0.5rem;
|
|
13
|
+
--space-sm: 0.75rem;
|
|
14
|
+
--space-md: 1rem;
|
|
15
|
+
--space-lg: 1.5rem;
|
|
16
|
+
--space-xl: 2.5rem;
|
|
17
|
+
--space-2xl: 4rem;
|
|
18
|
+
|
|
19
|
+
--size-xs: 10rem;
|
|
20
|
+
--size-sm: 15rem;
|
|
21
|
+
--size-md: 20rem;
|
|
22
|
+
--size-lg: 30rem;
|
|
23
|
+
--size-xl: 40rem;
|
|
24
|
+
|
|
25
|
+
--measure: 60ch;
|
|
26
|
+
|
|
27
|
+
--font-body: system-ui, sans-serif;
|
|
28
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
29
|
+
--line-height: 1.5;
|
|
30
|
+
|
|
31
|
+
/* square is the design language; rounding back on is a theme:
|
|
32
|
+
:root { --radius-sm: .25rem; --radius-md: .375rem; --radius-lg: .75rem } */
|
|
33
|
+
--radius-sm: 0px;
|
|
34
|
+
--radius-md: 0px;
|
|
35
|
+
--radius-lg: 0px;
|
|
36
|
+
--radius-full: 9999px;
|
|
37
|
+
|
|
38
|
+
/* control heights, shadcn scale: sm 32 / md 36 / lg 40 */
|
|
39
|
+
--control-height-sm: 2rem;
|
|
40
|
+
--control-height: 2.25rem;
|
|
41
|
+
--control-height-lg: 2.5rem;
|
|
42
|
+
|
|
43
|
+
--focus-ring-width: 2px;
|
|
44
|
+
--focus-ring-offset: 2px;
|
|
45
|
+
--focus-ring-color: var(--color-accent);
|
|
46
|
+
|
|
47
|
+
/* checkmark glyph — SVG URIs can't read custom properties, so this
|
|
48
|
+
ships as a light-dark pair tracking on-primary (white / near-black) */
|
|
49
|
+
--check-glyph: light-dark(
|
|
50
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E"),
|
|
51
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23151922' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E"));
|
|
52
|
+
|
|
53
|
+
/* -------------------------------------------------------------- *
|
|
54
|
+
* Color. Two knobs — change the theme by changing the knobs:
|
|
55
|
+
* --hue: the accent hue (OKLCH degrees)
|
|
56
|
+
* --chroma: accent intensity
|
|
57
|
+
* Neutrals are faintly tinted toward the accent hue.
|
|
58
|
+
* Components only ever use semantic roles, never raw values.
|
|
59
|
+
* -------------------------------------------------------------- */
|
|
60
|
+
color-scheme: light dark;
|
|
61
|
+
|
|
62
|
+
--hue: 263;
|
|
63
|
+
--chroma: 0.21;
|
|
64
|
+
--neutral-chroma: 0;
|
|
65
|
+
|
|
66
|
+
/* status hues — knobs, same as the brand hue. statuses deliberately
|
|
67
|
+
do NOT ride --chroma: danger must stay urgent in a muted theme. */
|
|
68
|
+
--danger-hue: 25;
|
|
69
|
+
--success-hue: 150;
|
|
70
|
+
--warn-hue: 48; /* orange — amber reads as mud at solid lightness */
|
|
71
|
+
--status-chroma: 0.18;
|
|
72
|
+
|
|
73
|
+
/* Numbered scales — theme-relative (Radix convention): a step means
|
|
74
|
+
the same *thing* in both modes. 1 = app bg … 6–8 = borders …
|
|
75
|
+
9 = solid … 12 = high-contrast text. light-dark() is baked into
|
|
76
|
+
every step, so code written against steps gets dark mode free. */
|
|
77
|
+
--neutral-1: light-dark(oklch(100% var(--neutral-chroma) var(--hue)), oklch(14% var(--neutral-chroma) var(--hue)));
|
|
78
|
+
--neutral-2: light-dark(oklch(97.3% var(--neutral-chroma) var(--hue)), oklch(17% var(--neutral-chroma) var(--hue)));
|
|
79
|
+
--neutral-3: light-dark(oklch(95.2% var(--neutral-chroma) var(--hue)), oklch(20% var(--neutral-chroma) var(--hue)));
|
|
80
|
+
--neutral-4: light-dark(oklch(93% var(--neutral-chroma) var(--hue)), oklch(23% var(--neutral-chroma) var(--hue)));
|
|
81
|
+
--neutral-5: light-dark(oklch(91% var(--neutral-chroma) var(--hue)), oklch(26% var(--neutral-chroma) var(--hue)));
|
|
82
|
+
--neutral-6: light-dark(oklch(89% var(--neutral-chroma) var(--hue)), oklch(30% var(--neutral-chroma) var(--hue)));
|
|
83
|
+
--neutral-7: light-dark(oklch(82% var(--neutral-chroma) var(--hue)), oklch(37% var(--neutral-chroma) var(--hue)));
|
|
84
|
+
--neutral-8: light-dark(oklch(74% var(--neutral-chroma) var(--hue)), oklch(46% var(--neutral-chroma) var(--hue)));
|
|
85
|
+
--neutral-9: light-dark(oklch(55% var(--neutral-chroma) var(--hue)), oklch(58% var(--neutral-chroma) var(--hue)));
|
|
86
|
+
--neutral-10: light-dark(oklch(50% var(--neutral-chroma) var(--hue)), oklch(64% var(--neutral-chroma) var(--hue)));
|
|
87
|
+
--neutral-11: light-dark(oklch(44% var(--neutral-chroma) var(--hue)), oklch(71% var(--neutral-chroma) var(--hue)));
|
|
88
|
+
--neutral-12: light-dark(oklch(18% var(--neutral-chroma) var(--hue)), oklch(96% var(--neutral-chroma) var(--hue)));
|
|
89
|
+
|
|
90
|
+
/* accent scale: chroma tapers at the extremes, peaks at the solid */
|
|
91
|
+
--accent-1: light-dark(oklch(99% calc(var(--chroma) * 0.1) var(--hue)), oklch(19% calc(var(--chroma) * 0.15) var(--hue)));
|
|
92
|
+
--accent-2: light-dark(oklch(97.5% calc(var(--chroma) * 0.15) var(--hue)), oklch(23% calc(var(--chroma) * 0.2) var(--hue)));
|
|
93
|
+
--accent-3: light-dark(oklch(95% calc(var(--chroma) * 0.25) var(--hue)), oklch(27% calc(var(--chroma) * 0.35) var(--hue)));
|
|
94
|
+
--accent-4: light-dark(oklch(92% calc(var(--chroma) * 0.35) var(--hue)), oklch(31% calc(var(--chroma) * 0.5) var(--hue)));
|
|
95
|
+
--accent-5: light-dark(oklch(89% calc(var(--chroma) * 0.45) var(--hue)), oklch(35% calc(var(--chroma) * 0.6) var(--hue)));
|
|
96
|
+
--accent-6: light-dark(oklch(85% calc(var(--chroma) * 0.55) var(--hue)), oklch(39% calc(var(--chroma) * 0.7) var(--hue)));
|
|
97
|
+
--accent-7: light-dark(oklch(79% calc(var(--chroma) * 0.65) var(--hue)), oklch(46% calc(var(--chroma) * 0.8) var(--hue)));
|
|
98
|
+
--accent-8: light-dark(oklch(71% calc(var(--chroma) * 0.8) var(--hue)), oklch(58% calc(var(--chroma) * 0.9) var(--hue)));
|
|
99
|
+
--accent-9: light-dark(oklch(55% var(--chroma) var(--hue)), oklch(70% var(--chroma) var(--hue)));
|
|
100
|
+
--accent-10: light-dark(oklch(50% var(--chroma) var(--hue)), oklch(75% var(--chroma) var(--hue)));
|
|
101
|
+
--accent-11: light-dark(oklch(46% calc(var(--chroma) * 0.9) var(--hue)), oklch(80% calc(var(--chroma) * 0.9) var(--hue)));
|
|
102
|
+
--accent-12: light-dark(oklch(24% calc(var(--chroma) * 0.5) var(--hue)), oklch(94% calc(var(--chroma) * 0.3) var(--hue)));
|
|
103
|
+
|
|
104
|
+
/* Semantic roles — what components actually use. Defined in terms
|
|
105
|
+
of scale steps: knobs → scales → roles. */
|
|
106
|
+
--color-surface: var(--neutral-1);
|
|
107
|
+
--color-surface-raised: var(--neutral-2);
|
|
108
|
+
/* top-layer panels (dialog, menus, toasts): white in light like the
|
|
109
|
+
page; one step elevated in dark, where shadows can't carry lift */
|
|
110
|
+
--color-surface-overlay: light-dark(var(--neutral-1), var(--neutral-2));
|
|
111
|
+
--color-text: var(--neutral-12);
|
|
112
|
+
--color-text-muted: var(--neutral-11);
|
|
113
|
+
--color-border: var(--neutral-6);
|
|
114
|
+
--color-border-strong: var(--neutral-8);
|
|
115
|
+
--color-accent: var(--accent-9);
|
|
116
|
+
--color-on-accent: light-dark(
|
|
117
|
+
oklch(99% 0.01 var(--hue)),
|
|
118
|
+
oklch(18% 0.02 var(--hue)));
|
|
119
|
+
|
|
120
|
+
/* Primary — the action color, decoupled from accent (shadcn-school:
|
|
121
|
+
near-black light, near-white dark; the accent appears only where
|
|
122
|
+
attention is earned: focus, selection, checkmarks). Hover/active
|
|
123
|
+
are derived, so ANY value assigned to --color-primary gets correct
|
|
124
|
+
states free. Colorful primary is a two-line theme:
|
|
125
|
+
:root { --color-primary: var(--accent-9);
|
|
126
|
+
--color-on-primary: var(--color-on-accent); } */
|
|
127
|
+
--color-primary: var(--neutral-12);
|
|
128
|
+
--color-on-primary: var(--neutral-1);
|
|
129
|
+
--color-primary-hover: color-mix(in oklch, var(--color-primary) 90%, var(--color-surface));
|
|
130
|
+
--color-primary-active: color-mix(in oklch, var(--color-primary) 80%, var(--color-surface));
|
|
131
|
+
|
|
132
|
+
/* Status roles — the step-3/7/9/11 analogs, no full scales.
|
|
133
|
+
Off-role shades: raw oklch(L var(--status-chroma) var(--*-hue)). */
|
|
134
|
+
--color-danger-surface: light-dark(oklch(95% calc(var(--status-chroma) * 0.25) var(--danger-hue)), oklch(27% calc(var(--status-chroma) * 0.35) var(--danger-hue)));
|
|
135
|
+
--color-danger-border: light-dark(oklch(79% calc(var(--status-chroma) * 0.65) var(--danger-hue)), oklch(46% calc(var(--status-chroma) * 0.8) var(--danger-hue)));
|
|
136
|
+
--color-danger: light-dark(oklch(52% var(--status-chroma) var(--danger-hue)), oklch(70% var(--status-chroma) var(--danger-hue)));
|
|
137
|
+
--color-on-danger: light-dark(oklch(99% 0.01 var(--danger-hue)), oklch(18% 0.02 var(--danger-hue)));
|
|
138
|
+
--color-danger-text: light-dark(oklch(44% calc(var(--status-chroma) * 0.9) var(--danger-hue)), oklch(80% calc(var(--status-chroma) * 0.9) var(--danger-hue)));
|
|
139
|
+
|
|
140
|
+
--color-success-surface: light-dark(oklch(95% calc(var(--status-chroma) * 0.25) var(--success-hue)), oklch(27% calc(var(--status-chroma) * 0.35) var(--success-hue)));
|
|
141
|
+
--color-success-border: light-dark(oklch(79% calc(var(--status-chroma) * 0.65) var(--success-hue)), oklch(46% calc(var(--status-chroma) * 0.8) var(--success-hue)));
|
|
142
|
+
--color-success: light-dark(oklch(52% var(--status-chroma) var(--success-hue)), oklch(70% var(--status-chroma) var(--success-hue)));
|
|
143
|
+
--color-on-success: light-dark(oklch(99% 0.01 var(--success-hue)), oklch(18% 0.02 var(--success-hue)));
|
|
144
|
+
--color-success-text: light-dark(oklch(44% calc(var(--status-chroma) * 0.9) var(--success-hue)), oklch(80% calc(var(--status-chroma) * 0.9) var(--success-hue)));
|
|
145
|
+
|
|
146
|
+
/* warn solid runs lighter than its siblings — amber needs the
|
|
147
|
+
lightness to read as warning, and takes dark text in both modes */
|
|
148
|
+
--color-warn-surface: light-dark(oklch(95% calc(var(--status-chroma) * 0.25) var(--warn-hue)), oklch(27% calc(var(--status-chroma) * 0.35) var(--warn-hue)));
|
|
149
|
+
--color-warn-border: light-dark(oklch(79% calc(var(--status-chroma) * 0.65) var(--warn-hue)), oklch(46% calc(var(--status-chroma) * 0.8) var(--warn-hue)));
|
|
150
|
+
--color-warn: light-dark(oklch(78% var(--status-chroma) var(--warn-hue)), oklch(76% var(--status-chroma) var(--warn-hue)));
|
|
151
|
+
--color-on-warn: oklch(20% 0.03 var(--warn-hue));
|
|
152
|
+
--color-warn-text: light-dark(oklch(42% calc(var(--status-chroma) * 0.9) var(--warn-hue)), oklch(80% calc(var(--status-chroma) * 0.9) var(--warn-hue)));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ------------------------------------------------------------------ *
|
|
157
|
+
* Preset — constructive defaults, Kelp-school not Preflight-school:
|
|
158
|
+
* raw HTML should look finished, not like nothing. Zero-specificity
|
|
159
|
+
* (:where) inside a layer: doubly overridable.
|
|
160
|
+
* ------------------------------------------------------------------ */
|
|
161
|
+
@layer mica.preset {
|
|
162
|
+
*,
|
|
163
|
+
*::before,
|
|
164
|
+
*::after {
|
|
165
|
+
box-sizing: border-box;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
html {
|
|
169
|
+
-webkit-text-size-adjust: none;
|
|
170
|
+
text-size-adjust: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
:where(body) {
|
|
174
|
+
margin: 0;
|
|
175
|
+
min-block-size: 100svh;
|
|
176
|
+
font-family: var(--font-body);
|
|
177
|
+
line-height: var(--line-height);
|
|
178
|
+
background: var(--color-surface);
|
|
179
|
+
color: var(--color-text);
|
|
180
|
+
accent-color: var(--color-accent);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Vertical rhythm: flow content spaces itself; last child never
|
|
184
|
+
pushes against its container. Stacks override all of this with gap. */
|
|
185
|
+
:where(p, ol, ul, dl, dd, blockquote, pre, table, figure, fieldset,
|
|
186
|
+
details, address, audio, video, iframe) {
|
|
187
|
+
margin-block: 0 var(--flow-space, var(--space-md));
|
|
188
|
+
}
|
|
189
|
+
:where(p, ol, ul, dl, dd, blockquote, pre, table, figure, fieldset,
|
|
190
|
+
details, address, audio, video, iframe):last-child {
|
|
191
|
+
margin-block-end: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
:where(h1, h2, h3, h4, h5, h6) {
|
|
195
|
+
margin-block: 0 var(--space-sm);
|
|
196
|
+
line-height: 1.2;
|
|
197
|
+
text-wrap: balance;
|
|
198
|
+
}
|
|
199
|
+
/* Headings that follow content get breathing room above. */
|
|
200
|
+
:where(h2, h3, h4, h5, h6):where(:not(:first-child)) {
|
|
201
|
+
margin-block-start: var(--space-xl);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
:where(p) {
|
|
205
|
+
text-wrap: pretty;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* Media behaves like content, not like text. */
|
|
209
|
+
:where(img, svg, video, canvas, picture) {
|
|
210
|
+
display: block;
|
|
211
|
+
max-inline-size: 100%;
|
|
212
|
+
block-size: auto;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Form controls inherit typography instead of shipping 2009's. */
|
|
216
|
+
:where(button, input, select, textarea) {
|
|
217
|
+
font: inherit;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
:where(pre, code, kbd, samp) {
|
|
221
|
+
font-family: var(--font-mono);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* opt in to animating to/from intrinsic sizes (details, etc.) */
|
|
225
|
+
:where(html) {
|
|
226
|
+
interpolate-size: allow-keywords;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* Tokenized focus ring — the theming story for focus, day one. */
|
|
230
|
+
:where(:focus-visible) {
|
|
231
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
232
|
+
outline-offset: var(--focus-ring-offset);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
*,
|
|
237
|
+
*::before,
|
|
238
|
+
*::after {
|
|
239
|
+
animation-duration: 0.01ms !important;
|
|
240
|
+
animation-iteration-count: 1 !important;
|
|
241
|
+
transition-duration: 0.01ms !important;
|
|
242
|
+
scroll-behavior: auto !important;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ------------------------------------------------------------------ *
|
|
248
|
+
* Elements — Tier 1. Native elements styled directly; behavior is the
|
|
249
|
+
* browser's. Variants are classes: any collision with app CSS resolves
|
|
250
|
+
* in the user's favor because these live in a layer.
|
|
251
|
+
* ------------------------------------------------------------------ */
|
|
252
|
+
@layer mica.elements {
|
|
253
|
+
/* --- button (and things dressed as buttons) ---------------------- */
|
|
254
|
+
:where(button, .btn,
|
|
255
|
+
input:is([type="button"], [type="submit"], [type="reset"])) {
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
gap: var(--space-xs);
|
|
260
|
+
block-size: var(--control-height);
|
|
261
|
+
padding: 0 var(--space-md);
|
|
262
|
+
border: 1px solid var(--color-border-strong);
|
|
263
|
+
border-radius: var(--radius-md);
|
|
264
|
+
background: var(--neutral-2);
|
|
265
|
+
color: var(--color-text);
|
|
266
|
+
font-size: 0.875rem;
|
|
267
|
+
font-weight: 500;
|
|
268
|
+
text-decoration: none;
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
transition: background-color 0.15s, border-color 0.15s;
|
|
271
|
+
|
|
272
|
+
&:hover:not(:disabled, [aria-disabled="true"]) {
|
|
273
|
+
background: var(--neutral-3);
|
|
274
|
+
}
|
|
275
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
276
|
+
background: var(--neutral-4);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* variants */
|
|
280
|
+
&:where(.primary) {
|
|
281
|
+
background: var(--color-primary);
|
|
282
|
+
border-color: transparent;
|
|
283
|
+
color: var(--color-on-primary);
|
|
284
|
+
|
|
285
|
+
&:hover:not(:disabled, [aria-disabled="true"]) {
|
|
286
|
+
background: var(--color-primary-hover);
|
|
287
|
+
}
|
|
288
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
289
|
+
background: var(--color-primary-active);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* disabled = same fill at the base rule's reduced opacity — a
|
|
293
|
+
color-mix toward gray was tried and reads as an enabled
|
|
294
|
+
secondary button when primary is near-black */
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
&:where(.danger) {
|
|
298
|
+
background: var(--color-danger);
|
|
299
|
+
border-color: transparent;
|
|
300
|
+
color: var(--color-on-danger);
|
|
301
|
+
|
|
302
|
+
&:hover:not(:disabled, [aria-disabled="true"]) {
|
|
303
|
+
background: color-mix(in oklch, var(--color-danger) 88%, var(--neutral-12));
|
|
304
|
+
}
|
|
305
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
306
|
+
background: color-mix(in oklch, var(--color-danger) 76%, var(--neutral-12));
|
|
307
|
+
}
|
|
308
|
+
&:where(:disabled, [aria-disabled="true"]) {
|
|
309
|
+
background: color-mix(in oklch, var(--color-danger) 40%, var(--neutral-6));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* deliberately zero rest affordance — a ghost is invisible until
|
|
314
|
+
hover/focus. use .ghost for toolbars and icon rows, not as the
|
|
315
|
+
lone action on a page. */
|
|
316
|
+
&:where(.ghost) {
|
|
317
|
+
background: transparent;
|
|
318
|
+
border-color: transparent;
|
|
319
|
+
|
|
320
|
+
&:hover:not(:disabled, [aria-disabled="true"]) {
|
|
321
|
+
background: var(--neutral-3);
|
|
322
|
+
}
|
|
323
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
324
|
+
background: var(--neutral-4);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* sizes (shadcn: h-8 px-3 / h-10 px-6, text-sm throughout) */
|
|
329
|
+
&:where(.small) {
|
|
330
|
+
block-size: var(--control-height-sm);
|
|
331
|
+
padding-inline: var(--space-sm);
|
|
332
|
+
}
|
|
333
|
+
&:where(.large) {
|
|
334
|
+
block-size: var(--control-height-lg);
|
|
335
|
+
padding-inline: var(--space-lg);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
&:where(:disabled, [aria-disabled="true"]) {
|
|
339
|
+
cursor: not-allowed;
|
|
340
|
+
opacity: 0.55;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* --- fields: text inputs, textarea, select ----------------------- *
|
|
345
|
+
* One control family with buttons: same radius, same border weight.
|
|
346
|
+
* Focus ring comes from the preset; text fields match :focus-visible
|
|
347
|
+
* even on mouse focus, per spec.
|
|
348
|
+
* ------------------------------------------------------------------ */
|
|
349
|
+
:where(input, textarea, select):where(:not([type="checkbox"],
|
|
350
|
+
[type="radio"], [type="range"], [type="color"], [type="file"],
|
|
351
|
+
[type="button"], [type="submit"], [type="reset"],
|
|
352
|
+
[type="image"])) {
|
|
353
|
+
inline-size: 100%;
|
|
354
|
+
padding: var(--space-xs) var(--space-sm);
|
|
355
|
+
border: 1px solid var(--color-border-strong);
|
|
356
|
+
border-radius: var(--radius-md);
|
|
357
|
+
background: var(--color-surface);
|
|
358
|
+
color: var(--color-text);
|
|
359
|
+
line-height: 1.375; /* textarea rhythm; single-line heights are fixed below */
|
|
360
|
+
|
|
361
|
+
&::placeholder {
|
|
362
|
+
color: var(--color-text-muted);
|
|
363
|
+
opacity: 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
&:where(:disabled) {
|
|
367
|
+
cursor: not-allowed;
|
|
368
|
+
opacity: 0.55;
|
|
369
|
+
background: var(--neutral-2);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/* validation — only after the user has interacted (:user-invalid),
|
|
373
|
+
never on pristine fields. First consumer of the status roles. */
|
|
374
|
+
&:where(:user-invalid) {
|
|
375
|
+
border-color: var(--color-danger);
|
|
376
|
+
--focus-ring-color: var(--color-danger);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* single-line controls share the fixed control height; text centers
|
|
381
|
+
in the content box (16px field text stays — iOS zoom rule) */
|
|
382
|
+
:where(select:not([multiple], [size]),
|
|
383
|
+
input:not([type="checkbox"], [type="radio"], [type="range"],
|
|
384
|
+
[type="color"], [type="file"], [type="button"],
|
|
385
|
+
[type="submit"], [type="reset"], [type="image"])) {
|
|
386
|
+
block-size: var(--control-height);
|
|
387
|
+
padding-block: 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* --- customizable select, where the platform allows -------------- *
|
|
391
|
+
* appearance: base-select opts into styleable pickers (Chromium).
|
|
392
|
+
* Elsewhere this whole rule drops (unknown pseudo-element) and the
|
|
393
|
+
* native picker remains — progressive enhancement, zero JS.
|
|
394
|
+
* Both declarations are required for the opt-in, and grouping them
|
|
395
|
+
* means they fail together: never a half-opted-in select.
|
|
396
|
+
* ------------------------------------------------------------------ */
|
|
397
|
+
:where(select),
|
|
398
|
+
::picker(select) {
|
|
399
|
+
appearance: base-select;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
@supports (appearance: base-select) {
|
|
403
|
+
/* the trigger is a flex row; children default to stretch, which
|
|
404
|
+
top-aligns the value text inside the fixed control height */
|
|
405
|
+
:where(select) {
|
|
406
|
+
align-items: center;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
::picker(select) {
|
|
410
|
+
border: 1px solid var(--color-border);
|
|
411
|
+
border-radius: var(--radius-md);
|
|
412
|
+
background: var(--color-surface-overlay);
|
|
413
|
+
padding: var(--space-2xs);
|
|
414
|
+
margin-block: var(--space-2xs);
|
|
415
|
+
/* dark surfaces need heavier shadows to register at all */
|
|
416
|
+
box-shadow: 0 8px 24px light-dark(oklch(0 0 0 / 0.15), oklch(0 0 0 / 0.5));
|
|
417
|
+
/* positioning stays the UA's (position-try-order: most-block-size):
|
|
418
|
+
it can open upward on near-ties, but it guarantees no clipping.
|
|
419
|
+
Forcing block-end + flip-block was tried and clips off-viewport
|
|
420
|
+
in tight space — don't fight the picker's positioning. */
|
|
421
|
+
opacity: 1;
|
|
422
|
+
translate: 0 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* enter animates; close is instant (shadcn behavior) — the
|
|
426
|
+
transition only exists while open, so unmatching :open drops it */
|
|
427
|
+
:where(select):open::picker(select) {
|
|
428
|
+
transition:
|
|
429
|
+
opacity 0.12s,
|
|
430
|
+
translate 0.12s,
|
|
431
|
+
overlay 0.12s allow-discrete,
|
|
432
|
+
display 0.12s allow-discrete;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
@starting-style {
|
|
436
|
+
::picker(select) {
|
|
437
|
+
opacity: 0;
|
|
438
|
+
translate: 0 -4px;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
:where(select) :where(option) {
|
|
443
|
+
padding: var(--space-xs) var(--space-sm);
|
|
444
|
+
border-radius: var(--radius-sm);
|
|
445
|
+
cursor: pointer;
|
|
446
|
+
}
|
|
447
|
+
:where(select) :where(option):where(:hover, :focus-visible) {
|
|
448
|
+
background: var(--neutral-3);
|
|
449
|
+
outline: none; /* hover-follows-focus pattern; ring is redundant here */
|
|
450
|
+
}
|
|
451
|
+
:where(select) :where(option):checked {
|
|
452
|
+
font-weight: 500;
|
|
453
|
+
}
|
|
454
|
+
:where(select) :where(option)::checkmark {
|
|
455
|
+
color: var(--color-accent);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
:where(select)::picker-icon {
|
|
459
|
+
color: var(--color-text-muted);
|
|
460
|
+
transition: rotate 0.15s;
|
|
461
|
+
}
|
|
462
|
+
:where(select):open::picker-icon {
|
|
463
|
+
rotate: 180deg;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* opt-in via select.js: the picker overlays the trigger with the
|
|
467
|
+
selected option kept in place (macOS-native / Base UI behavior).
|
|
468
|
+
JS supplies only --m-sel-index; all geometry lives here.
|
|
469
|
+
offset = index × row (1lh + option padding) + picker padding/border;
|
|
470
|
+
max() keeps the top on-screen for selections deep in long lists. */
|
|
471
|
+
:where(select.picker-aligned)::picker(select) {
|
|
472
|
+
position-area: none;
|
|
473
|
+
margin-block: 0;
|
|
474
|
+
inset-inline-start: calc(anchor(left) - var(--space-2xs) - 1px);
|
|
475
|
+
min-inline-size: calc(anchor-size(width) + 2 * (var(--space-2xs) + 1px));
|
|
476
|
+
inset-block-start: max(
|
|
477
|
+
var(--space-2xs),
|
|
478
|
+
calc(
|
|
479
|
+
anchor(top) - var(--m-sel-index, 0) * (1lh + 2 * var(--space-xs)) -
|
|
480
|
+
var(--space-2xs) - 1px
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
/* unfold in place, don't slide */
|
|
484
|
+
translate: 0 0;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* textarea grows with content — zero JS */
|
|
489
|
+
:where(textarea) {
|
|
490
|
+
field-sizing: content;
|
|
491
|
+
min-block-size: 3lh;
|
|
492
|
+
resize: vertical;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* --- form grouping: label, fieldset, legend ---------------------- *
|
|
496
|
+
* Plain by default (shadcn-school): a fieldset is a titled group,
|
|
497
|
+
* not a bordered box. The legend reads as a small section heading.
|
|
498
|
+
* Space contents with m-vstack or rhythm.
|
|
499
|
+
* ------------------------------------------------------------------ */
|
|
500
|
+
:where(label) {
|
|
501
|
+
font-weight: 500;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
:where(fieldset) {
|
|
505
|
+
border: 0;
|
|
506
|
+
padding: 0;
|
|
507
|
+
margin: 0;
|
|
508
|
+
min-inline-size: 0; /* fieldsets otherwise refuse to shrink */
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
:where(legend) {
|
|
512
|
+
font-weight: 600;
|
|
513
|
+
padding: 0;
|
|
514
|
+
margin-block-end: var(--space-md);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* a disabled fieldset dims everything it silences */
|
|
518
|
+
:where(fieldset:disabled) {
|
|
519
|
+
color: var(--color-text-muted);
|
|
520
|
+
|
|
521
|
+
& :where(label) {
|
|
522
|
+
color: var(--color-text-muted);
|
|
523
|
+
cursor: not-allowed;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* --- checkbox, radio, switch: drawn, semantics stay native ------- */
|
|
528
|
+
:where(input:is([type="checkbox"], [type="radio"])) {
|
|
529
|
+
appearance: none;
|
|
530
|
+
inline-size: 1.25rem;
|
|
531
|
+
block-size: 1.25rem;
|
|
532
|
+
margin: 0;
|
|
533
|
+
flex-shrink: 0;
|
|
534
|
+
vertical-align: -0.25em;
|
|
535
|
+
border: 1px solid var(--color-border-strong);
|
|
536
|
+
border-radius: var(--radius-sm);
|
|
537
|
+
background: var(--color-surface) center / contain no-repeat;
|
|
538
|
+
cursor: pointer;
|
|
539
|
+
transition: background-color 0.15s, border-color 0.15s,
|
|
540
|
+
background-position 0.15s;
|
|
541
|
+
|
|
542
|
+
&:checked {
|
|
543
|
+
border-color: var(--color-primary);
|
|
544
|
+
background-color: var(--color-primary);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
&:disabled {
|
|
548
|
+
cursor: not-allowed;
|
|
549
|
+
opacity: 0.55;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
:where(input[type="checkbox"]:not(.switch)) {
|
|
554
|
+
&:checked {
|
|
555
|
+
background-image: var(--check-glyph);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* indeterminate dash — a gradient, no glyph needed */
|
|
559
|
+
&:indeterminate {
|
|
560
|
+
border-color: var(--color-primary);
|
|
561
|
+
background-color: var(--color-primary);
|
|
562
|
+
background-image: linear-gradient(
|
|
563
|
+
var(--color-on-primary),
|
|
564
|
+
var(--color-on-primary)
|
|
565
|
+
);
|
|
566
|
+
background-size: 55% 2px;
|
|
567
|
+
background-position: center;
|
|
568
|
+
background-repeat: no-repeat;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* radio: "square target" — a solid pip in every state. muted pip
|
|
573
|
+
unchecked, bright pip on the primary fill when checked. the round
|
|
574
|
+
radio is a theme:
|
|
575
|
+
input[type="radio"] {
|
|
576
|
+
border-radius: var(--radius-full);
|
|
577
|
+
background-image: radial-gradient(circle closest-side,
|
|
578
|
+
var(--color-on-primary) 40%, transparent 46%);
|
|
579
|
+
} */
|
|
580
|
+
:where(input[type="radio"]) {
|
|
581
|
+
background-image: linear-gradient(
|
|
582
|
+
var(--color-border-strong),
|
|
583
|
+
var(--color-border-strong)
|
|
584
|
+
);
|
|
585
|
+
background-position: center;
|
|
586
|
+
background-size: 8px 8px;
|
|
587
|
+
background-repeat: no-repeat;
|
|
588
|
+
|
|
589
|
+
&:checked {
|
|
590
|
+
background-image: linear-gradient(
|
|
591
|
+
var(--color-on-primary),
|
|
592
|
+
var(--color-on-primary)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* switch: a checkbox wearing a track; the thumb is a positioned
|
|
598
|
+
gradient (pseudo-elements don't render on inputs). square, per the
|
|
599
|
+
design language — a solid linear-gradient block */
|
|
600
|
+
:where(input[type="checkbox"].switch) {
|
|
601
|
+
inline-size: 2.25rem;
|
|
602
|
+
border-color: transparent;
|
|
603
|
+
border-radius: var(--radius-sm);
|
|
604
|
+
background-color: var(--neutral-7);
|
|
605
|
+
background-image: linear-gradient(white, white);
|
|
606
|
+
background-size: 1rem calc(100% - 4px);
|
|
607
|
+
background-position: 2px 50%;
|
|
608
|
+
background-repeat: no-repeat;
|
|
609
|
+
|
|
610
|
+
&:checked {
|
|
611
|
+
background-color: var(--color-primary);
|
|
612
|
+
background-position: calc(100% - 2px) 50%;
|
|
613
|
+
background-image: linear-gradient(
|
|
614
|
+
var(--color-on-primary),
|
|
615
|
+
var(--color-on-primary)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/* disabled + on: desaturate toward neutral, same recipe as disabled
|
|
621
|
+
.primary buttons — inert, not just faded (placed after the switch
|
|
622
|
+
block so it wins its background-color at equal specificity) */
|
|
623
|
+
:where(input:is([type="checkbox"], [type="radio"])):disabled:is(
|
|
624
|
+
:checked,
|
|
625
|
+
:indeterminate
|
|
626
|
+
) {
|
|
627
|
+
border-color: transparent;
|
|
628
|
+
background-color: color-mix(in oklch, var(--color-primary) 40%, var(--neutral-6));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* labels that wrap a check control become the click target */
|
|
632
|
+
:where(label:has(> input:is([type="checkbox"], [type="radio"]))) {
|
|
633
|
+
display: inline-flex;
|
|
634
|
+
align-items: center;
|
|
635
|
+
gap: var(--space-xs);
|
|
636
|
+
cursor: pointer;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/* a disabled control dims its whole label, not just itself */
|
|
640
|
+
:where(label:has(> input:disabled)) {
|
|
641
|
+
color: var(--color-text-muted);
|
|
642
|
+
cursor: not-allowed;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/* --- content: tables, lists, code, quotes ------------------------ */
|
|
646
|
+
:where(table) {
|
|
647
|
+
inline-size: 100%;
|
|
648
|
+
border-collapse: collapse;
|
|
649
|
+
font-variant-numeric: tabular-nums;
|
|
650
|
+
}
|
|
651
|
+
:where(caption) {
|
|
652
|
+
caption-side: bottom;
|
|
653
|
+
margin-block-start: var(--space-xs);
|
|
654
|
+
color: var(--color-text-muted);
|
|
655
|
+
font-size: 0.875rem;
|
|
656
|
+
text-align: start;
|
|
657
|
+
}
|
|
658
|
+
:where(th) {
|
|
659
|
+
text-align: start;
|
|
660
|
+
font-weight: 500;
|
|
661
|
+
color: var(--color-text-muted);
|
|
662
|
+
}
|
|
663
|
+
:where(th, td) {
|
|
664
|
+
padding: var(--space-sm);
|
|
665
|
+
border-block-end: 1px solid var(--color-border);
|
|
666
|
+
}
|
|
667
|
+
:where(tbody tr:last-child) :where(th, td) {
|
|
668
|
+
border-block-end: 0;
|
|
669
|
+
}
|
|
670
|
+
:where(tbody tr):hover {
|
|
671
|
+
background: var(--color-surface-raised);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
:where(ul, ol) {
|
|
675
|
+
padding-inline-start: var(--space-lg);
|
|
676
|
+
}
|
|
677
|
+
:where(li) {
|
|
678
|
+
margin-block: var(--space-2xs);
|
|
679
|
+
}
|
|
680
|
+
:where(li)::marker {
|
|
681
|
+
color: var(--color-text-muted);
|
|
682
|
+
}
|
|
683
|
+
:where(dt) {
|
|
684
|
+
font-weight: 500;
|
|
685
|
+
}
|
|
686
|
+
:where(dd) {
|
|
687
|
+
margin-inline-start: 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
:where(code, kbd, samp) {
|
|
691
|
+
font-size: 0.875em;
|
|
692
|
+
}
|
|
693
|
+
:where(:not(pre) > code) {
|
|
694
|
+
background: var(--color-surface-raised);
|
|
695
|
+
border: 1px solid var(--color-border);
|
|
696
|
+
padding: 0.125em 0.375em;
|
|
697
|
+
border-radius: var(--radius-sm);
|
|
698
|
+
}
|
|
699
|
+
:where(pre) {
|
|
700
|
+
background: var(--color-surface-raised);
|
|
701
|
+
border: 1px solid var(--color-border);
|
|
702
|
+
border-radius: var(--radius-sm);
|
|
703
|
+
padding: var(--space-md);
|
|
704
|
+
overflow-x: auto;
|
|
705
|
+
}
|
|
706
|
+
:where(kbd) {
|
|
707
|
+
background: var(--color-surface);
|
|
708
|
+
border: 1px solid var(--color-border-strong);
|
|
709
|
+
border-block-end-width: 2px;
|
|
710
|
+
padding: 0.125em 0.375em;
|
|
711
|
+
border-radius: var(--radius-sm);
|
|
712
|
+
font-size: 0.8em;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
:where(blockquote) {
|
|
716
|
+
border-inline-start: 2px solid var(--color-border-strong);
|
|
717
|
+
padding-inline-start: var(--space-md);
|
|
718
|
+
color: var(--color-text-muted);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
:where(hr) {
|
|
722
|
+
border: 0;
|
|
723
|
+
border-block-start: 1px solid var(--color-border-strong);
|
|
724
|
+
margin-block: var(--space-lg);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* --- progress & meter: thin bars (shadcn-school) ------------------ *
|
|
728
|
+
* vendor pseudos must stay in separate rules — an unknown selector
|
|
729
|
+
* in a list invalidates the whole rule.
|
|
730
|
+
* ------------------------------------------------------------------ */
|
|
731
|
+
:where(progress, meter) {
|
|
732
|
+
appearance: none;
|
|
733
|
+
inline-size: 100%;
|
|
734
|
+
block-size: 0.5rem;
|
|
735
|
+
border: 0;
|
|
736
|
+
border-radius: var(--radius-sm);
|
|
737
|
+
overflow: hidden;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
:where(progress) {
|
|
741
|
+
background: color-mix(in oklch, var(--color-primary) 20%, transparent);
|
|
742
|
+
}
|
|
743
|
+
:where(progress)::-webkit-progress-bar {
|
|
744
|
+
background: transparent;
|
|
745
|
+
}
|
|
746
|
+
:where(progress)::-webkit-progress-value {
|
|
747
|
+
background: var(--color-primary);
|
|
748
|
+
}
|
|
749
|
+
:where(progress)::-moz-progress-bar {
|
|
750
|
+
background: var(--color-primary);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* meter grades through the status roles */
|
|
754
|
+
:where(meter) {
|
|
755
|
+
background: var(--neutral-4);
|
|
756
|
+
}
|
|
757
|
+
:where(meter)::-webkit-meter-bar {
|
|
758
|
+
background: transparent;
|
|
759
|
+
border: 0;
|
|
760
|
+
block-size: 0.5rem;
|
|
761
|
+
}
|
|
762
|
+
:where(meter)::-webkit-meter-optimum-value {
|
|
763
|
+
background: var(--color-success);
|
|
764
|
+
}
|
|
765
|
+
:where(meter)::-webkit-meter-suboptimum-value {
|
|
766
|
+
background: var(--color-warn);
|
|
767
|
+
}
|
|
768
|
+
:where(meter)::-webkit-meter-even-less-good-value {
|
|
769
|
+
background: var(--color-danger);
|
|
770
|
+
}
|
|
771
|
+
:where(meter):-moz-meter-optimum::-moz-meter-bar {
|
|
772
|
+
background: var(--color-success);
|
|
773
|
+
}
|
|
774
|
+
:where(meter):-moz-meter-sub-optimum::-moz-meter-bar {
|
|
775
|
+
background: var(--color-warn);
|
|
776
|
+
}
|
|
777
|
+
:where(meter):-moz-meter-sub-sub-optimum::-moz-meter-bar {
|
|
778
|
+
background: var(--color-danger);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/* --- dialog: modal + drawer --------------------------------------- *
|
|
782
|
+
* The browser brings focus trapping, Esc, top layer, ::backdrop, and
|
|
783
|
+
* form[method=dialog] closing. Invoker commands (commandfor/command)
|
|
784
|
+
* make open/close zero-JS. Exit animations: transition-behavior
|
|
785
|
+
* allow-discrete + @starting-style.
|
|
786
|
+
* ------------------------------------------------------------------ */
|
|
787
|
+
:where(dialog) {
|
|
788
|
+
border: 1px solid var(--color-border);
|
|
789
|
+
border-radius: var(--radius-lg);
|
|
790
|
+
background: var(--color-surface-overlay);
|
|
791
|
+
color: var(--color-text);
|
|
792
|
+
/* sections carry their own padding; the dialog is a bare frame */
|
|
793
|
+
padding: 0;
|
|
794
|
+
overflow: hidden;
|
|
795
|
+
inline-size: calc(100vw - 2rem);
|
|
796
|
+
max-inline-size: var(--size-lg);
|
|
797
|
+
max-block-size: calc(100svh - 2rem);
|
|
798
|
+
box-shadow: 0 16px 48px light-dark(oklch(0 0 0 / 0.2), oklch(0 0 0 / 0.6));
|
|
799
|
+
opacity: 0;
|
|
800
|
+
scale: 0.95;
|
|
801
|
+
transition:
|
|
802
|
+
opacity 0.15s,
|
|
803
|
+
scale 0.15s,
|
|
804
|
+
translate 0.15s,
|
|
805
|
+
overlay 0.15s allow-discrete,
|
|
806
|
+
display 0.15s allow-discrete;
|
|
807
|
+
}
|
|
808
|
+
/* composition: dialog > header (title + description) / body / footer.
|
|
809
|
+
layout lives on [open] — unconditional display would defeat the
|
|
810
|
+
UA's display:none on closed dialogs */
|
|
811
|
+
:where(dialog[open]) {
|
|
812
|
+
display: flex;
|
|
813
|
+
flex-direction: column;
|
|
814
|
+
opacity: 1;
|
|
815
|
+
scale: 1;
|
|
816
|
+
|
|
817
|
+
& > :where(*) {
|
|
818
|
+
margin-block: 0; /* the dialog owns spacing, like a stack */
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* the body: whatever sits between header and footer. one wrapper
|
|
822
|
+
element; it pads itself and is the scrollable region */
|
|
823
|
+
& > :where(:not(header, footer, button.close)) {
|
|
824
|
+
padding: var(--space-md);
|
|
825
|
+
flex: 1;
|
|
826
|
+
min-block-size: 0;
|
|
827
|
+
overflow-y: auto;
|
|
828
|
+
font-size: 0.875rem; /* body must not outweigh the 14px title */
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/* corner close button — <button class="close" commandfor="…"
|
|
833
|
+
command="close" aria-label="Close">✕</button> (or wrap it in a
|
|
834
|
+
method=dialog form). absolute is safe: the dialog is positioned.
|
|
835
|
+
shared with toasts (popovertarget/popovertargetaction=hide there). */
|
|
836
|
+
:where(dialog, [popover].toast) > :where(button.close) {
|
|
837
|
+
position: absolute;
|
|
838
|
+
inset-block-start: var(--space-sm);
|
|
839
|
+
inset-inline-end: var(--space-sm);
|
|
840
|
+
padding: var(--space-2xs);
|
|
841
|
+
border: 0;
|
|
842
|
+
background: transparent;
|
|
843
|
+
color: var(--color-text-muted);
|
|
844
|
+
line-height: 1;
|
|
845
|
+
|
|
846
|
+
&:hover:not(:disabled, [aria-disabled="true"]) {
|
|
847
|
+
background: var(--neutral-3);
|
|
848
|
+
color: var(--color-text);
|
|
849
|
+
}
|
|
850
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
851
|
+
background: var(--neutral-4);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
:where(dialog) > :where(header) {
|
|
856
|
+
display: flex;
|
|
857
|
+
flex-direction: column;
|
|
858
|
+
gap: var(--space-2xs);
|
|
859
|
+
padding: var(--space-md);
|
|
860
|
+
border-block-end: 1px solid var(--color-border);
|
|
861
|
+
|
|
862
|
+
/* long titles must not run under the corner X */
|
|
863
|
+
:where(dialog:has(> button.close)) > & {
|
|
864
|
+
padding-inline-end: calc(var(--space-md) + var(--space-xl));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
& > :where(*) {
|
|
868
|
+
margin-block: 0;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
& :where(h1, h2, h3, h4) {
|
|
872
|
+
font-size: 0.875rem;
|
|
873
|
+
font-weight: 500;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
& :where(p) {
|
|
877
|
+
color: var(--color-text-muted);
|
|
878
|
+
font-size: 0.75rem;
|
|
879
|
+
line-height: 1.6;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
:where(dialog) > :where(footer) {
|
|
884
|
+
display: flex;
|
|
885
|
+
flex-wrap: wrap;
|
|
886
|
+
gap: var(--space-sm);
|
|
887
|
+
justify-content: flex-end;
|
|
888
|
+
padding: var(--space-md);
|
|
889
|
+
border-block-start: 1px solid var(--color-border);
|
|
890
|
+
|
|
891
|
+
& > :where(*) {
|
|
892
|
+
margin-block: 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* a method=dialog form is the idiomatic close mechanism — let its
|
|
896
|
+
buttons participate in the footer's row directly */
|
|
897
|
+
& > :where(form[method="dialog" i]) {
|
|
898
|
+
display: contents;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
@starting-style {
|
|
902
|
+
:where(dialog[open]) {
|
|
903
|
+
opacity: 0;
|
|
904
|
+
scale: 0.95;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
:where(dialog)::backdrop {
|
|
909
|
+
background: transparent;
|
|
910
|
+
transition:
|
|
911
|
+
background-color 0.15s,
|
|
912
|
+
overlay 0.15s allow-discrete,
|
|
913
|
+
display 0.15s allow-discrete;
|
|
914
|
+
}
|
|
915
|
+
:where(dialog[open])::backdrop {
|
|
916
|
+
background: oklch(0 0 0 / 0.4);
|
|
917
|
+
}
|
|
918
|
+
@starting-style {
|
|
919
|
+
:where(dialog[open])::backdrop {
|
|
920
|
+
background: transparent;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/* drawer (shadcn sheet anatomy): inline-end panel, fade + 2.5rem
|
|
925
|
+
nudge rather than a full slide; no section dividers; footer pinned
|
|
926
|
+
to the bottom with stacked actions */
|
|
927
|
+
:where(dialog.drawer) {
|
|
928
|
+
margin-inline: auto 0;
|
|
929
|
+
margin-block: 0;
|
|
930
|
+
block-size: 100svh;
|
|
931
|
+
max-block-size: 100svh;
|
|
932
|
+
inline-size: 75vw;
|
|
933
|
+
max-inline-size: 24rem; /* sheet's max-w-sm */
|
|
934
|
+
border: 0;
|
|
935
|
+
border-inline-start: 1px solid var(--color-border);
|
|
936
|
+
font-size: 0.75rem;
|
|
937
|
+
line-height: 1.6;
|
|
938
|
+
scale: 1;
|
|
939
|
+
translate: 2.5rem 0;
|
|
940
|
+
}
|
|
941
|
+
:where(dialog.drawer[open]) {
|
|
942
|
+
translate: 0 0;
|
|
943
|
+
|
|
944
|
+
& > :where(:not(header, footer, button.close)) {
|
|
945
|
+
font-size: inherit; /* panel runs 12/relaxed throughout */
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
@starting-style {
|
|
949
|
+
:where(dialog.drawer[open]) {
|
|
950
|
+
translate: 2.5rem 0;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
:where(dialog.drawer) > :where(header) {
|
|
955
|
+
border-block-end: 0;
|
|
956
|
+
}
|
|
957
|
+
:where(dialog.drawer) > :where(footer) {
|
|
958
|
+
margin-block-start: auto;
|
|
959
|
+
flex-direction: column;
|
|
960
|
+
align-items: stretch;
|
|
961
|
+
gap: var(--space-xs);
|
|
962
|
+
border-block-start: 0;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/* --- accordion / disclosure: details + summary -------------------- *
|
|
966
|
+
* shadcn accordion look; exclusivity is <details name="group">.
|
|
967
|
+
* open/close animation via ::details-content + interpolate-size,
|
|
968
|
+
* graceful (instant) where unsupported.
|
|
969
|
+
* ------------------------------------------------------------------ */
|
|
970
|
+
:where(details) {
|
|
971
|
+
border-block-end: 1px solid var(--color-border);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
:where(summary) {
|
|
975
|
+
display: flex;
|
|
976
|
+
align-items: center;
|
|
977
|
+
justify-content: space-between;
|
|
978
|
+
gap: var(--space-sm);
|
|
979
|
+
padding-block: var(--space-md);
|
|
980
|
+
font-size: 0.875rem;
|
|
981
|
+
font-weight: 500;
|
|
982
|
+
cursor: pointer;
|
|
983
|
+
list-style: none;
|
|
984
|
+
|
|
985
|
+
&:hover {
|
|
986
|
+
text-decoration: underline;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/* chevron — masked so it rides currentColor-ish tokens */
|
|
990
|
+
&::after {
|
|
991
|
+
content: "";
|
|
992
|
+
inline-size: 1rem;
|
|
993
|
+
block-size: 1rem;
|
|
994
|
+
flex-shrink: 0;
|
|
995
|
+
background: var(--color-text-muted);
|
|
996
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
|
|
997
|
+
center / contain no-repeat;
|
|
998
|
+
transition: rotate 0.2s;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
:where(summary)::-webkit-details-marker {
|
|
1002
|
+
display: none;
|
|
1003
|
+
}
|
|
1004
|
+
:where(details[open]) > :where(summary)::after {
|
|
1005
|
+
rotate: 180deg;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/* content: everything after the summary */
|
|
1009
|
+
:where(details) > :where(:not(summary)) {
|
|
1010
|
+
margin-block: 0 var(--space-md);
|
|
1011
|
+
font-size: 0.875rem;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
:where(details)::details-content {
|
|
1015
|
+
block-size: 0;
|
|
1016
|
+
overflow-y: clip;
|
|
1017
|
+
transition:
|
|
1018
|
+
block-size 0.2s,
|
|
1019
|
+
content-visibility 0.2s allow-discrete;
|
|
1020
|
+
}
|
|
1021
|
+
:where(details[open])::details-content {
|
|
1022
|
+
block-size: auto;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/* --- popover: panels + menus -------------------------------------- *
|
|
1026
|
+
* toggle/light-dismiss/Esc are the browser's (popovertarget). anchored
|
|
1027
|
+
* to the invoker where anchor positioning exists; elsewhere the UA's
|
|
1028
|
+
* centered fallback applies. items close declaratively via
|
|
1029
|
+
* popovertarget/command — zero JS throughout.
|
|
1030
|
+
* ------------------------------------------------------------------ */
|
|
1031
|
+
:where([popover]) {
|
|
1032
|
+
border: 1px solid color-mix(in oklch, var(--color-text) 12%, transparent);
|
|
1033
|
+
border-radius: var(--radius-md);
|
|
1034
|
+
background: var(--color-surface-overlay);
|
|
1035
|
+
color: var(--color-text);
|
|
1036
|
+
padding: var(--space-sm);
|
|
1037
|
+
inline-size: max-content;
|
|
1038
|
+
max-inline-size: min(18rem, calc(100vw - 2rem));
|
|
1039
|
+
font-size: 0.75rem;
|
|
1040
|
+
line-height: 1.6;
|
|
1041
|
+
box-shadow: 0 8px 24px light-dark(oklch(0 0 0 / 0.15), oklch(0 0 0 / 0.5));
|
|
1042
|
+
opacity: 1;
|
|
1043
|
+
scale: 1;
|
|
1044
|
+
}
|
|
1045
|
+
/* enter animates; close is instant (house rule for transient layers) */
|
|
1046
|
+
:where([popover]:popover-open) {
|
|
1047
|
+
transition:
|
|
1048
|
+
opacity 0.1s,
|
|
1049
|
+
scale 0.1s,
|
|
1050
|
+
overlay 0.1s allow-discrete,
|
|
1051
|
+
display 0.1s allow-discrete;
|
|
1052
|
+
}
|
|
1053
|
+
@starting-style {
|
|
1054
|
+
:where([popover]:popover-open) {
|
|
1055
|
+
opacity: 0;
|
|
1056
|
+
scale: 0.95;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/* anchor to the invoker where the platform can */
|
|
1060
|
+
@supports (position-area: block-end) {
|
|
1061
|
+
:where([popover]) {
|
|
1062
|
+
position-area: block-end span-inline-end;
|
|
1063
|
+
position-try-fallbacks: flip-block, flip-inline;
|
|
1064
|
+
margin: var(--space-2xs);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/* menu: a popover of buttons/links; separators are plain hrs */
|
|
1069
|
+
:where([popover].menu) {
|
|
1070
|
+
padding: var(--space-2xs);
|
|
1071
|
+
min-inline-size: 8rem;
|
|
1072
|
+
|
|
1073
|
+
& > :where(button, a) {
|
|
1074
|
+
display: flex;
|
|
1075
|
+
align-items: center;
|
|
1076
|
+
justify-content: flex-start;
|
|
1077
|
+
gap: var(--space-xs);
|
|
1078
|
+
inline-size: 100%;
|
|
1079
|
+
block-size: auto;
|
|
1080
|
+
padding: var(--space-xs);
|
|
1081
|
+
border: 0;
|
|
1082
|
+
border-radius: var(--radius-sm);
|
|
1083
|
+
background: transparent;
|
|
1084
|
+
color: var(--color-text);
|
|
1085
|
+
font-size: 0.75rem;
|
|
1086
|
+
font-weight: 400;
|
|
1087
|
+
text-align: start;
|
|
1088
|
+
text-decoration: none;
|
|
1089
|
+
cursor: default;
|
|
1090
|
+
|
|
1091
|
+
/* :not() guards mirror the base button states — an override of
|
|
1092
|
+
state must match the state's specificity (house rule) */
|
|
1093
|
+
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1094
|
+
&:focus-visible:not(:disabled, [aria-disabled="true"]) {
|
|
1095
|
+
background: var(--color-accent);
|
|
1096
|
+
color: var(--color-on-accent);
|
|
1097
|
+
outline: none; /* the highlight is the focus indicator here */
|
|
1098
|
+
}
|
|
1099
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
1100
|
+
background: var(--color-accent);
|
|
1101
|
+
color: var(--color-on-accent);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
&.danger {
|
|
1105
|
+
color: var(--color-danger-text);
|
|
1106
|
+
|
|
1107
|
+
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1108
|
+
&:active:not(:disabled, [aria-disabled="true"]),
|
|
1109
|
+
&:focus-visible:not(:disabled, [aria-disabled="true"]) {
|
|
1110
|
+
background: var(--color-danger-surface);
|
|
1111
|
+
color: var(--color-danger-text);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
&:disabled {
|
|
1116
|
+
opacity: 0.5;
|
|
1117
|
+
background: transparent;
|
|
1118
|
+
color: var(--color-text);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
& > :where(hr) {
|
|
1123
|
+
margin-block: var(--space-2xs);
|
|
1124
|
+
margin-inline: calc(-1 * var(--space-2xs));
|
|
1125
|
+
border-block-start-color: var(--color-border);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/* --- combobox: m-combobox + combobox.js (Tier 2) ------------------ *
|
|
1130
|
+
* without the module: a native datalist — functional autocomplete.
|
|
1131
|
+
* with it: styled listbox, filtering, active-descendant keys.
|
|
1132
|
+
* positioned within the wrapper (no top layer) so it works everywhere.
|
|
1133
|
+
* ------------------------------------------------------------------ */
|
|
1134
|
+
:where(m-combobox) {
|
|
1135
|
+
display: block;
|
|
1136
|
+
position: relative;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
:where(m-combobox) > :where([role="listbox"]) {
|
|
1140
|
+
position: absolute;
|
|
1141
|
+
inset-block-start: calc(100% + var(--space-2xs));
|
|
1142
|
+
inset-inline: 0;
|
|
1143
|
+
z-index: 50;
|
|
1144
|
+
max-block-size: 15rem;
|
|
1145
|
+
overflow-y: auto;
|
|
1146
|
+
padding: var(--space-2xs);
|
|
1147
|
+
border: 1px solid color-mix(in oklch, var(--color-text) 12%, transparent);
|
|
1148
|
+
border-radius: var(--radius-md);
|
|
1149
|
+
background: var(--color-surface-overlay);
|
|
1150
|
+
box-shadow: 0 8px 24px light-dark(oklch(0 0 0 / 0.15), oklch(0 0 0 / 0.5));
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
:where(m-combobox) :where([role="option"]) {
|
|
1154
|
+
padding: var(--space-xs);
|
|
1155
|
+
border-radius: var(--radius-sm);
|
|
1156
|
+
font-size: 0.875rem;
|
|
1157
|
+
cursor: default;
|
|
1158
|
+
|
|
1159
|
+
&:hover,
|
|
1160
|
+
&[aria-selected="true"] {
|
|
1161
|
+
background: var(--color-accent);
|
|
1162
|
+
color: var(--color-on-accent);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/* --- tabs: m-tabs + tabs.js (Tier 2) ------------------------------ *
|
|
1167
|
+
* without the module: inert nav, all panels visible in order —
|
|
1168
|
+
* complete content. with it: tablist semantics + roving focus.
|
|
1169
|
+
* shadcn segmented look: muted track, surface active tab.
|
|
1170
|
+
* ------------------------------------------------------------------ */
|
|
1171
|
+
:where(m-tabs) {
|
|
1172
|
+
display: block;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
:where(m-tabs) > :where(nav) {
|
|
1176
|
+
display: inline-flex;
|
|
1177
|
+
gap: var(--space-2xs);
|
|
1178
|
+
padding: var(--space-2xs);
|
|
1179
|
+
background: var(--neutral-3);
|
|
1180
|
+
border-radius: var(--radius-md);
|
|
1181
|
+
|
|
1182
|
+
& > :where(button) {
|
|
1183
|
+
block-size: var(--control-height-sm);
|
|
1184
|
+
padding-inline: var(--space-sm);
|
|
1185
|
+
border: 0;
|
|
1186
|
+
border-radius: var(--radius-sm);
|
|
1187
|
+
background: transparent;
|
|
1188
|
+
color: var(--color-text-muted);
|
|
1189
|
+
|
|
1190
|
+
/* :not() guards mirror the base button states (house rule) */
|
|
1191
|
+
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1192
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
1193
|
+
background: transparent;
|
|
1194
|
+
color: var(--color-text);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
&[aria-selected="true"] {
|
|
1198
|
+
background: var(--color-surface);
|
|
1199
|
+
color: var(--color-text);
|
|
1200
|
+
box-shadow: 0 1px 2px oklch(0 0 0 / 0.08);
|
|
1201
|
+
|
|
1202
|
+
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1203
|
+
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
1204
|
+
background: var(--color-surface);
|
|
1205
|
+
color: var(--color-text);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
:where(m-tabs) > :where(section) {
|
|
1212
|
+
padding-block: var(--space-md);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/* --- toast: display only (queueing/auto-dismiss are Tier 2) ------- *
|
|
1216
|
+
* <div popover="manual" class="toast" role="status"> — manual so it
|
|
1217
|
+
* survives light dismiss and other popovers; role=status announces.
|
|
1218
|
+
* ------------------------------------------------------------------ */
|
|
1219
|
+
:where([popover].toast) {
|
|
1220
|
+
position-area: none; /* not anchored — pinned to the corner */
|
|
1221
|
+
/* --m-toast-offset comes from toast.js when stacking; 0 without it */
|
|
1222
|
+
inset: auto var(--space-lg) calc(var(--space-lg) + var(--m-toast-offset, 0px)) auto;
|
|
1223
|
+
margin: 0;
|
|
1224
|
+
inline-size: min(22rem, calc(100vw - 2rem));
|
|
1225
|
+
max-inline-size: none;
|
|
1226
|
+
padding: var(--space-md);
|
|
1227
|
+
padding-inline-end: var(--space-2xl); /* room for the corner X */
|
|
1228
|
+
translate: 0 0;
|
|
1229
|
+
|
|
1230
|
+
& :where(b) {
|
|
1231
|
+
display: block;
|
|
1232
|
+
font-weight: 500;
|
|
1233
|
+
}
|
|
1234
|
+
& :where(span, p) {
|
|
1235
|
+
color: var(--color-text-muted);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/* status edge variants */
|
|
1239
|
+
&:where(.success) {
|
|
1240
|
+
border-inline-start: 2px solid var(--color-success);
|
|
1241
|
+
}
|
|
1242
|
+
&:where(.danger) {
|
|
1243
|
+
border-inline-start: 2px solid var(--color-danger);
|
|
1244
|
+
}
|
|
1245
|
+
&:where(.warn) {
|
|
1246
|
+
border-inline-start: 2px solid var(--color-warn);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/* arrives from below rather than zooming; reflow (restacking) eases */
|
|
1250
|
+
:where([popover].toast:popover-open) {
|
|
1251
|
+
transition:
|
|
1252
|
+
opacity 0.1s,
|
|
1253
|
+
translate 0.1s,
|
|
1254
|
+
inset-block-end 0.15s,
|
|
1255
|
+
overlay 0.1s allow-discrete,
|
|
1256
|
+
display 0.1s allow-discrete;
|
|
1257
|
+
}
|
|
1258
|
+
@starting-style {
|
|
1259
|
+
:where([popover].toast:popover-open) {
|
|
1260
|
+
translate: 0 0.75rem;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/* --- tooltip: css-only, [data-tip] -------------------------------- *
|
|
1265
|
+
* inverted chip (primary on on-primary), shows on hover/focus after a
|
|
1266
|
+
* beat, hides instantly. data-tip is visual — pair it with aria-label
|
|
1267
|
+
* or visible text for AT.
|
|
1268
|
+
* ------------------------------------------------------------------ */
|
|
1269
|
+
:where([data-tip]) {
|
|
1270
|
+
position: relative;
|
|
1271
|
+
}
|
|
1272
|
+
:where([data-tip])::after {
|
|
1273
|
+
content: attr(data-tip);
|
|
1274
|
+
position: absolute;
|
|
1275
|
+
inset-block-end: calc(100% + var(--space-xs));
|
|
1276
|
+
inset-inline-start: 50%;
|
|
1277
|
+
translate: -50% 2px;
|
|
1278
|
+
inline-size: max-content;
|
|
1279
|
+
max-inline-size: 18rem;
|
|
1280
|
+
padding: 0.375rem var(--space-sm);
|
|
1281
|
+
border-radius: var(--radius-sm);
|
|
1282
|
+
background: var(--color-primary);
|
|
1283
|
+
color: var(--color-on-primary);
|
|
1284
|
+
font-size: 0.75rem;
|
|
1285
|
+
font-weight: 400;
|
|
1286
|
+
line-height: 1.4;
|
|
1287
|
+
opacity: 0;
|
|
1288
|
+
pointer-events: none;
|
|
1289
|
+
transition:
|
|
1290
|
+
opacity 0.1s,
|
|
1291
|
+
translate 0.1s;
|
|
1292
|
+
z-index: 10;
|
|
1293
|
+
}
|
|
1294
|
+
:where([data-tip]:hover, [data-tip]:focus-visible)::after {
|
|
1295
|
+
opacity: 1;
|
|
1296
|
+
translate: -50% 0;
|
|
1297
|
+
transition-delay: 0.3s;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/* --- validation errors ------------------------------------------- *
|
|
1301
|
+
* Tier 1 (no JS): a generic <m-error> after a field shows when the
|
|
1302
|
+
* field is :user-invalid. Submit attempts fall back to native bubbles.
|
|
1303
|
+
* Tier 2 (mica/field.js): <m-field> suppresses bubbles and activates
|
|
1304
|
+
* per-cause <m-error match="..."> messages. Rules below are all
|
|
1305
|
+
* zero-specificity; source order resolves them.
|
|
1306
|
+
* ------------------------------------------------------------------ */
|
|
1307
|
+
:where(m-field) {
|
|
1308
|
+
display: flex;
|
|
1309
|
+
flex-direction: column;
|
|
1310
|
+
gap: var(--space-2xs);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/* every rule here is zero-specificity: source order alone resolves */
|
|
1314
|
+
:where(m-error) {
|
|
1315
|
+
display: none;
|
|
1316
|
+
font-size: 0.875rem;
|
|
1317
|
+
color: var(--color-danger-text);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/* Tier-1 fallback: generic (matchless) error after user interaction */
|
|
1321
|
+
:where(input:user-invalid, textarea:user-invalid, select:user-invalid)
|
|
1322
|
+
~ :where(m-error:not([match])) {
|
|
1323
|
+
display: block;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
/* Tier 2: once m-field upgrades, the module owns visibility */
|
|
1327
|
+
:where(m-field:defined) :where(m-error) {
|
|
1328
|
+
display: none;
|
|
1329
|
+
}
|
|
1330
|
+
:where(m-error[active]) {
|
|
1331
|
+
display: block;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/* ------------------------------------------------------------------ *
|
|
1336
|
+
* Layout primitives (Tier 0)
|
|
1337
|
+
*
|
|
1338
|
+
* Mechanism: attributes only set custom properties; rules read custom
|
|
1339
|
+
* properties. `gap="lg"` and `style="--gap: 2.5rem"` are the same thing.
|
|
1340
|
+
* ------------------------------------------------------------------ */
|
|
1341
|
+
@layer mica.layout {
|
|
1342
|
+
/* Custom elements default to display:inline; give ours a sane base. */
|
|
1343
|
+
m-vstack,
|
|
1344
|
+
m-hstack,
|
|
1345
|
+
m-zstack,
|
|
1346
|
+
m-center,
|
|
1347
|
+
m-box,
|
|
1348
|
+
m-grid,
|
|
1349
|
+
m-sidecar,
|
|
1350
|
+
m-switcher,
|
|
1351
|
+
m-reel {
|
|
1352
|
+
display: block;
|
|
1353
|
+
box-sizing: border-box;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/* Shared attribute vocabulary -> custom properties */
|
|
1357
|
+
:is(m-vstack, m-hstack, m-grid, m-sidecar, m-switcher, m-reel) {
|
|
1358
|
+
&[gap="none"] { --gap: 0; }
|
|
1359
|
+
&[gap="2xs"] { --gap: var(--space-2xs); }
|
|
1360
|
+
&[gap="xs"] { --gap: var(--space-xs); }
|
|
1361
|
+
&[gap="sm"] { --gap: var(--space-sm); }
|
|
1362
|
+
&[gap="md"] { --gap: var(--space-md); }
|
|
1363
|
+
&[gap="lg"] { --gap: var(--space-lg); }
|
|
1364
|
+
&[gap="xl"] { --gap: var(--space-xl); }
|
|
1365
|
+
&[gap="2xl"] { --gap: var(--space-2xl); }
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
:is(m-vstack, m-hstack) {
|
|
1369
|
+
&[align="start"] { --align: flex-start; }
|
|
1370
|
+
&[align="center"] { --align: center; }
|
|
1371
|
+
&[align="end"] { --align: flex-end; }
|
|
1372
|
+
&[align="stretch"] { --align: stretch; }
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/* --- m-vstack: vertical flow with consistent gaps ---------------- */
|
|
1376
|
+
m-vstack {
|
|
1377
|
+
display: flex;
|
|
1378
|
+
flex-direction: column;
|
|
1379
|
+
align-items: var(--align, stretch);
|
|
1380
|
+
gap: var(--gap, var(--space-md));
|
|
1381
|
+
|
|
1382
|
+
/* the stack owns spacing; preset rhythm yields to gap */
|
|
1383
|
+
& > * { margin-block: 0; }
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/* --- m-hstack: one horizontal row; `wrap` to allow wrapping ------ */
|
|
1387
|
+
m-hstack {
|
|
1388
|
+
display: flex;
|
|
1389
|
+
align-items: var(--align, center);
|
|
1390
|
+
justify-content: var(--justify, flex-start);
|
|
1391
|
+
gap: var(--gap, var(--space-md));
|
|
1392
|
+
|
|
1393
|
+
&[wrap] { flex-wrap: wrap; }
|
|
1394
|
+
|
|
1395
|
+
& > * { margin-block: 0; }
|
|
1396
|
+
|
|
1397
|
+
&[justify="start"] { --justify: flex-start; }
|
|
1398
|
+
&[justify="center"] { --justify: center; }
|
|
1399
|
+
&[justify="end"] { --justify: flex-end; }
|
|
1400
|
+
&[justify="between"] { --justify: space-between; }
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/* --- m-zstack: layering; children occupy the same grid cell ------ */
|
|
1404
|
+
m-zstack {
|
|
1405
|
+
display: grid;
|
|
1406
|
+
align-items: var(--align, stretch);
|
|
1407
|
+
justify-items: var(--justify, stretch);
|
|
1408
|
+
|
|
1409
|
+
&[align="start"] { --align: start; }
|
|
1410
|
+
&[align="center"] { --align: center; }
|
|
1411
|
+
&[align="end"] { --align: end; }
|
|
1412
|
+
&[align="stretch"] { --align: stretch; }
|
|
1413
|
+
|
|
1414
|
+
&[justify="start"] { --justify: start; }
|
|
1415
|
+
&[justify="center"] { --justify: center; }
|
|
1416
|
+
&[justify="end"] { --justify: end; }
|
|
1417
|
+
&[justify="stretch"] { --justify: stretch; }
|
|
1418
|
+
|
|
1419
|
+
/* every layer shares one cell; paint order is DOM order.
|
|
1420
|
+
per-layer placement is just user CSS: place-self on the child. */
|
|
1421
|
+
& > * {
|
|
1422
|
+
grid-area: 1 / 1;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/* --- m-center: intrinsic horizontal centering with a measure ----- */
|
|
1427
|
+
m-center {
|
|
1428
|
+
max-inline-size: var(--max, var(--measure));
|
|
1429
|
+
margin-inline: auto;
|
|
1430
|
+
padding-inline: var(--gutter, 0);
|
|
1431
|
+
|
|
1432
|
+
&[max="xs"] { --max: var(--size-xs); }
|
|
1433
|
+
&[max="sm"] { --max: var(--size-sm); }
|
|
1434
|
+
&[max="md"] { --max: var(--size-md); }
|
|
1435
|
+
&[max="lg"] { --max: var(--size-lg); }
|
|
1436
|
+
&[max="xl"] { --max: var(--size-xl); }
|
|
1437
|
+
|
|
1438
|
+
&[gutter="sm"] { --gutter: var(--space-sm); }
|
|
1439
|
+
&[gutter="md"] { --gutter: var(--space-md); }
|
|
1440
|
+
&[gutter="lg"] { --gutter: var(--space-lg); }
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/* --- m-box: padding, an inside for content ----------------------- */
|
|
1444
|
+
m-box {
|
|
1445
|
+
padding: var(--pad, var(--space-md));
|
|
1446
|
+
|
|
1447
|
+
&[pad="none"] { --pad: 0; }
|
|
1448
|
+
&[pad="xs"] { --pad: var(--space-xs); }
|
|
1449
|
+
&[pad="sm"] { --pad: var(--space-sm); }
|
|
1450
|
+
&[pad="md"] { --pad: var(--space-md); }
|
|
1451
|
+
&[pad="lg"] { --pad: var(--space-lg); }
|
|
1452
|
+
&[pad="xl"] { --pad: var(--space-xl); }
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/* --- m-grid: responsive grid, no media queries ------------------- */
|
|
1456
|
+
m-grid {
|
|
1457
|
+
display: grid;
|
|
1458
|
+
grid-template-columns: repeat(
|
|
1459
|
+
auto-fit,
|
|
1460
|
+
minmax(min(var(--min, var(--size-sm)), 100%), 1fr)
|
|
1461
|
+
);
|
|
1462
|
+
gap: var(--gap, var(--space-md));
|
|
1463
|
+
|
|
1464
|
+
&[min="xs"] { --min: var(--size-xs); }
|
|
1465
|
+
&[min="sm"] { --min: var(--size-sm); }
|
|
1466
|
+
&[min="md"] { --min: var(--size-md); }
|
|
1467
|
+
&[min="lg"] { --min: var(--size-lg); }
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/* --- m-sidecar: fixed + flexible pair, stacks when narrow -------- */
|
|
1471
|
+
m-sidecar {
|
|
1472
|
+
display: flex;
|
|
1473
|
+
flex-wrap: wrap;
|
|
1474
|
+
gap: var(--gap, var(--space-md));
|
|
1475
|
+
|
|
1476
|
+
/* the sidecar is the first child by default; `side="end"` flips it */
|
|
1477
|
+
& > :first-child {
|
|
1478
|
+
flex-basis: var(--side-width, var(--size-xs));
|
|
1479
|
+
flex-grow: 1;
|
|
1480
|
+
}
|
|
1481
|
+
& > :last-child {
|
|
1482
|
+
flex-basis: 0;
|
|
1483
|
+
flex-grow: 999;
|
|
1484
|
+
min-inline-size: var(--content-min, 50%);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/* side="end": the *last* child is the sidecar (no visual reordering) */
|
|
1488
|
+
&[side="end"] > :first-child {
|
|
1489
|
+
flex-basis: 0;
|
|
1490
|
+
flex-grow: 999;
|
|
1491
|
+
min-inline-size: var(--content-min, 50%);
|
|
1492
|
+
}
|
|
1493
|
+
&[side="end"] > :last-child {
|
|
1494
|
+
flex-basis: var(--side-width, var(--size-xs));
|
|
1495
|
+
flex-grow: 1;
|
|
1496
|
+
min-inline-size: auto;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
&[side-width="xs"] { --side-width: var(--size-xs); }
|
|
1500
|
+
&[side-width="sm"] { --side-width: var(--size-sm); }
|
|
1501
|
+
&[side-width="md"] { --side-width: var(--size-md); }
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/* --- m-switcher: row that switches to a column below a threshold - */
|
|
1505
|
+
m-switcher {
|
|
1506
|
+
display: flex;
|
|
1507
|
+
flex-wrap: wrap;
|
|
1508
|
+
gap: var(--gap, var(--space-md));
|
|
1509
|
+
|
|
1510
|
+
& > * {
|
|
1511
|
+
flex-basis: calc((var(--threshold, var(--size-lg)) - 100%) * 999);
|
|
1512
|
+
flex-grow: 1;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
&[threshold="sm"] { --threshold: var(--size-sm); }
|
|
1516
|
+
&[threshold="md"] { --threshold: var(--size-md); }
|
|
1517
|
+
&[threshold="lg"] { --threshold: var(--size-lg); }
|
|
1518
|
+
&[threshold="xl"] { --threshold: var(--size-xl); }
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/* --- m-reel: horizontal scroll with snap ------------------------- */
|
|
1522
|
+
m-reel {
|
|
1523
|
+
display: flex;
|
|
1524
|
+
gap: var(--gap, var(--space-md));
|
|
1525
|
+
overflow-x: auto;
|
|
1526
|
+
scroll-snap-type: inline proximity;
|
|
1527
|
+
|
|
1528
|
+
& > * {
|
|
1529
|
+
flex-shrink: 0;
|
|
1530
|
+
scroll-snap-align: start;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
&[snap="none"] { scroll-snap-type: none; }
|
|
1534
|
+
&[snap="mandatory"] { scroll-snap-type: inline mandatory; }
|
|
1535
|
+
}
|
|
1536
|
+
}
|