theme builder. closes #93

This commit is contained in:
Jon McClure 2023-07-09 00:53:28 +01:00
parent f949fefb07
commit 69751eeb6e
16 changed files with 411 additions and 17 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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;

View 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;

View 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;

View file

@ -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;
}
}

View 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;

View file

@ -0,0 +1,9 @@
.newtheme {
position: sticky;
top: 10px;
p {
font-size: 14px;
line-height: 18px;
color: #666;
}
}

View 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;

View 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;

View file

@ -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;
}
}
}

View 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;
}
}

View file

@ -1,6 +0,0 @@
module.exports = {
"extends": [
"eslint:recommended",
"plugin:react/recommended"
]
};

View file

@ -1,7 +0,0 @@
import React from 'react';
const ThemeBuilder = (props) => (
<button>Build</button>
);
export default ThemeBuilder;

View file

@ -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 />

View file

@ -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==