diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..e7ad777
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,18 @@
+version: 2
+updates:
+ - package-ecosystem: "gomod"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ vendor: true
+
+ - package-ecosystem: "npm"
+ directory: "/frontend"
+ schedule:
+ interval: "weekly"
+ versioning-strategy: increase
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9655cad..eed0a66 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -110,6 +110,36 @@ jobs:
recreate: true
path: code-coverage-results.md
+ govulncheck:
+ name: Go Vulnerability Check
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ security-events: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "1.25.0"
+ check-latest: true
+ cache: true
+ cache-dependency-path: go.sum
+
+ - name: Install system deps (Wails CGO)
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
+
+ - name: Run govulncheck
+ run: |
+ go install golang.org/x/vuln/cmd/govulncheck@latest
+ govulncheck ./internal/... || true
+
frontend-tests:
name: Frontend Tests
needs: build
diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
new file mode 100644
index 0000000..bcc969c
--- /dev/null
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -0,0 +1,21 @@
+name: Dependabot auto-merge
+on: pull_request
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ if: github.event.pull_request.user.login == 'dependabot[bot]'
+ steps:
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v2
+ - name: Enable auto-merge for Dependabot PRs
+ if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 0000000..3b3367e
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,14 @@
+name: Dependency Review
+on: pull_request
+
+permissions:
+ contents: read
+
+jobs:
+ dependency-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Dependency Review
+ uses: actions/dependency-review-action@v4
diff --git a/.gitignore b/.gitignore
index ad402ff..a61690b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ frontend/test-results/
test-results/
frontend/playwright-report/
playwright-report/
+package-lock.json
diff --git a/docs/superpowers/plans/2026-05-09-theme-system-plan.md b/docs/superpowers/plans/2026-05-09-theme-system-plan.md
new file mode 100644
index 0000000..3fb5eb1
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-09-theme-system-plan.md
@@ -0,0 +1,1720 @@
+# Theme System Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add VS Code-style holistic theme system — app UI chrome + code syntax highlighting — with light mode, system detection, and a gallery of 8 themes (2 built-in + 6 curated).
+
+**Architecture:** Single `ThemeContext` drives all theming via CSS custom properties. Built-in themes use Radix Colors CSS + `.dark` class. Gallery themes apply colors via JS `style.setProperty()`. `localStorage.themeMode` stores the single selection (`"system"` | theme name). CodeMirror editors consume a `HighlightStyle` from context.
+
+**Tech Stack:** Radix Colors + CSS custom properties + Tailwind v4 `@theme` + React Context + CodeMirror 6
+
+---
+
+## File Structure
+
+```
+frontend/src/
+├── App.jsx # Remove inline colors, wrap ThemeProvider
+├── globals.css # Add @theme, Radix Colors imports, @custom-variant dark
+├── context/
+│ └── ThemeContext.jsx # NEW: ThemeProvider + useTheme hook
+├── theme/
+│ ├── index.js # NEW: configureTheme, THEMES registry
+│ ├── builtins.js # NEW: GitHub Dark + Light theme objects
+│ ├── one-dark-pro.js # NEW
+│ ├── dracula.js # NEW
+│ ├── nord.js # NEW
+│ ├── catppuccin-mocha.js # NEW
+│ ├── solarized-dark.js # NEW
+│ ├── solarized-light.js # NEW
+│ └── scope-mapping.js # NEW: VS Code scope → Lezer tag
+├── components/
+│ ├── SettingsModal.jsx # REWRITE: Carbon → Radix Dialog + RadioGroup + Select
+│ ├── CommandPalette.jsx # UPDATE: theme switch commands
+│ ├── Sidebar.jsx # MIGRATE: inline hex → CSS vars
+│ ├── TitleBar.jsx # MIGRATE: inline hex → CSS vars
+│ ├── ui/Button.jsx # MIGRATE: inline hex → Tailwind classes
+│ └── inputs/
+│ ├── carbonCodeMirrorTheme.js # DELETE
+│ ├── CodeEditor.jsx # UPDATE: consume theme from context
+│ └── HighlightedCode.jsx # UPDATE: consume theme from context
+├── pages/
+│ ├── CodeFormatter/index.jsx # REWRITE: Prism → HighlightedCode
+│ ├── CodeEncoder/index.jsx # MIGRATE: textarea → CodeEditor/HighlightedCode
+│ ├── CodeEncrypter/index.jsx # MIGRATE: textarea → CodeEditor/HighlightedCode
+│ ├── HashGenerator/index.jsx # MIGRATE: textarea → CodeEditor/HighlightedCode
+│ ├── CodeConverter/index.jsx # MIGRATE: textarea → CodeEditor/HighlightedCode
+│ ├── TextUtilities/index.jsx # MIGRATE: textarea → CodeEditor/HighlightedCode
+│ ├── JwtDebugger/index.jsx # MIGRATE + Carbon icons → Lucide
+│ ├── NumberConverter/ConversionCard.jsx # MIGRATE: Carbon TextInput → input.jsx
+│ ├── NumberConverter/index.jsx # MIGRATE: textarea → CodeEditor
+│ ├── DataGenerator/index.jsx # MIGRATE: textarea → CodeEditor
+│ ├── DateTimeConverter/index.jsx # MIGRATE: textarea → CodeEditor (number fields unchanged)
+│ ├── ColorConverter/index.jsx # MIGRATE: textarea → CodeEditor
+│ ├── RegExpTester.jsx # MIGRATE: textarea → CodeEditor
+│ ├── CronJobParser.jsx # MIGRATE: textarea → CodeEditor
+│ ├── BarcodeGenerator.jsx # MIGRATE: textarea → CodeEditor
+│ └── TextDiffChecker/index.jsx # MIGRATE: textarea → CodeEditor
+├── spotlight.css # UPDATE: forced dark → theme vars
+├── index.scss # DELETE
+├── style.css # DELETE
+├── App.css # DELETE
+└── e2e/theme.spec.js # NEW: 15 E2E theme scenarios
+
+dep: ADD: @radix-ui/react-dialog, @radix-ui/react-radio-group
+dep: REMOVE: @carbon/react, @carbon/styles, @carbon/icons-react, prismjs
+```
+
+### Task Dependency Graph
+
+```
+Task 1 (deps install)
+ ├── Task 2 (globals.css + Radix Colors)
+ │ ├── Task 3 (Theme definitions)
+ │ │ └── Task 4 (scope-mapping)
+ │ └── Task 5 (ThemeContext)
+ │ ├── Task 6 (App.jsx wrapper)
+ │ │ ├── Task 8 (Sidebar)
+ │ │ ├── Task 9 (TitleBar)
+ │ │ ├── Task 10 (Button)
+ │ │ └── Task 11 (CodeEditor + HighlightedCode)
+ │ └── Task 7 (SettingsModal)
+ │ └── Task 12 (CommandPalette)
+ ├── Task 13 (Delete Carbon files + deps)
+ │ ├── Task 14 (ConversionCard + StatusMessages)
+ │ └── Task 15 (CodeFormatter Prism→HighlightedCode)
+ └── Task 16-31 (Tool textarea→CodeEditor, one per tool)
+```
+
+---
+
+### Task 1: Install new Radix packages + remove Carbon deps
+
+**Files:**
+- Modify: `frontend/package.json`
+- Run: `bun install`
+
+- [ ] **Step 1: Add and remove dependencies**
+
+```bash
+cd frontend
+bun add @radix-ui/react-dialog @radix-ui/react-radio-group
+bun remove @carbon/react @carbon/styles @carbon/icons-react prismjs
+```
+
+List the 3 new Carbon-dependent components that need rewriting (SettingsModal, ConversionCard, StatusMessages) — these will be handled in later tasks.
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add frontend/package.json frontend/bun.lock
+git commit -m "build: install radix dialog+radiogroup, remove carbon deps and prismjs"
+```
+
+---
+
+### Task 2: Set up CSS layer — Radix Colors + @theme block + dark variant
+
+**Files:**
+- Modify: `frontend/src/globals.css`
+
+Radix Colors CSS files provide the light/dark variable scales. The `@theme` block maps our shadcn-style tokens to Tailwind utilities. A `@custom-variant dark` powers the `dark:` modifier via `.dark` class.
+
+- [ ] **Step 1: Write globals.css with Radix Colors + @theme**
+
+```css
+@import 'tailwindcss';
+
+/* Radix Colors — pick neutral gray + blue as primary + red/green for semantic */
+@import '@radix-ui/colors/gray.css';
+@import '@radix-ui/colors/gray-dark.css';
+@import '@radix-ui/colors/blue.css';
+@import '@radix-ui/colors/blue-dark.css';
+@import '@radix-ui/colors/red.css';
+@import '@radix-ui/colors/red-dark.css';
+@import '@radix-ui/colors/green.css';
+@import '@radix-ui/colors/green-dark.css';
+
+@source "./**/*.jsx";
+@source "./**/*.js";
+@source "./**/*.tsx";
+@source "./**/*.ts";
+@source "../index.html";
+
+/* Tailwind dark variant — class-based */
+@custom-variant dark (&:where(.dark, .dark *));
+
+/* Light theme (default) */
+:root {
+ --background: var(--gray-1);
+ --foreground: var(--gray-12);
+ --card: var(--gray-2);
+ --card-foreground: var(--gray-12);
+ --popover: var(--gray-2);
+ --popover-foreground: var(--gray-12);
+ --primary: var(--blue-9);
+ --primary-foreground: white;
+ --secondary: var(--gray-3);
+ --secondary-foreground: var(--gray-12);
+ --muted: var(--gray-3);
+ --muted-foreground: var(--gray-11);
+ --accent: var(--blue-3);
+ --accent-foreground: var(--blue-12);
+ --destructive: var(--red-9);
+ --destructive-foreground: white;
+ --border: var(--gray-6);
+ --input: var(--gray-3);
+ --ring: var(--blue-9);
+ --success: var(--green-9);
+ --warning: #e5c07b;
+
+ /* Component-specific tokens */
+ --sidebar-background: var(--gray-2);
+ --sidebar-foreground: var(--gray-12);
+ --sidebar-accent: var(--blue-9);
+ --titlebar-background: var(--gray-2);
+ --scrollbar-thumb: var(--gray-7);
+ --scrollbar-track: transparent;
+}
+
+/* Dark theme overrides — Radix Colors auto-switches gray-* values in .dark */
+.dark {
+ --background: var(--gray-1);
+ --foreground: var(--gray-12);
+ --card: var(--gray-2);
+ --card-foreground: var(--gray-12);
+ --popover: var(--gray-2);
+ --popover-foreground: var(--gray-12);
+ --primary: var(--blue-9);
+ --primary-foreground: white;
+ --secondary: var(--gray-3);
+ --secondary-foreground: var(--gray-12);
+ --muted: var(--gray-3);
+ --muted-foreground: var(--gray-11);
+ --accent: var(--blue-3);
+ --accent-foreground: var(--blue-12);
+ --destructive: var(--red-9);
+ --destructive-foreground: white;
+ --border: var(--gray-6);
+ --input: var(--gray-3);
+ --ring: var(--blue-9);
+ --success: var(--green-9);
+ --warning: #e5c07b;
+
+ --sidebar-background: var(--gray-2);
+ --sidebar-foreground: var(--gray-12);
+ --sidebar-accent: var(--blue-9);
+ --titlebar-background: var(--gray-2);
+ --scrollbar-thumb: var(--gray-7);
+ --scrollbar-track: transparent;
+}
+
+/* @theme block maps tokens to Tailwind utilities */
+@theme {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-success: var(--success);
+ --color-warning: var(--warning);
+}
+
+/* Scrollbar using theme variables */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--scrollbar-track); }
+::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background-color: var(--gray-8); }
+* { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); }
+```
+
+- [ ] **Step 2: Remove `import './App.css'` from App.jsx**
+
+Remove line 9 in `App.jsx`: `import './App.css';`
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add frontend/src/globals.css frontend/src/App.jsx
+git commit -m "feat: set up Radix Colors with @theme block and class-based dark mode"
+```
+
+---
+
+### Task 3: Create theme definition files
+
+**Files:**
+- Create: `frontend/src/theme/`
+- All 8 theme definition files + barrel index
+
+Each theme is a JS object with `name`, `type`, `colors`, and `tokenColors`. Built-in themes (`builtins.js`) also export default values for non-gallery use.
+
+- [ ] **Step 1: Create `frontend/src/theme/scope-mapping.js`**
+
+```js
+import { tags as t } from '@lezer/highlight';
+
+export const SCOPE_TO_TAG = {
+ keyword: t.keyword,
+ string: t.string,
+ number: t.number,
+ comment: t.blockComment,
+ type: t.typeName,
+ function: t.function(t.variableName),
+ variable: t.variableName,
+ operator: t.operator,
+ punctuation: t.punctuation,
+ tag: t.tagName,
+ attribute: t.attributeName,
+ property: t.propertyName,
+ constant: t.constant(t.variableName),
+ bool: t.bool,
+ 'null': t.null,
+ class: t.className,
+ 'definition': t.definitionModifier,
+};
+```
+
+- [ ] **Step 2: Create `frontend/src/theme/builtins.js`**
+
+```js
+export const THEMES = {
+ 'github-dark': {
+ name: 'GitHub Dark',
+ type: 'dark',
+ isBuiltIn: true,
+ colors: {
+ background: 'var(--gray-1)',
+ foreground: 'var(--gray-12)',
+ card: 'var(--gray-2)',
+ 'card-foreground': 'var(--gray-12)',
+ popover: 'var(--gray-2)',
+ 'popover-foreground': 'var(--gray-12)',
+ primary: 'var(--blue-9)',
+ 'primary-foreground': 'white',
+ secondary: 'var(--gray-3)',
+ 'secondary-foreground': 'var(--gray-12)',
+ muted: 'var(--gray-3)',
+ 'muted-foreground': 'var(--gray-11)',
+ accent: 'var(--gray-4)',
+ 'accent-foreground': 'var(--gray-12)',
+ destructive: 'var(--red-9)',
+ 'destructive-foreground': 'white',
+ border: 'var(--gray-6)',
+ input: 'var(--gray-3)',
+ ring: 'var(--blue-9)',
+ success: 'var(--green-9)',
+ warning: '#e5c07b',
+ 'sidebar-background': 'var(--gray-2)',
+ 'sidebar-foreground': 'var(--gray-12)',
+ 'sidebar-accent': 'var(--blue-9)',
+ 'titlebar-background': 'var(--gray-2)',
+ 'scrollbar-thumb': 'var(--gray-7)',
+ 'scrollbar-track': 'transparent',
+ },
+ tokenColors: [
+ { scope: 'keyword', color: '#d73a49' },
+ { scope: 'string', color: '#032f62' },
+ { scope: 'number', color: '#005cc5' },
+ { scope: 'comment', color: '#6a737d' },
+ { scope: 'type', color: '#6f42c1' },
+ { scope: 'function', color: '#6f42c1' },
+ { scope: 'variable', color: '#e36209' },
+ { scope: 'operator', color: '#d73a49' },
+ { scope: 'punctuation', color: '#24292e' },
+ { scope: 'tag', color: '#22863a' },
+ { scope: 'attribute', color: '#6f42c1' },
+ { scope: 'property', color: '#005cc5' },
+ { scope: 'constant', color: '#005cc5' },
+ { scope: 'bool', color: '#005cc5' },
+ { scope: 'null', color: '#005cc5' },
+ { scope: 'class', color: '#6f42c1' },
+ { scope: 'definition', color: '#6f42c1' },
+ ],
+ },
+ 'github-light': {
+ name: 'GitHub Light',
+ type: 'light',
+ isBuiltIn: true,
+ colors: {
+ background: 'var(--gray-1)',
+ foreground: 'var(--gray-12)',
+ card: 'var(--gray-2)',
+ 'card-foreground': 'var(--gray-12)',
+ popover: 'var(--gray-2)',
+ 'popover-foreground': 'var(--gray-12)',
+ primary: 'var(--blue-9)',
+ 'primary-foreground': 'white',
+ secondary: 'var(--gray-3)',
+ 'secondary-foreground': 'var(--gray-12)',
+ muted: 'var(--gray-3)',
+ 'muted-foreground': 'var(--gray-11)',
+ accent: 'var(--gray-4)',
+ 'accent-foreground': 'var(--gray-12)',
+ destructive: 'var(--red-9)',
+ 'destructive-foreground': 'white',
+ border: 'var(--gray-6)',
+ input: 'var(--gray-3)',
+ ring: 'var(--blue-9)',
+ success: 'var(--green-9)',
+ warning: '#e5c07b',
+ 'sidebar-background': 'var(--gray-2)',
+ 'sidebar-foreground': 'var(--gray-12)',
+ 'sidebar-accent': 'var(--blue-9)',
+ 'titlebar-background': 'var(--gray-2)',
+ 'scrollbar-thumb': 'var(--gray-7)',
+ 'scrollbar-track': 'transparent',
+ },
+ // GitHub Light syntax colors (always hex, never var() — used for CodeMirror)
+ tokenColors: [
+ { scope: 'keyword', color: '#d73a49' },
+ { scope: 'string', color: '#032f62' },
+ { scope: 'number', color: '#005cc5' },
+ { scope: 'comment', color: '#6a737d' },
+ { scope: 'type', color: '#6f42c1' },
+ { scope: 'function', color: '#6f42c1' },
+ { scope: 'variable', color: '#e36209' },
+ { scope: 'operator', color: '#d73a49' },
+ { scope: 'punctuation', color: '#24292e' },
+ { scope: 'tag', color: '#22863a' },
+ { scope: 'attribute', color: '#6f42c1' },
+ { scope: 'property', color: '#005cc5' },
+ { scope: 'constant', color: '#005cc5' },
+ { scope: 'bool', color: '#005cc5' },
+ { scope: 'null', color: '#005cc5' },
+ { scope: 'class', color: '#6f42c1' },
+ { scope: 'definition', color: '#6f42c1' },
+ ],
+ },
+};
+```
+
+- [ ] **Step 3: Create 6 gallery theme files**
+
+Each file follows the same pattern. Example for `frontend/src/theme/one-dark-pro.js`:
+
+```js
+export default {
+ name: 'One Dark Pro',
+ type: 'dark',
+ isBuiltIn: false,
+ colors: {
+ background: '#282c34',
+ foreground: '#abb2bf',
+ card: '#2c323c',
+ 'card-foreground': '#abb2bf',
+ popover: '#2c323c',
+ 'popover-foreground': '#abb2bf',
+ primary: '#61afef',
+ 'primary-foreground': '#ffffff',
+ secondary: '#3b4048',
+ 'secondary-foreground': '#abb2bf',
+ muted: '#3b4048',
+ 'muted-foreground': '#818896',
+ accent: '#61afef',
+ 'accent-foreground': '#ffffff',
+ destructive: '#e06c75',
+ 'destructive-foreground': '#ffffff',
+ border: '#3b4048',
+ input: '#3b4048',
+ ring: '#61afef',
+ success: '#98c379',
+ 'success-foreground': '#ffffff',
+ warning: '#e5c07b',
+ 'warning-foreground': '#282c34',
+ 'sidebar-background': '#21252b',
+ 'sidebar-foreground': '#abb2bf',
+ 'sidebar-accent': '#61afef',
+ 'titlebar-background': '#21252b',
+ 'scrollbar-thumb': '#3b4048',
+ 'scrollbar-track': '#21252b',
+ },
+ tokenColors: [
+ { scope: 'keyword', color: '#c678dd' },
+ { scope: 'string', color: '#98c379' },
+ { scope: 'number', color: '#d19a66' },
+ { scope: 'comment', color: '#5c6370' },
+ { scope: 'type', color: '#e5c07b' },
+ { scope: 'function', color: '#61afef' },
+ { scope: 'variable', color: '#e06c75' },
+ { scope: 'operator', color: '#56b6c2' },
+ { scope: 'punctuation', color: '#abb2bf' },
+ { scope: 'tag', color: '#e06c75' },
+ { scope: 'attribute', color: '#d19a66' },
+ { scope: 'property', color: '#61afef' },
+ { scope: 'constant', color: '#d19a66' },
+ { scope: 'bool', color: '#d19a66' },
+ { scope: 'null', color: '#d19a66' },
+ { scope: 'class', color: '#e5c07b' },
+ { scope: 'definition', color: '#61afef' },
+ ],
+};
+```
+
+Create these 6 files with their respective colors:
+- `frontend/src/theme/one-dark-pro.js` — One Dark Pro colors (shown above)
+- `frontend/src/theme/dracula.js` — Dracula theme (background: #282a36, foreground: #f8f8f2, pink accents)
+- `frontend/src/theme/nord.js` — Nord theme (background: #2e3440, foreground: #d8dee9, frost blue accents)
+- `frontend/src/theme/catppuccin-mocha.js` — Catppuccin Mocha (background: #1e1e2e, foreground: #cdd6f4, mauve accents)
+- `frontend/src/theme/solarized-dark.js` — Solarized Dark (background: #002b36, foreground: #839496, teal accents)
+- `frontend/src/theme/solarized-light.js` — Solarized Light (background: #fdf6e3, foreground: #657b83, yellow accents)
+
+Each file exports a `default` object with the same structure (name, type, isBuiltIn: false, colors, tokenColors).
+
+- [ ] **Step 4: Create `frontend/src/theme/index.js` barrel**
+
+```js
+import { THEMES as builtins } from './builtins';
+import oneDarkPro from './one-dark-pro';
+import dracula from './dracula';
+import nord from './nord';
+import catppuccinMocha from './catppuccin-mocha';
+import solarizedDark from './solarized-dark';
+import solarizedLight from './solarized-light';
+
+export const THEME_TOKENS = [
+ 'background', 'foreground', 'card', 'card-foreground',
+ 'popover', 'popover-foreground', 'primary', 'primary-foreground',
+ 'secondary', 'secondary-foreground', 'muted', 'muted-foreground',
+ 'accent', 'accent-foreground', 'destructive', 'destructive-foreground',
+ 'border', 'input', 'ring', 'success', 'success-foreground',
+ 'warning', 'warning-foreground',
+ 'sidebar-background', 'sidebar-foreground', 'sidebar-accent',
+ 'titlebar-background', 'scrollbar-thumb', 'scrollbar-track',
+];
+
+export const BUILT_IN_THEME_KEYS = ['github-dark', 'github-light'];
+
+export const allThemes = [
+ builtins['github-dark'],
+ builtins['github-light'],
+ oneDarkPro,
+ dracula,
+ nord,
+ catppuccinMocha,
+ solarizedDark,
+ solarizedLight,
+];
+
+export function getThemeByKey(key) {
+ if (builtins[key]) return builtins[key];
+ return allThemes.find(t => t.name.toLowerCase().replace(/\s+/g, '-') === key);
+}
+
+export function resolveTheme(themeMode) {
+ if (themeMode === 'system') {
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ return {
+ theme: prefersDark ? builtins['github-dark'] : builtins['github-light'],
+ actualType: prefersDark ? 'dark' : 'light',
+ };
+ }
+ const theme = getThemeByKey(themeMode);
+ if (theme) return { theme, actualType: theme.type };
+ // Fallback
+ return { theme: builtins['github-dark'], actualType: 'dark' };
+}
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add frontend/src/theme/
+git commit -m "feat: create 8 theme definitions with scope-mapping and barrel"
+```
+
+---
+
+### Task 4: Create ThemeContext provider
+
+**Files:**
+- Create: `frontend/src/context/ThemeContext.jsx`
+
+Single provider that manages the theme selection, applies CSS vars, generates CodeMirror HighlightStyle, and handles system detection.
+
+- [ ] **Step 1: Write `ThemeContext.jsx`**
+
+```jsx
+import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
+import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
+import { EditorView } from '@codemirror/view';
+import { SCOPE_TO_TAG } from '../theme/scope-mapping';
+import { THEME_TOKENS, resolveTheme, allThemes, BUILT_IN_THEME_KEYS } from '../theme';
+
+const ThemeContext = createContext(null);
+
+export function useTheme() {
+ const ctx = useContext(ThemeContext);
+ if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
+ return ctx;
+}
+
+function buildHighlightStyle(theme) {
+ if (!theme?.tokenColors?.length) return null;
+ return HighlightStyle.define(
+ theme.tokenColors.map(tc => ({
+ tag: SCOPE_TO_TAG[tc.scope] || tc.scope,
+ color: tc.color,
+ }))
+ );
+}
+
+export function ThemeProvider({ children }) {
+ const [themeMode, setThemeModeState] = useState(() => {
+ return localStorage.getItem('themeMode') || 'system';
+ });
+
+ const setThemeMode = useCallback((mode) => {
+ localStorage.setItem('themeMode', mode);
+ setThemeModeState(mode);
+ }, []);
+
+ // Resolve theme + detect system preference
+ const [systemPrefersDark, setSystemPrefersDark] = useState(() => {
+ if (typeof window === 'undefined') return false;
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ });
+
+ useEffect(() => {
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
+ const handler = (e) => setSystemPrefersDark(e.matches);
+ mq.addEventListener('change', handler);
+ return () => mq.removeEventListener('change', handler);
+ }, []);
+
+ const resolved = useMemo(() => {
+ if (themeMode === 'system') {
+ return {
+ theme: systemPrefersDark ? allThemes[0] : allThemes[1],
+ actualType: systemPrefersDark ? 'dark' : 'light',
+ };
+ }
+ return resolveTheme(themeMode);
+ }, [themeMode, systemPrefersDark]);
+
+ const { theme } = resolved;
+
+ // Apply CSS vars to :root
+ useEffect(() => {
+ const root = document.documentElement;
+
+ if (theme.isBuiltIn) {
+ // Clear any JS-set overrides, let CSS handle it
+ THEME_TOKENS.forEach(token => root.style.removeProperty(`--${token}`));
+ if (theme.type === 'dark') {
+ root.classList.add('dark');
+ } else {
+ root.classList.remove('dark');
+ }
+ } else {
+ // Gallery theme: apply all colors via JS
+ root.classList.remove('dark');
+ for (const [token, value] of Object.entries(theme.colors)) {
+ root.style.setProperty(`--${token}`, value);
+ }
+ }
+ }, [theme]);
+
+ // Build CodeMirror HighlightStyle
+ const highlightStyle = useMemo(() => buildHighlightStyle(theme), [theme]);
+
+ const editorExtensions = useMemo(() => {
+ if (!highlightStyle) return [];
+ return [
+ EditorView.theme({
+ '&': { backgroundColor: 'var(--background)', color: 'var(--foreground)' },
+ '.cm-content': { caretColor: 'var(--foreground)', fontFamily: "'Menlo', 'Monaco', 'Courier New', monospace" },
+ '.cm-gutters': { backgroundColor: 'var(--card)', borderRight: '1px solid var(--border)', color: 'var(--muted-foreground)' },
+ '.cm-activeLineGutter': { backgroundColor: 'var(--muted)' },
+ '&.cm-focused .cm-cursor': { borderLeftColor: 'var(--foreground)' },
+ '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'var(--accent)' },
+ }),
+ syntaxHighlighting(highlightStyle),
+ ];
+ }, [highlightStyle]);
+
+ const value = useMemo(() => ({
+ themeMode,
+ setThemeMode,
+ theme,
+ actualType: resolved.actualType,
+ editorExtensions,
+ allThemes,
+ }), [themeMode, setThemeMode, theme, resolved.actualType, editorExtensions]);
+
+ return (
+
+ When enabled, clicking the close button will minimize the app to the system tray instead of quitting. +
++ +// After: +import HighlightedCode from '../../../components/inputs/HighlightedCode'; +// ...+``` + +Language mapping from CodeFormatter format to HighlightedCode: +- `json` → `json` (same) +- `xml` → `xml` (same) +- `html` → `html` (same) +- `css` → `css` (same) + +- [ ] **Step 2: Remove Prism import line and CSS** + +Delete these lines: +```js +import Prism from 'prismjs'; +import 'prismjs/components/prism-json'; +import 'prismjs/components/prism-xml-doc'; +import 'prismjs/components/prism-css'; +import 'prismjs/components/prism-markup'; +import 'prismjs/themes/prism-tomorrow.css'; +``` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/pages/CodeFormatter/index.jsx +git commit -m "feat: replace Prism.js with HighlightedCode in CodeFormatter" +``` + +--- + +### Task 15-30: Migrate tool pages from textarea → CodeEditor/HighlightedCode + +**Pattern (same for all tools):** + +Each tool page has Input (editable) and Output (read-only) panes. Most use `