mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(webapp): keep the provenance line on mobile, unclipped in History (BEA-70) (#121)
Below 900px the file view hid the who/when/how-hot line outright
(`#meta { display: none }`) and the History run header ellipsised its
note and author to `claude-…` / `Alice <ali…` — the line the product is
differentiated by, gone exactly on the surface people catch up from.
CSS-only, inside the existing `@media (max-width: 900px)` block:
- `#topbar` wraps (`height: auto; min-height: 52px`) and `#meta` takes its
own full-width row, left-aligned and wrapping. `order: 1` is what keeps
Search / Share / ⋯ on row 1 — meta precedes them in the DOM, so a bare
flex-wrap would drag them down. `#meta:empty` keeps folder, dashboard
and history routes from gaining a blank strip.
- `.hrun-head` wraps; the note loses its 46% cap and both spans stop
ellipsising, with `.hrun-meta` on its own line under the note and the
time still on row 1.
Desktop (≥901px) is untouched — every rule lives inside the breakpoint.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
631e5be947
commit
5fb834e3a4
@@ -111,6 +111,94 @@ test("the gutter belongs to the scroll container, not the column", async ({ page
|
||||
}
|
||||
});
|
||||
|
||||
/* 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
|
||||
History. Desktop values are read first and compared, rather than hard-coded:
|
||||
the seeded hub's read counts drift, so a literal would flake. */
|
||||
test("provenance survives to a phone on the file view and in History", async ({ page }) => {
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
const runHead = page.locator(".hrun-head").first();
|
||||
|
||||
const read = async () => {
|
||||
await page.goto(`/${pid}/index.md`);
|
||||
// Not waitForSelector: #meta is always in the DOM, empty until the file
|
||||
// loads and (before this fix) display:none below 900px.
|
||||
await expect(page.locator("#meta")).not.toBeEmpty();
|
||||
const meta = await page.locator("#meta").textContent();
|
||||
await page.goto(`/${pid}/history`);
|
||||
await page.waitForSelector(".hrun-head");
|
||||
return {
|
||||
meta,
|
||||
note: await runHead.locator(".hrun-note").textContent(),
|
||||
runMeta: await runHead.locator(".hrun-meta").textContent(),
|
||||
time: await runHead.locator(".hrun-time").textContent(),
|
||||
};
|
||||
};
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
const desktop = await read();
|
||||
expect(desktop.meta, "desktop provenance line").toBeTruthy();
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto(`/${pid}/index.md`);
|
||||
await expect(page.locator("#meta")).not.toBeEmpty();
|
||||
await expect(page.locator("#meta"), "390px: provenance line visible").toBeVisible();
|
||||
|
||||
const m = await page.evaluate(() => {
|
||||
const bar = document.querySelector("#topbar") as HTMLElement;
|
||||
const meta = document.querySelector("#meta") as HTMLElement;
|
||||
const btns = [...bar.querySelectorAll<HTMLElement>(".btn, .icon-btn")].filter(
|
||||
(b) => b.getBoundingClientRect().width > 0,
|
||||
);
|
||||
const last = btns[btns.length - 1].getBoundingClientRect();
|
||||
const content = document.querySelector("#content") as HTMLElement;
|
||||
return {
|
||||
// Actions stay flush right on row 1, above the wrapped meta row.
|
||||
gapFromRight: Math.round(bar.getBoundingClientRect().right - last.right),
|
||||
actionsAboveMeta: last.bottom <= meta.getBoundingClientRect().top + 1,
|
||||
tap: Math.min(...btns.map((b) => b.getBoundingClientRect().height)),
|
||||
// Nothing clipped, and the grown topbar pushes content down rather
|
||||
// than overlapping it.
|
||||
clipped: meta.scrollWidth > meta.clientWidth + 1 || meta.scrollHeight > meta.clientHeight + 1,
|
||||
contentBelow: content.getBoundingClientRect().top >= bar.getBoundingClientRect().bottom - 1,
|
||||
overflow: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||
};
|
||||
});
|
||||
expect(m.gapFromRight, "390px: actions flush right").toBeLessThanOrEqual(10);
|
||||
expect(m.actionsAboveMeta, "390px: actions stayed on the first row").toBe(true);
|
||||
expect(m.tap, "390px: action tap target").toBeGreaterThanOrEqual(44);
|
||||
expect(m.clipped, "390px: provenance line clipped").toBe(false);
|
||||
expect(m.contentBelow, "390px: topbar overlaps the page").toBe(true);
|
||||
expect(m.overflow, "390px: horizontal page scroll").toBe(false);
|
||||
|
||||
const mobile = await read();
|
||||
expect(mobile, "390px: same provenance as desktop").toEqual(desktop);
|
||||
|
||||
const clip = await page.evaluate(() => {
|
||||
const head = document.querySelector(".hrun-head") as HTMLElement;
|
||||
const bad = (sel: string) => {
|
||||
const el = head.querySelector(sel) as HTMLElement;
|
||||
return el.scrollWidth > el.clientWidth + 1;
|
||||
};
|
||||
return { note: bad(".hrun-note"), meta: bad(".hrun-meta") };
|
||||
});
|
||||
expect(clip.note, "390px: run note clipped").toBe(false);
|
||||
expect(clip.meta, "390px: run meta clipped").toBe(false);
|
||||
|
||||
// Routes with no provenance must not gain a blank strip: the topbar is
|
||||
// exactly its desktop height there.
|
||||
await page.goto(`/${pid}/notes`);
|
||||
await page.waitForSelector(".dl-row");
|
||||
const folder = await page.evaluate(() => ({
|
||||
metaShown: getComputedStyle(document.querySelector("#meta") as HTMLElement).display !== "none",
|
||||
barHeight: Math.round((document.querySelector("#topbar") as HTMLElement).getBoundingClientRect().height),
|
||||
}));
|
||||
expect(folder.metaShown, "390px: empty meta on a folder route").toBe(false);
|
||||
expect(folder.barHeight, "390px: folder topbar height").toBe(52);
|
||||
});
|
||||
|
||||
/* Mobile folder rows used to drop .dl-meta entirely below 430px, leaving an
|
||||
unlabelled coloured dot as the only signal — and the dot's meaning lived in
|
||||
a title= that touch never shows and screen readers never read. The meta now
|
||||
|
||||
@@ -882,10 +882,19 @@ a.ai-main:hover { color: var(--accent); }
|
||||
#topbar .btn .ico { width: 18px; height: 18px; }
|
||||
#more-btn:not([hidden]) { display: inline-flex; }
|
||||
#history-btn, #upload-btn, #download { display: none !important; }
|
||||
/* Desktop right-aligns the actions via #meta's flex:1 — with meta
|
||||
hidden here, the crumb becomes the spacer so Search/Share/⋯ pin to
|
||||
the top-right corner. */
|
||||
#meta { display: none; }
|
||||
/* Who/when/how-hot is the line people catch up from, so it survives to a
|
||||
phone on its own row instead of being hidden. `order: 1` is load-bearing:
|
||||
meta sits before the actions in the DOM, so a bare flex-wrap would drag
|
||||
Search/Share/⋯ down with it. The crumb keeps flex:1 and stays the spacer
|
||||
that pins them to the top-right corner. */
|
||||
#topbar { flex-wrap: wrap; height: auto; min-height: 52px; }
|
||||
#meta {
|
||||
order: 1; flex: 1 1 100%; text-align: left;
|
||||
white-space: normal; overflow: visible; padding: 0 0 8px;
|
||||
}
|
||||
/* Folder, dashboard and history routes carry no meta — without this they
|
||||
gain a blank strip under the crumb. */
|
||||
#meta:empty { display: none; }
|
||||
#crumb { flex: 1; }
|
||||
#vault { padding: 0 8px 0 12px; }
|
||||
.icon-btn2, #signout, #tree .row, #projects .row { height: 44px; }
|
||||
@@ -904,6 +913,13 @@ a.ai-main:hover { color: var(--accent); }
|
||||
.hf-search input, .hf-user, .hf-date, .hf-clear { height: 44px; }
|
||||
.hf-dates { flex: 1 1 100%; }
|
||||
.hf-date { flex: 1; width: auto; min-width: 0; }
|
||||
/* The desktop run header ellipsises the note at 46% and the meta at
|
||||
whatever is left, which on a phone clips to `claude-…` / `Alice <ali…` —
|
||||
the two fields the row exists to carry. Let it wrap instead; `order: 1`
|
||||
drops the meta below the note while the time stays on the first row. */
|
||||
.hrun-head { flex-wrap: wrap; row-gap: 4px; }
|
||||
.hrun-note { max-width: none; white-space: normal; overflow: visible; }
|
||||
.hrun-meta { order: 1; flex: 1 1 100%; white-space: normal; overflow: visible; }
|
||||
.ai-btn, .ai-del { height: auto; min-height: 44px; padding: 0 12px; }
|
||||
/* react-table renders rows as `display: table-row`, which makes every
|
||||
flex rule above inert and lets the last column (Remove) fall outside
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -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-Cw2mk7qH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cb0dESCp.css">
|
||||
<script type="module" crossorigin src="/assets/index-C_yRHCCD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C52IQv2y.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user