@gitlab/ui 72.11.1 → 72.12.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [72.12.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v72.12.0...v72.12.1) (2024-01-24)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Building of `dist/` folder ([6592727](https://gitlab.com/gitlab-org/gitlab-ui/commit/6592727b7b2de66a5d40111d37fd331ca65e2411))
7
+
8
+ # [72.12.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v72.11.1...v72.12.0) (2024-01-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * **DesignTokens:** create GlColorContrast component ([17dfa56](https://gitlab.com/gitlab-org/gitlab-ui/commit/17dfa56d0839681a741195910a86debeaede491b))
14
+
1
15
  ## [72.11.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v72.11.0...v72.11.1) (2024-01-23)
2
16
 
3
17
 
@@ -1,29 +1,16 @@
1
- import { colorFromBackground, relativeLuminance, rgbFromHex } from '../utils/utils';
2
1
  import { WHITE, GRAY_950 } from '../../dist/tokens/js/tokens';
2
+ import { colorFromBackground } from '../utils/utils';
3
+ import GlColorContrast from '../internal/color_contrast/color_contrast';
3
4
 
4
- const CONTRAST_LEVELS = [{
5
- grade: 'F',
6
- min: 0,
7
- max: 3
8
- }, {
9
- grade: 'AA+',
10
- min: 3,
11
- max: 4.5
12
- }, {
13
- grade: 'AA',
14
- min: 4.5,
15
- max: 7
16
- }, {
17
- grade: 'AAA',
18
- min: 7,
19
- max: 22
20
- }];
5
+ const components = {
6
+ GlColorContrast
7
+ };
21
8
  const methods = {
22
9
  isAlpha(value) {
23
10
  return value.startsWith('rgba(');
24
11
  },
25
- getTokenName(name, key) {
26
- return [name, key].filter(Boolean).join('.');
12
+ getTokenName(token) {
13
+ return token.path.filter(Boolean).join('.');
27
14
  },
28
15
  getTextColorClass(value) {
29
16
  if (this.isAlpha(value)) return '';
@@ -32,78 +19,36 @@ const methods = {
32
19
  'gl-text-gray-950': textColorVariant === 'dark',
33
20
  'gl-text-white': textColorVariant === 'light'
34
21
  };
35
- },
36
- getColorContrast() {
37
- let foreground = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'light';
38
- let background = arguments.length > 1 ? arguments[1] : undefined;
39
- const foregroundColor = {
40
- light: WHITE,
41
- dark: GRAY_950
42
- };
43
- // Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
44
- const foregroundLuminance = relativeLuminance(rgbFromHex(foregroundColor[foreground])) + 0.05;
45
- const backgroundLuminance = relativeLuminance(rgbFromHex(background)) + 0.05;
46
- let score = foregroundLuminance / backgroundLuminance;
47
- if (backgroundLuminance > foregroundLuminance) {
48
- score = 1 / score;
49
- }
50
- const level = CONTRAST_LEVELS.find(_ref => {
51
- let {
52
- min,
53
- max
54
- } = _ref;
55
- return score >= min && score < max;
56
- });
57
- return {
58
- score: (Math.round(score * 10) / 10).toFixed(1),
59
- level
60
- };
61
- },
62
- getColorContrastClass(foreground, background) {
63
- const {
64
- grade
65
- } = this.getColorContrast(foreground, background).level;
66
- const isFail = grade === 'F';
67
- const classes = {
68
- light: isFail ? 'gl-inset-border-1-red-500 gl-text-red-500' : 'gl-text-gray-950',
69
- dark: isFail ? 'gl-inset-border-1-red-300 gl-text-red-300' : 'gl-text-white'
70
- };
71
- return classes[foreground];
72
22
  }
73
23
  };
74
- const template = `
24
+ const template = function () {
25
+ let lightBackground = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : WHITE;
26
+ let darkBackground = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : GRAY_950;
27
+ return `
75
28
  <ul
76
29
  class="gl-list-style-none gl-m-0 gl-p-0"
77
30
  >
78
31
  <li
79
- v-for="(token, key) in tokens"
80
- :key="key"
32
+ v-for="token in tokens"
33
+ :key="token.name"
81
34
  class="gl-display-flex gl-flex-wrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-p-3"
82
- :class="getTextColorClass(token.$value)"
83
- :style="{ backgroundColor: token.$value }"
35
+ :class="getTextColorClass(token.value)"
36
+ :style="{ backgroundColor: token.value }"
84
37
  >
85
- <code class="gl-reset-color">{{ getTokenName(name, key) }}</code>
38
+ <code class="gl-reset-color">{{ getTokenName(token) }}</code>
86
39
  <div class="gl-display-flex gl-align-items-center gl-gap-3">
87
- <code class="gl-reset-color">{{ token.$value }}</code>
88
- <code
89
- v-if="!isAlpha(token.$value)"
90
- class="gl-w-10 gl-text-center gl-rounded-base gl-font-xs gl-p-2 gl-bg-gray-950"
91
- :class="getColorContrastClass('dark', token.$value)"
92
- >
93
- {{ getColorContrast('dark', token.$value).level.grade }}
94
- {{ getColorContrast('dark', token.$value).score }}
95
- </code>
96
- <code
97
- v-if="!isAlpha(token.$value)"
98
- class="gl-w-10 gl-text-center gl-rounded-base gl-font-xs gl-p-2 gl-bg-white"
99
- :class="getColorContrastClass('light', token.$value)"
100
- >
101
- {{ getColorContrast('light', token.$value).level.grade }}
102
- {{ getColorContrast('light', token.$value).score }}
103
- </code>
40
+ <code class="gl-reset-color">{{ token.value }}</code>
41
+ <gl-color-contrast v-if="!isAlpha(token.value)" :foreground="token.value" background="${darkBackground}" />
42
+ <gl-color-contrast v-if="!isAlpha(token.value)" :foreground="token.value" background="${lightBackground}" />
104
43
  </div>
105
44
  </li>
106
45
  </ul>
107
46
  `;
47
+ };
48
+ const colorTokenStoryOptions = {
49
+ components,
50
+ methods,
51
+ template: template()
52
+ };
108
53
 
109
- export { methods, template };
54
+ export { colorTokenStoryOptions, components, methods, template };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 23 Jan 2024 17:14:57 GMT
3
+ * Generated on Wed, 24 Jan 2024 12:28:35 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 23 Jan 2024 17:14:57 GMT
3
+ * Generated on Wed, 24 Jan 2024 12:28:35 GMT
4
4
  */
5
5
 
6
6
  :root.gl-dark {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 23 Jan 2024 17:14:58 GMT
3
+ * Generated on Wed, 24 Jan 2024 12:28:35 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#133a03";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 23 Jan 2024 17:14:57 GMT
3
+ * Generated on Wed, 24 Jan 2024 12:28:35 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#ddfab7";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Tue, 23 Jan 2024 17:14:58 GMT
3
+ // Generated on Wed, 24 Jan 2024 12:28:35 GMT
4
4
 
5
5
  $red-950: #fff4f3;
6
6
  $red-900: #fcf1ef;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Tue, 23 Jan 2024 17:14:57 GMT
3
+ // Generated on Wed, 24 Jan 2024 12:28:35 GMT
4
4
 
5
5
  $gl-line-height-52: 3.25rem;
6
6
  $gl-line-height-44: 2.75rem;
@@ -7,6 +7,24 @@ function appendDefaultOption(options) {
7
7
  };
8
8
  }
9
9
  const COMMA = ',';
10
+ const CONTRAST_LEVELS = [{
11
+ grade: 'F',
12
+ min: 0,
13
+ max: 3
14
+ }, {
15
+ grade: 'AA+',
16
+ min: 3,
17
+ max: 4.5
18
+ }, {
19
+ grade: 'AA',
20
+ min: 4.5,
21
+ max: 7
22
+ }, {
23
+ grade: 'AAA',
24
+ min: 7,
25
+ max: 22
26
+ }];
27
+ const HEX_REGEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
10
28
  const LEFT_MOUSE_BUTTON = 0;
11
29
  const glThemes = ['indigo', 'blue', 'light-blue', 'green', 'red', 'light-red'];
12
30
  const variantOptions = {
@@ -267,4 +285,4 @@ const loadingIconVariants = {
267
285
  dots: 'dots'
268
286
  };
269
287
 
270
- export { COMMA, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeIconSizeOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, buttonCategoryOptions, buttonSizeOptions, buttonVariantOptions, colorThemes, columnOptions, datepickerWidthOptionsMap, defaultDateFormat, drawerVariants, dropdownAllowedAutoPlacements, dropdownPlacements, dropdownVariantOptions, focusableTags, formInputWidths, formStateOptions, glThemes, iconSizeOptions, keyboard, labelColorOptions, labelSizeOptions, loadingIconSizes, loadingIconVariants, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, variantOptions, variantOptionsWithNoDefault, viewModeOptions };
288
+ export { COMMA, CONTRAST_LEVELS, HEX_REGEX, LEFT_MOUSE_BUTTON, alertVariantIconMap, alertVariantOptions, alignOptions, avatarShapeOptions, avatarSizeOptions, avatarsInlineSizeOptions, badgeForButtonOptions, badgeIconSizeOptions, badgeSizeOptions, badgeVariantOptions, bannerVariants, buttonCategoryOptions, buttonSizeOptions, buttonVariantOptions, colorThemes, columnOptions, datepickerWidthOptionsMap, defaultDateFormat, drawerVariants, dropdownAllowedAutoPlacements, dropdownPlacements, dropdownVariantOptions, focusableTags, formInputWidths, formStateOptions, glThemes, iconSizeOptions, keyboard, labelColorOptions, labelSizeOptions, loadingIconSizes, loadingIconVariants, maxZIndex, modalButtonDefaults, modalSizeOptions, popoverPlacements, resizeDebounceTime, tabsButtonDefaults, targetOptions, toggleLabelPosition, tokenVariants, tooltipActionEvents, tooltipDelay, tooltipPlacements, triggerVariantOptions, truncateOptions, variantCssColorMap, variantOptions, variantOptionsWithNoDefault, viewModeOptions };
@@ -1,5 +1,5 @@
1
1
  import { isVisible } from 'bootstrap-vue/esm/utils/dom';
2
- import { COMMA, labelColorOptions, focusableTags } from './constants';
2
+ import { COMMA, labelColorOptions, CONTRAST_LEVELS, focusableTags } from './constants';
3
3
 
4
4
  function debounceByAnimationFrame(fn) {
5
5
  let requestId;
@@ -78,6 +78,26 @@ function colorFromBackground(backgroundColor) {
78
78
  // as this will solve weird color combinations in the mid tones
79
79
  return contrastLight >= contrastRatio || contrastLight > contrastDark ? labelColorOptions.light : labelColorOptions.dark;
80
80
  }
81
+ function getColorContrast(foreground, background) {
82
+ // Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
83
+ const backgroundLuminance = relativeLuminance(rgbFromHex(background)) + 0.05;
84
+ const foregroundLuminance = relativeLuminance(rgbFromHex(foreground)) + 0.05;
85
+ let score = backgroundLuminance / foregroundLuminance;
86
+ if (foregroundLuminance > backgroundLuminance) {
87
+ score = 1 / score;
88
+ }
89
+ const level = CONTRAST_LEVELS.find(_ref => {
90
+ let {
91
+ min,
92
+ max
93
+ } = _ref;
94
+ return score >= min && score < max;
95
+ });
96
+ return {
97
+ score: (Math.round(score * 10) / 10).toFixed(1),
98
+ level
99
+ };
100
+ }
81
101
  function uid() {
82
102
  return Math.random().toString(36).substring(2);
83
103
  }
@@ -174,4 +194,4 @@ function filterVisible(els) {
174
194
  return (els || []).filter(el => isVisible(el));
175
195
  }
176
196
 
177
- export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };
197
+ export { colorFromBackground, debounceByAnimationFrame, filterVisible, focusFirstFocusableElement, getColorContrast, hexToRgba, isDev, isElementFocusable, isElementTabbable, logWarning, relativeLuminance, rgbFromHex, rgbFromString, stopEvent, throttle, toSrgb, uid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "72.11.1",
3
+ "version": "72.12.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,8 @@
1
+ ## Usage
2
+
3
+ `GlColorContrast` is an **internal** component to display color contrast
4
+ scores and levels for design token stories.
5
+
6
+ `GlColorContrast` accepts `foreground` and `background` color props to
7
+ calculate a contrast score (e.g. `4.5`) and level (e.g. `AA`) consistent
8
+ with [WCAG 2.1 1.4.3: Contrast (Minimum) level](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html).
@@ -0,0 +1,34 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import ColorContrast from './color_contrast.vue';
3
+
4
+ describe('color contrast component', () => {
5
+ let wrapper;
6
+
7
+ const createComponent = (props) => {
8
+ wrapper = shallowMount(ColorContrast, {
9
+ propsData: {
10
+ ...props,
11
+ },
12
+ });
13
+ };
14
+
15
+ it('renders AAA level and score', () => {
16
+ createComponent({ foreground: '#000', background: '#fff' });
17
+ expect(wrapper.text()).toBe('AAA 21.0');
18
+ });
19
+
20
+ it('renders AA level and score', () => {
21
+ createComponent({ foreground: '#666', background: '#fff' });
22
+ expect(wrapper.text()).toBe('AA 5.7');
23
+ });
24
+
25
+ it('renders AA+ level and score', () => {
26
+ createComponent({ foreground: '#888', background: '#fff' });
27
+ expect(wrapper.text()).toBe('AA+ 3.5');
28
+ });
29
+
30
+ it('renders F level and score', () => {
31
+ createComponent({ foreground: '#999', background: '#fff' });
32
+ expect(wrapper.text()).toBe('F 2.8');
33
+ });
34
+ });
@@ -0,0 +1,41 @@
1
+ import GlColorContrast from './color_contrast.vue';
2
+ import readme from './color_contrast.md';
3
+
4
+ const components = { GlColorContrast };
5
+
6
+ const generateProps = ({ foreground = '#ffffff', background = '#1f75cb' } = {}) => ({
7
+ foreground,
8
+ background,
9
+ });
10
+
11
+ export const Default = (args, { argTypes }) => ({
12
+ components,
13
+ props: Object.keys(argTypes),
14
+ template: `
15
+ <gl-color-contrast
16
+ :foreground="foreground"
17
+ :background="background"
18
+ />
19
+ `,
20
+ });
21
+ Default.args = generateProps();
22
+
23
+ export default {
24
+ title: 'internal/color_contrast',
25
+ component: GlColorContrast,
26
+ parameters: {
27
+ docs: {
28
+ description: {
29
+ component: readme,
30
+ },
31
+ },
32
+ },
33
+ argTypes: {
34
+ foreground: {
35
+ control: 'color',
36
+ },
37
+ background: {
38
+ control: 'color',
39
+ },
40
+ },
41
+ };
@@ -0,0 +1,52 @@
1
+ <script>
2
+ import { HEX_REGEX } from '../../utils/constants';
3
+ import { getColorContrast } from '../../utils/utils';
4
+
5
+ export default {
6
+ name: 'GlColorContrast',
7
+ props: {
8
+ foreground: {
9
+ type: String,
10
+ required: true,
11
+ validator: (value) => HEX_REGEX.test(value),
12
+ },
13
+ background: {
14
+ type: String,
15
+ required: true,
16
+ validator: (value) => HEX_REGEX.test(value),
17
+ },
18
+ },
19
+ computed: {
20
+ isValidColorCombination() {
21
+ return HEX_REGEX.test(this.foreground) && HEX_REGEX.test(this.background);
22
+ },
23
+ classes() {
24
+ if (!this.isValidColorCombination) return 'gl-text-gray-950';
25
+ const { grade } = this.contrast.level;
26
+ const isFail = grade === 'F';
27
+ const contrastScore = getColorContrast('#fff', this.background).score > 4.5;
28
+ const textClass = contrastScore ? 'gl-text-white' : 'gl-text-gray-950';
29
+ const failClasses = contrastScore
30
+ ? 'gl-inset-border-1-red-300 gl-text-red-300'
31
+ : 'gl-inset-border-1-red-500 gl-text-red-500';
32
+ return [textClass, isFail ? failClasses : ''];
33
+ },
34
+ contrast() {
35
+ return getColorContrast(this.foreground, this.background);
36
+ },
37
+ },
38
+ };
39
+ </script>
40
+
41
+ <template>
42
+ <code
43
+ class="gl-w-10 gl-text-center gl-rounded-base gl-font-xs gl-p-2"
44
+ :class="classes"
45
+ :style="{ backgroundColor: background }"
46
+ >
47
+ <template v-if="isValidColorCombination">
48
+ {{ contrast.level.grade }} {{ contrast.score }}
49
+ </template>
50
+ <template v-else>???</template>
51
+ </code>
52
+ </template>
@@ -1,40 +1,54 @@
1
- import { methods, template } from './common_story_options';
2
- import colorTokens from './color.dark.tokens.json';
1
+ import COMPILED_TOKENS from '../../dist/tokens/json/tokens.dark.json';
2
+ import { colorTokenStoryOptions } from './common_story_options';
3
3
 
4
- const generateProps = ({ name = '', tokens = colorTokens } = {}) => ({
5
- name,
6
- tokens,
7
- });
4
+ const generateProps = ({ tokens = {} } = {}) => ({ tokens });
8
5
 
9
6
  export const Default = (args, { argTypes }) => ({
10
7
  props: Object.keys(argTypes),
11
- methods,
12
- template,
8
+ ...colorTokenStoryOptions,
13
9
  });
14
10
  Default.args = generateProps({
15
11
  tokens: {
16
- white: colorTokens.white,
17
- black: colorTokens.black,
12
+ white: COMPILED_TOKENS.white,
13
+ black: COMPILED_TOKENS.black,
18
14
  },
19
15
  });
20
16
 
21
- export const Gray = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
22
- Gray.args = generateProps({ name: 'gray', tokens: colorTokens.gray });
17
+ export const Gray = (args, { argTypes }) => ({
18
+ props: Object.keys(argTypes),
19
+ ...colorTokenStoryOptions,
20
+ });
21
+ Gray.args = generateProps({ tokens: COMPILED_TOKENS.gray });
23
22
 
24
- export const Blue = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
25
- Blue.args = generateProps({ name: 'blue', tokens: colorTokens.blue });
23
+ export const Blue = (args, { argTypes }) => ({
24
+ props: Object.keys(argTypes),
25
+ ...colorTokenStoryOptions,
26
+ });
27
+ Blue.args = generateProps({ tokens: COMPILED_TOKENS.blue });
26
28
 
27
- export const Green = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
28
- Green.args = generateProps({ name: 'green', tokens: colorTokens.green });
29
+ export const Green = (args, { argTypes }) => ({
30
+ props: Object.keys(argTypes),
31
+ ...colorTokenStoryOptions,
32
+ });
33
+ Green.args = generateProps({ tokens: COMPILED_TOKENS.green });
29
34
 
30
- export const Orange = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
31
- Orange.args = generateProps({ name: 'orange', tokens: colorTokens.orange });
35
+ export const Orange = (args, { argTypes }) => ({
36
+ props: Object.keys(argTypes),
37
+ ...colorTokenStoryOptions,
38
+ });
39
+ Orange.args = generateProps({ tokens: COMPILED_TOKENS.orange });
32
40
 
33
- export const Red = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
34
- Red.args = generateProps({ name: 'red', tokens: colorTokens.red });
41
+ export const Red = (args, { argTypes }) => ({
42
+ props: Object.keys(argTypes),
43
+ ...colorTokenStoryOptions,
44
+ });
45
+ Red.args = generateProps({ tokens: COMPILED_TOKENS.red });
35
46
 
36
- export const Purple = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
37
- Purple.args = generateProps({ name: 'purple', tokens: colorTokens.purple });
47
+ export const Purple = (args, { argTypes }) => ({
48
+ props: Object.keys(argTypes),
49
+ ...colorTokenStoryOptions,
50
+ });
51
+ Purple.args = generateProps({ tokens: COMPILED_TOKENS.purple });
38
52
 
39
53
  // eslint-disable-next-line storybook/csf-component
40
54
  export default {
@@ -1,54 +1,37 @@
1
- import { methods, template } from './common_story_options';
2
- import colorTokens from './color.data_viz.dark.tokens.json';
1
+ import COMPILED_TOKENS from '../../dist/tokens/json/tokens.dark.json';
2
+ import { colorTokenStoryOptions } from './common_story_options';
3
3
 
4
- const generateProps = ({ name = '', tokens = colorTokens } = {}) => ({
5
- name,
6
- tokens,
7
- });
4
+ const generateProps = ({ tokens = {} } = {}) => ({ tokens });
8
5
 
9
6
  export const DataVizGreen = (args, { argTypes }) => ({
10
7
  props: Object.keys(argTypes),
11
- methods,
12
- template,
13
- });
14
- DataVizGreen.args = generateProps({
15
- name: 'data-viz.green',
16
- tokens: colorTokens['data-viz'].green,
8
+ ...colorTokenStoryOptions,
17
9
  });
10
+ DataVizGreen.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].green });
18
11
 
19
12
  export const DataVizAqua = (args, { argTypes }) => ({
20
13
  props: Object.keys(argTypes),
21
- methods,
22
- template,
14
+ ...colorTokenStoryOptions,
23
15
  });
24
- DataVizAqua.args = generateProps({ name: 'data-viz.aqua', tokens: colorTokens['data-viz'].aqua });
16
+ DataVizAqua.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].aqua });
25
17
 
26
18
  export const DataVizBlue = (args, { argTypes }) => ({
27
19
  props: Object.keys(argTypes),
28
- methods,
29
- template,
20
+ ...colorTokenStoryOptions,
30
21
  });
31
- DataVizBlue.args = generateProps({ name: 'data-viz.blue', tokens: colorTokens['data-viz'].blue });
22
+ DataVizBlue.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].blue });
32
23
 
33
24
  export const DataVizMagenta = (args, { argTypes }) => ({
34
25
  props: Object.keys(argTypes),
35
- methods,
36
- template,
37
- });
38
- DataVizMagenta.args = generateProps({
39
- name: 'data-viz.magenta',
40
- tokens: colorTokens['data-viz'].magenta,
26
+ ...colorTokenStoryOptions,
41
27
  });
28
+ DataVizMagenta.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].magenta });
42
29
 
43
30
  export const DataVizOrange = (args, { argTypes }) => ({
44
31
  props: Object.keys(argTypes),
45
- methods,
46
- template,
47
- });
48
- DataVizOrange.args = generateProps({
49
- name: 'data-viz.orange',
50
- tokens: colorTokens['data-viz'].orange,
32
+ ...colorTokenStoryOptions,
51
33
  });
34
+ DataVizOrange.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].orange });
52
35
 
53
36
  // eslint-disable-next-line storybook/csf-component
54
37
  export default {
@@ -1,54 +1,37 @@
1
- import { methods, template } from './common_story_options';
2
- import colorTokens from './color.data_viz.tokens.json';
1
+ import COMPILED_TOKENS from '../../dist/tokens/json/tokens.json';
2
+ import { colorTokenStoryOptions } from './common_story_options';
3
3
 
4
- const generateProps = ({ name = '', tokens = colorTokens } = {}) => ({
5
- name,
6
- tokens,
7
- });
4
+ const generateProps = ({ tokens = {} } = {}) => ({ tokens });
8
5
 
9
6
  export const DataVizGreen = (args, { argTypes }) => ({
10
7
  props: Object.keys(argTypes),
11
- methods,
12
- template,
13
- });
14
- DataVizGreen.args = generateProps({
15
- name: 'data-viz.green',
16
- tokens: colorTokens['data-viz'].green,
8
+ ...colorTokenStoryOptions,
17
9
  });
10
+ DataVizGreen.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].green });
18
11
 
19
12
  export const DataVizAqua = (args, { argTypes }) => ({
20
13
  props: Object.keys(argTypes),
21
- methods,
22
- template,
14
+ ...colorTokenStoryOptions,
23
15
  });
24
- DataVizAqua.args = generateProps({ name: 'data-viz.aqua', tokens: colorTokens['data-viz'].aqua });
16
+ DataVizAqua.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].aqua });
25
17
 
26
18
  export const DataVizBlue = (args, { argTypes }) => ({
27
19
  props: Object.keys(argTypes),
28
- methods,
29
- template,
20
+ ...colorTokenStoryOptions,
30
21
  });
31
- DataVizBlue.args = generateProps({ name: 'data-viz.blue', tokens: colorTokens['data-viz'].blue });
22
+ DataVizBlue.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].blue });
32
23
 
33
24
  export const DataVizMagenta = (args, { argTypes }) => ({
34
25
  props: Object.keys(argTypes),
35
- methods,
36
- template,
37
- });
38
- DataVizMagenta.args = generateProps({
39
- name: 'data-viz.magenta',
40
- tokens: colorTokens['data-viz'].magenta,
26
+ ...colorTokenStoryOptions,
41
27
  });
28
+ DataVizMagenta.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].magenta });
42
29
 
43
30
  export const DataVizOrange = (args, { argTypes }) => ({
44
31
  props: Object.keys(argTypes),
45
- methods,
46
- template,
47
- });
48
- DataVizOrange.args = generateProps({
49
- name: 'data-viz.orange',
50
- tokens: colorTokens['data-viz'].orange,
32
+ ...colorTokenStoryOptions,
51
33
  });
34
+ DataVizOrange.args = generateProps({ tokens: COMPILED_TOKENS['data-viz'].orange });
52
35
 
53
36
  // eslint-disable-next-line storybook/csf-component
54
37
  export default {
@@ -1,58 +1,43 @@
1
- import { methods, template } from './common_story_options';
2
- import colorTokens from './color.theme.dark.tokens.json';
1
+ import COMPILED_TOKENS from '../../dist/tokens/json/tokens.dark.json';
2
+ import { colorTokenStoryOptions } from './common_story_options';
3
3
 
4
- const generateProps = ({ name = '', tokens = colorTokens } = {}) => ({
5
- name,
6
- tokens,
7
- });
4
+ const generateProps = ({ tokens = {} } = {}) => ({ tokens });
8
5
 
9
6
  export const ThemeIndigo = (args, { argTypes }) => ({
10
7
  props: Object.keys(argTypes),
11
- methods,
12
- template,
8
+ ...colorTokenStoryOptions,
13
9
  });
14
- ThemeIndigo.args = generateProps({ name: 'theme.indigo', tokens: colorTokens.theme.indigo });
10
+ ThemeIndigo.args = generateProps({ tokens: COMPILED_TOKENS.theme.indigo });
15
11
 
16
12
  export const ThemeBlue = (args, { argTypes }) => ({
17
13
  props: Object.keys(argTypes),
18
- methods,
19
- template,
14
+ ...colorTokenStoryOptions,
20
15
  });
21
- ThemeBlue.args = generateProps({ name: 'theme.blue', tokens: colorTokens.theme.blue });
16
+ ThemeBlue.args = generateProps({ tokens: COMPILED_TOKENS.theme.blue });
22
17
 
23
18
  export const ThemeLightBlue = (args, { argTypes }) => ({
24
19
  props: Object.keys(argTypes),
25
- methods,
26
- template,
27
- });
28
- ThemeLightBlue.args = generateProps({
29
- name: 'theme.light-blue',
30
- tokens: colorTokens.theme['light-blue'],
20
+ ...colorTokenStoryOptions,
31
21
  });
22
+ ThemeLightBlue.args = generateProps({ tokens: COMPILED_TOKENS.theme['light-blue'] });
32
23
 
33
24
  export const ThemeGreen = (args, { argTypes }) => ({
34
25
  props: Object.keys(argTypes),
35
- methods,
36
- template,
26
+ ...colorTokenStoryOptions,
37
27
  });
38
- ThemeGreen.args = generateProps({ name: 'theme.green', tokens: colorTokens.theme.green });
28
+ ThemeGreen.args = generateProps({ tokens: COMPILED_TOKENS.theme.green });
39
29
 
40
30
  export const ThemeRed = (args, { argTypes }) => ({
41
31
  props: Object.keys(argTypes),
42
- methods,
43
- template,
32
+ ...colorTokenStoryOptions,
44
33
  });
45
- ThemeRed.args = generateProps({ name: 'theme.red', tokens: colorTokens.theme.red });
34
+ ThemeRed.args = generateProps({ tokens: COMPILED_TOKENS.theme.red });
46
35
 
47
36
  export const ThemeLightRed = (args, { argTypes }) => ({
48
37
  props: Object.keys(argTypes),
49
- methods,
50
- template,
51
- });
52
- ThemeLightRed.args = generateProps({
53
- name: 'theme.light-red',
54
- tokens: colorTokens.theme['light-red'],
38
+ ...colorTokenStoryOptions,
55
39
  });
40
+ ThemeLightRed.args = generateProps({ tokens: COMPILED_TOKENS.theme['light-red'] });
56
41
 
57
42
  // eslint-disable-next-line storybook/csf-component
58
43
  export default {
@@ -1,58 +1,43 @@
1
- import { methods, template } from './common_story_options';
2
- import colorTokens from './color.theme.tokens.json';
1
+ import COMPILED_TOKENS from '../../dist/tokens/json/tokens.json';
2
+ import { colorTokenStoryOptions } from './common_story_options';
3
3
 
4
- const generateProps = ({ name = '', tokens = colorTokens } = {}) => ({
5
- name,
6
- tokens,
7
- });
4
+ const generateProps = ({ tokens = {} } = {}) => ({ tokens });
8
5
 
9
6
  export const ThemeIndigo = (args, { argTypes }) => ({
10
7
  props: Object.keys(argTypes),
11
- methods,
12
- template,
8
+ ...colorTokenStoryOptions,
13
9
  });
14
- ThemeIndigo.args = generateProps({ name: 'theme.indigo', tokens: colorTokens.theme.indigo });
10
+ ThemeIndigo.args = generateProps({ tokens: COMPILED_TOKENS.theme.indigo });
15
11
 
16
12
  export const ThemeBlue = (args, { argTypes }) => ({
17
13
  props: Object.keys(argTypes),
18
- methods,
19
- template,
14
+ ...colorTokenStoryOptions,
20
15
  });
21
- ThemeBlue.args = generateProps({ name: 'theme.blue', tokens: colorTokens.theme.blue });
16
+ ThemeBlue.args = generateProps({ tokens: COMPILED_TOKENS.theme.blue });
22
17
 
23
18
  export const ThemeLightBlue = (args, { argTypes }) => ({
24
19
  props: Object.keys(argTypes),
25
- methods,
26
- template,
27
- });
28
- ThemeLightBlue.args = generateProps({
29
- name: 'theme.light-blue',
30
- tokens: colorTokens.theme['light-blue'],
20
+ ...colorTokenStoryOptions,
31
21
  });
22
+ ThemeLightBlue.args = generateProps({ tokens: COMPILED_TOKENS.theme['light-blue'] });
32
23
 
33
24
  export const ThemeGreen = (args, { argTypes }) => ({
34
25
  props: Object.keys(argTypes),
35
- methods,
36
- template,
26
+ ...colorTokenStoryOptions,
37
27
  });
38
- ThemeGreen.args = generateProps({ name: 'theme.green', tokens: colorTokens.theme.green });
28
+ ThemeGreen.args = generateProps({ tokens: COMPILED_TOKENS.theme.green });
39
29
 
40
30
  export const ThemeRed = (args, { argTypes }) => ({
41
31
  props: Object.keys(argTypes),
42
- methods,
43
- template,
32
+ ...colorTokenStoryOptions,
44
33
  });
45
- ThemeRed.args = generateProps({ name: 'theme.red', tokens: colorTokens.theme.red });
34
+ ThemeRed.args = generateProps({ tokens: COMPILED_TOKENS.theme.red });
46
35
 
47
36
  export const ThemeLightRed = (args, { argTypes }) => ({
48
37
  props: Object.keys(argTypes),
49
- methods,
50
- template,
51
- });
52
- ThemeLightRed.args = generateProps({
53
- name: 'theme.light-red',
54
- tokens: colorTokens.theme['light-red'],
38
+ ...colorTokenStoryOptions,
55
39
  });
40
+ ThemeLightRed.args = generateProps({ tokens: COMPILED_TOKENS.theme['light-red'] });
56
41
 
57
42
  // eslint-disable-next-line storybook/csf-component
58
43
  export default {
@@ -1,40 +1,54 @@
1
- import { methods, template } from './common_story_options';
2
- import colorTokens from './color.tokens.json';
1
+ import COMPILED_TOKENS from '../../dist/tokens/json/tokens.json';
2
+ import { colorTokenStoryOptions } from './common_story_options';
3
3
 
4
- const generateProps = ({ name = '', tokens = colorTokens } = {}) => ({
5
- name,
6
- tokens,
7
- });
4
+ const generateProps = ({ tokens = {} } = {}) => ({ tokens });
8
5
 
9
6
  export const Default = (args, { argTypes }) => ({
10
7
  props: Object.keys(argTypes),
11
- methods,
12
- template,
8
+ ...colorTokenStoryOptions,
13
9
  });
14
10
  Default.args = generateProps({
15
11
  tokens: {
16
- white: colorTokens.white,
17
- black: colorTokens.black,
12
+ white: COMPILED_TOKENS.white,
13
+ black: COMPILED_TOKENS.black,
18
14
  },
19
15
  });
20
16
 
21
- export const Gray = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
22
- Gray.args = generateProps({ name: 'gray', tokens: colorTokens.gray });
17
+ export const Gray = (args, { argTypes }) => ({
18
+ props: Object.keys(argTypes),
19
+ ...colorTokenStoryOptions,
20
+ });
21
+ Gray.args = generateProps({ tokens: COMPILED_TOKENS.gray });
23
22
 
24
- export const Blue = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
25
- Blue.args = generateProps({ name: 'blue', tokens: colorTokens.blue });
23
+ export const Blue = (args, { argTypes }) => ({
24
+ props: Object.keys(argTypes),
25
+ ...colorTokenStoryOptions,
26
+ });
27
+ Blue.args = generateProps({ tokens: COMPILED_TOKENS.blue });
26
28
 
27
- export const Green = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
28
- Green.args = generateProps({ name: 'green', tokens: colorTokens.green });
29
+ export const Green = (args, { argTypes }) => ({
30
+ props: Object.keys(argTypes),
31
+ ...colorTokenStoryOptions,
32
+ });
33
+ Green.args = generateProps({ tokens: COMPILED_TOKENS.green });
29
34
 
30
- export const Orange = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
31
- Orange.args = generateProps({ name: 'orange', tokens: colorTokens.orange });
35
+ export const Orange = (args, { argTypes }) => ({
36
+ props: Object.keys(argTypes),
37
+ ...colorTokenStoryOptions,
38
+ });
39
+ Orange.args = generateProps({ tokens: COMPILED_TOKENS.orange });
32
40
 
33
- export const Red = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
34
- Red.args = generateProps({ name: 'red', tokens: colorTokens.red });
41
+ export const Red = (args, { argTypes }) => ({
42
+ props: Object.keys(argTypes),
43
+ ...colorTokenStoryOptions,
44
+ });
45
+ Red.args = generateProps({ tokens: COMPILED_TOKENS.red });
35
46
 
36
- export const Purple = (args, { argTypes }) => ({ props: Object.keys(argTypes), methods, template });
37
- Purple.args = generateProps({ name: 'purple', tokens: colorTokens.purple });
47
+ export const Purple = (args, { argTypes }) => ({
48
+ props: Object.keys(argTypes),
49
+ ...colorTokenStoryOptions,
50
+ });
51
+ Purple.args = generateProps({ tokens: COMPILED_TOKENS.purple });
38
52
 
39
53
  // eslint-disable-next-line storybook/csf-component
40
54
  export default {
@@ -1,24 +1,21 @@
1
- import { methods, template } from './common_story_options';
2
- import colorTokens from './color.transparency.tokens.json';
1
+ import COMPILED_TOKENS from '../../dist/tokens/json/tokens.json';
2
+ import { colorTokenStoryOptions } from './common_story_options';
3
3
 
4
- const generateProps = ({ name = '', tokens = colorTokens } = {}) => ({
5
- name,
6
- tokens,
7
- });
4
+ const generateProps = ({ tokens = {} } = {}) => ({ tokens });
8
5
 
9
6
  export const Gray = (args, { argTypes }) => ({
10
7
  props: Object.keys(argTypes),
11
- methods,
12
- template,
8
+ ...colorTokenStoryOptions,
13
9
  });
14
- Gray.args = generateProps({ name: 't-gray-a', tokens: colorTokens['t-gray-a'] });
10
+ Gray.args = generateProps({ tokens: COMPILED_TOKENS['t-gray-a'] });
15
11
 
16
12
  export const White = (args, { argTypes }) => ({
17
13
  props: Object.keys(argTypes),
18
- methods,
19
- template: `<div class="gl-bg-gray-900 gl-text-white">${template}</div>`,
14
+ components: colorTokenStoryOptions.components,
15
+ methods: colorTokenStoryOptions.methods,
16
+ template: `<div class="gl-bg-gray-900 gl-text-white">${colorTokenStoryOptions.template}</div>`,
20
17
  });
21
- White.args = generateProps({ name: 't-white-a', tokens: colorTokens['t-white-a'] });
18
+ White.args = generateProps({ tokens: COMPILED_TOKENS['t-white-a'] });
22
19
 
23
20
  // eslint-disable-next-line storybook/csf-component
24
21
  export default {
@@ -1,35 +1,17 @@
1
- import { colorFromBackground, relativeLuminance, rgbFromHex } from '../utils/utils';
2
1
  import { WHITE, GRAY_950 } from '../../dist/tokens/js/tokens';
2
+ import { colorFromBackground } from '../utils/utils';
3
+ import GlColorContrast from '../internal/color_contrast/color_contrast.vue';
3
4
 
4
- const CONTRAST_LEVELS = [
5
- {
6
- grade: 'F',
7
- min: 0,
8
- max: 3,
9
- },
10
- {
11
- grade: 'AA+',
12
- min: 3,
13
- max: 4.5,
14
- },
15
- {
16
- grade: 'AA',
17
- min: 4.5,
18
- max: 7,
19
- },
20
- {
21
- grade: 'AAA',
22
- min: 7,
23
- max: 22,
24
- },
25
- ];
5
+ export const components = {
6
+ GlColorContrast,
7
+ };
26
8
 
27
9
  export const methods = {
28
10
  isAlpha(value) {
29
11
  return value.startsWith('rgba(');
30
12
  },
31
- getTokenName(name, key) {
32
- return [name, key].filter(Boolean).join('.');
13
+ getTokenName(token) {
14
+ return token.path.filter(Boolean).join('.');
33
15
  },
34
16
  getTextColorClass(value) {
35
17
  if (this.isAlpha(value)) return '';
@@ -39,71 +21,31 @@ export const methods = {
39
21
  'gl-text-white': textColorVariant === 'light',
40
22
  };
41
23
  },
42
- getColorContrast(foreground = 'light', background) {
43
- const foregroundColor = {
44
- light: WHITE,
45
- dark: GRAY_950,
46
- };
47
- // Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
48
- const foregroundLuminance = relativeLuminance(rgbFromHex(foregroundColor[foreground])) + 0.05;
49
- const backgroundLuminance = relativeLuminance(rgbFromHex(background)) + 0.05;
50
-
51
- let score = foregroundLuminance / backgroundLuminance;
52
- if (backgroundLuminance > foregroundLuminance) {
53
- score = 1 / score;
54
- }
55
-
56
- const level = CONTRAST_LEVELS.find(({ min, max }) => {
57
- return score >= min && score < max;
58
- });
59
-
60
- return {
61
- score: (Math.round(score * 10) / 10).toFixed(1),
62
- level,
63
- };
64
- },
65
- getColorContrastClass(foreground, background) {
66
- const { grade } = this.getColorContrast(foreground, background).level;
67
- const isFail = grade === 'F';
68
- const classes = {
69
- light: isFail ? 'gl-inset-border-1-red-500 gl-text-red-500' : 'gl-text-gray-950',
70
- dark: isFail ? 'gl-inset-border-1-red-300 gl-text-red-300' : 'gl-text-white',
71
- };
72
- return classes[foreground];
73
- },
74
24
  };
75
25
 
76
- export const template = `
26
+ export const template = (lightBackground = WHITE, darkBackground = GRAY_950) => `
77
27
  <ul
78
28
  class="gl-list-style-none gl-m-0 gl-p-0"
79
29
  >
80
30
  <li
81
- v-for="(token, key) in tokens"
82
- :key="key"
31
+ v-for="token in tokens"
32
+ :key="token.name"
83
33
  class="gl-display-flex gl-flex-wrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-p-3"
84
- :class="getTextColorClass(token.$value)"
85
- :style="{ backgroundColor: token.$value }"
34
+ :class="getTextColorClass(token.value)"
35
+ :style="{ backgroundColor: token.value }"
86
36
  >
87
- <code class="gl-reset-color">{{ getTokenName(name, key) }}</code>
37
+ <code class="gl-reset-color">{{ getTokenName(token) }}</code>
88
38
  <div class="gl-display-flex gl-align-items-center gl-gap-3">
89
- <code class="gl-reset-color">{{ token.$value }}</code>
90
- <code
91
- v-if="!isAlpha(token.$value)"
92
- class="gl-w-10 gl-text-center gl-rounded-base gl-font-xs gl-p-2 gl-bg-gray-950"
93
- :class="getColorContrastClass('dark', token.$value)"
94
- >
95
- {{ getColorContrast('dark', token.$value).level.grade }}
96
- {{ getColorContrast('dark', token.$value).score }}
97
- </code>
98
- <code
99
- v-if="!isAlpha(token.$value)"
100
- class="gl-w-10 gl-text-center gl-rounded-base gl-font-xs gl-p-2 gl-bg-white"
101
- :class="getColorContrastClass('light', token.$value)"
102
- >
103
- {{ getColorContrast('light', token.$value).level.grade }}
104
- {{ getColorContrast('light', token.$value).score }}
105
- </code>
39
+ <code class="gl-reset-color">{{ token.value }}</code>
40
+ <gl-color-contrast v-if="!isAlpha(token.value)" :foreground="token.value" background="${darkBackground}" />
41
+ <gl-color-contrast v-if="!isAlpha(token.value)" :foreground="token.value" background="${lightBackground}" />
106
42
  </div>
107
43
  </li>
108
44
  </ul>
109
45
  `;
46
+
47
+ export const colorTokenStoryOptions = {
48
+ components,
49
+ methods,
50
+ template: template(),
51
+ };
@@ -6,6 +6,31 @@ function appendDefaultOption(options) {
6
6
 
7
7
  export const COMMA = ',';
8
8
 
9
+ export const CONTRAST_LEVELS = [
10
+ {
11
+ grade: 'F',
12
+ min: 0,
13
+ max: 3,
14
+ },
15
+ {
16
+ grade: 'AA+',
17
+ min: 3,
18
+ max: 4.5,
19
+ },
20
+ {
21
+ grade: 'AA',
22
+ min: 4.5,
23
+ max: 7,
24
+ },
25
+ {
26
+ grade: 'AAA',
27
+ min: 7,
28
+ max: 22,
29
+ },
30
+ ];
31
+
32
+ export const HEX_REGEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
33
+
9
34
  export const LEFT_MOUSE_BUTTON = 0;
10
35
 
11
36
  export const glThemes = ['indigo', 'blue', 'light-blue', 'green', 'red', 'light-red'];
@@ -1,5 +1,5 @@
1
1
  import { isVisible } from 'bootstrap-vue/src/utils/dom';
2
- import { COMMA, labelColorOptions, focusableTags } from './constants';
2
+ import { COMMA, CONTRAST_LEVELS, labelColorOptions, focusableTags } from './constants';
3
3
 
4
4
  export function debounceByAnimationFrame(fn) {
5
5
  let requestId;
@@ -90,6 +90,26 @@ export function colorFromBackground(backgroundColor, contrastRatio = 2.4) {
90
90
  : labelColorOptions.dark;
91
91
  }
92
92
 
93
+ export function getColorContrast(foreground, background) {
94
+ // Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
95
+ const backgroundLuminance = relativeLuminance(rgbFromHex(background)) + 0.05;
96
+ const foregroundLuminance = relativeLuminance(rgbFromHex(foreground)) + 0.05;
97
+
98
+ let score = backgroundLuminance / foregroundLuminance;
99
+ if (foregroundLuminance > backgroundLuminance) {
100
+ score = 1 / score;
101
+ }
102
+
103
+ const level = CONTRAST_LEVELS.find(({ min, max }) => {
104
+ return score >= min && score < max;
105
+ });
106
+
107
+ return {
108
+ score: (Math.round(score * 10) / 10).toFixed(1),
109
+ level,
110
+ };
111
+ }
112
+
93
113
  export function uid() {
94
114
  return Math.random().toString(36).substring(2);
95
115
  }