Files
grimoire/src/index.css
Alejandro f464c68bde feat: theme selector (#95)
* feat: Add reusable theme system with Plan 9 proof of concept

Implement a comprehensive theme system that:
- Defines typed Theme interface with colors, syntax highlighting, scrollbar, and gradient variables
- Creates ThemeProvider with React context for runtime theme switching
- Persists theme selection to localStorage
- Includes 3 built-in themes: dark (default), light, and plan9

Theme structure supports:
- Core UI colors (background, foreground, primary, secondary, accent, etc.)
- Status colors (success, warning, info) replacing hardcoded Tailwind colors
- Syntax highlighting variables for code blocks
- Diff highlighting colors (inserted, deleted, meta)
- Scrollbar styling variables
- Gradient colors for branding

Technical changes:
- Update CSS to use new theme variables throughout
- Update prism-theme.css to use syntax variables instead of hardcoded values
- Remove chart colors (unused)
- Add success/warning/info to tailwind.config.js
- Wire up ThemeProvider in main.tsx

For Nostr publishing (future):
- d tag: "grimoire-theme"
- name tag: theme display name

* feat: Add theme selector to user menu, remove configurable border radius

- Remove border radius from theme configuration (borders are always square)
- Add theme selector dropdown to user menu (available to all users)
- Theme selector shows active theme indicator
- Theme selection persists via localStorage

* fix: Improve theme contrast and persistence

- Fix theme persistence: properly check localStorage before using default
- Plan9: make blue subtler (reduce saturation), darken gradient colors
  for better contrast on pale yellow background
- Light theme: improve contrast with darker muted foreground and borders
- Change theme selector from flat list to dropdown submenu

* fix: Replace Plan9 yellow accent with purple, add zap/live theme colors

- Replace Plan9's bright yellow accent with purple (good contrast on pale yellow)
- Add zap and live colors to theme system (used by ZapReceiptRenderer, StatusBadge)
- Make light theme gradient orange darker for better contrast
- Update ZapReceiptRenderer to use theme zap color instead of hardcoded yellow-500
- Update StatusBadge to use theme live color instead of hardcoded red-600
- Add CSS variables and Tailwind utilities for zap/live colors

* fix: Make gradient orange darker, theme status colors

- Make gradient orange darker in light and plan9 themes for better contrast
- Make req viewer status colors themeable:
  - loading/connecting → text-warning
  - live/receiving → text-success
  - error/failed → text-destructive
  - eose → text-info
- Update relay status icons to use theme colors
- Update tests to expect theme color classes

* fix: Use themeable zap color for active user names

- Replace hardcoded text-orange-400 with text-zap in UserName component
- Replace hardcoded text-orange-400 with text-zap in SpellRenderer ($me placeholder)
- Now uses dark amber/gold with proper contrast on light/plan9 themes

* feat: Add highlight theme color for active user display

Add dedicated 'highlight' color to theme system for displaying the
logged-in user's name, replacing the use of 'zap' color which felt
semantically incorrect. The highlight color is optimized for contrast
on each theme's background.

- Add highlight to ThemeColors interface and apply.ts
- Add --highlight CSS variable to index.css (light and dark)
- Add highlight to tailwind.config.js
- Configure appropriate highlight values for dark, light, and plan9 themes
- Update UserName.tsx to use text-highlight for active account
- Update SpellRenderer.tsx MePlaceholder to use text-highlight

* fix: Restore original orange-400 highlight color for dark theme

Update dark theme highlight to match original text-orange-400 color
(27 96% 61%) for backward compatibility with existing appearance.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 19:24:37 +01:00

416 lines
10 KiB
CSS

/* Prism syntax highlighting theme */
@import "./styles/prism-theme.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom scrollbar styling - uses theme variables */
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--scrollbar-thumb)) hsl(var(--scrollbar-track));
}
*::-webkit-scrollbar {
width: 8px;
height: 8px;
}
*::-webkit-scrollbar-track {
background: hsl(var(--scrollbar-track));
}
*::-webkit-scrollbar-thumb {
background-color: hsl(var(--scrollbar-thumb));
border-radius: 4px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--scrollbar-thumb-hover));
}
@layer base {
:root {
/* Core colors - light theme defaults (overridden by ThemeProvider) */
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
/* Status colors */
--success: 142 76% 36%;
--warning: 45 93% 47%;
--info: 199 89% 48%;
/* Nostr-specific colors */
--zap: 45 93% 40%;
--live: 0 72% 45%;
/* UI highlight (active user, self-references) */
--highlight: 25 90% 35%;
/* Syntax highlighting */
--syntax-comment: 215.4 16.3% 46.9%;
--syntax-punctuation: 222.2 84% 30%;
--syntax-property: 222.2 47.4% 11.2%;
--syntax-string: 142 60% 30%;
--syntax-keyword: 270 80% 50%;
--syntax-function: 222.2 47.4% 11.2%;
--syntax-variable: 222.2 84% 4.9%;
--syntax-operator: 222.2 84% 20%;
/* Diff colors */
--diff-inserted: 142 60% 30%;
--diff-inserted-bg: 142 60% 50% / 0.15;
--diff-deleted: 0 70% 45%;
--diff-deleted-bg: 0 70% 50% / 0.15;
--diff-meta: 199 80% 40%;
--diff-meta-bg: 199 80% 50% / 0.1;
/* Scrollbar */
--scrollbar-thumb: 222.2 84% 4.9% / 0.2;
--scrollbar-thumb-hover: 222.2 84% 4.9% / 0.3;
--scrollbar-track: 0 0% 0% / 0;
/* Gradient colors (RGB values) */
--gradient-1: 234 179 8;
--gradient-2: 249 115 22;
--gradient-3: 147 51 234;
--gradient-4: 6 182 212;
}
/* Dark theme - applied via .dark class or ThemeProvider */
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 70%;
--accent: 270 100% 70%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
/* Status colors */
--success: 142 76% 36%;
--warning: 45 93% 47%;
--info: 199 89% 48%;
/* Nostr-specific colors */
--zap: 45 93% 58%;
--live: 0 72% 51%;
/* UI highlight (active user, self-references) */
--highlight: 27 96% 61%;
/* Syntax highlighting */
--syntax-comment: 215 20.2% 70%;
--syntax-punctuation: 210 40% 70%;
--syntax-property: 210 40% 98%;
--syntax-string: 215 20.2% 70%;
--syntax-keyword: 210 40% 98%;
--syntax-function: 210 40% 98%;
--syntax-variable: 210 40% 98%;
--syntax-operator: 210 40% 98%;
/* Diff colors */
--diff-inserted: 134 60% 76%;
--diff-inserted-bg: 145 63% 42% / 0.1;
--diff-deleted: 0 100% 76%;
--diff-deleted-bg: 0 100% 60% / 0.1;
--diff-meta: 190 77% 70%;
--diff-meta-bg: 190 77% 70% / 0.08;
/* Scrollbar */
--scrollbar-thumb: 0 0% 100% / 0.2;
--scrollbar-thumb-hover: 0 0% 100% / 0.3;
--scrollbar-track: 0 0% 0% / 0;
/* Gradient colors (RGB values) */
--gradient-1: 250 204 21;
--gradient-2: 251 146 60;
--gradient-3: 168 85 247;
--gradient-4: 34 211 238;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-mono;
}
}
@layer utilities {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.text-grimoire-gradient {
background: linear-gradient(
to bottom,
rgb(var(--gradient-1)),
rgb(var(--gradient-2)),
rgb(var(--gradient-3)),
rgb(var(--gradient-4))
);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
/* react-medium-image-zoom theme customization - uses background with opacity */
[data-rmiz-modal-overlay] {
background-color: hsl(var(--background) / 0.92) !important;
}
[data-rmiz-modal-content] {
box-shadow: 0 0 40px hsl(var(--foreground) / 0.2);
}
/* React Mosaic Theme Customization */
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme {
background: hsl(var(--background));
}
/* Smooth animations for window resizing and repositioning */
/* Only animate during preset application, not manual resize/drag */
body.animating-layout
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-tile {
transition:
width 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
height 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
top 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
left 150ms cubic-bezier(0.25, 0.1, 0.25, 1);
contain: layout; /* Isolate layout calculations for better performance */
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window
.mosaic-window-toolbar {
background: hsl(var(--background));
border: none;
border-bottom: 1px solid hsl(var(--border));
border-radius: 0;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window
.mosaic-window-title {
color: hsl(var(--foreground));
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window {
background: hsl(var(--background));
outline: none;
border-radius: 0 !important;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window * {
border-radius: 0 !important;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window::before,
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window::after {
display: none;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme.bp4-dark .mosaic-window,
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme.bp4-dark .mosaic-preview {
box-shadow: none;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window
.mosaic-window-toolbar {
background: hsl(var(--muted));
border: none;
border-bottom: 1px solid hsl(var(--border));
color: hsl(var(--foreground));
height: 30px;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window
.mosaic-window-title {
color: hsl(var(--foreground));
font-family: inherit;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window
.mosaic-window-body {
background: hsl(var(--background));
color: hsl(var(--foreground));
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-window-controls {
color: hsl(var(--muted-foreground));
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window-controls:hover {
color: hsl(var(--foreground));
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window-toolbar
.separator {
border-left: 1px solid hsl(var(--border));
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme
.mosaic-window-body-overlay {
background: hsl(var(--background));
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-preview {
background: hsl(var(--accent) / 0.3);
border: 2px solid hsl(var(--primary));
outline: none;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-drop-target {
border: 2px solid var(--border);
outline: none;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split:hover {
background: hsl(var(--primary));
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-row {
width: 4px;
margin: 0 -2px;
}
.mosaic.mosaic-blueprint-theme.mosaic-blueprint-theme .mosaic-split.-column {
height: 4px;
margin: -2px 0;
}
/* Accessibility: Focus indicators for keyboard navigation */
@layer base {
/* Focus-visible for buttons and interactive elements */
button:focus-visible,
a:focus-visible,
[role="button"]:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
/* Focus-visible for input elements */
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 0;
}
/* Focus-visible for command launcher items */
[cmdk-item]:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: -2px;
}
/* Focus-visible for tab buttons */
.tabbar-button:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
}
/* TipTap Editor Styles */
.ProseMirror {
min-height: 1.25rem;
line-height: 1.25rem;
position: relative;
}
.ProseMirror:focus {
outline: none;
}
.ProseMirror p {
margin: 0;
line-height: 1.25rem;
}
.ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: hsl(var(--muted-foreground));
pointer-events: none;
position: absolute;
left: 0;
top: 0;
}
/* Mention styles */
.ProseMirror .mention {
color: hsl(var(--primary));
background-color: hsl(var(--primary) / 0.1);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
text-decoration: none;
cursor: pointer;
font-weight: 500;
}
.ProseMirror .mention:hover {
background-color: hsl(var(--primary) / 0.2);
}
/* Emoji styles */
.ProseMirror .emoji-node {
display: inline-flex;
align-items: center;
vertical-align: middle;
}
.ProseMirror .emoji-image {
height: 1.2em;
width: auto;
vertical-align: middle;
object-fit: contain;
}
.ProseMirror .emoji-unicode {
font-size: 1.1em;
line-height: 1;
vertical-align: middle;
}