diff options
| author | Solstice <solstice@local> | 2026-06-09 01:02:30 -0700 |
|---|---|---|
| committer | Solstice <solstice@local> | 2026-06-09 01:02:30 -0700 |
| commit | 4e2d978eb5fc9457d5b913bc10faf1266e6dcda4 (patch) | |
| tree | 835f8cdc1160fe979a39e0bdad0c5179cc49820d /src | |
| parent | f43f549ffbe3074977116c9f35aa7064d6a4bd95 (diff) | |
chore: final polish and preparation for release
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.tsx | 4 | ||||
| -rw-r--r-- | src/components/AmbientControl.tsx | 9 | ||||
| -rw-r--r-- | src/components/NotificationOverlay.tsx | 4 | ||||
| -rw-r--r-- | src/components/SettingsPanel.tsx | 38 | ||||
| -rw-r--r-- | src/components/TaskList.tsx | 9 | ||||
| -rw-r--r-- | src/components/TimerView.tsx | 37 | ||||
| -rw-r--r-- | src/styles/global.css | 20 |
7 files changed, 85 insertions, 36 deletions
diff --git a/src/App.tsx b/src/App.tsx index cf1e6a6..29ac64f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -70,7 +70,8 @@ function App() { <div style={{ display: 'flex', - alignItems: 'center', + alignItems: 'flex-start', + flexWrap: 'wrap', justifyContent: 'space-between', gap: '16px', padding: '12px 16px', @@ -91,6 +92,7 @@ function App() { color: 'var(--fg-3)', cursor: 'pointer', borderRadius: 'var(--r-2)', + marginLeft: 'auto', transition: 'color 0.15s ease', }} onMouseEnter={(e) => { diff --git a/src/components/AmbientControl.tsx b/src/components/AmbientControl.tsx index 9eabb09..dd58235 100644 --- a/src/components/AmbientControl.tsx +++ b/src/components/AmbientControl.tsx @@ -24,15 +24,18 @@ export function AmbientControl() { style={{ display: 'flex', alignItems: 'center', + flexWrap: 'wrap', gap: '12px', + rowGap: '8px', padding: '8px 10px', border: '1px solid var(--line-2)', borderRadius: 'var(--r-3)', background: 'rgba(17, 21, 28, 0.85)', boxShadow: 'var(--shadow-1)', + minWidth: 0, }} > - <div style={{ display: 'flex', flexDirection: 'column', gap: '2px', minWidth: '90px' }}> + <div style={{ display: 'flex', flexDirection: 'column', gap: '2px', minWidth: '90px', flex: '1 1 180px' }}> <span className="eyebrow" style={{ letterSpacing: '0.14em' }}> Ambient </span> @@ -41,6 +44,7 @@ export function AmbientControl() { fontFamily: 'var(--font-sans)', fontSize: '12px', color: available ? 'var(--fg-3)' : 'var(--negative)', + lineHeight: 1.4, }} > {available ? 'Looping background audio' : 'Add rain.ogg, cafe.ogg, or white_noise.ogg'} @@ -61,6 +65,7 @@ export function AmbientControl() { padding: '6px 10px', outline: 'none', minWidth: '132px', + flex: '0 0 auto', }} > {SOUND_OPTIONS.map((option) => ( @@ -70,7 +75,7 @@ export function AmbientControl() { ))} </select> - <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> + <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' }}> <input type="range" min={0} diff --git a/src/components/NotificationOverlay.tsx b/src/components/NotificationOverlay.tsx index 965f19d..2b8861c 100644 --- a/src/components/NotificationOverlay.tsx +++ b/src/components/NotificationOverlay.tsx @@ -64,9 +64,11 @@ export function NotificationOverlay({ visible, taskId, onDismiss }: Notification display: 'flex', flexDirection: 'column', alignItems: 'center', + textAlign: 'center', gap: '8px', - animation: 'slideDown 0.2s ease', + animation: 'slideDown 0.15s ease', minWidth: '280px', + maxWidth: 'calc(100vw - 32px)', }} > <span diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 0d3cbf9..71bc7b8 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; import { useSettingsStore, Settings } from '../store/settingsStore'; +const clampValue = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)); + interface SettingsPanelProps { open: boolean; onClose: () => void; @@ -22,21 +24,31 @@ export function SettingsPanel({ open, onClose }: SettingsPanelProps) { useEffect(() => { if (settings) { - setWorkMins(Math.round(settings.work_duration_secs / 60)); - setShortBreakMins(Math.round(settings.short_break_secs / 60)); - setLongBreakMins(Math.round(settings.long_break_secs / 60)); - setSessionsBeforeLong(settings.sessions_before_long_break); + setWorkMins(clampValue(Math.round(settings.work_duration_secs / 60), 1, 120)); + setShortBreakMins(clampValue(Math.round(settings.short_break_secs / 60), 1, 60)); + setLongBreakMins(clampValue(Math.round(settings.long_break_secs / 60), 1, 60)); + setSessionsBeforeLong(clampValue(settings.sessions_before_long_break, 1, 10)); } }, [settings]); if (!open) return null; const handleSave = async () => { + const nextWorkMins = clampValue(workMins, 1, 120); + const nextShortBreakMins = clampValue(shortBreakMins, 1, 60); + const nextLongBreakMins = clampValue(longBreakMins, 1, 60); + const nextSessionsBeforeLong = clampValue(sessionsBeforeLong, 1, 10); + + setWorkMins(nextWorkMins); + setShortBreakMins(nextShortBreakMins); + setLongBreakMins(nextLongBreakMins); + setSessionsBeforeLong(nextSessionsBeforeLong); + const s: Settings = { - work_duration_secs: workMins * 60, - short_break_secs: shortBreakMins * 60, - long_break_secs: longBreakMins * 60, - sessions_before_long_break: sessionsBeforeLong, + work_duration_secs: nextWorkMins * 60, + short_break_secs: nextShortBreakMins * 60, + long_break_secs: nextLongBreakMins * 60, + sessions_before_long_break: nextSessionsBeforeLong, }; await updateSettings(s); onClose(); @@ -64,7 +76,9 @@ export function SettingsPanel({ open, onClose }: SettingsPanelProps) { borderRadius: 'var(--r-3)', boxShadow: 'var(--shadow-3)', padding: '32px', - width: '360px', + width: 'min(360px, calc(100vw - 32px))', + maxHeight: 'calc(100vh - 32px)', + overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '24px', @@ -109,6 +123,10 @@ export function SettingsPanel({ open, onClose }: SettingsPanelProps) { /> </div> + <span style={{ fontFamily: 'var(--font-sans)', fontSize: '12px', color: 'var(--fg-4)', lineHeight: 1.4 }}> + Values are clamped to sensible minimums before saving. + </span> + <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}> <button onClick={onClose} @@ -203,7 +221,7 @@ function NumberField({ label, unit, value, min, max, onChange }: NumberFieldProp min={min} max={max} value={value} - onChange={(e) => onChange(Math.min(max, Math.max(min, parseInt(e.target.value) || min)))} + onChange={(e) => onChange(clampValue(parseInt(e.target.value, 10) || min, min, max))} style={{ fontFamily: 'var(--font-mono)', fontSize: '14px', diff --git a/src/components/TaskList.tsx b/src/components/TaskList.tsx index 6ecce42..6cd9c3f 100644 --- a/src/components/TaskList.tsx +++ b/src/components/TaskList.tsx @@ -10,10 +10,12 @@ export function TaskList() { const [newName, setNewName] = useState(''); const [newSessions, setNewSessions] = useState(4); + const clampSessions = (value: number) => Math.min(20, Math.max(1, value)); + const handleAdd = async () => { const name = newName.trim(); if (!name) return; - await addTask(name, newSessions); + await addTask(name, clampSessions(newSessions)); setNewName(''); setNewSessions(4); setShowForm(false); @@ -133,7 +135,7 @@ export function TaskList() { min={1} max={20} value={newSessions} - onChange={(e) => setNewSessions(Math.max(1, parseInt(e.target.value) || 1))} + onChange={(e) => setNewSessions(clampSessions(parseInt(e.target.value, 10) || 1))} style={{ fontFamily: 'var(--font-mono)', fontSize: '14px', @@ -185,9 +187,10 @@ export function TaskList() { fontFamily: 'var(--font-sans)', fontSize: '13px', color: 'var(--fg-4)', + lineHeight: 1.5, }} > - No tasks yet + No tasks yet. Add one to track deliberate focus sessions. </div> )} {tasks.map((task) => { diff --git a/src/components/TimerView.tsx b/src/components/TimerView.tsx index 973f09f..896c03a 100644 --- a/src/components/TimerView.tsx +++ b/src/components/TimerView.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { useTimerStore, TimerPhase } from '../store/timerStore'; import { useTaskStore } from '../store/taskStore'; @@ -7,6 +8,18 @@ const STROKE = 8; const RADIUS = (RING_SIZE - STROKE) / 2; const CIRCUMFERENCE = 2 * Math.PI * RADIUS; +const TASK_LABEL_STYLE: CSSProperties = { + fontFamily: 'var(--font-sans)', + fontSize: '14px', + color: 'var(--fg-3)', + maxWidth: '320px', + textAlign: 'center', + minHeight: '20px', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}; + function phaseLabel(phase: TimerPhase): string { switch (phase) { case 'work': return 'Focus'; @@ -60,9 +73,9 @@ export function TimerView() { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - gap: '24px', + gap: '20px', flex: 1, - padding: '32px', + padding: '24px 20px', }} > {/* Eyebrow */} @@ -142,25 +155,10 @@ export function TimerView() { </div> {/* Current task name */} - {currentTask && ( - <span - style={{ - fontFamily: 'var(--font-sans)', - fontSize: '14px', - color: 'var(--fg-3)', - maxWidth: '320px', - textAlign: 'center', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - }} - > - {currentTask.name} - </span> - )} + <span style={TASK_LABEL_STYLE}>{currentTask ? currentTask.name : 'No task selected'}</span> {/* Controls */} - <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}> + <div style={{ display: 'flex', gap: '8px', alignItems: 'center', justifyContent: 'center', flexWrap: 'wrap' }}> <button onClick={running ? handlePause : handleStart} style={{ @@ -213,6 +211,7 @@ function GhostButton({ fontSize: '14px', fontWeight: 500, padding: '8px 16px', + minWidth: '72px', borderRadius: 'var(--r-1)', border: '1px solid var(--line-2)', background: 'transparent', diff --git a/src/styles/global.css b/src/styles/global.css index e4a0d96..007c653 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -75,4 +75,24 @@ #root { min-height: 100vh; } + + button, + input, + select { + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; + } + + button:focus-visible, + input:focus-visible, + select:focus-visible { + outline: none; + border-color: var(--brass); + box-shadow: var(--shadow-brass); + } + + button:disabled, + input:disabled, + select:disabled { + cursor: not-allowed; + } } |
