Phase 2: React scaffolding, self-hosted fonts, design tokens
Vite builds to dist/, deliberately not www/. www/ is still produced by sync-www.sh for the Phase 1 APK, which stays installable throughout the port; cutover is a one-line webDir change in capacitor.config.json. base is relative because Capacitor serves the bundle from the APK, where absolute asset paths would 404. index.html omits viewport-fit=cover on purpose. That string is what makes Capacitor's SystemBars plugin pass insets through to the page; without it the plugin pads the decor view and the layout is always between the status and gesture bars. Phase 1 learned this the hard way. Fonts are self-hosted for offline use, latin subsets only: three files at 68K, against the 133K Barlow costs today. Space Grotesk is variable, so one file covers 400-700. Dropped latin-ext and vietnamese. Carets are drawn as SVG, not typed. U+25BE and U+23EE sit outside every Latin subset and would fall back to a system font mid-label. base.css carries the settled palette and the measured vertical budget as tokens, so a component cannot quietly exceed it: 8 + 44 + 8 + 525 + 8 + 84 + 8 + 50 = 735 Verified: typecheck clean, 103 tests, vite build emits with the fonts copied, and www/ is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
88
src/App.tsx
Normal file
88
src/App.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* App shell: the two pickers, a placeholder panel, and the tab bar.
|
||||
*
|
||||
* Angular: useState is roughly a component field plus change detection. Setting
|
||||
* it re-runs this function, so `entries` and `counts` below are recomputed each
|
||||
* render — cheap here, and `useMemo` is the explicit opt-out if it stops being.
|
||||
*/
|
||||
import { useMemo, useState } from "react";
|
||||
import { ENTRIES } from "./domain/entries";
|
||||
import { CTX } from "./domain/labels";
|
||||
import { controlName } from "./domain/format";
|
||||
import { layerCounts, orderLayerKeys } from "./domain/layers";
|
||||
import type { ContextCode } from "./domain/types";
|
||||
import styles from "./App.module.css";
|
||||
|
||||
const TABS = ["Explore", "Drill", "Find", "Rules"] as const;
|
||||
|
||||
/** Drawn, not typed: the caret glyphs sit outside every Latin font subset. */
|
||||
function Caret() {
|
||||
return (
|
||||
<svg width="9" height="6" viewBox="0 0 9 6" aria-hidden="true">
|
||||
<path d="M1 1l3.5 3.5L8 1" fill="none" stroke="currentColor" strokeWidth="1.6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const layerLabel = (key: string): string =>
|
||||
key === "" ? "NO MODIFIER" : key.split("+").map(controlName).join(" + ");
|
||||
|
||||
export function App() {
|
||||
const [ctx, setCtx] = useState<ContextCode>("S");
|
||||
const [layer, setLayer] = useState("");
|
||||
const [tab, setTab] = useState<(typeof TABS)[number]>("Explore");
|
||||
|
||||
const counts = useMemo(() => layerCounts(ENTRIES, ctx), [ctx]);
|
||||
const layers = useMemo(() => orderLayerKeys(counts.keys()), [counts]);
|
||||
|
||||
// Changing mode can strip the held layer out from under us.
|
||||
const activeLayer = counts.has(layer) ? layer : "";
|
||||
|
||||
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]!);
|
||||
}}
|
||||
>
|
||||
<b>{CTX[ctx].toUpperCase()}</b>
|
||||
<Caret />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={styles.picker}
|
||||
onClick={() => {
|
||||
const i = layers.indexOf(activeLayer);
|
||||
setLayer(layers[(i + 1) % layers.length]!);
|
||||
}}
|
||||
>
|
||||
<b>{layerLabel(activeLayer)}</b>
|
||||
<span className={styles.count}>{counts.get(activeLayer) ?? 0}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.panel}>
|
||||
<p className={styles.placeholder}>
|
||||
{counts.get(activeLayer) ?? 0} functions on this layer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.info} />
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user