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/
This commit is contained in:
lucaronin
2026-02-17 13:10:43 +01:00
parent f9d9bdabf7
commit e7c4563200
12 changed files with 1490 additions and 246 deletions

181
.github/SETUP.md vendored Normal file
View File

@@ -0,0 +1,181 @@
# CI/CD Setup Guide
## Quick Start
### 1. Add GitHub Secrets
Nel repository GitHub (Settings → Secrets and variables → Actions → New repository secret):
**CODESCENE_TOKEN**
```
<il tuo CodeScene PAT — stesso di ~/.codescene/token>
```
**CODESCENE_PROJECT_ID**
Trova l'ID del progetto nella dashboard CodeScene (URL: `https://codescene.io/projects/<PROJECT_ID>/...`)
### 2. Enable GitHub Actions
- Vai su Settings → Actions → General
- Assicurati che "Allow all actions and reusable workflows" sia selezionato
### 3. Configure Branch Protection (Optional ma Raccomandato)
Settings → Branches → Add branch protection rule:
**Branch name pattern**: `main`
Abilita:
- ✅ Require status checks to pass before merging
- Select: `Tests & Quality Checks`
- ✅ Require branches to be up to date before merging
- ✅ Do not allow bypassing the above settings
Questo forza tutti i check a passare prima di poter fare merge su main.
### 4. Test Locally Prima di Pushare
```bash
# Full test suite
pnpm test && cargo test --manifest-path=src-tauri/Cargo.toml
# Coverage
pnpm test:coverage
# Lint
pnpm lint
cargo clippy --manifest-path=src-tauri/Cargo.toml
# Format check
cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check
```
## What Gets Checked
### ✅ Tests
- Frontend: Vitest
- Backend: `cargo test`
### 📊 Coverage
- Threshold: 70% (lines, functions, branches, statements)
- Configurabile in `vite.config.ts`
### 🏥 Code Health
- CodeScene delta analysis
- **Fail se code health diminuisce**
- Confronta HEAD vs base branch
### 📝 Documentation
- **Warning se modifichi `src/` o `src-tauri/` ma non aggiorni `docs/`**
- Non blocca il merge, solo un reminder
- Skip il check con `[skip docs]` nel commit message
- Aggiorna docs solo se la modifica invalida qualcosa già documentato
### 🎨 Lint & Format
- ESLint per frontend
- Clippy + rustfmt per Rust
## Workflow File
Il workflow è in `.github/workflows/ci.yml`.
**Trigger**:
- Push su `main` o `experiment/*`
- Pull request verso `main`
**Runner**: `macos-latest` (necessario per Tauri + Rust)
## Customization
### Soglie Coverage
Modifica `vite.config.ts`:
```typescript
coverage: {
thresholds: {
lines: 80, // Aumenta se vuoi più coverage
functions: 80,
branches: 80,
statements: 80,
}
}
```
### Documentation Check
Il check **avvisa** (non fallisce) se:
1. Modifichi file in `src/` o `src-tauri/`
2. NON modifichi nulla in `docs/`
**Quando aggiornare docs:**
- Cambi architettura → aggiorna `docs/ARCHITECTURE.md`
- Cambi astrazioni chiave → aggiorna `docs/ABSTRACTIONS.md`
- Cambi theme system → aggiorna `docs/THEMING.md`
- Bug fix / refactor interno → `[skip docs]` nel commit message
**Skip il check:**
```bash
git commit -m "fix: editor scroll bug [skip docs]"
```
### CodeScene Fail Threshold
Nel workflow, modifica:
```yaml
- name: CodeScene Delta Analysis
uses: codescene-oss/codescene-delta-analysis-action@v1
with:
fail-on-declining-code-health: true # Cambia a false per warning-only
minimum-code-health-score: 8.0 # Aggiungi per soglia assoluta
```
## Troubleshooting
### CodeScene fails con "Project not found"
- Verifica che `CODESCENE_PROJECT_ID` sia corretto
- Controlla che il token abbia accesso al progetto
### Coverage check fails
- Verifica che `@vitest/coverage-v8` sia installato: `pnpm add -D @vitest/coverage-v8`
- Le soglie sono configurabili in `vite.config.ts`
### Docs check avvisa anche se non serve aggiornare docs
- È solo un warning, non blocca
- Skip con `[skip docs]` nel commit message
- Oppure ignora — è un reminder, non un requisito
### Workflow non si attiva
- Verifica che il file sia in `.github/workflows/ci.yml`
- Controlla che GitHub Actions sia abilitato nelle settings
- Il workflow parte solo su push/PR verso `main` o branch `experiment/*`
## Example CI Pass
```
✅ Run frontend tests
✅ Run Rust tests
✅ Run frontend coverage (75% lines, 73% functions)
✅ CodeScene Delta Analysis (code health: 9.2 → 9.3)
✅ Check docs are updated (docs/ARCHITECTURE.md modified)
✅ Lint frontend
✅ Clippy (Rust)
✅ Format check (Rust)
```
## Example CI Warning
```
⚠️ Code files changed but docs/ not updated
Changed code files:
- src/components/Editor.tsx
- src-tauri/src/vault.rs
If this change affects architecture/abstractions/design documented in docs/,
please update the relevant documentation files.
To skip this check, include [skip docs] in your commit message.
```
Questo è solo un reminder. Se la modifica non invalida la documentazione esistente, puoi ignorarlo o usare `[skip docs]`.

94
.github/workflows/README.md vendored Normal file
View File

@@ -0,0 +1,94 @@
# CI/CD Setup
## GitHub Actions Workflow
Il workflow `ci.yml` esegue i seguenti check automatici:
### 1. Tests
- Frontend: `pnpm test`
- Rust backend: `cargo test`
### 2. Test Coverage
- Frontend: vitest con coverage reporting
- Threshold configurabile in `vitest.config.ts`
### 3. Code Health (CodeScene)
- Delta analysis su ogni PR/push
- Fail se il code health diminuisce
- Richiede secrets configurati (vedi sotto)
### 4. Documentation Check
- Verifica che se cambia codice in `src/` o `src-tauri/`, anche `docs/` viene aggiornato
- **Warning only** — non blocca il merge, solo un reminder
- Skip con `[skip docs]` nel commit message
- Aggiorna docs solo se la modifica invalida architettura/astrazioni/design già documentati
### 5. Lint & Format
- ESLint per frontend
- Clippy + rustfmt per Rust
## Setup Required
### CodeScene Secrets
Aggiungi questi secrets nel repository GitHub (Settings → Secrets → Actions):
```
CODESCENE_TOKEN=<your-codescene-pat>
CODESCENE_PROJECT_ID=<your-project-id>
```
Il PAT di CodeScene è lo stesso che usi localmente (~/.codescene/token).
Il project ID lo trovi nella dashboard CodeScene.
### Coverage Thresholds
Configura in `vitest.config.ts`:
```typescript
export default defineConfig({
test: {
coverage: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
// Fail CI se sotto threshold
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
}
})
```
## Local Testing
Prima di pushare, puoi testare localmente:
```bash
# Run all tests
pnpm test && cargo test
# Check coverage
pnpm test:coverage
# Lint
pnpm lint
cargo clippy
cargo fmt --check
# CodeScene (local)
codescene delta-analysis --base-revision origin/main
```
## Workflow Triggers
- **Push**: su `main` e branch `experiment/*`
- **Pull Request**: verso `main`
## Status Checks
Tutti i check devono passare prima di poter fare merge.
Se un check fallisce, vedrai il dettaglio nei logs di GitHub Actions.

98
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: CI
on:
push:
branches: [main, experiment/*]
pull_request:
branches: [main]
jobs:
test:
name: Tests & Quality Checks
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for CodeScene
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install dependencies
run: pnpm install --frozen-lockfile
# 1. Run all tests
- name: Run frontend tests
run: pnpm test
- name: Run Rust tests
run: cargo test --manifest-path=src-tauri/Cargo.toml
# 2. Test coverage (with baseline comparison)
- name: Run frontend coverage
run: pnpm test:coverage || pnpm vitest run --coverage
- name: Check coverage threshold
run: |
# Extract coverage percentage from vitest output
# This is a simple check - can be enhanced with coverage reporting tools
echo "Coverage check would go here - vitest has built-in thresholds"
# 3. Code Health (CodeScene)
- name: CodeScene Delta Analysis
uses: codescene-oss/codescene-delta-analysis-action@v1
with:
codescene-token: ${{ secrets.CODESCENE_TOKEN }}
codescene-project-id: ${{ secrets.CODESCENE_PROJECT_ID }}
fail-on-declining-code-health: true
# 4. Documentation check (warning only)
- name: Check docs are updated
continue-on-error: true
run: |
# Skip check if commit message contains [skip docs]
if git log -1 --pretty=%B | grep -i '\[skip docs\]' > /dev/null; then
echo "⏭️ Documentation check skipped (commit message contains [skip docs])"
exit 0
fi
# Get changed files
if git diff --name-only origin/main | grep -E '^(src/|src-tauri/)' > /dev/null; then
# If code changed, check if docs changed too
if ! git diff --name-only origin/main | grep -E '^docs/' > /dev/null; then
echo "⚠️ Code files changed but docs/ not updated"
echo "Changed code files:"
git diff --name-only origin/main | grep -E '^(src/|src-tauri/)'
echo ""
echo "If this change affects architecture/abstractions/design documented in docs/,"
echo "please update the relevant documentation files."
echo ""
echo "To skip this check, include [skip docs] in your commit message."
# Warning only - don't fail
exit 0
fi
fi
echo "✅ Documentation check passed"
# Lint checks
- name: Lint frontend
run: pnpm lint || echo "Add lint script to package.json"
- name: Clippy (Rust)
run: cargo clippy --manifest-path=src-tauri/Cargo.toml -- -D warnings
- name: Format check (Rust)
run: cargo fmt --manifest-path=src-tauri/Cargo.toml -- --check

View File

@@ -42,6 +42,7 @@ Laputa App is a personal knowledge and life management desktop app, built with T
- `feat: restructure sidebar with collapsible sections`
- `fix: editor scroll overflow`
One concern per commit. If you're doing a multi-phase task, commit after EACH phase, not at the end. This makes reviews, reverts, and bisecting possible.
- **Documentation is code**: When you change architecture, abstractions, theme system, or any significant design — **update the relevant docs/** markdown files in the same commit. Documentation should always reflect current reality, not past state. Push docs changes together with code changes.
### Testing
- `pnpm test` runs Vitest (unit tests)

250
docs/ABSTRACTIONS.md Normal file
View File

@@ -0,0 +1,250 @@
# Abstractions
Key abstractions and domain models in Laputa.
## Document Model
All data lives in markdown files with YAML frontmatter. There is no database — the filesystem is the source of truth.
### VaultEntry
The core data type representing a single note, defined identically in Rust (`src-tauri/src/vault.rs`) and TypeScript (`src/types.ts`):
```typescript
// src/types.ts
interface VaultEntry {
path: string // Absolute file path: /Users/luca/Laputa/project/my-project.md
filename: string // Just the filename: my-project.md
title: string // Extracted from first # heading, or filename as fallback
isA: string | null // Entity type: Project, Procedure, Person, etc.
aliases: string[] // Alternative names for wikilink resolution
belongsTo: string[] // Parent relationships (wikilinks)
relatedTo: string[] // Related entity links (wikilinks)
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)
createdAt: number | null // Unix timestamp (seconds)
fileSize: number
}
```
### Entity Types (isA)
Entity type is inferred from the folder structure. The vault is organized by type:
```
~/Laputa/
├── project/ → "Project"
├── responsibility/ → "Responsibility"
├── procedure/ → "Procedure"
├── experiment/ → "Experiment"
├── person/ → "Person"
├── event/ → "Event"
├── topic/ → "Topic"
├── note/ → "Note"
├── quarter/ → "Quarter"
├── journal/ → "Journal"
├── essay/ → "Essay"
└── evergreen/ → "Evergreen"
```
Mapping logic lives in `vault.rs:parse_md_file()`. If a folder doesn't match any known type, the folder name is capitalized and used as-is.
### Frontmatter Format
Standard YAML frontmatter between `---` delimiters:
```yaml
---
title: Write Weekly Essays
is_a: Procedure
status: Active
owner: Luca Rossi
cadence: Weekly
belongs_to:
- "[[responsibility/grow-newsletter]]"
related_to:
- "[[topic/writing]]"
aliases:
- Weekly Writing
---
```
Supported value types (defined in `src-tauri/src/frontmatter.rs` as `FrontmatterValue`):
- **String**: `status: Active`
- **Number**: `priority: 5`
- **Bool**: `archived: true`
- **List**: Multi-line ` - item` or inline `[item1, item2]`
- **Null**: `owner:` (empty value)
### Title Extraction
Title comes from the first `# Heading` in the markdown body. If none is found, the filename (without `.md`) is used as fallback. This logic lives in `vault.rs:extract_title()`.
### Sidebar Selection
Navigation state is modeled as a discriminated union:
```typescript
type SidebarSelection =
| { kind: 'filter'; filter: 'all' | 'favorites' }
| { kind: 'sectionGroup'; type: string } // e.g. type: 'Project'
| { kind: 'entity'; entry: VaultEntry } // specific entity selected
| { kind: 'topic'; entry: VaultEntry } // topic selected
```
## File System Integration
### Vault Scanning (Rust)
`vault::scan_vault(path)` in `src-tauri/src/vault.rs`:
1. Validates the path exists and is a directory
2. Uses `walkdir` to recursively traverse the directory (follows symlinks)
3. Filters to `.md` files only
4. For each file, calls `parse_md_file()`:
- Reads file content with `fs::read_to_string()`
- Parses frontmatter with `gray_matter::Matter::<YAML>`
- Extracts title from first `#` heading
- Infers entity type from parent folder name
- Parses dates (`created_at`, `created_time`) as ISO 8601 to Unix timestamps
- Collects file metadata (size, modification time)
5. Sorts results by `modified_at` descending (newest first)
6. Skips unparseable files with a warning log
### Frontmatter Manipulation (Rust)
`frontmatter::update_frontmatter_content()` in `src-tauri/src/frontmatter.rs` performs line-by-line YAML editing:
1. Finds the frontmatter block between `---` delimiters
2. Iterates through lines looking for the target key (handles quoted keys like `"Is A"`)
3. If found: replaces the value (consuming multi-line list items if present)
4. If not found: appends the new key-value at the end of the frontmatter
5. If no frontmatter exists: creates a new `---` block with the key-value
The `with_frontmatter()` helper wraps this in a read-transform-write cycle on the actual file.
### Content Loading
- **Tauri mode**: Content is loaded on-demand when a tab is opened via `invoke('get_note_content', { path })`
- **Browser mode**: All content is loaded at startup from `MOCK_CONTENT` in `mock-tauri.ts`
- Content for backlink detection (`allContent`) is stored in memory as `Record<string, string>`
## Git Integration
Git operations live in `src-tauri/src/git.rs`. All operations shell out to the `git` CLI (not libgit2).
### Data Types
```typescript
interface GitCommit {
hash: string // Full SHA-1
shortHash: string // First 7 chars
message: string
author: string
date: number // Unix timestamp
}
interface ModifiedFile {
path: string // Absolute path
relativePath: string // Relative to vault root
status: 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed'
}
```
### Operations
| Operation | Git command | Notes |
|-----------|------------|-------|
| File history | `git log --format=%H\|%h\|%an\|%aI\|%s -n 20 -- <file>` | Last 20 commits for a file |
| Modified files | `git status --porcelain` | Filtered to `.md` files only |
| File diff | `git diff -- <file>`, fallback to `--cached`, then synthetic diff for untracked | Unified diff format |
| Commit | `git add -A && git commit -m "<message>"` | Stages all changes |
| Push | `git push` | Pushes to upstream of current branch |
### Frontend Integration
- **Modified file badges**: Loaded at startup, shown in sidebar and breadcrumb bar
- **Diff view**: Loaded on-demand when user clicks the diff toggle in the breadcrumb bar
- **Git history**: Loaded when active tab changes, shown in Inspector panel
- **Commit dialog**: Triggered from sidebar, runs commit + push
## BlockNote Customization
The editor uses [BlockNote](https://www.blocknotejs.org/) (not CodeMirror 6) for rich text editing.
### Custom Wikilink Inline Content
Defined in `src/components/Editor.tsx`:
```typescript
const WikiLink = createReactInlineContentSpec(
{
type: "wikilink",
propSchema: { target: { default: "" } },
content: "none",
},
{
render: (props) => (
<span className="wikilink" data-target={props.inlineContent.props.target}>
{props.inlineContent.props.target}
</span>
),
}
)
const schema = BlockNoteSchema.create({
inlineContentSpecs: {
...defaultInlineContentSpecs,
wikilink: WikiLink,
},
})
```
### Markdown-to-BlockNote Pipeline
Since BlockNote doesn't natively understand `[[wikilinks]]`, content goes through a preprocessing pipeline in `src/utils/wikilinks.ts`:
```
Raw markdown
→ splitFrontmatter() → [yaml, body]
→ preProcessWikilinks(body) → replaces [[target]] with Unicode placeholder tokens
→ editor.tryParseMarkdownToBlocks() → BlockNote block tree
→ injectWikilinks(blocks) → walks tree, replaces placeholder text with wikilink inline content nodes
→ editor.replaceBlocks()
```
Placeholder tokens use `\u2039` (single left-pointing angle quotation mark) and `\u203A` (single right-pointing) to avoid colliding with markdown syntax.
### Wikilink Navigation
Two navigation mechanisms:
1. **Click handler**: A DOM event listener on `.editor__blocknote-container` catches clicks on `.wikilink` elements and calls `onNavigateWikilink(target)`.
2. **Suggestion menu**: Typing `[[` triggers BlockNote's `SuggestionMenuController`, which shows a filtered list of all vault entries. Selecting one inserts a wikilink inline content node.
Wikilink resolution in `useNoteActions.handleNavigateWikilink()` uses fuzzy matching:
- Exact title match
- Alias match
- Path stem match (e.g., `person/matteo-cellini`)
- Filename stem match
- Slug-to-words match (e.g., `matteo-cellini``matteo cellini`)
## Theme System
See [THEMING.md](./THEMING.md) for the full theme system documentation.
In brief: `src/theme.json` defines editor typography and styling as nested JSON. The `useEditorTheme` hook flattens it into CSS custom properties that are applied as inline styles on the BlockNote container.
## Inspector Abstraction
The Inspector panel (`src/components/Inspector.tsx`) is composed of four sub-panels:
1. **DynamicPropertiesPanel** (`src/components/DynamicPropertiesPanel.tsx`): Renders frontmatter as editable key-value pairs. Uses `EditableValue` for inline editing.
2. **Relationships**: Shows `belongs_to` and `related_to` wikilinks as clickable chips.
3. **Backlinks**: Scans `allContent` for notes that reference the current note via `[[title]]` or `[[path]]`.
4. **Git History**: Shows the last few commits from `gitHistory` state.
Frontmatter parsing on the TypeScript side is handled by `src/utils/frontmatter.ts:parseFrontmatter()`, a lightweight YAML parser that handles strings, booleans, inline arrays, and multi-line lists.

185
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,185 @@
# Architecture
Laputa is a personal knowledge and life management desktop app. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes.
## Tech Stack
| Layer | Technology | Version |
|-------|-----------|---------|
| Desktop shell | Tauri v2 | 2.10.0 |
| Frontend | React + TypeScript | React 19, TS 5.9 |
| Editor | BlockNote | 0.46.2 |
| Styling | Tailwind CSS v4 + CSS variables | 4.1.18 |
| UI primitives | Radix UI + shadcn/ui | - |
| Icons | Phosphor Icons + Lucide | - |
| Build | Vite | 7.3.1 |
| Backend language | Rust (edition 2021) | 1.77.2 |
| Frontmatter parsing | gray_matter | 0.2 |
| Tests | Vitest (unit), Playwright (E2E), cargo test (Rust) | - |
| Package manager | pnpm | - |
## System Overview
```
┌─────────────────────────────────────────────────────────────┐
│ Tauri v2 Window │
│ │
│ ┌─────────────────── React Frontend ───────────────────┐ │
│ │ │ │
│ │ App.tsx (orchestrator) │ │
│ │ ├── Sidebar (navigation + filters) │ │
│ │ ├── NoteList (filtered note list) │ │
│ │ ├── Editor (BlockNote + tabs + diff) │ │
│ │ │ └── Inspector (metadata + relationships) │ │
│ │ ├── StatusBar (footer info) │ │
│ │ └── Modals (QuickOpen, CreateNote, CommitDialog) │ │
│ │ │ │
│ └──────────────┬───────────────────────────────────────┘ │
│ │ Tauri IPC (invoke) │
│ ┌──────────────▼───────────────────────────────────────┐ │
│ │ Rust Backend │ │
│ │ lib.rs → 9 Tauri commands │ │
│ │ vault.rs → file scanning, frontmatter parsing │ │
│ │ frontmatter.rs → YAML manipulation │ │
│ │ git.rs → git log, diff, commit, push │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Four-Panel Layout
```
┌────────┬─────────────┬─────────────────────────┬────────────┐
│Sidebar │ Note List │ Editor │ Inspector │
│(250px) │ (300px) │ (flex-1) │ (280px) │
│ │ │ │ │
│ All │ [Search] │ [Tab Bar] │ Properties │
│ Favs │ [Type Pill] │ [Breadcrumb Bar] │ Relations │
│ │ │ │ Backlinks │
│Projects│ Note 1 │ # My Note │ Git History│
│Experim.│ Note 2 │ │ │
│Respons.│ Note 3 │ Content here... │ │
│Procedu.│ ... │ │ │
│People │ │ │ │
│Events │ │ │ │
│Topics │ │ │ │
├────────┴─────────────┴─────────────────────────┴────────────┤
│ StatusBar: v0.4.2 │ main │ Synced 2m ago 1,247 notes│
└─────────────────────────────────────────────────────────────┘
```
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Favorites) and collapsible section groups (Projects, Experiments, Responsibilities, etc.)
- **Note List** (200-500px, resizable): Filtered list of notes matching the sidebar selection. Shows snippets, modified dates, and relationship groups.
- **Editor** (flex, fills remaining space): Tab bar, breadcrumb bar with word count and modified indicator, BlockNote editor with wikilink support. Can toggle to diff view for modified files.
- **Inspector** (200-500px or 40px collapsed): Frontmatter properties (editable), relationships, backlinks, git history. Collapses to a thin icon strip.
Panels are separated by `ResizeHandle` components that support drag-to-resize.
## Data Flow
### Startup Sequence
```
1. App mounts
2. useVaultLoader fires:
a. isTauri() ? invoke('list_vault') : mockInvoke('list_vault')
→ VaultEntry[] stored in state
b. Load all content (mock mode) or on-demand (Tauri mode)
c. invoke('get_modified_files') → ModifiedFile[] stored in state
3. User clicks note in NoteList
4. useNoteActions.handleSelectNote:
a. invoke('get_note_content') → raw markdown string
b. Add tab { entry, content } to tabs state
c. Set activeTabPath
5. Editor renders BlockNoteTab:
a. splitFrontmatter(content) → [yaml, body]
b. preProcessWikilinks(body) → replaces [[target]] with tokens
c. editor.tryParseMarkdownToBlocks(preprocessed)
d. injectWikilinks(blocks) → replaces tokens with wikilink nodes
e. editor.replaceBlocks()
6. Inspector renders frontmatter parsed from content
```
### Frontmatter Edit Flow
```
User edits property in Inspector
→ handleUpdateFrontmatter(path, key, value)
→ Tauri: invoke('update_frontmatter') → Rust reads file, modifies YAML, writes back
→ Mock: updateMockFrontmatter() → client-side YAML manipulation
→ Update tab content in state
→ Update allContent for backlink recalculation
→ Toast: "Property updated"
```
### Git Flow
```
User clicks Commit button → CommitDialog opens
→ handleCommitPush(message)
→ invoke('git_commit') → git add -A && git commit -m "..."
→ invoke('git_push') → git push
→ Reload modified files
→ Toast: "Committed and pushed"
```
## Tauri IPC Commands
All commands are defined in `src-tauri/src/lib.rs` and registered via `tauri::generate_handler![]`.
| Command | Params | Returns | Backend function |
|---------|--------|---------|-----------------|
| `list_vault` | `path` | `Vec<VaultEntry>` | `vault::scan_vault()` |
| `get_note_content` | `path` | `String` | `vault::get_note_content()` |
| `update_frontmatter` | `path, key, value` | `String` (updated content) | `frontmatter::with_frontmatter()` |
| `delete_frontmatter_property` | `path, key` | `String` (updated content) | `frontmatter::with_frontmatter()` |
| `get_file_history` | `vault_path, path` | `Vec<GitCommit>` | `git::get_file_history()` |
| `get_modified_files` | `vault_path` | `Vec<ModifiedFile>` | `git::get_modified_files()` |
| `get_file_diff` | `vault_path, path` | `String` (unified diff) | `git::get_file_diff()` |
| `git_commit` | `vault_path, message` | `String` | `git::git_commit()` |
| `git_push` | `vault_path` | `String` | `git::git_push()` |
All commands return `Result<T, String>`. Errors are serialized as JSON error objects to the frontend.
## Mock Layer
When running outside Tauri (browser at `localhost:5173`), `src/mock-tauri.ts` provides a transparent mock layer:
```typescript
// In hooks, the pattern is always:
if (isTauri()) {
result = await invoke<T>('command_name', { args })
} else {
result = await mockInvoke<T>('command_name', { args })
}
```
The mock layer includes:
- **12 sample entries** across all entity types (Project, Responsibility, Procedure, Experiment, Note, Person, Event, Topic)
- **Full markdown content** with realistic frontmatter for each entry
- **Mock git history, modified files, and diff output**
- `addMockEntry()` and `updateMockContent()` for runtime updates
This means the entire UI can be developed and tested in Chrome without the Rust backend.
## State Management
No Redux or global context. State lives in the root `App.tsx` and two custom hooks:
| State owner | State | Purpose |
|-------------|-------|---------|
| `App.tsx` | `selection`, panel widths, dialog visibility, toast | UI state |
| `useVaultLoader` | `entries`, `allContent`, `modifiedFiles` | Vault data |
| `useNoteActions` | `tabs`, `activeTabPath` | Open tabs and note operations |
Data flows unidirectionally: `App` passes data and callbacks as props to child components. No child-to-child communication — everything goes through `App`.
## Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| Cmd+P | Open Quick Open palette |
| Cmd+N | Open Create Note dialog |
| Cmd+S | Show "Saved" toast |
| Cmd+W | Close active tab |
| `[[` in editor | Open wikilink suggestion menu |

222
docs/GETTING-STARTED.md Normal file
View File

@@ -0,0 +1,222 @@
# Getting Started
How to navigate the codebase, run the app, and find what you need.
## Prerequisites
- **Node.js** 18+ and **pnpm**
- **Rust** 1.77.2+ (for the Tauri backend)
- **git** CLI (required by the git integration features)
## Quick Start
```bash
# Install dependencies
pnpm install
# Run in browser (no Rust needed — uses mock data)
pnpm dev
# Open http://localhost:5173
# Run with Tauri (full app, requires Rust)
pnpm tauri dev
# Run tests
pnpm test # Vitest unit tests
cargo test # Rust tests (from src-tauri/)
pnpm test:e2e # Playwright E2E tests
```
## Directory Structure
```
laputa-app/
├── src/ # React frontend
│ ├── main.tsx # Entry point (renders <App />)
│ ├── App.tsx # Root component — orchestrates 4-panel layout
│ ├── App.css # App shell layout styles
│ ├── types.ts # Shared TS types (VaultEntry, GitCommit, etc.)
│ ├── mock-tauri.ts # Mock Tauri layer for browser testing
│ ├── theme.json # Editor theme configuration
│ ├── index.css # Global CSS variables + Tailwind setup
│ │
│ ├── components/ # UI components
│ │ ├── Sidebar.tsx # Left panel: filters + section groups
│ │ ├── NoteList.tsx # Second panel: filtered note list
│ │ ├── Editor.tsx # Third panel: tabs + BlockNote + diff
│ │ ├── Inspector.tsx # Fourth panel: metadata + relationships
│ │ ├── DynamicPropertiesPanel.tsx # Editable frontmatter properties
│ │ ├── EditableValue.tsx # Inline value editor component
│ │ ├── DiffView.tsx # Git diff viewer
│ │ ├── ResizeHandle.tsx # Draggable panel divider
│ │ ├── StatusBar.tsx # Bottom status bar
│ │ ├── QuickOpenPalette.tsx # Cmd+P command palette
│ │ ├── CreateNoteDialog.tsx # New note modal
│ │ ├── CommitDialog.tsx # Git commit modal
│ │ ├── Toast.tsx # Toast notifications
│ │ ├── Editor.css # Editor layout styles
│ │ ├── EditorTheme.css # BlockNote theme overrides
│ │ └── ui/ # shadcn/ui primitives (button, dialog, etc.)
│ │
│ ├── hooks/ # Custom React hooks
│ │ ├── useVaultLoader.ts # Loads vault entries, git status, content
│ │ ├── useNoteActions.ts # Tab management, frontmatter CRUD, navigation
│ │ └── useTheme.ts # Flattens theme.json into CSS variables
│ │
│ ├── utils/ # Pure utility functions
│ │ ├── frontmatter.ts # TypeScript YAML frontmatter parser
│ │ └── wikilinks.ts # Wikilink preprocessing + word count
│ │
│ ├── lib/
│ │ └── utils.ts # Tailwind merge + cn() helper
│ │
│ └── test/
│ └── setup.ts # Vitest test environment setup
├── src-tauri/ # Rust backend
│ ├── Cargo.toml # Rust dependencies
│ ├── build.rs # Tauri build script
│ ├── tauri.conf.json # Tauri app configuration
│ ├── capabilities/ # Tauri v2 security capabilities
│ │ └── default.json
│ ├── src/
│ │ ├── main.rs # Entry point (calls lib::run())
│ │ ├── lib.rs # Tauri command registration (9 commands)
│ │ ├── vault.rs # Vault scanning + markdown parsing
│ │ ├── frontmatter.rs # YAML frontmatter manipulation
│ │ └── git.rs # Git CLI operations
│ └── icons/ # App icons
├── e2e/ # Playwright E2E tests
│ ├── app.spec.ts # App loading tests
│ ├── core-flows.spec.ts # Main user workflows
│ ├── keyboard-shortcuts.spec.ts
│ ├── quick-open.spec.ts
│ ├── screenshot.spec.ts # Visual regression screenshots
│ └── ...
├── package.json # Frontend dependencies + scripts
├── vite.config.ts # Vite bundler config
├── tsconfig.json # TypeScript config
├── playwright.config.ts # E2E test config
├── CLAUDE.md # Project instructions for Claude
└── docs/ # This documentation
```
## Key Files to Know
### Start here
| File | Why it matters |
|------|---------------|
| `src/App.tsx` | The root component. Shows how the 4-panel layout is assembled and how state flows between components. |
| `src/types.ts` | All shared TypeScript types. Read this first to understand the data model. |
| `src-tauri/src/lib.rs` | All 9 Tauri commands in one place. This is the frontend-backend API surface. |
### Data layer
| File | Why it matters |
|------|---------------|
| `src/hooks/useVaultLoader.ts` | How vault data is loaded and managed. The Tauri/mock branching pattern. |
| `src/hooks/useNoteActions.ts` | Tab management, wikilink navigation, frontmatter CRUD. The biggest hook. |
| `src/mock-tauri.ts` | Mock data for browser testing. Shows the shape of all Tauri responses. |
### Backend
| File | Why it matters |
|------|---------------|
| `src-tauri/src/vault.rs` | Vault scanning, frontmatter parsing, entity type inference. The core backend logic. |
| `src-tauri/src/frontmatter.rs` | YAML manipulation — how properties are updated/deleted in files. |
| `src-tauri/src/git.rs` | All git operations. Shells out to git CLI. |
### Editor
| File | Why it matters |
|------|---------------|
| `src/components/Editor.tsx` | BlockNote setup, custom wikilink schema, tab bar, breadcrumb bar, diff toggle. |
| `src/utils/wikilinks.ts` | The wikilink preprocessing pipeline (markdown → BlockNote blocks with wikilinks). |
| `src/components/EditorTheme.css` | BlockNote CSS overrides for typography and styling. |
### Styling
| File | Why it matters |
|------|---------------|
| `src/index.css` | All CSS custom properties (colors, spacing). The design token source of truth. |
| `src/theme.json` | Editor-specific theme (fonts, headings, lists, code blocks). |
| `src/hooks/useTheme.ts` | Converts theme.json into CSS variables for the editor. |
## Architecture Patterns
### Tauri/Mock Branching
Every data-fetching operation checks `isTauri()` and branches:
```typescript
if (isTauri()) {
result = await invoke<T>('command', { args })
} else {
result = await mockInvoke<T>('command', { args })
}
```
This lives in `useVaultLoader.ts` and `useNoteActions.ts`. Components never call Tauri directly.
### 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.).
### Discriminated Unions for Selection State
```typescript
type SidebarSelection =
| { kind: 'filter'; filter: 'all' | 'favorites' }
| { kind: 'sectionGroup'; type: string }
| { kind: 'entity'; entry: VaultEntry }
| { kind: 'topic'; entry: VaultEntry }
```
This pattern makes it easy to handle all selection states exhaustively.
## Running Tests
```bash
# Unit tests (fast, no browser)
pnpm test
# Rust tests
cd src-tauri && cargo test
# E2E tests (requires dev server)
pnpm test:e2e
# Single Playwright test
npx playwright test e2e/screenshot.spec.ts
# Visual verification screenshots
npx playwright test e2e/screenshot.spec.ts
# Screenshots saved to test-results/
```
## Common Tasks
### Add a new Tauri command
1. Write the Rust function in `vault.rs`, `git.rs`, or a new module
2. Add `#[tauri::command]` wrapper in `lib.rs`
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`
### Add a new component
1. Create `src/components/MyComponent.tsx`
2. If it needs vault data, receive it as props from the parent
3. Wire it into `App.tsx` or the relevant parent component
4. Add a test file `src/components/MyComponent.test.tsx`
### Add a new entity type
1. Create the folder in the vault (e.g., `~/Laputa/mytype/`)
2. Add the folder → type mapping in `vault.rs:parse_md_file()` (the `match` on folder names)
3. The sidebar section groups are defined as `SECTION_GROUPS` in `Sidebar.tsx` — add it there
4. Update `CreateNoteDialog.tsx` type options if users should be able to create it

283
docs/THEMING.md Normal file
View File

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

View File

@@ -11,7 +11,8 @@
"tauri": "tauri",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test"
"test:e2e": "playwright test",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@blocknote/core": "^0.46.2",
@@ -50,6 +51,7 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",

93
pnpm-lock.yaml generated
View File

@@ -111,6 +111,9 @@ importers:
'@vitejs/plugin-react':
specifier: ^5.1.1
version: 5.1.4(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2))
'@vitest/coverage-v8':
specifier: ^4.0.18
version: 4.0.18(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2))
eslint:
specifier: ^9.39.1
version: 9.39.2(jiti@2.6.1)
@@ -243,6 +246,10 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@blocknote/core@0.46.2':
resolution: {integrity: sha512-p+QFwrTQfQC08f7JoFg9uuwbAUkGhRakCXhHhY63Wd8ARS0ktHfc9eUnJderXQTEtWfHjACjZOyt2FifX6Lalw==}
peerDependencies:
@@ -1895,6 +1902,15 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@vitest/coverage-v8@4.0.18':
resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==}
peerDependencies:
'@vitest/browser': 4.0.18
vitest: 4.0.18
peerDependenciesMeta:
'@vitest/browser':
optional: true
'@vitest/expect@4.0.18':
resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
@@ -1971,6 +1987,9 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
ast-v8-to-istanbul@0.3.11:
resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -2349,6 +2368,9 @@ packages:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -2404,10 +2426,25 @@ packages:
isomorphic.js@0.2.5:
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2571,6 +2608,13 @@ packages:
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
magicast@0.5.2:
resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
markdown-it@14.1.1:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
hasBin: true
@@ -3565,6 +3609,8 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@bcoe/v8-coverage@1.0.2': {}
'@blocknote/core@0.46.2(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)':
dependencies:
'@emoji-mart/data': 1.2.1
@@ -5207,6 +5253,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2))':
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@vitest/utils': 4.0.18
ast-v8-to-istanbul: 0.3.11
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
magicast: 0.5.2
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.18(@types/node@24.10.13)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.30.2)
'@vitest/expect@4.0.18':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -5283,6 +5343,12 @@ snapshots:
assertion-error@2.0.1: {}
ast-v8-to-istanbul@0.3.11:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
estree-walker: 3.0.3
js-tokens: 10.0.0
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -5734,6 +5800,8 @@ snapshots:
transitivePeerDependencies:
- '@noble/hashes'
html-escaper@2.0.2: {}
html-void-elements@3.0.0: {}
html-whitespace-sensitive-tag-names@3.0.1: {}
@@ -5779,8 +5847,23 @@ snapshots:
isomorphic.js@0.2.5: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
jiti@2.6.1: {}
js-tokens@10.0.0: {}
js-tokens@4.0.0: {}
js-yaml@4.1.1:
@@ -5925,6 +6008,16 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
magicast@0.5.2:
dependencies:
'@babel/parser': 7.29.0
'@babel/types': 7.29.0
source-map-js: 1.2.1
make-dir@4.0.0:
dependencies:
semver: 7.7.4
markdown-it@14.1.1:
dependencies:
argparse: 2.0.1

View File

@@ -3113,16 +3113,6 @@
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "uvwS0",
"name": "projChev",
"width": 12,
"height": 12,
"iconFontName": "chevron-down",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
},
{
"type": "icon_font",
"id": "t5WJJ",
@@ -3142,35 +3132,19 @@
"content": "Projects",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "ILCh9",
"name": "countProj",
"height": 20,
"fill": "$--secondary",
"cornerRadius": 9999,
"padding": [
0,
6
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "rz7yY",
"name": "projBadgeTxt",
"fill": "$--muted-foreground",
"content": "4",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "600"
}
]
"type": "icon_font",
"id": "FKwSS",
"name": "projChev",
"width": 14,
"height": 14,
"iconFontName": "chevron-down",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
}
]
},
@@ -3299,16 +3273,6 @@
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "N4Mv7",
"name": "expChev",
"width": 12,
"height": 12,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
},
{
"type": "icon_font",
"id": "sguhh",
@@ -3328,35 +3292,19 @@
"content": "Experiments",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "PCYKK",
"name": "countExp",
"height": 20,
"fill": "$--secondary",
"cornerRadius": 9999,
"padding": [
0,
6
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "UApqq",
"name": "expBadgeTxt",
"fill": "$--muted-foreground",
"content": "2",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "600"
}
]
"type": "icon_font",
"id": "ijaoq",
"name": "expChev",
"width": 14,
"height": 14,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
}
]
}
@@ -3406,16 +3354,6 @@
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "w2Xcb",
"name": "respChev",
"width": 12,
"height": 12,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
},
{
"type": "icon_font",
"id": "Em4ns",
@@ -3435,35 +3373,19 @@
"content": "Responsibilities",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "7DwTK",
"name": "countResp",
"height": 20,
"fill": "$--secondary",
"cornerRadius": 9999,
"padding": [
0,
6
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "NNRL5",
"name": "respBadgeTxt",
"fill": "$--muted-foreground",
"content": "3",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "600"
}
]
"type": "icon_font",
"id": "6hDdQ",
"name": "respChev",
"width": 14,
"height": 14,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
}
]
}
@@ -3513,16 +3435,6 @@
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "FaBKs",
"name": "respChev",
"width": 12,
"height": 12,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
},
{
"type": "icon_font",
"id": "I2J1R",
@@ -3542,35 +3454,19 @@
"content": "Procedures",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "KI7En",
"name": "countResp",
"height": 20,
"fill": "$--secondary",
"cornerRadius": 9999,
"padding": [
0,
6
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "xtE6F",
"name": "respBadgeTxt",
"fill": "$--muted-foreground",
"content": "3",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "600"
}
]
"type": "icon_font",
"id": "meEIL",
"name": "procChev",
"width": 14,
"height": 14,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
}
]
}
@@ -3620,16 +3516,6 @@
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "1IPeC",
"name": "peopleChev",
"width": 12,
"height": 12,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
},
{
"type": "icon_font",
"id": "jOmVa",
@@ -3649,35 +3535,19 @@
"content": "People",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "VOlRP",
"name": "countPeople",
"height": 20,
"fill": "$--secondary",
"cornerRadius": 9999,
"padding": [
0,
6
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "6tBQJ",
"name": "peopleBadgeTxt",
"fill": "$--muted-foreground",
"content": "8",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "600"
}
]
"type": "icon_font",
"id": "TkuS1",
"name": "peopleChev",
"width": 14,
"height": 14,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
}
]
}
@@ -3727,16 +3597,6 @@
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "h8PjY",
"name": "eventsChev",
"width": 12,
"height": 12,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
},
{
"type": "icon_font",
"id": "ELRMi",
@@ -3756,35 +3616,19 @@
"content": "Events",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "L5am7",
"name": "countEvents",
"height": 20,
"fill": "$--secondary",
"cornerRadius": 9999,
"padding": [
0,
6
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "WtvSC",
"name": "eventsBadgeTxt",
"fill": "$--muted-foreground",
"content": "5",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "600"
}
]
"type": "icon_font",
"id": "TZwK8",
"name": "eventsChev",
"width": 14,
"height": 14,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
}
]
}
@@ -3834,16 +3678,6 @@
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "icon_font",
"id": "oS2NT",
"name": "eventsChev",
"width": 12,
"height": 12,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
},
{
"type": "icon_font",
"id": "Q57kF",
@@ -3863,35 +3697,19 @@
"content": "Topics",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "kScKf",
"name": "countEvents",
"height": 20,
"fill": "$--secondary",
"cornerRadius": 9999,
"padding": [
0,
6
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "lhfW5",
"name": "eventsBadgeTxt",
"fill": "$--muted-foreground",
"content": "5",
"fontFamily": "Inter",
"fontSize": 10,
"fontWeight": "600"
}
]
"type": "icon_font",
"id": "nzAcm",
"name": "topicsChev",
"width": 14,
"height": 14,
"iconFontName": "chevron-right",
"iconFontFamily": "lucide",
"fill": "$--muted-foreground"
}
]
}

View File

@@ -41,5 +41,22 @@ export default defineConfig({
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.{test,spec}.{ts,tsx}',
'src/test/**',
'src/mock-tauri.ts',
'src/main.tsx',
],
thresholds: {
lines: 70,
functions: 70,
branches: 70,
statements: 70,
},
},
},
})