mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(web): design-review fixes to the column system
A design pass over the new tiers found four problems, two of them
introduced by the refactor itself:
- Insights was assigned `wide`, but its charts are viewBox="0 0 720 …"
SVGs at width:100% — a wider column didn't show more, it magnified:
measured 1.67x at 1600px, painting a 10.5px treemap label at 21px,
larger than the page h1. Insights moves back to `app` and .in-chart
caps at its 760px design width. Widening a column must never mean
scaling content up; that line is now written into shell.tsx.
- /install rendered the same ConnectGuide as the project home, but
wrapped in the .onboard card: x=652 w=560 top=186 against home's
x=492 w=880 top=96 — two sidebar items apart, same component, three
different numbers. It renders directly now. .onboard stays what it
is, the empty-state hero card.
- History was `app`, so `.htime { margin-left: auto }` stranded each
timestamp ~600px from its path. It's a listing — same rows as the
folder view — so it belongs in `read` alongside it.
- --hero-top: 10vh is viewport-relative in the wrong direction: 84px on
a 390x844 phone against 80px on a 1280x800 desktop, i.e. the smallest
screen paid the most. Now clamp(32px, 8vh, 88px).
Also fixes the Copy button in the guide's code blocks: it was absolutely
positioned over a scrolling box, so its 72px of reserved padding
scrolled away with the content and the button landed on top of the
command (visible mid-token at 560px and on mobile). The block is a grid
now — code scrolls in its own track, the button can't overlap it.
layout.spec.ts gains three assertions: /install and home render the
guide identically, no chart scales past ~1.0x at 1600, and the tier map
matches the new assignments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6cc61292ee
commit
afac6bef65
@@ -42,14 +42,50 @@ test("every view shares one column system", async ({ page }) => {
|
||||
}
|
||||
};
|
||||
|
||||
await visit("", "app"); // project home / install guide
|
||||
await visit("/history", "app");
|
||||
await visit("", "app"); // project home
|
||||
await visit("/install", "app"); // the same guide, so the same column
|
||||
await visit("/settings", "app");
|
||||
await visit("/insights", "wide");
|
||||
await visit("/insights", "app"); // charts cap their own measure; the column is normal
|
||||
await visit("/history", "read"); // a listing, like the folder view it shares rows with
|
||||
await visit("/index.md", "read"); // rendered markdown
|
||||
await visit("/notes", "read"); // folder listing
|
||||
});
|
||||
|
||||
test("the install route and the project home render the guide identically", async ({ page }) => {
|
||||
// They are two sidebar items apart and show the same component; /install
|
||||
// used to wrap it in the .onboard card — 320px narrower, 90px lower.
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
const box = async (path: string) => {
|
||||
await page.goto(`http://localhost:8993/${pid}${path}`);
|
||||
await page.waitForSelector(".guide");
|
||||
return page.evaluate(() => {
|
||||
const r = (document.querySelector(".guide") as HTMLElement).getBoundingClientRect();
|
||||
return { left: Math.round(r.left), width: Math.round(r.width), top: Math.round(r.top) };
|
||||
});
|
||||
};
|
||||
expect(await box("/install")).toEqual(await box(""));
|
||||
});
|
||||
|
||||
test("charts never scale past the size they were drawn at", async ({ page }) => {
|
||||
// .in-chart SVGs are viewBox="0 0 720 …" at width:100%, so an unbounded
|
||||
// column magnifies them — labels ended up larger than the page title.
|
||||
await page.setViewportSize({ width: 1600, height: 900 });
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
await page.goto(`http://localhost:8993/${pid}/insights`);
|
||||
await page.waitForSelector(".in-chart");
|
||||
const worst = await page.evaluate(() => {
|
||||
let max = 0;
|
||||
for (const el of document.querySelectorAll(".in-chart")) {
|
||||
const vb = (el.getAttribute("viewBox") || "0 0 720 0").split(/\s+/);
|
||||
max = Math.max(max, el.getBoundingClientRect().width / Number(vb[2]));
|
||||
}
|
||||
return max;
|
||||
});
|
||||
expect(worst, "chart scale factor").toBeLessThanOrEqual(1.06);
|
||||
});
|
||||
|
||||
test("the gutter belongs to the scroll container, not the column", async ({ page }) => {
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
|
||||
@@ -236,7 +236,6 @@ export default function Browser(props: {
|
||||
if (panel) {
|
||||
view = panel.body;
|
||||
} else if (route.view === "insights") {
|
||||
pageWidth = "wide"; // treemap + coverage matrix need the room
|
||||
view = props.canInsights ? (
|
||||
<Insights
|
||||
flatFiles={flatFiles}
|
||||
@@ -251,6 +250,7 @@ export default function Browser(props: {
|
||||
<div className="empty">Insights is for hub admins and org owners.</div>
|
||||
);
|
||||
} else if (route.view === "history") {
|
||||
pageWidth = "read"; // a scannable list, like the folder listing it shares rows with
|
||||
view = (
|
||||
<HistoryView
|
||||
apiBase={apiBase}
|
||||
|
||||
@@ -163,12 +163,11 @@ export default function HubApp({ config }: { config: ServerConfig }) {
|
||||
? { crumb: "Project settings", body: <ProjectSettings project={current} org={org} /> }
|
||||
: route.view === "install"
|
||||
? {
|
||||
// The same guide the project home shows, in the same column —
|
||||
// it used to sit in the .onboard card, 320px narrower and 90px
|
||||
// lower than home, two sidebar items apart.
|
||||
crumb: "Installation",
|
||||
body: (
|
||||
<div className="onboard">
|
||||
<ConnectGuide project={current} />
|
||||
</div>
|
||||
),
|
||||
body: <ConnectGuide project={current} />,
|
||||
}
|
||||
: null;
|
||||
|
||||
|
||||
@@ -84,9 +84,17 @@ export function Icon({ name }: { name: string }) {
|
||||
/* The column system, in one place. `#content` owns scrolling and the page
|
||||
gutter; `<Page>` owns width and centering — nothing else may set either.
|
||||
Three widths cover every view: `read` for prose and listings (line length
|
||||
rules), `app` for structured views, `wide` for data-dense ones. Views used
|
||||
to declare their own max-width (560px to unbounded, half of them
|
||||
uncentered), so no two routes shared a column. */
|
||||
rules), `app` for structured views, `wide` for content that is itself a
|
||||
page (a rendered HTML file in its frame). Views used to declare their own
|
||||
max-width (560px to unbounded, half of them uncentered), so no two routes
|
||||
shared a column.
|
||||
|
||||
The line: <Page> sets the COLUMN, a view may still cap its own MEASURE
|
||||
(`.nf-sub`, a chart's design width). What a view must never do is declare
|
||||
a page-level width — that is how the tiers drifted apart the first time.
|
||||
Widening a column also never means scaling content up: Insights sits at
|
||||
`app` and its charts cap themselves, because at `wide` the viewBox SVGs
|
||||
just zoomed (a 10.5px label painted at 21px). */
|
||||
export type PageWidth = "read" | "app" | "wide";
|
||||
|
||||
export function Page(props: {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
--page-read: 704px; /* prose + listings: line length rules */
|
||||
--page-app: 880px; /* structured views: guide, history, settings, admin */
|
||||
--page-wide: 1200px; /* data-dense: treemap, coverage matrix, big tables */
|
||||
--hero-top: 10vh; /* one vertical start for short centered states */
|
||||
--hero-top: clamp(32px, 8vh, 88px); /* one vertical start for short centered states */
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
@@ -376,8 +376,9 @@ button, input, a.btn { font-family: inherit; }
|
||||
.gd-step-title { font-weight: 600; font-size: 14px; color: var(--text); }
|
||||
.gd-desc { margin: 2px 0 8px 32px; color: var(--text-faint); font-size: 13px; line-height: 1.5; }
|
||||
.gd-extra { font-size: 12.5px; margin-top: 6px; }
|
||||
.gd-code { position: relative; margin: 6px 0 6px 32px; padding: 10px 72px 10px 12px; background: var(--bg-raise); border: 1px solid var(--border); border-radius: var(--r-card); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; line-height: 1.6; color: var(--text); overflow-x: auto; white-space: pre; }
|
||||
.gd-copy { position: absolute; top: 7px; right: 7px; font: inherit; font-family: inherit; font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 6px; border: 1px solid var(--border-2); background: var(--bg-raise); color: var(--text-faint); cursor: pointer; box-shadow: -14px 0 12px -6px var(--bg-raise); }
|
||||
.gd-code { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 10px; margin: 6px 0 6px 32px; padding: 10px 12px; background: var(--bg-raise); border: 1px solid var(--border); border-radius: var(--r-card); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; line-height: 1.6; color: var(--text); }
|
||||
.gd-code > code { display: block; min-width: 0; overflow-x: auto; white-space: pre; }
|
||||
.gd-copy { align-self: start; font: inherit; font-family: inherit; font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 6px; border: 1px solid var(--border-2); background: var(--bg-raise); color: var(--text-faint); cursor: pointer; }
|
||||
.gd-copy:hover { color: var(--accent-bright); border-color: var(--accent-dim); }
|
||||
/* A single unnumbered step has no badge to indent under. */
|
||||
.gd-solo .gd-desc, .gd-solo .gd-code { margin-left: 0; }
|
||||
@@ -396,7 +397,7 @@ button, input, a.btn { font-family: inherit; }
|
||||
.in-lens-btn { font: inherit; font-size: 12px; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--border); background: none; color: var(--text-faint); cursor: pointer; }
|
||||
.in-lens-btn:hover { color: var(--text); }
|
||||
.in-lens-btn.active { color: var(--accent); border-color: var(--accent); }
|
||||
.in-chart { width: 100%; height: auto; border: 1px solid var(--border); border-radius: var(--r-card); background: var(--bg-side); margin-bottom: 6px; }
|
||||
.in-chart { width: 100%; max-width: 760px; height: auto; border: 1px solid var(--border); border-radius: var(--r-card); background: var(--bg-side); margin-bottom: 6px; }
|
||||
.in-axis { stroke: var(--border); stroke-width: 1; }
|
||||
.in-threshold { stroke: var(--border); stroke-width: 1; stroke-dasharray: 4 4; }
|
||||
.in-danger-zone { fill: rgba(242, 109, 109, .05); }
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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 100 100'><text y='.9em' font-size='90'>🐻</text></svg>">
|
||||
<script type="module" crossorigin src="/assets/index-BZydVmgc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DgVZuodz.css">
|
||||
<script type="module" crossorigin src="/assets/index-Dnpnr_Sl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-177q_1QG.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user