Review-surface polish, reading-palette + autosave epics, design-system consolidation - #12
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>
…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>
…eading-palette + autosave epics Four bodies of work, verified together: attn-evme — reading surface colour. One ink for prose (blockquote un-greyed); links carry the accent plus a load-bearing rest underline (no lightness passes AA-vs-page and G183-vs-body at once on this palette); code/tables/frontmatter recessed into the page (embedded surfaces step toward the foreground, never lighter than Paper); syntax theme re-picked (github-*-high-contrast + one colorReplacements patch) after the surface move stranded vitesse-light. attn-yzsa — desktop autosave. New native-autosave.ts (pure gate + clock, 20 cases): debounce + ceiling, disk-conflict HOLD that refuses to overwrite a file changed underneath the editor, ⌘S as immediate flush. 'Changes autosaved' is now true on both surfaces instead of true on one. attn-64iy tail + design-system consolidation (10 ratified issues): - type ramp gains its micro end (11px standard, 10px floor) — 78 arbitrary text-[Npx] utilities collapsed onto named steps; pill radius unified at 9999px; radius/size strays snapped - literals -> tokens: 17 font stacks, review-hue colours (fixing a dark-mode contrast bug in suggestion ghosts and de-Tailwinding ins/del marks) - one document surface: dead Viewer.svelte deleted; table surface single- sourced in a new @layer doc under an explicit six-layer cascade order (theme < base < chrome < doc < components < utilities), retiring the twin-rule and unlayered-link escape hatches and the mt-*/mb-* trap - one save-state vocabulary: save-state-copy.ts, hosted SaveState union derives from it; reviewer header reconciled to the panel-glyph pair (attn-o17v) Guard rails added: reading-palette.spec.ts (live contrast sweep, both themes), doc-surface-parity, design-doc-parity (DESIGN.md frontmatter pinned to tokens.css, mutation-tested), save-state-copy orphan sweep, header parity extensions. DESIGN.md: Two-Tier Surface Rule, The Layer Order, links-are-action under One Pencil; sidecar refreshed without regenerating the hand-authored doc. 120 unit test files, 21 Playwright tests, svelte-check at its pre-existing baseline. Desktop autosave proven against a real daemon writing a real file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bridge callback is the only writer of lastPayload, and TypeScript cannot see into it — after a literal null assignment the local narrows to null and every later optional read collapses to never (6 svelte-check errors, which npm run check turns into a CI failure now the WIP file is committed). Reading through a function boundary resets the narrowing; no runtime change, and the harness still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pre-existing on the branch from the rail-width work (1062d1c) — the multiline assert_eq rewrap rustfmt wants. Rust Quality's fmt check is the whole failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clippy 1.97's assertions_on_constants (under -D warnings) rejects runtime assert! on constants. The two range invariants move into a const block — compile-time instead of test-time, strictly earlier, same intent. The behavioural assertion on normalize_rail_width stays a runtime test. Pre-existing from the rail-width work, surfaced once the branch hit CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
CI status: 5/6 green — Web Check, Rust Quality, Binary Size Gate, and both Vercel checks pass as of 64ad625 (Web Check and Rust Quality were red and fixed in this PR: svelte-check narrowing errors in a committed WIP test, a rustfmt diff, and a clippy 1.97 assertions_on_constants). Relay Tests (Ubuntu) is red with the known failure tracked as attn-i38g — the same two anti-enumeration rate-limit cases that have failed main's CI since at least 2026-07-27 (main's last five runs all conclude failure; the tests pass locally with ~100x margin under their timeouts). This PR contains no relay changes; a rerun of the job reproduced the same failure. Fixing it is attn-i38g's scope — masking it here with a timeout bump or skip would hide a real main-branch problem. 🤖 Generated with Claude Code |
… render Two user reports against /: HEADER (--header-surface, new token). On the panel plane the header vanished into the sidebar — its third user-reported move (paper -> panel-surface -> its own plane). One step darker than the panel plane and warmed toward the clay on Paper; lifted and steel-tinted in Ink. A tinted ground is furniture, not a mark, so the One Pencil Rule is untouched. Applied to all three headers (native, hosted owner, browser reviewer) — one grammar, one plane. The palette probe now sweeps the header (doc name, muted icon, saved glyph); the saved glyph is classified at WCAG 1.4.11's non-text 3:1 — the standard DESIGN.md always stated for it — and measures 4.14:1 Paper / 7.08:1 Ink, re-quoted in DESIGN.md from the probe. SVG. The attn_svg block rule refused the two shapes agents actually write: an SVG glued to the following prose line, and one glued under the preceding line — both rendered as a paragraph of escaped source. The rule now fires on both: the blank-line-after condition is dropped, and alt:['paragraph'] lets '<svg' interrupt a running paragraph like a heading or fence. Contract stated exactly in the rule comment: separated sources stay byte-exact; glued sources render and normalise to the separated shape on first save (parse-serialize is idempotent). Mid-line SVGs, list items, blockquotes and 4-space indents stay text, deliberately. Security unchanged: widening WHERE the rule fires does not widen WHAT renders — same sanitiser allowlist, same NodeView second gate. Roundtrip suite updated: 24 cases including the new fixed-point assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rand
Three owner requests, two of which converged:
WEB header should match the 'Choose file' button below it — that button is
bg-primary. DESKTOP header should be 'orange/blue' — which is exactly what
--primary is in the two themes. So --header-surface becomes var(--primary),
declared at :root so it freezes the real accent and resolves per theme. This
is the header's fourth surface; the trajectory is the argument: paper
(invisible), panel-surface (vanished into the sidebar), a warm tinted plane
(still too quiet), the accent.
An accent ground breaks every control on it — the header vocabulary is
written in --foreground/--muted-foreground/--accent and, worst, --primary,
which is now the ground itself; the active-pill convention would have been an
invisible accent-on-accent. Rather than rewrite ~8 components, the header
re-points those tokens for its own subtree, so every Tailwind utility inside
adapts with no component edits. Polarity flips between themes (Paper's accent
is dark, Ink's is light), so nothing is hard-coded white — --primary-foreground
is correct in both. --amber-deep goes with them: DESIGN.md already held that
the save chip's real signal is the glyph, not the tint.
The four floating cards that render INSIDE the header subtree (ShareChip,
SnapshotBadge, OutboxIndicator, PeerStrip) restore the base palette from new
--chrome-* captures at :root. That capture trick — a var() in a custom-property
declaration resolves on the element it applies to, so it freezes the root
value — was verified empirically in a browser before the design was built on it.
DESKTOP DROPS THE BRAND (owner: 'not needed, just show the name of the active
file'). brandPlacement is three-way now: none on desktop (the Dock icon, window
and app menu already supply identity), sidebar in a browser tab with one,
header in a browser tab without — the empty state is the file picker, where
'what is this?' is a real question. The old `!brandInHeader` inversion no longer means 'sidebar'
(it now also means 'none') and was corrected at the call site.
Also fixes a real accuracy bug in reading-palette.spec.ts: contrast() dropped
foreground alpha, crediting translucent text with its solid colour's ratio —
always in the flattering direction. It now composites. Header muted icons read
4.84:1 Paper / 6.29:1 Ink under the corrected maths, still clear AA; doc name
6.65:1 / 8.41:1.
DESIGN.md records the plane as a One Pencil PLANE EXCEPTION — the rule governs
marks made ON a surface, and this is a surface made OF the accent, where the
pencil inverts. The frontmatter uses a {colors.primary} token ref and the
parity test learned to resolve refs against var(--x) rather than demanding a
duplicated literal.
Verified: brand count 0 and doc name shown in the real desktop window, both
themes screenshotted on desktop and web; 120 unit files, 21 Playwright,
svelte-check 0 errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ce37862 to
9d69992
Compare
Reported on the desktop app: the filter bar hung out over the document, its search field crossing the sidebar's own edge. The sidebar is user-resizable between SIDEBAR_MIN_WIDTH (180px) and 480px. Measured, the filter stopped shrinking at 194px — so anywhere below ~200px it overflowed. TWO causes, and fixing either alone leaves the bug: 1. .sidebar-controls is a GRID and grid items default to min-width:auto, so they refuse to shrink below their own min-content. 2. That min-content was dominated by the text input's INTRINSIC width. An <input> is sized from its `size` attribute (default 20 characters, ~130px), and the `min-width: 0` already on .sidebar-filter-input does NOT remove it: that declaration governs flex shrinking INSIDE the filter, not the filter's min-content contribution to the grid above it. This is why the obvious reading of the CSS says it should already have worked. min-width:0 on the controls' grid items plus `size="1"` on the input drops the grid's min-content from 218px to 42px; at a 180px sidebar the filter now sits at 155px inside its 156px content box. Chose this over raising SIDEBAR_MIN_WIDTH: the minimum is a product decision about how narrow the tree may get, and it should not be quietly set by an input element's default character count. New web/e2e/sidebar-narrow.spec.ts asserts the OUTCOME rather than the mechanism — every laid-out descendant inside the container's right edge, at six widths including 170px (deliberately below the clamp, so a future minimum cannot silently reintroduce this) — plus that the filter still filters once it can shrink. Negative-controlled: reverting the fix fails exactly the three narrow widths. Verified on the real desktop window at 180px: zero overflowing elements. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
35e6c3e to
acd6a84
Compare
node_modules/.vite/vitest/<hash>/results.json was tracked. The gitignore only
carried `web/node_modules/`, which left the repository ROOT uncovered — and a
root node_modules does get created here (there is a root package.json), so the
artifact walked straight through the gap.
The rule is now unanchored `node_modules/`, which git matches at any depth, so
neither location can leak again.
Worth naming what the file actually was: a cached record of one FAILING test
run ("embedded-svg-roundtrip.test.ts", failed: true) frozen at 2026-08-06.
Stale state that asserts something false about the suite is worse than plain
noise if a tool ever reads it back.
It entered in 1062d1c, which predates this session's work, so this only removes
it from the tree going forward — the blob stays in history. Say the word if you
want it purged from history properly instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two data-loss findings from the Codex review of this branch.
P1 — autosave was bound to a moment, not to a document. The controller kept
only a pending bit, and the commit callback resolved the active path when the
timer fired. Switching tabs, closing the active tab, switching projects, or the
daemon pushing a new document mid-debounce all replaced the buffer with a write
still owed.
The consequence is worse than a dropped write. `edit_save` carries only
content; the daemon resolves the target from its own active_path (src/ipc.rs),
so a late timer does not save the old file late — it saves the old buffer's
text into the newly opened file. The frontend cannot retarget it, because the
target was never the frontend's to name.
Fixed at both levels:
- `replaceDocument()` funnels every buffer-replacing site in App.svelte,
flushing while the path is still current so the edits land where they
belong. User-initiated navigation aborts when a held disk conflict makes
the write unflushable, leaving the person on the file whose edits are at
stake with both resolutions already on screen. The daemon's own
setContent proceeds — it is authoritative about what is displayed.
- An identity interlock in NativeAutosave refuses any write whose document
changed since the burst began. Only openPathNow was guarded before, which
is the point: "changes the open document" is not a property the type
system can see, so the backstop has to be structural.
P2 — the browser dev loop reported saves it discarded. Locally picked files
stayed editable and sent `edit_save`, but the mock IPC handled only `navigate`,
so the store kept the original immutable File. The chip said saved; switching
tabs or sharing re-read the original bytes and the edits were gone with no
error anywhere. The store now owns the active-path mirror (it is the module
that delivers content, so it is the one that knows) and applies writes,
advancing lastModified so the app does not read its own save back as someone
else's concurrent write.
Every new assertion was negative-controlled: each fails with the fix removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Started as review-surface polish (attn-rd3j); the branch now carries five verified bodies of work.
What's here
attn-64iy — browser App.svelte repair + shell-aware chrome. The comment round trip works in the browser dev loop (mock share publishes real snapshots with canonical anchor indexes); composer entry points fail honestly instead of silently; chrome is shell-aware (brand takes the freed corner in a browser tab, desktop keeps traffic-light clearance); header cluster evened out; comments toggle uses the panel glyph pair; ShareChip rests as a ghost.
attn-evme — Kindle-calm reading surface. One ink for prose; links are the accent with a load-bearing rest underline (the contrast maths shows no colour alone can pass AA against the page and 3:1 against body text); code/tables/frontmatter recessed into the page; syntax theme re-picked for the new ground.
attn-yzsa — desktop autosave.
native-autosave.ts(pure gate + clock): debounce + ceiling, a disk-conflict HOLD that refuses to overwrite a file changed underneath the editor, ⌘S as immediate flush. "Changes autosaved" is now true on both surfaces. Proven against a real daemon writing a real file.Design-system consolidation — 10 ratified issues. Type ramp gains its micro end (11px standard / 10px floor; 78 arbitrary sizes collapsed); pill radius unified; literals → tokens (fixing a dark-mode contrast bug in suggestion ghosts); dead static renderer deleted; the document surface single-sourced under an explicit six-layer cascade (
theme < base < chrome < doc < components < utilities), retiring the table-twin and unlayered-link escape hatches; one save-state vocabulary; all three headers reconciled (attn-o17v).Guard rails added
reading-palette.spec.ts(live contrast sweep, both themes, canvas-resolved colours),doc-surface-parity,design-doc-parity(DESIGN.md frontmatter pinned to tokens.css, mutation-tested),save-state-copyorphan sweep, header-parity extensions,review-bar-cluster,shell,compose-availability,mock-share-snapshot,native-autosave(20 cases).Verification
local-file-source.test.tsWIP)🤖 Generated with Claude Code