From a1c86d37e971fccb224e5d4cf5869ce3167845c1 Mon Sep 17 00:00:00 2001 From: luckmer Date: Tue, 10 Mar 2026 18:48:13 +0100 Subject: [PATCH 1/5] custom notifications --- package.json | 3 +- pnpm-lock.yaml | 45 +---- src/App.tsx | 4 +- src/components/ProgressBar/index.tsx | 23 +++ src/pages/Toast/index.tsx | 161 ++++++++++++++++++ .../actions/notifications/addNotification.ts | 83 +++++---- src/store/notifications/notifications.ts | 22 ++- src/styles/index.css | 22 +++ src/toaster/index.tsx | 60 +++++++ src/types/notifications/interfaces.ts | 13 ++ 10 files changed, 358 insertions(+), 78 deletions(-) create mode 100644 src/components/ProgressBar/index.tsx create mode 100644 src/pages/Toast/index.tsx create mode 100644 src/toaster/index.tsx create mode 100644 src/types/notifications/interfaces.ts diff --git a/package.json b/package.json index 60630ff5..c0320f28 100644 --- a/package.json +++ b/package.json @@ -52,14 +52,13 @@ "postcss": "^8.5.6", "postcss-import": "^16.1.1", "prettier": "^3.8.1", - "solid-headless": "^0.13.1", "solid-icons": "^1.2.0", "solid-js": "^1.9.3", - "solid-sonner": "^0.2.8", "solidjs-use": "^2.3.0", "storybook": "^10.2.8", "storybook-solidjs-vite": "^10.0.9", "tailwindcss": "3.4.17", + "uuid": "^13.0.0", "vitest": "^4.0.18" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f6dec5e..6572a197 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,18 +80,12 @@ importers: prettier: specifier: ^3.8.1 version: 3.8.1 - solid-headless: - specifier: ^0.13.1 - version: 0.13.1(solid-js@1.9.11) solid-icons: specifier: ^1.2.0 version: 1.2.0(solid-js@1.9.11) solid-js: specifier: ^1.9.3 version: 1.9.11 - solid-sonner: - specifier: ^0.2.8 - version: 0.2.8(solid-js@1.9.11) solidjs-use: specifier: ^2.3.0 version: 2.3.0 @@ -104,6 +98,9 @@ importers: tailwindcss: specifier: 3.4.17 version: 3.4.17 + uuid: + specifier: ^13.0.0 + version: 13.0.0 vitest: specifier: ^4.0.18 version: 4.0.18(@types/node@25.2.3)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)(yaml@2.8.2) @@ -1840,12 +1837,6 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - solid-headless@0.13.1: - resolution: {integrity: sha512-FZJai49YmdBu6oEo8aJGPMQ1Qn8xiW0cnD6vNFDIQWMKJdXEUtDEwz0hTR9aZ7Epq3IkrZs+98E0vNiv1+pZpA==} - engines: {node: '>=10'} - peerDependencies: - solid-js: ^1.2 - solid-icons@1.2.0: resolution: {integrity: sha512-yjQxWQMi9l19P5Af9vjsvalTMDFPDL4XD6etrUdNxNVhFp1bMYF0SFwUa5VYDfJ7u0SF3Qkz/F7BZxqcLUwklw==} peerDependencies: @@ -1859,17 +1850,6 @@ packages: peerDependencies: solid-js: ^1.3 - solid-sonner@0.2.8: - resolution: {integrity: sha512-EQ2EIznvHHpAmkYh2CTu0AdCgmPJRJWLGFRWygE8j+vMEfvIV2wotHU5qgWzqzVTG1SODGsay2Lwq6ENWx/rPA==} - peerDependencies: - solid-js: ^1.6.0 - - solid-use@0.6.2: - resolution: {integrity: sha512-0ShJ5s+4PIN0pJB/BtsQucsZB+xnUeeTGaxErQDu6USn5jygZWXicAtOEvFbI8gv40xE751uY1Tz7Aib9lxL/Q==} - engines: {node: '>=10'} - peerDependencies: - solid-js: ^1.5 - solidjs-use@2.3.0: resolution: {integrity: sha512-ZyH2jiUSQU+5S5vd8zyvqz9X2ikhMjJhDeh/TjRzjLDJlZ1RuZaMxdM+jGsrJxo3X/xBW9kDpYNONXs6wYrQCQ==} @@ -2027,6 +2007,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + vite-plugin-solid@2.11.10: resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} peerDependencies: @@ -3755,11 +3739,6 @@ snapshots: siginfo@2.0.0: {} - solid-headless@0.13.1(solid-js@1.9.11): - dependencies: - solid-js: 1.9.11 - solid-use: 0.6.2(solid-js@1.9.11) - solid-icons@1.2.0(solid-js@1.9.11): dependencies: solid-js: 1.9.11 @@ -3779,14 +3758,6 @@ snapshots: transitivePeerDependencies: - supports-color - solid-sonner@0.2.8(solid-js@1.9.11): - dependencies: - solid-js: 1.9.11 - - solid-use@0.6.2(solid-js@1.9.11): - dependencies: - solid-js: 1.9.11 - solidjs-use@2.3.0: dependencies: '@solidjs-use/shared': 2.3.0 @@ -3968,6 +3939,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@13.0.0: {} + vite-plugin-solid@2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@6.4.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): dependencies: '@babel/core': 7.29.0 diff --git a/src/App.tsx b/src/App.tsx index 80817169..666139ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,8 +7,8 @@ import { } from '@store/notifications/notifications' import { debug } from '@tauri-apps/plugin-log' import { lazy, onMount, Suspense } from 'solid-js' -import { Toaster } from 'solid-sonner' import { usePersistentStore } from './persistenStore' +import { Toaster } from './toaster' import { runWatchers } from './watchers' const Modals = lazy(() => import('@containers/Modals')) const AppRoutes = lazy(() => import('@routes/Routes')) @@ -36,7 +36,7 @@ const App = () => { - + ) } diff --git a/src/components/ProgressBar/index.tsx b/src/components/ProgressBar/index.tsx new file mode 100644 index 00000000..e66b72c8 --- /dev/null +++ b/src/components/ProgressBar/index.tsx @@ -0,0 +1,23 @@ +import { Component } from 'solid-js' + +interface IProps { + duration: number + color: string + paused: boolean +} + +const ProgressBar: Component = (props) => ( +
+
+
+) + +export default ProgressBar diff --git a/src/pages/Toast/index.tsx b/src/pages/Toast/index.tsx new file mode 100644 index 00000000..a5da6409 --- /dev/null +++ b/src/pages/Toast/index.tsx @@ -0,0 +1,161 @@ +import DefaultButton from '@components/Buttons/DefaultButton' +import ProgressBar from '@components/ProgressBar' +import Typography from '@components/Typography' +import { IToast } from '@interfaces/notifications/interfaces' +import { AiOutlineCheckCircle, AiOutlineClose } from 'solid-icons/ai' +import { + Component, + createEffect, + createMemo, + createSignal, + onCleanup, + onMount, + Show, +} from 'solid-js' + +interface ToastComponentProps { + toast: IToast + index: number + total: number + hovering: boolean + onStartRemoving: (id: number) => void + dismiss: (id: number) => void +} + +const Toast: Component = (props) => { + const [removing, setRemoving] = createSignal(false) + const [mounted, setMounted] = createSignal(false) + + const theme = createMemo(() => { + return { + bg: '#0d5c3a', + border: '#1a8c59', + icon: '#5deca0', + text: '#e8fff4', + sub: '#9de8c0', + glow: 'rgba(29,174,100,0.35)', + } + }) + + let timer: ReturnType | undefined + let remaining = props.toast.duration + let startTime = Date.now() + + onMount(() => { + setTimeout(() => { + setMounted(true) + }, 20) + }) + + const removeToast = (): void => { + setRemoving(true) + props.onStartRemoving(props.toast.id) + setTimeout(() => props.dismiss(props.toast.id), 340) + } + + createEffect(() => { + if (props.hovering) { + clearTimeout(timer) + remaining -= Date.now() - startTime + return + } + + timer = setTimeout(removeToast, remaining) + startTime = Date.now() + onCleanup(() => clearTimeout(timer)) + }) + + const multiplier = createMemo(() => { + if (props.toast.description) { + return 75 + } + return 55 + }) + + const outerTransform = (): string => { + const scale = props.hovering ? 1 : 1 - props.index * 0.042 + const translateY = props.hovering ? props.index * multiplier() : props.index * 10 + + if (removing()) { + return `translateY(calc(-100% - 32px)) scale(${scale})` + } + if (!mounted()) { + return `translateY(calc(-100% - 12px)) scale(${scale}) translateX(0px)` + } + return `translateY(${translateY}px) scale(${scale}) translateX(${0}px)` + } + + const outerOpacity = (): string => { + if (removing()) return '0' + if (!mounted()) return '0' + if (props.hovering && props.index <= 5) return '1' + return String(props.index >= 4 ? 0 : 1 - props.index * 0.13) + } + + const outerTransition = (): string => { + if (removing()) { + return 'transform 0.34s cubic-bezier(0.4, 0, 1, 1), opacity 0.26s ease' + } + return 'transform 0.42s cubic-bezier(0.25, 1.0, 0.5, 1), opacity 0.34s ease' + } + + return ( +
+
+
+
+ +
+
+
+ + {props.toast.message} + + + + {props.toast.description} + + +
+ + + +
+
+ +
+
+ ) +} + +export default Toast diff --git a/src/store/actions/notifications/addNotification.ts b/src/store/actions/notifications/addNotification.ts index b8ef6f1a..e6bd5919 100644 --- a/src/store/actions/notifications/addNotification.ts +++ b/src/store/actions/notifications/addNotification.ts @@ -1,7 +1,10 @@ import { NOTIFICATION_ACTION, NOTIFICATION_TYPE } from '@interfaces/notifications/enums' -import { NotificationAction, Notifications } from '@store/notifications/notifications' +import { + NotificationAction, + Notifications, + setNotification, +} from '@store/notifications/notifications' import { enableNotifications, globalNotificationsType } from '@store/notifications/selectors' -import { toast } from 'solid-sonner' import { checkPermission } from './checkPermission' import { handleSound } from './handleSound' @@ -20,50 +23,60 @@ const mapNotificationCallback = ( } export const addNotification = (notification: Notifications) => { - // for some god forsaken reason webkit does not support it - // and will happily crash on you with null pointer :) - if (navigator.userAgent.toLowerCase().includes('linux')){ - return - } - if (!enableNotifications()) return checkPermission() const { message, type } = notification - const toastOptions = { - style: { - background: '#0D1B26', - color: '#fff', - border: '1px solid #192736', - textAlign: 'left', + const toastMap: Record void> = { + [NOTIFICATION_TYPE.ERROR]: (msg) => { + setNotification({ + type: NOTIFICATION_TYPE.ERROR, + message: msg, + duration: 5000, + id: Date.now(), + }) }, - actionButtonStyle: { - background: '#192736', - border: '1px solid #192736', - color: '#FFFFFF', + [NOTIFICATION_TYPE.SUCCESS]: (msg) => { + setNotification({ + type: NOTIFICATION_TYPE.SUCCESS, + message: msg, + duration: 5000, + id: Date.now(), + }) }, - class: 'my-custom-toast', - action: { - label: 'X', - onClick: () => console.log('Undo'), + [NOTIFICATION_TYPE.INFO]: (msg) => { + setNotification({ + type: NOTIFICATION_TYPE.INFO, + message: msg, + duration: 5000, + id: Date.now(), + }) + }, + [NOTIFICATION_TYPE.WARNING]: (msg) => { + setNotification({ + type: NOTIFICATION_TYPE.WARNING, + message: msg, + duration: 5000, + id: Date.now(), + }) + }, + [NOTIFICATION_TYPE.DEFAULT]: (msg) => { + setNotification({ + type: NOTIFICATION_TYPE.DEFAULT, + message: msg, + duration: 5000, + id: Date.now(), + }) }, - } - - const toastMap: Record void> = { - [NOTIFICATION_TYPE.ERROR]: (msg) => toast.error(msg, toastOptions), - [NOTIFICATION_TYPE.SUCCESS]: (msg) => toast.success(msg, toastOptions), - [NOTIFICATION_TYPE.INFO]: (msg) => toast.info(msg, toastOptions), - [NOTIFICATION_TYPE.WARNING]: (msg) => toast.warning(msg, toastOptions), - [NOTIFICATION_TYPE.DEFAULT]: (msg) => toast(msg, toastOptions), } mapNotificationCallback(globalNotificationsType(), { callbackOS: () => { - toast(message, { - action: { - label: 'Close', - onClick: () => console.log('Undo'), - }, + setNotification({ + type: NOTIFICATION_TYPE.DEFAULT, + message: message, + duration: 5000, + id: Date.now(), }) }, callbackApp: () => { diff --git a/src/store/notifications/notifications.ts b/src/store/notifications/notifications.ts index 34c29707..cb54924d 100644 --- a/src/store/notifications/notifications.ts +++ b/src/store/notifications/notifications.ts @@ -1,5 +1,5 @@ import { NOTIFICATION_ACTION, NOTIFICATION_TYPE } from '@interfaces/notifications/enums' -import { ToasterStore } from 'solid-headless' +import { IToast } from '@interfaces/notifications/interfaces' import { createStore, produce } from 'solid-js/store' export interface NotificationAction { @@ -14,14 +14,14 @@ export interface Notifications { } export interface AppStoreNotifications { - notifications: ToasterStore + notifications: IToast[] enableNotificationsSounds: boolean enableNotifications: boolean globalNotificationsType: NOTIFICATION_ACTION } const defaultState: AppStoreNotifications = { - notifications: new ToasterStore(), + notifications: [], enableNotificationsSounds: true, enableNotifications: true, globalNotificationsType: NOTIFICATION_ACTION.APP, @@ -53,4 +53,20 @@ export const setGlobalNotificationsType = (type: NOTIFICATION_ACTION) => { ) } +export const setNotification = (toast: IToast) => { + setState( + produce((s) => { + s.notifications = [toast, ...s.notifications] + }), + ) +} + +export const dismissNotification = (id: number) => { + setState( + produce((s) => { + s.notifications = s.notifications.filter((t) => t.id !== id) + }), + ) +} + export const notificationsState = () => state diff --git a/src/styles/index.css b/src/styles/index.css index 86857a4e..5a62cb3d 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -81,3 +81,25 @@ body { .my-custom-toast { text-align: left; } + +@keyframes slideIn { + 0% { + transform: translateY(calc(-100% - 12px)); + opacity: 0; + } + 40% { + opacity: 1; + } + 100% { + transform: translateY(0px); + opacity: 1; + } +} +@keyframes shrink { + from { + transform: scaleX(1); + } + to { + transform: scaleX(0); + } +} diff --git a/src/toaster/index.tsx b/src/toaster/index.tsx new file mode 100644 index 00000000..cd251a1a --- /dev/null +++ b/src/toaster/index.tsx @@ -0,0 +1,60 @@ +import Toast from '@pages/Toast' +import { dismissNotification } from '@store/notifications/notifications' +import { notifications } from '@store/notifications/selectors' +import { createSignal, For } from 'solid-js' + +export const Toaster = () => { + const [removingIds, setRemovingIds] = createSignal>(new Set()) + const [hovering, setHovering] = createSignal(false) + + const onStartRemoving = (id: number): void => { + setRemovingIds((prev) => new Set([...prev, id])) + setTimeout(() => { + setRemovingIds((prev) => { + const next = new Set(prev) + next.delete(id) + return next + }) + }, 400) + } + + const effectiveIndex = (id: number): number => { + const removing = removingIds() + let idx = 0 + for (const t of notifications()) { + if (t.id === id) return idx + if (!removing.has(t.id)) idx++ + } + return idx + } + + return ( +
setHovering(true)} + onMouseLeave={() => setHovering(false)} + style={{ + position: 'fixed', + top: '24px', + left: '50%', + transform: 'translateX(-50%)', + width: '360px', + height: hovering() ? `${Math.min(notifications().length, 5) * 72 + 2}px` : '80px', + transition: 'height 0.38s cubic-bezier(0.34, 1.1, 0.64, 1)', + 'z-index': '999', + 'pointer-events': notifications().length ? 'all' : 'none', + }}> + + {(t) => ( + + )} + +
+ ) +} diff --git a/src/types/notifications/interfaces.ts b/src/types/notifications/interfaces.ts new file mode 100644 index 00000000..ebeeaee1 --- /dev/null +++ b/src/types/notifications/interfaces.ts @@ -0,0 +1,13 @@ +import { NOTIFICATION_TYPE } from './enums' + +export interface ToastOptions { + description?: string + duration?: number +} + +export interface IToast extends ToastOptions { + type: NOTIFICATION_TYPE + id: number + message: string + duration: number +} From 89dcd0370765fb612107e1d5f4a0083785177162 Mon Sep 17 00:00:00 2001 From: luckmer Date: Wed, 11 Mar 2026 18:28:13 +0100 Subject: [PATCH 2/5] set storybook --- src-tauri/src/lib.rs | 31 ++++++ src/common/theme.ts | 2 +- src/components/ProgressBar/index.stories.tsx | 38 ++++++++ src/components/Toast/index.stories.tsx | 99 ++++++++++++++++++++ src/{pages => components}/Toast/index.tsx | 82 +++++++--------- src/store/notifications/notifications.ts | 2 + src/toaster/index.tsx | 16 ++-- 7 files changed, 214 insertions(+), 56 deletions(-) create mode 100644 src/components/ProgressBar/index.stories.tsx create mode 100644 src/components/Toast/index.stories.tsx rename src/{pages => components}/Toast/index.tsx (72%) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b987f3af..0660edff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -23,6 +23,37 @@ struct SingleInstancePayload { } pub fn run() { + #[cfg(target_os = "linux")] + { + // Fix for webkit issues on Linux with nvidia and/or some wayland compositors + // + // Related issues and code adapted from: + // https://github.com/tauri-apps/tauri/issues/13493 + + log::trace!("Setting linux specific workaround envs for GDK webkit issues:"); + + let is_wayland_display = std::env::var_os("WAYLAND_DISPLAY"); + let xdg_session_type = std::env::var("XDG_SESSION_TYPE") + .unwrap_or_default() + .to_lowercase(); + + let is_wayland = is_wayland_display.is_some() || xdg_session_type == "wayland"; + let compositor = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); + log::trace!( + "Display: wayland={is_wayland}, compositor={compositor}, session={xdg_session_type}" + ); + + if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none() { + log::trace!("setting: WEBKIT_DISABLE_COMPOSITING_MODE=1"); + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); + } + + if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() { + log::trace!("setting: WEBKIT_DISABLE_DMABUF_RENDERER=1"); + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + } + let app = tauri::Builder::default(); // Note: This is a workaround for a bug in tauri that causes the window to not resize properly inducing a noticeable lag diff --git a/src/common/theme.ts b/src/common/theme.ts index a22207ae..a930cba4 100644 --- a/src/common/theme.ts +++ b/src/common/theme.ts @@ -57,7 +57,7 @@ export const theme = { 100: '#9793FD', }, transparentPurple: { - 200: ' #817df7b3', + 200: '#817df7b3', 100: '#817df780', }, grey: { diff --git a/src/components/ProgressBar/index.stories.tsx b/src/components/ProgressBar/index.stories.tsx new file mode 100644 index 00000000..1f5e5f32 --- /dev/null +++ b/src/components/ProgressBar/index.stories.tsx @@ -0,0 +1,38 @@ +import { Meta, StoryObj } from 'storybook-solidjs-vite' +import Toast from './index' + +const meta: Meta = { + title: 'Components/Components/ProgressBar', + component: Toast, + parameters: { + layout: 'centered', + backgrounds: { + default: 'dark', + values: [{ name: 'dark', value: '#0a0a0a' }], + }, + }, + argTypes: { + duration: { + control: { type: 'number', min: 0, max: 5000 }, + }, + paused: { control: 'boolean' }, + color: { control: 'color' }, + }, + args: { + duration: 2000, + paused: false, + color: '#fff', + }, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + duration: 2000, + paused: false, + color: '#fff', + }, +} diff --git a/src/components/Toast/index.stories.tsx b/src/components/Toast/index.stories.tsx new file mode 100644 index 00000000..7ce20faf --- /dev/null +++ b/src/components/Toast/index.stories.tsx @@ -0,0 +1,99 @@ +import { NOTIFICATION_TYPE } from '@interfaces/notifications/enums' +import { Meta, StoryObj } from 'storybook-solidjs-vite' +import Toast from './index' + +const meta: Meta = { + title: 'Components/Components/Toast', + component: Toast, + parameters: { + layout: 'centered', + backgrounds: { + default: 'dark', + values: [{ name: 'dark', value: '#0a0a0a' }], + }, + }, + argTypes: { + toast: { control: 'object' }, + index: { control: { type: 'number', min: 0, max: 5 } }, + total: { control: { type: 'number', min: 1, max: 10 } }, + hovering: { control: 'boolean' }, + onStartRemoving: { action: 'onStartRemoving' }, + dismiss: { action: 'dismiss' }, + }, + args: { + index: 0, + total: 1, + hovering: false, + }, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + toast: { + id: 1, + type: NOTIFICATION_TYPE.DEFAULT, + message: 'This is a default notification', + duration: 5000, + }, + }, +} + +export const Success: Story = { + args: { + toast: { + id: 2, + type: NOTIFICATION_TYPE.SUCCESS, + message: 'Operation completed successfully', + duration: 5000, + }, + }, +} + +export const Warning: Story = { + args: { + toast: { + id: 3, + type: NOTIFICATION_TYPE.WARNING, + message: 'Proceed with caution', + duration: 5000, + }, + }, +} + +export const Info: Story = { + args: { + toast: { + id: 4, + type: NOTIFICATION_TYPE.INFO, + message: 'Here is some useful information', + duration: 5000, + }, + }, +} + +export const Error: Story = { + args: { + toast: { + id: 5, + type: NOTIFICATION_TYPE.ERROR, + message: 'Something went wrong', + duration: 5000, + }, + }, +} + +export const LongMessage: Story = { + args: { + toast: { + id: 10, + type: NOTIFICATION_TYPE.WARNING, + message: 'Your session is about to expire due to inactivity', + description: 'Please save your work. You will be logged out in 60 seconds.', + duration: 8000, + }, + }, +} diff --git a/src/pages/Toast/index.tsx b/src/components/Toast/index.tsx similarity index 72% rename from src/pages/Toast/index.tsx rename to src/components/Toast/index.tsx index a5da6409..043996f4 100644 --- a/src/pages/Toast/index.tsx +++ b/src/components/Toast/index.tsx @@ -1,19 +1,28 @@ import DefaultButton from '@components/Buttons/DefaultButton' import ProgressBar from '@components/ProgressBar' import Typography from '@components/Typography' +import { NOTIFICATION_TYPE } from '@interfaces/notifications/enums' import { IToast } from '@interfaces/notifications/interfaces' -import { AiOutlineCheckCircle, AiOutlineClose } from 'solid-icons/ai' +import theme from '@src/common/theme' import { + AiOutlineCheckCircle, + AiOutlineClose, + AiOutlineInfoCircle, + AiTwotoneWarning, +} from 'solid-icons/ai' +import { + Accessor, Component, createEffect, createMemo, createSignal, + JSX, onCleanup, onMount, Show, } from 'solid-js' -interface ToastComponentProps { +interface IProps { toast: IToast index: number total: number @@ -22,21 +31,10 @@ interface ToastComponentProps { dismiss: (id: number) => void } -const Toast: Component = (props) => { +const Toast: Component = (props) => { const [removing, setRemoving] = createSignal(false) const [mounted, setMounted] = createSignal(false) - const theme = createMemo(() => { - return { - bg: '#0d5c3a', - border: '#1a8c59', - icon: '#5deca0', - text: '#e8fff4', - sub: '#9de8c0', - glow: 'rgba(29,174,100,0.35)', - } - }) - let timer: ReturnType | undefined let remaining = props.toast.duration let startTime = Date.now() @@ -72,7 +70,7 @@ const Toast: Component = (props) => { return 55 }) - const outerTransform = (): string => { + const outerTransform = createMemo(() => { const scale = props.hovering ? 1 : 1 - props.index * 0.042 const translateY = props.hovering ? props.index * multiplier() : props.index * 10 @@ -83,58 +81,48 @@ const Toast: Component = (props) => { return `translateY(calc(-100% - 12px)) scale(${scale}) translateX(0px)` } return `translateY(${translateY}px) scale(${scale}) translateX(${0}px)` - } + }) - const outerOpacity = (): string => { + const outerOpacity = createMemo(() => { if (removing()) return '0' if (!mounted()) return '0' if (props.hovering && props.index <= 5) return '1' return String(props.index >= 4 ? 0 : 1 - props.index * 0.13) - } + }) - const outerTransition = (): string => { + const outerTransition = createMemo(() => { if (removing()) { return 'transform 0.34s cubic-bezier(0.4, 0, 1, 1), opacity 0.26s ease' } return 'transform 0.42s cubic-bezier(0.25, 1.0, 0.5, 1), opacity 0.34s ease' - } + }) + + const icon: Accessor> = createMemo(() => { + return { + [NOTIFICATION_TYPE.SUCCESS]: , + [NOTIFICATION_TYPE.DEFAULT]: , + [NOTIFICATION_TYPE.WARNING]: , + [NOTIFICATION_TYPE.INFO]: , + [NOTIFICATION_TYPE.ERROR]: , + } + }) return (
-
+
-
- -
+
{icon()[props.toast.type]}
-
- +
+ {props.toast.message} @@ -143,6 +131,8 @@ const Toast: Component = (props) => {
+
+
@@ -150,7 +140,7 @@ const Toast: Component = (props) => {
diff --git a/src/store/notifications/notifications.ts b/src/store/notifications/notifications.ts index cb54924d..e0ce378b 100644 --- a/src/store/notifications/notifications.ts +++ b/src/store/notifications/notifications.ts @@ -54,8 +54,10 @@ export const setGlobalNotificationsType = (type: NOTIFICATION_ACTION) => { } export const setNotification = (toast: IToast) => { + const limit = 30 setState( produce((s) => { + if (s.notifications.length >= limit) s.notifications.pop() s.notifications = [toast, ...s.notifications] }), ) diff --git a/src/toaster/index.tsx b/src/toaster/index.tsx index cd251a1a..1677008e 100644 --- a/src/toaster/index.tsx +++ b/src/toaster/index.tsx @@ -1,4 +1,5 @@ -import Toast from '@pages/Toast' +import Toast from '@components/Toast' +import { classNames } from '@src/utils' import { dismissNotification } from '@store/notifications/notifications' import { notifications } from '@store/notifications/selectors' import { createSignal, For } from 'solid-js' @@ -32,16 +33,13 @@ export const Toaster = () => {
setHovering(true)} onMouseLeave={() => setHovering(false)} + class={classNames( + 'fixed top-6 left-1/2 -translate-x-1/2 w-[360px] z-[999] transition-[height] duration-[380ms] [cubic-bezier(0.34,1.1,0.64,1)]', + notifications().length ? 'pointer-events-auto' : 'pointer-events-none', + )} style={{ - position: 'fixed', - top: '24px', - left: '50%', - transform: 'translateX(-50%)', - width: '360px', height: hovering() ? `${Math.min(notifications().length, 5) * 72 + 2}px` : '80px', - transition: 'height 0.38s cubic-bezier(0.34, 1.1, 0.64, 1)', - 'z-index': '999', - 'pointer-events': notifications().length ? 'all' : 'none', + 'transition-timing-function': 'cubic-bezier(0.34, 1.1, 0.64, 1)', }}> {(t) => ( From 42e4a100fc0b51e751f22c0dd3f910b59583b47c Mon Sep 17 00:00:00 2001 From: luckmer Date: Wed, 11 Mar 2026 18:32:39 +0100 Subject: [PATCH 3/5] update notification width --- src/components/Toast/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Toast/index.tsx b/src/components/Toast/index.tsx index 043996f4..a3762dfc 100644 --- a/src/components/Toast/index.tsx +++ b/src/components/Toast/index.tsx @@ -109,7 +109,7 @@ const Toast: Component = (props) => { return (
= (props) => { 'z-index': String(props.total - props.index), 'will-change': 'transform, opacity', }}> -
+
{icon()[props.toast.type]}
From 78ba90b60c55d85e71f1fda0ea9b8df2a6b15976 Mon Sep 17 00:00:00 2001 From: luckmer Date: Wed, 11 Mar 2026 18:37:55 +0100 Subject: [PATCH 4/5] update tailwind config --- src/components/ProgressBar/index.tsx | 7 +++---- src/styles/index.css | 26 -------------------------- src/toaster/index.tsx | 2 +- tailwind.config.mjs | 9 +++++++++ 4 files changed, 13 insertions(+), 31 deletions(-) diff --git a/src/components/ProgressBar/index.tsx b/src/components/ProgressBar/index.tsx index e66b72c8..659dbdf3 100644 --- a/src/components/ProgressBar/index.tsx +++ b/src/components/ProgressBar/index.tsx @@ -7,13 +7,12 @@ interface IProps { } const ProgressBar: Component = (props) => ( -
+
diff --git a/src/styles/index.css b/src/styles/index.css index 5a62cb3d..30fb15d7 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -77,29 +77,3 @@ body { background-color 0.5s; color: #a8b1ff; } - -.my-custom-toast { - text-align: left; -} - -@keyframes slideIn { - 0% { - transform: translateY(calc(-100% - 12px)); - opacity: 0; - } - 40% { - opacity: 1; - } - 100% { - transform: translateY(0px); - opacity: 1; - } -} -@keyframes shrink { - from { - transform: scaleX(1); - } - to { - transform: scaleX(0); - } -} diff --git a/src/toaster/index.tsx b/src/toaster/index.tsx index 1677008e..b73f7867 100644 --- a/src/toaster/index.tsx +++ b/src/toaster/index.tsx @@ -34,7 +34,7 @@ export const Toaster = () => { onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)} class={classNames( - 'fixed top-6 left-1/2 -translate-x-1/2 w-[360px] z-[999] transition-[height] duration-[380ms] [cubic-bezier(0.34,1.1,0.64,1)]', + 'fixed top-6 left-1/2 -translate-x-1/2 w-[400px] z-[999] transition-[height] duration-[380ms] [cubic-bezier(0.34,1.1,0.64,1)]', notifications().length ? 'pointer-events-auto' : 'pointer-events-none', )} style={{ diff --git a/tailwind.config.mjs b/tailwind.config.mjs index bcdf9745..fcfa68b4 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -48,6 +48,15 @@ module.exports = { gridTemplateColumns: { '1/5': '1fr 5fr', }, + keyframes: { + shrink: { + from: { transform: 'scaleX(1)' }, + to: { transform: 'scaleX(0)' }, + }, + }, + animation: { + shrink: 'shrink 300ms ease-out forwards', + }, }, ...theme, }, From 6338d3d10bd01fb5e272a64ae53ddb28b7371adc Mon Sep 17 00:00:00 2001 From: luckmer Date: Wed, 11 Mar 2026 18:39:34 +0100 Subject: [PATCH 5/5] Connect tauri drag region --- src/toaster/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/toaster/index.tsx b/src/toaster/index.tsx index b73f7867..7309176d 100644 --- a/src/toaster/index.tsx +++ b/src/toaster/index.tsx @@ -31,10 +31,11 @@ export const Toaster = () => { return (
setHovering(true)} onMouseLeave={() => setHovering(false)} class={classNames( - 'fixed top-6 left-1/2 -translate-x-1/2 w-[400px] z-[999] transition-[height] duration-[380ms] [cubic-bezier(0.34,1.1,0.64,1)]', + ' fixed top-6 left-1/2 -translate-x-1/2 w-[400px] z-[999] transition-[height] duration-[380ms] [cubic-bezier(0.34,1.1,0.64,1)]', notifications().length ? 'pointer-events-auto' : 'pointer-events-none', )} style={{