removed dependency for toasts
Massimo Melina committed
Feb 28, 2024 at 23:52 UTC
a2c61b393d22e1f0bbf10bb7a5f7fa812bcd5a1e
9 files changed
+128
-23
frontend/package.json
-1
@@ -12,7 +12,6 @@
12
"lodash": "^4.17.21",
13
"react": "^18.2.0",
14
"react-dom": "^18.2.0",
15
- "react-hot-toast": "^2.4.1",
15
"react-router-dom": "^6.1.1",
16
"talkr": "^3.3.9",
17
"tssrp6a": "^3.0.0",
frontend/src/App.ts
+2
-9
@@ -10,7 +10,7 @@ import { I18Nprovider } from './i18n'
10
import { proxy, useSnapshot } from "valtio"
11
import { Spinner } from "./components"
12
import { getHFS, getPrefixUrl } from '@hfs/shared'
13
-import { Toaster } from 'react-hot-toast'
13
+import { Toasts } from './toasts'
14
15
function App() {
16
useTheme()
@@ -23,14 +23,7 @@ function App() {
23
return h(I18Nprovider, {},
24
h(BrowserRouter, {},
25
h(NavigationExtractor, {},
26
- h(Toaster, {
27
- toastOptions: {
28
- style: {
29
- background: 'var(--faint-contrast)',
30
- color: 'var(--text-high-contrast)',
31
- },
32
- }
33
- }),
26
+ h(Toasts),
27
h(Dialogs, {},
28
h(Routes, {},
29
h(Route, { path:'*', element: h(BrowseFiles) })
frontend/src/clip.ts
+2
-2
@@ -2,7 +2,7 @@ import { createElement as h, Fragment } from 'react'
2
import { DirList, state, useSnapState } from './state'
3
import { Btn } from './components'
4
import { t, useI18N } from './i18n'
5
-import { alertDialog } from './dialog'
5
+import { alertDialog, toast } from './dialog'
6
import { useNavigate } from 'react-router-dom'
7
import { dirname, HTTP_MESSAGES, xlate } from '../../src/cross'
8
import { apiCall } from '@hfs/shared/api'
@@ -63,5 +63,5 @@ export function ClipBar() {
63
export function cut(files: DirList) {
64
state.clip = files
65
if (files.length)
66
- return alertDialog(t('after_cut', "Your selection is now in the clipboard.\nGo to destination folder to paste."), 'info')
66
+ return toast(t('after_cut', "Your selection is now in the clipboard.\nGo to destination folder to paste."), 'info')
67
}
\ No newline at end of file
frontend/src/dialog.ts
+4
-9
@@ -1,15 +1,15 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { createElement as h, ReactElement, ReactNode, useEffect, useRef, useState, KeyboardEvent,
4
- InputHTMLAttributes, isValidElement } from 'react'
4
+ InputHTMLAttributes } from 'react'
5
import './dialog.css'
6
import { newDialog, closeDialog, DialogOptions, dialogsDefaults } from '@hfs/shared/dialogs'
7
import _ from 'lodash'
8
import { useInterval } from 'usehooks-ts'
9
import { t } from './i18n'
10
-import { err2msg, hIcon, isCtrlKey, pendingPromise, Promisable } from './misc'
11
-import hotToast from 'react-hot-toast'
10
+import { err2msg, isCtrlKey, pendingPromise, Promisable } from './misc'
11
export * from '@hfs/shared/dialogs'
12
+export { toast } from './toasts'
13
14
_.merge(dialogsDefaults, { closableProps: { 'aria-label': t`Close` } })
15
@@ -90,7 +90,7 @@ export async function promptDialog(msg: string, { def, type, helperText, trim=tr
90
}
91
}
92
93
-type AlertType = 'error' | 'warning' | 'info'
93
+export type AlertType = 'error' | 'warning' | 'info'
94
95
export function alertDialog(msg: ReactElement | string | Error, type:AlertType='info') {
96
if (msg instanceof Error)
@@ -168,8 +168,3 @@ export function confirmDialog(msg: ReactElement | string, options: ConfirmOption
168
)
169
}
170
}
171
-
172
-export function toast(msg: string | ReactElement, type: AlertType | 'success' | ReactElement='info') {
173
- console.debug("toast", msg)
174
- hotToast(msg, { position: 'top-right', icon: isValidElement(type) ? type : hIcon(type) })
175
-}
frontend/src/icons.ts
+1
-1
@@ -43,7 +43,7 @@ const SYS_ICONS: Record<string, [string] | [string, string | false]> = { // fals
43
paste: ['📋'],
44
shuffle: ['🔀'],
45
repeat: ['🔁', 'reload'],
46
- success: ['👍', false],
46
+ success: ['👍', 'check'],
47
warning: ['⚠️', false],
48
}
49
frontend/src/index.scss
+7
@@ -15,6 +15,9 @@
15
--button-text: #eaeaea;
16
--focus-color: #468;
17
--separator: " – "; // fallback
18
+ --success: #8c8;
19
+ --warning: #cc8;
20
+ --error: #c88;
21
@supports (content: "" / "") {
22
--separator: " – " / ""; // skip for screen-reader
23
}
@@ -28,6 +31,10 @@
31
--good-contrast: #aaa;
32
--button-bg: #345;
33
--button-text: #999;
34
+ --success: #383;
35
+ --warning: #883;
36
+ --error: #a33;
37
+
38
color-scheme: dark;
39
.highlightedText { color: #fff; text-shadow: 0 0 3px #fff; }
40
a { color:#8ac; }
frontend/src/options.ts
+1
-1
@@ -4,7 +4,7 @@ import { newDialog } from './dialog'
4
import { state, useSnapState } from './state'
5
import { createElement as h } from 'react'
6
import { Checkbox, FlexV, Select } from './components'
7
-import { FRONTEND_OPTIONS, hIcon, MAX_TILE_SIZE, SORT_BY_OPTIONS, THEME_OPTIONS } from './misc'
7
+import { hIcon, MAX_TILE_SIZE, SORT_BY_OPTIONS, THEME_OPTIONS } from './misc'
8
import { MenuLink } from './menu'
9
import { t } from './i18n'
10
import _ from 'lodash'
frontend/src/toasts.scss
new
+45
@@ -0,0 +1,45 @@
1
+.toasts {
2
+ position: absolute; right: 0;
3
+ display: flex;
4
+ flex-direction: column;
5
+ margin-top: .5em;
6
+ gap: .5em;
7
+ z-index: 1001; // more than dialog backdrop
8
+}
9
+.toast {
10
+ background: var(--faint-contrast);
11
+ color: var(--text-high-contrast);
12
+ transition: all .5s ease-in;
13
+ position: relative;
14
+ left: -.5em;
15
+ overflow: hidden;
16
+ display: flex;
17
+ align-items: center;
18
+ padding: .3em .6em;
19
+ border-radius: .5em;
20
+ box-shadow: 0 0 .3em .3em #8883;
21
+ box-sizing: border-box;
22
+ .toast-icon { margin-right: .3em; animation: zoomRotating 1.5s; }
23
+ &.toast-success { background-color: var(--success) }
24
+ &.toast-warning { background-color: var(--warning) }
25
+ &.toast-error { background-color: var(--error) }
26
+
27
+ &.before {
28
+ left: 100%;
29
+ transform: scale(.1);
30
+ padding-top: 0;
31
+ padding-bottom: 0;
32
+ }
33
+ &.after {
34
+ height: 0 !important;
35
+ padding-top: 0;
36
+ padding-bottom: 0;
37
+ transition-duration: .2s;
38
+ transform: scale(0);
39
+ }
40
+}
41
+
42
+@keyframes zoomRotating {
43
+ 0% { transform: scale(0); }
44
+ 100% { transform: scale(1) rotate(360deg); }
45
+}
frontend/src/toasts.ts
new
+66
@@ -0,0 +1,66 @@
1
+import { createElement as h, HTMLAttributes, isValidElement, ReactElement, useEffect, useState, useRef,
2
+ useCallback, cloneElement, useMemo } from 'react'
3
+import { proxy, useSnapshot } from 'valtio'
4
+import { AlertType } from './dialog'
5
+import { hIcon, pendingPromise } from './misc'
6
+import _ from 'lodash'
7
+import './toasts.scss'
8
+
9
+type ToastType = AlertType | 'success'
10
+type Content = string | ReactElement
11
+
12
+export function toast(content: Content, type: ToastType='info', { timeout=5_000 }: { timeout?: number } & Omit<ToastOptions, 'id' | 'content' | 'type'>={}) {
13
+ console.debug("toast", content)
14
+ const id = Math.random()
15
+ toasts.push({ id, content, type })
16
+ const closed = pendingPromise()
17
+ setTimeout(close, timeout)
18
+ return {
19
+ close,
20
+ closed
21
+ }
22
+
23
+ function close() {
24
+ const it = _.find(toasts, { id })
25
+ if (!it) return
26
+ it.closed = true
27
+ closed.resolve()
28
+ }
29
+}
30
+
31
+interface ToastOptions extends Omit<Partial<HTMLAttributes<HTMLDivElement>>, 'id' | 'content'> {
32
+ id: number
33
+ content: Content
34
+ type: ToastType
35
+ closed?: boolean
36
+}
37
+const toasts = proxy<ToastOptions[]>([])
38
+
39
+export function Toasts() {
40
+ const snap = useSnapshot(toasts)
41
+ return h('div', { className: 'toasts' },
42
+ snap.map(d =>
43
+ h(Toast, { key: d.id, ...(d as any) }))
44
+ )
45
+}
46
+
47
+function Toast({ content, type, closed, id, ...props }: ToastOptions) {
48
+ const [addClass, setAddClass] = useState('before')
49
+ const [height, setHeight] = useState('')
50
+ useEffect(() => {
51
+ setAddClass(closed ? 'after' : '')
52
+ }, [closed])
53
+ const onTransitionEnd = useCallback(() => {
54
+ const el = ref.current
55
+ if (!addClass && el) // just entered
56
+ setHeight(el.clientHeight + 'px')
57
+ if (addClass === 'after')
58
+ _.remove(toasts, { id })
59
+ }, [addClass])
60
+ const ref = useRef<HTMLDivElement | null>()
61
+ content = useMemo(() => isValidElement(content) ? cloneElement(content) : content, [content]) // proxied elements are frozen, and crash
62
+ return h('div', { ...props, ref, style: { height }, onTransitionEnd, className: `toast ${addClass} ${_.isString(type) ? 'toast-' + type : ''} ${props.className || ''}`},
63
+ h('div', { className: 'toast-icon' }, isValidElement(type) ? type : hIcon(type)),
64
+ h('div', { className: 'toast-content' }, content)
65
+ )
66
+}
\ No newline at end of file