mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
e1bd27aae4
* 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>
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
/**
|
|
* Converts FinSitter theme CSS files into Plannotator theme format.
|
|
*
|
|
* FinSitter: .theme-gruvbox-dark { ... } and .theme-gruvbox-light { ... } as separate files
|
|
* Plannotator: .theme-gruvbox { ... (dark) } and .theme-gruvbox.light { ... } in one file
|
|
*
|
|
* Also injects defaults for Plannotator-specific tokens (success, warning, code-bg, focus-highlight).
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
|
|
const FINSITTER_DIR = '/Users/ramos/mrxtek/finsitter/reactronite/src/app/styles/themes';
|
|
const OUTPUT_DIR = join(import.meta.dir, '../packages/ui/themes');
|
|
|
|
// Themes to include, grouped by merged name
|
|
const THEME_MAP: Record<string, { dark?: string; light?: string }> = {
|
|
'adwaita': { dark: 'adwaita-dark.css', light: 'adwaita.css' },
|
|
'bold-tech': { dark: 'bold-tech-dark.css', light: 'bold-tech-light.css' },
|
|
'caffeine': { dark: 'caffine-dark.css', light: 'caffine-light.css' },
|
|
'cobalt2': { dark: 'cobalt2.css' },
|
|
'cyberdyne': { dark: 'cyberdyne.css' },
|
|
'cyberfunk': { dark: 'cyberfunk-dark.css', light: 'cyberfunk-light.css' },
|
|
'doom-64': { dark: 'doom-64-dark.css', light: 'doom-64-light.css' },
|
|
'dracula': { dark: 'dracula.css' },
|
|
'gruvbox': { dark: 'gruvbox-dark.css', light: 'gruvbox-light.css' },
|
|
'ir-black': { dark: 'ir-black.css' },
|
|
'nord': { dark: 'nord.css' },
|
|
'paulmillr': { dark: 'paulmillr.css' },
|
|
'quantum-rose': { dark: 'quantum-rose-dark.css', light: 'quantum-rose-light.css' },
|
|
'solarized': { dark: 'solarized-dark.css', light: 'solarized-light.css' },
|
|
'solar-dusk': { dark: 'solar-dusk-dark.css', light: 'solar-dusk-light.css' },
|
|
'terminal': { dark: 'terminal.css' },
|
|
'tinacious': { light: 'tinacious-light.css' },
|
|
};
|
|
|
|
// Plannotator-specific token defaults
|
|
const PLANNOTATOR_DEFAULTS_DARK = `
|
|
--success: oklch(0.72 0.17 150);
|
|
--success-foreground: oklch(0.15 0.02 260);
|
|
--warning: oklch(0.75 0.15 85);
|
|
--warning-foreground: oklch(0.20 0.02 260);
|
|
--code-bg: oklch(0.13 0.015 260);
|
|
--focus-highlight: oklch(0.70 0.20 200);`;
|
|
|
|
const PLANNOTATOR_DEFAULTS_LIGHT = `
|
|
--success: oklch(0.45 0.20 150);
|
|
--success-foreground: oklch(1 0 0);
|
|
--warning: oklch(0.55 0.18 85);
|
|
--warning-foreground: oklch(0.18 0.02 260);
|
|
--code-bg: oklch(0.96 0.005 260);
|
|
--focus-highlight: oklch(0.70 0.20 200);`;
|
|
|
|
function extractTokens(css: string): string {
|
|
// Extract just the CSS variable declarations from inside the selector block
|
|
const match = css.match(/\{([\s\S]*)\}/);
|
|
if (!match) return '';
|
|
return match[1].trim();
|
|
}
|
|
|
|
function hasToken(tokens: string, name: string): boolean {
|
|
return tokens.includes(`--${name}:`);
|
|
}
|
|
|
|
function addMissingDefaults(tokens: string, defaults: string): string {
|
|
const lines = defaults.trim().split('\n');
|
|
const missing: string[] = [];
|
|
for (const line of lines) {
|
|
const match = line.match(/--([^:]+):/);
|
|
if (match && !hasToken(tokens, match[1].trim())) {
|
|
missing.push(line);
|
|
}
|
|
}
|
|
if (missing.length === 0) return tokens;
|
|
return tokens + '\n\n /* Plannotator extended tokens */' + missing.join('');
|
|
}
|
|
|
|
function convertTheme(name: string, config: { dark?: string; light?: string }): string {
|
|
const lines: string[] = [];
|
|
|
|
if (config.dark) {
|
|
const darkCss = readFileSync(join(FINSITTER_DIR, config.dark), 'utf-8');
|
|
let darkTokens = extractTokens(darkCss);
|
|
darkTokens = addMissingDefaults(darkTokens, PLANNOTATOR_DEFAULTS_DARK);
|
|
lines.push(`.theme-${name} {`);
|
|
lines.push(` ${darkTokens}`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
if (config.light) {
|
|
const lightCss = readFileSync(join(FINSITTER_DIR, config.light), 'utf-8');
|
|
let lightTokens = extractTokens(lightCss);
|
|
lightTokens = addMissingDefaults(lightTokens, PLANNOTATOR_DEFAULTS_LIGHT);
|
|
|
|
if (config.dark) {
|
|
lines.push('');
|
|
lines.push(`.theme-${name}.light {`);
|
|
} else {
|
|
// Light-only theme: light tokens go into .light, generate dark from light with note
|
|
lines.push(`/* Dark mode: uses light colors as base (light-only source theme) */`);
|
|
lines.push(`.theme-${name} {`);
|
|
lines.push(` ${lightTokens}`);
|
|
lines.push(`}`);
|
|
lines.push('');
|
|
lines.push(`.theme-${name}.light {`);
|
|
}
|
|
lines.push(` ${lightTokens}`);
|
|
lines.push(`}`);
|
|
} else if (config.dark) {
|
|
// Dark-only: duplicate dark tokens for light with a note
|
|
const darkCss = readFileSync(join(FINSITTER_DIR, config.dark), 'utf-8');
|
|
let darkTokens = extractTokens(darkCss);
|
|
darkTokens = addMissingDefaults(darkTokens, PLANNOTATOR_DEFAULTS_LIGHT);
|
|
lines.push('');
|
|
lines.push(`/* Light mode: uses dark colors as base (dark-only source theme) */`);
|
|
lines.push(`.theme-${name}.light {`);
|
|
lines.push(` ${darkTokens}`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
// Convert all themes
|
|
for (const [name, config] of Object.entries(THEME_MAP)) {
|
|
const output = convertTheme(name, config);
|
|
const outPath = join(OUTPUT_DIR, `${name}.css`);
|
|
writeFileSync(outPath, output + '\n');
|
|
console.log(` Converted: ${name}.css`);
|
|
}
|
|
|
|
console.log(`\nDone! ${Object.keys(THEME_MAP).length} themes converted.`);
|