fix(webapp): #content shows it scrolls, Public links moves up (BEA-79) (#128)

Project settings at 1440x900 ended at People. Public links — the section
that answers "is anything of ours public right now?" — was below the fold
of #content, the app's only scroll container, and with macOS overlay
scrollbars nothing at rest said there was more.

#content now takes a real scrollbar. The plan called for scrollbar-width
+ scrollbar-color alongside ::-webkit-scrollbar; measured, that combination
is a no-op: Chromium drops every ::-webkit-scrollbar rule as soon as either
standard property is set on the same element, and the standard properties
alone leave the macOS overlay bar, which takes no width and is invisible at
rest. So the standard properties sit behind @supports not
selector(::-webkit-scrollbar) — Firefox only. Verified: 10px reserved in
Chromium, 0 on touch.

Public links also moves from fourth card to second (General → Public links
→ People → About → Danger zone), so the security answer is above the fold
even without the cue.

browse.spec reached the shares table by .last(), which the reorder would
have silently repointed at the members table — both render through
AdminTable. It selects .shares-table now.

Known gap: Firefox on macOS always uses overlay scrollbars and no CSS
overrides it, so the bar there stays hidden at rest — that is an OS
setting, not something this can fix.
This commit is contained in:
Snow Lee (Sungwon)
2026-08-11 01:36:19 +09:00
committed by GitHub
parent 7a20849631
commit 6bb7debe2b
8 changed files with 68 additions and 13 deletions
+2 -1
View File
@@ -435,7 +435,8 @@ test("public links: banner and settings table fit a 390px viewport", async ({ pa
await expect(page.locator(".admin-item", { hasText: "guide.md" })).toBeVisible();
expect(await sideways()).toBe(false);
// The table takes its own horizontal scroll rather than widening the page.
const box = page.locator(".project-settings .admin-card-table").last();
// By class, not by position: People renders through the same AdminTable.
const box = page.locator(".project-settings .shares-table");
expect(await box.evaluate((el) => getComputedStyle(el).overflowX)).toBe("auto");
await page.request.delete(`/api/shares/${made.token}`);
+4
View File
@@ -31,6 +31,10 @@ export async function login(page: Page, email: string = ADMIN) {
sessions.set(email, await page.context().cookies());
}
// Note for anything that scrolls or screenshots a route: the document itself
// never scrolls — #content is the one scroll container. A full-page screenshot
// stops at the viewport and window.scrollTo does nothing; scroll #content.
export async function wikiId(page: Page): Promise<string> {
const out = await (await page.request.get("/api/projects")).json();
return out.projects.find((p: { name: string }) => p.name === "wiki").id;
@@ -111,6 +111,35 @@ test("the gutter belongs to the scroll container, not the column", async ({ page
}
});
/* BEA-79: Public links was invisible at 1440x900 below the fold of #content,
which is the app's only scroll container, and with overlay scrollbars nothing
at rest said there was more. Two halves: the container now takes a real
scrollbar (so it consumes width, so it is visible), and the security section
sits second instead of fourth. */
test("settings shows Public links high, and #content shows it scrolls", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto(`http://localhost:8993/${pid}/settings`);
await expect(page.locator(".project-settings [data-slot=card-title]")).toHaveText([
"General",
"Public links",
"People",
"About",
"Danger zone",
]);
// A classic scrollbar takes layout width; an overlay one doesn't. This is
// the affordance — no hover, no scroll event.
const bar = await page.evaluate(() => {
const c = document.querySelector("#content") as HTMLElement;
return { gap: c.offsetWidth - c.clientWidth, overflows: c.scrollHeight > c.clientHeight };
});
expect(bar.overflows, "settings must overflow at 1440x900").toBe(true);
expect(bar.gap, "#content reserves a visible scrollbar").toBeGreaterThan(0);
});
/* The who/when/how-hot line is what the product is differentiated by, and a
phone is exactly when you're catching up but 900px used to `display:
none` it on the file view and ellipsise it to `claude-` / `Alice <ali` in
@@ -26,9 +26,10 @@ import { atLeast } from "../api/types";
import type { Org, PermLevel, Project, ProjectPerms } from "../api/types";
// Settings for the open project (sidebar menu): General edits the name,
// description and icon; About holds the identity facts; People says who can
// do what; the danger zone deletes. Install/connect lives on the Installation
// page.
// description and icon; Public links answers "is anything of ours public right
// now?" and sits high for that reason; People says who can do what; About
// holds the identity facts; the danger zone deletes. Install/connect lives on
// the Installation page.
const MAX_DESC = 280;
@@ -219,6 +220,10 @@ export function ProjectSettings({
</CardContent>
</Card>
<PublicLinks project={project} />
<People project={project} org={org} />
<Card>
<CardHeader>
<CardTitle>About</CardTitle>
@@ -246,10 +251,6 @@ export function ProjectSettings({
</CardContent>
</Card>
<People project={project} org={org} />
<PublicLinks project={project} />
{/* Admin-only, and only as UX: handleProjectDelete enforces it too. */}
{mayEdit && (
<Card className="ps-danger">
+21 -1
View File
@@ -400,7 +400,27 @@ button, input, a.btn { font-family: inherit; }
/* The scroll container owns scrolling and the page gutter never a width.
Width and centering belong to .page (one per view), so every route shares
the same column edges and the gutter never eats into the reading measure. */
#content { flex: 1; overflow-y: auto; padding: 44px 40px 110px; scroll-behavior: smooth; }
#content { flex: 1; overflow-y: auto; padding: 44px 40px 110px; scroll-behavior: smooth; scrollbar-gutter: stable; }
/* #content is the only scroller in the app, so with the platform's overlay
scrollbars nothing at rest says a route has more below (that's how Public
links went missing on settings). Giving ::-webkit-scrollbar an explicit
width turns it into a classic, always-visible bar which is also what makes
scrollbar-gutter mean anything on macOS. Pointer-fine only: touch keeps
overlay scrollbars and the mobile layout is untouched. */
@media (pointer: fine) {
#content::-webkit-scrollbar { width: 10px; }
#content::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 5px; }
#content::-webkit-scrollbar-track { background: transparent; }
/* Chromium drops every ::-webkit-scrollbar rule the moment scrollbar-width
or scrollbar-color is set on the same element and the standard
properties alone leave macOS's overlay bar, which is invisible at rest and
takes no width. So the standard properties are for engines with no webkit
pseudo-element (Firefox) only. Setting both is the trap: it silently
restores the bug. */
@supports not selector(::-webkit-scrollbar) {
#content { scrollbar-width: thin; scrollbar-color: var(--border-2) transparent; }
}
}
.page { width: 100%; max-width: var(--page-app); margin-inline: auto; min-width: 0; }
.page.read { max-width: var(--page-read); }
.page.wide { max-width: var(--page-wide); }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
<script type="module" crossorigin src="/assets/index-BeL6RCAd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C62PcDae.css">
<script type="module" crossorigin src="/assets/index-wL8nzVtO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BdCy9HmN.css">
</head>
<body>
<div id="root"></div>