diff --git a/openspec/changes/ui-polish/.openspec.yaml b/openspec/changes/ui-polish/.openspec.yaml new file mode 100644 index 0000000..4f63482 --- /dev/null +++ b/openspec/changes/ui-polish/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-15 diff --git a/openspec/changes/ui-polish/design.md b/openspec/changes/ui-polish/design.md new file mode 100644 index 0000000..1d8b4e4 --- /dev/null +++ b/openspec/changes/ui-polish/design.md @@ -0,0 +1,141 @@ +# Design: ui-polish + +## Context + +Current state (main.rs): the window opens with default `WindowOptions` (stock +OS titlebar); the header row holds text-glyph ◀/▶ buttons and a "Calendar" +button; Backlinks and Similar render as bottom-docked panels under the outline +(a code comment marks docked-vs-floating as an open question — this change +answers it: sidebar); the outline is a gpui `list` with custom-measured rows +and named layout knobs (line height 1.3, 32px/level indent, bullet column +derived from line height); back/forward already save/restore `ListState` +scroll offsets, applied as hard jumps; page views render the page title as the +root bullet, indenting all content one level under it. + +## Goals / Non-Goals + +**Goals:** + +- One visual system: client-drawn chrome, real icons, a sidebar that owns + reference/pick panels, hierarchy guides, eased motion, and page titles that + read as titles. +- Keep the outline's virtualized `list` rendering and the named-knob layout + style intact — every addition is expressed in those terms. + +**Non-Goals:** + +- New panel *content* (no new backlinks/graph/outline panels — the sidebar + ships hosting exactly Calendar and Similar; Backlinks migration is a + follow-up candidate). +- Wheel-input smoothing (native feel stays; only programmatic scrolls + animate). +- Theming/theme-switching work — colors stay the current palette. +- Zoomed-block views keep their current root rendering; whether a zoomed + block should get the heading treatment is an open question, decided + separately (see Open Questions). + +## Decisions + +### D1 — Chrome: transparent titlebar + app-drawn title row + +Use gpui's client-decoration path: `TitlebarOptions` with +`appears_transparent` (verify the exact gpui 0.2 API at implementation time — +Zed's Windows titlebar is the reference implementation), then render a +titlebar row as the top of the app: drag region across its width, +minimize/maximize/close caption controls on the right, and the existing +header controls (nav arrows, calendar/sidebar toggles) folded into it — one +row instead of stock-titlebar-plus-header. Double-click on the drag region +maximizes; Windows is the only target that must be correct (per README, +other platforms are untested). Risk hedge: if gpui 0.2's Windows caption +handling proves incomplete, the fallback is keeping the stock titlebar and +shipping everything else — chrome is severable. + +### D2 — Icons: bundle only the Tabler glyphs we use + +Tabler icons are MIT (license file ships alongside). Bundle just the needed +SVGs (arrow-left, arrow-right, calendar, sidebar toggle, chevron for +collapse, window caption glyphs if D1 wants them) under +`crates/trawler/assets/icons/`, exposed through an `AssetSource` registered +at app start (the same bundling spirit as the fonts — no system deps, +identical everywhere), rendered via gpui's `svg()` and tinted with the text +color so they follow the palette. No icon-font, no full icon set (Tabler is +5k+ icons; we take single-digit counts). + +### D3 — Sidebar: a host with registered panels, not a bespoke column + +A right-hand sidebar host owning: collapsed flag, width (drag-handle resize +with min/max clamps), and an ordered set of panels, each a title + render +function — Calendar and Similar are the two initial registrations. Toggle via +a titlebar button and a keybinding (`Ctrl+Shift+B`, matching the +sidebar-toggle convention elsewhere; final binding checked against the keymap +at implementation). Collapsed/width state persists in memory for the session; +on-disk persistence (a small local ui-state file — explicitly *not* in the +Loro doc, it's device state not graph data) is a stretch task. The calendar +keeps its popup behavior when invoked from `Ctrl+Shift+C` semantics — opening +the calendar now means revealing/focusing its sidebar panel. Similar moves +out of the bottom dock, resolving the docked-vs-floating open question; +Backlinks intentionally stays at the bottom this change (one migration at a +time — if the sidebar feels right, it follows). + +### D4 — Threading: per-row ancestor guides, virtualization-friendly + +Standard tree-guide algorithm, computed per row so it works inside the +virtualized `list`: for each visible row, for each ancestor level, draw a +vertical segment in that level's indent column if the ancestor chain +continues below (i.e. that ancestor has a later sibling, or the row itself +has following siblings/children at the innermost level). Segments render in +the existing indent columns (32px/level knob), hairline width, at a muted +color; the innermost segment connects to the parent's bullet position +(bullet column knob) so the line visually emanates from the bullet, +Logseq-style. New named knobs: guide color, guide width. Static guides only — +hover-highlighting a thread is a possible follow-up, not in scope. + +### D5 — Smooth scrolling: ease programmatic offsets only + +All programmatic scrolls (back/forward restore, follow-reference, zoom +in/out, scroll-to-focused-block) animate from the current `ListState` offset +to the target over a short ease-out (~180ms — named knob), driven by gpui's +animation frame path. Wheel/trackpad input remains untouched. A single named +constant (`SCROLL_ANIMATION_MS`) doubles as the reduced-motion switch: 0 +disables animation entirely, and the UI tests run with it at 0 so existing +scroll assertions stay deterministic. + +### D6 — Page title as heading + +Page roots stop rendering as a top-level bullet. Instead: the title renders +as a large heading (no bullet, no fold affordance, heading-scale text — new +named knobs for heading size/weight/spacing), and the page's children render +starting at indent level 0 — bullets at the left margin, content un-indented. +The heading remains the same one-hot editable block it is today (editing it +is still how a page rename works; journal date titles keep whatever +non-editable treatment they have now). Focus navigation (Up from the first +child) lands on the heading exactly as it lands on the root bullet today — +only rendering changes, no tree or focus-model changes. This is the severable +item: it touches row rendering and the UI tests' depth assertions, and can be +split into its own change at implementation time if it grows. + +## Risks / Trade-offs + +- [gpui 0.2 Windows client-decoration API may be incomplete] → D1's fallback: + ship without custom chrome; everything else is independent. +- [Threading cost per row in a virtualized list] → the guide computation is + O(depth) per visible row against already-materialized rows; depth is small + and rows are already custom-measured. No whole-tree pass. +- [Scroll animation vs. the list's logical offsets during edits] → animations + target logical offsets and cancel on user wheel input or a new programmatic + scroll; the edit-preserves-scroll path (`scroll_to(scroll_top)`) stays a + hard set, never animated. +- [Heading change breaks UI-test depth expectations] → the tests are updated + in the same change; the devtools `dump` depth field keeps reporting tree + depth (unchanged semantics), so only visual-layout assertions move. +- [Sidebar steals horizontal space from the outline on small windows] → + collapse is one keystroke, and the width clamp keeps a minimum outline + width. + +## Open Questions + +1. Should a zoomed-into block adopt the heading treatment (Logseq shows + breadcrumb + normal block)? Deferred — decide when using D6 daily. +2. Sidebar state persistence: session-only vs a local `ui-state.json` in the + graph dir (device-local, disposable). Stretch task either way. +3. Exact keybinding for the sidebar toggle, pending keymap conflict check. diff --git a/openspec/changes/ui-polish/proposal.md b/openspec/changes/ui-polish/proposal.md new file mode 100644 index 0000000..0e8dff4 --- /dev/null +++ b/openspec/changes/ui-polish/proposal.md @@ -0,0 +1,63 @@ +# Proposal: ui-polish + +## Why + +The MVP's chrome is functional but rough: a stock OS titlebar over a custom +app, text-glyph navigation arrows (◀ ▶), reference panels (Similar) docked +awkwardly at the bottom of the outline, no visual thread connecting a block +to its ancestors in deep outlines, hard scroll jumps on navigation, and +journal/page views that render the page title as the top bullet — indenting +all real content one level and making the title read as just another block. +These are individually small, collectively the difference between "works" +and "feels finished." + +## What Changes + +- **Custom window chrome**: client-drawn titlebar integrated with the app + header (drag region, caption controls), replacing the stock OS titlebar. +- **Tabler icons**: bundle the handful of needed icons (MIT-licensed SVGs) as + assets and replace the text glyphs — back/forward arrows first, plus + calendar and sidebar-toggle affordances. Presentation only. +- **Right-hand sidebar**: collapsible, resizable, hosting registered panels — + the calendar picker and the Similar panel move into it (Backlinks stays put + for now, an obvious future panel). Keyboard toggle; state remembered. +- **Bullet threading**: Logseq-style vertical guide lines in the indent + columns connecting each parent bullet to its children, making hierarchical + position legible at depth. +- **Smooth scrolling**: programmatic scrolls (navigation restore, follow-ref, + zoom, scroll-to-focus) animate with a short ease instead of jumping; wheel + input stays native. +- **Page title as heading** (severable — flagged by the proposer as possibly + its own change; kept here since it's pure rendering, but cleanly splittable + at implementation time): page roots render as a large heading instead of a + top-level bullet, with the page's children starting at the first indent + level — the Logseq treatment. Applies to journal date pages and named pages + alike. + +## Capabilities + +### New Capabilities + +- `app-chrome`: the window shell — custom titlebar and the right sidebar + (collapse/resize/panel hosting). + +### Modified Capabilities + +- `outline-editor`: adds rendering requirements for bullet threading, animated + programmatic scrolling, and page-root-as-heading layout. + +_(Moving the calendar and Similar panels is unspecced presentation — the +search spec's "Similar blocks" requirement mandates behavior, not placement — +so no `search`/`journal`/`graph-navigation` deltas are needed.)_ + +## Impact + +- `crates/trawler/src/main.rs` (and likely new modules split out of it): + titlebar element, sidebar host + panels, row rendering (threading, heading), + scroll animation. `WindowOptions` changes at startup. +- `crates/trawler/assets/`: bundled Tabler SVGs + license file; an + `AssetSource` registration so gpui's `svg()` can resolve them. +- Devtools `dump` gains sidebar state (additive — the dump spec's field list + is a minimum), so UI tests can assert sidebar behavior. +- UI tests: existing row-layout assertions will need updating where the + heading/threading changes row structure. diff --git a/openspec/changes/ui-polish/specs/app-chrome/spec.md b/openspec/changes/ui-polish/specs/app-chrome/spec.md new file mode 100644 index 0000000..8f7b2d8 --- /dev/null +++ b/openspec/changes/ui-polish/specs/app-chrome/spec.md @@ -0,0 +1,40 @@ +# app-chrome Specification + +## ADDED Requirements + +### Requirement: Client-drawn window chrome +The application window SHALL present a client-drawn titlebar integrated with +the application header — a drag region, window caption controls +(minimize/maximize/close), and the header's navigation controls in one row — +in place of the stock OS titlebar. Standard window gestures (drag to move, +double-click to maximize/restore) MUST keep working. + +#### Scenario: Window remains manageable +- **WHEN** the user drags the titlebar row or double-clicks its empty region +- **THEN** the window moves or toggles maximized exactly as with a native + titlebar, and the caption controls minimize, maximize/restore, and close + the window + +### Requirement: Collapsible right sidebar hosting panels +The application SHALL provide a right-hand sidebar that hosts named panels — +initially the calendar picker and the Similar-blocks panel — and SHALL be +collapsible via a visible control and a keyboard shortcut, and resizable by +dragging its edge within clamped bounds. Collapsing the sidebar MUST NOT +discard panel state. Sidebar visibility and width SHALL be remembered for at +least the duration of the session. + +#### Scenario: Panels live in the sidebar +- **WHEN** the sidebar is open with a block focused +- **THEN** the calendar and the Similar-blocks list render as sidebar panels, + and the Similar panel no longer renders at the bottom of the outline + +#### Scenario: Toggle from the keyboard +- **WHEN** the user presses the sidebar toggle shortcut twice +- **THEN** the sidebar collapses and reopens at its prior width with its + panels intact, without moving editor focus + +#### Scenario: Calendar invocation targets the sidebar +- **WHEN** the user invokes the calendar (shortcut or button) while the + sidebar is collapsed +- **THEN** the sidebar opens revealing the calendar panel, and day selection + navigates to (or creates) that day's journal page exactly as before diff --git a/openspec/changes/ui-polish/specs/outline-editor/spec.md b/openspec/changes/ui-polish/specs/outline-editor/spec.md new file mode 100644 index 0000000..9033266 --- /dev/null +++ b/openspec/changes/ui-polish/specs/outline-editor/spec.md @@ -0,0 +1,51 @@ +# outline-editor Delta: ui-polish + +## ADDED Requirements + +### Requirement: Hierarchy guides +Rendered outline rows SHALL display vertical guide lines in their indent +columns connecting each parent's bullet to its descendant rows, so a block's +ancestor chain is visually traceable at any depth. Guides MUST render +correctly under virtualization (computed per visible row, no whole-tree +pass) and respect the outline's named layout knobs. + +#### Scenario: Deep block is visually anchored +- **WHEN** a block nested four levels deep is visible +- **THEN** each of its four indent columns shows a guide segment exactly when + the corresponding ancestor chain continues at that level, and the innermost + guide connects toward its parent's bullet + +### Requirement: Animated programmatic scrolling +Programmatic scroll changes (navigation history restore, follow-reference, +zoom, scroll-to-focused-block) SHALL animate to the target offset with a +short easing rather than jumping. User wheel/trackpad input MUST NOT be +altered and MUST cancel any in-flight animation. Setting the animation +duration knob to zero SHALL disable animation entirely (reduced motion, and +the deterministic mode used by UI tests). Scroll preservation during edits +MUST remain instantaneous, never animated. + +#### Scenario: Back-navigation glides +- **WHEN** the user navigates back to a view whose saved scroll offset + differs from the current one +- **THEN** the outline animates to the saved offset over the configured + duration, and a wheel event during the animation stops it at the user's + control + +### Requirement: Page root renders as a heading +A page's root SHALL render as a large heading — no bullet, no fold +affordance — with the page's children rendered from the first indent level +(bullets at the left margin). The heading remains the page's editable root +block: focus, editing, and rename behavior are unchanged from the current +root bullet. Tree depth semantics (including devtools dump depth) are +unaffected — this is rendering only. + +#### Scenario: Journal page reads as a titled document +- **WHEN** a journal date page is displayed +- **THEN** the date renders as a heading with the day's blocks starting + un-indented beneath it, one indent level shallower than the previous + title-as-bullet rendering + +#### Scenario: Heading is still the root block +- **WHEN** the user presses Up from the first child block +- **THEN** focus lands on the heading exactly as it previously landed on the + root bullet, and editing it behaves as editing the root block always has diff --git a/openspec/changes/ui-polish/tasks.md b/openspec/changes/ui-polish/tasks.md new file mode 100644 index 0000000..fc87255 --- /dev/null +++ b/openspec/changes/ui-polish/tasks.md @@ -0,0 +1,40 @@ +## 1. Icons and asset plumbing + +- [ ] 1.1 Add `AssetSource` registration at app start; bundle selected Tabler SVGs under `crates/trawler/assets/icons/` with the MIT license file +- [ ] 1.2 Replace the ◀/▶ header glyphs with Tabler arrow icons (tinted via text color); add calendar and sidebar-toggle icons + +## 2. Window chrome (design D1) + +- [ ] 2.1 Switch `WindowOptions` to a transparent/client titlebar (verify exact gpui 0.2 API; Zed's Windows titlebar is the reference); render the titlebar row: drag region, caption controls, existing header controls folded in +- [ ] 2.2 Verify drag-to-move, double-click maximize/restore, minimize/close on Windows via dev-loop; if gpui's caption handling is incomplete, fall back to stock titlebar and record the finding in this file + +## 3. Sidebar (design D3) + +- [ ] 3.1 Sidebar host: collapsed flag, clamped drag-resize width, ordered panel registrations (title + render fn); session-persistent state +- [ ] 3.2 Move the calendar picker into a sidebar panel; calendar invocation (shortcut/button) opens the sidebar and reveals it; day-click behavior unchanged +- [ ] 3.3 Move the Similar panel from the bottom dock into the sidebar; remove the bottom-dock rendering; Backlinks stays put +- [ ] 3.4 Toggle keybinding (check keymap for conflicts; design suggests Ctrl+Shift+B) + titlebar toggle button +- [ ] 3.5 Extend the devtools `dump` with sidebar state (open/width/panels) — additive field +- [ ] 3.6 UI tests: toggle preserves panel state and editor focus; calendar invocation reveals panel; similar panel renders in sidebar + +## 4. Bullet threading (design D4) + +- [ ] 4.1 Per-row ancestor-guide computation (segment per indent column where the chain continues below) and rendering in the indent columns; named knobs for guide color/width; innermost segment meets the parent bullet position +- [ ] 4.2 UI test / dump-based assertions over a fixture page with known nesting; visual check via dev-loop screenshots at several depths + +## 5. Smooth scrolling (design D5) + +- [ ] 5.1 Scroll animator over `ListState` logical offsets: ease-out over `SCROLL_ANIMATION_MS` (named knob, 0 = disabled), cancelled by wheel input or a superseding programmatic scroll +- [ ] 5.2 Route back/forward restore, follow-reference, zoom, and scroll-to-focus through the animator; leave edit-time scroll preservation as a hard set +- [ ] 5.3 UI tests run with the knob at 0; add one animation test asserting the offset converges to target and wheel cancels + +## 6. Page title as heading (design D6 — severable) + +- [ ] 6.1 Render page roots as heading rows (no bullet/fold affordance; heading-scale named knobs); children start at indent level 0 +- [ ] 6.2 Preserve focus/edit/rename behavior on the heading row (Up from first child lands on it; editing = root-block editing) +- [ ] 6.3 Update UI-test layout assertions for the shallower indent; confirm devtools dump depth semantics unchanged; dev-loop screenshot of a journal page before/after + +## 7. Verification and docs + +- [ ] 7.1 `cargo clippy --workspace --all-targets -- -D warnings` and `cargo test --workspace` pass +- [ ] 7.2 Full dev-loop pass: screenshots of chrome, sidebar open/collapsed, threading at depth, journal heading; README keyboard-reference table updated (sidebar toggle, calendar behavior)