Add files via upload

This commit is contained in:
jasonMPM
2026-03-05 15:39:21 -06:00
committed by GitHub
parent 3333dc59d8
commit edb0e3a539
11 changed files with 592 additions and 104 deletions

View File

@@ -1,16 +1,28 @@
import { useEffect } from 'react'
export default function Modal({ isOpen, onClose, title, children, size='md' }) {
import { useEffect, useState } from 'react'
export default function Modal({ isOpen, onClose, title, children, size = 'md' }) {
const [visible, setVisible] = useState(false)
useEffect(() => {
if (isOpen) requestAnimationFrame(() => setVisible(true))
else setVisible(false)
}, [isOpen])
useEffect(() => {
const h = (e) => { if (e.key === 'Escape') onClose() }
if (isOpen) document.addEventListener('keydown', h)
return () => document.removeEventListener('keydown', h)
}, [isOpen, onClose])
if (!isOpen) return null
const sizes = { sm:'max-w-md', md:'max-w-lg', lg:'max-w-2xl', xl:'max-w-4xl' }
const sizes = { sm: 'max-w-md', md: 'max-w-lg', lg: 'max-w-2xl', xl: 'max-w-4xl' }
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
<div className={`relative w-full ${sizes[size]} mx-4 bg-surface-raised border border-surface-border rounded-xl shadow-2xl`}>
<div className={`absolute inset-0 bg-black/70 backdrop-blur-sm transition-opacity duration-200 ${visible ? 'opacity-100' : 'opacity-0'}`}
onClick={onClose} />
<div className={`relative w-full ${sizes[size]} mx-4 bg-surface-raised border border-surface-border rounded-xl shadow-2xl
transition-all duration-200 ${visible ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-95 translate-y-2'}`}>
<div className="flex items-center justify-between px-6 py-4 border-b border-surface-border">
<h2 className="text-lg font-semibold text-gold">{title}</h2>
<button onClick={onClose} className="text-text-muted hover:text-text-primary transition-colors text-xl leading-none"></button>

View File

@@ -0,0 +1,50 @@
import { useEffect, useState } from 'react'
import useToastStore from '../../store/useToastStore'
function ToastItem({ toast }) {
const removeToast = useToastStore(s => s.removeToast)
const [secs, setSecs] = useState(toast.duration)
useEffect(() => {
const t = setInterval(() => setSecs(s => {
if (s <= 1) { clearInterval(t); removeToast(toast.id); return 0 }
return s - 1
}), 1000)
return () => clearInterval(t)
}, [])
return (
<div className="flex items-center gap-3 bg-surface-elevated border border-surface-border rounded-xl px-4 py-3 shadow-2xl min-w-[300px] animate-slide-up">
<span className="text-text-primary text-sm flex-1">{toast.message}</span>
{toast.undoFn && (
<button onClick={() => { toast.undoFn(); removeToast(toast.id) }}
className="text-gold hover:text-gold-light text-xs font-bold border border-gold/40 hover:border-gold rounded px-2.5 py-1 transition-all flex-shrink-0">
Undo
</button>
)}
<div className="flex items-center gap-1.5 flex-shrink-0">
<div className="relative w-5 h-5">
<svg className="w-5 h-5 -rotate-90" viewBox="0 0 20 20">
<circle cx="10" cy="10" r="8" fill="none" stroke="#2E2E2E" strokeWidth="2"/>
<circle cx="10" cy="10" r="8" fill="none" stroke="#C9A84C" strokeWidth="2"
strokeDasharray={`${(secs / toast.duration) * 50.3} 50.3`} strokeLinecap="round"/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-[8px] text-text-muted font-mono">{secs}</span>
</div>
<button onClick={() => removeToast(toast.id)} className="text-text-muted hover:text-text-primary transition-colors"></button>
</div>
</div>
)
}
export default function ToastContainer() {
const toasts = useToastStore(s => s.toasts)
if (!toasts.length) return null
return (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[300] flex flex-col-reverse gap-2 items-center pointer-events-none">
{toasts.map(t => (
<div key={t.id} className="pointer-events-auto"><ToastItem toast={t} /></div>
))}
</div>
)
}