@volverjs/style 0.1.29 → 0.1.30-beta.1
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.
- package/design-tokens.json +503 -7
- package/dist/base.css +1 -1
- package/dist/components/vv-alert.css +1 -1
- package/dist/components/vv-attachment-group.css +1 -0
- package/dist/components/vv-attachment.css +1 -0
- package/dist/components/vv-avatar.css +1 -1
- package/dist/components/vv-badge.css +1 -1
- package/dist/components/vv-breadcrumb.css +1 -1
- package/dist/components/vv-bubble-group.css +1 -0
- package/dist/components/vv-bubble.css +1 -0
- package/dist/components/vv-button.css +1 -1
- package/dist/components/vv-calendar.css +1 -0
- package/dist/components/vv-checkbox-group.css +1 -1
- package/dist/components/vv-checkbox.css +1 -1
- package/dist/components/vv-command.css +1 -0
- package/dist/components/vv-dialog.css +1 -1
- package/dist/components/vv-dropdown-action.css +1 -1
- package/dist/components/vv-dropdown-option.css +1 -1
- package/dist/components/vv-empty.css +1 -0
- package/dist/components/vv-field.css +1 -0
- package/dist/components/vv-input-file.css +1 -1
- package/dist/components/vv-input-otp.css +1 -0
- package/dist/components/vv-input-range.css +1 -1
- package/dist/components/vv-input-text.css +1 -1
- package/dist/components/vv-item-group.css +1 -0
- package/dist/components/vv-item.css +1 -0
- package/dist/components/vv-kbd-group.css +1 -0
- package/dist/components/vv-kbd.css +1 -0
- package/dist/components/vv-marker.css +1 -0
- package/dist/components/vv-message-scroller.css +1 -0
- package/dist/components/vv-message.css +1 -0
- package/dist/components/vv-nav.css +1 -1
- package/dist/components/vv-pagination.css +1 -0
- package/dist/components/vv-popover.css +1 -0
- package/dist/components/vv-progress.css +1 -1
- package/dist/components/vv-prose.css +1 -0
- package/dist/components/vv-questionnaire.css +1 -0
- package/dist/components/vv-radio-group.css +1 -1
- package/dist/components/vv-radio.css +1 -1
- package/dist/components/vv-select.css +1 -1
- package/dist/components/vv-separator.css +1 -0
- package/dist/components/vv-sidebar.css +1 -0
- package/dist/components/vv-spinner.css +1 -0
- package/dist/components/vv-table.css +1 -1
- package/dist/components/vv-text.css +1 -1
- package/dist/components/vv-textarea.css +1 -1
- package/dist/components.css +1 -1
- package/dist/export.css +1 -1
- package/dist/props/colors.css +1 -1
- package/dist/props/input.css +1 -1
- package/dist/props.css +1 -1
- package/dist/themes/dark/components.css +1 -1
- package/dist/themes/dark/props.css +1 -1
- package/dist/themes/dark/volver.css +1 -1
- package/dist/utilities/colors.css +1 -1
- package/dist/utilities/layout.css +1 -1
- package/dist/utilities.css +1 -1
- package/dist/volver.css +1 -1
- package/package.json +49 -1
- package/src/_config.scss +23 -0
- package/src/_preflight.scss +14 -2
- package/src/components/index.scss +28 -0
- package/src/components/vv-attachment-group.scss +17 -0
- package/src/components/vv-attachment.scss +17 -0
- package/src/components/vv-bubble-group.scss +17 -0
- package/src/components/vv-bubble.scss +17 -0
- package/src/components/vv-calendar.scss +25 -0
- package/src/components/vv-command.scss +17 -0
- package/src/components/vv-empty.scss +17 -0
- package/src/components/vv-field.scss +25 -0
- package/src/components/vv-input-otp.scss +17 -0
- package/src/components/vv-item-group.scss +17 -0
- package/src/components/vv-item.scss +39 -0
- package/src/components/vv-kbd-group.scss +17 -0
- package/src/components/vv-kbd.scss +17 -0
- package/src/components/vv-marker.scss +17 -0
- package/src/components/vv-message-scroller.scss +24 -0
- package/src/components/vv-message.scss +25 -0
- package/src/components/vv-pagination.scss +17 -0
- package/src/components/vv-popover.scss +17 -0
- package/src/components/vv-prose.scss +17 -0
- package/src/components/vv-questionnaire.scss +28 -0
- package/src/components/vv-separator.scss +17 -0
- package/src/components/vv-sidebar.scss +17 -0
- package/src/components/vv-spinner.scss +27 -0
- package/src/presets/_outlined-fields.scss +346 -0
- package/src/props/colors.scss +6 -0
- package/src/settings/_colors.scss +161 -61
- package/src/settings/_index.scss +26 -0
- package/src/settings/_input.scss +9 -0
- package/src/settings/components/_choice.scss +104 -0
- package/src/settings/components/_vv-alert.scss +37 -12
- package/src/settings/components/_vv-attachment-group.scss +10 -0
- package/src/settings/components/_vv-attachment.scss +135 -0
- package/src/settings/components/_vv-avatar.scss +17 -7
- package/src/settings/components/_vv-badge.scss +21 -13
- package/src/settings/components/_vv-breadcrumb.scss +1 -1
- package/src/settings/components/_vv-bubble-group.scss +31 -0
- package/src/settings/components/_vv-bubble.scss +69 -0
- package/src/settings/components/_vv-button.scss +48 -5
- package/src/settings/components/_vv-calendar.scss +102 -0
- package/src/settings/components/_vv-checkbox-group.scss +33 -2
- package/src/settings/components/_vv-checkbox.scss +7 -2
- package/src/settings/components/_vv-command.scss +99 -0
- package/src/settings/components/_vv-dialog.scss +129 -26
- package/src/settings/components/_vv-dropdown-action.scss +2 -1
- package/src/settings/components/_vv-dropdown-option.scss +3 -3
- package/src/settings/components/_vv-empty.scss +71 -0
- package/src/settings/components/_vv-field.scss +111 -0
- package/src/settings/components/_vv-input-file.scss +6 -6
- package/src/settings/components/_vv-input-otp.scss +55 -0
- package/src/settings/components/_vv-input-range.scss +2 -2
- package/src/settings/components/_vv-input-text.scss +6 -6
- package/src/settings/components/_vv-item-group.scss +68 -0
- package/src/settings/components/_vv-item.scss +201 -0
- package/src/settings/components/_vv-kbd-group.scss +9 -0
- package/src/settings/components/_vv-kbd.scss +20 -0
- package/src/settings/components/_vv-marker.scss +32 -0
- package/src/settings/components/_vv-message-scroller.scss +65 -0
- package/src/settings/components/_vv-message.scss +95 -0
- package/src/settings/components/_vv-nav.scss +7 -3
- package/src/settings/components/_vv-pagination.scss +78 -0
- package/src/settings/components/_vv-popover.scss +53 -0
- package/src/settings/components/_vv-progress.scss +4 -4
- package/src/settings/components/_vv-prose.scss +274 -0
- package/src/settings/components/_vv-questionnaire.scss +77 -0
- package/src/settings/components/_vv-radio.scss +6 -2
- package/src/settings/components/_vv-select.scss +5 -5
- package/src/settings/components/_vv-separator.scss +79 -0
- package/src/settings/components/_vv-sidebar.scss +91 -0
- package/src/settings/components/_vv-spinner.scss +26 -0
- package/src/settings/components/_vv-table.scss +63 -0
- package/src/settings/components/_vv-text.scss +2 -2
- package/src/settings/components/_vv-textarea.scss +6 -6
- package/src/themes/dark/components/_vv-checkbox.scss +27 -0
- package/src/themes/dark/components/_vv-radio.scss +27 -0
- package/src/themes/dark/components/index.scss +2 -0
- package/src/themes/dark/props/_colors.scss +6 -0
- package/src/themes/dark/settings/_colors.scss +97 -32
- package/src/themes/dark/settings/_index.scss +2 -0
- package/src/themes/dark/settings/components/_choice.scss +15 -0
- package/src/themes/dark/settings/components/_vv-alert.scss +6 -48
- package/src/themes/dark/settings/components/_vv-avatar.scss +1 -16
- package/src/themes/dark/settings/components/_vv-badge.scss +1 -32
- package/src/themes/dark/settings/components/_vv-button.scss +0 -8
- package/src/themes/dark/settings/components/_vv-checkbox.scss +7 -0
- package/src/themes/dark/settings/components/_vv-dropdown-action.scss +2 -3
- package/src/themes/dark/settings/components/_vv-dropdown-option.scss +2 -19
- package/src/themes/dark/settings/components/_vv-nav.scss +2 -15
- package/src/themes/dark/settings/components/_vv-radio.scss +7 -0
- package/src/tools/_functions.scss +430 -2
- package/src/tools/mixin-modules/_bem.scss +5 -39
- package/src/tools/mixin-modules/_states.scss +77 -0
- package/src/tools/mixin-modules/_theme.scss +135 -121
- package/src/utilities/colors.scss +49 -4
- package/src/utilities/layout.scss +25 -0
|
@@ -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
|
-
//
|
|
18
|
-
|
|
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(
|
|
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
|
-
@
|
|
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 {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
@use 'sass:map';
|
|
2
|
+
|
|
3
|
+
// ============================================
|
|
4
|
+
// States shared by the BEM walk and the theme walk
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
// Loaded by `_bem.scss` and `_theme.scss` and deliberately not forwarded by
|
|
8
|
+
// `_index.scss`: both walks read the same table, so a state added here reaches
|
|
9
|
+
// the light rules and the dark overrides at once, and neither can drift from
|
|
10
|
+
// the other again. The theme walk used to keep its own copy, which gave
|
|
11
|
+
// `disabled` no `[aria-disabled="true"]`, put `:not([disabled])` in front of
|
|
12
|
+
// `:checked` and dropped `popover` and `popover-open` altogether.
|
|
13
|
+
|
|
14
|
+
// List of valid CSS states
|
|
15
|
+
$valid-states: (
|
|
16
|
+
active, focus, hover, focus-within, target, visited, focus-visible,
|
|
17
|
+
disabled, checked, checked-within, focus-visible-within, multiple, selected,
|
|
18
|
+
pressed-within, readonly, empty, placeholder-shown, dirty, valid, invalid,
|
|
19
|
+
indeterminate, first-child, last-child, open, close, determinate, pressed,
|
|
20
|
+
current, current-date, current-page, popover, popover-open, sort-ascending,
|
|
21
|
+
sort-descending, sort-none
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Selector suffixes appended to the block, and to its alias when there is one,
|
|
25
|
+
// for the states that are not plain pseudo-classes. A state can need more than
|
|
26
|
+
// one suffix: "disabled" also matches the aria attribute. States missing here
|
|
27
|
+
// fall back to ":not([disabled]):<state>", while "current" adds no suffix at
|
|
28
|
+
// all and keeps only the BEM selectors built by the caller.
|
|
29
|
+
$state-suffixes: (
|
|
30
|
+
checked: ':checked',
|
|
31
|
+
checked-within: ':has(input:checked)',
|
|
32
|
+
close: ':not([open])',
|
|
33
|
+
// The page a link points to, the way a pagination, a breadcrumb or a
|
|
34
|
+
// navigation marks it. `current` stays a class, as it always was.
|
|
35
|
+
current-page: '[aria-current="page"]',
|
|
36
|
+
// Today in a calendar
|
|
37
|
+
current-date: '[aria-current="date"]',
|
|
38
|
+
dirty: ':not(:has(*:placeholder-shown))',
|
|
39
|
+
disabled: ('[disabled]', '[aria-disabled="true"]'),
|
|
40
|
+
focus-visible: ':not(:active):not([disabled]):not([tabindex="-1"]):not([aria-disabled="true"]):focus-visible',
|
|
41
|
+
// A keyboard focus on the input of the block, for a control whose native
|
|
42
|
+
// input is not painted (a segment, a card): `focus-within` would also ring
|
|
43
|
+
// it on a mouse click. Like "checked-within" it looks for the input only,
|
|
44
|
+
// so a button elsewhere in the block does not ring it.
|
|
45
|
+
focus-visible-within: ':has(input:focus-visible)',
|
|
46
|
+
multiple: '[multiple]',
|
|
47
|
+
open: '[open]',
|
|
48
|
+
// Element promoted to the top layer through the Popover API: "popover"
|
|
49
|
+
// matches it whether it is open or not, "popover-open" only when it is
|
|
50
|
+
popover: '[popover]',
|
|
51
|
+
popover-open: ':popover-open',
|
|
52
|
+
pressed: '[aria-pressed="true"]',
|
|
53
|
+
// A block whose toggle is a child button: `aria-pressed` is valid on a
|
|
54
|
+
// button only, so a row chosen through its entry carries it one level down.
|
|
55
|
+
// The child combinator keeps a pressed button nested deeper (in the actions
|
|
56
|
+
// of a row) from marking the whole block.
|
|
57
|
+
pressed-within: ':has(> [aria-pressed="true"])',
|
|
58
|
+
readonly: '[readonly]',
|
|
59
|
+
selected: '[aria-selected="true"]',
|
|
60
|
+
// A sortable column header, read from `aria-sort` on the `th`: `none`
|
|
61
|
+
// is sortable and not sorted yet
|
|
62
|
+
sort-ascending: '[aria-sort="ascending"]',
|
|
63
|
+
sort-descending: '[aria-sort="descending"]',
|
|
64
|
+
sort-none: '[aria-sort="none"]',
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// The suffixes of a state, as a list: its entry in `$state-suffixes`, the
|
|
68
|
+
// pseudo-class fallback, or nothing for "current"
|
|
69
|
+
// @param {String} $state - State name
|
|
70
|
+
// @return {List} Suffixes, comma separated
|
|
71
|
+
@function state-suffixes($state) {
|
|
72
|
+
@if $state == 'current' {
|
|
73
|
+
@return ();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@return map.get($state-suffixes, $state) or ':not([disabled]):#{$state}';
|
|
77
|
+
}
|