diff --git a/CHANGELOG.md b/CHANGELOG.md index 47d8d6f8..9dc02e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ _Note: Gaps between patch versions are faulty, broken or test releases._ * Prevent `TypeError` when `assets` or `modules` are undefined in `analyzer.js` ([#679](https://github.com/webpack-contrib/webpack-bundle-analyzer/pull/679) by [@Srushti-33](https://github.com/Srushti-33)) +* **New Feature** + * Add optional dark/light mode toggle ([#683](https://github.com/webpack/webpack-bundle-analyzer/pull/683) by [@theEquinoxDev](https://github.com/theEquinoxDev)) + + ## 5.0.1 * **Bug Fix** diff --git a/client/assets/icon-moon.svg b/client/assets/icon-moon.svg new file mode 100644 index 00000000..6d6762cc --- /dev/null +++ b/client/assets/icon-moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/assets/icon-sun.svg b/client/assets/icon-sun.svg new file mode 100644 index 00000000..c41a0594 --- /dev/null +++ b/client/assets/icon-sun.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/components/Button.css b/client/components/Button.css index 56e40af6..618baa60 100644 --- a/client/components/Button.css +++ b/client/components/Button.css @@ -1,19 +1,20 @@ .button { - background: #fff; - border: 1px solid #aaa; + background: var(--bg-primary); + border: 1px solid var(--border-color); border-radius: 4px; cursor: pointer; display: inline-block; font: var(--main-font); outline: none; padding: 5px 7px; - transition: background .3s ease; + transition: background .3s ease, border-color .3s ease, color .3s ease; white-space: nowrap; + color: var(--text-primary); } .button:focus, .button:hover { - background: #ffefd7; + background: var(--hover-bg); } .button.active { diff --git a/client/components/Dropdown.css b/client/components/Dropdown.css index f3b78e8a..fe683ace 100644 --- a/client/components/Dropdown.css +++ b/client/components/Dropdown.css @@ -10,12 +10,14 @@ } .input { - border: 1px solid #aaa; + border: 1px solid var(--border-color); border-radius: 4px; display: block; width: 100%; - color: #7f7f7f; + color: var(--text-secondary); height: 27px; + background: var(--bg-primary); + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; } .option { diff --git a/client/components/Icon.css b/client/components/Icon.css index 729f5580..480d6e8f 100644 --- a/client/components/Icon.css +++ b/client/components/Icon.css @@ -1,4 +1,9 @@ .icon { background: no-repeat center/contain; display: inline-block; + filter: invert(0); +} + +[data-theme="dark"] .icon { + filter: invert(1); } diff --git a/client/components/Icon.jsx b/client/components/Icon.jsx index 5a2371d4..5cb6415a 100644 --- a/client/components/Icon.jsx +++ b/client/components/Icon.jsx @@ -4,6 +4,8 @@ import PureComponent from '../lib/PureComponent'; import iconArrowRight from '../assets/icon-arrow-right.svg'; import iconPin from '../assets/icon-pin.svg'; +import iconMoon from '../assets/icon-moon.svg'; +import iconSun from '../assets/icon-sun.svg'; const ICONS = { 'arrow-right': { @@ -13,6 +15,14 @@ const ICONS = { 'pin': { src: iconPin, size: [12, 18] + }, + 'moon': { + src: iconMoon, + size: [24, 24] + }, + 'sun': { + src: iconSun, + size: [24, 24] } }; diff --git a/client/components/Search.css b/client/components/Search.css index c85f6409..8551aa42 100644 --- a/client/components/Search.css +++ b/client/components/Search.css @@ -13,11 +13,19 @@ } .input { - border: 1px solid #aaa; + border: 1px solid var(--border-color); border-radius: 4px; display: block; flex: 1; padding: 5px; + background: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; +} + +.input:focus { + outline: none; + border-color: var(--text-secondary); } .clear { diff --git a/client/components/Sidebar.css b/client/components/Sidebar.css index 54241e06..956460a6 100644 --- a/client/components/Sidebar.css +++ b/client/components/Sidebar.css @@ -1,13 +1,14 @@ @value toggleTime: 200ms; .container { - background: #fff; + background: var(--bg-primary); border: none; - border-right: 1px solid #aaa; + border-right: 1px solid var(--border-color); box-sizing: border-box; max-width: calc(50% - 10px); opacity: 0.95; z-index: 1; + transition: background-color 0.3s ease, border-color 0.3s ease; } .container:not(.hidden) { @@ -45,6 +46,16 @@ padding: 0; } +.container :global(.themeToggle) { + position: absolute; + top: 10px; + left: 15px; + z-index: 10; + height: 26px; + width: 27px; + padding: 0; +} + .pinButton, .toggleButton { cursor: pointer; diff --git a/client/components/Sidebar.jsx b/client/components/Sidebar.jsx index 66ac044b..82ffd70a 100644 --- a/client/components/Sidebar.jsx +++ b/client/components/Sidebar.jsx @@ -4,6 +4,7 @@ import cls from 'classnames'; import s from './Sidebar.css'; import Button from './Button'; import Icon from './Icon'; +import ThemeToggle from './ThemeToggle'; const toggleTime = parseInt(s.toggleTime); @@ -48,6 +49,7 @@ export default class Sidebar extends Component { className={className} onClick={this.handleClick} onMouseLeave={this.handleMouseLeave}> + {visible && + ); + } + + handleToggle = () => { + store.toggleDarkMode(); + } +} diff --git a/client/store.js b/client/store.js index 1695db11..59bbeaf3 100644 --- a/client/store.js +++ b/client/store.js @@ -12,6 +12,19 @@ export class Store { @observable defaultSize; @observable selectedSize; @observable showConcatenatedModulesContent = (localStorage.getItem('showConcatenatedModulesContent') === true); + @observable darkMode = (() => { + const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + + try { + const saved = localStorage.getItem('darkMode'); + if (saved !== null) return saved === 'true'; + } catch (e) { + // Some browsers might not have localStorage available and we can fail silently + } + + return systemPrefersDark; + })(); + setModules(modules) { walkModules(modules, module => { @@ -179,6 +192,24 @@ export class Store { return filteredModules; }, []); } + + toggleDarkMode() { + this.darkMode = !this.darkMode; + try { + localStorage.setItem('darkMode', this.darkMode); + } catch (e) { + // Some browsers might not have localStorage available and we can fail silently + } + this.updateTheme(); + } + + updateTheme() { + if (this.darkMode) { + document.documentElement.setAttribute('data-theme', 'dark'); + } else { + document.documentElement.removeAttribute('data-theme'); + } + } } export const store = new Store(); diff --git a/client/viewer.css b/client/viewer.css index f10404d8..7f9876a2 100644 --- a/client/viewer.css +++ b/client/viewer.css @@ -1,5 +1,24 @@ :root { --main-font: normal 11px Verdana, sans-serif; + --bg-primary: #fff; + --bg-secondary: #f5f5f5; + --text-primary: #000; + --text-secondary: #666; + --border-color: #aaa; + --border-light: #ddd; + --shadow: rgba(0, 0, 0, 0.1); + --hover-bg: rgba(0, 0, 0, 0.05); +} + +[data-theme="dark"] { + --bg-primary: #1e1e1e; + --bg-secondary: #252525; + --text-primary: #e0e0e0; + --text-secondary: #a0a0a0; + --border-color: #404040; + --border-light: #333; + --shadow: rgba(0, 0, 0, 0.3); + --hover-bg: rgba(255, 255, 255, 0.05); } :global html, @@ -10,6 +29,9 @@ overflow: hidden; padding: 0; width: 100%; + background: var(--bg-primary); + color: var(--text-primary); + transition: background-color 0.3s ease, color 0.3s ease; } :global body.resizing { diff --git a/client/viewer.jsx b/client/viewer.jsx index 19f32c4e..5fa8ea95 100644 --- a/client/viewer.jsx +++ b/client/viewer.jsx @@ -21,6 +21,7 @@ window.addEventListener('load', () => { store.defaultSize = `${window.defaultSizes}Size`; store.setModules(window.chartData); store.setEntrypoints(window.entrypoints); + store.updateTheme(); render( , document.getElementById('app')