Fix lit pads not highlighting; stop swallowing haptics errors

A lit pad kept its grey text and only its border turned orange, so pads
looked unhighlighted next to every other key. Cause was cascade order,
not logic: .pad is a single-class selector like .lit, so equal
specificity meant whichever came last won, and .pad was declared after.
The state classes now sit at the bottom of the file with a comment
saying why, and a lit pad computes rgb(255,74,0) for text and border.

native.ts reported nothing when haptics failed because I wrote
catch(() => {}) and discarded the reason. It now reports once per
distinct failure - enough to read in logcat, quiet enough not to spam on
every key press.

That reporting shows the software path is fine: isNativePlatform true,
Haptics available and bridged, VIBRATE granted, and both impact and
vibrate resolve without rejecting. So the silence is below the bridge -
most likely the phone's own touch-vibration setting - and not something
this code can detect.

141 tests, typecheck clean, installed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 21:05:01 +03:00
parent d0f872fc8b
commit 6889167fcf
2 changed files with 63 additions and 45 deletions

View File

@@ -1,4 +1,10 @@
/* Measured geometry: 8 + 36 + 8 + 124 + 10 + 134 + 10 + 187 + 8 = 525. */ /* Measured geometry: 8 + 36 + 8 + 124 + 10 + 134 + 10 + 187 + 8 = 525.
*
* Ordering matters here: the state classes (.lit, .sel, .held, .down) live at
* the BOTTOM of this file. They are single-class selectors, same specificity as
* .pad or .fx, so whichever comes last wins. With .pad declared after .lit, a
* lit pad kept its grey text and only the border turned orange — pads looked
* unhighlighted next to every other key. */
.panel { .panel {
height: var(--panel-h); height: var(--panel-h);
@@ -16,11 +22,6 @@
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
/* Pressed, whether or not it is lit. */
.down {
transform: scale(0.95);
}
/* ---- shared key look ---- */ /* ---- shared key look ---- */
.key, .key,
@@ -53,27 +54,6 @@
overflow: hidden; overflow: hidden;
} }
/* Lit is an outline, not a fill. The SHIFT layer lights 35 of 54 controls, and
35 solid orange keys is a wall; solid is reserved for the selected one. */
.lit {
border-color: var(--accent);
color: var(--accent);
font-weight: 700;
}
.sel {
background: var(--accent);
border-color: var(--accent);
color: var(--on-accent);
font-weight: 700;
}
.held {
background: var(--ink);
color: var(--page);
font-weight: 700;
}
.badge { .badge {
position: absolute; position: absolute;
top: 1px; top: 1px;
@@ -102,20 +82,6 @@
color: var(--pad-ink); color: var(--pad-ink);
} }
/* A round key cannot hold a lit function's label at full size: SHIFT + VALUE is
"MARK FN", which spilled outside the circle. Clip it, and shrink when lit. */
.knob,
.valueKnob {
overflow: hidden;
}
.knob.lit,
.knob.sel,
.valueKnob.lit,
.valueKnob.sel {
font-size: 6px;
}
/* ---- fx and the screen ---- */ /* ---- fx and the screen ---- */
.screenRow { .screenRow {
@@ -286,6 +252,11 @@
border-radius: 4px; border-radius: 4px;
} }
.side {
color: var(--accent);
font-weight: 700;
}
/* The pad's number, as printed on the hardware. */ /* The pad's number, as printed on the hardware. */
.num { .num {
font-family: var(--mono); font-family: var(--mono);
@@ -294,7 +265,39 @@
color: var(--muted); color: var(--muted);
} }
.side { /* ---- state: LAST in the file, so it wins over .pad, .fx, .side and .knob ---- */
/* Lit is an outline, not a fill. The SHIFT layer lights 35 of 54 controls, and
35 solid orange keys is a wall; solid is reserved for the selected one. */
.lit {
border-color: var(--accent);
color: var(--accent); color: var(--accent);
font-weight: 700; font-weight: 700;
} }
.sel {
background: var(--accent);
border-color: var(--accent);
color: var(--on-accent);
font-weight: 700;
}
.held {
background: var(--ink);
color: var(--page);
font-weight: 700;
}
/* A round key cannot hold a lit function's label at full size: SHIFT + VALUE is
MARK FN, which spilled outside the circle. Shrink it when lit. */
.knob.lit,
.knob.sel,
.valueKnob.lit,
.valueKnob.sel {
font-size: 6px;
}
/* Pressed, whether or not it is lit. */
.down {
transform: scale(0.95);
}

View File

@@ -9,18 +9,33 @@ import { Haptics, ImpactStyle, NotificationType } from "@capacitor/haptics";
const isNative = (): boolean => Capacitor.isNativePlatform(); const isNative = (): boolean => Capacitor.isNativePlatform();
/**
* Swallowing these was a mistake: haptics did nothing on the device and the
* reason had been discarded. Report once per distinct failure — enough to read
* in logcat, quiet enough not to spam on every key press.
*/
const reported = new Set<string>();
function report(where: string, error: unknown): void {
const message = error instanceof Error ? error.message : String(error);
const key = where + ":" + message;
if (reported.has(key)) return;
reported.add(key);
console.error("haptics " + where + " failed:", message);
}
/** A light tick under every key press, as the reference's buzz() did. */ /** A light tick under every key press, as the reference's buzz() did. */
export function tapFeedback(): void { export function tapFeedback(): void {
if (!isNative()) return; if (!isNative()) return;
void Haptics.impact({ style: ImpactStyle.Light }).catch(() => {}); Haptics.impact({ style: ImpactStyle.Light }).catch((e) => report("impact", e));
} }
/** Shaped feedback on a graded answer: right and wrong should feel different. */ /** Shaped feedback on a graded answer: right and wrong should feel different. */
export function gradeFeedback(correct: boolean): void { export function gradeFeedback(correct: boolean): void {
if (!isNative()) return; if (!isNative()) return;
void Haptics.notification({ Haptics.notification({
type: correct ? NotificationType.Success : NotificationType.Error, type: correct ? NotificationType.Success : NotificationType.Error,
}).catch(() => {}); }).catch((e) => report("notification", e));
} }
export function exitApp(): void { export function exitApp(): void {