French dragged her whole page sideways

The bug reads as a language-picker bug — switch off Mandarin on a phone and
the writing starts scrolling left and right — but the picker only changes
how wide Petal's own chrome wants to be. Every pill says its thing twice,
历史 · History against Historique · History, and every pill is nowrap and
shrink-0. So the title row's width is a property of the langpack: 459px in
Chinese, 551px in French, 506px in Portuguese, against the 338px column a
390px phone gives it. That row lives in the editor pane, and a pane that is
overflow-y: auto has an overflow-x of auto too, whatever the stylesheet
says. The overflow had nowhere to go but the page of writing.

Chinese was already 120px over. French is simply where it stopped being
possible to ignore.

The pills now live in a strip that scrolls itself, with overscroll-behavior
so a swipe off the end doesn't turn into a page gesture. What that buys is
the thing worth keeping: every label stays bilingual at every width. The
first version of this fix dropped the English half on phones, which fixed
the geometry by taking away the half she is learning from — on the device
she writes on most.

A scrolled pill that has left the screen is indistinguishable from a pill
that isn't there, so each edge with more behind it fades, the same hint
.petal-toolbar gives with its clipped right edge, except this row can be
scrolled from either end and has to point the right way. ChromeStrip sets
data-edge from the scroll position and re-measures when the pills resize —
which is also what catches every label changing width at once when she
switches her pair.

The tone and export menus had to leave with them. A scroll container clips
its absolutely-positioned children, so both menus would have been trapped in
a 36px-tall box; they anchor against the viewport now.

Two smaller ones the same measurement turned up:

  - The header overflowed the viewport itself below 360px — a real
    page-level scroll, 40px of it at 320px, off 🌷 Jardim de palavras ·
    Garden. Narrower padding on phones, and under 360px the wordmark
    yields. Of everything in that row it is the one thing that can go: she
    is already inside the app, and the blossom stays.
  - The language picker wanted 307px inside a 280px drawer and spilled out
    of it. It wraps now. That one was broken in Chinese too, at 291px.

Verified in a real browser rather than by arithmetic: 310px to 780px in 10px
steps, in all three packs, no page overflow and no editor-pane overflow at
any width. Edge fades flip correctly, the export menu opens unclipped with
the strip scrolled to its end, tsc clean, 195 frontend tests pass. Desktop
is untouched — one 40px header row, as before.

Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
This commit is contained in:
prosolis
2026-07-27 20:10:17 -07:00
parent 3e714b6f00
commit ac1c6cddb0
7 changed files with 224 additions and 32 deletions
+39 -25
View File
@@ -7,6 +7,7 @@ import { useTags } from './hooks/useTags'
import { DocList } from './components/DocList/DocList'
import { EditorCore, type EditorChange } from './components/Editor/EditorCore'
import { ToneSelect } from './components/Editor/ToneSelect'
import { ChromeStrip } from './components/Editor/ChromeStrip'
import { ExportMenu } from './components/Export/ExportMenu'
import { HistoryPanel } from './components/History/HistoryPanel'
import { GardenPanel } from './components/Garden/GardenPanel'
@@ -435,7 +436,7 @@ export default function App() {
<PetalFall night={night} />
<header
onMouseDown={handleChromeDown}
className="petal-no-print flex h-12 shrink-0 items-center gap-2 px-5"
className="petal-no-print flex h-12 shrink-0 items-center gap-2 px-3 md:px-5"
style={{ borderBottom: '1px solid var(--color-border)' }}
>
<button
@@ -451,7 +452,15 @@ export default function App() {
</button>
<span className="text-xl">🌸</span>
<span className="text-lg font-extrabold text-plum">Petal</span>
{/* The wordmark is the first thing to go on a small phone. The header
holds a hamburger, a name and the garden button, and the garden
button's label is a langpack string: 词汇花园 · Garden is 130px
where Jardim de palavras · Garden is nearly 200, which is the
difference between fitting a 320px screen and scrolling the whole
app sideways. Of everything in this row, the one that can be spared
is the app's own name — she is already inside the app, and the
blossom stays. */}
<span className="petal-wordmark text-lg font-extrabold text-plum">Petal</span>
<button
type="button"
onClick={() => setGardenOpen(true)}
@@ -503,38 +512,43 @@ export default function App() {
className="flex flex-1 flex-col overflow-y-auto px-6 py-8"
>
<div ref={canvasRef} className="mx-auto flex w-full max-w-[720px] flex-1 flex-col">
<div className="mb-5 flex items-center gap-3">
{/* Title, then the three chrome pills. Their labels are
bilingual and don't shrink, so how much width this row
wants is a property of the langpack — 历史 is two glyphs
where Historique is ten — and on a phone no language's
version of it fits. The pills therefore live in a strip
that scrolls itself; the title takes its own line below
the drawer breakpoint so it keeps its full width. */}
<div className="mb-5 flex flex-wrap items-center gap-2 md:gap-3">
<input
value={title}
onChange={(e) => handleTitleChange(e.target.value)}
placeholder="Untitled"
aria-label="Document title"
className="min-w-0 flex-1 bg-transparent text-3xl font-extrabold text-plum focus:outline-none"
className="min-w-0 flex-1 basis-full bg-transparent text-3xl font-extrabold text-plum focus:outline-none md:basis-0"
style={{ fontFamily: 'var(--font-ui)' }}
/>
<div className="petal-no-print shrink-0">
<ChromeStrip className="petal-no-print flex items-center gap-2 py-0.5 md:gap-3">
<ToneSelect value={tone} onChange={handleToneChange} />
</div>
<button
type="button"
onClick={() => setHistoryOpen(true)}
aria-label="Version history"
title="Browse and restore earlier versions"
className="petal-no-print inline-flex h-9 shrink-0 items-center gap-1.5 whitespace-nowrap px-3 text-sm font-bold"
style={{
borderRadius: 'var(--radius-pill)',
background: 'var(--color-surface)',
color: 'var(--color-plum)',
boxShadow: 'var(--shadow-soft)',
}}
>
<span aria-hidden>🕘</span>
<span>{t.app.history}</span>
<span style={{ color: 'var(--color-muted)' }}>· History</span>
</button>
<div className="petal-no-print">
<button
type="button"
onClick={() => setHistoryOpen(true)}
aria-label="Version history"
title="Browse and restore earlier versions"
className="inline-flex h-9 shrink-0 items-center gap-1.5 whitespace-nowrap px-3 text-sm font-bold"
style={{
borderRadius: 'var(--radius-pill)',
background: 'var(--color-surface)',
color: 'var(--color-plum)',
boxShadow: 'var(--shadow-soft)',
}}
>
<span aria-hidden>🕘</span>
<span>{t.app.history}</span>
<span style={{ color: 'var(--color-muted)' }}>· History</span>
</button>
<ExportMenu docId={currentDoc.id} />
</div>
</ChromeStrip>
</div>
<EditorCore
key={`${currentDoc.id}:${editorEpoch}`}