Compare commits
2 Commits
v0.2026041
...
v0.2026041
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27e371c7fe | ||
|
|
867e97b199 |
88
README.md
88
README.md
@@ -3,3 +3,91 @@
|
||||
Personal knowledge and life management desktop app built with Tauri v2 + React + TypeScript + BlockNote.
|
||||
|
||||
## Documentation
|
||||
|
||||
- 📐 [ARCHITECTURE.md](docs/ARCHITECTURE.md) — System design, tech stack, data flow
|
||||
- 🧩 [ABSTRACTIONS.md](docs/ABSTRACTIONS.md) — Core abstractions and models
|
||||
- 🚀 [GETTING-STARTED.md](docs/GETTING-STARTED.md) — How to navigate the codebase
|
||||
- 🎨 [THEMING.md](docs/THEMING.md) — Theme system and customization
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm 8+
|
||||
- Rust (latest stable)
|
||||
- macOS (for development)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run dev server
|
||||
pnpm dev
|
||||
|
||||
# Open in browser (mock mode)
|
||||
open http://localhost:5173
|
||||
|
||||
# Or run in Tauri
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Frontend tests
|
||||
pnpm test
|
||||
|
||||
# Backend tests
|
||||
cargo test
|
||||
|
||||
# Coverage
|
||||
pnpm test:coverage
|
||||
|
||||
# E2E tests
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
# Lint
|
||||
pnpm lint
|
||||
|
||||
# Rust checks
|
||||
cargo clippy
|
||||
cargo fmt --check
|
||||
|
||||
# CodeScene (via Claude Code)
|
||||
claude 'Check code health with CodeScene MCP'
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
See [AGENTS.md](AGENTS.md) for coding guidelines and workflow. [CLAUDE.md](CLAUDE.md) remains as a compatibility shim for Claude Code.
|
||||
|
||||
**Key principles:**
|
||||
- Small, atomic commits
|
||||
- Test as you go
|
||||
- Visual verification mandatory
|
||||
- Documentation updated with code changes
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions runs on every push to `main`:
|
||||
- ✅ Tests (frontend + Rust)
|
||||
- 📊 Coverage (70% threshold)
|
||||
- 🎨 Lint & format
|
||||
- ⚠️ Documentation check
|
||||
|
||||
See [.github/SETUP.md](.github/SETUP.md) for CI/CD configuration.
|
||||
|
||||
## Git Hooks
|
||||
|
||||
Husky installs the git hooks from `.husky/` during `pnpm install`. The repo enforces `main`-only commits and pushes; see [.github/HOOKS.md](.github/HOOKS.md) for details.
|
||||
|
||||
## License
|
||||
|
||||
Private repository — not licensed for public use.
|
||||
|
||||
@@ -48,6 +48,11 @@ describe('ColorSwatch', () => {
|
||||
})
|
||||
|
||||
describe('ColorEditableValue', () => {
|
||||
it('left-aligns the text display in view mode', () => {
|
||||
render(<ColorEditableValue value="#3b82f6" isEditing={false} onStartEdit={vi.fn()} onSave={vi.fn()} onCancel={vi.fn()} />)
|
||||
expect(screen.getByText('#3b82f6')).toHaveClass('text-left')
|
||||
})
|
||||
|
||||
it('shows swatch when value is a valid hex color', () => {
|
||||
render(<ColorEditableValue value="#3b82f6" isEditing={false} onStartEdit={vi.fn()} onSave={vi.fn()} onCancel={vi.fn()} />)
|
||||
expect(screen.getByTestId('color-swatch')).toBeTruthy()
|
||||
|
||||
@@ -102,7 +102,7 @@ export function ColorEditableValue({ value, isEditing, onStartEdit, onSave, onCa
|
||||
<span className="inline-flex h-6 min-w-0 items-center gap-1.5">
|
||||
{showSwatch && <ColorSwatch color={value} onChange={handlePickerChange} />}
|
||||
<span
|
||||
className="min-w-0 cursor-pointer truncate rounded px-1 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
className="min-w-0 cursor-pointer truncate rounded px-1 text-left text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
onClick={onStartEdit}
|
||||
title={value || 'Click to edit'}
|
||||
>
|
||||
|
||||
@@ -129,6 +129,30 @@ describe('DynamicPropertiesPanel', () => {
|
||||
expect(screen.getByText('Luca')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('left-aligns mixed property value displays', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
entry={makeEntry()}
|
||||
content=""
|
||||
frontmatter={{
|
||||
Owner: 'Luca',
|
||||
History_confidence: 0.84,
|
||||
Date: '2026-04-11',
|
||||
icon: 'rocket',
|
||||
color: '#3b82f6',
|
||||
Window_end: null,
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Luca').parentElement).toHaveClass('justify-start', 'text-left')
|
||||
expect(screen.getByText('0.84').parentElement).toHaveClass('justify-start', 'text-left')
|
||||
expect(screen.getByTestId('date-display')).toHaveClass('text-left')
|
||||
expect(screen.getByTestId('icon-editable-display')).toHaveClass('text-left')
|
||||
expect(screen.getByText('#3b82f6')).toHaveClass('text-left')
|
||||
expect(screen.getByText('\u2014').parentElement).toHaveClass('justify-start', 'text-left')
|
||||
})
|
||||
|
||||
it('hides Owner with wikilink value from Properties panel', () => {
|
||||
render(
|
||||
<DynamicPropertiesPanel
|
||||
|
||||
@@ -42,6 +42,11 @@ describe('EditableValue', () => {
|
||||
expect(value).toHaveClass('truncate')
|
||||
})
|
||||
|
||||
it('left-aligns plain text values in view mode', () => {
|
||||
render(<EditableValue value="Active" onSave={onSave} onCancel={onCancel} isEditing={false} onStartEdit={onStartEdit} />)
|
||||
expect(screen.getByText('Active').parentElement).toHaveClass('justify-start', 'text-left')
|
||||
})
|
||||
|
||||
it('shows input in editing mode', () => {
|
||||
render(<EditableValue value="Active" onSave={onSave} onCancel={onCancel} isEditing={true} onStartEdit={onStartEdit} />)
|
||||
const input = screen.getByDisplayValue('Active')
|
||||
@@ -238,6 +243,11 @@ describe('UrlValue', () => {
|
||||
expect(screen.getByTestId('url-link')).toHaveTextContent('https://example.com')
|
||||
})
|
||||
|
||||
it('left-aligns URL values in view mode', () => {
|
||||
render(<UrlValue value="https://example.com" onSave={onSave} onCancel={onCancel} isEditing={false} onStartEdit={onStartEdit} />)
|
||||
expect(screen.getByTestId('url-link')).toHaveClass('justify-start', 'text-left')
|
||||
})
|
||||
|
||||
it('opens URL via openExternalUrl on click', () => {
|
||||
render(<UrlValue value="https://example.com" onSave={onSave} onCancel={onCancel} isEditing={false} onStartEdit={onStartEdit} />)
|
||||
fireEvent.click(screen.getByTestId('url-link'))
|
||||
|
||||
@@ -66,7 +66,7 @@ export function UrlValue({
|
||||
return (
|
||||
<span className="group/url flex w-full min-w-0 items-center gap-1">
|
||||
<span
|
||||
className="inline-flex h-6 min-w-0 flex-1 cursor-pointer items-center justify-end overflow-hidden rounded-md px-2 text-right text-[12px] text-[var(--accent-blue)] underline decoration-[var(--accent-blue)]/40 transition-colors hover:decoration-[var(--accent-blue)]"
|
||||
className="inline-flex h-6 min-w-0 flex-1 cursor-pointer items-center justify-start overflow-hidden rounded-md px-2 text-left text-[12px] text-[var(--accent-blue)] underline decoration-[var(--accent-blue)]/40 transition-colors hover:decoration-[var(--accent-blue)]"
|
||||
onClick={handleOpen}
|
||||
title={value}
|
||||
data-testid="url-link"
|
||||
@@ -125,7 +125,7 @@ export function EditableValue({
|
||||
|
||||
return (
|
||||
<span
|
||||
className="inline-flex h-6 w-full min-w-0 cursor-pointer items-center justify-end overflow-hidden rounded-md px-2 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
className="inline-flex h-6 w-full min-w-0 cursor-pointer items-center justify-start overflow-hidden rounded-md px-2 text-left text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
onClick={onStartEdit}
|
||||
title={value || 'Click to edit'}
|
||||
>
|
||||
|
||||
@@ -22,6 +22,20 @@ function renderIconValue(overrides: Partial<React.ComponentProps<typeof IconEdit
|
||||
}
|
||||
|
||||
describe('IconEditableValue', () => {
|
||||
it('left-aligns the icon display in view mode', () => {
|
||||
render(
|
||||
<IconEditableValue
|
||||
value="rocket"
|
||||
onSave={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
isEditing={false}
|
||||
onStartEdit={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('icon-editable-display')).toHaveClass('text-left')
|
||||
})
|
||||
|
||||
it('shows searchable icon results with previews while editing', () => {
|
||||
renderIconValue()
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ export function IconEditableValue({
|
||||
|
||||
return (
|
||||
<span
|
||||
className="inline-flex h-6 min-w-0 max-w-full cursor-pointer items-center truncate rounded-md px-2 text-right text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
className="inline-flex h-6 min-w-0 max-w-full cursor-pointer items-center truncate rounded-md px-2 text-left text-[12px] text-secondary-foreground transition-colors hover:bg-muted"
|
||||
onClick={onStartEdit}
|
||||
title={value || 'Click to edit'}
|
||||
data-testid="icon-editable-display"
|
||||
|
||||
@@ -174,7 +174,7 @@ function DateValue({ value, onSave, autoOpen = false, onCancel }: {
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={`inline-flex h-6 min-w-0 cursor-pointer items-center gap-1 border-none px-2 text-right text-[12px] font-medium transition-colors hover:opacity-80${formatted ? ' rounded-md bg-muted text-accent-foreground' : ' bg-transparent text-muted-foreground'}`}
|
||||
className={`inline-flex h-6 min-w-0 cursor-pointer items-center gap-1 border-none px-2 text-left text-[12px] font-medium transition-colors hover:opacity-80${formatted ? ' rounded-md bg-muted text-accent-foreground' : ' bg-transparent text-muted-foreground'}`}
|
||||
title={value}
|
||||
data-testid="date-display"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user