Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add dark mode #2322

Merged
merged 14 commits into from
Feb 26, 2025
Merged
25 changes: 14 additions & 11 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,27 @@ import getStore from '../src/bundles/index.js'
import i18n from '../src/i18n.js'
import DndBackend from '../src/lib/dnd-backend.js'
import { HeliaProvider, ExploreProvider } from 'ipld-explorer-components/providers'
import { ThemeProvider } from '../src/context/theme-provider.tsx'

/**
* @type {import('@storybook/addons').BaseAnnotations}
*/
const baseAnnotations = {
decorators: [
(Story) => (
<Provider store={getStore(undefined)}>
<I18nextProvider i18n={i18n} >
<DndProvider backend={DndBackend}>
<HeliaProvider>
<ExploreProvider>
<Story />
</ExploreProvider>
</HeliaProvider>
</DndProvider>
</I18nextProvider>
</Provider>
<ThemeProvider>
<Provider store={getStore(undefined)}>
<I18nextProvider i18n={i18n} >
<DndProvider backend={DndBackend}>
<HeliaProvider>
<ExploreProvider>
<Story />
</ExploreProvider>
</HeliaProvider>
</DndProvider>
</I18nextProvider>
</Provider>
</ThemeProvider>
)
],
/**
Expand Down
77 changes: 43 additions & 34 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import Notify from './components/notify/Notify.js'
import Connected from './components/connected/Connected.js'
import TourHelper from './components/tour/TourHelper.js'
import FilesExploreForm from './files/explore-form/files-explore-form.tsx'
import { ThemeProvider, ThemeContext } from './context/theme-provider.tsx'
import { ThemeToggle } from './components/theme-toggle/toggle.tsx'

export class App extends Component {
static propTypes = {
Expand All @@ -32,6 +34,8 @@ export class App extends Component {
isOver: PropTypes.bool.isRequired
}

static contextType = ThemeContext

constructor (props) {
super(props)
props.doSetupLocalStorage()
Expand Down Expand Up @@ -63,44 +67,49 @@ 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
<div className='sans-serif h-100 relative' onClick={getNavHelper(this.props.doUpdateUrl)}>
{/* Tinted overlay that appears when dragging and dropping an item */}
{ canDrop && isOver && <div className='h-100 top-0 right-0 fixed appOverlay' style={{ background: 'rgba(99, 202, 210, 0.2)' }} /> }
<div className='flex flex-row-reverse-l flex-column-reverse justify-end justify-start-l' style={{ minHeight: '100vh' }}>
<div className='flex-auto-l'>
<div className='flex items-center ph3 ph4-l' style={{ WebkitAppRegion: 'drag', height: 75, background: '#F0F6FA', paddingTop: '20px', paddingBottom: '15px' }}>
<div className='joyride-app-explore' style={{ width: 560 }}>
<FilesExploreForm onBrowse={doFilesNavigateTo} />
<div>
<ThemeProvider>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div className='sans-serif h-100 relative' onClick={getNavHelper(this.props.doUpdateUrl)}>
{/* Tinted overlay that appears when dragging and dropping an item */}
{ canDrop && isOver && <div className='h-100 top-0 right-0 fixed appOverlay' style={{ background: 'rgba(99, 202, 210, 0.2)' }} /> }
<div className='flex flex-row-reverse-l flex-column-reverse justify-end justify-start-l' style={{ minHeight: '100vh' }}>
<div className='flex-auto-l'>
<div className='flex items-center ph3 ph4-l webui-header' style={{ WebkitAppRegion: 'drag', height: 75, background: '#F0F6FA', paddingTop: '20px', paddingBottom: '15px' }}>
<div className='joyride-app-explore' style={{ width: 560 }}>
<FilesExploreForm onBrowse={doFilesNavigateTo} />
</div>
<div className='dn flex-ns flex-auto items-center justify-end'>
<TourHelper />
<Connected className='joyride-app-status' />
<div className='pa3'>
<ThemeToggle />
</div>
</div>
</div>
<main className='bg-white pv3 pa3 pa4-l'>
{ (ipfsReady || url === '/welcome' || url.startsWith('/settings'))
? <Page />
: <ComponentLoader />
}
</main>
</div>
<div className='dn flex-ns flex-auto items-center justify-end'>
<TourHelper />
<Connected className='joyride-app-status' />
<div className='navbar-container flex-none-l bg-navy'>
<NavBar />
</div>
</div>
<main className='bg-white pv3 pa3 pa4-l'>
{ (ipfsReady || url === '/welcome' || url.startsWith('/settings'))
? <Page />
: <ComponentLoader />
}
</main>
</div>
<div className='navbar-container flex-none-l bg-navy'>
<NavBar />
<ReactJoyride
run={showTooltip}
steps={appTour.getSteps({ t })}
styles={appTour.styles}
callback={this.handleJoyrideCb}
scrollToFirstStep
disableOverlay
locale={getJoyrideLocales(t)}
/>
<Notify />
</div>
</div>

<ReactJoyride
run={showTooltip}
steps={appTour.getSteps({ t })}
styles={appTour.styles}
callback={this.handleJoyrideCb}
scrollToFirstStep
disableOverlay
locale={getJoyrideLocales(t)}
/>

<Notify />
</ThemeProvider>
</div>
)
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/api-address-form/ApiAddressForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { connect } from 'redux-bundler-react'
import { withTranslation } from 'react-i18next'
import Button from '../button/button.tsx'
import { checkValidAPIAddress } from '../../bundles/ipfs-provider.js'
import { useTheme } from '../../hooks/theme'

const ApiAddressForm = ({ t, doUpdateIpfsApiAddress, ipfsApiAddress, ipfsInitFailed }) => {
const [value, setValue] = useState(asAPIString(ipfsApiAddress))
const initialIsValidApiAddress = !checkValidAPIAddress(value)
const [showFailState, setShowFailState] = useState(initialIsValidApiAddress || ipfsInitFailed)
const [isValidApiAddress, setIsValidApiAddress] = useState(initialIsValidApiAddress)
const { isDarkTheme } = useTheme()

// Updates the border of the input to indicate validity
useEffect(() => {
Expand Down Expand Up @@ -46,12 +48,14 @@ const ApiAddressForm = ({ t, doUpdateIpfsApiAddress, ipfsApiAddress, ipfsInitFai
onChange={onChange}
onKeyPress={onKeyPress}
value={value}
style={{ background: isDarkTheme ? 'var(--filter-peers-dark)' : '', border: isDarkTheme ? '0.4px solid var(--border-color)' : '' }}
/>
<div className='tr'>
<Button
minWidth={100}
height={40}
className='mt2 mt0-l ml2-l tc'
style={{ background: isDarkTheme ? 'var(--input-btn-bg)' : '' }}
disabled={!isValidApiAddress || value === ipfsApiAddress}>
{t('actions.submit')}
</Button>
Expand Down
5 changes: 4 additions & 1 deletion src/components/box/Box.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React from 'react'
import { useTheme } from '../../hooks/theme'
import ErrorBoundary from '../error/ErrorBoundary.js'

export const Box = ({
className = 'pa4',
style,
themed,
children,
...props
}) => {
const { isDarkTheme } = useTheme()
return (
<section className={className} style={{ background: '#fbfbfb', ...style }}>
<section className={className} style={{ background: isDarkTheme ? 'var(--element-bg)' : 'var(--element-bg-light)', ...style }}>
<ErrorBoundary>
{children}
</ErrorBoundary>
Expand Down
6 changes: 5 additions & 1 deletion src/components/public-gateway-form/PublicGatewayForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { connect } from 'redux-bundler-react'
import { withTranslation } from 'react-i18next'
import Button from '../button/button.tsx'
import { checkValidHttpUrl, checkViaImgSrc, DEFAULT_PATH_GATEWAY } from '../../bundles/gateway.js'
import { useTheme } from '../../hooks/theme'

const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => {
const [value, setValue] = useState(publicGateway)
const initialIsValidGatewayUrl = !checkValidHttpUrl(value)
const [showFailState, setShowFailState] = useState(initialIsValidGatewayUrl)
const [isValidGatewayUrl, setIsValidGatewayUrl] = useState(initialIsValidGatewayUrl)

const { isDarkTheme } = useTheme()
// Updates the border of the input to indicate validity
useEffect(() => {
setShowFailState(!isValidGatewayUrl)
Expand Down Expand Up @@ -60,6 +61,7 @@ const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => {
onChange={onChange}
onKeyPress={onKeyPress}
value={value}
style={{ background: isDarkTheme ? 'var(--filter-peers-dark)' : '', border: isDarkTheme ? '0.4px solid var(--border-color)' : '' }}
/>
<div className='tr'>
<Button
Expand All @@ -68,6 +70,7 @@ const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => {
height={40}
bg='bg-charcoal'
className='tc'
style={{ background: isDarkTheme ? 'var(--input-btn-bg)' : '' }}
disabled={value === DEFAULT_PATH_GATEWAY}
onClick={onReset}>
{t('app:actions.reset')}
Expand All @@ -77,6 +80,7 @@ const PublicGatewayForm = ({ t, doUpdatePublicGateway, publicGateway }) => {
minWidth={100}
height={40}
className='mt2 mt0-l ml2-l tc'
style={{ background: isDarkTheme ? 'var(--input-btn-bg)' : '' }}
disabled={!isValidGatewayUrl || value === publicGateway}>
{t('actions.submit')}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { connect } from 'redux-bundler-react'
import { withTranslation } from 'react-i18next'
import Button from '../button/button.tsx'
import { checkValidHttpUrl, checkSubdomainGateway, DEFAULT_SUBDOMAIN_GATEWAY } from '../../bundles/gateway.js'
import { useTheme } from '../../hooks/theme'

const PublicSubdomainGatewayForm = ({ t, doUpdatePublicSubdomainGateway, publicSubdomainGateway }) => {
const [value, setValue] = useState(publicSubdomainGateway)
const initialIsValidGatewayUrl = !checkValidHttpUrl(value)
const [isValidGatewayUrl, setIsValidGatewayUrl] = useState(initialIsValidGatewayUrl)
const { isDarkTheme } = useTheme()

// Updates the border of the input to indicate validity
useEffect(() => {
Expand Down Expand Up @@ -64,6 +66,7 @@ const PublicSubdomainGatewayForm = ({ t, doUpdatePublicSubdomainGateway, publicS
onChange={onChange}
onKeyPress={onKeyPress}
value={value}
style={{ background: isDarkTheme ? 'var(--filter-peers-dark)' : '', border: isDarkTheme ? '0.4px solid var(--border-color)' : '' }}
/>
<div className='tr'>
<Button
Expand All @@ -72,6 +75,7 @@ const PublicSubdomainGatewayForm = ({ t, doUpdatePublicSubdomainGateway, publicS
height={40}
bg='bg-charcoal'
className='tc'
style={{ background: isDarkTheme ? 'var(--input-btn-bg)' : '' }}
disabled={value === DEFAULT_SUBDOMAIN_GATEWAY}
onClick={onReset}>
{t('app:actions.reset')}
Expand All @@ -80,6 +84,7 @@ const PublicSubdomainGatewayForm = ({ t, doUpdatePublicSubdomainGateway, publicS
id='public-subdomain-gateway-submit-button'
minWidth={100}
height={40}
style={{ background: isDarkTheme ? 'var(--input-btn-bg)' : '' }}
className='mt2 mt0-l ml2-l tc'
disabled={!isValidGatewayUrl || value === publicSubdomainGateway}>
{t('actions.submit')}
Expand Down
53 changes: 53 additions & 0 deletions src/components/theme-toggle/theme-toggle.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.theme-toggle {
--size: 1.55rem;
--icon-fill: hsl(210, 22%, 22%);
--icon-fill-hover: hsl(210, 22%, 12%);

background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
}

.theme-toggle > svg {
inline-size: 100%;
block-size: 100%;
stroke-linecap: round;
color: #378085;
}

[data-theme='dark'] .theme-toggle {
--icon-fill: hsl(25, 100%, 50%);
--icon-fill-hover: hsl(25, 100%, 40%);

svg {
color: #fff;
}
}

.theme-toggle:hover,
.theme-toggle:focus-visible {
background: hsl(0 0% 50% / 0.1);
}

@media (prefers-reduced-motion: no-preference) {
.theme-toggle {
transition: background-color 0.3s ease;
}

.theme-toggle > svg {
transition: transform 0.5s ease;
}

.theme-toggle:hover > svg,
.theme-toggle:focus-visible > svg {
transform: scale(1.1);
}
}
50 changes: 50 additions & 0 deletions src/components/theme-toggle/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import './theme-toggle.css'
import { useTheme } from '../../hooks/theme'

export const ThemeToggle = () => {
const { isDarkTheme, toggleTheme } = useTheme()
return (
<button
className="theme-toggle"
onClick={() => toggleTheme()}
onKeyDown={toggleTheme}
tabIndex={0}
aria-label={`Toggle ${isDarkTheme ? 'dark' : 'light'} mode`}
role="switch"
aria-checked={isDarkTheme}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
color="#fff"
>
{isDarkTheme
? (
<>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</>
)
: (
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
)}
</svg>
</button>
)
}
Loading
Loading