@volverjs/style 0.1.29 → 0.1.30

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 (158) hide show
  1. package/design-tokens.json +503 -7
  2. package/dist/base.css +1 -1
  3. package/dist/components/vv-alert.css +1 -1
  4. package/dist/components/vv-attachment-group.css +1 -0
  5. package/dist/components/vv-attachment.css +1 -0
  6. package/dist/components/vv-avatar.css +1 -1
  7. package/dist/components/vv-badge.css +1 -1
  8. package/dist/components/vv-breadcrumb.css +1 -1
  9. package/dist/components/vv-bubble-group.css +1 -0
  10. package/dist/components/vv-bubble.css +1 -0
  11. package/dist/components/vv-button.css +1 -1
  12. package/dist/components/vv-calendar.css +1 -0
  13. package/dist/components/vv-checkbox-group.css +1 -1
  14. package/dist/components/vv-checkbox.css +1 -1
  15. package/dist/components/vv-command.css +1 -0
  16. package/dist/components/vv-dialog.css +1 -1
  17. package/dist/components/vv-dropdown-action.css +1 -1
  18. package/dist/components/vv-dropdown-option.css +1 -1
  19. package/dist/components/vv-empty.css +1 -0
  20. package/dist/components/vv-field.css +1 -0
  21. package/dist/components/vv-input-file.css +1 -1
  22. package/dist/components/vv-input-otp.css +1 -0
  23. package/dist/components/vv-input-range.css +1 -1
  24. package/dist/components/vv-input-text.css +1 -1
  25. package/dist/components/vv-item-group.css +1 -0
  26. package/dist/components/vv-item.css +1 -0
  27. package/dist/components/vv-kbd-group.css +1 -0
  28. package/dist/components/vv-kbd.css +1 -0
  29. package/dist/components/vv-marker.css +1 -0
  30. package/dist/components/vv-message-scroller.css +1 -0
  31. package/dist/components/vv-message.css +1 -0
  32. package/dist/components/vv-nav.css +1 -1
  33. package/dist/components/vv-pagination.css +1 -0
  34. package/dist/components/vv-popover.css +1 -0
  35. package/dist/components/vv-progress.css +1 -1
  36. package/dist/components/vv-prose.css +1 -0
  37. package/dist/components/vv-questionnaire.css +1 -0
  38. package/dist/components/vv-radio-group.css +1 -1
  39. package/dist/components/vv-radio.css +1 -1
  40. package/dist/components/vv-select.css +1 -1
  41. package/dist/components/vv-separator.css +1 -0
  42. package/dist/components/vv-sidebar.css +1 -0
  43. package/dist/components/vv-spinner.css +1 -0
  44. package/dist/components/vv-table.css +1 -1
  45. package/dist/components/vv-text.css +1 -1
  46. package/dist/components/vv-textarea.css +1 -1
  47. package/dist/components.css +1 -1
  48. package/dist/export.css +1 -1
  49. package/dist/props/colors.css +1 -1
  50. package/dist/props/input.css +1 -1
  51. package/dist/props.css +1 -1
  52. package/dist/themes/dark/components.css +1 -1
  53. package/dist/themes/dark/props.css +1 -1
  54. package/dist/themes/dark/volver.css +1 -1
  55. package/dist/utilities/colors.css +1 -1
  56. package/dist/utilities/layout.css +1 -1
  57. package/dist/utilities.css +1 -1
  58. package/dist/volver.css +1 -1
  59. package/package.json +64 -16
  60. package/src/_config.scss +23 -0
  61. package/src/_preflight.scss +14 -2
  62. package/src/base.scss +1 -1
  63. package/src/components/index.scss +28 -0
  64. package/src/components/vv-attachment-group.scss +17 -0
  65. package/src/components/vv-attachment.scss +17 -0
  66. package/src/components/vv-bubble-group.scss +17 -0
  67. package/src/components/vv-bubble.scss +17 -0
  68. package/src/components/vv-calendar.scss +25 -0
  69. package/src/components/vv-command.scss +17 -0
  70. package/src/components/vv-empty.scss +17 -0
  71. package/src/components/vv-field.scss +25 -0
  72. package/src/components/vv-input-otp.scss +17 -0
  73. package/src/components/vv-item-group.scss +17 -0
  74. package/src/components/vv-item.scss +39 -0
  75. package/src/components/vv-kbd-group.scss +17 -0
  76. package/src/components/vv-kbd.scss +17 -0
  77. package/src/components/vv-marker.scss +17 -0
  78. package/src/components/vv-message-scroller.scss +24 -0
  79. package/src/components/vv-message.scss +25 -0
  80. package/src/components/vv-pagination.scss +17 -0
  81. package/src/components/vv-popover.scss +17 -0
  82. package/src/components/vv-prose.scss +17 -0
  83. package/src/components/vv-questionnaire.scss +28 -0
  84. package/src/components/vv-separator.scss +17 -0
  85. package/src/components/vv-sidebar.scss +17 -0
  86. package/src/components/vv-spinner.scss +27 -0
  87. package/src/presets/_outlined-fields.scss +346 -0
  88. package/src/props/colors.scss +6 -0
  89. package/src/settings/_colors.scss +161 -61
  90. package/src/settings/_index.scss +26 -0
  91. package/src/settings/_input.scss +11 -2
  92. package/src/settings/components/_choice.scss +104 -0
  93. package/src/settings/components/_vv-alert.scss +37 -12
  94. package/src/settings/components/_vv-attachment-group.scss +10 -0
  95. package/src/settings/components/_vv-attachment.scss +135 -0
  96. package/src/settings/components/_vv-avatar.scss +19 -9
  97. package/src/settings/components/_vv-badge.scss +47 -14
  98. package/src/settings/components/_vv-breadcrumb.scss +1 -1
  99. package/src/settings/components/_vv-bubble-group.scss +31 -0
  100. package/src/settings/components/_vv-bubble.scss +69 -0
  101. package/src/settings/components/_vv-button.scss +55 -13
  102. package/src/settings/components/_vv-calendar.scss +102 -0
  103. package/src/settings/components/_vv-checkbox-group.scss +33 -2
  104. package/src/settings/components/_vv-checkbox.scss +8 -3
  105. package/src/settings/components/_vv-command.scss +99 -0
  106. package/src/settings/components/_vv-dialog.scss +129 -26
  107. package/src/settings/components/_vv-dropdown-action.scss +3 -2
  108. package/src/settings/components/_vv-dropdown-option.scss +3 -3
  109. package/src/settings/components/_vv-empty.scss +71 -0
  110. package/src/settings/components/_vv-field.scss +111 -0
  111. package/src/settings/components/_vv-input-file.scss +8 -8
  112. package/src/settings/components/_vv-input-otp.scss +55 -0
  113. package/src/settings/components/_vv-input-range.scss +3 -3
  114. package/src/settings/components/_vv-input-text.scss +7 -7
  115. package/src/settings/components/_vv-item-group.scss +68 -0
  116. package/src/settings/components/_vv-item.scss +201 -0
  117. package/src/settings/components/_vv-kbd-group.scss +9 -0
  118. package/src/settings/components/_vv-kbd.scss +20 -0
  119. package/src/settings/components/_vv-marker.scss +32 -0
  120. package/src/settings/components/_vv-message-scroller.scss +65 -0
  121. package/src/settings/components/_vv-message.scss +95 -0
  122. package/src/settings/components/_vv-nav.scss +7 -3
  123. package/src/settings/components/_vv-pagination.scss +78 -0
  124. package/src/settings/components/_vv-popover.scss +53 -0
  125. package/src/settings/components/_vv-progress.scss +4 -4
  126. package/src/settings/components/_vv-prose.scss +274 -0
  127. package/src/settings/components/_vv-questionnaire.scss +77 -0
  128. package/src/settings/components/_vv-radio.scss +7 -3
  129. package/src/settings/components/_vv-select.scss +6 -6
  130. package/src/settings/components/_vv-separator.scss +79 -0
  131. package/src/settings/components/_vv-sidebar.scss +91 -0
  132. package/src/settings/components/_vv-spinner.scss +26 -0
  133. package/src/settings/components/_vv-table.scss +63 -0
  134. package/src/settings/components/_vv-text.scss +3 -3
  135. package/src/settings/components/_vv-textarea.scss +7 -7
  136. package/src/themes/dark/components/_vv-checkbox.scss +27 -0
  137. package/src/themes/dark/components/_vv-radio.scss +27 -0
  138. package/src/themes/dark/components/index.scss +2 -0
  139. package/src/themes/dark/props/_colors.scss +6 -0
  140. package/src/themes/dark/settings/_colors.scss +97 -32
  141. package/src/themes/dark/settings/_index.scss +2 -0
  142. package/src/themes/dark/settings/_input.scss +4 -8
  143. package/src/themes/dark/settings/components/_choice.scss +15 -0
  144. package/src/themes/dark/settings/components/_vv-alert.scss +6 -48
  145. package/src/themes/dark/settings/components/_vv-avatar.scss +1 -16
  146. package/src/themes/dark/settings/components/_vv-badge.scss +1 -32
  147. package/src/themes/dark/settings/components/_vv-button.scss +0 -15
  148. package/src/themes/dark/settings/components/_vv-checkbox.scss +7 -0
  149. package/src/themes/dark/settings/components/_vv-dropdown-action.scss +2 -3
  150. package/src/themes/dark/settings/components/_vv-dropdown-option.scss +2 -19
  151. package/src/themes/dark/settings/components/_vv-nav.scss +2 -15
  152. package/src/themes/dark/settings/components/_vv-radio.scss +7 -0
  153. package/src/tools/_functions.scss +430 -2
  154. package/src/tools/mixin-modules/_bem.scss +5 -39
  155. package/src/tools/mixin-modules/_states.scss +77 -0
  156. package/src/tools/mixin-modules/_theme.scss +135 -121
  157. package/src/utilities/colors.scss +49 -4
  158. package/src/utilities/layout.scss +25 -0
@@ -1,46 +1,15 @@
1
+ // The colored variants take the readable roles, which follow the theme
1
2
  $dark-vv-badge: (
2
3
  modifier: (
3
4
  outline: (
4
- color: var(--color-brand-lighten-3),
5
5
  modifier: (
6
- danger: (
7
- color: var(--color-danger-lighten-3),
8
- ),
9
- success: (
10
- color: var(--color-success-lighten-3),
11
- ),
12
- warning: (
13
- color: var(--color-warning),
14
- ),
15
- info: (
16
- color: var(--color-info),
17
- ),
18
- accent: (
19
- color: var(--color-accent-lighten-3),
20
- ),
21
6
  gray: (
22
7
  color: var(--color-gray-lighten-3),
23
8
  ),
24
9
  ),
25
10
  ),
26
11
  ghost: (
27
- color: var(--color-brand-lighten-3),
28
12
  modifier: (
29
- danger: (
30
- color: var(--color-danger-lighten-3),
31
- ),
32
- success: (
33
- color: var(--color-success-lighten-3),
34
- ),
35
- warning: (
36
- color: var(--color-warning),
37
- ),
38
- info: (
39
- color: var(--color-info),
40
- ),
41
- accent: (
42
- color: var(--color-accent-lighten-3),
43
- ),
44
13
  gray: (
45
14
  color: var(--color-gray-lighten-3),
46
15
  ),
@@ -21,9 +21,6 @@ $dark-vv-button: (
21
21
  border-color: var(--color-gray-darken-1),
22
22
  text-shadow: 0 1px 0 var(--color-gray-darken-5),
23
23
  state: (
24
- focus-visible: (
25
- outline: var(--spacing-px) solid var(--color-gray),
26
- ),
27
24
  hover: (
28
25
  background: var(--color-gray-darken-4),
29
26
  ),
@@ -38,10 +35,6 @@ $dark-vv-button: (
38
35
  color: var(--color-gray-lighten-2),
39
36
  text-shadow: 0 1px 0 var(--color-gray-darken-4),
40
37
  state: (
41
- focus-visible: (
42
- outline: var(--spacing-px) solid var(--color-gray),
43
- outline-offset: var(--spacing-0),
44
- ),
45
38
  hover: (
46
39
  background: var(--color-gray-darken-4),
47
40
  ),
@@ -50,13 +43,5 @@ $dark-vv-button: (
50
43
  ),
51
44
  ),
52
45
  ),
53
- link: (
54
- color: var(--color-brand-lighten-3),
55
- state: (
56
- active: (
57
- color: var(--color-brand-lighten-2),
58
- ),
59
- ),
60
- ),
61
46
  ),
62
47
  ) !default;
@@ -0,0 +1,7 @@
1
+ @use 'choice' as *;
2
+
3
+ $dark-vv-checkbox: (
4
+ modifier: (
5
+ segment: $dark-choice-segment,
6
+ ),
7
+ );
@@ -1,3 +1,2 @@
1
- $dark-vv-dropdown-action: (
2
- color: var(--color-brand-lighten-3),
3
- );
1
+ // Its text colors are the readable roles, which the dark theme redeclares
2
+ $dark-vv-dropdown-action: ();
@@ -1,19 +1,2 @@
1
- $dark-vv-dropdown-option: (
2
- state: (
3
- focus-visible: (
4
- color: var(--color-success-lighten-3),
5
- ),
6
- selected: (
7
- color: var(--color-brand-lighten-3),
8
- ),
9
- ),
10
- modifier: (
11
- unselectable: (
12
- state: (
13
- focus-visible: (
14
- color: var(--color-danger-lighten-5),
15
- ),
16
- ),
17
- ),
18
- ),
19
- );
1
+ // Its text colors are the readable roles, which the dark theme redeclares
2
+ $dark-vv-dropdown-option: ();
@@ -1,15 +1,2 @@
1
- $dark-vv-nav: (
2
- modifier: (
3
- tabs: (
4
- element: (
5
- item-label: (
6
- state: (
7
- current: (
8
- color: var(--color-brand-lighten-3),
9
- ),
10
- ),
11
- ),
12
- ),
13
- ),
14
- ),
15
- );
1
+ // Its text colors are the readable roles, which the dark theme redeclares
2
+ $dark-vv-nav: ();
@@ -0,0 +1,7 @@
1
+ @use 'choice' as *;
2
+
3
+ $dark-vv-radio: (
4
+ modifier: (
5
+ segment: $dark-choice-segment,
6
+ ),
7
+ );
@@ -156,12 +156,16 @@
156
156
  // @param {String} $name - Color name for CSS variables
157
157
  // @param {Number} $steps - Number of lighten/darken steps (default: 5)
158
158
  // @param {Number} $scale - Lightness increment per step (default: 10%)
159
+ // @param {Boolean} $text - Add the tokens for text on the color: the
160
+ // `contrast` texts (relative-contrast-value()) and the covers
161
+ // (relative-cover-value()) (default: false)
159
162
  // @return {Map} Complete shades map with base + lighten + darken variants
160
163
  @function color-mix-shades-map(
161
164
  $color,
162
165
  $name,
163
166
  $steps: 5,
164
- $scale: 10%
167
+ $scale: 10%,
168
+ $text: false
165
169
  ) {
166
170
  // Start with base color
167
171
  $result: color-base-map($color, $name);
@@ -180,6 +184,10 @@
180
184
  $result: map.merge($result, (#{$key}: relative-color-value($name, $change)));
181
185
  }
182
186
 
187
+ @if $text {
188
+ $result: -with-text-tokens($result, $name, $relative: true, $scale: $scale);
189
+ }
190
+
183
191
  @return $result;
184
192
  }
185
193
 
@@ -260,16 +268,432 @@
260
268
  @return $result;
261
269
  }
262
270
 
271
+ // A neutral that takes its hue from the tint. While $color is still the
272
+ // default derived from $tint, hsl(color-hue($tint) $saturation $lightness),
273
+ // it is written relative to var(--color-tint), so a runtime override of the
274
+ // tint moves it too. A color set explicitly is returned as it is.
275
+ // @param {Color} $color - The neutral, as configured
276
+ // @param {Color} $tint - The color the default takes its hue from
277
+ // @param {Number} $saturation - Saturation of the default
278
+ // @param {Number} $lightness - Lightness of the default
279
+ // @param {Number} $alpha - Alpha of the default (default: 1)
280
+ // @return {Color|String} $color, or its relative color syntax expression
281
+ @function tinted-neutral-value($color, $tint, $saturation, $lightness, $alpha: 1) {
282
+ @if $color != hsl(color-hue($tint), $saturation, $lightness, $alpha) {
283
+ @return $color;
284
+ }
285
+
286
+ @if $alpha == 1 {
287
+ @return string.unquote('hsl(from var(--color-tint) h #{$saturation} #{$lightness})');
288
+ }
289
+
290
+ @return string.unquote('hsl(from var(--color-tint) h #{$saturation} #{$lightness} / #{$alpha})');
291
+ }
292
+
293
+ // ============================================
294
+ // Contrast Functions
295
+ // ============================================
296
+
297
+ // WCAG 2 relative luminance: the Y of the color in CIE XYZ (D65), which is
298
+ // the weighted sum of the linear sRGB channels that WCAG defines.
299
+ // @param {Color} $color - Any color
300
+ // @return {Number} Luminance, 0 for black and 1 for white
301
+ @function color-luminance($color) {
302
+ @return color.channel($color, 'y', $space: xyz-d65);
303
+ }
304
+
305
+ // The luminance at which $light and $dark give the same WCAG contrast ratio:
306
+ // below it $light reads better, above it $dark does. For white and black it
307
+ // is sqrt(1.05 * 0.05) - 0.05, about 0.1791.
308
+ // @param {Color} $light - The light text color (default: white)
309
+ // @param {Color} $dark - The dark text color (default: black)
310
+ // @return {Number} Threshold luminance
311
+ @function contrast-threshold($light: #fff, $dark: #000) {
312
+ @return math.sqrt((color-luminance($light) + 0.05) * (color-luminance($dark) + 0.05)) - 0.05;
313
+ }
314
+
315
+ // Pick at compile time the text color that reads better on $color
316
+ // @param {Color} $color - The filled background
317
+ // @param {Color} $light - The light text color (default: white)
318
+ // @param {Color} $dark - The dark text color (default: black)
319
+ // @return {Color} $light or $dark, whichever has the higher contrast ratio
320
+ @function contrast-color($color, $light: #fff, $dark: #000) {
321
+ @if color-luminance($color) < contrast-threshold($light, $dark) {
322
+ @return $light;
323
+ }
324
+
325
+ @return $dark;
326
+ }
327
+
328
+ // The choice of contrast-color() between white and black made in CSS, so it
329
+ // follows a runtime override of --color-#{$name}. Two nested relative colors:
330
+ // - the inner one reads the luminance as the `y` channel of xyz-d65, and a
331
+ // clamp turns it into a step, 1 below the threshold and 0 above it, that it
332
+ // stores as its alpha. The xyz-d65 channels are numbers in every engine,
333
+ // while Safari 17 reads those of srgb as percentages and drops the whole
334
+ // value as soon as one meets a number.
335
+ // - the outer one copies that alpha into the three sRGB channels. The
336
+ // keyword has the type of those channels in each engine, a percentage in
337
+ // Safari 17 and a number elsewhere, so it can stand alone in them (not in
338
+ // a sum with a number), and no color space conversion touches it, so the
339
+ // result is exactly white or black. Writing the channels in xyz-d65 would
340
+ // not be: Chrome turns the white point into 0.99987 1.00005 1.00007, and
341
+ // text drawn with it differs from white at the antialiased edges.
342
+ // @param {String} $name - Shade name for CSS variables (`brand`, `brand-darken-1`)
343
+ // @return {String} CSS relative color syntax expression
344
+ @function relative-contrast-value($name) {
345
+ @return string.unquote(
346
+ 'color(from color(from var(--color-#{$name}) xyz-d65 x y z / #{-contrast-step()}) srgb alpha alpha alpha / 1)'
347
+ );
348
+ }
349
+
350
+ // The step of the contrast token, read from the `y` of an xyz-d65 origin: 1
351
+ // where white text reads better, 0 where black does. The threshold has four
352
+ // decimals, 0.1791, the most stylelint allows in the source.
353
+ @function -contrast-step() {
354
+ $threshold: math.div(math.round(contrast-threshold() * 10000), 10000);
355
+
356
+ @return 'clamp(0, (#{$threshold} - y) * 1000000, 1)';
357
+ }
358
+
359
+ // The fill under the contrast text of a shade: the shade itself where that
360
+ // text turns dark, transparent where it stays white, with the step of
361
+ // relative-contrast-value() on the same shade. vv-button lays it over its
362
+ // dark emboss, which suits light text only, so the emboss disappears under
363
+ // dark text.
364
+ // @param {String} $name - Shade name for CSS variables (`brand-darken-1`)
365
+ // @return {String} CSS relative color syntax expression
366
+ @function relative-cover-value($name) {
367
+ @return string.unquote('color(from var(--color-#{$name}) xyz-d65 x y z / calc(1 - #{-contrast-step()}))');
368
+ }
369
+
370
+ // ============================================
371
+ // Readable Functions
372
+ // ============================================
373
+
374
+ // The luminance that text on $surface has to stay beyond for a contrast
375
+ // ratio of $ratio: at most this much on a light surface, at least this much
376
+ // on a dark one. Rounded to four decimals, toward the safe side.
377
+ // @param {Color} $surface - The surface the text sits on
378
+ // @param {Number} $ratio - The contrast ratio to keep
379
+ // @return {Number} Bound luminance
380
+ @function readable-luminance($surface, $ratio) {
381
+ $y: color-luminance($surface);
382
+
383
+ @if $y >= contrast-threshold() {
384
+ @return math.div(math.floor((math.div($y + 0.05, $ratio) - 0.05) * 10000), 10000);
385
+ }
386
+
387
+ @return math.div(math.ceil(($ratio * ($y + 0.05) - 0.05) * 10000), 10000);
388
+ }
389
+
390
+ // The tint, among every hue at $saturation and $lightness, that text has the
391
+ // hardest time reading on: the darkest of them when they are light, the
392
+ // lightest when they are dark. The tinted surfaces are
393
+ // hsl(from <color> h s <lightness>), and full saturation is where their
394
+ // luminance strays furthest from that of a grey, so a bound computed on this
395
+ // tint holds for whatever color is set at runtime. The neutral surfaces take
396
+ // their hue from --color-tint, which can change at runtime too.
397
+ // @param {Number} $lightness - Lightness of the surfaces
398
+ // @param {Number} $saturation - Saturation of the surfaces (default: 100%)
399
+ // @return {Color} The tint
400
+ @function readable-worst-tint($lightness, $saturation: 100%) {
401
+ $light: $lightness >= 50%;
402
+ $worst: null;
403
+
404
+ @for $hue from 0 to 360 {
405
+ $tint: hsl($hue, $saturation, $lightness);
406
+
407
+ @if not $worst or ($light and color-luminance($tint) < color-luminance($worst)) or
408
+ (not $light and color-luminance($tint) > color-luminance($worst))
409
+ {
410
+ $worst: $tint;
411
+ }
412
+ }
413
+
414
+ @return $worst;
415
+ }
416
+
417
+ // The surface readable-luminance() keeps text on: the hardest hue of
418
+ // $surface, $offset lightness away from it, where the deepest neutral surface
419
+ // that carries text in a color sits.
420
+ // @param {Color} $surface - The base surface of the theme
421
+ // @param {Number} $offset - Lightness from it to the deepest surface
422
+ // @return {Color} The surface to compute the bound on
423
+ @function readable-surface($surface, $offset) {
424
+ @return readable-worst-tint(
425
+ color.channel($surface, 'lightness', $space: hsl) + $offset,
426
+ color.channel($surface, 'saturation', $space: hsl)
427
+ );
428
+ }
429
+
430
+ // A color written as text, moved just far enough to reach the luminance
431
+ // $bound, and left alone when it already does. The math runs in xyz-d65,
432
+ // whose channels are numbers in every engine (Safari 17 reads those of srgb
433
+ // and the `alpha` keyword as percentages), and where scaling is scaling
434
+ // linear light: the color keeps its chromaticity and lands on the bound.
435
+ // - Toward black, on a light surface, the channels are scaled by bound / y.
436
+ // - Toward white, on a dark surface, they are scaled up by the same ratio,
437
+ // which keeps the color as vivid as it was, then clipped to the sRGB gamut
438
+ // (-gamut-clip()), and what the clip took is made up by moving their
439
+ // distance from the white point by (1 - bound) / (1 - y). Mixing toward
440
+ // white alone would pull the chroma down, and a dark color raised on a dark
441
+ // surface would come out greyish.
442
+ // A color that already reads goes through a round trip in xyz-d65 that lands
443
+ // within a fraction of an 8-bit step.
444
+ // @param {String} $origin - Any CSS color, `var(--color-brand)`
445
+ // @param {Number} $bound - The luminance to reach, from readable-luminance()
446
+ // @param {Boolean} $lighten - Move toward white, for a dark surface (default: false)
447
+ // @return {String} CSS relative color syntax expression
448
+ @function readable-color-value($origin, $bound, $lighten: false) {
449
+ @if not $lighten {
450
+ $scale: 'min(1, #{$bound} / max(y, 0.0001))';
451
+
452
+ @return string.unquote('color(from #{$origin} xyz-d65 calc(x * #{$scale}) min(y, #{$bound}) calc(z * #{$scale}))');
453
+ }
454
+
455
+ $scale: 'max(1, #{$bound} / max(y, 0.0001))';
456
+ $lifted: -gamut-clip(
457
+ 'color(from #{$origin} xyz-d65 calc(x * #{$scale}) calc(y * #{$scale}) calc(z * #{$scale}))'
458
+ );
459
+ $x: math.div(math.round(color.channel(#fff, 'x', $space: xyz-d65) * 10000), 10000);
460
+ $z: math.div(math.round(color.channel(#fff, 'z', $space: xyz-d65) * 10000), 10000);
461
+ $scale: 'min(1, #{1 - $bound} / max(1 - y, 0.0001))';
462
+
463
+ @return string.unquote(
464
+ 'color(from #{$lifted} xyz-d65 calc(#{$x} - (#{$x} - x) * #{$scale}) max(y, #{$bound}) calc(#{$z} - (#{$z} - z) * #{$scale}))'
465
+ );
466
+ }
467
+
468
+ // The readable role a step further from the surface: its luminance scaled by
469
+ // $factor, below 1 on a light surface and above it on a dark one, where the
470
+ // result is clipped to the sRGB gamut. Each channel ends at least as far from
471
+ // the surface as the role, so it reads at least as well.
472
+ // @param {String} $readable - The readable role, `var(--color-brand-readable)`
473
+ // @param {Number} $factor - The luminance factor of the step
474
+ // @return {String} CSS relative color syntax expression
475
+ @function readable-strong-value($readable, $factor) {
476
+ $value: 'color(from #{$readable} xyz-d65 calc(x * #{$factor}) calc(y * #{$factor}) calc(z * #{$factor}))';
477
+
478
+ @if $factor > 1 {
479
+ @return -gamut-clip($value);
480
+ }
481
+
482
+ @return string.unquote($value);
483
+ }
484
+
485
+ // A color scaled up in xyz-d65 can leave the sRGB gamut, where a browser
486
+ // would map it on its own terms. Each channel is clipped to 1 in srgb, where
487
+ // `alpha` is the unit in the type every engine reads the channels in, 1 or
488
+ // 100%, as long as the color is opaque.
489
+ @function -gamut-clip($color) {
490
+ @return string.unquote('color(from #{$color} srgb min(r, alpha) min(g, alpha) min(b, alpha))');
491
+ }
492
+
493
+ // The text and graphic roles of the palette colors, as a map to deep-merge
494
+ // into a colors map: `readable` and `readable-strong` next to the shades of
495
+ // each color, the text on the neutral surfaces and the one a step further
496
+ // from them, `graphic`, the color of a bar, a focus ring or a state border on
497
+ // the same surfaces, and `<name>-readable` in `surface`, the text on the
498
+ // tinted surface of the color. Each role starts from the color itself and is
499
+ // moved to $bound, to $graphic-bound for `graphic`, or to $surface-bound on
500
+ // the tinted surface, and `readable-strong` is `readable` with its luminance
501
+ // scaled by $strong. Without $relative, for the HSL channel branch, which has
502
+ // no relative color syntax, a role is the first shade of the color that
503
+ // reaches its bound, chosen at compile time (-readable-shade()).
504
+ // @param {Map} $colors - Color name to its compile time value
505
+ // @param {Number} $bound - Bound of the text on the neutral surfaces, from readable-luminance()
506
+ // @param {Number} $surface-bound - Bound of the text on the tinted surfaces
507
+ // @param {Number} $graphic-bound - Bound of a graphic on the neutral surfaces
508
+ // @param {Number} $strong - Luminance factor of `readable-strong`
509
+ // @param {Boolean} $lighten - The surface is dark (default: false)
510
+ // @param {Boolean} $relative - Compute the roles in CSS (default: true)
511
+ // @return {Map} (brand: (readable, readable-strong, graphic), ..., surface: (brand-readable, ...))
512
+ @function readable-roles($colors, $bound, $surface-bound, $graphic-bound, $strong, $lighten: false, $relative: true) {
513
+ $result: ();
514
+ $surface: ();
515
+
516
+ @each $name, $color in $colors {
517
+ $origin: 'var(--color-#{$name})';
518
+
519
+ @if $relative {
520
+ $result: map.set(
521
+ $result,
522
+ $name,
523
+ (
524
+ readable: readable-color-value($origin, $bound, $lighten),
525
+ readable-strong: readable-strong-value('var(--color-#{$name}-readable)', $strong),
526
+ graphic: readable-color-value($origin, $graphic-bound, $lighten),
527
+ )
528
+ );
529
+ $surface: map.set(
530
+ $surface,
531
+ string.unquote('#{$name}-readable'),
532
+ readable-color-value($origin, $surface-bound, $lighten)
533
+ );
534
+ } @else {
535
+ $step: -readable-shade($color, $bound, $lighten);
536
+ $target: color-luminance(-legacy-shade($color, $step, $lighten)) * $strong;
537
+ $result: map.set(
538
+ $result,
539
+ $name,
540
+ (
541
+ readable: -legacy-shade-var($name, $step, $lighten),
542
+ readable-strong: -legacy-shade-var(
543
+ $name,
544
+ -readable-shade($color, $target, $lighten, $from: $step + 1),
545
+ $lighten
546
+ ),
547
+ graphic: -legacy-shade-var($name, -readable-shade($color, $graphic-bound, $lighten), $lighten),
548
+ )
549
+ );
550
+ $surface: map.set(
551
+ $surface,
552
+ string.unquote('#{$name}-readable'),
553
+ -legacy-shade-var($name, -readable-shade($color, $surface-bound, $lighten), $lighten)
554
+ );
555
+ }
556
+ }
557
+
558
+ @return map.set($result, surface, $surface);
559
+ }
560
+
561
+ // The first step, from $from to 5, of the shades of $color away from the
562
+ // surface (darker with a light surface, lighter with a dark one) whose
563
+ // luminance reaches $bound, or 5 when none does
564
+ @function -readable-shade($color, $bound, $lighten, $from: 0) {
565
+ @for $step from math.min($from, 5) through 5 {
566
+ $y: color-luminance(-legacy-shade($color, $step, $lighten));
567
+
568
+ @if if(sass($lighten): $y >= $bound; else: $y <= $bound) {
569
+ @return $step;
570
+ }
571
+ }
572
+
573
+ @return 5;
574
+ }
575
+
576
+ // A shade of the HSL channel branch at compile time: the lightness scaled by
577
+ // 1 - 10% per step (darken), or 1 + 10% per step (lighten)
578
+ @function -legacy-shade($color, $step, $lighten) {
579
+ $multiplier: if(sass($lighten): 1 + 0.1 * $step; else: 1 - 0.1 * $step);
580
+ $lightness: math.min(100%, color.channel($color, 'lightness', $space: hsl) * $multiplier);
581
+
582
+ @return color.change($color, $lightness: $lightness, $space: hsl);
583
+ }
584
+
585
+ @function -legacy-shade-var($name, $step, $lighten) {
586
+ @if $step == 0 {
587
+ @return string.unquote('var(--color-#{$name})');
588
+ }
589
+
590
+ @return string.unquote('var(--color-#{$name}-#{if(sass($lighten): lighten; else: darken)}-#{$step})');
591
+ }
592
+
593
+ // Add the tokens for text on a color to its shades map: `contrast` and
594
+ // `cover` next to the base and to every darken key (`darken-1-contrast`,
595
+ // `darken-1-cover`), the shades a filled component paints. With $relative
596
+ // they are computed in CSS. Without, for the HSL channel branch, the contrast
597
+ // text is chosen at compile time by contrast-color(), on the shade of $color,
598
+ // and the cover follows that choice.
599
+ // @param {Map} $map - A shades map
600
+ // @param {String} $name - Color name for CSS variables
601
+ // @param {Boolean} $relative - Compute the tokens in CSS
602
+ // @param {Number} $scale - Lightness factor per step of the shades (default: 10%)
603
+ // @param {Color} $color - The base color, required without $relative (default: null)
604
+ // @return {Map} The map with the text tokens
605
+ @function -with-text-tokens($map, $name, $relative, $scale: 10%, $color: null) {
606
+ $result: $map;
607
+
608
+ @each $key, $value in $map {
609
+ $key: '#{$key}';
610
+ $base: $key == $name;
611
+ $darken: string.index($key, 'darken-') == 1;
612
+ $token: string.index($key, 'contrast') or string.index($key, 'cover');
613
+
614
+ @if not $token and ($base or $darken) {
615
+ $shade: if(sass($base): $name; else: '#{$name}-#{$key}');
616
+ $suffix: if(sass($base): ''; else: '#{$key}-');
617
+
618
+ @if $relative {
619
+ $result: map.set($result, string.unquote('#{$suffix}contrast'), relative-contrast-value($shade));
620
+ $result: map.set($result, string.unquote('#{$suffix}cover'), relative-cover-value($shade));
621
+ } @else {
622
+ $multiplier: 1;
623
+
624
+ @if $darken {
625
+ $multiplier: 1 - math.div($scale, 100%) * -to-number(string.slice($key, string.length('darken-') + 1));
626
+ }
627
+
628
+ $lightness: color.channel($color, 'lightness', $space: hsl) * $multiplier;
629
+ $contrast: contrast-color(color.change($color, $lightness: $lightness, $space: hsl));
630
+ $cover: if(sass($contrast == #fff): transparent; else: string.unquote('var(--color-#{$shade})'));
631
+ $result: map.set($result, string.unquote('#{$suffix}contrast'), $contrast);
632
+ $result: map.set($result, string.unquote('#{$suffix}cover'), $cover);
633
+ }
634
+ }
635
+ }
636
+
637
+ @return $result;
638
+ }
639
+
640
+ // '3' to 3
641
+ @function -to-number($string) {
642
+ $digits: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
643
+ $result: 0;
644
+
645
+ @for $index from 1 through string.length($string) {
646
+ $result: $result * 10 + map.get($digits, string.slice($string, $index, $index));
647
+ }
648
+
649
+ @return $result;
650
+ }
651
+
652
+ // The fixed lightness steps of word and surface, calc(l + 12), rewritten in
653
+ // percent, calc(l + 12%). Safari 17 reads `l` as a percentage and drops the
654
+ // first form, Chrome and Firefox read it as a number and drop the second, so
655
+ // the props emit both, the second under @supports. Entries that are not a
656
+ // fixed step are left out.
657
+ // @param {Map} $map - A colors map
658
+ // @return {Map} The fixed steps alone, in percent
659
+ @function fixed-steps-in-percent($map) {
660
+ $result: ();
661
+
662
+ @each $key, $value in $map {
663
+ @if meta.type-of($value) == 'map' {
664
+ $steps: fixed-steps-in-percent($value);
665
+
666
+ @if list.length($steps) > 0 {
667
+ $result: map.set($result, $key, $steps);
668
+ }
669
+ } @else if meta.type-of($value) == 'string' and
670
+ (string.index($value, ' h s calc(l + ') or string.index($value, ' h s calc(l - '))
671
+ {
672
+ $result: map.set(
673
+ $result,
674
+ $key,
675
+ string.unquote(string.slice($value, 1, -3) + '%))')
676
+ );
677
+ }
678
+ }
679
+
680
+ @return $result;
681
+ }
682
+
263
683
  // ============================================
264
684
  // Legacy Color Functions (HSL channel-based)
265
685
  // ============================================
686
+ // @param {Boolean} $text - Add the tokens for text on the color: the
687
+ // `contrast` texts and the covers, chosen at compile time by
688
+ // contrast-color() (see -with-text-tokens()) (default: false)
266
689
  @function color-shades-map(
267
690
  $color,
268
691
  $name,
269
692
  $steps: 5,
270
693
  $scale: 10%,
271
694
  $fixed: false,
272
- $noVarExport: false
695
+ $noVarExport: false,
696
+ $text: false
273
697
  ) {
274
698
  $result: map.merge(
275
699
  map.set(
@@ -296,6 +720,10 @@
296
720
  )
297
721
  );
298
722
 
723
+ @if $text {
724
+ $result: -with-text-tokens($result, $name, $relative: false, $scale: $scale, $color: $color);
725
+ }
726
+
299
727
  @return $result;
300
728
  }
301
729
 
@@ -8,43 +8,15 @@
8
8
  @use 'helpers' as *;
9
9
  @use 'props' as *;
10
10
  @use 'attrs' as *;
11
+ @use 'states' as *;
11
12
 
12
13
  // ============================================
13
14
  // BEM Mixins
14
15
  // Generate BEM-structured CSS from maps
15
16
  // ============================================
16
17
 
17
- // List of valid CSS states
18
- $-valid-states: (
19
- active, focus, hover, focus-within, target, visited, focus-visible,
20
- disabled, checked, checked-within, multiple, selected, readonly,
21
- empty, placeholder-shown, dirty, valid, invalid, indeterminate,
22
- first-child, last-child, open, close, determinate, pressed, current,
23
- popover, popover-open
24
- );
25
-
26
- // Selector suffixes appended to the block, and to its alias when there is one,
27
- // for the states that are not plain pseudo-classes. A state can need more than
28
- // one suffix: "disabled" also matches the aria attribute. States missing here
29
- // fall back to ":not([disabled]):<state>", while "current" adds no suffix at
30
- // all and keeps only the BEM selectors built by the caller.
31
- $-state-suffixes: (
32
- checked: ':checked',
33
- checked-within: ':has(input:checked)',
34
- close: ':not([open])',
35
- dirty: ':not(:has(*:placeholder-shown))',
36
- disabled: ('[disabled]', '[aria-disabled="true"]'),
37
- focus-visible: ':not(:active):not([disabled]):not([tabindex="-1"]):not([aria-disabled="true"]):focus-visible',
38
- multiple: '[multiple]',
39
- open: '[open]',
40
- // Element promoted to the top layer through the Popover API: "popover"
41
- // matches it whether it is open or not, "popover-open" only when it is
42
- popover: '[popover]',
43
- popover-open: ':popover-open',
44
- pressed: '[aria-pressed="true"]',
45
- readonly: '[readonly]',
46
- selected: '[aria-selected="true"]',
47
- );
18
+ // The valid states and their selector suffixes live in `_states.scss`, shared
19
+ // with the theme walk.
48
20
 
49
21
  // ============================================
50
22
  // Main BEM mixin
@@ -448,7 +420,7 @@ $-state-suffixes: (
448
420
 
449
421
  @if meta.type-of(map.get($map, $key)) == 'map' {
450
422
  @each $state, $value in map.get($map, $key) {
451
- @if list.index($-valid-states, $state) {
423
+ @if list.index($valid-states, $state) {
452
424
  $selectors: ();
453
425
  $block-items: selector.parse($block);
454
426
  $alias-items: ();
@@ -546,13 +518,7 @@ $-state-suffixes: (
546
518
  // Helper to add state-specific selectors
547
519
  // Returns the updated selectors list
548
520
  @function -add-state-selectors($state, $block-item, $modifier, $alias, $selectors) {
549
- @if $state == 'current' {
550
- @return $selectors;
551
- }
552
-
553
- $suffixes: map.get($-state-suffixes, $state) or ':not([disabled]):#{$state}';
554
-
555
- @each $suffix in $suffixes {
521
+ @each $suffix in state-suffixes($state) {
556
522
  $selectors: list.join($selectors, '#{$modifier}#{$block-item}#{$suffix}');
557
523
 
558
524
  @if $alias {