From 6889167fcfdfb14266b3c82e21b5fbf161a76c56 Mon Sep 17 00:00:00 2001 From: ncode Date: Thu, 17 Sep 2026 21:05:01 +0300 Subject: [PATCH] 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 --- src/ui/Panel.module.css | 87 +++++++++++++++++++++-------------------- src/ui/native.ts | 21 ++++++++-- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/src/ui/Panel.module.css b/src/ui/Panel.module.css index 907dda3..f7a7234 100644 --- a/src/ui/Panel.module.css +++ b/src/ui/Panel.module.css @@ -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 { height: var(--panel-h); @@ -16,11 +22,6 @@ -webkit-touch-callout: none; } -/* Pressed, whether or not it is lit. */ -.down { - transform: scale(0.95); -} - /* ---- shared key look ---- */ .key, @@ -53,27 +54,6 @@ 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 { position: absolute; top: 1px; @@ -102,20 +82,6 @@ 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 ---- */ .screenRow { @@ -286,6 +252,11 @@ border-radius: 4px; } +.side { + color: var(--accent); + font-weight: 700; +} + /* The pad's number, as printed on the hardware. */ .num { font-family: var(--mono); @@ -294,7 +265,39 @@ 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); 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); +} diff --git a/src/ui/native.ts b/src/ui/native.ts index b5d158e..64e2554 100644 --- a/src/ui/native.ts +++ b/src/ui/native.ts @@ -9,18 +9,33 @@ import { Haptics, ImpactStyle, NotificationType } from "@capacitor/haptics"; 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(); + +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. */ export function tapFeedback(): void { 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. */ export function gradeFeedback(correct: boolean): void { if (!isNative()) return; - void Haptics.notification({ + Haptics.notification({ type: correct ? NotificationType.Success : NotificationType.Error, - }).catch(() => {}); + }).catch((e) => report("notification", e)); } export function exitApp(): void {