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 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 01:22:45 +03:00
parent 4961500fa7
commit c7c827ecf8
6 changed files with 228 additions and 29 deletions

View File

@@ -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<ContextCode>("S");
const [layer, setLayer] = useState("");
const [selected, setSelected] = useState<Entry | null>(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 (
<div className={styles.app}>
<div className={styles.strip}>
<button
className={styles.picker}
onClick={() => {
const order = Object.keys(CTX) as ContextCode[];
setCtx(order[(order.indexOf(ctx) + 1) % order.length]!);
}}
>
<button className={styles.picker} onClick={chooseCtx}>
<b>{CTX[ctx].toUpperCase()}</b>
<Caret />
</button>
<button
className={styles.picker}
onClick={() => {
const i = layers.indexOf(activeLayer);
setLayer(layers[(i + 1) % layers.length]!);
}}
>
<button className={styles.picker} onClick={chooseLayer}>
<b>{layerLabel(activeLayer)}</b>
<span className={styles.count}>{onLayer.length}</span>
</button>
@@ -76,15 +94,22 @@ export function App() {
<Panel
lit={lit}
held={held}
selected={selected?.tset[0] ?? null}
onPress={press}
screen={{
top: CTX[ctx].toUpperCase(),
right: activeLayer === "" ? "NO MODIFIER" : layerLabel(activeLayer),
main: `${onLayer.length} function${onLayer.length === 1 ? "" : "s"}`,
chips: onLayer.slice(0, 3).map((e) => e.short),
right: layerLabel(activeLayer),
main: selected ? selected.short : `${onLayer.length} functions`,
chips: selected ? [] : onLayer.slice(0, 3).map((e) => e.short),
}}
/>
<div className={styles.info} />
<InfoCard
entry={selected}
ctx={ctx}
hint={CTXHINT[ctx]}
count={onLayer.length}
/>
<nav className={styles.nav}>
{TABS.map((name) => (