Skip to content

fix: make a composed board a first-class board, and give the app a mobile layout - #12

Merged
Seanathon merged 2 commits into
mainfrom
fix/live-status-and-composed-board-cards
Aug 19, 2026
Merged

fix: make a composed board a first-class board, and give the app a mobile layout#12
Seanathon merged 2 commits into
mainfrom
fix/live-status-and-composed-board-cards

Conversation

@Seanathon

Copy link
Copy Markdown
Owner

Four reported bugs shared one cause: the app knew the two seeded boards and treated everything the composer made as an approximation of them. Fixing that turned up several more, all pre-existing and all found from real use.

The reported bugs

Live status never reached a board you switched to. The status stream is board-scoped (/events?boardId=…), but only load() subscribed and that runs once at boot. Switching boards left the client attached to the boot board's stream, so the server's boardId filter dropped every event — cards on any other board sat in their skeleton until a manual refresh. That is why Inspiration worked and net-new boards, Inbox and Library did not. setActiveCollection now re-subscribes.

Composed tiles rendered a title and nothing else. /api/collections derives type: "inspiration" for any grid board, so composed boards fell into renderGrid, which reads meta.tags / design.steal_this — keys their descriptors do not have. The data was always there (the detail modal showed it); the renderer could not see it. Adds a descriptor-driven card: cardSummary picks a category badge, a lead line and chips out of whatever fields the board declared, with grid and list renderers over it. The seeded boards keep their bespoke renderers untouched.

Compose looked like a dead modal for 90 seconds. Its only feedback was a .save-status span, which is opacity: 0 until something adds .visible — nothing did, so the message was literally invisible. Adds a waiting state with a live clock and an honest phase line (no fake percentage — nothing knows how far along the model is), and rebuilds the proposal so the lens outranks the field inventory: a two-column definition list rather than 13 bordered boxes with ragged rows and an orphan.

Headless Claude ran on the operator's interactive default, usually Opus, for a fixed-schema extraction. Both headless callers now ask for Sonnet unless LLM_MODEL is set.

Mobile

The app had no mobile layout. The only responsive rules covered the empty state, the welcome modal and the library list; the header, filters, board switcher and tag cloud were fixed horizontal rows that overflowed the viewport, so the page scrolled sideways into empty space and the logo clipped to "Boa".

Below 900px — every common vertical tablet: iPad Mini 744, iPad 810, iPad Pro 11" 834 — the two header rows collapse to one and a left drawer takes the rest: boards, with "Edit this board" branching off the active row so "this" has a referent, then that board's filters, view controls and tags, over a footer carrying Clear / Show N and the data affordance.

The drawer hosts the desktop's own nodes rather than copying them. syncChrome() moves the real #collection-switcher, .filters, .header-right, .add-bar and tag clouds into it and moves them back above the breakpoint, so every listener already bound keeps working and there is no second copy of any control to keep in sync.

Adding is a dock, not a header mode: the + morphs to a and the URL field slides out beside the button, so the field arrives where you pressed rather than at the far end of the page. It submits through the same #add-btn the desktop uses.

Other fixes, all pre-existing

  • The header overlapped itself. #collection-switcher was flex-shrink: 0 beside a fixed-width .add-input, so past ~5 boards neither side could give: the left group overflowed its box and painted over the add bar by 76px. The board list now scrolls, with ⚙ and + pinned outside it so the create affordance can never scroll away.
  • Sort was inverted. added holds only a date, so same-day items tie, and the tie-break ran backwards against a newest-first API — "Recently added" listed a board added today oldest-first. Extracted to a tested sortItems().
  • Modals did not lock the page behind them, and a mobile max-height would have clipped them, since .modal is overflow: hidden and the overlay is what scrolls.
  • Grid view was capped at 1600px. It now runs full width, so a wide display buys columns (6 at 1920px) instead of margins; list view keeps an 1100px measure.
  • Touch targets: card buttons were 30px, tier buttons ~21px, chips ~19px.
  • viewport-fit=cover, so the dock clears the home indicator.
  • The add button says "Add with Claude" — the extra word cost header width the switcher needed.

Verification

591 tests and typecheck green. Driven in a real browser against a copy of a real board at 377 / 390 / 400 / 768 / 1113 / 1613 / 1920px: no horizontal overflow at any width, no console errors across all five boards, drawer focus-trapped and Esc-dismissable, scroll lock released every time. An item added to a composed board fills its badge, lead and tags live with no refresh, and a new /events request opens on each board switch.

Desktop non-interference was measured, not asserted: ten header and content elements compared against the stashed pre-change build at 1113px, identical geometry.

Worth a reviewer's attention

  • Desktop changed three times (header overlap, button label, full-width grid). Each was explicitly requested; the mobile work itself touches nothing above 900px.
  • One deviation from spec: wrapping tag chips are 40px, not the 44px floor stated in the design brief. Thirty chips at 44px made the drawer section unreasonably tall. Clears WCAG 2.2's 24px floor, but it is not the number promised.
  • Left alone deliberately: --text-3 is 2.3:1 (dark) / 2.6:1 (light) and fails AA. It is used app-wide including on .data-fab and .tag. Fixed only where it landed on newly authored markup; fixing the token globally would restyle the whole app.
  • Not verified on real hardware. The synchronous-focus fix that lets iOS open the keyboard is reasoned from the platform rule, not observed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01C83mrW9X8zBLY1sSZRgdCa

Seanathon and others added 2 commits August 19, 2026 03:22
Four bugs that all shared one cause — the app knew the two seeded boards
and treated everything the composer made as an approximation of them.

Live status never reached a board you switched to. The status stream is
board-scoped (/events?boardId=…), but only load() subscribed, and load()
runs at boot. Switching boards left the client attached to the boot
board's stream, so the server's boardId filter dropped every event:
cards on any other board sat in their skeleton until a refresh.
setActiveCollection now re-subscribes.

Composed tiles rendered a title and nothing else. /api/collections
derives type "inspiration" for any grid board, so composed boards fell
into renderGrid, which reads meta.tags/design.steal_this — keys their
descriptors don't have. Adds a descriptor-driven card (cardSummary picks
a category badge, a lead line and chips out of whatever fields the board
declared) plus grid and list renderers on it. The seeded boards keep
their bespoke renderers untouched.

Compose gave no feedback for the longest wait in the app. Its only
signal was a .save-status span, which is opacity:0 until something adds
.visible — nothing did, so a 90s round trip read as a dead modal. Adds a
waiting state with a live clock and an honest phase line, and rebuilds
the proposal so the lens (the stance) outranks the field inventory: the
fields are a two-column definition list, not 13 bordered boxes with
ragged rows and an orphan.

Headless claude ran on whatever the operator's interactive default was,
usually Opus, for a fixed-schema extraction. Both headless callers now
ask for Sonnet unless a model is configured.

Verified in a browser against a real board: a new /events opens per
switch, and an item added to a composed board fills its badge, lead and
tags live with no refresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C83mrW9X8zBLY1sSZRgdCa
The app had no mobile layout. The only responsive rules in index.html
covered the empty state, the welcome modal and the library list; the
header, filters, board switcher and tag cloud were fixed horizontal rows
that overflowed the viewport, so the page scrolled sideways into empty
space and the logo was clipped to "Boa".

Below 900px (every common vertical tablet: iPad Mini 744, iPad 810,
iPad Pro 11" 834) the two header rows collapse to one — wordmark, a
board chip, search, menu — and a left drawer takes the rest: boards,
with "Edit this board" branching off the active row so "this" has a
referent, then that board's filters, view controls and tags, over a
footer carrying Clear / Show N and the data affordance.

The drawer HOSTS the desktop's own nodes rather than copying them.
syncChrome() moves the real #collection-switcher, .filters,
.header-right, .add-bar and tag clouds into it and moves them back
above the breakpoint, so every listener already bound keeps working and
there is no second copy of any control to keep in sync.

Adding is a dock, not a header mode: the + morphs to a ✓ and the URL
field slides out beside the button, so the field arrives where you
pressed rather than at the far end of the page. It submits through the
same #add-btn the desktop uses.

Every layout change lives inside the media query and every new element
is display:none above it, so the desktop cascade reaches none of it —
verified by measuring ten header and content elements against the
pre-change build at 1113px and getting identical geometry.

Also fixed, all pre-existing and all reported from real use:

- The header overlapped itself. #collection-switcher was flex-shrink:0
  beside a fixed-width .add-input, so past ~5 boards neither side could
  give: the left group overflowed its box and painted over the add bar
  by 76px. The board list now scrolls, with ⚙ and + pinned outside it
  so the create affordance can never scroll away.
- Sort was inverted. `added` holds only a date, so same-day items tie,
  and the tie-break ran backwards against a newest-first API — "Recently
  added" listed a board added today oldest-first. Extracted to a tested
  sortItems().
- Modals didn't lock the page behind them, and a mobile max-height would
  have clipped them, since .modal is overflow:hidden and the overlay is
  what scrolls.
- Grid view was capped at 1600px. It now runs full width, so a wide
  display buys columns (6 at 1920px) instead of margins; list view keeps
  an 1100px measure.
- Touch targets: card buttons were 30px, tier buttons ~21px, chips ~19px.
- viewport-fit=cover, so the dock clears the home indicator.

The add button says "Add with Claude" — the extra word cost header width
the switcher needed.

Verified in a browser at 377/390/400/768/1113/1613/1920px: no horizontal
overflow at any width, no console errors across all five boards, drawer
focus-trapped and Esc-dismissable, scroll lock released every time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C83mrW9X8zBLY1sSZRgdCa
@Seanathon
Seanathon merged commit c9095dc into main Aug 19, 2026
2 checks passed
@Seanathon
Seanathon deleted the fix/live-status-and-composed-board-cards branch August 19, 2026 11:03
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