From a9ae7c65c387bcf144de8df0a13dbbfd7496cc1e Mon Sep 17 00:00:00 2001 From: Clawd Date: Sat, 28 Feb 2026 15:21:27 -0800 Subject: Restyle UI with light/dark theme support - Add light/dark theme toggle with localStorage persistence - Add 'Claude Flow' wordmark in header - Switch to monospace font (SF Mono, Cascadia Code, etc.) - Update accent colors (lighter blue) - Add theme-aware CodeMirror styling (oneDark vs defaultHighlightStyle) - Update window title to 'Claude Flow' - Refine spacing and visual polish throughout - Add .claude-flow/ artifacts from self-restyling session --- .claude-flow/plan.md | 1090 ++++++++++++++++++++++++++++++ .claude-flow/research.md | 214 ++++++ renderer/index.html | 2 +- renderer/src/App.tsx | 18 + renderer/src/components/DocumentPane.tsx | 10 +- renderer/src/components/Header.tsx | 14 + renderer/src/styles/globals.css | 259 ++++--- 7 files changed, 1523 insertions(+), 84 deletions(-) create mode 100644 .claude-flow/plan.md create mode 100644 .claude-flow/research.md diff --git a/.claude-flow/plan.md b/.claude-flow/plan.md new file mode 100644 index 0000000..bbcae6a --- /dev/null +++ b/.claude-flow/plan.md @@ -0,0 +1,1090 @@ +# Implementation Plan + +## Goal +Restyle the Claude Flow Electron app with: +1. Fix the window/tab title from "minimal" → "Claude Flow" +2. Add a "CLAUDE FLOW" wordmark to the header +3. Apply a cipherpunk aesthetic (full monospace UI font, sharper geometry, electric-blue accent, uppercase meta labels, focus glow) +4. Add light/dark mode with a text toggle (`[dark]` / `[light]`) persisted to `localStorage` + +## Approach +Five files touched in order of dependency. No new npm packages. The CSS rewrite is the biggest change; the JS/TSX changes are small and surgical. + +**Order of changes:** +1. `renderer/index.html` — title fix (trivial, isolated) +2. `renderer/src/styles/globals.css` — full restyle (all visual work) +3. `renderer/src/App.tsx` — add theme state, wire it down +4. `renderer/src/components/Header.tsx` — wordmark + toggle button +5. `renderer/src/components/DocumentPane.tsx` — dynamic CodeMirror theme + +--- + +## Changes + +### 1. Fix Window Title +**File:** `renderer/index.html` +**What:** Change `minimal` to `Claude Flow` on line 10. + +```html + +minimal + + +Claude Flow +``` + +--- + +### 2. Restyle the CSS +**File:** `renderer/src/styles/globals.css` +**What:** Complete replacement of the file. Key changes from the original: +- `:root` dark-mode accent updated to `#60a5fa` / `#93c5fd` +- `body` font-family changed to full monospace stack +- New `html[data-theme="light"]` block added after `:root` +- `border-radius` reduced to 2px on all interactive controls (4px on chat bubbles) +- `text-transform: uppercase; letter-spacing: 0.07em` added to all meta/chrome labels +- Focus glow (`box-shadow`) added on inputs and focused buttons +- New `.app-wordmark` class added +- New `.theme-toggle` class added +- All existing selectors preserved; only values changed + +```css +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ── Dark theme (default) ────────────────────────────────────── */ +:root { + --bg-primary: #1a1a1a; + --bg-secondary: #252525; + --bg-tertiary: #333; + --border: #444; + --text-primary: #e0e0e0; + --text-secondary: #888; + --accent: #60a5fa; /* electric blue — brighter than original #3b82f6 */ + --accent-hover: #93c5fd; + --success: #10b981; + --warning: #f59e0b; + --danger: #ef4444; +} + +/* ── Light theme overrides ───────────────────────────────────── */ +html[data-theme="light"] { + --bg-primary: #f4f4f2; + --bg-secondary: #e8e8e5; + --bg-tertiary: #d8d8d4; + --border: #b4b4b0; + --text-primary: #1a1a18; + --text-secondary: #5a5a56; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --success: #059669; + --warning: #d97706; + --danger: #dc2626; +} + +/* ── Base ────────────────────────────────────────────────────── */ +body { + font-family: "SF Mono", "Cascadia Code", "JetBrains Mono", "Fira Code", + Monaco, "Courier New", monospace; + background: var(--bg-primary); + color: var(--text-primary); + overflow: hidden; + font-size: 13px; +} + +.app { + display: flex; + flex-direction: column; + height: 100vh; +} + +/* ── Header ──────────────────────────────────────────────────── */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + -webkit-app-region: drag; +} + +.header-left, +.header-right { + display: flex; + align-items: center; + gap: 8px; + -webkit-app-region: no-drag; +} + +/* App wordmark */ +.app-wordmark { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--text-primary); + padding-right: 12px; + border-right: 1px solid var(--border); + margin-right: 4px; + user-select: none; + white-space: nowrap; +} + +.header select, +.header button { + padding: 5px 10px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-primary); + cursor: pointer; + font-size: 12px; + font-family: inherit; +} + +.header button:hover { + background: var(--border); +} + +.header button.btn-delete { + background: transparent; + border: 1px solid var(--border); + padding: 5px 8px; + font-size: 13px; +} + +.header button.btn-delete:hover { + background: var(--danger); + border-color: var(--danger); +} + +/* Theme toggle */ +.theme-toggle { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: lowercase; + opacity: 0.7; + transition: opacity 0.15s; +} + +.theme-toggle:hover { + opacity: 1; + background: var(--bg-tertiary) !important; +} + +/* Phase indicator */ +.phase-indicator { + display: flex; + gap: 4px; +} + +.phase-step { + padding: 3px 10px; + font-size: 11px; + letter-spacing: 0.07em; + text-transform: uppercase; + border-radius: 2px; + background: var(--bg-tertiary); + color: var(--text-secondary); +} + +.phase-step.active { + background: var(--accent); + color: white; +} + +.phase-step.complete { + background: var(--success); + color: white; +} + +/* ── Main Content ─────────────────────────────────────────────── */ +.main-content { + flex: 1; + display: flex; + overflow: hidden; +} + +/* ── Document Pane ───────────────────────────────────────────── */ +.document-pane { + flex: 1; + display: flex; + flex-direction: column; + border-right: 1px solid var(--border); + min-width: 0; + overflow: hidden; +} + +.document-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px 16px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + font-size: 11px; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--text-secondary); +} + +.document-header button { + padding: 3px 8px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-primary); + cursor: pointer; + font-size: 11px; + font-family: inherit; + letter-spacing: 0.05em; +} + +.document-header button:hover { + background: var(--border); +} + +.document-content { + flex: 1; + overflow-y: auto; + padding: 24px; +} + +.document-content.editing { + font-family: inherit; + font-size: 13px; + line-height: 1.6; + background: var(--bg-primary); + border: none; + resize: none; + color: var(--text-primary); +} + +.codemirror-editor { + flex: 1; + overflow: hidden; + min-height: 0; +} + +.codemirror-editor .cm-editor { + height: 100%; + max-width: 100%; +} + +.codemirror-editor .cm-scroller { + overflow: auto !important; +} + +.codemirror-editor .cm-gutters { + background: var(--bg-secondary); + border-right: 1px solid var(--border); +} + +.document-content.rendered { + line-height: 1.7; +} + +.document-content.rendered h1 { + font-size: 22px; + margin: 24px 0 16px; + letter-spacing: -0.01em; +} +.document-content.rendered h2 { + font-size: 17px; + margin: 20px 0 12px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} +.document-content.rendered h3 { + font-size: 14px; + margin: 16px 0 8px; +} +.document-content.rendered p { + margin: 8px 0; + line-height: 1.6; +} +.document-content.rendered code { + background: var(--bg-tertiary); + padding: 2px 6px; + border-radius: 2px; + font-size: 12px; + font-family: inherit; +} +.document-content.rendered pre { + background: var(--bg-tertiary); + padding: 16px; + border-radius: 2px; + overflow-x: auto; + margin: 16px 0; +} +.document-content.rendered pre code { + background: none; + padding: 0; +} +.document-content.rendered ul, +.document-content.rendered ol { + margin: 12px 0; + padding-left: 24px; +} +.document-content.rendered li { + margin-bottom: 6px; + line-height: 1.5; +} +.document-content.rendered ul.contains-task-list { + list-style: none; + padding-left: 0; +} +.document-content.rendered li.task-list-item { + display: flex; + align-items: flex-start; + gap: 8px; +} +.document-content.rendered li.task-list-item input[type="checkbox"] { + margin-top: 4px; +} +.document-content.rendered table { + width: 100%; + border-collapse: collapse; + margin: 16px 0; + font-size: 12px; +} +.document-content.rendered th, +.document-content.rendered td { + padding: 8px 12px; + text-align: left; + border: 1px solid var(--border); +} +.document-content.rendered th { + background: var(--bg-tertiary); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 11px; +} +.document-content.rendered tr:nth-child(even) td { + background: var(--bg-secondary); +} +.document-content.rendered blockquote { + border-left: 3px solid var(--accent); + margin: 16px 0; + padding-left: 16px; + color: var(--text-secondary); +} +.document-content.rendered hr { + border: none; + border-top: 1px solid var(--border); + margin: 24px 0; +} +.document-content.rendered a { + color: var(--accent); + text-decoration: none; +} +.document-content.rendered a:hover { + text-decoration: underline; +} +.document-content.rendered .empty { + color: var(--text-secondary); + font-style: italic; +} + +.badge { + background: var(--accent); + color: white; + padding: 2px 8px; + border-radius: 2px; + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +/* ── Chat Pane ───────────────────────────────────────────────── */ +.chat-pane { + width: 380px; + display: flex; + flex-direction: column; + background: var(--bg-secondary); +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 16px; +} + +.message { + margin-bottom: 10px; + padding: 9px 13px; + border-radius: 4px; + max-width: 90%; + font-size: 13px; + line-height: 1.5; + white-space: pre-wrap; +} + +.message.user { + background: var(--accent); + margin-left: auto; + color: white; +} + +.message.assistant { + background: var(--bg-tertiary); +} + +.message.loading { + color: var(--text-secondary); + font-style: italic; +} + +.chat-input { + display: flex; + gap: 8px; + padding: 12px; + border-top: 1px solid var(--border); +} + +.chat-input input { + flex: 1; + padding: 9px 13px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-primary); + font-size: 13px; + font-family: inherit; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.chat-input input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2); +} + +html[data-theme="light"] .chat-input input:focus { + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); +} + +.chat-input button { + padding: 9px 15px; + background: var(--accent); + border: none; + border-radius: 2px; + color: white; + cursor: pointer; + font-size: 12px; + font-family: inherit; + letter-spacing: 0.05em; + text-transform: uppercase; + transition: background 0.15s; +} + +.chat-input button:hover:not(:disabled) { + background: var(--accent-hover); +} + +.chat-input button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* ── Action Bar ──────────────────────────────────────────────── */ +.action-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + background: var(--bg-secondary); + border-top: 1px solid var(--border); +} + +.action-bar-left, +.action-bar-right { + display: flex; + align-items: center; + gap: 16px; +} + +.token-indicator { + display: flex; + align-items: center; + gap: 8px; +} + +.token-bar { + width: 100px; + height: 4px; + background: var(--bg-tertiary); + border-radius: 1px; + overflow: hidden; +} + +.token-fill { + height: 100%; + transition: width 0.3s ease; +} + +.token-label { + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary); +} + +.permission-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--text-secondary); + cursor: pointer; +} + +.permission-toggle input { + cursor: pointer; +} + +.btn-secondary { + padding: 6px 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 2px; + color: var(--text-primary); + cursor: pointer; + font-size: 12px; + font-family: inherit; + letter-spacing: 0.05em; + transition: background 0.15s; +} + +.btn-secondary:hover:not(:disabled) { + background: var(--border); +} + +.btn-secondary:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.btn-primary { + padding: 6px 18px; + background: var(--accent); + border: none; + border-radius: 2px; + color: white; + cursor: pointer; + font-weight: 600; + font-size: 12px; + font-family: inherit; + letter-spacing: 0.07em; + text-transform: uppercase; + transition: background 0.15s; +} + +.btn-primary:hover:not(:disabled) { + background: var(--accent-hover); +} + +.btn-primary:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.implementing-status { + color: var(--success); + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +/* ── Error Bar ───────────────────────────────────────────────── */ +.error-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + background: var(--danger); + color: white; + font-size: 12px; + letter-spacing: 0.03em; +} + +.error-bar button { + background: none; + border: none; + color: white; + font-size: 16px; + cursor: pointer; + padding: 0 4px; +} + +.error-bar button:hover { + opacity: 0.8; +} + +/* ── Onboarding ──────────────────────────────────────────────── */ +.onboarding h1 { + font-size: 24px; + margin-bottom: 8px; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.onboarding h2 { + font-size: 13px; + margin-top: 28px; + margin-bottom: 12px; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.1em; +} + +.onboarding p { + margin: 12px 0; + line-height: 1.6; +} + +.onboarding ol { + margin: 12px 0 12px 24px; +} + +.onboarding li { + margin: 8px 0; + line-height: 1.5; +} + +.onboarding pre { + background: var(--bg-tertiary); + padding: 12px 16px; + border-radius: 2px; + margin: 12px 0; +} + +.onboarding code { + font-family: inherit; + font-size: 12px; +} + +.onboarding a { + color: var(--accent); + text-decoration: none; +} + +.onboarding a:hover { + text-decoration: underline; +} + +.onboarding-tip { + margin-top: 28px; + padding: 16px; + background: var(--bg-tertiary); + border-left: 3px solid var(--accent); + border-radius: 0 2px 2px 0; +} +``` + +--- + +### 3. Add Theme State to App +**File:** `renderer/src/App.tsx` +**What:** Three additions to the existing file — no existing logic is touched. + +**3a. Add `Theme` type and `theme` state** — insert after the existing imports, before the `App` function: + +```typescript +// Add this type alias near the top, alongside other local types if any, +// or just before the App function: +type Theme = "dark" | "light"; +``` + +**3b. Add `theme` state and side-effects** — insert inside `App()`, after the existing `useState` declarations (after `const [error, setError] = useState(null)`): + +```typescript +const [theme, setTheme] = useState( + () => (localStorage.getItem("cf-theme") as Theme) ?? "dark" +); + +// Keep document.documentElement in sync, and persist to localStorage +useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem("cf-theme", theme); +}, [theme]); + +const handleToggleTheme = () => + setTheme((t) => (t === "dark" ? "light" : "dark")); +``` + +**3c. Pass new props to `Header` and `DocumentPane`** — update the JSX inside the `return`: + +```tsx +// Header — add two new props: +
+ +// DocumentPane — add one new prop: + +``` + +--- + +### 4. Update Header Component +**File:** `renderer/src/components/Header.tsx` +**What:** Add `theme` and `onToggleTheme` to the props interface, insert wordmark element, add toggle button. + +**Complete updated file:** + +```tsx +import React from "react"; +import type { Project, Session, Phase } from "../types"; + +type Theme = "dark" | "light"; + +interface HeaderProps { + projects: Project[]; + sessions: Session[]; + selectedProject: Project | null; + selectedSession: Session | null; + onSelectProject: (project: Project | null) => void; + onSelectSession: (session: Session | null) => void; + onCreateProject: () => void; + onCreateSession: () => void; + onDeleteProject?: (id: string) => void; + onDeleteSession?: (id: string) => void; + theme: Theme; + onToggleTheme: () => void; +} + +const phaseLabels: Record = { + research: "Research", + plan: "Plan", + implement: "Implement", +}; + +const phases: Phase[] = ["research", "plan", "implement"]; + +export function Header({ + projects, + sessions, + selectedProject, + selectedSession, + onSelectProject, + onSelectSession, + onCreateProject, + onCreateSession, + onDeleteProject, + onDeleteSession, + theme, + onToggleTheme, +}: HeaderProps) { + const handleDeleteProject = () => { + if (!selectedProject || !onDeleteProject) return; + if (confirm(`Delete project "${selectedProject.name}"? This cannot be undone.`)) { + onDeleteProject(selectedProject.id); + } + }; + + const handleDeleteSession = () => { + if (!selectedSession || !onDeleteSession) return; + if (confirm(`Delete session "${selectedSession.name}"? This cannot be undone.`)) { + onDeleteSession(selectedSession.id); + } + }; + + return ( +
+
+ {/* ── Wordmark ── */} + Claude Flow + + + + {selectedProject && onDeleteProject && ( + + )} + + {selectedProject && ( + <> + + + {selectedSession && onDeleteSession && ( + + )} + + )} +
+ +
+ {selectedSession && ( +
+ {phases.map((phase) => { + const phaseIndex = phases.indexOf(phase); + const currentIndex = phases.indexOf(selectedSession.phase); + const isComplete = phaseIndex < currentIndex; + const isActive = phase === selectedSession.phase; + + return ( + + {phaseLabels[phase]} + + ); + })} +
+ )} + + {/* ── Theme toggle ── */} + +
+
+ ); +} +``` + +--- + +### 5. Dynamic CodeMirror Theme in DocumentPane +**File:** `renderer/src/components/DocumentPane.tsx` +**What:** Three surgical changes to `MarkdownEditor` — add `theme` prop, update imports, update `useEffect`. + +**5a. Update imports** — add `syntaxHighlighting` and `defaultHighlightStyle` to the `@codemirror/language` import: + +```typescript +// BEFORE: +import { markdown } from "@codemirror/lang-markdown"; +import { languages } from "@codemirror/language-data"; + +// AFTER: +import { markdown } from "@codemirror/lang-markdown"; +import { languages } from "@codemirror/language-data"; +import { syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language"; +``` + +**5b. Update `MarkdownEditor` props interface** — add `theme`: + +```typescript +// BEFORE: +function MarkdownEditor({ + content, + onChange, + disabled, +}: { + content: string; + onChange: (content: string) => void; + disabled: boolean; +}) + +// AFTER: +function MarkdownEditor({ + content, + onChange, + disabled, + theme, +}: { + content: string; + onChange: (content: string) => void; + disabled: boolean; + theme: "dark" | "light"; +}) +``` + +**5c. Update the `useEffect` inside `MarkdownEditor`** — swap the theme extension and add `theme` to the dependency array: + +```typescript +// BEFORE (inside useEffect): +extensions: [ + lineNumbers(), + highlightActiveLine(), + drawSelection(), + history(), + keymap.of([...defaultKeymap, ...historyKeymap]), + markdown({ codeLanguages: languages }), + oneDark, // ← hardcoded + updateListener, + EditorView.editable.of(!disabled), + EditorView.lineWrapping, + EditorView.theme({ ... }), +], +// dependency array: +}, [disabled]); + +// AFTER: +extensions: [ + lineNumbers(), + highlightActiveLine(), + drawSelection(), + history(), + keymap.of([...defaultKeymap, ...historyKeymap]), + markdown({ codeLanguages: languages }), + theme === "dark" ? oneDark : syntaxHighlighting(defaultHighlightStyle), // ← dynamic + updateListener, + EditorView.editable.of(!disabled), + EditorView.lineWrapping, + EditorView.theme({ ... }), +], +// dependency array: +}, [disabled, theme]); // ← theme added +``` + +**5d. Update `DocumentPaneProps` interface and pass `theme` through** — add to the interface and forward to `MarkdownEditor`: + +```typescript +// BEFORE: +interface DocumentPaneProps { + content: string; + onChange: (content: string) => void; + phase: Phase; + disabled: boolean; + showOnboarding?: boolean; +} + +// AFTER: +interface DocumentPaneProps { + content: string; + onChange: (content: string) => void; + phase: Phase; + disabled: boolean; + showOnboarding?: boolean; + theme: "dark" | "light"; +} +``` + +And in the `DocumentPane` function body, destructure `theme` and pass it to `MarkdownEditor`: + +```tsx +// BEFORE: +export function DocumentPane({ + content, + onChange, + phase, + disabled, + showOnboarding, +}: DocumentPaneProps) + +// AFTER: +export function DocumentPane({ + content, + onChange, + phase, + disabled, + showOnboarding, + theme, +}: DocumentPaneProps) +``` + +```tsx +// BEFORE (in JSX): + + +// AFTER: + +``` + +--- + +## TODO +- [x] **1.** `renderer/index.html` — change `minimal` to `Claude Flow` +- [x] **2.** `renderer/src/styles/globals.css` — full replacement with new CSS (monospace body, electric accent, 2px radii, uppercase labels, light theme block, focus glow, `.app-wordmark`, `.theme-toggle`) +- [x] **3a.** `renderer/src/App.tsx` — add `type Theme` alias +- [x] **3b.** `renderer/src/App.tsx` — add `theme` state + `useEffect` + `handleToggleTheme` +- [x] **3c.** `renderer/src/App.tsx` — pass `theme`/`onToggleTheme` to `Header`, pass `theme` to `DocumentPane` +- [x] **4.** `renderer/src/components/Header.tsx` — full replacement (new props, wordmark, toggle button) +- [x] **5a.** `renderer/src/components/DocumentPane.tsx` — add `syntaxHighlighting, defaultHighlightStyle` import +- [x] **5b.** `renderer/src/components/DocumentPane.tsx` — add `theme` to `MarkdownEditor` props +- [x] **5c.** `renderer/src/components/DocumentPane.tsx` — dynamic theme extension + `theme` in dep array +- [x] **5d.** `renderer/src/components/DocumentPane.tsx` — add `theme` to `DocumentPaneProps`, destructure, forward to `MarkdownEditor` + +--- + +## Risks / Considerations + +**CodeMirror reinitialization on theme switch**: Adding `theme` to the `useEffect` dependency array means the entire editor is torn down and recreated when the theme changes. This is intentional and correct — CodeMirror extensions are baked into the `EditorState` at creation time and can't be hot-swapped. The existing `content` sync `useEffect` will immediately restore the document contents after reinit, so no text loss occurs. There is a brief visual flash on theme toggle; this is acceptable. + +**`data-theme` initial state and flash of wrong theme**: The `useState` initializer reads `localStorage` synchronously, and the `useEffect` sets `data-theme` on first render. Because this happens before paint (Electron loads the renderer in a hidden window and only shows it after load), there should be no flash of the wrong theme in production. In dev the Vite HMR setup may briefly show unstyled content; this is not a concern for shipping. + +**Monospace font rendering differences per OS**: "SF Mono" ships with macOS/Xcode; "Cascadia Code" ships with Windows Terminal; "JetBrains Mono" and "Fira Code" are user-installed. The fallback chain is safe — `Monaco` is widely available on macOS, and `"Courier New", monospace` are universal final fallbacks. No font files are bundled; this is system-font-only. + +**`Theme` type duplication**: The `type Theme = "dark" | "light"` alias is defined in both `App.tsx` and `Header.tsx`. This is a minor smell. If it grows to more places, move it into `renderer/src/types.ts`. For this task (2 files) the duplication is acceptable. + +**Light mode CodeMirror gutter background**: The `.codemirror-editor .cm-gutters` rule uses `var(--bg-secondary)` which will automatically pick up the light-mode value — no extra change needed there. + +**`select` element styling in light mode**: Native ` { @@ -129,6 +138,11 @@ export function Header({ })} )} + + {/* ── Theme toggle ── */} +
); diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css index 7ad199e..97b7bb8 100644 --- a/renderer/src/styles/globals.css +++ b/renderer/src/styles/globals.css @@ -4,25 +4,44 @@ padding: 0; } +/* ── Dark theme (default) ────────────────────────────────────── */ :root { - --bg-primary: #1a1a1a; - --bg-secondary: #252525; - --bg-tertiary: #333; - --border: #444; - --text-primary: #e0e0e0; + --bg-primary: #1a1a1a; + --bg-secondary: #252525; + --bg-tertiary: #333; + --border: #444; + --text-primary: #e0e0e0; --text-secondary: #888; - --accent: #3b82f6; - --accent-hover: #2563eb; - --success: #10b981; - --warning: #f59e0b; - --danger: #ef4444; -} - + --accent: #60a5fa; + --accent-hover: #93c5fd; + --success: #10b981; + --warning: #f59e0b; + --danger: #ef4444; +} + +/* ── Light theme overrides ───────────────────────────────────── */ +html[data-theme="light"] { + --bg-primary: #f4f4f2; + --bg-secondary: #e8e8e5; + --bg-tertiary: #d8d8d4; + --border: #b4b4b0; + --text-primary: #1a1a18; + --text-secondary: #5a5a56; + --accent: #2563eb; + --accent-hover: #1d4ed8; + --success: #059669; + --warning: #d97706; + --danger: #dc2626; +} + +/* ── Base ────────────────────────────────────────────────────── */ body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: "SF Mono", "Cascadia Code", "JetBrains Mono", "Fira Code", + Monaco, "Courier New", monospace; background: var(--bg-primary); color: var(--text-primary); overflow: hidden; + font-size: 13px; } .app { @@ -31,12 +50,12 @@ body { height: 100vh; } -/* Header */ +/* ── Header ──────────────────────────────────────────────────── */ .header { display: flex; justify-content: space-between; align-items: center; - padding: 12px 16px; + padding: 10px 16px; background: var(--bg-secondary); border-bottom: 1px solid var(--border); -webkit-app-region: drag; @@ -50,15 +69,30 @@ body { -webkit-app-region: no-drag; } +/* App wordmark */ +.app-wordmark { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--text-primary); + padding-right: 12px; + border-right: 1px solid var(--border); + margin-right: 4px; + user-select: none; + white-space: nowrap; +} + .header select, .header button { - padding: 6px 12px; + padding: 5px 10px; background: var(--bg-tertiary); border: 1px solid var(--border); - border-radius: 4px; + border-radius: 2px; color: var(--text-primary); cursor: pointer; - font-size: 13px; + font-size: 12px; + font-family: inherit; } .header button:hover { @@ -68,8 +102,8 @@ body { .header button.btn-delete { background: transparent; border: 1px solid var(--border); - padding: 6px 8px; - font-size: 14px; + padding: 5px 8px; + font-size: 13px; } .header button.btn-delete:hover { @@ -77,15 +111,32 @@ body { border-color: var(--danger); } +/* Theme toggle */ +.theme-toggle { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: lowercase; + opacity: 0.7; + transition: opacity 0.15s; +} + +.theme-toggle:hover { + opacity: 1; + background: var(--bg-tertiary) !important; +} + +/* Phase indicator */ .phase-indicator { display: flex; gap: 4px; } .phase-step { - padding: 4px 12px; - font-size: 12px; - border-radius: 4px; + padding: 3px 10px; + font-size: 11px; + letter-spacing: 0.07em; + text-transform: uppercase; + border-radius: 2px; background: var(--bg-tertiary); color: var(--text-secondary); } @@ -100,14 +151,14 @@ body { color: white; } -/* Main Content */ +/* ── Main Content ─────────────────────────────────────────────── */ .main-content { flex: 1; display: flex; overflow: hidden; } -/* Document Pane */ +/* ── Document Pane ───────────────────────────────────────────── */ .document-pane { flex: 1; display: flex; @@ -121,21 +172,29 @@ body { display: flex; justify-content: space-between; align-items: center; - padding: 8px 16px; + padding: 7px 16px; background: var(--bg-secondary); border-bottom: 1px solid var(--border); - font-size: 14px; + font-size: 11px; + letter-spacing: 0.07em; + text-transform: uppercase; color: var(--text-secondary); } .document-header button { - padding: 4px 8px; + padding: 3px 8px; background: var(--bg-tertiary); border: 1px solid var(--border); - border-radius: 4px; + border-radius: 2px; color: var(--text-primary); cursor: pointer; - font-size: 12px; + font-size: 11px; + font-family: inherit; + letter-spacing: 0.05em; +} + +.document-header button:hover { + background: var(--border); } .document-content { @@ -145,8 +204,8 @@ body { } .document-content.editing { - font-family: "SF Mono", Monaco, "Cascadia Code", monospace; - font-size: 14px; + font-family: inherit; + font-size: 13px; line-height: 1.6; background: var(--bg-primary); border: none; @@ -179,16 +238,19 @@ body { } .document-content.rendered h1 { - font-size: 28px; + font-size: 22px; margin: 24px 0 16px; + letter-spacing: -0.01em; } .document-content.rendered h2 { - font-size: 22px; + font-size: 17px; margin: 20px 0 12px; color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; } .document-content.rendered h3 { - font-size: 18px; + font-size: 14px; margin: 16px 0 8px; } .document-content.rendered p { @@ -198,14 +260,14 @@ body { .document-content.rendered code { background: var(--bg-tertiary); padding: 2px 6px; - border-radius: 4px; - font-size: 13px; - font-family: "SF Mono", Monaco, "Cascadia Code", monospace; + border-radius: 2px; + font-size: 12px; + font-family: inherit; } .document-content.rendered pre { background: var(--bg-tertiary); padding: 16px; - border-radius: 8px; + border-radius: 2px; overflow-x: auto; margin: 16px 0; } @@ -238,17 +300,20 @@ body { width: 100%; border-collapse: collapse; margin: 16px 0; - font-size: 14px; + font-size: 12px; } .document-content.rendered th, .document-content.rendered td { - padding: 10px 12px; + padding: 8px 12px; text-align: left; border: 1px solid var(--border); } .document-content.rendered th { background: var(--bg-tertiary); font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + font-size: 11px; } .document-content.rendered tr:nth-child(even) td { background: var(--bg-secondary); @@ -280,11 +345,13 @@ body { background: var(--accent); color: white; padding: 2px 8px; - border-radius: 4px; - font-size: 11px; + border-radius: 2px; + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; } -/* Chat Pane */ +/* ── Chat Pane ───────────────────────────────────────────────── */ .chat-pane { width: 380px; display: flex; @@ -299,11 +366,11 @@ body { } .message { - margin-bottom: 12px; - padding: 10px 14px; - border-radius: 8px; + margin-bottom: 10px; + padding: 9px 13px; + border-radius: 4px; max-width: 90%; - font-size: 14px; + font-size: 13px; line-height: 1.5; white-space: pre-wrap; } @@ -311,6 +378,7 @@ body { .message.user { background: var(--accent); margin-left: auto; + color: white; } .message.assistant { @@ -331,27 +399,38 @@ body { .chat-input input { flex: 1; - padding: 10px 14px; + padding: 9px 13px; background: var(--bg-tertiary); border: 1px solid var(--border); - border-radius: 8px; + border-radius: 2px; color: var(--text-primary); - font-size: 14px; + font-size: 13px; + font-family: inherit; + transition: border-color 0.15s, box-shadow 0.15s; } .chat-input input:focus { outline: none; border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2); +} + +html[data-theme="light"] .chat-input input:focus { + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); } .chat-input button { - padding: 10px 16px; + padding: 9px 15px; background: var(--accent); border: none; - border-radius: 8px; + border-radius: 2px; color: white; cursor: pointer; - font-size: 14px; + font-size: 12px; + font-family: inherit; + letter-spacing: 0.05em; + text-transform: uppercase; + transition: background 0.15s; } .chat-input button:hover:not(:disabled) { @@ -359,16 +438,16 @@ body { } .chat-input button:disabled { - opacity: 0.5; + opacity: 0.4; cursor: not-allowed; } -/* Action Bar */ +/* ── Action Bar ──────────────────────────────────────────────── */ .action-bar { display: flex; justify-content: space-between; align-items: center; - padding: 12px 16px; + padding: 10px 16px; background: var(--bg-secondary); border-top: 1px solid var(--border); } @@ -388,9 +467,9 @@ body { .token-bar { width: 100px; - height: 6px; + height: 4px; background: var(--bg-tertiary); - border-radius: 3px; + border-radius: 1px; overflow: hidden; } @@ -400,7 +479,9 @@ body { } .token-label { - font-size: 12px; + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; color: var(--text-secondary); } @@ -408,7 +489,9 @@ body { display: flex; align-items: center; gap: 6px; - font-size: 13px; + font-size: 11px; + letter-spacing: 0.05em; + text-transform: uppercase; color: var(--text-secondary); cursor: pointer; } @@ -418,13 +501,16 @@ body { } .btn-secondary { - padding: 8px 16px; + padding: 6px 14px; background: var(--bg-tertiary); border: 1px solid var(--border); - border-radius: 6px; + border-radius: 2px; color: var(--text-primary); cursor: pointer; - font-size: 14px; + font-size: 12px; + font-family: inherit; + letter-spacing: 0.05em; + transition: background 0.15s; } .btn-secondary:hover:not(:disabled) { @@ -432,19 +518,23 @@ body { } .btn-secondary:disabled { - opacity: 0.5; + opacity: 0.4; cursor: not-allowed; } .btn-primary { - padding: 8px 20px; + padding: 6px 18px; background: var(--accent); border: none; - border-radius: 6px; + border-radius: 2px; color: white; cursor: pointer; - font-weight: 500; - font-size: 14px; + font-weight: 600; + font-size: 12px; + font-family: inherit; + letter-spacing: 0.07em; + text-transform: uppercase; + transition: background 0.15s; } .btn-primary:hover:not(:disabled) { @@ -452,16 +542,18 @@ body { } .btn-primary:disabled { - opacity: 0.5; + opacity: 0.4; cursor: not-allowed; } .implementing-status { color: var(--success); - font-size: 14px; + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; } -/* Error Bar */ +/* ── Error Bar ───────────────────────────────────────────────── */ .error-bar { display: flex; justify-content: space-between; @@ -469,14 +561,15 @@ body { padding: 8px 16px; background: var(--danger); color: white; - font-size: 14px; + font-size: 12px; + letter-spacing: 0.03em; } .error-bar button { background: none; border: none; color: white; - font-size: 18px; + font-size: 16px; cursor: pointer; padding: 0 4px; } @@ -485,17 +578,21 @@ body { opacity: 0.8; } -/* Onboarding */ +/* ── Onboarding ──────────────────────────────────────────────── */ .onboarding h1 { - font-size: 32px; + font-size: 24px; margin-bottom: 8px; + letter-spacing: 0.05em; + text-transform: uppercase; } .onboarding h2 { - font-size: 20px; + font-size: 13px; margin-top: 28px; margin-bottom: 12px; color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.1em; } .onboarding p { @@ -515,13 +612,13 @@ body { .onboarding pre { background: var(--bg-tertiary); padding: 12px 16px; - border-radius: 6px; + border-radius: 2px; margin: 12px 0; } .onboarding code { - font-family: "SF Mono", Monaco, monospace; - font-size: 13px; + font-family: inherit; + font-size: 12px; } .onboarding a { @@ -538,5 +635,5 @@ body { padding: 16px; background: var(--bg-tertiary); border-left: 3px solid var(--accent); - border-radius: 0 6px 6px 0; + border-radius: 0 2px 2px 0; } -- cgit v1.2.3