Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions app/frontend/src/components/sidebar/window-row.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -944,9 +944,9 @@ describe("WindowRow", () => {
const stripe = zone.querySelector('[style*="border-left"]') as HTMLElement | null;
expect(stripe).not.toBeNull();
expect(stripe!.style.borderLeft).toContain("solid");
// Edge-anchored: a small 2px inset from the sidebar edge (full-bleed
// Edge-anchored: a small 4px inset from the sidebar edge (full-bleed
// rows), not the old 17px post-icon-zone placement.
expect(stripe!.style.left).toBe("2px");
expect(stripe!.style.left).toBe("4px");
// No next-state ghost preview element exists anymore.
expect(zone.querySelectorAll('[style*="border-left"]').length).toBe(1);
// Container must not be present twice (single stripe).
Expand All @@ -961,10 +961,10 @@ describe("WindowRow", () => {
// stripe carry no aria-hidden).
const icon = zone.querySelector('[aria-hidden="true"]') as HTMLElement | null;
expect(icon).not.toBeNull();
// Inset ICON_EDGE_INSET (10px) off the sidebar edge — past the widest
// (double) stripe's 8px right edge — with the 12px icon-zone width
// Inset ICON_EDGE_INSET (12px) off the sidebar edge — past the widest
// (double) stripe's 10px right edge — with the 12px icon-zone width
// unchanged, so the hover icon sits beside the stripe, not over it.
expect(icon!.style.left).toBe("10px");
expect(icon!.style.left).toBe("12px");
expect(icon!.style.width).toBe("12px");
});
});
Expand Down
8 changes: 4 additions & 4 deletions app/frontend/src/components/sidebar/window-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,13 +469,13 @@ function WindowRowInner({
* marker stripe anchors near-flush at the sidebar edge (`STRIPE_EDGE_INSET`);
* the hover palette-icon zone is inset `ICON_EDGE_INSET`px off that edge,
* spanning to `ICON_EDGE_INSET + ICON_ZONE_WIDTH` — past the widest (double,
* 2+6=8px) stripe, so the hover icon sits beside the stripe rather than over
* 4+6=10px) stripe, so the hover icon sits beside the stripe rather than over
* it. The icon keeps an explicit `z-10` (layering below) as a guard for any
* residual sub-pixel overlap. */
const LABEL_ZONE_WIDTH = 26; // full zone: icon home + clearance before the dot
const ICON_ZONE_WIDTH = 12; // 12px icon zone: home of the hover palette icon
const ICON_EDGE_INSET = 10; // icon-zone inset off the physical sidebar edge — clears the widest (double, 2+6=8px) marker stripe so the hover icon sits beside it, not over it
const STRIPE_EDGE_INSET = 2; // stripe inset from the zone's/sidebar's left edge (near-flush per the full-bleed spec)
const ICON_EDGE_INSET = 12; // icon-zone inset off the physical sidebar edge — clears the widest (double, 4+6=10px) marker stripe so the hover icon sits beside it, not over it
const STRIPE_EDGE_INSET = 4; // stripe inset from the zone's/sidebar's left edge (near-flush per the full-bleed spec)

type LabelZoneProps = {
marker?: string;
Expand All @@ -497,7 +497,7 @@ type LabelZoneProps = {
* The marker stripe is DISPLAY-ONLY, anchored near-flush at the sidebar's left
* edge (`STRIPE_EDGE_INSET`px). The icon zone is inset `ICON_EDGE_INSET`px off
* the physical edge (spanning to `ICON_EDGE_INSET + ICON_ZONE_WIDTH`) so the
* hover icon clears both the sidebar boundary and the widest (8px) stripe,
* hover icon clears both the sidebar boundary and the widest (10px) stripe,
* sitting beside it (explicit `z-10` on the icon container guards residual
* overlap; the zone's own `z-20` scopes the stack). `cursor: pointer` (menu-opener
* semantics). Active on coarse pointers — touch gets direct label access.
Expand Down
6 changes: 3 additions & 3 deletions docs/memory/run-kit/ui-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -1450,11 +1450,11 @@ The whole 26px to the left of the status dot on each window row is ONE interacti

**One 26px target, opens the picker — never cycles, never selects**: clicking anywhere in the zone opens the combined Label picker anchored at the row's bottom-left (an `absolute left-0 top-full z-50` wrapper; the row root stays overflow-free so the `top-full` popover is not clipped even on a selected+double row). The click `stopPropagation`s so it does not select the row, and coexists with drag-reorder. Cursor is `pointer` (menu-opener semantics). The zone is enabled on non-ghost rows wired with both the color and marker write seams plus a `server` (`labelZoneEnabled`).

**Zone geometry**: an absolute `z-20` sibling positioned `left-0` spanning `LABEL_ZONE_WIDTH = 26`px (full row height). Because the row box is full-bleed (§ Row Anatomy), `left-0` IS the physical sidebar edge — the zone needs no negative offset, so the coordinate expression is a plain `left-0` overlay. The zone's absolute span (sidebar x = 0…26) is 26px of the true sidebar edge. The palette-icon home is a 12px band (`ICON_ZONE_WIDTH = 12`) inset `ICON_EDGE_INSET = 10`px off the physical edge (sidebar x = 10…22) — past the widest (double, 2+6=8px) marker stripe, so the hover icon sits beside the stripe rather than over it. The button content is padded clear with `pl-[30px]` (12px hierarchy indent + 18px content padding) so the leading StatusDot + name start 4px past the zone's inner edge — the dot keeps its own hover-card and click-to-select, and the dot + name keep their exact x-positions (no content shift). Active on coarse pointers — touch gets direct label access (no `coarse:pointer-events-none`).
**Zone geometry**: an absolute `z-20` sibling positioned `left-0` spanning `LABEL_ZONE_WIDTH = 26`px (full row height). Because the row box is full-bleed (§ Row Anatomy), `left-0` IS the physical sidebar edge — the zone needs no negative offset, so the coordinate expression is a plain `left-0` overlay. The zone's absolute span (sidebar x = 0…26) is 26px of the true sidebar edge. The palette-icon home is a 12px band (`ICON_ZONE_WIDTH = 12`) inset `ICON_EDGE_INSET = 12`px off the physical edge (sidebar x = 12…24) — past the widest (double, 4+6=10px) marker stripe, so the hover icon sits beside the stripe rather than over it. The button content is padded clear with `pl-[30px]` (12px hierarchy indent + 18px content padding) so the leading StatusDot + name start 4px past the zone's inner edge — the dot keeps its own hover-card and click-to-select, and the dot + name keep their exact x-positions (no content shift). Active on coarse pointers — touch gets direct label access (no `coarse:pointer-events-none`).

**Hover affordance — palette icon + two-stage zone glow**: at rest the zone is transparent with no icon. On ROW hover (`group-hover`) the shipped `PaletteIcon` (~11px, in the 12px icon zone at `left: ICON_EDGE_INSET = 10`px, beside the edge-anchored stripe rather than over it) fades to ~65% and the whole zone glows ~12% family color; when the ZONE ITSELF is hovered the icon reaches full opacity and the glow deepens to ~24% (`color-mix`). The icon is drawn in the row's guarded family color (`markerColor`) on colored rows and inherited monochrome on uncolored rows. The icon container keeps an explicit `z-10` (the stripe is a DOM sibling rendered before it) as a guard so any residual overlap layers icon-over-stripe deterministically inside the zone's own `z-20` stacking context, not by incidental sibling order. `aria-label="Set window label"` names the zone for pointer AT users and test selection.
**Hover affordance — palette icon + two-stage zone glow**: at rest the zone is transparent with no icon. On ROW hover (`group-hover`) the shipped `PaletteIcon` (~11px, in the 12px icon zone at `left: ICON_EDGE_INSET = 12`px, beside the edge-anchored stripe rather than over it) fades to ~65% and the whole zone glows ~12% family color; when the ZONE ITSELF is hovered the icon reaches full opacity and the glow deepens to ~24% (`color-mix`). The icon is drawn in the row's guarded family color (`markerColor`) on colored rows and inherited monochrome on uncolored rows. The icon container keeps an explicit `z-10` (the stripe is a DOM sibling rendered before it) as a guard so any residual overlap layers icon-over-stripe deterministically inside the zone's own `z-20` stacking context, not by incidental sibling order. `aria-label="Set window label"` names the zone for pointer AT users and test selection.

**Marker stripe is display-only**: the marker renders as a left-edge stripe (3px dotted / 3px solid / 6px double in the guarded family color, gray uncolored) via the shared `markerStripeStyle` helper, anchored near-flush at the sidebar's physical left edge — `left: STRIPE_EDGE_INSET = 2`px (`window-row.tsx:474`, `:530`), `right: 0`. `dotted` is a fixed-rhythm `repeating-linear-gradient` (3px dash / 3px gap, `background-size: 3px 100%`), NOT `border-left: dotted` — a dotted border distributes dots per-element and restarts at every row, so stacked dotted rows showed seams while solid/double merged; the gradient's 6px period divides the 24px (36px coarse) row height exactly, so the rhythm carries across adjacent rows. It is purely a display of current state — clicking it opens the picker; it does not cycle. `themes.ts` owns `MARKER_STATES = ["", "dotted", "solid", "double"]` and the `markerStripeStyle(state, color)` helper, shared by this stripe and the Label picker's marker cells so the stripe vocabulary lives in exactly one place. Markers are available on all rows including uncolored ones.
**Marker stripe is display-only**: the marker renders as a left-edge stripe (3px dotted / 3px solid / 6px double in the guarded family color, gray uncolored) via the shared `markerStripeStyle` helper, anchored near-flush at the sidebar's physical left edge — `left: STRIPE_EDGE_INSET = 4`px (`window-row.tsx:474`, `:530`), `right: 0`. `dotted` is a fixed-rhythm `repeating-linear-gradient` (3px dash / 3px gap, `background-size: 3px 100%`), NOT `border-left: dotted` — a dotted border distributes dots per-element and restarts at every row, so stacked dotted rows showed seams while solid/double merged; the gradient's 6px period divides the 24px (36px coarse) row height exactly, so the rhythm carries across adjacent rows. It is purely a display of current state — clicking it opens the picker; it does not cycle. `themes.ts` owns `MARKER_STATES = ["", "dotted", "solid", "double"]` and the `markerStripeStyle(state, color)` helper, shared by this stripe and the Label picker's marker cells so the stripe vocabulary lives in exactly one place. Markers are available on all rows including uncolored ones.

**Marker/color plumbing**: `WindowInfo.marker?: string` (`src/types.ts`) carries the state from the `@rk_marker` window option (architecture.md § `@rk_marker`). `setWindowMarker(server, windowId, marker | null)` in `client.ts` delegates to `setWindowOptions({"@rk_marker": ...})` (empty/`null` clears). `sidebar/index.tsx` owns `onColorChange` + `onMarkerChange` handlers, prop-drilled through `ServerGroup` to `WindowRow`; the Label picker passes the **exact** picked color and marker state straight to these (no cycling — any state is one click). The `Window: Label` palette action opens this same picker via an imperative `label-popover:open` CustomEvent that the matching `WindowRow` listens for (`{ detail: { server, windowId } }`, mirroring the `pin-popover:open` pattern); the row registers both listeners in one `useEffect`. **Scope: window rows only** — session rows and server tiles get the owned-palette color affordance but no marker and no left-edge label zone.

Expand Down
Loading