|
| 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 |
0 commit comments