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
79 changes: 53 additions & 26 deletions apps/web/src/pages/library-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -480,33 +480,60 @@ export function LibraryPage({
) : null}
{workbenchScope !== null && onScopeChange !== undefined ? (
// One control, two states — answers exactly one question
// ("whose files"), styled as the bordered segmented group
// `ViewToggle` already uses for rows/grid in this same bar, so
// it reads as one control rather than two stray chips.
<div
role="group"
aria-label="Files scope"
className="hidden items-center gap-0.5 rounded-md border border-border p-0.5 lg:flex"
>
<Button
type="button"
size="sm"
variant={scope === "workbench" ? "outline" : "ghost"}
aria-pressed={scope === "workbench"}
onClick={() => onScopeChange("workbench")}
// ("whose files"). At lg the bordered segmented group matches
// `ViewToggle` in this bar; below lg that group is hidden and
// the overflow menu in this same slot is the way to reach
// All workbenches.
<>
<div
role="group"
aria-label="Files scope"
className="hidden items-center gap-0.5 rounded-md border border-border p-0.5 lg:flex"
>
{workbenchScope.title}
</Button>
<Button
type="button"
size="sm"
variant={scope === "all" ? "outline" : "ghost"}
aria-pressed={scope === "all"}
onClick={() => onScopeChange("all")}
>
All workbenches
</Button>
</div>
<Button
type="button"
size="sm"
variant={scope === "workbench" ? "outline" : "ghost"}
aria-pressed={scope === "workbench"}
onClick={() => onScopeChange("workbench")}
>
{workbenchScope.title}
</Button>
<Button
type="button"
size="sm"
variant={scope === "all" ? "outline" : "ghost"}
aria-pressed={scope === "all"}
onClick={() => onScopeChange("all")}
>
All workbenches
</Button>
</div>
<div className="lg:hidden">
<Menu>
<MenuTrigger asChild>
<Button
type="button"
size="sm"
variant="ghost"
aria-label="Files scope"
>
{scope === "all"
? "All workbenches"
: workbenchScope.title}
</Button>
</MenuTrigger>
<MenuContent align="end">
<MenuItem onSelect={() => onScopeChange("workbench")}>
{workbenchScope.title}
</MenuItem>
<MenuItem onSelect={() => onScopeChange("all")}>
All workbenches
</MenuItem>
</MenuContent>
</Menu>
</div>
</>
) : null}
<Menu>
<MenuTrigger asChild>
Expand Down
27 changes: 27 additions & 0 deletions apps/web/test/library-page-selection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,33 @@ describe("LibraryPage top-nav action placement", () => {
);
});

test("the files scope control stays in the document below the lg breakpoint", () => {
act(() => {
root.render(
<LibraryPage
artifacts={artifacts}
workbenchScope={{ title: "Launch plan" }}
scope="all"
onScopeChange={() => undefined}
/>,
);
});
const topBarActions = container.querySelector(
'[data-testid="stage-top-bar-actions"]',
);
const scopeControls = [
...(topBarActions?.querySelectorAll('[aria-label="Files scope"]') ?? []),
];
expect(scopeControls.length).toBeGreaterThan(0);
// `hidden` is display:none. A Files-scope control that only exists
// behind `hidden lg:flex` vanishes below 1024px with no way to reach
// All workbenches. At least one control must stay in the tree without
// that class — the overflow menu in this bar, or the group itself.
expect(
scopeControls.some((el) => !el.className.split(/\s+/).includes("hidden")),
).toBe(true);
});

test("the file detail pane fills the available height", () => {
act(() => {
root.render(
Expand Down
Loading