refactor: archive GUI clients; keep minimal TUI with hooks/API
Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/* Context-occupancy control below the composer plus its click-open breakdown
|
||||
panel (menu surface: elevation-prominent). */
|
||||
|
||||
.root {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: none;
|
||||
padding: 1px 8px;
|
||||
border: none;
|
||||
border-radius: var(--dsw-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-family: inherit;
|
||||
font-size: var(--dsh-content-font-size-secondary, 13px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.trigger[aria-expanded='true'] {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.trigger:hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.track {
|
||||
fill: none;
|
||||
stroke: var(--dsw-alias-border-l3);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.fill {
|
||||
fill: none;
|
||||
stroke: var(--dsw-alias-label-tertiary);
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: fixed;
|
||||
z-index: 1100;
|
||||
box-sizing: border-box;
|
||||
width: min(264px, calc(100vw - 24px));
|
||||
padding: 12px;
|
||||
border: 0;
|
||||
border-radius: var(--dsw-radius-lg);
|
||||
background: var(--dsw-specific-menu);
|
||||
backdrop-filter: var(--dsw-menu-backdrop-filter);
|
||||
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
||||
box-shadow: var(--dsw-elevation-prominent);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.figures {
|
||||
margin-left: auto;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.percent {
|
||||
font-weight: 500;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.headline {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
/* The headline brackets the reading, so the side a locale leaves empty must
|
||||
drop out of the flex row rather than spend a gap. */
|
||||
.headline:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
margin: 10px 0 12px;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
corner-shape: round;
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.segment {
|
||||
flex: none;
|
||||
min-width: 2px;
|
||||
height: 100%;
|
||||
border-radius: 1px;
|
||||
background: var(--meter-tint, var(--dsw-alias-label-tertiary));
|
||||
}
|
||||
|
||||
.swatch {
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
background: var(--meter-tint);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.colorSystem {
|
||||
--meter-tint: var(--dsw-static-neutral-bluish-400);
|
||||
}
|
||||
|
||||
.colorTools {
|
||||
/* The design platform ships no purple static token; violet-400 literal. */
|
||||
--meter-tint: rgb(167, 139, 250);
|
||||
}
|
||||
|
||||
.colorMessages {
|
||||
--meter-tint: var(--dsw-static-blue-450);
|
||||
}
|
||||
|
||||
.rows {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.row dt {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.row dd {
|
||||
margin: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
.root {
|
||||
position: relative; /* positioning context for slot-owned absolute chrome */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
background: var(--dsw-alias-bg-base);
|
||||
}
|
||||
|
||||
/* 76px with its rule: the height of the Sidebar's tab strip and header row
|
||||
(ui-dockkit `.tabStrip` 38px + the pane header's 38px), so the two rules
|
||||
meet at the column edge. The rows add up to it — 10px top inset, the 30px
|
||||
title row, the tab strip's 10px margin, and a 25px view row. When the views
|
||||
and the More launcher do not fit on one row, the row wraps and the header
|
||||
grows past 76px. */
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
flex: none;
|
||||
box-sizing: border-box;
|
||||
min-height: 76px;
|
||||
padding: 10px 28px 0 20px;
|
||||
border-bottom: 0.5px solid var(--dsw-alias-border-l3);
|
||||
}
|
||||
|
||||
.header:where(:not(:has(.tabs))) {
|
||||
min-height: 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Blank Sessions retain navigation controls without conversation title or tabs. */
|
||||
.headerBlank {
|
||||
min-height: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.headerBlank .headerCorner {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Only the macOS titlebar needs reserved drag space before a Session exists. */
|
||||
:global(html:not([data-platform='darwin'])) .headerSessionless {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:global(html:not([data-platform='darwin'])) .headerSessionless .titleRow {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* macOS hiddenInset titlebar: the header block owns the window drag over its
|
||||
own box — title row plus the view tab strip, so the draggable geometry is the
|
||||
header's geometry and no frame band has to match its height. The header marks
|
||||
itself `data-window-drag` (ConversationHeader); below it the title-row clusters
|
||||
subtract their whole boxes, and individual controls are covered by the base.css
|
||||
interactive-element rule. A covering overlay lives beside #root
|
||||
(ui-settings-general), which puts it after this row in document order. */
|
||||
:global([data-platform='darwin']) .headerLeading,
|
||||
:global([data-platform='darwin']) .headerActions,
|
||||
:global([data-platform='darwin']) .headerUtilities,
|
||||
:global([data-platform='darwin']) .headerCorner {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
grid-column: 2;
|
||||
/* Anonymous inline-size container (CSS modules hash container-names per
|
||||
module, so occupants match the nearest container): header occupants such
|
||||
as the preset label and slot actions shed their text at row-width
|
||||
breakpoints. The minmax(0, 1fr) column sizes the row, so inline-size
|
||||
containment cannot collapse it. */
|
||||
container-type: inline-size;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-height: 30px;
|
||||
/* Clears the frame's window-chrome band (traffic lights + shell.leading
|
||||
controls, ui-layout AppFrame) when it is published; the header already
|
||||
pads 20px of it. Zero everywhere else (the fallback makes the max
|
||||
negative). */
|
||||
padding-inline-start: max(0px, calc(var(--dsh-frame-leading-clearance, 0px) - 20px));
|
||||
}
|
||||
|
||||
/* Header actions do not shrink, so on a narrow row they overflow the cluster.
|
||||
The cluster clips them, hit testing included, so the next control keeps
|
||||
every click. The trailing padding spans the gap before that control and
|
||||
holds the fade: content that fits ends before it, and only overflowing
|
||||
content reaches it. The 4px block and start padding, offset by negative
|
||||
margins, keeps focus rings inside the clip. `clip` rather than `hidden`
|
||||
keeps the cluster from becoming a scroll container that focus could scroll. */
|
||||
.titleCluster {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
--title-cluster-fade: 20px;
|
||||
margin: -4px calc(-1 * var(--title-cluster-fade)) -4px -4px;
|
||||
padding: 4px var(--title-cluster-fade) 4px 4px;
|
||||
overflow: clip;
|
||||
mask-image: linear-gradient(to left, transparent calc(0.3 * var(--title-cluster-fade)), #000 var(--title-cluster-fade));
|
||||
}
|
||||
|
||||
/* Without utilities the corner's 8px margin is the gap the fade spans. */
|
||||
.titleCluster:has(+ .headerUtilities:empty) {
|
||||
--title-cluster-fade: 8px;
|
||||
}
|
||||
|
||||
/* The auto grid column collapses with an empty or CSS-hidden occupant.
|
||||
The occupant owns its trailing gap so hidden controls reserve no space. */
|
||||
.headerLeading {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: inline-block;
|
||||
max-width: 220px;
|
||||
overflow: hidden;
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
border-radius: var(--dsw-radius-md);
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.crumbSubagent {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
button.crumb:hover {
|
||||
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;
|
||||
}
|
||||
|
||||
.headerUtilities {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.headerUtilities:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The far-right corner seat reaches 16px into the header's 28px right padding,
|
||||
so its control sits past the utilities' edge; it is laid out only while its
|
||||
occupant renders something. Its left margin equals the utilities' 8px gap so
|
||||
the header's trailing controls space evenly. */
|
||||
.headerCorner {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
margin-right: -16px;
|
||||
}
|
||||
|
||||
.headerCorner:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The tab row sits above the header paint so the active bar covers its rule. */
|
||||
.tabs {
|
||||
grid-column: 1 / -1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 16px 20px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
:global(html:not([data-platform])) .tabs {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.viewTabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 32px;
|
||||
min-width: 0;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
/* The 16px text line retains 9px below it. The press target extends 10px
|
||||
above the text into the strip's top margin and 10px to each side, matching
|
||||
the inline padding of the Sidebar tab chips (ui-dockkit `.tab`); negative
|
||||
margins keep the layout box at its 25px row and text width. The active bar
|
||||
spans the text and reaches 1px past the tab box to sit over the header
|
||||
rule. */
|
||||
.tab {
|
||||
--dsh-tab-hit-inline: 10px;
|
||||
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
min-height: 35px;
|
||||
margin: -10px calc(-1 * var(--dsh-tab-hit-inline)) 0;
|
||||
padding: 10px var(--dsh-tab-hit-inline) 9px;
|
||||
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: var(--dsh-tab-hit-inline);
|
||||
bottom: -1px;
|
||||
left: var(--dsh-tab-hit-inline);
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
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);
|
||||
}
|
||||
|
||||
/* The More launcher centers its 22px box on the tab's 16px text line. */
|
||||
.viewsMoreAnchor {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.viewsMore {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-top: -3px;
|
||||
padding: 0;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.viewsMoreActive { color: var(--dsw-alias-state-business-primary); }
|
||||
|
||||
.tabLabel { display: block; max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.viewsMore[aria-expanded='true'] {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.viewsMoreMenu {
|
||||
width: 288px;
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
|
||||
.viewArea {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Width handles: 10px col-resize strips beside the transcript, absolute in
|
||||
.body (NOT the scrollport: an absolute strip there would extend the
|
||||
scrollable range; NOT .root: a strip from the column top would paint its
|
||||
glow through the header's transparent background). The inner edge sits
|
||||
24px outside the content column and the strip extends 10px outward; the
|
||||
outer edge is clamped to keep a 24px safe zone against the
|
||||
column edges (sidebar side and scrollbar side stay drag-free). When the
|
||||
margin cannot fit inset + strip + safe zone the computed width goes
|
||||
negative and the strip resolves to zero — no mispositioned hit area on
|
||||
narrow columns. Known half-scrollbar (~4px) centering offset vs the
|
||||
gutter-reserved content box is accepted: it consumes part of the 10px
|
||||
strip but remains within it. */
|
||||
.widthHandle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
/* Concrete overflow content establishes z-index 1, while the sticky
|
||||
composer remains above both layers. */
|
||||
z-index: 0;
|
||||
width: min(
|
||||
10px,
|
||||
calc((100% - var(--dsh-chat-content-width)) / 2 - 24px - 24px)
|
||||
);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.widthHandle[data-side='left'] {
|
||||
right: calc(50% + var(--dsh-chat-content-width) / 2 + 24px);
|
||||
}
|
||||
|
||||
.widthHandle[data-side='right'] {
|
||||
left: calc(50% + var(--dsh-chat-content-width) / 2 + 24px);
|
||||
}
|
||||
|
||||
/* The glow: a short 2px bar following the captured pointer's Y, centered in the strip
|
||||
(29px from the content column), not a full-height rule — a 16px solid core
|
||||
centered on the pointer
|
||||
(published during drag as --dsh-width-handle-pointer-y), fading to
|
||||
transparent over 28px each side (72px total). A solid CORE, not a single
|
||||
solid stop: a lone peak washes out over the fade. The resting scrollbar
|
||||
tint keeps this secondary affordance quiet in both themes. Before the first
|
||||
drag move the fallback centers the glow at 50% height; hover leaves its position unchanged. */
|
||||
.widthHandle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent calc(var(--dsh-width-handle-pointer-y, 50%) - 36px),
|
||||
var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) - 8px),
|
||||
var(--dsw-alias-scrollbar-bg-l1) calc(var(--dsh-width-handle-pointer-y, 50%) + 8px),
|
||||
transparent calc(var(--dsh-width-handle-pointer-y, 50%) + 36px)
|
||||
);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.widthHandle[data-side='left']::after {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.widthHandle[data-side='right']::after {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.widthHandle[data-dragging]::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.widthHandle:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* A drag starts above the composer band. Pointer capture keeps it active when
|
||||
crossing the footer; lift only the active handle so its indicator remains
|
||||
visible without making the resting footer gutter draggable. */
|
||||
.widthHandle[data-dragging] {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
/* Views that elect a composer overlay (trajectory etc.) own their full-bleed
|
||||
scrollers; the strips would sit over their content and steal clicks. */
|
||||
.root:has([data-conversation-composer-overlay]) .widthHandle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Band below the header: the scrollport plus the width handles. Anchoring the
|
||||
handles here keeps their full-height strips and glow under the header's
|
||||
bottom edge at every header height. */
|
||||
.body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
/* Shared width axis for the transcript, dock cards, composer, and takeover
|
||||
cards. Main width controls publish the live body width and optional user
|
||||
preference; embedded occurrences override this adaptive axis below. */
|
||||
--dsh-chat-content-width: var(
|
||||
--dsh-chat-user-width,
|
||||
clamp(680px, calc(var(--dsh-conversation-column-width, 0px) * 0.64), 920px)
|
||||
);
|
||||
--dsh-composer-card-max-width: calc(var(--dsh-chat-content-width) + 32px);
|
||||
--dsh-composer-side-clearance: 16px;
|
||||
--dsh-composer-dock-inset: 8px;
|
||||
}
|
||||
|
||||
.embeddedBody {
|
||||
overflow: hidden;
|
||||
--dsh-chat-content-width: min(calc(100% - 32px), 920px);
|
||||
--dsh-composer-card-max-width: min(calc(100% - 16px), 952px);
|
||||
--dsh-composer-side-clearance: 8px;
|
||||
--dsh-composer-dock-inset: 8px;
|
||||
}
|
||||
|
||||
.scrollBody {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
/* The product-wide 2px scrollbar offset (ui-workspace WorkspaceBrowser
|
||||
`.list`, ui-sidebar-files FilesBody `.body`): the bar sits 2px clear of
|
||||
the column's edge instead of flush against it. */
|
||||
margin-right: 2px;
|
||||
overflow-y: auto;
|
||||
/* 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/archived/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* The bar also stops 2px short of the scroller's ends, matching its 2px edge
|
||||
offset. WebKit-only: the Firefox path has no track to inset. */
|
||||
.scrollBody::-webkit-scrollbar-track {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.root[data-phase='active'] .viewArea,
|
||||
.embeddedBody[data-content-phase='active'] .viewArea {
|
||||
flex: 1 0 auto;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.root[data-phase='active'] .composerSeat,
|
||||
.embeddedBody[data-content-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. The pinned compaction
|
||||
header (MessageItem.module.css, ui-chat) and the turn-navigation rail slot
|
||||
(TurnNavigator.module.css, ui-chat) also take 7 to clear the same banners.
|
||||
Among elements of equal rank the later one in DOM order wins, so the
|
||||
composer stays above the pinned header on the extreme small-viewport
|
||||
overlap and the pinned header stays above the rail. */
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/* An open @/slash menu (data-trigger-menu, ui-input-trigger) renders inside
|
||||
the input card, so the seat's stacking context caps its z-index; lift the
|
||||
seat above the back-to-bottom control (z-index 8, ui-chat ChatView) while
|
||||
the menu is open. The steady state stays 7 so the control keeps painting
|
||||
over the card. */
|
||||
.root[data-phase='active'] .composerSeat:has([data-trigger-menu]) {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
/* 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`: a single-axis scroller computes the
|
||||
other axis to `auto`, and the seat's absolute positioning below needs the
|
||||
box to stay a containing block in every engine. */
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
/* The view owns its own scrollers (the trajectory ledger scrolls inside the
|
||||
view), so this box never scrolls here; reserving a gutter would only
|
||||
narrow the view's content by the bar's width. Chat keeps the reservation
|
||||
on the same element, so the seat below compensates with the bar's width
|
||||
([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */
|
||||
scrollbar-gutter: auto;
|
||||
}
|
||||
|
||||
.scrollBody:has([data-conversation-composer-overlay]) > :global([data-slot='conversation.session']) > .viewArea {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scrollBody:has([data-conversation-composer-overlay]) > .composerSeat {
|
||||
position: absolute;
|
||||
/* Chat's seat rides this box's content box, which its own scrolling shortens
|
||||
by the bar's width; the overlay seat is against the padding box, so it
|
||||
must give back the same width to keep the input card in place across
|
||||
tabs. Reads ui-theme's --dsh-scrollbar-width, which mirrors the WebKit
|
||||
bar's layout width. Chat reserves the engine-resolved width, so on an
|
||||
engine where the resolved width differs from this fixed 8px the card
|
||||
drifts by half the difference — accepted residual risk, recorded in
|
||||
([decision](../../../../../../.agents/notes/archived/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md)). */
|
||||
right: var(--dsh-scrollbar-width);
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
.heroWorkspaceRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
margin-top: 4px;
|
||||
/* figma drew px 8; left nudged +12 so the chip's folder glyph lines up
|
||||
closer to the card's inner controls below; right keeps the trailing
|
||||
preset seat off the card edge. */
|
||||
padding: 0 16px 0 20px;
|
||||
}
|
||||
|
||||
/* Plugin options share the row, pushed to its right edge. */
|
||||
.heroOptions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.heroOptions:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 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,
|
||||
.embeddedBody[data-content-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,
|
||||
.embeddedBody[data-content-phase='settling'] .composerSeat {
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/* NEW SESSION hero (figma Input_Bottom 75:8208): fish + title, workspace chip
|
||||
above the shared InputBar card. The input itself is InputBar — only stack
|
||||
geometry and the chip live here. */
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* Cap matches the InputBar card. Glow may paint past the sides. */
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
/* figma 75:8208: 12 between title block / workspace / card. */
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: var(--dsh-composer-card-max-width);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* figma 34:10411: fish + title, gap 10, centered; 26/32 wt500. The preview
|
||||
badge is a product addition outside that source: a mono superscript pill
|
||||
riding the title's top-right. */
|
||||
.headline {
|
||||
display: flex;
|
||||
/* Overflow-driven: the fish and the title group are two wrap units sharing
|
||||
one centered line while they fit; squeezed, the fish takes its own line
|
||||
above the group. No breakpoint. */
|
||||
flex-wrap: wrap;
|
||||
column-gap: 10px;
|
||||
/* Wrapped, the fish sits on its own line; more air than the 10px side gap
|
||||
keeps it reading as a mark above the title, not a squeezed inline glyph. */
|
||||
row-gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
/* Title text + badge as one wrap unit so the title never breaks mid-text with
|
||||
the badge stranded beside a fragment. When the badge no longer fits beside
|
||||
the title it wraps to its own centered line below; while both fit, the group
|
||||
is content-sized and centering is a no-op, keeping the superscript snug. */
|
||||
.titleGroup {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* Superscript sits 7 from the title. */
|
||||
column-gap: 7px;
|
||||
row-gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.previewBadge {
|
||||
align-self: flex-start;
|
||||
margin-top: 2px;
|
||||
padding: 1px 7px 0;
|
||||
border: 0.5px solid var(--dsw-alias-interactive-bg-hover);
|
||||
border-radius: 999px;
|
||||
corner-shape: round;
|
||||
background: var(--dsw-alias-state-business-tertiary);
|
||||
color: var(--dsw-alias-label-primary-bluish);
|
||||
font-family: var(--ds-font-family-code);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Keep hover detection on a stationary box while the mark moves within it;
|
||||
hover rules target the hitbox so a slot-occupant mark swims too. */
|
||||
.fishHitbox {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* The swim morph's curves poke slightly past the viewBox edges, so clipping
|
||||
would slice the tail mid-wag. The hero mark stays in the same primary ink
|
||||
as its headline. */
|
||||
.fish {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
transform-origin: 50% 60%;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
/* Hover swim: a slow continuous sway about the body center — the head
|
||||
(left) lifts as the whale rises, then settles back with a small
|
||||
counter-tilt, like treading water. Negative rotation = head up (the head
|
||||
is the left half; origin 50% 60%). */
|
||||
@keyframes hero-fish-swim {
|
||||
0%,
|
||||
100% {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
35% {
|
||||
transform: rotate(-4deg) translate(-0.4px, -0.9px);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: rotate(1.6deg) translate(0.3px, 0.2px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
|
||||
.fishHitbox:hover .fish {
|
||||
animation: hero-fish-swim 1.6s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
/* Workspace row sits 12px above the input card (figma y80 → y112). */
|
||||
.body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.body > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Must beat `.body > :not(.glow)` specificity so the open Menu (and its
|
||||
right-hand submenu) paints above the InputBar card. */
|
||||
.body > .workspaceRow {
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
/* figma 75:8208 workspace row: px 8 above the card. */
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
/* Folder + label + chevron — transparent at rest; fill only on hover / open.
|
||||
Overflow-driven degradation, no breakpoint: the label shrinks with an
|
||||
ellipsis down to nothing, so folder and chevron always sit snug against
|
||||
whatever text remains — full text, ellipsized text, or none. */
|
||||
.workspace {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: min(100%, 360px);
|
||||
min-height: 28px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
border-radius: var(--dsw-radius-sm);
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.workspace[aria-expanded='true'] {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.workspace:not(:disabled):hover {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
/* Locked form (bound guidance state): a static echo — no hover feedback, no
|
||||
pointer affordance; label keeps full contrast. */
|
||||
.workspace:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.folder {
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.workspaceLabel {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
flex: none;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
/* Dialog field: 44 tall on the modal's 332 content column, hairline
|
||||
border, pad 14/7, 14/22 wt400 primary text, caption placeholder. Focus
|
||||
keeps the resting border (design shows no focus ring). */
|
||||
.modalInput {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 7px 14px;
|
||||
border: 0.5px solid var(--dsw-alias-border-l4);
|
||||
border-radius: var(--dsw-radius-lg);
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.modalInput::placeholder {
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
.modalInput:disabled {
|
||||
color: var(--dsw-alias-label-dimmed);
|
||||
}
|
||||
|
||||
.modalAction {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.modalError {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
/* Side pads ride the shared clearance (figma Input_Bottom drew L32/R32/B8;
|
||||
the sides narrow with the shared width axis); the bottom gradient mask
|
||||
is owned by the chat scroller. No top pad: the composer stack's gap owns
|
||||
the space above; the status strip still carries its own margin. */
|
||||
padding: 0 var(--dsh-composer-side-clearance) 4px;
|
||||
}
|
||||
|
||||
.dock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
max-width: 100%;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.hero .dock:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero {
|
||||
/* No bottom pad in the centered hero, but the side clearance must survive:
|
||||
the hero wrapper is full-width on narrow viewports, so this padding is
|
||||
the only thing keeping the card off the edges there. */
|
||||
padding: 0 var(--dsh-composer-side-clearance);
|
||||
}
|
||||
|
||||
.notice {
|
||||
width: 100%;
|
||||
max-width: var(--dsh-composer-card-max-width);
|
||||
margin-bottom: 6px;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--dsw-radius-md);
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-sizing: border-box;
|
||||
position: relative; /* overlay anchor positioning context */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* figma Input 75:8208: 12px between the text area and the button row; the
|
||||
card's top pad before .InputText tightened from the drawn 10 to 8. */
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
max-width: var(--dsh-composer-card-max-width);
|
||||
padding-top: 8px;
|
||||
/* Input stroke: the light neutral (l2, one step above the menus' l1),
|
||||
drawn as the elevation hairline so it costs no layout; the shadow is the
|
||||
soft elevation tier. Fill: the input surface token (elevated in dark). */
|
||||
border: 0;
|
||||
--dsw-elevation-stroke-color: var(--dsw-alias-border-l2);
|
||||
border-radius: var(--dsw-radius-panel);
|
||||
background: var(--dsw-specific-input-major);
|
||||
box-shadow: var(--dsw-elevation-soft);
|
||||
/* The draft text follows the Settings font-size preference (the textarea,
|
||||
mirror, and backdrop all inherit from here, so the three layers stay in
|
||||
step); the line height keeps the default 10px leading via the shared px
|
||||
delta. */
|
||||
font-size: var(--dsh-content-font-size, 14px);
|
||||
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
|
||||
/* Elevated surface in dark, same as the menus: the draft scrollport inside
|
||||
scrolls once the composer hits its height cap, so the thumb takes the l2
|
||||
pair. Declared on the card because the elevation belongs to the surface,
|
||||
and the custom properties inherit down to the box that actually scrolls
|
||||
(see ui-theme styles/scrollbar.css for the rebinding contract). */
|
||||
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
||||
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
.card {
|
||||
font-size: max(16px, var(--dsh-content-font-size, 14px));
|
||||
}
|
||||
}
|
||||
|
||||
/* No-workspace trigger state: an inset 1px dashed outline marks the card as a
|
||||
workspace picker rather than a live composer. Keeping the complete stroke
|
||||
inside the rounded box prevents clipped corner pixels from reading heavier. */
|
||||
.cardWorkspaceTrigger {
|
||||
--dsw-elevation-stroke-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cardWorkspaceTrigger::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed currentColor;
|
||||
border-radius: var(--dsw-radius-panel);
|
||||
color: var(--dsw-alias-border-l4);
|
||||
transition: color 100ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Disabled toolbar controls neither receive nor swallow clicks in the trigger
|
||||
state: pointer events fall through to the card's own click handler, making
|
||||
the whole card one pick target. */
|
||||
.cardWorkspaceTrigger :disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.cardWorkspaceTrigger:hover::after {
|
||||
color: var(--dsw-alias-state-business-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.accessory {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 0;
|
||||
}
|
||||
|
||||
/* Floating overlay anchor (menu / popupSelect shell): entries position
|
||||
themselves against the card (bottom: 100% + gap); closed entries render null. */
|
||||
.overlayAnchor {
|
||||
position: absolute;
|
||||
inset: 0 0 auto;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The draft's scrollport, and the ONLY scrolling box in the composer. The
|
||||
contenteditable grows with its content inside; the 14-line cap lives here
|
||||
because this is the box the cap describes. */
|
||||
.scroll {
|
||||
max-height: var(--dsh-composer-text-max-height);
|
||||
overflow-y: auto;
|
||||
/* Keeps the bar off the card's right border; .input's right pad gives the
|
||||
4px back so the text column is unchanged. */
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* The card's corner arc still overlaps the scrollport's top (8px top
|
||||
pad reaches only y=8): start the thumb's travel below the arc so it never
|
||||
pokes outside the capsule. WebKit-path only; Firefox's thin bar has no
|
||||
track margin, an accepted remainder. */
|
||||
.scroll::-webkit-scrollbar-track {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Auto-grow anchor: the contenteditable is in normal flow and sets the
|
||||
draft's height; the placeholder rides it absolutely. */
|
||||
.grow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Machine pending dot (adjudicating / submitting). */
|
||||
.pending {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
corner-shape: round;
|
||||
background: var(--dsw-alias-state-business-primary);
|
||||
animation: input-pending 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes input-pending {
|
||||
from { opacity: 0.35; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* The contenteditable draft surface (grows with its content; .scroll caps
|
||||
and scrolls it). figma .InputText 34:10434 drew pl 16 / pr 12 / pt 4; the
|
||||
left pad tightened to 14, and the right pad is 8 here plus .scroll's 4px
|
||||
bar inset. */
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
/* Docked floor: one 24px line + 4px top pad = 28 content height; 36 leaves
|
||||
an 8px breath below the line (the hero keeps its own 52px floor below). */
|
||||
min-height: 36px;
|
||||
padding: 4px 8px 0 14px;
|
||||
font-family: var(--dsw-font-family);
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
outline: none;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
/* Business blue, not brand-primary: that token resolves to ink in this sheet. */
|
||||
caret-color: var(--dsw-alias-state-business-primary);
|
||||
}
|
||||
|
||||
/* Lexical paragraphs are <p> blocks: strip the UA margins so the surface
|
||||
keeps the textarea's line rhythm. */
|
||||
.input p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Claim ghost hint as generated content after the last paragraph: the bar
|
||||
sets --dsh-composer-hint (a quoted string) while the claim's args are
|
||||
blank; without the variable the declaration is invalid and nothing shows. */
|
||||
.input p:last-child::after {
|
||||
content: var(--dsh-composer-hint);
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
.input[data-composer-composing] p:last-child::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* A selected command chip stands in for the caret until the next key. */
|
||||
.input[data-composer-chip-selected] {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.input[data-composer-composing] + .placeholder {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* figma 34:10434: #ADB2B8 light / #81858C dark — the caption pair exactly.
|
||||
Insets mirror .input's pads (the other right 4px sits on .scroll). One
|
||||
ellipsized line: a wrapped second line would clip against the docked
|
||||
36px floor. */
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
inset: 4px 8px auto 14px;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.inputDisabled {
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input[aria-haspopup='menu'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Hero (centered empty-state) keeps the 2-line floor (figma min-h 52 = ~2 × 24
|
||||
line + 4pt); the docked composer collapses to the content height. */
|
||||
.hero .input {
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
/* The hero's 52px floor fits two lines, so its placeholder clamps at two
|
||||
instead of the docked single line. */
|
||||
.hero .placeholder {
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
}
|
||||
|
||||
/* Toolbar: attach + Plan + Read-only on the left; model + send on the right
|
||||
(figma Input_Bottom chrome). */
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
/* 2px moved from the bottom pad to the top: the whole control row sits 2px
|
||||
lower in the card (it read too high against the textarea) while the
|
||||
controls' own centering stays untouched. */
|
||||
padding: 2px 8px 6px;
|
||||
min-width: 0;
|
||||
/* Size container so the chips inside can collapse to icon-only when the
|
||||
card runs out of row width (PermissionSelect @container rule). Anonymous
|
||||
on purpose: CSS modules hash container-name per module, so a name declared
|
||||
here can never match a query in another module's sheet. */
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.row[data-model-compact] {
|
||||
--dsh-composer-model-text-display: none;
|
||||
--dsh-composer-model-icon-display: block;
|
||||
}
|
||||
|
||||
.tools,
|
||||
.modes,
|
||||
.trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* figma 75:8208 drew 16 between + and the mode chips and 4 between Plan /
|
||||
Read-only; both gaps settled on 12 so the pill chips read as separate
|
||||
controls without drifting from the + control. */
|
||||
.tools {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modes {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.trailing {
|
||||
flex: none;
|
||||
/* Wrap keeps the left mode chips and the right controls apart when the card
|
||||
runs out of row width: the trailing group (model + send) moves to its own
|
||||
line instead of the left group shrinking until its chip overlaps the
|
||||
model trigger (external:107). The auto margin re-anchors it right on the
|
||||
wrapped line; on a single line space-between already pins it right. */
|
||||
margin-left: auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Compact spacing preserves room for the fixed-width controls in narrow cards. */
|
||||
@container (max-width: 560px) {
|
||||
.tools,
|
||||
.modes,
|
||||
.trailing {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attach circle (figma + control): 28px, selector fill, primary glyph. */
|
||||
.add {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
corner-shape: round;
|
||||
background: var(--dsw-specific-selector);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.add:hover:not(:disabled) {
|
||||
background: var(--dsw-alias-interactive-bg-hover-solid);
|
||||
}
|
||||
}
|
||||
|
||||
.add:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Plan / Read-only / model — native <select>, chip-like closed chrome
|
||||
(figma ToggleButton: 13/20 medium secondary, 12px chevron). */
|
||||
.select {
|
||||
max-width: 220px;
|
||||
height: 28px;
|
||||
padding: 0 20px 0 8px;
|
||||
border: none;
|
||||
border-radius: var(--dsw-radius-sm);
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 4px center;
|
||||
background-size: 12px 12px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.select:hover:not(:disabled) {
|
||||
background-color: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.select:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Primary send (figma IconButton 34:10465): 34px circle, #3964FE light /
|
||||
#679EFE dark — the info-fill pair (500→400), NOT button-primary (ink);
|
||||
white glyph; empty text = 0.4 opacity. */
|
||||
.primary {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
corner-shape: round;
|
||||
background: var(--dsw-alias-button-info-fill);
|
||||
/* Static white, not the foreground token: the arrow stays white on the blue
|
||||
fill in both themes (design 34:10465). */
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background-color 100ms ease;
|
||||
/* Opts out of the row's 2px downward shift (.row top pad): the send circle
|
||||
keeps its original seat while the smaller chips sit lower. Transform, not
|
||||
margin, so flex centering math is untouched. */
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.primary:hover:not(:disabled) {
|
||||
background: var(--dsw-alias-button-info-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Sending: the message waits for the Host to accept it. The circle keeps its
|
||||
full fill rather than the disabled fade, and an arc turns where the arrow was. */
|
||||
.primary[data-sending]:disabled {
|
||||
opacity: 1;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.sendingSpinner {
|
||||
animation: input-sending 800ms linear infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sendingSpinner {
|
||||
animation-duration: 2400ms;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes input-sending {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.retry {
|
||||
margin-left: 8px;
|
||||
padding: 1px 8px;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: var(--dsw-radius-sm);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.standardControls { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
.tools[hidden], .standardControls[hidden] { display: none; }
|
||||
.trailingActive { flex: 1; margin-left: 0; }
|
||||
.activity { display: flex; align-items: center; flex: none; }
|
||||
.activity:empty { display: none; }
|
||||
.activityExpanded { flex: 1; min-width: 0; }
|
||||
@@ -0,0 +1,123 @@
|
||||
.root {
|
||||
box-sizing: border-box;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
width: calc(
|
||||
100% -
|
||||
var(--dsh-composer-side-clearance) -
|
||||
var(--dsh-composer-side-clearance) -
|
||||
var(--dsh-composer-dock-inset) -
|
||||
var(--dsh-composer-dock-inset) -
|
||||
var(--dsh-composer-dock-inset) -
|
||||
var(--dsh-composer-dock-inset)
|
||||
);
|
||||
max-width: calc(
|
||||
var(--dsh-composer-card-max-width) -
|
||||
var(--dsh-composer-dock-inset) -
|
||||
var(--dsh-composer-dock-inset) -
|
||||
var(--dsh-composer-dock-inset) -
|
||||
var(--dsh-composer-dock-inset)
|
||||
);
|
||||
border: 0;
|
||||
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
||||
border-radius: var(--dsw-radius-lg);
|
||||
background: var(--dsw-specific-menu);
|
||||
backdrop-filter: var(--dsw-menu-backdrop-filter);
|
||||
box-shadow: var(--dsw-elevation-panel);
|
||||
/* The menu surface and the scrolling `.list` use the l2 scrollbar colors.
|
||||
Declared here because the custom properties inherit into `.list` (see
|
||||
ui-theme styles/scrollbar.css for the rebinding contract). */
|
||||
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
||||
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lead {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: none;
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.progress {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.glyph {
|
||||
display: grid;
|
||||
flex: none;
|
||||
place-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Figma strip is single-line; long items ellipsize with no inline expand. */
|
||||
.content {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
Reference in New Issue
Block a user