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')