Skip to content

Review-surface polish: window chrome, review-exit guard, theming, code blocks (attn-rd3j) - #12

Draft
angusbezzina wants to merge 50 commits into
mainfrom
angus/attn-review-tweaks
Draft

Review-surface polish: window chrome, review-exit guard, theming, code blocks (attn-rd3j)#12
angusbezzina wants to merge 50 commits into
mainfrom
angus/attn-review-tweaks

Conversation

@angusbezzina

Copy link
Copy Markdown
Collaborator

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.css typed the whole page, not the document

Bare p / h1 / ul / li selectors applied everywhere, 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 reported "weird modal checkmark" was never an element in ShareDialog — it was the leak. Typography is now scoped to .attn-doc, and the .attn-chrome opt-out class that existed purely to undo the leak is deleted along with its six consumers.

The bullets were a faked absolutely-positioned ::before dot with no list-style fallback, so they disappeared entirely whenever the positioning context shifted — exactly the reported symptom. They are real ::marker glyphs now, which cannot detach from their line.

The table "rail" was 13px of stolen layout

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. That reserved gutter, painted with an always-on thumb, is the rail. Measured alternatives:

approach reserved
current (scrollbar-width: thin) 13px
scrollbar-width: auto 17px
webkit revert 17px
hidden 0px

Nothing restores overlay behaviour, so prose blocks 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; the first attempt silently lost the cascade.

Review-exit membership came from the wrong set

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 and the new guard never fired. Added roomPublishesPath, 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.rs renders no HTML and comrak's syntect feature 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

  • Hidden titlebar — new zoom_window IPC; double-click zooms and restores, attached to every existing drag surface, sharing dragWindow's interactive-element exclusion.
  • Code copy button — was appended inside the horizontal scroller, so it slid away. Moved onto a non-scrolling frame, along with the language label, which had the same defect.
  • Scrollbars in chrome — dialog bodies, the project-switcher dropdown and the Share dialog's lists route through the shared ScrollArea (measured: 0px reserved, vs 13px native).
  • Appearance — three-state Paper / Ink / System, durable in a new prefs.json and stamped into the page before the bundle loads, so launching never shows a frame of the wrong theme.
  • Typeset presets — Editorial / Modern / Compact per shadcn's typeset model, in a new Settings dialog reachable from the command palette. Editorial declares no overrides, so the default cannot drift.

Verification

Driven against the running app, not just the test suite:

  • double-click zoomed 960×720 → 1512×887 → back; double-clicking a button did not zoom
  • copy button moved 0px while content scrolled 106px
  • reserved scrollbar space 13px → 0 on every overflowing block
  • exit prompt fires, cancel preserves, confirm tears down cleanly, re-entering re-activates
  • system → dark resolved on a dark-mode Mac; a manual Paper choice survived a daemon restart

That pass 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.sh and test-review-e2e.sh are 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/.gitignore so it cannot be committed by accident.

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
attn Ready Ready Preview Aug 7, 2026 2:30am

Request Review

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>
angusbezzina and others added 22 commits August 4, 2026 21:26
…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>
angusbezzina and others added 23 commits August 4, 2026 22:47
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant