diff --git a/src/App.js b/src/App.js index af0f0d854..df6b4b61e 100644 --- a/src/App.js +++ b/src/App.js @@ -18,6 +18,7 @@ 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' export class App extends Component { static propTypes = { @@ -32,6 +33,8 @@ export class App extends Component { isOver: PropTypes.bool.isRequired } + static contextType = ThemeContext + constructor (props) { super(props) props.doSetupLocalStorage() @@ -62,45 +65,48 @@ export class App extends Component { render () { const { t, route: Page, ipfsReady, doFilesNavigateTo, routeInfo: { url }, connectDropTarget, canDrop, isOver, showTooltip } = this.props + const currentTheme = typeof window !== 'undefined' && localStorage.getItem('theme') 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 &&
} -
-
-
-
- +
+ + {/* 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 &&
} +
+
+
+
+ +
+
+ + +
+
+
+ { (ipfsReady || url === '/welcome' || url.startsWith('/settings')) + ? + : + } +
-
- - +
+
-
- { (ipfsReady || url === '/welcome' || url.startsWith('/settings')) - ? - : - } -
-
-
- + +
-
- - - - +
) } diff --git a/src/components/theme-toggle/theme-toggle.css b/src/components/theme-toggle/theme-toggle.css new file mode 100644 index 000000000..9f723f592 --- /dev/null +++ b/src/components/theme-toggle/theme-toggle.css @@ -0,0 +1,48 @@ +.theme-toggle { + --size: 2rem; + --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; +} + +[data-theme='dark'] .theme-toggle { + --icon-fill: hsl(25, 100%, 50%); + --icon-fill-hover: hsl(25, 100%, 40%); +} + +.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); + } +} diff --git a/src/components/theme-toggle/toggle.tsx b/src/components/theme-toggle/toggle.tsx new file mode 100644 index 000000000..d65bbe52b --- /dev/null +++ b/src/components/theme-toggle/toggle.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import './theme-toggle.css' +import { useTheme } from '../../hooks/theme' + +export const ThemeToggle = () => { + const { currentTheme: isDarkTheme, toggleTheme, toggleThemeWithKey } = useTheme() + return ( + + ) +} diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx new file mode 100644 index 000000000..2023a5b5d --- /dev/null +++ b/src/context/theme-provider.tsx @@ -0,0 +1,49 @@ +import React from 'react' + +export interface ThemeProviderProps { + children: React.ReactNode +} + +export type Theme = 'light' | 'dark' + +export type ThemeContextValues = { + currentTheme: Theme, + toggleTheme: () => void; + toggleThemeWithKey: (event: React.KeyboardEvent) => void; +} + +const createThemeContext = () => React.createContext(null) +export const ThemeContext = createThemeContext() + +export const ThemeProvider = ({ children }: ThemeProviderProps) => { + const [theme, setTheme] = React.useState(() => { + const savedTheme = + typeof window !== 'undefined' && localStorage.getItem('theme') + if (savedTheme) return savedTheme === 'dark' + return window.matchMedia('prefers-color-scheme: dark').matches + }) + React.useEffect(() => { + const htmlElem = document.documentElement + const currentTheme = theme ? 'dark' : 'light' + htmlElem.setAttribute('data-theme', currentTheme) + localStorage.setItem('theme', currentTheme) + htmlElem.setAttribute('aria-label', `Current theme: ${currentTheme}`) + }, [theme]) + const toggleTheme = () => setTheme((prevTheme) => !prevTheme) + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + toggleTheme() + } + } + const values: ThemeContextValues = { + currentTheme: theme as unknown as Theme, + toggleTheme, + toggleThemeWithKey: handleKeyDown + } + return ( + + {children} + + ) +} diff --git a/src/hooks/theme.ts b/src/hooks/theme.ts new file mode 100644 index 000000000..1b1c21bac --- /dev/null +++ b/src/hooks/theme.ts @@ -0,0 +1,10 @@ +import React from 'react' +import { ThemeContext, ThemeContextValues } from '../context/theme-provider' + +export const useTheme = () => { + const context = React.useContext(ThemeContext) + if(context === null) { + throw new Error('Theme context is missing You probably forgot to wrap the component depending on theme in ') + } + return context as ThemeContextValues +} diff --git a/src/index.css b/src/index.css index 87793d8c3..9d4c94c82 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -@import './reset.css'; +@import "./reset.css"; @import "../node_modules/tachyons"; /* They forgot to include word break: https://github.com/tachyons-css/tachyons/issues/563 */ @import "../node_modules/tachyons/src/_word-break.css"; @@ -8,15 +8,59 @@ body { overflow-y: scroll; } -.placeholder-light::placeholder{ - color: #CAD3D8; +[data-theme="dark"] { + --background: #181a1b; + --text: #e8e6e3; + --element-bg: #1a1c1e; + --navy-dark: #0b3a53; + --navy-text-color: rgb(202, 198, 191); + --snow-muted: rgb(28, 30, 31); + + body { + transition: background .3s ease-in; + } + + /* from inspecting the DOM, most of the classes here are from tachyon and ipfs-css + * so when the data-theme attr is set to dark, we'll have them updated + */ + .bg-snow-muted { + background: var(--snow-muted); + } + + .charcoal { + color: rgb(196, 191, 183); + } + + section { + background: var(--element-bg) !important; + } + + .navy { + color: var(--navy-text-color); + } + + .joyride-app-explore < div { + border: 1px solid red; + } +} + +html #root { + background-color: var(--background); + color: var(--text); + transition: background-color 0.3s ease, color 0.3s ease; +} + +.placeholder-light::placeholder { + color: #cad3d8; } .bg-near-white { background-color: #fbfbfb; } -html, body, #root { +html, +body, +#root { min-height: 100%; } @@ -26,7 +70,7 @@ html, body, #root { @media only screen and (min-width: 60em) { .appOverlay { - width: calc(100% - 148px) + width: calc(100% - 148px); } } @@ -36,5 +80,5 @@ html, body, #root { .react-joyride__tooltip button { font-size: 14px !important; - font-family: 'Montserrat'; + font-family: "Montserrat"; } diff --git a/src/navigation/NavBar.js b/src/navigation/NavBar.js index 9925dd224..894537778 100644 --- a/src/navigation/NavBar.js +++ b/src/navigation/NavBar.js @@ -12,6 +12,7 @@ import StrokeIpld from '../icons/StrokeIpld.js' // Styles import './NavBar.css' +import { ThemeToggle } from '../components/theme-toggle/toggle' const NavLink = ({ to, @@ -73,6 +74,9 @@ export const NavBar = ({ t }) => {
+
+ +
{ gitRevision && } diff --git a/src/reset.css b/src/reset.css index 90f447a1c..bd6864097 100644 --- a/src/reset.css +++ b/src/reset.css @@ -10,7 +10,7 @@ button { button:not(.disabled) { cursor: pointer; } - + button:not(.default):focus-visible { outline: 1px solid #bbb; outline-offset: -1px; @@ -32,3 +32,12 @@ button.hoverable-button:hover { button.hoverable-button:focus { outline: none; } + +/* override tachyons */ +[data-theme='dark'] .bg-white { + background-color: var(--background) !important; +} + +[data-theme='dark'] .near-black { + color: var(--text) !important; +} diff --git a/tsconfig.json b/tsconfig.json index ec69689fc..42e62a1b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -57,6 +57,8 @@ "include": [ "**/*.ts", "@types", + "src/context", + "src/hooks", // "src/**/*.js", // TODO: Include all js files when typecheck passes "src/bundles/files/**/*.js", "src/bundles/analytics.js",