[ui] fix: the tab scrollbar was swallowing clicks on half of every tab
Reproduced the "clicking POWER does nothing" report with a real mouse: elementFromPoint at the button's own centre returned the strip container, not the button. The horizontal scrollbar was painted over the lower half of every tab and ate clicks there — which is exactly why keyboard paging still worked and clicking didn't. Fixed by giving the scrollbar its own lane (padding-bottom >= its height). The full button height now hit-tests to the button and a real click activates the tab. The strip itself is the approved round-5 design: one row that scrolls rather than wrapping. The catalog keeps growing — RESEARCH joined in v4 and threw REMOVE onto a line of its own — and a wrapping row moves every other tab under the player's cursor. Paging with [ ] scrolls the active tab into view. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c441ec3a53
commit
7e24eab171
@ -115,6 +115,9 @@ export function createBuildBar(data: GameData, sel: BuildSelection): BuildBar {
|
||||
function render() {
|
||||
buildButtons(pages[active]);
|
||||
for (let i = 0; i < tabs.length; i++) cls(tabs[i], 'is-active', i === active);
|
||||
// The strip scrolls, so paging with [ ] must bring its tab into view or the player
|
||||
// pages blind. `nearest` avoids yanking the strip when the tab is already visible.
|
||||
tabs[active]?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
||||
sync();
|
||||
}
|
||||
|
||||
|
||||
@ -125,8 +125,30 @@ const CSS = `
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Kind tabs. A page holds at most nine so 1-9 always reaches every machine. */
|
||||
.fk-tabs { display: flex; gap: 3px; margin-bottom: 5px; flex-wrap: wrap; justify-content: center; }
|
||||
/* Kind tabs. A page holds at most nine so 1-9 always reaches every machine.
|
||||
v5: a single scrolling strip rather than a wrapping row — the catalog keeps growing
|
||||
(RESEARCH joined in v4 and threw REMOVE onto a line of its own), and tabs jumping to a
|
||||
second row moves every other tab under the player's cursor. */
|
||||
.fk-tabs {
|
||||
display: flex; gap: 3px; margin-bottom: 5px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
max-width: 100%;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--fk-line) transparent;
|
||||
/* The scrollbar gets its OWN lane below the buttons. With too little padding it is
|
||||
painted over the lower half of every tab and swallows clicks there — the exact
|
||||
"clicking POWER does nothing" bug from review, which is why keyboard paging still
|
||||
worked. Must stay >= the scrollbar height below. */
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
.fk-tabs::-webkit-scrollbar { height: 4px; }
|
||||
.fk-tabs::-webkit-scrollbar-track { background: transparent; }
|
||||
.fk-tabs::-webkit-scrollbar-thumb { background: var(--fk-line); }
|
||||
.fk-tabs::-webkit-scrollbar-thumb:hover { background: var(--fk-dim); }
|
||||
/* Tabs must not shrink into unreadable slivers when the strip overflows. */
|
||||
.fk-tab { flex: 0 0 auto; }
|
||||
.fk-tab {
|
||||
background: var(--fk-bg-solid);
|
||||
border: 1px solid var(--fk-line);
|
||||
@ -138,9 +160,8 @@ const CSS = `
|
||||
.fk-tab:hover { color: var(--fk-fg); border-color: var(--fk-cool); }
|
||||
.fk-tab.is-active { color: var(--fk-cool); border-color: var(--fk-cool); background: #0c1a1a; }
|
||||
|
||||
/* REMOVE is a mode, not a machine — it sits with the tabs and reads as a hazard.
|
||||
No auto margin: with the RESEARCH tab added the row wraps, and an auto margin threw
|
||||
REMOVE onto a line of its own. */
|
||||
/* REMOVE is a mode, not a machine — it sits at the end of the strip and reads as a
|
||||
hazard. It scrolls with the tabs; the hotkey (X) is always available regardless. */
|
||||
.fk-tab-remove { margin-left: 10px; color: var(--fk-dim); }
|
||||
.fk-tab-remove:hover { color: var(--fk-red); border-color: var(--fk-red); }
|
||||
.fk-tab-remove.is-armed {
|
||||
@ -227,6 +248,11 @@ const CSS = `
|
||||
.fk-ins-heat-state { font-size: 10px; color: var(--fk-amber); margin-top: 3px; }
|
||||
.fk-ins-heat-state.is-scram { color: var(--fk-red); font-weight: bold; }
|
||||
|
||||
/* MOSQUITO EXPOSURE — wildlife is a hazard, not a fault: amber, quiet, matter-of-fact. */
|
||||
.fk-ins-exposure { display: none; margin-top: 6px; }
|
||||
.fk-ins-exposure.is-on { display: block; }
|
||||
.fk-ins-exposure-pct { color: var(--fk-amber); font-size: 10px; }
|
||||
|
||||
/* Process select: a native <select> beaten into the house style. */
|
||||
.fk-ins-recipe {
|
||||
width: 100%;
|
||||
@ -354,10 +380,107 @@ const CSS = `
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- screen enlarge */
|
||||
|
||||
#fk-screenview {
|
||||
display: none;
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(4, 3, 8, 0.86);
|
||||
align-items: center; justify-content: center;
|
||||
pointer-events: auto;
|
||||
z-index: 40;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
#fk-screenview.is-open { display: flex; }
|
||||
/* THE SCREEN itself is enlarged (see screenview.ts — it's a WebGL canvas and can't be
|
||||
mirrored), so the backdrop only dims. */
|
||||
#screen { cursor: zoom-in; }
|
||||
|
||||
/* ---------------------------------------------------------------- tuner dock */
|
||||
|
||||
#fk-tuner {
|
||||
/* Off-stage until the fossil is dug up. */
|
||||
display: none;
|
||||
bottom: 10px; right: 10px;
|
||||
width: 250px;
|
||||
z-index: 13;
|
||||
}
|
||||
#fk-tuner.is-revealed {
|
||||
display: block;
|
||||
animation: fk-tuner-in 620ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
/* Arrives like equipment being wheeled in, not like a notification. */
|
||||
@keyframes fk-tuner-in {
|
||||
0% { opacity: 0; transform: translateX(28px); }
|
||||
100% { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
#fk-tuner.is-muted { opacity: 0.72; }
|
||||
|
||||
/* The dock and the toast stack share the bottom-right corner. Once the dock is in, the
|
||||
toasts stand off above it rather than printing over the dial. The class goes on the
|
||||
toast stack itself rather than on an ancestor — same element, no descendant matching
|
||||
to get wrong. */
|
||||
#fk-toasts.is-standoff { bottom: 176px; }
|
||||
|
||||
.fk-tuner-readout {
|
||||
color: var(--fk-dim);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
margin: 4px 0 6px;
|
||||
min-height: 14px;
|
||||
}
|
||||
.fk-tuner-readout.is-locked { color: var(--fk-cool); text-shadow: 0 0 6px rgba(63,255,224,0.5); }
|
||||
|
||||
.fk-tuner-dial {
|
||||
position: relative;
|
||||
height: 14px;
|
||||
background: var(--fk-bg-solid);
|
||||
border: 1px solid var(--fk-line);
|
||||
cursor: ew-resize;
|
||||
pointer-events: auto;
|
||||
/* Tick marks, so the dial reads as a radio scale rather than a progress bar. */
|
||||
background-image: repeating-linear-gradient(
|
||||
90deg, var(--fk-line) 0 1px, transparent 1px 10px);
|
||||
}
|
||||
.fk-tuner-dial-fill {
|
||||
position: absolute; top: 0; bottom: 0; left: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(63, 255, 224, 0.25));
|
||||
border-right: 2px solid var(--fk-cool);
|
||||
box-shadow: 0 0 8px rgba(63, 255, 224, 0.6);
|
||||
}
|
||||
|
||||
.fk-tuner-bands { display: flex; gap: 3px; margin-top: 6px; }
|
||||
.fk-tuner-band {
|
||||
flex: 1;
|
||||
background: var(--fk-bg-solid); border: 1px solid var(--fk-line);
|
||||
color: var(--fk-faint); font: inherit; font-size: 9px; letter-spacing: 0.1em;
|
||||
padding: 2px 0; cursor: pointer; pointer-events: auto;
|
||||
}
|
||||
.fk-tuner-band:hover { color: var(--fk-fg); border-color: var(--fk-cool); }
|
||||
.fk-tuner-band.is-active { color: var(--fk-cool); border-color: var(--fk-cool); background: #0c1a1a; }
|
||||
|
||||
.fk-tuner-volrow { display: flex; align-items: center; gap: 5px; margin-top: 6px; }
|
||||
.fk-tuner-vol-label { color: var(--fk-faint); font-size: 9px; }
|
||||
.fk-tuner-vol {
|
||||
position: relative; flex: 1; height: 6px;
|
||||
background: var(--fk-bg-solid); border: 1px solid var(--fk-line);
|
||||
cursor: pointer; pointer-events: auto;
|
||||
}
|
||||
.fk-tuner-vol-fill { position: absolute; top: 0; bottom: 0; left: 0; background: var(--fk-dim); }
|
||||
.fk-tuner-mute {
|
||||
background: var(--fk-bg-solid); border: 1px solid var(--fk-line);
|
||||
color: var(--fk-dim); font: inherit; font-size: 9px;
|
||||
padding: 2px 6px; cursor: pointer; pointer-events: auto;
|
||||
}
|
||||
.fk-tuner-mute:hover { border-color: var(--fk-amber); color: var(--fk-amber); }
|
||||
.fk-tuner-mute.is-on { color: var(--fk-amber); border-color: var(--fk-amber); }
|
||||
.fk-tuner-hint { color: var(--fk-faint); font-size: 8px; letter-spacing: 0.1em; margin-top: 6px; }
|
||||
|
||||
/* ---------------------------------------------------------------- toasts */
|
||||
|
||||
#fk-toasts {
|
||||
position: fixed; bottom: 10px; right: 10px;
|
||||
transition: bottom 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
|
||||
pointer-events: none;
|
||||
z-index: 11;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user