-
-
- Ao finalizar um ciclo, o CodeFocus detecta automaticamente quais commits foram feitos nesta pasta durante a sessão.
+
+
+ Ao finalizar um ciclo, o CodeFocus detecta automaticamente quais commits foram feitos durante a sessão.
-
-
- {settings.git_repo_path || 'Nenhum repositório selecionado'}
+
+
+
+
+ {settings.git_repo_path || 'Nenhum repositório selecionado'}
+
{settings.git_repo_path && (
)}
-
-
+
- {/* Ações */}
-
+ {/* Footer actions */}
+
)
- if (asPage) return {content}
+ if (asPage) return {content}
return (
-
-
{content}
+
)
}
diff --git a/src/components/TagManager.jsx b/src/components/TagManager.jsx
index 9f7352d..471c724 100644
--- a/src/components/TagManager.jsx
+++ b/src/components/TagManager.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'
+import { Tag, Plus, Trash2, Check, Flame, CheckSquare, ListTodo, ListChecks, X } from 'lucide-react'
import { dbService } from '../services/dbService'
const TagManager = () => {
@@ -80,117 +81,213 @@ const TagManager = () => {
}
if (loading) {
- return
+ return (
+
+ {[0,1,2].map((i) =>
)}
+
+ )
}
return (
-
-
-
Gerenciador de Tarefas
-
Organize suas tarefas e acompanhe seu progresso
+
+ {/* Header */}
+
+
Tarefas
+
Organize com tags e acompanhe os pomodoros
{/* Stats */}
-
+
{[
- { label: 'Total', value: stats.total, colors: 'from-blue-600/20 to-purple-600/20' },
- { label: 'Concluídas', value: stats.completed, colors: 'from-green-600/20 to-emerald-600/20' },
- { label: 'Ativas', value: stats.active, colors: 'from-orange-600/20 to-red-600/20' },
- { label: 'Pomodoros', value: stats.totalPomodoros, colors: 'from-purple-600/20 to-pink-600/20' },
- ].map((s) => (
-
-
{s.label}
-
{s.value}
+ { label: 'Total', value: stats.total, icon: ListTodo, accent: '#6366f1' },
+ { label: 'Concluídas', value: stats.completed, icon: ListChecks, accent: '#10b981' },
+ { label: 'Ativas', value: stats.active, icon: CheckSquare, accent: '#f59e0b' },
+ { label: 'Pomodoros', value: stats.totalPomodoros, icon: Flame, accent: '#f43f5e' },
+ ].map(({ label, value, icon: Icon, accent }) => (
+
+
+
+ {label}
+
+
{value}
))}
- {/* Tags */}
-
-
🏷️ Gerenciar Tags
-
+ {/* Tags manager */}
+
+
+
+
Gerenciar Tags
+
+
setNewTag(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTag()}
placeholder="Nova tag..."
- className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ className="flex-1 px-3 py-2 rounded-lg text-sm text-white placeholder-white/30 transition-all"
+ style={{ background: 'var(--surface-3)', border: '1px solid var(--border-subtle)' }}
/>
-
-
-
- {tags.map((tag) => (
-
setSelectedTags((p) => p.includes(tag.name) ? p.filter((t) => t !== tag.name) : [...p, tag.name])}
- >
- {tag.name}
-
-
- ))}
+
+ {tags.length > 0 ? (
+
+ {tags.map((tag) => (
+
setSelectedTags((p) => p.includes(tag.name) ? p.filter((t) => t !== tag.name) : [...p, tag.name])}
+ className="group flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium cursor-pointer transition-all"
+ style={selectedTags.includes(tag.name) ? {
+ background: 'rgba(99,102,241,0.25)',
+ color: '#a5b4fc',
+ border: '1px solid rgba(99,102,241,0.45)',
+ } : {
+ background: 'var(--surface-3)',
+ color: 'var(--text-secondary)',
+ border: '1px solid var(--border-subtle)',
+ }}
+ >
+
+ {tag.name}
+
+
+ ))}
+
+ ) : (
+
Nenhuma tag criada ainda.
+ )}
- {/* Nova Tarefa */}
-
-
➕ Nova Tarefa
-
+ {/* New task */}
+
+
+
setNewTask(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTask()}
placeholder="O que você precisa fazer?"
- className="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
+ className="w-full px-3 py-2.5 rounded-xl text-sm text-white placeholder-white/30 transition-all"
+ style={{ background: 'var(--surface-3)', border: '1px solid var(--border-subtle)' }}
/>
{selectedTags.length > 0 && (
-
-
Tags:
- {selectedTags.map((t) =>
#{t})}
+
+ {selectedTags.map((t) => (
+
+ #{t}
+
+ ))}
)}
-
- {/* Filtros */}
-
+ {/* Filter */}
+
{[['all', 'Todas'], ['active', 'Ativas'], ['completed', 'Concluídas']].map(([f, label]) => (
-
setFilter(f)} className={`px-4 py-2 rounded-lg font-medium transition-all ${filter === f ? 'bg-white/20 text-white' : 'text-white/60 hover:text-white hover:bg-white/10'}`}>
+ setFilter(f)}
+ className="px-4 py-2.5 text-xs font-medium relative transition-colors"
+ style={filter === f ? { color: 'var(--text-primary)' } : { color: 'var(--text-muted)' }}
+ >
{label}
+ {filter === f && }
))}
- {/* Lista de Tarefas */}
-
+ {/* Task list */}
+
{filteredTasks.length === 0 ? (
-
Nenhuma tarefa encontrada
+
+
+
Nenhuma tarefa encontrada
+
) : filteredTasks.map((task) => (
-
-
-
toggleTask(task.id)} className={`w-6 h-6 rounded-full border-2 transition-all flex items-center justify-center ${task.completed ? 'bg-green-500 border-green-500' : 'border-white/40 hover:border-white'}`}>
- {task.completed && ✓}
-
-
-
{task.text}
- {task.tags?.length > 0 && (
-
- {task.tags.map((t, i) => #{t})}
-
- )}
-
-
- incrementPomodoro(task.id)} className="px-3 py-1 bg-red-500/20 hover:bg-red-500/30 text-red-300 rounded-lg text-sm flex items-center gap-1">
- 🍅{task.pomodoro_count || 0}
-
- removeTask(task.id)} className="p-2 text-white/40 hover:text-red-400 transition-colors">🗑️
-
+
+ {/* Checkbox */}
+
toggleTask(task.id)}
+ className="w-5 h-5 rounded-md shrink-0 flex items-center justify-center transition-all"
+ style={task.completed ? {
+ background: 'rgba(16,185,129,0.25)',
+ border: '1.5px solid rgba(16,185,129,0.5)',
+ } : {
+ border: '1.5px solid var(--border-default)',
+ }}
+ >
+ {task.completed && }
+
+
+ {/* Text + tags */}
+
+
{task.text}
+ {task.tags?.length > 0 && (
+
+ {task.tags.map((t, i) => (
+
+ #{t}
+
+ ))}
+
+ )}
+
+ {/* Pomodoro counter */}
+
incrementPomodoro(task.id)}
+ className="flex items-center gap-1 px-2 py-1 rounded-lg text-xs font-medium transition-all hover:scale-[1.05] shrink-0"
+ style={{ background: 'rgba(244,63,94,0.12)', border: '1px solid rgba(244,63,94,0.25)', color: 'rgba(244,63,94,0.8)' }}
+ >
+
+ {task.pomodoro_count || 0}
+
+
+ {/* Delete */}
+
removeTask(task.id)}
+ className="w-7 h-7 rounded-lg flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all hover:bg-red-500/15"
+ style={{ color: 'var(--text-muted)' }}
+ >
+
+
))}
diff --git a/src/components/Timer.jsx b/src/components/Timer.jsx
index 1d163d0..54500b0 100644
--- a/src/components/Timer.jsx
+++ b/src/components/Timer.jsx
@@ -1,10 +1,15 @@
import { useState, useEffect, forwardRef, useImperativeHandle, useRef, useCallback } from 'react'
+import { Play, Pause, RotateCcw, CheckCircle, StickyNote, X, Brain, Coffee, Moon } from 'lucide-react'
import notificationManager from '../utils/notificationUtils'
import GitDetectModal from './GitDetectModal'
import { loadSettings } from '../utils/settingsUtils'
import { dbService } from '../services/dbService'
import { useTodayStats } from '../hooks/useTodayStats'
+const PHASE_COLORS = { focus: '#f43f5e', shortBreak: '#14b8a6', longBreak: '#38bdf8' }
+const PHASE_NAMES = { focus: 'Foco', shortBreak: 'Pausa Curta', longBreak: 'Pausa Longa' }
+const PHASE_ICONS = { focus: Brain, shortBreak: Coffee, longBreak: Moon }
+
const Timer = forwardRef((props, ref) => {
const [settings, setSettings] = useState(loadSettings())
const [pendingAutoStartPhase, setPendingAutoStartPhase] = useState(null)
@@ -50,7 +55,6 @@ const Timer = forwardRef((props, ref) => {
const inputRef = useRef(null)
const [todayStats, refreshTodayStats] = useTodayStats(isRunning)
- // Atualizar settings quando mudarem
useEffect(() => {
const onStorage = (e) => {
if (e.key === 'codefocus-settings') setSettings(loadSettings())
@@ -59,7 +63,6 @@ const Timer = forwardRef((props, ref) => {
return () => window.removeEventListener('storage', onStorage)
}, [])
- // Carregar tags
useEffect(() => {
dbService.tags.list().then((list) => {
setAvailableTags(list.map((t) => t.name))
@@ -74,14 +77,12 @@ const Timer = forwardRef((props, ref) => {
try { localStorage.setItem(getFocusCountKey(), String(n)) } catch {}
}, [])
- // Salvar estado
useEffect(() => {
try {
localStorage.setItem('codefocus-timer-state', JSON.stringify({ timeLeft, isPaused, currentPhase, cycleName }))
} catch {}
}, [timeLeft, isPaused, currentPhase, cycleName])
- // Salvar sessão no histórico local
const saveSession = (duration, phase, name, tags = [], note = '') => {
try {
const history = JSON.parse(localStorage.getItem('codefocus-history') || '[]')
@@ -112,16 +113,9 @@ const Timer = forwardRef((props, ref) => {
const total = getPhaseDuration(currentPhase)
return ((total - timeLeft) / total) * 100
}
- const getPhaseColor = () => ({ focus: '#FF6B6B', shortBreak: '#4ECDC4', longBreak: '#45B7D1' }[currentPhase] || '#FF6B6B')
- const getPhaseName = () => ({ focus: 'FOCUS', shortBreak: 'SHORT BREAK', longBreak: 'LONG BREAK' }[currentPhase] || '')
- const getStatusMessage = () => {
- if (!isRunning && !isPaused && currentPhase === 'focus') return 'Pronto para focar? 🎯'
- if (isRunning) return { focus: 'Mantenha o foco! 💪', shortBreak: 'Descanse um pouco 😌', longBreak: 'Hora de relaxar 🌟' }[currentPhase] || ''
- if (isPaused) return 'Timer pausado ⏸️'
- return ''
- }
+ const getPhaseColor = () => PHASE_COLORS[currentPhase] || PHASE_COLORS.focus
+ const getPhaseName = () => PHASE_NAMES[currentPhase] || 'Foco'
- // Timer principal
useEffect(() => {
let interval = null
if (isRunning && timeLeft > 0) {
@@ -193,9 +187,14 @@ const Timer = forwardRef((props, ref) => {
notificationManager.notifyPauseStarted('longBreak')
}
- const pauseTimer = () => { setIsRunning(false); setIsPaused(true); notificationManager.notifyTimerPaused() }
- const resumeTimer = () => { setIsRunning(true); setIsPaused(false); notificationManager.notifyTimerResumed() }
- const resetTimer = () => { setIsRunning(false); setIsPaused(false); setTimeLeft((settings.focus_time || 25) * 60); setCurrentPhase('focus') }
+ const pauseTimer = () => { setIsRunning(false); setIsPaused(true); notificationManager.notifyTimerPaused() }
+ const resumeTimer = () => { setIsRunning(true); setIsPaused(false); notificationManager.notifyTimerResumed() }
+ const resetTimer = () => {
+ setIsRunning(false)
+ setIsPaused(false)
+ setTimeLeft((settings.focus_time || 25) * 60)
+ setCurrentPhase('focus')
+ }
const handleStartFocus = () => {
if (!cycleName.trim()) {
@@ -265,165 +264,280 @@ const Timer = forwardRef((props, ref) => {
}
const progress = calculateProgress()
+ const phaseColor = getPhaseColor()
+ const PhaseIcon = PHASE_ICONS[currentPhase] || Brain
return (
-
- {/* Status */}
-
-
{getStatusMessage()}
+
+
+ {/* Phase badge */}
+
{/* Timer circular */}
+ {/* Track */}
+ {/* Ambient glow ring */}
+ {isRunning && (
+
+ )}
+ {/* Progress ring */}
-
-
+
+ {/* Center content */}
+
+
{formatTime(timeLeft)}
-
- {cycleName && currentPhase === 'focus' ? cycleName : getPhaseName()}
-
-
{Math.round(progress)}% completo
+
{Math.round(progress)}%
+ {/* Cycle name display (when running/paused) */}
+ {(isRunning || isPaused) && cycleName && (
+
+ {cycleName}
+
+ )}
+
{/* Quick Stats */}
-
+
-
Sessões hoje
+
Sessões
{todayStats.sessions}
+
-
Tempo total
-
{todayStats.minutes}min
+
Minutos
+
{todayStats.minutes}
- {/* Controles */}
-
- {!isRunning && !isPaused && (
-
- {currentPhase === 'focus' && (
- setCycleName(e.target.value)}
- ref={inputRef}
- className="px-5 py-2.5 rounded-full bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white/20 transition-all w-72 text-center text-sm"
- onKeyDown={(e) => { if (e.key === 'Enter' && cycleName.trim()) startFocus() }}
- />
- )}
-
- ▶
- Iniciar Foco
-
-
+ {/* Controls */}
+
+ {/* Input (idle focus phase) */}
+ {!isRunning && !isPaused && currentPhase === 'focus' && (
+
setCycleName(e.target.value)}
+ ref={inputRef}
+ className="px-5 py-2.5 rounded-full text-white placeholder-white/30 text-center text-sm w-72 transition-all"
+ style={{
+ background: 'var(--surface-3)',
+ border: '1px solid var(--border-subtle)',
+ }}
+ onFocus={(e) => { e.target.style.borderColor = 'var(--border-default)' }}
+ onBlur={(e) => { e.target.style.borderColor = 'var(--border-subtle)' }}
+ onKeyDown={(e) => { if (e.key === 'Enter' && cycleName.trim()) startFocus() }}
+ />
)}
- {isRunning && (
-
+ {/* Primary action buttons */}
+
+ {/* Play / Pause / Resume */}
+ {!isRunning && !isPaused && (
- ⏸ Pausar
+
- {currentPhase === 'focus' && (
+ )}
+
+ {isRunning && (
+ <>
- ✓ Concluir
+
- )}
-
- )}
+ {currentPhase === 'focus' && (
+
+
+
+ )}
+ >
+ )}
- {isPaused && (
-
-
- ▶ Continuar
-
-
- ↺ Reiniciar
-
-
- )}
+ {isPaused && (
+ <>
+
+
+
+
+
+
+ >
+ )}
- {isRunning && currentPhase === 'focus' && (
-
+ {/* Note button (visible while running) */}
+ {isRunning && currentPhase === 'focus' && (
setShowNoteModal(true)}
- className="px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white/80 hover:text-white transition-all text-sm flex items-center gap-2"
+ className="w-10 h-10 rounded-full flex items-center justify-center transition-all duration-150 hover:scale-105"
+ style={{
+ background: 'var(--surface-3)',
+ border: '1px solid var(--border-subtle)',
+ color: 'var(--text-muted)',
+ }}
+ aria-label="Adicionar nota"
+ title="Adicionar nota"
>
- 📝 Adicionar nota
+
-
- )}
+ )}
+
- {/* Modal de nota rápida (durante o ciclo) */}
+ {/* Session note modal */}
{showNoteModal && !showGitModal && (
-
-
-
-
📝 Nota da Sessão
-
setShowNoteModal(false)} className="text-white/60 hover:text-white">✕
+
+
+
+
+
setShowNoteModal(false)}
+ className="w-7 h-7 rounded-lg flex items-center justify-center hover:bg-white/10 transition-colors"
+ style={{ color: 'var(--text-muted)' }}>
+
+
-
)}
- {/* Git Modal (ao finalizar ciclo) */}
+ {/* Git Modal */}
{
diff --git a/src/services/dbService.js b/src/services/dbService.js
index bb86a24..ae7e870 100644
--- a/src/services/dbService.js
+++ b/src/services/dbService.js
@@ -279,6 +279,29 @@ const cycles = {
productivity_score: score,
}
},
+
+ async statsMonthly() {
+ const db = await getDb()
+ const rows = await db.select(
+ `SELECT strftime('%Y-W%W', created_at) as week_label,
+ MIN(date(created_at)) as week_start,
+ phase, duration
+ FROM cycles
+ WHERE created_at >= datetime('now', '-28 days')
+ ORDER BY week_start ASC`
+ )
+ const byWeek = {}
+ for (const row of rows) {
+ if (!byWeek[row.week_label]) {
+ byWeek[row.week_label] = { week: row.week_label, week_start: row.week_start, total_focus_minutes: 0, sessions: 0 }
+ }
+ if (row.phase === 'focus') {
+ byWeek[row.week_label].total_focus_minutes += row.duration || 0
+ byWeek[row.week_label].sessions += 1
+ }
+ }
+ return Object.values(byWeek)
+ },
}
// ========== TAGS ==========
diff --git a/src/styles/globals.css b/src/styles/globals.css
index d2dfeea..195dc3e 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -2,24 +2,77 @@
@tailwind components;
@tailwind utilities;
-/* Scrollbar global */
+/* ─── Design Tokens ─── */
+:root {
+ --surface-0: #0d0f12;
+ --surface-1: #13161b;
+ --surface-2: rgba(255,255,255,0.04);
+ --surface-3: rgba(255,255,255,0.08);
+ --border-subtle: rgba(255,255,255,0.07);
+ --border-default: rgba(255,255,255,0.12);
+ --text-primary: #f1f5f9;
+ --text-secondary: rgba(255,255,255,0.55);
+ --text-muted: rgba(255,255,255,0.28);
+ --phase-focus: #f43f5e;
+ --phase-short: #14b8a6;
+ --phase-long: #38bdf8;
+ --radius-sm: 8px;
+ --radius-md: 12px;
+ --radius-lg: 16px;
+ --radius-xl: 20px;
+}
+
+/* ─── Utility Classes ─── */
+@layer components {
+ .card {
+ background: var(--surface-2);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ backdrop-filter: blur(12px);
+ }
+ .card-hover {
+ transition: background 150ms ease, border-color 150ms ease;
+ }
+ .card-hover:hover {
+ background: var(--surface-3);
+ border-color: var(--border-default);
+ }
+ .skeleton {
+ background: linear-gradient(
+ 90deg,
+ rgba(255,255,255,0.04) 25%,
+ rgba(255,255,255,0.08) 50%,
+ rgba(255,255,255,0.04) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: var(--radius-sm);
+ }
+}
+
+/* ─── Scrollbar ─── */
::-webkit-scrollbar { width: 6px; height: 6px; }
-::-webkit-scrollbar-track { background: rgba(255,255,255,0.05); border-radius: 3px; }
-::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 3px; }
-::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.3); }
+::-webkit-scrollbar-track { background: rgba(255,255,255,0.03); border-radius: 3px; }
+::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
-/* Seleção de texto */
+/* ─── Text selection ─── */
::selection { background: rgba(59, 130, 246, 0.3); }
-/* Animações */
+/* ─── Animations ─── */
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideIn { from { transform: translateY(-10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
+@keyframes slideUp { from { transform: translateY(6px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
+@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
+@keyframes glowPulse { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } }
-.animate-fade-in { animation: fadeIn 0.3s ease-in-out; }
-.animate-slide-in { animation: slideIn 0.3s ease-out; }
+.animate-fade-in { animation: fadeIn 200ms ease-out; }
+.animate-slide-in { animation: slideIn 200ms ease-out; }
+.animate-slide-up { animation: slideUp 200ms ease-out; }
+.animate-glow-pulse { animation: glowPulse 2s ease-in-out infinite; }
-/* Remover outline padrão em inputs */
+/* ─── Input reset ─── */
input:focus, textarea:focus, select:focus { outline: none; }
diff --git a/src/utils/notificationUtils.js b/src/utils/notificationUtils.js
index f82e36f..5d127d5 100644
--- a/src/utils/notificationUtils.js
+++ b/src/utils/notificationUtils.js
@@ -48,10 +48,11 @@ class NotificationManager {
showToast(title, message = '', type = 'info', duration = 4000) {
this.createToastContainer()
- const toast = document.createElement('div')
const typeColors = { info: '#3B82F6', success: '#10B981', warning: '#F59E0B', error: '#EF4444' }
- const icons = { success: '✅', warning: '⚠️', error: '❌', info: '🎯' }
+ const icons = { success: '✅', warning: '⚠️', error: '❌', info: '🎯' }
+ // Build DOM manually — never use innerHTML with dynamic values (XSS risk)
+ const toast = document.createElement('div')
toast.style.cssText = `
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.2); border-left: 4px solid ${typeColors[type] || typeColors.info};
@@ -60,16 +61,39 @@ class NotificationManager {
pointer-events: auto; transform: translateX(100%);
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
`
- toast.innerHTML = `
-
-
${icons[type] || icons.info}
-
-
${title}
- ${message ? `
${message}
` : ''}
-
-
×
-
- `
+
+ const row = document.createElement('div')
+ row.style.cssText = 'display:flex;align-items:flex-start;gap:12px'
+
+ const iconEl = document.createElement('div')
+ iconEl.style.cssText = 'font-size:20px;line-height:1;margin-top:2px'
+ iconEl.textContent = icons[type] || icons.info
+
+ const body = document.createElement('div')
+ body.style.cssText = 'flex:1'
+
+ const titleEl = document.createElement('div')
+ titleEl.style.cssText = 'font-weight:600;font-size:14px;margin-bottom:4px'
+ titleEl.textContent = title
+
+ body.appendChild(titleEl)
+
+ if (message) {
+ const msgEl = document.createElement('div')
+ msgEl.style.cssText = 'font-size:13px;opacity:.8;line-height:1.4'
+ msgEl.textContent = message
+ body.appendChild(msgEl)
+ }
+
+ const closeBtn = document.createElement('button')
+ closeBtn.style.cssText = 'background:none;border:none;color:rgba(255,255,255,.6);cursor:pointer;font-size:18px;line-height:1;padding:0'
+ closeBtn.textContent = '×'
+ closeBtn.addEventListener('click', () => toast.remove())
+
+ row.appendChild(iconEl)
+ row.appendChild(body)
+ row.appendChild(closeBtn)
+ toast.appendChild(row)
this.toastContainer.appendChild(toast)
setTimeout(() => { toast.style.transform = 'translateX(0)' }, 10)
diff --git a/tailwind.config.js b/tailwind.config.js
index 9f6fb9f..a204ede 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -4,13 +4,24 @@ export default {
theme: {
extend: {
colors: {
- gray: {
- 750: '#2d374850',
- },
+ gray: { 750: '#2d374850' },
+ },
+ spacing: {
+ '18': '4.5rem',
+ '22': '5.5rem',
+ },
+ fontSize: {
+ '2xs': ['0.625rem', { lineHeight: '1rem' }],
+ },
+ fontFamily: {
+ mono: ['"JetBrains Mono"', '"Fira Code"', 'Menlo', 'monospace'],
},
animation: {
- 'fade-in': 'fadeIn 0.3s ease-in-out',
- 'slide-in': 'slideIn 0.3s ease-out',
+ 'fade-in': 'fadeIn 200ms ease-out',
+ 'slide-in': 'slideIn 200ms ease-out',
+ 'slide-up': 'slideUp 200ms ease-out',
+ 'glow-pulse': 'glowPulse 2s ease-in-out infinite',
+ 'shimmer': 'shimmer 1.5s infinite',
},
keyframes: {
fadeIn: {
@@ -21,6 +32,18 @@ export default {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
+ slideUp: {
+ '0%': { transform: 'translateY(6px)', opacity: '0' },
+ '100%': { transform: 'translateY(0)', opacity: '1' },
+ },
+ glowPulse: {
+ '0%,100%': { opacity: '0.6' },
+ '50%': { opacity: '1' },
+ },
+ shimmer: {
+ '0%': { backgroundPosition: '200% 0' },
+ '100%': { backgroundPosition: '-200% 0' },
+ },
},
},
},