diff --git a/web/package-lock.json b/web/package-lock.json index 7372409e9..ab6144ec1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -421,6 +421,17 @@ "typed-colors": "^1.0.0", "typed-figures": "^1.0.0", "yargs": "^16.2.0" + }, + "dependencies": { + "enhanced-resolve": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", + "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + } } }, "@typed/fp": { @@ -444,6 +455,13 @@ "newtype-ts": "^0.3.4", "ts-toolbelt": "^8.0.7", "tslib": "^2.0.3" + }, + "dependencies": { + "json-schema": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz", + "integrity": "sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ==" + } } }, "@types/json-schema": { @@ -836,15 +854,6 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "enhanced-resolve": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz", - "integrity": "sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw==", - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1047,6 +1056,11 @@ "resolved": "https://registry.npmjs.org/hyperhtml-style/-/hyperhtml-style-0.1.2.tgz", "integrity": "sha512-ZDRYNClEaqUS0a8RAED0nQRqWmZk7ctdyij3Iw/PqUUef6xhYO87nx9vJNuxg7Yc6J2FdJjXRKbB0iud2ZyzwQ==" }, + "idb-keyval": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-5.0.2.tgz", + "integrity": "sha512-1DYjY/nX2U9pkTkwFoAmKcK1ZWmkNgO32Oon9tp/9+HURizxUQ4fZRxMJZs093SldP7q6dotVj03kIkiqOILyA==" + }, "ignore": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", @@ -1201,11 +1215,6 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, - "json-schema": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.5.tgz", - "integrity": "sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ==" - }, "json5": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", diff --git a/web/package.json b/web/package.json index e5ec8e7c6..18b3cfc54 100644 --- a/web/package.json +++ b/web/package.json @@ -11,6 +11,7 @@ "@snowpack/plugin-postcss": "^1.1.0", "autoprefixer": "^10.2.1", "cross-env": "^7.0.3", + "idb-keyval": "^5.0.2", "immer": "^8.0.1", "postcss": "^8.2.2", "postcss-cli": "^8.3.1", diff --git a/web/src/App.jsx b/web/src/App.jsx index aba1a4357..01923ff36 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -4,6 +4,7 @@ import AppBar from './components/AppBar'; import Camera from './Camera'; import CameraMap from './CameraMap'; import Cameras from './Cameras'; +import { DarkModeProvider } from './context'; import Debug from './Debug'; import Event from './Event'; import Events from './Events'; @@ -15,28 +16,30 @@ import Api, { FetchStatus, useConfig } from './api'; export default function App() { const { data, status } = useConfig(); return ( -
- - {status !== FetchStatus.LOADED ? ( -
- -
- ) : ( -
- -
- - - - - - - {import.meta.env.SNOWPACK_MODE !== 'development' ? : null} - - + +
+ + {status !== FetchStatus.LOADED ? ( +
+
-
- )} -
+ ) : ( +
+ +
+ + + + + + + {import.meta.env.SNOWPACK_MODE !== 'development' ? : null} + + +
+
+ )} +
+ ); } diff --git a/web/src/Settings.jsx b/web/src/Settings.jsx new file mode 100644 index 000000000..ef47a55aa --- /dev/null +++ b/web/src/Settings.jsx @@ -0,0 +1,34 @@ +import { h } from 'preact'; +import { useDarkMode } from './context'; +import { useCallback } from 'preact/hooks'; + +export default function Settings() { + const { currentMode, persistedMode, setDarkMode } = useDarkMode(); + + const handleSelect = useCallback( + (event) => { + const mode = event.target.value; + setDarkMode(mode); + }, + [setDarkMode] + ); + + return ( +
+ +
+ ); +} diff --git a/web/src/Sidebar.jsx b/web/src/Sidebar.jsx index 61f01c70e..cd7b313fe 100644 --- a/web/src/Sidebar.jsx +++ b/web/src/Sidebar.jsx @@ -4,30 +4,6 @@ import LinkedLogo from './components/LinkedLogo'; import { Link as RouterLink } from 'preact-router/match'; import { useCallback, useState } from 'preact/hooks'; -function HamburgerIcon() { - return ( - - - - ); -} - -function CloseIcon() { - return ( - - - - ); -} - function NavLink({ className = '', href, text }) { const external = href.startsWith('http'); const El = external ? Link : RouterLink; @@ -45,30 +21,14 @@ function NavLink({ className = '', href, text }) { } export default function Sidebar() { - const [open, setOpen] = useState(false); - - const handleToggle = useCallback(() => { - setOpen(!open); - }, [open, setOpen]); - return ( -
+
-
-
); } diff --git a/web/src/components/RelativeModal.jsx b/web/src/components/RelativeModal.jsx index d82490650..31412cb33 100644 --- a/web/src/components/RelativeModal.jsx +++ b/web/src/components/RelativeModal.jsx @@ -76,13 +76,15 @@ export default function RelativeModal({ className, role = 'dialog', children, on
0 ? `width: ${position.width}px; top: ${position.top}px; left: ${position.left}px` : ''} + style={ + position.width > 0 ? `min-width: ${position.width}px; top: ${position.top}px; left: ${position.left}px` : '' + } > {children}
diff --git a/web/src/context/index.jsx b/web/src/context/index.jsx new file mode 100644 index 000000000..213177084 --- /dev/null +++ b/web/src/context/index.jsx @@ -0,0 +1,66 @@ +import { h, createContext } from 'preact'; +import { get as getData, set as setData } from 'idb-keyval'; +import produce from 'immer'; +import { useCallback, useContext, useEffect, useState } from 'preact/hooks'; + +const DarkMode = createContext(null); + +export function DarkModeProvider({ children }) { + const [persistedMode, setPersistedMode] = useState(null); + const [currentMode, setCurrentMode] = useState(persistedMode !== 'media' ? persistedMode : null); + + const setDarkMode = useCallback( + (value) => { + setPersistedMode(value); + setData('darkmode', value); + if (value !== 'media') { + setCurrentMode(value); + } + }, + [setPersistedMode] + ); + + useEffect(() => { + async function load() { + const darkmode = await getData('darkmode'); + setDarkMode(darkmode || 'media'); + } + + load(); + }, []); + + if (persistedMode === null) { + return null; + } + + const handleMediaMatch = useCallback( + ({ matches }) => { + if (matches) { + setCurrentMode('dark'); + } else { + setCurrentMode('light'); + } + }, + [setCurrentMode] + ); + + useEffect(() => { + if (persistedMode !== 'media') { + return; + } + + const query = window.matchMedia('(prefers-color-scheme: dark)'); + query.addEventListener('change', handleMediaMatch); + handleMediaMatch(query); + }, [persistedMode]); + + return ( + +
{children}
+
+ ); +} + +export function useDarkMode() { + return useContext(DarkMode); +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 455a205ca..da77e7270 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -2,7 +2,7 @@ module.exports = { purge: ['./public/**/*.html', './src/**/*.jsx'], - darkMode: 'media', + darkMode: 'class', theme: { extend: {}, },