feat: frontend status toast overlay, while refactoring document store usage.
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { useStatusToast } from '../contexts/StatusToastContext';
|
||||||
|
import '../styles/status-toast.css';
|
||||||
|
|
||||||
|
const FADE_OUT_DURATION = 300; // Match CSS animation duration
|
||||||
|
|
||||||
|
const StatusToastOverlay: React.FC = () => {
|
||||||
|
const { toasts, removeToast } = useStatusToast();
|
||||||
|
const [exitingToasts, setExitingToasts] = useState<Set<string>>(new Set());
|
||||||
|
const [displayToasts, setDisplayToasts] = useState(toasts);
|
||||||
|
const prevToastIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Detect when toasts are removed from context and trigger fade
|
||||||
|
useEffect(() => {
|
||||||
|
const currentIds = new Set(toasts.map(t => t.id));
|
||||||
|
const prevIds = prevToastIdsRef.current;
|
||||||
|
|
||||||
|
// Find toasts that were removed
|
||||||
|
const removedIds = Array.from(prevIds).filter((id) => typeof id === 'string' && !currentIds.has(id));
|
||||||
|
|
||||||
|
// Trigger fade for removed toasts
|
||||||
|
if (removedIds.length > 0) {
|
||||||
|
setExitingToasts(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
removedIds.forEach(id => next.add(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove from display after fade
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisplayToasts(current => current.filter(t => !removedIds.includes(t.id)));
|
||||||
|
setExitingToasts(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
removedIds.forEach(id => next.delete(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, FADE_OUT_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new toasts to display
|
||||||
|
const newToasts = toasts.filter(t => !prevIds.has(t.id));
|
||||||
|
if (newToasts.length > 0) {
|
||||||
|
setDisplayToasts(toasts);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevToastIdsRef.current = currentIds;
|
||||||
|
}, [toasts]);
|
||||||
|
|
||||||
|
const handleRemove = useCallback((id: string) => {
|
||||||
|
removeToast(id);
|
||||||
|
}, [removeToast]);
|
||||||
|
|
||||||
|
if (displayToasts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="status-toast-container">
|
||||||
|
{displayToasts.map((toast) => {
|
||||||
|
const isExiting = exitingToasts.has(toast.id);
|
||||||
|
const classNames = [
|
||||||
|
'status-toast-pill',
|
||||||
|
`status-toast-pill--${toast.variant}`,
|
||||||
|
isExiting ? 'status-toast-pill--exiting' : '',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={toast.id}
|
||||||
|
className={classNames}
|
||||||
|
onClick={() => handleRemove(toast.id)}
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusToastOverlay;
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export type ToastVariant = 'info' | 'success' | 'error';
|
||||||
|
|
||||||
|
export interface ToastMessage {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
variant: ToastVariant;
|
||||||
|
timestamp: number;
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusToastContextValue {
|
||||||
|
toasts: ToastMessage[];
|
||||||
|
showToast: (message: string, variant?: ToastVariant, duration?: number) => void;
|
||||||
|
removeToast: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusToastContext = createContext<StatusToastContextValue | null>(null);
|
||||||
|
|
||||||
|
const DEFAULT_DURATIONS: Record<ToastVariant, number> = {
|
||||||
|
success: 3000,
|
||||||
|
info: 5000,
|
||||||
|
error: 8000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_TOASTS = 3;
|
||||||
|
|
||||||
|
export const StatusToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [toasts, setToasts] = useState<ToastMessage[]>([]);
|
||||||
|
const timeoutRefs = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
|
const removeToast = useCallback((id: string) => {
|
||||||
|
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||||
|
|
||||||
|
// Clear timeout if it exists
|
||||||
|
const timeout = timeoutRefs.current.get(id);
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeoutRefs.current.delete(id);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showToast = useCallback((message: string, variant: ToastVariant = 'info', duration?: number) => {
|
||||||
|
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const finalDuration = duration ?? DEFAULT_DURATIONS[variant];
|
||||||
|
|
||||||
|
const newToast: ToastMessage = {
|
||||||
|
id,
|
||||||
|
message,
|
||||||
|
variant,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
duration: finalDuration,
|
||||||
|
};
|
||||||
|
|
||||||
|
setToasts((prev) => {
|
||||||
|
const updated = [...prev, newToast];
|
||||||
|
|
||||||
|
// If we exceed max toasts, remove the oldest ones
|
||||||
|
if (updated.length > MAX_TOASTS) {
|
||||||
|
const removed = updated.slice(0, updated.length - MAX_TOASTS);
|
||||||
|
removed.forEach((toast) => {
|
||||||
|
const timeout = timeoutRefs.current.get(toast.id);
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeoutRefs.current.delete(toast.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated.slice(-MAX_TOASTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set auto-dismiss timeout - trigger fade then remove
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
removeToast(id);
|
||||||
|
}, finalDuration);
|
||||||
|
|
||||||
|
timeoutRefs.current.set(id, timeout);
|
||||||
|
}, [removeToast]);
|
||||||
|
|
||||||
|
// Cleanup all timeouts on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
const timeouts = timeoutRefs.current;
|
||||||
|
return () => {
|
||||||
|
timeouts.forEach((timeout) => clearTimeout(timeout));
|
||||||
|
timeouts.clear();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value: StatusToastContextValue = {
|
||||||
|
toasts,
|
||||||
|
showToast,
|
||||||
|
removeToast,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusToastContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</StatusToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStatusToast = (): StatusToastContextValue => {
|
||||||
|
const context = useContext(StatusToastContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useStatusToast must be used within StatusToastProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -108,7 +108,7 @@ export const useCardPointer = (
|
|||||||
}
|
}
|
||||||
}, 500); // 500ms long press
|
}, 500); // 500ms long press
|
||||||
}
|
}
|
||||||
}, [card, isSelected, selection, onSelect, addPointer, activePointersRef]);
|
}, [card, isSelected, selection, onSelect, addPointer, activePointersRef, requestCanvasFocus]);
|
||||||
|
|
||||||
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
const onPointerMove = useCallback((e: React.PointerEvent) => {
|
||||||
// Ignore interactions on interactive child elements
|
// Ignore interactions on interactive child elements
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { EditIcon, IconX, CheckIcon, PlusIcon } from '../ui/icons';
|
import { EditIcon, IconX, PlusIcon } from '../ui/icons';
|
||||||
import InlineRenameInput from './components/InlineRenameInput';
|
import InlineRenameInput from './components/InlineRenameInput';
|
||||||
import SelectionAssignmentMenu, {
|
import SelectionAssignmentMenu, {
|
||||||
SelectionAssignmentMenuItem,
|
SelectionAssignmentMenuItem,
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useCallback, useState } from 'react';
|
|
||||||
|
|
||||||
export type StatusVariant = 'info' | 'success' | 'error';
|
|
||||||
|
|
||||||
export interface StatusMessage {
|
|
||||||
message: string;
|
|
||||||
variant: StatusVariant;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDocumentsStore = () => {
|
|
||||||
const [status, setStatus] = useState<StatusMessage | null>(null);
|
|
||||||
|
|
||||||
const setStatusMessage = useCallback((message?: string | null, variant: StatusVariant = 'info') => {
|
|
||||||
setStatus(message ? { message, variant } : null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { status, setStatusMessage };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDocumentsStore;
|
|
||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
} from '../../app/workspaceUtils';
|
} from '../../app/workspaceUtils';
|
||||||
import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey';
|
import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey';
|
||||||
import useDocumentsSearch from '../../app/useDocumentsSearch';
|
import useDocumentsSearch from '../../app/useDocumentsSearch';
|
||||||
import useDocumentsStore from './store/useDocumentsStore';
|
import { useStatusToast, type ToastVariant } from '../../contexts/StatusToastContext';
|
||||||
import useAuthManager from './useAuthManager';
|
import useAuthManager from './useAuthManager';
|
||||||
import useTenantManager from './useTenantManager';
|
import useTenantManager from './useTenantManager';
|
||||||
import useDocuments from './useDocuments';
|
import useDocuments from './useDocuments';
|
||||||
@@ -153,7 +153,16 @@ const useDocumentsWorkspace = ({
|
|||||||
const tenantOptions: TenantOption[] = Array.isArray(tenantOptionsRaw)
|
const tenantOptions: TenantOption[] = Array.isArray(tenantOptionsRaw)
|
||||||
? (tenantOptionsRaw as TenantOption[])
|
? (tenantOptionsRaw as TenantOption[])
|
||||||
: [];
|
: [];
|
||||||
const { status, setStatusMessage } = useDocumentsStore();
|
const { showToast } = useStatusToast();
|
||||||
|
const setStatusMessage = useCallback(
|
||||||
|
(message?: string | null, variant: ToastVariant = 'info') => {
|
||||||
|
if (message) {
|
||||||
|
showToast(message, variant);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[showToast],
|
||||||
|
);
|
||||||
|
const status = null; // No longer used, kept for backward compatibility
|
||||||
const handleApiReport = useCallback(
|
const handleApiReport = useCallback(
|
||||||
({ message, variant }) => setStatusMessage(message, variant),
|
({ message, variant }) => setStatusMessage(message, variant),
|
||||||
[setStatusMessage],
|
[setStatusMessage],
|
||||||
@@ -1216,7 +1225,6 @@ const useDocumentsWorkspace = ({
|
|||||||
appStatus,
|
appStatus,
|
||||||
previewActive,
|
previewActive,
|
||||||
handleLogout,
|
handleLogout,
|
||||||
status,
|
|
||||||
tenantName,
|
tenantName,
|
||||||
tenantOptions,
|
tenantOptions,
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { useDocumentsPreferences } from './app/useDocumentsPreferences';
|
|||||||
import { AppShellContext } from './appShellContext';
|
import { AppShellContext } from './appShellContext';
|
||||||
import useDocumentsWorkspace from './hooks/documents/useDocumentsWorkspace';
|
import useDocumentsWorkspace from './hooks/documents/useDocumentsWorkspace';
|
||||||
import UploadQueueOverlay from './app/UploadQueueOverlay';
|
import UploadQueueOverlay from './app/UploadQueueOverlay';
|
||||||
|
import { StatusToastProvider } from './contexts/StatusToastContext';
|
||||||
|
import StatusToastOverlay from './components/StatusToastOverlay';
|
||||||
|
|
||||||
const AppLayout: React.FC = () => {
|
const AppLayout: React.FC = () => {
|
||||||
const documentsPreferences = useDocumentsPreferences();
|
const documentsPreferences = useDocumentsPreferences();
|
||||||
@@ -67,6 +69,7 @@ const AppLayout: React.FC = () => {
|
|||||||
queue={contextValue.uploadQueue || []}
|
queue={contextValue.uploadQueue || []}
|
||||||
onClearQueue={contextValue.clearUploadQueue}
|
onClearQueue={contextValue.clearUploadQueue}
|
||||||
/>
|
/>
|
||||||
|
<StatusToastOverlay />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
{managementModals}
|
{managementModals}
|
||||||
{settingsOpen ? (
|
{settingsOpen ? (
|
||||||
@@ -99,8 +102,10 @@ if (!container) {
|
|||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
root.render(
|
root.render(
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
|
<StatusToastProvider>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
</StatusToastProvider>
|
||||||
</AppStateProvider>,
|
</AppStateProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
|
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||||
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
|
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
|
||||||
import SidebarFolderList from './components/SidebarFolderList';
|
import SidebarFolderList from './components/SidebarFolderList';
|
||||||
@@ -40,7 +39,6 @@ interface SidebarProps {
|
|||||||
onCreateTag?: (label: string) => Promise<void> | void;
|
onCreateTag?: (label: string) => Promise<void> | void;
|
||||||
onCreateCorrespondent?: (name: string) => Promise<void> | void;
|
onCreateCorrespondent?: (name: string) => Promise<void> | void;
|
||||||
onLogout?: () => void;
|
onLogout?: () => void;
|
||||||
status?: StatusMessage | null;
|
|
||||||
tenantName?: string | null;
|
tenantName?: string | null;
|
||||||
tenants?: TenantOption[];
|
tenants?: TenantOption[];
|
||||||
activeTenantId?: Identifier | null;
|
activeTenantId?: Identifier | null;
|
||||||
@@ -72,7 +70,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
onCreateTag,
|
onCreateTag,
|
||||||
onCreateCorrespondent,
|
onCreateCorrespondent,
|
||||||
onLogout,
|
onLogout,
|
||||||
status = null,
|
|
||||||
tenantName,
|
tenantName,
|
||||||
tenants = [],
|
tenants = [],
|
||||||
activeTenantId = null,
|
activeTenantId = null,
|
||||||
@@ -122,12 +119,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="panel-body sidebar__body">
|
<div className="panel-body sidebar__body">
|
||||||
{status && (
|
|
||||||
<div className="sidebar__status">
|
|
||||||
<div className={`status-banner ${status.variant}`}>{status.message}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SidebarSearch />
|
<SidebarSearch />
|
||||||
|
|
||||||
<SidebarFolderList
|
<SidebarFolderList
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { DragEvent } from 'react';
|
import type { DragEvent } from 'react';
|
||||||
import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils';
|
import { TAG_FILTER_UNTAGGED } from '../app/workspaceUtils';
|
||||||
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
|
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
|
||||||
interface FolderTreeNode {
|
interface FolderTreeNode {
|
||||||
@@ -89,7 +88,6 @@ interface UseSidebarPropsArgs {
|
|||||||
appStatus: string;
|
appStatus: string;
|
||||||
previewActive: boolean;
|
previewActive: boolean;
|
||||||
handleLogout: () => void | Promise<void>;
|
handleLogout: () => void | Promise<void>;
|
||||||
status: StatusMessage | null;
|
|
||||||
tenantName: string | null;
|
tenantName: string | null;
|
||||||
tenantOptions: TenantOption[];
|
tenantOptions: TenantOption[];
|
||||||
currentTenantId: Identifier | null;
|
currentTenantId: Identifier | null;
|
||||||
@@ -119,10 +117,8 @@ interface SidebarHookResult {
|
|||||||
onCreateTag: (label: string) => void;
|
onCreateTag: (label: string) => void;
|
||||||
correspondents: CorrespondentOption[];
|
correspondents: CorrespondentOption[];
|
||||||
onCreateCorrespondent: (name: string) => void;
|
onCreateCorrespondent: (name: string) => void;
|
||||||
appStatus: string;
|
|
||||||
previewActive: boolean;
|
previewActive: boolean;
|
||||||
onLogout: UseSidebarPropsArgs['handleLogout'];
|
onLogout: UseSidebarPropsArgs['handleLogout'];
|
||||||
status: StatusMessage | null;
|
|
||||||
tenantName: string | null;
|
tenantName: string | null;
|
||||||
tenants: TenantOption[];
|
tenants: TenantOption[];
|
||||||
activeTenantId: Identifier | null;
|
activeTenantId: Identifier | null;
|
||||||
@@ -150,7 +146,6 @@ const useSidebarProps = ({
|
|||||||
appStatus,
|
appStatus,
|
||||||
previewActive,
|
previewActive,
|
||||||
handleLogout,
|
handleLogout,
|
||||||
status,
|
|
||||||
tenantName,
|
tenantName,
|
||||||
tenantOptions,
|
tenantOptions,
|
||||||
currentTenantId,
|
currentTenantId,
|
||||||
@@ -183,7 +178,6 @@ const useSidebarProps = ({
|
|||||||
appStatus,
|
appStatus,
|
||||||
previewActive,
|
previewActive,
|
||||||
onLogout: handleLogout,
|
onLogout: handleLogout,
|
||||||
status,
|
|
||||||
tenantName,
|
tenantName,
|
||||||
tenants: tenantOptions,
|
tenants: tenantOptions,
|
||||||
activeTenantId: currentTenantId,
|
activeTenantId: currentTenantId,
|
||||||
@@ -227,7 +221,6 @@ const useSidebarProps = ({
|
|||||||
previewActive,
|
previewActive,
|
||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
status,
|
|
||||||
tags,
|
tags,
|
||||||
tenantName,
|
tenantName,
|
||||||
tenantOptions,
|
tenantOptions,
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
.status-toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 2rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 5000000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toast-pill {
|
||||||
|
position: relative;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--fg);
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
max-width: 90vw;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toast-pill--success {
|
||||||
|
background: linear-gradient(var(--success-subtle), var(--success-subtle)), var(--surface);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toast-pill--info {
|
||||||
|
background: linear-gradient(var(--surface-subtle), var(--surface-subtle)), var(--surface);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toast-pill--error {
|
||||||
|
background: linear-gradient(var(--danger-soft), var(--danger-soft)), var(--surface);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toast-pill--exiting {
|
||||||
|
animation: fadeOut 0.3s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.status-toast-container {
|
||||||
|
bottom: 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
left: 1rem;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toast-pill {
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user