Skip to content

Commit efecf76

Browse files
cscheidclaude
andcommitted
Refactor hub-client color scheme: auto/dark/light with browser detection
Replace the binary light/dark toggle with a three-way color scheme preference (auto, dark, light). Auto mode follows the browser's prefers-color-scheme setting. The preference is persisted via the existing preferences service. - Add ThemeContext with ThemeProvider/useTheme hook - Add colorScheme to preferences schema (default: 'auto') - Create global theme.css with CSS variables for dark and light themes - Migrate all component CSS from hardcoded colors to CSS variables (Editor, FileSidebar, SidebarTabs, OutlinePanel, MinimalHeader, ReplayDrawer, NewFileDialog, ShareDialog, ViewToggleControl, MarkdownSummary, Toast, ProjectSelector, ProjectSetSetup) - Monaco editor follows the effective theme (vs-dark / light) - Theme toggle shows current mode (💻 auto, 🌙 dark, ☀️ light) - Preview pane is intentionally unaffected (out of scope) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 57d96fc commit efecf76

24 files changed

Lines changed: 807 additions & 394 deletions

.beads/issues.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
{"id":"bd-e9m6","title":"Fix json_filter to invoke Python scripts via interpreter on Windows","description":"Command::new(filter_path) fails on Windows for .py files with OS error 193 (not a valid Win32 application). Need to detect script extensions and invoke via interpreter, like Pandoc does.","notes":"PR #89 created. Pandoc-style exists-then-dispatch pattern with find_python/find_bash caching.","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-03-28T11:41:01.990041300Z","created_by":"cderv","updated_at":"2026-03-30T15:20:29.633028Z","closed_at":"2026-03-30T15:20:29.632662400Z","close_reason":"PR #89 merged into main","source_repo":".","compaction_level":0,"original_size":0}
4242
{"id":"bd-gk74","title":"WASM build: exception-handling unstable feature warning and missing wasm-bindgen","description":"npm run build:all in hub-client shows warning spam from unstable exception-handling target feature on every crate, and fails due to missing wasm-bindgen CLI. Plan: claude-notes/plans/2026-03-30-wasm-build-warnings.md","status":"open","priority":2,"issue_type":"bug","created_at":"2026-03-30T17:17:39.857777Z","created_by":"cscheid","updated_at":"2026-03-30T17:17:39.857777Z","source_repo":".","compaction_level":0,"original_size":0}
4343
{"id":"bd-i1df","title":"Kanban: Card detail view on title click","description":"Clicking a card title opens a detail modal showing all card information: title, type, status, created/deadline dates, priority, full body text. Plan: claude-notes/plans/2026-02-11-kanban-ui-enhancements.md","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-02-11T18:32:37.650902Z","created_by":"cscheid","updated_at":"2026-02-11T18:36:06.274831Z","closed_at":"2026-02-11T18:36:06.274814Z","close_reason":"Implemented - card detail modal on title click","source_repo":".","compaction_level":0,"original_size":0}
44+
{"id":"bd-nxe8","title":"Hub-client: refactor color scheme to auto/dark/light with browser detection","description":"Replace the binary light/dark toggle in ProjectSelector with a three-way color scheme preference (auto/dark/light). Auto mode follows prefers-color-scheme. Persisted via preferences service. Monaco editor follows the setting. Preview documents are out of scope. Plan: claude-notes/plans/2026-04-02-hub-client-color-scheme.md","status":"open","priority":2,"issue_type":"feature","created_at":"2026-04-02T21:21:58.194813Z","created_by":"cscheid","updated_at":"2026-04-02T21:21:58.194813Z","source_repo":".","compaction_level":0,"original_size":0}
4445
{"id":"bd-p2tx","title":"Support <#foo> anchor shorthand notation in qmd","description":"Add support for <#foo> as shorthand for [foo](#foo){.anchor} in qmd reader and writer. Reader: intercept html_element nodes matching <#id> pattern and convert to Link with .anchor class. Writer: detect matching Link nodes and emit <#id> shorthand. Plan: claude-notes/plans/2026-02-10-anchor-shorthand.md","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-02-10T21:35:30.243155Z","created_by":"cscheid","updated_at":"2026-02-10T22:10:45.943407Z","closed_at":"2026-02-10T22:10:45.943379Z","close_reason":"Implemented anchor shorthand <#foo> in reader and writer. All 6394 workspace tests pass.","source_repo":".","compaction_level":0,"original_size":0}
4546
{"id":"bd-rcdo","title":"InlineSplice silently drops header attribute changes","description":"When changing a Header's attributes (classes, key-value pairs) without changing its inline content, the incremental writer's InlineSplice path preserves the original block text verbatim — including the old attribute suffix. The new attributes are silently lost. Discovered while testing the kanban demo's setCardStatus mutation.","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-02-11T14:11:27.114610Z","created_by":"cscheid","updated_at":"2026-02-11T14:17:36.513428Z","closed_at":"2026-02-11T14:17:36.513409Z","close_reason":"Fixed: block_attrs_eq checks classes, kvs, and explicit IDs. 9 tests, all 6402 workspace tests pass.","source_repo":".","compaction_level":0,"original_size":0}
4647
{"id":"bd-s0ln","title":"hub-react-todo demo app (AST sync proof of concept)","description":"Standalone Vite React app in q2-demos/hub-react-todo that connects to a sync server, subscribes to a QMD file, and renders a live-updating todo list from the Pandoc AST. Demonstrates onASTChanged callback from Phase 1 of AST sync client API. Plan: claude-notes/plans/2026-02-06-hub-react-todo-demo.md","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-06T20:15:06.711433Z","created_by":"cscheid","updated_at":"2026-02-06T20:15:06.711433Z","source_repo":".","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-s0ln","depends_on_id":"bd-3lsb","type":"discovered-from","created_at":"2026-02-06T20:15:06.711433Z","created_by":"cscheid","metadata":"{}","thread_id":""}]}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Hub-Client Color Scheme Refactor
2+
3+
## Overview
4+
5+
Replace the current binary light/dark toggle in ProjectSelector with a proper three-way color scheme preference ("follow user", "force dark", "force light") that:
6+
7+
1. Respects the browser's `prefers-color-scheme` media query by default
8+
2. Persists the user's choice via the existing preferences service
9+
3. Applies consistently across the entire hub-client UI (project selector, editor, Monaco)
10+
4. Does **not** affect the rendered Quarto preview documents (out of scope)
11+
12+
## Current State
13+
14+
- **Theme toggle**: Boolean `useState(false)` in `ProjectSelector.tsx:96`, not persisted
15+
- **Theme CSS**: CSS variables in `ProjectSelector.css` — dark default, `.light-theme` class overrides
16+
- **Monaco**: Hardcoded `theme="vs-dark"` in `Editor.tsx:997`
17+
- **Editor CSS**: All hardcoded dark colors (`Editor.css`, `MarkdownSummary.css`)
18+
- **Toast CSS**: Uses `@media (prefers-color-scheme: light)` independently
19+
- **index.css**: Has a `@media (prefers-color-scheme: light)` block for root styles
20+
- **ProjectSetSetup.css**: Uses dark CSS variables with fallbacks, no light variant
21+
- **Preferences system**: Existing `services/preferences/` with Zod schema, localStorage persistence, and `usePreference` hook — ready to use
22+
- **ViewModeContext**: Good pattern to follow for a ThemeContext (React context + persistence)
23+
24+
## Design Decisions
25+
26+
### Color scheme type
27+
28+
```typescript
29+
type ColorScheme = 'auto' | 'dark' | 'light';
30+
```
31+
32+
- `auto` (default): Follow `prefers-color-scheme` via `matchMedia`
33+
- `dark`: Force dark theme
34+
- `light`: Force light theme
35+
36+
### Architecture: React Context + CSS class on `<html>`
37+
38+
Create a `ThemeContext` (following the `ViewModeContext` pattern) that:
39+
40+
1. Reads the preference from the preferences service
41+
2. Listens to `matchMedia('(prefers-color-scheme: dark)')` for `auto` mode
42+
3. Resolves the effective theme (`'dark' | 'light'`) and exposes it to consumers
43+
4. Sets class `dark` or `light` on `document.documentElement`
44+
45+
CSS uses `:root.light` / `:root.dark` selectors. This is the standard idiomatic pattern (used by Tailwind and most CSS frameworks). Class-based is preferred over `data-theme` because we don't need multiple coexisting themes on the same page — the only component that might differ (Quarto preview) lives in an iframe.
46+
47+
### Monaco integration
48+
49+
Monaco's `theme` prop is controlled by the resolved effective theme:
50+
- `'dark'``"vs-dark"`
51+
- `'light'``"light"` (Monaco's built-in light theme)
52+
53+
### CSS migration
54+
55+
Move theme CSS variables from `.project-selector` / `.project-selector.light-theme` scope to `:root.dark` / `:root.light`. This allows all components (editor, toasts, etc.) to use the same variables.
56+
57+
Hardcoded colors in `Editor.css`, `MarkdownSummary.css`, `ProjectSetSetup.css` get replaced with theme CSS variables.
58+
59+
### Toggle UI
60+
61+
Icon-cycle button showing the **current** selection (not the next one):
62+
63+
- ☀️ when in light mode ("I'm currently light")
64+
- 🌙 when in dark mode ("I'm currently dark")
65+
- 💻 (or similar system/monitor icon) when in auto mode ("I'm following your OS")
66+
67+
Clicking cycles: `auto → dark → light → auto`
68+
69+
Tooltip shows the current mode name (e.g., "Theme: Follow system").
70+
71+
## Work Items
72+
73+
### Phase 1: Infrastructure
74+
75+
- [x] Add `colorScheme` field to preferences schema (`'auto' | 'dark' | 'light'`, default `'auto'`)
76+
- [x] Create `ThemeContext.tsx` with `ThemeProvider` and `useTheme` hook
77+
- Exposes `{ colorScheme, setColorScheme, cycleColorScheme, effectiveTheme }`
78+
- Uses `getPreference`/`setPreference` for persistence
79+
- Listens to `matchMedia` change events for `auto` mode
80+
- Sets class `dark` or `light` on `<html>` element
81+
- [x] Wire `ThemeProvider` into the app's component tree (wraps ProjectSelector, Editor, and Toast)
82+
83+
### Phase 2: CSS migration
84+
85+
- [x] Move CSS variables from `.project-selector` / `.project-selector.light-theme` to `:root.dark` / `:root.light` (created `theme.css`, imported in `main.tsx`)
86+
- [x] Update `ProjectSelector.css` to remove the old scoped theme variables, use the global ones
87+
- [x] Add light-theme CSS variables for Editor components (`Editor.css`, `MarkdownSummary.css`)
88+
- [x] Update `ProjectSetSetup.css` to use theme variables
89+
- [x] Update `Toast.css` to use theme variables (replaced `@media prefers-color-scheme` with CSS variables)
90+
- [x] Update `index.css` to use CSS variables (replaced `@media prefers-color-scheme` block)
91+
92+
### Phase 3: Component updates
93+
94+
- [x] Update `ProjectSelector.tsx`: replace `useState(false)` toggle with `useTheme()` context; change toggle UI to icon-cycle showing current mode (auto=💻, dark=🌙, light=☀️), cycling `auto → dark → light → auto`
95+
- [x] Update `Editor.tsx`: use `useTheme()` to set Monaco `theme` prop (`vs-dark` / `light`)
96+
- [x] Update `Editor.css`: replace hardcoded dark colors with CSS variables (editor header, margins, status indicators, etc.)
97+
98+
### Phase 4: Cleanup & verification
99+
100+
- [x] Remove any dead code (old `lightTheme` state, unused CSS `.light-theme` selectors converted to `:root.light`)
101+
- [x] Verify `npm run build:all` passes
102+
- [ ] Manual testing: verify all three modes work across project selector and editor
103+
- [ ] Verify preview pane is unaffected

hub-client/src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ProjectSetSetup from './components/ProjectSetSetup';
55
import Editor from './components/Editor';
66
import Toast from './components/Toast';
77
import { ViewModeProvider } from './components/ViewModeContext';
8+
import { ThemeProvider } from './components/ThemeContext';
89
import { LoginScreen } from './components/auth/LoginScreen';
910
import {
1011
connect,
@@ -558,7 +559,7 @@ function App() {
558559
}
559560

560561
return (
561-
<>
562+
<ThemeProvider>
562563
{!project ? (
563564
<ProjectSelector
564565
onSelectProject={handleSelectProject}
@@ -599,7 +600,7 @@ function App() {
599600
visible={showSaveToast}
600601
onHide={() => setShowSaveToast(false)}
601602
/>
602-
</>
603+
</ThemeProvider>
603604
);
604605
}
605606

0 commit comments

Comments
 (0)