Skip to content

Commit e1bd27a

Browse files
backnotpropclaude
andauthored
feat: custom theme system with 18 built-in themes (#294)
* feat: custom theme system with 15 built-in themes Consolidate CSS theming into a single source of truth (packages/ui/theme.css) and introduce a multi-theme architecture where each theme defines both dark and light mode variants. Users can pick a color palette (theme) and separately toggle dark/light mode within it. - Extract shared color tokens, Tailwind bridge, and base styles into packages/ui/theme.css - Replace hardcoded oklch values with token references (oklch from var syntax) - Fix 3 light-mode bugs in review-editor diff colors - Create 15 built-in themes: Plannotator (default), Claude+, Soft Pop, Adwaita, Caffeine, Cyberdyne, Cyberfunk, Doom 64, Dracula, Gruvbox, PaulMillr, Quantum Rose, Solar Dusk, Terminal, Tinacious - Expand ThemeProvider to manage colorTheme + mode independently - Add Theme tab to Settings with mode toggle, search, and swatch grid - Dark-only themes (Dracula, Terminal, etc.) suppress light class to prevent broken styling; light-only themes (Tinacious) force it - Cookie persistence: plannotator-color-theme for palette, existing key for mode - Include theme conversion script for FinSitter theme adaptation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: refine themes, add Catppuccin/Rosé Pine/Monokai Pro/Synthwave 84/Tokyo Night Theme curation: - Replace Cyberdyne with Synthwave '84 (from robb0wen/synthwave-vscode) - Replace Cyberfunk with Catppuccin (Mocha dark + Latte light), Rosé Pine (dark + Dawn light), Monokai Pro (dark only), Tokyo Night (Storm + Day) - Rewrite Gruvbox from canonical source (morhetz/gruvbox) - Rewrite Adwaita from canonical VS Code theme (piousdeer/vscode-adwaita) - Rewrite PaulMillr from Ghostty canonical palette Theme fixes: - Fix faded Send Feedback button on Solar Dusk, Quantum Rose, Caffeine (accent colors were too dark/invisible at 15% opacity) - Fix Dracula/Terminal/PaulMillr/Tinacious light mode breakage — dark-only themes now suppress .light class via modeSupport in ThemeProvider - Fix code block backgrounds — use --code-bg token instead of --muted - Add faint green grid overlay for Terminal theme - Alphabetize theme registry (Plannotator first) Code review diff theming: - Pass theme colors into @pierre/diffs shadow DOM via unsafeCSS prop - Dynamic themeType based on resolved mode 18 built-in themes: Plannotator, Absolutely, Adwaita, Caffeine, Catppuccin, Doom 64, Dracula, Gruvbox, Monokai Pro, PaulMillr, Quantum Rose, Rosé Pine, Soft Pop, Solar Dusk, Synthwave '84, Terminal, Tinacious, Tokyo Night Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: increase settings modal height for theme grid visibility Remove 340px cap on theme grid, bump content area from 70vh to 85vh so all themes are visible without scrolling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: memoize ThemeProvider context, deduplicate Mode type, clean up DiffViewer From /simplify review: - Export Mode type from ThemeProvider, import in ThemeTab (was duplicated) - Add resolvedMode to context — consumers no longer re-query matchMedia - Memoize context value with useMemo, setters with useCallback (prevents unnecessary re-renders of all useTheme consumers) - Consolidate DiffViewer's two pierre state vars into single object - Use resolvedMode from context in DiffViewer instead of classList check - Format crammed single-line extended tokens in 4 theme CSS files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prevent FOUC, fix system mode stale read, restore marketing theme class P1: Marketing site inline script now sets theme-{name} class on <html> before first paint (reads plannotator-color-theme cookie). Without this, CSS tokens under .theme-* selectors were never active. P2: System mode effect now re-reads matchMedia.matches immediately when entering system mode, not just on future changes. Fixes stale resolvedMode when OS preference changed while pinned to explicit dark/light. P3: ThemeProvider applies theme class synchronously during render (not in a passive useEffect) to prevent flash of unstyled content on hard refresh. Also extracted resolveThemeClasses to module scope to avoid useCallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add Quick Copy button, fix import icon, comment out agent badge - Add Quick Copy button to annotation panel footer (splits horizontally with existing Quick Share). Copies annotations wrapped with the deny preamble so output is paste-ready for agent sessions. - Export Modal annotations copy also wraps with deny preamble - Extract wrapFeedbackForAgent() utility in parser.ts as single source of truth for the preamble text - Fix desktop import icon to match mobile (arrow-into-document, not download) - Comment out code review agent badge — unreliable across multiple harnesses Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 54e951e commit e1bd27a

36 files changed

Lines changed: 1990 additions & 545 deletions

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ plannotator/
4646
│ │ ├── ide.ts # VS Code diff integration (openEditorDiff)
4747
│ │ ├── editor-annotations.ts # VS Code editor annotation endpoints
4848
│ │ └── project.ts # Project name detection for tags
49-
│ ├── ui/ # Shared React components
49+
│ ├── ui/ # Shared React components + theme
50+
│ │ ├── theme.css # Single source of truth for color tokens + Tailwind bridge
5051
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
5152
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
5253
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser

apps/marketing/src/layouts/Base.astro

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,19 @@ import '../styles/global.css';
4646
<body class="min-h-screen antialiased">
4747
<slot />
4848
<script is:inline>
49-
// Read theme cookie and set class before paint to prevent FOUC
49+
// Read theme cookies and set classes before paint to prevent FOUC
5050
(function() {
51-
var match = document.cookie.match(/(?:^|; )plannotator-theme=([^;]*)/);
52-
var theme = match ? decodeURIComponent(match[1]) : 'dark';
5351
var root = document.documentElement;
54-
root.classList.remove('light');
55-
if (theme === 'light') {
52+
// Color theme (palette) — marketing uses whatever the user set in the app
53+
var colorMatch = document.cookie.match(/(?:^|; )plannotator-color-theme=([^;]*)/);
54+
var colorTheme = colorMatch ? decodeURIComponent(colorMatch[1]) : 'plannotator';
55+
root.classList.add('theme-' + colorTheme);
56+
// Mode (dark/light/system)
57+
var modeMatch = document.cookie.match(/(?:^|; )plannotator-theme=([^;]*)/);
58+
var mode = modeMatch ? decodeURIComponent(modeMatch[1]) : 'dark';
59+
if (mode === 'light') {
5660
root.classList.add('light');
57-
} else if (theme === 'system') {
61+
} else if (mode === 'system') {
5862
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
5963
root.classList.add('light');
6064
}

apps/marketing/src/styles/global.css

Lines changed: 1 addition & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -6,151 +6,7 @@
66
@source "../layouts/**/*.astro";
77
@source "../pages/**/*.astro";
88

9-
:root {
10-
--background: oklch(0.15 0.02 260);
11-
--foreground: oklch(0.90 0.01 260);
12-
--card: oklch(0.22 0.02 260);
13-
--card-foreground: oklch(0.90 0.01 260);
14-
--popover: oklch(0.28 0.025 260);
15-
--popover-foreground: oklch(0.90 0.01 260);
16-
--primary: oklch(0.75 0.18 280);
17-
--primary-foreground: oklch(0.15 0.02 260);
18-
--secondary: oklch(0.65 0.15 180);
19-
--secondary-foreground: oklch(0.15 0.02 260);
20-
--muted: oklch(0.26 0.02 260);
21-
--muted-foreground: oklch(0.72 0.02 260);
22-
--accent: oklch(0.70 0.20 60);
23-
--accent-foreground: oklch(0.15 0.02 260);
24-
--destructive: oklch(0.65 0.20 25);
25-
--destructive-foreground: oklch(0.98 0 0);
26-
--border: oklch(0.35 0.02 260);
27-
--input: oklch(0.26 0.02 260);
28-
--ring: oklch(0.75 0.18 280);
29-
--success: oklch(0.72 0.17 150);
30-
--success-foreground: oklch(0.15 0.02 260);
31-
--warning: oklch(0.75 0.15 85);
32-
--warning-foreground: oklch(0.20 0.02 260);
33-
34-
--font-sans: 'Inter', system-ui, sans-serif;
35-
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
36-
--radius: 0.75rem;
37-
}
38-
39-
.light {
40-
--background: oklch(0.97 0.005 260);
41-
--foreground: oklch(0.18 0.02 260);
42-
--card: oklch(1 0 0);
43-
--card-foreground: oklch(0.18 0.02 260);
44-
--popover: oklch(1 0 0);
45-
--popover-foreground: oklch(0.18 0.02 260);
46-
--primary: oklch(0.50 0.25 280);
47-
--primary-foreground: oklch(1 0 0);
48-
--secondary: oklch(0.50 0.18 180);
49-
--secondary-foreground: oklch(1 0 0);
50-
--muted: oklch(0.92 0.01 260);
51-
--muted-foreground: oklch(0.40 0.02 260);
52-
--accent: oklch(0.60 0.22 50);
53-
--accent-foreground: oklch(0.18 0.02 260);
54-
--destructive: oklch(0.50 0.25 25);
55-
--destructive-foreground: oklch(1 0 0);
56-
--border: oklch(0.88 0.01 260);
57-
--input: oklch(0.92 0.01 260);
58-
--ring: oklch(0.50 0.25 280);
59-
--success: oklch(0.45 0.20 150);
60-
--success-foreground: oklch(1 0 0);
61-
--warning: oklch(0.55 0.18 85);
62-
--warning-foreground: oklch(0.18 0.02 260);
63-
}
64-
65-
@theme inline {
66-
--color-background: var(--background);
67-
--color-foreground: var(--foreground);
68-
--color-card: var(--card);
69-
--color-card-foreground: var(--card-foreground);
70-
--color-popover: var(--popover);
71-
--color-popover-foreground: var(--popover-foreground);
72-
--color-primary: var(--primary);
73-
--color-primary-foreground: var(--primary-foreground);
74-
--color-secondary: var(--secondary);
75-
--color-secondary-foreground: var(--secondary-foreground);
76-
--color-muted: var(--muted);
77-
--color-muted-foreground: var(--muted-foreground);
78-
--color-accent: var(--accent);
79-
--color-accent-foreground: var(--accent-foreground);
80-
--color-destructive: var(--destructive);
81-
--color-destructive-foreground: var(--destructive-foreground);
82-
--color-success: var(--success);
83-
--color-success-foreground: var(--success-foreground);
84-
--color-warning: var(--warning);
85-
--color-warning-foreground: var(--warning-foreground);
86-
--color-border: var(--border);
87-
--color-input: var(--input);
88-
--color-ring: var(--ring);
89-
--font-sans: var(--font-sans);
90-
--font-mono: var(--font-mono);
91-
--radius-sm: calc(var(--radius) - 4px);
92-
--radius-md: calc(var(--radius) - 2px);
93-
--radius-lg: var(--radius);
94-
--radius-xl: calc(var(--radius) + 4px);
95-
}
96-
97-
* {
98-
border-color: var(--border);
99-
}
100-
101-
body {
102-
font-family: var(--font-sans);
103-
background: var(--background);
104-
color: var(--foreground);
105-
font-feature-settings: "ss01", "ss02", "cv01";
106-
}
107-
108-
/* Subtle grid background */
109-
.bg-grid {
110-
background-image:
111-
linear-gradient(to right, oklch(0.32 0.02 260 / 0.5) 1px, transparent 1px),
112-
linear-gradient(to bottom, oklch(0.32 0.02 260 / 0.5) 1px, transparent 1px);
113-
background-size: 24px 24px;
114-
}
115-
116-
.light .bg-grid {
117-
background-image:
118-
linear-gradient(to right, oklch(0.90 0.01 260 / 0.6) 1px, transparent 1px),
119-
linear-gradient(to bottom, oklch(0.90 0.01 260 / 0.6) 1px, transparent 1px);
120-
}
121-
122-
/* Glow effects */
123-
.glow-primary {
124-
box-shadow: 0 0 20px oklch(0.75 0.18 280 / 0.15), 0 0 40px oklch(0.75 0.18 280 / 0.05);
125-
}
126-
127-
.glow-sm {
128-
box-shadow: 0 0 10px oklch(0.75 0.18 280 / 0.1);
129-
}
130-
131-
/* Custom scrollbar */
132-
::-webkit-scrollbar { width: 6px; height: 6px; }
133-
::-webkit-scrollbar-track { background: transparent; }
134-
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
135-
::-webkit-scrollbar-thumb:hover { background: var(--muted-foreground); }
136-
137-
/* Selection */
138-
::selection {
139-
background: oklch(0.75 0.18 280 / 0.3);
140-
}
141-
142-
/* Smooth transitions */
143-
* {
144-
transition-property: color, background-color, border-color, box-shadow, opacity, transform;
145-
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
146-
transition-duration: 150ms;
147-
}
148-
149-
/* Focus states */
150-
:focus-visible {
151-
outline: 2px solid var(--ring);
152-
outline-offset: 2px;
153-
}
9+
@import "../../../../packages/ui/theme.css";
15410

15511
/* Shiki dual theme — dark by default, light when .light class */
15612
.astro-code {

packages/editor/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect, useMemo, useRef } from 'react';
2-
import { parseMarkdownToBlocks, exportAnnotations, exportLinkedDocAnnotations, exportEditorAnnotations, extractFrontmatter, Frontmatter } from '@plannotator/ui/utils/parser';
2+
import { parseMarkdownToBlocks, exportAnnotations, exportLinkedDocAnnotations, exportEditorAnnotations, extractFrontmatter, wrapFeedbackForAgent, Frontmatter } from '@plannotator/ui/utils/parser';
33
import { Viewer, ViewerHandle } from '@plannotator/ui/components/Viewer';
44
import { AnnotationPanel } from '@plannotator/ui/components/AnnotationPanel';
55
import { ExportModal } from '@plannotator/ui/components/ExportModal';
@@ -1041,7 +1041,7 @@ const App: React.FC = () => {
10411041
className="w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors flex items-center gap-2"
10421042
>
10431043
<svg className="w-3.5 h-3.5 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
1044-
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m4-4l-4 4m0 0l-4-4m4 4V4" />
1044+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4M10 17l5-5-5-5M15 12H3" />
10451045
</svg>
10461046
Import Review
10471047
</button>
@@ -1229,6 +1229,9 @@ const App: React.FC = () => {
12291229
editorAnnotations={editorAnnotations}
12301230
onDeleteEditorAnnotation={deleteEditorAnnotation}
12311231
onClose={() => setIsPanelOpen(false)}
1232+
onQuickCopy={async () => {
1233+
await navigator.clipboard.writeText(wrapFeedbackForAgent(annotationsOutput));
1234+
}}
12321235
/>
12331236
</div>
12341237

0 commit comments

Comments
 (0)