@samitouri / QOSami-HFS / commits / a2c61b39

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