From cae509e52b493e30689b2e4c9711ae2aabe3ebeb Mon Sep 17 00:00:00 2001 From: Dmitriy Vasilev Date: Sun, 6 Sep 2026 20:12:59 +0700 Subject: [PATCH] feat(website): the Spec Explorer works on a phone At 375px the desktop layout was kept intact: a 300px library beside the detail pane. That left roughly 150px for the detail, which wrapped every word of the description onto its own line -- "hello_worl / -- start / here / The / smallest / spec" -- and the row still overflowed, so the page scrolled sideways and the search box was clipped off the top. Below 760px it is now master-detail, the way a phone expects: the library is the landing view at full width, tapping a spec opens it, and a back control returns. Above 760px nothing changes. The threshold is chosen against the layout rather than a device: 760px is where the library at its preferred 300px stops leaving the detail pane enough room for a readable line. Tablets in landscape keep both panes. Three details that are not incidental: - The pane switch lives in the list item's onClick, not inside pick(). The explorer also picks a spec on load, and routing that through pick() dropped a first-time visitor straight into a file they had not chosen. A shared link still opens its spec, because that one was asked for. - The back control is 44px tall, the smallest reliably tappable target. - The new flag is `phone`, not `narrow`. A `narrow` already exists in this file meaning "< 1100px" and only trims chrome; reusing it would have tied a layout change to a threshold chosen for something else. Verified at 375x812 against the built artifact in a real browser, not by reasoning about CSS: horizontal overflow measured at 0px on both panes (scrollWidth === clientWidth === 375), the list lands first, a tap opens the detail with the back control, back returns to the library with the selection still highlighted. Re-checked at 1280x800: two panes, no back control, Chip tab present, unchanged. EN and RU copy for the new control. Co-Authored-By: Claude Opus 5 --- apps/website/src/pages/SpecExplorer.tsx | 89 ++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/apps/website/src/pages/SpecExplorer.tsx b/apps/website/src/pages/SpecExplorer.tsx index de324264f4..859a385bc4 100644 --- a/apps/website/src/pages/SpecExplorer.tsx +++ b/apps/website/src/pages/SpecExplorer.tsx @@ -54,6 +54,7 @@ const UI = { loading: 'Loading compiler…', compiling: 'Analysing…', back: '← Home', + backToLibrary: 'All specs', chipTitle: 'ON THE CHIP', chipDerived: 'Derived from what this spec declares: the bit width of every constant, the field layout of every packed struct, and the parameter and return widths of every function. Those are hardware facts the language requires you to state.', @@ -149,6 +150,7 @@ const UI = { loading: 'Загрузка компилятора…', compiling: 'Анализ…', back: '← На главную', + backToLibrary: 'Все спеки', chipTitle: 'НА КРИСТАЛЛЕ', chipDerived: 'Построено из того, что объявляет спека: разрядность каждой константы, раскладка полей каждой packed-структуры, ширины параметров и возврата каждой функции. Это аппаратные факты, которые язык требует указать явно.', @@ -402,6 +404,37 @@ export default function SpecExplorer() { const [tagSel, setTagSel] = useState([]) const [tagsOpen, setTagsOpen] = useState(false) const [selected, setSelected] = useState(null) + + /** + * Below this width the two panes cannot both be useful. + * + * The desktop layout is a 300px library beside the detail pane. On a 375px + * phone that left 150-odd pixels for the detail, which wrapped every word of + * the description onto its own line -- and the row still overflowed, so the + * page scrolled sideways and the search box was clipped off the top. + * + * 760px is chosen against the layout, not a device: it is where the library + * at its 300px preferred width stops leaving the detail pane enough room for + * a readable line. Tablets in landscape stay on the two-pane layout. + */ + // Distinct from the existing `narrow` (< 1100px), which only trims chrome. + // This one changes the layout's shape, so it gets its own name and threshold. + const [phone, setPhone] = useState( + () => typeof window !== 'undefined' && window.matchMedia('(max-width: 760px)').matches, + ) + useEffect(() => { + const mq = window.matchMedia('(max-width: 760px)') + const onChange = (e: MediaQueryListEvent) => setPhone(e.matches) + mq.addEventListener('change', onChange) + return () => mq.removeEventListener('change', onChange) + }, []) + + /** + * Which pane a narrow screen is showing. Master-detail, the way a phone + * expects: the library is the landing view, tapping a spec opens it, and a + * back control returns. Ignored entirely when the two-pane layout fits. + */ + const [mobilePane, setMobilePane] = useState<'list' | 'detail'>('list') const [source, setSource] = useState('') const [result, setResult] = useState(null) const [busy, setBusy] = useState(false) @@ -446,6 +479,10 @@ export default function SpecExplorer() { // filter that excludes the very spec the link named would show an // empty list next to an open file. if (!target.tutorial) setHealthFilter('all') + // A shared link names a spec on purpose, so on a phone it opens that + // spec. The featured-spec fallback does not: nobody asked for it, and + // the library is the honest landing view. + if (wanted && target.path === wanted) setMobilePane('detail') void pickRef.current?.(target) } }) @@ -790,16 +827,17 @@ export default function SpecExplorer() { } -
+
{/* library */} -
- + } {/* main */} -
+ {(!phone || mobilePane === 'detail') &&
+ {/* The way back on a phone. Nothing else returns to the library: + the aside is not on screen, and the browser's back button + belongs to the route, not to this pane. */} + {phone && ( + + )} {err && (
{err}
)} @@ -1610,7 +1679,7 @@ export default function SpecExplorer() {
)} - + } )