589 lines
24 KiB
TypeScript
589 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useTimerStore } from '@/lib/store';
|
|
import { URGENCY_ORDER, getUrgencyConfig } from '@/lib/urgency';
|
|
import type { UrgencyLevel } from '@/lib/urgency';
|
|
import { CASCADE_PRESET_LABELS } from '@/lib/cascade';
|
|
import type { CascadePreset } from '@/lib/cascade';
|
|
import { X, AlarmClock, Timer, Coffee, Sparkles, CalendarDays } from 'lucide-react';
|
|
import { useMaintenanceMode } from '@/app/providers';
|
|
import { BUILT_IN_CATEGORIES, getCategoryById } from '@/lib/categories';
|
|
import { parseNaturalLanguage } from '@/lib/nl-parser';
|
|
import type { ParseResult } from '@/lib/nl-parser';
|
|
import { alarmSchema, countdownSchema, pomodoroSchema, eventSchema } from '@/lib/schemas';
|
|
|
|
type TabType = 'alarm' | 'countdown' | 'pomodoro' | 'event';
|
|
|
|
interface CreateTimerModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function CreateTimerModal({ isOpen, onClose }: CreateTimerModalProps) {
|
|
const { addAlarm, addCountdown, addPomodoro, addEvent } = useTimerStore();
|
|
const maintenanceMode = useMaintenanceMode();
|
|
|
|
const [tab, setTab] = useState<TabType>('countdown');
|
|
const [nlInput, setNlInput] = useState('');
|
|
const [nlResult, setNlResult] = useState<ParseResult | null>(null);
|
|
const [label, setLabel] = useState('');
|
|
const [urgency, setUrgency] = useState<UrgencyLevel>('standard');
|
|
const [cascadePreset, setCascadePreset] = useState<CascadePreset>('standard');
|
|
const [category, setCategory] = useState<string>('');
|
|
|
|
// Alarm fields
|
|
const [alarmTime, setAlarmTime] = useState('');
|
|
|
|
// Countdown fields
|
|
const [hours, setHours] = useState(0);
|
|
const [minutes, setMinutes] = useState(25);
|
|
const [seconds, setSeconds] = useState(0);
|
|
|
|
// Pomodoro fields
|
|
const [workMin, setWorkMin] = useState(25);
|
|
const [breakMin, setBreakMin] = useState(5);
|
|
const [longBreakMin, setLongBreakMin] = useState(15);
|
|
const [rounds, setRounds] = useState(4);
|
|
|
|
// Event fields
|
|
const [eventDate, setEventDate] = useState('');
|
|
|
|
// Custom pre-warning message
|
|
const [customMessage, setCustomMessage] = useState('');
|
|
|
|
// Validation errors
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const handleNlChange = (value: string) => {
|
|
setNlInput(value);
|
|
if (value.trim()) {
|
|
setNlResult(parseNaturalLanguage(value));
|
|
} else {
|
|
setNlResult(null);
|
|
}
|
|
};
|
|
|
|
const handleNlCreate = () => {
|
|
if (!nlResult?.success || !nlResult.timer) return;
|
|
const t = nlResult.timer;
|
|
if (t.type === 'countdown' && t.durationMs) {
|
|
addCountdown({
|
|
label: t.label,
|
|
durationMs: t.durationMs,
|
|
urgency: t.urgency,
|
|
cascade: { preset: t.cascade, intervals: [] },
|
|
});
|
|
} else if (t.type === 'alarm' && t.targetTime) {
|
|
addAlarm({
|
|
label: t.label,
|
|
targetTime: t.targetTime,
|
|
urgency: t.urgency,
|
|
cascade: { preset: t.cascade, intervals: [] },
|
|
});
|
|
} else if (t.type === 'pomodoro') {
|
|
addPomodoro({
|
|
label: t.label,
|
|
config: { workMinutes: 25, breakMinutes: 5, longBreakMinutes: 15, rounds: t.pomodoroRounds ?? 4 },
|
|
urgency: t.urgency,
|
|
});
|
|
}
|
|
setNlInput('');
|
|
setNlResult(null);
|
|
setLabel('');
|
|
onClose();
|
|
};
|
|
|
|
// When category changes, update urgency + cascade defaults
|
|
const handleCategoryChange = (catId: string) => {
|
|
setCategory(catId);
|
|
if (catId) {
|
|
const cat = getCategoryById(catId);
|
|
if (cat) {
|
|
setUrgency(cat.defaultUrgency);
|
|
setCascadePreset(cat.defaultCascade);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
setErrors({});
|
|
const cascade = { preset: cascadePreset, intervals: [] as number[] };
|
|
const catOrUndef = category || undefined;
|
|
|
|
if (tab === 'alarm') {
|
|
const result = alarmSchema.safeParse({ label: label || 'Alarm', alarmTime, urgency, cascadePreset });
|
|
if (!result.success) {
|
|
const fieldErrors: Record<string, string> = {};
|
|
for (const issue of result.error.issues) { fieldErrors[issue.path[0] as string] = issue.message; }
|
|
setErrors(fieldErrors);
|
|
return;
|
|
}
|
|
const [h, m] = alarmTime.split(':').map(Number);
|
|
const target = new Date();
|
|
target.setHours(h, m, 0, 0);
|
|
if (target.getTime() <= Date.now()) {
|
|
target.setDate(target.getDate() + 1);
|
|
}
|
|
addAlarm({
|
|
label: result.data.label,
|
|
targetTime: target.getTime(),
|
|
urgency,
|
|
cascade,
|
|
category: catOrUndef,
|
|
customMessage: customMessage || undefined,
|
|
});
|
|
} else if (tab === 'countdown') {
|
|
const result = countdownSchema.safeParse({ label: label || 'Countdown', hours, minutes, seconds, urgency, cascadePreset });
|
|
if (!result.success) {
|
|
const fieldErrors: Record<string, string> = {};
|
|
for (const issue of result.error.issues) { fieldErrors[issue.path[0] as string] = issue.message; }
|
|
setErrors(fieldErrors);
|
|
return;
|
|
}
|
|
const durationMs = (hours * 3600 + minutes * 60 + seconds) * 1000;
|
|
addCountdown({
|
|
label: result.data.label,
|
|
durationMs,
|
|
urgency,
|
|
cascade,
|
|
category: catOrUndef,
|
|
customMessage: customMessage || undefined,
|
|
});
|
|
} else if (tab === 'pomodoro') {
|
|
const result = pomodoroSchema.safeParse({ label: label || 'Focus Session', workMinutes: workMin, breakMinutes: breakMin, longBreakMinutes: longBreakMin, rounds, urgency });
|
|
if (!result.success) {
|
|
const fieldErrors: Record<string, string> = {};
|
|
for (const issue of result.error.issues) { fieldErrors[issue.path[0] as string] = issue.message; }
|
|
setErrors(fieldErrors);
|
|
return;
|
|
}
|
|
addPomodoro({
|
|
label: result.data.label,
|
|
config: {
|
|
workMinutes: workMin,
|
|
breakMinutes: breakMin,
|
|
longBreakMinutes: longBreakMin,
|
|
rounds,
|
|
},
|
|
urgency,
|
|
});
|
|
} else if (tab === 'event') {
|
|
const result = eventSchema.safeParse({ label: label || 'Event Countdown', eventDate, urgency, cascadePreset });
|
|
if (!result.success) {
|
|
const fieldErrors: Record<string, string> = {};
|
|
for (const issue of result.error.issues) { fieldErrors[issue.path[0] as string] = issue.message; }
|
|
setErrors(fieldErrors);
|
|
return;
|
|
}
|
|
const target = new Date(eventDate).getTime();
|
|
addEvent({
|
|
label: result.data.label,
|
|
targetTime: target,
|
|
urgency,
|
|
category: catOrUndef,
|
|
customMessage: customMessage || undefined,
|
|
});
|
|
}
|
|
|
|
// Reset form
|
|
setLabel('');
|
|
setAlarmTime('');
|
|
setHours(0);
|
|
setMinutes(25);
|
|
setSeconds(0);
|
|
setCategory('');
|
|
setEventDate('');
|
|
setCustomMessage('');
|
|
setErrors({});
|
|
onClose();
|
|
};
|
|
|
|
const tabs: { key: TabType; label: string; icon: React.ReactNode }[] = [
|
|
{ key: 'countdown', label: 'Countdown', icon: <Timer size={16} /> },
|
|
{ key: 'alarm', label: 'Alarm', icon: <AlarmClock size={16} /> },
|
|
{ key: 'pomodoro', label: 'Pomodoro', icon: <Coffee size={16} /> },
|
|
{ key: 'event', label: 'Event', icon: <CalendarDays size={16} /> },
|
|
];
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
{/* Backdrop */}
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
|
|
{/* Modal */}
|
|
<div
|
|
className="relative w-full max-w-md mx-4 rounded-2xl border shadow-2xl overflow-hidden"
|
|
style={{
|
|
backgroundColor: 'var(--cm-bg-elevated)',
|
|
borderColor: 'var(--cm-border)',
|
|
}}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b" style={{ borderColor: 'var(--cm-border)' }}>
|
|
<h2 className="text-lg font-semibold" style={{ color: 'var(--cm-text-primary)' }}>
|
|
New Timer
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1 rounded-lg transition-colors cursor-pointer"
|
|
style={{ color: 'var(--cm-text-tertiary)' }}
|
|
aria-label="Close dialog"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Natural Language Input */}
|
|
<div className="p-4 border-b" style={{ borderColor: 'var(--cm-border)' }}>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Sparkles size={14} style={{ color: 'var(--cm-accent-secondary)' }} />
|
|
<label className="text-xs font-medium" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
Quick create — type naturally
|
|
</label>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={nlInput}
|
|
onChange={(e) => handleNlChange(e.target.value)}
|
|
onKeyDown={(e) => { if (e.key === 'Enter' && nlResult?.success) handleNlCreate(); }}
|
|
placeholder='e.g. "meeting in 30 min" or "alarm at 3pm"'
|
|
className="flex-1 px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: nlResult?.success ? 'var(--cm-accent-secondary)' : 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
/>
|
|
{nlResult?.success && (
|
|
<button
|
|
onClick={handleNlCreate}
|
|
className="px-4 py-2 rounded-lg text-xs font-medium transition-colors cursor-pointer"
|
|
style={{ backgroundColor: 'var(--cm-accent-secondary)', color: 'var(--cm-black)' }}
|
|
>
|
|
Create
|
|
</button>
|
|
)}
|
|
</div>
|
|
{nlResult && nlInput.trim() && (
|
|
<div className="mt-2 text-xs" style={{ color: nlResult.success ? 'var(--cm-accent-secondary)' : 'var(--cm-text-tertiary)' }}>
|
|
{nlResult.success && nlResult.timer ? (
|
|
<span>
|
|
{nlResult.timer.type === 'pomodoro' ? 'Pomodoro' : nlResult.timer.type === 'alarm' ? 'Alarm' : 'Countdown'}
|
|
{' — '}{nlResult.timer.label}
|
|
{nlResult.timer.durationMs ? ` (${Math.round(nlResult.timer.durationMs / 60000)}m)` : ''}
|
|
{nlResult.timer.urgency !== 'standard' ? ` [${nlResult.timer.urgency}]` : ''}
|
|
</span>
|
|
) : (
|
|
<span style={{ color: 'var(--cm-text-tertiary)' }}>{nlResult.error}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="flex border-b" style={{ borderColor: 'var(--cm-border)' }}>
|
|
{tabs.map((t) => (
|
|
<button
|
|
key={t.key}
|
|
onClick={() => setTab(t.key)}
|
|
className="flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium transition-colors cursor-pointer"
|
|
style={{
|
|
color: tab === t.key ? 'var(--cm-accent)' : 'var(--cm-text-tertiary)',
|
|
borderBottom: tab === t.key ? '2px solid var(--cm-accent)' : '2px solid transparent',
|
|
}}
|
|
>
|
|
{t.icon} {t.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Form */}
|
|
<div className="p-4 space-y-4">
|
|
{/* Label */}
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Label
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={label}
|
|
onChange={(e) => setLabel(e.target.value)}
|
|
placeholder={tab === 'pomodoro' ? 'Focus Session' : tab === 'alarm' ? 'Wake up' : 'Timer'}
|
|
className="w-full px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: errors.label ? 'var(--cm-danger)' : 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
/>
|
|
{errors.label && <p className="text-xs mt-1" style={{ color: 'var(--cm-danger)' }}>{errors.label}</p>}
|
|
</div>
|
|
|
|
{/* Tab-specific fields */}
|
|
{tab === 'alarm' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Time
|
|
</label>
|
|
<input
|
|
type="time"
|
|
value={alarmTime}
|
|
onChange={(e) => setAlarmTime(e.target.value)}
|
|
className="w-full px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: errors.alarmTime ? 'var(--cm-danger)' : 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
/>
|
|
{errors.alarmTime && <p className="text-xs mt-1" style={{ color: 'var(--cm-danger)' }}>{errors.alarmTime}</p>}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'countdown' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Duration
|
|
</label>
|
|
<div className="flex gap-2">
|
|
{[
|
|
{ label: 'H', value: hours, setter: setHours, max: 23 },
|
|
{ label: 'M', value: minutes, setter: setMinutes, max: 59 },
|
|
{ label: 'S', value: seconds, setter: setSeconds, max: 59 },
|
|
].map((field) => (
|
|
<div key={field.label} className="flex-1">
|
|
<input
|
|
type="number"
|
|
min={0}
|
|
max={field.max}
|
|
value={field.value}
|
|
onChange={(e) => field.setter(Math.min(field.max, Math.max(0, parseInt(e.target.value) || 0)))}
|
|
className="w-full px-3 py-2 rounded-lg border text-sm text-center focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
/>
|
|
<span className="block text-center text-xs mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
{field.label}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
{errors.minutes && <p className="text-xs mt-1" style={{ color: 'var(--cm-danger)' }}>{errors.minutes}</p>}
|
|
{/* Quick presets */}
|
|
<div className="flex gap-2 mt-2">
|
|
{[
|
|
{ label: '5m', h: 0, m: 5, s: 0 },
|
|
{ label: '15m', h: 0, m: 15, s: 0 },
|
|
{ label: '25m', h: 0, m: 25, s: 0 },
|
|
{ label: '45m', h: 0, m: 45, s: 0 },
|
|
{ label: '1h', h: 1, m: 0, s: 0 },
|
|
].map((preset) => (
|
|
<button
|
|
key={preset.label}
|
|
onClick={() => { setHours(preset.h); setMinutes(preset.m); setSeconds(preset.s); }}
|
|
className="flex-1 py-1 rounded-md text-xs font-medium transition-colors cursor-pointer"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-muted)',
|
|
color: 'var(--cm-text-secondary)',
|
|
}}
|
|
>
|
|
{preset.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'pomodoro' && (
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{[
|
|
{ label: 'Work (min)', value: workMin, setter: setWorkMin },
|
|
{ label: 'Break (min)', value: breakMin, setter: setBreakMin },
|
|
{ label: 'Long Break (min)', value: longBreakMin, setter: setLongBreakMin },
|
|
{ label: 'Rounds', value: rounds, setter: setRounds },
|
|
].map((field) => (
|
|
<div key={field.label}>
|
|
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
{field.label}
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min={1}
|
|
max={120}
|
|
value={field.value}
|
|
onChange={(e) => field.setter(Math.max(1, parseInt(e.target.value) || 1))}
|
|
className="w-full px-3 py-2 rounded-lg border text-sm text-center focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{tab === 'event' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Event Date
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={eventDate}
|
|
onChange={(e) => setEventDate(e.target.value)}
|
|
min={new Date().toISOString().split('T')[0]}
|
|
className="w-full px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
/>
|
|
{errors.eventDate && <p className="text-xs mt-1" style={{ color: 'var(--cm-danger)' }}>{errors.eventDate}</p>}
|
|
{eventDate && new Date(eventDate).getTime() > Date.now() && (
|
|
<p className="text-xs mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
{Math.ceil((new Date(eventDate).getTime() - Date.now()) / 86_400_000)} days from now · Milestone warnings at 30, 7, 3, 1 days
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Category */}
|
|
{tab !== 'pomodoro' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Category
|
|
</label>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
<button
|
|
onClick={() => handleCategoryChange('')}
|
|
className="px-2.5 py-1 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
|
style={{
|
|
backgroundColor: !category ? 'var(--cm-accent)' : 'var(--cm-surface-muted)',
|
|
color: !category ? 'var(--cm-white)' : 'var(--cm-text-tertiary)',
|
|
border: !category ? '1px solid var(--cm-accent)' : '1px solid transparent',
|
|
}}
|
|
>
|
|
None
|
|
</button>
|
|
{BUILT_IN_CATEGORIES.map((cat) => (
|
|
<button
|
|
key={cat.id}
|
|
onClick={() => handleCategoryChange(cat.id)}
|
|
className="px-2.5 py-1 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
|
style={{
|
|
backgroundColor: category === cat.id ? `${cat.color}20` : 'var(--cm-surface-muted)',
|
|
color: category === cat.id ? cat.color : 'var(--cm-text-tertiary)',
|
|
border: category === cat.id ? `1px solid ${cat.color}60` : '1px solid transparent',
|
|
}}
|
|
>
|
|
{cat.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Urgency (non-pomodoro) */}
|
|
{tab !== 'pomodoro' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Urgency
|
|
</label>
|
|
<div className="flex gap-1">
|
|
{URGENCY_ORDER.map((level) => {
|
|
const config = getUrgencyConfig(level);
|
|
return (
|
|
<button
|
|
key={level}
|
|
onClick={() => setUrgency(level)}
|
|
className="flex-1 py-1.5 rounded-lg text-xs font-medium transition-all cursor-pointer"
|
|
style={{
|
|
backgroundColor: urgency === level ? config.bgColor : 'var(--cm-surface-muted)',
|
|
color: urgency === level ? config.color : 'var(--cm-text-tertiary)',
|
|
border: urgency === level ? `1px solid ${config.borderColor}` : '1px solid transparent',
|
|
}}
|
|
>
|
|
{config.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Custom pre-warning message (non-pomodoro) */}
|
|
{tab !== 'pomodoro' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Custom Warning Message <span className="text-xs font-normal" style={{ color: 'var(--cm-text-tertiary)' }}>(optional)</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={customMessage}
|
|
onChange={(e) => setCustomMessage(e.target.value)}
|
|
placeholder='e.g. "Review your agenda" or "Bring your insurance card"'
|
|
maxLength={200}
|
|
className="w-full px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
/>
|
|
<p className="text-xs mt-1" style={{ color: 'var(--cm-text-tertiary)' }}>
|
|
Shown in pre-warning notifications instead of auto-generated tips
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cascade preset (non-pomodoro) */}
|
|
{tab !== 'pomodoro' && (
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--cm-text-secondary)' }}>
|
|
Pre-warning Cascade
|
|
</label>
|
|
<select
|
|
value={cascadePreset}
|
|
onChange={(e) => setCascadePreset(e.target.value as CascadePreset)}
|
|
className="w-full px-3 py-2 rounded-lg border text-sm focus:outline-none focus:ring-2 cursor-pointer"
|
|
style={{
|
|
backgroundColor: 'var(--cm-surface-card)',
|
|
borderColor: 'var(--cm-border)',
|
|
color: 'var(--cm-text-primary)',
|
|
}}
|
|
>
|
|
{Object.entries(CASCADE_PRESET_LABELS).map(([key, label]) => (
|
|
<option key={key} value={key}>{label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{/* Create button */}
|
|
<button
|
|
onClick={handleCreate}
|
|
disabled={maintenanceMode}
|
|
className="w-full py-3 rounded-xl text-sm font-semibold transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
|
|
style={{
|
|
backgroundColor: 'var(--cm-accent)',
|
|
color: 'var(--cm-white)',
|
|
}}
|
|
>
|
|
{maintenanceMode ? 'Timer creation disabled (maintenance)' : `Create ${tab === 'pomodoro' ? 'Pomodoro' : tab === 'alarm' ? 'Alarm' : 'Countdown'}`}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|