Skip to content

Shade a Chat row's whole height, so neighbouring rows' shades meet, and keep thread rows inside their panel - #882

Merged
lavaman131 merged 3 commits into
mainfrom
row-shade-full-height
Nov 12, 2026
Merged

lavaman131 merged 3 commits into
mainfrom
row-shade-full-height

Conversation

@lavaman131

Copy link
Copy Markdown
Collaborator

Shade a Chat row over its whole pitch, as Slack's rows tile its pane

What the maintainer saw

After the change that took the hover shade to Chat's side edges went live: "why don't the containers
for the message hover still not encapsulate the entire box? I still see a gap ... with some margin or
padding".

Measured on a hosted deployment, side to side was already 0px on message rows. Three things were
still wrong:

  1. Top and bottom. Chat's rows are 16px apart, and messages in a group 4px. A row's shade reached
    4px above its message (32px over a group's first message) and 4px or 8px below. So:
    • neighbouring shades overlapped by 2px to 4px, and
    • the pointer found a row only inside the message's own box. In every gap, and over every
      avatar, it was on no row at all. Moving a mouse down Chat crossed unshaded bands of 6px to 8px,
      and the shaded box looked inset.
  2. Folded thread. With a thread open and the document folded to its bar, a thread row's shade ran
    20px past the thread panel, to the window's edge and past the header card above it.
  3. Run card. The 1px hairline between two workflow runs belonged to no run's line. The shade
    stopped under it, and the pointer on it was on no line.

The cause

  • The shade's insets (--chat-highlight-top, then -var(--spacing) or -2 * var(--spacing) below)
    were set as "a little room around the message", not as the middle of the gap to each neighbour.
  • :hover was the message's own box. The shade had pointer-events: none, so it was never where the
    pointer found the row.
  • The folded split pane's clip path let the thread draw 12px into the frame's gutter after it, and the
    thread's transcript reached 20px past its panel into it.
  • In the run card, the hairline was .run-row + .run-row { border-top }: the row's border, outside
    the line (.run-head) that is hovered and shaded.

The fix

Chat rows (message-actions.css):

  • Each message's shade reaches to the middle of the gap on both sides. That is 2px inside a group and
    8px between rows (--chat-gap-above, --chat-gap-below). A group's first message reaches over its
    name line, which stands 2px above its row, to the same middle.
  • So neighbouring shades meet with no band and no overlap. This holds next to notices and other rows
    that are not messages, at a thread's lead, and at both ends of the transcript.
  • Under a mouse, inside a transcript, the shade itself is where the pointer finds the row, and so is
    the avatar (@media (hover: hover) and (pointer: fine)). The pointer passes from one row's shade
    straight into the next one's, as in Slack.
  • A touch screen keeps the rows' own boxes. A reaction pill's 44px touch target stands past its row,
    and must still take the tap (message-toolbar-layout caught this in a first version).
  • Every shaded state uses the same pitch: hover, keyboard focus, menu or picker open, a long press,
    a jumped-to wash, and an open thread's root. A long-pressed row's toolbar lane is part of its own
    pitch.
  • A thread's root, now drawn as a Chat row in the thread's lead (from origin/main), reaches halfway
    to the provenance line and the replies' rule, which are 12px from it (thread-panel.css, through
    --chat-row-half).
  • Content does not move. The toolbar still stands 16px above its row's shade, so it sits 2px lower.

Folded thread (theme.css, workspace-sizing.css):

  • The folded pane's clip path is gone, and the folded thread's transcript ends at its panel. Its rows
    stop where the thread's header rule ends.
  • The pane's clip keeps its unbounded top and foot, so images above Chat still load lazily
    (chat-images passes).

Run card (run-card.css, run-row.css):

  • The hairline is now the line's own top border. The pointer on it is on the line, and the line's
    shade runs under it. The card and its lines do not move.

DESIGN.md records the pitch, the folded thread's edge and the run card's hairline.

Evidence

Synthetic documents only. Chopin has one theme: .dark and the system preference read the same
tokens, and this change adds no colour.

Before (b8bf8eb) After
1440, the pointer 4px above the agent's row, in the gap
1440, the agent's row hovered
1440, the second of a group hovered
1440, thread open, document folded: the thread's right edge
1440, thread open, document folded
Phone, a long-pressed row
Run card, a run hovered

In the gap shots, the pointer is 4px above the agent's row. Before, it is on no row and nothing is
shaded. After, the agent's row is shaded with its toolbar. In the folded thread, the shade ran to the
window's edge; now it ends with the thread's header rule. The run card's change is one pixel: the
hairline above the hovered line is drawn over the shade.

What the tests check

e2e/chat-row-edges.e2e.ts gains pitches(). For every message in a transcript it checks:

  • the shade starts and ends at the middle of the gap to each neighbour, within 0.5px, so neighbouring
    shades meet with no gap and no overlap;
  • under a mouse, sweeping down the container's start, the avatars and the text, the pointer finds
    the row whose pitch it is in at every pixel, and no message between rows that are not messages.

It runs in Chat and in the thread at 1440 (document folded), 1920 (thread beside the document), 1100
and 640, and on a phone (extents only: a touch screen has no pointer moving down it). Those layouts
include a notice, a grouped pair, the agent's rows, an image row and a thread's lead. The tests also:

  • hover 4px into the gaps above and below a row and expect its shade;
  • check that a jumped-to wash covers the shade's pitch;
  • measure a folded thread's rows against the thread panel.

e2e/run-card.e2e.ts checks that the second run's shade meets the run above it and that the hairline
finds its line.

On b8bf8eb, 6 of these tests fail. They report every shade 2px off its pitch, unshaded bands at
every gap and avatar, and a 1px gap in the run card. All pass here.

Contrast

  • No colour changed. The row shade, selected shade and wash keep their tokens, and
    chat-hover-contrast passes. It reads the time, links, the replies count and the toolbar icons
    against the rendered shade.
  • The run card's hairline now lies on the hover surface on the hovered line. Read from the
    screenshots' pixels:
    • hairline on the card: 1.17:1;
    • hairline on the shade: 1.18:1;
    • shade on the card: 1.06:1.
      The hairline keeps its contrast against what is behind it. It is a decorative divider, unchanged
      in colour.

Checks run

  • bun run types: passes.
  • bun run ci: passes.
  • Playwright, after rebasing onto origin/main (aaa1d6c):
    • chat-row-edges, chat-threads, chat-thread-reactions, message-toolbar-layout and
      chat-toolbar-in-view: 43 passed;
    • run-card, chat-hover-contrast, chat-images, chat-motion, chat-phone, message-actions,
      pane-resize and pane-work: 97 passed.
  • Playwright on b8bf8eb: chat-row-edges and run-card fail (6 tests), as above.
  • bun run design:behavior on the final head: 103 passed. Three tests (build-blockers, the dark
    contrast plate and cursors) timed out waiting for the audit page to load, and passed when run again
    (22 passed). The dev server was run with CHOKIDAR_USEPOLLING=1, because the shared machine was
    out of inotify instances. Before the rebase, the whole suite passed (106).
  • bun run build:
    • initial JS 259,958 B raw and 82,477 B gzip (budget 260,500 / 83,450);
    • initial CSS 184,261 B raw and 32,644 B gzip (budget 185,000 / 33,000).
      The initial stylesheet is 290 B smaller than on b8bf8eb.

Judgement calls

  • Half the gap, not row padding. Each shade reaches halfway into the gaps. I did not move the gaps
    into row padding, because that would change every row's box, the toolbar's anchor and the
    transcript's layout, and pane-work's counts with them. Only the shade's insets and its hit area
    changed.
  • The row is measured by its layout box. A group's name line stands 2px above its row (the
    column's -mt-0.5), so the shade reaches 6px above the name line and 8px above the row.
  • The hit area is mouse-only and transcript-only. On a touch screen, a reaction pill's 44px target
    must win over the next row. Outside a transcript (the design audit's plates), nothing clips the
    shade's 100vw sides, so a hit area there would cover other controls.
  • The avatar is the row's under a mouse. It has no action of its own, and in Slack the pointer on
    it shades the row.
  • Where a folded thread ends. I chose the thread panel's own edge ([data-thread-panel]), where
    its header rule ends. I did not choose the frame's edge 8px further, under the header card's corner.
    The maintainer's report named the panel's edge. With the document open, the thread still reaches
    the sheet, over the 8px where Resize chat is taken, as main Chat does.
  • The toolbar's position. The toolbar keeps standing 16px above its row's shade, as Slack's does
    above its row. So it sits 2px lower than before, or 4px higher for a group resumed after an offer
    card. The offer card is the only row that splits a group; that case is covered by CSS and not by a
    test.
  • The run card's hairline. The hairline now belongs to the run below it. Hovering a run shades its
    hairline too, and the hairline stays drawn over the shade. An open run's stages are not part of its
    line, and are not shaded, as before.

Impeccable passes

The audit (interaction states), polish and the critique against Slack are in critique.md beside this
file. In short:

  • Every shaded state now tiles the transcript as Slack's rows tile its pane.
  • Nothing moves, and no colour, motion or token was added.
  • Touch targets keep winning on touch screens.

desktop-1440-page-after

desktop-1440-page-before

run-card-hairline-after

run-card-hairline-before

In Chat's workflow runs card, the hairline between two runs was the lower run's row border, outside
its line. A hovered line's shade stopped under it, and a pointer on that pixel was on no line, so
moving down the card crossed a 1px unshaded band between every two lines.

The hairline is now the line's own top border, so the pointer on it is on the line, and the line's
shade runs under it from the card's left edge to its right. The hairline is drawn over the shade, as
before, at the same contrast against what is behind it. Nothing moves: the border is where the row's
was, and the card keeps its box.

run-card's hover test now checks that the second run's shade meets the run above it and that the
hairline finds its line. Offsets are read from inside the line's border. It fails on b8bf8eb with
a 1px gap.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline (subagent)
Assistant-verification: bun run types
Assistant-verification: bun run ci
Assistant-verification: playwright run-card (passes; the new check fails on b8bf8eb)
User-preference: "why don't the containers for the message hover still not encapsulate the entire box? I still see a gap ... with some margin or padding"
Co-authored-by: Alex Lavaee <lavaman131@github.com>
A shown Chat row was shaded edge to edge but not top to bottom. Its shade reached 4px above a
message (32px over a group's first, its name line and 8px) and 4px or 8px below, while messages in a
group are 4px apart and the transcript's rows 16px. So neighbouring shades overlapped by 2px to 4px,
and, more visibly, the pointer found the row only in the message's own box: in every gap, and over
every avatar, it was on no row at all. Moving a mouse down Chat crossed unshaded bands of 6px to 8px
between rows, and the shaded box looked inset.

Each message's shade now reaches to the middle of the gap on both sides: 2px in a group, 8px
between rows (over the name line for a group's first message, which stands 2px above its row). So
neighbouring shades meet with no band and no overlap, past notices and other rows that are not
messages, at a thread's lead and at both ends of the transcript. Under a mouse, inside a transcript,
the shade itself and the avatar are where the pointer finds the row, so the pointer passes from one
row's shade straight into the next one's. A touch screen keeps the rows' own boxes, so a reaction's
44px target that stands past its row still takes the tap. Every shaded state (hover, keyboard focus,
menu or picker open, a long press, a jumped-to wash, an open thread's root) uses the same pitch, and
a long-pressed row's toolbar lane is part of its own. Content does not move; the toolbar keeps
standing 16px above its row's shade, 2px lower than before.

With a thread open and the document folded to its bar, the thread's rows were shaded 20px past the
thread panel, to the window's edge and past the header above them. They now stop at the panel's
edge, where its header's rule ends: the folded pane's clip path and the thread's reach into the
gutter after it are gone. The pane's clip keeps its unbounded top and foot, so lazy images above
Chat still load (chat-images passes).

chat-row-edges measures every message's shade against its pitch and sweeps a mouse's hit test down
the container's start, the avatars and the text, in Chat and in the thread, at 1440 (folded), 1920,
1100, 640 and on a phone (extents only there), with a notice, a grouped pair, the agent's rows and an
image row. It also hovers 4px into the gaps above and below a row, checks a jumped-to wash covers the
shade's pitch, and measures a folded thread's rows against the thread panel. These fail on
b8bf8eb.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline (subagent)
Assistant-verification: bun run types
Assistant-verification: bun run ci
Assistant-verification: playwright chat-row-edges, run-card, chat-hover-contrast, chat-images, chat-motion, chat-phone, chat-threads, chat-toolbar-in-view, message-actions, message-toolbar-layout, pane-resize, pane-work (136 passed)
Assistant-verification: playwright chat-row-edges and run-card on b8bf8eb (6 of the extended tests fail there)
Assistant-verification: bun run design:behavior (106 passed)
Assistant-verification: bun run build (initial JS 259,958 B raw / 82,474 B gzip; CSS 184,261 B raw / 32,644 B gzip)
User-preference: "why don't the containers for the message hover still not encapsulate the entire box? I still see a gap ... with some margin or padding"
Co-authored-by: Alex Lavaee <lavaman131@github.com>
…rule

A thread's root is now drawn as Chat draws a message, inside the thread's lead, between the
provenance line and the replies' rule, 12px from each. Its shade took the transcript's half gap of
8px, so it reached 2px over each of them. It now takes half of the lead's gap (6px), through one
custom property for the half of the gap between rows (`--chat-row-half`), which a group's first and
last messages and the name line's reach read.

chat-row-edges reads the lead's rows one by one, so the root's row is measured against its pitch and
swept for bands like any other; it failed on the rebased branch before this change, with the root
found 2px into the lead's rows.

Assistant-model: Claude Opus 5.5
Assistant-workflow: inline (subagent)
Assistant-verification: bun run types
Assistant-verification: bun run ci
Assistant-verification: playwright chat-row-edges, chat-threads, chat-thread-reactions, message-toolbar-layout, chat-toolbar-in-view (43 passed)
Assistant-verification: playwright run-card, chat-hover-contrast, chat-images, chat-motion, chat-phone, message-actions, pane-resize, pane-work (97 passed)
User-preference: "why don't the containers for the message hover still not encapsulate the entire box? I still see a gap ... with some margin or padding"
Co-authored-by: Alex Lavaee <lavaman131@github.com>
@lavaman131
lavaman131 merged commit 0f2a8e2 into main Nov 12, 2026
24 checks passed
@lavaman131
lavaman131 deleted the row-shade-full-height branch November 12, 2026 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant