Skip to content

Commit dd4274b

Browse files
TimMikeladzeclaude
andcommitted
fix(app): show the toolbar on mobile instead of hiding it
The landing page carried a `display: none !important` on `.devbar-toolbar` below 640px, dating from when the toolbar had no responsive rules at all and its centred pill overlapped the hero. The toolbar now edge-anchors itself on narrow viewports, so the rule only had one effect left: the page selling devbar showed no devbar on a phone. Drop the rule and give the footer enough bottom padding to clear the bar, which floats over the last ~56px of the page. The bar's own narrow-viewport row is ~60px wider than a 390px phone. It was already scrollable, but with the scrollbar hidden the trailing controls just looked chopped off. Fade the scrollable edge so they read as more to scroll to, and contain the overscroll so a swipe on the bar does not drag the page. Covered by test/e2e/mobile-toolbar.spec.ts: visible and edge-anchored at 390px, 40px touch targets, trailing controls reachable by an actual user-scrollable overflow. playwright.config.ts now takes PORT. A long-lived `test/ui` dev server on 3847 kept serving a stale bundle to every later run, so this fix looked untested until the suite got a fresh port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFn2hPe9mhLC23vBFhi3Qx
1 parent 7ccd47a commit dd4274b

5 files changed

Lines changed: 96 additions & 9 deletions

File tree

app/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1576,7 +1576,7 @@ const footerLinks = [
15761576

15771577
function Footer() {
15781578
return (
1579-
<footer className="border-t border-border pt-8 mt-4 pb-8">
1579+
<footer className="site-footer border-t border-border pt-8 mt-4 pb-8">
15801580
<div className="flex flex-wrap items-center gap-1 text-[13px]">
15811581
<span className="flex items-center gap-2 text-fg font-semibold tracking-tight mr-3">
15821582
<Wordmark />

app/src/index.css

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1744,11 +1744,15 @@ html.dark .page-grid::before {
17441744
}
17451745
}
17461746

1747-
/* ──────── Landing: hide the dogfooded toolbar on mobile ────────
1748-
Its default fixed position overlaps hero/pricing content below sm.
1749-
Docks cleanly on desktop, so only hide on narrow screens. */
1747+
/* ──────── Landing: dogfooded toolbar on mobile ────────
1748+
This used to be `display: none` below sm, back when the toolbar had no
1749+
responsive rules and its centred 32px bar overlapped the hero. The toolbar
1750+
now edge-anchors itself under 640px (touch-sized targets, safe-area inset),
1751+
so it stays visible here — hiding the product on the page selling it was the
1752+
worse trade. It floats over the last ~56px of the page, so the footer buys
1753+
back that height rather than sitting under the bar. */
17501754
@media (max-width: 640px) {
1751-
.devbar-toolbar {
1752-
display: none !important;
1755+
.site-footer {
1756+
padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
17531757
}
17541758
}

playwright.config.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import { defineConfig } from "@playwright/test";
22

3+
// A long-lived dev server on 3847 serves a stale bundle to every later run, so
4+
// PORT picks a free one instead of reusing (or killing) whatever holds it.
5+
const port = Number(process.env.PORT ?? 3847);
6+
const baseURL = `http://localhost:${port}`;
7+
38
export default defineConfig({
49
testDir: "./test/e2e",
510
timeout: 30_000,
611
retries: 0,
712
use: {
8-
baseURL: "http://localhost:3847",
13+
baseURL,
914
headless: true,
1015
},
1116
webServer: {
1217
command: "bun run --cwd test/ui dev",
13-
url: "http://localhost:3847",
14-
reuseExistingServer: true,
18+
url: baseURL,
19+
env: { PORT: String(port) },
20+
reuseExistingServer: !process.env.PORT,
1521
timeout: 10_000,
1622
},
1723
projects: [

src/toolbar/toolbar.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3197,7 +3197,14 @@
31973197
/* 10 controls at touch size exceed a phone's width — scroll rather than
31983198
letting anything become unreachable. */
31993199
overflow-x: auto;
3200+
overscroll-behavior-x: contain;
32003201
scrollbar-width: none;
3202+
/* The row is ~60px wider than a 390px phone, so the trailing controls sit
3203+
past the edge. Fade them instead of hard-clipping: a half-drawn icon
3204+
under a hard edge reads as a broken bar, under a fade it reads as more
3205+
to scroll to. */
3206+
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
3207+
mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
32013208
}
32023209

32033210
.devbar-bar::-webkit-scrollbar {
@@ -3211,6 +3218,10 @@
32113218
bottom: auto;
32123219
overflow-x: visible;
32133220
justify-content: flex-start;
3221+
/* Column layout is content-height and never overflows sideways — the
3222+
horizontal fade would just dim its right edge. */
3223+
-webkit-mask-image: none;
3224+
mask-image: none;
32143225
}
32153226

32163227
.devbar-bar-btn {

test/e2e/mobile-toolbar.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
const PHONE = { width: 390, height: 844 };
4+
5+
test.describe("Toolbar on narrow viewports", () => {
6+
test.beforeEach(async ({ page }) => {
7+
await page.setViewportSize(PHONE);
8+
await page.goto("/");
9+
await page.waitForSelector(".devbar-bar");
10+
});
11+
12+
test("bar stays visible and edge-anchored on a phone-width viewport", async ({ page }) => {
13+
const bar = page.locator(".devbar-bar");
14+
await expect(bar).toBeVisible();
15+
16+
const box = (await bar.boundingBox())!;
17+
// Edge-anchored with an 8px inset rather than the desktop centred pill.
18+
expect(box.x).toBeLessThanOrEqual(10);
19+
expect(box.x + box.width).toBeGreaterThanOrEqual(PHONE.width - 10);
20+
expect(box.y + box.height).toBeGreaterThan(PHONE.height - 100);
21+
});
22+
23+
test("controls keep a touch-sized target", async ({ page }) => {
24+
const buttons = page.locator(".devbar-bar .devbar-bar-btn");
25+
const count = await buttons.count();
26+
expect(count).toBeGreaterThan(0);
27+
28+
for (let i = 0; i < count; i++) {
29+
const box = (await buttons.nth(i).boundingBox())!;
30+
expect(box.width).toBeGreaterThanOrEqual(40);
31+
expect(box.height).toBeGreaterThanOrEqual(40);
32+
}
33+
});
34+
35+
test("trailing controls stay reachable when the row overflows the phone", async ({ page }) => {
36+
// Ten touch-sized controls are wider than 390px. They must scroll into
37+
// reach rather than sit clipped past the bar's right edge.
38+
const metrics = await page.evaluate(() => {
39+
const bar = document.querySelector<HTMLElement>(".devbar-bar")!;
40+
const buttons = bar.querySelectorAll<HTMLElement>(".devbar-bar-btn");
41+
const last = buttons[buttons.length - 1];
42+
const overflows = bar.scrollWidth > bar.clientWidth;
43+
const beforeRight = last.getBoundingClientRect().right;
44+
bar.scrollLeft = bar.scrollWidth;
45+
return {
46+
overflows,
47+
scrolled: bar.scrollLeft > 0,
48+
beforeRight,
49+
afterRight: last.getBoundingClientRect().right,
50+
barRight: bar.getBoundingClientRect().right,
51+
overflowX: getComputedStyle(bar).overflowX,
52+
mask: getComputedStyle(bar).maskImage || getComputedStyle(bar).webkitMaskImage,
53+
};
54+
});
55+
56+
expect(metrics.overflows).toBe(true);
57+
// `overflow-x: hidden` still answers to scrollLeft, so a programmatic
58+
// scroll alone proves nothing — the axis has to be user-scrollable.
59+
expect(metrics.overflowX).toMatch(/auto|scroll/);
60+
expect(metrics.scrolled).toBe(true);
61+
expect(metrics.beforeRight).toBeGreaterThan(metrics.barRight);
62+
expect(metrics.afterRight).toBeLessThanOrEqual(metrics.barRight + 1);
63+
// A fade marks the clipped edge as "more to scroll to", not a broken bar.
64+
expect(metrics.mask).toContain("linear-gradient");
65+
});
66+
});

0 commit comments

Comments
 (0)