From 6ddf6f9a7759a012e4604f947f2617c1ec104107 Mon Sep 17 00:00:00 2001 From: ncode Date: Thu, 17 Sep 2026 21:10:41 +0300 Subject: [PATCH] Show the gesture, and offer the full text when the card clips Two faults behind one report. The card clipped long descriptions, and it never rendered the how field at all - so for SHIFT + REMAIN, where the instruction IS a three-second hold, the most important part was missing rather than merely shortened. Measured rather than assumed: that entry is Live mode, how is "Hold SHIFT, hold REMAIN 3 s", desc is 112 characters, and the pair renders 43px tall into a 29px two-line clamp. A full line was being lost. The gesture now leads the card text in the accent colour, and a MORE button opens a sheet with the combo, the gesture, the full description, the modes it applies in and the manual page. Whether to offer MORE is decided by measuring scrollHeight against clientHeight, not by guessing a character count, so it appears exactly when text is really cut. 39 of 159 entries qualify: 24 carry a gesture, 17 have a description over 110 characters. The card stays 84px because the panel needs that space, so a sheet is the right place for the overflow, as in the reference. 141 tests, typecheck clean, installed. Co-Authored-By: Claude Opus 5 --- src/App.module.css | 26 +++++++++++++++++++++++ src/App.tsx | 42 +++++++++++++++++++++++++++++++++++--- src/ui/InfoCard.module.css | 24 ++++++++++++++++++++++ src/ui/InfoCard.tsx | 35 ++++++++++++++++++++++++++----- 4 files changed, 119 insertions(+), 8 deletions(-) diff --git a/src/App.module.css b/src/App.module.css index d68b444..0e6a231 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -112,3 +112,29 @@ .sheetLabel:first-child { margin-top: 0; } + +/* The detail sheet: what the 84px card cannot hold. */ +.detailCombo { + margin: 0; + font-family: var(--mono); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + color: var(--accent); +} + +/* The gesture is the instruction for the 24 entries that have one. */ +.detailHow { + margin: 0; + font-size: 13px; + font-weight: 700; + line-height: 1.3; +} + +.detailBody { + margin: 0; + font-size: 13px; + line-height: 1.4; + color: var(--muted); + text-wrap: pretty; +} diff --git a/src/App.tsx b/src/App.tsx index ce76cb0..f56a5bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "r import { DECKS, boxDistribution, deckPool, knownCount, pickCard } from "./domain/drill"; import type { DeckId, Direction, InputMode } from "./domain/drill"; import { ENTRIES, layerKeyOf } from "./domain/entries"; -import { controlName } from "./domain/format"; +import { comboText, contextNames, controlName } from "./domain/format"; import { CTX, CTXHINT, NOLATCH } from "./domain/labels"; import { entriesByTarget, @@ -35,7 +35,7 @@ import styles from "./App.module.css"; const TABS = ["Explore", "Drill", "Find", "Rules"] as const; type Tab = (typeof TABS)[number]; -type OpenSheet = null | "mode" | "layer" | "settings"; +type OpenSheet = null | "mode" | "layer" | "settings" | "detail"; /** Longer than this and the press was a hold to peek, not a tap to lock. */ const HOLD_MS = 350; @@ -322,7 +322,13 @@ export function App() { onPickOption={(name) => dispatch({ type: "pickOption", name })} /> ) : ( - + setSheet("detail")} + /> )} + setSheet(null)} + > + {selected ? ( + <> +

Combo

+

{comboText(selected)}

+ + {selected.how ? ( + <> +

Gesture

+

{selected.how}

+ + ) : null} + +

What it does

+

+ {selected.desc || "No description in the reference."} +

+ +

Where

+

+ {contextNames(selected.ctx).join(", ")} · manual p.{selected.page} +

+ + ) : null} +
+ setSheet(null)}> {(Object.keys(CTX) as ContextCode[]).map((code) => ( void; } -export function InfoCard({ entry, hint, count }: InfoCardProps) { +export function InfoCard({ entry, hint, count, onMore }: InfoCardProps) { + const body = useRef(null); + const [clipped, setClipped] = useState(false); + + // Measured rather than guessed from a character count: the card is a fixed + // 84px, so whether the text fits depends on how it wraps. + useEffect(() => { + const el = body.current; + setClipped(el !== null && el.scrollHeight > el.clientHeight + 1); + }, [entry]); + if (!entry) { return (
@@ -27,6 +40,10 @@ export function InfoCard({ entry, hint, count }: InfoCardProps) { ); } + // The gesture matters as much as the description — SHIFT + REMAIN is a + // three-second hold — so a card carrying one always offers the full text. + const showMore = clipped || entry.how !== undefined; + return (
@@ -38,11 +55,19 @@ export function InfoCard({ entry, hint, count }: InfoCardProps) { ))} - P.{entry.page} + + {showMore ? ( + + ) : null} + P.{entry.page} +

{entry.name}

-

- {entry.desc || contextNames(entry.ctx).join(", ")} +

+ {entry.how ? {entry.how}. : null} + {entry.desc}

);