diff --git a/.changeset/rail-cell-rings-and-blink.md b/.changeset/rail-cell-rings-and-blink.md new file mode 100644 index 000000000..1d9a314e6 --- /dev/null +++ b/.changeset/rail-cell-rings-and-blink.md @@ -0,0 +1,5 @@ +--- +"@internal/server": patch +--- + +Two rulings on the rail's cells. Every open pane now wears ONE thin selection ring, the focused one included: the wider focus ring read too heavy at cell size, so focus and set share the soft single line. And the working cell retired its bright-green beat: it now pulses the half-green fill behind a constant white letter, and a cell with no selection ring keeps a thin dark frame so the empty half is still a square, never a floating glyph. diff --git a/apps/server/web/docs/sidebar.md b/apps/server/web/docs/sidebar.md index b44fc787f..90f119705 100644 --- a/apps/server/web/docs/sidebar.md +++ b/apps/server/web/docs/sidebar.md @@ -86,9 +86,12 @@ size, never a second encoding**: fills from `DOT_CLASS`, the bell from `BELL_TONE`/`showsBell` (exported for this), the row's gestures whole on one element, and the same reveal tooltip, opened BELOW the cell (a right-side popup lands on the next cell in the rail), labels bolded by -`TooltipLabelledLines`. Its working blink is layered (plate, fading fill, -glyph-sized letter chip): at 24px both the dot's vanishing act and a dark -letter on the bare plate read broken. Cells wear their PANE's initial, a +`TooltipLabelledLines`. Its working blink pulses the FILL only, the +half-green idle tile fading in and out behind a constant WHITE initial (the +solid-green beat and its dark knockout letter were retired 2026-09-27). On +the empty half the square stays there: an un-ringed working cell carries a +static `--border` frame the fill pulses inside, while a selected/focused one +needs no frame under its ring. Cells wear their PANE's initial, a hint never a key, and the flat plates are FNV-1a over the machine NAME into eight theme-tuned `--node-tint-*` tokens (278–332, the canvas hue is 296): pure function, no per-machine state, a rename can move a colour, two diff --git a/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx b/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx index a18422eb0..29dee140a 100644 --- a/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx +++ b/apps/server/web/src/components/sidebar/__tests__/rail-subshells.test.tsx @@ -294,10 +294,11 @@ describe("the cell modes keep the row tree's facts", () => { describe("a workspace's selection (operator ask 2026-09-27)", () => { const nodeOf = (header: HTMLElement | undefined) => header?.getAttribute("aria-controls") ?? ""; - it("soft-rings every open pane and white-rings only the dock-focused one", async () => { + it("soft-rings every open pane, focused included, at one width", async () => { // Standing in /workspaces/w1 with a AND b open. Both are SELECTED (the set) - // so both soft-ring; the dock has focused `a`, so only `a` upgrades to the - // bold white ring (operator 2026-09-27). Focus arrives through the + // so both wear the soft ring; the dock-focused `a` wears the SAME ring — the + // `ring-2` focus upgrade was retired the same day it arrived (operator + // 2026-09-27: too heavy at cell size). Focus arrives through the // workspace-focus store the dock publishes — the rail cannot see dockview. setWorkspaceFocusedId("a"); await withRail( @@ -316,9 +317,14 @@ describe("a workspace's selection (operator ask 2026-09-27)", () => { .slice(0, 2) .map((l) => l.getAttribute("href")), ).toEqual(["/subshells/a", "/subshells/b"]); - expect(tok("a")).toContain("ring-2"); // focused → white - expect(tok("b")).toContain("ring-foreground/70"); // set → soft - expect(tok("b")).not.toContain("ring-2"); // only the focused pane is bold + // One ring, one width, the whole set — the bare `ring-1` token is the + // selection (hover adds only the prefixed `hover:ring-1`), and the + // retired `ring-2` must be gone from both. + for (const id of ["a", "b"]) { + expect(tok(id)).toContain("ring-1"); + expect(tok(id)).toContain("ring-foreground/70"); + expect(tok(id)).not.toContain("ring-2"); + } }, { path: "/workspaces/w1", panes: [{ subshellId: "a" }, { subshellId: "b" }] }, ); diff --git a/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx b/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx index e91b802d6..a898329e2 100644 --- a/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx +++ b/apps/server/web/src/components/sidebar/__tests__/subshell-cell-grid.test.tsx @@ -259,13 +259,15 @@ describe("SubshellCell", () => { const link = await screen.findByRole("link"); expect(link.className).toContain("bg-destructive"); // The "you are here" ring is the theme ink (`--foreground`: frost on the - // void) at FULL brightness, width 2 — the operator's ask (2026-09-27) was - // a WHITE ring around the open pane. Asserted as bare TOKENS: the focus - // variant is `focus-visible:ring-2`/`ring-ring` and hover is - // `hover:ring-1`/`hover:ring-foreground`, all prefixed, so a bare `ring-2` + // void) at 70, width 1 — the operator's ruling (2026-09-27): ONE soft + // ring for the whole open set, the `ring-2` focus bump retired the same + // day as too heavy. Asserted as bare TOKENS: the focus variant is + // `focus-visible:ring-2`/`ring-ring` and hover is + // `hover:ring-1`/`hover:ring-foreground`, all prefixed, so a bare `ring-1` // or `ring-foreground/70` can only be the (soft) selection. const toks = link.className.split(/\s+/); - expect(toks).toContain("ring-2"); + expect(toks).toContain("ring-1"); + expect(toks).not.toContain("ring-2"); expect(toks).toContain("ring-foreground/70"); expect(link.className).toContain("focus-visible:ring-ring"); // Hover answers "which one am I on" with the same INK at full @@ -326,7 +328,7 @@ describe("SubshellCell", () => { } }); - it("the blinking cell is a two-state tile: active/idle pulses, a legible letter every beat (operator ask 2026-09-27)", async () => { + it("the working cell pulses its fill only: no solid-green beat, one white initial (operator ruling 2026-09-27)", async () => { const restore = mockFetch(); try { renderCell( @@ -338,54 +340,78 @@ describe("SubshellCell", () => { />, ); const link = await screen.findByRole("link"); - // A cell cannot blink to nothing the way the dot does (no letter to - // strand, no outline to empty), so it blinks BETWEEN two states: a full - // green ACTIVE tile with a dark initial pulsing over a half-green IDLE - // tile with a white initial on the opposite beat. Exactly one tile is - // opaque at any moment, so a box with a readable letter is always there. - // The anchor carries only posture: relative, a transparent field, and - // NEITHER a fill nor a pulse of its own (both live on the two child - // tiles). The interim "green survives as an edge" reading (the ring) and - // the single-fade reading (empty tile / stranded letter) are both dead. + // The blink lives on the FILL alone: a half-green (`bg-success/50`, the + // idle fill) tile pulsing on the plain `subshell-dot-blink` over the + // bare rail, with a constant white initial above it. The retired shape + // blinked full-green + dark-knockout over half-green + white so no beat + // lacked a box; the operator killed the solid-green beat the same day + // (transparent background, white letter). The anchor carries posture + // only: relative, transparent, NO fill and NO pulse of its own. const has = (el: Element | undefined, tok: string) => !!el && el.className.split(/\s+/).includes(tok); expect(link.className).toContain("relative"); expect(link.className).toContain("bg-transparent"); - expect(link.className).not.toContain("bg-success"); - expect(link.className).not.toContain("subshell-dot-blink"); + // Exact tokens: a solid `bg-success` (the retired ACTIVE fill) must be + // gone while `bg-success/50` is its own token and still present. + const linkToks = link.className.split(/\s+/); + expect(linkToks).not.toContain("bg-success"); + expect(linkToks).not.toContain("subshell-dot-blink"); expect(link.className).not.toContain("border-success"); const spans = [...link.querySelectorAll("span")]; expect(spans.find((s) => s.className.includes("bg-muted"))).toBeUndefined(); - // The IDLE tile (under): half green, white initial, complementary pulse. - const idle = spans.find((s) => has(s, "bg-success/50")); - expect(idle).toBeTruthy(); - expect(has(idle, "subshell-dot-blink-alt")).toBe(true); - expect(has(idle, "text-foreground")).toBe(true); - expect(has(idle, "absolute") && has(idle, "inset-0") && has(idle, "rounded-md")).toBe(true); - expect(idle?.getAttribute("aria-hidden")).toBe("true"); - expect(idle?.textContent).toBe("A"); - - // The ACTIVE tile (over): full green, dark initial, the plain pulse. - const activeTile = spans.find((s) => has(s, "bg-success") && has(s, "subshell-dot-blink")); - expect(activeTile).toBeTruthy(); - expect(has(activeTile, "subshell-dot-blink-alt")).toBe(false); - expect(has(activeTile, "text-background")).toBe(true); - expect(has(activeTile, "absolute") && has(activeTile, "inset-0") && has(activeTile, "rounded-md")).toBe(true); - expect(activeTile?.getAttribute("aria-hidden")).toBe("true"); - expect(activeTile?.textContent).toBe("A"); - - // Exactly one pulse of each kind across the cell, and both carry the - // initial — so the link's own text reads the letter twice even though a - // viewer only ever sees one (the visible half differs by opacity, not DOM). - expect(spans.filter((s) => s.className.includes("subshell-dot-blink-alt"))).toHaveLength(1); - expect( - spans.filter((s) => s.className.includes("subshell-dot-blink") && !s.className.includes("-alt")), - ).toHaveLength(1); - expect(link.textContent).toBe("AA"); - // The viewed pane still rings on the anchor (the soft white selection ring). - expect(has(link, "ring-2")).toBe(true); + // The FILL: half green, pulsing, aria-hidden, and it carries NO letter + // (the retired `blink-alt` complement and the dark knockout are gone + // with the solid-green beat). + const fill = spans.find((s) => has(s, "subshell-dot-blink")); + expect(fill).toBeTruthy(); + expect(has(fill, "bg-success/50")).toBe(true); + expect(spans.filter((s) => s.className.includes("subshell-dot-blink"))).toHaveLength(1); + expect(spans.find((s) => s.className.includes("blink-alt"))).toBeUndefined(); + expect(has(fill, "absolute") && has(fill, "inset-0") && has(fill, "rounded-md")).toBe(true); + expect(fill?.getAttribute("aria-hidden")).toBe("true"); + expect(fill?.textContent).toBe(""); + + // The LETTER: exactly one, white, positioned above the fill. + expect(link.textContent).toBe("A"); + const letter = fill?.nextElementSibling ?? undefined; + expect(has(letter, "relative")).toBe(true); + expect(has(letter, "text-foreground")).toBe(true); + expect(has(letter, "text-background")).toBe(false); + expect(letter?.textContent).toBe("A"); + // The viewed pane still rings on the anchor (the soft 1px selection ring). + // The ring IS the frame here, so no border rides along with it. + expect(has(link, "ring-1")).toBe(true); expect(has(link, "ring-foreground/70")).toBe(true); + expect(has(link, "ring-2")).toBe(false); + expect(has(link, "border-border")).toBe(false); + } finally { + restore(); + } + }); + + it("an un-ringed working cell keeps the dark frame the fill blinks inside (operator 2026-09-27)", async () => { + const restore = mockFetch(); + try { + renderCell( + , + ); + const link = await screen.findByRole("link"); + const has2 = (el: Element | undefined, tok: string) => !!el && el.className.split(/\s+/).includes(tok); + // The transparent half must not be a letter floating on bare rail: the + // anchor carries a static `--border` frame the fill pulses inside. Only + // where there is no ring — a selected/focused cell's frame is the ring. + expect(has2(link, "border")).toBe(true); + expect(has2(link, "border-border")).toBe(true); + expect(has2(link, "ring-foreground/70")).toBe(false); + // And it does NOT blink: the frame is on the anchor, the pulse stays on + // the fill child. + expect(has2(link, "subshell-dot-blink")).toBe(false); } finally { restore(); } @@ -641,7 +667,7 @@ describe("SubshellCellGrid", () => { // GROUPED view's cell-to-cell exactly (operator: "we should have the // same gap as we do in the grouped view"), and a 4px tint gutter on ALL // sides — the selection ring paints OUTSIDE the cell box, so p-0.5 left - // it touching the plate edge (p-1 clears the ring-2 selection on every side). + // it touching the plate edge (p-1 clears the ring-1 selection on every side). expect(plateA?.className).toContain("p-1"); expect(plateA?.className).toContain("gap-1.5"); expect(plateA?.className).toContain("rounded-lg"); @@ -683,7 +709,7 @@ describe("SubshellCellGrid", () => { } }); - it("soft-rings the open SET and white-rings only the focused pane", async () => { + it("soft-rings every open pane, focused included, at one width", async () => { const restore = mockFetch(); try { const rows = [sub({ id: "a", name: "one" }), sub({ id: "b", name: "two" }), sub({ id: "c", name: "three" })]; @@ -701,13 +727,15 @@ describe("SubshellCellGrid", () => { (screen.getAllByRole("link") as HTMLElement[]).find((l) => l.getAttribute("href") === `/subshells/${id}`) ?.className ?? "" ).split(/\s+/); - // (operator 2026-09-27): the SET (a, c) wears the soft `ring-foreground/70`; - // only the focused pane (c) upgrades to the bold `ring-2`. Bare tokens — + // (operator 2026-09-27): ONE ring for the open set — a, c and the focused + // c alike wear the soft `ring-1 ring-foreground/70`. Bare tokens: // hover/focus add only prefixed variants, so a lone `b` carries none. - expect(toks("a")).toContain("ring-foreground/70"); - expect(toks("a")).not.toContain("ring-2"); - expect(toks("c")).toContain("ring-2"); - expect(toks("b")).not.toContain("ring-2"); + for (const id of ["a", "c"]) { + expect(toks(id)).toContain("ring-1"); + expect(toks(id)).toContain("ring-foreground/70"); + expect(toks(id)).not.toContain("ring-2"); + } + expect(toks("b")).not.toContain("ring-1"); expect(toks("b")).not.toContain("ring-foreground/70"); } finally { restore(); diff --git a/apps/server/web/src/components/sidebar/rail-subshells.tsx b/apps/server/web/src/components/sidebar/rail-subshells.tsx index fd2aafc98..5634b1df2 100644 --- a/apps/server/web/src/components/sidebar/rail-subshells.tsx +++ b/apps/server/web/src/components/sidebar/rail-subshells.tsx @@ -229,14 +229,15 @@ export function RailSubshells({ (sub: SubshellView) => nodeLabelFor(sub.nodeId || FALLBACK_NODE_ID, nodeData?.nodes, nodeData === undefined).label, [nodeData], ); - // Two selection levels (operator ask 2026-09-27). SELECTED is the SET of open - // panes — the viewed `/subshells/:id` plus every pane the current - // `/workspaces/:id` holds open (the SAME cached query the route runs, a hit - // while a workspace page is up, disabled elsewhere). FOCUSED is the single - // pane: the URL's on a subshell page, or the one the DOCK has active on a - // workspace page (the dock publishes it through `lib/workspace-focus`; the - // sidebar is a different tree and cannot otherwise see dockview's focus). The - // set marks every open cell/row; only the focused one wears the bold ring. + // SELECTED is the SET of open panes — the viewed `/subshells/:id` plus every + // pane the current `/workspaces/:id` holds open (the SAME cached query the + // route runs, a hit while a workspace page is up, disabled elsewhere). + // FOCUSED is the single pane: the URL's on a subshell page, or the one the + // DOCK has active on a workspace page (the dock publishes it through + // `lib/workspace-focus`; the sidebar is a different tree and cannot otherwise + // see dockview's focus). The set marks every open cell/row. On the ROWS the + // two still differ (set = accent fill, focus adds the ring); on the CELLS one + // ring now covers both (operator 2026-09-27: the wide focus ring read heavy). const focusedFromUrl = subshellIdFromPath(location.pathname); const workspaceId = workspaceIdFromPath(location.pathname); const { data: workspaceDetail } = useWorkspace(workspaceId ?? "", { enabled: workspaceId !== null }); diff --git a/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx b/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx index a311265e1..e10627acd 100644 --- a/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx +++ b/apps/server/web/src/components/sidebar/subshell-cell-grid.tsx @@ -145,15 +145,15 @@ export function flatCellRows( const INITIAL_CLASS = "text-foreground"; /** Solid fills where white drops below AA (design-tokens contrast math): - * `waiting` on `--warning` (1.34:1), `node-offline` on `--destructive` (2.77:1), - * and the working cell's ACTIVE tile on full `--success` — so `active` is listed - * even though it only ever shows as the top tile of the active/idle blink (see - * the render site). Each keeps its own pairing: the bright green tile takes a - * dark initial, the semitransparent idle tile beneath it keeps white (4.34:1). - * Ruled 2026-09-27 (image #9): the ask was the green BACKGROUND legible, not an - * edge, and the operator named the darker font directly. */ + * `waiting` on `--warning` (1.34:1) and `node-offline` on `--destructive` + * (2.77:1). `active` was the third entry — the blink's full-`--success` tile + * took a dark initial (image #9 ruling, 2026-09-27, same day) — but the + * operator retired that pairing: the working cell now shows NO full-green + * beat at all (see the render site), so there is no bright field to knock + * out. White on the remaining states stands: idle's `success/50` clears + * 4.34:1, and the working letter sits on the rail or the half fill, white + * both beats. */ const DARK_KNOCKOUT: Partial> = { - active: true, waiting: true, "node-offline": true, }; @@ -185,13 +185,14 @@ export interface SubshellCellLabels { * same tooltip string. * * The square itself carries the state: `DOT_CLASS` fills for everything quiet, - * the working state blinks between two tiles (see {@link blinking} below), and a + * the working state pulses a half-green fill behind a constant white initial + * (see {@link blinking} below), and a * bell (`showsBell`) REPLACES the fill, the dot's posture — the glyph names - * "unseen push", the tone keeps the state. Selection reads on TWO levels - * (operator 2026-09-27): every open pane's cell (the workspace's SET) wears a - * thin `ring-1 ring-foreground/70`, and the one cell the dock has FOCUSED wears - * the thicker `ring-2 ring-foreground/70` — both share the `/70` tone, so focus - * reads bolder by WIDTH, not a brighter line. The accessible name is "name: status + * "unseen push", the tone keeps the state. Every open pane's cell (the + * workspace's SET, focused included) wears ONE soft `ring-1 ring-foreground/70` + * (operator ruling 2026-09-27, same day as the two-width version it replaces: + * the `ring-2` focus ring read too heavy at cell size, and at one width the + * two levels stop being tellable apart, so there is one level). The accessible name is "name: status * word", and for a bell "name: unseen notification (status word)" — the shared * {@link bellAnnouncement} — since a square of colour has nothing to read out * and its glyph is aria-hidden. @@ -210,7 +211,8 @@ export function SubshellCell({ subshell: SubshellView; /** Open in the current workspace (or the viewed page) — marks the SET. */ selected: boolean; - /** The pane the dock has focused — the single cell with the white ring. */ + /** The pane the dock has focused. It rings like every other open cell; kept + * because a focused pane must wear the ring even before the SET catches up. */ focused: boolean; /** * The cell is a pane of the workspace on screen, so a click should FOCUS that @@ -222,13 +224,19 @@ export function SubshellCell({ }) { const indicator = subshellIndicator(subshell); const bell = showsBell(subshell); - // The ACTIVE square blinks, but between two states rather than green and - // nothing: an ACTIVE tile pulsing over an IDLE tile (see the render site), so a - // box with a legible initial is on screen every beat. A cell cannot blink to - // empty the way the dot does — the dot has no letter to strand and no outline - // to empty (images #10/#11/#14 were each a different symptom of that). Every - // other state keeps its single static fill on the anchor exactly as before — - // `terminated`'s hollow reading especially: nothing behind it. + // The ACTIVE square pulses its FILL only: a `bg-success/50` tile blinking + // over the bare rail (see the render site) behind a constant white initial. + // It used to blink between two painted tiles — full green with a dark + // knockout letter over the half-green one — so no beat ever lacked a box. + // The operator retired the solid-green beat on 2026-09-27: transparent + // background + white letter on that half, which also retires the dark + // knockout (images #10/#11/#14 were symptoms of stranding a DARK letter on + // an empty tile; a white one reads on the rail exactly as `terminated`'s + // does — and an UN-RINGED working cell keeps a static `--border` frame so + // even the empty half is a square, operator 2026-09-27 follow-up). Every + // other state keeps its single static fill on the anchor + // exactly as before — `terminated`'s hollow reading especially: nothing + // behind it. const blinking = !bell && indicator === "active"; const cell = ( @@ -267,21 +275,26 @@ export function SubshellCell({ // and there is nothing to blink behind it. "relative border border-border bg-transparent" : blinking - ? // No field and no frame of its own: the blinking green box - // (with its letter) is a child layer, so the transparent - // half is genuinely empty rail, not a dark letter stranded - // inside a frame (image #10). Only the selection/focus ring - // (below) can still name the cell in that gap. - "relative bg-transparent" + ? // No field of its own: the blinking half-green fill is a + // child layer, so the transparent half is genuinely empty + // rail with the white initial riding over it (the + // constant letter is a sibling above the fill). The + // square keeps a dark `--border` frame on that half — + // operator 2026-09-27: the letter floating with no box + // read broken, the frame is what stays — but only when + // the cell has NO ring: a selected/focused working cell + // already wears its frame in ink, and a border under the + // ring would just be a second line. + cn("relative bg-transparent", !selected && !focused && "border border-border") : DOT_CLASS[indicator], - // Two selection levels (operator 2026-09-27), both SOFT so the - // box matches the rows: the focused pane wears a `ring-2` - // (`ring-foreground/70`), the other open panes (the set) keep the - // same tone at width 1. Focus reads bolder by WIDTH, not by a - // harsh full-brightness line. Hover answers "which one am I on" - // at width 1 full ink; focus stays the orchid RING token. + // ONE ring for the whole open SET (operator 2026-09-27), SOFT so + // the box matches the rows: `ring-1 ring-foreground/70` on every + // open pane, focused or not. The focus bump to `ring-2` from the + // same day read too heavy and was retired the same day. Hover + // answers "which one am I on" at width 1 full ink; keyboard + // focus stays the orchid RING token. "hover:ring-1 hover:ring-foreground", - focused ? "ring-2 ring-foreground/70" : selected ? "ring-1 ring-foreground/70" : undefined, + focused || selected ? "ring-1 ring-foreground/70" : undefined, )} /> } @@ -289,38 +302,24 @@ export function SubshellCell({ {bell ? ( ) : blinking ? ( - // The working cell blinks between two REAL states, never to an - // empty tile: an ACTIVE tile (`bg-success` + a dark initial) on top - // of an IDLE tile (`bg-success/50` + a white initial) underneath, - // the two driven by complementary opacity pulses (`subshell-dot-blink` - // / `subshell-dot-blink-alt`). Exactly one tile is fully painted at - // any beat, so a box carrying a legible letter is ALWAYS there — the - // dot can blink to nothing because it has no letter; a cell cannot - // (that empty tile was image #14, the ghost #10, the stranded letter - // #11). Under reduced-motion neither class applies, the ACTIVE tile - // paints over the idle one, and the cell rests as solid green + dark - // initial. Each tile keeps its own state's contrast pairing. + // The working cell pulses its FILL, never its letter: a half-green + // tile (`bg-success/50`, the idle fill, driven by the plain + // `subshell-dot-blink` opacity pulse so it switches on the same + // beat as the dot) with a constant WHITE initial riding above it. + // The transparent half is the bare rail with the white letter on it + // — legible exactly as `terminated`'s hollow cell is, which is what + // lets the solid-green + dark-knockout beat go away (operator + // ruling 2026-09-27: that pairing read wrong; the letter is white, + // the background transparent). The fill is aria-hidden layout, the + // letter is the one the link reads out. Under reduced-motion the + // class carries nothing: the cell rests as the half-green fill + + // white initial, like idle, with the tooltip still saying working. <> - {labels.initial} - - - {labels.initial} - + className={cn("subshell-dot-blink absolute inset-0 rounded-md", DOT_CLASS.idle)} + /> + {labels.initial} ) : labels.initial ? ( // Solid light fills defeat white (see `DARK_KNOCKOUT`); every @@ -397,9 +396,10 @@ export function SubshellCellGrid({ */ selectedIds: ReadonlySet; /** - * The single pane the dock has focused (or the viewed page's id) — the one - * cell that upgrades to the bold white ring. `selectedIds` and `focusedId` - * are different signals on purpose (operator 2026-09-27). + * The single pane the dock has focused (or the viewed page's id). Since the + * one-ring ruling (operator 2026-09-27) it renders no differently from the + * SET — it is carried so a just-focused pane rings even a beat before the + * SET's query catches up. */ focusedId: string | null; /** diff --git a/apps/server/web/src/styles.css b/apps/server/web/src/styles.css index 94b74f8a7..21de11ec6 100644 --- a/apps/server/web/src/styles.css +++ b/apps/server/web/src/styles.css @@ -329,18 +329,15 @@ body { animation: subshell-dot-blink 1.6s steps(1) infinite; } - /* The exact complement of `.subshell-dot-blink` (same 1.6s / steps(1), the - two opacity halves swapped). The rail's working CELL needs both at once: - the green box and its dark letter ride the plain blink, and a WHITE letter - underneath rides THIS one, so a readable initial is present in either - half. Without it the transparent half leaves an empty tile (the dot can - vanish clean because it carries no letter; a cell cannot) — operator device - review 2026-09-27, image #11. Under `prefers-reduced-motion` neither class - exists, both letters sit at full opacity, and the green box paints over - the white one, so the resting cell is the solid green + dark letter. */ - .subshell-dot-blink-alt { - animation: subshell-dot-blink-alt 1.6s steps(1) infinite; - } + /* The rail's working CELL rides this same class, on the fill only: a + `bg-success/50` tile blinking behind a constant white initial, so the + letter never rides the pulse. It used to need the exact complement of + this class (a second, `blink-alt` pulse carrying the idle tile under the + solid-green one, so a DARK letter was never stranded on an empty beat — + operator device review 2026-09-27, image #11); the letter turned white + the same day and the complement went with the solid-green beat it + existed to pair with. Under `prefers-reduced-motion` the class carries + nothing and the cell rests as the half-green fill + white initial. */ /* Input queue badge (spec 2026-09-21 Wave A): the three chevrons pulse in a stagger so in-flight keystrokes read as moving, not stuck. One keyframe, @@ -365,17 +362,6 @@ body { opacity: 0; } } -/* The complement of `subshell-dot-blink`: visible exactly where that one hides, - so the white initial underneath the blinking green box reads through the - transparent half. See `.subshell-dot-blink-alt`. */ -@keyframes subshell-dot-blink-alt { - 0% { - opacity: 0; - } - 50% { - opacity: 1; - } -} @keyframes assistant-in { from { opacity: 0;