mirror of
https://github.com/nextcloud/server.git
synced 2026-02-20 00:12:30 -05:00
Merge pull request #40773 from nextcloud/fix/contrast-maxcontrast-vs-hover
fix(theming): Ensure all text colors have enough contrast for accessibility
This commit is contained in:
commit
32eaf57e01
12 changed files with 235 additions and 32 deletions
128
apps/theming/__tests__/accessibility.cy.ts
Normal file
128
apps/theming/__tests__/accessibility.cy.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved
|
||||
import style from '!raw-loader!../css/default.css'
|
||||
|
||||
const testCases = {
|
||||
'Main text': {
|
||||
foregroundColors: [
|
||||
'color-main-text',
|
||||
// 'color-text-light', deprecated
|
||||
// 'color-text-lighter', deprecated
|
||||
'color-text-maxcontrast',
|
||||
'color-text-maxcontrast-default',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-background-main',
|
||||
'color-background-hover',
|
||||
'color-background-dark',
|
||||
// 'color-background-darker', this should only be used for elements not for text
|
||||
],
|
||||
},
|
||||
Primary: {
|
||||
foregroundColors: [
|
||||
'color-primary-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
// 'color-primary-default', this should only be used for elements not for text!
|
||||
// 'color-primary-hover', this should only be used for elements and not for text!
|
||||
'color-primary',
|
||||
],
|
||||
},
|
||||
'Primary light': {
|
||||
foregroundColors: [
|
||||
'color-primary-light-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-primary-light',
|
||||
'color-primary-light-hover',
|
||||
],
|
||||
},
|
||||
'Primary element': {
|
||||
foregroundColors: [
|
||||
'color-primary-element-text',
|
||||
'color-primary-element-text-dark',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-primary-element',
|
||||
'color-primary-element-hover',
|
||||
],
|
||||
},
|
||||
'Primary element light': {
|
||||
foregroundColors: [
|
||||
'color-primary-element-light-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-primary-element-light',
|
||||
'color-primary-element-light-hover',
|
||||
],
|
||||
},
|
||||
'Servity information texts': {
|
||||
foregroundColors: [
|
||||
'color-error-text',
|
||||
'color-warning-text',
|
||||
'color-success-text',
|
||||
'color-info-text',
|
||||
],
|
||||
backgroundColors: [
|
||||
'color-background-main',
|
||||
'color-background-hover',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper element with color and background set
|
||||
*
|
||||
* @param foreground The foreground color (css variable without leading --)
|
||||
* @param background The background color
|
||||
*/
|
||||
function createTestCase(foreground: string, background: string) {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.innerText = `${foreground} ${background}`
|
||||
wrapper.style.color = `var(--${foreground})`
|
||||
wrapper.style.backgroundColor = `var(--${background})`
|
||||
wrapper.style.padding = '4px'
|
||||
wrapper.setAttribute('data-cy-testcase', '')
|
||||
return wrapper
|
||||
}
|
||||
|
||||
describe('Accessibility of Nextcloud theming', () => {
|
||||
before(() => {
|
||||
cy.injectAxe()
|
||||
|
||||
const el = document.createElement('style')
|
||||
el.innerText = style
|
||||
document.head.appendChild(el)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.document().then(doc => {
|
||||
const root = doc.querySelector('[data-cy-root]')
|
||||
if (root === null) {
|
||||
throw new Error('No test root found')
|
||||
}
|
||||
for (const child of root.children) {
|
||||
root.removeChild(child)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
for (const [name, { backgroundColors, foregroundColors }] of Object.entries(testCases)) {
|
||||
context(`Accessibility of CSS color variables for ${name}`, () => {
|
||||
for (const foreground of foregroundColors) {
|
||||
for (const background of backgroundColors) {
|
||||
it(`color contrast of ${foreground} on ${background}`, () => {
|
||||
const element = createTestCase(foreground, background)
|
||||
cy.document().then(doc => {
|
||||
const root = doc.querySelector('[data-cy-root]')
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(root).not.to.be.undefined
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
root!.appendChild(element)
|
||||
cy.checkA11y('[data-cy-testcase]')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -6,16 +6,20 @@
|
|||
--filter-background-blur: blur(25px);
|
||||
--gradient-main-background: var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%;
|
||||
--color-background-hover: #f5f5f5;
|
||||
/** Can be used e.g. to colorize selected table rows */
|
||||
--color-background-dark: #ededed;
|
||||
/** This should only be used for elements, not as a text background! Otherwise it will not work for accessibility. */
|
||||
--color-background-darker: #dbdbdb;
|
||||
--color-placeholder-light: #e6e6e6;
|
||||
--color-placeholder-dark: #cccccc;
|
||||
--color-main-text: #222222;
|
||||
--color-text-maxcontrast: #767676;
|
||||
--color-text-maxcontrast-default: #767676;
|
||||
--color-text-maxcontrast-background-blur: #646464;
|
||||
--color-text-light: #222222;
|
||||
--color-text-lighter: #767676;
|
||||
--color-text-maxcontrast: #6b6b6b;
|
||||
--color-text-maxcontrast-default: #6b6b6b;
|
||||
--color-text-maxcontrast-background-blur: #595959;
|
||||
/** @deprecated use ` --color-main-text` instead */
|
||||
--color-text-light: var(--color-main-text);
|
||||
/** @deprecated use `--color-text-maxcontrast` instead */
|
||||
--color-text-lighter: var(--color-text-maxcontrast);
|
||||
--color-scrollbar: rgba(34,34,34, .15);
|
||||
--color-error: #d91812;
|
||||
--color-error-rgb: 217,24,18;
|
||||
|
|
@ -24,7 +28,7 @@
|
|||
--color-warning: #c28900;
|
||||
--color-warning-rgb: 194,137,0;
|
||||
--color-warning-hover: #cea032;
|
||||
--color-warning-text: #996c00;
|
||||
--color-warning-text: #8f6500;
|
||||
--color-success: #2d7b41;
|
||||
--color-success-rgb: 45,123,65;
|
||||
--color-success-hover: #448955;
|
||||
|
|
@ -64,20 +68,20 @@
|
|||
--background-invert-if-bright: invert(100%);
|
||||
--background-image-invert-if-bright: no;
|
||||
--primary-invert-if-bright: no;
|
||||
--color-primary: #006aa3;
|
||||
--color-primary: #00679e;
|
||||
--color-primary-default: #0082c9;
|
||||
--color-primary-text: #ffffff;
|
||||
--color-primary-hover: #3287b5;
|
||||
--color-primary-light: #e5f0f5;
|
||||
--color-primary-light-text: #002a41;
|
||||
--color-primary-light-hover: #dbe5ea;
|
||||
--color-primary-element: #006aa3;
|
||||
--color-primary-element-hover: #1f7cae;
|
||||
--color-primary-hover: #3285b1;
|
||||
--color-primary-light: #e5eff5;
|
||||
--color-primary-light-text: #00293f;
|
||||
--color-primary-light-hover: #dbe4ea;
|
||||
--color-primary-element: #00679e;
|
||||
--color-primary-element-hover: #1674a6;
|
||||
--color-primary-element-text: #ffffff;
|
||||
--color-primary-element-light: #e5f0f5;
|
||||
--color-primary-element-light-hover: #dbe5ea;
|
||||
--color-primary-element-light-text: #002a41;
|
||||
--color-primary-element-text-dark: #ededed;
|
||||
--color-primary-element-text-dark: #f0f0f0;
|
||||
--color-primary-element-light: #e5eff5;
|
||||
--color-primary-element-light-hover: #dbe4ea;
|
||||
--color-primary-element-light-text: #00293f;
|
||||
--gradient-primary-background: linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%);
|
||||
--image-background-default: url('/apps/theming/img/background/kamil-porembinski-clouds.jpg');
|
||||
--color-background-plain: #0082c9;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class BackgroundService {
|
|||
// true when the background is bright and need dark icons
|
||||
public const THEMING_MODE_DARK = 'dark';
|
||||
public const DEFAULT_COLOR = '#0082c9';
|
||||
public const DEFAULT_ACCESSIBLE_COLOR = '#006aa3';
|
||||
public const DEFAULT_ACCESSIBLE_COLOR = '#00679e';
|
||||
|
||||
public const BACKGROUND_SHIPPED = 'shipped';
|
||||
public const BACKGROUND_CUSTOM = 'custom';
|
||||
|
|
|
|||
|
|
@ -64,15 +64,15 @@ trait CommonThemeTrait {
|
|||
|
||||
// used for buttons, inputs...
|
||||
'--color-primary-element' => $colorPrimaryElement,
|
||||
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 75),
|
||||
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 82),
|
||||
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
|
||||
// mostly used for disabled states
|
||||
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 6),
|
||||
|
||||
// used for hover/focus states
|
||||
'--color-primary-element-light' => $colorPrimaryElementLight,
|
||||
'--color-primary-element-light-hover' => $this->util->mix($colorPrimaryElementLight, $colorMainText, 90),
|
||||
'--color-primary-element-light-text' => $this->util->mix($colorPrimaryElement, $this->util->invertTextColor($colorPrimaryElementLight) ? '#000000' : '#ffffff', -20),
|
||||
// mostly used for disabled states
|
||||
'--color-primary-element-text-dark' => $this->util->darken($this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff', 7),
|
||||
|
||||
// to use like this: background-image: var(--gradient-primary-background);
|
||||
'--gradient-primary-background' => 'linear-gradient(40deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
|
|
|
|||
|
|
@ -84,8 +84,8 @@ class DarkTheme extends DefaultTheme implements ITheme {
|
|||
'--color-text-maxcontrast' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-background-blur' => $this->util->lighten($colorTextMaxcontrast, 2),
|
||||
'--color-text-light' => $this->util->darken($colorMainText, 10),
|
||||
'--color-text-lighter' => $this->util->darken($colorMainText, 20),
|
||||
'--color-text-light' => 'var(--color-main-text)', // deprecated
|
||||
'--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated
|
||||
|
||||
'--color-error' => $colorError,
|
||||
'--color-error-rgb' => join(',', $this->util->hexToRGB($colorError)),
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ class DefaultTheme implements ITheme {
|
|||
public function getCSSVariables(): array {
|
||||
$colorMainText = '#222222';
|
||||
$colorMainTextRgb = join(',', $this->util->hexToRGB($colorMainText));
|
||||
$colorTextMaxcontrast = $this->util->lighten($colorMainText, 33);
|
||||
// Color that still provides enough contrast for text, so we need a ratio of 4.5:1 on main background AND hover
|
||||
$colorTextMaxcontrast = '#6b6b6b'; // 4.5 : 1 for hover background and background dark
|
||||
$colorMainBackground = '#ffffff';
|
||||
$colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground));
|
||||
$colorBoxShadow = $this->util->darken($colorMainBackground, 70);
|
||||
|
|
@ -137,8 +138,8 @@ class DefaultTheme implements ITheme {
|
|||
'--color-text-maxcontrast' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-default' => $colorTextMaxcontrast,
|
||||
'--color-text-maxcontrast-background-blur' => $this->util->darken($colorTextMaxcontrast, 7),
|
||||
'--color-text-light' => $colorMainText,
|
||||
'--color-text-lighter' => $this->util->lighten($colorMainText, 33),
|
||||
'--color-text-light' => 'var(--color-main-text)', // deprecated
|
||||
'--color-text-lighter' => 'var(--color-text-maxcontrast)', // deprecated
|
||||
|
||||
'--color-scrollbar' => 'rgba(' . $colorMainTextRgb . ', .15)',
|
||||
|
||||
|
|
@ -150,7 +151,7 @@ class DefaultTheme implements ITheme {
|
|||
'--color-warning' => $colorWarning,
|
||||
'--color-warning-rgb' => join(',', $this->util->hexToRGB($colorWarning)),
|
||||
'--color-warning-hover' => $this->util->mix($colorWarning, $colorMainBackground, 60),
|
||||
'--color-warning-text' => $this->util->darken($colorWarning, 8),
|
||||
'--color-warning-text' => $this->util->darken($colorWarning, 10),
|
||||
'--color-success' => $colorSuccess,
|
||||
'--color-success-rgb' => join(',', $this->util->hexToRGB($colorSuccess)),
|
||||
'--color-success-hover' => $this->util->mix($colorSuccess, $colorMainBackground, 78),
|
||||
|
|
|
|||
|
|
@ -157,6 +157,8 @@ class DefaultThemeTest extends TestCase {
|
|||
|
||||
$css = ":root {" . PHP_EOL . "$variables}" . PHP_EOL;
|
||||
$fallbackCss = file_get_contents(__DIR__ . '/../../css/default.css');
|
||||
// Remove comments
|
||||
$fallbackCss = preg_replace('/\s*\/\*[\s\S]*?\*\//m', '', $fallbackCss);
|
||||
|
||||
$this->assertEquals($css, $fallbackCss);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
import { colord } from 'colord'
|
||||
|
||||
export const defaultPrimary = '#0082c9'
|
||||
export const defaultAccessiblePrimary = '#006aa3'
|
||||
export const defaultAccessiblePrimary = '#00679e'
|
||||
export const defaultBackground = 'kamil-porembinski-clouds.jpg'
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import 'cypress-axe'
|
||||
|
||||
/* eslint-disable */
|
||||
import { mount } from '@cypress/vue2'
|
||||
|
||||
|
|
|
|||
68
package-lock.json
generated
68
package-lock.json
generated
|
|
@ -40,7 +40,6 @@
|
|||
"camelcase": "^8.0.0",
|
||||
"cancelable-promise": "^4.3.1",
|
||||
"clipboard": "^2.0.11",
|
||||
"colord": "^2.9.3",
|
||||
"core-js": "^3.33.0",
|
||||
"davclient.js": "github:owncloud/davclient.js.git#0.2.1",
|
||||
"debounce": "^1.2.1",
|
||||
|
|
@ -113,8 +112,10 @@
|
|||
"babel-jest": "^29.6.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-loader-exclude-node-modules-except": "^1.2.1",
|
||||
"colord": "^2.9.3",
|
||||
"css-loader": "^6.8.1",
|
||||
"cypress": "^13.3.0",
|
||||
"cypress-axe": "^1.5.0",
|
||||
"cypress-if": "^1.10.5",
|
||||
"cypress-split": "^1.15.3",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
|
|
@ -141,6 +142,7 @@
|
|||
"karma-viewport": "^1.0.9",
|
||||
"node-polyfill-webpack-plugin": "^2.0.1",
|
||||
"puppeteer": "^21.0.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
"regextras": "^0.8.0",
|
||||
"sass": "^1.66.1",
|
||||
"sass-loader": "^13.2.2",
|
||||
|
|
@ -7729,6 +7731,16 @@
|
|||
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.8.2.tgz",
|
||||
"integrity": "sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
|
||||
|
|
@ -9222,7 +9234,8 @@
|
|||
"node_modules/colord": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
|
||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
|
|
@ -10004,6 +10017,19 @@
|
|||
"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress-axe": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress-axe/-/cypress-axe-1.5.0.tgz",
|
||||
"integrity": "sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axe-core": "^3 || ^4",
|
||||
"cypress": "^10 || ^11 || ^12 || ^13"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress-if": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/cypress-if/-/cypress-if-1.10.5.tgz",
|
||||
|
|
@ -22145,6 +22171,44 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-loader": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
|
||||
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-loader/node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -67,7 +67,6 @@
|
|||
"camelcase": "^8.0.0",
|
||||
"cancelable-promise": "^4.3.1",
|
||||
"clipboard": "^2.0.11",
|
||||
"colord": "^2.9.3",
|
||||
"core-js": "^3.33.0",
|
||||
"davclient.js": "github:owncloud/davclient.js.git#0.2.1",
|
||||
"debounce": "^1.2.1",
|
||||
|
|
@ -140,8 +139,10 @@
|
|||
"babel-jest": "^29.6.4",
|
||||
"babel-loader": "^9.1.0",
|
||||
"babel-loader-exclude-node-modules-except": "^1.2.1",
|
||||
"colord": "^2.9.3",
|
||||
"css-loader": "^6.8.1",
|
||||
"cypress": "^13.3.0",
|
||||
"cypress-axe": "^1.5.0",
|
||||
"cypress-if": "^1.10.5",
|
||||
"cypress-split": "^1.15.3",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
|
|
@ -167,6 +168,7 @@
|
|||
"karma-viewport": "^1.0.9",
|
||||
"node-polyfill-webpack-plugin": "^2.0.1",
|
||||
"puppeteer": "^21.0.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
"regextras": "^0.8.0",
|
||||
"sass": "^1.66.1",
|
||||
"sass-loader": "^13.2.2",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"extends": "@vue/tsconfig/tsconfig.json",
|
||||
"include": ["./apps/**/*.ts", "./core/**/*.ts", "./*.d.ts"],
|
||||
"compilerOptions": {
|
||||
"types": ["cypress", "jest", "node", "vue"],
|
||||
"types": ["cypress", "cypress-axe", "jest", "node", "vue"],
|
||||
"outDir": "./dist/",
|
||||
"target": "ESNext",
|
||||
"module": "esnext",
|
||||
|
|
|
|||
Loading…
Reference in a new issue