theme builder. closes #93
This commit is contained in:
parent
f949fefb07
commit
69751eeb6e
16 changed files with 411 additions and 17 deletions
|
|
@ -59,6 +59,9 @@
|
|||
"babel-loader": "^9.1.2",
|
||||
"change-case": "^4.1.2",
|
||||
"chromatic": "^6.19.9",
|
||||
"colord": "^2.9.3",
|
||||
"css-color-converter": "^2.0.0",
|
||||
"deep-object-diff": "^1.1.9",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-standard-jsx": "^11.0.0",
|
||||
|
|
@ -80,6 +83,7 @@
|
|||
"prompts": "^2.4.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { HexAlphaColorPicker, HexColorInput } from 'react-colorful';
|
||||
|
||||
import React from 'react';
|
||||
import classes from './styles.module.scss';
|
||||
import { fromString } from 'css-color-converter';
|
||||
|
||||
const ColourPicker = ({ colour, onChange }) => {
|
||||
return (
|
||||
<div className={classes.colourpicker}>
|
||||
<HexColorInput color={fromString(colour.trim()).toHexString()} onChange={onChange} alpha prefixed />
|
||||
<HexAlphaColorPicker color={fromString(colour.trim()).toHexString()} onChange={onChange} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColourPicker;
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Key from './Key.jsx';
|
||||
import { Unstyled } from '@storybook/blocks';
|
||||
import Value from './Value.jsx';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const Customiser = ({ theme, themeName, setTheme }) => {
|
||||
return (
|
||||
<div className={classes.customiser}>
|
||||
<p>Pick parts of the theme to customise:</p>
|
||||
{Object.entries(theme).map(([key, value]) => {
|
||||
const props = {
|
||||
theme,
|
||||
setTheme,
|
||||
themeName: themeName,
|
||||
name: key,
|
||||
map: key,
|
||||
value,
|
||||
key: themeName + key,
|
||||
};
|
||||
return <Key {...props} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Customiser;
|
||||
33
src/docs/docs-components/ThemeBuilder/Customiser/Key.jsx
Normal file
33
src/docs/docs-components/ThemeBuilder/Customiser/Key.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import Value from './Value.jsx';
|
||||
|
||||
const Key = ({ value, name, map, themeName, setTheme, theme }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<div className="key">
|
||||
<button className={isOpen ? 'open' : ''} onClick={() => setIsOpen(o => !o)}>
|
||||
<div>
|
||||
<span className="material-symbols-outlined">{isOpen ? 'expand_less' : 'expand_more'}</span>
|
||||
</div> {name}
|
||||
</button>
|
||||
|
||||
{Object.entries(value).map(([key, value]) => {
|
||||
const props = {
|
||||
theme,
|
||||
setTheme,
|
||||
name: key,
|
||||
themeName,
|
||||
map: map + '.' + key,
|
||||
value,
|
||||
key: themeName + map + key,
|
||||
};
|
||||
if (!isOpen) return null;
|
||||
if (typeof value === 'object') return <Key {...props} />;
|
||||
return <Value {...props} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Key;
|
||||
46
src/docs/docs-components/ThemeBuilder/Customiser/Value.jsx
Normal file
46
src/docs/docs-components/ThemeBuilder/Customiser/Value.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import React, { useState } from 'react';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
|
||||
import ColourPicker from './ColourPicker.jsx';
|
||||
|
||||
const Input = ({ value, onChange }) => {
|
||||
// Number type
|
||||
if (!isNaN(value)) return <input type="number" value={value} onChange={(e) => onChange(e.target.value)}/>;
|
||||
// Colour type
|
||||
if (!/var\(.*\)/i.test(value) && CSS.supports('color', value)) return (
|
||||
<ColourPicker colour={value} onChange={onChange} />
|
||||
);
|
||||
// Text for the rest...
|
||||
return <input type="text" value={value} onChange={(e) => onChange(e.target.value)} />;
|
||||
}
|
||||
|
||||
const Value = ({ value, name, map, themeName, theme, setTheme }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onChange = (newValue) => {
|
||||
const mutableTheme = cloneDeep(theme);
|
||||
set(mutableTheme, map, newValue);
|
||||
setTheme(mutableTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="value">
|
||||
<label>
|
||||
<div>
|
||||
<button className={isOpen ? 'open' : ''} onClick={() => setIsOpen(o => !o)}>
|
||||
<div>
|
||||
<span className="material-symbols-outlined">{isOpen ? 'expand_less' : 'expand_more'}</span>
|
||||
</div> {name}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
{isOpen && (
|
||||
<div className="input-container">
|
||||
<Input value={value} key={themeName+map} onChange={onChange}/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Value;
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
.customiser :global {
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
div.key > button,
|
||||
div.value > label > div > button {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
div {
|
||||
margin-right: 4px;
|
||||
background-color: #ddd;
|
||||
border: 1px solid #bbb;
|
||||
color: #777;
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
transition: all 0.2s;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
display: inline-flex;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
border-radius: 1.2rem;
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
&.open {
|
||||
div {
|
||||
background-color: #666;
|
||||
border: 1px solid #bbb;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
div.key {
|
||||
div.key {
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
div.value {
|
||||
padding-left: 10px;
|
||||
div.input-container {
|
||||
padding-left: 10px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0 !important;
|
||||
margin: 5px 0 5px;
|
||||
padding: 2px 5px;
|
||||
background-color: #efefef;
|
||||
padding: 5px 5px !important;
|
||||
border: 1px solid #666 !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
input[type='color'] {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.colourpicker :global {
|
||||
width: 160px;
|
||||
.react-colorful {
|
||||
height: 140px;
|
||||
width: 160px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.react-colorful__saturation {
|
||||
border-radius: 0;
|
||||
}
|
||||
.react-colorful__hue {
|
||||
height: 20px;
|
||||
border-radius: 0;
|
||||
}
|
||||
.react-colorful__pointer {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
}
|
||||
35
src/docs/docs-components/ThemeBuilder/NewTheme/NewTheme.jsx
Normal file
35
src/docs/docs-components/ThemeBuilder/NewTheme/NewTheme.jsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { Unstyled } from '@storybook/blocks';
|
||||
import classes from './styles.module.scss';
|
||||
import darkTheme from '../../../../components/Theme/themes/dark';
|
||||
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import lightTheme from '../../../../components/Theme/themes/light';
|
||||
import prism from 'react-syntax-highlighter/dist/esm/styles/prism/prism';
|
||||
import svelteSyntax from '../../../../../.storybook/svelte-highlighting';
|
||||
import { updatedDiff } from 'deep-object-diff';
|
||||
|
||||
SyntaxHighlighter.registerLanguage('svelte', svelteSyntax);
|
||||
|
||||
const NewTheme = ({ theme, themeName }) => {
|
||||
console.log('rerenders NewTheme');
|
||||
const originalTheme = themeName === 'light' ? lightTheme : darkTheme;
|
||||
const updates = updatedDiff(originalTheme, theme);
|
||||
return (
|
||||
<div className={classes.newtheme}>
|
||||
<p>Use the code below to adapt the <code>Theme</code> component for your new design:</p>
|
||||
<SyntaxHighlighter language="svelte" style={prism}>
|
||||
{`<Theme
|
||||
base="${themeName}"
|
||||
theme={${JSON.stringify(updates, null, 2).replaceAll('"', '\'')}}
|
||||
>
|
||||
{/*...*/}
|
||||
</Theme>
|
||||
`}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewTheme;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
.newtheme {
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
36
src/docs/docs-components/ThemeBuilder/ThemeBuilder.jsx
Normal file
36
src/docs/docs-components/ThemeBuilder/ThemeBuilder.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Customiser from './Customiser/Customiser';
|
||||
import NewTheme from './NewTheme/NewTheme.jsx';
|
||||
import ThemeSwitch from './ThemeSwitch/Switch';
|
||||
import { Unstyled } from '@storybook/blocks';
|
||||
import classes from './styles.module.scss';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import darkTheme from '../../../components/Theme/themes/dark';
|
||||
import lightTheme from '../../../components/Theme/themes/light';
|
||||
|
||||
const ThemeBuilder = (props) => {
|
||||
const [themeName, setThemeName] = useState('light');
|
||||
const [theme, setTheme] = useState(cloneDeep(lightTheme));
|
||||
|
||||
useEffect(() => {
|
||||
const newTheme = themeName === 'light' ? lightTheme : darkTheme;
|
||||
setTheme(cloneDeep(newTheme));
|
||||
}, [themeName]);
|
||||
|
||||
return (
|
||||
<Unstyled>
|
||||
<div className={classes.themebuilder}>
|
||||
<div className="column">
|
||||
<ThemeSwitch setThemeName={setThemeName} themeName={themeName} />
|
||||
<Customiser theme={theme} setTheme={setTheme} themeName={themeName} key={themeName} />
|
||||
</div>
|
||||
<div className="column">
|
||||
<NewTheme theme={theme} themeName={themeName} />
|
||||
</div>
|
||||
</div>
|
||||
</Unstyled>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeBuilder;
|
||||
24
src/docs/docs-components/ThemeBuilder/ThemeSwitch/Switch.jsx
Normal file
24
src/docs/docs-components/ThemeBuilder/ThemeSwitch/Switch.jsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Unstyled } from '@storybook/blocks';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const ThemeSwitch = ({ themeName, setThemeName }) => {
|
||||
return (
|
||||
<div className={classes.switch}>
|
||||
<p>Choose a base theme:</p>
|
||||
<div className="container">
|
||||
<button
|
||||
className={themeName === 'light' ? 'active' : ''}
|
||||
onClick={() => setThemeName('light')}
|
||||
><span className="material-symbols-outlined">light_mode</span></button>
|
||||
<button
|
||||
className={themeName === 'dark' ? 'active' : ''}
|
||||
onClick={() => setThemeName('dark')}
|
||||
><span className="material-symbols-outlined">dark_mode</span></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemeSwitch;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
.switch :global {
|
||||
margin-bottom: 12px;
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
div {
|
||||
border: 1px solid #999;
|
||||
display: inline-block;
|
||||
border-radius: 50px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
button {
|
||||
padding: 5px 22px;
|
||||
background-color: #efefef;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
span {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/docs/docs-components/ThemeBuilder/styles.module.scss
Normal file
20
src/docs/docs-components/ThemeBuilder/styles.module.scss
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.themebuilder :global {
|
||||
margin: 2rem 0;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr;
|
||||
gap: 20px;
|
||||
|
||||
div.column {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #ddd;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
]
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const ThemeBuilder = (props) => (
|
||||
<button>Build</button>
|
||||
);
|
||||
|
||||
export default ThemeBuilder;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Meta } from '@storybook/addon-docs';
|
||||
import { parameters } from '$docs/utils/docsPage.js';
|
||||
|
||||
import ThemeBuilder from './ThemeBuilder.jsx';
|
||||
import ThemeBuilder from './../docs-components/ThemeBuilder/ThemeBuilder.jsx';
|
||||
|
||||
<Meta title="Theming/Theme builder" parameters={{ ...parameters }} />
|
||||
|
||||
# Theme builder
|
||||
|
||||
TK...
|
||||
Use this tool to customise your page's theme using the `Theme` component.
|
||||
|
||||
<ThemeBuilder />
|
||||
|
|
|
|||
33
yarn.lock
33
yarn.lock
|
|
@ -3448,6 +3448,11 @@ clone@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
||||
|
||||
color-convert@^0.5.2:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
|
||||
integrity sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
|
@ -3467,7 +3472,7 @@ color-name@1.1.3:
|
|||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
||||
|
||||
color-name@~1.1.4:
|
||||
color-name@^1.1.4, color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
|
@ -3477,6 +3482,11 @@ color-support@^1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
|
||||
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
|
||||
|
||||
colord@^2.9.3:
|
||||
version "2.9.3"
|
||||
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
|
||||
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
|
||||
|
||||
colorette@^2.0.19:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
|
|
@ -3622,6 +3632,20 @@ crypto-random-string@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
|
||||
|
||||
css-color-converter@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/css-color-converter/-/css-color-converter-2.0.0.tgz#70c00fa451a19675e2808f28de9be360c84db5fb"
|
||||
integrity sha512-oLIG2soZz3wcC3aAl/7Us5RS8Hvvc6I8G8LniF/qfMmrm7fIKQ8RIDDRZeKyGL2SrWfNqYspuLShbnjBMVWm8g==
|
||||
dependencies:
|
||||
color-convert "^0.5.2"
|
||||
color-name "^1.1.4"
|
||||
css-unit-converter "^1.1.2"
|
||||
|
||||
css-unit-converter@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
|
||||
integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
|
|
@ -3698,6 +3722,11 @@ deep-is@^0.1.3:
|
|||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
||||
deep-object-diff@^1.1.9:
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.9.tgz#6df7ef035ad6a0caa44479c536ed7b02570f4595"
|
||||
integrity sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA==
|
||||
|
||||
deepmerge@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
|
|
@ -7261,7 +7290,7 @@ raw-body@2.5.1:
|
|||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
react-colorful@^5.1.2:
|
||||
react-colorful@^5.1.2, react-colorful@^5.6.1:
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b"
|
||||
integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
|
||||
|
|
|
|||
Loading…
Reference in a new issue