fix: top-bar polish — lights on the pill line, rename escape hitbox, inert phase pill, chrome carve-outs
Traffic lights (owner report: 'way too low, not aligned with the phase pill'): measured in the running app — circles centered at 30.75pt vs the pill box's 29.25pt (band 0-59.5, pill box 19-39.5, pill ink 28.75; uppercase glyphs sit high). trafficLightPosition y 24 -> 22.5 lands the circle centers at 29.25, exactly on the pill. Owner confirmed aligned after this change. Rename escape hitbox (owner report): entering rename was already width-of- title only, but the editing input spanned flex-1 so clicking the rest of the bar stayed inside the input. The input is now shrink-to-fit ([field-sizing:content], min 8ch / max 70%) and any mousedown outside the input blurs it, running the existing onBlur commit path; the header drops lh-drag while renaming so the drag region cannot eat the escape click. Verified: click-away commits (exactly one PUT), Escape discards, Enter commits. Phase pill is pure decoration (owner report: 'should not be interactable in any way'): no tooltip, pointer-events-none, select-none — part of the drag surface instead of a carved-out target. Graph chrome was unclickable in Electron (owner report): home.tsx's floating drag strip underlays the z-10 controls row, and Electron's app-region mask ignores z-index, so the region ate their clicks (plain browser unaffected — DOM hit-testing is fine). lh-no-drag carve-outs on the chrome row and the node detail card (it is placed imperatively anywhere, including the top strip). LhMark home link now mirrors the ghost toggle: same size-9 chip geometry and hover:bg-accent treatment.
This commit is contained in:
+24
-2
@@ -637,9 +637,26 @@ export function Chat({
|
|||||||
60px is an ALIGNMENT, not just a size: it equals the sidebar's
|
60px is an ALIGNMENT, not just a size: it equals the sidebar's
|
||||||
LH wordmark band, so the header's border-b continues that
|
LH wordmark band, so the header's border-b continues that
|
||||||
band's bottom edge as one line across the app (owner, round 6). */}
|
band's bottom edge as one line across the app (owner, round 6). */}
|
||||||
<header className="lh-drag flex h-15 items-center gap-3 border-b border-border px-3">
|
<header
|
||||||
|
// While renaming the bar is NOT a drag surface: app-region:drag
|
||||||
|
// would eat the mousedown, so click-to-escape (below) could
|
||||||
|
// never fire. Rename mode is interactive mode (owner ask).
|
||||||
|
className={
|
||||||
|
renaming
|
||||||
|
? "flex h-15 items-center gap-3 border-b border-border px-3"
|
||||||
|
: "lh-drag flex h-15 items-center gap-3 border-b border-border px-3"
|
||||||
|
}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
// Escape hitbox: any mousedown outside the input itself blurs
|
||||||
|
// it, running the existing onBlur commit path. The input is
|
||||||
|
// shrink-to-fit (field-sizing), so the rest of the bar is
|
||||||
|
// this spacer + inert chips — all click-to-exit (owner ask).
|
||||||
|
if (renaming && e.target !== titleInputRef.current) titleInputRef.current?.blur();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SidebarToggle />
|
<SidebarToggle />
|
||||||
{renaming ? (
|
{renaming ? (
|
||||||
|
<>
|
||||||
<input
|
<input
|
||||||
ref={titleInputRef}
|
ref={titleInputRef}
|
||||||
value={titleDraft}
|
value={titleDraft}
|
||||||
@@ -651,8 +668,13 @@ export function Chat({
|
|||||||
onBlur={() => void commitRename(titleDraft)}
|
onBlur={() => void commitRename(titleDraft)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
aria-label="Session title"
|
aria-label="Session title"
|
||||||
className="lh-no-drag min-w-0 flex-1 bg-transparent font-title text-3xl tracking-tight caret-current outline-none"
|
className="lh-no-drag min-w-[8ch] max-w-[70%] shrink-0 bg-transparent font-title text-3xl tracking-tight caret-current outline-none [field-sizing:content]"
|
||||||
/>
|
/>
|
||||||
|
{/* flex-1 spacer during rename too, so the chips stay at
|
||||||
|
the right edge; clicks here commit via the header
|
||||||
|
mousedown handler. */}
|
||||||
|
<div className="min-w-4 flex-1" aria-hidden="true" />
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -969,8 +969,12 @@ export function GraphView() {
|
|||||||
without shifting its siblings — every button is 44px tall,
|
without shifting its siblings — every button is 44px tall,
|
||||||
top alignment pins the row. Coverage renders even when the
|
top alignment pins the row. Coverage renders even when the
|
||||||
graph is EMPTY — an empty graph with unextracted sessions is
|
graph is EMPTY — an empty graph with unextracted sessions is
|
||||||
exactly when it matters (extract → the graph gains nodes). */}
|
exactly when it matters (extract → the graph gains nodes).
|
||||||
<div className="absolute right-3 top-3 z-10 flex items-start gap-2">
|
lh-no-drag (2026-09-17): home.tsx's floating drag strip
|
||||||
|
underlays this row — Electron's drag mask ignores z-index,
|
||||||
|
so without an explicit carve-out the controls were
|
||||||
|
unclickable in the desktop app (owner report). */}
|
||||||
|
<div className="lh-no-drag absolute right-3 top-3 z-10 flex items-start gap-2">
|
||||||
{!isEmpty && (
|
{!isEmpty && (
|
||||||
<>
|
<>
|
||||||
{/* Search — its own container (icon + input + no-match note). */}
|
{/* Search — its own container (icon + input + no-match note). */}
|
||||||
@@ -1055,7 +1059,10 @@ export function GraphView() {
|
|||||||
// Positioned imperatively in the layout effect above; hidden
|
// Positioned imperatively in the layout effect above; hidden
|
||||||
// until placed so the flip never flashes in the wrong spot.
|
// until placed so the flip never flashes in the wrong spot.
|
||||||
style={{ visibility: "hidden", left: 0, top: 0 }}
|
style={{ visibility: "hidden", left: 0, top: 0 }}
|
||||||
className="absolute z-10 w-80 max-w-[calc(100%-1rem)] rounded-xl border bg-card p-4 text-left shadow-lg"
|
// lh-no-drag: the card is placed anywhere on the canvas —
|
||||||
|
// when it lands in the top strip it must stay interactive
|
||||||
|
// over home.tsx's drag surface (2026-09-17).
|
||||||
|
className="lh-no-drag absolute z-10 w-80 max-w-[calc(100%-1rem)] rounded-xl border bg-card p-4 text-left shadow-lg"
|
||||||
>
|
>
|
||||||
<div className="text-sm font-medium leading-snug">{selected.node.name}</div>
|
<div className="text-sm font-medium leading-snug">{selected.node.name}</div>
|
||||||
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">{selected.node.statement}</div>
|
<div className="mt-1 text-xs leading-relaxed text-muted-foreground">{selected.node.statement}</div>
|
||||||
|
|||||||
@@ -34,12 +34,15 @@ const CHIP_LABEL: Record<Phase3, string> = {
|
|||||||
/** The header chip: the phase label, monochrome — label-only by design
|
/** The header chip: the phase label, monochrome — label-only by design
|
||||||
* (see the component note: the pill's curve suits text, not glyphs). */
|
* (see the component note: the pill's curve suits text, not glyphs). */
|
||||||
export function PhaseChip({ phase }: { phase: Phase3 }) {
|
export function PhaseChip({ phase }: { phase: Phase3 }) {
|
||||||
|
// Pure decoration (owner ask, 2026-09-17): no tooltip, not selectable,
|
||||||
|
// not interactive in any way — no pointer events at all, so it is part
|
||||||
|
// of the drag surface instead of a carved-out target.
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-testid="phase-chip"
|
data-testid="phase-chip"
|
||||||
data-phase={phase}
|
data-phase={phase}
|
||||||
title={`Session phase: ${CHIP_LABEL[phase]}`}
|
aria-hidden="true"
|
||||||
className="lh-no-drag flex shrink-0 items-center rounded-full border border-border/80 px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
className="pointer-events-none flex shrink-0 select-none items-center rounded-full border border-border/80 px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
||||||
>
|
>
|
||||||
{CHIP_LABEL[phase]}
|
{CHIP_LABEL[phase]}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -264,10 +264,12 @@ function SessionsSidebar() {
|
|||||||
home link, still vertically centered by the band's items-center.
|
home link, still vertically centered by the band's items-center.
|
||||||
The 92px .lh-lights-gutter keeps it clear of the traffic lights
|
The 92px .lh-lights-gutter keeps it clear of the traffic lights
|
||||||
with the lights' own edge-inset as the gap. */}
|
with the lights' own edge-inset as the gap. */}
|
||||||
|
{/* Hover mirrors the ghost toggle buttons (owner ask, 2026-09-17):
|
||||||
|
same size-9 chip geometry, same hover:bg-accent treatment. */}
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
aria-label="Home"
|
aria-label="Home"
|
||||||
className="lh-no-drag shrink-0 rounded-md transition-opacity hover:opacity-80"
|
className="lh-no-drag flex size-9 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50"
|
||||||
>
|
>
|
||||||
<LhMark className="size-6" />
|
<LhMark className="size-6" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
+6
-3
@@ -187,11 +187,14 @@ function createWindow(url) {
|
|||||||
// Merged chrome (owner, 2026-09-17): the system title bar is gone;
|
// Merged chrome (owner, 2026-09-17): the system title bar is gone;
|
||||||
// the app's own 60px header bands ARE the title bar. The traffic
|
// the app's own 60px header bands ARE the title bar. The traffic
|
||||||
// lights float over the sidebar's LH wordmark band, vertically
|
// lights float over the sidebar's LH wordmark band, vertically
|
||||||
// centered on its 60px line (12px circles: y 24–36), with the LH
|
// Aligned to the PHASE PILL's drawn box, not the band's geometric
|
||||||
// wordmark starting at x≈80 (components/sidebar.tsx gutter).
|
// center (owner ask, 2026-09-17): macOS centers each 12px circle at
|
||||||
|
// y+6.75, and the pill box center measures 29.25 in the running app
|
||||||
|
// (band 0–59.5, pill box 19–39.5) — y 22.5 lands the circles at
|
||||||
|
// 29.25, exactly on the pill. Wordmark gutter: components/sidebar.tsx.
|
||||||
// Dragging comes from the renderer's .lh-drag bands (app/globals.css).
|
// Dragging comes from the renderer's .lh-drag bands (app/globals.css).
|
||||||
titleBarStyle: "hiddenInset",
|
titleBarStyle: "hiddenInset",
|
||||||
trafficLightPosition: { x: 16, y: 24 },
|
trafficLightPosition: { x: 16, y: 22.5 },
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user