feat: merged title bar — drag bands, owner's mark, title-width rename hit-box

The system title bar disappears (titleBarStyle hiddenInset +
trafficLightPosition {16,24}): the app's own 60px header bands become the
window's drag surface, and the traffic lights float over the sidebar's
mark band, vertically centered on the same line as the chat header's
phase pill (pixel-verified: circle centers y 24–36.5pt, pill box center
30pt).

- electron/main.cjs: hiddenInset chrome; dev shell (LH_DEV_URL) gets its
  own userData (-dev suffix) so it never fights the packaged app's
  single-instance lock — verified: the lock silently killed the dev
  shell while the packaged app was open (that also proved the lock works
  across separately-launched builds).
- app/globals.css: .lh-electron-gated utilities — drag/no-drag bands,
  92px traffic-light gutter (.lh-lights-gutter / .lh-closed-toggle:
  lights end ~75pt + the lights' own 16px edge inset as the gap),
  home's floating strip becomes a drag surface only in the shell.
  Everything is inert in the plain browser.
- sidebar.tsx: SidebarProvider sets <html class=lh-electron> post-mount
  (UA detection; no hydration mismatch, absent in browser); LH wordmark
  text replaced by the owner's mark (components/lh-mark.tsx, inlined
  icon-no-padding.svg, 24px, home link kept); SidebarToggle gains the
  92px margin when closed so it never slides under the static lights.
- chat.tsx: the rename hit-box shrinks to the title text itself —
  flex-1 spanned the whole bar, so every click started a rename and the
  window lost its grip. Shrink-to-fit + a flex-1 drag spacer (chips stay
  right) + hover:bg-accent affordance (clear when hover renames vs
  drags; -mx/px cancel for zero shift). The renaming input keeps flex-1
  for edit room.
- home.tsx / new-session.tsx / phase-divider.tsx: drag bands + carve-outs.

Live-verified in the dev shell (pixel analysis) and headless browser:
lights/pill/mark centered, 17pt lights→mark gap, rename click→select-all
→Escape roundtrip, browser layout untouched.

Gates: typecheck, eslint, 500/500 tests, build.
This commit is contained in:
amosblomqvist
2026-09-17 16:55:07 +03:00
parent 7902984bd6
commit 9a6d422d59
8 changed files with 141 additions and 15 deletions
+34 -7
View File
@@ -5,6 +5,7 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { PanelLeftCloseIcon, PanelLeftOpenIcon, PlusIcon, SearchIcon } from "lucide-react";
import { ActivitySection } from "@/components/activity-dock";
import { LhMark } from "@/components/lh-mark";
import { BrailleSpinner } from "@/components/braille-spinner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -36,6 +37,15 @@ export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
// Electron shell detection (merged chrome, 2026-09-17): keys the
// .lh-electron CSS (app/globals.css) that turns the header bands
// into the window's drag surface. Post-mount only — server HTML
// and first client render must agree (no hydration mismatch),
// and in the plain browser the class never appears.
if (typeof navigator !== "undefined" && navigator.userAgent.includes("Electron")) {
document.documentElement.classList.add("lh-electron");
}
const stored = localStorage.getItem(STORAGE_KEY);
// Default: open (first run introduces the sidebar).
setOpen(stored === null ? true : stored === "1");
@@ -74,8 +84,8 @@ export function SidebarToggle() {
return (
<span
className={cn(
"sidebar-motion relative flex h-9 shrink-0 overflow-hidden duration-200",
open ? "w-0 -mr-3" : "w-9 mr-0",
"sidebar-motion lh-no-drag relative flex h-9 shrink-0 overflow-hidden duration-200",
open ? "w-0 -mr-3" : "w-9 mr-0 lh-closed-toggle",
)}
>
<Button
@@ -245,12 +255,29 @@ function SessionsSidebar() {
aria-hidden={!open}
>
<div className="flex h-full w-72 flex-col border-r bg-background">
{/* LH wordmark — the owner's header; close toggle sits at the edge. */}
<div className="flex items-center justify-between px-4 py-3">
<Link href="/" className="text-sm font-bold uppercase tracking-widest hover:opacity-80">
LH
{/* LH wordmark — the owner's header; close toggle sits at the edge.
Merged chrome (2026-09-17): in Electron this band is the
window's drag surface and starts after the traffic lights
(.lh-lights-gutter); in the browser those classes are inert. */}
<div className="lh-drag lh-lights-gutter flex items-center justify-between px-4 py-3">
{/* The owner's mark (2026-09-17) replaces the "LH" text — still the
home link, still vertically centered by the band's items-center.
The 92px .lh-lights-gutter keeps it clear of the traffic lights
with the lights' own edge-inset as the gap. */}
<Link
href="/"
aria-label="Home"
className="lh-no-drag shrink-0 rounded-md transition-opacity hover:opacity-80"
>
<LhMark className="size-6" />
</Link>
<Button variant="ghost" size="icon" aria-label="Close sidebar" onClick={toggle}>
<Button
variant="ghost"
size="icon"
aria-label="Close sidebar"
onClick={toggle}
className="lh-no-drag"
>
<PanelLeftCloseIcon className="size-4" />
</Button>
</div>