@apliteni/apliteni-ui 0.26.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/react/README.md +111 -2
  4. package/react/dist/index.css +11 -13
  5. package/react/dist/index.d.ts +111 -4
  6. package/react/dist/index.js +811 -167
  7. package/src/components/back.js +57 -0
  8. package/src/components/command-palette.js +597 -0
  9. package/src/components/confirm.js +3 -2
  10. package/src/components/drawer.js +31 -2
  11. package/src/components/dropdown.js +192 -17
  12. package/src/components/feedback.js +2 -2
  13. package/src/components/index.js +5 -2
  14. package/src/components/loading.js +3 -2
  15. package/src/components/nav.js +15 -7
  16. package/src/components/overlay.js +67 -14
  17. package/src/components/pagination.js +344 -0
  18. package/src/components/shell.js +11 -2
  19. package/src/components/tabs.js +7 -1
  20. package/src/components/tooltip.js +237 -0
  21. package/src/components/topbar.js +9 -4
  22. package/src/index.css +4 -0
  23. package/src/index.js +4 -0
  24. package/src/inline.js +8 -0
  25. package/src/motion.js +43 -2
  26. package/src/styles/back.css +42 -0
  27. package/src/styles/badge.css +5 -7
  28. package/src/styles/base.css +8 -7
  29. package/src/styles/button.css +14 -0
  30. package/src/styles/card.css +12 -8
  31. package/src/styles/code.css +3 -4
  32. package/src/styles/command-palette.css +321 -0
  33. package/src/styles/confirm.css +11 -11
  34. package/src/styles/drawer.css +57 -15
  35. package/src/styles/dropdown.css +70 -11
  36. package/src/styles/feedback.css +2 -0
  37. package/src/styles/footer.css +3 -4
  38. package/src/styles/input.css +7 -1
  39. package/src/styles/layout.css +3 -2
  40. package/src/styles/loading.css +5 -0
  41. package/src/styles/nav.css +12 -8
  42. package/src/styles/pagination.css +124 -0
  43. package/src/styles/success.css +3 -2
  44. package/src/styles/table.css +4 -5
  45. package/src/styles/tabs.css +3 -0
  46. package/src/styles/tooltip.css +65 -0
  47. package/src/styles/topbar.css +3 -4
  48. package/src/tokens/tokens.css +14 -8
@@ -0,0 +1,321 @@
1
+ /* ============================================================================
2
+ * Command palette — a text box and a ranked list, over a scrim, near the top of
3
+ * the window. Fully token-driven so it re-themes across accents + light/dark.
4
+ * Shares its scrim, its Escape and its focus trap with the drawer and the
5
+ * confirm (see overlay.js), and paints one layer above the drawer and one below
6
+ * the confirm a row opens.
7
+ *
8
+ * Local tokens so the panel and scrim re-theme cleanly:
9
+ * --cmdk-surface panel background --cmdk-shadow panel elevation
10
+ * --cmdk-scrim backdrop colour --cmdk-w panel width
11
+ *
12
+ * Two densities, one markup: the default packs a row onto one line, and
13
+ * `.ui-cmdk--roomy` gives the description a line of its own.
14
+ *
15
+ * why: docs/specification.md#the-command-palette */
16
+
17
+ .ui-cmdk {
18
+ --cmdk-surface: var(--surface-2);
19
+ --cmdk-shadow: var(--shadow-lg);
20
+ --cmdk-scrim: var(--scrim);
21
+ --cmdk-w: 560px;
22
+ --cmdk-top: 12vh;
23
+ --cmdk-list-h: 332px;
24
+
25
+ position: fixed;
26
+ inset: 0;
27
+ /* One above the drawer, one below the confirm: a palette is summoned
28
+ deliberately and must be seen, so it paints over a drawer that was already
29
+ open; a confirm is a question about what is under it and paints over both. */
30
+ z-index: calc(var(--z-overlay) + 1);
31
+ pointer-events: none; /* container never blocks; scrim/panel opt back in */
32
+
33
+ /* One place owns "is this palette on screen at all", and the scrim and panel
34
+ inherit it. Only the HIDE is transitioned, so the panel is still on screen
35
+ while it fades out; the show is instant (`.is-open` cancels the transition)
36
+ because openCommandPalette() focuses the text box in the same frame it
37
+ opens, and focus() on a hidden element does nothing. `linear` and not
38
+ var(--ease): visibility is discrete, easing a step buys nothing, and a curve
39
+ that leaves [0, 1] would flip it in the middle of the fade. */
40
+ visibility: hidden;
41
+ transition: visibility var(--dur-med) linear;
42
+ }
43
+ .ui-cmdk.is-open { visibility: visible; transition: none; }
44
+
45
+ /* Hit-testable only while open. The root stays visible for --dur-med after
46
+ closeCommandPalette() has stopped trapping, and a row still clickable in that
47
+ window runs the caller's command a second time. */
48
+ .ui-cmdk.is-open .ui-cmdk__scrim,
49
+ .ui-cmdk.is-open .ui-cmdk__panel { pointer-events: auto; }
50
+
51
+ .ui-cmdk__scrim {
52
+ position: absolute;
53
+ inset: 0;
54
+ background: var(--cmdk-scrim);
55
+ opacity: 0;
56
+ transition: opacity var(--dur-med) var(--ease);
57
+ }
58
+ .ui-cmdk.is-open .ui-cmdk__scrim { opacity: 1; }
59
+
60
+ /* Panel — near the top rather than centred: the list grows downward, and a
61
+ centred panel moves the text box every time the number of results changes. */
62
+ .ui-cmdk__panel {
63
+ position: absolute;
64
+ top: var(--cmdk-top);
65
+ left: 50%;
66
+ width: min(var(--cmdk-w), calc(100vw - var(--space-8)));
67
+ max-height: min(72vh, calc(100vh - var(--cmdk-top) - var(--space-8)));
68
+ display: flex;
69
+ flex-direction: column;
70
+ font-family: var(--font-sans);
71
+ background: var(--cmdk-surface);
72
+ border: 1px solid var(--border);
73
+ border-radius: var(--radius-lg);
74
+ box-shadow: var(--cmdk-shadow);
75
+ overflow: hidden;
76
+ opacity: 0;
77
+ transform: translate(-50%, -8px);
78
+ transition:
79
+ opacity var(--dur-med) var(--ease),
80
+ transform var(--dur-med) var(--ease);
81
+ }
82
+ .ui-cmdk.is-open .ui-cmdk__panel { opacity: 1; transform: translate(-50%, 0); }
83
+ .ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--cmdk-shadow), var(--ring); }
84
+
85
+ /* -- The text box --------------------------------------------------------- */
86
+
87
+ .ui-cmdk__search {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: var(--space-3);
91
+ padding: 0 var(--space-4);
92
+ border-bottom: 1px solid var(--border);
93
+ }
94
+ .ui-cmdk__search-ic {
95
+ display: inline-flex;
96
+ color: var(--muted);
97
+ flex: none;
98
+ }
99
+ /* 2.2 of a 24 box drawn at 17px = 1.56 CSS px, which is what keeps the mark a
100
+ graphic rather than a smudge. why: docs/specification.md#icons-and-glyphs */
101
+ .ui-cmdk__search-ic svg { width: 17px; height: 17px; stroke: currentColor; fill: none; stroke-width: 2.2; }
102
+
103
+ /* Not .ui-input: this one has no box of its own — the panel is the box — and a
104
+ second border inside a bordered panel reads as a form somebody has to submit. */
105
+ .ui-cmdk__input {
106
+ flex: 1;
107
+ min-width: 0;
108
+ /* Literal px, and a line-height it does not inherit: the target-size gate
109
+ reads declarations rather than a laid-out box, and `var()` inside a padding
110
+ shorthand measures as nothing there.
111
+ why: CONTRIBUTING.md#an-unresolved-var-measures-nothing-and-reports-green */
112
+ padding: 16px 0;
113
+ line-height: 1.5;
114
+ font-family: inherit;
115
+ font-size: var(--text-md);
116
+ font-weight: var(--weight-normal);
117
+ color: var(--text);
118
+ background: none;
119
+ border: 0;
120
+ /* The one control in the kit that removes the indicator and puts no
121
+ box-shadow: var(--ring) back, against base.css. It is the only tab stop in
122
+ the dialog and it holds focus from the frame it opens, so a ring would be
123
+ painted the whole time the palette is up and would mark nothing; the panel
124
+ already carries one, and the caret says where the typing goes. */
125
+ outline: none;
126
+ }
127
+ .ui-cmdk__input::placeholder { color: var(--muted); }
128
+
129
+ /* -- The list ------------------------------------------------------------- */
130
+
131
+ .ui-cmdk__list {
132
+ flex: 1;
133
+ min-height: 0;
134
+ max-height: var(--cmdk-list-h);
135
+ overflow-y: auto;
136
+ padding: var(--space-2);
137
+ }
138
+ .ui-cmdk__group + .ui-cmdk__group { margin-top: var(--space-2); }
139
+
140
+ /* Sentence case at 13px medium — the kit's label role, not a heading and not a
141
+ shout. A group name is the one word the reader scans for. */
142
+ .ui-cmdk__group-head {
143
+ padding: var(--space-2) var(--space-3) var(--space-1);
144
+ font-size: var(--text-sm);
145
+ font-weight: var(--weight-medium);
146
+ color: var(--muted);
147
+ }
148
+
149
+ /* Row — [icon] [label + description] [badge] [keys]. A <div role="option">, so
150
+ the reset a button or a link would need is not needed here. */
151
+ /* The four declarations after `display` are the reset a row written as a
152
+ <button> needs: the browser gives one a 2px outset border, `font: 400
153
+ 13.3333px Arial`, centred text and a shrink-to-fit width, and a consumer who
154
+ needs the row operable from the keyboard should get the row this sheet
155
+ describes rather than that. `font: inherit` comes before the line-height it
156
+ keeps. why: docs/specification.md#a-dropdown-row-is-a-div-a-link-or-a-button */
157
+ .ui-cmdk__item {
158
+ display: flex;
159
+ border: 0;
160
+ font: inherit;
161
+ text-align: left;
162
+ width: 100%;
163
+ align-items: center;
164
+ gap: var(--space-3);
165
+ padding: 8px 12px;
166
+ line-height: 1.5;
167
+ border-radius: var(--radius-sm);
168
+ color: var(--text);
169
+ cursor: pointer;
170
+ /* The active row is painted, never faded in: the row moves on every arrow
171
+ press and a transition would leave two rows looking half-chosen. */
172
+ background: none;
173
+ }
174
+ .ui-cmdk__item[hidden] { display: none; } /* motion: still — a row the query no longer answers is gone on the next keystroke */
175
+ .ui-cmdk__item.is-active { background: var(--surface-3); }
176
+ .ui-cmdk__item.is-active .ui-cmdk__label { color: var(--strong); }
177
+
178
+ .ui-cmdk__ic {
179
+ display: inline-flex;
180
+ color: var(--dim);
181
+ flex: none;
182
+ }
183
+ /* 2.3 of a 24 box drawn at 16px = 1.53 CSS px. Same arithmetic as the dropdown's
184
+ row icon next door. why: docs/specification.md#icons-and-glyphs */
185
+ .ui-cmdk__ic svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.3; }
186
+ .ui-cmdk__item.is-active .ui-cmdk__ic { color: var(--accent); }
187
+
188
+ .ui-cmdk__main {
189
+ display: flex;
190
+ align-items: baseline;
191
+ gap: var(--space-2);
192
+ min-width: 0;
193
+ flex: 1;
194
+ }
195
+ .ui-cmdk__label {
196
+ font-size: var(--text-base);
197
+ font-weight: var(--weight-medium);
198
+ color: var(--text);
199
+ white-space: nowrap;
200
+ overflow: hidden;
201
+ text-overflow: ellipsis;
202
+ }
203
+ .ui-cmdk__desc {
204
+ font-size: var(--text-sm);
205
+ /* --dim and not --muted: a row's note is how two rows called INV-4812 and
206
+ INV-4809 are told apart, which is not something a reader may skip.
207
+ why: docs/specification.md#colour-and-contrast */
208
+ color: var(--dim);
209
+ white-space: nowrap;
210
+ overflow: hidden;
211
+ text-overflow: ellipsis;
212
+ min-width: 0;
213
+ }
214
+
215
+ .ui-cmdk__badge {
216
+ flex: none;
217
+ padding: 2px 8px;
218
+ border-radius: var(--radius-pill);
219
+ border: 1px solid var(--border);
220
+ font-size: var(--text-xs);
221
+ color: var(--dim);
222
+ }
223
+
224
+ /* Destructive rows take the kit's danger ink, never the accent, and stay quiet
225
+ until the reader is on them — the same pair .ui-dropdown__item.is-danger
226
+ writes with :hover. A palette row is reached with an arrow key far more often
227
+ than with a pointer, so `.is-active` carries the signal beside :hover: it is
228
+ the same moment, and it is the row Enter would run.
229
+ why: docs/specification.md#colour-and-contrast */
230
+ .ui-cmdk__item.is-danger .ui-cmdk__label { color: var(--muted); }
231
+ .ui-cmdk__item.is-danger:hover .ui-cmdk__label,
232
+ .ui-cmdk__item.is-danger.is-active .ui-cmdk__label { color: var(--pink); }
233
+ .ui-cmdk__item.is-danger.is-active .ui-cmdk__ic { color: var(--pink); }
234
+
235
+ /* A disabled row is painted, never faded. It takes --disabled-ink-bare, the ink
236
+ for a control with no box of its own: --disabled-surface IS --surface-2,
237
+ which is the panel behind it, so a disabled row paints no box a reader can
238
+ see and its label is read on whatever the panel is.
239
+ why: docs/specification.md#colour-and-contrast */
240
+ .ui-cmdk__item.is-disabled { cursor: default; color: var(--disabled-ink-bare); }
241
+ .ui-cmdk__item.is-disabled .ui-cmdk__label,
242
+ .ui-cmdk__item.is-disabled .ui-cmdk__desc,
243
+ .ui-cmdk__item.is-disabled .ui-cmdk__ic { color: var(--disabled-ink-bare); }
244
+
245
+ /* -- Keys, empty state, legend -------------------------------------------- */
246
+
247
+ .ui-cmdk__keys { display: inline-flex; gap: 4px; flex: none; }
248
+ .ui-cmdk__key {
249
+ min-width: 20px;
250
+ padding: 2px 5px;
251
+ border: 1px solid var(--border);
252
+ border-radius: var(--radius-xs);
253
+ background: var(--surface);
254
+ font-family: var(--font-sans);
255
+ font-size: var(--text-xs);
256
+ line-height: 1.5;
257
+ color: var(--dim);
258
+ text-align: center;
259
+ }
260
+
261
+ .ui-cmdk__empty {
262
+ margin: 0;
263
+ padding: var(--space-6) var(--space-4) var(--space-8);
264
+ text-align: center;
265
+ font-size: var(--text-base);
266
+ color: var(--dim);
267
+ }
268
+
269
+ .ui-cmdk__foot {
270
+ display: flex;
271
+ gap: var(--space-4);
272
+ align-items: center;
273
+ padding: var(--space-2) var(--space-4);
274
+ border-top: 1px solid var(--border);
275
+ font-size: var(--text-xs);
276
+ color: var(--muted);
277
+ }
278
+ .ui-cmdk__foot span { display: inline-flex; align-items: center; gap: 5px; }
279
+
280
+ /* -- Roomy ---------------------------------------------------------------- */
281
+ /* The same markup, two lines to a row: the description drops under the label
282
+ and the icon gets a tile of its own. For a palette whose rows need a sentence
283
+ to tell apart — an invoice from an invoice — rather than a list of verbs. */
284
+
285
+ .ui-cmdk--roomy { --cmdk-w: 640px; --cmdk-list-h: 396px; }
286
+ .ui-cmdk--roomy .ui-cmdk__item { padding: 9px 12px; gap: var(--space-3); }
287
+ .ui-cmdk--roomy .ui-cmdk__main { display: block; }
288
+ .ui-cmdk--roomy .ui-cmdk__label { display: block; }
289
+ .ui-cmdk--roomy .ui-cmdk__desc { display: block; margin-top: 1px; }
290
+ .ui-cmdk--roomy .ui-cmdk__ic {
291
+ width: 30px;
292
+ height: 30px;
293
+ align-items: center;
294
+ justify-content: center;
295
+ border-radius: var(--radius-sm);
296
+ background: var(--surface);
297
+ border: 1px solid var(--border);
298
+ }
299
+ .ui-cmdk--roomy .ui-cmdk__item.is-active .ui-cmdk__ic { background: var(--surface-2); }
300
+
301
+ /* -- Narrow --------------------------------------------------------------- */
302
+ /* At the kit's narrowest step the palette is the page: full width, off the top
303
+ edge, and the key legend goes — there is no keyboard to legend.
304
+ why: docs/specification.md#breakpoints */
305
+ @media (max-width: 560px) {
306
+ .ui-cmdk { --cmdk-top: var(--space-4); --cmdk-list-h: 60vh; }
307
+ .ui-cmdk__panel { width: calc(100vw - var(--space-4)); }
308
+ .ui-cmdk__foot { display: none; }
309
+ }
310
+
311
+ /* Under reduced motion the net gives every element a 0.01ms transition, so a
312
+ child inherits `visible` one tick after the root: the text box
313
+ openCommandPalette() focuses is still hidden in that frame, and focus falls to
314
+ <body> — the arrows and every letter then reach nothing. So an open palette
315
+ carries no transition inside it at all — for as long as it is open, not only
316
+ in the frame it opens — which is what reduced motion asked for anyway: under
317
+ the net nothing inside had longer than 0.01ms to run.
318
+ why: docs/specification.md#reduced-motion-travels-with-the-stylesheet */
319
+ @media (prefers-reduced-motion: reduce) {
320
+ .ui-cmdk.is-open * { transition: none !important; }
321
+ }
@@ -20,10 +20,10 @@
20
20
 
21
21
  position: fixed;
22
22
  inset: 0;
23
- /* One above the drawer's own overlay level: a confirm asks a question about the
24
- drawer it was opened over, so it has to be readable above it whatever order
25
- the two are mounted in. */
26
- z-index: calc(var(--z-overlay) + 1);
23
+ /* Top of the overlay stack one above the palette, two above the drawer: a
24
+ confirm asks a question about whichever of them opened it, so it has to be
25
+ readable above both whatever order the three are mounted in. */
26
+ z-index: calc(var(--z-overlay) + 2);
27
27
  pointer-events: none; /* container never blocks; scrim/panel opt back in */
28
28
 
29
29
  /* Only the hide is transitioned, so the panel is still on screen while it
@@ -119,12 +119,12 @@
119
119
  margin-top: var(--space-2);
120
120
  }
121
121
 
122
- /* -- Reduced motion: no rise, just a fade --------------------------------- */
122
+ /* Under reduced motion the net makes every change instant, and gives every
123
+ element a 0.01ms transition: a child inherits `visible` one tick after the
124
+ root, so the answer openConfirm() focuses is still hidden and focus falls to
125
+ <body>. So an open confirm carries no transition inside it at all — for as
126
+ long as it is open, not only in the frame it opens.
127
+ why: docs/specification.md#reduced-motion-travels-with-the-stylesheet */
123
128
  @media (prefers-reduced-motion: reduce) {
124
- .ui-confirm { transition: visibility var(--dur-fast) linear; }
125
- .ui-confirm__panel {
126
- transform: none;
127
- transition: opacity var(--dur-fast) linear;
128
- }
129
- .ui-confirm__scrim { transition: opacity var(--dur-fast) linear; }
129
+ .ui-confirm.is-open * { transition: none !important; }
130
130
  }
@@ -2,8 +2,8 @@
2
2
  * Drawer — edge-anchored overlay panel (Sheet / Slide-over): a panel that
3
3
  * slides in from any screen edge over a scrim. Fully token-driven so it
4
4
  * re-themes across accents + light/dark. Shares its scrim + focus trap with
5
- * confirm() (see overlay.js). `prefers-reduced-motion` drops the slide for a
6
- * plain fade.
5
+ * confirm() (see overlay.js). Under `prefers-reduced-motion` the kit's net
6
+ * (reduced-motion.css) makes the slide and the fade instant.
7
7
  *
8
8
  * Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
9
9
  * --drawer-surface panel background --drawer-shadow panel elevation
@@ -58,10 +58,7 @@
58
58
  flex-direction: column;
59
59
  background: var(--drawer-surface);
60
60
  box-shadow: var(--drawer-shadow);
61
- opacity: 1;
62
- transition:
63
- transform var(--dur-med) var(--ease),
64
- opacity var(--dur-med) var(--ease);
61
+ transition: transform var(--dur-med) var(--ease);
65
62
  }
66
63
  .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--drawer-shadow), var(--ring); }
67
64
  .ui-drawer.is-open .ui-drawer__panel { transform: none; }
@@ -110,6 +107,10 @@
110
107
  .ui-drawer--top.ui-drawer--lg, .ui-drawer--bottom.ui-drawer--lg { --drawer-h: 72vh; }
111
108
 
112
109
  /* -- Slots ----------------------------------------------------------------- */
110
+ /* The header keeps its rule, and the footer keeps its. A drawer's normal state is a
111
+ long record scrolling, and those two lines are what say where the scrolling body
112
+ ends: the one place inside the panel a line still earns.
113
+ why: docs/specification.md#the-drawer */
113
114
  .ui-drawer__header {
114
115
  display: flex;
115
116
  align-items: center;
@@ -174,14 +175,55 @@
174
175
  flex: none;
175
176
  }
176
177
 
177
- /* -- Reduced motion: no slide, just a fade -------------------------------- */
178
+ /* -- Groups and rows ------------------------------------------------------- */
179
+ /* drawerSection(): a heading over label/value rows. The value sits beside its
180
+ label rather than at the far edge, so nothing has to lead the eye across the
181
+ panel, and rows are held apart by space alone. Group from group is the one
182
+ division inside the body worth a line; the body's padding insets it, so it
183
+ parts the groups without cutting the panel in two.
184
+ why: docs/specification.md#the-drawer */
185
+ .ui-drawer__section + .ui-drawer__section {
186
+ margin-top: var(--space-6);
187
+ padding-top: var(--space-6);
188
+ border-top: 1px solid var(--border);
189
+ }
190
+ /* An h3, and it opts out of the display face for the reason .ui-drawer__title does.
191
+ why: docs/specification.md#typefaces */
192
+ .ui-drawer__section-title {
193
+ margin: 0 0 var(--space-2);
194
+ font-family: var(--font-sans);
195
+ font-size: var(--text-sm);
196
+ /* Semibold, and so outside the rank table, which has nothing at this size and
197
+ weight. A group heading set in the label rank would match the row labels
198
+ under it in size and weight both, and differ from them by colour alone. */
199
+ font-weight: var(--weight-semibold);
200
+ color: var(--strong);
201
+ }
202
+ .ui-drawer__rows {
203
+ display: grid;
204
+ grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
205
+ column-gap: var(--space-4);
206
+ margin: 0;
207
+ }
208
+ .ui-drawer__row { display: contents; }
209
+ .ui-drawer__row dt,
210
+ .ui-drawer__row dd {
211
+ margin: 0;
212
+ padding-block: var(--space-1);
213
+ overflow-wrap: anywhere;
214
+ }
215
+ .ui-drawer__row dt { color: var(--muted); }
216
+ .ui-drawer__row dd { color: var(--text); font-variant-numeric: tabular-nums; }
217
+ .ui-drawer__rows + * { margin-top: var(--space-3); }
218
+
219
+ /* Under reduced motion the net gives every element a 0.01ms transition, so a
220
+ child inherits `visible` one tick after the root: the control openDrawer()
221
+ focuses is still hidden in that frame, and focus lands on the panel. So an
222
+ open drawer carries no transition inside it at all — for as long as it is
223
+ open, not only in the frame it opens — which is what reduced motion asked
224
+ for anyway: under the net nothing inside had longer than 0.01ms to run.
225
+ why: docs/specification.md#reduced-motion-travels-with-the-stylesheet */
178
226
  @media (prefers-reduced-motion: reduce) {
179
- .ui-drawer { transition: visibility var(--dur-fast) linear; }
180
- .ui-drawer__panel {
181
- transform: none !important;
182
- opacity: 0;
183
- transition: opacity var(--dur-fast) linear;
184
- }
185
- .ui-drawer.is-open .ui-drawer__panel { opacity: 1; }
186
- .ui-drawer__scrim { transition: opacity var(--dur-fast) linear; }
227
+ .ui-drawer.is-open * { transition: none !important; }
187
228
  }
229
+
@@ -137,7 +137,7 @@
137
137
  /* Trailing selected tick (listbox variant) */
138
138
  .ui-dropdown__tick { margin-left: auto; flex: none; display: none; color: var(--accent); }
139
139
  .ui-dropdown__tick svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.4; }
140
- .ui-dropdown__item.is-selected .ui-dropdown__tick { display: inline-flex; }
140
+ .ui-dropdown__item.is-selected .ui-dropdown__tick { display: inline-flex; } /* motion: still — a mark inside a row whose highlight already transitions; picking it closes the panel, which fades out over --dur-med */
141
141
  .ui-dropdown__item.is-selected .ui-dropdown__label { color: var(--accent); }
142
142
 
143
143
  /* When a row has both a badge and a tick, keep the badge from eating the margin */
@@ -145,10 +145,9 @@
145
145
 
146
146
  /* Trailing status badge */
147
147
  .ui-dropdown__badge {
148
- font-size: 9.5px;
149
- letter-spacing: 0.12em;
150
- text-transform: uppercase;
151
- font-weight: 700;
148
+ /* rank: chip */
149
+ font-size: var(--text-xs);
150
+ font-weight: var(--weight-semibold);
152
151
  padding: 2px 7px;
153
152
  border-radius: var(--radius-pill);
154
153
  margin-left: auto;
@@ -166,13 +165,13 @@
166
165
  .ui-dropdown__item.is-danger .ui-dropdown__label { color: var(--muted); }
167
166
  .ui-dropdown__item.is-danger:hover .ui-dropdown__label { color: var(--pink); }
168
167
 
169
- /* Grouped sections */
170
- .ui-dropdown__section + .ui-dropdown__section { border-top: 1px solid var(--border); margin-top: 5px; padding-top: 5px; }
168
+ /* Grouped sections. `~` and :not([hidden]), because `+` counts a group a search
169
+ query has hidden and drew the divider above the first group left showing. */
170
+ .ui-dropdown__section:not([hidden]) ~ .ui-dropdown__section:not([hidden]) { border-top: 1px solid var(--border); margin-top: 5px; padding-top: 5px; }
171
171
  .ui-dropdown__group {
172
- font-size: 10px;
173
- letter-spacing: 0.1em;
174
- text-transform: uppercase;
175
- font-weight: 700;
172
+ /* rank: label */
173
+ font-size: var(--text-sm);
174
+ font-weight: var(--weight-medium);
176
175
  color: var(--muted);
177
176
  padding: 7px 12px 4px;
178
177
  }
@@ -187,3 +186,63 @@
187
186
  border-bottom: 1px solid var(--border);
188
187
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
189
188
  }
189
+
190
+ /* Search — `search: true` pins a field above the rows and filters them as the
191
+ reader types. The rows move into a list of their own, so the field stays put
192
+ while the list scrolls. Every rule below keys on a class only the search
193
+ variant emits, so the plain dropdown renders as it did.
194
+ why: docs/specification.md#a-dropdown-with-a-search-field */
195
+ .ui-dropdown__panel--search { min-width: 280px; }
196
+ /* Open, the panel is visible at once instead of at the first step of the
197
+ `visibility` transition, because a browser will not focus a field in a box
198
+ that is still `hidden` and opening puts focus in the field. Closing still fades. */
199
+ .ui-dropdown.open .ui-dropdown__panel--search,
200
+ .ui-dropdown__panel--search.is-open { transition-property: opacity, transform; }
201
+
202
+ .ui-dropdown__search { position: relative; display: flex; align-items: center; margin-bottom: 6px; }
203
+ .ui-dropdown__search-ic {
204
+ position: absolute;
205
+ left: 11px;
206
+ display: inline-flex;
207
+ color: var(--muted);
208
+ pointer-events: none;
209
+ }
210
+ /* 16px at 2.4 paints 1.6px, over the 1.5px floor stories/glyph-stroke.test.js holds. */
211
+ .ui-dropdown__search-ic svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.4; }
212
+ .ui-dropdown__search-input {
213
+ width: 100%;
214
+ font: inherit;
215
+ font-size: 12.5px;
216
+ color: var(--strong);
217
+ background: var(--surface);
218
+ border: 1px solid var(--border);
219
+ border-radius: var(--radius-sm);
220
+ padding: 8px 12px 8px 34px;
221
+ /* The height Chromium paints (38.25px), stated so the target-size gate, which
222
+ cannot see an <input>'s line of text, reads the box a reader hits. */
223
+ min-height: 38px;
224
+ transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
225
+ }
226
+ /* iOS Safari zooms the page into a focused field under 16px, and opening the
227
+ panel focuses this one. A real 16px, not a scaled one: the zoom reads the
228
+ computed size, and a transform would shrink the ring and border with it. */
229
+ @media (pointer: coarse) { .ui-dropdown__search-input { font-size: 16px; } }
230
+ .ui-dropdown__search-input::placeholder { color: var(--muted); }
231
+ .ui-dropdown__search-input:hover { border-color: var(--border-strong); }
232
+ .ui-dropdown__search-input:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
233
+
234
+ .ui-dropdown__list { max-height: 300px; overflow-y: auto; }
235
+ /* A row is `display: flex`, which outranks the UA's [hidden], so a row the
236
+ query hides is put away here. */
237
+ .ui-dropdown__list [hidden] { display: none; } /* motion: still — a row the query no longer matches is gone on the next keystroke */
238
+
239
+ /* The row Enter would pick. Focus stays in the field, which carries the ring,
240
+ so this row takes the hover fill and a bar rather than a second ring. */
241
+ .ui-dropdown__item.is-active { background: var(--surface); box-shadow: inset 2px 0 0 var(--accent); }
242
+
243
+ /* No match — a nudge and no action, per Guidelines / Microcopy. Empty while
244
+ anything matches, so it takes no room. */
245
+ .ui-dropdown__none { display: flex; flex-direction: column; gap: 3px; padding: 14px 12px 10px; text-align: center; }
246
+ .ui-dropdown__none:empty { display: none; }
247
+ .ui-dropdown__none-title { font-size: 12.5px; font-weight: 600; color: var(--strong); }
248
+ .ui-dropdown__none-hint { font-size: 11px; color: var(--muted); line-height: 1.35; }
@@ -127,6 +127,8 @@
127
127
  /* Error banner */
128
128
  .ui-fbc__err { margin: 14px 20px 0; background: color-mix(in srgb, var(--pink) 14%, transparent); color: var(--pink); font-size: 13px; padding: 11px 15px; border-radius: 11px; display: none; }
129
129
  .ui-fbc__err.show { display: block; }
130
+ /* A failed send's line fades in; wireFeedback() adds the class as it shows it. */
131
+ .ui-fbc__err.is-entering { animation: m-fade var(--dur-fast) var(--ease-out) both; }
130
132
 
131
133
  @media (prefers-reduced-motion: reduce) {
132
134
  .ui-fbpill, .ui-fbpill::before, .ui-fbcomposer, .ui-fbscrim, .ui-fbck, .ui-fbck-m, .ui-fbck-s { animation: none !important; transition: none !important; }
@@ -71,10 +71,9 @@
71
71
  gap: 30px 56px;
72
72
  }
73
73
  .ui-footer__col-title {
74
- font-size: 11px;
75
- font-weight: 700;
76
- letter-spacing: 0.1em;
77
- text-transform: uppercase;
74
+ /* rank: label */
75
+ font-size: var(--text-sm);
76
+ font-weight: var(--weight-medium);
78
77
  color: var(--footer-text);
79
78
  margin: 0 0 14px;
80
79
  }
@@ -49,7 +49,13 @@
49
49
 
50
50
  /* The value a reader typed is still the value; it goes quiet, it does not fade
51
51
  into the field under it the way an opacity would take both down together. #220 */
52
+ /* `.ui-select` joins these two in #273. It had no disabled rule at all, and it
53
+ sets `background: var(--surface-2)` as an author declaration — which outranks
54
+ the UA's own grey-out — so a disabled select rendered pixel-identical to a live
55
+ one. A pager marked busy showed every button and the jump input visibly off
56
+ beside a size control that still looked available. */
52
57
  .ui-input:disabled,
58
+ .ui-select:disabled,
53
59
  .ui-textarea:disabled {
54
60
  background: var(--disabled-surface);
55
61
  border-color: var(--disabled-border);
@@ -134,7 +140,7 @@
134
140
  width: 5px; height: 10px;
135
141
  border: solid var(--accent-contrast);
136
142
  border-width: 0 2px 2px 0;
137
- transform: rotate(45deg);
143
+ transform: rotate(45deg); /* motion: still — the turn is the tick's shape, not travel; it lands with the box's fill, which transitions */
138
144
  }
139
145
  .ui-check input[type="radio"]:checked::after {
140
146
  content: "";
@@ -78,7 +78,7 @@
78
78
  smallest of three weak signals doing the most work, so it grows with the row. */
79
79
  .ui-app__rail .ui-nav__ic svg { opacity: 0.62; }
80
80
  .ui-app__rail .ui-nav__item.is-active .ui-nav__ic svg,
81
- .ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; }
81
+ .ui-app__rail .ui-nav__item.is-current .ui-nav__ic svg { opacity: 1; stroke: currentColor; } /* motion: still — the glyph brightens with its row, whose own highlight already transitions */
82
82
  .ui-app__rail .ui-nav__item.is-active::before { width: 3px; height: 62%; }
83
83
 
84
84
  /* Sign out is a navigation row and rests like one. The danger intent is the
@@ -133,7 +133,8 @@
133
133
  .ui-app__main .ui-nav--crumbs { margin-bottom: var(--space-5); }
134
134
  .ui-app__main h1 {
135
135
  color: var(--strong);
136
- font-size: 30px;
136
+ /* rank: page-title */
137
+ font-size: var(--text-2xl);
137
138
  font-weight: var(--weight-bold);
138
139
  line-height: 1.1;
139
140
  letter-spacing: var(--tracking-tight);
@@ -31,6 +31,11 @@
31
31
  .ui-busy {
32
32
  display: block;
33
33
  }
34
+ /* What setBusy() puts in the body fades in, without travel. The region the page
35
+ loads with is not animated: only setBusy() adds the class. */
36
+ .ui-busy__body.is-entering {
37
+ animation: m-fade var(--dur-med) var(--ease-out) both;
38
+ }
34
39
 
35
40
  /* -- Skeleton -------------------------------------------------------------- */
36
41
  .ui-skel {