- CommandPalette (Alt+K): fuzzy action launcher with keyboard nav. - Toasts: ephemeral notification stack + provider. - useGlobalHotkeys: Alt+K palette toggle, G-chord navigation (G D/F/M/L/B/A/S/U/E/C), respects editable-element focus. - Layout/App: wire ToastProvider at root, mount the palette inside the authed shell, introduce the global search box in the top bar. - MazeNETRoute now renders TopologyList inline when no ?topology is present, instead of bouncing through a redirect. - index.css: a few global token tweaks consumed by the new chrome. Fixes a latent breakage: Config.tsx and MazeNET already imported ./Toasts/useToast but the directory was never committed.
34 lines
1.0 KiB
TypeScript
34 lines
1.0 KiB
TypeScript
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
import Toasts from './Toasts';
|
|
import { ToastContext } from './toast-context';
|
|
import type { Toast, ToastInput } from './toast-context';
|
|
|
|
const DISMISS_MS = 3200;
|
|
|
|
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
const [items, setItems] = useState<Toast[]>([]);
|
|
const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
|
|
|
|
const push = useCallback((t: ToastInput) => {
|
|
const id = Date.now() + Math.random();
|
|
setItems(prev => [...prev, { ...t, id }]);
|
|
const timer = setTimeout(() => {
|
|
setItems(prev => prev.filter(x => x.id !== id));
|
|
timers.current.delete(id);
|
|
}, DISMISS_MS);
|
|
timers.current.set(id, timer);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const map = timers.current;
|
|
return () => { map.forEach(clearTimeout); map.clear(); };
|
|
}, []);
|
|
|
|
return (
|
|
<ToastContext.Provider value={{ push }}>
|
|
{children}
|
|
<Toasts items={items} />
|
|
</ToastContext.Provider>
|
|
);
|
|
};
|