frontend: light/dark theme

Massimo Melina committed Jan 9, 2022 at 12:07 UTC d2333d5a1889c2ab79fb30fd4bec9f77d2b0afd9
8 files changed +78 -12
frontend/src/App.tsx
+2
@@ -3,9 +3,11 @@ import { createElement as h, Fragment } from 'react'
3 import { BrowseFiles } from "./BrowseFiles";
4 import { Dialogs } from './dialog'
5 import { useApi } from "./api";
6 +import useTheme from "./useTheme";
7
8 function App() {
9 const extras = useApi('extras_to_load')
10 + useTheme()
11 return h(Fragment, {},
12 h(BrowserRouter, {},
13 h(Routes, {},
frontend/src/components.ts
+13 -2
@@ -19,8 +19,8 @@ export function FlexV(props:any) {
19 return h(Flex, { vert:true, ...props })
20 }
21
22 -interface CheckboxOptions { children?:ReactNode, value:any, onChange?:(v:boolean)=>void }
23 -export function Checkbox({ onChange, value, children, ...props }:CheckboxOptions) {
22 +interface CheckboxProps { children?:ReactNode, value:any, onChange?:(v:boolean)=>void }
23 +export function Checkbox({ onChange, value, children, ...props }:CheckboxProps) {
24 return h('label', {},
25 h('input',{
26 type:'checkbox',
@@ -32,3 +32,14 @@ export function Checkbox({ onChange, value, children, ...props }:CheckboxOptions
32 children
33 )
34 }
35 +
36 +type Options = { label:string, value:string }[]
37 +interface SelectProps { value:any, onChange?:(v:string)=>void, options:Options }
38 +export function Select({ onChange, value, options, ...props }:SelectProps) {
39 + return h('select', {
40 + onChange: ev => // @ts-ignore
41 + onChange?.(ev.target.value),
42 + value,
43 + ...props,
44 + }, options.map(({ value, label }) => h('option', { key:value, value }, label)))
45 +}
frontend/src/dialog.css
+1
@@ -11,6 +11,7 @@
11 }
12 .dialog {
13 background: #fff;
14 + background: var(--bg);
15 padding: 1em 1.5em;
16 border-radius: 1em;
17 position: relative;
frontend/src/index.scss
+31 -7
@@ -1,4 +1,24 @@
1 +:root {
2 + --bg: #fff;
3 + --text: #555;
4 + --mild-contrast: #0005;
5 + --button-bg: #bcd;
6 + --button-text: #444;
7 + .theme-dark {
8 + --bg: #000;
9 + --text: #999;
10 + --mild-contrast: #fff5;
11 + --good-contrast: #fffa;
12 + --button-bg: #345;
13 + --button-text: #999;
14 + a { color:#68a; }
15 + .dialog-confirm, .dialog-prompt { --color:#77a }
16 + .dialog-closer { background: #844 }
17 + .dialog-icon { color: #ccc; }
18 + }
19 +}
20 body {
21 + background: var(--bg);
22 margin: 0;
23 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
24 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
@@ -11,7 +31,7 @@ body {
31 margin: auto;
32 }
33 #root, input {
14 - color: #555;
34 + color: var(--text);
35 }
36 code {
37 font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
@@ -19,7 +39,9 @@ code {
39 input, select {
40 padding: 0.3em 0.4em;
41 border-radius: 0.5em;
22 - border-color: #0005;
42 + background: var(--bg);
43 + border-color: var(--mild-contrast);
44 + color: var(--good-contrast);
45 }
46 input[type=checkbox] {
47 vertical-align: text-bottom;
@@ -35,8 +57,8 @@ a {
57 color: #357;
58 }
59 button {
38 - background-color: #bcd;
39 - color: #444;
60 + background-color: var(--button-bg);
61 + color: var(--button-text);
62 padding: 0.5em 1em;
63 border: transparent;
64 text-decoration: none;
@@ -56,7 +78,7 @@ button.toggled {
78 header {
79 position: sticky;
80 top: 0;
59 - background: #fff;
81 + background: var(--bg);
82 padding: .2em;
83 }
84
@@ -83,8 +105,8 @@ header {
105 padding: 0.1em 0.6em 0.2em;
106 line-height: 1.8em;
107 border-radius: 0.7em;
86 - background-color: #bcd;
87 - color: #444;
108 + background-color: var(--button-bg);
109 + color: var(--button-text);
110
111 border-top: 1px solid #666;
112 margin-right: -0.1em;
@@ -162,6 +184,8 @@ button label {
184 cursor: inherit;
185 margin-left: 0.5em;
186 }
187 +select { font-size:110% }
188 +
189 .dialog.working {
190 background: none;
191 font-size: 5em;
frontend/src/options.ts
+9 -1
@@ -1,7 +1,7 @@
1 import { newDialog } from './dialog'
2 import { state, useSnapState } from './state'
3 import { createElement as h } from 'react'
4 -import { Checkbox, FlexV } from './components'
4 +import { Checkbox, FlexV, Select } from './components'
5 import { hIcon } from './misc'
6
7 export function showOptions (){
@@ -24,6 +24,14 @@ export function showOptions (){
24 state.foldersFirst = v
25 }
26 }, 'Folders first'),
27 +
28 + h(Select, {
29 + options: ['light', 'dark'].map(s => ({ label:'theme: ' + s, value: s })),
30 + value: snap.theme,
31 + onChange(v) {
32 + state.theme = v
33 + }
34 + })
35 )
36 }
37 }
frontend/src/state.ts
+3 -1
@@ -12,6 +12,7 @@ export const state = proxy<{
12 filteredEntries: number,
13 sortBy: string,
14 foldersFirst: boolean,
15 + theme: string,
16 }>({
17 iconsClass: '',
18 username: '',
@@ -20,6 +21,7 @@ export const state = proxy<{
21 filteredEntries: -1,
22 sortBy: 'name',
23 foldersFirst: true,
24 + theme: '',
25 })
26
27 export function useSnapState() {
@@ -27,7 +29,7 @@ export function useSnapState() {
29 }
30
31 const SETTINGS_KEY = 'hfs_settings'
30 -const SETTINGS_TO_STORE: (keyof typeof state)[] = ['sortBy','foldersFirst']
32 +const SETTINGS_TO_STORE: (keyof typeof state)[] = ['sortBy','foldersFirst','theme']
33
34 loadSettings()
35 for (const k of SETTINGS_TO_STORE)
frontend/src/useTheme.ts new
+19
@@ -0,0 +1,19 @@
1 +import { state, useSnapState } from './state'
2 +import { useEffect } from 'react'
3 +
4 +export default () => {
5 + const { theme } = useSnapState()
6 + useEffect(()=>{
7 + const e = document.body
8 + if (!e) return
9 + const t = state.theme
10 + const pre = 'theme-'
11 + const ct = pre+t
12 + const list = e.classList
13 + for (const c of Array.from(list))
14 + if (c.startsWith(pre) && c !== ct)
15 + list.remove(c)
16 + if (t)
17 + list.add(ct)
18 + }, [theme])
19 +}
todo.md
-1
@@ -13,7 +13,6 @@
13 - archive for search results
14 - archive only selected files
15 - https
16 -- frontend: light/dark theme
16 - webdav?
17 - vfs: ability to remove/hide/rename files deep in a source
18 - administration interface