Files
tolaria/docs/THEMING.md

284 lines
8.8 KiB
Markdown
Raw Normal View History

# Theming
How the visual theme system works in Laputa.
## Overview
Laputa has two layers of theming:
1. **Global CSS variables** (`src/index.css`): App-wide colors, borders, backgrounds — used by all components via Tailwind and direct CSS.
2. **Editor theme** (`src/theme.json`): Editor-specific typography and styling — converted to CSS variables by `useEditorTheme` and applied to the BlockNote container.
Currently the app is **light mode only** (dark mode was removed for simplicity).
## Layer 1: Global CSS Variables
Defined in `src/index.css` under `:root`:
### Color Palette
```css
/* Primary brand */
--primary: #155DFF; /* Blue — links, accents, active states */
--primary-foreground: #FFFFFF;
/* Text hierarchy */
--text-primary: #37352F; /* Main text (Notion-like dark gray) */
--text-secondary: #787774; /* Secondary text */
--text-muted: #B4B4B4; /* Muted/placeholder text */
--text-heading: #37352F; /* Headings */
/* Backgrounds */
--bg-primary: #FFFFFF; /* Main content area */
--bg-sidebar: #F7F6F3; /* Sidebar background */
--bg-hover: #EBEBEA; /* Hover state */
--bg-hover-subtle: #F0F0EF; /* Subtle hover (code blocks) */
--bg-selected: #E8F4FE; /* Selected state (blue tint) */
/* Borders */
--border-primary: #E9E9E7; /* Standard borders */
/* Accent colors */
--accent-blue: #155DFF;
--accent-green: #00B38B;
--accent-orange: #D9730D;
--accent-red: #E03E3E;
--accent-purple: #A932FF;
--accent-yellow: #F0B100;
/* Light accent backgrounds (for badges/pills) */
--accent-blue-light: #155DFF14;
--accent-green-light: #00B38B14;
--accent-purple-light: #A932FF14;
--accent-red-light: #E03E3E14;
--accent-yellow-light: #F0B10014;
```
### shadcn/ui Variables
The app uses [shadcn/ui](https://ui.shadcn.com/) components, which require their own CSS variable naming convention:
```css
--background: #FFFFFF;
--foreground: #37352F;
--card: #FFFFFF;
--card-foreground: #37352F;
--primary: #155DFF;
--secondary: #EBEBEA;
--muted: #F0F0EF;
--muted-foreground: #787774;
--accent: #EBEBEA;
--destructive: #E03E3E;
--border: #E9E9E7;
--ring: #155DFF;
--sidebar: #F7F6F3;
```
### Tailwind v4 Integration
The `@theme inline` block in `index.css` bridges CSS variables to Tailwind's color system:
```css
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
/* ... etc */
}
```
This enables Tailwind classes like `bg-background`, `text-primary`, `border-border`, etc.
## Layer 2: Editor Theme (theme.json)
`src/theme.json` controls the BlockNote editor's typography and element styling. It's a nested JSON object with the following structure:
### Structure
```json
{
"editor": {
"fontFamily": "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
"fontSize": 16,
"lineHeight": 1.5,
"maxWidth": 720,
"paddingHorizontal": 40,
"paddingVertical": 20,
"paragraphSpacing": 8
},
"headings": {
"h1": { "fontSize": 32, "fontWeight": 700, "lineHeight": 1.2, "marginTop": 32, "marginBottom": 12, "color": "var(--text-heading)", "letterSpacing": -0.5 },
"h2": { "fontSize": 27, "fontWeight": 600, "lineHeight": 1.4, "marginTop": 28, "marginBottom": 10 },
"h3": { "fontSize": 20, "fontWeight": 600, "lineHeight": 1.4, "marginTop": 24, "marginBottom": 8 },
"h4": { "fontSize": 20, "fontWeight": 600, "lineHeight": 1.4, "marginTop": 20, "marginBottom": 6 }
},
"lists": {
"bulletSymbol": "\u2022",
"bulletSize": 28,
"bulletColor": "#177bfd",
"indentSize": 24,
"itemSpacing": 4,
"paddingLeft": 8,
"nestedBulletSymbols": ["\u2022", "\u25e6", "\u25aa"],
"bulletGap": 6
},
"checkboxes": {
"size": 18,
"borderRadius": 3,
"checkedColor": "var(--accent-blue)",
"uncheckedBorderColor": "var(--text-muted)",
"gap": 8
},
"inlineStyles": {
"bold": { "fontWeight": 700, "color": "var(--text-primary)" },
"italic": { "fontStyle": "italic" },
"strikethrough": { "color": "var(--text-tertiary)", "textDecoration": "line-through" },
"code": { "fontFamily": "'SF Mono', 'Fira Code', monospace", "fontSize": 14, "backgroundColor": "var(--bg-hover-subtle)", "paddingHorizontal": 4, "paddingVertical": 2, "borderRadius": 3 },
"link": { "color": "var(--accent-blue)", "textDecoration": "underline" },
"wikilink": { "color": "var(--accent-blue)", "textDecoration": "none", "borderBottom": "1px dotted var(--accent-blue)", "cursor": "pointer" }
},
"codeBlocks": {
"fontFamily": "'SF Mono', 'Fira Code', monospace",
"fontSize": 13,
"lineHeight": 1.5,
"backgroundColor": "var(--bg-card)",
"paddingHorizontal": 16,
"paddingVertical": 12,
"borderRadius": 6,
"marginVertical": 12
},
"blockquote": {
"borderLeftWidth": 3,
"borderLeftColor": "var(--accent-blue)",
"paddingLeft": 16,
"marginVertical": 12,
"color": "var(--text-secondary)",
"fontStyle": "italic"
},
"table": {
"borderColor": "var(--border-primary)",
"headerBackground": "var(--bg-card)",
"cellPaddingHorizontal": 12,
"cellPaddingVertical": 8,
"fontSize": 14
},
"horizontalRule": {
"color": "var(--border-primary)",
"marginVertical": 24,
"thickness": 1
},
"colors": {
"background": "var(--bg-primary)",
"text": "var(--text-primary)",
"textSecondary": "var(--text-secondary)",
"textMuted": "var(--text-muted)",
"heading": "var(--text-heading)",
"accent": "var(--accent-blue)",
"selection": "var(--bg-selected)",
"cursor": "var(--text-primary)"
}
}
```
### How theme.json Becomes CSS
The `useEditorTheme` hook (`src/hooks/useTheme.ts`) flattens the nested structure into CSS custom properties:
```typescript
function flattenTheme(obj, prefix = '--') {
// Recursively flattens:
// { editor: { fontSize: 16 } } → { '--editor-font-size': '16px' }
// { headings: { h1: { fontSize: 32 } } } → { '--headings-h1-font-size': '32px' }
}
```
Key transformations:
- **camelCase → kebab-case**: `fontSize``font-size`
- **Numeric values get `px`**: `16``16px`
- **Unitless exceptions**: `lineHeight`, `fontWeight`, `opacity` stay as plain numbers
- **String values pass through**: `"var(--text-heading)"``var(--text-heading)`
- **Arrays are skipped**: `nestedBulletSymbols` is ignored in CSS flattening
The resulting flat map is applied as inline styles on the BlockNote container:
```tsx
<div className="editor__blocknote-container" style={cssVars as React.CSSProperties}>
<BlockNoteView editor={editor} theme="light" />
</div>
```
### EditorTheme.css
`src/components/EditorTheme.css` contains CSS selectors that consume these variables to style BlockNote's internal elements (headings, lists, code blocks, etc.). This file uses selectors like `.bn-editor [data-content-type="heading"]` to target BlockNote's rendered DOM.
## How to Modify Styles
### Change a global color
Edit `src/index.css` — find the variable under `:root` and change its value:
```css
/* Before */
--primary: #155DFF;
/* After */
--primary: #FF5500;
```
All components using `text-primary`, `bg-primary`, `var(--primary)`, etc. will update automatically.
### Change editor typography
Edit `src/theme.json`:
```json
{
"editor": {
"fontSize": 18, // was 16
"fontFamily": "'Georgia', serif" // was Inter
}
}
```
The `useEditorTheme` hook will automatically regenerate the CSS variables on next render.
### Change heading sizes
Edit the `headings` section in `src/theme.json`:
```json
{
"headings": {
"h1": { "fontSize": 36 }, // was 32
"h2": { "fontSize": 28 } // was 27
}
}
```
### Change wikilink appearance
Edit `inlineStyles.wikilink` in `src/theme.json`:
```json
{
"inlineStyles": {
"wikilink": {
"color": "var(--accent-purple)",
"borderBottom": "2px solid var(--accent-purple)"
}
}
}
```
### Add a new CSS variable
1. Add it to `:root` in `src/index.css`
2. If it needs to be available in Tailwind, add a mapping in the `@theme inline` block
3. Reference it anywhere as `var(--my-variable)` in CSS or `theme.json`
## Design Decisions
- **CSS variables over Tailwind config**: Colors are defined as CSS variables rather than Tailwind config values, because they need to be shared with non-Tailwind code (BlockNote, inline styles, theme.json).
- **theme.json for editor only**: The editor theme is separate from global styles because it controls BlockNote-specific typography that doesn't apply to the rest of the app (sidebar, dialogs, etc.).
- **No dark mode (for now)**: Dark mode was removed to simplify the initial build. The CSS variable architecture supports it — just add a `[data-theme="dark"]` or `@media (prefers-color-scheme: dark)` section in `index.css`.
- **Inline styles for editor**: The editor theme is applied via inline styles (not a stylesheet) because the values come from JSON and are computed at runtime by the hook.