Phase 2: Find and Rules

Both tabs were rendering Explore. They now take the whole screen, as the
reference does by hiding the unit on each: a list or a page of prose
wants the room more than the panel does. Picking a search result jumps
to Explore with that combo lit, which is the reference's data-go.

The rules text is extracted verbatim rather than retyped or reworded.
Two bugs on the way there, both worth recording:

- RULES is a template literal that itself contains template literals
  inside a ${} interpolation, so the bracket scanner used for the combo
  table stops at the first inner backtick. The nine title and body pairs
  are matched out of the source instead, with a count assertion so drift
  fails loudly rather than silently dropping a rule.
- PADMAP was read with g() but never added to the list of declarations
  evaluated into the vm, so it was simply not in scope.

The extractor now asserts 9 rules and 4 pad-map rows alongside the 159
combos.

141 tests, typecheck clean. Both tabs verified by rendering them, Find
with a seeded query since headless Firefox cannot type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 10:49:50 +03:00
parent b875342864
commit 6d1f219e8a
7 changed files with 467 additions and 1 deletions

View File

@@ -20,10 +20,13 @@ import {
orderLayerKeys,
} from "./domain/layers";
import type { ContextCode, ControlId, Entry } from "./domain/types";
import { searchEntries } from "./domain/search";
import { DrillCard } from "./ui/DrillCard";
import { DrillStrip } from "./ui/DrillStrip";
import { FindPanel } from "./ui/FindPanel";
import { InfoCard } from "./ui/InfoCard";
import { Panel } from "./ui/Panel";
import { RulesPanel } from "./ui/RulesPanel";
import { Sheet, SheetOption } from "./ui/Sheet";
import { drillReducer, initialDrillState } from "./ui/drillReducer";
import { loadState, saveState } from "./ui/persistedState";
@@ -71,6 +74,18 @@ export function App() {
const [layer, setLayer] = useState("");
const [selected, setSelected] = useState<Entry | null>(null);
// --- Find ---
const [query, setQuery] = useState("");
const results = useMemo(() => searchEntries(ENTRIES, query), [query]);
/** Jump from a search result to that combo, lit on the panel. */
function goTo(entry: Entry) {
setCtx(entry.ctx[0] ?? "S");
setLayer(entry.key);
setSelected(entry);
setTab("Explore");
}
const counts = useMemo(() => layerCounts(ENTRIES, ctx), [ctx]);
const layers = useMemo(() => orderLayerKeys(counts.keys()), [counts]);
const activeLayer = counts.has(layer) ? layer : "";
@@ -121,6 +136,33 @@ export function App() {
}, [drill.card, drill.answered]);
const isDrill = tab === "Drill";
// Find and Rules take the whole screen: the reference hides the unit on both,
// and a list or a page of prose wants the room more than the panel does.
const isPanelTab = tab === "Explore" || isDrill;
if (!isPanelTab) {
return (
<div className={styles.app}>
{tab === "Find" ? (
<FindPanel query={query} results={results} onQuery={setQuery} onPick={goTo} />
) : (
<RulesPanel />
)}
<nav className={styles.nav}>
{TABS.map((name) => (
<button
key={name}
className={name === tab ? styles.tabOn : styles.tab}
onClick={() => setTab(name)}
>
{name.toUpperCase()}
</button>
))}
</nav>
</div>
);
}
return (
<div className={styles.app}>