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
+57
View File
@@ -571,6 +571,63 @@ button, a, input {
}
}
/* Small phones only: see the header in App.tsx for why the wordmark yields.
The threshold is low on purpose — with the header's narrow-screen padding the
longest garden label Petal ships still fits a 360px phone with its name
beside it, so only the smallest screens ever pay for the room. */
@media (max-width: 360px) {
.petal-wordmark {
display: none;
}
}
/* The document's chrome pills (tone, history, export) as a strip that scrolls
within itself. Their labels say everything twice — 历史 · History, Historique
· History — so how wide the row wants to be is a property of the langpack:
~460px in Chinese, ~550px in French, against the ~340px a phone gives it.
Something has to give, and the choice here is *not* the second half of the
label: the English is what she is learning, and a pill that quietly stops
teaching on the device she writes on most is the wrong trade.
So the row keeps every word and scrolls sideways — but only itself. Before
this, the overflow escaped to the editor pane and the whole page of writing
dragged sideways with it, which is the bug this fixes. overscroll-behavior
keeps a swipe that runs out of pills from turning into a page-level gesture,
and the scrollbar is hidden because a half-visible pill at the edge is the
affordance, the same hint .petal-toolbar gives with its faded edge. */
.petal-chrome-strip {
max-width: 100%;
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
-ms-overflow-style: none;
}
.petal-chrome-strip::-webkit-scrollbar {
display: none;
}
/* Which edge has more behind it. ChromeStrip sets data-edge from the scroll
position; a strip whose pills all fit is left alone entirely, so the fade
only ever appears when it means something. The mask has to clear the pills'
soft shadow, hence the fade starting a little inside the edge. */
.petal-chrome-strip[data-edge='right'] {
-webkit-mask-image: linear-gradient(to right, #000 88%, transparent 100%);
mask-image: linear-gradient(to right, #000 88%, transparent 100%);
}
.petal-chrome-strip[data-edge='left'] {
-webkit-mask-image: linear-gradient(to left, #000 88%, transparent 100%);
mask-image: linear-gradient(to left, #000 88%, transparent 100%);
}
.petal-chrome-strip[data-edge='both'] {
-webkit-mask-image: linear-gradient(
to right,
transparent 0%,
#000 12%,
#000 88%,
transparent 100%
);
mask-image: linear-gradient(to right, transparent 0%, #000 12%, #000 88%, transparent 100%);
}
/* Print / Save-as-PDF: strip every bit of app chrome and editing decoration so
only the title and the writing itself reach the page. This is Petal's PDF
path — it uses the browser's own fonts, so CJK renders correctly with no