316 lines
9.9 KiB
CSS
316 lines
9.9 KiB
CSS
/* Conversation column skeleton: header (breadcrumb row only for subagents not fork + tabs) over the view
|
||
area, composer InputBar at the bottom. Column width/squeeze is layout's;
|
||
this fills its cell. Figma: Header 39:27730 (83px two-row), tabs 13px with
|
||
a 3px active bar. */
|
||
|
||
.root {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-width: 0;
|
||
background: var(--dsw-alias-bg-base);
|
||
|
||
/* Shared width axis for the whole column: one content width W
|
||
(--dsh-chat-content-width) for the transcript, the dock cards
|
||
(todo/goal/queue: card minus four insets, 4 x 8 = 32), and the takeover
|
||
cards (question/approval/plan review); the input card alone is W + 32px.
|
||
The relation also holds when a narrow viewport shrinks everything: the
|
||
chat scroller and the takeover frames pad clearance + 16px per side while
|
||
the input card clears the bare clearance, so the input card stays exactly
|
||
content + 32px at every width. Declared on the root because the
|
||
transcript and the composer seat are sibling subtrees. */
|
||
--dsh-chat-content-width: 748px;
|
||
--dsh-composer-card-max-width: calc(var(--dsh-chat-content-width) + 32px);
|
||
--dsh-composer-side-clearance: 16px;
|
||
--dsh-composer-dock-inset: 8px;
|
||
}
|
||
|
||
.header {
|
||
flex: none;
|
||
padding: 12px 28px 0 20px;
|
||
border-bottom: 1px solid var(--dsw-alias-border-l2);
|
||
}
|
||
|
||
/* Blank hero/settling: keep the strict Session header mounted without taking
|
||
column space; the root-owned scrollport and composer remain below it. */
|
||
.headerHidden {
|
||
display: none;
|
||
}
|
||
|
||
.titleRow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-height: 32px;
|
||
}
|
||
.crumbs {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.crumbSeg {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.crumbSep {
|
||
/* figma: "/" separators are 14px caption gray (75:7903), one tint lighter than crumb text. */
|
||
color: var(--dsw-alias-label-caption);
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
}
|
||
|
||
.crumb {
|
||
max-width: 220px;
|
||
overflow: hidden;
|
||
padding: 4px 8px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
background: transparent;
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
color: var(--dsw-alias-label-tertiary);
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.crumb:hover:not(:disabled) {
|
||
background: var(--dsw-alias-interactive-bg-hover);
|
||
}
|
||
|
||
.crumbCurrent {
|
||
font-weight: 500;
|
||
color: var(--dsw-alias-label-primary);
|
||
cursor: default;
|
||
}
|
||
|
||
.headerActions {
|
||
display: flex;
|
||
flex: none;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* figma Tab_Group 34:11441: 35px strip, gap 36, pad-left 8, tabs bottom-aligned. */
|
||
.tabs {
|
||
display: flex;
|
||
gap: 36px;
|
||
margin-top: 4px;
|
||
padding-left: 8px;
|
||
}
|
||
|
||
/* figma .Tab 34:11442: 13/16 text (figma wt510, rendered 500), gap 8 to the 3px bar (no bottom rounding). */
|
||
.tab {
|
||
position: relative;
|
||
padding: 0 0 11px;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 13px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: var(--dsw-alias-label-tertiary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.tab::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
height: 3px;
|
||
background: transparent;
|
||
}
|
||
|
||
/* Selected tab is blue, not ink: brand-primary resolves to neutral black in
|
||
this token sheet, so the selected state rides the business blue — the
|
||
nearest semantic token that stays blue in both themes. */
|
||
.tabActive {
|
||
color: var(--dsw-alias-state-business-primary);
|
||
}
|
||
|
||
.tabActive::after {
|
||
background: var(--dsw-alias-state-business-primary);
|
||
}
|
||
|
||
.viewArea {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* Composer context stack (Figma 9:937): standalone dock cards share one
|
||
rhythm above the input card. */
|
||
.composerStack {
|
||
/* Horizontal geometry (card width, clearance, dock inset) rides the shared
|
||
.root variables above so takeover siblings match the stack. */
|
||
--dsh-composer-stack-gap: 6px;
|
||
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--dsh-composer-stack-gap);
|
||
}
|
||
|
||
/* Common seat for the composer chain (fallback + elected overlay siblings). */
|
||
.composerSeat {
|
||
display: flex;
|
||
flex: none;
|
||
flex-direction: column;
|
||
/* One cap for every scrolling text region a composer seat can hold: the
|
||
InputBar draft (figma Input 75:8208 max 14 lines × 24px line) and the
|
||
takeover panels' bodies top out at the same height, so electing a
|
||
takeover never grows the footer past the card it replaces. Declared on
|
||
the seat because it is the chain's only shared ancestor — fallback and
|
||
elected overlay are siblings — and custom properties inherit down to
|
||
whichever entry is mounted. */
|
||
--dsh-composer-text-max-height: 336px;
|
||
}
|
||
|
||
/* Active phase: header is ordinary column chrome above the scrollport (not
|
||
sticky). The scroll body holds the transcript and the sticky composer seat
|
||
so wheel over the footer moves the flow. */
|
||
.root[data-phase='active'] {
|
||
overflow: hidden;
|
||
}
|
||
|
||
.root[data-phase='active'] .header {
|
||
flex: none;
|
||
}
|
||
|
||
.scrollBody {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
/* The column scrolls on ONE axis. Stating `hidden` rather than leaving the
|
||
initial `visible` is what removes the horizontal bar: a box that scrolls in
|
||
one axis computes `visible` to `auto` in the other, so any bleed becomes
|
||
user-scrollable. `.heroGlow` bleeds by construction (1051/776 of the hero
|
||
box), which put a horizontal scrollbar under every center column narrower
|
||
than the glow. Clipping is unchanged — `overflow-y: auto` already made this
|
||
a scroll container that clips both axes, so this only takes away the bar. */
|
||
overflow-x: hidden;
|
||
/* Reserved unconditionally: the composer seat rides this box's content box in
|
||
Chat and its padding box under a view's composer overlay, so an `auto`
|
||
gutter moves the input card sideways by the bar's width whenever the two
|
||
differ ([decision](../../../../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */
|
||
scrollbar-gutter: stable;
|
||
}
|
||
|
||
.root[data-phase='active'] .viewArea {
|
||
flex: 1 0 auto;
|
||
min-height: auto;
|
||
}
|
||
|
||
.root[data-phase='active'] .composerSeat {
|
||
position: sticky;
|
||
bottom: 0;
|
||
/* Above markdown CodeBlock sticky banners (z-index 6) so the footer never
|
||
paints under a sticking code header while scrolling. */
|
||
z-index: 7;
|
||
/* Input mask (figma 1205:27463): transcript fades out under a FIXED 36px
|
||
band at the seat's top (the figma 24% of the resting ~150px composer),
|
||
solid below — px stops, not %, so a growing draft only widens the solid
|
||
region and the fade band never stretches. The 0px stop is bg-base at
|
||
zero alpha (not white, which the figma export hardcodes) so both themes
|
||
fade from their own base. */
|
||
background: linear-gradient(
|
||
180deg,
|
||
color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent) 0px,
|
||
var(--dsw-alias-bg-base) 36px
|
||
);
|
||
}
|
||
|
||
/* Views may opt into a composer overlay while ConversationRoot retains
|
||
ownership of the seat geometry and its active-phase precedence. */
|
||
.scrollBody:has([data-conversation-composer-overlay]) {
|
||
position: relative;
|
||
/* A clipping box nothing scrolls out of, stated as a scroll container on both
|
||
axes rather than `overflow: hidden`: WebKit honours the reservation above
|
||
only in the `overflow-y: auto` form, and a single-axis scroller computes
|
||
the other axis to `auto`
|
||
([decision](../../../../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.scrollBody:has([data-conversation-composer-overlay]) > .viewArea {
|
||
flex: 1 1 0;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.scrollBody:has([data-conversation-composer-overlay]) > .composerSeat {
|
||
position: absolute;
|
||
right: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
}
|
||
|
||
/* Hero phase: the composer stack (hero chrome + workspace row + card) is
|
||
flex-centered in the column; composer phase docks it at the bottom. Flex,
|
||
NOT absolute+transform: a transform would make this box the containing
|
||
block for position:fixed descendants (pickers/modals), shrinking them. */
|
||
.composerHero {
|
||
position: relative; /* .heroGlow positioning context */
|
||
align-self: center;
|
||
/* figma 75:8208 drew 12 between all three rows; the workspace row now sits
|
||
8 above the card (its margin-top restores 12 under the hero chrome). */
|
||
gap: 8px;
|
||
/* Foot inside the centered box floats the stack a bit above true center. */
|
||
padding-bottom: 32px;
|
||
/* Card cap + both clearances: the hero input card lands at exactly the same
|
||
width as the docked composer at every viewport. */
|
||
width: min(calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance)), 100%);
|
||
z-index: 1;
|
||
}
|
||
|
||
/* Blue backdrop ellipse (figma 313:14109), centered on the input card: the
|
||
card's resting center sits ~92px above the stack bottom (32 foot pad +
|
||
half of the ~120px two-row card); width tracks the card (glow asset 1051
|
||
vs design card 776) so blur scales in userSpace with it. z-index -1 keeps
|
||
it behind the in-flow hero content inside this stacking context. */
|
||
.heroGlow {
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 92px;
|
||
z-index: -1;
|
||
width: calc(100% * 1051 / 776);
|
||
aspect-ratio: 1051 / 468;
|
||
transform: translate(-50%, 50%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.heroWorkspaceRow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
margin-top: 4px;
|
||
/* figma drew px 8; nudged +12 so the chip's folder glyph lines up closer to
|
||
the card's inner controls below. */
|
||
padding-left: 20px;
|
||
}
|
||
|
||
/* Hero: the composer sits inside the session scroll body; center there so
|
||
the tree seat matches active (sticky footer) without a Root remount. */
|
||
.root[data-phase='hero'] .scrollBody {
|
||
justify-content: center;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* Settling (session replaying, hero/docked unknown): keep the composer seat
|
||
mounted but invisible so no wrong layout flashes before the phase lands. */
|
||
.root[data-phase='settling'] .composerSeat {
|
||
visibility: hidden;
|
||
}
|