feat(ui): full-bleed library grid and a resizable docked asset panel - #22
Merged
Merged
Conversation
… sit
The library was built in M1 for one screen size and never revisited while
M3-M6 accreted panels onto it. Two consequences, both reported by the user.
The grid sat in `mx-auto max-w-7xl`, so above 1280px the library was a
centred column with dead margins either side, and its `grid-cols-2 →
sm:3 → lg:4 → xl:6` ramp stopped at six — past that, tiles stretched
instead of multiplying. It is full-bleed now, on `auto-fill` tracks that
add columns as room appears: 2 on a 360px phone, 4 at 1280, 6 at 1920, 8
at 2560. `auto-fill` rather than `auto-fit` so a half-empty last row stays
left-aligned instead of stretching two tiles across the screen, and a
`max-w-[25rem]` on the card is what actually caps a tile, since auto-fill
tracks reach roughly twice their minimum before a further column fits.
Opening an asset was a 896px centred modal with a fixed 320px sidebar and
five hard-coded viewport caps — 45vh video, 60vh image, 38vh transcript
twice, 90vh dialog — small on a large screen, cramped on a small one, and
covering the library either way. It now docks against the right edge from
1024px up, drag-resizable from a `role="separator"` handle (arrow keys and
Home/End too, double-click to reset) with the width remembered, and the
grid reflows beside it. Narrower than that, it is a full-screen sheet.
The media pins to the top and the rest lives in tabs that fill the
remaining height, so the transcript gets the panel instead of 38vh; past
`@4xl` the media moves beside the tabs instead.
Container queries, not viewport breakpoints. Once the panel is open and
drag-resizable the viewport no longer describes how much room the grid
beside it has, so `xl:` is simply the wrong signal. That is the one new
dependency, build-time only.
`AssetDetail` baked `fixed inset-0 … bg-black/50` into itself, which is
why all three of its callers got modal chrome whether it suited them or
not. Positioning moved to `DetailDock`; the content fills whatever box it
is handed. `/a/{id}` was the clearest victim — it drew a floating dialog
over an empty shell, and is a full-width page now. Its four tests asserted
`role="dialog"`, which was asserting the bug, so they assert the heading
instead; the stale `usageApi.forAsset` mock beside them, whose field names
are not on `UsageTotals` and were hidden by an `as never`, is fixed too.
Description and summary auto-grow rather than scrolling inside 80px, and
re-measure on a `ResizeObserver` because the panel's width changes under
them without the text ever changing.
`Tabs` is the first UI primitive here. It keeps hidden panels mounted:
`TranscriptPanel` fetches on mount, so rendering only the active tab would
refetch the transcript and lose its scroll position every time you checked
the description and came back.
Verified in a real browser at 390/768/1280/1920/2560, light and dark, with
the API stubbed at the network layer — column counts and tile widths come
out as designed and nothing throws.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTm2BUsWF7ufB3fYs9kNgB
Written before the pull request existed, so the entry had nowhere to send a later session looking for the reasoning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTm2BUsWF7ufB3fYs9kNgB
davior
marked this pull request as ready for review
September 17, 2026 07:02
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.
M1 built the library UI for one screen size and never revisited it while M3–M6 accreted panels onto
AssetDetail. Two consequences, both reported by the user.The grid wasted the screen
LibraryViewsat inmx-auto max-w-7xl, so above 1280px the library was a centred column with dead margins either side. Itsgrid-cols-2 → sm:3 → lg:4 → xl:6ramp then stopped at six, so past 1280px tiles stretched instead of multiplying.It is full-bleed now, on
auto-filltracks that add columns as room appears rather than at four fixed viewport widths. Measured in a real browser:auto-fillrather thanauto-fit, so a half-empty last row stays left-aligned instead of stretching two tiles across the screen. Amax-w-[25rem]on the card is what actually caps a tile — auto-fill tracks reach roughly twice their minimum before a further column fits — and the grid'sjustify-items-centerkeeps a capped tile centred in its track rather than leaving the gutter all on one side.Opening an asset took over badly
AssetDetailwas an 896px centred modal with a fixed 320px sidebar and five hard-coded viewport caps (45vhvideo,60vhimage,38vhtranscript ×2,90vhdialog) — small on a large screen, cramped on a small one, and covering the library either way.It now docks against the right edge from 1024px up, drag-resizable from a handle, with the library visible and reflowing beside it. Below 1024px it is a full-screen sheet. The media pins to the top and the rest lives in tabs that fill the remaining height, so the transcript gets the whole panel instead of a 38vh window; past
@4xlthe media moves beside the tabs instead of above them.The drag handle is a real
role="separator"— arrow keys, Home/End, double-click to reset — and the width is remembered inlocalStorage.Container queries, not viewport breakpoints
Once the panel is open and drag-resizable, the viewport no longer describes how much room the grid beside it has, so
xl:is the wrong signal. Both the grid and the panel interior are driven by@container. That is the one new dependency (@tailwindcss/container-queries), build-time only, and it keeps everything in class strings rather than adding a semantic class tomain.css, which is a vendored file exempt from Prettier.Chrome split from content
AssetDetailbakedfixed inset-0 … bg-black/50into itself, which is why all three of its callers got modal chrome whether it suited them or not. Positioning moved to a newDetailDock; the content fills whatever box it is handed./a/{id}was the clearest victim — it drew a floating dialog over an empty shell. It is a full-width page now, which is what the route was for.Changes
New —
components/DetailDock.tsx,components/Tabs.tsx(the first UI primitive here),utils/useMediaQuery.ts,utils/useResizablePanel.ts,utils/useAutoGrow.ts, plus co-located tests for the two components.Reworked —
components/AssetDetail.tsx,views/LibraryView.tsx,views/SearchView.tsx,views/AssetView.tsx,components/AssetCard.tsx.Touched —
TranscriptPanel/DocumentTextPanelgainflex-1on their roots so they fill a tab;tailwind.config.jsandtest-setup.ts(stubs formatchMediaandResizeObserver, alongside the two jsdom already needed).Description and summary auto-grow instead of scrolling inside 80px, and re-measure on a
ResizeObserverbecause the panel's width changes under them without the text ever changing.Tabskeeps hidden panels mounted:TranscriptPanelfetches on mount, so rendering only the active tab would refetch the transcript and throw away its scroll position every time you checked the description and came back.Test impact
LibraryView.test.tsxneeded no changes —DetailDockkeepsrole="dialog"and the accessible name — and gains one test for swapping assets without closing the panel.AssetView.test.tsx's fourrole="dialog"assertions became heading assertions./a/{id}is a page now, and asserting it is a dialog would be asserting the bug. The staleusageApi.forAssetmock beside them is fixed too — its field names (total_cost,events,estimated) are not onUsageTotals, and anas nevercast was hiding it.257 tests pass;
format:check,lint,vitestandbuildare all green. No backend files are touched.Verification
Driven in a real Chromium at 390/768/1280/1920/2560, light and dark, with the API stubbed at the network layer: column counts and tile widths come out as tabled above, the grid reflows live as the panel is dragged, the transcript fills the panel, the sheet covers the screen on a phone, and nothing throws.
Known gaps, deliberately not closed here
Both are recorded under Outstanding, unscheduled in
docs/plan-of-attack.mdrather than widening this change:TagInputand the transcript segment editor handle Escape withoutstopPropagation, so dismissing a suggestion menu or abandoning a half-typed correction also closes the panel. Predates this work — the old modal had the same listener.inert. Docked, none of it applies.While in that section, two entries it still listed as gaps were struck: the
/a/{id}deep link and the 401 interceptor were both built incdcabbf, and a handoff doc that reports closed gaps as open defeats its own purpose.🤖 Generated with Claude Code
https://claude.ai/code/session_01YTm2BUsWF7ufB3fYs9kNgB
Generated by Claude Code