actual-css 0.9.2 → 0.11.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 (136) hide show
  1. package/CHANGELOG.md +169 -1
  2. package/README.md +65 -44
  3. package/components.json +35 -14
  4. package/dist/actual.full.css +877 -583
  5. package/dist/actual.full.js +1 -1
  6. package/dist/actual.full.min.css +1 -1
  7. package/docs/design-notes/app-ui.md +1 -1
  8. package/docs/design-notes/column-layout.md +103 -520
  9. package/docs/design-notes/enhancement-contract.md +16 -7
  10. package/docs/design-notes/forced-colors.md +12 -1
  11. package/docs/design-notes/naming.md +1 -1
  12. package/docs/design-notes/platform-alignment.md +8 -4
  13. package/docs/design-notes/rich-text.md +61 -0
  14. package/docs/design-notes/steps.md +2 -2
  15. package/docs/design-notes/surface-reparenting.md +8 -9
  16. package/docs/navigation.json +4 -0
  17. package/docs/pages/components/accordion.md +55 -1
  18. package/docs/pages/components/alert.md +9 -15
  19. package/docs/pages/components/aura.md +12 -13
  20. package/docs/pages/components/badge.md +16 -7
  21. package/docs/pages/components/bleed.md +42 -0
  22. package/docs/pages/components/button.md +30 -4
  23. package/docs/pages/components/card.md +21 -86
  24. package/docs/pages/components/chat.md +5 -0
  25. package/docs/pages/components/close.md +48 -0
  26. package/docs/pages/components/data-list.md +26 -1
  27. package/docs/pages/components/dialog.md +21 -18
  28. package/docs/pages/components/drawer.md +9 -9
  29. package/docs/pages/components/navbar.md +5 -5
  30. package/docs/pages/components/pagination.md +15 -15
  31. package/docs/pages/components/shimmer.md +57 -0
  32. package/docs/pages/components/skeleton.md +2 -1
  33. package/docs/pages/components/spinner.md +14 -9
  34. package/docs/pages/components/table.md +13 -0
  35. package/docs/pages/enhancements/flyout.md +2 -0
  36. package/docs/pages/enhancements/overview.md +5 -0
  37. package/docs/pages/enhancements/tabs.md +4 -0
  38. package/docs/pages/enhancements/tooltip.md +3 -1
  39. package/docs/pages/examples/overview.md +2 -1
  40. package/docs/pages/forms/choice-cards.md +116 -4
  41. package/docs/pages/forms/combobox.md +39 -12
  42. package/docs/pages/forms/joined-controls.md +2 -5
  43. package/docs/pages/forms/overview.md +10 -1
  44. package/docs/pages/forms/range.md +110 -5
  45. package/docs/pages/forms/rich-text.md +110 -0
  46. package/docs/pages/forms/validation.md +5 -1
  47. package/docs/pages/foundations/theming.md +110 -18
  48. package/docs/pages/foundations/tokens.md +41 -13
  49. package/docs/pages/guides/bootstrap.md +2 -1
  50. package/docs/pages/guides/introduction.md +15 -23
  51. package/docs/pages/guides/modular-import.md +54 -7
  52. package/docs/pages/guides/progressive-enhancement.md +19 -17
  53. package/docs/pages/guides/tailwind.md +21 -7
  54. package/docs/pages/layout/column-layout.md +92 -54
  55. package/docs/pages/layout/media.md +1 -1
  56. package/docs/pages/layout/overview.md +1 -1
  57. package/docs/pages/utilities/overview.md +1 -1
  58. package/docs/pages/utilities/sizing-wrapping.md +5 -3
  59. package/llms.txt +25 -4
  60. package/package.json +10 -6
  61. package/reserved-classes.json +6 -30
  62. package/skills/actual-css/PROJECT.md.example +65 -0
  63. package/skills/actual-css/README.md +64 -0
  64. package/skills/actual-css/SKILL.md +234 -0
  65. package/skills/actual-css/references/accessibility.md +44 -0
  66. package/skills/actual-css/references/api-and-vocabulary.md +94 -0
  67. package/skills/actual-css/references/enhancements.md +59 -0
  68. package/skills/actual-css/references/forms.md +49 -0
  69. package/skills/actual-css/references/layout-and-theming.md +82 -0
  70. package/skills/actual-css/references/review-checklist.md +51 -0
  71. package/skills/actual-css/scripts/inspect-actual-css.mjs +171 -0
  72. package/src/css/components/accordion.css +44 -7
  73. package/src/css/components/alert.css +23 -43
  74. package/src/css/components/app-nav.css +9 -0
  75. package/src/css/components/avatar.css +1 -1
  76. package/src/css/components/badge.css +11 -39
  77. package/src/css/components/bleed.css +79 -0
  78. package/src/css/components/button.css +6 -17
  79. package/src/css/components/card.css +7 -40
  80. package/src/css/components/close.css +91 -0
  81. package/src/css/components/data-list.css +18 -0
  82. package/src/css/components/drawer.css +15 -56
  83. package/src/css/components/fab.css +10 -0
  84. package/src/css/components/flyout.css +5 -1
  85. package/src/css/components/index.css +2 -0
  86. package/src/css/components/join.css +36 -20
  87. package/src/css/components/menu.css +12 -0
  88. package/src/css/components/meter.css +1 -1
  89. package/src/css/components/modal.css +10 -57
  90. package/src/css/components/navbar.css +27 -0
  91. package/src/css/components/pagination.css +3 -2
  92. package/src/css/components/rating.css +1 -1
  93. package/src/css/components/status-bar.css +2 -2
  94. package/src/css/components/steps.css +3 -3
  95. package/src/css/components/tab.css +3 -3
  96. package/src/css/components/table.css +6 -0
  97. package/src/css/components/tooltip.css +1 -1
  98. package/src/css/core/focus.css +19 -3
  99. package/src/css/core/theme.css +33 -43
  100. package/src/css/core/tokens.css +9 -5
  101. package/src/css/core/variants.css +5 -34
  102. package/src/css/effects/aura.css +51 -33
  103. package/src/css/effects/index.css +1 -0
  104. package/src/css/effects/shimmer.css +57 -0
  105. package/src/css/forms/choice-card.css +142 -24
  106. package/src/css/forms/choice.css +2 -3
  107. package/src/css/forms/control.css +17 -10
  108. package/src/css/forms/custom-select.css +4 -4
  109. package/src/css/forms/native.css +4 -4
  110. package/src/css/forms/otp.css +9 -1
  111. package/src/css/forms/range.css +72 -14
  112. package/src/css/forms/switch.css +2 -3
  113. package/src/css/layout/app-layout.css +1 -1
  114. package/src/css/layout/column-layout.css +26 -111
  115. package/src/css/utilities/extra.css +7 -6
  116. package/src/js/command.js +61 -32
  117. package/src/js/dialog.js +103 -47
  118. package/src/js/dismiss.js +1 -2
  119. package/src/js/enhance.js +91 -126
  120. package/src/js/escape.js +1 -0
  121. package/src/js/flyout.js +10 -1
  122. package/src/js/focus-group.js +2 -1
  123. package/src/js/full.js +1 -0
  124. package/src/js/keys.js +14 -0
  125. package/src/js/menu.js +2 -1
  126. package/src/js/password.js +1 -8
  127. package/src/js/range.js +151 -0
  128. package/src/js/scrollspy.js +4 -5
  129. package/src/js/status.js +21 -10
  130. package/src/js/surface.js +12 -8
  131. package/src/js/tab.js +2 -1
  132. package/src/js/tooltip.js +29 -2
  133. package/src/js/validation.js +8 -15
  134. package/dist/actual.css +0 -876
  135. package/dist/actual.js +0 -1
  136. package/dist/actual.min.css +0 -1
@@ -6,6 +6,12 @@
6
6
  * .table-wrap provides the scroll container and outer chrome; .table owns row
7
7
  * and cell rules. Tokens are sourced from the shared scale.
8
8
  *
9
+ * Responsive strategy is horizontal overflow: the wrap scrolls, the table
10
+ * keeps its columns. Restructuring rows into cards would destroy the
11
+ * column scanability tables exist for, and needs data-label duplication
12
+ * because CSS cannot join a cell to its header — that representation choice
13
+ * belongs to the application, not the framework.
14
+ *
9
15
  * Public hooks:
10
16
  * --table-min
11
17
  * --table-cell-pad
@@ -22,7 +22,7 @@
22
22
  */
23
23
  .tooltip {
24
24
  --tooltip-bg: var(--surface-solid);
25
- --tooltip-fg: var(--surface);
25
+ --tooltip-fg: var(--surface-solid-fg);
26
26
  --tooltip-arrow-size: 5px;
27
27
 
28
28
  position: fixed;
@@ -3,9 +3,25 @@
3
3
  pointer/mouse focus and interactive components layer their ring on top.
4
4
 
5
5
  Invariant: an interactive component must never rely on box-shadow alone
6
- for its keyboard focus indicator. Components that remove the native
7
- outline on :focus-visible replace it with a transparent outline — invisible
8
- normally, forced to the system focus color under forced-colors. */
6
+ for its keyboard focus indicator; forced colors keep outlines and drop
7
+ shadows.
8
+
9
+ Two contracts share the job, both --focus-ring-width wide. Text-like
10
+ elements, whose ink is the thing being focused (links, summary, .nav-link,
11
+ .menu-item, breadcrumb links, .navbar-brand), declare no outline and take
12
+ the generic fallback below: a currentColor line that follows whatever the
13
+ application painted. Boxed components, with a fill or border of their own,
14
+ draw the themed ring instead — a solid line in --focus: inset on fields
15
+ (inside their border box), offset by --focus-outline-offset on discrete
16
+ actions. The color never derives from an intent or a local
17
+ surface — the theme guarantees --focus at 3:1 against both --surface and
18
+ --surface-solid, so one token reads on the page, on a solid band and on
19
+ any nested surface.
20
+
21
+ Documented exception: an invalid field focuses in --form-invalid-border
22
+ (danger), so the error stays visible at the point of correction. The border
23
+ carries the same hook; report:theme-contrast measures that line at 3:1
24
+ against the field's background alongside --focus. */
9
25
  :focus {
10
26
  outline: var(--focus-outline);
11
27
  outline-offset: var(--focus-outline-offset);
@@ -7,6 +7,8 @@
7
7
  * Shadow color, Focus ring derivation.
8
8
  * Derived: "Theme-derived aliases", Tone subsystem, Backdrop fill — reference
9
9
  * other tokens; hard-coded overrides on a custom theme will break the intent.
10
+ * Exception: --surface-opaque and --surface-solid-fg are public; a theme with
11
+ * translucent surfaces overrides them with opaque values.
10
12
  * Internal: --hgroup-gap.
11
13
  */
12
14
  :root {
@@ -72,10 +74,10 @@
72
74
 
73
75
  /* Lines and focus */
74
76
  --border: hsl(260 15% 88%);
75
- --focus: var(--neutral);
76
- /* Pre-color-mix fallback only — superseded below by a derivation from
77
- --focus so overriding --neutral/--focus never leaves a stale ring. */
78
- --focus-ring: hsl(258 7% 43% / 0.3);
77
+ /* A primary-hued mid-tone on purpose, one value for both schemes: focus
78
+ lines must hold 3:1 against --surface and --surface-solid alike (page,
79
+ fields, a solid band), which rules out the primary itself. */
80
+ --focus: hsl(268 30% 55%);
79
81
  --hover-overlay: hsl(260 20% 10% / 0.04);
80
82
 
81
83
  /* Radius scale — themes may override the whole scale (e.g. [data-theme="square"]) */
@@ -98,7 +100,12 @@
98
100
  --heading: var(--text);
99
101
  --selection-bg: var(--primary);
100
102
  --selection-fg: var(--primary-fg);
101
- --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
103
+ /* Surface roles that must stay opaque when a theme paints translucent
104
+ surfaces; both equal --surface on an opaque palette. --surface-opaque
105
+ hides or separates what is painted beneath a ring, separator or marker —
106
+ never a fill. --surface-solid-fg is the ink on --surface-solid. */
107
+ --surface-opaque: var(--surface);
108
+ --surface-solid-fg: var(--surface);
102
109
  }
103
110
 
104
111
  /* Soft foreground hooks — the soft ink is the intent rebated toward the local
@@ -107,18 +114,17 @@
107
114
  default theme: a custom property bakes its var() references where it is
108
115
  declared, so these resolve against root/light/dark tokens on every boundary.
109
116
  Presets and custom islands keep their --soft-fg-mix / raw-intent ink unless
110
- they declare hooks themselves — :where() keeps this reset from outranking a
111
- theme's own declaration (e.g. brutalist's 45% soft-fg-mix). */
112
- :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) {
113
- --primary-soft-fg: initial;
114
- --secondary-soft-fg: initial;
115
- --success-soft-fg: initial;
116
- --warning-soft-fg: initial;
117
- --danger-soft-fg: initial;
118
- --neutral-soft-fg: initial;
119
- }
120
-
121
- :root,
117
+ they declare hooks themselves.
118
+
119
+ Why :where(:root) and why the reset comes second: both rules match when a
120
+ custom theme sits on <html> itself. A bare :root is (0,1,0) and would
121
+ outrank the zero-specificity reset, so the same theme resolved a calibrated
122
+ Ink & Terra ink on <html data-theme="brand"> and the raw --soft-fg-mix ink
123
+ on <div data-theme="brand"> — one attribute, two soft palettes. Levelling
124
+ both rules to zero specificity makes source order the tiebreaker, so the
125
+ reset must stay below. Zero specificity is also what lets a theme declare
126
+ its own hooks (e.g. brutalist's 45% soft-fg-mix) and win at (0,1,0). */
127
+ :where(:root),
122
128
  [data-theme="light"],
123
129
  [data-theme="dark"] {
124
130
  --secondary-soft-fg: color-mix(in oklab, var(--text) 10%, var(--secondary) 90%);
@@ -128,6 +134,15 @@
128
134
  --neutral-soft-fg: color-mix(in oklab, var(--text) 20%, var(--neutral) 80%);
129
135
  }
130
136
 
137
+ :where([data-theme]:not([data-theme="light"]):not([data-theme="dark"])) {
138
+ --primary-soft-fg: initial;
139
+ --secondary-soft-fg: initial;
140
+ --success-soft-fg: initial;
141
+ --warning-soft-fg: initial;
142
+ --danger-soft-fg: initial;
143
+ --neutral-soft-fg: initial;
144
+ }
145
+
131
146
  /* Native platform controls (checkbox, radio, range, progress) that are not
132
147
  restyled by a component class still follow the theme accent. Declared on
133
148
  every island: accent-color inherits as a computed value, so islands must
@@ -149,20 +164,6 @@
149
164
  --tone-fg: var(--text);
150
165
  }
151
166
 
152
- /* Focus ring derivation — color-mix tier. Runs whether or not light-dark()
153
- is also supported, so it covers the color-mix-without-light-dark browser
154
- tier on its own; the light-dark block below refines it further with the
155
- light/dark alpha split once both features are available. Declared on every
156
- [data-theme] island (not just the built-in light/dark pair) so custom
157
- islands recolor the ring for free; a preset that re-declares --focus-ring
158
- later in source order intentionally pins its own ring instead. */
159
- @supports (color: color-mix(in oklch, red, white)) {
160
- :root,
161
- [data-theme] {
162
- --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent);
163
- }
164
- }
165
-
166
167
  /* Auto dark mode via light-dark(). Resolves based on the element's color-scheme:
167
168
  :root (light dark) follows the OS, [data-theme="light"] forces light,
168
169
  [data-theme="dark"] forces dark by overriding color-scheme.
@@ -199,22 +200,11 @@
199
200
  --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%));
200
201
 
201
202
  --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%));
202
- --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28));
203
203
  --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06));
204
204
  --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%));
205
205
  }
206
206
  }
207
207
 
208
- @supports (color: color-mix(in oklch, red, white)) and (color: light-dark(red, blue)) {
209
- :root,
210
- [data-theme] {
211
- --focus-ring: light-dark(
212
- color-mix(in oklch, var(--focus) 30%, transparent),
213
- color-mix(in oklch, var(--focus) 28%, transparent)
214
- );
215
- }
216
- }
217
-
218
208
  @media (prefers-contrast: more) {
219
209
  :root,
220
210
  /* The repeated attribute intentionally outranks single-attribute preset
@@ -222,7 +212,7 @@
222
212
  [data-theme][data-theme] {
223
213
  --border-width: 2px;
224
214
  --focus-outline-offset: 3px;
225
- --focus-ring-width: 4px;
215
+ --focus-ring-width: 3px;
226
216
  --text-muted: var(--text);
227
217
  --text-subtle: var(--text);
228
218
  }
@@ -78,11 +78,15 @@
78
78
  --ease-enter: cubic-bezier(0.2, 0, 0, 1);
79
79
  --ease-exit: cubic-bezier(0.4, 0, 1, 1);
80
80
 
81
- /* Focus ring — shared by every interactive component's :focus-visible */
81
+ /* Generic focus fallback — an adaptive currentColor line for elements
82
+ without a component ring (links, summary, plain items). Components draw
83
+ the themed ring instead: --focus-ring-width solid --focus. See focus.css
84
+ for the two contracts. Both share --focus-ring-width: they differ in
85
+ color only. */
82
86
  --focus-outline-color: currentColor;
83
- --focus-outline: calc(var(--border-width) * 2) solid var(--focus-outline-color);
87
+ --focus-outline: var(--focus-ring-width) solid var(--focus-outline-color);
84
88
  --focus-outline-offset: 2px;
85
- --focus-ring-width: 3px;
89
+ --focus-ring-width: 2px;
86
90
 
87
91
  /* Status dot positioning — used by .avatar > .badge:empty. The offset is
88
92
  the circle-aware inset (1 - 1/√2) / 2 ≈ 14.65%; the ring surrounds the
@@ -143,8 +147,8 @@
143
147
  --state-selected-fg: var(--primary-fg);
144
148
  --state-disabled: var(--text-subtle);
145
149
 
146
- /* Status dot ring — follows the island's surface. */
147
- --indicator-ring: var(--surface);
150
+ /* Status dot ring — cuts the dot out of what it overlaps. */
151
+ --indicator-ring: var(--surface-opaque);
148
152
  }
149
153
 
150
154
  :root {
@@ -2,13 +2,9 @@
2
2
  * Cross-cutting variant token mapping.
3
3
  *
4
4
  * Variant scope: .solid/.soft/.outline/.surface set --ui-* for components that
5
- * opt into shared variants. .inverted is a surface modifier, not a variant: it paints
6
- * the element directly (background/color/border-color) so it works on any bare
7
- * block, and remaps the shared --ui-* tokens so participating components
8
- * (.card, .navbar, .alert, .badge) invert through their own rules. Loaded
9
- * before components, so a component with its own explicit surface (e.g.
10
- * .accordion) naturally keeps that surface — .inverted never forces a
11
- * non-participant.
5
+ * opt into shared variants. A subtree that needs another palette or scheme is
6
+ * a data-theme island, never a variant: only an island recomputes every token
7
+ * (fields, states, intents), where a remap of a few --ui-* tokens cannot.
12
8
  * Component-specific defaults (.btn solid-by-default, .badge
13
9
  * soft-by-default, .alert soft-by-default) live with their components:
14
10
  * components/button.css — .btn default solid + .btn.ghost + .btn.link
@@ -64,28 +60,6 @@
64
60
  --ui-border: var(--border);
65
61
  }
66
62
 
67
- /* Inverse surface treatment. Paints the element directly (so it works on any
68
- bare block) and remaps the shared --ui-* tokens so opt-in components
69
- (.card, .navbar) invert through their own rules. Because this loads before
70
- component base surfaces, components with their own explicit surface
71
- (e.g. .accordion) keep it — participation is opt-in, never forced. --heading
72
- is set so global headings (var(--heading, inherit)) do not inherit an outside
73
- heading color onto the dark surface; surface-owning descendants derive
74
- their contextual aliases from their own local surface. */
75
- .inverted {
76
- --ui-bg: var(--surface-solid);
77
- --ui-fg: var(--surface);
78
- --ui-border: transparent;
79
- --ui-hover-bg: var(--surface-solid);
80
-
81
- --heading: var(--surface);
82
- --busy-overlay-bg: var(--surface-solid);
83
-
84
- background: var(--ui-bg);
85
- color: var(--ui-fg);
86
- border-color: var(--ui-border);
87
- }
88
-
89
63
  .compact {
90
64
  --gap: var(--space-20);
91
65
  --density-space: var(--space-20);
@@ -103,7 +77,8 @@
103
77
  /* Shared control scale. Wrappers are listed when their descendants form one
104
78
  component (field, choice-card, floating-field, input-icon, join, pagination,
105
79
  tabs, flyout). A local size declaration beats inherited density because it
106
- is declared on the sized component itself. */
80
+ is declared on the sized component itself. Guarded by check:sync
81
+ (size-scale-participants): .sm and .lg must list the same components. */
107
82
  :where(
108
83
  .btn,
109
84
  .input,
@@ -169,10 +144,6 @@
169
144
  );
170
145
  /* @sync soft-recipe */
171
146
  }
172
-
173
- .inverted {
174
- --ui-hover-bg: color-mix(in oklch, var(--surface-solid) 88%, black);
175
- }
176
147
  }
177
148
 
178
149
  /* Interactive hover recipes live in the components that consume them:
@@ -35,36 +35,55 @@
35
35
 
36
36
  .aura {
37
37
  --aura-color: var(--intent, var(--primary));
38
- --aura-width: 2px;
38
+ --aura-width: 1px;
39
39
  --aura-blur: 0.25rem;
40
- --aura-intensity: 1;
40
+ --aura-intensity: 0.4;
41
41
  --aura-radius: var(--radius-lg);
42
42
  --aura-duration: 6s;
43
43
 
44
44
  position: relative;
45
45
  isolation: isolate;
46
46
  display: inline-grid;
47
- padding: var(--aura-width);
48
47
  border-radius: calc(var(--aura-radius) + var(--aura-width));
49
- border: var(--border-width) solid transparent;
48
+
49
+ /* The frame is the border itself, so --aura-width is the painted thickness.
50
+ A transparent border stays for forced colors, where it becomes the visible
51
+ boundary; a padding frame inside it would paint both, doubling the line. */
52
+ border: var(--aura-width) solid transparent;
50
53
  background: var(--aura-color);
51
54
 
52
55
  /* A short feathered accent rather than a long wedge with a hard edge, which
53
56
  reads as one lit side. Nothing tints the frame outside it: a constant tint
54
- across 2px draws an outline, the opposite of the effect. */
57
+ across the frame draws an outline, the opposite of the effect. `border-box`
58
+ sizes the gradient to the border box: the default padding-box origin would
59
+ tile a neighbouring copy of the gradient into the border, which is now the
60
+ whole frame. */
55
61
  background: conic-gradient(
56
- from var(--aura-angle),
57
- transparent 0 78%,
58
- var(--aura-color) 93%,
59
- transparent
60
- );
62
+ from var(--aura-angle),
63
+ transparent 0 78%,
64
+ var(--aura-color) 93%,
65
+ transparent
66
+ )
67
+ border-box;
68
+
69
+ /* Far halo — flat and static. At this blur the rotating detail is no longer
70
+ legible, so there is nothing to gain from animating a second wide blurred
71
+ layer. This is the layer that stays lit at all times, so it — not the
72
+ frame — is what keeps the object present between two passes of the
73
+ accent. An outer shadow never paints under the box, so it stays out of a
74
+ translucent child. min() because a color-mix() share above 100% is
75
+ invalid, where the opacity it replaces clamped. */
76
+ box-shadow: 0 0 calc(var(--aura-blur) * 8) var(--aura-blur)
77
+ color-mix(in oklab, var(--aura-color) min(100%, calc(15% * var(--aura-intensity))), transparent);
61
78
  }
62
79
 
63
- /* Near halo — inherits the frame gradient, so it tracks the lit accent. */
80
+ /* Near halo — inherits the frame gradient, so it tracks the lit accent. An
81
+ absolute inset starts at the padding edge, inside the border that is the
82
+ frame, so the negative inset puts the halo back on the frame. */
64
83
  .aura::before {
65
84
  position: absolute;
66
85
  z-index: -1;
67
- inset: 0;
86
+ inset: calc(-1 * var(--aura-width));
68
87
  border-radius: inherit;
69
88
  background: inherit;
70
89
  filter: blur(var(--aura-blur));
@@ -73,29 +92,22 @@
73
92
  content: "";
74
93
  }
75
94
 
76
- /* Far halo — flat and static. At this blur the rotating detail is no longer
77
- legible, so there is nothing to gain from animating a second wide blurred
78
- layer. It outgrows the frame, otherwise the child covers it and the whole
79
- effect reads as a drop shadow. This is the layer that stays lit at all
80
- times, so it — not the frame — is what keeps the object present between two
81
- passes of the accent. */
95
+ /* Occlusion — the frame and the near halo paint across the whole box, and only
96
+ their edge is meant to show. This mask sits above both, under the child, so
97
+ a transparent or translucent child (an .outline alert, a glass surface) is
98
+ framed rather than filled with the frame color. It follows the child's
99
+ shape through --aura-radius. */
82
100
  .aura::after {
83
101
  position: absolute;
84
102
  z-index: -1;
85
- inset: calc(-1 * var(--aura-blur));
86
- border-radius: calc(var(--aura-radius) + var(--aura-width) + var(--aura-blur));
87
- background: var(--aura-color);
88
- filter: blur(calc(var(--aura-blur) * 4));
89
- opacity: calc(0.15 * var(--aura-intensity));
103
+ inset: 0;
104
+ border-radius: var(--aura-radius);
105
+ background: var(--surface-opaque);
90
106
  pointer-events: none;
91
107
  content: "";
92
108
  }
93
109
 
94
- /* Lifts the child above both halos. It also has to be opaque: the frame paints
95
- across the whole box and only its edge is meant to show, so a transparent
96
- child (an .outline alert, a .ghost button) is filled with the frame color
97
- rather than framed by it. Documented as a constraint, not worked around —
98
- masking a ring would cost more than the case is worth. */
110
+ /* Lifts the child above the halo and the mask. */
99
111
  .aura > * {
100
112
  position: relative;
101
113
  }
@@ -118,7 +130,7 @@
118
130
  animation: aura-breathe-near var(--aura-duration) ease-in-out infinite;
119
131
  }
120
132
 
121
- .aura-glow::after {
133
+ .aura-glow {
122
134
  animation: aura-breathe-far var(--aura-duration) ease-in-out infinite;
123
135
  }
124
136
  }
@@ -130,8 +142,8 @@
130
142
  @media (prefers-reduced-motion: reduce) {
131
143
  .aura:not(.aura-glow) {
132
144
  background:
133
- radial-gradient(4rem 4rem at 0% 0%, var(--aura-color), transparent 70%),
134
- radial-gradient(4rem 4rem at 100% 100%, var(--aura-color), transparent 70%);
145
+ radial-gradient(4rem 4rem at 0% 0%, var(--aura-color), transparent 70%) border-box,
146
+ radial-gradient(4rem 4rem at 100% 100%, var(--aura-color), transparent 70%) border-box;
135
147
  }
136
148
  }
137
149
 
@@ -152,10 +164,16 @@
152
164
  }
153
165
  }
154
166
 
167
+ /* The far halo is the element's own shadow: opening its spread and share
168
+ breathes it without scaling the child. */
155
169
  @keyframes aura-breathe-far {
156
170
  50% {
157
- opacity: calc(0.28 * var(--aura-intensity));
158
- transform: scale(1.07);
171
+ box-shadow: 0 0 calc(var(--aura-blur) * 10) calc(var(--aura-blur) * 2)
172
+ color-mix(
173
+ in oklab,
174
+ var(--aura-color) min(100%, calc(28% * var(--aura-intensity))),
175
+ transparent
176
+ );
159
177
  }
160
178
  }
161
179
 
@@ -1,2 +1,3 @@
1
1
  /* Effects — purely visual enrichments. */
2
2
  @import "./aura.css";
3
+ @import "./shimmer.css";
@@ -0,0 +1,57 @@
1
+ /*
2
+ * Highlight sweeping across a text label — an activity in progress.
3
+ *
4
+ * Docs: docs/pages/components/shimmer.md
5
+ *
6
+ * Presentation only: it carries no state and selects no ARIA attribute, so the
7
+ * label stays whatever the author makes it (a role="status" message, a list
8
+ * item, a heading).
9
+ *
10
+ * Public hooks:
11
+ * --shimmer-color
12
+ * --shimmer-highlight
13
+ */
14
+ .shimmer {
15
+ --shimmer-color: var(--text-muted);
16
+ --shimmer-highlight: var(--text);
17
+
18
+ color: var(--shimmer-color);
19
+ }
20
+
21
+ /* The sweep paints the glyphs through a clipped background, which needs the
22
+ glyph fill transparent. Without background-clip: text that would erase the
23
+ label, so the transparency lives under the same query as the clip. It is
24
+ the fill, not `color`: a label class that sets its own color (.muted, a
25
+ caption style) would otherwise win the cascade and paint opaque glyphs over
26
+ the sweep. Reduced motion keeps the plain label — a frozen band would read
27
+ as a random tint. */
28
+ @supports (background-clip: text) {
29
+ @media (prefers-reduced-motion: no-preference) {
30
+ .shimmer {
31
+ -webkit-text-fill-color: transparent;
32
+ background:
33
+ linear-gradient(
34
+ 90deg,
35
+ var(--shimmer-color) 40%,
36
+ var(--shimmer-highlight) 50%,
37
+ var(--shimmer-color) 60%
38
+ )
39
+ 0 0 / 300% 100%;
40
+ background-clip: text;
41
+ animation: actual-shimmer var(--duration-shimmer) linear infinite;
42
+ }
43
+ }
44
+ }
45
+
46
+ /* The band occupies the middle third of a background three times the label's
47
+ width, so each pass leaves the end of the label before the next one enters
48
+ at the start. */
49
+ @keyframes actual-shimmer {
50
+ from {
51
+ background-position: 100% 0;
52
+ }
53
+
54
+ to {
55
+ background-position: 0% 0;
56
+ }
57
+ }