feat: add Dunk It button animation with CSS transitions (#427)
Add a CSS-only Dunk It button to task cards with a 4-phase state machine (idle, success, error, dismissing). Uses inline CSS transitions for animations with no external dependencies. Supersedes #373
This commit is contained in:
parent
2f2531f3d1
commit
d0cd8701c7
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "ملاحظات المراجعة...",
|
||||
"submit": "إرسال",
|
||||
"loadingTranscript": "جارٍ تحميل النص...",
|
||||
"noSessionMessages": "لا توجد رسائل جلسة"
|
||||
"noSessionMessages": "لا توجد رسائل جلسة",
|
||||
"dunkIt": "أنجزها"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "إدارة Cron",
|
||||
|
|
|
|||
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "Prüfnotizen...",
|
||||
"submit": "Einreichen",
|
||||
"loadingTranscript": "Transkript laden...",
|
||||
"noSessionMessages": "Keine Sitzungsnachrichten"
|
||||
"noSessionMessages": "Keine Sitzungsnachrichten",
|
||||
"dunkIt": "Erledigen"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "Cron-Verwaltung",
|
||||
|
|
|
|||
|
|
@ -819,7 +819,8 @@
|
|||
"reviewNotesPlaceholder": "Review notes...",
|
||||
"submit": "Submit",
|
||||
"loadingTranscript": "Loading transcript...",
|
||||
"noSessionMessages": "No session messages"
|
||||
"noSessionMessages": "No session messages",
|
||||
"dunkIt": "Dunk It"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "Cron Management",
|
||||
|
|
|
|||
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "Notas de revisión...",
|
||||
"submit": "Enviar",
|
||||
"loadingTranscript": "Cargando transcripción...",
|
||||
"noSessionMessages": "Sin mensajes de sesión"
|
||||
"noSessionMessages": "Sin mensajes de sesión",
|
||||
"dunkIt": "Completar"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "Gestión de cron",
|
||||
|
|
|
|||
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "Notes de révision...",
|
||||
"submit": "Soumettre",
|
||||
"loadingTranscript": "Chargement de la transcription...",
|
||||
"noSessionMessages": "Aucun message de session"
|
||||
"noSessionMessages": "Aucun message de session",
|
||||
"dunkIt": "Terminer"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "Gestion des crons",
|
||||
|
|
|
|||
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "レビューノート...",
|
||||
"submit": "送信",
|
||||
"loadingTranscript": "トランスクリプトを読み込み中...",
|
||||
"noSessionMessages": "セッションメッセージなし"
|
||||
"noSessionMessages": "セッションメッセージなし",
|
||||
"dunkIt": "完了"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "Cron管理",
|
||||
|
|
|
|||
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "검토 메모...",
|
||||
"submit": "제출",
|
||||
"loadingTranscript": "기록 로딩 중...",
|
||||
"noSessionMessages": "세션 메시지 없음"
|
||||
"noSessionMessages": "세션 메시지 없음",
|
||||
"dunkIt": "완료"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "크론 관리",
|
||||
|
|
|
|||
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "Notas de revisão...",
|
||||
"submit": "Enviar",
|
||||
"loadingTranscript": "Carregando transcrição...",
|
||||
"noSessionMessages": "Sem mensagens de sessão"
|
||||
"noSessionMessages": "Sem mensagens de sessão",
|
||||
"dunkIt": "Concluir"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "Gerenciamento de cron",
|
||||
|
|
|
|||
|
|
@ -670,7 +670,8 @@
|
|||
"reviewNotesPlaceholder": "Заметки проверки...",
|
||||
"submit": "Отправить",
|
||||
"loadingTranscript": "Загрузка транскрипта...",
|
||||
"noSessionMessages": "Нет сообщений сессии"
|
||||
"noSessionMessages": "Нет сообщений сессии",
|
||||
"dunkIt": "Завершить"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "Управление cron",
|
||||
|
|
|
|||
|
|
@ -908,7 +908,8 @@
|
|||
"reviewNotesPlaceholder": "审查备注...",
|
||||
"submit": "提交",
|
||||
"loadingTranscript": "加载记录中...",
|
||||
"noSessionMessages": "无会话消息"
|
||||
"noSessionMessages": "无会话消息",
|
||||
"dunkIt": "完成"
|
||||
},
|
||||
"cronManagement": {
|
||||
"title": "定时任务管理",
|
||||
|
|
|
|||
|
|
@ -310,6 +310,57 @@ function MentionTextarea({
|
|||
)
|
||||
}
|
||||
|
||||
type DunkPhase = 'idle' | 'success' | 'error' | 'dismissing'
|
||||
|
||||
function DunkItButton({ taskId, onDunked }: { taskId: number; onDunked: (id: number) => void }) {
|
||||
const t = useTranslations('taskBoard')
|
||||
const [phase, setPhase] = useState<DunkPhase>('idle')
|
||||
|
||||
const handleClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (phase !== 'idle') return
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${taskId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: 'done' }),
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed')
|
||||
setPhase('success')
|
||||
setTimeout(() => {
|
||||
setPhase('dismissing')
|
||||
setTimeout(() => onDunked(taskId), 400)
|
||||
}, 600)
|
||||
} catch {
|
||||
setPhase('error')
|
||||
setTimeout(() => setPhase('idle'), 1500)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={phase !== 'idle' && phase !== 'error'}
|
||||
title={t('dunkIt')}
|
||||
style={{
|
||||
padding: '2px 8px',
|
||||
fontSize: '11px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid',
|
||||
cursor: phase === 'idle' ? 'pointer' : 'default',
|
||||
transition: 'all 0.3s ease',
|
||||
transform: phase === 'success' ? 'scale(1.15)' : phase === 'dismissing' ? 'scale(0.8) translateY(-10px)' : 'scale(1)',
|
||||
opacity: phase === 'dismissing' ? 0 : 1,
|
||||
borderColor: phase === 'success' ? 'rgb(34 197 94 / 0.5)' : phase === 'error' ? 'rgb(239 68 68 / 0.5)' : 'hsl(var(--border))',
|
||||
backgroundColor: phase === 'success' ? 'rgb(34 197 94 / 0.15)' : phase === 'error' ? 'rgb(239 68 68 / 0.15)' : 'transparent',
|
||||
color: phase === 'success' ? 'rgb(34 197 94)' : phase === 'error' ? 'rgb(239 68 68)' : 'inherit',
|
||||
}}
|
||||
>
|
||||
{phase === 'success' ? '!' : phase === 'error' ? '!!' : phase === 'dismissing' ? '!' : 'Dunk'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
interface SpawnFormData {
|
||||
task: string
|
||||
model: string
|
||||
|
|
@ -990,6 +1041,9 @@ export function TaskBoardPanel() {
|
|||
)}
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{task.status !== 'done' && (
|
||||
<DunkItButton taskId={task.id} onDunked={() => fetchData()} />
|
||||
)}
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
|
||||
task.priority === 'critical' ? 'bg-red-500/20 text-red-400' :
|
||||
task.priority === 'high' ? 'bg-orange-500/20 text-orange-400' :
|
||||
|
|
|
|||
Loading…
Reference in New Issue