From bf82b312f08004522a35b613b00239158c308685 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 30 Aug 2026 04:58:52 +0000 Subject: [PATCH] desktop: finish the UI, and fix what was quietly broken in it The renderer looked unfinished rather than broken, so this is mostly polish -- but four things in it were genuinely wrong, and they are the reason it read that way. Actual defects: - DialogFooter hardcoded `-mx-4 -mb-4` to cancel DialogContent's `p-4`. Both dialogs here pass `p-0`, so the footer hung 16px past the rounded corner as a detached bar. The bleed now follows a `--dialog-pad` var. - DialogContent ended its class list with `sm:max-w-sm`, which beat every `max-w-*` a caller passed: the mirror preview asked for max-w-2xl and rendered at 384px, squashing its four-column summary and delete list. - The progress bar styled the Progress ROOT (a flex wrapper) instead of the track, and recoloured `[&>div]` on finish -- the track, not the fill -- so a completed transfer turned the whole bar green whatever the percentage said. Both are addressed by slot now. - formatDate used toISOString(), so every mtime in both panes was shown in UTC while the user's clock said something else. Finishing touches: - The Settings gear had no onClick at all. It now opens the two actions that already exist behind the bridge, plus the project link. - "Hidden" was a raw next to shadcn fields, while ui/checkbox.tsx sat unused. - Each pane spent four full-width toolbars on chrome before showing a file; the path, filter and hidden toggle are one row now. - The idle transfer band was 74px of "Nothing transferring" -- the tallest thing in the window, and there to say nothing was happening. It is a slim strip that names the route and whether deletes are armed. - The rail stacked a 64px button on a 50px button with two corner radii and truncated its labels mid-word. One width, one radius, and "Sync to" in front of the name so the pair stop reading as a destination picker. - Light theme painted white panes on a white window, so a pane was a hairline around nothing. The ground steps back and panels have depth. - Panes answer to the keyboard: arrows, PageUp/Down, Home/End, Enter to open, Backspace for the parent, shift for a range, ctrl/cmd-A. - Loading is skeleton rows; empty, no-match and error each have a state instead of one line of grey text. - The mirror confirm opens focused on Cancel rather than on the trust checkbox, and its delete list no longer reserves 210px for two files. Also drops the third-party analytics tag from the renderer. The shell's own CSP (`script-src 'self'`) blocks it outright, so it never reported anything -- all it did was make a desktop file transfer tool look like it phones home. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TA4kaFusnS896fmoeWHiAa --- apps/desktop/src/app/globals.css | 81 +++- apps/desktop/src/app/layout.tsx | 19 +- apps/desktop/src/app/page.tsx | 150 +++++- .../src/components/connection-dialog.tsx | 18 +- apps/desktop/src/components/pane.tsx | 435 ++++++++++++++---- .../desktop/src/components/transfer-panel.tsx | 279 +++++++---- apps/desktop/src/components/transfer-rail.tsx | 177 +++++-- apps/desktop/src/components/ui/dialog.tsx | 16 +- apps/desktop/src/lib/format.ts | 15 +- 9 files changed, 903 insertions(+), 287 deletions(-) diff --git a/apps/desktop/src/app/globals.css b/apps/desktop/src/app/globals.css index 9994569..66f4288 100644 --- a/apps/desktop/src/app/globals.css +++ b/apps/desktop/src/app/globals.css @@ -50,6 +50,7 @@ --color-info-surface: #12244a; --color-info-line: #2f5fd0; --color-info-ink: #9dc0ff; + } /* @@ -60,6 +61,25 @@ color-scheme: dark; --radius: 0.625rem; + /* + * The control scale. Every toolbar button, field and rail action picks a + * height from these rather than inventing one -- which is what left the rail + * stacking a 64px button on a 50px button with two different corner radii. + */ + --control-sm: 26px; + --control: 30px; + --control-lg: 34px; + --row: 32px; + + /* + * Elevation. A panel used to be a 1px border and nothing else, which on the + * light theme meant a white card on a white window: the panes had no + * presence at all. Depth is a token so both themes get a considered version + * rather than a shadow someone guessed at inline. + */ + --shadow-panel: 0 1px 2px rgb(0 0 0 / 0.3), 0 8px 24px -14px rgb(0 0 0 / 0.6); + --shadow-armed: 0 1px 0 rgb(255 255 255 / 0.1) inset, 0 6px 18px -6px rgb(11 98 253 / 0.55); + --background: #070c16; --foreground: #e8edf6; --card: #0e1522; @@ -91,7 +111,13 @@ @media (prefers-color-scheme: light) { :root { color-scheme: light; - --background: #ffffff; + /* + * The window ground is deliberately NOT white. It used to be, and so did + * the panes sitting on it, so a pane was a hairline rectangle around + * nothing and the whole app read as an unstyled document. The panes keep + * white; the ground steps back so they read as panels. + */ + --background: #eef1f6; --foreground: #001830; --card: #ffffff; --card-foreground: #001830; @@ -110,16 +136,26 @@ --input: #dfe4ed; --ring: #0b62fd; - --color-ink: #ffffff; - --color-chrome: #f7f9fc; + /* + * Chrome sits a step off card in BOTH themes, so a pane header and a pane + * footer read as chrome rather than as more list. Light had them both at + * pure white, which left only the hairline border to say where the toolbar + * ended -- the same mistake as the ground being white. + */ + --color-ink: #eef1f6; + --color-chrome: #f8fafc; --color-surface: #ffffff; - --color-raised: #f2f5fa; - --color-sunken: #f7f9fc; - --color-line: #dfe4ed; + --color-raised: #f4f7fb; + --color-sunken: #f4f7fa; + --color-line: #e2e7ef; --color-line-strong: #cbd4e2; --color-dim: #35435c; --color-faint: #7a869c; + /* Depth is subtler on white than it can afford to be on near-black. */ + --shadow-panel: 0 1px 2px rgb(16 32 64 / 0.06), 0 6px 20px -12px rgb(16 32 64 / 0.28); + --shadow-armed: 0 1px 0 rgb(255 255 255 / 0.25) inset, 0 6px 16px -6px rgb(11 98 253 / 0.45); + /* tints, not near-blacks, so they read as a highlight on a white window */ --color-danger-surface: #fef2f2; --color-danger-line: #fbcfd3; @@ -178,6 +214,39 @@ body { user-select: text; } +/* Elevation as a class rather than a Tailwind shadow utility, so the light + theme's override of --shadow-panel actually reaches it. */ +.elevation-panel { + box-shadow: var(--shadow-panel); +} +.elevation-armed { + box-shadow: var(--shadow-armed); +} + +/* + * Sizes, dates and counts are compared down a column, so their glyphs have to + * occupy the same width. Without this the mono column still jitters, because + * IBM Plex Mono's proportional fallback wins for any digit it does not cover. + */ +.numeric { + font-family: var(--font-mono), ui-monospace, monospace; + font-variant-numeric: tabular-nums; + font-feature-settings: 'tnum' 1; +} + +/* + * One focus ring for the whole app. shadcn's own controls already draw this; + * the hand-rolled toolbar buttons did not, so tabbing through a pane went + * invisible halfway across. + */ +.focus-ring { + outline: none; +} +.focus-ring:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 1px; +} + /* A scrollbar that does not shout inside a dark pane. */ *::-webkit-scrollbar { width: 10px; diff --git a/apps/desktop/src/app/layout.tsx b/apps/desktop/src/app/layout.tsx index 2e85ec8..a6b37ad 100644 --- a/apps/desktop/src/app/layout.tsx +++ b/apps/desktop/src/app/layout.tsx @@ -1,15 +1,15 @@ import type { Metadata } from 'next' -import { IBM_Plex_Mono, IBM_Plex_Sans, Geist } from 'next/font/google' +import { IBM_Plex_Mono, Geist } from 'next/font/google' import './globals.css' -import { cn } from "@/lib/utils"; -import Script from "next/script"; +import { cn } from '@/lib/utils' +import { TooltipProvider } from '@/components/ui/tooltip' /** * Self-hosted at build time by next/font, which matters here: the renderer * runs under `font-src 'self' data:`, so a stylesheet from Google would be * blocked and the app would silently fall back to a system face. */ -const geist = Geist({subsets:['latin'],variable:'--font-sans'}) +const geist = Geist({ subsets: ['latin'], variable: '--font-sans', display: 'swap' }) const mono = IBM_Plex_Mono({ subsets: ['latin'], @@ -25,8 +25,15 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {children}