From dc59b7de5a8e1d2a3156bc61bf37ed51b6a4c08d Mon Sep 17 00:00:00 2001 From: Kush Munjal Date: Sun, 12 Jul 2026 19:33:47 +0530 Subject: [PATCH 1/3] addded dark mode issue 1702 --- package-lock.json | 38 +++--- package.json | 1 + ...sgtpooki+react-app-rewired-esm+2.2.1.patch | 19 +++ public/locales/en/settings.json | 1 + src/App.js | 15 ++- src/components/box/Box.js | 2 +- src/components/button/button.tsx | 2 +- .../is-not-connected/is-not-connected.tsx | 4 +- src/components/modal/modal.test.tsx | 11 +- src/components/modal/modal.tsx | 3 +- src/components/progress-bar/ProgressBar.js | 2 +- .../theme-selector/ThemeSelector.js | 34 +++++ .../theme-selector/ThemeSelector.stories.js | 25 ++++ .../theme-selector/ThemeSelector.test.js | 24 ++++ .../theme-selector/TopBarThemeToggle.js | 52 ++++++++ src/contexts/theme-context.js | 58 +++++++++ src/files/breadcrumbs/Breadcrumbs.js | 2 +- src/files/dropdown/Dropdown.js | 2 +- .../file-import-status/FileImportStatus.js | 3 +- .../modals/publish-modal/PublishModal.js | 2 +- src/files/sort-dropdown/SortDropdown.js | 2 +- src/index.css | 118 ++++++++++++++++++ src/peers/WorldMap/WorldMap.js | 2 +- src/settings/SettingsPage.js | 9 +- src/status/NodeBandwidthChart.js | 88 +++++++------ src/status/Speedometer.js | 9 +- 26 files changed, 446 insertions(+), 82 deletions(-) create mode 100644 patches/@sgtpooki+react-app-rewired-esm+2.2.1.patch create mode 100644 src/components/theme-selector/ThemeSelector.js create mode 100644 src/components/theme-selector/ThemeSelector.stories.js create mode 100644 src/components/theme-selector/ThemeSelector.test.js create mode 100644 src/components/theme-selector/TopBarThemeToggle.js create mode 100644 src/contexts/theme-context.js diff --git a/package-lock.json b/package-lock.json index 3fc7ad359..0560cd463 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@loadable/component": "^5.14.1", "@multiformats/multiaddr": "^12.5.1", + "@multiformats/multiaddr-matcher": "^2.0.2", "@multiformats/multiaddr-to-uri": "^11.0.2", "@tableflip/react-dropdown": "^1.3.0", "brace": "^0.11.1", @@ -3821,6 +3822,17 @@ "progress-events": "^1.0.0" } }, + "node_modules/@helia/block-brokers/node_modules/@multiformats/multiaddr-matcher": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-matcher/-/multiaddr-matcher-1.8.0.tgz", + "integrity": "sha512-tR/HFhDucXjvwCef5lfXT7kikqR2ffUjliuYlg/RKYGPySVKVlvrDufz86cIuHNc+i/fNR16FWWgD/pMJ6RW4w==", + "license": "Apache-2.0 OR MIT", + "dependencies": { + "@chainsafe/is-ip": "^2.0.1", + "@multiformats/multiaddr": "^12.0.0", + "multiformats": "^13.0.0" + } + }, "node_modules/@helia/block-brokers/node_modules/@multiformats/multiaddr-to-uri": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-to-uri/-/multiaddr-to-uri-10.1.2.tgz", @@ -6448,14 +6460,12 @@ } }, "node_modules/@multiformats/multiaddr-matcher": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-matcher/-/multiaddr-matcher-1.4.0.tgz", - "integrity": "sha512-Riu+JbTolhzAEgZH3xexLKVn2Oe+xUEPCNHuURqKcE9Pa3RxwsuhldykUWmbsDifXOV4TJCc1LGADFHmpS1y5w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@multiformats/multiaddr-matcher/-/multiaddr-matcher-2.0.2.tgz", + "integrity": "sha512-si7EZCI93mfBJKKRkh+u2bB9W6W5APVN3XfdwuseEJ0OS7ysg0Jno9SuAi0bRzsl5OEFESoF71SjsRqgp8PXAA==", "license": "Apache-2.0 OR MIT", "dependencies": { - "@chainsafe/is-ip": "^2.0.1", - "@multiformats/multiaddr": "^12.0.0", - "multiformats": "^13.0.0" + "@multiformats/multiaddr": "^12.0.0" } }, "node_modules/@multiformats/multiaddr-to-uri": { @@ -16595,7 +16605,7 @@ "version": "1.7.42", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.42.tgz", "integrity": "sha512-iQrRk3SKndQZ4ptJv1rzeQSiCYQIhMjiO97QXOlCcCoaazOLKPnLnXzU4Kv0FuBFyYfG2FE94BoR0XI2BN02qw==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -16637,7 +16647,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16654,7 +16663,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16671,7 +16679,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -16688,7 +16695,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16705,7 +16711,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16722,7 +16727,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16739,7 +16743,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16756,7 +16759,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16773,7 +16775,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16790,7 +16791,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -16804,7 +16804,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@swc/jest": { @@ -16829,7 +16829,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.13.tgz", "integrity": "sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" diff --git a/package.json b/package.json index 8397638c1..816111c7b 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "@loadable/component": "^5.14.1", "@multiformats/multiaddr": "^12.5.1", + "@multiformats/multiaddr-matcher": "^2.0.2", "@multiformats/multiaddr-to-uri": "^11.0.2", "@tableflip/react-dropdown": "^1.3.0", "brace": "^0.11.1", diff --git a/patches/@sgtpooki+react-app-rewired-esm+2.2.1.patch b/patches/@sgtpooki+react-app-rewired-esm+2.2.1.patch new file mode 100644 index 000000000..09108c7e4 --- /dev/null +++ b/patches/@sgtpooki+react-app-rewired-esm+2.2.1.patch @@ -0,0 +1,19 @@ +diff --git a/node_modules/@sgtpooki/react-app-rewired-esm/config-overrides.js b/node_modules/@sgtpooki/react-app-rewired-esm/config-overrides.js +index 6791f7c..2a7255f 100644 +--- a/node_modules/@sgtpooki/react-app-rewired-esm/config-overrides.js ++++ b/node_modules/@sgtpooki/react-app-rewired-esm/config-overrides.js +@@ -1,4 +1,5 @@ + import { createRequire } from "node:module"; ++import { pathToFileURL } from "node:url"; + export const require = createRequire(import.meta.url); + + const { paths } = require("./index.cjs"); +@@ -6,7 +7,7 @@ const { paths } = require("./index.cjs"); + // before overrides scripts are read + require(paths.scriptVersion + "/config/env"); + +-let override = await import(paths.configOverrides + ".js"); ++let override = await import(pathToFileURL(paths.configOverrides + ".js").href); + override = override.default || override; + + const webpack = diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 9c320f9cd..d0203528e 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -13,6 +13,7 @@ "removeAutoUpload": "Disable Auto Upload" }, "language": "Language", + "themeDescription": "Choose your preferred color theme.", "analytics": "Analytics", "cliTutorMode": "CLI Tutor Mode", "config": "Kubo Config", diff --git a/src/App.js b/src/App.js index 46fc1c677..a612bf375 100644 --- a/src/App.js +++ b/src/App.js @@ -17,7 +17,9 @@ import ComponentLoader from './loader/ComponentLoader.js' import Notify from './components/notify/Notify.js' import Connected from './components/connected/Connected.js' import TourHelper from './components/tour/TourHelper.js' +import TopBarThemeToggle from './components/theme-selector/TopBarThemeToggle.js' import FilesExploreForm from './files/explore-form/files-explore-form.tsx' +import { ThemeProvider } from './contexts/theme-context.js' export class App extends Component { static propTypes = { @@ -62,18 +64,21 @@ export class App extends Component { render () { const { t, route: Page, ipfsReady, doFilesNavigateTo, routeInfo: { url }, connectDropTarget, canDrop, isOver, showTooltip } = this.props - return connectDropTarget( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -
+ return ( + + {connectDropTarget( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions +
{/* Tinted overlay that appears when dragging and dropping an item */} { canDrop && isOver &&
}
-
+
+ {!url.startsWith('/diagnostics') && }
@@ -102,6 +107,8 @@ export class App extends Component {
+ )} + ) } } diff --git a/src/components/box/Box.js b/src/components/box/Box.js index a7f81f0f0..161c711f9 100644 --- a/src/components/box/Box.js +++ b/src/components/box/Box.js @@ -13,7 +13,7 @@ export const Box = ({ children }) => { return ( -
+
{children} diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx index aaab0a1f7..7ca23b9f7 100644 --- a/src/components/button/button.tsx +++ b/src/components/button/button.tsx @@ -15,7 +15,7 @@ export interface ButtonProps extends Omit, type: ButtonProps['type']) => { if (danger) return 'bg-red fill-white white' - if (disabled) return 'bg-gray-muted fill-snow light-gray' + if (disabled) return 'bg-black-10 fill-charcoal-muted charcoal-muted' if (type === 'link') return 'link bg-transparent' return `${fill} ${bg} ${color}` } diff --git a/src/components/is-not-connected/is-not-connected.tsx b/src/components/is-not-connected/is-not-connected.tsx index 0bb1e4a05..f397b92a5 100644 --- a/src/components/is-not-connected/is-not-connected.tsx +++ b/src/components/is-not-connected/is-not-connected.tsx @@ -42,7 +42,7 @@ const IsNotConnected = () => { { !isSameOrigin && (
-
  • Is your Kubo RPC API configured to allow cross-origin (CORS) requests? If not, run these commands and then start your daemon from the terminal:
  • +
  • Is your Kubo RPC API configured to allow cross-origin (CORS) requests? If not, run these commands and then start your daemon from the terminal:
  • @@ -80,7 +80,7 @@ const IsNotConnected = () => {
    )} -
  • Is your Kubo RPC on a port other than 5001? If your node is configured with a custom RPC API address, enter it here.
  • +
  • Is your Kubo RPC on a port other than 5001? If your node is configured with a custom RPC API address, enter it here.
  • diff --git a/src/components/modal/modal.test.tsx b/src/components/modal/modal.test.tsx index d260b7ed9..56c7ed14b 100644 --- a/src/components/modal/modal.test.tsx +++ b/src/components/modal/modal.test.tsx @@ -84,14 +84,13 @@ describe('Modal', () => { it('renders with default props', () => { const { container } = render() const actionsElement = container.firstChild as HTMLElement - expect(actionsElement).toHaveClass('flex', 'justify-between', 'pa2') - expect(actionsElement).toHaveStyle({ backgroundColor: '#f4f6f8' }) + expect(actionsElement).toHaveClass('flex', 'justify-between', 'pa2', 'bg-snow') }) it('renders with custom justify prop', () => { const { container } = render() const actionsElement = container.firstChild as HTMLElement - expect(actionsElement).toHaveClass('flex', 'justify-center', 'pa2') + expect(actionsElement).toHaveClass('flex', 'justify-center', 'pa2', 'bg-snow') }) it('renders with custom className', () => { @@ -99,7 +98,7 @@ describe('Modal', () => { const { container } = render() const actionsElement = container.firstChild as HTMLElement expect(actionsElement).toHaveClass(customClass) - expect(actionsElement).toHaveClass('flex', 'justify-between', 'pa2') + expect(actionsElement).toHaveClass('flex', 'justify-between', 'pa2', 'bg-snow') }) it('renders children correctly', () => { @@ -121,8 +120,8 @@ describe('Modal', () => { it('applies default styling classes and background color', () => { const { container } = render() const actionsElement = container.firstChild as HTMLElement - expect(actionsElement).toHaveClass('flex', 'justify-between', 'pa2') - expect(actionsElement.className).toBe('flex justify-between pa2 ') + expect(actionsElement).toHaveClass('flex', 'justify-between', 'pa2', 'bg-snow') + expect(actionsElement.className).toBe('flex justify-between pa2 bg-snow ') }) }) diff --git a/src/components/modal/modal.tsx b/src/components/modal/modal.tsx index 270841005..b36cb6b60 100644 --- a/src/components/modal/modal.tsx +++ b/src/components/modal/modal.tsx @@ -31,8 +31,7 @@ export const ModalActions: React.FC = ({ ...props }) => (
    {children} diff --git a/src/components/progress-bar/ProgressBar.js b/src/components/progress-bar/ProgressBar.js index 4af66158c..b5d8a4072 100644 --- a/src/components/progress-bar/ProgressBar.js +++ b/src/components/progress-bar/ProgressBar.js @@ -4,7 +4,7 @@ import './ProgressBar.css' const ProgressBar = ({ bg, br, className, style, width, height, progress, time, ...props }) => { return ( -
    +
    {time ?
    :
    } diff --git a/src/components/theme-selector/ThemeSelector.js b/src/components/theme-selector/ThemeSelector.js new file mode 100644 index 000000000..ed56ce744 --- /dev/null +++ b/src/components/theme-selector/ThemeSelector.js @@ -0,0 +1,34 @@ +import React from 'react' +import { withTranslation } from 'react-i18next' +import { useTheme } from '../../contexts/theme-context.js' +import Title from '../../settings/Title.js' + +const ThemeSelector = ({ t }) => { + const { theme, setTheme } = useTheme() + + const onChange = (e) => { + setTheme(e.target.value) + } + + return ( +
    + {t('theme') || 'Theme'} +
    + + + {t('themeDescription') || 'Choose a theme or sync with your system preference.'} + +
    +
    + ) +} + +export default withTranslation('settings')(ThemeSelector) diff --git a/src/components/theme-selector/ThemeSelector.stories.js b/src/components/theme-selector/ThemeSelector.stories.js new file mode 100644 index 000000000..97c7acb72 --- /dev/null +++ b/src/components/theme-selector/ThemeSelector.stories.js @@ -0,0 +1,25 @@ +// @ts-check +import ThemeSelector from './ThemeSelector.js' +import { ThemeProvider } from '../../contexts/theme-context.js' + +/** + * @type {import('@storybook/react').Meta} + */ +export default { + title: 'Settings/Theme Selector', + component: ThemeSelector, + decorators: [ + (Story) => ( + +
    + +
    +
    + ) + ] +} + +/** + * @type {import('@storybook/react').StoryObj} + */ +export const Default = {} diff --git a/src/components/theme-selector/ThemeSelector.test.js b/src/components/theme-selector/ThemeSelector.test.js new file mode 100644 index 000000000..b6cc36518 --- /dev/null +++ b/src/components/theme-selector/ThemeSelector.test.js @@ -0,0 +1,24 @@ +import React from 'react' +import '@testing-library/jest-dom' +import { render, screen } from '@testing-library/react' +import ThemeSelector from './ThemeSelector.js' +import { ThemeProvider } from '../../contexts/theme-context.js' +import { I18nextProvider } from 'react-i18next' +import i18n from '../../i18n.js' + +describe('ThemeSelector', () => { + it('renders correctly', () => { + render( + + + + + + ) + + expect(screen.getByRole('combobox')).toBeInTheDocument() + expect(screen.getByText('Auto')).toBeInTheDocument() + expect(screen.getByText('Light')).toBeInTheDocument() + expect(screen.getByText('Dark')).toBeInTheDocument() + }) +}) diff --git a/src/components/theme-selector/TopBarThemeToggle.js b/src/components/theme-selector/TopBarThemeToggle.js new file mode 100644 index 000000000..e81b5ba36 --- /dev/null +++ b/src/components/theme-selector/TopBarThemeToggle.js @@ -0,0 +1,52 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useTheme } from '../../contexts/theme-context.js' +import StrokeMonitor from '../../icons/StrokeMonitor.js' + +const SunIcon = ({ className, width, height }) => ( + + + + + + + + + + + +) + +const MoonIcon = ({ className, width, height }) => ( + + + +) + +export const TopBarThemeToggle = () => { + const { theme, setTheme } = useTheme() + const { t } = useTranslation('settings') + + const cycleTheme = () => { + if (theme === 'auto') setTheme('light') + else if (theme === 'light') setTheme('dark') + else setTheme('auto') + } + + let Icon = StrokeMonitor + if (theme === 'light') Icon = SunIcon + if (theme === 'dark') Icon = MoonIcon + + return ( + + ) +} + +export default TopBarThemeToggle diff --git a/src/contexts/theme-context.js b/src/contexts/theme-context.js new file mode 100644 index 000000000..eb171dd84 --- /dev/null +++ b/src/contexts/theme-context.js @@ -0,0 +1,58 @@ +import React, { createContext, useContext, useEffect, useState } from 'react' + +export const ThemeContext = createContext({ + theme: 'auto', + setTheme: (/** @type {string} */ _newTheme) => {} +}) + +export const useTheme = () => useContext(ThemeContext) + +// Default theme is auto +const DEFAULT_THEME = 'auto' + +/** + * @param {Object} props + * @param {import('react').ReactNode} props.children + */ +export const ThemeProvider = ({ children }) => { + const [theme, setThemeState] = useState(() => { + return localStorage.getItem('ipfs-webui-theme') || DEFAULT_THEME + }) + + useEffect(() => { + const mediaQuery = window.matchMedia + ? window.matchMedia('(prefers-color-scheme: dark)') + : { matches: false, addEventListener: () => {}, removeEventListener: () => {} } + + const applyTheme = () => { + const isDark = theme === 'dark' || (theme === 'auto' && mediaQuery.matches) + document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light') + } + + applyTheme() + + const handleChange = () => { + if (theme === 'auto') { + applyTheme() + } + } + + mediaQuery.addEventListener('change', handleChange) + return () => { + mediaQuery.removeEventListener('change', handleChange) + } + }, [theme]) + /** + * @param {string} newTheme + */ + const setTheme = (newTheme) => { + localStorage.setItem('ipfs-webui-theme', newTheme) + setThemeState(newTheme) + } + + return ( + + {children} + + ) +} diff --git a/src/files/breadcrumbs/Breadcrumbs.js b/src/files/breadcrumbs/Breadcrumbs.js index 630ecd81b..08994a33f 100644 --- a/src/files/breadcrumbs/Breadcrumbs.js +++ b/src/files/breadcrumbs/Breadcrumbs.js @@ -87,7 +87,7 @@ const Breadcrumbs = ({ t, tReady, path, onClick, className, onContextMenuHandle, return (