Initial commit: Learning Lab Express
Includes fixes found while reviewing the app: - Results: open each result by a type+agent+id key, so a video and a material draft with the same id no longer open each other's popup - AuthGate: clear the query cache during render, before pages build their queries, and remount pages on organization switch (pages no longer stay on skeletons for up to a minute after sign-in or refresh) - Instruction: keep only failed rules after a partial approve, so retrying does not create the successful schedules twice; drop the monthly note once count or frequency is edited - Parser/schedules: never schedule a count below one - Audio: ignore the end event of a cancelled utterance when changing speed - Downloads: revoke the object URL later so Safari/Firefox can read it - Results: refetch SCORM content when any part's revision changes; show a failed video as failed instead of "still rendering" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
28
src/components/Toast.tsx
Normal file
28
src/components/Toast.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Short notices at the bottom of the screen (styles from the mockup's .toast), instead of alert().
|
||||
// A module-level store, so mutations can report from anywhere without a context provider.
|
||||
import { useSyncExternalStore } from 'react'
|
||||
|
||||
interface Notice { id: number; text: string; error: boolean }
|
||||
|
||||
let current: Notice | null = null
|
||||
let nextId = 1
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
const listeners = new Set<() => void>()
|
||||
const emit = () => listeners.forEach(l => l())
|
||||
|
||||
export function toast(text: string, { error = false } = {}) {
|
||||
current = { id: nextId++, text, error }
|
||||
clearTimeout(timer)
|
||||
// Errors stay a little longer: they usually need reading.
|
||||
timer = setTimeout(() => { current = null; emit() }, error ? 7000 : 3500)
|
||||
emit()
|
||||
}
|
||||
|
||||
export function Toaster() {
|
||||
const notice = useSyncExternalStore(cb => { listeners.add(cb); return () => listeners.delete(cb) }, () => current)
|
||||
return (
|
||||
<div className={`toast${notice ? ' show' : ''}${notice?.error ? ' error' : ''}`} role={notice?.error ? 'alert' : 'status'} aria-live="polite">
|
||||
{notice?.text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user