docs: audit ARCHITECTURE.md, ABSTRACTIONS.md, GETTING-STARTED.md — remove stale theme/tab/favorites refs

- Remove Theme System sections (removed in ADR-0013)
- Delete stale THEMING.md (283 lines documenting removed system)
- Remove Favorites from sidebar (removed in codebase)
- Remove Closed Tab History section (single note model, ADR-0003)
- Fix commands.rs → commands/ (split into modules)
- Remove stale config/relations.md and config/semantic-properties.md refs
- Fix protected folders list (remove _themes/, theme/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-28 11:03:01 +01:00
parent 5c0e7b2987
commit 1ba74d4b1d
4 changed files with 41 additions and 399 deletions

View File

@@ -14,7 +14,7 @@ These frontmatter field names have special meaning in Laputa's UI:
| Field | Meaning | UI behavior |
|---|---|---|
| `title:` | Human-readable title (synced with filename) | Tab label, breadcrumb, sidebar. Filename = `slugify(title).md` |
| `title:` | Human-readable title (synced with filename) | Breadcrumb, sidebar. Filename = `slugify(title).md` |
| `type:` | Entity type (Project, Person, Quarter…) | Type chip in note list + sidebar grouping |
| `status:` | Lifecycle stage (active, done, blocked…) | Colored chip in note list + editor header |
| `url:` | External link | Clickable link chip in editor header |
@@ -25,7 +25,7 @@ These frontmatter field names have special meaning in Laputa's UI:
| `Belongs to:` | Parent relationship | Relationship chip in Properties panel |
| `Related to:` | Lateral relationship | Relationship chip in Properties panel |
The list of default-shown relationships and semantic property rendering rules can be customized via `config/relations.md` and `config/semantic-properties.md` in the vault.
Relationship fields are detected dynamically — any frontmatter field containing `[[wikilink]]` values is treated as a relationship (see [ADR-0010](adr/0010-dynamic-wikilink-relationship-detection.md)).
### System Properties (underscore convention)
@@ -72,7 +72,6 @@ classDiagram
+Record~string,string[]~ relationships
+String[] outgoingLinks
+String? status
+String? owner
+Number? modifiedAt
+Number? createdAt
+Number wordCount
@@ -121,9 +120,8 @@ interface VaultEntry {
relationships: Record<string, string[]> // All frontmatter fields containing wikilinks
outgoingLinks: string[] // All [[wikilinks]] found in note body
status: string | null // Active, Done, Paused, Archived, Dropped
owner: string | null // Person responsible
cadence: string | null // Update frequency: Weekly, Monthly, etc.
modifiedAt: number | null // Unix timestamp (seconds)
// Note: owner and cadence are now in the generic `properties` map
createdAt: number | null // Unix timestamp (seconds)
fileSize: number
wordCount: number | null // Body word count (excludes frontmatter)
@@ -149,8 +147,7 @@ Type is determined **purely** from the `type:` frontmatter field — it is never
├── some-topic.md ← type: Topic
├── ...
├── type/ ← type definition documents
── config/ ← meta-configuration files (agents.md, etc.)
└── theme/ ← vault-based themes (legacy location)
── config/ ← meta-configuration files (agents.md, etc.)
```
New notes are created at the vault root: `{vault}/{slug}.md`. Changing a note's type only requires updating the `type:` field in frontmatter — the file does not move. The `type/` folder exists solely for type definition documents, and `config/` for configuration files.
@@ -193,10 +190,8 @@ Standard YAML frontmatter between `---` delimiters:
```yaml
---
title: Write Weekly Essays
is_a: Procedure
type: Procedure
status: Active
owner: Luca Rossi
cadence: Weekly
belongs_to:
- "[[grow-newsletter]]"
related_to:
@@ -270,7 +265,7 @@ type SidebarSelection =
1. Validates the path exists and is a directory
2. Scans root-level `.md` files (non-recursive)
3. Recursively scans protected folders: `type/`, `config/`, `attachments/`, `_themes/`, `theme/`
3. Recursively scans protected folders: `type/`, `config/`, `attachments/`
4. Files in non-protected subfolders are **not indexed** (flat vault enforcement)
5. For each `.md` file, calls `parse_md_file()`:
- Reads content with `fs::read_to_string()`
@@ -373,7 +368,7 @@ interface PulseCommit {
### Frontend Integration
- **Modified file badges**: Orange dots in sidebar and tab bar
- **Modified file badges**: Orange dots in sidebar
- **Diff view**: Toggle in breadcrumb bar → shows unified diff
- **Git history**: Shown in Inspector panel for active note
- **Commit dialog**: Triggered from sidebar or Cmd+K
@@ -443,58 +438,13 @@ Wikilink resolution (`resolveEntry` in `src/utils/wikilink.ts`) uses multi-pass
Toggle via Cmd+K → "Raw Editor" or breadcrumb bar button. Uses CodeMirror 6 (`useCodeMirror` hook) to edit the raw markdown + frontmatter directly. Changes saved via the same `save_note_content` command.
## Theme System
## Styling
See [THEMING.md](./THEMING.md) for the full theme system documentation.
The app uses a single light theme — the vault-based theming system was removed (see [ADR-0013](adr/0013-remove-theming-system.md)). Styling is defined in two layers:
### Overview
Two-layer theming:
1. **Global CSS variables** (`src/index.css`): App-wide colors via `:root`, bridged to Tailwind v4
2. **Editor theme** (`src/theme.json`): BlockNote typography, flattened to CSS vars by `useEditorTheme`
### Vault-Based Themes
Themes are markdown notes in `theme/` with `type: Theme` frontmatter. Each property becomes a CSS variable with `--` prefix.
```yaml
---
type: Theme
Description: Light theme with warm, paper-like tones
background: "#FFFFFF"
foreground: "#37352F"
accent-blue: "#155DFF"
editor-font-size: 16
editor-line-height: 1.5
---
```
### ThemeManager
`useThemeManager` hook manages the theme lifecycle:
```typescript
interface ThemeManager {
themes: ThemeFile[]
activeThemeId: string | null
activeTheme: ThemeFile | null
isDark: boolean
switchTheme(themeId: string): Promise<void>
createTheme(name?: string): Promise<string>
reloadThemes(): Promise<void>
updateThemeProperty(key: string, value: string): Promise<void>
}
```
- Detects dark backgrounds via luminance calculation → sets `color-scheme` and `data-theme-mode`
- Live preview: re-applies when active theme note is saved
- Three built-in themes: Default (light), Dark (deep navy), Minimal (high contrast)
- Legacy JSON themes (`_themes/*.json`) supported for backward compatibility
### Theme Property Editor
`ThemePropertyEditor` component provides an interactive UI for editing theme properties. Uses `themeSchema.ts` to determine input types (color picker, number slider, text field) based on property names and values.
## Inspector Abstraction
The Inspector panel (`src/components/Inspector.tsx`) is composed of sub-panels:
@@ -510,10 +460,6 @@ The Inspector panel (`src/components/Inspector.tsx`) is composed of sub-panels:
4. **GitHistoryPanel**: Shows recent commits from file history with relative timestamps.
## Closed Tab History
`useClosedTabHistory` hook (`src/hooks/useClosedTabHistory.ts`) provides a LIFO stack for closed tab entries, used by `useTabManagement` to support Cmd+Shift+T reopen. Each entry stores the note's path, tab index, and full `VaultEntry`. The stack is in-memory only (resets on restart), capped at 20 entries, and deduplicates by path.
## Search
### Search

View File

@@ -114,11 +114,11 @@ flowchart TD
WS["WelcomeScreen\n(onboarding)"]
SB["Sidebar\n(navigation + filters + types)"]
NL["NoteList / PulseView\n(filtered list / activity)"]
ED["Editor\n(BlockNote + tabs + diff + raw)"]
ED["Editor\n(BlockNote + diff + raw)"]
IN["Inspector\n(metadata + relationships)"]
AIC["AIChatPanel\n(API-based chat)"]
AIP["AiPanel\n(Claude CLI agent + tools)"]
SP["SearchPanel\n(keyword/semantic/hybrid)"]
SP["SearchPanel\n(keyword search)"]
ST["StatusBar\n(vault picker + sync + version)"]
CP["CommandPalette\n(Cmd+K launcher)"]
@@ -132,7 +132,7 @@ flowchart TD
FM["frontmatter/"]
GIT["git/"]
GH["github/"]
THEME["theme/"]
SETTINGS["settings.rs"]
SEARCH["search.rs"]
CLI["claude_cli.rs"]
end
@@ -160,10 +160,10 @@ flowchart TD
│Sidebar │ Note List │ Editor │ Inspector │
│(250px) │ (300px) │ (flex-1) │ (280px) │
│ │ OR │ │ OR │
│ All │ Pulse View │ [Tab Bar] │ AI Chat │
Favs │ │ [Breadcrumb Bar] │ OR │
Changes│ [Search] │ │ AI Agent │
Pulse │ [Sort/Filt] │ # My Note │ │
│ All │ Pulse View │ [Breadcrumb Bar] │ AI Chat │
Changes│ │ │ OR │
Pulse │ [Search] │ # My Note │ AI Agent │
Inbox │ [Sort/Filt] │ │ │
│ │ │ │ Context │
│Projects│ Note 1 │ Content here... │ Messages │
│Experim.│ Note 2 │ (BlockNote or Raw) │ Actions │
@@ -176,9 +176,9 @@ flowchart TD
└──────────────────────────────────────────────────────────────┘
```
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Favorites, Changes, Pulse) and collapsible type-based section groups. Each type can have a custom icon, color, sort, and visibility set via its type document in `type/`.
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse) and collapsible type-based section groups. Each type can have a custom icon, color, sort, and visibility set via its type document in `type/`.
- **Note List / Pulse View** (200-500px, resizable): When a section group or filter is selected, shows filtered notes with snippets, modified dates, and status indicators. When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
- **Editor** (flex, fills remaining space): Tab bar with modified dots, breadcrumb bar with word count, BlockNote rich text editor with wikilink support. Can toggle to diff view (modified files) or raw CodeMirror view. Decomposed into `Editor` (orchestrator), `EditorContent`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, `useEditorSave`, `useRawMode`.
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count, BlockNote rich text editor with wikilink support. Can toggle to diff view (modified files) or raw CodeMirror view. Decomposed into `Editor` (orchestrator), `EditorContent`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, `useEditorSave`, `useRawMode`. Navigation history (Cmd+[/]) replaces tabs.
- **Inspector / AI Chat / AI Agent** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, instances, backlinks, git history), AI Chat panel (API-based), and AI Agent panel (Claude CLI subprocess with tool execution). The Sparkle icon in the breadcrumb bar toggles between them. When viewing a Type note, the Inspector shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
Panels are separated by `ResizeHandle` components that support drag-to-resize.
@@ -413,24 +413,13 @@ flowchart TD
G --> H([VaultEntry list ready])
```
## Theme System
## Styling
See [THEMING.md](./THEMING.md) for the full theme system documentation.
### Two-Layer Architecture
The app uses a single light theme with no user-configurable theming (see [ADR-0013](adr/0013-remove-theming-system.md)).
1. **Global CSS variables** (`src/index.css`): App-wide colors, borders, backgrounds. Bridged to Tailwind v4 via `@theme inline`.
2. **Editor theme** (`src/theme.json`): BlockNote-specific typography. Flattened to CSS vars by `useEditorTheme`.
### Vault-Based Themes
Themes are markdown notes in the `theme/` folder with `type: Theme` frontmatter (`Is A: Theme` accepted as legacy alias). Each frontmatter property becomes a CSS variable. Managed by `useThemeManager` hook and the `src-tauri/src/theme/` Rust module (create, seed, defaults).
- **Vault settings**: `.laputa/settings.json` stores the active theme reference
- **Legacy support**: `_themes/*.json` files still supported for backward compatibility
- **Built-in themes**: Default (light), Dark, Minimal — auto-seeded on vault open
- **Live preview**: Re-applies when the active theme note is saved
## Vault Management
### Vault List
@@ -444,14 +433,14 @@ Persisted at `~/.config/com.laputa.app/vaults.json`:
}
```
Managed by `useVaultSwitcher` hook. Switching vaults closes all tabs and resets sidebar.
Managed by `useVaultSwitcher` hook. Switching vaults resets sidebar and clears the active note.
### Vault Config
Per-vault UI settings stored in `ui.config.md` at vault root (YAML frontmatter in a markdown note):
- `zoom`: Float zoom level (0.81.5)
- `view_mode`: "all" | "editor-list" | "editor-only"
- `editor_mode`: "raw" | "preview" (persists across tab switches and sessions)
- `editor_mode`: "raw" | "preview" (persists across note switches and sessions)
- `tag_colors`, `status_colors`: Custom color overrides
- `property_display_modes`: Property display preferences
@@ -501,7 +490,7 @@ sequenceDiagram
participant MCP as MCP Server
participant U as User
T->>T: run_startup_tasks()<br/>(purge trash, seed themes, register MCP)
T->>T: run_startup_tasks()<br/>(purge trash, register MCP)
T->>MCP: spawn_ws_bridge() — ports 9710 + 9711
T->>A: App mounts
@@ -514,7 +503,6 @@ sequenceDiagram
T-->>VL: VaultEntry[]
VL->>T: invoke('get_modified_files')
VL->>T: useMcpStatus — register if needed
VL->>T: useThemeManager — load active theme
VL-->>A: entries ready
end
@@ -599,7 +587,6 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `frontmatter/` | YAML frontmatter read/write (`mod.rs`, `yaml.rs`, `ops.rs`) |
| `git/` | Git operations (`commit.rs`, `status.rs`, `history.rs`, `conflict.rs`, `remote.rs`, `pulse.rs`) |
| `github/` | GitHub OAuth + API (`auth.rs`, `api.rs`, `clone.rs`) |
| `theme/` | Theme management (`mod.rs`, `create.rs`, `defaults.rs`, `seed.rs`) |
| `search.rs` | Keyword search — walkdir-based vault file scan |
| `claude_cli.rs` | Claude CLI subprocess spawning + NDJSON stream parsing |
| `ai_chat.rs` | Direct Anthropic API client (non-streaming, for Tauri builds) |
@@ -740,12 +727,12 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
| `App.tsx` | `selection`, panel widths, dialog visibility, toast, view mode | UI state |
| `useVaultLoader` | `entries`, `allContent`, `modifiedFiles` | Vault data |
| `useNoteActions` | `tabs`, `activeTabPath` | Composes `useNoteCreation` + `useNoteRename` + `frontmatterOps` |
| `useNoteCreation` | — (delegates to `useTabManagement`) | Note/type/daily-note creation with optimistic persistence |
| `useNoteRename` | — (delegates to `useTabManagement`) | Note renaming with wikilink update |
| `useNoteCreation` | — | Note/type/daily-note creation with optimistic persistence |
| `useNoteRename` | — | Note renaming with wikilink update |
| `frontmatterOps` | — (pure functions) | Frontmatter CRUD: key→VaultEntry mapping, mock/Tauri dispatch |
| `useTabManagement` | Tab ordering, pinning, swapping, closed-tab history | Tab lifecycle |
| `useTabManagement` | Navigation history, note switching | Note navigation lifecycle |
| `useVaultSwitcher` | `vaultPath`, `extraVaults` | Vault switching |
| `useThemeManager` | `themes`, `activeThemeId`, `isDark` | Theme state |
| `useTheme` | Editor theme CSS vars | Editor typography theme |
| `useAIChat` | `messages`, `isStreaming` | AI chat conversation |
| `useAiAgent` | `messages`, `status`, tool actions | AI agent conversation |
| `useAutoSync` | Sync interval, pull/push state | Git auto-sync |
@@ -763,8 +750,7 @@ Data flows unidirectionally: `App` passes data and callbacks as props to child c
| Cmd+P | Open quick open palette |
| Cmd+N | Create new note |
| Cmd+S | Save current note |
| Cmd+W | Close active tab |
| Cmd+Shift+T | Reopen last closed tab (LIFO history, up to 20) |
| Cmd+[ / Cmd+] | Navigate back / forward (replaces tabs) |
| Cmd+Z / Cmd+Shift+Z | Undo / Redo |
| Cmd+19 | Switch to tab N |
| Cmd+[ / Cmd+] | Navigate back / forward |

View File

@@ -46,7 +46,7 @@ laputa-app/
│ │ ├── NoteList.tsx # Second panel: filtered note list
│ │ ├── NoteItem.tsx # Individual note item
│ │ ├── PulseView.tsx # Git activity feed (replaces NoteList)
│ │ ├── Editor.tsx # Third panel: tabs + editor orchestration
│ │ ├── Editor.tsx # Third panel: editor orchestration
│ │ ├── EditorContent.tsx # Editor content area
│ │ ├── EditorRightPanel.tsx # Right panel toggle
│ │ ├── editorSchema.tsx # BlockNote schema + wikilink type
@@ -61,12 +61,11 @@ laputa-app/
│ │ ├── SettingsPanel.tsx # App settings
│ │ ├── StatusBar.tsx # Bottom bar: vault picker + sync
│ │ ├── CommandPalette.tsx # Cmd+K command launcher
│ │ ├── TabBar.tsx # Tab management
│ │ ├── BreadcrumbBar.tsx # Breadcrumb + word count + actions
│ │ ├── WelcomeScreen.tsx # Onboarding screen
│ │ ├── GitHubVaultModal.tsx # GitHub vault clone/create
│ │ ├── GitHubDeviceFlow.tsx # GitHub OAuth device flow
│ │ ├── ThemePropertyEditor.tsx # Interactive theme editor
│ │ ├── TitleField.tsx # Editable note title above editor
│ │ ├── ConflictResolverModal.tsx # Git conflict resolution
│ │ ├── CommitDialog.tsx # Git commit modal
│ │ ├── CreateNoteDialog.tsx # New note modal
@@ -87,7 +86,6 @@ laputa-app/
│ │ ├── useNoteActions.ts # Composes creation + rename + frontmatter
│ │ ├── useNoteCreation.ts # Note/type/daily-note creation
│ │ ├── useNoteRename.ts # Note renaming + wikilink updates
│ │ ├── useTabManagement.ts # Tab ordering + lifecycle
│ │ ├── useAIChat.ts # AI chat state
│ │ ├── useAiAgent.ts # AI agent state + tool tracking
│ │ ├── useAiActivity.ts # MCP UI bridge listener
@@ -95,7 +93,6 @@ laputa-app/
│ │ ├── useConflictResolver.ts # Git conflict handling
│ │ ├── useEditorSave.ts # Auto-save with debounce
│ │ ├── useTheme.ts # Flatten theme.json → CSS vars
│ │ ├── useThemeManager.ts # Vault theme lifecycle
│ │ ├── useUnifiedSearch.ts # Keyword search
│ │ ├── useNoteSearch.ts # Note search
│ │ ├── useCommandRegistry.ts # Command palette registry
@@ -116,7 +113,7 @@ laputa-app/
│ │ ├── ai-chat.ts # Chat API client + token estimation
│ │ ├── ai-context.ts # Context snapshot builder
│ │ ├── noteListHelpers.ts # Sorting, filtering, date formatting
│ │ ├── themeSchema.ts # Theme editor schema builder
│ │ ├── wikilink.ts # Wikilink resolution
│ │ ├── configMigration.ts # localStorage → vault config migration
│ │ ├── iconRegistry.ts # Phosphor icon registry
│ │ ├── propertyTypes.ts # Property type definitions
@@ -138,7 +135,7 @@ laputa-app/
│ ├── src/
│ │ ├── main.rs # Entry point (calls lib::run())
│ │ ├── lib.rs # Tauri setup + command registration (61 commands)
│ │ ├── commands.rs # All Tauri command handlers
│ │ ├── commands/ # Tauri command handlers (split into modules)
│ │ ├── vault/ # Vault module
│ │ │ ├── mod.rs # Core types, parse_md_file, scan_vault
│ │ │ ├── cache.rs # Git-based incremental caching
@@ -155,8 +152,7 @@ laputa-app/
│ │ │ ├── conflict.rs, remote.rs, pulse.rs
│ │ ├── github/ # GitHub module
│ │ │ ├── mod.rs, auth.rs, api.rs, clone.rs
│ │ ├── theme/ # Theme module
│ │ │ ├── mod.rs, create.rs, defaults.rs, seed.rs
│ │ ├── telemetry.rs # Sentry init + path scrubber
│ │ ├── search.rs # Keyword search (walkdir-based)
│ │ ├── claude_cli.rs # Claude CLI subprocess management
│ │ ├── ai_chat.rs # Direct Anthropic API client
@@ -197,7 +193,7 @@ laputa-app/
|------|---------------|
| `src/App.tsx` | Root component. Shows the 4-panel layout, state flow, and how all features connect. |
| `src/types.ts` | All shared TypeScript types. Read this first to understand the data model. |
| `src-tauri/src/commands.rs` | All 61 Tauri command handlers. This is the frontend-backend API surface. |
| `src-tauri/src/commands/` | Tauri command handlers (split into modules). This is the frontend-backend API surface. |
| `src-tauri/src/lib.rs` | Tauri setup, command registration, startup tasks, WebSocket bridge lifecycle. |
### Data layer
@@ -225,7 +221,7 @@ laputa-app/
| File | Why it matters |
|------|---------------|
| `src/components/Editor.tsx` | BlockNote setup, tab bar, breadcrumb bar, diff/raw toggle. |
| `src/components/Editor.tsx` | BlockNote setup, breadcrumb bar, diff/raw toggle. |
| `src/components/editorSchema.tsx` | Custom wikilink inline content type definition. |
| `src/utils/wikilinks.ts` | Wikilink preprocessing pipeline (markdown ↔ BlockNote). |
| `src/components/RawEditorView.tsx` | CodeMirror 6 raw markdown editor. |
@@ -239,14 +235,12 @@ laputa-app/
| `src/hooks/useAiAgent.ts` | Agent state: messages, streaming, tool tracking, file detection. |
| `src/utils/ai-context.ts` | Context snapshot builder for AI conversations. |
### Styling & Themes
### Styling
| File | Why it matters |
|------|---------------|
| `src/index.css` | All CSS custom properties. The design token source of truth. |
| `src/theme.json` | Editor-specific theme (fonts, headings, lists, code blocks). |
| `src/hooks/useThemeManager.ts` | Vault theme lifecycle (switch, create, apply, live preview). |
| `docs/THEMING.md` | Full theme system documentation. |
### Settings & Config
@@ -274,7 +268,7 @@ This lives in `useVaultLoader.ts` and `useNoteActions.ts`. Components never call
### Props-Down, Callbacks-Up
No global state management (no Redux, no Context). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, `onCloseTab`, etc.).
No global state management (no Redux, no Context). `App.tsx` owns the state and passes it down as props. Child-to-parent communication uses callback props (`onSelectNote`, etc.).
### Discriminated Unions for Selection State
@@ -317,7 +311,7 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
### Add a new Tauri command
1. Write the Rust function in the appropriate module (`vault/`, `git/`, etc.)
2. Add a command handler in `commands.rs`
2. Add a command handler in `commands/`
3. Register it in the `generate_handler![]` macro in `lib.rs`
4. Call it from the frontend via `invoke()` in the appropriate hook
5. Add a mock handler in `mock-tauri.ts`
@@ -342,11 +336,10 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
2. Add a corresponding menu bar item in `menu.rs` for discoverability
3. If it has a keyboard shortcut, register it in `useAppKeyboard.ts`
### Add or modify a theme
### Modify styling
1. **Vault-based** (preferred): Create/edit a markdown note in `theme/` with `type: Theme` frontmatter
2. **Programmatic**: Edit defaults in `src-tauri/src/theme/defaults.rs`
3. See `docs/THEMING.md` for the full property reference
1. **Global CSS variables**: Edit `src/index.css`
2. **Editor typography**: Edit `src/theme.json`
### Work with the AI agent

View File

@@ -1,283 +0,0 @@
# 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.