From 20412e900120f3966f2313c520ffa6c2f7bb05f1 Mon Sep 17 00:00:00 2001 From: Andrew Poncher Date: Thu, 16 Jul 2026 19:14:37 -0400 Subject: [PATCH] =?UTF-8?q?Mobile:=20swipe=20navigation=20+=20scroll-inste?= =?UTF-8?q?ad-of-clip=20slides=20(=E2=89=A4640px)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On phones, a slide whose content runs taller than the viewport was clipped at both ends (flex centering + overflow hidden), and the only way to advance was the small dock arrows. - Deck.tsx: quick horizontal swipes page the deck (build reveals included). Vertical drags are left free for slide scrolling; slow or diagonal drags don't page; touches on the dock, rail, grid, presenter, annotator, and form controls are ignored. - base.css (≤640px): slides become scroll containers with `justify-content: safe center` — short slides stay centered, tall ones scroll, with bottom padding so the last content clears the dock. The atmosphere layer is pinned static (its -15% bleed + drift scale created ~140px of phantom scroll on every slide once slides could scroll). Split stacks by content height instead of crushing media into the leftover row. StatGrid compacts to 2-up cards so six stats fit one phone screen. Dock and annotator bar respect safe-area insets. - index.html: viewport-fit=cover for notched phones. - SKILL.md: document the phone behavior per CONTRIBUTING. npx tsc --noEmit and npm run build both pass; desktop (>640px) rendering is unchanged. Co-Authored-By: Claude Fable 5 --- .bolt/skills/slides/SKILL.md | 4 ++ index.html | 2 +- src/deck/Deck.tsx | 43 ++++++++++++++++++++ src/styles/base.css | 78 ++++++++++++++++++++++++++++++++---- 4 files changed, 119 insertions(+), 8 deletions(-) diff --git a/.bolt/skills/slides/SKILL.md b/.bolt/skills/slides/SKILL.md index 99b6e5b..d98189d 100644 --- a/.bolt/skills/slides/SKILL.md +++ b/.bolt/skills/slides/SKILL.md @@ -216,6 +216,10 @@ to the screen, so nothing scales-and-clips: with **`.appmock`** (sidebar + content; sidebar column collapses on phones) and put **`.hide-narrow`** on chrome that should vanish on small screens. - **One idea per slide**, sized to fill ~one screen with deliberate negative space. +- **Phones (≤640px)**: a slide taller than the screen scrolls vertically instead of + clipping, and swiping left/right pages the deck (`StatGrid` also compacts to + 2-up cards there — built in). Still aim to fit one screen; scroll is the escape + hatch, not the layout plan. - **Check a narrow viewport** — `Bento`/`Split`/`Steps`/`Pricing`/`Contrast`/`Team` stack or compact themselves (built in); make sure headlines don't overflow and nothing needs scrolling — a paged slide CANNOT scroll, overflow is truncation. diff --git a/index.html b/index.html index fb4074a..0ad76f1 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Replace — your deck title @@ -263,6 +264,48 @@ export default function Deck({ children }: { children: ReactNode }) { return () => window.removeEventListener('keydown', onKey); }, [next, prev, go, total, toggleRail, toggleGrid, toggleFs, openPresenter]); + // touch: a quick horizontal swipe pages; vertical drags stay free so a + // slide taller than a phone screen can scroll (see base.css ≤640px) + useEffect(() => { + let x0 = 0, + y0 = 0, + t0 = 0, + live = false; + const onStart = (e: TouchEvent) => { + const t = e.target as HTMLElement | null; + if ( + e.touches.length !== 1 || + t?.closest( + '.noir-dock,.noir-rail,.noir-grid,.noir-presenter,.ann-bar,.ann-canvas,button,a,textarea,input' + ) + ) { + live = false; + return; + } + live = true; + x0 = e.touches[0].clientX; + y0 = e.touches[0].clientY; + t0 = performance.now(); + }; + const onEnd = (e: TouchEvent) => { + if (!live) return; + live = false; + const dx = e.changedTouches[0].clientX - x0; + const dy = e.changedTouches[0].clientY - y0; + // slow drags and diagonal/vertical moves are scroll intent, not paging + if (performance.now() - t0 > 600) return; + if (Math.abs(dx) < 48 || Math.abs(dx) < 1.5 * Math.abs(dy)) return; + if (dx < 0) next(); + else prev(); + }; + window.addEventListener('touchstart', onStart, { passive: true }); + window.addEventListener('touchend', onEnd, { passive: true }); + return () => { + window.removeEventListener('touchstart', onStart); + window.removeEventListener('touchend', onEnd); + }; + }, [next, prev]); + // safety net: if the authored deck kept the placeholder tab title, derive // one from the current slide's heading so shared links look right. useEffect(() => { diff --git a/src/styles/base.css b/src/styles/base.css index 7d4c1f1..59976ba 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1827,6 +1827,61 @@ strong { grid-template-columns: 180px 1fr; } @media (max-width: 640px) { + /* a slide taller than the screen scrolls instead of clipping both ends + (flex centering can't overflow-scroll upward) — `safe center` keeps short + slides centered and degrades to top-aligned where unsupported; the extra + bottom padding only bites on slides that scroll, clearing the dock */ + .slide { + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + justify-content: flex-start; + justify-content: safe center; + padding-bottom: calc(var(--gutter-y) + 92px); + } + /* the -15% atmosphere bleed (and drift's scale) would give every slide + ~140px of phantom scroll once slides are scroll containers — pin the + gradient static; the 26s drift isn't perceptible at phone size */ + .slide::before { + inset: 0; + animation: none; + transform: none; + } + /* stacked split: media keeps its natural height instead of being crushed + into the leftover row, and the slide scrolls when the stack runs long */ + .slide.full { + padding-bottom: 0; + } + .split { + height: auto; + min-height: 100%; + grid-template-rows: auto minmax(min-content, 1fr); + padding-bottom: 92px; + } + /* stat grid: 2-up compact cards so six stats fit one phone screen — + full-width stacking runs ~1.5 screens tall */ + .stat-grid { + grid-template-columns: repeat(2, 1fr); + gap: 10px; + } + .stat-card { + padding: 14px 12px; + gap: 4px; + } + .stat-card .tick { + margin-bottom: 2px; + } + .stat-value { + font-size: clamp(24px, 7vw, 34px); + } + .stat-label { + font-size: 13.5px; + margin-top: 4px; + } + .stat-caption { + font-size: 12px; + line-height: 1.35; + } .appmock { grid-template-columns: 1fr; } @@ -1841,17 +1896,26 @@ strong { .ch-donut { --ch-size: 120px !important; } - /* data tables keep their column integrity and pan horizontally instead */ + /* data tables FIT the phone width — sideways panning buries the far + columns. Type/pads shrink, headers and label cells wrap; number strings + carry no break points so they stay whole. overflow-x remains only as the + escape hatch for genuinely too-wide tables. */ .dtable { overflow-x: auto; } .dtable table { - min-width: 540px; + min-width: 0; + font-size: 11px; + } + .dtable th { + font-size: 9px; + letter-spacing: 0.05em; + padding: 7px 6px; + white-space: normal; } - .dtable th, .dtable td { - white-space: nowrap; - padding: 9px 12px; + padding: 8px 6px; + white-space: normal; } } @@ -2716,7 +2780,7 @@ strong { @media (max-width: 640px) { .noir-dock { opacity: 1; - bottom: 14px; + bottom: calc(14px + env(safe-area-inset-bottom, 0px)); display: flex; flex-direction: column; align-items: center; @@ -2768,7 +2832,7 @@ strong { /* annotation toolbar: one swipeable row on phones (wrapping made the 999px-radius pill collapse into a pinched oval); clears the taller dock */ .ann-bar { - bottom: 104px; + bottom: calc(104px + env(safe-area-inset-bottom, 0px)); flex-wrap: nowrap; justify-content: flex-start; overflow-x: auto;