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