mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-21 12:08:03 +02:00
feat(v2): dark mode toggle customization (#3127)
* Add dark/light style fields to config * Update yarn.lock * Remove css content * Add documentation * Add icon fields to toggle component * Add config validation fields * Remove changes from docusaurus.config * Add unicode documentation example * Fix default values * Add color mode config default * Add lodash to theme-classic * Change themeConfigSchema name to match other packages * Add themeConfig color-mode tests * Add default config merge function * Remove unneeded object merging * Add more documentation
This commit is contained in:
parent
e442ac95ee
commit
53b28d2bb2
7 changed files with 171 additions and 24 deletions
|
@ -20,6 +20,7 @@
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"copy-text-to-clipboard": "^2.2.0",
|
"copy-text-to-clipboard": "^2.2.0",
|
||||||
"infima": "0.2.0-alpha.12",
|
"infima": "0.2.0-alpha.12",
|
||||||
|
"lodash": "^4.17.19",
|
||||||
"parse-numeric-range": "^0.0.2",
|
"parse-numeric-range": "^0.0.2",
|
||||||
"prism-react-renderer": "^1.1.0",
|
"prism-react-renderer": "^1.1.0",
|
||||||
"prismjs": "^1.20.0",
|
"prismjs": "^1.20.0",
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
|
||||||
|
const {
|
||||||
|
validateThemeConfig,
|
||||||
|
DEFAULT_COLOR_MODE_CONFIG,
|
||||||
|
} = require('../validateThemeConfig');
|
||||||
|
|
||||||
|
const mergeDefault = (config) => merge({}, DEFAULT_COLOR_MODE_CONFIG, config);
|
||||||
|
|
||||||
|
function testValidateThemeConfig(themeConfig) {
|
||||||
|
function validate(schema, cfg) {
|
||||||
|
const {value, error} = schema.validate(cfg, {
|
||||||
|
convert: false,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateThemeConfig({themeConfig, validate});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('color mode config', () => {
|
||||||
|
test('minimal config', () => {
|
||||||
|
const colorMode = {
|
||||||
|
switchConfig: {
|
||||||
|
darkIcon: '🌙',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(testValidateThemeConfig({colorMode})).toEqual({
|
||||||
|
colorMode: mergeDefault(colorMode),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('max config', () => {
|
||||||
|
const colorMode = {
|
||||||
|
defaultMode: 'dark',
|
||||||
|
disableSwitch: false,
|
||||||
|
respectPrefersColorScheme: true,
|
||||||
|
switchConfig: {
|
||||||
|
darkIcon: '🌙',
|
||||||
|
darkIconStyle: {
|
||||||
|
marginTop: '1px',
|
||||||
|
marginLeft: '2px',
|
||||||
|
},
|
||||||
|
lightIcon: '☀️',
|
||||||
|
lightIconStyle: {
|
||||||
|
marginLeft: '1px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(testValidateThemeConfig({colorMode})).toEqual({
|
||||||
|
colorMode: mergeDefault(colorMode),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('undefined config', () => {
|
||||||
|
const colorMode = undefined;
|
||||||
|
expect(testValidateThemeConfig({colorMode})).toEqual({
|
||||||
|
colorMode: mergeDefault(colorMode),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty config', () => {
|
||||||
|
const colorMode = {};
|
||||||
|
expect(testValidateThemeConfig({colorMode})).toEqual({
|
||||||
|
colorMode: mergeDefault(colorMode),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty switch config', () => {
|
||||||
|
const colorMode = {
|
||||||
|
switchConfig: {},
|
||||||
|
};
|
||||||
|
expect(testValidateThemeConfig({colorMode})).toEqual({
|
||||||
|
colorMode: mergeDefault(colorMode),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const Module = require('module');
|
const Module = require('module');
|
||||||
const ThemeConfigSchema = require('./themeConfigSchema');
|
const {validateThemeConfig} = require('./validateThemeConfig');
|
||||||
|
|
||||||
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
const createRequire = Module.createRequire || Module.createRequireFromPath;
|
||||||
const requireFromDocusaurusCore = createRequire(
|
const requireFromDocusaurusCore = createRequire(
|
||||||
|
@ -120,6 +120,4 @@ module.exports = function (context, options) {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.validateThemeConfig = ({validate, themeConfig}) => {
|
module.exports.validateThemeConfig = validateThemeConfig;
|
||||||
return validate(ThemeConfigSchema, themeConfig);
|
|
||||||
};
|
|
||||||
|
|
|
@ -13,17 +13,40 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
const Moon = () => <span className={clsx(styles.toggle, styles.moon)} />;
|
const Dark = ({icon, style}) => (
|
||||||
const Sun = () => <span className={clsx(styles.toggle, styles.sun)} />;
|
<span className={clsx(styles.toggle, styles.dark)} style={style}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
const Light = ({icon, style}) => (
|
||||||
|
<span className={clsx(styles.toggle, styles.light)} style={style}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
export default function (props: ComponentProps<typeof Toggle>): JSX.Element {
|
export default function (props: ComponentProps<typeof Toggle>): JSX.Element {
|
||||||
const {isClient} = useDocusaurusContext();
|
const {
|
||||||
|
siteConfig: {
|
||||||
|
themeConfig: {
|
||||||
|
colorMode: {
|
||||||
|
switchConfig: {
|
||||||
|
darkIcon,
|
||||||
|
darkIconStyle,
|
||||||
|
lightIcon,
|
||||||
|
lightIconStyle,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isClient
|
||||||
|
} = useDocusaurusContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toggle
|
<Toggle
|
||||||
disabled={!isClient}
|
disabled={!isClient}
|
||||||
icons={{
|
icons={{
|
||||||
checked: <Moon />,
|
checked: <Dark icon={darkIcon} style={darkIconStyle} />,
|
||||||
unchecked: <Sun />,
|
unchecked: <Light icon={lightIcon} style={lightIconStyle} />,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,12 +16,6 @@
|
||||||
.toggle::before {
|
.toggle::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
.moon::before {
|
|
||||||
content: '\1F31C';
|
|
||||||
}
|
|
||||||
.sun::before {
|
|
||||||
content: '\1F31E';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* styles for React Toggle
|
* styles for React Toggle
|
||||||
|
|
|
@ -7,6 +7,19 @@
|
||||||
|
|
||||||
const Joi = require('@hapi/joi');
|
const Joi = require('@hapi/joi');
|
||||||
|
|
||||||
|
const DEFAULT_COLOR_MODE_CONFIG = {
|
||||||
|
defaultMode: 'light',
|
||||||
|
disableSwitch: false,
|
||||||
|
respectPrefersColorScheme: false,
|
||||||
|
switchConfig: {
|
||||||
|
darkIcon: '🌜',
|
||||||
|
darkIconStyle: {},
|
||||||
|
lightIcon: '🌞',
|
||||||
|
lightIconStyle: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
exports.DEFAULT_COLOR_MODE_CONFIG = DEFAULT_COLOR_MODE_CONFIG;
|
||||||
|
|
||||||
const NavbarItemPosition = Joi.string().equal('left', 'right').default('left');
|
const NavbarItemPosition = Joi.string().equal('left', 'right').default('left');
|
||||||
|
|
||||||
// TODO we should probably create a custom navbar item type "dropdown"
|
// TODO we should probably create a custom navbar item type "dropdown"
|
||||||
|
@ -102,14 +115,28 @@ const NavbarItemSchema = Joi.object().when('type', {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ColorModeSchema = Joi.object({
|
const ColorModeSchema = Joi.object({
|
||||||
defaultMode: Joi.string().equal('dark', 'light').default('light'),
|
defaultMode: Joi.string()
|
||||||
disableSwitch: Joi.bool().default(false),
|
.equal('dark', 'light')
|
||||||
respectPrefersColorScheme: Joi.bool().default(false),
|
.default(DEFAULT_COLOR_MODE_CONFIG.defaultMode),
|
||||||
}).default({
|
disableSwitch: Joi.bool().default(DEFAULT_COLOR_MODE_CONFIG.disableSwitch),
|
||||||
defaultMode: 'light',
|
respectPrefersColorScheme: Joi.bool().default(
|
||||||
disableSwitch: false,
|
DEFAULT_COLOR_MODE_CONFIG.respectPrefersColorScheme,
|
||||||
respectPrefersColorScheme: false,
|
),
|
||||||
});
|
switchConfig: Joi.object({
|
||||||
|
darkIcon: Joi.string().default(
|
||||||
|
DEFAULT_COLOR_MODE_CONFIG.switchConfig.darkIcon,
|
||||||
|
),
|
||||||
|
darkIconStyle: Joi.object().default(
|
||||||
|
DEFAULT_COLOR_MODE_CONFIG.switchConfig.darkIconStyle,
|
||||||
|
),
|
||||||
|
lightIcon: Joi.string().default(
|
||||||
|
DEFAULT_COLOR_MODE_CONFIG.switchConfig.lightIcon,
|
||||||
|
),
|
||||||
|
lightIconStyle: Joi.object().default(
|
||||||
|
DEFAULT_COLOR_MODE_CONFIG.switchConfig.lightIconStyle,
|
||||||
|
),
|
||||||
|
}).default(DEFAULT_COLOR_MODE_CONFIG.switchConfig),
|
||||||
|
}).default(DEFAULT_COLOR_MODE_CONFIG);
|
||||||
|
|
||||||
const FooterLinkItemSchema = Joi.object({
|
const FooterLinkItemSchema = Joi.object({
|
||||||
to: Joi.string(),
|
to: Joi.string(),
|
||||||
|
@ -178,4 +205,6 @@ const ThemeConfigSchema = Joi.object({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = ThemeConfigSchema;
|
exports.validateThemeConfig = ({validate, themeConfig}) => {
|
||||||
|
return validate(ThemeConfigSchema, themeConfig);
|
||||||
|
};
|
|
@ -158,6 +158,22 @@ Example:
|
||||||
```js title="docusaurus.config.js"
|
```js title="docusaurus.config.js"
|
||||||
module.exports = {
|
module.exports = {
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
|
colorMode: {
|
||||||
|
defaultMode: 'light',
|
||||||
|
disableSwitch: false,
|
||||||
|
respectPrefersColorScheme: true,
|
||||||
|
switchConfig: {
|
||||||
|
darkIcon: '🌙',
|
||||||
|
darkIconStyle: { // Style object passed to inline CSS
|
||||||
|
// For more information about styling options visit: https://reactjs.org/docs/dom-elements.html#style
|
||||||
|
marginLeft: '2px',
|
||||||
|
},
|
||||||
|
lightIcon: '\u2600',
|
||||||
|
lightIconStyle: {
|
||||||
|
marginLeft: '1px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
navbar: {
|
navbar: {
|
||||||
title: 'Site Title',
|
title: 'Site Title',
|
||||||
logo: {
|
logo: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue