summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/AmbientControl.tsx9
-rw-r--r--src/components/NotificationOverlay.tsx4
-rw-r--r--src/components/SettingsPanel.tsx38
-rw-r--r--src/components/TaskList.tsx9
-rw-r--r--src/components/TimerView.tsx37
5 files changed, 62 insertions, 35 deletions
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',