actual-css 0.1.0 → 0.2.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 (51) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +2 -0
  3. package/dist/actual.css +356 -124
  4. package/dist/actual.js +2 -2
  5. package/dist/actual.js.map +13 -13
  6. package/dist/actual.min.css +1 -1
  7. package/docs/README.md +2 -0
  8. package/docs/components.md +2 -2
  9. package/docs/design-notes/browser-support.md +9 -0
  10. package/docs/design-notes/enhancement-contract.md +104 -0
  11. package/docs/design-notes/naming.md +1 -0
  12. package/docs/design-notes/widget-primitives.md +83 -0
  13. package/docs/forms.md +4 -4
  14. package/docs/javascript.md +104 -34
  15. package/docs/patterns.md +89 -0
  16. package/docs/ui.md +22 -30
  17. package/llms.txt +6 -0
  18. package/package.json +14 -8
  19. package/src/css/base.css +0 -4
  20. package/src/css/components/alert.css +88 -2
  21. package/src/css/components/badge.css +30 -7
  22. package/src/css/components/busy.css +1 -0
  23. package/src/css/components/button.css +6 -8
  24. package/src/css/components/flyout.css +0 -1
  25. package/src/css/components/join.css +1 -2
  26. package/src/css/components/scrollspy.css +2 -2
  27. package/src/css/components/spinner.css +4 -4
  28. package/src/css/components/status-bar.css +3 -4
  29. package/src/css/components/tab.css +1 -0
  30. package/src/css/focus.css +37 -0
  31. package/src/css/forms/choice-card.css +1 -2
  32. package/src/css/forms/choice.css +1 -2
  33. package/src/css/forms/control.css +1 -10
  34. package/src/css/forms/index.css +1 -0
  35. package/src/css/forms/native.css +8 -32
  36. package/src/css/forms/range.css +95 -0
  37. package/src/css/forms/switch.css +1 -2
  38. package/src/css/optional/scroller.css +6 -0
  39. package/src/css/theme.css +9 -9
  40. package/src/css/themes/edge.css +3 -27
  41. package/src/css/tokens.css +9 -3
  42. package/src/js/context-menu.js +21 -34
  43. package/src/js/enhance.js +26 -0
  44. package/src/js/flyout.js +12 -19
  45. package/src/js/menu.js +15 -14
  46. package/src/js/scrollspy.js +125 -62
  47. package/src/js/selectors.js +14 -24
  48. package/src/js/status.js +13 -13
  49. package/src/js/surface.js +57 -12
  50. package/src/js/tab.js +10 -13
  51. package/src/js/validation.js +11 -43
@@ -22,7 +22,7 @@
22
22
  }
23
23
 
24
24
  .alert {
25
- --alert-pad-block: var(--variant-pad-block, var(--space-4));
25
+ --alert-pad-block: var(--variant-pad-block, var(--space-3));
26
26
  --alert-pad-inline: var(--space-4);
27
27
  --alert-font-size: var(--variant-font-size, var(--font-size-md));
28
28
  --alert-icon-size: var(--variant-icon-size, 1.25rem);
@@ -72,7 +72,7 @@
72
72
  }
73
73
 
74
74
  @supports (color: color-mix(in oklch, red, white)) {
75
- .alert:where(:not(.solid, .outline)) {
75
+ .alert:where(:not(.solid, .outline, .callout)) {
76
76
  /* @sync soft-recipe */
77
77
  --ui-bg: color-mix(in oklch, var(--surface) var(--soft-bg-mix), var(--intent, var(--neutral)));
78
78
  --ui-border: color-mix(
@@ -135,3 +135,89 @@
135
135
  readable color, and text on a Highlight surface would trigger Chromium's
136
136
  Canvas backplate (unreadable) — so .solid intentionally falls back to the
137
137
  default look. */
138
+
139
+ /* Callout — neutral surface with an inset accent band on the leading edge.
140
+ The ::before pseudo is absolutely positioned and inset to respect the
141
+ outer border-radius, so it never needs its own radius. */
142
+ .alert.callout {
143
+ --alert-bg: var(--surface-subtle);
144
+ --ui-bg: var(--alert-bg);
145
+ --ui-fg: var(--text);
146
+ --ui-border: var(--border);
147
+ position: relative;
148
+ padding-inline-start: calc(var(--alert-pad-inline) + var(--space-3));
149
+ }
150
+
151
+ .alert.callout::before {
152
+ content: "";
153
+ position: absolute;
154
+ inset-block: var(--space-2);
155
+ inset-inline-start: var(--space-2);
156
+ inline-size: 0.25rem;
157
+ border-radius: var(--radius-full);
158
+ background: var(--intent, var(--neutral));
159
+ }
160
+
161
+ /* Admonition — structured title/body anatomy with a tinted title band.
162
+ The root is layout-neutralized: no padding, no gap, no icon grid. */
163
+ .alert.admonition {
164
+ padding: 0;
165
+ gap: 0;
166
+ display: block;
167
+ overflow: clip;
168
+ }
169
+
170
+ .alert-title {
171
+ padding-block: var(--alert-pad-block);
172
+ padding-inline: var(--alert-pad-inline);
173
+
174
+ @supports (color: color-mix(in oklch, red, white)) {
175
+ background: color-mix(
176
+ in oklch,
177
+ var(--surface) var(--soft-bg-mix),
178
+ var(--intent, var(--neutral))
179
+ );
180
+ }
181
+
182
+ @supports not (color: color-mix(in oklch, red, white)) {
183
+ background: var(--intent, var(--neutral));
184
+ }
185
+
186
+ border-start-start-radius: calc(var(--radius-lg) - var(--border-width));
187
+ border-start-end-radius: calc(var(--radius-lg) - var(--border-width));
188
+ }
189
+
190
+ .alert-title:where(summary) {
191
+ cursor: pointer;
192
+ user-select: none;
193
+ list-style: none;
194
+ }
195
+
196
+ .alert-title:where(summary)::-webkit-details-marker {
197
+ display: none;
198
+ }
199
+
200
+ .alert-title[aria-hidden] {
201
+ display: inline-flex;
202
+ gap: var(--space-2);
203
+ align-items: center;
204
+ }
205
+
206
+ .alert-body {
207
+ padding-block: var(--alert-pad-block);
208
+ padding-inline: var(--alert-pad-inline);
209
+ color: var(--text);
210
+ }
211
+
212
+ .alert-body > :first-child {
213
+ margin-block-start: 0;
214
+ }
215
+
216
+ .alert-body > :last-child {
217
+ margin-block-end: 0;
218
+ }
219
+
220
+ details.alert.admonition[open] .alert-title {
221
+ border-end-start-radius: 0;
222
+ border-end-end-radius: 0;
223
+ }
@@ -1,8 +1,9 @@
1
1
  /*
2
2
  * Badges for compact labels, counts, and state hints.
3
3
  *
4
- * Solid by default. Shared .soft/.outline variants stay available for quieter
5
- * tags without adding a badge-specific color API.
4
+ * Soft by default. .solid for counters and notification badges. The dot
5
+ * (.badge:empty) stays solid regardless — a soft dot is pointless.
6
+ * Shared .outline variant stays available.
6
7
  */
7
8
  /* Intent boundary — inherited intents do not tint this component.
8
9
  Local intent classes still win because :where() has zero specificity. */
@@ -22,9 +23,9 @@
22
23
  --badge-pad-x: 0.55em;
23
24
  --badge-font-size: var(--variant-compact-font-size, 0.8125rem);
24
25
  --badge-dot-size: 0.5rem;
25
- --badge-bg: var(--ui-bg, var(--intent, var(--neutral)));
26
- --badge-fg: var(--ui-fg, var(--intent-fg, var(--neutral-fg)));
27
- --badge-border: var(--ui-border, transparent);
26
+ --badge-bg: var(--ui-bg, transparent);
27
+ --badge-fg: var(--ui-fg, var(--intent, var(--text)));
28
+ --badge-border: var(--ui-border, var(--intent, var(--border)));
28
29
  --badge-radius: 999px;
29
30
 
30
31
  display: inline-flex;
@@ -49,8 +50,31 @@
49
50
  box-shadow var(--duration);
50
51
  }
51
52
 
53
+ /* Soft by default — duplicated here with @sync markers for check-sync.js
54
+ to guard, the same way alert.css guards its own soft recipe. */
55
+ @supports (color: color-mix(in oklch, red, white)) {
56
+ .badge:where(:not(.solid, .outline)) {
57
+ /* @sync soft-recipe */
58
+ --badge-bg: color-mix(
59
+ in oklch,
60
+ var(--surface) var(--soft-bg-mix),
61
+ var(--intent, var(--neutral))
62
+ );
63
+ --badge-fg: var(--intent, var(--text));
64
+ --badge-border: color-mix(
65
+ in oklch,
66
+ var(--surface) var(--soft-border-mix),
67
+ var(--intent, var(--neutral))
68
+ );
69
+ /* @sync soft-recipe */
70
+ }
71
+ }
72
+
52
73
  .badge:empty {
53
74
  --badge-size: var(--badge-dot-size);
75
+ --badge-bg: var(--intent, var(--neutral));
76
+ --badge-fg: var(--intent-fg, var(--neutral-fg));
77
+ --badge-border: transparent;
54
78
  padding: 0;
55
79
  border: 0;
56
80
  inline-size: var(--badge-size);
@@ -83,8 +107,7 @@
83
107
  }
84
108
 
85
109
  /* .solid/.soft/.outline are shared via variants.css, including the
86
- .soft/.outline hover recipe (--ui-hover-bg). The --badge-* fallbacks make
87
- .badge solid by default. */
110
+ .soft/.outline hover recipe (--ui-hover-bg). */
88
111
  @media (hover: hover) {
89
112
  :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"])):hover {
90
113
  background: var(--ui-hover-bg, var(--badge-bg));
@@ -28,6 +28,7 @@
28
28
  }
29
29
 
30
30
  [aria-busy="true"]:not(.btn) > .spinner:last-child {
31
+ --spinner-size: 2rem;
31
32
  position: absolute;
32
33
  inset: 0;
33
34
  z-index: 2;
@@ -33,7 +33,6 @@
33
33
  --btn-border: var(--ui-border, var(--btn-default-border, transparent));
34
34
  --btn-radius: var(--radius);
35
35
  --btn-focus-color: var(--intent, var(--focus));
36
- --btn-focus-outline-width: calc(var(--border-width) * 2);
37
36
  --btn-hover-bg: var(--ui-hover-bg, var(--btn-bg));
38
37
  --btn-hover-overlay: var(--hover-overlay, rgb(0 0 0 / 0.08));
39
38
 
@@ -41,7 +40,7 @@
41
40
  display: inline-flex;
42
41
  align-items: center;
43
42
  justify-content: center;
44
- gap: 0.5em;
43
+ gap: 0.375em;
45
44
  min-block-size: var(--btn-min-size);
46
45
  padding-block: var(--space-2);
47
46
  padding-block: max(var(--space-1), calc((var(--btn-min-size) - 1lh) / 2));
@@ -57,7 +56,7 @@
57
56
  color: var(--btn-fg);
58
57
  font: inherit;
59
58
  font-size: var(--btn-font-size);
60
- font-weight: var(--font-weight-strong);
59
+ font-weight: var(--font-weight-medium);
61
60
  line-height: 1;
62
61
  text-decoration: none;
63
62
  cursor: pointer;
@@ -133,14 +132,14 @@
133
132
  }
134
133
 
135
134
  .btn:focus-visible {
136
- outline: var(--btn-focus-outline-width) solid var(--btn-focus-color);
137
- outline-offset: var(--focus-outline-offset);
138
- box-shadow: 0 0 0 3px var(--btn-focus-ring-color, var(--focus-ring));
135
+ outline: none;
136
+ box-shadow: 0 0 0 var(--focus-ring-width) var(--btn-focus-ring-color, var(--focus-ring));
139
137
  }
140
138
 
141
139
  .btn.link:focus-visible {
142
- outline-color: currentColor;
140
+ outline: var(--focus-outline);
143
141
  outline-offset: 0.2em;
142
+ box-shadow: none;
144
143
  }
145
144
 
146
145
  .btn:disabled {
@@ -164,7 +163,6 @@ a.btn[aria-disabled="true"] {
164
163
  }
165
164
 
166
165
  .btn[aria-busy="true"] > .spinner {
167
- --spinner-size: 1em;
168
166
  --spinner-width: 0.15em;
169
167
  }
170
168
 
@@ -186,7 +186,6 @@
186
186
  background: var(--surface-subtle);
187
187
  outline: var(--focus-outline);
188
188
  outline-offset: var(--focus-outline-offset);
189
- box-shadow: var(--focus-ring-shadow);
190
189
  }
191
190
 
192
191
  :is(.flyout, .flyout :where(li))
@@ -95,8 +95,7 @@
95
95
  focus, and unsupported :has() browsers retain the control's normal ring. */
96
96
  .join:has(> :is(.input, .textarea, .select):focus-visible) {
97
97
  border-radius: var(--join-radius);
98
- outline: var(--focus-outline);
99
- outline-offset: var(--focus-outline-offset);
98
+ outline: none;
100
99
  box-shadow: var(--focus-ring-shadow);
101
100
  }
102
101
 
@@ -11,8 +11,8 @@
11
11
  * Progressive enhancement: on browsers that support :target-current,
12
12
  * this file enables the native target group and marks the active link.
13
13
  *
14
- * Falls back gracefully: links still work when IntersectionObserver or
15
- * :target-current is unavailable.
14
+ * Falls back gracefully: links still work when :target-current is unavailable
15
+ * and when the JavaScript enhancer is not loaded at all.
16
16
  */
17
17
 
18
18
  @supports selector(.scrollspy a:target-current) {
@@ -18,8 +18,8 @@
18
18
  }
19
19
 
20
20
  .spinner {
21
- --spinner-size: 1.5rem;
22
- --spinner-width: 2px;
21
+ --spinner-size: 1em;
22
+ --spinner-width: 0.125em;
23
23
  --spinner-track: currentColor;
24
24
  --spinner-value: transparent;
25
25
 
@@ -45,11 +45,11 @@
45
45
  }
46
46
 
47
47
  .spinner.sm {
48
- --spinner-size: 1rem;
48
+ --spinner-size: 0.75em;
49
49
  }
50
50
 
51
51
  .spinner.lg {
52
- --spinner-size: 3rem;
52
+ --spinner-size: 2rem;
53
53
  --spinner-width: 3px;
54
54
  }
55
55
 
@@ -10,10 +10,9 @@
10
10
  * the coloring via --intent / --intent-fg. status.js applies these as plain
11
11
  * classes on the target; it does not know their names or that they exist.
12
12
  *
13
- * Public API: .status-bar + [data-status], both required — status.js reads
14
- * the class name from js/selectors.js rather than hardcoding it, but still
15
- * needs it to scope its document-wide lookup away from unrelated elements
16
- * that carry their own generic `data-status`.
13
+ * CSS API: .status-bar
14
+ * JS target contract: [data-status][role="status"]
15
+ * Normally combined, but they describe separate concerns.
17
16
  */
18
17
 
19
18
  /* Intent boundary — inherited intents do not tint this component.
@@ -72,6 +72,7 @@
72
72
 
73
73
  .tab:focus-visible {
74
74
  outline: var(--focus-outline);
75
+ outline-color: var(--focus);
75
76
  outline-offset: -2px;
76
77
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
77
78
  }
package/src/css/focus.css CHANGED
@@ -11,3 +11,40 @@
11
11
  outline: none;
12
12
  }
13
13
  }
14
+
15
+ /*
16
+ * Forced-colors focus — centralised so individual components do not need
17
+ * their own fallback. The double :focus keeps specificity high enough
18
+ * (0,0,2,0) to beat a single :focus-visible; against a class selector like
19
+ * .input:focus-visible it ties, but this file loads last so it wins on
20
+ * source order.
21
+ *
22
+ * .choice-card and .join receive focus indirectly (:has on a wrapper that
23
+ * never matches :focus), so they are listed explicitly.
24
+ *
25
+ * .range is excluded from the rectangular fallback: its focus is drawn on
26
+ * the thumb pseudo-element, which is the manipulated surface.
27
+ *
28
+ * box-shadow: none is redundant when forced-color-adjust is auto, but safe
29
+ * when a component uses forced-color-adjust: none (e.g. checked switches /
30
+ * checkboxes).
31
+ */
32
+ @media (forced-colors: active) {
33
+ :focus:focus-visible:not(.range),
34
+ .choice-card:has(:focus-visible),
35
+ .join:has(> :is(.input, .textarea, .select):focus-visible) {
36
+ outline: var(--focus-outline);
37
+ outline-offset: var(--focus-outline-offset);
38
+ box-shadow: none;
39
+ }
40
+
41
+ .range:focus-visible::-webkit-slider-thumb {
42
+ outline: var(--focus-outline);
43
+ outline-offset: var(--focus-outline-offset);
44
+ }
45
+
46
+ .range:focus-visible::-moz-range-thumb {
47
+ outline: var(--focus-outline);
48
+ outline-offset: var(--focus-outline-offset);
49
+ }
50
+ }
@@ -63,8 +63,7 @@
63
63
 
64
64
  /* Focus ring on the label follows the hidden input's focus. */
65
65
  .choice-card:has(:focus-visible) {
66
- outline: var(--focus-outline);
67
- outline-offset: var(--focus-outline-offset);
66
+ outline: none;
68
67
  box-shadow: var(--focus-ring-shadow);
69
68
  }
70
69
 
@@ -156,8 +156,7 @@
156
156
  .check:focus-visible,
157
157
  .radio:focus-visible {
158
158
  --choice-border: var(--focus);
159
- outline: var(--focus-outline);
160
- outline-offset: var(--focus-outline-offset);
159
+ outline: none;
161
160
  box-shadow: var(--focus-ring-shadow);
162
161
  }
163
162
 
@@ -71,8 +71,7 @@
71
71
  .textarea:focus-visible,
72
72
  .select:focus-visible {
73
73
  --control-border: var(--focus);
74
- outline: var(--focus-outline);
75
- outline-offset: var(--focus-outline-offset);
74
+ outline: none;
76
75
  box-shadow: var(--focus-ring-shadow);
77
76
  }
78
77
 
@@ -96,11 +95,3 @@
96
95
  .textarea::placeholder {
97
96
  color: var(--text-muted);
98
97
  }
99
-
100
- @media (forced-colors: active) {
101
- .input:focus-visible,
102
- .textarea:focus-visible,
103
- .select:focus-visible {
104
- box-shadow: none;
105
- }
106
- }
@@ -10,6 +10,7 @@
10
10
  @import "./select.css";
11
11
  @import "./custom-select.css";
12
12
  @import "./native.css";
13
+ @import "./range.css";
13
14
  @import "./switch.css";
14
15
  @import "./validation.css";
15
16
  @import "./form-actions.css";
@@ -1,21 +1,11 @@
1
1
  /*
2
- * Native-only control polish: file and range inputs that defeat most custom
3
- * styling, so the framework stays out of their way and just keeps them readable
4
- * and theme-aware.
2
+ * Native-only control polish: file input that mostly defeats custom styling,
3
+ * so the framework stays out of the way and just keeps it readable and
4
+ * theme-aware.
5
+ *
6
+ * Range has its own custom-styled file (range.css) so the focus ring can
7
+ * target the thumb pseudo-element rather than the rectangular CSS box.
5
8
  */
6
- /* Intent boundary — inherited intents do not tint this component.
7
- Local intent classes still win because :where() has zero specificity. */
8
- :where(.range) {
9
- /* @sync intent-boundary */
10
- --intent: initial;
11
- --intent-fg: initial;
12
- --ui-bg: initial;
13
- --ui-fg: initial;
14
- --ui-border: initial;
15
- --ui-hover-bg: initial;
16
- /* @sync intent-boundary */
17
- }
18
-
19
9
  .file {
20
10
  color: var(--text-muted);
21
11
  }
@@ -48,21 +38,7 @@
48
38
  }
49
39
  }
50
40
 
51
- .range {
52
- inline-size: 100%;
53
- accent-color: var(--intent, var(--primary));
54
- }
55
-
56
- .file:focus-visible,
57
- .range:focus-visible {
58
- outline: var(--focus-outline);
59
- outline-offset: var(--focus-outline-offset);
41
+ .file:focus-visible {
42
+ outline: none;
60
43
  box-shadow: var(--focus-ring-shadow);
61
44
  }
62
-
63
- @media (forced-colors: active) {
64
- .file:focus-visible,
65
- .range:focus-visible {
66
- box-shadow: none;
67
- }
68
- }
@@ -0,0 +1,95 @@
1
+ /*
2
+ * Range slider — custom-styled so the focus ring always targets the
3
+ * user-manipulated surface (the thumb) rather than the CSS box.
4
+ *
5
+ * appearance: none gives us full control over track and thumb; without it,
6
+ * pseudo-element rules for ::-webkit-slider-thumb and ::-moz-range-thumb
7
+ * are not reliably applied across browsers.
8
+ */
9
+
10
+ /* Intent boundary — inherited intents do not tint this component.
11
+ Local intent classes still win because :where() has zero specificity. */
12
+ :where(.range) {
13
+ /* @sync intent-boundary */
14
+ --intent: initial;
15
+ --intent-fg: initial;
16
+ --ui-bg: initial;
17
+ --ui-fg: initial;
18
+ --ui-border: initial;
19
+ --ui-hover-bg: initial;
20
+ /* @sync intent-boundary */
21
+ }
22
+
23
+ .range {
24
+ --range-thumb-size: 1.25rem;
25
+ --range-track-height: 0.25rem;
26
+ --range-thumb-bg: var(--intent, var(--primary));
27
+ --range-track-bg: var(--border);
28
+
29
+ appearance: none;
30
+ inline-size: 100%;
31
+ block-size: var(--range-thumb-size);
32
+ background: transparent;
33
+ cursor: pointer;
34
+ }
35
+
36
+ .range::-webkit-slider-runnable-track {
37
+ block-size: var(--range-track-height);
38
+ border-radius: var(--radius-full);
39
+ background: var(--range-track-bg);
40
+ }
41
+
42
+ .range::-moz-range-track {
43
+ block-size: var(--range-track-height);
44
+ border-radius: var(--radius-full);
45
+ background: var(--range-track-bg);
46
+ }
47
+
48
+ .range::-webkit-slider-thumb {
49
+ appearance: none;
50
+ inline-size: var(--range-thumb-size);
51
+ block-size: var(--range-thumb-size);
52
+ border-radius: var(--radius-full);
53
+ border: 0;
54
+ background: var(--range-thumb-bg);
55
+ margin-block-start: calc((var(--range-track-height) - var(--range-thumb-size)) / 2);
56
+ cursor: inherit;
57
+ transition: box-shadow var(--duration);
58
+ }
59
+
60
+ .range::-moz-range-thumb {
61
+ inline-size: var(--range-thumb-size);
62
+ block-size: var(--range-thumb-size);
63
+ border-radius: var(--radius-full);
64
+ border: 0;
65
+ background: var(--range-thumb-bg);
66
+ cursor: inherit;
67
+ transition: box-shadow var(--duration);
68
+ }
69
+
70
+ .range:focus-visible {
71
+ outline: none;
72
+ }
73
+
74
+ /*
75
+ * The 1px surface-coloured separator keeps the ring visually distinct from
76
+ * the small, coloured thumb — unlike a full-size input where the same gap
77
+ * would read as an unwanted void.
78
+ */
79
+ .range:focus-visible::-webkit-slider-thumb {
80
+ box-shadow:
81
+ 0 0 0 1px var(--surface),
82
+ 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
83
+ }
84
+
85
+ .range:focus-visible::-moz-range-thumb {
86
+ box-shadow:
87
+ 0 0 0 1px var(--surface),
88
+ 0 0 0 calc(1px + var(--focus-ring-width)) var(--focus-ring);
89
+ }
90
+
91
+ .range:disabled {
92
+ --range-thumb-bg: var(--text-subtle);
93
+ cursor: not-allowed;
94
+ opacity: var(--disabled-opacity);
95
+ }
@@ -86,8 +86,7 @@
86
86
 
87
87
  .switch:focus-visible {
88
88
  --switch-border: var(--focus);
89
- outline: var(--focus-outline);
90
- outline-offset: var(--focus-outline-offset);
89
+ outline: none;
91
90
  box-shadow: var(--focus-ring-shadow);
92
91
  }
93
92
 
@@ -16,6 +16,12 @@
16
16
  scrollbar-width: thin;
17
17
  }
18
18
 
19
+ /* Opt-in gutter: reserves space so the layout does not shift when
20
+ the scrollbar appears. Use alongside a known-overflow container. */
21
+ .scroller.stable-gutter {
22
+ scrollbar-gutter: stable;
23
+ }
24
+
19
25
  .scroller::-webkit-scrollbar {
20
26
  width: var(--scroller-size);
21
27
  height: var(--scroller-size);
package/src/css/theme.css CHANGED
@@ -61,11 +61,11 @@
61
61
 
62
62
  /* Lines and focus */
63
63
  --border: hsl(260 15% 88%);
64
- --focus: var(--primary);
64
+ --focus: var(--neutral);
65
65
  /* Pre-color-mix fallback only — superseded below by a derivation from
66
- --focus so overriding --primary/--focus never leaves a stale ring. */
67
- --focus-ring: hsl(268 32% 33% / 0.22);
68
- --focus-ring-shadow: 0 0 0 3px var(--focus-ring);
66
+ --focus so overriding --neutral/--focus never leaves a stale ring. */
67
+ --focus-ring: hsl(258 7% 43% / 0.3);
68
+ --focus-ring-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
69
69
  --hover-overlay: hsl(260 20% 10% / 0.04);
70
70
 
71
71
  /* Radius scale — themes may override the whole scale (e.g. [data-theme="square"]) */
@@ -106,7 +106,7 @@
106
106
  :root,
107
107
  [data-theme="light"],
108
108
  [data-theme="dark"] {
109
- --focus-ring: color-mix(in oklch, var(--focus) 22%, transparent);
109
+ --focus-ring: color-mix(in oklch, var(--focus) 30%, transparent);
110
110
  }
111
111
  }
112
112
 
@@ -146,7 +146,7 @@
146
146
  --text-subtle: light-dark(hsl(258 7% 57%), hsl(258 7% 54%));
147
147
 
148
148
  --border: light-dark(hsl(260 15% 88%), hsl(262 12% 24%));
149
- --focus-ring: light-dark(hsl(268 32% 33% / 0.22), hsl(268 42% 76% / 0.28));
149
+ --focus-ring: light-dark(hsl(258 7% 43% / 0.3), hsl(258 10% 72% / 0.28));
150
150
  --hover-overlay: light-dark(hsl(260 20% 10% / 0.04), hsl(0 0% 100% / 0.06));
151
151
  --shadow-color: light-dark(hsl(260 25% 10%), hsl(262 40% 2%));
152
152
  }
@@ -156,7 +156,7 @@
156
156
  :root,
157
157
  [data-theme="light"] {
158
158
  --focus-ring: light-dark(
159
- color-mix(in oklch, var(--focus) 22%, transparent),
159
+ color-mix(in oklch, var(--focus) 30%, transparent),
160
160
  color-mix(in oklch, var(--focus) 28%, transparent)
161
161
  );
162
162
  }
@@ -166,7 +166,7 @@
166
166
  :root,
167
167
  [data-theme="light"],
168
168
  [data-theme="dark"] {
169
- --focus-outline: calc(var(--border-width) * 2) solid Highlight;
169
+ --focus-outline-color: Highlight;
170
170
  --focus-ring-shadow: none;
171
171
 
172
172
  --backdrop-fill: Canvas;
@@ -179,7 +179,7 @@
179
179
  [data-theme="dark"] {
180
180
  --border-width: 2px;
181
181
  --focus-outline-offset: 3px;
182
- --focus-ring-shadow: 0 0 0 4px var(--focus-ring);
182
+ --focus-ring-width: 4px;
183
183
  --text-muted: var(--text);
184
184
  --text-subtle: var(--text);
185
185
  }