From 2370e19c93f095f78edc9081d63d6ca5b9def4bb Mon Sep 17 00:00:00 2001 From: Chayan Das <01chayandas@gmail.com> Date: Sat, 7 Feb 2026 05:54:12 +0530 Subject: [PATCH] convert files page for dark mode --- public/locales/en/settings.json | 2 + src/App.js | 18 +- src/bundles/index.js | 4 +- src/bundles/theme.js | 96 ++++++ src/bundles/theme.test.js | 31 ++ .../analytics-toggle/AnalyticsToggle.js | 10 +- src/components/box/Box.js | 2 +- src/components/card/card.tsx | 8 +- src/components/checkbox/Checkbox.css | 4 +- .../experiments/ExperimentsPanel.js | 3 +- src/components/ipns-manager/IpnsManager.js | 2 +- src/components/modal/modal.tsx | 9 +- .../pinning-manager/PinningManager.js | 2 +- src/components/popover/Popover.js | 6 +- src/components/progress-bar/ProgressBar.js | 2 +- src/components/radio/radio.css | 4 +- src/components/theme-toggle/ThemeToggle.js | 78 +++++ src/components/tooltip/Tooltip.tsx | 11 +- src/components/tooltip/icon-tooltip.tsx | 8 +- src/diagnostics/dht-provide/Operations.tsx | 2 +- src/diagnostics/dht-provide/Schedule.tsx | 4 +- src/diagnostics/dht-provide/Workers.tsx | 6 +- src/diagnostics/diagnostics-content.tsx | 16 +- .../logs-screen/golog-level-autocomplete.tsx | 27 +- .../logs-screen/golog-level-section.tsx | 8 +- src/diagnostics/logs-screen/log-viewer.css | 2 +- src/files/breadcrumbs/Breadcrumbs.js | 12 +- src/files/context-menu/ContextMenu.js | 20 +- src/files/dropdown/Dropdown.js | 15 +- src/files/file-icon/FileIcon.js | 16 +- .../file-import-status/FileImportStatus.css | 10 +- .../file-import-status/FileImportStatus.js | 20 +- src/files/file-input/FileInput.js | 12 +- src/files/file/File.js | 12 +- src/files/files-grid/files-grid.tsx | 4 +- src/files/files-grid/grid-file.css | 31 +- src/files/header/Header.js | 10 +- .../info-boxes/add-files-info/AddFilesInfo.js | 2 +- .../companion-info/CompanionInfo.js | 2 +- src/files/pin-icon/PinIcon.js | 10 +- src/files/search-filter/SearchFilter.tsx | 8 +- src/files/selected-actions/SelectedActions.js | 42 +-- src/files/sort-dropdown/SortDropdown.js | 30 +- src/icons/StrokeMonitor.tsx | 46 ++- src/icons/StrokeMoon.tsx | 11 + src/icons/StrokeSun.tsx | 19 ++ src/index.css | 1 + src/lib/chart-colors.js | 59 ++++ src/lib/tours.js | 157 +++++---- src/navigation/NavBar.css | 16 +- src/peers/AddConnection/AddConnection.js | 2 +- src/peers/PeersPage.js | 2 +- src/peers/WorldMap/Map.js | 6 +- src/settings/SettingsPage.js | 11 +- src/status/CountryChart.js | 7 +- src/status/NetworkTraffic.js | 11 +- src/status/Speedometer.js | 9 +- src/status/StatusPage.js | 2 +- src/theme.css | 301 ++++++++++++++++++ 59 files changed, 990 insertions(+), 291 deletions(-) create mode 100644 src/bundles/theme.js create mode 100644 src/bundles/theme.test.js create mode 100644 src/components/theme-toggle/ThemeToggle.js create mode 100644 src/icons/StrokeMoon.tsx create mode 100644 src/icons/StrokeSun.tsx create mode 100644 src/lib/chart-colors.js create mode 100644 src/theme.css diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 98585c8e3..082ac04c2 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -13,6 +13,8 @@ "removeAutoUpload": "Disable Auto Upload" }, "language": "Language", + "theme": "Theme", + "themeDescription": "Choose between light, dark, or system theme (follows your operating system preference).", "analytics": "Analytics", "cliTutorMode": "CLI Tutor Mode", "config": "Kubo Config", diff --git a/src/App.js b/src/App.js index 46fc1c677..ae749eab6 100644 --- a/src/App.js +++ b/src/App.js @@ -17,6 +17,7 @@ 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 ThemeToggle from './components/theme-toggle/ThemeToggle.js' import FilesExploreForm from './files/explore-form/files-explore-form.tsx' export class App extends Component { @@ -69,16 +70,17 @@ export class App extends Component { { canDrop && isOver &&
}
-
+
{!url.startsWith('/diagnostics') && } +
-
+
{ (ipfsReady || url === '/welcome' || url.startsWith('/settings')) ? : @@ -93,7 +95,17 @@ export class App extends Component { { + if (typeof window === 'undefined') return THEMES.LIGHT + return window.matchMedia('(prefers-color-scheme: dark)').matches ? THEMES.DARK : THEMES.LIGHT +} + +const getInitialTheme = () => { + const saved = readSetting(THEME_KEY) + if (saved && Object.values(THEMES).includes(saved)) { + return saved + } + // Default to dark theme instead of system + return THEMES.DARK +} + +const applyTheme = (theme) => { + const effectiveTheme = theme === THEMES.SYSTEM ? getSystemTheme() : theme + document.documentElement.setAttribute('data-theme', effectiveTheme) + document.documentElement.classList.remove('theme-light', 'theme-dark') + document.documentElement.classList.add(`theme-${effectiveTheme}`) +} + +const bundle = { + name: 'theme', + + reducer: (state = getInitialTheme(), action) => { + if (action.type === 'THEME_SET') { + return action.payload + } + return state + }, + + selectTheme: state => state.theme, + + selectEffectiveTheme: createSelector( + 'selectTheme', + (theme) => { + return theme === THEMES.SYSTEM ? getSystemTheme() : theme + } + ), + + doSetTheme: (theme) => ({ dispatch }) => { + if (!Object.values(THEMES).includes(theme)) { + console.error(`Invalid theme: ${theme}`) + return + } + writeSetting(THEME_KEY, theme) + applyTheme(theme) + dispatch({ type: 'THEME_SET', payload: theme }) + }, + + doToggleTheme: () => ({ dispatch, store }) => { + const currentTheme = store.selectTheme() + const themes = [THEMES.LIGHT, THEMES.DARK, THEMES.SYSTEM] + const currentIndex = themes.indexOf(currentTheme) + const nextTheme = themes[(currentIndex + 1) % themes.length] + dispatch({ actionCreator: 'doSetTheme', args: [nextTheme] }) + }, + + init: (store) => { + // Apply initial theme + const theme = store.selectTheme() + applyTheme(theme) + + // Listen for system theme changes + if (typeof window !== 'undefined') { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const handleChange = () => { + const currentTheme = store.selectTheme() + if (currentTheme === THEMES.SYSTEM) { + applyTheme(THEMES.SYSTEM) + } + } + + // Modern browsers + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener('change', handleChange) + } else { + // Fallback for older browsers + mediaQuery.addListener(handleChange) + } + } + } +} + +export default bundle +export { THEMES } diff --git a/src/bundles/theme.test.js b/src/bundles/theme.test.js new file mode 100644 index 000000000..43009a09f --- /dev/null +++ b/src/bundles/theme.test.js @@ -0,0 +1,31 @@ +import themeBundle, { THEMES } from './theme.js' + +describe('theme bundle', () => { + it('should have correct name', () => { + expect(themeBundle.name).toBe('theme') + }) + + it('should export THEMES constants', () => { + expect(THEMES.LIGHT).toBe('light') + expect(THEMES.DARK).toBe('dark') + expect(THEMES.SYSTEM).toBe('system') + }) + + it('should have initial state', () => { + const state = themeBundle.reducer(undefined, {}) + expect([THEMES.LIGHT, THEMES.DARK, THEMES.SYSTEM]).toContain(state) + }) + + it('should handle THEME_SET action', () => { + const state = themeBundle.reducer(THEMES.LIGHT, { + type: 'THEME_SET', + payload: THEMES.DARK + }) + expect(state).toBe(THEMES.DARK) + }) + + it('should select theme from state', () => { + const state = { theme: THEMES.DARK } + expect(themeBundle.selectTheme(state)).toBe(THEMES.DARK) + }) +}) diff --git a/src/components/analytics-toggle/AnalyticsToggle.js b/src/components/analytics-toggle/AnalyticsToggle.js index 22e5ec473..527a53381 100644 --- a/src/components/analytics-toggle/AnalyticsToggle.js +++ b/src/components/analytics-toggle/AnalyticsToggle.js @@ -6,7 +6,7 @@ import Details from '../details/Details.js' const ExampleRequest = ({ url, method = 'GET' }) => { return ( -
+    
       {method} {url}
     
) @@ -17,7 +17,7 @@ const QueryParams = ({ url }) => { const params = (new URL(url)).searchParams const entries = [...params] return ( -
+
{entries.map(([key, value]) => (
{key}:
@@ -32,9 +32,9 @@ const AnalyticType = ({ children, onChange, enabled, label, summary, exampleRequ // show hide state. update react. const [isOpen, setOpen] = useState(false) return ( -
+
- {label} } />
– {summary}
@@ -119,7 +119,7 @@ const AnalyticsToggle = ({ analyticsActionsToRecord, analyticsConsent, doToggleC
    {analyticsActionsToRecord.map(name => (
  • - {name} + {name}
  • ))}
diff --git a/src/components/box/Box.js b/src/components/box/Box.js index a7f81f0f0..d910efdd0 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/card/card.tsx b/src/components/card/card.tsx index 628c30221..7450e67c9 100644 --- a/src/components/card/card.tsx +++ b/src/components/card/card.tsx @@ -11,8 +11,8 @@ export const Card: React.FC<{ }> = ({ className = '', style, children }) => { return ( {children} @@ -27,7 +27,7 @@ export const CardHeader: React.FC<{ children: React.ReactNode }> = ({ className = '', children }) => { return ( -
+
{children}
) @@ -97,7 +97,7 @@ export const CardFooter: React.FC<{ children: React.ReactNode }> = ({ className = '', children }) => { return ( -
+
{children}
) diff --git a/src/components/checkbox/Checkbox.css b/src/components/checkbox/Checkbox.css index 3dc37ebe9..80c2e12ea 100644 --- a/src/components/checkbox/Checkbox.css +++ b/src/components/checkbox/Checkbox.css @@ -1,5 +1,5 @@ .Checkbox > span:first-of-type { - background-color: #DDE6EB; + background-color: var(--theme-bg-secondary); } .Checkbox > input { @@ -16,5 +16,5 @@ } .Checkbox input:focus + span { - outline: 1px solid #bbb; + outline: 1px solid var(--theme-border-primary); } diff --git a/src/components/experiments/ExperimentsPanel.js b/src/components/experiments/ExperimentsPanel.js index d95dd1506..2578d8066 100644 --- a/src/components/experiments/ExperimentsPanel.js +++ b/src/components/experiments/ExperimentsPanel.js @@ -18,7 +18,8 @@ const Experiments = ({ doExpToggleAction, experiments, t }) => { return (

{tkey('title', key)}

{tkey('description', key)}

diff --git a/src/components/ipns-manager/IpnsManager.js b/src/components/ipns-manager/IpnsManager.js index 25fee8baa..461987bcf 100644 --- a/src/components/ipns-manager/IpnsManager.js +++ b/src/components/ipns-manager/IpnsManager.js @@ -84,7 +84,7 @@ export const IpnsManager = ({ t, ipfsReady, doFetchIpnsKeys, doGenerateIpnsKey, return (
-
+
{({ width }) => ( = ({ }) => (
{children} @@ -49,8 +49,8 @@ export const ModalBody: React.FC = ({
{Icon && (
@@ -83,7 +83,8 @@ export const Modal: React.FC = ({ > {onCancel && ( )} diff --git a/src/components/pinning-manager/PinningManager.js b/src/components/pinning-manager/PinningManager.js index fb158707e..949726e58 100644 --- a/src/components/pinning-manager/PinningManager.js +++ b/src/components/pinning-manager/PinningManager.js @@ -50,7 +50,7 @@ export const PinningManager = ({ pinningServices, ipfsReady, arePinningServicesS return (
-
+
{({ width }) => (
{ return ( -
{ return ( -
+
{time ?
:
} diff --git a/src/components/radio/radio.css b/src/components/radio/radio.css index 7c7eac621..36ab1aa7a 100644 --- a/src/components/radio/radio.css +++ b/src/components/radio/radio.css @@ -1,5 +1,5 @@ .Radio > span:first-of-type { - background-color: #DDE6EB; + background-color: var(--theme-bg-secondary); } .Radio > input { @@ -16,5 +16,5 @@ } .Radio input:focus + span { - outline: 1px solid #bbb; + outline: 1px solid var(--theme-border-primary); } diff --git a/src/components/theme-toggle/ThemeToggle.js b/src/components/theme-toggle/ThemeToggle.js new file mode 100644 index 000000000..d54d3aaf7 --- /dev/null +++ b/src/components/theme-toggle/ThemeToggle.js @@ -0,0 +1,78 @@ +import React from 'react' +import { connect } from 'redux-bundler-react' +import { THEMES } from '../../bundles/theme.js' +import StrokeSun from '../../icons/StrokeSun' +import StrokeMoon from '../../icons/StrokeMoon' +import StrokeMonitor from '../../icons/StrokeMonitor' + +const ThemeToggle = ({ theme, doSetTheme, className = '' }) => { + const getIcon = () => { + const iconStyle = { + width: 24, + height: 24, + color: 'var(--theme-text-primary)', + stroke: 'var(--theme-text-primary)' + } + + switch (theme) { + case THEMES.LIGHT: + return + case THEMES.DARK: + return + case THEMES.SYSTEM: + return + default: + return + } + } + + const getLabel = () => { + switch (theme) { + case THEMES.LIGHT: + return 'Light' + case THEMES.DARK: + return 'Dark' + case THEMES.SYSTEM: + return 'System' + default: + return 'Light' + } + } + + const cycleTheme = () => { + const themes = [THEMES.LIGHT, THEMES.DARK, THEMES.SYSTEM] + const currentIndex = themes.indexOf(theme) + const nextTheme = themes[(currentIndex + 1) % themes.length] + doSetTheme(nextTheme) + } + + return ( + + ) +} + +export default connect( + 'selectTheme', + 'doSetTheme', + ThemeToggle +) diff --git a/src/components/tooltip/Tooltip.tsx b/src/components/tooltip/Tooltip.tsx index d7ea4b021..a16221131 100644 --- a/src/components/tooltip/Tooltip.tsx +++ b/src/components/tooltip/Tooltip.tsx @@ -62,9 +62,11 @@ const Tooltip: React.FC = ({ children, text, ...props }) => { left: '50%', transform: 'translate(-50%, 100%)', wordWrap: 'break-word', - width: '100%' + width: '100%', + backgroundColor: 'var(--theme-bg-modal)', + color: 'var(--theme-text-primary)' }} - className={`white z-max bg-navy-muted br2 pa1 f6 absolute ${tooltipDisplayClass}`} + className={`z-max br2 pa1 f6 absolute ${tooltipDisplayClass}`} > = ({ children, text, ...props }) => { transform: 'translate(-50%, -50%) rotate(45deg)', borderRadius: '2px 0px 0px', left: '50%', - zIndex: -1 + zIndex: -1, + backgroundColor: 'var(--theme-bg-modal)' }} - className='db bg-navy-muted absolute' + className='db absolute' /> {text}
diff --git a/src/components/tooltip/icon-tooltip.tsx b/src/components/tooltip/icon-tooltip.tsx index 8bb93d83d..7aee228cf 100644 --- a/src/components/tooltip/icon-tooltip.tsx +++ b/src/components/tooltip/icon-tooltip.tsx @@ -29,7 +29,9 @@ const IconTooltip: React.FC = ({ children, text, position, for zIndex: 1000, wordWrap: 'break-word' as const, width: 'max-content' as const, - maxWidth: '200px' + maxWidth: '200px', + backgroundColor: 'var(--theme-bg-modal)', + color: 'var(--theme-text-primary)' } switch (position) { @@ -126,9 +128,9 @@ const IconTooltip: React.FC = ({ children, text, position, for
-
+
{text}
diff --git a/src/diagnostics/dht-provide/Operations.tsx b/src/diagnostics/dht-provide/Operations.tsx index 44a6e1b66..e2265f308 100644 --- a/src/diagnostics/dht-provide/Operations.tsx +++ b/src/diagnostics/dht-provide/Operations.tsx @@ -70,7 +70,7 @@ export const Operations: React.FC = ({ sweep }) => { )} {/* Cumulative stats */} -
+
= ({ sweep }) => { value={eta ?? PLACEHOLDER} /> -
+
= ({ sweep }) => { {sweep.schedule?.next_reprovide_prefix || PLACEHOLDER}} + value={{sweep.schedule?.next_reprovide_prefix || PLACEHOLDER}} /> = ({ sweep }) => { dedicatedBurst > 0 ? activeBurst / availableBurst : 0 const Bar: React.FC<{ value: number }> = ({ value }) => ( -
+
= ({ sweep }) => {
-
+
= ({ sweep }) => { )}
-
+
( {label} @@ -116,7 +108,7 @@ const DiagnosticsContent: React.FC = () => { return (
{/* Tab Navigation */} -
+