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