Files
tolaria/docs/THEMING.md
lucaronin e7c4563200 feat: add comprehensive documentation and CI/CD pipeline
- Add docs/ with ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED, THEMING
- Add GitHub Actions workflow with automated quality checks:
  • Tests (frontend + Rust)
  • Coverage (70% threshold)
  • Code Health (CodeScene delta analysis)
  • Documentation check (warning only)
  • Lint & Format (ESLint + Clippy + rustfmt)
- Configure coverage in vite.config.ts (v8 provider, 70% threshold)
- Add test:coverage script to package.json
- Install @vitest/coverage-v8
- Update CLAUDE.md with docs-update requirement
- Add CI/CD setup guides in .github/
2026-02-17 13:10:43 +01:00

8.8 KiB

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

/* 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 components, which require their own CSS variable naming convention:

--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:

@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

{
  "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:

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: fontSizefont-size
  • Numeric values get px: 1616px
  • 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:

<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:

/* 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:

{
  "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:

{
  "headings": {
    "h1": { "fontSize": 36 },  // was 32
    "h2": { "fontSize": 28 }   // was 27
  }
}

Edit inlineStyles.wikilink in src/theme.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.