Personal outliner built with Rust.
4

Configure Feed

Select the types of activity you want to include in your feed.

propose ui-polish: client-drawn chrome, Tabler icons, collapsible right sidebar hosting calendar and similar panels, bullet threading, eased programmatic scrolling, page title as heading

graham.systems (Jul 15, 2026, 11:28 AM -0700) 5d188508 aafdbf5e

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