tabs: the chrome was two rows, and the seam was a third thing in the window

The page kept a header of its own — brand on the left, Disconnect on the right —
above the workspace's action row. Two rows for five controls, when the action row
already had an empty middle wide enough for both. It cost every pane about 28px,
and a pane is measured in rows of terminal you can read.

The workspace's row now takes what sits at its ends as `start` and `end`, so the
page still owns its brand and its way out and there is one row on the screen.
Outer padding and the gaps come down with it.

A pane's name bar goes 36px to 28px for the same reason, times the number of panes.

The divider's grabber was a 3px pill on a 12px strip that filled with light on
hover, which drew a third object between two terminals. What it CATCHES and what
it LOOKS like are separate: the hit area stays a thumb wide for touch, the mark
becomes a hairline, and it brightens on hover where the pointer already is.
This commit is contained in:
zooqueen
2026-08-15 10:27:16 -07:00
parent fa4a4dbcb7
commit 1f41854833
2 changed files with 55 additions and 26 deletions
+29 -19
View File
@@ -205,30 +205,40 @@ export default function App() {
}
return (
<main className="flex h-dvh flex-col gap-2 p-2">
<header className="flex shrink-0 items-center gap-2 text-xs text-[var(--text-tertiary)]">
<Link href="/" className="font-mono text-[var(--text-secondary)] hover:text-foreground">
Tabs
</Link>
{error ? <span className="truncate text-amber-500">{error}</span> : null}
<button
type="button"
onClick={() => {
void signOut();
setToken(null);
setData(null);
}}
className="ml-auto shrink-0 rounded px-2 py-1 hover:bg-muted hover:text-[var(--text-secondary)]"
>
Disconnect
</button>
</header>
<main className="flex h-dvh flex-col p-1.5">
<div className="min-h-0 flex-1">
{data ? (
<Workspace
hosts={hosts}
mint={mint}
// The brand and the way out ride in the workspace's action row.
// They were a row of their own, which cost every pane ~28px of
// terminal for two controls that fit in the gap beside the splits —
// and a workspace is measured in rows you can read.
start={
<Link
href="/"
className="shrink-0 font-mono text-[var(--text-secondary)] hover:text-foreground"
>
Tabs
</Link>
}
end={
<>
{error ? <span className="truncate text-amber-500">{error}</span> : null}
<button
type="button"
onClick={() => {
void signOut();
setToken(null);
setData(null);
}}
className="shrink-0 rounded px-2 py-1 hover:bg-muted hover:text-[var(--text-secondary)]"
>
Disconnect
</button>
</>
}
// Started, then read back, then named — in that order. The workspace
// opens a shell on the name it gets, and a pane can only mint a
// ticket for a machine the registry has already handed back, so the
+26 -7
View File
@@ -33,7 +33,7 @@
* and keeps stacked ones stacked. Same tree, same renderer.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Cloud, Columns2, Loader2, Monitor, Plus, Rows2, X } from 'lucide-react';
import {
@@ -102,8 +102,16 @@ export function Workspace({
hosts,
mint,
onLaunch,
start,
end,
}: {
hosts: TerminalHost[];
/** What sits at the ends of the action row — the brand, and the way out.
* They are the page's, not the workspace's, but they are three controls
* wide and a row of their own cost every pane 28px of terminal. A workspace
* is measured in rows you can read, so the row they belong in is this one. */
start?: ReactNode;
end?: ReactNode;
/** A fresh URL for a pane, for whatever that pane shows. EVERY pane, not only
* a sandbox's: a machine you linked and a machine we started differ in who
* serves the page and what credential opens it, and in nothing a layout cares
@@ -461,9 +469,10 @@ export function Workspace({
watchable.length ? openHere(screenOn, watchable, dir, target) : launch('desktop');
return (
<div className="flex h-full w-full flex-col gap-2">
<div className="flex h-full w-full flex-col gap-1.5">
{/* One row. Actions, not a status report. */}
<div className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground">
{start}
<Act
run={() => openShell(focus ? 'row' : null, focus)}
icon={<Plus className="h-3.5 w-3.5" />}
@@ -508,6 +517,7 @@ export function Workspace({
<Rows2 className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Down</span>
</button>
{end}
</span>
</div>
@@ -583,8 +593,12 @@ export function Workspace({
height: `${r.height}%`,
}}
>
{/* A pane's name bar. It is chrome around the only thing on the
page worth reading, so it is as short as a touch target
allows and no shorter — every pixel here is a row of
terminal, times the number of panes. */}
<div
className={`flex h-9 shrink-0 items-center gap-1.5 px-2 text-xs ${
className={`flex h-7 shrink-0 items-center gap-1.5 px-2 text-xs ${
on ? 'bg-muted text-foreground' : 'bg-card text-muted-foreground'
}`}
>
@@ -745,7 +759,7 @@ export function Workspace({
}}
role="separator"
aria-orientation={d.dir === 'row' ? 'vertical' : 'horizontal'}
className={`absolute z-20 touch-none bg-transparent hover:bg-foreground/20 ${
className={`group absolute z-20 touch-none bg-transparent ${
d.dir === 'row'
? 'w-3 -translate-x-1/2 cursor-col-resize'
: 'h-6 -translate-y-1/2 cursor-row-resize sm:h-3'
@@ -758,11 +772,16 @@ export function Workspace({
}}
>
{/* A visible grabber: a touch divider you cannot see is one that
does not exist. */}
does not exist. What it LOOKS like and what it CATCHES are
separate — the hit area above stays a thumb wide, while the
mark is a hairline, because a seam between two terminals
should read as a seam and not as a third thing in the
window. It brightens on hover, where the pointer already
is. */}
<span
aria-hidden
className={`pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground/25 ${
d.dir === 'row' ? 'h-8 w-[3px]' : 'h-[3px] w-8'
className={`pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-foreground/20 transition-colors group-hover:bg-foreground/40 ${
d.dir === 'row' ? 'h-6 w-px' : 'h-px w-6'
}`}
/>
</div>