Workspace tabs are a real 7.css tablist
The tab strip was a hand-rolled lookalike: bg-accent for the active tab and an absolutely-positioned bg-primary underline, competing with a tab component 7.css already ships. Replaced with its actual contract — menu[role=tablist] plus one button[role=tab][aria-selected] per workspace — so 7.css draws the joined inactive tabs and the raised white active one. Per the agreed design the per-tab X is gone: real Win7 tab controls had none, and a button nested in a button is invalid HTML. Closing keeps Ctrl/Cmd+W and gains middle-click (auxclick, guarded on button 1 so the right button never closes a tab). Drag-reorder moved onto the tab button itself and still works. Three things 7.css does that had to be accommodated, each found by looking rather than by reasoning: - Its selector is the DESCENDANT `menu[role=tablist] button`, so the +, update pill and right-hand tools deliberately stay outside the menu. - The strip is a .w7-toolbar, whose neutralizer strips background, border and both pseudo-elements to make toolbar buttons flat. That flattened the tabs to nothing, so the rule now exempts :not([role=tab]) — tabs want the full Aero slab, their neighbours want flat. - overflow-x:auto on the scroller forces computed overflow-y from visible to auto, so the strip clips vertically. 7.css raises the active tab with margin-top:-2px, which put its top border outside the scroll box and silently deleted it. The strip now reserves 2px of padding-top, mirroring the padding-left:3px 7.css already reserves for the same tab's -3px left margin. Caught by pixel-scanning the tab's top edge against 7.css's own reference markup in the harness: reference had #8e8f8f, ours jumped straight from toolbar grey to tab fill. Also: tabs.css hardcodes color:#222 on every tablist button, outside 7.css's var system, leaving labels near-invisible in dark. Routed through a new --w7-tab-c token declared light in :root.win7 and overridden in BOTH dark blocks (attribute and prefers-color-scheme), matching the ListView and menu literals. And its `display: block` outranks Tailwind's .flex, which jammed the icon against the label — flex/gap restored in skinfix. role="tabpanel" on the panel area was considered and skipped: 7.css gives it padding:14px, margin-bottom:9px and its own border, which would fight the split-pane layout for a join that cannot read anyway with the panel toolbar row in between. The harness grew a section 5 mirroring this markup, because the live app starts with exactly one workspace and there is no way to click "+" from a headless capture — multi-tab state is only checkable there. Verified in both themes headless and on a live window capture. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+46
-1
@@ -231,7 +231,7 @@
|
||||
</div>
|
||||
</figure>
|
||||
<figure class="cell" style="margin:0">
|
||||
<figcaption>tabs — menu[role=tablist]</figcaption>
|
||||
<figcaption>reference — menu[role=tablist]</figcaption>
|
||||
<menu role="tablist">
|
||||
<button role="tab" aria-selected="true">local</button>
|
||||
<button role="tab">server-a</button>
|
||||
@@ -240,5 +240,50 @@
|
||||
<div role="tabpanel" style="padding:10px">Tab body</div>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<h2>5 — Workspace tab strip (ours, as tab-bar.ts renders it)</h2>
|
||||
<!--
|
||||
The live app only ever has ONE workspace at startup and there is no way to
|
||||
click "+" from a headless capture, so multi-tab state can only be checked
|
||||
here. This mirrors tab-bar.ts exactly: the tabs sit in a menu[role=tablist]
|
||||
inside a .w7-toolbar row, and the "+" / right-hand tools sit OUTSIDE the
|
||||
menu so 7.css's descendant selector does not style them as tabs.
|
||||
-->
|
||||
<div class="pair">
|
||||
<figure class="cell" style="margin:0">
|
||||
<figcaption>ours — 3 workspaces, 2nd active</figcaption>
|
||||
<div class="w7-toolbar relative flex h-9 items-center gap-0.5 border-b border-border bg-chrome pt-1 pr-1.5 pl-1.5">
|
||||
<menu role="tablist" class="flex min-w-0 items-center overflow-x-auto">
|
||||
<button role="tab" class="flex max-w-52 min-w-0 shrink-0 items-center gap-1.5">
|
||||
<span class="truncate">Local</span>
|
||||
</button>
|
||||
<button role="tab" aria-selected="true" class="flex max-w-52 min-w-0 shrink-0 items-center gap-1.5">
|
||||
<span class="truncate">build-server</span>
|
||||
</button>
|
||||
<button role="tab" class="flex max-w-52 min-w-0 shrink-0 items-center gap-1.5">
|
||||
<span class="truncate">a-very-long-workspace-name-that-truncates</span>
|
||||
</button>
|
||||
</menu>
|
||||
<button class="ml-0.5 grid size-6 place-items-center rounded-md">+</button>
|
||||
<div class="flex-1"></div>
|
||||
<button class="grid h-6 px-2 py-1 place-items-center rounded-md text-[11px]">Connections</button>
|
||||
</div>
|
||||
<div style="height:60px;background:var(--w7-tab-bg,#fff);border:1px solid #8e8f8f;border-top:0"></div>
|
||||
</figure>
|
||||
<figure class="cell" style="margin:0">
|
||||
<figcaption>ours — single workspace (the live startup state)</figcaption>
|
||||
<div class="w7-toolbar relative flex h-9 items-center gap-0.5 border-b border-border bg-chrome pt-1 pr-1.5 pl-1.5">
|
||||
<menu role="tablist" class="flex min-w-0 items-center overflow-x-auto">
|
||||
<button role="tab" aria-selected="true" class="flex max-w-52 min-w-0 shrink-0 items-center gap-1.5">
|
||||
<span class="truncate">Local</span>
|
||||
</button>
|
||||
</menu>
|
||||
<button class="ml-0.5 grid size-6 place-items-center rounded-md">+</button>
|
||||
<div class="flex-1"></div>
|
||||
<button class="grid h-6 px-2 py-1 place-items-center rounded-md text-[11px]">Connections</button>
|
||||
</div>
|
||||
<div style="height:60px;background:var(--w7-tab-bg,#fff);border:1px solid #8e8f8f;border-top:0"></div>
|
||||
</figure>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -45,55 +45,51 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
<div
|
||||
class="w7-toolbar relative flex h-9 items-center gap-0.5 border-b border-border bg-chrome pt-1 pr-1.5 pl-1.5 electrobun-webkit-app-region-drag"
|
||||
>
|
||||
<div
|
||||
<!-- 7.css's real tab component. The contract is exact: menu[role=tablist]
|
||||
for the strip and a button[aria-selected] per tab — 7.css then draws
|
||||
the joined inactive tabs and raises the active one (white fill, no
|
||||
bottom border, pulled 2px down over the row's border so it reads as
|
||||
joined to the content). Everything the hand-rolled version painted
|
||||
(bg-accent, the absolutely-positioned bg-primary underline) is gone:
|
||||
it was a lookalike competing with the framework.
|
||||
NOTE the selector is "menu[role=tablist] button" — a DESCENDANT, so
|
||||
any button in here becomes a tab. The "+" and the right-hand tools
|
||||
deliberately live OUTSIDE this element.
|
||||
(No backticks in this template: it is a TS template literal.) -->
|
||||
<menu
|
||||
#tabScroll
|
||||
class="flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
role="tablist"
|
||||
class="flex min-w-0 items-center overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
@for (ws of panels.workspaces(); track ws.id; let i = $index) {
|
||||
<div
|
||||
<!-- No ✕ inside the tab: real Win7 tab controls had none, and a button
|
||||
nested in a button is invalid HTML. Closing is Ctrl/Cmd+W or
|
||||
middle-click (auxclick), so a pointer path survives. -->
|
||||
<button
|
||||
role="tab"
|
||||
[attr.aria-selected]="ws.id === panels.activeWorkspaceId()"
|
||||
[attr.data-tab-id]="ws.id"
|
||||
class="group relative flex max-w-52 shrink-0 items-center rounded-md electrobun-webkit-app-region-no-drag"
|
||||
[class.bg-accent]="ws.id === panels.activeWorkspaceId()"
|
||||
(click)="panels.setActiveWorkspace(ws.id)"
|
||||
(auxclick)="onAuxClick($event, ws.id)"
|
||||
class="flex max-w-52 min-w-0 shrink-0 items-center gap-1.5 electrobun-webkit-app-region-no-drag"
|
||||
[class.opacity-50]="draggedId() === ws.id"
|
||||
[style.box-shadow]="dropIndicatorShadow(i)"
|
||||
[title]="panels.workspaceName(ws)"
|
||||
draggable="true"
|
||||
(dragstart)="onDragStart($event, ws.id)"
|
||||
(dragover)="onDragOver($event, i)"
|
||||
(drop)="onDrop($event, i)"
|
||||
(dragend)="onDragEnd()"
|
||||
>
|
||||
<button
|
||||
(click)="panels.setActiveWorkspace(ws.id)"
|
||||
class="flex min-w-0 items-center gap-1.5 py-1.5 pr-1 pl-2.5 text-xs"
|
||||
[class.text-foreground]="ws.id === panels.activeWorkspaceId()"
|
||||
[class.text-muted-foreground]="ws.id !== panels.activeWorkspaceId()"
|
||||
[title]="panels.workspaceName(ws)"
|
||||
>
|
||||
<ng-icon
|
||||
[name]="panels.workspaceIsRemote(ws) ? 'mynaServers' : 'mynaHardDrive'"
|
||||
size="0.85rem"
|
||||
class="shrink-0"
|
||||
[class.text-primary]="panels.workspaceIsRemote(ws)"
|
||||
/>
|
||||
<span class="truncate">{{ panels.workspaceName(ws) }}</span>
|
||||
</button>
|
||||
@if (panels.workspaces().length > 1) {
|
||||
<button
|
||||
(click)="close($event, ws.id)"
|
||||
[title]="i18n.t('tabBar.closeTab', { mod: modLabel })"
|
||||
class="mr-1 grid size-4 shrink-0 place-items-center rounded text-muted-foreground opacity-0 group-hover:opacity-100 hover:bg-border hover:text-foreground"
|
||||
>
|
||||
<ng-icon name="mynaX" size="0.7rem" />
|
||||
</button>
|
||||
} @else {
|
||||
<span class="mr-1 size-4 shrink-0"></span>
|
||||
}
|
||||
@if (ws.id === panels.activeWorkspaceId()) {
|
||||
<div class="absolute inset-x-0 -bottom-px h-0.5 rounded-full bg-primary"></div>
|
||||
}
|
||||
</div>
|
||||
<ng-icon
|
||||
[name]="panels.workspaceIsRemote(ws) ? 'mynaServers' : 'mynaHardDrive'"
|
||||
size="0.85rem"
|
||||
class="shrink-0"
|
||||
/>
|
||||
<span class="truncate">{{ panels.workspaceName(ws) }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</menu>
|
||||
<button
|
||||
(click)="panels.newWorkspace()"
|
||||
[title]="i18n.t('tabBar.newTab', { mod: modLabel })"
|
||||
@@ -448,7 +444,7 @@ export class TabBar {
|
||||
readonly draggedId = signal<number | null>(null);
|
||||
readonly dropTarget = signal<{ index: number; before: boolean } | null>(null);
|
||||
|
||||
private readonly tabScroll = viewChild.required<ElementRef<HTMLDivElement>>('tabScroll');
|
||||
private readonly tabScroll = viewChild.required<ElementRef<HTMLMenuElement>>('tabScroll');
|
||||
|
||||
constructor() {
|
||||
// keep the active tab visible when the strip overflows
|
||||
@@ -465,6 +461,18 @@ export class TabBar {
|
||||
this.panels.closeWorkspace(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Middle-click closes the tab — the pointer path that replaced the per-tab ✕.
|
||||
* `auxclick` is the non-primary-button click event; guard on button 1 so the
|
||||
* right button (which also fires auxclick) never closes anything. Refuses the
|
||||
* last tab, matching closeWorkspace's own floor.
|
||||
*/
|
||||
onAuxClick(event: MouseEvent, id: number): void {
|
||||
if (event.button !== 1) return;
|
||||
event.preventDefault();
|
||||
if (this.panels.workspaces().length > 1) this.close(event, id);
|
||||
}
|
||||
|
||||
/** "Visual Studio Code.app" -> "Visual Studio Code"; empty -> system default. */
|
||||
editorAppName(): string {
|
||||
const path = this.settings.editorApp();
|
||||
|
||||
+49
-4
@@ -415,6 +415,9 @@ body {
|
||||
--w7-lv-head-grad: linear-gradient(180deg, #fff 45%, #fafafa 0, #f0f0f0);
|
||||
--w7-lv-head-bd: #d7d7d7;
|
||||
--w7-lv-cell-bd: #eee;
|
||||
/* Tabs (tabs.css). 7.css hardcodes `color: #222` on every tablist button,
|
||||
outside its own var system, which is unreadable on the dark surface. */
|
||||
--w7-tab-c: #222;
|
||||
/* Menu (menu.css) */
|
||||
--w7-mn-c: #000;
|
||||
--w7-mn-bd: #0006;
|
||||
@@ -472,6 +475,7 @@ body {
|
||||
--w7-lv-head-grad: linear-gradient(180deg, var(--w7-el-bg) 45%, var(--w7-el-bg-d) 0, var(--w7-el-bg-s-1));
|
||||
--w7-lv-head-bd: var(--w7-el-bd-d);
|
||||
--w7-lv-cell-bd: var(--w7-el-bd-d);
|
||||
--w7-tab-c: var(--w7-el-c);
|
||||
--w7-mn-c: var(--w7-el-c);
|
||||
--w7-mn-bd: var(--w7-el-bd-d);
|
||||
--w7-mn-stripe: inset 1px 0 #00000059, inset -1px 0 #ffffff14;
|
||||
@@ -528,6 +532,7 @@ body {
|
||||
--w7-lv-head-grad: linear-gradient(180deg, var(--w7-el-bg) 45%, var(--w7-el-bg-d) 0, var(--w7-el-bg-s-1));
|
||||
--w7-lv-head-bd: var(--w7-el-bd-d);
|
||||
--w7-lv-cell-bd: var(--w7-el-bd-d);
|
||||
--w7-tab-c: var(--w7-el-c);
|
||||
--w7-mn-c: var(--w7-el-c);
|
||||
--w7-mn-bd: var(--w7-el-bd-d);
|
||||
--w7-mn-stripe: inset 1px 0 #00000059, inset -1px 0 #ffffff14;
|
||||
@@ -652,15 +657,20 @@ body {
|
||||
Explorer's command bar worked exactly that way. `w7-toolbar` marks the
|
||||
three header rows; a plain `.title-bar` sibling is deliberately NOT one of
|
||||
them, so its controls keep the real Aero treatment. `w7-flat` is the
|
||||
per-button escape hatch. Both classes are inert under the modern skin. */
|
||||
.win7 .w7-toolbar button,
|
||||
per-button escape hatch. Both classes are inert under the modern skin.
|
||||
`:not([role='tab'])` is load-bearing: the workspace strip IS a w7-toolbar,
|
||||
so without the exemption this rule would strip the border, gradient and
|
||||
both pseudo-elements off every tab and leave 7.css's tab component drawing
|
||||
nothing. Tabs want the full Aero slab; the +/Connections/options buttons
|
||||
beside them want the flat treatment. */
|
||||
.win7 .w7-toolbar button:not([role='tab']),
|
||||
.win7 .w7-flat {
|
||||
background: revert-layer;
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.win7 .w7-toolbar button::before,
|
||||
.win7 .w7-toolbar button::after,
|
||||
.win7 .w7-toolbar button:not([role='tab'])::before,
|
||||
.win7 .w7-toolbar button:not([role='tab'])::after,
|
||||
.win7 .w7-flat::before,
|
||||
.win7 .w7-flat::after {
|
||||
content: none;
|
||||
@@ -808,6 +818,41 @@ body {
|
||||
color: var(--w7-mn-check-c);
|
||||
}
|
||||
|
||||
/* ---- Tabs (workspace strip) ----
|
||||
7.css owns the tab CHROME; these restore the tab CONTENT layout it breaks.
|
||||
`menu[role=tablist] button` sets `display: block` at (0,2,1), which
|
||||
outranks Tailwind's `.flex` in `utilities` — so the icon and the label
|
||||
rendered jammed together with no gap and no truncation. Restate the flex
|
||||
box here (skinfix outranks skin) and leave every painted property alone.
|
||||
The gap matches the gap-1.5 the template asks for. */
|
||||
.win7 menu[role='tablist'] button[role='tab'] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* tabs.css hardcodes `color: #222` on every tablist button — a light-mode
|
||||
literal outside 7.css's var system, which left the labels near-invisible
|
||||
on the dark surface. Same failure and same remedy as the ListView and menu
|
||||
literals above: route it through a token that both dark blocks override. */
|
||||
.win7 menu[role='tablist'] button {
|
||||
color: var(--w7-tab-c);
|
||||
}
|
||||
|
||||
/* The workspace strip scrolls horizontally when tabs overflow. Setting
|
||||
`overflow-x: auto` forces the computed `overflow-y` from visible to auto
|
||||
as well (CSS will not pair visible with a non-visible axis), so the strip
|
||||
clips vertically — and 7.css raises the ACTIVE tab with `margin-top: -2px`,
|
||||
which put its top border outside the scroll box and silently deleted it.
|
||||
Reserve those 2px as padding so the pulled-up tab lands inside the box.
|
||||
7.css already reserves the matching 3px on the left (padding-left: 3px)
|
||||
for the same tab's -3px left margin; it just never anticipated a scroller.
|
||||
Verified by pixel scan: the active tab's top edge must be #8e8f8f, not a
|
||||
straight jump from the toolbar grey to the tab fill. */
|
||||
.win7 .w7-toolbar menu[role='tablist'] {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.win7 .window.glass > .title-bar {
|
||||
background: var(--w7-glass-title);
|
||||
background-attachment: fixed;
|
||||
|
||||
Reference in New Issue
Block a user