Draw the tab list as a webview, VS-style
A tree view row cannot be styled: no border, no spacing, and a long file name gets middle-ellipsized in a narrow strip — which on a real project left "ClientDogSurger...nionBehaviour.cs" and hid the project text entirely. Drawing the rows means controlling all of it. - A coloured left border per project instead of a tinted icon, so the file icon slot is free and the name gets the width. The border uses var(--vscode-verticalTabs-projectN): VS Code injects every contributed theme colour into a webview, so overrides in colorCustomizations still apply and nothing is hardcoded. - The name is split into stem and extension, and only the stem ellipsizes, so ".cs" survives on a long name in a narrow view. - Configurable row spacing and border width, a rule under the pinned block, a dirty dot that the close button replaces on hover, italics for preview and for tabs that cannot be focused. - Selection, arrow/Home/End/Enter/Delete keys, middle-click to close, and an HTML context menu, since contributes.menus does not reach webview rows. - Clicking a webview row posts nothing rather than asking for an open that would silently do nothing. Actions moved to actions.ts so the wiring, the view and the tests share one implementation; extension.ts is now just registration. The per-row commands are gone — rows act through webview messages — and the palette keeps close / closeOthers / togglePin acting on the active editor. Verified in a browser harness at side-bar width: all twelve project colours resolve, rows stay a consistent 24px, and click, pin, close, middle-click, keyboard and both context-menu variants post the right intents. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+252
@@ -0,0 +1,252 @@
|
||||
/* Vertical Tabs — the tab list */
|
||||
|
||||
:root {
|
||||
--row-gap: 2px;
|
||||
--border-width: 3px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-sideBar-background, transparent);
|
||||
font-family: var(--vscode-font-family);
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
#list {
|
||||
height: 100%;
|
||||
padding: 3px 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* --- a row -------------------------------------------------------------- */
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-height: 24px;
|
||||
margin: 0 2px var(--row-gap);
|
||||
padding: 2px 4px 2px 5px;
|
||||
border-left: var(--border-width) solid var(--tab-color, transparent);
|
||||
border-radius: 0 3px 3px 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
/* the close button only takes space while it is visible */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
background: var(--vscode-list-activeSelectionBackground);
|
||||
}
|
||||
|
||||
.tab.selected:not(.active) {
|
||||
background: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
#list:focus-visible .tab.selected {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.tab.inert {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tab.inert .name {
|
||||
font-style: italic;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* --- the file name ------------------------------------------------------ */
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The stem shrinks and ellipsizes; the extension never does, so ".cs" stays
|
||||
readable on a long name in a narrow strip. */
|
||||
.stem {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ext {
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tab.preview .name {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dir {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 0.9em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- markers and buttons ------------------------------------------------ */
|
||||
|
||||
.marker {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vscode-icon-foreground);
|
||||
border-radius: 3px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.marker svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
button.marker {
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab:hover button.marker,
|
||||
.tab.active button.marker,
|
||||
button.marker.on,
|
||||
button.marker:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
button.marker:hover {
|
||||
background: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
button.marker:focus-visible {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
/* An unsaved file shows a dot, which the close button replaces on hover. */
|
||||
.dirty {
|
||||
flex: 0 0 auto;
|
||||
width: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dirty::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.tab:hover .dirty,
|
||||
.tab.active .dirty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab:not(.is-dirty) .dirty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab:not(:hover):not(.active) button.close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- the pinned block --------------------------------------------------- */
|
||||
|
||||
.separator {
|
||||
height: 1px;
|
||||
margin: 4px 6px 5px;
|
||||
background: var(--vscode-sideBarSectionHeader-border, var(--vscode-panel-border));
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* --- context menu ------------------------------------------------------- */
|
||||
|
||||
#menu {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
min-width: 180px;
|
||||
padding: 4px;
|
||||
color: var(--vscode-menu-foreground, var(--vscode-foreground));
|
||||
background: var(--vscode-menu-background, var(--vscode-editorWidget-background));
|
||||
border: 1px solid var(--vscode-menu-border, var(--vscode-editorWidget-border));
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.36));
|
||||
}
|
||||
|
||||
#menu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
color: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#menu button:hover,
|
||||
#menu button:focus-visible {
|
||||
color: var(--vscode-menu-selectionForeground, inherit);
|
||||
background: var(--vscode-menu-selectionBackground, var(--vscode-list-hoverBackground));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#menu .menu-separator {
|
||||
height: 1px;
|
||||
margin: 4px 2px;
|
||||
background: var(--vscode-menu-separatorBackground, var(--vscode-panel-border));
|
||||
}
|
||||
|
||||
/* --- empty state -------------------------------------------------------- */
|
||||
|
||||
#empty {
|
||||
padding: 10px 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
#empty[hidden] {
|
||||
display: none;
|
||||
}
|
||||
Reference in New Issue
Block a user