Let the setup flow's action rows wrap, and measure headroom per word

At 390 px with labels a third longer the action row pushed "Confirm and
continue" past the viewport; both rows now wrap. The string-headroom
sweep lengthens every word by 30% rather than hanging the whole surplus
on the last one — that manufactured an unbreakable run no locale
produces — leaves screen-reader-only boxes alone, and accepts a select
value's ellipsis, which is the primitive's design app-wide.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 16:49:18 +02:00
1 parent d91a8dfebe
commit 5c5a592177
4 files changed
+28 -10

No files matched your search

+20 -5
View File
@@ -57,9 +57,11 @@ const RAW_KEY = /\b[a-z]+(?:[A-Z][a-z]+)?\.[a-z][A-Za-z0-9_.-]+\b/;
/**
* Lengthen every text node under the shell by 30%, then measure. Padding
* appends letters to the LAST word so the longest unbreakable run grows,
* which is the case that overflows; a padding that only added spaces would
* wrap for free and prove nothing.
* lengthens every WORD by 30%, so the longest unbreakable run grows in the
* same proportion as the sentence — which is what a longer translation does.
* Appending the whole surplus to one word would manufacture an unbreakable
* run no locale produces; adding only spaces would wrap for free and prove
* nothing.
*/
async function padAndMeasure(page: Page) {
return page.evaluate((shellSelector) => {
@@ -76,8 +78,11 @@ async function padAndMeasure(page: Page) {
letters >= 3 &&
node.parentElement?.closest("script,style") === null
) {
const extra = Math.ceil(letters * 0.3);
node.textContent = text.replace(/\s*$/, "") + "x".repeat(extra);
node.textContent = text.replace(/\S+/g, (word) =>
word.length >= 2
? word + "x".repeat(Math.ceil(word.length * 0.3))
: word,
);
padded += 1;
}
node = walker.nextNode();
@@ -86,6 +91,16 @@ async function padAndMeasure(page: Page) {
const doc = document.documentElement;
const clippers = [...shell.querySelectorAll<HTMLElement>("*")]
.filter((el) => {
// Screen-reader-only text is a 1 px clipped box on purpose; it is
// read, not seen, and cannot truncate.
if (el.classList.contains("sr-only") || el.clientWidth <= 1) {
return false;
}
// A select's value ellipsises by design (shadcn `SelectValue`, app
// wide): the full text is the option in the list and the control's
// accessible name, and a trigger that grew with its longest option
// would move every neighbour. Not a layout that lost headroom.
if (el.dataset.slot === "select-value") return false;
const overflowX = getComputedStyle(el).overflowX;
return overflowX === "hidden" || overflowX === "clip";
})
+2 -2
View File
@@ -226,7 +226,7 @@ export function BaselineForm({
/>
<div
className="flex items-center justify-between gap-2 pt-2"
className="flex flex-wrap items-center justify-between gap-2 pt-2"
data-slot="onboarding-step-actions"
>
{backHref ? (
@@ -238,7 +238,7 @@ export function BaselineForm({
) : (
<span aria-hidden="true" />
)}
<div className="flex items-center gap-2">
<div className="ml-auto flex flex-wrap items-center justify-end gap-2">
<Button
type="button"
variant="ghost"
+1 -1
View File
@@ -200,7 +200,7 @@ export function ConfirmScreen({ state }: { state: OnboardingStateDto }) {
onCreated={() => void finish()}
/>
<p className="text-sm">{t("onboarding.flow.confirm.managedLater")}</p>
<div className="flex items-center justify-between gap-2 pt-2">
<div className="flex flex-wrap items-center justify-between gap-2 pt-2">
{back ? (
<Button asChild variant="ghost" className="min-h-11 min-w-11">
<Link href={screenHref(back)} data-slot="onboarding-back">
+5 -2
View File
@@ -35,7 +35,10 @@ export function StepActions({
return (
<div
className="flex items-center justify-between gap-2 pt-2"
// `flex-wrap` on both rows: a locale whose labels run 30% longer must
// drop the action group to its own line at 390 px, not push it past
// the viewport (the string-headroom sweep measures exactly this).
className="flex flex-wrap items-center justify-between gap-2 pt-2"
data-slot="onboarding-step-actions"
>
{backHref ? (
@@ -47,7 +50,7 @@ export function StepActions({
) : (
<span aria-hidden="true" />
)}
<div className="flex items-center gap-2">
<div className="ml-auto flex flex-wrap items-center justify-end gap-2">
{onSkip ? (
<Button
type="button"