diff --git a/package.json b/package.json index 90c7a725..d1bb3680 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/docs/docs-components/ThemeBuilder/Customiser/ColourPicker.jsx b/src/docs/docs-components/ThemeBuilder/Customiser/ColourPicker.jsx new file mode 100644 index 00000000..3b25dc54 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/Customiser/ColourPicker.jsx @@ -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 ( +
+ + +
+ ) +} + +export default ColourPicker; diff --git a/src/docs/docs-components/ThemeBuilder/Customiser/Customiser.jsx b/src/docs/docs-components/ThemeBuilder/Customiser/Customiser.jsx new file mode 100644 index 00000000..093c3589 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/Customiser/Customiser.jsx @@ -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 ( +
+

Pick parts of the theme to customise:

+ {Object.entries(theme).map(([key, value]) => { + const props = { + theme, + setTheme, + themeName: themeName, + name: key, + map: key, + value, + key: themeName + key, + }; + return ; + })} +
+ ); +} + +export default Customiser; diff --git a/src/docs/docs-components/ThemeBuilder/Customiser/Key.jsx b/src/docs/docs-components/ThemeBuilder/Customiser/Key.jsx new file mode 100644 index 00000000..eae0bde5 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/Customiser/Key.jsx @@ -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 ( +
+ + + {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 ; + return ; + })} +
+ ); +} + +export default Key; diff --git a/src/docs/docs-components/ThemeBuilder/Customiser/Value.jsx b/src/docs/docs-components/ThemeBuilder/Customiser/Value.jsx new file mode 100644 index 00000000..5bfae2de --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/Customiser/Value.jsx @@ -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 onChange(e.target.value)}/>; + // Colour type + if (!/var\(.*\)/i.test(value) && CSS.supports('color', value)) return ( + + ); + // Text for the rest... + return 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 ( +
+ + {isOpen && ( +
+ +
+ )} +
+ ); +} + +export default Value; \ No newline at end of file diff --git a/src/docs/docs-components/ThemeBuilder/Customiser/styles.module.scss b/src/docs/docs-components/ThemeBuilder/Customiser/styles.module.scss new file mode 100644 index 00000000..0d72be99 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/Customiser/styles.module.scss @@ -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; + } +} diff --git a/src/docs/docs-components/ThemeBuilder/NewTheme/NewTheme.jsx b/src/docs/docs-components/ThemeBuilder/NewTheme/NewTheme.jsx new file mode 100644 index 00000000..de2996eb --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/NewTheme/NewTheme.jsx @@ -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 ( +
+

Use the code below to adapt the Theme component for your new design:

+ + {` + {/*...*/} + + `} + +
+ ); +} + +export default NewTheme; diff --git a/src/docs/docs-components/ThemeBuilder/NewTheme/styles.module.scss b/src/docs/docs-components/ThemeBuilder/NewTheme/styles.module.scss new file mode 100644 index 00000000..c75d2d51 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/NewTheme/styles.module.scss @@ -0,0 +1,9 @@ +.newtheme { + position: sticky; + top: 10px; + p { + font-size: 14px; + line-height: 18px; + color: #666; + } +} diff --git a/src/docs/docs-components/ThemeBuilder/ThemeBuilder.jsx b/src/docs/docs-components/ThemeBuilder/ThemeBuilder.jsx new file mode 100644 index 00000000..503b7d1d --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/ThemeBuilder.jsx @@ -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 ( + +
+
+ + +
+
+ +
+
+
+ ); +} + +export default ThemeBuilder; diff --git a/src/docs/docs-components/ThemeBuilder/ThemeSwitch/Switch.jsx b/src/docs/docs-components/ThemeBuilder/ThemeSwitch/Switch.jsx new file mode 100644 index 00000000..41ae4780 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/ThemeSwitch/Switch.jsx @@ -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 ( +
+

Choose a base theme:

+
+ + +
+
+ ); +} + +export default ThemeSwitch; diff --git a/src/docs/docs-components/ThemeBuilder/ThemeSwitch/styles.module.scss b/src/docs/docs-components/ThemeBuilder/ThemeSwitch/styles.module.scss new file mode 100644 index 00000000..bab6fa59 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/ThemeSwitch/styles.module.scss @@ -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; + } + } +} diff --git a/src/docs/docs-components/ThemeBuilder/styles.module.scss b/src/docs/docs-components/ThemeBuilder/styles.module.scss new file mode 100644 index 00000000..3d1b1988 --- /dev/null +++ b/src/docs/docs-components/ThemeBuilder/styles.module.scss @@ -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; + } +} diff --git a/src/docs/theme-builder/.eslintrc.cjs b/src/docs/theme-builder/.eslintrc.cjs deleted file mode 100644 index ed3da2bc..00000000 --- a/src/docs/theme-builder/.eslintrc.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ] -}; diff --git a/src/docs/theme-builder/ThemeBuilder.jsx b/src/docs/theme-builder/ThemeBuilder.jsx deleted file mode 100644 index 665766a1..00000000 --- a/src/docs/theme-builder/ThemeBuilder.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -const ThemeBuilder = (props) => ( - -); - -export default ThemeBuilder; diff --git a/src/docs/theme-builder/theme-builder.stories.mdx b/src/docs/theme-builder/theme-builder.stories.mdx index 4959e421..d25b3031 100644 --- a/src/docs/theme-builder/theme-builder.stories.mdx +++ b/src/docs/theme-builder/theme-builder.stories.mdx @@ -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'; # Theme builder -TK... +Use this tool to customise your page's theme using the `Theme` component. diff --git a/yarn.lock b/yarn.lock index 07de7fdd..9c22f6b4 100644 --- a/yarn.lock +++ b/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==