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": "ملاحظات المراجعة...",
|
"reviewNotesPlaceholder": "ملاحظات المراجعة...",
|
||||||
"submit": "إرسال",
|
"submit": "إرسال",
|
||||||
"loadingTranscript": "جارٍ تحميل النص...",
|
"loadingTranscript": "جارٍ تحميل النص...",
|
||||||
"noSessionMessages": "لا توجد رسائل جلسة"
|
"noSessionMessages": "لا توجد رسائل جلسة",
|
||||||
|
"dunkIt": "أنجزها"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "إدارة Cron",
|
"title": "إدارة Cron",
|
||||||
|
|
|
||||||
|
|
@ -670,7 +670,8 @@
|
||||||
"reviewNotesPlaceholder": "Prüfnotizen...",
|
"reviewNotesPlaceholder": "Prüfnotizen...",
|
||||||
"submit": "Einreichen",
|
"submit": "Einreichen",
|
||||||
"loadingTranscript": "Transkript laden...",
|
"loadingTranscript": "Transkript laden...",
|
||||||
"noSessionMessages": "Keine Sitzungsnachrichten"
|
"noSessionMessages": "Keine Sitzungsnachrichten",
|
||||||
|
"dunkIt": "Erledigen"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "Cron-Verwaltung",
|
"title": "Cron-Verwaltung",
|
||||||
|
|
|
||||||
|
|
@ -819,7 +819,8 @@
|
||||||
"reviewNotesPlaceholder": "Review notes...",
|
"reviewNotesPlaceholder": "Review notes...",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"loadingTranscript": "Loading transcript...",
|
"loadingTranscript": "Loading transcript...",
|
||||||
"noSessionMessages": "No session messages"
|
"noSessionMessages": "No session messages",
|
||||||
|
"dunkIt": "Dunk It"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "Cron Management",
|
"title": "Cron Management",
|
||||||
|
|
|
||||||
|
|
@ -670,7 +670,8 @@
|
||||||
"reviewNotesPlaceholder": "Notas de revisión...",
|
"reviewNotesPlaceholder": "Notas de revisión...",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"loadingTranscript": "Cargando transcripción...",
|
"loadingTranscript": "Cargando transcripción...",
|
||||||
"noSessionMessages": "Sin mensajes de sesión"
|
"noSessionMessages": "Sin mensajes de sesión",
|
||||||
|
"dunkIt": "Completar"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "Gestión de cron",
|
"title": "Gestión de cron",
|
||||||
|
|
|
||||||
|
|
@ -670,7 +670,8 @@
|
||||||
"reviewNotesPlaceholder": "Notes de révision...",
|
"reviewNotesPlaceholder": "Notes de révision...",
|
||||||
"submit": "Soumettre",
|
"submit": "Soumettre",
|
||||||
"loadingTranscript": "Chargement de la transcription...",
|
"loadingTranscript": "Chargement de la transcription...",
|
||||||
"noSessionMessages": "Aucun message de session"
|
"noSessionMessages": "Aucun message de session",
|
||||||
|
"dunkIt": "Terminer"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "Gestion des crons",
|
"title": "Gestion des crons",
|
||||||
|
|
|
||||||
|
|
@ -670,7 +670,8 @@
|
||||||
"reviewNotesPlaceholder": "レビューノート...",
|
"reviewNotesPlaceholder": "レビューノート...",
|
||||||
"submit": "送信",
|
"submit": "送信",
|
||||||
"loadingTranscript": "トランスクリプトを読み込み中...",
|
"loadingTranscript": "トランスクリプトを読み込み中...",
|
||||||
"noSessionMessages": "セッションメッセージなし"
|
"noSessionMessages": "セッションメッセージなし",
|
||||||
|
"dunkIt": "完了"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "Cron管理",
|
"title": "Cron管理",
|
||||||
|
|
|
||||||
|
|
@ -670,7 +670,8 @@
|
||||||
"reviewNotesPlaceholder": "검토 메모...",
|
"reviewNotesPlaceholder": "검토 메모...",
|
||||||
"submit": "제출",
|
"submit": "제출",
|
||||||
"loadingTranscript": "기록 로딩 중...",
|
"loadingTranscript": "기록 로딩 중...",
|
||||||
"noSessionMessages": "세션 메시지 없음"
|
"noSessionMessages": "세션 메시지 없음",
|
||||||
|
"dunkIt": "완료"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "크론 관리",
|
"title": "크론 관리",
|
||||||
|
|
|
||||||
|
|
@ -670,7 +670,8 @@
|
||||||
"reviewNotesPlaceholder": "Notas de revisão...",
|
"reviewNotesPlaceholder": "Notas de revisão...",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"loadingTranscript": "Carregando transcrição...",
|
"loadingTranscript": "Carregando transcrição...",
|
||||||
"noSessionMessages": "Sem mensagens de sessão"
|
"noSessionMessages": "Sem mensagens de sessão",
|
||||||
|
"dunkIt": "Concluir"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "Gerenciamento de cron",
|
"title": "Gerenciamento de cron",
|
||||||
|
|
|
||||||
|
|
@ -670,7 +670,8 @@
|
||||||
"reviewNotesPlaceholder": "Заметки проверки...",
|
"reviewNotesPlaceholder": "Заметки проверки...",
|
||||||
"submit": "Отправить",
|
"submit": "Отправить",
|
||||||
"loadingTranscript": "Загрузка транскрипта...",
|
"loadingTranscript": "Загрузка транскрипта...",
|
||||||
"noSessionMessages": "Нет сообщений сессии"
|
"noSessionMessages": "Нет сообщений сессии",
|
||||||
|
"dunkIt": "Завершить"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "Управление cron",
|
"title": "Управление cron",
|
||||||
|
|
|
||||||
|
|
@ -908,7 +908,8 @@
|
||||||
"reviewNotesPlaceholder": "审查备注...",
|
"reviewNotesPlaceholder": "审查备注...",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"loadingTranscript": "加载记录中...",
|
"loadingTranscript": "加载记录中...",
|
||||||
"noSessionMessages": "无会话消息"
|
"noSessionMessages": "无会话消息",
|
||||||
|
"dunkIt": "完成"
|
||||||
},
|
},
|
||||||
"cronManagement": {
|
"cronManagement": {
|
||||||
"title": "定时任务管理",
|
"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 {
|
interface SpawnFormData {
|
||||||
task: string
|
task: string
|
||||||
model: string
|
model: string
|
||||||
|
|
@ -990,6 +1041,9 @@ export function TaskBoardPanel() {
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-1.5 shrink-0">
|
<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 ${
|
<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 === 'critical' ? 'bg-red-500/20 text-red-400' :
|
||||||
task.priority === 'high' ? 'bg-orange-500/20 text-orange-400' :
|
task.priority === 'high' ? 'bg-orange-500/20 text-orange-400' :
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue