From 07bb22eb2d13df62128fe40dc00ee26422e1210a Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 1 Sep 2026 15:32:21 +0200 Subject: [PATCH] One border weight, and no sliver beside the nav The nav's right edge read as two lines because it was two things: its own border, and a sliver of the stage's background showing through a sub-pixel gap before the app's webview began. The stage rect is now rounded to whole pixels and to the same edges each time, so the app covers the stage exactly. Borders are one weight throughout and the lightest that still separates - slate-200 in light, slate-800 in dark. Nothing drawn heavier than it needs to be to read as an edge. --- .../specs/2026-09-01-work-app-design.md | 10 ++++++++++ src/App.tsx | 8 +++++++- src/components/Nav.tsx | 2 +- src/components/ui.tsx | 14 +++++++------- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/superpowers/specs/2026-09-01-work-app-design.md b/docs/superpowers/specs/2026-09-01-work-app-design.md index ffda87c..db90472 100644 --- a/docs/superpowers/specs/2026-09-01-work-app-design.md +++ b/docs/superpowers/specs/2026-09-01-work-app-design.md @@ -302,6 +302,16 @@ The margin That margin is the only part of the window that is not a web page, an therefore the only place left to grab it by. Full screen has no use for it and gets the room back. +### Borders + +One weight everywhere, and the lightest that still separates: `slate-200` in light, +`slate-800` in dark. Nothing is drawn heavier than it needs to be to read as an edge. + +The stage rect is rounded to whole pixels, and to the same edges every time. A fractional +left edge leaves a sliver of the stage's own background between the nav's border and the +app beside it, and at a hairline weight that sliver reads as a second border line running +the full height of the window. + ## Dialogs An app's webview is a native view that paints above the shell, so a dialog cannot simply diff --git a/src/App.tsx b/src/App.tsx index cf89d80..61199b4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,7 +56,13 @@ export default function App() { const el = stageRef.current; if (!el) return; const r = el.getBoundingClientRect(); - void api.setStage(r.x, r.y, r.width, r.height, 0); + /* Rounded to whole pixels, and to the same edges each time. A fractional + left edge leaves a sliver of the stage's own background showing between + the nav's border and the app — which reads as a second, lighter border + line running down the whole window. */ + const x = Math.round(r.x); + const y = Math.round(r.y); + void api.setStage(x, y, Math.round(r.right) - x, Math.round(r.bottom) - y, 0); }, []); useLayoutEffect(() => { diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index 3547182..e3a59cf 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -38,7 +38,7 @@ export default function Nav({ const disabled = !activeId; const shell = - "flex shrink-0 flex-col overflow-hidden border-r border-slate-300 bg-white " + + "flex shrink-0 flex-col overflow-hidden border-r border-slate-200 bg-white " + "dark:border-slate-800 dark:bg-slate-900"; /* Back, forward, reload and settings. Nothing hides an element here — that diff --git a/src/components/ui.tsx b/src/components/ui.tsx index 31d49a0..50e57f9 100644 --- a/src/components/ui.tsx +++ b/src/components/ui.tsx @@ -25,8 +25,8 @@ export const SECTION = "border-b border-slate-200 px-4 py-4 dark:border-slate-80 /* No width: callers set it. A `w-full` baked in here loses the specificity coin-toss against a `w-[110px]` or a `flex-1` at the call site. */ export const INPUT = - `${CONTROL_H} min-w-0 rounded-lg border border-slate-300 bg-white px-3 text-[12px] outline-none ` + - "placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:placeholder:text-slate-500"; + `${CONTROL_H} min-w-0 rounded-lg border border-slate-200 bg-white px-3 text-[12px] outline-none ` + + "placeholder:text-slate-400 dark:border-slate-800 dark:bg-slate-800 dark:placeholder:text-slate-500"; export const SUBPANEL = "rounded-lg bg-slate-50 p-3 dark:bg-slate-800/50"; const BTN_BASE = @@ -34,9 +34,9 @@ const BTN_BASE = "disabled:cursor-not-allowed cursor-pointer"; export const BTN = - `${BTN_BASE} border border-slate-300 px-2.5 font-medium ` + + `${BTN_BASE} border border-slate-200 px-2.5 font-medium ` + "hover:border-sky-500 hover:text-sky-600 disabled:opacity-40 " + - "dark:border-slate-700 dark:hover:border-sky-500 dark:hover:text-sky-400"; + "dark:border-slate-800 dark:hover:border-sky-500 dark:hover:text-sky-400"; export const BTN_PRIMARY = `${BTN_BASE} bg-sky-500 px-3 font-semibold text-white hover:bg-sky-400 disabled:opacity-40`; @@ -86,7 +86,7 @@ export function Segmented({ return (
{options.map((o) => { const active = o.value === value; @@ -173,8 +173,8 @@ export function Dialog({ wide ? "max-w-[min(1120px,92vw)] max-h-[90vh] overflow-y-auto" : "max-w-sm" - } rounded-2xl border border-slate-300 bg-white p-5 shadow-2xl - dark:border-slate-700 dark:bg-slate-900`} + } rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl + dark:border-slate-800 dark:bg-slate-900`} >

{title}