Files
deepseek-harness/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css
T
Yif 45c9205caf feat(client): localize composer hints and rework input command interaction
Unify the /plan claimed hint with the plan placeholder through a locale
namespace, localize slash menu group titles, replace the PermissionSelect
native select with the Menu primitive, add a goal pause verb chain, clamp
anchored popups to the viewport with scroll-into-view and outside-dismiss,
and fix onPasteUpgrade insertedRange to account for the chip trailing gap.
2026-07-29 19:46:48 +08:00

398 lines
12 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 (800 is a cap, not a fixed size — layout rule: the box shrinks with
the center column keeping its padding). 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;
/* figma Input_Bottom: pad L32/R32/B12; the bottom gradient mask is owned by
the chat scroller. Top 6 is the gap under the dock todo strip (12px todo
margin + 6px here); error/status strips still carry their own margin. */
padding: 6px 32px 12px;
}
.hero {
padding: 0;
}
.error,
.status {
width: 100%;
max-width: 800px;
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: 800px;
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 {
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: 800px;
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: 20px;
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 textarea 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 textarea 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);
}
.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;
}
/* Mirror-div auto-grow wrapper: the hidden mirror is in normal flow and sets the height
(min 2 lines / max 14 lines); the textarea rides it absolutely. 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;
overflow-y: auto;
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;
line-height: inherit;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
/* 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;
/* figma min-h 52 (= ~2 × 24 line + 4pt); 14-line cap (336px). */
min-height: 52px;
max-height: 336px;
overflow: hidden;
}
/* 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;
padding: 0 10px 10px 10px;
min-width: 0;
}
.tools,
.modes,
.trailing {
display: flex;
align-items: center;
min-width: 0;
}
/* figma 75:8208: 16 between + and the mode chips; 4 between Plan / Read-only. */
.tools {
gap: 16px;
}
.modes {
gap: 4px;
}
.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;
}
.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;
}