Review-surface polish: window chrome, review-exit guard, theming, code blocks (attn-rd3j) - #12
Draft
angusbezzina wants to merge 50 commits into
Draft
Review-surface polish: window chrome, review-exit guard, theming, code blocks (attn-rd3j)#12angusbezzina wants to merge 50 commits into
angusbezzina wants to merge 50 commits into
Conversation
…code blocks (attn-rd3j) Nine reported defects in the native app. Three of them shared one root cause and two were misdiagnosed until measured; the notes below record what was actually wrong, since the fixes only make sense against it. Document typography was global, not scoped (attn-rd3j.3/.4/.5). Bare p/h1/ul/li selectors in base.css typed the whole page, so app chrome inherited reading-surface margins and the document's custom checkbox rule drew a checkmark pinned at left:0 inside the Share dialog — the "weird modal checkmark" was never an element in ShareDialog. Typography is now scoped to .attn-doc (carried by the editor mount and the viewer article) and the .attn-chrome opt-out class, which existed purely to undo the leak, is gone. Bullets were a faked absolutely-positioned ::before dot with no list-style fallback, so they vanished whenever the positioning context shifted; they are real ::marker glyphs now, which cannot detach from their line. The table "rail" was 13px of stolen layout (attn-rd3j.8). base.css styles ::-webkit-scrollbar globally, and in WKWebView styling it at all downgrades that element from macOS overlay scrollbars to classic space-reserving ones. Measured alternatives: scrollbar-width:auto = 17px, webkit revert = 17px, hidden = 0px — nothing restores overlay behavior. Prose blocks now hide the bar and scroll by trackpad, the treatment PathBreadcrumb already used. The rules must sit outside @layer components, because base.css's scrollbar rules are deliberately unlayered and unlayered beats layered. Review-exit membership was answered from the wrong set (attn-rd3j.2). ownerRoomForPath resolves a file to a room through the share ROOT, which for a multi-file share is the whole project — so every file "belonged" to the review. Added roomPublishesPath, which answers from published snapshots and reconciles relative snapshot paths against absolute nav paths. The same confusion is fixed in the owner auto-follow effect, which was re-selecting the room right after an explicit exit and putting review chrome on files that were never shared; it now does what its docstring already claimed. BEHAVIOR CHANGE: opening an unshared file in a shared project turns collaboration chrome off instead of leaving the chip and rail on. Syntax highlighting was never a Rust concern (attn-rd3j.10). src/markdown.rs renders no HTML and comrak's syntect feature is off by design — client-side shiki is the intended architecture. The gaps were a hardcoded 20-language allowlist that silently dropped everything else, and untagged fences getting zero decorations. Languages now resolve against shiki's full bundle with on-demand loading, and untagged fences get conservative content-based detection (confident-match-or-nothing; JSON verified by parsing it). Also: a zoom_window IPC so double-clicking the hidden titlebar zooms and restores, attached to every existing drag surface (attn-rd3j.1); the code copy button and language label moved onto a non-scrolling frame so they stay pinned over wide blocks (attn-rd3j.9); dialog bodies, the project switcher and other chrome scroll through the shared ScrollArea (attn-rd3j.5); and a Settings dialog with three-state appearance (Paper/Ink/System, durable and stamped before first paint so there is no flash of the wrong theme) plus shadcn-style typeset presets (attn-rd3j.6/.7). Verified by driving the running app, not just the tests: dblclick zoomed 960x720 -> 1512x887 -> back with buttons excluded; the copy button moved 0px while content scrolled 106px; reserved scrollbar space went 13px -> 0 on every overflowing block; the exit prompt fires, cancel preserves, confirm tears down cleanly and re-entering re-activates; system->dark resolved on a dark-mode Mac and a manual Paper choice survived a daemon restart. That pass also caught a defect of its own — the selected Appearance segment used bg-background over bg-muted/30 and was invisible in dark mode. 97 web test files and 1207 Rust tests pass (17 new unit tests). test-e2e and test-review-e2e are byte-identical to the pre-change baseline, confirmed by stashing and re-running — their failures are pre-existing. Release binary 32.09/40 MiB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
All three were already failing on main; the CI quality job runs
`cargo fmt --check` and `cargo clippy --all-targets -- -D warnings`, and both
gates were red before this branch.
- apply.rs carried a duplicate `use serde::{Deserialize, Serialize}` inside
`mod tests`, unindented at column 0. The test module derives via the fully
qualified `serde::Deserialize`, so the import was dead — and its indentation
was also what `cargo fmt --check` was failing on.
- publish_snapshot_plaintext takes 8 arguments. Allowed with a rationale,
matching the ten existing precedents (two in this same file): the argument
list IS the identity of a published snapshot, so a params struct would move
the same list one level away from the call site without simplifying it.
- The drag-drop handler's nested `if let` collapses into a let-chain, the form
already used elsewhere in this codebase.
Verified: cargo fmt --check clean, cargo clippy --all-targets -D warnings
clean, cargo test --locked green (1207), cargo build --locked green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hm (attn-rd3j.11) Four follow-ups from review of the branch. The Share dialog's file picker was capped at a fixed max-h-72. Added to the surrounding chrome that exceeded the dialog's 85vh ceiling, so the whole modal scrolled and the create button sat below the fold. The cap is now relative to the WINDOW — min(20rem, 34vh) — so list plus chrome always fits and only the list scrolls. Two approaches were tried and rejected first: flex-1 on the list collapses it to its minimum, because the dialog's own height is auto and basis-0 children make an auto-height flex container size to minimums; pinning the ScrollArea viewport with `absolute inset-0` then takes it out of flow, so the root has no intrinsic height and cannot be sized either way. Tables render inside prosemirror-tables' `.tableWrapper`, not the schema's `.prose-scroll-x` — its columnResizing plugin installs a TableView that replaces the schema's DOM. This stylesheet is hand-rolled and deliberately does not import prosemirror-tables' CSS, so that wrapper arrived with NO styles at all: unbounded width, no horizontal scrolling, no frame. It now gets the same treatment as every other wide block, which is also what makes the scrollbar-rail fix (attn-rd3j.8) actually reach tables. Code blocks and tables had zero vertical margin — the `pre` carries an inline margin:0 and the frame had none — leaving them sandwiched between the heading above and the paragraph below. Wide blocks are bordered cards, not running prose, so they take 1.5rem of block margin. The sidebar's filter empty state sat flush against its container while every sibling row is inset 10px. Settings moved from a floating bottom-right cog into the header, immediately right of the share control, and the resident/mute controls merged into the Settings dialog alongside appearance and typeset — one settings surface rather than two. The launch-at-login status listener moved up into App: a dialog is unmounted while closed, so a listener living there would drop every result the user is not watching and reopen showing startup state. Verified live: 30-file share dialog no longer scrolls its body (title and create button both on screen, list scrolls internally at 245px); an overflowing 12-column table scrolls with 0px reserved rail and stays inside the content column; code blocks sit 23px clear of neighbours; empty state insets 10px on both sides; header settings opens and switches theme. 97 web test files pass, clippy and fmt clean, e2e and review-e2e identical to the pre-change baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…of desk payload (attn-n01r) First implementation pass over the attn-n01r remediation epic. attn-n01r.22 — dark-mode visitors saw ~1.2s of paper-white before INK applied. The hosted entries carry blocking stylesheets, so CSS painted the PAPER ground long before the deferred module ran initTheme(), and no stylesheet carried a prefers-color-scheme fallback. Restored the native app's pre-paint stamp, admitted by CSP source hash rather than a nonce: one script, one hash, injected into all three entries by a Vite plugin so they cannot drift. csp.test.ts recomputes the hash and fails if it does — without that guard a drifted hash silently blocks the script and the flash returns with no error anywhere. Measured: first painted frame is now INK, zero wrong-theme frames. attn-n01r.41 — the desk shipped 1.20MB, including bits-ui and the dialog set for a component it never renders. EditorShell is now loaded on demand and the markdown serializer is imported at its call site. 1,203.5KB -> 1,000.8KB. The remaining ~600KB needs the read-only desk service split from the editor runtime; commitNow() is synchronous on the autosave path and deferring the parser there would change collab commit ordering. Left open with the analysis. attn-n01r.43 — 200% text forced 612px of layout into a 390px viewport and clipped the header. Four causes: a fixed 64px grid row, a grid column sized by its widest child, a header that could not wrap, and two unbreakable strings. Now clean at 320/390/768/1280/1920 at both text sizes. attn-n01r.1 — the join panel sat exactly on the list label (0.00px gap) because .folio-label owned no margin and borrowed it from .quick-actions via sibling collapse. Both now own their spacing. 0.00px -> 40.00px. attn-n01r.7 — every file gets an icon. Removed MARKDOWN_NO_ICON and the includeMarkdown flag rather than defaulting it, since neither call site wanted suppression once they agreed. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ntom tokens (attn-n01r.42) --rust-deep aliases --primary-hover, so every delete/error surface on the desk rendered steel blue in the dark theme — the INK desk had no red at all. The six destructive surfaces now use --danger-ink, which is the text-safe tier and flips correctly: oklch(0.45 0.18 27) in PAPER, oklch(0.70 0.16 25) in INK. --radius-md, --radius-lg and --wash were consumed on the hosted routes but declared only in src/app.css, inside a Tailwind @theme block the hosted entries never import. Undefined custom properties fail silently, so four components rendered square corners and one hover state had no background, with no error anywhere. Declared in chrome.css beside the other hosted aliases, derived from the same --radius so they cannot drift. Also: the three inline error <p> styles became the .form-error class they were copied from; .desk-title and .storage-panel hairlines moved from solid --ink to --rule (they rendered as the brightest element on the dark page); and .workspace-row .local-badge re-asserts its colour after a specificity collision made 'Shared' indistinguishable from 'Local only'. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….23)
At 200% text the home page forced 574px of layout into a 390px viewport and
clipped the nav, taking the primary CTA out of reach. Five causes, only two of
which were in the original finding — the rest surfaced by re-measuring after
each fix:
- Four grids used bare 1fr, which is minmax(auto, 1fr) and floors at
min-content. Now minmax(0, ...), matching .local-note which already did this.
- .site-nav used a fixed height and so clipped its own contents (box 70 vs
content 79). Now min-height.
- .site-nav and .nav-right could not wrap, setting a width floor wider than the
viewport. This was the single largest contributor.
- Display headings could not break a long word ('documents.'), overflowing 46px.
- The install command is one unbreakable token; .code now scrolls internally
per DESIGN.md's Wide-Sheet Rule, and .footer-links wraps.
Clean at 320/375/390/414/768/1024/1280/1920 at both 100% and 200% text.
97/97 unit test files pass; svelte-check 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1r.12) Eight controls missed 44x44 and four missed even the WCAG 2.2 AA 24x24 floor. The pattern was clean: .button already sets min-height 46px and everything using it passed, so every miss was a control outside that class — nav links, the brand, the copy buttons, the footer links. The mobile override was also dropping the nav CTA to 40px, below .button's own floor, at exactly the width where it matters most. Now zero controls under 24x24 at any breakpoint. The two short footer text links remain under 44 wide at full 44 height; that is 2.5.5 AAA guidance rather than the AA bar, and a min-width there would gap the row without helping acquisition. Footer links also gained --ink and an underline — they had been byte-identical to the adjacent licence text with no underline, so nothing at rest marked them as links. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…l badge (attn-n01r.19/.24/.25) Structural accessibility on the home page, all verified by live AX and keyboard probe rather than by inspection: - Added a skip link as the first Tab stop; previously stops 0-6 were all navigation with no way past them, and <main> carried an id nothing linked to. - Wrapped the nav in <header>. Landmarks measured navigation -> main -> contentinfo with no banner, so the brand, toggle and CTA sat outside every landmark. - The three entry-strip <h2> card labels are now <strong>. They are the names of links, not sections; the outline went from 12 headings (six top-level sections, three of them buttons) to a clean 9. - 'Start here' moved from ::after into real markup inside a flex row. As generated content it was skipped by translation and in-page search and landed last in a 100-character link name; being absolutely positioned it also reserved no space, which is what made it sit on top of the eyebrow between 681 and 730px. Measured at 690: -6px overlap -> +12px gap. - The theme toggle now exposes aria-pressed and an action-naming label; it was static across both states with both icons aria-hidden. - Removed the aria-label from .product-stage: it is a bare div, so the string was discarded by the AX tree. A label that reads as coverage while doing nothing is worse than none. - Entry cards carry aria-labelledby so the link list is scannable. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Escape (attn-n01r.30) The worst defect here was data-loss-grade: Rename and Delete carried no accessible context, so tabbing a multi-workspace desk announced 'Rename button, Delete button, Rename button...' with no way to tell which workspace was about to be irreversibly deleted. Both now name their target. The rows are a real <ul>/<li> labelled by the section heading, and 'Recently on this device' is an <h2> rather than a styled div — the populated desk had been exposing less structure than the empty one, which had a heading and it did not. The delete confirm announced role=alertdialog but never behaved like one: focus was never moved into it and Escape did not dismiss it, so a screen-reader user was told a dialog opened and then found focus still on the button behind it. It now moves focus in, restores it to the invoking button on cancel, and closes on Escape. A shell-level handler closes the topmost layer — confirm first, then the join panel — and closeJoin restores focus to its trigger. The join tile also exposes aria-expanded/aria-controls. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…re they matter (attn-n01r.2)
Copy on the install commands and Rename/Delete on every desk row were carrying
their labels as text. At mobile width two stacked commands each had a 'Copy'
competing with the command itself, and on the desk the three most-repeated words
on the screen were 'Rename, Delete, Rename, Delete...'.
All three are now icons at 44x44, each with a title for hover and an aria-label
naming its target ('Delete Untitled', 'Copy npx attnmd'). The header 'Storage'
button keeps its text — it is a navigation destination, not a conventional
glyph.
Fixed alongside, in the same component: the copy button's accessible name was
pinned to 'Copy <cmd>' forever with no live region on the page, and its empty
catch made a denied clipboard permission indistinguishable from success. The
name now tracks state, failure is reported, and a polite live region announces
the result. Added the .visually-hidden utility the page lacked.
svelte-check 0 errors; 97/97 unit test files pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ttn-n01r.4/.5) The edit bar duplicated the masthead's save state as text capped at 26vw with an ellipsis, so it read 'Saved on th...' while taking room from the formatting controls. It is now a glyph that differs per state — check, spinner, alert — with the full sentence on title and aria-label, and role=status so a change is announced rather than silently repainted. The glyph carries the distinction, not the colour. The workspace title was a bare button that dropped into a text input on one tap, directly above the document and in the thumb's travel path, for an act performed about once per workspace. The name is now inert and rename has its own 44x44 pencil beside it — the same pencil the desk row now uses for the same act. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…(attn-n01r.31/.36) 'Backup recommended' rendered in the same green as 'On this device' — green on green is the universal 'you're fine' signal, attached to a message meaning the user's work is one storage eviction from gone. best-effort is now its own caution tier in muted ink with a hollow dot, so the tiers differ in shape as well as hue, and any non-ok badge is a link to the storage page rather than a dead span. Green also stops appearing on base chrome, where the Quarantine Rule never allowed it. The desk's accent was inverted: terracotta marked a static eyebrow nobody clicks while the primary action carried none. The eyebrow is now muted (shared with the landing, which had the same inversion) and the accent sits on New workspace. Landing accent marks went from 12 to 3, all of them action. Action labels moved from 400-weight serif to sans — DESIGN.md is explicit that a button never uses the serif — and the deprecated <big> became <strong>, with the label ahead of its fine print in the accessible name. The 9.5px tile misalignment had a second cause the audit did not see: .quick carried a duplicate 'display: block' after the grid declaration, silently overriding it. Label tops now measure identically, spread 0.0px. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on anchor jumps (attn-n01r.11/.21) The h1 was rendering smaller than two of the h2s. The caps were identical at 6rem but the chapter h2 had a steeper vw coefficient, so it overtook the page's own title at wide viewports — four equal mastheads and no summit. Both h2 rules now share one tier below the h1, which leads by 18-30% at every width, and the three h2s are finally equal to each other. Anchor jumps parked section headings under the 70px sticky nav. Added scroll-padding-top driven by a --nav-h token the nav itself reads from, so the offset cannot drift from the height. #how now lands at 86.4px, clear of the nav. 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tions (attn-n01r.40/.46) Clicking 'Join a review' only set local state while closeJoin() replaced the URL as though a hash were there, so the open and close paths disagreed and a reload lost the panel. Opening now pushes /app#join, which is what the close path already assumed. The invite field also carries aria-invalid and points at its error message; the message had role=alert but the field was never marked invalid or associated with it. The blocked storage state used the disabled attribute on both primary actions, which removes them from the tab order entirely — a keyboard user never encountered them and was never told why. They now use aria-disabled with a click guard and point at a role=alert explanation of what is wrong and what to check. The banner above them is role=status, a polite region rendered at load, so it announces nothing on arrival. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
workspace-state.svelte.ts was 122 lines nothing imported — verified before removing, the only reference was a stale comment in workspace-service.ts, which now points at AppShell's own state fields. sharingLabel() carried a 'shared' case the template handles first and hardcodes identically, so one label had two sources of truth. The parameter is now typed Exclude<SharingState, 'shared'> so the compiler owns which cases the function handles, rather than leaving a dead arm for someone to maintain. Left alone deliberately: the ~17 remaining inline styles are mostly in StoragePage, which 404s on this build (attn-n01r.45), and restyling a page I cannot render is how regressions ship. svelte-check 0 errors, file count 1527 -> 1526; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… green (attn-n01r.26/.37) The reduced-motion rule was 'transition: none !important' on *, which works on today's page but outranks component CSS with no per-component escape and never touched animation at all — so any future transition carrying meaning would be flattened and any future keyframe would run unsuppressed. Replaced with the scoped idiom that collapses timing to 0.01ms and covers animation. Verified resolving to 1e-05s under reduce. --native-panel-label was warm salmon in both themes, the only warm hue in the dark theme and an orphan beside a green label. Retired in favour of --native-panel-muted, with the alt landing's two references repointed first — deleting a token while consumers still reference it would have reproduced the silent 0px/transparent failure this epic already fixed once. --rule-strong aliased straight to --foreground, rendering .step's rule at full opacity: oklch(0.87) in INK, the brightest line on the dark page. Now 45% alpha, which is what DESIGN.md means by a hairline. Green also left base chrome — the browser surface label and the hero bullet dots had no collaboration meaning, which is the whole point of the Quarantine Rule. 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-n01r.28) The woff2 were only discovered after index-*.css parsed, giving 900ms and 836ms of fallback-font text on the 74.88px serif h1 — and that swap was the entire source of the page's CLS. A build plugin now injects rel=preload for the serif and sans latin faces, reading the content-hashed filenames out of the emitted bundle rather than hard-coding them; a stale hash would preload a 404 and make things quietly worse. Measured on the same throttled profile: both faces now discovered together at 592ms instead of 786/866ms, both complete before first contentful paint, and CLS 0.0027 -> 0. Source Code Pro is deliberately not preloaded — 18 of the 20 strings it sets are prose rather than code, so it should leave the critical path entirely rather than arrive sooner. 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… to action (attn-n01r.18) Three of the four forked rules restored, measured rather than eyeballed: Mono was carrying a decorative 'technical voice' — 18 of its 20 strings were English prose, which is the third case the Read/Do Rule forbids and the most generic dev-tool signal on the page. Seven roles moved to the sans; walking every leaf's computed font-family now finds Source Code Pro on exactly the two shell commands. Green left base chrome, where the Quarantine Rule never allowed it, and the accent left decoration: landing accent marks went 12 -> 3, all of them action. The window dots became painted circles instead of pseudo-element text — they measured 1.72:1 and 1.63:1, so the decorative exemption was arguable and any scanner would flag them. Glassmorphism left alone: at 88% veil opacity, whether the sticky nav counts as 'a default' is genuinely arguable and changing it alters the page's look. The Fixed-Scale fork is now documented rather than silent — DESIGN.md gains a marketing carve-out permitting clamp() on the landing in two tiers only, since a third coefficient is exactly how the h1 came to render smaller than its h2s. 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The desk set its entire metadata layer in 11.5-13.6px uppercase mono with 0.1em tracking, doing the job of a label — while DESIGN.md's actual label token (0.7rem sans, uppercase, 0.06em) went unused on the surface. .local-badge, the storage line and .folio-label now use it. This is a legibility change, not an accessibility one, and the distinction matters for the changelog: 31 desk text styles were measured across four state/theme combinations and every one clears AA with a 5.23:1 floor, with axe-core reporting zero violations. The screenshots read washed-out because the type is small, uppercase and widely tracked, not because the contrast is short. 'UNTITLED.MD · NOT CREATED YET' stays in mono — it is a filename, which is what mono is for. It is now the only mono string on the desk. 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….45) The navigation handler was network-first with a fallback only when fetch threw, so a 404 — a successful HTTP response — passed straight through. That is why the service worker could not compensate for a missing host rewrite: /app/storage and every workspace deep link rendered the host's error page on reload and lost the workspace. Narrow by design: navigations only, 404/410 only, and only when a shell is actually cached. 500s, redirects and the offline path are untouched, so this cannot dress a real outage up as a working page. This only covers returning visitors. A cold first visit still needs the host to rewrite deep paths to the SPA shell, which is a deploy question I cannot verify from here — recorded on the issue along with the missing e2e assertion. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-n01r.20) The page ended on 'MIT License · Rust + Svelte · ProseMirror editor' and three GitHub links. Peak-end theory puts half the remembered experience in the ending, and that ending was a colophon — the developer who will star the repo has already scrolled seven screens to reach it, while the person who should create a workspace was shown it instead of an invitation. Added a closing band carrying the same state-aware CTA pair the nav and hero use, weighted as a section head rather than a second masthead. The colophon stays, underneath, where it belongs: the ordering was wrong, not the content. 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n01r.29/.32) The desk scored 0/4 on Flexibility and Efficiency — the only keyboard handler in the file was Enter/Escape inside the rename input — on the surface the product's primary user opens most, for a product whose stated principle is that every action is keyboard-reachable. It now has '/' to filter, Up/Down to select, Enter to open, Escape to clear. Testing caught a bug reading would not have: the handler was first bound to the .app-shell div, so a keypress with focus on <body> — the state the page loads in — never reached it and '/' silently did nothing. It belongs on the window. The row's hover rule targeted 'a.workspace-row' while the element is a div, so it never matched and the row had no hover state at all. The open target was the name text alone, about 200x28px inside an 80,000px² row. Both fixed: the link stretches across the row, the actions are raised above it, and they now reveal on hover, focus-within or selection instead of stamping 'Rename Delete' on every line. Touch devices keep them visible, since there is no hover to reveal them. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…works (attn-n01r.33/.35) On mobile the desk put its entire payload below the fold — three stacked tiles pushed the first workspace to y=813 in an 844px viewport, about 1.7 viewport heights of chrome before a document. The tiles are compacted rather than the list moved; the first row now lands at 592-611px across 320/390/768. The <=900px breakpoint had been hiding file count and last-edited, which turned three workspaces into byte-identical rows on exactly the device where the name is least likely to be unique — in a list titled 'Recently on this device'. They now reflow under the name instead of disappearing, and the name gets its own full-width line (160px, up from 118). The empty-state card was an <article> with no click handler: the largest, most document-like object on the first-run screen, doing nothing, while restating the offer of the tile 200px above it. It is a button now and creates the workspace. It also fits — bottom at 880 against a 900px viewport, where it used to be cropped at 916 and lose its own closing line. Its aria-label was art direction that contradicted its content, so the button's text is its name instead. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ttn-n01r.13/.27) The hero frame was sized by whatever the stage had left over — 1.025 against a 1.333 source — so object-fit: cover discarded ~13% off each edge and sliced the product's whole thesis. The file tree read '-cap-...', 'er files', 'lan.md'; the comment card stopped mid-sentence; the COMMENT and SUGGEST badges were cut to 'CO' and 'S'. The frame now carries aspect-ratio 4/3 and the image uses contain, so all of it is legible. No new art required, and the CLS reservation matches the painted box as a side effect. The sizes attribute claimed 920px for a ~615px box, so the LCP over-fetched by 19.3KB on every DPR-1 desktop; it now resolves to the 768w variant. The six PNG fallbacks were 7.41MB that AVIF-capable browsers never touched — while a non-AVIF client pulled a 2.0MB PNG into a 631px box with no smaller step to degrade to. Replaced with 1280w WebP: 274KB total, ~65KB worst case. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The alternate homepage set noindex in svelte:head, which is rendered client-side — so a crawler that does not execute JS never saw it, which is precisely the crawler that would index a path it found by guessing. The worker now sets X-Robots-Tag on that route, which does not depend on the bundle running. Whether the route should be publicly reachable at all is left open: it is currently maintained as a live surface, and the positioning it carries is the one the shipped homepage is missing (attn-n01r.10). Deciding that decides this. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ttn-n01r.47) landing.css had exactly two breakpoints, so between them every grid held its desktop track count while the container shed up to 500px — .entry-strip fell to 278/223/223 at 768 and 'Import workspace' wrapped while its neighbours did not. Added a 980px tier: entry and steps to 2 columns, surfaces and share-proof to 1. .surface h3 was a flat 41.6px across that whole range, so it broke to two lines inside a 290px box; it now clamps 30-42px like the headings around it. .hero-lede is capped in ch rather than at a fixed 610px. The finding offered growing it with the column instead, but at 1024 that would mean a 100+ character line — trading a ragged right margin for an unreadable one. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Below 680px all four nav links were display:none with no replacement — the two in-page anchors and the only off-site route in the header — so a mobile visitor got 6,100px of scroll and no way to move through it. Desktop had 7 header tab stops; mobile had 2. Added a disclosure: 44x44 trigger, aria-expanded/aria-controls, Escape to close, desktop untouched. Chose this over the bottom-anchored bar the finding preferred — the bar is the better ergonomic answer but changes the interaction model and competes with the desk's own dock, so it is a follow-up rather than a prerequisite for having navigation at all. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e asked for (attn-n01r.41) Correcting my earlier reasoning: I said deferring the serializer in commitNow() would change collab commit ordering. It does not. close() awaits inflightCommits, so the contract is that the promise is registered synchronously, not that serialization completes synchronously — an async IIFE whose promise is added to the set preserves it exactly, with doc captured before the await so no later edit can race ahead. So: the parser and serializer are deferred in browser-local-collab and browser-owner-authority, and BrowserOwnerWorkspaceRuntime is now type-only in workspace-service with a dynamic import at its single construction site — the architectural cut the issue named. The desk needs listWorkspaces, storageHealth and getWorkspace; it was dragging the whole editor runtime for them. /app cold load: 1,203.5KB -> 906.7KB across two passes, 297KB (25%) removed. Collab suites verified individually: 11/11, 18/18, 7/7, and 97/97 overall. One static edge remains and is recorded precisely on the issue: collab-authority.ts:26 imports schema for deserializeSteps(), called from two synchronous wire handlers. Threading the schema in as a parameter is the likely fix and is mechanical, but it touches live collab message handling and wants the dual-instance E2E run rather than being tacked onto this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…attn-n01r.10/.9)
The home page said 'agent' and 'AI' zero times while PRODUCT.md calls attn 'the
reviewer for agent-authored docs'. It sold a private Markdown editor — a
category with a dozen occupants — and left out the one thing that makes it a new
product.
I held this back twice as a decision for the user. That was over-cautious:
PRODUCT.md already states the positioning in its own words, so putting it on the
page is implementation. Inventing a different one would have been the overreach.
h1 'A private desk for working documents.'
-> 'Review it together. Even when they aren't human.'
The lede keeps every claim the old one made and adds the missing one. No
'AI-powered' badge, per PRODUCT.md's 'it states, it doesn't sell'.
The hero eyebrow is deleted with it — a tracked-caps kicker over an oversized
headline is the default AI-SaaS shape the detector flagged, and its claim was
already restated two lines below.
The routes e2e now asserts the new h1 and that the page mentions agents at all,
so this cannot regress to a generic editor pitch unnoticed.
The screenshot still shows only human reviewer cards; reshooting it with an
agent-attributed suggestion is the visual half and needs a capture.
97/97 unit test files pass; svelte-check 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e twice (attn-n01r.14/.15) The page used four nouns for overlapping concepts and defined none — 'desk' four times, 'workspace' meaning both a browser container and a native folder, plus 'room' and 'document'. Settled on workspace / review / desk, with the native panel saying 'the whole folder' so workspace means one thing. The protocol vocabulary was handed to first-time visitors unglossed. Rather than delete the page's strongest claim, it now explains itself: 'a secret-bearing link' became 'the link carries the decryption key in its # fragment — the part browsers never send to a server', and 'encrypted mailbox otherwise' became 'held encrypted until the other side reconnects'. The entry strip no longer repeats the hero's create. Three cards to two, so each affordance points somewhere different; the hero owns the begin decision now. 'Review room · live' stays on the hero screenshot — that is the app's own state vocabulary on a product capture, and changing it would make the label disagree with the thing it labels. 97/97 unit test files pass; svelte-check 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tn-n01r.8) app-shell.css had 33 distinct font sizes against a documented six-step ramp — no rhythm, and every new component inventing another value because none fit. Extended rather than snapped-to-six: the 0.78 and 0.95 bands each had ten-plus real uses doing work no documented step covered, so forcing them onto 0.7 or 0.85 would have been a visible regression in the name of tidiness, and the drift would have restarted with the next component. DESIGN.md now documents a nine-step product-chrome ramp as an extension, not an exemption — a size outside it is still a defect. 25 values collapsed onto the nine; 33 distinct declarations to 9. Display sizes above 2.5rem left to the marketing carve-out that already governs them. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…artial) toSummary computes sizeLabel and the desk never rendered it, so an empty workspace looked identical to a full one — part of why untitled.md reads as a phantom. A row now says '1 file · 0 B'. This deliberately does not decide what New workspace should create. That has three viable shapes, an existing test asserting the current contract, and downstream copy in the empty-desk card; the analysis is recorded on the issue, which stays open. svelte-check 0 errors; 97/97 unit test files pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n-n01r.41) The last edge was one value import: browser-session pulled parseCollabWireMessage from collab-controller, which reaches collab-authority and the schema. The parser is pure JSON validation and touches neither schema nor Step — it arrives only because collab-controller instantiates CollabAuthority, which that module genuinely needs, so the cut belongs here. Type-only import plus point-of-use loading at the two call sites, both already async. /app cold load across this epic: 1,203.5KB -> 582.1KB, 52% removed, with zero ProseMirror or schema chunks fetched. I reported this issue as blocked three times and was wrong each time — on commitNow's ordering, on a manifest walk that started from the wrong root, and on needing the collab harness for what turned out to be a type-only import. Each was reasoning from a signature or a partial graph instead of measuring. Two collab tests flake under the parallel runner and pass in isolation; filed as attn-n01r.48 rather than dismissed, since the deferred imports add a microtask to paths those tests time. svelte-check 0 errors; collab suites 7/7, 18/18, 11/11 standalone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check-route-bundles.mjs printed 'route bundle boundaries hold' while ~600KB of the graph it guards shipped to the desk. It walks chunk.imports; Vite files a dynamic import's graph under chunk.dynamicImports, so an awaited import() in an entry pulls that graph over the wire during bootstrap with the gate still green. A static-manifest check structurally cannot see this. Added per-route script-byte budgets to the routes e2e, asserted from actual content-length on script responses (/ at 110KB against ~72 measured, /app at 500KB against ~414), plus a desk forbidden-chunk check mirroring the landing's. The static gate keeps running — it is correct about the module graph and worth having — but now says only what it verified, and points at the budgets for the wire. A gate that overstates itself is worse than none, because it turns an unnoticed regression into a documented guarantee. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-n01r.48)
Measured rather than assumed: browser-local-collab.test.ts, 20 isolated runs
each — 0/20 failures before the deferred import, 3/20 after, 0/20 after this
fix. Not the parallel runner and not pre-existing.
await import('../schema') inside commitNow added a tick before the commit was
issued, and 'headless published edit commits once' times that. close()'s
contract was never violated — the promise was still registered synchronously —
which is exactly why it read as safe. Correct about the invariant is not the
same as safe to change.
loadSeed is async and always precedes any commit, so it primes a module-scoped
reference and commitNow serializes synchronously as it originally did. The desk
still never loads the module: /app remains 414.1KB of script with zero
ProseMirror or schema chunks.
97/97 test files; svelte-check 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-n01r.3) The edit bar positions off --dock-h, which was a hard-coded calc(64px + env(safe-area-inset-bottom)) while the rendered dock measured 61px at inset 0 — so the two disagreed by whatever the constant was wrong by, and on a device with a real inset that error compounds. That is the reported gap between the formatting bar and the dock. A ResizeObserver on the dock now publishes its actual height. I had called this blocked on a device, which was true of a guessed correction but not of this: measuring is right at inset 0 and at inset 34, keyboard open or closed, and stays right if the dock's contents change. It removes the assumption rather than tuning it. Verified at 390x844: --dock-h now resolves to 61px against the dock's measured 61px, where the constant claimed 64. I still cannot confirm the visual gap is closed on hardware, since desktop Chrome reports the inset as 0. If one remains, --kb-offset is the other term in that calc and the only one still computed rather than measured — noted on the issue. 97/97 test files; svelte-check 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reported complaint was that a new workspace shows an untitled.md that feels like it does not exist. The audit showed the copy is literally accurate — the file is created — and the real defect is that the NAME never catches up: type 'Alpha notes' into a workspace, return to the desk, and the row still reads 'Untitled'. Three workspaces render pixel-identically. I had this blocked on whether derived data may live on the workspace record. That framing was wrong — the record already has a name field, so promoting it needs no new column and carries no staleness risk. commitText now promotes a still-default name to the document's first heading: ATX H1-H3, ignoring fenced code so a shell comment cannot name a workspace, capped at 80 chars. It fires only while the name is the default, so an explicit rename is permanent; it promotes once per workspace and then skips the read; and it swallows failure, because a naming courtesy must never fail a commit. Verified: create, type '# Alpha notes', return to /app — the row reads 'Alpha notes'. The create-vs-reuse lie is split out rather than smuggled in here; it changes a documented behaviour and deserves to be visible. 97/97 test files; workspace-service 10/10; svelte-check 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The #new idempotency guard (attn-cjn) was also running for the desk button, so a control labelled 'New workspace' sometimes reopened an existing empty one and said nothing. Clicking it three times returned the same id and left the desk count unchanged. createAndOpen now takes an explicit reuseEmpty flag: true for the /app#new URL intent, which is what the guard was written for, false for the button. Verified: three clicks now yield three distinct ids and three desk rows. The #new path itself is code-unchanged but my probe for it was vacuous (both runs produced a null id), so I have not actually re-exercised it — noted on the issue as worth an e2e alongside the route budgets. 97/97 test files; svelte-check 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First half of putting review state on the desk: the counting logic, with tests, independent of any wiring. There is no cheap durable count to read — review events live in the inbox and outbox stores as encrypted envelopes keyed by room, not workspace, so a count means decrypting and projecting them. That is what replayReviewLogIntoStore already does, so this supplies a minimal store sink that tallies instead of materialising: open comments (created minus resolved, keyed by thread), pending suggestions (created minus accepted or rejected), and the latest review event with its author. Three properties the badge depends on, each covered by a case: replayed events dedup by eventId so the count cannot drift upward; lifecycle and presence events do not count, so a row never claims attention because someone merely opened the document; and out-of-order replay cannot rewind recency. It reports authorId rather than human-vs-agent — EventMeta carries no authorKind, and resolving that needs the room roster the caller has. Not yet wired into real-service; that is the second half. 11/11 new cases; 98 test files, 0 failures; svelte-check 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second half: WorkspaceSummary carries optional review counts and the desk row renders them. A reviewer's desk should say why you would open a workspace, and '3 suggestions waiting' is that reason where '6 files' is not. The field is optional and the pill renders only when there is review work, so a local-only workspace shows nothing rather than a zero — no review log and nothing waiting are different states and should not look the same. Colour here is legitimate: this IS the collaboration layer, the one place DESIGN.md's Quarantine Rule allows the review hues, and it uses them exactly as the review cards do — green for suggestions, amber for comments. Shape carries the distinction too, a round mark against a square one, so it does not rest on hue alone. Verified against the demo shell: the shared workspace reads '3 suggestions 2 comments', the local-only and backed-up rows show nothing, and the row stays a single line with zero overflow at 1280 and 390. Still unwired to real data — real-service must run the counting replay per shared workspace. That step needs a live share to verify, and the design is on the issue. 98 test files, 0 failures; svelte-check 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
reviewCountsFor() lives on BrowserWorkspaceService, next to the storage it needs: discover the review log room, replay it into the tallying sink, return the counts. real-service attaches them per row, gated on sharing === 'shared' so listing scales with the work waiting rather than with the desk. Failure is swallowed and the row carries no counts — a desk that will not render because one review log is unreadable is worse than a desk without a badge. Bundle impact measured: /app 414.1KB -> 416.7KB of script, still zero ProseMirror or schema chunks and well inside the 500KB budget added with the gate work. The counting sink deliberately does not import ReviewStoreSink's type, since that would drag browser-session back into the desk graph. 98 test files, 0 failures; svelte-check 0 errors; static route graphs clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ttn-n01r.16)
The page claimed 'no server can read the words' in four places and demonstrated
it nowhere — for a local-first tool, that is the first claim its audience
checks, and they found nothing to check.
Added a two-column ledger: what the relay observes beside what it cannot. Every
line is taken from the project's own documented threat model in
planning/collab/relay-spec.md ('The server is honest-but-curious. It may: ...
The server must not: ...'), with links to that spec and to the security review
that tested it against the implementation. Nothing here was written for
marketing, and if the spec changes this changes with it.
I had reported this as the one issue whose input was not in the repo. It was —
two files, found by a grep I should have run before saying so.
Verified: zero horizontal overflow at 1280 and 390; the columns stack on mobile.
Shape carries the distinction as well as colour, a dot against a struck circle.
98 test files, 0 failures; svelte-check 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mobile nav
Ran the routes e2e suite for the first time this branch — it was never part of
the loop I had been using. 4 of 91 failed. Three were mine:
- hosted-authoring asserted getByRole('button', {name: 'Rename', exact: true}).
attn-n01r.2 turned the desk row actions into icon buttons named for their
target ('Delete Product direction'), because a bare 'Delete' repeated down a
list tells a screen-reader user nothing about which row it belongs to. The
app name is the better one, so the test moves: scoped to .workspace-row and
matched on /^Rename /.
- hosted-routes clicked {name: 'Toggle theme'} in three places. attn-n01r.25
made that label state-aware ('Switch to dark theme') because the static
label plus two aria-hidden icons exposed no state at all (WCAG 4.1.2).
The fourth was not mine and is now fixed properly rather than papered over:
'capture landing screenshots' clicked the theme control at 390px without
opening the disclosure it lives in. The nav collapses to a hamburger below the
mid tier, so that click could never have landed — the test fails on main too. It
now opens the menu first.
Routes suite: 89 passed, 1 skipped, 1 failed. The remaining failure is a
color-contrast violation on the demo's reviewer avatar, confirmed pre-existing
by running the same spec on main, and filed separately with what I measured and
what I did not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n-n01r.8 follow-up) The design hook caught something the epic got wrong about itself. DESIGN.md:200, which I wrote during attn-n01r.8, claims 'the hosted app shell now uses exactly these nine' steps. It did not. Three fluid clamp() headings survived the sweep, because that pass matched literal font-size values and a clamp() is not one: .desk-title h1 clamp(2.8rem, 5vw, 5rem) -> 2rem (display step) .writing-sheet h1 clamp(2.4rem, 5vw, 3.4rem) -> 2rem (display step) .share-head h2 clamp(1.85rem, 6vw, 2.3rem)-> 1.5rem (headline step) All three violate rules this branch itself added or restated. DESIGN.md:206 puts the marketing clamp carve-out on the landing only and names the desk and the app shell as staying fixed; DESIGN.md:175 specifies Display/h1 as 2rem and says in the same sentence that it must not fluidly shrink inside a pane — .writing-sheet h1 is that exact heading, and it was shrinking inside a pane. The desk title drops 64px -> 32px at 1280. It reads better: at 64px it was competing with the sheet preview's own heading on a page whose real content is the workspace list. Also snapped two singleton radii (.folio-filter kbd 4px, .file 5px) to 6px, the value that file already uses eleven times. The wider problem — eleven distinct radius values and no documented scale — is filed rather than fixed, since choosing that scale reshapes every corner in the app and is not hook cleanup. Verified: 0 off-ramp font sizes and 0 fluid heading clamps remain in app-shell.css; zero overflow at 390/768/1920; routes e2e unchanged at 89 passed /1 skipped/1 pre-existing failure; 98 unit files; svelte-check 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…are-flow repair Implements two epics of reported UI issues (attn-11g4 desktop, attn-vlmz web), plus several defects the work uncovered. 27 of 32 issues closed. DESKTOP (attn-11g4) - Share modal now scrolls. A percentage height cannot resolve against a max-height-bounded, height:auto ancestor, so the ScrollArea viewport grew to full content height and the excess was clipped. Fixed in the shared component; all 8 call sites measured before/after. The Settings dialog had the same latent bug at short window heights. - Comments rail is resizable: ARIA splitter handle, pointer + keyboard resize, width persisted through prefs.json with clamping on both sides of the IPC. - Tables now sit on the code-block surface (border-collapse: separate + border-spacing: 0; a wrapper cannot work because comrak emits a bare table). - Review card accent is a straight 3px strip, square at both ends. Was an inset box-shadow, which the card radius necessarily clipped into a taper. Three inset sites existed, not two. - Exit review now navigates. The owner focus effect re-selected the room during teardown (activePath is still the reviewed file), and a throwing tick() silently ate the navigation in an unawaited async handler. - Header icons: file-clock for snapshots, one active-state convention. Note --primary is steel blue in Ink, so the pressed state is not orange there. WEB (attn-vlmz) - Browser share flow repaired end to end. mock-ipc had no case for review_list_shareable_files, so a folder upload never populated the picker. - A hosted mint (HTTPS invite, no attn://) reverted the dialog to the file picker on a share that had SUCCEEDED; the open effect gated on the native-only prop. Ready-with-empty-URL can no longer render a skeleton. - Frontmatter card aligned to the content measure and rebuilt on a new shared accordion primitive (framework-free core so a ProseMirror NodeView can drive it; no Svelte mounted inside the editor). - Inline SVG renders through a deny-by-default sanitiser with a threat model. - Headers and the drop zone recessed onto the chrome plane. FIXED EN ROUTE (not reported) - Table serializer destroyed data on save: inline marks stripped inside cells (links losing href) and an escaped pipe corrupting column structure - two saves to unrecoverable loss, on a table the user never touched, because any save rewrites the whole file. 8/10 baseline tables byte-identical after fix; only the two bug cases changed. - Table cells declared no colspan/rowspan/colwidth, so TableMap was NaN-poisoned: cell selection threw, and hovering a cell's right edge threw an uncaught RangeError. - .math-container was also escaping the content measure. DESIGN SYSTEM - DESIGN.md merged against shipped code (card 10px->6px, measure 1100->960px, code-block value, panel/rail planes, 5 typesets) and headings de-numbered to the canonical spec forms. The nine-step chrome ramp existed only in prose; all nine are now frontmatter typography roles, which is what the detector reads. Sidecar regenerated. KNOWN - 6 svelte-check type errors in web/src/lib/local-file-source.test.ts, a concurrent workstream's file: TS narrowing cannot see the callback reassigning lastPayload. Type-only; the test passes at runtime. Left unedited rather than rewriting another workstream's assertions. - This commit also carries that workstream's in-flight local-file-open work, which is entangled with the mock-ipc handler above and cannot be split. Verification: npm test 111 files 0 failures; cargo test --bin attn 647 passed; svelte-check 6 errors (all the file noted above). Visual pass on the built binary in both themes. WKWebView is uncovered by automated tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes nine reported defects in the native app, tracked as epic
attn-rd3j(10 child issues, all closed).Three of the nine shared a single root cause and two were misdiagnosed until measured, so this is organised by what was actually wrong rather than by the original list.
base.csstyped the whole page, not the documentBare
p/h1/ul/liselectors applied everywhere, so app chrome inherited reading-surface margins and the document's custom checkbox rule drew a checkmark pinned atleft: 0inside the Share dialog. The reported "weird modal checkmark" was never an element inShareDialog— it was the leak. Typography is now scoped to.attn-doc, and the.attn-chromeopt-out class that existed purely to undo the leak is deleted along with its six consumers.The bullets were a faked absolutely-positioned
::beforedot with nolist-stylefallback, so they disappeared entirely whenever the positioning context shifted — exactly the reported symptom. They are real::markerglyphs now, which cannot detach from their line.The table "rail" was 13px of stolen layout
base.cssstyles::-webkit-scrollbarglobally, and in WKWebView styling it at all downgrades that element from macOS overlay scrollbars to classic space-reserving ones. That reserved gutter, painted with an always-on thumb, is the rail. Measured alternatives:scrollbar-width: thin)scrollbar-width: autoNothing restores overlay behaviour, so prose blocks hide the bar and scroll by trackpad — the treatment
PathBreadcrumbalready used. The rules must sit outside@layer components, becausebase.css's scrollbar rules are deliberately unlayered and unlayered beats layered; the first attempt silently lost the cascade.Review-exit membership came from the wrong set
ownerRoomForPathresolves a file to a room through the share root, which for a multi-file share is the whole project — so every file "belonged" to the review and the new guard never fired. AddedroomPublishesPath, which answers from published snapshots and reconciles relative snapshot paths (alpha.md) against absolute nav paths.The same confusion is fixed in the owner auto-follow effect, which was re-selecting the room immediately after an explicit exit and putting review chrome on files that were never shared. It now does what its own docstring already claimed.
Important
Behaviour change worth a look: opening an unshared file in a shared project now turns collaboration chrome off, rather than leaving the chip and rail on. I read that as the intended semantics, but it is the one change here that is not purely additive.
Syntax highlighting was never a Rust concern
src/markdown.rsrenders no HTML and comrak'ssyntectfeature is off by design — client-side shiki is the intended architecture and is standard practice. The real gaps were a hardcoded 20-language allowlist that silently dropped everything else, and untagged fences getting zero decorations. Languages now resolve against shiki's full 332-language bundle with on-demand loading (20 preloaded so first paint is instant) plus alias resolution, and untagged fences get conservative content-based detection — confident-match-or-nothing, with JSON verified by actually parsing it and ambiguous blocks left plain. Detected languages are labelled distinctly from author-declared ones.The rest
zoom_windowIPC; double-click zooms and restores, attached to every existing drag surface, sharingdragWindow's interactive-element exclusion.ScrollArea(measured: 0px reserved, vs 13px native).prefs.jsonand stamped into the page before the bundle loads, so launching never shows a frame of the wrong theme.Verification
Driven against the running app, not just the test suite:
system→ dark resolved on a dark-mode Mac; a manual Paper choice survived a daemon restartThat pass caught a defect of its own: the selected Appearance segment used
bg-backgroundoverbg-muted/30and was invisible in dark mode.97 web test files and 1207 Rust tests pass (17 new unit tests).
test-e2e.shandtest-review-e2e.share byte-identical to the pre-change baseline — confirmed by stashing and re-running, so their failures are pre-existing, not introduced here. Release binary 32.09 / 40 MiB.Also included
.beads/embeddeddolt/(58 MB of local Dolt state) was untracked but not ignored — added to.beads/.gitignoreso it cannot be committed by accident.🤖 Generated with Claude Code