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/SettingsPanel.tsx | 240 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 240 insertions(+)
 create mode 100644 src/components/SettingsPanel.tsx

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

diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx
new file mode 100644
index 0000000..0d3cbf9
--- /dev/null
+++ b/src/components/SettingsPanel.tsx
@@ -0,0 +1,240 @@
+import { useState, useEffect } from 'react';
+import { useSettingsStore, Settings } from '../store/settingsStore';
+
+interface SettingsPanelProps {
+  open: boolean;
+  onClose: () => void;
+}
+
+export function SettingsPanel({ open, onClose }: SettingsPanelProps) {
+  const { settings, fetchSettings, updateSettings } = useSettingsStore();
+
+  const [workMins, setWorkMins] = useState(25);
+  const [shortBreakMins, setShortBreakMins] = useState(5);
+  const [longBreakMins, setLongBreakMins] = useState(15);
+  const [sessionsBeforeLong, setSessionsBeforeLong] = useState(4);
+
+  useEffect(() => {
+    if (open && !settings) {
+      fetchSettings();
+    }
+  }, [open, settings, fetchSettings]);
+
+  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);
+    }
+  }, [settings]);
+
+  if (!open) return null;
+
+  const handleSave = async () => {
+    const s: Settings = {
+      work_duration_secs: workMins * 60,
+      short_break_secs: shortBreakMins * 60,
+      long_break_secs: longBreakMins * 60,
+      sessions_before_long_break: sessionsBeforeLong,
+    };
+    await updateSettings(s);
+    onClose();
+  };
+
+  return (
+    <div
+      onClick={onClose}
+      style={{
+        position: 'fixed',
+        inset: 0,
+        background: 'rgba(11, 14, 19, 0.70)',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        zIndex: 100,
+      }}
+    >
+      {/* Modal card */}
+      <div
+        onClick={(e) => e.stopPropagation()}
+        style={{
+          background: 'var(--ink-1)',
+          border: '1px solid var(--line-2)',
+          borderRadius: 'var(--r-3)',
+          boxShadow: 'var(--shadow-3)',
+          padding: '32px',
+          width: '360px',
+          display: 'flex',
+          flexDirection: 'column',
+          gap: '24px',
+        }}
+      >
+        <span className="eyebrow" style={{ letterSpacing: '0.18em' }}>
+          Settings
+        </span>
+
+        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
+          <NumberField
+            label="Work duration"
+            unit="min"
+            value={workMins}
+            min={1}
+            max={120}
+            onChange={setWorkMins}
+          />
+          <NumberField
+            label="Short break"
+            unit="min"
+            value={shortBreakMins}
+            min={1}
+            max={60}
+            onChange={setShortBreakMins}
+          />
+          <NumberField
+            label="Long break"
+            unit="min"
+            value={longBreakMins}
+            min={1}
+            max={60}
+            onChange={setLongBreakMins}
+          />
+          <NumberField
+            label="Sessions before long break"
+            unit="sessions"
+            value={sessionsBeforeLong}
+            min={1}
+            max={10}
+            onChange={setSessionsBeforeLong}
+          />
+        </div>
+
+        <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
+          <button
+            onClick={onClose}
+            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',
+            }}
+            onMouseEnter={(e) => {
+              (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--brass)';
+            }}
+            onMouseLeave={(e) => {
+              (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--line-2)';
+            }}
+          >
+            Cancel
+          </button>
+          <button
+            onClick={handleSave}
+            style={{
+              fontFamily: 'var(--font-sans)',
+              fontSize: '14px',
+              fontWeight: 600,
+              padding: '8px 20px',
+              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)';
+            }}
+          >
+            Save
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+interface NumberFieldProps {
+  label: string;
+  unit: string;
+  value: number;
+  min: number;
+  max: number;
+  onChange: (v: number) => void;
+}
+
+function NumberField({ label, unit, value, min, max, onChange }: NumberFieldProps) {
+  return (
+    <div
+      style={{
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'space-between',
+        gap: '16px',
+      }}
+    >
+      <label
+        style={{
+          fontFamily: 'var(--font-sans)',
+          fontSize: '14px',
+          color: 'var(--fg-2)',
+          flex: 1,
+        }}
+      >
+        {label}
+      </label>
+      <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
+        <input
+          type="number"
+          min={min}
+          max={max}
+          value={value}
+          onChange={(e) => onChange(Math.min(max, Math.max(min, parseInt(e.target.value) || min)))}
+          style={{
+            fontFamily: 'var(--font-mono)',
+            fontSize: '14px',
+            fontVariantNumeric: 'tabular-nums',
+            padding: '5px 8px',
+            borderRadius: 'var(--r-2)',
+            border: '1px solid var(--line-2)',
+            background: 'var(--ink-3)',
+            color: 'var(--fg-1)',
+            outline: 'none',
+            width: '56px',
+            textAlign: 'center',
+          }}
+          onFocus={(e) => {
+            (e.currentTarget as HTMLInputElement).style.boxShadow = 'var(--shadow-brass)';
+          }}
+          onBlur={(e) => {
+            (e.currentTarget as HTMLInputElement).style.boxShadow = 'none';
+          }}
+        />
+        <span
+          style={{
+            fontFamily: 'var(--font-sans)',
+            fontSize: '12px',
+            color: 'var(--fg-4)',
+            minWidth: '44px',
+          }}
+        >
+          {unit}
+        </span>
+      </div>
+    </div>
+  );
+}
-- 
cgit v1.3-2-g0d8e

