style(room): lead the 1:1 stream row with the rail+dot, move the timestamp beside the nick and tint the peer nick lavender

This commit is contained in:
heaven 2026-05-30 14:35:41 +03:00
parent 109941e0dd
commit a84c534179
4 changed files with 150 additions and 158 deletions

View file

@ -27,7 +27,7 @@ export function EventContent({
}: EventContentProps) { }: EventContentProps) {
const compact = useScreenSizeContext() === ScreenSize.Mobile; const compact = useScreenSizeContext() === ScreenSize.Mobile;
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const timeRef = useRef<HTMLDivElement>(null); const timeRef = useRef<HTMLSpanElement>(null);
const railRef = useRef<HTMLSpanElement>(null); const railRef = useRef<HTMLSpanElement>(null);
const dotRef = useRef<HTMLSpanElement>(null); const dotRef = useRef<HTMLSpanElement>(null);
const bodyRef = useRef<HTMLDivElement>(null); const bodyRef = useRef<HTMLDivElement>(null);
@ -49,17 +49,15 @@ export function EventContent({
} }
// Sysline = thin one-line state-event row that lives ON the rail. // Sysline = thin one-line state-event row that lives ON the rail.
// Same 3-track grid as message rows (StreamRoot) — track 1 timestamp, // Same 2-track grid as message rows (StreamRoot) — track 1 dot column,
// track 2 dot column, track 3 body — so the dot's X aligns with the // track 2 body — so the dot's X aligns with the dots above and below it.
// dots above and below it. // The timestamp trails the body (content → time), mirroring the message
// header's nick → time order now that the dot leads the row.
return ( return (
<div <div
className={classNames(layoutCss.StreamRoot({ compact }), layoutCss.StreamSysline)} className={classNames(layoutCss.StreamRoot({ compact }), layoutCss.StreamSysline)}
ref={rootRef} ref={rootRef}
> >
<div className={layoutCss.StreamTimeColumn} ref={timeRef}>
{time}
</div>
<span className={layoutCss.StreamDotColumn} aria-hidden> <span className={layoutCss.StreamDotColumn} aria-hidden>
<span <span
className={classNames( className={classNames(
@ -83,6 +81,11 @@ export function EventContent({
<Box gap="200" alignItems="Center" style={{ minWidth: 0 }} ref={bodyRef}> <Box gap="200" alignItems="Center" style={{ minWidth: 0 }} ref={bodyRef}>
<Icon style={{ opacity: 0.5, flexShrink: 0 }} size="50" src={iconSrc} /> <Icon style={{ opacity: 0.5, flexShrink: 0 }} size="50" src={iconSrc} />
<div className={layoutCss.StreamSyslineBody}>{content}</div> <div className={layoutCss.StreamSyslineBody}>{content}</div>
{time && (
<span className={layoutCss.StreamSyslineTime} ref={timeRef}>
{time}
</span>
)}
</Box> </Box>
</div> </div>
); );

View file

@ -4,7 +4,6 @@ import { as, toRem } from 'folds';
import * as css from './layout.css'; import * as css from './layout.css';
import { useStreamLayoutDebug } from './streamDebug'; import { useStreamLayoutDebug } from './streamDebug';
import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { Time } from '../Time';
// Day-divider rows fall back to this `S400` MessageBase spacing variant // Day-divider rows fall back to this `S400` MessageBase spacing variant
// (see RoomTimeline.renderDayDivider). Message rows force collapse=true // (see RoomTimeline.renderDayDivider). Message rows force collapse=true
@ -13,12 +12,6 @@ import { Time } from '../Time';
// over the tighter gap stays hidden by the dot halo. // over the tighter gap stays hidden by the dot halo.
export const STREAM_MESSAGE_SPACING = '400' as const; export const STREAM_MESSAGE_SPACING = '400' as const;
// Sample timestamp used by the day-divider's invisible track-1 placeholder.
// Any ts works — `<Time compact />` formats with the locale's hh:mm or HH:mm,
// and tabular-nums makes every formatted timestamp the same width within a
// chat. The auto-sized grid column then matches the surrounding message rows.
const DAY_DIVIDER_PLACEHOLDER_TS = 0;
// Rail-dot diameters. The base dot is 9px (see `StreamDotSize` / // Rail-dot diameters. The base dot is 9px (see `StreamDotSize` /
// `StreamDotColumn` in layout.css.ts, which keep the rail X consistent). The // `StreamDotColumn` in layout.css.ts, which keep the rail X consistent). The
// neutral gray dot is 0.95× that; the «state» dots (green = read, gold = // neutral gray dot is 0.95× that; the «state» dots (green = read, gold =
@ -32,14 +25,15 @@ const STREAM_DOT_PROMINENT = toRem(9.405);
// Stream layout — DM «VS Code chat» redesign // Stream layout — DM «VS Code chat» redesign
// (docs/plans/dm_stream_vscode_redesign.md). // (docs/plans/dm_stream_vscode_redesign.md).
// //
// Visual structure (3-track CSS grid, see StreamRoot in layout.css.ts): // Visual structure (2-track CSS grid, see StreamRoot in layout.css.ts):
// ┌─G─┬─time─┬─G─┬─dot─┬─G─┬───── bubble (1fr) ─────────┐ // ┌─G─┬─dot─┬─G─┬───── content (1fr) ───────────────────┐
// nick · time ← header line
// bubble / text
// //
// All three gaps come from one `--vojo-stream-gap` custom property — a // The rail + dot lead (track 1), hugging the left screen edge; the nick
// single recipe variant per breakpoint. The browser auto-sizes the time // and a muted timestamp ride the header line at the top of the content
// track to the rendered timestamp width, so 24h "16:58" and AM/PM // column (track 2), in that order — rail+dot → nick → time. The two gaps
// "11:30 PM" both produce a symmetric layout with equal gaps; the dot // (screen→dot, dot→content) come from the StreamRoot recipe per breakpoint.
// and bubble simply shift right when the format is wider.
export type StreamLayoutProps = { export type StreamLayoutProps = {
time?: ReactNode; time?: ReactNode;
@ -53,15 +47,16 @@ export type StreamLayoutProps = {
isOwn?: boolean; isOwn?: boolean;
compact?: boolean; compact?: boolean;
// Same-sender continuation row (the whole run after the first message, any // Same-sender continuation row (the whole run after the first message, any
// minute): drop the rail dot + timestamp + nick and stack the body tight // minute): drop the rail dot and the whole header line (nick + timestamp)
// under the previous one. The timestamp is kept in the DOM (invisible) only // and stack the body tight under the previous one. The caller also passes
// to reserve the time-track width. The caller also passes `header={undefined}` // `header={undefined}` for collapsed rows — and since the time now rides the
// for collapsed rows. See RoomTimeline `collapsed`. // header line rather than its own grid track, dropping the header drops the
// time with it (no width reservation needed). See RoomTimeline `collapsed`.
collapsed?: boolean; collapsed?: boolean;
// Author name — rendered as a bold label ABOVE the bubble, on the // Author name — rendered as a bold label ABOVE the bubble, leading the
// dot/timestamp baseline (DM «VS Code chat» redesign). `undefined` for // header line (nick → time) on the dot baseline (DM «VS Code chat»
// media rows (the name is overlaid on the media instead) and for collapsed // redesign). `undefined` for collapsed continuation rows. Media rows still
// continuation rows. // show the nick here too (above the media, no overlay).
header?: ReactNode; header?: ReactNode;
railStart?: boolean; railStart?: boolean;
railEnd?: boolean; railEnd?: boolean;
@ -100,21 +95,14 @@ export const StreamDayDivider = as<'div', StreamDayDividerProps>(
aria-label={typeof label === 'string' ? label : undefined} aria-label={typeof label === 'string' ? label : undefined}
ref={ref} ref={ref}
> >
{/* Track 1: invisible <Time> placeholder so the auto-track sizes {/* Track 1: dot column, holds the rail line + the larger day-dot.
to the same width as adjacent message rows keeps the rail Same fixed width + paddingLeft anchor as message rows, so the
and day-dot at a consistent X across the timeline. */} day-dot lands on the exact same X as the message dots. */}
<span
className={classNames(css.StreamHeaderTime, css.StreamDayTimePlaceholder)}
aria-hidden
>
<Time ts={DAY_DIVIDER_PLACEHOLDER_TS} compact />
</span>
{/* Track 2: dot column, holds the rail line + the larger day-dot. */}
<span className={css.StreamDotColumn} aria-hidden> <span className={css.StreamDotColumn} aria-hidden>
<span className={css.StreamRail} /> <span className={css.StreamRail} />
<span className={css.StreamDayDot} /> <span className={css.StreamDayDot} />
</span> </span>
{/* Track 3: `─── label ───` line. */} {/* Track 2: `─── label ───` line. */}
<div className={css.StreamDayLineWrap} aria-hidden> <div className={css.StreamDayLineWrap} aria-hidden>
<span className={css.StreamDayLineSegment} /> <span className={css.StreamDayLineSegment} />
<span className={css.StreamDayLabel}>{label}</span> <span className={css.StreamDayLabel}>{label}</span>
@ -180,16 +168,7 @@ export const StreamLayout = as<'div', StreamLayoutProps>(
{...props} {...props}
ref={rootRef} ref={rootRef}
> >
{/* Collapsed rows keep the timestamp in the DOM (so the auto-sized {/* Track 1: rail + dot, leading the row at the left screen edge. */}
time track stays the same width and the body column doesn't shift)
but hide it only the first message of the minute shows its time. */}
<span
className={classNames(css.StreamHeaderTime, collapsed && css.StreamHeaderTimeHidden)}
aria-hidden={collapsed || undefined}
ref={timeRef}
>
{time}
</span>
<span className={css.StreamDotColumn} aria-hidden> <span className={css.StreamDotColumn} aria-hidden>
{/* The rail is suppressed entirely on trailing continuation rows {/* The rail is suppressed entirely on trailing continuation rows
(after the last dot) so the line stops at the last dot instead of (after the last dot) so the line stops at the last dot instead of
@ -223,10 +202,23 @@ export const StreamLayout = as<'div', StreamLayoutProps>(
</span> </span>
)} )}
</span> </span>
{/* Track 2: header line (nick → muted time) + bubble + reactions. */}
<div className={css.StreamColumn}> <div className={css.StreamColumn}>
{/* The header prints once at the head of a same-sender run; every
continuation row drops it (header is undefined when collapsed), so
the nick + timestamp only show on the first message of a run. The
nick colour is per-side: own = white/black, peer (vojo) = lavender. */}
{header && ( {header && (
<div className={css.StreamName} ref={headerRef}> <div
className={classNames(css.StreamName, isOwn ? css.StreamNameOwn : css.StreamNamePeer)}
ref={headerRef}
>
{header} {header}
{time && (
<span className={css.StreamHeaderTime} ref={timeRef}>
{time}
</span>
)}
</div> </div>
)} )}
<div <div

View file

@ -141,42 +141,34 @@ export const UsernameBold = style({
// Stream layout (DM «VS Code chat» redesign — see // Stream layout (DM «VS Code chat» redesign — see
// docs/plans/dm_stream_vscode_redesign.md). // docs/plans/dm_stream_vscode_redesign.md).
// //
// Symmetric three-gap layout, expressed as a 3-track CSS grid: // Rail-first layout, expressed as a 2-track CSS grid (the rail + dot hug
// the screen edge; the nick and timestamp share the header line):
// //
// ┌─G─┬─time─┬─G─┬─dot─┬─G─┬───── bubble (1fr) ─────────┐ // ┌─G─┬─dot─┬─G─┬───── content (1fr) ────────────────────┐
// row row // row │ │ nick · time ← header line row
// left right // left rail │ bubble / text right
// //
// where G = `column-gap` = `padding-left` of the row root. Both inset // where G = `column-gap` = `padding-left` of the row root. Track 1 is the
// values come from the same custom property, so the gap from the row's // dot column (sized to the dot diameter), anchored `G` off the screen edge
// left edge to the timestamp text equals the gap between time and dot // so the rail reads as a margin-rail down the left side. Track 2 (`1fr`) is
// equals the gap between dot and bubble — symmetric by construction, no // the content column: the header line (`nick` then a muted `time`) followed
// per-side magic constants. // by the bubble / plain text. The timestamp is no longer its own grid track
// // — it rides the header line to the right of the nick, so there's no
// Track 1 is `auto`, so the browser sizes it to the rendered timestamp // locale-width auto-track to keep in sync any more.
// width — no JS measurement, no locale branching. 24h "16:58" auto-sizes
// to ~33 px and the whole row block contracts left; AM/PM "11:30 PM"
// auto-sizes to ~52 px and the block expands right. Both keep the same
// `G` gaps; the dot and bubble shift together as one unit.
// //
// Two gap dimensions, set per-breakpoint via the `compact` recipe // Two gap dimensions, set per-breakpoint via the `compact` recipe
// variant on the row roots: // variant on the row roots:
// //
// - StreamRowPadVar → screen→time gap (the row's `padding-left`) // - StreamRowPadVar → screen→dot gap (the row's `padding-left`)
// - StreamRowGapVar → time→dot and dot→bubble gaps (the `column-gap`) // - StreamRowGapVar → dot→content gap (the `column-gap`)
// //
// Splitting these lets the inter-element gaps tighten on desktop and // Splitting these lets the dot→content gap tighten on desktop and loosen
// loosen on mobile without disturbing the screen-edge anchor (which the // on mobile without disturbing the screen-edge anchor.
// user dialled in earlier and asked to keep).
// //
// The whole time→dot→nick block was nudged ~1mm (~4px) to the right per the // Mobile: pad = S200 (8px off the screen edge); gap = S200.
// latest request — both pad values stepped up one token. // Desktop: pad = S500 (20px — clears the PageNav rail); gap = S500 / 1.1 ≈
// Mobile: pad = S200 (8px — was S100; +4px ≈ 1mm off the screen edge); gap = // 18.2 px (desktop dot→content gap shrunk by 1.1× to keep the layout tight
// S200 (the user asked to double the inter-element gap on native). // without dropping a whole token).
// Desktop: pad = S500 (20px — was S400; +4px ≈ 1mm further from the PageNav,
// the column still clears the nav rail); gap = S500 / 1.1 ≈ 18.2 px (the user
// asked to shrink the desktop inter-element gap by 1.1× — keeps the layout
// tighter without dropping a whole token).
const StreamRowPadVar = createVar(); const StreamRowPadVar = createVar();
const StreamRowGapVar = createVar(); const StreamRowGapVar = createVar();
const StreamRowPadMobile = config.space.S200; const StreamRowPadMobile = config.space.S200;
@ -218,10 +210,10 @@ export const StreamRoot = recipe({
base: { base: {
position: 'relative', position: 'relative',
display: 'grid', display: 'grid',
// Three tracks: time (auto = formatted-timestamp width) | dot column // Two tracks: dot column (auto = dot diameter) | content (1fr). The dot
// (auto = dot diameter) | bubble (1fr). The grid auto-sizes track 1 // column leads so the rail + dot hug the left screen edge; the nick and
// to the rendered timestamp width — locale-agnostic, no JS measure. // timestamp ride the header line inside the content column.
gridTemplateColumns: 'auto auto 1fr', gridTemplateColumns: 'auto 1fr',
columnGap: StreamRowGapVar, columnGap: StreamRowGapVar,
paddingLeft: StreamRowPadVar, paddingLeft: StreamRowPadVar,
alignItems: 'flex-start', alignItems: 'flex-start',
@ -269,28 +261,7 @@ export const StreamRoot = recipe({
defaultVariants: { compact: false, collapsed: false }, defaultVariants: { compact: false, collapsed: false },
}); });
// Sysline timestamp. Now a regular grid item in track 1 — sized to its // DotColumn — grid track 1. Always StreamDotSize wide regardless of row
// content width like the message-row timestamp, just inheriting the
// row's `align-items: center` so the single-line sysline reads on a
// shared centreline with the rest of the row.
export const StreamTimeColumn = style({
fontSize: toRem(11),
fontVariantNumeric: 'tabular-nums',
color: color.Surface.OnContainer,
opacity: 0.55,
whiteSpace: 'nowrap',
fontFamily:
'"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
});
globalStyle(`${StreamTimeColumn} time`, {
display: 'block',
fontFamily: 'inherit',
fontSize: toRem(11),
lineHeight: StreamTimeLineHeight,
});
// DotColumn — grid track 2. Always StreamDotSize wide regardless of row
// type so message / sysline / day-divider rows all hit the same vertical // type so message / sysline / day-divider rows all hit the same vertical
// X for the rail line — otherwise the rail would visibly kink at row // X for the rail line — otherwise the rail would visibly kink at row
// boundaries with different dot sizes. Stretches to row content height // boundaries with different dot sizes. Stretches to row content height
@ -411,12 +382,12 @@ export const StreamDotFill = style({
transition: 'opacity 220ms ease, filter 220ms ease', transition: 'opacity 220ms ease, filter 220ms ease',
}); });
// Wrapper for grid track 3 — holds the bubble and the reactions row stacked. // Wrapper for grid track 2 — holds the header line (nick + time), the bubble
// `min-width: 0` lets fit-content bubbles shrink below their content's natural // and the reactions row stacked. `min-width: 0` lets fit-content bubbles shrink
// size when the grid track is narrower than the bubble (otherwise they'd // below their content's natural size when the grid track is narrower than the
// overflow). // bubble (otherwise they'd overflow).
export const StreamColumn = style({ export const StreamColumn = style({
gridColumn: 3, gridColumn: 2,
minWidth: 0, minWidth: 0,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -537,11 +508,13 @@ export const StreamBubble = recipe({
}, },
}); });
// Author name — sits ABOVE the bubble as the first child of StreamColumn // Header line — sits ABOVE the bubble as the first child of StreamColumn
// (track 3), aligned on the dot/timestamp baseline. Bold, a step larger than // (track 2), aligned on the dot baseline. Holds the bold author nick followed
// chat body, pure white (dark) / black (light) via `--vojo-stream-name`. The // by a muted timestamp (rail+dot → nick → time, per the latest request). Bold,
// inner Username button inherits colour/size/weight from here, so callers // a step larger than chat body; the nick colour comes from the own/peer
// pass the name without their own colour/size. // modifier classes below (own = white/black, peer = lavender). The inner Username
// button inherits colour/size/weight from here; the timestamp carries its own
// explicit muted colour so it does NOT pick up the nick tint.
export const StreamName = style({ export const StreamName = style({
position: 'relative', position: 'relative',
// Symmetric top-left corner (user request): the vertical gap from the nick // Symmetric top-left corner (user request): the vertical gap from the nick
@ -554,34 +527,48 @@ export const StreamName = style({
lineHeight: StreamNameLineHeight, lineHeight: StreamNameLineHeight,
minHeight: StreamNameLineHeight, minHeight: StreamNameLineHeight,
fontWeight: 700, fontWeight: 700,
color: 'var(--vojo-stream-name)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: toRem(6), gap: toRem(8),
flexWrap: 'nowrap', flexWrap: 'nowrap',
minWidth: 0, minWidth: 0,
// Bound the label to the message column so a long display name truncates // Bound the label to the message column so a long display name truncates
// (ellipsis) instead of overflowing the rail. StreamColumn uses // (ellipsis) instead of overflowing the row. StreamColumn uses
// `align-items: flex-start`, so without this the flex container would // `align-items: flex-start`, so without this the flex container would
// content-size past the column edge. // content-size past the column edge.
maxWidth: '100%', maxWidth: '100%',
}); });
// Let the (single) name child shrink so css.Username's ellipsis engages on // Own / peer nick colour. Applied alongside StreamName so the inner Username
// long display names — replaces the old `<Text truncate>` wrapper. Must be a // inherits the tint. Own = the original neutral white (dark) / black (light);
// globalStyle: vanilla-extract `style()` selectors may only target `&`. // peer (the vojo side) = lavender, the Dawn brand accent. Both bind to CSS
// vars (see index.css) so they stay tunable.
export const StreamNameOwn = style({
color: 'var(--vojo-stream-name-own)',
});
export const StreamNamePeer = style({
color: 'var(--vojo-stream-name-peer)',
});
// Let the nick child shrink so css.Username's ellipsis engages on long display
// names — replaces the old `<Text truncate>` wrapper. Must be a globalStyle:
// vanilla-extract `style()` selectors may only target `&`. The timestamp child
// opts out of shrinking via `flex-shrink: 0` in StreamHeaderTime below.
globalStyle(`${StreamName} > *`, { globalStyle(`${StreamName} > *`, {
minWidth: 0, minWidth: 0,
}); });
// Message-row timestamp — grid item in track 1, content-sized. Pushed // Header timestamp — inline on the header line, to the RIGHT of the nick
// down by the same Y as StreamHeaderDotHalo so timestamp / dot / Username // (rail+dot → nick → time). Centred against the taller nick by the header
// share one baseline. // row's `align-items: center`, so no manual baseline offset is needed. Carries
// its own explicit muted colour so the nick's green/lavender tint does not
// bleed onto it, and `flex-shrink: 0` so the nick truncates first.
export const StreamHeaderTime = style({ export const StreamHeaderTime = style({
paddingTop: `calc(${StreamHeaderInnerCenterY} - (${StreamTimeLineHeight} / 2))`, flexShrink: 0,
fontSize: toRem(11), fontSize: toRem(11),
lineHeight: StreamTimeLineHeight, lineHeight: StreamTimeLineHeight,
fontVariantNumeric: 'tabular-nums', fontVariantNumeric: 'tabular-nums',
fontWeight: 400,
color: color.Surface.OnContainer, color: color.Surface.OnContainer,
opacity: 0.55, opacity: 0.55,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -596,17 +583,10 @@ globalStyle(`${StreamHeaderTime} time`, {
lineHeight: StreamTimeLineHeight, lineHeight: StreamTimeLineHeight,
}); });
// Collapsed continuation rows keep the timestamp in the DOM so the auto-sized
// time track stays the same width (body column doesn't shift) but render it
// invisible — a same-sender run shows its dot+timestamp only on the first row.
export const StreamHeaderTimeHidden = style({
visibility: 'hidden',
});
// Sysline — thin single-line state-event row inside Stream layout. // Sysline — thin single-line state-event row inside Stream layout.
// Composes with StreamRoot so the time / dot / content tracks line up // Composes with StreamRoot so the dot / content tracks line up vertically
// vertically with message rows above and below. Override align-items to // with message rows above and below (dot column | body). Override align-items
// center because the sysline content is one line tall and reads best // to center because the sysline content is one line tall and reads best
// vertically centred. // vertically centred.
export const StreamSysline = style({ export const StreamSysline = style({
alignItems: 'center', alignItems: 'center',
@ -629,16 +609,38 @@ export const StreamSyslineBody = style({
minWidth: 0, minWidth: 0,
}); });
// Day divider row — uses the SAME 3-track grid as message rows so its // Sysline timestamp — trailing the body on the same line (content → time),
// dot and rail land on the exact same X as the dots/rails above and below // mirroring the message header's nick → time order now that the dot leads the
// it (otherwise the rail would visibly kink at the day boundary). Track 1 // row. Muted mono, never shrinks, so the italic body truncates first.
// holds an invisible `<Time>` placeholder rendered by Stream.tsx so the export const StreamSyslineTime = style({
// `auto` track sizes to the same width as the surrounding message rows. flexShrink: 0,
fontSize: toRem(11),
lineHeight: StreamTimeLineHeight,
fontVariantNumeric: 'tabular-nums',
color: color.Surface.OnContainer,
opacity: 0.45,
whiteSpace: 'nowrap',
fontFamily:
'"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
});
globalStyle(`${StreamSyslineTime} time`, {
display: 'block',
fontFamily: 'inherit',
fontSize: toRem(11),
lineHeight: StreamTimeLineHeight,
});
// Day divider row — uses the SAME 2-track grid as message rows (dot column |
// content) so its dot and rail land on the exact same X as the dots/rails above
// and below it (otherwise the rail would visibly kink at the day boundary).
// Track 1 is the dot column (fixed dot width, anchored by the same paddingLeft),
// so the dots align by construction — no invisible time placeholder needed.
export const StreamDayRoot = recipe({ export const StreamDayRoot = recipe({
base: { base: {
position: 'relative', position: 'relative',
display: 'grid', display: 'grid',
gridTemplateColumns: 'auto auto 1fr', gridTemplateColumns: 'auto 1fr',
columnGap: StreamRowGapVar, columnGap: StreamRowGapVar,
paddingLeft: StreamRowPadVar, paddingLeft: StreamRowPadVar,
alignItems: 'center', alignItems: 'center',
@ -668,17 +670,6 @@ export const StreamDayRoot = recipe({
defaultVariants: { compact: false }, defaultVariants: { compact: false },
}); });
// Off-screen placeholder timestamp inside the day divider's track 1 —
// invisible, but takes the same auto-width as the live timestamps in
// surrounding message rows so the grid columns align. Override the
// header time's paddingTop because the placeholder needs no header-line
// alignment (the day divider is already vertically centred on the row).
export const StreamDayTimePlaceholder = style({
visibility: 'hidden',
pointerEvents: 'none',
paddingTop: 0,
});
export const StreamDayLabel = style({ export const StreamDayLabel = style({
fontSize: toRem(11), fontSize: toRem(11),
textTransform: 'uppercase', textTransform: 'uppercase',
@ -710,7 +701,7 @@ export const StreamDayDot = style({
zIndex: 2, zIndex: 2,
}); });
// `─── Сегодня ───` line — grid item in track 3. Flex row with two flex:1 // `─── Сегодня ───` line — grid item in track 2. Flex row with two flex:1
// hairlines and the label centered between them. The dot's overflow into // hairlines and the label centered between them. The dot's overflow into
// the columnGap means the leftmost line segment naturally starts past // the columnGap means the leftmost line segment naturally starts past
// the dot's right edge. // the dot's right edge.

View file

@ -34,14 +34,17 @@
/* DM 1-1 «VS Code chat» redesign tokens (see /* DM 1-1 «VS Code chat» redesign tokens (see
docs/plans/dm_stream_vscode_redesign.md). Light-theme defaults here; docs/plans/dm_stream_vscode_redesign.md). Light-theme defaults here;
dark overrides in `.dark-theme`. dark overrides in `.dark-theme`.
* stream-name author label colour (pure black light / pure white * stream-name-own own author label colour (pure black light /
dark, per spec). white dark the original neutral nick colour).
* stream-name-peer peer (vojo) author label colour (lavender, the
Dawn brand accent). Both per-side, see StreamName.
* dot-neutral gray rail dot for unread / in-flight (green = my-read, * dot-neutral gray rail dot for unread / in-flight (green = my-read,
gold = mention, red = my-failed are folds tokens in gold = mention, red = my-failed are folds tokens in
useDotColor). useDotColor).
NB: the incoming-bubble fill is NOT a var `StreamBubble` binds it to NB: the incoming-bubble fill is NOT a var `StreamBubble` binds it to
`color.Surface.Container` so it always matches the composer card. */ `color.Surface.Container` so it always matches the composer card. */
--vojo-stream-name: #000000; --vojo-stream-name-own: #000000;
--vojo-stream-name-peer: #5b6aff;
--vojo-dot-neutral: #9aa0aa; --vojo-dot-neutral: #9aa0aa;
--font-emoji: 'Twemoji_DISABLED'; --font-emoji: 'Twemoji_DISABLED';
@ -68,8 +71,11 @@
--vojo-timeline-rail: #2a2e38; --vojo-timeline-rail: #2a2e38;
/* DM 1-1 «VS Code chat» redesign dark palette. (Incoming-bubble fill /* DM 1-1 «VS Code chat» redesign dark palette. (Incoming-bubble fill
binds to color.Surface.Container in StreamBubble, not a var here.) */ binds to color.Surface.Container in StreamBubble, not a var here.)
--vojo-stream-name: #ffffff; Own nick = white (original neutral), peer (vojo) nick = the #9580ff
Dawn brand lavender. */
--vojo-stream-name-own: #ffffff;
--vojo-stream-name-peer: #9580ff;
--vojo-dot-neutral: #6b7280; --vojo-dot-neutral: #6b7280;
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif; --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;