Compare commits

...

5 Commits

Author SHA1 Message Date
Test
14b5c34b94 docs: add ADR-0029, ADR-0030; update README index (guard — from commits 1ae1377, a59640) 2026-03-30 08:01:55 +02:00
Test
a7a61d9751 fix: resize handle fills full panel height via self-stretch
The ResizeHandle div was not stretching to the full height of its flex
row parent, making it only interactive in the top portion. Adding
self-stretch (align-self: stretch) ensures it fills the complete cross-axis
height of the container, so the user can drag at any vertical position.

Fixes: Properties panel resize handle active area.
2026-03-30 07:48:48 +02:00
Test
68066b857f fix: make breadcrumb bar draggable as window drag region
Add data-tauri-drag-region to the BreadcrumbBar container so users can
drag the window by clicking empty space in the breadcrumb bar. Tauri
automatically excludes interactive children (buttons, links) from the
drag region.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 05:59:37 +02:00
Test
858468aec6 fix: remove broken category nav strip from emoji picker
The row of group icons between the search bar and emoji grid looked like
selectable emoji but only scrolled to groups — confusing users who expected
clicking them to select an emoji. Remove the strip entirely so the picker
opens directly with search + categorized grid.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 03:56:40 +02:00
Test
67ac8db888 fix: shorten Pulse view time filter labels to save space
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 02:26:30 +02:00
8 changed files with 78 additions and 33 deletions

View File

@@ -0,0 +1,29 @@
---
type: ADR
id: "0029"
title: "Domain command builder pattern for useCommandRegistry"
status: active
date: 2026-03-30
---
## Context
`useCommandRegistry` was a 224-line "brain method" (CodeScene hotspot) that defined all command palette commands inline: navigation, note actions, git operations, view toggles, settings, type management, and filter controls. This monolithic structure scored 39 on CodeScene's complexity scale (target: ≤9.5 for hotspots), making it increasingly hard to add new commands without touching the central file.
## Decision
**Split command definitions into focused domain modules under `src/hooks/commands/`, each exporting a `build*Commands(config)` factory function. `useCommandRegistry` becomes a thin assembler that calls each builder and merges the results.** Domain modules: `navigationCommands`, `noteCommands`, `gitCommands`, `viewCommands`, `settingsCommands`, `typeCommands`, `filterCommands`. Shared types live in `commands/types.ts`; public API re-exported from `commands/index.ts`.
## Options considered
- **Option A** (chosen): Domain builder modules — each module owns its command shape and receives typed config. `useCommandRegistry` is pure assembly. All new files score 9.5810.0. Downside: more files to navigate.
- **Option B**: Split by file but keep one large hook calling sub-hooks — sub-hooks still need shared state passed down, similar coupling. No real complexity win.
- **Option C**: Register commands imperatively via a global registry — decouples callers entirely. Downside: harder to trace, no TypeScript inference at the registration site, over-engineering for current scale.
## Consequences
- Adding a new command means editing the relevant domain module (e.g. `noteCommands.ts`) only, not touching the assembler.
- Each domain module receives only the config it needs — explicit, typed interface, no hook dependency.
- `useCommandRegistry` reduced from 224 lines to a thin assembler.
- Pattern is consistent with the Rust commands/ module split (ADR-0030).
- Re-evaluation trigger: if command count grows to the point where the assembler itself becomes a complexity hotspot.

View File

@@ -0,0 +1,29 @@
---
type: ADR
id: "0030"
title: "Rust commands/ module split by domain"
status: active
date: 2026-03-30
---
## Context
`src-tauri/src/commands.rs` grew to 937 lines as Tauri command handlers accumulated for vault CRUD, git/GitHub sync, AI, system, and window operations. All commands shared a single file with no domain separation, making it hard to navigate, review, and extend. The file was a CodeScene hotspot dragging down overall code health.
## Decision
**Replace `commands.rs` with a `commands/` module split by domain: `vault.rs`, `git.rs`, `github.rs`, `ai.rs`, `system.rs`, and `mod.rs` (shared utilities + re-exports).** Each file owns the Tauri command handlers for its domain and the `#[cfg(desktop)]` / `#[cfg(mobile)]` stubs for platform-conditional availability. `mod.rs` is kept thin (≤100 lines) with no command logic — only re-exports and shared helpers (`expand_tilde`, `parse_build_label`).
## Options considered
- **Option A** (chosen): Domain-based module split — mirrors the TypeScript `hooks/commands/` pattern (ADR-0029). Each file is independently reviewable and scores well on code health. Downside: more files to navigate.
- **Option B**: Split by platform (`desktop.rs`, `mobile.rs`) — aligns with `#[cfg(...)]` guards but mixes domain concerns. Harder to find a specific command.
- **Option C**: Keep monolith but add section comments — zero file-count cost, but doesn't solve complexity or reviewability.
## Consequences
- `github.rs` separates GitHub OAuth/API commands from git sync commands (`git.rs`), matching the underlying Rust module split (`github/` vs `git/`).
- Platform stubs (`#[cfg(mobile)]` error returns) live alongside the desktop implementation in the same domain file.
- `mod.rs` re-exports all command functions so `lib.rs` `invoke_handler!` registration is unchanged.
- New Tauri commands go into the appropriate domain file; if no domain fits, create a new one rather than putting it in `mod.rs`.
- Re-evaluation trigger: if a single domain file (e.g. `vault.rs`) itself grows beyond ~300 lines and becomes a hotspot.

View File

@@ -82,4 +82,7 @@ proposed → active → superseded
| [0024](0024-cache-outside-vault.md) | Vault cache stored outside vault directory | active |
| [0025](0025-type-field-canonical.md) | type: as canonical field (replacing Is A:) | active |
| [0026](0026-props-down-no-global-state.md) | Props-down callbacks-up (no global state) | active |
| [0027](0027-dual-ai-architecture.md) | Dual AI architecture (API chat + CLI agent) | active |
| [0027](0027-dual-ai-architecture.md) | Dual AI architecture (API chat + CLI agent) | superseded |
| [0028](0028-cli-agent-only-no-api-key.md) | CLI agent only — no direct Anthropic API key | active |
| [0029](0029-domain-command-builder-pattern.md) | Domain command builder pattern for useCommandRegistry | active |
| [0030](0030-rust-commands-module-split.md) | Rust commands/ module split by domain | active |

View File

@@ -48,6 +48,14 @@ const defaultProps = {
onToggleDiff: vi.fn(),
}
describe('BreadcrumbBar — drag region', () => {
it('has data-tauri-drag-region on the container', () => {
const { container } = render(<BreadcrumbBar entry={baseEntry} {...defaultProps} />)
const bar = container.firstElementChild as HTMLElement
expect(bar.dataset.tauriDragRegion).toBeDefined()
})
})
describe('BreadcrumbBar — trash/restore', () => {
it('shows trash button for non-trashed note', () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onTrash={vi.fn()} onRestore={vi.fn()} />)

View File

@@ -168,6 +168,7 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({
}: BreadcrumbBarProps) {
return (
<div
data-tauri-drag-region
className="flex shrink-0 items-center justify-between"
style={{
height: 45,

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useEffect, useCallback } from 'react'
import { EMOJI_GROUPS, EMOJIS_BY_GROUP, GROUP_ICONS, GROUP_SHORT_LABELS, searchEmojis } from '../utils/emoji'
import { EMOJI_GROUPS, EMOJIS_BY_GROUP, GROUP_SHORT_LABELS, searchEmojis } from '../utils/emoji'
interface EmojiPickerProps {
onSelect: (emoji: string) => void
@@ -11,7 +11,6 @@ export function EmojiPicker({ onSelect, onClose }: EmojiPickerProps) {
const inputRef = useRef<HTMLInputElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const scrollRef = useRef<HTMLDivElement>(null)
const groupRefs = useRef<Map<string, HTMLDivElement>>(new Map())
useEffect(() => {
setTimeout(() => inputRef.current?.focus(), 50)
@@ -45,13 +44,6 @@ export function EmojiPicker({ onSelect, onClose }: EmojiPickerProps) {
onClose()
}, [onSelect, onClose])
const scrollToGroup = useCallback((group: string) => {
const el = groupRefs.current.get(group)
if (el && scrollRef.current) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, [])
const searchResults = search.trim() ? searchEmojis(search) : null
const isSearching = searchResults !== null
@@ -73,20 +65,6 @@ export function EmojiPicker({ onSelect, onClose }: EmojiPickerProps) {
data-testid="emoji-picker-search"
/>
</div>
{!isSearching && (
<div className="flex gap-0.5 border-b border-border px-2 py-1.5 overflow-x-auto">
{EMOJI_GROUPS.map(group => (
<button
key={group}
className="shrink-0 rounded px-1.5 py-1 text-base transition-colors hover:bg-secondary"
onClick={() => scrollToGroup(group)}
title={GROUP_SHORT_LABELS[group]}
>
{GROUP_ICONS[group]}
</button>
))}
</div>
)}
<div ref={scrollRef} className="max-h-[300px] overflow-y-auto p-2" data-testid="emoji-picker-grid">
{isSearching ? (
searchResults.length > 0 ? (
@@ -113,10 +91,7 @@ export function EmojiPicker({ onSelect, onClose }: EmojiPickerProps) {
const emojis = EMOJIS_BY_GROUP.get(group)
if (!emojis?.length) return null
return (
<div
key={group}
ref={el => { if (el) groupRefs.current.set(group, el) }}
>
<div key={group}>
<div className="sticky top-0 z-10 bg-popover px-1 pb-1 pt-2 text-[11px] font-medium text-muted-foreground">
{GROUP_SHORT_LABELS[group]}
</div>

View File

@@ -67,7 +67,7 @@ export function ResizeHandle({ onResize }: ResizeHandleProps) {
return (
<div
className="-ml-1 w-1 shrink-0 cursor-col-resize bg-transparent transition-colors hover:bg-[var(--border)]"
className="-ml-1 w-1 shrink-0 self-stretch cursor-col-resize bg-transparent transition-colors hover:bg-[var(--border)]"
onMouseDown={handleMouseDown}
/>
)

View File

@@ -8,10 +8,10 @@ interface InboxFilterPillsProps {
}
const PILLS: { value: InboxPeriod; label: string }[] = [
{ value: 'week', label: 'This week' },
{ value: 'month', label: 'This month' },
{ value: 'quarter', label: 'This quarter' },
{ value: 'all', label: 'All time' },
{ value: 'week', label: 'Week' },
{ value: 'month', label: 'Month' },
{ value: 'quarter', label: 'Quarter' },
{ value: 'all', label: 'All' },
]
function InboxFilterPillsInner({ active, counts, onChange }: InboxFilterPillsProps) {