/* Reset */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
overflow: hidden;
font-family: var(--wsb-font-sans);
font-size: var(--wsb-font-size-base);
line-height: 1.5;
color: var(--vscode-editor-foreground);
background: var(--vscode-editor-background);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "cv11", "ss01";
text-rendering: optimizeLegibility;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: var(--wb-control-size-6);
height: var(--wb-control-size-6);
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--vscode-panel-border);
border-radius: var(--wb-corner-radius-3);
}
::-webkit-scrollbar-thumb:hover {
background: var(--vscode-disabledForeground);
}
/* Selection */
::selection {
background: var(--vscode-focusBorder);
color: white;
}
/* Focus visible */
:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 1px;
}
/* Button reset */
button {
font: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Input reset */
input,
select {
font: inherit;
color: inherit;
}
/* Code */
code,
pre {
font-family: var(--wsb-font-mono);
font-size: var(--wsb-font-size-base);
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
/* Utility: visually hidden */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Skeleton shimmer — global utility for loading placeholders */
@keyframes skel-shimmer {
0% { background-position: -400px 0; }
100% { background-position: 400px 0; }
}
.skel {
border-radius: var(--wsb-radius-sm);
background: linear-gradient(
90deg,
var(--vscode-panel-border) 0%,
color-mix(in srgb, var(--vscode-panel-border) 60%, var(--vscode-sideBar-background)) 40%,
var(--vscode-panel-border) 80%
);
background-size: 800px 100%;
animation: skel-shimmer 1.6s ease-in-out infinite;
}
/* Mark tag (search highlights) */
mark {
background: var(--vscode-editorWarning-foreground);
color: var(--vscode-editor-foreground);
border-radius: var(--wb-corner-radius-2);
padding: 0 var(--wb-spacing-2);
}
/* In-session find highlights */
mark.search-highlight {
background: color-mix(in srgb, var(--vscode-editorWarning-foreground) 35%, transparent);
color: inherit;
border-radius: var(--wb-corner-radius-2);
padding: 0 var(--wb-spacing-1);
}
mark.search-highlight--current {
background: var(--vscode-editorWarning-foreground);
color: var(--vscode-editor-foreground);
outline: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground) 80%, var(--vscode-editor-foreground));
outline-offset: 1px;
}
/* Modal shared styles */
.modal-overlay {
position: fixed;
inset: 0;
background: var(--vscode-widget-shadow);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-panel {
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-lg);
box-shadow: var(--wb-shadow-md);
overflow: hidden;
}
.modal-panel .modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--wsb-space-5) var(--wsb-space-6);
border-bottom: 1px solid var(--vscode-panel-border);
}
.modal-panel .modal-title {
font-size: var(--wsb-font-size-base);
font-weight: 600;
color: var(--vscode-editor-foreground);
}
.modal-panel .modal-close {
width: var(--wb-icon-button-size);
height: var(--wb-icon-button-size);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--wsb-font-size-lg);
color: var(--vscode-disabledForeground);
border-radius: var(--wsb-radius-sm);
}
.modal-panel .modal-close:hover {
background: var(--vscode-list-hoverBackground);
color: var(--vscode-editor-foreground);
}
.modal-panel .modal-body {
padding: var(--wsb-space-6);
}
.modal-panel .modal-btn {
height: var(--wb-control-height);
padding: 0 var(--wsb-space-5);
border-radius: var(--wsb-radius-sm);
font-size: var(--wsb-font-size-md);
font-weight: 500;
cursor: pointer;
background: var(--vscode-list-hoverBackground);
color: var(--vscode-foreground);
border: 1px solid var(--vscode-panel-border);
}
.modal-panel .modal-btn:hover {
background: var(--vscode-input-background);
color: var(--vscode-editor-foreground);
}
.modal-panel .modal-btn-primary {
background: var(--vscode-focusBorder);
color: white;
border-color: var(--vscode-focusBorder);
}
.modal-panel .modal-btn-primary:hover {
opacity: 0.9;
}
.modal-panel .modal-btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.modal-panel .modal-error {
font-size: var(--wsb-font-size-md);
color: var(--vscode-errorForeground, #f85149);
background: var(--vscode-input-background);
padding: var(--wsb-space-4) var(--wsb-space-5);
border-radius: var(--wsb-radius-sm);
border: 1px solid var(--vscode-errorForeground, #f85149);
word-break: break-word;
}
.modal-panel .modal-spinner {
width: var(--wb-icon-size-24);
height: var(--wb-icon-size-24);
border: 2px solid var(--vscode-panel-border);
border-top-color: var(--vscode-focusBorder);
border-radius: 50%;
animation: modal-spin 0.8s linear infinite;
}
@keyframes modal-spin {
to {
transform: rotate(360deg);
}
}
/* ---------------------------------------------------------------------------
Workbench primitives
---------------------------------------------------------------------------
The vocabulary the panels and pages already speak — `.primary`, `.secondary`,
`.danger`, `.field`, `.section-title`, `.chip` — declared ONCE, here.
They were spelled out again in every component that used them, which is how
two buttons a centimetre apart came to have different radii and a chip in
one panel looked nothing like the same chip in the next. A component keeps
its LAYOUT — what sits beside what, and how wide — and takes its LOOK from
here. A component that needs a look this does not have extends this file
rather than inventing one locally.
Svelte scopes component styles, so a component that still declares its own
copy keeps winning on specificity: adopting these is a deletion, not a
rewrite, and can happen a panel at a time.
------------------------------------------------------------------------ */
.primary,
.secondary,
.danger,
.danger-outline {
padding: var(--wsb-space-2) var(--wb-spacing-9);
font-size: var(--wsb-font-size-sm);
border-radius: var(--wsb-radius-sm);
cursor: pointer;
color: inherit;
font-family: inherit;
}
.primary {
background: color-mix(in srgb, var(--vscode-focusBorder) 22%, transparent);
border: 1px solid var(--vscode-focusBorder);
}
.secondary {
background: color-mix(in srgb, currentColor 8%, transparent);
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
}
.danger {
background: color-mix(in srgb, var(--wb-error) 20%, transparent);
border: 1px solid var(--wb-error);
}
/* Arms a destructive act without being one: the outline asks, the solid one
does it. Two looks because they are two steps. */
.danger-outline {
background: none;
border: 1px solid color-mix(in srgb, var(--wb-error) 50%, transparent);
color: var(--wb-error);
}
.primary:disabled,
.secondary:disabled,
.danger:disabled,
.danger-outline:disabled {
opacity: 0.5;
cursor: default;
}
/* A button that is a word, not a control: inline actions inside a line of
text, where a bordered button would break the sentence. */
.quiet {
background: none;
border: none;
padding: 0 var(--wsb-space-1);
font: inherit;
font-size: var(--wsb-font-size-sm);
color: var(--vscode-textLink-foreground);
cursor: pointer;
}
.quiet:hover {
text-decoration: underline;
}
.tiny {
padding: var(--wb-spacing-1) var(--wsb-space-3);
font-size: var(--wb-font-size-10-5);
}
.button-row {
display: flex;
gap: var(--wsb-space-2);
flex-wrap: wrap;
align-items: center;
}
/* One labelled control. The label sits above the input because a workbench
panel is a column, and beside it the two would fight for the width. */
.field {
display: flex;
flex-direction: column;
gap: var(--wsb-space-1);
font-size: var(--wsb-font-size-sm);
}
.field-label,
.subtle {
color: var(--vscode-descriptionForeground);
}
.field-input {
padding: var(--wb-spacing-3) var(--wsb-space-3);
font-size: var(--wsb-font-size-sm);
font-family: inherit;
border-radius: var(--wsb-radius-sm);
border: 1px solid var(--vscode-input-border, color-mix(in srgb, currentColor 25%, transparent));
background: var(--vscode-input-background, transparent);
color: var(--vscode-input-foreground, inherit);
}
.field-input:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: -1px;
}
.check {
display: flex;
align-items: center;
gap: var(--wsb-space-3);
font-size: var(--wsb-font-size-sm);
color: var(--vscode-descriptionForeground);
cursor: pointer;
}
.section-title {
padding: var(--wsb-space-4) 0 var(--wb-spacing-3);
font-size: var(--wsb-font-size-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--vscode-descriptionForeground);
}
/* What is not there, said in the same voice everywhere. */
.empty {
color: var(--vscode-descriptionForeground);
font-size: var(--wsb-font-size-sm);
}
/* ---------------------------------------------------------------------------
A sidebar panel's own chrome
---------------------------------------------------------------------------
Every panel names itself in the same bar, in the same place, and puts what
it can DO to itself at the right end of that bar. The explorer established
it; the rest each had their own idea. Four panels had no title at all, five
spelled the bar `sidebar-header` with an ad-hoc `↺` glyph for refresh, one
called it `ext-header`, and one put its refresh in a floating strip below
the top — so "where is refresh" had six answers and, on the boards panel,
none.
A panel keeps its BODY. This is the bar above it.
------------------------------------------------------------------------ */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--wsb-space-4);
/* Height rather than vertical padding: the bar has to be the same height as
the kit's tab strip beside it whatever is in it, and padding makes the
height a consequence of the tallest control instead. */
height: var(--wsb-header-height);
padding: 0 var(--wsb-panel-padding-inline);
flex-shrink: 0;
border-bottom: 1px solid var(--vscode-panel-border);
}
.panel-title {
font-size: var(--wsb-font-size-sm);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--vscode-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.panel-actions {
display: flex;
align-items: center;
gap: var(--wsb-space-1);
flex: none;
}
/* 24px of target around a 14px glyph. An action the size of its own picture
is one people miss — and these are the actions a panel is opened for. */
.panel-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--wb-icon-button-size);
height: var(--wb-icon-button-size);
border: none;
background: transparent;
color: var(--vscode-disabledForeground);
border-radius: var(--wsb-radius-sm);
cursor: pointer;
}
.panel-action-btn:hover:not(:disabled) {
color: var(--vscode-foreground);
background: var(--vscode-list-hoverBackground);
}
.panel-action-btn:disabled {
opacity: 0.5;
cursor: default;
}
/* Turning while it works: the one piece of motion in a panel, and it means
exactly one thing. */
.panel-action-btn.spinning {
animation: wsb-spin 0.8s linear infinite;
color: var(--vscode-focusBorder);
}
/* A rejection that happens outside a panel store still has a place to land.
Domain failures belong in the panel's own result area; this is the shared
protocol boundary, for failures that otherwise become invisible rejected
promises. */
.panel-refresh-error {
flex-shrink: 0;
padding: var(--wb-spacing-5) var(--wsb-panel-padding-inline);
border-bottom: 1px solid color-mix(in srgb, var(--wb-error) 24%, transparent);
background: color-mix(in srgb, var(--wb-error) 8%, transparent);
color: var(--wb-error);
font-size: var(--wsb-font-size-xs);
line-height: 1.4;
}
@keyframes wsb-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* A word worth seeing from across the room, inside a line that is mostly not
about it. */
.mark-error,
.mark-warning {
padding: 0 var(--wb-spacing-4);
border-radius: var(--wb-corner-radius-3);
color: var(--wb-mark-ink);
font-weight: 600;
}
.mark-error {
background: var(--wb-mark-error-bg);
}
.mark-warning {
background: var(--wb-mark-warning-bg);
}
/* When a panel's data was last read, at the foot of the panel.
Below the thing it describes and quiet enough to be looked up rather than
read: it was at the TOP, where the freshness of a list was the first thing a
panel said and the list itself the second. A failure is the exception — that
one is worth the colour, because a stale list and a list nobody could
refresh look identical without it. */
.panel-standing {
margin-top: auto;
flex-shrink: 0;
padding: var(--wb-spacing-5) var(--wsb-panel-padding-inline);
border-top: 1px solid var(--vscode-panel-border);
font-size: var(--wsb-font-size-xs);
line-height: 1.4;
color: var(--vscode-descriptionForeground);
opacity: 0.75;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.panel-standing--problem {
color: var(--wb-error);
opacity: 1;
white-space: normal;
}
/* A content page's own chrome: what this page is, said once at the top, in the
same place and the same size on every page. A page that titles itself 18px
where the next one uses 20px reads as a different application. */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--wsb-space-6);
flex-wrap: wrap;
margin-bottom: var(--wsb-space-6);
}
.page-title {
font-size: var(--wsb-font-size-xl);
font-weight: 600;
margin: 0;
line-height: 1.25;
color: var(--vscode-editor-foreground);
}
.page-subtitle {
margin: var(--wsb-space-2) 0 0;
font-size: var(--wsb-font-size-md);
color: var(--vscode-descriptionForeground);
line-height: 1.5;
}
/* The controls that act on the whole page, gathered at its top right — one
place to look for them, however many there are. */
.page-actions {
display: flex;
gap: var(--wsb-space-3);
align-items: center;
flex-wrap: wrap;
}
/* A choice between a handful of things, as one control rather than a row of
loose buttons: the segments touch, so the group reads as one question. */
.segmented {
display: inline-flex;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: var(--wsb-radius-sm);
overflow: hidden;
}
.segmented > button {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
background: none;
border: none;
border-right: 1px solid color-mix(in srgb, currentColor 12%, transparent);
padding: var(--wsb-space-2) var(--wb-spacing-10);
font: inherit;
font-size: var(--wsb-font-size-sm);
color: var(--vscode-descriptionForeground);
cursor: pointer;
white-space: nowrap;
}
.segmented > button:last-child {
border-right: none;
}
.segmented > button:hover {
background: color-mix(in srgb, currentColor 8%, transparent);
color: var(--vscode-editor-foreground);
}
.segmented > button[aria-pressed="true"] {
background: color-mix(in srgb, var(--vscode-focusBorder) 20%, transparent);
color: var(--vscode-editor-foreground);
}
/* A count beside the thing it counts. */
.count-badge {
font-size: var(--wsb-font-size-xs);
line-height: var(--wb-line-height-16);
min-width: var(--wb-control-size-16);
padding: 0 var(--wb-spacing-5);
text-align: center;
border-radius: var(--wb-corner-radius-8);
background: var(--vscode-badge-background, color-mix(in srgb, currentColor 12%, transparent));
color: var(--vscode-badge-foreground, inherit);
}
/* An action that is an icon: 24px of target for a 12px glyph, because a
control the size of its own picture is one people miss. */
.icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--wb-icon-button-size);
height: var(--wb-icon-button-size);
flex: none;
padding: 0;
border: none;
border-radius: var(--wsb-radius-sm);
background: none;
color: var(--vscode-descriptionForeground);
cursor: pointer;
}
.icon-button:hover {
background: color-mix(in srgb, currentColor 12%, transparent);
color: var(--vscode-editor-foreground);
}
/* A small piece of standing: a label, a state, a count. One shape, and the
colour says which kind — a state that looked like a label is a board nobody
can read at a glance. */
.chip {
display: inline-flex;
align-items: center;
gap: var(--wsb-space-2);
font-size: var(--wb-font-size-10-5);
line-height: 1.6;
padding: 0 var(--wb-spacing-7);
border-radius: var(--wb-corner-radius-9);
border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
color: var(--vscode-descriptionForeground);
white-space: nowrap;
}
.chip--open {
color: var(--wb-state-open);
border-color: color-mix(in srgb, var(--wb-state-open) 45%, transparent);
}
.chip--merged {
color: var(--wb-state-merged);
border-color: color-mix(in srgb, var(--wb-state-merged) 45%, transparent);
}
.chip--closed {
color: var(--wb-state-closed);
border-color: color-mix(in srgb, var(--wb-state-closed) 45%, transparent);
}
.chip--draft {
color: var(--wb-state-draft);
border-color: color-mix(in srgb, var(--wb-state-draft) 45%, transparent);
}
/* Context menu (portal-based, appended to document.body) */
.context-menu {
position: fixed;
z-index: 9999;
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-md);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
padding: var(--wsb-space-2) 0;
min-width: 140px;
}
.context-menu .context-menu-item {
display: block;
width: 100%;
padding: var(--wb-menu-padding-block) var(--wb-menu-padding-inline);
font-size: var(--wsb-font-size-md);
color: var(--vscode-editor-foreground);
text-align: left;
background: none;
border: none;
cursor: pointer;
font-family: var(--wsb-font-sans);
}
.context-menu .context-menu-item:hover {
background: var(--vscode-list-hoverBackground);
}
.context-menu .context-menu-item:disabled {
color: var(--vscode-disabledForeground);
opacity: 0.5;
cursor: default;
}
.context-menu .context-menu-item:disabled:hover {
background: none;
}
.context-menu .context-menu-item.danger {
color: var(--vscode-errorForeground, #e55);
}
.context-menu .context-menu-item.danger:hover {
background: color-mix(in srgb, var(--vscode-errorForeground, #e55) 10%, transparent);
}
.context-menu .context-menu-item--danger {
color: var(--wb-error);
}
.context-menu .context-menu-item--danger:hover {
background: color-mix(in srgb, var(--wb-error) 10%, transparent);
}
/* A group heading inside a menu. Never clickable: it names what the items
below it act ON, which is the difference between committing one repository
and committing forty. */
.context-menu .context-menu-label {
padding: var(--wsb-space-2) var(--wb-menu-padding-inline) var(--wsb-space-1);
font-size: var(--wsb-font-size-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--vscode-disabledForeground);
}
.context-menu .context-menu-separator {
height: 1px;
margin: var(--wb-spacing-3) 0;
background: var(--vscode-panel-border);
}