Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions frontend/app/e2e/project-nav.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,48 @@ test("the column's items are real links, so the section has an address before it
await expect(page.getByTestId("schema-editor")).toBeVisible();
await expect(page.getByTestId("nav-schema")).toHaveAttribute("aria-current", "page");
});

/**
* The product's responsive floor, asserted rather than eyeballed.
*
* A page that scrolls sideways is a defect; a *local* scroller that does not
* widen the page is not, and this file's own strip is deliberately one. So the
* assertion is on the document, which is the thing a user drags.
*
* Every width here caught something real: the schema's action row came to 460px
* and overflowed to about 500px, the dataset tabs could not shrink past their
* labels and counts, and the gallery's batch actions and its state filter each
* widened the page on their own. A regression in any of them is a sideways
* scrollbar on a screen somebody works in all day.
*
* One test per route rather than one sweep: fifteen navigations in a single test
* runs past the suite's 20s budget, and a timeout reads as "the floor is broken"
* when it means "the test did too much".
*/
const FLOOR_WIDTHS = [320, 375, 480] as const;

for (const route of [...SECTIONS, "batches-detail"] as const) {
const path =
route === "batches-detail"
? `/projects/${PROJECT}/batches/${BATCH}`
: `/projects/${PROJECT}/${route}`;

test(`${route} does not scroll the document sideways at the responsive floor`, async ({
page,
}) => {
await page.setViewportSize({ width: FLOOR_WIDTHS[0], height: 800 });
await openCold(page, path);
await expect(page.getByTestId("project-tabs")).toBeVisible();

for (const width of FLOOR_WIDTHS) {
await page.setViewportSize({ width, height: 800 });
const overflow = await page.evaluate(() => {
const de = document.documentElement;
return de.scrollWidth - de.clientWidth;
});
expect(overflow, `${path} at ${width}px scrolls the document sideways`).toBeLessThanOrEqual(
0,
);
}
});
}
11 changes: 10 additions & 1 deletion frontend/ui-core/src/screens/DatasetScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,15 @@ export function DatasetScreen({ projectId, tab, onTabChange }: DatasetScreenProp
>
{/* The product's one tab shape — a row on a full-width hairline, the
active tab's rule sitting on it — with the count as a chip beside the
two views whose size is the first thing anybody asks. */}
two views whose size is the first thing anybody asks.

The strip scrolls within its own row rather than widening the page,
which is what the project navigation does at the same width and for
the same reason: a trigger carries a label and a count, so it has a
width it cannot shrink past, and truncating the labels would cost
more than a scroll does. The padding pair keeps the focus ring off
the scroller's clip. */}
<div className="min-w-0 overflow-x-auto pb-1.5 -mb-1.5">
<TabsList variant="line">
<TabsTrigger value="overview" data-testid="dataset-tab-overview">
Overview
Expand All @@ -198,6 +206,7 @@ export function DatasetScreen({ projectId, tab, onTabChange }: DatasetScreenProp
{releases.data !== undefined && <Badge>{releases.data.total}</Badge>}
</TabsTrigger>
</TabsList>
</div>

<TabsContent value="overview">
<Async query={stats} loadingRows={3}>
Expand Down
13 changes: 12 additions & 1 deletion frontend/ui-core/src/screens/GalleryScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,10 @@ function BatchHeader({
/>
</div>

<div className="flex items-center gap-2">
{/* Wraps rather than widening the page. Every control here is one a
batch's state offers, so none may be dropped at a narrow width; they
take a second line instead. */}
<div className="flex flex-wrap items-center gap-2">
{/*
Draft only, and it opens the dialog rather than sending anything:
approval carries a partition, pins the schema and cuts the jobs, and
Expand Down Expand Up @@ -997,6 +1000,13 @@ function Toolbar({
>
{showSegments && (
<>
{/* The row above already wraps, but the control is one joined pill and
is wider than the narrowest viewport on its own. It scrolls within
its own row rather than widening the page — the project navigation's
answer to the same shape — because squashing five state filters
costs more than a scroll does. The padding pair keeps the focus ring
off the scroller's clip. */}
<div className="max-w-full overflow-x-auto pb-1.5 -mb-1.5">
<div
className="inline-flex rounded-md border border-border p-0.5"
role="group"
Expand All @@ -1022,6 +1032,7 @@ function Toolbar({
</button>
))}
</div>
</div>

<label className="flex items-center gap-2 text-xs text-muted-foreground">
Order
Expand Down
4 changes: 3 additions & 1 deletion frontend/ui-core/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,9 @@ function Resume({
data-kind={resume.kind}
>
<h2 className="text-xs font-medium text-muted-foreground">Continue where you left off</h2>
<div className="flex items-center gap-4">
{/* The resume control keeps its own width and wraps below the thumbnail
and the counts rather than pushing the page wider. */}
<div className="flex flex-wrap items-center gap-4">
{resume.thumbnail_asset_id !== null && (
<div className="size-16 shrink-0 overflow-hidden rounded-sm">
<AssetThumbnail
Expand Down
8 changes: 6 additions & 2 deletions frontend/ui-core/src/screens/SchemaEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -709,16 +709,20 @@ export function SchemaEditor({
now"; there is no now — a published version is immutable, and the
navigator is the way back to the one that can be edited. Rendering it
greyed would also leave it in the DOM, where a test could click it. */}
{/* The row wraps rather than widening the page: the note field and the
two controls come to 460px, which stops fitting below about 500px.
Nova buttons do not shrink, by design, so the row is what gives. */}
{past === undefined && (
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
{/* Optional, and unlabelled beyond its placeholder because it is one
field: a version's commit message, written once at publish. There is
no edit path for it afterwards, so this is the only place it exists. */}
<Input
aria-label="Why this version"
placeholder="Why this version? (optional)"
data-testid="version-note"
className="w-56"
// Full width while the row is wrapped, its own width once the row fits.
className="w-full min-w-0 sm:w-56"
disabled={draftLocked}
value={note}
onChange={(event) => {
Expand Down
Loading