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
@@ -5,20 +5,29 @@
5
5
  *
6
6
  * Each .choice-card is a <label> wrapping a native radio or checkbox. When
7
7
  * :has() is supported the input is visually hidden and the card mirrors its
8
- * state: checked border and tint, checkmark badge (round for radio, rounded
9
- * square for checkbox), focus ring, disabled dimming. Without :has() the
10
- * native input stays visible so selection state never disappears.
8
+ * state: an indicator on the leading edge (a ring for radio, a box for
9
+ * checkbox) that fills with a check, checked border and tint, focus ring,
10
+ * disabled dimming. Without :has() the native input stays visible in the
11
+ * indicator's slot, so selection state never disappears.
11
12
  *
12
- * The badge overhangs the top-right corner by 0.375em; ancestors with
13
- * overflow: hidden will clip it. Pair with grid or flex utilities for group
14
- * layout.
13
+ * The card is a two-column grid: the indicator in the first column, level
14
+ * with the first row, and each child element of the label on its own row in
15
+ * the second. Wrap loose text in an element. A corner badge for tiles is a
16
+ * documented recipe on these same layers, not a variant.
17
+ *
18
+ * .chip is the compact pill form (filter chips, selectable tags): control
19
+ * height, and a leading dot that turns into the check in a fixed slot, so
20
+ * checking never moves the label.
15
21
  *
16
22
  * Public hooks:
17
23
  * --choice-card-pad
24
+ * --choice-card-gap
18
25
  * --choice-card-radius
19
26
  * --choice-card-border
20
27
  * --choice-card-bg
21
28
  * --choice-card-check-size
29
+ * --choice-chip-dot (read-only, on .chip ::before: the check-slot centre as a
30
+ * circle() position, for a theme that grows the dot into a fill)
22
31
  */
23
32
  :where(.choice-card) {
24
33
  --intent: initial;
@@ -27,19 +36,25 @@
27
36
 
28
37
  .choice-card {
29
38
  --choice-card-pad: var(--density-space, var(--space-40));
39
+ --choice-card-gap: var(--space-30);
30
40
  --choice-card-radius: var(--radius);
31
41
  --choice-card-border: var(--border);
32
42
  --choice-card-bg: var(--surface);
33
- --choice-card-check-size: 1.5em;
43
+ --choice-card-check-size: 1.25em;
34
44
 
35
45
  position: relative;
36
- display: block;
46
+ display: grid;
47
+ grid-template-columns: var(--choice-card-check-size) minmax(0, 1fr);
48
+ /* why: a card stretched by a taller grid neighbour keeps its rows at the
49
+ top instead of spreading them. */
50
+ align-content: start;
51
+ column-gap: var(--choice-card-gap);
37
52
  padding: var(--choice-card-pad);
38
53
  border: var(--border-width) solid var(--form-invalid-border, var(--choice-card-border));
39
54
  border-radius: var(--choice-card-radius);
40
55
  background: var(--choice-card-bg);
41
56
  color: var(--text);
42
- /* Consumed so .sm/.lg reach the card; the badge's em sizing follows. */
57
+ /* Consumed so .sm/.lg reach the card; the indicator's em sizing follows. */
43
58
  font-size: var(--control-font-size);
44
59
  cursor: pointer;
45
60
  transition:
@@ -48,6 +63,18 @@
48
63
  box-shadow var(--duration);
49
64
  }
50
65
 
66
+ .choice-card > * {
67
+ grid-column: 2;
68
+ }
69
+
70
+ /* The input owns the indicator's cell: visible there without :has(), hidden
71
+ in it with :has(). */
72
+ .choice-card > :is(input[type="radio"], input[type="checkbox"]) {
73
+ grid-area: 1 / 1;
74
+ place-self: center;
75
+ margin: 0;
76
+ }
77
+
51
78
  /* The native input is visually hidden but stays focusable and participates in
52
79
  form submission and keyboard navigation (sr-only pattern). Guarded: without
53
80
  :has() the card cannot mirror the input's state, so the visible native
@@ -68,8 +95,8 @@
68
95
 
69
96
  /* Focus ring on the label follows the hidden input's focus. */
70
97
  .choice-card:has(:focus-visible) {
71
- outline: 2px solid transparent;
72
- box-shadow: var(--focus-ring-shadow);
98
+ outline: var(--focus-ring-width) solid var(--focus);
99
+ outline-offset: var(--focus-outline-offset);
73
100
  }
74
101
 
75
102
  .choice-card:has(:disabled) {
@@ -93,32 +120,41 @@
93
120
  }
94
121
  }
95
122
 
96
- /* Checkmark badge — always rendered at scale(0) and scaled in on check, so
97
- the entrance can transition (a pseudo created on :checked could not). */
123
+ /* Indicator: ::before is the ring or box, ::after the check. Both share the
124
+ input's cell, centred on the first line — not the first row, which a long
125
+ label wraps — and stay hidden until :has() can mirror the input. The em
126
+ margin is the lh-less fallback at the default 1.5 line-height. */
98
127
  .choice-card::before,
99
128
  .choice-card::after {
100
129
  content: "";
101
- position: absolute;
102
- inset-inline-end: -0.375em;
103
- inset-block-start: -0.375em;
130
+ grid-area: 1 / 1;
131
+ align-self: start;
132
+ margin-block-start: calc((1.5em - var(--choice-card-check-size)) / 2);
133
+ margin-block-start: calc((1lh - var(--choice-card-check-size)) / 2);
104
134
  inline-size: var(--choice-card-check-size);
105
135
  block-size: var(--choice-card-check-size);
106
136
  transform: scale(0);
107
- transition: transform var(--duration);
137
+ transition:
138
+ transform var(--duration),
139
+ background-color var(--duration),
140
+ border-color var(--duration);
108
141
  }
109
142
 
110
- /* Badge disc — circle for radio, rounded square for checkbox */
143
+ /* A border, not an inset shadow: forced colors drop shadows, and the resting
144
+ ring is the only sign of an unchecked option. The invalid border reaches it
145
+ through the same hook as the card's. */
111
146
  .choice-card::before {
112
- background: var(--intent, var(--state-selected));
113
- box-shadow: 0 0 0 2px var(--surface);
147
+ border: max(var(--border-width), 1.5px) solid var(--form-invalid-border, var(--text-muted));
114
148
  z-index: 1;
115
149
  }
116
150
 
117
- .choice-card:has(input[type="radio"])::before {
151
+ /* :where() keeps the shape at single-class weight, so .chip can reshape the
152
+ layer without out-arguing the input-type test. */
153
+ .choice-card:where(:has(input[type="radio"]))::before {
118
154
  border-radius: var(--radius-full);
119
155
  }
120
156
 
121
- .choice-card:has(input[type="checkbox"])::before {
157
+ .choice-card:where(:has(input[type="checkbox"]))::before {
122
158
  border-radius: var(--radius-sm);
123
159
  }
124
160
 
@@ -130,18 +166,86 @@
130
166
  z-index: 2;
131
167
  }
132
168
 
133
- .choice-card:has(:checked)::before,
169
+ /* Guarded with the input's hiding above: a resting ring beside a visible
170
+ native input would draw the indicator twice. */
171
+ @supports selector(:has(*)) {
172
+ .choice-card::before {
173
+ transform: none;
174
+ }
175
+ }
176
+
177
+ .choice-card:has(:checked)::before {
178
+ border-color: var(--intent, var(--state-selected));
179
+ background: var(--intent, var(--state-selected));
180
+ }
181
+
134
182
  .choice-card:has(:checked)::after {
135
183
  transform: scale(1);
136
184
  }
137
185
 
186
+ /* Chip — the rules above stay in force; this section only relocates the
187
+ indicator. Must follow them: several ties are won by source order. */
188
+ .choice-card.chip {
189
+ --choice-card-pad: 0 var(--control-pad-x);
190
+ --choice-card-radius: var(--radius-full);
191
+ --choice-card-check-size: 1em;
192
+
193
+ display: inline-flex;
194
+ align-items: center;
195
+ min-block-size: var(--control-size);
196
+ line-height: 1.2;
197
+ /* The dot layer sits under the label at z-index -1. */
198
+ isolation: isolate;
199
+ }
200
+
201
+ /* ::before is a full-bleed layer clipped to a dot centred on the check slot,
202
+ so a theme can grow the same clip into a selected fill. The chip itself
203
+ must not clip (its focus ring is an outline), hence the inherited radius. */
204
+ .choice-card.chip::before {
205
+ --choice-chip-dot: calc(var(--control-pad-x) + var(--choice-card-check-size) / 2) 50%;
206
+
207
+ position: absolute;
208
+ inset: 0;
209
+ /* why: an absolute box honours self-alignment — the card's start would
210
+ shrink this layer to 0 height and the dot with it. */
211
+ align-self: auto;
212
+ margin: 0;
213
+ inline-size: auto;
214
+ block-size: auto;
215
+ border: 0;
216
+ border-radius: inherit;
217
+ background: var(--text-subtle);
218
+ transform: none;
219
+ clip-path: circle(0.1875em at var(--choice-chip-dot));
220
+ z-index: -1;
221
+ transition: clip-path var(--duration);
222
+ }
223
+
224
+ .choice-card.chip:has(:checked)::before {
225
+ clip-path: circle(0 at var(--choice-chip-dot));
226
+ }
227
+
228
+ /* The check keeps the card's scale-in, in flow ahead of the label. */
229
+ .choice-card.chip::after {
230
+ order: -1;
231
+ flex: none;
232
+ align-self: center;
233
+ margin-block: 0;
234
+ margin-inline: 0 0.375em;
235
+ background: var(--intent, var(--state-selected));
236
+ -webkit-mask-size: contain;
237
+ mask-size: contain;
238
+ }
239
+
138
240
  /* The native input is hidden and the checked marker is custom-drawn: without
139
241
  this repair, forced colors pushes disc and check to the same system color
140
242
  and the state disappears. Highlight/Canvas/HighlightText are preserved as-is,
141
243
  so no forced-color-adjust is needed. The pseudo is not focusable — the
142
- outline is a separator against the card, not a focus channel. */
244
+ outline separates the disc from whatever it overlaps (the card's border,
245
+ for a corner-badge recipe), not a focus channel. */
143
246
  @media (forced-colors: active) {
144
247
  .choice-card:has(:checked)::before {
248
+ border-color: Highlight;
145
249
  background: Highlight;
146
250
  outline: 2px solid Canvas;
147
251
  }
@@ -149,4 +253,18 @@
149
253
  .choice-card:has(:checked)::after {
150
254
  background: HighlightText;
151
255
  }
256
+
257
+ /* A chip has no disc behind its check: the check carries the state alone,
258
+ and dropping the checked layer also drops any theme fill. */
259
+ .choice-card.chip::before {
260
+ background: CanvasText;
261
+ }
262
+
263
+ .choice-card.chip:has(:checked)::before {
264
+ content: none;
265
+ }
266
+
267
+ .choice-card.chip:has(:checked)::after {
268
+ background: CanvasText;
269
+ }
152
270
  }
@@ -228,9 +228,8 @@
228
228
 
229
229
  .check:focus-visible,
230
230
  .radio:focus-visible {
231
- --choice-border: var(--focus);
232
- outline: 2px solid transparent;
233
- box-shadow: var(--focus-ring-shadow);
231
+ outline: var(--focus-ring-width) solid var(--focus);
232
+ outline-offset: var(--focus-outline-offset);
234
233
  }
235
234
 
236
235
  .check:disabled,
@@ -24,6 +24,8 @@
24
24
  * --control-cursor
25
25
  * --control-border-block-end-color
26
26
  * --control-border-block-end-width
27
+ * --control-hover-border (input and textarea)
28
+ * --control-hover-bg (select)
27
29
  */
28
30
  .input,
29
31
  .textarea,
@@ -51,8 +53,8 @@
51
53
  cursor: var(--control-cursor);
52
54
  /* No outline: none here — the core :focus fallback must survive for browsers
53
55
  without :focus-visible. Pointer focus is hidden by core's
54
- :focus:not(:focus-visible); keyboard focus swaps in the transparent
55
- outline + ring below. */
56
+ :focus:not(:focus-visible); keyboard focus draws the inset outline below
57
+ (invalid fields take --form-invalid-border, the documented exception). */
56
58
  transition:
57
59
  border-color var(--duration),
58
60
  box-shadow var(--duration),
@@ -68,19 +70,24 @@
68
70
 
69
71
  .input:hover:not(:disabled),
70
72
  .textarea:hover:not(:disabled) {
71
- --control-border: var(--text-subtle);
73
+ --control-border: var(--control-hover-border, var(--text-subtle));
72
74
  }
73
75
 
74
76
  .select:hover:not(:disabled) {
75
- --control-bg: var(--surface-subtle);
77
+ --control-bg: var(--control-hover-bg, var(--surface-subtle));
76
78
  }
77
79
 
78
- .input:focus-visible,
79
- .textarea:focus-visible,
80
- .select:focus-visible {
81
- --control-border: var(--focus);
82
- outline: 2px solid transparent;
83
- box-shadow: var(--focus-ring-shadow);
80
+ /* Field focus is an inset outline: offset = -width keeps every pixel inside
81
+ the control's border box (any smaller offset bleeds outward), so it covers
82
+ the border, never touches a .join neighbour or a scrollport edge, and needs
83
+ no group treatment. Width follows --focus-ring-width, including its
84
+ prefers-contrast step. Border = shape and validation, outline = focus. */
85
+ .input:focus-visible:not(:disabled),
86
+ .textarea:focus-visible:not(:disabled),
87
+ .select:focus-visible:not(:disabled) {
88
+ outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
89
+ outline-offset: calc(var(--focus-ring-width) * -1);
90
+ box-shadow: none;
84
91
  }
85
92
 
86
93
  .input[readonly],
@@ -9,7 +9,6 @@
9
9
  * Arbitrary menu and popover behavior belongs elsewhere.
10
10
  *
11
11
  * Internal:
12
- * --control-border
13
12
  * --control-bg
14
13
  */
15
14
  /* This enhancement is excluded from forced-colors, where the native/shared
@@ -34,11 +33,12 @@
34
33
  box-shadow: var(--shadow-popout);
35
34
  }
36
35
 
36
+ /* Same inset outline as a focused field (control.css). */
37
37
  &:open {
38
- --control-border: var(--border);
39
38
  --control-bg: var(--surface-raised);
40
- outline: none;
41
- box-shadow: var(--focus-ring-shadow);
39
+ outline: var(--focus-ring-width) solid var(--form-invalid-border, var(--focus));
40
+ outline-offset: calc(var(--focus-ring-width) * -1);
41
+ box-shadow: none;
42
42
  }
43
43
 
44
44
  &::picker-icon {
@@ -44,8 +44,8 @@
44
44
  }
45
45
 
46
46
  .file:focus-visible {
47
- outline: 2px solid transparent;
48
- box-shadow: var(--focus-ring-shadow);
47
+ outline: var(--focus-ring-width) solid var(--focus);
48
+ outline-offset: var(--focus-outline-offset);
49
49
  }
50
50
 
51
51
  /* Native color picker — keep the browser's dialog and selected-color UI. The
@@ -81,8 +81,8 @@
81
81
  }
82
82
 
83
83
  .color:focus-visible {
84
- outline: 2px solid transparent;
85
- box-shadow: var(--focus-ring-shadow);
84
+ outline: var(--focus-ring-width) solid var(--focus);
85
+ outline-offset: var(--focus-outline-offset);
86
86
  }
87
87
 
88
88
  @media (forced-colors: active) {
@@ -64,7 +64,7 @@
64
64
  padding-block: 0;
65
65
  padding-inline: calc((var(--otp-cell-size) - 1ch) / 2) 0;
66
66
  border: 0;
67
- outline: 2px solid transparent;
67
+ outline: none;
68
68
  box-shadow: none;
69
69
  background: transparent;
70
70
  color: var(--text);
@@ -89,6 +89,14 @@
89
89
  character after it: the state rules select the cells as following siblings
90
90
  (input:focus ~ span, input:disabled ~ span, …), so that order is part of the
91
91
  contract. */
92
+ /* The documented markup puts .input on the native input, so the field ring
93
+ (.input:focus-visible:not(:disabled), control.css) would outrank the reset
94
+ above and draw that group ring; this selector must stay one step above it.
95
+ No transparent outline either: forced colors would paint it as a ring. */
96
+ .otp > input:focus-visible:not(:disabled) {
97
+ outline: none;
98
+ }
99
+
92
100
  .otp > input:focus ~ span {
93
101
  --otp-cell-border: var(--focus);
94
102
  --otp-cell-border-width: calc(var(--border-width) * 2);
@@ -13,6 +13,10 @@
13
13
  * --range-track-height
14
14
  * --range-thumb-bg
15
15
  * --range-track-bg
16
+ * --range-fill-bg
17
+ *
18
+ * Runtime-written (by actual-css/js/range, never set by hand):
19
+ * --range-progress
16
20
  */
17
21
 
18
22
  /* Intent boundary — inherited intents do not tint this component.
@@ -22,10 +26,17 @@
22
26
  }
23
27
 
24
28
  .range {
25
- --range-thumb-size: 1.25rem;
26
- --range-track-height: 0.25rem;
29
+ --range-thumb-size: 1.125rem;
30
+ --range-track-height: 0.625rem;
27
31
  --range-thumb-bg: var(--intent, var(--primary));
28
32
  --range-track-bg: var(--border);
33
+ --range-fill-bg: var(--intent, var(--primary));
34
+ /* Fill share, written by actual-css/js/range. 0% with no runtime means a
35
+ plain track — the control stays fully usable without JavaScript. */
36
+ --range-progress: 0%;
37
+ /* Logical fill direction: the gradient below cannot use flow-relative
38
+ keywords, so RTL flips the angle through this hook instead. */
39
+ --range-fill-from: to right;
29
40
 
30
41
  appearance: none;
31
42
  -webkit-appearance: none;
@@ -35,16 +46,29 @@
35
46
  cursor: pointer;
36
47
  }
37
48
 
49
+ :where([dir="rtl"]) .range,
50
+ .range:dir(rtl) {
51
+ --range-fill-from: to left;
52
+ }
53
+
38
54
  .range::-webkit-slider-runnable-track {
39
55
  block-size: var(--range-track-height);
40
56
  border-radius: var(--radius-full);
41
- background: var(--range-track-bg);
57
+ background: linear-gradient(
58
+ var(--range-fill-from),
59
+ var(--range-fill-bg) var(--range-progress),
60
+ var(--range-track-bg) var(--range-progress)
61
+ );
42
62
  }
43
63
 
44
64
  .range::-moz-range-track {
45
65
  block-size: var(--range-track-height);
46
66
  border-radius: var(--radius-full);
47
- background: var(--range-track-bg);
67
+ background: linear-gradient(
68
+ var(--range-fill-from),
69
+ var(--range-fill-bg) var(--range-progress),
70
+ var(--range-track-bg) var(--range-progress)
71
+ );
48
72
  }
49
73
 
50
74
  .range::-webkit-slider-thumb {
@@ -57,6 +81,10 @@
57
81
  background: var(--range-thumb-bg);
58
82
  margin-block-start: calc((var(--range-track-height) - var(--range-thumb-size)) / 2);
59
83
  cursor: inherit;
84
+ /* Permanent surface separator: the fill defaults to the thumb color, so
85
+ without this the thumb would melt into the filled part of the track.
86
+ Focus-visible keeps this ring and adds the --focus ring outside it. */
87
+ box-shadow: 0 0 0 2px var(--surface-opaque);
60
88
  transition: box-shadow var(--duration);
61
89
  }
62
90
 
@@ -67,6 +95,7 @@
67
95
  border: 0;
68
96
  background: var(--range-thumb-bg);
69
97
  cursor: inherit;
98
+ box-shadow: 0 0 0 2px var(--surface-opaque);
70
99
  transition: box-shadow var(--duration);
71
100
  }
72
101
 
@@ -75,20 +104,23 @@
75
104
  }
76
105
 
77
106
  /*
78
- * The 1px surface-coloured separator keeps the ring visually distinct from
79
- * the small, coloured thumb — unlike a full-size input where the same gap
80
- * would read as an unwanted void.
107
+ * A thumb pseudo-element takes no reliable outline, so its ring is a solid
108
+ * --focus shadow (the transparent outline above covers forced colors). The
109
+ * surface-coloured separator keeps the ring visually distinct from the
110
+ * coloured thumb — unlike a full-size input where the same gap would
111
+ * read as an unwanted void. It doubles as the resting separator between
112
+ * the thumb and the same-coloured fill.
81
113
  */
82
114
  .range:focus-visible::-webkit-slider-thumb {
83
115
  box-shadow:
84
- 0 0 0 1px var(--surface),
85
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
116
+ 0 0 0 2px var(--surface-opaque),
117
+ 0 0 0 calc(2px + var(--focus-ring-width)) var(--focus);
86
118
  }
87
119
 
88
120
  .range:focus-visible::-moz-range-thumb {
89
121
  box-shadow:
90
- 0 0 0 1px var(--surface),
91
- 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
122
+ 0 0 0 2px var(--surface-opaque),
123
+ 0 0 0 calc(2px + var(--focus-ring-width)) var(--focus);
92
124
  }
93
125
 
94
126
  .range:disabled {
@@ -96,22 +128,48 @@
96
128
  cursor: not-allowed;
97
129
  opacity: var(--disabled-opacity);
98
130
  }
99
-
100
131
  /* Forced colors: the track must stay a visible line against the page (Canvas
101
132
  would merge with it) and the thumb needs a real boundary to remain
102
- distinguishable from the track. Disabled keeps GrayText via :disabled.
103
- Focus stays on the transparent outline of :focus-visible. */
133
+ distinguishable from the track. The fill keeps its two-tone split with
134
+ system colors so the value stays readable. Disabled keeps GrayText via
135
+ :disabled. Focus stays on the transparent outline of :focus-visible. Track
136
+ and thumb fills are written on the properties, not only the hooks: an
137
+ application hook override at the same specificity loads after this file
138
+ and would win the variable, leaving a Canvas-remapped fill.
139
+ The split lives in a background-image, which forced-color-adjust: auto
140
+ drops — so the tracks opt out locally. The colors stay system keywords,
141
+ keeping native contrast adaptation; only the host keeps adjusting. */
104
142
  @media (forced-colors: active) {
105
143
  .range {
106
144
  --range-track-bg: CanvasText;
107
145
  --range-thumb-bg: ButtonFace;
146
+ --range-fill-bg: Highlight;
108
147
  }
109
148
 
149
+ .range::-webkit-slider-runnable-track {
150
+ forced-color-adjust: none;
151
+ background: linear-gradient(
152
+ var(--range-fill-from),
153
+ Highlight var(--range-progress),
154
+ CanvasText var(--range-progress)
155
+ );
156
+ }
157
+
158
+ .range::-moz-range-track {
159
+ forced-color-adjust: none;
160
+ background: linear-gradient(
161
+ var(--range-fill-from),
162
+ Highlight var(--range-progress),
163
+ CanvasText var(--range-progress)
164
+ );
165
+ }
110
166
  .range::-webkit-slider-thumb {
167
+ background: ButtonFace;
111
168
  border: var(--border-width) solid ButtonText;
112
169
  }
113
170
 
114
171
  .range::-moz-range-thumb {
172
+ background: ButtonFace;
115
173
  border: var(--border-width) solid ButtonText;
116
174
  }
117
175
  }
@@ -128,9 +128,8 @@
128
128
  }
129
129
 
130
130
  .switch:focus-visible {
131
- --switch-border: var(--focus);
132
- outline: 2px solid transparent;
133
- box-shadow: var(--focus-ring-shadow);
131
+ outline: var(--focus-ring-width) solid var(--focus);
132
+ outline-offset: var(--focus-outline-offset);
134
133
  }
135
134
 
136
135
  .switch:disabled {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Docs: docs/pages/layout/app-layout.md
9
9
  *
10
- * Public hook:
10
+ * Public hooks:
11
11
  * --app-nav-side-size
12
12
  */
13
13
  .app-layout {