@nubisco/ui 5.7.0 → 5.8.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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubisco/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.0",
|
|
4
4
|
"description": "Vue 3 UI component library",
|
|
5
5
|
"packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/// Tokens derived from `--nb-c-primary`.
|
|
2
|
+
///
|
|
3
|
+
/// ## Why these cannot be plain aliases at `:root`
|
|
4
|
+
///
|
|
5
|
+
/// A custom property is substituted at computed-value time **on the element
|
|
6
|
+
/// that declares it**. `--nb-c-focus-ring: var(--nb-c-primary)` written at
|
|
7
|
+
/// `:root` therefore freezes to `:root`'s primary, and every descendant
|
|
8
|
+
/// inherits that frozen literal. Re-pointing `--nb-c-primary` further down the
|
|
9
|
+
/// tree, which is exactly what a named theme does, does not reach it.
|
|
10
|
+
///
|
|
11
|
+
/// Measured in Chrome against a document whose `:root` primary was the default
|
|
12
|
+
/// and whose themed scope set its own:
|
|
13
|
+
///
|
|
14
|
+
/// ```text
|
|
15
|
+
/// :root inside [data-nb-theme]
|
|
16
|
+
/// --nb-c-primary #5c35c4 #5b48c9 follows
|
|
17
|
+
/// --nb-c-focus-ring #5c35c4 #5c35c4 does not
|
|
18
|
+
/// --nb-c-primary-subtle #5c35c4… #5c35c4… does not
|
|
19
|
+
/// ```
|
|
20
|
+
///
|
|
21
|
+
/// So any scope that re-points the primary has to re-emit these alongside it.
|
|
22
|
+
/// They live here, as mixins, so the formulas exist once: `_theme.scss` calls
|
|
23
|
+
/// them for the default theme and `_theme-api.scss` calls them for every named
|
|
24
|
+
/// theme. Two copies would agree today and drift the first time one is edited.
|
|
25
|
+
///
|
|
26
|
+
/// @group @nubisco/theme-api
|
|
27
|
+
|
|
28
|
+
/// The pale wash of the accent, for an edge drawn ON a primary fill.
|
|
29
|
+
///
|
|
30
|
+
/// @param {Number} $weight - how much accent the mix carries. The light half
|
|
31
|
+
/// uses 39% and the dark half 50%, because the dark primary is already a
|
|
32
|
+
/// lighter step of the ramp and needs less white to land on the same wash.
|
|
33
|
+
@mixin subtle($weight) {
|
|
34
|
+
--nb-c-primary-subtle: color-mix(
|
|
35
|
+
in srgb,
|
|
36
|
+
var(--nb-c-primary) #{$weight},
|
|
37
|
+
var(--nb-c-white)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The focus ring, used by every interactive control via `:focus-visible`.
|
|
42
|
+
@mixin focus-ring {
|
|
43
|
+
--nb-c-focus-ring: var(--nb-c-primary);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The shell chrome that carries the accent.
|
|
47
|
+
///
|
|
48
|
+
/// The sidebar is a near-black tint of the accent rather than a raw palette
|
|
49
|
+
/// tint, so re-pointing the primary re-tints the rail with it. That is
|
|
50
|
+
/// deliberate: it is what makes the shell follow a product's identity instead
|
|
51
|
+
/// of the vendor's.
|
|
52
|
+
@mixin shell {
|
|
53
|
+
--nb-shell-sidebar-bg: color-mix(
|
|
54
|
+
in srgb,
|
|
55
|
+
var(--nb-c-primary) 20%,
|
|
56
|
+
var(--nb-c-black)
|
|
57
|
+
);
|
|
58
|
+
--nb-shell-sidebar-link-active-bg: color-mix(
|
|
59
|
+
in srgb,
|
|
60
|
+
var(--nb-c-primary) 25%,
|
|
61
|
+
transparent
|
|
62
|
+
);
|
|
63
|
+
// Light tint of --nb-c-primary: keeps the accent on-brand and rebrandable
|
|
64
|
+
// while staying legible (~6:1) on the dark rail, where --nb-c-primary itself
|
|
65
|
+
// is too dark (~2.5:1).
|
|
66
|
+
--nb-shell-sidebar-link-active-color: color-mix(
|
|
67
|
+
in srgb,
|
|
68
|
+
var(--nb-c-primary) 60%,
|
|
69
|
+
var(--nb-c-white)
|
|
70
|
+
);
|
|
71
|
+
// The grab-line on the inspector's resize handle. A token, not a literal
|
|
72
|
+
// brand hex: a white-label product should never have to name the vendor's
|
|
73
|
+
// ramp to restyle the frame.
|
|
74
|
+
--nb-shell-inspector-resize-color: var(--nb-c-primary);
|
|
75
|
+
}
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
@use 'sass:map';
|
|
53
53
|
@use 'sass:meta';
|
|
54
54
|
@use 'sass:string';
|
|
55
|
+
@use 'accent' as accent;
|
|
55
56
|
@use 'logic' as logic;
|
|
57
|
+
@use 'variables/categories' as categories;
|
|
56
58
|
|
|
57
59
|
/// The tints every palette colour is expanded into, matching the library's own
|
|
58
60
|
/// ramp exactly.
|
|
@@ -84,7 +86,23 @@ $a11y-roles:
|
|
|
84
86
|
/// are generated by one implementation rather than by two that agree today.
|
|
85
87
|
@mixin _palette($palette) {
|
|
86
88
|
@each $name, $base in $palette {
|
|
87
|
-
|
|
89
|
+
/*
|
|
90
|
+
* A palette entry is either one colour or a `(light, dark)` pair.
|
|
91
|
+
*
|
|
92
|
+
* With one colour, `make-shades` anchors it at its own lightness step and
|
|
93
|
+
* generates the rest. With two, it anchors BOTH at their own steps and
|
|
94
|
+
* interpolates between them, which is what an accent needs: a category's
|
|
95
|
+
* light and dark values are picked independently for contrast, and neither
|
|
96
|
+
* is a step of the other's ramp. Anchoring only the light one and hoping
|
|
97
|
+
* the dark half lands nearby does not hold, measured: the four category
|
|
98
|
+
* pairs anchor at 700/400, 500/200, 700/400 and 500/300.
|
|
99
|
+
*/
|
|
100
|
+
$shades: null;
|
|
101
|
+
@if meta.type-of($base) == list {
|
|
102
|
+
$shades: logic.make-shades(list.nth($base, 1), list.nth($base, 2));
|
|
103
|
+
} @else {
|
|
104
|
+
$shades: logic.make-shades($base);
|
|
105
|
+
}
|
|
88
106
|
@each $tint in $tints {
|
|
89
107
|
$value: map.get($shades, $tint);
|
|
90
108
|
--nb-c-#{$name}-#{$tint}: #{$value};
|
|
@@ -191,3 +209,103 @@ $a11y-roles:
|
|
|
191
209
|
}
|
|
192
210
|
@return true;
|
|
193
211
|
}
|
|
212
|
+
|
|
213
|
+
/// The ramp steps a theme can point at, as emitted by `_palette`.
|
|
214
|
+
$_min-step: 100;
|
|
215
|
+
$_max-step: 900;
|
|
216
|
+
|
|
217
|
+
/// Keeps a derived step inside the range the ramp actually emits.
|
|
218
|
+
@function _step($level, $offset) {
|
|
219
|
+
$v: $level + $offset;
|
|
220
|
+
@if $v < $_min-step {
|
|
221
|
+
@return $_min-step;
|
|
222
|
+
}
|
|
223
|
+
@if $v > $_max-step {
|
|
224
|
+
@return $_max-step;
|
|
225
|
+
}
|
|
226
|
+
@return $v;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Themes an app from its audience category.
|
|
230
|
+
///
|
|
231
|
+
/// This is the one call an app makes. It takes the category the product
|
|
232
|
+
/// belongs to, resolves that to an accent pair, and wires it through the same
|
|
233
|
+
/// `theme()` machinery a hand-written theme uses.
|
|
234
|
+
///
|
|
235
|
+
/// ```scss
|
|
236
|
+
/// @use '@nubisco/ui/styles/theme-api' as nb;
|
|
237
|
+
///
|
|
238
|
+
/// @include nb.app-theme('acta', $category: 'engineers');
|
|
239
|
+
/// ```
|
|
240
|
+
///
|
|
241
|
+
/// An app with an identity of its own overrides the accent and keeps the
|
|
242
|
+
/// category, which still drives anything that groups products by audience:
|
|
243
|
+
///
|
|
244
|
+
/// ```scss
|
|
245
|
+
/// @include nb.app-theme('doodloop',
|
|
246
|
+
/// $category: 'creatives',
|
|
247
|
+
/// $accent: (#0b7285, #41d6e0)
|
|
248
|
+
/// );
|
|
249
|
+
/// ```
|
|
250
|
+
///
|
|
251
|
+
/// ## What it emits
|
|
252
|
+
///
|
|
253
|
+
/// The accent becomes a full ramp named `accent`, anchored at both ends, and
|
|
254
|
+
/// `primary` / `primary-hover` / `primary-active` point at steps of it: the
|
|
255
|
+
/// anchor step, then two steps away from the surface, darker in light mode and
|
|
256
|
+
/// lighter in dark. Each carries its `-a11y` foreground. Everything derived
|
|
257
|
+
/// from the primary is re-emitted in the same scope, because a `var()` alias
|
|
258
|
+
/// written at `:root` would not follow (see `_accent.scss`).
|
|
259
|
+
///
|
|
260
|
+
/// `--nb-c-category` carries the audience colour separately from the accent.
|
|
261
|
+
/// For most apps they are the same value; for one that overrode its accent
|
|
262
|
+
/// they are not, and a surface that groups products by audience wants the
|
|
263
|
+
/// category, not the app's own colour.
|
|
264
|
+
///
|
|
265
|
+
/// @param {String} $id - the app's identifier, chosen by the app. The library
|
|
266
|
+
/// keeps no registry of these: it never needs to know which products exist.
|
|
267
|
+
/// @param {String} $category - one of `creatives`, `engineers`, `home`, `fun`
|
|
268
|
+
/// @param {List} $accent - an optional `(light, dark)` override
|
|
269
|
+
@mixin app-theme($id, $category, $accent: null) {
|
|
270
|
+
$category-pair: categories.category($category);
|
|
271
|
+
$pair: $category-pair;
|
|
272
|
+
@if $accent != null {
|
|
273
|
+
$pair: categories.accent-pair($accent);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
$light-hex: list.nth($pair, 1);
|
|
277
|
+
$dark-hex: list.nth($pair, 2);
|
|
278
|
+
$light-level: logic.color-level($light-hex);
|
|
279
|
+
$dark-level: logic.color-level($dark-hex);
|
|
280
|
+
|
|
281
|
+
@include theme(
|
|
282
|
+
$id,
|
|
283
|
+
$palette: (accent: $pair),
|
|
284
|
+
$light: (
|
|
285
|
+
primary: 'accent-#{$light-level}',
|
|
286
|
+
primary-hover: 'accent-#{_step($light-level, 50)}',
|
|
287
|
+
primary-active: 'accent-#{_step($light-level, 100)}',
|
|
288
|
+
category: list.nth($category-pair, 1)
|
|
289
|
+
),
|
|
290
|
+
$dark: (
|
|
291
|
+
primary: 'accent-#{$dark-level}',
|
|
292
|
+
primary-hover: 'accent-#{_step($dark-level, -50)}',
|
|
293
|
+
primary-active: 'accent-#{_step($dark-level, -100)}',
|
|
294
|
+
category: list.nth($category-pair, 2)
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// The primary-derived tokens, re-emitted in the theme's own scopes. Without
|
|
299
|
+
// these the app gets its accent on buttons and the vendor's purple on focus
|
|
300
|
+
// rings and the whole shell.
|
|
301
|
+
[data-nb-theme='#{$id}'] {
|
|
302
|
+
@include accent.subtle(39%);
|
|
303
|
+
@include accent.focus-ring;
|
|
304
|
+
@include accent.shell;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.dark[data-nb-theme='#{$id}'],
|
|
308
|
+
.dark [data-nb-theme='#{$id}'] {
|
|
309
|
+
@include accent.subtle(50%);
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/styles/_theme.scss
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
@use 'sass:color';
|
|
2
2
|
@use 'sass:map';
|
|
3
|
+
@use 'accent';
|
|
3
4
|
@use 'config';
|
|
4
5
|
@use 'variables/layout';
|
|
5
6
|
@use 'variables/colors';
|
|
@@ -158,11 +159,7 @@
|
|
|
158
159
|
// hover border visibly moves. The dark theme repeats the mix at 50%
|
|
159
160
|
// because its --nb-c-primary is already a lighter step of the same ramp
|
|
160
161
|
// and needs less white to land on the same wash (dE2000 0.00 there).
|
|
161
|
-
|
|
162
|
-
in srgb,
|
|
163
|
-
var(--nb-c-primary) 39%,
|
|
164
|
-
var(--nb-c-white)
|
|
165
|
-
);
|
|
162
|
+
@include accent.subtle(39%);
|
|
166
163
|
|
|
167
164
|
--nb-c-secondary: var(--nb-c-plain-black-700);
|
|
168
165
|
--nb-c-secondary-a11y: var(--nb-c-plain-black-700-a11y);
|
|
@@ -375,7 +372,7 @@
|
|
|
375
372
|
--nb-c-component-discrete-text: var(--nb-c-nouveau-gray-700);
|
|
376
373
|
|
|
377
374
|
// Focus ring — used by all interactive controls via :focus-visible
|
|
378
|
-
|
|
375
|
+
@include accent.focus-ring;
|
|
379
376
|
|
|
380
377
|
// ── Scrim / overlay ───────────────────────────────────────────────────
|
|
381
378
|
// The dim laid over the page by anything that takes over the viewport:
|
|
@@ -588,11 +585,7 @@
|
|
|
588
585
|
// Dark chrome derived from the brand: a near-black tint of --nb-c-primary so
|
|
589
586
|
// rebranding the primary colour re-tints the sidebar too (semantic, not a
|
|
590
587
|
// raw palette tint).
|
|
591
|
-
|
|
592
|
-
in srgb,
|
|
593
|
-
var(--nb-c-primary) 20%,
|
|
594
|
-
var(--nb-c-black)
|
|
595
|
-
);
|
|
588
|
+
@include accent.shell;
|
|
596
589
|
--nb-shell-sidebar-width: calc(var(--nb-base-unit) * 7); // 56px
|
|
597
590
|
// The verbose variant's rail width. A token rather than a literal inside
|
|
598
591
|
// Shell.vue's variant rule, so a product with long navigation labels can
|
|
@@ -613,19 +606,6 @@
|
|
|
613
606
|
var(--nb-c-white) 90%,
|
|
614
607
|
transparent
|
|
615
608
|
);
|
|
616
|
-
--nb-shell-sidebar-link-active-bg: color-mix(
|
|
617
|
-
in srgb,
|
|
618
|
-
var(--nb-c-primary) 25%,
|
|
619
|
-
transparent
|
|
620
|
-
);
|
|
621
|
-
// Light tint of --nb-c-primary: keeps the accent on-brand and rebrandable
|
|
622
|
-
// while staying legible (~6:1) on the dark rail, where --nb-c-primary itself
|
|
623
|
-
// is too dark (~2.5:1).
|
|
624
|
-
--nb-shell-sidebar-link-active-color: color-mix(
|
|
625
|
-
in srgb,
|
|
626
|
-
var(--nb-c-primary) 60%,
|
|
627
|
-
var(--nb-c-white)
|
|
628
|
-
);
|
|
629
609
|
--nb-shell-topbar-height: calc(var(--nb-base-unit) * 6.25); // 50px
|
|
630
610
|
|
|
631
611
|
// Shell chrome. Every rule that paints the frame reads one of these, so a
|
|
@@ -672,10 +652,6 @@
|
|
|
672
652
|
--nb-shell-inspector-max-width: max(calc(var(--nb-base-unit) * 36), 50vw);
|
|
673
653
|
--nb-shell-inspector-border: 1px solid var(--nb-c-border);
|
|
674
654
|
--nb-shell-inspector-bg: var(--nb-c-surface);
|
|
675
|
-
// The grab-line on the inspector's resize handle. A token, not a literal
|
|
676
|
-
// brand hex: a white-label product should never have to name the vendor's
|
|
677
|
-
// ramp to restyle the frame.
|
|
678
|
-
--nb-shell-inspector-resize-color: var(--nb-c-primary);
|
|
679
655
|
// The dimmer behind the navigation drawer at collapsed widths. Black at
|
|
680
656
|
// 45% reads on both themes because it darkens whatever is under it rather
|
|
681
657
|
// than painting a colour of its own.
|
|
@@ -707,11 +683,7 @@
|
|
|
707
683
|
--nb-c-primary: #{colors.get-color('grape-hyacinth', 400)};
|
|
708
684
|
// Same wash, re-mixed: the dark --nb-c-primary is a 400, so it takes less
|
|
709
685
|
// white than the light 500 does to reach the tint this token used to alias.
|
|
710
|
-
|
|
711
|
-
in srgb,
|
|
712
|
-
var(--nb-c-primary) 50%,
|
|
713
|
-
var(--nb-c-white)
|
|
714
|
-
);
|
|
686
|
+
@include accent.subtle(50%);
|
|
715
687
|
--nb-c-primary-a11y: #{colors.color-contrast(
|
|
716
688
|
colors.get-color('grape-hyacinth', 400)
|
|
717
689
|
)};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
@use 'sass:list';
|
|
2
|
+
@use 'sass:map';
|
|
3
|
+
@use 'sass:meta';
|
|
4
|
+
@use '../logic/a11y' as a11y;
|
|
5
|
+
@use 'layers';
|
|
6
|
+
|
|
7
|
+
/// The audience categories, and the accent pair each one resolves to.
|
|
8
|
+
///
|
|
9
|
+
/// Nubisco groups its products by audience: creatives, engineers, home, fun.
|
|
10
|
+
/// These are the four accents that grouping resolves to, each a
|
|
11
|
+
/// `(light, dark)` pair.
|
|
12
|
+
///
|
|
13
|
+
/// ## What is deliberately not here
|
|
14
|
+
///
|
|
15
|
+
/// There is no product list. The library holds the four values and nothing
|
|
16
|
+
/// else: it does not know which product belongs to which category, and it must
|
|
17
|
+
/// not. A registry of product ids here would mean cutting a library release
|
|
18
|
+
/// every time a product is added or renamed, and it would invert the
|
|
19
|
+
/// dependency, making the design system know about the things that consume it.
|
|
20
|
+
/// An app names its own category when it themes itself, so a new product needs
|
|
21
|
+
/// no change to this file. The product-to-category map is a portfolio fact and
|
|
22
|
+
/// lives in the workspace standards.
|
|
23
|
+
///
|
|
24
|
+
/// ## Why these exact values
|
|
25
|
+
///
|
|
26
|
+
/// Every value clears WCAG AA (4.5:1) against all four surface layers of the
|
|
27
|
+
/// mode it belongs to. That is a stricter bar than measuring against pure
|
|
28
|
+
/// white and pure black, and it is the one that matters: the library never
|
|
29
|
+
/// renders a pure-white surface. Light accents are measured against the
|
|
30
|
+
/// darkest light layer (`layer-2`, `#e7e8e9`) and dark accents against the
|
|
31
|
+
/// lightest dark layer (`layer-3`, `#3d3e3f`), because those are the worst
|
|
32
|
+
/// cases. `tests/categoryContrast.test.ts` measures the compiled values and
|
|
33
|
+
/// fails if an edit drops one below the bar.
|
|
34
|
+
///
|
|
35
|
+
/// Hover and active are the adjacent steps of the same ramp, darker in light
|
|
36
|
+
/// mode and lighter in dark, so both are strictly further from the surface
|
|
37
|
+
/// than the base. The base is therefore the worst case, and testing it is
|
|
38
|
+
/// enough.
|
|
39
|
+
///
|
|
40
|
+
/// @type Map
|
|
41
|
+
/// @access public
|
|
42
|
+
/// @group @nubisco/colors
|
|
43
|
+
$categories: (
|
|
44
|
+
creatives: (
|
|
45
|
+
#846201,
|
|
46
|
+
#e8c15a,
|
|
47
|
+
),
|
|
48
|
+
engineers: (
|
|
49
|
+
#5b48c9,
|
|
50
|
+
#a99dff,
|
|
51
|
+
),
|
|
52
|
+
home: (
|
|
53
|
+
#086f9e,
|
|
54
|
+
#5ec1e8,
|
|
55
|
+
),
|
|
56
|
+
fun: (
|
|
57
|
+
#b83266,
|
|
58
|
+
#f686ae,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
/// The `(light, dark)` accent pair for a category.
|
|
63
|
+
///
|
|
64
|
+
/// An unknown name is a build error rather than a silent fallback: a typo that
|
|
65
|
+
/// quietly resolved to the default accent would ship a product in the wrong
|
|
66
|
+
/// colour and nothing would say so.
|
|
67
|
+
///
|
|
68
|
+
/// @param {String} $name - a key of `$categories`
|
|
69
|
+
/// @return {List} the `(light, dark)` pair
|
|
70
|
+
/// @access public
|
|
71
|
+
/// @group @nubisco/colors
|
|
72
|
+
@function category($name) {
|
|
73
|
+
@if not map.has-key($categories, $name) {
|
|
74
|
+
@error "Unknown category #{$name}. Expected one of: #{map.keys($categories)}.";
|
|
75
|
+
}
|
|
76
|
+
@return map.get($categories, $name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Validates a `(light, dark)` accent pair supplied by an app.
|
|
80
|
+
///
|
|
81
|
+
/// An app with an established identity of its own passes its own pair rather
|
|
82
|
+
/// than taking its category's. This checks the shape so a malformed value
|
|
83
|
+
/// fails at build time instead of emitting nonsense custom properties.
|
|
84
|
+
///
|
|
85
|
+
/// @param {List} $accent - a two-item list of colours
|
|
86
|
+
/// @return {List} the same pair, once validated
|
|
87
|
+
/// @access public
|
|
88
|
+
/// @group @nubisco/colors
|
|
89
|
+
@function accent-pair($accent) {
|
|
90
|
+
@if meta.type-of($accent) != list or list.length($accent) != 2 {
|
|
91
|
+
@error "An accent must be a (light, dark) pair of colours, got #{$accent}.";
|
|
92
|
+
}
|
|
93
|
+
@each $value in $accent {
|
|
94
|
+
@if meta.type-of($value) != color {
|
|
95
|
+
@error "An accent pair must hold colours, got #{$value}.";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
@return $accent;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// The surfaces an accent is measured against, per mode.
|
|
102
|
+
///
|
|
103
|
+
/// These are the library's own layers, resolved from
|
|
104
|
+
/// `variables/_layers.scss`, not pure white and pure black. That distinction
|
|
105
|
+
/// is the point: an accent verified against a surface the product never
|
|
106
|
+
/// renders has not been verified. The worst case is the darkest light layer
|
|
107
|
+
/// and the lightest dark layer, and both are in here.
|
|
108
|
+
///
|
|
109
|
+
/// @type Map
|
|
110
|
+
/// @access private
|
|
111
|
+
/// @group @nubisco/colors
|
|
112
|
+
/// Read from the layer ramps rather than restated, so a change to the surface
|
|
113
|
+
/// system is measured against automatically instead of silently invalidating
|
|
114
|
+
/// numbers copied out of it.
|
|
115
|
+
$_grounds: (
|
|
116
|
+
light: (
|
|
117
|
+
layers.level(layers.$light, surface, 0),
|
|
118
|
+
layers.level(layers.$light, surface, 1),
|
|
119
|
+
layers.level(layers.$light, surface, 2),
|
|
120
|
+
layers.level(layers.$light, surface, 3),
|
|
121
|
+
),
|
|
122
|
+
dark: (
|
|
123
|
+
layers.level(layers.$dark, surface, 0),
|
|
124
|
+
layers.level(layers.$dark, surface, 1),
|
|
125
|
+
layers.level(layers.$dark, surface, 2),
|
|
126
|
+
layers.level(layers.$dark, surface, 3),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
/// Fails the build if an accent pair does not clear WCAG AA on every surface.
|
|
131
|
+
///
|
|
132
|
+
/// The library measures the accents it ships, in
|
|
133
|
+
/// `tests/categoryContrast.test.ts`. It cannot measure an accent an app
|
|
134
|
+
/// supplies, because it never sees it. An app that overrides its accent calls
|
|
135
|
+
/// this so its own build holds it to the same bar.
|
|
136
|
+
///
|
|
137
|
+
/// ```scss
|
|
138
|
+
/// @debug nb.assert-accent-contrast((#0b7285, #41d6e0));
|
|
139
|
+
/// ```
|
|
140
|
+
///
|
|
141
|
+
/// @param {List} $accent - a `(light, dark)` pair
|
|
142
|
+
/// @param {Number} $minimum - the ratio to clear, 4.5 by default
|
|
143
|
+
/// @return {String} a summary, so the call reads as an assertion that returns
|
|
144
|
+
/// @access public
|
|
145
|
+
/// @group @nubisco/colors
|
|
146
|
+
@function assert-accent-contrast($accent, $minimum: 4.5) {
|
|
147
|
+
$pair: accent-pair($accent);
|
|
148
|
+
$worst: 21;
|
|
149
|
+
|
|
150
|
+
@each $mode, $grounds in $_grounds {
|
|
151
|
+
$value: list.nth($pair, 2);
|
|
152
|
+
$direction: 'lighter';
|
|
153
|
+
@if $mode == light {
|
|
154
|
+
$value: list.nth($pair, 1);
|
|
155
|
+
$direction: 'darker';
|
|
156
|
+
}
|
|
157
|
+
@each $ground in $grounds {
|
|
158
|
+
$ratio: a11y.contrast-ratio($value, $ground);
|
|
159
|
+
@if $ratio < $minimum {
|
|
160
|
+
@error "Accent #{$value} is #{$ratio}:1 on #{$mode} surface #{$ground}, below #{$minimum}:1. Pick a #{$direction} value.";
|
|
161
|
+
}
|
|
162
|
+
@if $ratio < $worst {
|
|
163
|
+
$worst: $ratio;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@return 'accent clears #{$minimum}:1 on every surface (worst #{$worst}:1)';
|
|
169
|
+
}
|