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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user