mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
521 lines
17 KiB
CSS
521 lines
17 KiB
CSS
/* One-glyph font: maps ONLY U+FFFC to a blank 4em-advance glyph (every other
|
||
codepoint falls through to the next family). Loaded first in the composer
|
||
font stack, it gives the placeholder a real cell width INSIDE the textarea,
|
||
so the backdrop chip (same char, same stack) matches it by construction —
|
||
the two layers cannot drift and the chip gets a usable label cell. */
|
||
@font-face {
|
||
font-family: 'DshChipCell';
|
||
src: url('data:font/ttf;base64,AAEAAAAKAIAAAwAgT1MvMkT8SmIAAAEoAAAAYGNtYXAADQBPAAABkAAAADRnbHlmAAAAAAAAAcwAAAABaGVhZCwtPGoAAACsAAAANmhoZWEDIg7bAAAA5AAAACRobXR4EZQAAAAAAYgAAAAIbG9jYQAAAAAAAAHEAAAABm1heHAAAwACAAABCAAAACBuYW1lvljk2gAAAdAAAABscG9zdNNweNQAAAI8AAAALQABAAAAAQAAdia1tV8PPPUAAwPoAAAAAOaLfcUAAAAA5ot9xQAAAAAAAAAAAAAAAwACAAAAAAAAAAEAAAMg/zgAAA+gAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAEAAAACAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwjKAZAABQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAPz8/PwAA//z//AMg/zgAAAMgAMgAAAAAAAAAAAAAAAAAAAAgAAAB9AAAD6AAAAAAAAIAAAADAAAAFAADAAEAAAAUAAQAIAAAAAQABAABAAD//P//AAD//P//AAUAAQAAAAAAAAAAAAAAAAAAAAAAAAAEADYAAQAAAAAAAQALAAAAAQAAAAAAAgAHAAsAAwABBAkAAQAWABIAAwABBAkAAgAOAChEc2hDaGlwQ2VsbFJlZ3VsYXIARABzAGgAQwBoAGkAcABDAGUAbABsAFIAZQBnAHUAbABhAHIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAABAgZvYmpyZXAAAAA=') format('truetype');
|
||
}
|
||
|
||
/* Floating capsule input (figma Input_Bottom 75:8208): card floats above the
|
||
viewport bottom inside the centered message column; textarea on top, action
|
||
row below, one primary circle button bottom-right. Input width rides the
|
||
column (--dsh-composer-card-max-width = chat content + 32px, 16px per side,
|
||
is a cap, not a fixed size — layout rule: the box shrinks with the center
|
||
column keeping its clearance). Hero variant = the same card centered in the
|
||
empty state; the transition between the two is a position move of one
|
||
component. */
|
||
|
||
.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; error/status strips still carry their own margin. */
|
||
padding: 0 var(--dsh-composer-side-clearance) 8px;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.error,
|
||
.status {
|
||
width: 100%;
|
||
max-width: var(--dsh-composer-card-max-width);
|
||
margin-bottom: 6px;
|
||
padding: 4px 8px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
}
|
||
|
||
.status {
|
||
background: var(--dsw-alias-interactive-bg-hover);
|
||
color: var(--dsw-alias-label-secondary);
|
||
}
|
||
|
||
.notice {
|
||
width: 100%;
|
||
max-width: var(--dsh-composer-card-max-width);
|
||
margin-bottom: 6px;
|
||
padding: 4px 8px;
|
||
border-radius: 8px;
|
||
background: var(--dsw-alias-interactive-bg-hover);
|
||
color: var(--dsw-alias-label-secondary);
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
}
|
||
|
||
.noticeError {
|
||
background: var(--dsw-alias-interactive-bg-hover-danger);
|
||
color: var(--dsw-alias-state-error-primary);
|
||
}
|
||
|
||
.error {
|
||
background: var(--dsw-alias-interactive-bg-hover-danger);
|
||
color: var(--dsw-alias-state-error-primary);
|
||
}
|
||
|
||
.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; 10px
|
||
top pad on the card before .InputText. */
|
||
gap: 12px;
|
||
width: 100%;
|
||
max-width: var(--dsh-composer-card-max-width);
|
||
padding-top: 10px;
|
||
/* Input stroke: black/0.10 light, white/0.06 dark (figma darkmode note says
|
||
the input border is one notch weaker than buttons) — exactly the
|
||
l2-darkmode-thin pair. Fill: the input surface token (elevated in dark). */
|
||
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
|
||
border-radius: 22px;
|
||
background: var(--dsw-specific-input-major);
|
||
box-shadow: var(--dsw-shadow-lv2);
|
||
font-size: 16px;
|
||
line-height: 24px;
|
||
/* 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);
|
||
}
|
||
|
||
.dragActive {
|
||
border-color: var(--dsw-alias-state-business-primary);
|
||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 24%, transparent), var(--dsw-shadow-lv2);
|
||
}
|
||
|
||
.dropHint {
|
||
position: absolute;
|
||
z-index: 2;
|
||
inset: 4px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 16px;
|
||
background: color-mix(in srgb, var(--dsw-specific-input-major) 88%, var(--dsw-alias-state-business-primary));
|
||
color: var(--dsw-alias-state-business-primary);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.accessory {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px 0;
|
||
}
|
||
|
||
.attachments {
|
||
display: flex;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
padding: 12px 12px 0;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
}
|
||
|
||
.attachment {
|
||
position: relative;
|
||
flex: 0 0 72px;
|
||
width: 72px;
|
||
height: 72px;
|
||
}
|
||
|
||
.thumbnail {
|
||
width: 72px;
|
||
height: 72px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
|
||
border-radius: 12px;
|
||
background: var(--dsw-alias-interactive-bg-hover);
|
||
cursor: zoom-in;
|
||
}
|
||
|
||
.thumbnail img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.remove {
|
||
position: absolute;
|
||
top: -6px;
|
||
right: -6px;
|
||
display: grid;
|
||
place-items: center;
|
||
width: 22px;
|
||
height: 22px;
|
||
padding: 0;
|
||
border: 1px solid var(--dsw-specific-input-major);
|
||
border-radius: 999px;
|
||
background: var(--dsw-alias-label-primary);
|
||
color: var(--dsw-specific-input-major);
|
||
font-size: 16px;
|
||
line-height: 1;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 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
|
||
caret is the textarea's and every visible glyph is the backdrop's, so the two
|
||
layers stay together only by riding one offset the browser applies to both at
|
||
once. Scrolling one box and assigning the offset to the other cannot hold —
|
||
a wheel gesture is composited off the main thread, so the assignment lands
|
||
frames late and the words visibly trail the caret. 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;
|
||
}
|
||
|
||
/* Mirror-div auto-grow stack: the hidden mirror is in normal flow and sets the FULL draft
|
||
height (min 2 lines in hero); backdrop and textarea ride it absolutely, so both layers are
|
||
as tall as the draft and the scrollport above shows a window onto them. Mirror and textarea
|
||
MUST share font, line-height, padding and wrapping rules or heights diverge. */
|
||
.grow {
|
||
position: relative;
|
||
}
|
||
|
||
/* Decoration backdrop: same metrics as the textarea, transparent glyphs; only
|
||
the highlight backgrounds and the ghost hint show through the transparent
|
||
textarea background above it. */
|
||
.backdrop {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
color: var(--dsw-alias-label-primary);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.hlToken {
|
||
background-color: transparent;
|
||
color: var(--dsw-alias-state-warn-label);
|
||
}
|
||
|
||
.hlSegment {
|
||
border-radius: 4px;
|
||
background-color: transparent;
|
||
color: transparent;
|
||
}
|
||
|
||
.hint {
|
||
color: var(--dsw-alias-label-caption);
|
||
}
|
||
|
||
/* Machine pending dot (adjudicating / submitting). */
|
||
.pending {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
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; }
|
||
}
|
||
|
||
.input {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
resize: none;
|
||
/* Never a scroller of its own: it is as tall as the draft, so it has no
|
||
scrollable overflow to hold an offset that could differ from the glyphs'.
|
||
The browser still reveals the caret — the scroll-into-view walks up to
|
||
.scroll and moves both layers together. */
|
||
overflow: hidden;
|
||
border: none;
|
||
outline: none;
|
||
background: transparent;
|
||
color: transparent;
|
||
/* Business blue, not brand-primary: that token resolves to ink in this sheet. */
|
||
caret-color: var(--dsw-alias-state-business-primary);
|
||
}
|
||
|
||
.input,
|
||
.mirror,
|
||
.backdrop {
|
||
/* Textareas default to content-box (unlike buttons/inputs): without this the
|
||
width:100% textarea gains its padding OUTSIDE the card and text runs past
|
||
the right padding — and wraps 28px later than the mirror/backdrop layers. */
|
||
box-sizing: border-box;
|
||
/* figma .InputText 34:10434: pl 16 / pr 12 / pt 4. Backdrop MUST share these
|
||
metrics or the highlight ranges drift off the glyphs. */
|
||
padding: 4px 12px 0 16px;
|
||
/* DshChipCell first: ONLY U+FFFC resolves there (4em blank cell — the chip
|
||
slot); everything else falls through to the app stack. All three layers
|
||
share the stack, so placeholder advances agree by construction. */
|
||
font-family: 'DshChipCell', var(--dsw-font-family);
|
||
font-size: inherit;
|
||
/* Three consumers, not two: the mirror sizes the stack, the layers must break
|
||
lines identically, and the caret reveal parses this value to step one line
|
||
down for a caret that sits after a newline. That parse needs a length, so a
|
||
theme resolving this to `normal` would make the reveal a silent no-op. */
|
||
line-height: inherit;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
overflow-wrap: anywhere;
|
||
/* These three MUST wrap at one width: the mirror decides the box height
|
||
the other two are laid out in, and a glyph layer that breaks lines
|
||
elsewhere than the textarea puts the words under the wrong caret. They do
|
||
so by construction now that all three sit INSIDE .scroll — a scrollbar
|
||
that consumes layout space narrows the scrollport, which is their shared
|
||
containing block, so it costs all three the same width on every engine.
|
||
Scrolling the textarea itself is what used to break this, and no property
|
||
fixed it: WebKit reserved gutter space for the overflow-y:auto textarea
|
||
and not for the overflow:hidden layers beside it, leaving them 8px apart
|
||
(768 against 776) — worth 2 to 5 wrapped lines on a long draft. */
|
||
}
|
||
|
||
/* figma 34:10434: #ADB2B8 light / #81858C dark — the caption pair exactly. */
|
||
.input::placeholder {
|
||
color: var(--dsw-alias-label-caption);
|
||
user-select: none;
|
||
}
|
||
|
||
/* Running lock: grayed but the draft stays visible; the turn ending re-enables. */
|
||
.input:disabled {
|
||
color: var(--dsw-alias-label-tertiary);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.mirror {
|
||
visibility: hidden;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 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 .mirror {
|
||
min-height: 52px;
|
||
}
|
||
|
||
/* Toolbar: attach + Plan + Read-only on the left; model + send on the right
|
||
(figma Input_Bottom chrome). */
|
||
.row {
|
||
display: flex;
|
||
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 card
|
||
height and the controls' own centering stay 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;
|
||
}
|
||
|
||
.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; the chip gap widened to 12 so the pill chips read as separate
|
||
controls. */
|
||
.tools {
|
||
gap: 16px;
|
||
}
|
||
|
||
.modes {
|
||
gap: 12px;
|
||
}
|
||
|
||
.trailing {
|
||
flex: none;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 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;
|
||
background: var(--dsw-specific-selector);
|
||
color: var(--dsw-alias-label-primary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.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: 8px;
|
||
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;
|
||
}
|
||
|
||
.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;
|
||
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);
|
||
}
|
||
|
||
.primary:hover:not(:disabled) {
|
||
background: var(--dsw-alias-button-info-hover);
|
||
}
|
||
|
||
.primary:disabled {
|
||
opacity: 0.4;
|
||
cursor: default;
|
||
}
|
||
|
||
.retry {
|
||
margin-left: 8px;
|
||
padding: 1px 8px;
|
||
border: 1px solid currentColor;
|
||
border-radius: 4px;
|
||
background: transparent;
|
||
color: inherit;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Plain-text reference highlight (decision 21): a pure range mark over the
|
||
draft's own glyphs — advance untouched, so the two layers cannot drift.
|
||
Chip family colors; clone keeps rounded ends on soft-wrap fragments. */
|
||
.textRef {
|
||
background-color: transparent;
|
||
color: var(--dsw-alias-state-business-primary);
|
||
box-decoration-break: clone;
|
||
-webkit-box-decoration-break: clone;
|
||
}
|
||
.textRef:after {
|
||
display: none;
|
||
}
|
||
|
||
/* Reference chip: rendered in the backdrop at the placeholder offset. Hard
|
||
alignment constraint: the chip's advance must equal the textarea's U+FFFC
|
||
advance EXACTLY or every glyph after it drifts (caret/selection follow the
|
||
textarea character stream). The ::before renders the same U+FFFC through
|
||
the same font stack (DshChipCell 4em cell), so both layers agree by
|
||
construction — no measured widths. The label overlays the cell, clipped
|
||
with an ellipsis; the full name rides the title tooltip. */
|
||
.chip {
|
||
position: relative;
|
||
border-radius: 6px;
|
||
background: rgba(97, 135, 216, 0.22);
|
||
}
|
||
|
||
.chip::before {
|
||
content: '\FFFC';
|
||
color: transparent;
|
||
}
|
||
|
||
.chipLabel {
|
||
/* Compensated-scale centering: overflow clipping happens BEFORE transform,
|
||
so the box is laid out at 1/0.72 of the cell and scaled back down — the
|
||
clip edge then lands on the visual cell edge, not mid-glyph. */
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: calc(100% / 0.72 - 10px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
color: var(--dsw-alias-label-primary);
|
||
white-space: nowrap;
|
||
transform: translate(-50%, -50%) scale(0.72);
|
||
}
|
||
|
||
.chipInvalid {
|
||
background: rgba(216, 97, 97, 0.2);
|
||
text-decoration: line-through;
|
||
opacity: 0.7;
|
||
}
|