From 6cb006cc136f1fc5c83537cc30c64d223d1755e4 Mon Sep 17 00:00:00 2001
From: Solstice <solstice@local>
Date: Tue, 9 Jun 2026 00:22:18 -0700
Subject: feat: frontend view, state management, and user interface

---
 src/components/TimerView.tsx | 236 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 236 insertions(+)
 create mode 100644 src/components/TimerView.tsx

(limited to 'src/components/TimerView.tsx')

diff --git a/src/components/TimerView.tsx b/src/components/TimerView.tsx
new file mode 100644
index 0000000..b56e6a8
--- /dev/null
+++ b/src/components/TimerView.tsx
@@ -0,0 +1,236 @@
+import { invoke } from '@tauri-apps/api/core';
+import { useTimerStore, TimerPhase } from '../store/timerStore';
+import { useTaskStore } from '../store/taskStore';
+
+const RING_SIZE = 280;
+const STROKE = 8;
+const RADIUS = (RING_SIZE - STROKE) / 2;
+const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
+
+function phaseLabel(phase: TimerPhase): string {
+  switch (phase) {
+    case 'work': return 'Focus';
+    case 'short_break': return 'Short Break';
+    case 'long_break': return 'Long Break';
+  }
+}
+
+function phaseColor(phase: TimerPhase): string {
+  switch (phase) {
+    case 'work': return 'var(--brass)';
+    case 'short_break': return 'var(--positive)';
+    case 'long_break': return 'var(--info)';
+  }
+}
+
+function eyebrowText(phase: TimerPhase, sessionCount: number): string {
+  if (phase === 'work') {
+    return `WORK · SESSION ${sessionCount + 1}`;
+  }
+  if (phase === 'short_break') return 'SHORT BREAK';
+  return 'LONG BREAK';
+}
+
+function formatTime(secs: number): string {
+  const m = Math.floor(secs / 60);
+  const s = secs % 60;
+  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
+}
+
+export function TimerView() {
+  const { phase, remainingSecs, totalSecs, running, sessionCount, currentTaskId } =
+    useTimerStore();
+  const tasks = useTaskStore((s) => s.tasks);
+
+  const currentTask = tasks.find((t) => t.id === currentTaskId) ?? null;
+
+  const progress = totalSecs > 0 ? remainingSecs / totalSecs : 1;
+  const dashOffset = CIRCUMFERENCE * (1 - progress);
+  const arcColor = phaseColor(phase);
+
+  const handleStart = () => invoke('start_timer');
+  const handlePause = () => invoke('pause_timer');
+  const handleSkip = () => invoke('skip_phase');
+  const handleReset = () => invoke('reset_timer');
+
+  return (
+    <div
+      style={{
+        display: 'flex',
+        flexDirection: 'column',
+        alignItems: 'center',
+        justifyContent: 'center',
+        gap: '24px',
+        flex: 1,
+        padding: '32px',
+      }}
+    >
+      {/* Eyebrow */}
+      <span className="eyebrow" style={{ letterSpacing: '0.18em' }}>
+        {eyebrowText(phase, sessionCount)}
+      </span>
+
+      {/* Circular ring */}
+      <div style={{ position: 'relative', width: RING_SIZE, height: RING_SIZE }}>
+        <svg
+          width={RING_SIZE}
+          height={RING_SIZE}
+          style={{ transform: 'rotate(-90deg)' }}
+        >
+          {/* Background track */}
+          <circle
+            cx={RING_SIZE / 2}
+            cy={RING_SIZE / 2}
+            r={RADIUS}
+            fill="none"
+            stroke="var(--line-2)"
+            strokeWidth={STROKE}
+          />
+          {/* Progress arc */}
+          <circle
+            cx={RING_SIZE / 2}
+            cy={RING_SIZE / 2}
+            r={RADIUS}
+            fill="none"
+            stroke={arcColor}
+            strokeWidth={STROKE}
+            strokeLinecap="round"
+            strokeDasharray={CIRCUMFERENCE}
+            strokeDashoffset={dashOffset}
+            style={{ transition: 'stroke-dashoffset 0.8s ease, stroke 0.3s ease' }}
+          />
+        </svg>
+
+        {/* Center content */}
+        <div
+          style={{
+            position: 'absolute',
+            inset: 0,
+            display: 'flex',
+            flexDirection: 'column',
+            alignItems: 'center',
+            justifyContent: 'center',
+            gap: '4px',
+          }}
+        >
+          <span
+            style={{
+              fontFamily: 'var(--font-display)',
+              fontStyle: 'italic',
+              fontSize: '56px',
+              fontWeight: 400,
+              lineHeight: 1,
+              color: 'var(--fg-1)',
+              letterSpacing: '-0.02em',
+              fontVariantNumeric: 'tabular-nums',
+            }}
+          >
+            {formatTime(remainingSecs)}
+          </span>
+          <span
+            style={{
+              fontFamily: 'var(--font-sans)',
+              fontSize: '12px',
+              fontWeight: 600,
+              textTransform: 'uppercase',
+              letterSpacing: '0.12em',
+              color: 'var(--fg-3)',
+            }}
+          >
+            {phaseLabel(phase)}
+          </span>
+        </div>
+      </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>
+      )}
+
+      {/* Controls */}
+      <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
+        <button
+          onClick={running ? handlePause : handleStart}
+          style={{
+            fontFamily: 'var(--font-sans)',
+            fontSize: '14px',
+            fontWeight: 600,
+            padding: '8px 24px',
+            borderRadius: 'var(--r-1)',
+            border: 'none',
+            background: 'var(--brass)',
+            color: 'var(--brass-ink)',
+            cursor: 'pointer',
+            transition: 'background 0.15s ease',
+          }}
+          onMouseEnter={(e) => {
+            (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass-bright)';
+          }}
+          onMouseLeave={(e) => {
+            (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass)';
+          }}
+          onMouseDown={(e) => {
+            (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass-deep)';
+          }}
+          onMouseUp={(e) => {
+            (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass-bright)';
+          }}
+        >
+          {running ? 'Pause' : 'Start'}
+        </button>
+
+        <GhostButton onClick={handleSkip}>Skip</GhostButton>
+        <GhostButton onClick={handleReset}>Reset</GhostButton>
+      </div>
+    </div>
+  );
+}
+
+function GhostButton({
+  onClick,
+  children,
+}: {
+  onClick: () => void;
+  children: React.ReactNode;
+}) {
+  return (
+    <button
+      onClick={onClick}
+      style={{
+        fontFamily: 'var(--font-sans)',
+        fontSize: '14px',
+        fontWeight: 500,
+        padding: '8px 16px',
+        borderRadius: 'var(--r-1)',
+        border: '1px solid var(--line-2)',
+        background: 'transparent',
+        color: 'var(--brass)',
+        cursor: 'pointer',
+        transition: 'border-color 0.15s ease, color 0.15s ease',
+      }}
+      onMouseEnter={(e) => {
+        const el = e.currentTarget as HTMLButtonElement;
+        el.style.borderColor = 'var(--brass)';
+      }}
+      onMouseLeave={(e) => {
+        const el = e.currentTarget as HTMLButtonElement;
+        el.style.borderColor = 'var(--line-2)';
+      }}
+    >
+      {children}
+    </button>
+  );
+}
-- 
cgit v1.3-2-g0d8e

