Fix the dead win7 context menu, flatten the selection, add a tab menu
Right-click did nothing under the win7 skin, and then swallowed the next
click. 7.css declares `position: relative` on ul[role=menu] and the `skin`
layer outranks `utilities`, so Tailwind's `.fixed` lost: [style.left/top.px]
became flow offsets from the menu's static position — the last child of the
h-screen frame — and every menu rendered below the viewport, unreachable
behind overflow:hidden. Its backdrop is a plain <div>, unmatched by 7.css, so
that stayed genuinely fixed and ate the following click. Regressed in 0ae2a62,
when the menu adopted 7.css's real markup.
Restated `position: fixed` in skinfix, scoped to a new `w7-ctx` marker so
7.css's own menus keep theirs. scripts/menu-probe.ts is the regression test:
it renders the menu at declared coordinates, pixel-scans whether it landed
there, and carries a marker-less control menu (the bug verbatim) as its own
discriminator. A resting pixel diff cannot see this class of bug.
Selection band goes flat: --w7-sel-bg/-bd (#cbe8f6/#26a0da) is what Explorer
actually painted, where 7.css reused --w7-el-grad-h, its Aero element-hover
ramp. That var is left alone — buttons and tabs still want the gloss. The
unfocused pane gets the grey idle band; the band still never disappears with
focus, it only weakens. Harness diff confirms the change touches selection
rows and nothing else, in both themes.
New workspace-tab right-click menu in both skins: New Tab, Close Tab, Close
Other Tabs, Close Tabs to the Right, Disconnect. The target id travels inside
the action string because the right-clicked tab is not necessarily the active
one. Illegal entries are disabled rather than omitted, the way Win7 greyed
out — and since 7.css's [aria-disabled] handling is CSS-only, the keyboard
paths check the flag themselves.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,18 @@
|
||||
--w7-lv-head-grad: linear-gradient(180deg, #fff 45%, #fafafa 0, #f0f0f0);
|
||||
--w7-lv-head-bd: #d7d7d7;
|
||||
--w7-lv-cell-bd: #eee;
|
||||
/* Selected row. Explorer painted a FLAT blue, not a gradient — 7.css reuses
|
||||
its Aero element-hover gradient for the band, which is what made the list
|
||||
look glossy. These are dedicated tokens on purpose: --w7-el-grad-h is
|
||||
consumed by buttons and tabs and must keep its gradient.
|
||||
`-idle` is the unfocused pane's band. Explorer greyed the selection out
|
||||
when the list lost focus rather than dropping it, which is exactly the
|
||||
commander convention this app already follows (see the pane-focus model
|
||||
below): the band never disappears, only its strength changes. */
|
||||
--w7-sel-bg: #cbe8f6;
|
||||
--w7-sel-bd: #26a0da;
|
||||
--w7-sel-bg-idle: #dedede;
|
||||
--w7-sel-bd-idle: #c9c9c9;
|
||||
/* 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;
|
||||
@@ -188,6 +200,10 @@
|
||||
--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-sel-bg: #2a4a63;
|
||||
--w7-sel-bd: #4a8cbe;
|
||||
--w7-sel-bg-idle: #3a3a3a;
|
||||
--w7-sel-bd-idle: #4c4c4c;
|
||||
--w7-tab-c: var(--w7-el-c);
|
||||
--w7-mn-c: var(--w7-el-c);
|
||||
--w7-mn-bd: var(--w7-el-bd-d);
|
||||
@@ -245,7 +261,11 @@
|
||||
--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-sel-bg: #2a4a63;
|
||||
--w7-sel-bd: #4a8cbe;
|
||||
--w7-sel-bg-idle: #3a3a3a;
|
||||
--w7-sel-bd-idle: #4c4c4c;
|
||||
--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;
|
||||
@@ -561,8 +581,8 @@ html.win7 body {
|
||||
border-right: none;
|
||||
}
|
||||
.win7 table > tbody > tr.highlighted {
|
||||
background: var(--w7-li-bg-hl);
|
||||
border-color: var(--w7-li-bd-hl);
|
||||
background: var(--w7-sel-bg-idle);
|
||||
border-color: var(--w7-sel-bd-idle);
|
||||
}
|
||||
|
||||
/* ---- Window frame ----
|
||||
@@ -645,7 +665,7 @@ html.win7 body {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.win7 table > tbody > tr.highlighted {
|
||||
border-color: var(--w7-li-bd-hl);
|
||||
border-color: var(--w7-sel-bd-idle);
|
||||
}
|
||||
|
||||
/* Pane-focus model (commander convention, matching Win7 Explorer):
|
||||
@@ -653,15 +673,19 @@ html.win7 body {
|
||||
tr.highlighted covers "selected via Space" AND "cursor row" (the template
|
||||
ORs them). What changes with pane focus is the band's strength, never
|
||||
whether it exists:
|
||||
- focused pane -> strong Aero band (below)
|
||||
- unfocused pane -> 7.css's own tr.highlighted, which IS Win7's
|
||||
inactive-selection wash — faint by design, not a bug.
|
||||
- focused pane -> the flat blue selection (below)
|
||||
- unfocused pane -> the grey idle band, which is what Explorer drew
|
||||
when the list lost focus — faint by design, not a bug.
|
||||
The dotted focus rectangle is NOT the focus signal; it is layered on top
|
||||
of the band in the focused pane only, the way Win7 drew the keyboard
|
||||
focus rect over the selected item. */
|
||||
focus rect over the selected item.
|
||||
FLAT, not a gradient: 7.css bands the row with --w7-el-grad-h, its Aero
|
||||
element-hover ramp, which reads as a glossy button rather than a file
|
||||
selection. Explorer used one solid fill and a solid border. Keep the
|
||||
1px border and 7.css's 3px radius — those ARE Explorer's selection box. */
|
||||
.win7 table.is-active > tbody > tr.highlighted {
|
||||
background: var(--w7-el-grad-h);
|
||||
border-color: var(--w7-el-bd-h);
|
||||
background: var(--w7-sel-bg);
|
||||
border-color: var(--w7-sel-bd);
|
||||
}
|
||||
|
||||
/* One dotted rectangle around the ROW; per cell (tr > *) draws a dotted box
|
||||
@@ -684,6 +708,24 @@ html.win7 body {
|
||||
color: var(--w7-mn-c);
|
||||
border-color: var(--w7-mn-bd);
|
||||
}
|
||||
|
||||
/* THE POSITIONING ONE — this is why right-click looked dead under the skin.
|
||||
menu.css declares `position: relative` on ul[role=menu] (it needs it for
|
||||
the gutter stripe's ::before and for submenu anchoring), and `skin`
|
||||
outranks `utilities`, so Tailwind's `.fixed` on the app's menu lost. Its
|
||||
[style.left/top.px] then became FLOW offsets from the menu's static
|
||||
position — the last child of an h-screen flex column — and every menu
|
||||
rendered below the viewport, unreachable behind `overflow: hidden`. The
|
||||
backdrop is a plain <div>, which 7.css does not match, so it stayed truly
|
||||
fixed and swallowed the next click: "right click does nothing, then the
|
||||
app ignores me once".
|
||||
Scoped to the app menu's own marker class, never blanket: 7.css's own
|
||||
relative positioning must survive for any other ul[role=menu].
|
||||
Regression test: scripts/menu-probe.ts (a resting pixel diff cannot see a
|
||||
positioning bug — nothing about the idle window changes). */
|
||||
.win7 ul[role='menu'].w7-ctx {
|
||||
position: fixed;
|
||||
}
|
||||
.win7 ul[role='menu']:before {
|
||||
box-shadow: var(--w7-mn-stripe);
|
||||
}
|
||||
|
||||
@@ -143,6 +143,10 @@ export class App {
|
||||
this.settings.toggleColumn(action.slice('col:'.length) as ColumnKey);
|
||||
return;
|
||||
}
|
||||
if (action.startsWith('tab:')) {
|
||||
this.handleTabAction(action.slice('tab:'.length));
|
||||
return;
|
||||
}
|
||||
if (action.startsWith('open-stack:')) {
|
||||
const id = action.slice('open-stack:'.length);
|
||||
let stackConnections = this.connections.connections().filter((c) => c.stackId === id);
|
||||
@@ -219,6 +223,35 @@ export class App {
|
||||
}
|
||||
|
||||
/** Context-menu actions apply to the row/panel remembered at right-click time. */
|
||||
/**
|
||||
* Workspace-tab menu actions. The target id travels INSIDE the action string
|
||||
* because the right-clicked tab is not necessarily the active one — nothing
|
||||
* here may fall back to activeWorkspaceId.
|
||||
*/
|
||||
private handleTabAction(action: string): void {
|
||||
if (action === 'new') {
|
||||
this.panels.newWorkspace();
|
||||
return;
|
||||
}
|
||||
const parsed = /^([a-z-]+):(\d+)$/.exec(action);
|
||||
if (!parsed) return;
|
||||
const id = Number(parsed[2]);
|
||||
switch (parsed[1]) {
|
||||
case 'close':
|
||||
this.panels.closeWorkspace(id);
|
||||
break;
|
||||
case 'close-others':
|
||||
this.panels.closeOtherWorkspaces(id);
|
||||
break;
|
||||
case 'close-right':
|
||||
this.panels.closeWorkspacesToRight(id);
|
||||
break;
|
||||
case 'disconnect':
|
||||
void this.panels.disconnectWorkspace(id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleContextAction(action: string): void {
|
||||
const target = this.actions.menuTarget;
|
||||
if (!target) return;
|
||||
|
||||
@@ -33,6 +33,12 @@ interface MenuRow {
|
||||
* rendered div > button, concluded "7.css has no menu component", and
|
||||
* hand-built a lookalike. It does have one; the selectors just never matched.
|
||||
*
|
||||
* `w7-ctx` is OURS, not 7.css's: `ul[role=menu]` carries `position: relative`
|
||||
* in 7.css (for its gutter stripe and submenus), and the skin layer outranks
|
||||
* Tailwind's `.fixed` — so the menu laid out in flow, at the bottom of the
|
||||
* h-screen frame, off the viewport. The class scopes a `position: fixed`
|
||||
* restatement in 7like.css to THIS menu; see scripts/menu-probe.ts.
|
||||
*
|
||||
* Keyboard follows the platform: arrows move REAL focus between items (7.css
|
||||
* styles :focus-visible identically to :hover), Enter/Space on a button
|
||||
* activate it natively; only labels need a synthetic click.
|
||||
@@ -51,10 +57,17 @@ interface MenuRow {
|
||||
[style.left.px]="position().x"
|
||||
[style.top.px]="position().y"
|
||||
[style.width.rem]="menuWidthRem"
|
||||
class="fixed z-50 outline-none"
|
||||
class="w7-ctx fixed z-50 outline-none"
|
||||
>
|
||||
@for (row of rows(); track $index) {
|
||||
<li role="menuitem" [class.has-divider]="row.hasDivider" [title]="row.item.tooltip ?? ''">
|
||||
<!-- aria-disabled on the LI, not the button: that is where 7.css
|
||||
hangs its disabled state (opacity + pointer-events: none). -->
|
||||
<li
|
||||
role="menuitem"
|
||||
[class.has-divider]="row.hasDivider"
|
||||
[attr.aria-disabled]="row.item.disabled ? true : null"
|
||||
[title]="row.item.tooltip ?? ''"
|
||||
>
|
||||
@if (row.item.checked !== undefined) {
|
||||
<input type="checkbox" [id]="'ctx-check-' + $index" [checked]="row.item.checked" tabindex="-1" />
|
||||
<label [for]="'ctx-check-' + $index" tabindex="-1" (click)="pick(row.item, $event)">
|
||||
@@ -140,6 +153,9 @@ export class ContextMenu {
|
||||
// labels: don't let the click also toggle the real checkbox — the fresh
|
||||
// `checked` state comes back through settings when the menu reopens
|
||||
event?.preventDefault();
|
||||
// 7.css already blocks the pointer on [aria-disabled]; this covers the
|
||||
// keyboard path, which routes through focus and never consults CSS
|
||||
if (item.disabled) return;
|
||||
if (!item.action) return;
|
||||
const action = item.action;
|
||||
this.close();
|
||||
@@ -189,7 +205,13 @@ export class ContextMenu {
|
||||
private step(delta: number): void {
|
||||
const root = this.menuEl()?.nativeElement;
|
||||
if (!root) return;
|
||||
const targets = Array.from(root.querySelectorAll<HTMLElement>('li > button, li > label'));
|
||||
// disabled items are skipped, not merely unclickable: pointer-events:none
|
||||
// does not make an element unfocusable, so arrows would still land on them
|
||||
const targets = Array.from(
|
||||
root.querySelectorAll<HTMLElement>(
|
||||
'li:not([aria-disabled]) > button, li:not([aria-disabled]) > label',
|
||||
),
|
||||
);
|
||||
if (!targets.length) return;
|
||||
const current = targets.indexOf(document.activeElement as HTMLElement);
|
||||
const next = current < 0 && delta < 0 ? targets.length - 1 : (current + delta + targets.length) % targets.length;
|
||||
|
||||
@@ -3,8 +3,9 @@ import { NgIcon } from '@ng-icons/core';
|
||||
import { BRANDING } from '@shared/branding';
|
||||
import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n';
|
||||
import type { AccentTone } from '@shared/types';
|
||||
import { ContextMenuService } from '@core/context-menu.service';
|
||||
import { I18nService } from '@core/i18n.service';
|
||||
import { PanelsService } from '@core/panels.service';
|
||||
import { PanelsService, type Workspace } from '@core/panels.service';
|
||||
import { APP_SKIN, MOD_LABEL } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
@@ -71,6 +72,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
[attr.data-tab-id]="ws.id"
|
||||
(click)="panels.setActiveWorkspace(ws.id)"
|
||||
(auxclick)="onAuxClick($event, ws.id)"
|
||||
(contextmenu)="onTabContextMenu($event, ws, i)"
|
||||
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)"
|
||||
@@ -467,6 +469,7 @@ export class TabBar {
|
||||
readonly settings = inject(SettingsService);
|
||||
readonly i18n = inject(I18nService);
|
||||
readonly updater = inject(UpdaterService);
|
||||
private readonly contextMenu = inject(ContextMenuService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
readonly settingsOpen = signal(false);
|
||||
readonly localeOptions = SUPPORTED;
|
||||
@@ -526,6 +529,24 @@ export class TabBar {
|
||||
if (this.panels.workspaces().length > 1) this.close(event, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-click a tab. The menu targets THIS tab, not the active one — its
|
||||
* actions all carry the id — so right-clicking a background tab does not
|
||||
* yank focus to it first, the way Explorer and every browser behave.
|
||||
*/
|
||||
onTabContextMenu(event: MouseEvent, workspace: Workspace, index: number): void {
|
||||
event.preventDefault();
|
||||
this.contextMenu.open(
|
||||
event,
|
||||
this.contextMenu.tabItems({
|
||||
id: workspace.id,
|
||||
index,
|
||||
tabCount: this.panels.workspaces().length,
|
||||
isRemote: this.panels.workspaceIsRemote(workspace),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** "Visual Studio Code.app" -> "Visual Studio Code"; empty -> system default. */
|
||||
editorAppName(): string {
|
||||
const path = this.settings.editorApp();
|
||||
|
||||
Reference in New Issue
Block a user