From c7c827ecf8866e209fbfbfb23cc402a4f041dcf7 Mon Sep 17 00:00:00 2001 From: ncode Date: Thu, 17 Sep 2026 01:22:45 +0300 Subject: [PATCH] Phase 2: the info card, selection, and what 35 lit keys taught InfoCard fills the box that was empty: the layer hint and instruction when nothing is picked, or the selected combo as chips with its name, description and manual page. App carries the selection, and pressing a control that holds several functions cycles through them, as the reference does. Lit is now an outline rather than a fill, and solid orange means selected. This deviates from the mockups on purpose. They only ever showed three lit keys; the SHIFT layer lights 35 of 54 controls, and solid orange across all of them is a wall instead of a map. I checked that by rendering the SHIFT layer rather than assuming, which meant temporarily defaulting to it, capturing, and restoring the source. That capture also caught a defect nothing else would have: SHIFT + VALUE is MARK FN inside a 30px circle, and border-radius alone does not clip, so the label sat outside the knob. Round keys now hide overflow and shrink when lit. Verified in light, dark, and on the SHIFT layer at 360x735. Typecheck clean, 112 tests, www/ untouched so the Phase 1 APK still installs. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 9 +++++ src/App.tsx | 71 ++++++++++++++++++++++----------- src/ui/InfoCard.module.css | 81 ++++++++++++++++++++++++++++++++++++++ src/ui/InfoCard.tsx | 49 +++++++++++++++++++++++ src/ui/Panel.module.css | 23 +++++++++++ src/ui/Panel.tsx | 24 ++++++++--- 6 files changed, 228 insertions(+), 29 deletions(-) create mode 100644 src/ui/InfoCard.module.css create mode 100644 src/ui/InfoCard.tsx diff --git a/CLAUDE.md b/CLAUDE.md index e3b6dd2..591b87d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -218,6 +218,15 @@ missing from the panel. The bank row is pinned at six keys ending in SHIFT because `Panel.module.css` widens it with `:last-child:nth-child(6)`, which stops matching silently if that row ever changes length. +**Lit is an outline; the fill means selected.** This deviates from the mockups, +which only ever showed three lit keys at once. The SHIFT layer lights 35 of 54 +controls, and solid orange across all of them is a wall rather than a map. Orange +outline says "something is here"; solid orange says "this is the one you tapped". + +**Round keys clip and shrink when lit.** `SHIFT + VALUE` is `MARK FN` inside a +30px circle. `border-radius: 50%` alone does not clip, so the label escaped the +circle entirely; the knobs need `overflow: hidden` and a smaller size when lit. + **Two strings are load-bearing for migration** and must not be changed casually: `Entry.id` (below) and `STORAGE_KEY`, still `sp404mk2-layers-v1`. Together they mean the boxes already on the phone are picked up by the React app rather than diff --git a/src/App.tsx b/src/App.tsx index aed39ff..fe2e634 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,22 @@ /** - * App shell: pickers, the panel, and the tab bar. + * Explore: pick a mode and a layer, then read what the panel lights up. * * Angular: useState is roughly a component field plus change detection. Setting - * it re-runs this function, so derived values below are recomputed each render; + * it re-runs this function, so derived values are recomputed each render; * useMemo is the explicit opt-out when that stops being cheap. */ import { useMemo, useState } from "react"; import { ENTRIES } from "./domain/entries"; import { controlName } from "./domain/format"; -import { CTX } from "./domain/labels"; -import { entriesByTarget, entriesForLayer, layerCounts, orderLayerKeys } from "./domain/layers"; -import type { ContextCode } from "./domain/types"; +import { CTX, CTXHINT } from "./domain/labels"; +import { + entriesByTarget, + entriesForLayer, + layerCounts, + orderLayerKeys, +} from "./domain/layers"; +import type { ContextCode, ControlId, Entry } from "./domain/types"; +import { InfoCard } from "./ui/InfoCard"; import { Panel } from "./ui/Panel"; import styles from "./App.module.css"; @@ -31,6 +37,7 @@ const layerLabel = (key: string): string => export function App() { const [ctx, setCtx] = useState("S"); const [layer, setLayer] = useState(""); + const [selected, setSelected] = useState(null); const [tab, setTab] = useState<(typeof TABS)[number]>("Explore"); const counts = useMemo(() => layerCounts(ENTRIES, ctx), [ctx]); @@ -47,27 +54,38 @@ export function App() { const held = activeLayer === "" ? [] : activeLayer.split("+"); + /** A control can carry several functions; pressing it again cycles them. */ + function press(id: ControlId) { + const here = lit.get(id); + if (!here || here.length === 0) { + setSelected(null); + return; + } + const at = selected ? here.indexOf(selected) : -1; + setSelected(here[(at + 1) % here.length]!); + } + + function chooseCtx() { + const order = Object.keys(CTX) as ContextCode[]; + setCtx(order[(order.indexOf(ctx) + 1) % order.length]!); + setSelected(null); + } + + function chooseLayer() { + const at = layers.indexOf(activeLayer); + setLayer(layers[(at + 1) % layers.length]!); + setSelected(null); + } + return (
- - @@ -76,15 +94,22 @@ export function App() { e.short), + right: layerLabel(activeLayer), + main: selected ? selected.short : `${onLayer.length} functions`, + chips: selected ? [] : onLayer.slice(0, 3).map((e) => e.short), }} /> -
+