fix: stack the next-change line instead of letting it wrap

With the week down the right-hand side the main block is narrower, and
putting the French and English on one row set the two languages
competing for the width. In production that read as "Ouvre demain à" /
"11:00" on one side and the separator stranded on a second line with
"am" — visibly broken.

They are stacked now, French above English, which is already the pattern
in the banner, and each line is fitted to the width available rather
than allowed to wrap.

Found by rendering the real production payload rather than the sample
one: the sample had a short enough sentence to hide it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
This commit is contained in:
2026-09-21 23:10:57 +02:00
co-authored by Claude Opus 5
parent 185610ee61
commit ec826b378f
2 changed files with 55 additions and 9 deletions
+15 -3
View File
@@ -76,18 +76,30 @@ describe('screenTree', () => {
); );
}); });
it('drops the separator when only one language has something to say', () => { it('shows whichever language has something to say', () => {
const frenchOnly = JSON.stringify( const frenchOnly = JSON.stringify(
screenTree({ ...PAYLOAD, today: { ...PAYLOAD.today, next_change_en: '' } }), screenTree({ ...PAYLOAD, today: { ...PAYLOAD.today, next_change_en: '' } }),
); );
expect(frenchOnly).toContain('Ferme à 13:00'); expect(frenchOnly).toContain('Ferme à 13:00');
expect(frenchOnly).not.toContain('"·"'); expect(frenchOnly).not.toContain('Closes at');
const englishOnly = JSON.stringify( const englishOnly = JSON.stringify(
screenTree({ ...PAYLOAD, today: { ...PAYLOAD.today, next_change_fr: '' } }), screenTree({ ...PAYLOAD, today: { ...PAYLOAD.today, next_change_fr: '' } }),
); );
expect(englishOnly).toContain('Closes at 1:00 pm'); expect(englishOnly).toContain('Closes at 1:00 pm');
expect(englishOnly).not.toContain('"·"'); expect(englishOnly).not.toContain('Ferme à');
});
it('shrinks a long next-change line instead of wrapping it', () => {
// The main block is narrower now the week runs down the side; a long
// sentence used to wrap and strand half of itself on a second line.
const long = 'Ouvre lundi à 11:00 après la fermeture annuelle';
const tree = JSON.stringify(
screenTree({ ...PAYLOAD, today: { ...PAYLOAD.today, next_change_fr: long } }),
);
const sizes = [...tree.matchAll(/"fontSize":(\d+)/g)].map((match) => Number(match[1]));
// Something on the screen had to come down from the preferred 24.
expect(sizes.some((size) => size < 24 && size >= 18)).toBe(true);
}); });
it('drops the whole next-change line when there is nothing ahead', () => { it('drops the whole next-change line when there is nothing ahead', () => {
+40 -6
View File
@@ -220,21 +220,55 @@ function main(payload: ScreenPayload, width: number): ElementNode {
today.hours_text, today.hours_text,
) )
: null, : null,
nextChangeLine(payload), nextChangeLine(payload, width),
); );
} }
function nextChangeLine(payload: ScreenPayload): ElementNode | null { /**
* "Ferme à 18:30", then the English underneath.
*
* Stacked rather than side by side: the main block is narrower now that the
* week runs down the right, and a row put the two languages in competition
* for the width — the French wrapped and left the separator stranded on the
* next line with half the English. Two lines also match the banner, where
* French above English is already the pattern.
*/
function nextChangeLine(payload: ScreenPayload, width: number): ElementNode | null {
const { next_change_fr: fr, next_change_en: en } = payload.today; const { next_change_fr: fr, next_change_en: en } = payload.today;
if (!fr && !en) { if (!fr && !en) {
return null; return null;
} }
const available = width - 2 * PADDING;
return h( return h(
'div', 'div',
{ style: { display: 'flex', alignItems: 'baseline', marginTop: 12 } }, { style: { display: 'flex', flexDirection: 'column', marginTop: 12 } },
fr ? h('div', { style: { display: 'flex', fontSize: 24 } }, fr) : null, fr
fr && en ? h('div', { style: { display: 'flex', fontSize: 24, paddingLeft: 10, paddingRight: 10 } }, '·') : null, ? h(
en ? h('div', { style: { display: 'flex', fontSize: 20 } }, en) : null, 'div',
{
style: {
display: 'flex',
fontSize: fitFontSize(fr, { available, preferred: 24, minimum: 18 }),
},
},
fr,
)
: null,
en
? h(
'div',
{
style: {
display: 'flex',
fontSize: fitFontSize(en, { available, preferred: 20, minimum: 15 }),
marginTop: 2,
},
},
en,
)
: null,
); );
} }