Merge main into feat/7css-skin + give the HTML context menu an Aero skin
main retired electrobun's native context menu (a documented no-op on Linux) in favour of an HTML one on every platform. Merging that here needs the menu to look right under the Win7 skin as well. Conflicts, all resolved by keeping both sides: - src/bun/index.ts — createAppRPC keeps the branch's mainWindow getter, drops main's removed contextMenu dep. - ui/src/app/app.ts — imports both TitleBar and ContextMenu. - ui/src/app/platform.ts — kept the branch's version (its IS_MAC_CHROME accounts for the frameless win7 chrome, and MOD_LABEL correctly keys on the real platform rather than the chrome), plus exporting IS_MAC and adding IS_LINUX for the menu's per-OS shell wording. Aero menu (branch-only): 7.css has no menu component, so it is hand-authored across both layers — the row <button>s shed their push-button slab in `skin` (revert-layer only reaches `utilities` from there), and the square-cornered slab, hairline border and gradient selection band are stated in `skinfix`. Built strictly from existing --w7-* vars, never literal hex, so the hand-written dark Win7 palette recolours it for free. Hover and the keyboard cursor share one band (the component marks the cursor with `bg-accent`, which is overridden alongside :hover) as a real Win7 menu does. Also answers a question the merge raised: frameless windows DO work on Linux. The GTK wrapper honours titleBarStyle "hidden" — it calls gtk_window_set_decorated(false) and logs "GTK: Created window without decorations (custom titlebar)". Verified on GNOME/Wayland and documented in architecture.md. Verified: backend `tsc --noEmit` clean, `bun run build:ui` clean (492.52 kB initial, still under the 500 kB budget), app launches with the skin active, frameless, no console errors and no GBM failures. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -55,5 +55,6 @@
|
||||
<app-overwrite-dialog [request]="req" />
|
||||
}
|
||||
|
||||
<app-context-menu />
|
||||
<app-toasts />
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { I18nService } from './i18n.service';
|
||||
import { BRANDING } from '@shared/branding';
|
||||
import type { ColumnKey } from '@shared/types';
|
||||
import { ConnectionsService } from './connections.service';
|
||||
import { ContextMenu } from './context-menu';
|
||||
import { ChmodDialog } from './dialogs/chmod-dialog';
|
||||
import { ConfirmDialog } from './dialogs/confirm-dialog';
|
||||
import { OverwriteDialog } from './dialogs/overwrite-dialog';
|
||||
@@ -38,6 +39,7 @@ import { UpdaterService } from './updater.service';
|
||||
Toasts,
|
||||
TransferBar,
|
||||
TitleBar,
|
||||
ContextMenu,
|
||||
],
|
||||
templateUrl: './app.html',
|
||||
host: {
|
||||
@@ -194,10 +196,22 @@ export class App {
|
||||
const target = this.actions.menuTarget;
|
||||
if (!target) return;
|
||||
const { panel, side, entry } = target;
|
||||
// the empty-space menu acts on the directory itself
|
||||
const path = entry?.path ?? panel.cwd();
|
||||
switch (action) {
|
||||
case 'ctx-open':
|
||||
if (entry) panel.open(entry);
|
||||
break;
|
||||
// local shell integration — only bun can reach the OS shell/clipboard
|
||||
case 'ctx-reveal':
|
||||
void this.rpc.request.shellItemAction({ action: 'reveal', path });
|
||||
break;
|
||||
case 'ctx-open-default':
|
||||
void this.rpc.request.shellItemAction({ action: 'open', path });
|
||||
break;
|
||||
case 'ctx-copy-path':
|
||||
void this.rpc.request.shellItemAction({ action: 'copyPath', path });
|
||||
break;
|
||||
case 'ctx-edit':
|
||||
void this.actions.edit(panel);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import type { ColumnKey, EntryType } from '@shared/types';
|
||||
import { I18nService } from './i18n.service';
|
||||
import { IS_MAC, IS_WINDOWS } from './platform';
|
||||
import { RpcService } from './rpc.service';
|
||||
|
||||
/** Optional list columns, in display order (Name is fixed). */
|
||||
const COLUMN_KEYS: ColumnKey[] = ['size', 'mtime', 'kind', 'rights', 'owner', 'group'];
|
||||
|
||||
export interface MenuItem {
|
||||
type?: 'separator';
|
||||
label?: string;
|
||||
/** Emitted verbatim to the action dispatcher (`ctx-*` / `col:*`). */
|
||||
action?: string;
|
||||
checked?: boolean;
|
||||
tooltip?: string;
|
||||
/** Shortcut hint, rendered right-aligned. */
|
||||
accelerator?: string;
|
||||
}
|
||||
|
||||
export interface MenuRequest {
|
||||
x: number;
|
||||
y: number;
|
||||
items: MenuItem[];
|
||||
}
|
||||
|
||||
export interface RowMenuContext {
|
||||
path: string;
|
||||
name: string;
|
||||
type: EntryType;
|
||||
isLocal: boolean;
|
||||
hasEntry: boolean; // false = right-click on empty panel space
|
||||
}
|
||||
|
||||
// PORTABILITY: shell-integration wording per OS (resolved at build time for locale)
|
||||
const revealKey = (): string =>
|
||||
IS_MAC ? 'ctx.revealFinder' : IS_WINDOWS ? 'ctx.revealExplorer' : 'ctx.revealFileManager';
|
||||
const trashKey = (): string => (IS_WINDOWS ? 'ctx.moveToRecycleBin' : 'ctx.moveToTrash');
|
||||
|
||||
/**
|
||||
* Builds and opens the panel right-click menus. This used to be a native
|
||||
* electrobun ContextMenu on the bun side, but `showContextMenu` is a documented
|
||||
* no-op on Linux (electrobun's own dist/api/bun/proc/linux.md), which left the
|
||||
* whole right-click surface dead there. The menu is plain HTML now — one code
|
||||
* path on every platform — and still emits the exact same action strings, so
|
||||
* the dispatcher in app.ts is unchanged.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ContextMenuService {
|
||||
private readonly i18n = inject(I18nService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
|
||||
readonly request = signal<MenuRequest | null>(null);
|
||||
|
||||
open(event: MouseEvent, items: MenuItem[]): void {
|
||||
this.request.set({ x: event.clientX, y: event.clientY, items });
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.request.set(null);
|
||||
}
|
||||
|
||||
/** A picked item goes through the same dispatcher the app menubar uses. */
|
||||
onPick(action: string): void {
|
||||
this.rpc.emitMenuAction(action);
|
||||
}
|
||||
|
||||
/** Row / empty-space menu for a file panel. */
|
||||
rowItems(ctx: RowMenuContext): MenuItem[] {
|
||||
const t = (key: string) => this.i18n.t(key);
|
||||
const items: MenuItem[] = [];
|
||||
|
||||
if (!ctx.hasEntry || ctx.name === '..') {
|
||||
// empty space (or the parent row): folder-level actions
|
||||
items.push({ label: t('ctx.newFolder'), action: 'ctx-new-folder' });
|
||||
items.push({ label: t('ctx.newFile'), action: 'ctx-new-file' });
|
||||
items.push({ type: 'separator' });
|
||||
items.push({ label: t('ctx.refresh'), action: 'ctx-refresh' });
|
||||
if (ctx.isLocal) {
|
||||
items.push({ type: 'separator' });
|
||||
items.push({ label: t(revealKey()), action: 'ctx-reveal' });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
items.push({ label: t('ctx.copyToOther'), action: 'ctx-copy' });
|
||||
if (ctx.type === 'dir') {
|
||||
items.push({ label: t('ctx.open'), action: 'ctx-open' });
|
||||
} else {
|
||||
if (ctx.isLocal) {
|
||||
items.push({ label: t('ctx.open'), action: 'ctx-open-default', tooltip: t('ctx.openDefault') });
|
||||
}
|
||||
items.push({ label: t('ctx.edit'), action: 'ctx-edit', accelerator: 'F4' });
|
||||
}
|
||||
items.push({ label: t('ctx.rename'), action: 'ctx-rename' });
|
||||
// chmod on Windows local files only flips the read-only bit — hide the lie
|
||||
if (!(ctx.isLocal && IS_WINDOWS)) {
|
||||
items.push({ label: t('ctx.permissions'), action: 'ctx-chmod' });
|
||||
}
|
||||
items.push({ type: 'separator' });
|
||||
items.push({ label: t('ctx.copyPath'), action: 'ctx-copy-path' });
|
||||
if (ctx.isLocal) {
|
||||
items.push({ label: t(revealKey()), action: 'ctx-reveal' });
|
||||
}
|
||||
items.push({ type: 'separator' });
|
||||
if (ctx.isLocal) {
|
||||
items.push({
|
||||
label: t(trashKey()),
|
||||
action: 'ctx-trash',
|
||||
...(IS_MAC ? { tooltip: t('ctx.trashTooltip') } : {}),
|
||||
});
|
||||
}
|
||||
items.push({ label: t('ctx.delete'), action: 'ctx-delete' });
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Column picker for the list headers: checkable toggles per column. */
|
||||
columnItems(visible: ColumnKey[]): MenuItem[] {
|
||||
return COLUMN_KEYS.map((key) => ({
|
||||
label: this.i18n.t(`columns.${key}`),
|
||||
action: `col:${key}`,
|
||||
checked: visible.includes(key),
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { ContextMenuService, type MenuItem } from './context-menu.service';
|
||||
import { FocusService } from './focus.service';
|
||||
|
||||
/** Rough per-item metrics, used only to decide whether the menu must flip. */
|
||||
const ITEM_H = 26;
|
||||
const SEPARATOR_H = 7;
|
||||
const MENU_W = 224; // w-56
|
||||
const EDGE_GAP = 6;
|
||||
|
||||
/**
|
||||
* The panel right-click menu. Replaces electrobun's native ContextMenu, which
|
||||
* is a no-op on Linux — see ContextMenuService for the why. Emits the same
|
||||
* `ctx-*` / `col:*` action strings the native menu did.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-context-menu',
|
||||
imports: [NgIcon],
|
||||
template: `
|
||||
@if (menu.request(); as req) {
|
||||
<!-- click-outside catcher; mousedown so it beats the panel's own handlers -->
|
||||
<div class="fixed inset-0 z-50" (mousedown)="close()" (contextmenu)="onBackdropContext($event)"></div>
|
||||
<div
|
||||
#menuEl
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
(keydown)="onKeydown($event)"
|
||||
[style.left.px]="position().x"
|
||||
[style.top.px]="position().y"
|
||||
class="w7-menu fixed z-50 w-56 rounded-lg border border-border bg-card py-1 text-card-foreground shadow-xl outline-none"
|
||||
>
|
||||
@for (item of req.items; track $index) {
|
||||
@if (item.type === 'separator') {
|
||||
<div class="w7-menu-sep my-1 border-t border-border"></div>
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
[title]="item.tooltip ?? ''"
|
||||
[class.bg-accent]="$index === activeIndex()"
|
||||
(mouseenter)="activeIndex.set($index)"
|
||||
(click)="pick(item)"
|
||||
class="flex w-full items-center gap-2 px-3 py-1 text-left text-(length:--path-font) hover:bg-accent"
|
||||
>
|
||||
<span class="w-3.5 shrink-0">
|
||||
@if (item.checked) {
|
||||
<ng-icon name="mynaCheck" size="0.85rem" class="text-primary" />
|
||||
}
|
||||
</span>
|
||||
<span class="flex-1 truncate">{{ item.label }}</span>
|
||||
@if (item.accelerator) {
|
||||
<span class="shrink-0 text-muted-foreground">{{ item.accelerator }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
// Closing on scroll/blur matches native menu behavior: the anchor row moves
|
||||
// out from under a menu that would otherwise float over stale content.
|
||||
host: {
|
||||
'(window:blur)': 'close()',
|
||||
'(window:resize)': 'close()',
|
||||
'(document:scroll)': 'close()',
|
||||
},
|
||||
})
|
||||
export class ContextMenu {
|
||||
readonly menu = inject(ContextMenuService);
|
||||
private readonly focus = inject(FocusService);
|
||||
private readonly menuEl = viewChild<ElementRef<HTMLDivElement>>('menuEl');
|
||||
|
||||
readonly activeIndex = signal(-1);
|
||||
|
||||
/** Click point, flipped/clamped so the menu never overflows the viewport. */
|
||||
readonly position = computed(() => {
|
||||
const req = this.menu.request();
|
||||
if (!req) return { x: 0, y: 0 };
|
||||
const height =
|
||||
req.items.reduce((sum, i) => sum + (i.type === 'separator' ? SEPARATOR_H : ITEM_H), 0) + 8;
|
||||
const x =
|
||||
req.x + MENU_W > window.innerWidth - EDGE_GAP
|
||||
? Math.max(EDGE_GAP, req.x - MENU_W)
|
||||
: req.x;
|
||||
const y =
|
||||
req.y + height > window.innerHeight - EDGE_GAP
|
||||
? Math.max(EDGE_GAP, req.y - height)
|
||||
: req.y;
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Focus the menu once it exists so arrow keys / Escape reach it. Keyed on
|
||||
// the viewChild signal, NOT on request(): under zoneless CD an effect can
|
||||
// run before the @if has rendered the element.
|
||||
effect(() => this.menuEl()?.nativeElement.focus());
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (!this.menu.request()) return;
|
||||
this.menu.close();
|
||||
this.activeIndex.set(-1); // a fresh menu starts with nothing highlighted
|
||||
this.focus.bump();
|
||||
}
|
||||
|
||||
pick(item: MenuItem): void {
|
||||
if (!item.action) return;
|
||||
const action = item.action;
|
||||
this.close();
|
||||
this.menu.onPick(action);
|
||||
}
|
||||
|
||||
/** Right-clicking the backdrop closes rather than stacking a second menu. */
|
||||
onBackdropContext(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
}
|
||||
|
||||
onKeydown(event: KeyboardEvent): void {
|
||||
const items = this.menu.request()?.items ?? [];
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.close();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
this.step(items, 1);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
this.step(items, -1);
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ': {
|
||||
event.preventDefault();
|
||||
const item = items[this.activeIndex()];
|
||||
if (item) this.pick(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Move the highlight by one selectable item, skipping separators, wrapping. */
|
||||
private step(items: MenuItem[], delta: number): void {
|
||||
const count = items.length;
|
||||
if (!count) return;
|
||||
let index = this.activeIndex();
|
||||
for (let hops = 0; hops < count; hops++) {
|
||||
index = (index + delta + count) % count;
|
||||
if (items[index].type !== 'separator') {
|
||||
this.activeIndex.set(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import type { FileEntry } from '@shared/types';
|
||||
import { ContextMenuService } from './context-menu.service';
|
||||
import { DialogsService } from './dialogs.service';
|
||||
import { joinPath } from './format';
|
||||
import { type PanelState, type Side } from './panels.service';
|
||||
@@ -26,21 +27,25 @@ export class FileActionsService {
|
||||
private readonly rpc = inject(RpcService);
|
||||
private readonly toast = inject(ToastService);
|
||||
private readonly dialogs = inject(DialogsService);
|
||||
private readonly menu = inject(ContextMenuService);
|
||||
private readonly settings = inject(SettingsService);
|
||||
|
||||
menuTarget: MenuTarget | null = null;
|
||||
|
||||
/** Right-click entry point: remember the target and ask bun for a native menu. */
|
||||
async openContextMenu(target: MenuTarget): Promise<void> {
|
||||
/** Right-click entry point: remember the target, then open the menu on it. */
|
||||
openContextMenu(event: MouseEvent, target: MenuTarget): void {
|
||||
this.menuTarget = target;
|
||||
const { panel, entry } = target;
|
||||
await this.rpc.request.showRowMenu({
|
||||
path: entry?.path ?? panel.cwd(),
|
||||
name: entry?.name ?? '',
|
||||
type: entry?.type ?? 'dir',
|
||||
isLocal: panel.sessionId() === 'local',
|
||||
hasEntry: !!entry,
|
||||
});
|
||||
this.menu.open(
|
||||
event,
|
||||
this.menu.rowItems({
|
||||
path: entry?.path ?? panel.cwd(),
|
||||
name: entry?.name ?? '',
|
||||
type: entry?.type ?? 'dir',
|
||||
isLocal: panel.sessionId() === 'local',
|
||||
hasEntry: !!entry,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async trash(panel: PanelState, entry: FileEntry): Promise<void> {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { NgIcon } from '@ng-icons/core';
|
||||
import { I18nService } from '../i18n.service';
|
||||
import type { ColumnKey, FileEntry, SavedConnection, SshConfigHost } from '@shared/types';
|
||||
import { ConnectionsService } from '../connections.service';
|
||||
import { ContextMenuService } from '../context-menu.service';
|
||||
import { DialogsService } from '../dialogs.service';
|
||||
import { FileActionsService } from '../file-actions.service';
|
||||
import { fileGlyph, fileIconClass, fileKindKey } from '../file-icons';
|
||||
@@ -47,6 +48,7 @@ export class FilePanel {
|
||||
readonly transfers = inject(TransfersService);
|
||||
readonly connections = inject(ConnectionsService);
|
||||
readonly dialogs = inject(DialogsService);
|
||||
private readonly menu = inject(ContextMenuService);
|
||||
private readonly settings = inject(SettingsService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
readonly panel = computed(() => this.panels.get(this.side()));
|
||||
@@ -88,7 +90,7 @@ export class FilePanel {
|
||||
|
||||
headerContextMenu(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
void this.rpc.request.showColumnsMenu({ visible: this.settings.columns() });
|
||||
this.menu.open(event, this.menu.columnItems(this.settings.columns()));
|
||||
}
|
||||
|
||||
pickDrive(drive: string): void {
|
||||
@@ -238,14 +240,14 @@ export class FilePanel {
|
||||
// standard behavior: keep the selection when right-clicking inside it
|
||||
if (entry && !panel.selected().has(entry.name)) panel.clearSelection();
|
||||
panel.cursor.set(index);
|
||||
void this.actions.openContextMenu({ side: this.side(), panel, entry });
|
||||
this.actions.openContextMenu(event, { side: this.side(), panel, entry });
|
||||
}
|
||||
|
||||
listContextMenu(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
if (this.blocked()) return;
|
||||
this.activate();
|
||||
void this.actions.openContextMenu({ side: this.side(), panel: this.panel(), entry: null });
|
||||
this.actions.openContextMenu(event, { side: this.side(), panel: this.panel(), entry: null });
|
||||
}
|
||||
|
||||
openAt(index: number): void {
|
||||
|
||||
@@ -18,7 +18,7 @@ declare global {
|
||||
|
||||
export const PLATFORM = typeof window !== 'undefined' ? (window.__APP_PLATFORM ?? '') : '';
|
||||
|
||||
const IS_MAC = PLATFORM === 'darwin';
|
||||
export const IS_MAC = PLATFORM === 'darwin';
|
||||
|
||||
/**
|
||||
* The win7 skin makes the window frameless on EVERY platform and draws its own
|
||||
@@ -40,6 +40,8 @@ export const IS_MAC_CHROME = IS_MAC && !IS_WIN7_CHROME;
|
||||
|
||||
export const IS_WINDOWS = PLATFORM === 'win32';
|
||||
|
||||
export const IS_LINUX = PLATFORM === 'linux';
|
||||
|
||||
/** Shortcut-hint labels: "⌘W" on macOS, "Ctrl+W" elsewhere. Keyed on the real
|
||||
* platform, NOT IS_MAC_CHROME — the keyboard does not change with the skin. */
|
||||
export const MOD_LABEL = IS_MAC ? '⌘' : 'Ctrl+';
|
||||
|
||||
@@ -103,6 +103,15 @@ export class RpcService {
|
||||
this.menuListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch an action that originated in the UI (the HTML context menu) as if
|
||||
* it had arrived from bun, so app.ts keeps ONE handler for menu actions
|
||||
* regardless of whether the app menubar or a right-click produced them.
|
||||
*/
|
||||
emitMenuAction(action: string): void {
|
||||
for (const listener of this.menuListeners) listener(action);
|
||||
}
|
||||
|
||||
onSettingsChanged(listener: (settings: AppSettings) => void): void {
|
||||
this.settingsListeners.add(listener);
|
||||
}
|
||||
|
||||
@@ -579,6 +579,19 @@ body {
|
||||
.win7 .w7-flat::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* Context-menu rows are <button>s, so 7.css would paint each one as an Aero
|
||||
push-button slab. Strip the slab here (revert-layer needs `skin`); the
|
||||
Win7 menu paint itself is stated in `skinfix` below. */
|
||||
.win7 .w7-menu button {
|
||||
background: revert-layer;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.win7 .w7-menu button::before,
|
||||
.win7 .w7-menu button::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Above `skin`, for rules that must state a value rather than defer to a
|
||||
@@ -591,4 +604,44 @@ body {
|
||||
min-height: 23px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* Aero context menu. 7.css has no menu component, so this is hand-authored —
|
||||
but ONLY out of the existing --w7-* vars, never literal hex, so the
|
||||
hand-written dark Win7 palette above recolours it for free. Values are
|
||||
stated (not reverted), hence `skinfix` rather than `skin`.
|
||||
A real Win7 menu is a square-cornered white slab with a hairline border
|
||||
and a gradient selection band that spans the full row. */
|
||||
.win7 .w7-menu {
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--w7-el-bd);
|
||||
background: var(--w7-surface);
|
||||
color: var(--w7-el-c);
|
||||
font: var(--w7-font);
|
||||
padding: 2px;
|
||||
box-shadow: 0 2px 6px #0000004d;
|
||||
}
|
||||
.win7 .w7-menu button {
|
||||
border-radius: 0;
|
||||
padding: 3px 8px;
|
||||
color: var(--w7-el-c);
|
||||
}
|
||||
/* Hover AND keyboard highlight share the band — the utility `bg-accent` that
|
||||
drives the keyboard cursor is overridden here, so both look identical. */
|
||||
.win7 .w7-menu button:hover,
|
||||
.win7 .w7-menu button.bg-accent {
|
||||
background: var(--w7-li-bg-hl);
|
||||
border: 1px solid var(--w7-li-bd-hl);
|
||||
/* keep the 1px border from shifting the label */
|
||||
padding: 2px 7px;
|
||||
}
|
||||
.win7 .w7-menu .w7-menu-sep {
|
||||
border-color: var(--w7-el-bd);
|
||||
margin: 2px 0;
|
||||
}
|
||||
/* Accelerator hints ride the muted colour, like Win7's grey shortcut column.
|
||||
Keyed on the utility class the accelerator span carries — `:last-child`
|
||||
would also catch the LABEL on every item that has no accelerator. */
|
||||
.win7 .w7-menu button > span.text-muted-foreground {
|
||||
color: var(--w7-el-c-d);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user