资源管理器补全:文件树右键菜单新增「重命名」(行内编辑:预填+全选、 Enter/失焦提交、Esc 取消、IME 组合键守卫)与「删除」(Modal 确认,文件/ 目录文案区分,永久删除——宿主无回收站)。根行(工作区根)不提供两项, 服务端 fs.rename/fs.remove 双保险拒绝;目标已存在 409;link-aware—— symlink 行只改/删链接本身(lstat 判定),fence 校验对解析后目标进行。 落定收尾:pruneTree 清旧前缀缓存 + 收起 stale expanded + 单层重载父目录; tab 调解走 service(rename→updateTab 改路径改标题、delete→closeTab 关 受影响 tab,覆盖两侧分栏/底栏/浮动窗口,经 tree-mutations.ts)。 二级菜单视觉关系(layout.css,仍限 data-dsh-sidebar-submenu 作用域): 父行在子菜单打开期间保持高亮(宿主已渲染 aria-expanded="true"), 卡片间距 10px→4px(悬停走廊同步收窄),翻转规则一并 4px。 i18n 新增 7 key × 20 语言文件(locales.spec 的 key-set-parity 门禁)。 测试:fs-operations 追加 12 例(rename/remove 全失败面);新增 file-tree-rename-delete.spec.tsx 8 例(菜单可见性/重命名流/删除流/错误条); menu-flip.spec.tsx 契约扩展覆盖 tether 规则。设计文档 docs/plans/2026-09-04-file-tree-rename-delete-design.md。
2677 lines
72 KiB
CSS
2677 lines
72 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 ─────────────────────────────────────────────────────────────── */
|
|
|
|
/* Skinning contract (token-driven): every surface here consumes DSH's
|
|
generic tokens (`--dsw-alias-*`), NEVER `--dsw-specific-sidebar-fill` —
|
|
skin systems override that token to restyle the APP's own left-nav column
|
|
(dsh-web-ui skins make it translucent glass or theme colors, Aqua makes it
|
|
transparent), and a panel consuming it would lose its fill or clash with
|
|
the label tokens. The panels use the generic card surface
|
|
`--dsw-alias-bg-layer-1`, which every dsh-web-ui skin overrides — so
|
|
switching skins in the dsh-web-ui skin center re-skins the sidebar panels
|
|
automatically (see AGENTS.md §8). */
|
|
|
|
/* The unified panel host: a fixed, viewport-sized, click-transparent
|
|
containing block for every panel element. Panels are ABSOLUTE within it
|
|
(not fixed), so a desktop shell's intermediate wrapper transform can
|
|
never hijack their containing block — the host itself is the only fixed
|
|
element, appended directly to document.body. The layer sits at z-index 25:
|
|
above the app's shell overlay (AppFrame overlayLayer, 20) and in-flow
|
|
content, below DSH's ui-cordis dynamic-plugin panel (fixed, 30 — the
|
|
inventory/surfacing surface must never be hidden behind the workbench),
|
|
and below the app's overlay stack at 100+; each panel re-enables pointer
|
|
events on itself. Degraded mode ([data-dsh-panel-host-degraded]) covers
|
|
the rare shell that transforms <html>/<body> itself: the host becomes
|
|
absolute and the mount self-check pins it to the viewport every frame. */
|
|
:global([data-dsh-panel-host]) {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 25;
|
|
pointer-events: none;
|
|
/* Clip at the viewport edge: collapsed panels slide off-screen with
|
|
`transform: translate(102%)`, and a transformed element still
|
|
contributes to its ancestors' scrollable overflow — without this the
|
|
hidden panels extended the document's scrollable area, making the whole
|
|
page draggable left-right and up-down while any panel was collapsed.
|
|
The host is exactly viewport-sized (inset: 0), so the clip is invisible
|
|
until a panel slides in from the edge; internal scroll containers and
|
|
the app's fixed overlay stack are unaffected. `clip`, not `hidden`: a
|
|
hidden box stays a PROGRAMMATICALLY scrollable container, and a
|
|
scrollIntoView fired from panel content (the file tree's "Show in
|
|
folder" reveal) scrolled the host itself — shifting the whole panel,
|
|
tab bar included, out of the viewport with no way to scroll back.
|
|
The same class covers any scroll-into-view fixup — focus moving into
|
|
an off-viewport region, a nested iframe/workbench claiming focus
|
|
while it loads — so the guard belongs on the layer itself, not per
|
|
call site. The `hidden` declaration BEFORE `clip` is an old-engine
|
|
fallback, not dead style: engines without `clip` support (Safari < 16)
|
|
drop the `clip` line, which would leave this box `overflow: visible`
|
|
— no clipping at all, and the page-draggable regression above
|
|
returns. With `hidden` first those engines keep the clipping (plus
|
|
its pre-clip scrollability) while every clip-aware engine applies
|
|
the later `clip`. */
|
|
overflow: hidden;
|
|
overflow: clip;
|
|
}
|
|
|
|
:global([data-dsh-panel-host][data-dsh-panel-host-degraded]) {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
|
|
/* 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. Host-internal z-index 45
|
|
(above the panels' 40); the host's global layer is z-index 25, so the
|
|
whole cluster stays below DSH's ui-cordis dynamic-plugin panel (30) and
|
|
the app's overlay stack (menus/tooltips/modals sit at 100+), so a cordis
|
|
approval popup is never hidden behind the workbench (issue #52). Absolute
|
|
within the panel host (the host's inset: 0 makes viewport coordinates
|
|
identical). */
|
|
.toggleCluster {
|
|
position: absolute;
|
|
/* env() resolves to 0 on devices without a top inset (desktops). */
|
|
top: calc(3px + env(safe-area-inset-top));
|
|
right: 10px;
|
|
z-index: 45;
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: 4px;
|
|
pointer-events: auto;
|
|
/* The cluster recenters between the panel's tab strip (top: 3) and DSH's
|
|
session header row (top: 14, see the collapsed rule below) on every
|
|
expand/collapse — ride the panel slide's own duration/easing so the two
|
|
moves stay in lockstep instead of the cluster snapping. */
|
|
transition: top var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
|
}
|
|
|
|
/* 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):not([aria-disabled='true']) {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
.toggleButton:disabled,
|
|
.toggleButton[aria-disabled='true'] {
|
|
opacity: 0.4;
|
|
cursor: default;
|
|
}
|
|
|
|
.panel {
|
|
box-sizing: border-box;
|
|
position: absolute;
|
|
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;
|
|
/* Host-internal z-index 40 (sibling of the bottom panel's 40; the two
|
|
never overlap — the bottom panel's right edge is this panel's left
|
|
edge — and both sit below the toggle cluster's 45). The absolute
|
|
global layer comes from the host ([data-dsh-panel-host], 25) — the
|
|
whole stack stays above the app's in-flow content and below DSH's
|
|
ui-cordis dynamic-plugin panel (30) and the app's overlay stack (100+),
|
|
so menus/tooltips/modals always win over the panels (issue #52). The
|
|
corner handle is a child and stacks within the panel. */
|
|
z-index: 40;
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* Re-enable interaction inside the click-transparent panel host. */
|
|
pointer-events: auto;
|
|
/* The generic card surface (see the root contract above). Never
|
|
`--dsw-specific-sidebar-fill` — skin plugins restyle that token for the
|
|
app's own left nav and a panel consuming it loses its fill or clashes
|
|
with labels (#60/#105/#106). */
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
border-left: 1px solid var(--dsw-alias-border-l2);
|
|
/* Bottom safe-area (notched phones): the drawer's bottom content clears
|
|
the home indicator; 0 on desktops. */
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
/* 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;
|
|
/* Centered on the panel edge (an 8px hit strip straddling the border, the
|
|
same pattern as the host's own frame handles). */
|
|
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. Host-internal z-index 40 mirrors the
|
|
right panel; the absolute global layer comes from the host (25), below
|
|
DSH's ui-cordis dynamic-plugin panel (30) so the cordis inventory
|
|
surface floats above the workbench, and below the app's overlay stack
|
|
(100+) so popups win over the workbench (issue #52). */
|
|
.bottomPanel {
|
|
position: absolute;
|
|
bottom: 0;
|
|
z-index: 40;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
/* Re-enable interaction inside the click-transparent panel host. */
|
|
pointer-events: auto;
|
|
/* Bottom safe-area (notched phones); 0 on desktops. */
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
/* 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;
|
|
}
|
|
|
|
/* Layout isolation: panel-internal reflows (tabs, editors, terminals) never
|
|
leak into the app shell's layout, and the app shell's layout changes
|
|
never cascade through the panel subtree — the drag stays confined.
|
|
will-change: transform is only live while a drag is (the body attribute
|
|
toggles with the drag state), promoting the panels to their own layer
|
|
without paying the compositing cost at rest. */
|
|
.panel,
|
|
.bottomPanel {
|
|
contain: layout style;
|
|
}
|
|
|
|
body[data-dsh-sidebar-dragging] .panel,
|
|
body[data-dsh-sidebar-dragging] .bottomPanel {
|
|
will-change: transform;
|
|
}
|
|
|
|
/* The bottom panel's height drag: the top edge strip (mirror of the right
|
|
panel's width strip), centered on the border like the 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;
|
|
}
|
|
|
|
/* ── Free windows ──────────────────────────────────────────────────────── */
|
|
|
|
/* One floating tab: the same card surface as the panels, at viewport
|
|
coordinates inside the panel host (immune to shell transforms). z-index
|
|
42: above the panels (40) so a window drags across them, below the
|
|
toggle cluster (45) and the app's overlay stack (100+, issue #52).
|
|
Stacking among windows is the floats array order (siblings share the
|
|
z-index; DOM order wins). Deliberately NO box transition — the header/
|
|
resize drags write left/top/width/height per frame. Elevated surface,
|
|
so unlike the flat panels it earns a shadow token. */
|
|
.floatWindow {
|
|
position: absolute;
|
|
z-index: 42;
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* Re-enable interaction inside the click-transparent panel host. */
|
|
pointer-events: auto;
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
border-radius: 8px;
|
|
box-shadow: var(--dsw-shadow-lv3);
|
|
overflow: hidden;
|
|
contain: layout style;
|
|
}
|
|
|
|
.floatWindowDragging {
|
|
will-change: left, top, width, height;
|
|
}
|
|
|
|
/* The header: the move/dock-back drag handle, mirroring the tab strip's
|
|
chrome (34px, hairline bottom, secondary label). */
|
|
.floatHeader {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
height: 34px;
|
|
padding: 0 4px 0 10px;
|
|
font: var(--dsw-font-xxs-12);
|
|
color: var(--dsw-alias-label-secondary);
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
cursor: grab;
|
|
user-select: none;
|
|
}
|
|
|
|
.floatWindowDragging .floatHeader {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.floatTitle {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Mirrors .tabClose. */
|
|
.floatClose {
|
|
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;
|
|
}
|
|
|
|
.floatClose:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
/* Mirrors .paneContent's column-flex contract: the tab cell must hand its
|
|
child a definite WIDTH via align-stretch. A row flex (the default) sizes
|
|
the main axis (width) from content — intrinsic-zero hosts (e.g. a plugin
|
|
tab UI) collapse to a 0px-wide sliver and paint nothing (the blank
|
|
whale-report window). */
|
|
.floatContent {
|
|
flex: 1;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* The SE resize corner (mirror of .cornerHandle). */
|
|
.floatResize {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 14px;
|
|
height: 14px;
|
|
z-index: 2;
|
|
cursor: nwse-resize;
|
|
touch-action: none;
|
|
}
|
|
|
|
.floatResize:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover-accent);
|
|
}
|
|
|
|
/* A pane highlighted as the dock target while a float header drag hovers it
|
|
(the attribute is written directly by the drag at pointer cadence;
|
|
skin-addressable via [data-dsh-pane][data-dsh-float-dock-over]). */
|
|
.pane[data-dsh-float-dock-over] {
|
|
outline: 2px dashed var(--dsw-alias-interactive-bg-hover-accent);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
/* The drag-out hint over the conversation column: a dashed frame marking
|
|
the drop zone, pointer-transparent so it never disturbs the drag it
|
|
describes. z-index 46: above the windows (42) for unambiguous feedback,
|
|
still below the app's overlay stack. */
|
|
.floatDropHint {
|
|
position: absolute;
|
|
z-index: 46;
|
|
pointer-events: none;
|
|
border: 2px dashed var(--dsw-alias-interactive-bg-hover-accent);
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--dsw-alias-interactive-bg-hover-accent) 12%, transparent);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.floatDropHintLabel {
|
|
padding: 4px 12px;
|
|
border-radius: 999px;
|
|
font: var(--dsw-font-xxs-12);
|
|
color: var(--dsw-alias-label-primary);
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
}
|
|
|
|
/* ── Desktop drag-region opt-out ──────────────────────────────────────── */
|
|
|
|
/* Frameless desktop shells make the top strip of the web content draggable
|
|
(Electron `-webkit-app-region: drag`); pointer events over that strip are
|
|
swallowed as window drags unless the interactive element opts out
|
|
(issues #103/#111). The plugin's viewport-corner chrome must declare
|
|
`no-drag` itself — some shells (Tauri, older desktop builds, third-party
|
|
wrappers) have no blanket button rule. The property is a no-op in plain
|
|
browsers and in shells without drag regions — safe everywhere. */
|
|
.toggleCluster,
|
|
.toggleButton,
|
|
.tabBar,
|
|
.floatHeader {
|
|
-webkit-app-region: no-drag;
|
|
}
|
|
|
|
/* ── Position compatibility mode (Windows native title bar) ────────────── */
|
|
|
|
/* Windows frameless / hidden-title-bar windows draw the native caption
|
|
strip (minimize / maximize / close) at the window's top-right corner,
|
|
OVER the web content. When the user enables the "position compatibility"
|
|
pref, the sidebar yields the strip: the toggle cluster drops below it and
|
|
the right panel's content starts below it — the panel's surface still
|
|
spans the full height, so the caption buttons float over empty sidebar
|
|
fill (the VSCode hidden-title-bar look). The strip height is user-tunable
|
|
(`--dsh-title-bar-strip`, written by the Sidebar shell from
|
|
`titleBarStripPx`; 40px fallback). The bottom panel is unaffected: it
|
|
sits at the window's bottom, never under the top strip. */
|
|
:global(body[data-dsh-title-bar-compat]) .toggleCluster {
|
|
top: calc(var(--dsh-title-bar-strip, 40px) + 3px);
|
|
}
|
|
|
|
:global(body[data-dsh-title-bar-compat]) .panel {
|
|
/* The caption strip is drawn over the panel's own top edge: keep the
|
|
surface full-height and push the content (the tab strip) below the
|
|
strip instead. .panelResize is absolutely positioned and unaffected. */
|
|
padding-top: var(--dsh-title-bar-strip, 40px);
|
|
}
|
|
|
|
/* ── Collapsed cluster alignment with the DSH web session header ───────── */
|
|
|
|
/* While the right panel is open, the cluster rides the panel's 34px tab
|
|
strip (28px buttons at top: 3). Collapsed, it floats over DSH's own
|
|
session header instead — whose controls (title, "Session log" capsule)
|
|
are vertically centered at 28px on both desktop and narrow viewports.
|
|
top: 3 then strands the cluster ~11px above that row; re-center the
|
|
28px buttons at 14px. The attribute disappears while the panel is open,
|
|
so the tab-strip alignment above is untouched. With position
|
|
compatibility active the DSH header sits below the caption strip, hence
|
|
strip + 14. */
|
|
:global(body[data-dsh-sidebar-collapsed]) .toggleCluster {
|
|
top: calc(14px + env(safe-area-inset-top));
|
|
}
|
|
|
|
:global(body[data-dsh-sidebar-collapsed][data-dsh-title-bar-compat]) .toggleCluster {
|
|
top: calc(var(--dsh-title-bar-strip, 40px) + 14px);
|
|
}
|
|
|
|
/* 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. Rendered INSIDE the right panel and positioned
|
|
relative to it (left edge + the bottom panel's height from the layout
|
|
variable), so no JS-written viewport coordinates are involved. The
|
|
z-index is panel-local (above the panel's content; the panel itself sits
|
|
at 40, below the app's overlay stack). */
|
|
.cornerHandle {
|
|
position: absolute;
|
|
left: -6px;
|
|
bottom: calc(var(--dsh-sidebar-height, 0px) + 6px);
|
|
width: 12px;
|
|
height: 12px;
|
|
z-index: 2;
|
|
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;
|
|
}
|
|
|
|
/* One tab's descriptor badge (a count capped at 99+, or a short text). */
|
|
.tabBadge {
|
|
flex: none;
|
|
min-width: 16px;
|
|
height: 15px;
|
|
padding: 0 4px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 8px;
|
|
font: var(--dsw-font-xxxs-strong-11);
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-brand-primary);
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
/* ── Pinned-terminal tab (v0.17.0+) ───────────────────────────────────── */
|
|
|
|
/* Pinned virtual tabs (injected from other sessions) render inline in the
|
|
tab bar with a pin icon and a subtle tint to distinguish them from the
|
|
session's own tabs. Token-driven so skins follow. */
|
|
.pinnedTab {
|
|
color: var(--dsw-alias-label-tertiary);
|
|
font-style: italic;
|
|
}
|
|
|
|
.pinnedTab:hover {
|
|
color: var(--dsw-alias-label-secondary);
|
|
}
|
|
|
|
/* ── 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;
|
|
/* Deep trees truncate their labels instead of scrolling horizontally. */
|
|
overflow-x: hidden;
|
|
padding: 4px 8px 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;
|
|
/* border-box keeps the inline indent padding (depth * 22 + 6) INSIDE the
|
|
row's 100% width — without it deep rows overflow and force a horizontal
|
|
scrollbar on the tree body. */
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
max-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);
|
|
}
|
|
|
|
/* A "Show in folder" reveal highlight: the target row stays tinted until
|
|
the transient reveal set is cleared (a reload starts unhighlighted).
|
|
The DSH business-blue surface (the same source the host's nav items use
|
|
for their active accent) — a flat tint, deliberately no left bar. */
|
|
.explorerRowRevealed {
|
|
background: var(--dsw-alias-state-business-tertiary);
|
|
}
|
|
|
|
/* Reveal sets usually list sibling files — consecutive tinted rows merge
|
|
into ONE rounded block: the touching edges square off, so a multi-file
|
|
highlight has no per-row radius notches. A solo revealed row (or the
|
|
first/last of a run) keeps its full radius. Rows are flat siblings in
|
|
the tree body (depth is inline padding), so the sibling combinator sees
|
|
every adjacency. */
|
|
.explorerRowRevealed + .explorerRowRevealed {
|
|
border-top-left-radius: 0;
|
|
border-top-right-radius: 0;
|
|
}
|
|
|
|
.explorerRowRevealed:has(+ .explorerRowRevealed) {
|
|
border-bottom-left-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
|
|
.explorerDir {
|
|
font: var(--dsw-font-s-strong-14);
|
|
}
|
|
|
|
.explorerHidden {
|
|
opacity: 0.45;
|
|
}
|
|
|
|
/* The link badge on symlink rows (folder and file rows alike). */
|
|
.explorerSymlink {
|
|
flex: none;
|
|
color: var(--dsw-alias-label-tertiary);
|
|
}
|
|
|
|
/* A symlink whose target is missing: error ink on the name (the row stays
|
|
clickable — the editor surfaces the read failure). */
|
|
.explorerBroken .explorerName {
|
|
color: var(--dsw-alias-state-error-primary);
|
|
}
|
|
|
|
.explorerName {
|
|
/* min-width: 0 lets the flex item shrink below its content so the label
|
|
truncates with an ellipsis instead of widening the row. */
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ── Inline rename editor ─────────────────────────────────────────────────
|
|
The row swaps its name span for a plain input (same row metrics: 34px
|
|
height, 6px gap, xxs type); only tokens, no hardcoded colors. */
|
|
.explorerRenaming {
|
|
cursor: text;
|
|
}
|
|
|
|
.explorerRenameInput {
|
|
box-sizing: border-box;
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
height: 24px;
|
|
padding: 0 6px;
|
|
border: 1px solid var(--dsw-alias-state-business-primary);
|
|
border-radius: 6px;
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
color: var(--dsw-alias-label-primary);
|
|
font: var(--dsw-font-xxs-12);
|
|
}
|
|
|
|
.explorerRenameInput:focus {
|
|
outline: none;
|
|
border-color: var(--dsw-alias-state-business-primary);
|
|
}
|
|
|
|
/* The dismissable mutation-failure strip above the root row (rename/delete
|
|
refusals — raw server text, same policy as the fence notice). */
|
|
.explorerActionError {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
margin-bottom: 4px;
|
|
padding: 6px 8px;
|
|
border: 1px solid var(--dsw-alias-state-error-primary);
|
|
border-radius: 8px;
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
}
|
|
|
|
.explorerActionErrorText {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
font: var(--dsw-font-xxs-12);
|
|
color: var(--dsw-alias-state-error-primary);
|
|
}
|
|
|
|
.explorerActionErrorClose {
|
|
flex: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 20px;
|
|
height: 20px;
|
|
padding: 0;
|
|
border: none;
|
|
border-radius: 5px;
|
|
background: transparent;
|
|
color: var(--dsw-alias-label-tertiary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.explorerActionErrorClose:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
/* The delete-confirmation modal's description paragraph (the git lens's
|
|
.gitConfirmDesc sibling — same quiet single-question look). */
|
|
.explorerConfirmDesc {
|
|
margin: 0;
|
|
font: var(--dsw-font-s-14);
|
|
color: var(--dsw-alias-label-primary);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* The hover-revealed @-reference button at the row's far right (and the
|
|
transient "copied" label that replaces it after a copy). The only auto
|
|
margin in the row absorbs the filename's spare width, so a short name
|
|
cannot leave either affordance stranded in the middle. */
|
|
.explorerRef,
|
|
.explorerCopied {
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* Hidden rows keep the name at full width; hovering or focusing the row
|
|
reveals the reference button. */
|
|
.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;
|
|
}
|
|
|
|
/* Drag a file/folder over a tree row: the target directory row gets a filled
|
|
highlight (drops land inside it; file rows target their parent directory).
|
|
The zone frame around the whole tree is the portaled .uploadDropZone. */
|
|
.explorerRowDropTarget {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
outline: 1px dashed var(--dsw-alias-interactive-bg-hover-accent);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
/* The sidebar's drop surface: a pointer-inert frame portaled to
|
|
document.body, fixed over the explorer body's viewport rect at z-1001 —
|
|
above DSH's own whole-page drop mask (z-1000, see InputBar's
|
|
document-level intake). The giant box-shadow spread IS the full-viewport
|
|
mask (same token DSH uses for its drop mask): everything dims except the
|
|
zone rect, teaching the zone split — the dimmed conversation column still
|
|
takes drops into the chat natively, the clear frame marks the tree as the
|
|
workspace-upload zone. Deliberate exception to the "panel stays below the
|
|
DSH float stack" rule: transient and pointer-events none, so the drop
|
|
always lands on the row beneath. The hint pill docks at the TOP edge of
|
|
the zone (right under the search row, the first thing the eye meets),
|
|
leaving the rows — the actual drop targets — aimable. */
|
|
.uploadDropZone {
|
|
position: fixed;
|
|
z-index: 1001;
|
|
pointer-events: none;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding: 12px;
|
|
border: 2px dashed var(--dsw-alias-interactive-bg-hover-accent);
|
|
border-radius: 10px;
|
|
box-shadow: 0 0 0 200vmax var(--dsw-alias-bg-mask-drop);
|
|
animation: dsh-row-in 150ms var(--ds-ease-in-out);
|
|
}
|
|
|
|
/* The zone's drop hero, docked at the TOP edge (right under the search
|
|
row, the first thing the eye meets): the colorful tray illustration over
|
|
the hint pill — stacked so the rows below stay aimable. */
|
|
.uploadDropHero {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
max-width: 100%;
|
|
padding-top: 8px;
|
|
}
|
|
|
|
.uploadDropZonePill {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
max-width: 100%;
|
|
padding: 6px 12px;
|
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
border-radius: 999px;
|
|
background: var(--dsw-alias-bg-layer-2);
|
|
box-shadow: var(--dsw-shadow-lv2);
|
|
color: var(--dsw-alias-label-primary);
|
|
font: var(--dsw-font-xxs-strong-12);
|
|
}
|
|
|
|
.uploadDropZoneText {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* The left zone's invitation: centered in the viewport space beside the
|
|
tree (inline width = the zone's left edge), telling the user the dimmed
|
|
conversation column still accepts drops — into the chat, natively. */
|
|
.uploadDropChatHint {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 1002;
|
|
pointer-events: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
animation: dsh-row-in 150ms var(--ds-ease-in-out);
|
|
}
|
|
|
|
/* The invitation's content: the colorful photo-cards illustration over the
|
|
label — the drop overlay is this flow's one brand moment, so it gets the
|
|
color the rest of the UI never does (palette borrowed from DSH's native
|
|
drop illustration so the two zones read as one family). */
|
|
.uploadDropChatCard {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 12px;
|
|
max-width: 100%;
|
|
text-align: center;
|
|
color: var(--dsw-alias-label-primary);
|
|
font: var(--dsw-font-s-strong-14);
|
|
}
|
|
|
|
/* Full-window upload progress: blurred scrim + card (same mask/backdrop
|
|
tokens as the repo's Modal primitive; absolute within the tree panel so it
|
|
never covers the conversation column). */
|
|
.uploadOverlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 30;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--dsw-alias-bg-mask-1);
|
|
backdrop-filter: var(--dsw-mask-blur);
|
|
animation: dsh-row-in 150ms var(--ds-ease-in-out);
|
|
}
|
|
|
|
.uploadOverlayCard {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
min-width: 280px;
|
|
max-width: min(420px, calc(100% - 48px));
|
|
padding: 20px 24px;
|
|
border: 1px solid var(--dsw-alias-border-inverted);
|
|
border-radius: 24px;
|
|
background: var(--dsw-alias-bg-layer-2);
|
|
box-shadow: var(--dsw-shadow-lv3);
|
|
}
|
|
|
|
.uploadOverlayTitle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font: var(--dsw-font-s-strong-14);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
.uploadOverlayTitle > svg {
|
|
flex: none;
|
|
}
|
|
|
|
.uploadOverlayTitle > span {
|
|
min-width: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.uploadOverlayProgress {
|
|
height: 6px;
|
|
border-radius: 3px;
|
|
background: var(--dsw-alias-border-l2);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.uploadOverlayProgressFill {
|
|
height: 100%;
|
|
border-radius: 3px;
|
|
background: var(--dsw-alias-interactive-bg-hover-accent);
|
|
transition: width 150ms var(--ds-ease-in-out);
|
|
}
|
|
|
|
.uploadOverlayStatus {
|
|
min-height: 1em;
|
|
font: var(--dsw-font-xxs-12);
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--dsw-alias-label-tertiary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.uploadOverlayCancel {
|
|
align-self: flex-end;
|
|
height: 28px;
|
|
padding: 0 14px;
|
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
color: var(--dsw-alias-label-primary);
|
|
font: var(--dsw-font-xxs-strong-12);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.uploadOverlayCancel:hover:not(:disabled) {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
border-color: var(--dsw-alias-border-l2);
|
|
}
|
|
|
|
.uploadOverlayCancel:disabled {
|
|
opacity: 0.4;
|
|
cursor: default;
|
|
}
|
|
|
|
/* ── Editor ────────────────────────────────────────────────────────────── */
|
|
|
|
.editor {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.editorHeader {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 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;
|
|
}
|
|
|
|
/* Merged mode (the editorExplorer pref): the header's path input replaces
|
|
the plain title, and a toggleable file tree docks at the tab's right edge. */
|
|
.editorPathInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
box-sizing: border-box;
|
|
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);
|
|
}
|
|
|
|
.editorPathInput:focus {
|
|
outline: none;
|
|
border-color: var(--dsw-alias-border-l2);
|
|
}
|
|
|
|
/* The tree-panel toggle in the header: active = panel open (the selected
|
|
fill of the tab strip). */
|
|
.editorTreeToggleActive {
|
|
color: var(--dsw-alias-label-primary);
|
|
background: var(--dsw-alias-interactive-bg-active);
|
|
}
|
|
|
|
.editorBody {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
}
|
|
|
|
.editorMain {
|
|
flex: 1;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* The docked file tree: the wrapper owns the (drag-adjustable) width and
|
|
the hairline separation; the resize handle is a wider invisible strip on
|
|
its left edge with a hover affordance. */
|
|
.editorTreeDock {
|
|
position: relative;
|
|
flex: none;
|
|
min-height: 0;
|
|
display: flex;
|
|
border-left: 1px solid var(--dsw-alias-border-l1);
|
|
}
|
|
|
|
.editorTreeResize {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
/* Inside the dock (left edge): pointer capture on the handle keeps the
|
|
drag alive even when the pointer leaves the strip. */
|
|
left: 0;
|
|
width: 6px;
|
|
cursor: col-resize;
|
|
touch-action: none;
|
|
z-index: 3;
|
|
}
|
|
|
|
.editorTreeResize:hover {
|
|
background: var(--dsw-alias-border-l2);
|
|
}
|
|
|
|
.editorTreePanel {
|
|
/* Relative so the upload overlay and drop pill can cover the tree exactly. */
|
|
position: relative;
|
|
flex: 1;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* The tree panel's full-window form (the `full` prop): fill the tab body. */
|
|
.editorTreePanelFull {
|
|
flex: 1;
|
|
}
|
|
|
|
.editorTreeSearch {
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 8px;
|
|
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
}
|
|
|
|
.editorSearchInput {
|
|
flex: 1;
|
|
min-width: 0;
|
|
height: 26px;
|
|
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);
|
|
}
|
|
|
|
.editorSearchInput:focus {
|
|
outline: none;
|
|
border-color: var(--dsw-alias-border-l2);
|
|
}
|
|
|
|
.editorSearchHint {
|
|
padding: 8px 12px;
|
|
font: var(--dsw-font-xxs-12);
|
|
color: var(--dsw-alias-label-tertiary);
|
|
}
|
|
|
|
.editorSearchResult {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 4px 8px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: var(--dsw-alias-label-primary);
|
|
font: var(--dsw-font-xxs-12);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.editorSearchResult:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
}
|
|
|
|
.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 (legacy 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);
|
|
}
|
|
|
|
/* The workspace-fence refusal row (FenceErrorNotice): reason text plus the
|
|
* one-click global-off button share one baseline, wrapping when narrow. */
|
|
.fenceError {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
padding: 8px 16px;
|
|
font: var(--dsw-font-xxs-12);
|
|
color: var(--dsw-alias-state-error-primary);
|
|
}
|
|
|
|
.editorBanner {
|
|
flex: none;
|
|
padding: 4px 8px;
|
|
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%;
|
|
}
|
|
|
|
/* Code lines get the same 12/16 inset as the markdown preview, so toggling
|
|
preview/edit never jumps the text's resting position. */
|
|
.editorCm :global(.cm-scroller) {
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.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: 12px 16px;
|
|
font: var(--dsw-font-xs-13);
|
|
}
|
|
|
|
/* The DSH CodeBlock banner is position:sticky (top:0, z-index:6) so a block
|
|
keeps its copy action reachable inside a per-message scroll container —
|
|
the right call in chat, where each block owns its scroll box. In the file
|
|
preview every fence shares one `.editorMd` scroll container, so that sticky
|
|
banner pins to the top of the viewport until the *whole block* scrolls
|
|
past, which reads as a floating header over the document. Neutralize the
|
|
stickiness here (bannerWrap is the first element child of `.md-code-block`;
|
|
mermaid mounts are excluded since their own chrome replaces the banner). */
|
|
.editorMd :global(.md-code-block:not([data-mermaid-processed])) > :global(div):first-child {
|
|
position: static;
|
|
z-index: auto;
|
|
}
|
|
|
|
/* ── Mermaid diagram block (markdown preview, mermaid lazy chunk) ────────
|
|
Mirrors the md-code-block chrome the DSH CodeBlock renders, so a diagram
|
|
reads as a peer of the surrounding fences: hairline border, layer-2
|
|
header with the info string + copy action, layer-1 body. The SVG body is
|
|
static (no bindFunctions) and sanitized (no foreignObject/script/@attrs/
|
|
hrefs), so it carries no event surface beyond the copy button; the body
|
|
click opens the zoom modal on a sanitized clone. */
|
|
|
|
.mermaidWrap {
|
|
margin: 6px 0;
|
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
border-radius: 6px;
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mermaidHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 6px;
|
|
padding: 4px 8px;
|
|
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
background: var(--dsw-alias-bg-layer-2);
|
|
}
|
|
|
|
.mermaidInfo {
|
|
font: var(--dsw-font-xxxs-strong-11);
|
|
color: var(--dsw-alias-label-tertiary);
|
|
}
|
|
|
|
.mermaidCopy {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 0 6px;
|
|
height: 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
color: var(--dsw-alias-label-secondary);
|
|
font: var(--dsw-font-xxxs-11);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mermaidCopy:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
.mermaidBody {
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 10px;
|
|
overflow: auto;
|
|
cursor: zoom-in;
|
|
}
|
|
|
|
.mermaidBody :global(svg) {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
.mermaidError {
|
|
padding: 6px 10px;
|
|
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
color: var(--dsw-alias-state-error-primary);
|
|
font: var(--dsw-font-xxxs-11);
|
|
}
|
|
|
|
/* The diagram source shown under a render error (helps fixing the fence). */
|
|
.mermaidCode {
|
|
margin: 0;
|
|
padding: 8px 10px;
|
|
overflow: auto;
|
|
font: var(--dsw-font-xxxs-11);
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
}
|
|
|
|
/* The mermaid markdown container (single MarkdownText pass): a swapped
|
|
code block keeps its React-managed .md-code-block wrapper in the tree
|
|
(reconciliation safety), but the wrapper's own box is suppressed so only
|
|
the diagram chrome renders; the swapped-in host div flows as a block. */
|
|
.mermaidMarkdown :global(.md-code-block[data-mermaid-processed]) {
|
|
display: contents;
|
|
}
|
|
|
|
/* ── Mermaid zoom modal (click-to-enlarge; portalled to document.body) ──
|
|
Fixed overlay above the DSH overlay stack (1000+), token-driven surface.
|
|
The stage centers the (sanitized, cloned) svg and the modal owns the
|
|
transform; the drag/zoom listeners live in the component, nothing here
|
|
targets global class names. */
|
|
|
|
.mermaidModal {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 1000;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--dsw-alias-bg-mask-1);
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
.mermaidModalToolbar {
|
|
position: absolute;
|
|
top: 16px;
|
|
right: 16px;
|
|
display: flex;
|
|
gap: 8px;
|
|
z-index: 10;
|
|
}
|
|
|
|
.mermaidModalButton {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
background: var(--dsw-alias-bg-layer-2);
|
|
color: var(--dsw-alias-label-primary);
|
|
font: var(--dsw-font-xs-strong-13);
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.mermaidModalButton:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
}
|
|
|
|
.mermaidModalStage {
|
|
width: 90vw;
|
|
height: 80vh;
|
|
overflow: hidden;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.mermaidModalStage :global(svg) {
|
|
max-width: none;
|
|
max-height: none;
|
|
cursor: grab;
|
|
transform-origin: center center;
|
|
user-select: none;
|
|
-webkit-user-drag: none;
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.mermaidModalStage :global(svg):active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.mermaidModalHint {
|
|
position: absolute;
|
|
bottom: 16px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
color: var(--dsw-alias-label-tertiary);
|
|
font: var(--dsw-font-xxxs-11);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* ── 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);
|
|
}
|
|
|
|
/* The node-pty dependency failure banner (issue #140): a taller warn block
|
|
carrying the pasteable repair command (bash / cmd / PowerShell). */
|
|
.terminalDepsBanner {
|
|
flex: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding: 10px;
|
|
font: var(--dsw-font-xxs-12);
|
|
color: var(--dsw-alias-state-warn-label);
|
|
background: var(--dsw-alias-state-warn-tertiary);
|
|
}
|
|
|
|
.terminalDepsTitle {
|
|
font: var(--dsw-font-xxs-strong-12);
|
|
color: var(--dsw-alias-state-warn-primary);
|
|
}
|
|
|
|
.terminalDepsHint {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.terminalDepsCommandRow {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* The command the user pastes into a terminal / cmd. Selectable, wrapping,
|
|
and scrollable when long (Windows paths can get very long). */
|
|
.terminalRepairCommand {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin: 0;
|
|
padding: 6px 8px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 11px;
|
|
line-height: 1.5;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
user-select: text;
|
|
color: var(--dsw-alias-label-primary);
|
|
background: var(--dsw-alias-bg-layer-2);
|
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
border-radius: 4px;
|
|
max-height: 160px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.terminalDepsNote {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.terminalDepsActions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* The PER-TAB error strip: fills one tab's content area (unlike
|
|
.boundaryError, the full-height fixed rail for shell-level crashes), so a
|
|
crashing viewer/editor never hides the toggle cluster or the other tabs. */
|
|
.tabBoundaryError {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
align-items: flex-start;
|
|
padding: 12px 16px;
|
|
overflow: auto;
|
|
font: var(--dsw-font-xxs-12);
|
|
color: var(--dsw-alias-state-error-primary);
|
|
}
|
|
|
|
/* ── Diff tab (generic states + shell; the changes tab owns the rest in
|
|
changes.module.css, and the diff rows themselves live in
|
|
diff/diff.module.css) ────────────────────────────────────────────────── */
|
|
|
|
.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;
|
|
}
|
|
|
|
/* The diff tab (a change expanded from the changes tab's preview, like
|
|
VSCode's diff editor): header + scrollable diff body, no horizontal
|
|
panning; rows render through the shared diff module. */
|
|
.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 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. */
|
|
|
|
/* 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). */
|
|
|
|
/* ── 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,
|
|
.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,
|
|
.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;
|
|
}
|
|
}
|
|
|
|
/* The file tree "open with" menu rows: the label area is a BLOCK-level flex
|
|
row that fills the primitives `.itemLabel` span (which is itself `flex: 1`
|
|
inside the row), so the trailing control — the pushpin on submenu rows,
|
|
the chevron on the submenu parent — docks at the row's far right, exactly
|
|
like the trailing check on selected rows of every other menu. */
|
|
.openWithLabel {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.openWithName {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The trailing "expands into a submenu" chevron on the open-with parent row
|
|
(the primitives Menu renders no arrow for submenu parents). */
|
|
.openWithChevron {
|
|
flex: none;
|
|
color: var(--dsw-alias-label-tertiary);
|
|
}
|
|
|
|
/* The pushpin toggle on an "open with" submenu row: brand ink once the
|
|
target is pinned to the menu's top level. Sized for the compact menu's
|
|
26px rows (16px box, 12px glyph) — proportionate without overflowing. */
|
|
.openWithPin {
|
|
flex: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 5px;
|
|
color: var(--dsw-alias-label-tertiary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.openWithPin:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
.openWithPinActive {
|
|
color: var(--dsw-alias-state-business-primary);
|
|
}
|
|
|
|
/* ── Markdown preview: raw-HTML runs + table of contents ─────────────────
|
|
The HTML runs lifted out of the markdown source (MarkdownHtml.tsx) render
|
|
as sanitized block leaves; the TOC is a zero-height sticky bar keeping the
|
|
outline button pinned at the preview's top-right without consuming layout.
|
|
All visuals ride the alias tokens (skin system follows automatically). */
|
|
|
|
/* A sanitized HTML run leaf (block-level box; content is DOMPurify output). */
|
|
.editorHtmlBlock {
|
|
margin: 8px 0;
|
|
}
|
|
|
|
.editorHtmlBlock img,
|
|
.editorHtmlBlock video {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.editorHtmlBlock details {
|
|
margin: 4px 0;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.editorHtmlBlock summary {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* The zero-height sticky bar: occupies no layout, keeps the button visible
|
|
while the preview scrolls, and lets pointer events pass through the bar
|
|
itself (only the interactive children re-enable them). The bar's stacking
|
|
context also frames the outline panel, so its z-index must clear the DSH
|
|
CodeBlock banner (z-index:6) or the popover paints beneath a sticky fence
|
|
header. 7 sits just above that banner and below the portalled selection
|
|
popup (60) / mermaid modal (1000). */
|
|
.tocBar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 7;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
height: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tocButton {
|
|
pointer-events: auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 26px;
|
|
height: 26px;
|
|
margin: 4px 2px 0 0;
|
|
padding: 0;
|
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
border-radius: 6px;
|
|
background: var(--dsw-alias-bg-layer-1);
|
|
color: var(--dsw-alias-label-secondary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tocButton:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
/* The outline popover, anchored under the button (the bar is the containing
|
|
block; its height is 0, so the panel drops below the pinned button row). */
|
|
.tocPanel {
|
|
position: absolute;
|
|
top: 32px;
|
|
right: 2px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: min(300px, 82%);
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
padding: 4px;
|
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
border-radius: 8px;
|
|
background: var(--dsw-alias-bg-layer-2);
|
|
box-shadow: var(--dsw-shadow-lv2);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.tocItem {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
padding: 4px 8px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: var(--dsw-alias-label-secondary);
|
|
font: var(--dsw-font-xxs-12);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.tocItem:hover {
|
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
color: var(--dsw-alias-label-primary);
|
|
}
|
|
|
|
/* Indent by heading level (h2 = one step, h3 = two steps…). */
|
|
.tocItem[data-level='2'] { padding-left: 18px; }
|
|
.tocItem[data-level='3'] { padding-left: 28px; }
|
|
.tocItem[data-level='4'] { padding-left: 38px; }
|
|
.tocItem[data-level='5'] { padding-left: 48px; }
|
|
.tocItem[data-level='6'] { padding-left: 58px; }
|
|
|
|
/* The tiny level number keeps deep outlines scannable. */
|
|
.tocItemLevel {
|
|
flex: none;
|
|
font: var(--dsw-font-xxxs-11);
|
|
color: var(--dsw-alias-label-tertiary);
|
|
}
|
|
|
|
.tocItemText {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The jumped-to heading flashes once (the class is added to the host-rendered
|
|
heading element by md-toc.tsx; @keyframes rides the CSS module). */
|
|
@keyframes dsh-toc-flash {
|
|
0%, 60% { background: var(--dsw-alias-interactive-bg-hover); }
|
|
100% { background: transparent; }
|
|
}
|
|
|
|
.tocFlash {
|
|
animation: dsh-toc-flash 1.2s ease-out;
|
|
border-radius: 4px;
|
|
}
|