fix: make a composed board a first-class board, and give the app a mobile layout - #12
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 onlyload()subscribed and that runs once at boot. Switching boards left the client attached to the boot board's stream, so the server'sboardIdfilter 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.setActiveCollectionnow re-subscribes.Composed tiles rendered a title and nothing else.
/api/collectionsderivestype: "inspiration"for any grid board, so composed boards fell intorenderGrid, which readsmeta.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:cardSummarypicks 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-statusspan, which isopacity: 0until 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_MODELis 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-barand 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-btnthe desktop uses.Other fixes, all pre-existing
#collection-switcherwasflex-shrink: 0beside 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.addedholds 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 testedsortItems().max-heightwould have clipped them, since.modalisoverflow: hiddenand the overlay is what scrolls.viewport-fit=cover, so the dock clears the home indicator.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
/eventsrequest 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
--text-3is 2.3:1 (dark) / 2.6:1 (light) and fails AA. It is used app-wide including on.data-faband.tag. Fixed only where it landed on newly authored markup; fixing the token globally would restyle the whole app.🤖 Generated with Claude Code
https://claude.ai/code/session_01C83mrW9X8zBLY1sSZRgdCa