actual-css 0.5.0 → 0.7.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 (101) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/actual.css +1 -0
  3. package/dist/actual.full.css +589 -131
  4. package/dist/actual.full.js +1 -1
  5. package/dist/actual.full.min.css +1 -1
  6. package/dist/actual.min.css +1 -1
  7. package/docs/design-notes/browser-support.md +39 -14
  8. package/docs/design-notes/css-anchor.md +1 -1
  9. package/docs/design-notes/enhancement-contract.md +30 -13
  10. package/docs/design-notes/forced-colors.md +11 -7
  11. package/docs/design-notes/grid.md +4 -4
  12. package/docs/design-notes/naming.md +15 -9
  13. package/docs/design-notes/platform-alignment.md +60 -0
  14. package/docs/design-notes/popover-manual-poc.md +248 -0
  15. package/docs/design-notes/references.md +4 -0
  16. package/docs/design-notes/steps.md +748 -0
  17. package/docs/design-notes/surface-reparenting.md +122 -0
  18. package/docs/design-notes/widget-primitives.md +32 -15
  19. package/docs/navigation.json +1 -0
  20. package/docs/pages/components/alert.md +13 -8
  21. package/docs/pages/components/aura.md +102 -6
  22. package/docs/pages/components/avatar.md +9 -6
  23. package/docs/pages/components/badge.md +11 -7
  24. package/docs/pages/components/button.md +17 -17
  25. package/docs/pages/components/card.md +15 -8
  26. package/docs/pages/components/chat.md +1 -1
  27. package/docs/pages/components/dialog.md +10 -7
  28. package/docs/pages/components/drawer.md +1 -1
  29. package/docs/pages/components/fab.md +12 -10
  30. package/docs/pages/components/indicator.md +6 -6
  31. package/docs/pages/components/list.md +11 -2
  32. package/docs/pages/components/navbar.md +1 -1
  33. package/docs/pages/components/pagination.md +1 -1
  34. package/docs/pages/components/rating.md +4 -4
  35. package/docs/pages/components/skeleton.md +1 -1
  36. package/docs/pages/components/spinner.md +1 -1
  37. package/docs/pages/components/steps.md +376 -52
  38. package/docs/pages/components/table.md +5 -5
  39. package/docs/pages/enhancements/flyout.md +104 -12
  40. package/docs/pages/enhancements/overview.md +2 -2
  41. package/docs/pages/enhancements/scrollspy.md +1 -1
  42. package/docs/pages/enhancements/status-bar.md +20 -0
  43. package/docs/pages/enhancements/tabs.md +1 -1
  44. package/docs/pages/enhancements/tooltip.md +12 -0
  45. package/docs/pages/examples/overview.md +2 -0
  46. package/docs/pages/forms/choice-cards.md +4 -4
  47. package/docs/pages/forms/combobox.md +87 -0
  48. package/docs/pages/forms/floating-field.md +5 -3
  49. package/docs/pages/forms/joined-controls.md +4 -4
  50. package/docs/pages/forms/labels.md +8 -5
  51. package/docs/pages/forms/overview.md +25 -18
  52. package/docs/pages/forms/password.md +3 -2
  53. package/docs/pages/forms/range.md +13 -2
  54. package/docs/pages/forms/states-and-sizes.md +6 -4
  55. package/docs/pages/forms/validation.md +13 -9
  56. package/docs/pages/foundations/philosophy.md +15 -1
  57. package/docs/pages/foundations/theming.md +8 -0
  58. package/docs/pages/foundations/tokens.md +9 -6
  59. package/docs/pages/foundations/typography.md +1 -1
  60. package/docs/pages/guides/bootstrap.md +3 -3
  61. package/docs/pages/guides/introduction.md +1 -1
  62. package/docs/pages/guides/modular-import.md +21 -10
  63. package/docs/pages/guides/progressive-enhancement.md +96 -73
  64. package/docs/pages/guides/tailwind.md +2 -2
  65. package/docs/pages/layout/app-shell.md +4 -4
  66. package/docs/pages/layout/choosing.md +10 -6
  67. package/docs/pages/layout/grid.md +11 -10
  68. package/docs/pages/layout/stack.md +10 -6
  69. package/docs/pages/patterns/structured-lists.md +7 -7
  70. package/docs/pages/utilities/accessibility.md +1 -1
  71. package/docs/pages/utilities/flex.md +20 -13
  72. package/docs/pages/utilities/links.md +1 -1
  73. package/docs/pages/utilities/list.md +1 -1
  74. package/docs/pages/utilities/overflow.md +14 -7
  75. package/docs/pages/utilities/overview.md +22 -0
  76. package/docs/pages/utilities/shape.md +1 -1
  77. package/docs/pages/utilities/sizing-wrapping.md +10 -8
  78. package/docs/pages/utilities/spacing.md +26 -19
  79. package/docs/pages/utilities/text.md +1 -1
  80. package/llms.txt +27 -4
  81. package/package.json +7 -4
  82. package/scripts/reserved-classes.json +4 -1
  83. package/src/css/components/fab.css +44 -8
  84. package/src/css/components/flyout.css +49 -3
  85. package/src/css/components/join.css +7 -0
  86. package/src/css/components/list.css +3 -1
  87. package/src/css/components/menu.css +16 -10
  88. package/src/css/components/steps.css +277 -69
  89. package/src/css/components/tooltip.css +37 -1
  90. package/src/css/core/tokens.css +1 -0
  91. package/src/css/effects/aura.css +101 -11
  92. package/src/css/forms/custom-select.css +1 -1
  93. package/src/css/forms/floating-field.css +8 -6
  94. package/src/css/layout/container-query.css +37 -13
  95. package/src/css/layout/grid.css +7 -7
  96. package/src/css/layout/index.css +1 -1
  97. package/src/js/password.js +1 -1
  98. package/src/js/status.js +38 -2
  99. package/src/js/surface.js +28 -12
  100. package/src/js/tooltip.js +2 -2
  101. package/src/js/floating.js +0 -432
@@ -1,20 +1,54 @@
1
1
  /*
2
- * Steps — semantic ordered-list progression. One intrinsic layout: items fill
3
- * the row when they fit and hold a minimum width otherwise, so the sequence
4
- * scrolls horizontally only when it genuinely cannot fit. No breakpoint decides
5
- * this — the item minimum tracks the real content, not an arbitrary width.
2
+ * Steps — semantic ordered-list progression for a multi-step process.
6
3
  *
7
- * Markers come from the list counter; state stays in .complete and
8
- * aria-current="step", so the component needs no runtime.
4
+ * One primitive, two explicit compositions. Both classes are required:
5
+ *
6
+ * <ol class="steps steps-horizontal"> horizontal, 2 to 5 stages
7
+ * <ol class="steps steps-vertical"> vertical, any number of stages
8
+ *
9
+ * `.steps` is what a step *is* — the counter, the marker, `.step-label`,
10
+ * `.step-complete`, `aria-current="step"`. It carries no orientation, so it is
11
+ * never written alone. `.steps-horizontal` and `.steps-vertical` are what a
12
+ * sequence *looks like*, and they are peers: neither is the default the other
13
+ * has to exclude, so no rule says `:not(...)` and no rule undoes another's
14
+ * layout.
15
+ *
16
+ * Orientation and labels are independent dimensions. `.step-label` is optional
17
+ * in either orientation; when a sequence carries names, every step carries
18
+ * one:
19
+ *
20
+ * empty steps .step-label on every step
21
+ * horizontal compact progression named sequence, stacked or inline
22
+ * vertical compact rail named sequence, labels beside markers
23
+ *
24
+ * An empty step is a marker and nothing else, so it is compact by structure —
25
+ * `> li:empty` in both orientations, no variant class, and nothing authored
26
+ * only to be hidden again. The page around it owns the step name.
27
+ *
28
+ * A label may be an `a[href]`, which is the whole interactive surface: the
29
+ * marker is never navigable, so navigating a flow means naming its
30
+ * destinations. Anything else focusable in a step is out of contract.
31
+ *
32
+ * The framework never hides an authored label to solve a layout problem. A
33
+ * labelled row that outgrows its space scrolls. An `actual-container` size
34
+ * context buys it one thing: the wide representation, marker beside label.
35
+ *
36
+ * See docs/design-notes/steps.md for the threshold, the selector shapes, the
37
+ * rejected alternatives, and why there is no forced-colors branch.
38
+ *
39
+ * Each public hook is followed by the class that owns it.
9
40
  *
10
41
  * Public hooks:
11
- * --step-size
12
- * --step-min
13
- * --step-gap
14
- * --step-line-size
15
- * --step-marker-radius
16
- * --step-complete-mark
17
- * --step-connector
42
+ * --step-size .steps
43
+ * --step-inline-gap .steps
44
+ * --step-line-size .steps
45
+ * --step-marker-radius .steps
46
+ * --step-complete-mark .steps
47
+ * --step-min .steps-horizontal
48
+ * --step-gap .steps-horizontal
49
+ * --step-connector .steps-horizontal
50
+ * --step-inline-connector .steps-horizontal
51
+ * --step-vertical-gap .steps-vertical
18
52
  *
19
53
  * Internal:
20
54
  * --step-marker-bg
@@ -22,10 +56,12 @@
22
56
  * --step-marker-fg
23
57
  * --step-line
24
58
  */
59
+
60
+ /* Step primitive --------------------------------------------------------- */
61
+
25
62
  .steps {
26
63
  --step-size: 1.75rem;
27
- --step-min: 7rem;
28
- --step-gap: var(--space-20);
64
+ --step-inline-gap: var(--space-20);
29
65
  --step-line-size: var(--border-width);
30
66
  --step-marker-bg: var(--surface);
31
67
  --step-marker-border: var(--border);
@@ -35,29 +71,14 @@
35
71
 
36
72
  counter-reset: actual-step;
37
73
  display: flex;
38
- flex-direction: row;
39
- inline-size: 100%;
40
74
  margin: 0;
41
75
  padding: 0;
42
76
  list-style: none;
43
- /* Fill when it fits, scroll when it cannot: items never shrink below
44
- --step-min, so labels keep a readable width and the sequence scrolls
45
- horizontally instead of squishing. overscroll-behavior keeps the scroll
46
- from chaining to the page. */
47
- overflow-x: auto;
48
- overscroll-behavior-inline: contain;
49
77
  }
50
78
 
51
79
  .steps > li {
52
80
  position: relative;
53
- display: grid;
54
- flex: 1 0 var(--step-min);
55
- grid-template-rows: var(--step-size) auto;
56
- gap: var(--step-gap);
57
- justify-items: center;
58
- min-inline-size: max(var(--step-size), var(--step-min));
59
81
  color: var(--text-muted);
60
- text-align: center;
61
82
  counter-increment: actual-step;
62
83
  }
63
84
 
@@ -78,30 +99,38 @@
78
99
  line-height: 1;
79
100
  }
80
101
 
81
- .steps > li::after {
82
- content: "";
83
- position: absolute;
84
- inset-block-start: calc((var(--step-size) - var(--step-line-size)) / 2);
85
- inset-inline-start: calc(50% + var(--step-size) / 2);
86
- inline-size: calc(100% - var(--step-size));
87
- block-size: var(--step-line-size);
88
- /* A custom property resolves its var() before inheritance, so a default
89
- --step-connector: var(--step-line) on .steps would freeze the parent line
90
- color and a .complete child's connector would never follow its own line.
91
- Falling back to the local --step-line here keeps each connector in sync;
92
- an explicit --step-connector still wins when the author sets one. */
93
- background: var(--step-connector, var(--step-line));
102
+ /* The marker carries the state, so the label reads a notch below it — set on
103
+ the component, never per representation: a size that changed with the layout
104
+ would resize the text under a reader dragging a window. `color: inherit` is
105
+ here rather than in a rule of its own for `a.step-label`, because a span
106
+ inherits its ink anyway; only a link has anything to take back, and taking it
107
+ back for every label costs nothing and one rule less. A navigable label keeps
108
+ its underline, hover and focus ring — just not a link color, which would read
109
+ ahead of the marker states.
110
+
111
+ Scoped to `.steps`, unlike `.menu-item-text` and the framework's other
112
+ sub-part classes: a bare `.step-label` weighs (0,1,0), exactly tying
113
+ `.prose :where(a)`, so a stepper in prose would be decided by bundle order. */
114
+ .steps .step-label {
115
+ font-size: var(--font-size-sm);
116
+ line-height: var(--line-height-tight);
117
+ color: inherit;
94
118
  }
95
119
 
96
- .steps > li:last-child::after {
97
- content: none;
120
+ /* Each composition gives the connector its own geometry and background, so the
121
+ only shared fact is which steps get one — every step but the last. Stated as
122
+ the condition rather than as a `content: none` rule undoing a `content: ""`
123
+ rule: a pseudo-element with no content generates no box, so a composition can
124
+ address `> li::after` freely and still not trail a line past the last step. */
125
+ .steps > li:not(:last-child)::after {
126
+ content: "";
98
127
  }
99
128
 
100
- /* Three states must be readable without a check glyph: future stays neutral,
101
- complete is a filled marker, current is an outlined one. Filled reads as
102
- "done", outlined as "you are here" — the distinction survives a numbered
103
- stepper that never sets --step-complete-mark. */
104
- .steps > .complete {
129
+ /* Prefixed like `.step-label`, unlike the framework's bare `.primary` /
130
+ `.outline` modifiers: those qualify a component on its own element, this
131
+ marks one item inside one. A bare `.complete` would also claim a very
132
+ ordinary English word globally, for a single component's single state. */
133
+ .steps > .step-complete {
105
134
  --step-marker-bg: var(--state-selected);
106
135
  --step-marker-border: var(--state-selected);
107
136
  --step-marker-fg: var(--state-selected-fg);
@@ -110,10 +139,9 @@
110
139
  color: var(--text);
111
140
  }
112
141
 
113
- /* Declared after .complete at equal specificity, and it wins on every channel
114
- including the connector: the accented line means "already walked", so the
115
- segment leaving the current step must stay neutral. The line reaching the
116
- current step is owned by the previous step, which is complete. */
142
+ /* Complete owns the accented connector that reaches the current step, so the
143
+ connector leaving current goes back to neutral. Declared after
144
+ .step-complete at equal specificity: current wins on every channel. */
117
145
  .steps > [aria-current="step"] {
118
146
  --step-marker-bg: var(--surface);
119
147
  --step-marker-border: var(--state-selected);
@@ -123,27 +151,207 @@
123
151
  color: var(--text);
124
152
  }
125
153
 
126
- /* Thicker border is the current affordance in every mode — structural, so it
127
- survives forced colors, and it never competes with the outline channel owned
128
- by focus. */
129
154
  .steps > [aria-current="step"]::before {
130
155
  border-width: calc(var(--border-width) * 2);
131
156
  }
132
157
 
133
- /* A finished step may replace its number with a mark (e.g. a check glyph):
134
- --step-complete-mark accepts any content value and falls back to the
135
- counter, so existing numbered steppers are unchanged. Excluded on the
136
- current step so "current wins" holds on the glyph too, not only on color. */
137
- .steps > .complete:not([aria-current="step"])::before {
158
+ /* Weight marks "where I am now". Complete needs none: its filled disc already
159
+ reads as strongly. */
160
+ .steps > [aria-current="step"] .step-label {
161
+ font-weight: var(--font-weight-strong);
162
+ }
163
+
164
+ .steps > .step-complete:not([aria-current="step"])::before {
138
165
  content: var(--step-complete-mark, counter(actual-step));
139
166
  }
140
167
 
141
- /* Forced colors drop the accent fill, which would leave complete and future
142
- markers identical — current keeps its thicker ring, complete needs a system
143
- color to stay filled. Same exclusion, for the same reason. */
144
- @media (forced-colors: active) {
145
- .steps > .complete:not([aria-current="step"])::before {
146
- background: Highlight;
147
- color: HighlightText;
168
+ /* Horizontal ------------------------------------------------------------- */
169
+
170
+ .steps-horizontal {
171
+ --step-min: 7rem;
172
+ --step-gap: var(--space-20);
173
+
174
+ inline-size: 100%;
175
+ overflow-x: auto;
176
+ /* Stated, never left to `overflow-x` alone: one scrolling axis computes the
177
+ other to `auto`, and a row hugs fractional content closely enough that a
178
+ rounding difference then raises a phantom vertical scrollbar. */
179
+ overflow-y: hidden;
180
+ overscroll-behavior-inline: contain;
181
+ }
182
+
183
+ /* A scroll container clips at the padding edge, so a navigable row reserves the
184
+ focus ring's own width and offset. Only this composition scrolls, and only a
185
+ link has a ring to protect. */
186
+ .steps-horizontal:has(a[href]) {
187
+ padding-block: calc(var(--focus-outline-offset) + var(--border-width) * 2);
188
+ }
189
+
190
+ /* One explicit row, the marker's: a `.step-label` is auto-placed into an
191
+ implicit second row, and two tracks are also the only thing `gap` has to sit
192
+ between. So an empty step measures exactly `--step-size`, and the rule below
193
+ takes back nothing. `var(--step-size) auto` would build that second row
194
+ whether or not a label ever arrives, and the compact form would then have to
195
+ remove both the track and the gap. */
196
+ .steps-horizontal > li {
197
+ display: grid;
198
+ flex: 1 0 var(--step-min);
199
+ grid-template-rows: var(--step-size);
200
+ gap: var(--step-gap);
201
+ justify-items: center;
202
+ min-inline-size: max(var(--step-size), var(--step-min));
203
+ text-align: center;
204
+ }
205
+
206
+ /* An empty step is a marker and nothing else, so it needs a marker's width
207
+ rather than a reading width. That is what lets five of them fit a phone with
208
+ no size context and no variant class.
209
+
210
+ `justify-items` is not an override of the row's centring: the marker is
211
+ centred because a label sits under it and the two share a centre, and with no
212
+ label there is nothing to centre against. The marker takes the start of its
213
+ own share instead, the connector below fills the rest of it, and the last
214
+ step has no share to fill — so the track runs edge to edge and lines up with
215
+ the text around it, the way the inline representation does.
216
+
217
+ `:empty` says exactly what the markup says, the way `.badge:empty` does, and
218
+ it reads the item's own content: bare text in an `<li>` keeps a reading width
219
+ like its wrapped peers, where `> li:has(.step-label)` would give it a marker
220
+ floor and skew the row. It also predates `:has()` by a decade, so the compact
221
+ form reaches the Degraded tier and only the inline bonus needs `:has()`. */
222
+ .steps-horizontal > li:empty {
223
+ flex: 1 1 0;
224
+ justify-items: start;
225
+ min-inline-size: var(--step-size);
226
+ }
227
+
228
+ .steps-horizontal > li:empty::after {
229
+ inset-inline-start: var(--step-size);
230
+ }
231
+
232
+ .steps-horizontal > li:empty:last-child {
233
+ flex: 0 0 auto;
234
+ }
235
+
236
+ /* The pseudo-element is marker-height so --step-connector may be any complete
237
+ background value. */
238
+ .steps-horizontal > li::after {
239
+ position: absolute;
240
+ inset-block-start: 0;
241
+ inset-inline-start: calc(50% + var(--step-size) / 2);
242
+ inline-size: calc(100% - var(--step-size));
243
+ block-size: var(--step-size);
244
+ background: var(
245
+ --step-connector,
246
+ linear-gradient(var(--step-line), var(--step-line)) center /
247
+ 100% var(--step-line-size) no-repeat
248
+ );
249
+ }
250
+
251
+ /*
252
+ * The one horizontal container query. Past 60rem a labelled row has the room
253
+ * to put marker and label inline and still leave the connector a real region
254
+ * between groups, rather than a stub. Calibrated on five steps — the widest
255
+ * supported row, and therefore the binding case — and it counts nothing, so it
256
+ * serves every count. Below it, stacked; a marker row is already the shape
257
+ * this query produces and stays out of it.
258
+ *
259
+ * Horizontal is a 2-5 step component. Outside that domain the rule simply
260
+ * applies as written, because a row that is out of contract is not owed a
261
+ * curated fallback.
262
+ */
263
+ @supports (container-type: inline-size) {
264
+ @container actual-container (inline-size >= 60rem) {
265
+ .steps-horizontal:has(.step-label) > li {
266
+ display: flex;
267
+ flex: 1 1 auto;
268
+ align-items: center;
269
+ gap: var(--step-inline-gap);
270
+ min-inline-size: 0;
271
+ text-align: start;
272
+ white-space: nowrap;
273
+ }
274
+
275
+ .steps-horizontal:has(.step-label) > li::after {
276
+ position: static;
277
+ flex: 1 1 2rem;
278
+ inline-size: auto;
279
+ block-size: var(--step-size);
280
+ margin-inline-end: var(--step-inline-gap);
281
+ background: var(
282
+ --step-inline-connector,
283
+ var(
284
+ --step-connector,
285
+ linear-gradient(var(--step-line), var(--step-line)) center /
286
+ 100% var(--step-line-size) no-repeat
287
+ )
288
+ );
289
+ }
290
+
291
+ .steps-horizontal:has(.step-label) > li:last-child {
292
+ flex: 0 0 auto;
293
+ }
148
294
  }
149
295
  }
296
+
297
+ /* Vertical --------------------------------------------------------------- */
298
+
299
+ .steps-vertical {
300
+ --step-vertical-gap: var(--space-40);
301
+
302
+ flex-direction: column;
303
+ gap: var(--step-vertical-gap);
304
+ }
305
+
306
+ /* `justify-items` looks like a reset and is not one — nothing here resets
307
+ anything, since the horizontal rules never applied. Grid items blockify, so
308
+ without it a label stretches the whole 1fr track: 264px against 75px for an
309
+ `a.step-label`, a full-width focus ring the row does not give the same link. */
310
+ .steps-vertical > li {
311
+ display: grid;
312
+ grid-template-columns: var(--step-size) minmax(0, 1fr);
313
+ align-items: start;
314
+ justify-items: start;
315
+ gap: var(--step-inline-gap);
316
+ min-block-size: var(--step-size);
317
+ }
318
+
319
+ /* An empty step is a marker rail. The column cannot use the row's implicit
320
+ second track, because `grid-auto-flow` is `row`: an auto-placed label would
321
+ land *under* its marker. Its second track is therefore explicit, and this
322
+ takes it back.
323
+
324
+ Silent in a stretched column, where the item fills its line whatever its
325
+ tracks say. It bites in a shrink-to-fit box — which is what a sticky
326
+ progress rail usually is — where the label track and the gap to it make the
327
+ rail `--step-inline-gap` wider than the markers it draws: measured 36px
328
+ against 28px. No `gap: 0` needed; one track has nothing to sit beside. */
329
+ .steps-vertical > li:empty {
330
+ grid-template-columns: var(--step-size);
331
+ }
332
+
333
+ /* The row starts at the top so a wrapped label begins level with its marker.
334
+ align-self only bites on a single line, which would otherwise float above
335
+ the marker's centre.
336
+
337
+ The one `.step-label` rule that keeps a child combinator: `align-self` is a
338
+ grid-item property, so this declaration is meaningless on anything but a
339
+ direct child of the row. Elsewhere the label is addressed as a descendant —
340
+ the framework owns the structure, and repeating it buys nothing. */
341
+ .steps-vertical > li > .step-label {
342
+ align-self: center;
343
+ }
344
+
345
+ /* The vertical track publishes no hook of its own: a horizontal connector value
346
+ cannot transpose onto this geometry, and this selector is already a reachable
347
+ override for the design that finally asks. */
348
+ .steps-vertical > li::after {
349
+ position: absolute;
350
+ inset-block-start: var(--step-size);
351
+ inset-inline-start: calc((var(--step-size) - var(--step-line-size)) / 2);
352
+ inline-size: var(--step-line-size);
353
+ block-size: calc(100% - var(--step-size) + var(--step-vertical-gap));
354
+ background:
355
+ linear-gradient(var(--step-line), var(--step-line)) center / var(--step-line-size) 100%
356
+ no-repeat;
357
+ }
@@ -39,21 +39,57 @@
39
39
  transition: opacity var(--duration-fast) ease;
40
40
  }
41
41
 
42
+ /* Neutralize only the native geometry that conflicts with a positioned
43
+ tooltip. In particular, the UA overflow would turn the arrow into scroll
44
+ overflow instead of letting it project beyond the bubble. Gated on real
45
+ Popover support: the attribute selector matches whether or not the engine
46
+ implements Popover, and without it nothing hides the element, so an ungated
47
+ reset would leave a stray bubble parked at inset: auto. Popover sits outside
48
+ Actual's Minimal contract, so the gate tracks the capability rather than the
49
+ floor. */
50
+ @supports selector(:popover-open) {
51
+ .tooltip[popover] {
52
+ inset: auto;
53
+ margin: 0;
54
+ overflow: visible;
55
+ }
56
+
57
+ /* .tooltip declares no display of its own, so the UA already hides a closed
58
+ popover here. The rule is written anyway: both components advertised as
59
+ Popover-compatible should cede the hidden state the same way, and the
60
+ opacity target is what actually makes the native exit fade — see the
61
+ overlay note below. */
62
+ .tooltip[popover]:not(:popover-open) {
63
+ display: none;
64
+ opacity: 0;
65
+ }
66
+ }
67
+
42
68
  .tooltip[hidden] {
43
69
  display: none;
44
70
  opacity: 0;
45
71
  }
46
72
 
73
+ /* Entry opacity for both lifecycles: a runtime-driven tip is visible when it
74
+ is neither a popover nor [hidden], a native one when it is :popover-open.
75
+ compat-ok: [popover] appears here only under negation, which every engine
76
+ evaluates correctly whether or not it implements Popover, :popover-open
77
+ rides in a forgiving :is(), and @starting-style is itself unsupported across
78
+ part of the Minimal range — so the rule does not exist there at all. */
47
79
  @starting-style {
48
- .tooltip:not([hidden]) {
80
+ .tooltip:is(:not([popover]):not([hidden]), :popover-open) {
49
81
  opacity: 0;
50
82
  }
51
83
  }
52
84
 
85
+ /* overlay is the native popover counterpart of display: it holds the tip in
86
+ the top layer until the fade finishes, instead of letting hidePopover()
87
+ demote it on the first frame. Ignored where unsupported. */
53
88
  @supports (transition-behavior: allow-discrete) {
54
89
  .tooltip {
55
90
  transition:
56
91
  opacity var(--duration-fast) ease,
92
+ overlay var(--duration-fast) allow-discrete,
57
93
  display var(--duration-fast) allow-discrete;
58
94
  }
59
95
  }
@@ -149,6 +149,7 @@
149
149
  --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
150
150
  --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
151
151
  --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E");
152
+ --icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='12' y1='5' x2='12' y2='19'/%3E%3Cline x1='5' y1='12' x2='19' y2='12'/%3E%3C/svg%3E");
152
153
 
153
154
  /* Viewport size tokens — fallback vh/vw with dvh/vi enhancement.
154
155
  Consumed by full-viewport components (app-shell, drawer, modal, flyout).
@@ -1,12 +1,23 @@
1
1
  /*
2
2
  * Decorative intent-colored frame for one direct child.
3
3
  *
4
+ * The effect is built from three layers with distinct roles: a frame carrying
5
+ * a short accent of light, a small halo that belongs to the frame and keeps
6
+ * that light attached to the edge, and a wide static halo that belongs to the
7
+ * object and gives it a permanent, diffuse presence. Only the small halo
8
+ * follows the animated gradient, so a rotating aura repaints one blurred layer
9
+ * per frame, not two.
10
+ *
4
11
  * Public hooks:
5
12
  * --aura-color
6
13
  * --aura-width
7
14
  * --aura-blur
15
+ * --aura-intensity
8
16
  * --aura-radius
9
17
  * --aura-duration
18
+ *
19
+ * Internal:
20
+ * --aura-angle
10
21
  */
11
22
  @property --aura-angle {
12
23
  syntax: "<angle>";
@@ -23,7 +34,8 @@
23
34
  .aura {
24
35
  --aura-color: var(--intent, var(--primary));
25
36
  --aura-width: 2px;
26
- --aura-blur: 0.75rem;
37
+ --aura-blur: 0.25rem;
38
+ --aura-intensity: 1;
27
39
  --aura-radius: var(--radius-lg);
28
40
  --aura-duration: 6s;
29
41
 
@@ -34,9 +46,19 @@
34
46
  border-radius: calc(var(--aura-radius) + var(--aura-width));
35
47
  border: var(--border-width) solid transparent;
36
48
  background: var(--aura-color);
37
- background: conic-gradient(from var(--aura-angle), transparent 0 65%, var(--aura-color));
49
+
50
+ /* A short feathered accent rather than a long wedge with a hard edge, which
51
+ reads as one lit side. Nothing tints the frame outside it: a constant tint
52
+ across 2px draws an outline, the opposite of the effect. */
53
+ background: conic-gradient(
54
+ from var(--aura-angle),
55
+ transparent 0 78%,
56
+ var(--aura-color) 93%,
57
+ transparent
58
+ );
38
59
  }
39
60
 
61
+ /* Near halo — inherits the frame gradient, so it tracks the lit accent. */
40
62
  .aura::before {
41
63
  position: absolute;
42
64
  z-index: -1;
@@ -44,15 +66,40 @@
44
66
  border-radius: inherit;
45
67
  background: inherit;
46
68
  filter: blur(var(--aura-blur));
47
- opacity: 0.45;
69
+ opacity: calc(0.55 * var(--aura-intensity));
48
70
  pointer-events: none;
49
71
  content: "";
50
72
  }
51
73
 
74
+ /* Far halo — flat and static. At this blur the rotating detail is no longer
75
+ legible, so there is nothing to gain from animating a second wide blurred
76
+ layer. It outgrows the frame, otherwise the child covers it and the whole
77
+ effect reads as a drop shadow. This is the layer that stays lit at all
78
+ times, so it — not the frame — is what keeps the object present between two
79
+ passes of the accent. */
80
+ .aura::after {
81
+ position: absolute;
82
+ z-index: -1;
83
+ inset: calc(-1 * var(--aura-blur));
84
+ border-radius: calc(var(--aura-radius) + var(--aura-width) + var(--aura-blur));
85
+ background: var(--aura-color);
86
+ filter: blur(calc(var(--aura-blur) * 4));
87
+ opacity: calc(0.15 * var(--aura-intensity));
88
+ pointer-events: none;
89
+ content: "";
90
+ }
91
+
92
+ /* Lifts the child above both halos. It also has to be opaque: the frame paints
93
+ across the whole box and only its edge is meant to show, so a transparent
94
+ child (an .outline alert, a .ghost button) is filled with the frame color
95
+ rather than framed by it. Documented as a constraint, not worked around —
96
+ masking a ring would cost more than the case is worth. */
52
97
  .aura > * {
53
98
  position: relative;
54
99
  }
55
100
 
101
+ /* Glow lights the whole perimeter instead of a short accent, so both halos
102
+ become a single soft field around the child. */
56
103
  .aura-glow {
57
104
  background: var(--aura-color);
58
105
  }
@@ -61,6 +108,29 @@
61
108
  .aura:not(.aura-glow) {
62
109
  animation: aura-rotate var(--aura-duration) linear infinite;
63
110
  }
111
+
112
+ /* Both halos breathe: moving the near one alone changes a line's brightness,
113
+ which barely registers. Opening the wide field with it is what reads as a
114
+ breath. */
115
+ .aura-glow::before {
116
+ animation: aura-breathe-near var(--aura-duration) ease-in-out infinite;
117
+ }
118
+
119
+ .aura-glow::after {
120
+ animation: aura-breathe-far var(--aura-duration) ease-in-out infinite;
121
+ }
122
+ }
123
+
124
+ /* Without motion the accent would freeze at an arbitrary angle and read as a
125
+ broken animation, so it becomes a deliberate composition: two opposite
126
+ corners. Fixed radii keep those corners stable across aspect ratios;
127
+ proportional radii turn wide or tall elements into lit edges instead. */
128
+ @media (prefers-reduced-motion: reduce) {
129
+ .aura:not(.aura-glow) {
130
+ background:
131
+ radial-gradient(4rem 4rem at 0% 0%, var(--aura-color), transparent 70%),
132
+ radial-gradient(4rem 4rem at 100% 100%, var(--aura-color), transparent 70%);
133
+ }
64
134
  }
65
135
 
66
136
  @keyframes aura-rotate {
@@ -69,16 +139,36 @@
69
139
  }
70
140
  }
71
141
 
72
- @supports selector(:has(*)) {
73
- .aura:has(> .btn) {
74
- --aura-radius: var(--radius);
142
+ /* Only the peak is declared, so each halo breathes up from its own resting
143
+ opacity and never dips below the static glow. The peaks scale with the same
144
+ intensity as the resting values, which keeps the breath proportional at
145
+ every setting. */
146
+ @keyframes aura-breathe-near {
147
+ 50% {
148
+ opacity: calc(0.95 * var(--aura-intensity));
149
+ transform: scale(1.03);
75
150
  }
151
+ }
76
152
 
77
- .aura:has(> :is(.card, .alert)) {
78
- --aura-radius: var(--radius-lg);
153
+ @keyframes aura-breathe-far {
154
+ 50% {
155
+ opacity: calc(0.28 * var(--aura-intensity));
156
+ transform: scale(1.07);
79
157
  }
158
+ }
80
159
 
81
- .aura:has(> .badge) {
82
- --aura-radius: var(--radius-full);
83
- }
160
+ /* Each rule is a lone :has() setting a decorative radius, so a browser without
161
+ :has() simply drops it and keeps the default. No @supports needed — that
162
+ guard only earns its place around a selector list that mixes :has() with
163
+ selectors worth keeping. */
164
+ .aura:has(> .btn) {
165
+ --aura-radius: var(--radius);
166
+ }
167
+
168
+ .aura:has(> :is(.card, .alert)) {
169
+ --aura-radius: var(--radius-lg);
170
+ }
171
+
172
+ .aura:has(> .badge) {
173
+ --aura-radius: var(--radius-full);
84
174
  }
@@ -24,7 +24,7 @@
24
24
 
25
25
  &::picker(select) {
26
26
  appearance: base-select;
27
- max-height: min(24rem, calc(var(--viewport-block) * 0.7));
27
+ max-block-size: min(24rem, calc(var(--viewport-block) * 0.7));
28
28
  margin-block: var(--space-20);
29
29
  padding: 0;
30
30
  border: var(--border-width) solid var(--border);