Files
apes-sidebar/src/client/sidebar.module.css
T

1918 lines
42 KiB
CSS

/**
* Sidebar styles. Per the DSH web-styling rules every visual value rides the
* theme: --dsw-alias-* semantic tokens for colors, --dsw-font-* typography
* roles (size paired with line-height), and --ds-* for motion. The chrome
* mirrors the native DSH sidebar: flat (NO box-shadows anywhere), the
* sidebar fill surface, hairline borders, and the 28/36px circular icon
* controls with hover fills. No static palette values, no color literals, no
* theme selectors — light/dark adaptation is the theme package's job.
*/
/* ── Shell ─────────────────────────────────────────────────────────────── */
/* The persistent expand/collapse cluster at the top-right corner: two rail
controls side by side (bottom panel's glyph LEFT of the right panel's).
Always pinned to the viewport corner — inside the right panel's top-right
while it is open, sitting flush in the 34px tab strip (28px buttons at
top: 3) whose right end it really squeezes. */
.toggleCluster {
position: fixed;
top: 3px;
right: 10px;
z-index: 55;
display: flex;
flex-direction: row;
gap: 4px;
}
/* The tab strip of the OPEN right panel reserves the cluster's width at its
right end, so the tabs and the + menu genuinely yield space to the cluster
(never hiding under it). */
.panel:not(.panelHidden) .tabBar {
padding-right: 72px;
}
/* The closed-state affordance is the native rail icon control: a plain
28px circle (the app's icon-button size — the cluster must stay inside
the 34px tab-strip band so it never covers the pane headers below),
transparent fill, secondary ink; hover raises the fill. No border, no
shadow — the icon alone is the affordance. */
.toggleButton {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
transition:
background var(--ds-transition-duration-slow) var(--ds-ease-in-out),
color var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
.toggleButton:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.toggleButton:disabled {
opacity: 0.4;
cursor: default;
}
.panel {
position: fixed;
top: 0;
right: 0;
/* Full height: the bottom panel squeezes only the center column, so the
right panel never gives up any vertical position. */
bottom: 0;
z-index: 50;
display: flex;
flex-direction: column;
/* The native sidebar surface: bluish-50 over white / bluish-900 over dark,
exactly like the app's own sidebar column (SidebarRoot .root). */
background: var(--dsw-specific-sidebar-fill);
border-left: 1px solid var(--dsw-alias-border-l2);
/* Expand/collapse slides the panel; fullscreen animates the width. */
transition:
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out),
width var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
/* Collapsed: slid off-screen, interactive nothing, hidden after the slide. */
.panelHidden {
transform: translateX(102%);
pointer-events: none;
visibility: hidden;
transition:
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out),
width var(--ds-transition-duration-slow) var(--ds-ease-in-out),
visibility 0s linear var(--ds-transition-duration-slow);
}
/* Width drags write at pointer cadence; easing would detach the edge. */
.panel[data-dragging] {
transition: none;
}
.panelResize {
position: absolute;
left: -4px;
top: 0;
bottom: 0;
width: 8px;
cursor: col-resize;
z-index: 2;
touch-action: none;
}
.panelResizeActive {
background: var(--dsw-alias-interactive-bg-hover-accent);
}
.panelBody {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
}
/* ── Bottom panel ──────────────────────────────────────────────────────── */
/* The bottom panel: an independent workbench at the bottom of the CENTER
column only (the Sidebar shell writes left/right inline: from the app's
own left sidebar to the right panel's left edge) — the sidebars never
give up any position. Same surface as the right panel, with a top
hairline instead of the left one. */
.bottomPanel {
position: fixed;
bottom: 0;
z-index: 50;
display: flex;
flex-direction: column;
background: var(--dsw-specific-sidebar-fill);
border-top: 1px solid var(--dsw-alias-border-l2);
/* Expand/collapse slides the panel; height drags animate the height. */
transition:
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out),
height var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
/* Collapsed: slid off-screen, interactive nothing, hidden after the slide. */
.bottomPanelHidden {
transform: translateY(102%);
pointer-events: none;
visibility: hidden;
transition:
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out),
height var(--ds-transition-duration-slow) var(--ds-ease-in-out),
visibility 0s linear var(--ds-transition-duration-slow);
}
/* Height drags write at pointer cadence; easing would detach the edge. */
.bottomPanel[data-dragging] {
transition: none;
}
/* The bottom panel's height drag: the top edge strip (mirror of the right
panel's width strip). */
.bottomResize {
position: absolute;
left: 0;
right: 0;
top: -4px;
height: 8px;
cursor: row-resize;
z-index: 2;
touch-action: none;
}
.bottomResizeActive {
background: var(--dsw-alias-interactive-bg-hover-accent);
}
/* The bottom panel's close control: the app's icon-button pattern (28px
circle), pinned to the tab strip's right end — one tap collapses the
panel. The tab strips below reserve its width so the + menu never hides
under it. */
.bottomClose {
position: absolute;
top: 3px;
right: 6px;
z-index: 4;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
flex: none;
}
.bottomClose:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
/* The bottom panel's tab strips reserve the close control's width at their
right end, so the + menu never hides under it. */
.bottomPanel .tabBar {
padding-right: 40px;
}
/* The shared corner (both panels open): the intersection of the right
panel's left edge and the bottom panel's top edge. Horizontal drags
resize the width, vertical drags the height — the two panels drag
against each other. */
.cornerHandle {
position: fixed;
width: 12px;
height: 12px;
z-index: 52;
cursor: nwse-resize;
touch-action: none;
}
.cornerHandle:hover,
.cornerHandle[data-dragging] {
background: var(--dsw-alias-interactive-bg-hover-accent);
}
/* Icon button: the app's own icon-button pattern (ui-workspace) — a 28px
circle, secondary label, hover fill; no border so the icon alone is the
affordance. */
.iconButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: var(--dsw-alias-label-secondary);
cursor: pointer;
flex: none;
}
.iconButton:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.iconButton:disabled {
opacity: 0.4;
cursor: default;
}
/* ── Workbench / split panes ───────────────────────────────────────────── */
.workbench {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
}
.split {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
}
.splitRow {
flex-direction: row;
}
.splitCol {
flex-direction: column;
}
.splitChild {
position: relative;
display: flex;
overflow: hidden;
}
.divider {
position: relative;
flex: none;
z-index: 3;
touch-action: none;
}
/* The hit area stays 7px wide enough to grab, but the visual is a 1px
hairline centered in it — a divider line, not a thick band. */
.dividerRow::after,
.dividerCol::after {
content: '';
position: absolute;
background: var(--dsw-alias-border-l2);
transition: background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
.dividerRow {
width: 7px;
margin: 0 -2px;
cursor: col-resize;
}
.dividerRow::after {
top: 0;
bottom: 0;
left: 50%;
width: 1px;
transform: translateX(-50%);
}
.dividerCol {
height: 7px;
margin: -2px 0;
cursor: row-resize;
}
.dividerCol::after {
left: 0;
right: 0;
top: 50%;
height: 1px;
transform: translateY(-50%);
}
.divider:hover::after,
.dividerActive::after {
background: var(--dsw-alias-interactive-bg-hover-accent);
}
.pane {
position: relative;
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-base);
}
.paneDrop {
outline: 1px solid var(--dsw-alias-interactive-bg-hover-accent);
outline-offset: -1px;
}
/* VSCode-style drag-to-edge drop zones (25% edges, 50% center). */
.dropOverlay {
position: absolute;
z-index: 6;
pointer-events: none;
background: var(--dsw-alias-interactive-bg-hover-accent);
opacity: 0.5;
}
.dropLeft {
left: 0;
top: 0;
bottom: 0;
width: 25%;
}
.dropRight {
right: 0;
top: 0;
bottom: 0;
width: 25%;
}
.dropUp {
top: 0;
left: 0;
right: 0;
height: 25%;
}
.dropDown {
bottom: 0;
left: 0;
right: 0;
height: 25%;
}
.dropCenter {
left: 25%;
right: 25%;
top: 25%;
bottom: 25%;
background: transparent;
outline: 2px dashed var(--dsw-alias-interactive-bg-hover-accent);
outline-offset: -2px;
}
.paneContent {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* One tab's mounted content; inactive tabs stay mounted but hidden so
switching never tears down terminals/editors (state survives). */
.paneTab {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.paneTabHidden {
display: none;
}
/* An empty pane's welcome cards: a responsive grid of compact cards — as
many per row as the pane width allows; rows that cannot fit the pane
height are clipped (never scrolled or squeezed smaller). */
.paneEmptyCards {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
align-content: start;
gap: 8px;
padding: 12px;
overflow: hidden;
}
.paneCard {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 0;
padding: 12px 8px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 8px;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-secondary);
font: var(--dsw-font-xxs-strong-12);
cursor: pointer;
text-align: center;
}
.paneCard:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
border-color: var(--dsw-alias-border-l2);
}
.paneCard:disabled {
opacity: 0.45;
cursor: default;
}
/* ── Tab strip ─────────────────────────────────────────────────────────── */
.tabBar {
flex: none;
display: flex;
align-items: stretch;
height: 34px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
background: var(--dsw-alias-bg-layer-1);
}
.tabBarDrop {
outline: 1px dashed var(--dsw-alias-interactive-bg-hover-accent);
outline-offset: -1px;
}
.tabList {
flex: 1;
min-width: 0;
display: flex;
overflow-x: auto;
scrollbar-width: none;
}
.tabList::-webkit-scrollbar {
display: none;
}
.tab {
flex: none;
max-width: 160px;
min-width: 64px;
display: flex;
align-items: center;
gap: 4px;
padding: 0 4px 0 10px;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-secondary);
border-right: 1px solid var(--dsw-alias-border-l1);
background: transparent;
cursor: pointer;
user-select: none;
}
.tab:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.tabActive {
color: var(--dsw-alias-label-primary);
/* Native selected fill (Rows .sessionRow.selected) — no underline, no shadow. */
background: var(--dsw-alias-interactive-bg-active);
}
.tabTitle {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tabClose {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
flex: none;
}
.tabClose:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.tabBarPlus {
flex: none;
align-self: center;
/* Adjacent to the rightmost tab; sticky so overflowing tab rows keep it
visible at the right edge of the scrollport. */
position: sticky;
right: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin: 0 6px;
padding: 0;
border: none;
border-radius: 5px;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
}
.tabBarPlus:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
/* ── Explorer ──────────────────────────────────────────────────────────── */
.explorer {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.explorerHeader {
flex: none;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
height: 36px;
padding: 0 8px 0 12px;
}
.explorerRoot {
font: var(--dsw-font-s-14);
color: var(--dsw-alias-label-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.explorerBody {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 2px 6px 8px;
}
/* One tree row: the app's session-row cell (34px, radius 8, inset, hover
fill, 22px indent step, 14px ink) with a mount fade so expanding a
directory reveals its children gently (stable keys keep existing rows from
replaying it). */
.explorerRow {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
height: 34px;
padding: 0 8px;
border: none;
background: transparent;
font: var(--dsw-font-s-14);
color: var(--dsw-alias-label-primary);
text-align: left;
cursor: pointer;
white-space: nowrap;
border-radius: 8px;
animation: dsh-row-in 150ms var(--ds-ease-in-out);
}
.explorerRow:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.explorerDir {
font: var(--dsw-font-s-strong-14);
}
.explorerHidden {
opacity: 0.45;
}
.explorerName {
overflow: hidden;
text-overflow: ellipsis;
}
/* The hover-revealed @-reference button at the row's far right (and the
transient "copied" label that replaces it after a copy). Hidden rows
keep the name at full width; hovering or focusing the row reveals it. */
.explorerRef {
flex: none;
display: none;
align-items: center;
height: 20px;
padding: 0 8px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 999px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-tertiary);
font: var(--dsw-font-xxxs-strong-11);
cursor: pointer;
}
.explorerRef:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.explorerRow:hover .explorerRef,
.explorerRow:focus-within .explorerRef {
display: inline-flex;
}
.explorerCopied {
flex: none;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-label-tertiary);
}
.explorerError {
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-state-error-primary);
cursor: default;
}
/* Mount fade for tree rows (the app's session-row reveal). */
@keyframes dsh-row-in {
from { opacity: 0; }
}
.explorerEmpty {
padding: 16px;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
text-align: center;
}
/* ── Editor ────────────────────────────────────────────────────────────── */
.editor {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.editorHeader {
flex: none;
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
}
.editorTitle {
flex: 1;
min-width: 0;
font: var(--dsw-font-xxs-strong-12);
color: var(--dsw-alias-label-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.editorStatus {
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-label-tertiary);
}
.editorStatusError {
color: var(--dsw-alias-state-error-primary);
}
.dirtyDot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--dsw-alias-state-warn-primary);
flex: none;
}
.editorPlaceholder {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
padding: 16px;
text-align: center;
}
/* The unregistered tab type shown by the orphaned-tab placeholder. */
.orphanedType {
display: block;
margin-top: 8px;
font-size: 12px;
opacity: 0.7;
overflow-wrap: anywhere;
}
/* Binary / download-only files (pptx, OLE .doc/.xls/.ppt, unknown binaries):
a centered notice plus a download link — the user always gets the file. */
.editorBinary {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 24px 16px;
text-align: center;
}
.editorBinaryNotice {
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
}
.editorDownloadLink {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-primary);
font: var(--dsw-font-xxs-strong-12);
text-decoration: none;
cursor: pointer;
transition:
background var(--ds-transition-duration-slow) var(--ds-ease-in-out),
border-color var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
.editorDownloadLink:hover {
background: var(--dsw-alias-interactive-bg-hover);
border-color: var(--dsw-alias-border-l2);
}
.editorError {
padding: 12px 16px;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-state-error-primary);
}
.editorBanner {
flex: none;
padding: 4px 12px;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-state-warn-label);
background: var(--dsw-alias-state-warn-tertiary);
}
/* Live sandbox status row (HTML preview / browser tab): GREEN while the
sandbox is on (with the one-tap temporary unlock), RED while it is off
(global setting or temporary unlock) — the red state is the persistent
warning that the page runs on the GUI's own origin. */
.sandboxStatus {
flex: none;
display: flex;
align-items: center;
gap: 8px;
padding: 4px 10px;
font: var(--dsw-font-xxxs-11);
}
.sandboxStatusOn {
color: var(--dsw-alias-label-secondary);
background: var(--dsw-alias-bg-layer-1);
border-bottom: 1px solid var(--dsw-alias-border-l1);
}
.sandboxStatusOff {
color: var(--dsw-alias-state-error-primary);
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);
}
.sandboxDot {
flex: none;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--dsw-alias-state-success-primary);
}
.sandboxStatusOff .sandboxDot {
background: var(--dsw-alias-state-error-primary);
}
.sandboxStatusText {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sandboxAction {
flex: none;
padding: 2px 8px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: transparent;
font: inherit;
color: inherit;
cursor: pointer;
}
.sandboxAction:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
/* The HTML preview iframe: fills the editor body. Route-src (never srcdoc)
so the frame is cross-origin by construction even if the sandbox
attribute is ever dropped; sandboxed by default (see TextEditor). */
.editorHtml {
flex: 1;
min-height: 0;
width: 100%;
border: none;
background: var(--dsw-alias-bg-base);
}
/* ── Browser tab ──────────────────────────────────────────────────────── */
.browser {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.browserBar {
flex: none;
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
}
.browserInput {
flex: 1;
min-width: 0;
height: 28px;
padding: 0 10px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 6px;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-primary);
font: var(--dsw-font-xxs-12);
}
.browserInput:focus {
outline: none;
border-color: var(--dsw-alias-border-l2);
}
.browserMessage {
flex: none;
padding: 4px 12px;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-state-warn-label);
background: var(--dsw-alias-state-warn-tertiary);
}
.browserFrame {
flex: 1;
min-height: 0;
width: 100%;
border: none;
background: var(--dsw-alias-bg-base);
}
.browserStart {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
font: var(--dsw-font-xs-13);
color: var(--dsw-alias-label-tertiary);
}
/* Embed-refusal panel (X-Frame-Options / frame-ancestors): replaces the
blank refused iframe with the reason + open-in-browser actions. */
.browserBlocked {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 24px;
text-align: center;
color: var(--dsw-alias-state-warn-primary);
}
.browserBlockedTitle {
font: var(--dsw-font-xxs-strong-12);
color: var(--dsw-alias-label-primary);
}
.browserBlockedDesc {
max-width: 280px;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-label-secondary);
}
.browserBlockedActions {
display: flex;
gap: 8px;
margin-top: 6px;
}
.browserBlockedButton {
padding: 4px 12px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: var(--dsw-alias-bg-layer-1);
color: var(--dsw-alias-label-primary);
font: var(--dsw-font-xxxs-11);
cursor: pointer;
}
.browserBlockedButton:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.editorCm {
flex: 1;
min-height: 0;
overflow: hidden;
background: transparent;
}
.editorCmHidden {
display: none;
}
/* CodeMirror fills the pane; the theme supplies its own token palette
(a designed dark theme, same policy as the xterm palette). */
.editorCm :global(.cm-editor) {
height: 100%;
}
.editorCm :global(.cm-editor.cm-focused) {
outline: none;
}
/* Preview / edit segmented toggle (markdown and other previewable files). */
.editorModeToggle {
flex: none;
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 6px;
background: var(--dsw-alias-bg-layer-1);
}
.editorModeButton {
padding: 2px 8px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--dsw-alias-label-tertiary);
font: var(--dsw-font-xxxs-11);
cursor: pointer;
}
.editorModeButton:hover {
color: var(--dsw-alias-label-primary);
}
.editorModeActive {
background: var(--dsw-alias-bg-base);
color: var(--dsw-alias-label-primary);
}
.editorImageWrap {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
padding: 12px;
}
.editorImage {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.editorMd {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 10px 14px;
font: var(--dsw-font-xs-13);
}
/* The floating "add to conversation" button anchored above a text
selection in the preview/code viewers. Portalled to document.body and
position:fixed, so the editor's overflow clips cannot crop it; flat
chrome like the rest of the sidebar (hairline border, layer-2 fill,
hover fill — no shadow). */
.selectionPopup {
position: fixed;
z-index: 60;
transform: translate(-50%, calc(-100% - 8px));
display: inline-flex;
align-items: center;
height: 28px;
padding: 0 10px;
border: 1px solid var(--dsw-alias-border-l1);
border-radius: 6px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-primary);
font: var(--dsw-font-xxxs-strong-11);
white-space: nowrap;
cursor: pointer;
}
.selectionPopup:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
/* Word preview: the document scrolls independently while the zoom control
stays pinned to the bottom edge. */
.editorDocx {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-base);
}
.editorDocxViewport {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: auto;
background: var(--dsw-alias-bg-base);
}
/* docx-preview renders into a wrapper div with .docx className; the host
above scrolls while the wrapper holds the page flow. */
.editorDocxWrap {
flex: none;
padding: 16px;
}
.editorDocxZoom {
flex: none;
display: flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 4px 10px;
border-top: 1px solid var(--dsw-alias-border-l1);
background: var(--dsw-alias-bg-layer-1);
}
.editorDocxZoomHint,
.editorDocxZoomValue {
flex: none;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-label-tertiary);
}
.editorDocxZoomValue {
width: 36px;
text-align: right;
}
.editorDocxZoomRange {
flex: 1;
min-width: 72px;
accent-color: var(--dsw-alias-brand-primary);
cursor: pointer;
}
.editorPdf {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-base);
}
.editorPdfToolbar {
flex: none;
display: flex;
justify-content: flex-end;
padding: 6px 8px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
}
.editorPdfStage {
position: relative;
flex: 1;
min-height: 0;
display: flex;
}
.editorPdfFrame {
flex: 1;
min-height: 0;
width: 100%;
border: none;
background: var(--dsw-alias-bg-base);
}
.editorPdfFrameBlocked {
pointer-events: none;
}
.editorPdfDragShield {
position: absolute;
inset: 0;
z-index: 4;
pointer-events: none;
background: transparent;
}
.editorPdfDragShieldActive {
pointer-events: auto;
}
/* Tab drag must switch iframe hit-testing before the pointer can enter the
Chromium PDF browsing context. The body flag is set synchronously by the
drag source, avoiding React render timing entirely. */
:global(body[data-dsh-tab-dragging]) .editorPdfFrame {
pointer-events: none !important;
}
:global(body[data-dsh-tab-dragging]) .editorPdfDragShield {
pointer-events: auto !important;
}
.editorXlsx {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
background: var(--dsw-alias-bg-base);
}
.editorUniverHost {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.editorOfficeOverlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
background: var(--dsw-alias-bg-base);
}
.editorPptx {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-base);
}
.editorPptxToolbar {
flex: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 6px 8px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
}
.editorPptxToolbar .editorDownloadLink {
margin-left: auto;
}
.editorPptxButton {
height: 28px;
padding: 0 10px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-secondary);
font: var(--dsw-font-xxs-strong-12);
cursor: pointer;
}
.editorPptxButton:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.editorPptxButton:disabled {
opacity: 0.4;
cursor: default;
}
.editorPptxPosition {
min-width: 64px;
text-align: center;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
}
.editorPptxStage {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
}
.editorPptxHost {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: auto;
}
/* ── Terminal ──────────────────────────────────────────────────────────── */
.terminalWrap {
flex: 1;
min-height: 0;
position: relative;
display: flex;
flex-direction: column;
background: var(--dsw-alias-bg-base);
}
.terminal {
flex: 1;
min-height: 0;
padding: 6px 4px 6px 8px;
}
.terminal :global(.xterm) {
height: 100%;
}
.terminalBanner {
flex: none;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding: 3px 10px;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-state-warn-label);
background: var(--dsw-alias-state-warn-tertiary);
}
/* The attempted ws URL, shown when the connection failed (diagnosis). */
.terminalBannerUrl {
flex-basis: 100%;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
word-break: break-all;
opacity: 0.85;
}
.boundaryError {
position: fixed;
right: 0;
top: 0;
bottom: 0;
z-index: 50;
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
padding: 16px;
background: var(--dsw-alias-bg-layer-1);
border-left: 1px solid var(--dsw-alias-border-l2);
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-state-error-primary);
overflow: auto;
}
.terminalRetry {
flex: none;
padding: 1px 8px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-secondary);
font: var(--dsw-font-xxxs-strong-11);
cursor: pointer;
}
.terminalRetry:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
/* ── Git ───────────────────────────────────────────────────────────────── */
.git {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
/* Tab pages never scroll horizontally: content wraps or truncates to the
panel width instead of panning sideways. */
overflow-x: hidden;
}
.gitHeader {
flex: none;
display: flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 8px 0 12px;
}
.gitBranchSelect {
flex: 1;
min-width: 0;
height: 26px;
padding: 0 6px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: var(--dsw-alias-bg-base);
color: var(--dsw-alias-label-primary);
font: var(--dsw-font-xxs-12);
}
.gitSection {
border-top: 1px solid var(--dsw-alias-border-l1);
}
.gitSectionHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px 4px;
font: var(--dsw-font-xxxs-strong-11);
color: var(--dsw-alias-label-tertiary);
text-transform: uppercase;
}
.gitLink {
border: none;
background: transparent;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-brand-primary);
cursor: pointer;
padding: 0;
}
.gitLink:hover:not(:disabled) {
text-decoration: underline;
}
.gitLink:disabled {
opacity: 0.4;
cursor: default;
}
.gitRow {
display: flex;
align-items: center;
gap: 6px;
min-height: 34px;
margin: 0 6px;
padding: 0 8px;
border-radius: 8px;
animation: dsh-row-in 150ms var(--ds-ease-in-out);
}
.gitRow:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.gitRowSelected {
background: var(--dsw-alias-interactive-bg-active);
}
.gitRowMain {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
border: none;
background: transparent;
padding: 3px 0;
cursor: pointer;
text-align: left;
}
.gitBadge {
flex: none;
width: 20px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font: var(--dsw-font-xxxs-strong-11);
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-secondary);
}
.gitName {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: var(--dsw-font-s-14);
color: var(--dsw-alias-label-primary);
}
.gitEmpty {
padding: 4px 12px 8px;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
}
.gitPlaceholder {
padding: 16px;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
text-align: center;
}
.gitError {
padding: 8px 12px;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-state-error-primary);
white-space: pre-wrap;
}
.gitDiff {
border-top: 1px solid var(--dsw-alias-border-l1);
padding: 8px;
}
/* The diff tab (a change opened from the git panel, like VSCode's diff
editor): header + scrollable diff body, no horizontal panning. */
.gitDiffTab {
flex: 1;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
}
.gitDiffTabHeader {
flex: none;
display: flex;
align-items: center;
gap: 8px;
height: 36px;
padding: 0 8px 0 12px;
border-bottom: 1px solid var(--dsw-alias-border-l1);
}
.gitDiffTabTitle {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: var(--dsw-font-xxs-strong-12);
color: var(--dsw-alias-label-primary);
}
/* One changed file's section: path header, hunk headers, aligned lines. */
.gitDiffFile {
display: flex;
align-items: baseline;
gap: 6px;
padding: 8px 2px 2px;
}
.gitDiffFilePath {
font: var(--dsw-font-xxs-strong-12);
color: var(--dsw-alias-label-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gitDiffFileOld {
flex: none;
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-label-tertiary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 40%;
}
.gitDiffFileTag {
flex: none;
padding: 0 6px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
font: var(--dsw-font-xxxs-strong-11);
color: var(--dsw-alias-label-secondary);
}
.gitDiffHunk {
display: flex;
gap: 8px;
padding: 3px 2px;
font: var(--dsw-font-markdown-code-block-small);
color: var(--dsw-alias-label-tertiary);
}
.gitDiffHunkHeader {
flex: none;
color: var(--dsw-alias-label-secondary);
}
.gitDiffHunkSection {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* One aligned diff line: old gutter / new gutter / code. Lines WRAP instead
of scrolling horizontally (the tab-page rule: fill the width adaptively),
with the long-token fallback breaking anywhere a space never comes. */
.gitDiffLine {
display: flex;
align-items: stretch;
font: var(--dsw-font-markdown-code-block-small);
line-height: 20px;
white-space: pre-wrap;
overflow-wrap: anywhere;
min-width: 0;
}
.gitDiffNum {
flex: none;
width: 36px;
padding-right: 8px;
text-align: right;
color: var(--dsw-alias-label-tertiary);
user-select: none;
}
.gitDiffCode {
flex: 1;
min-width: 0;
overflow: visible;
}
.gitDiffCtx {
color: var(--dsw-alias-label-primary);
}
.gitDiffDel {
color: var(--dsw-alias-state-error-primary);
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);
}
.gitDiffAdd {
color: var(--dsw-alias-state-success-primary);
background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent);
}
.gitDiffMeta {
padding-left: 2px;
}
.gitDiffMetaText {
font: var(--dsw-font-xxxs-11);
font-style: italic;
color: var(--dsw-alias-label-tertiary);
}
.gitDiffExpand {
display: block;
width: 100%;
margin: 4px 0;
border: none;
background: transparent;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-brand-primary);
cursor: pointer;
text-align: center;
}
.gitDiffExpand:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.gitConfirmDesc {
margin: 0;
font: var(--dsw-font-s-14);
color: var(--dsw-alias-label-primary);
white-space: pre-wrap;
}
.gitCommit {
display: flex;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--dsw-alias-border-l1);
align-items: center;
}
.gitCommitInput {
flex: 1;
min-width: 0;
}
.gitCommitButton {
flex: none;
height: 26px;
padding: 0 12px;
border: none;
border-radius: 6px;
background: var(--dsw-alias-button-primary-fill);
color: var(--dsw-alias-label-primary-inverted);
font: var(--dsw-font-xxs-strong-12);
cursor: pointer;
}
.gitCommitButton:hover:not(:disabled) {
background: var(--dsw-alias-button-primary-hover);
}
.gitCommitButton:disabled {
opacity: 0.45;
cursor: default;
}
/* A history row: line 1 = hash + subject, line 2 = ref badges + author/date
(badges live on the second line so they never crowd the subject). */
.gitLogRow {
display: flex;
flex-direction: column;
gap: 2px;
padding: 5px 12px;
cursor: pointer;
border-radius: 8px;
}
.gitLogRow:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.gitLogLine1 {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.gitLogHash {
flex: none;
font: var(--dsw-font-markdown-code-block-small);
color: var(--dsw-alias-label-tertiary);
}
.gitLogLine2 {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
flex-wrap: wrap;
}
.gitLogRef {
flex: none;
padding: 0 5px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
font: var(--dsw-font-xxxs-strong-11);
color: var(--dsw-alias-brand-primary);
white-space: nowrap;
}
.gitLogSubject {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: var(--dsw-font-s-14);
color: var(--dsw-alias-label-primary);
}
.gitLogMeta {
font: var(--dsw-font-xxxs-11);
color: var(--dsw-alias-label-tertiary);
}
.gitLogMore {
display: block;
width: calc(100% - 24px);
margin: 4px 12px 8px;
padding: 6px 0;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 6px;
background: transparent;
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-secondary);
cursor: pointer;
}
.gitLogMore:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.gitLogMore:disabled {
opacity: 0.5;
cursor: default;
}
/* ── Intercepted produced-files row ────────────────────────────────────── */
.producedRow {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding: 4px 0;
}
.producedLabel {
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
}
.producedChip {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 200px;
padding: 2px 8px;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 999px;
background: var(--dsw-alias-bg-layer-2);
color: var(--dsw-alias-label-secondary);
font: var(--dsw-font-xxs-12);
cursor: pointer;
overflow: hidden;
}
.producedChip:hover {
background: var(--dsw-alias-interactive-bg-hover);
color: var(--dsw-alias-label-primary);
}
.producedChip span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.producedMore {
font: var(--dsw-font-xxs-12);
color: var(--dsw-alias-label-tertiary);
}
/* ── Shared interaction rules ──────────────────────────────────────────── */
/* Keyboard focus stays visible on every interactive control (the styling
policy requires focus visibility; the accent token doubles as the ring). */
.toggleButton:focus-visible,
.bottomClose:focus-visible,
.iconButton:focus-visible,
.tab:focus-visible,
.tabClose:focus-visible,
.tabBarPlus:focus-visible,
.paneCard:focus-visible,
.explorerRow:focus-visible,
.explorerRef:focus-visible,
.gitRowMain:focus-visible,
.gitLink:focus-visible,
.gitCommitButton:focus-visible,
.gitLogRow:focus-visible,
.gitLogMore:focus-visible,
.gitDiffExpand:focus-visible,
.terminalRetry:focus-visible,
.editorModeButton:focus-visible,
.editorDownloadLink:focus-visible,
.editorPptxButton:focus-visible,
.editorDocxZoomRange:focus-visible {
outline: 2px solid var(--dsw-alias-interactive-bg-hover-accent);
outline-offset: -1px;
}
/* Reduced motion: no slides, fades or color transitions. */
@media (prefers-reduced-motion: reduce) {
.panel,
.panelHidden,
.bottomPanel,
.bottomPanelHidden,
.toggleCluster,
.toggleButton,
.tab,
.tabBarPlus,
.paneCard,
.explorerRow,
.gitRow,
.divider,
.dividerRow::after,
.dividerCol::after {
transition: none;
animation: none;
}
}
/* ── Narrow (mobile) viewport ──────────────────────────────────────────── */
/* Pairs with NARROW_MAX_WIDTH = 768 in src/client/breakpoints.ts: widths
strictly below 768px (i.e. max-width: 767px) switch the sidebar to the
mobile layout — one full-screen drawer (the bottom panel's tabs were
migrated into the right tree), one toggle button. The structural
switches (which panels render, the migration) are JS-driven via
useNarrowViewport; this block only adapts the pure style details. */
@media (max-width: 767px) {
/* The toggle cluster holds a single button on narrow viewports (the
bottom panel button is not rendered), so the open panel's tab strip
reserves 40px instead of the desktop 72px — the tabs and + menu still
yield space to the one remaining control. */
.panel:not(.panelHidden) .tabBar {
padding-right: 40px;
}
/* Narrower tabs: a phone-width drawer fits more open tabs before the
strip scrolls. */
.tab {
min-width: 48px;
max-width: 128px;
}
}