mirror of
https://github.com/hanzo-apps/tabs.git
synced 2026-09-15 19:32:29 +00:00
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:
+29
-19
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user