From ec826b378f76ba5f0d17b69c1c5ec28e19170170 Mon Sep 17 00:00:00 2001 From: vl Date: Mon, 21 Sep 2026 23:10:57 +0200 Subject: [PATCH] fix: stack the next-change line instead of letting it wrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd --- lib/screen/render.test.ts | 18 ++++++++++++--- lib/screen/render.ts | 46 ++++++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/lib/screen/render.test.ts b/lib/screen/render.test.ts index d806c98..c4163d2 100644 --- a/lib/screen/render.test.ts +++ b/lib/screen/render.test.ts @@ -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( screenTree({ ...PAYLOAD, today: { ...PAYLOAD.today, next_change_en: '' } }), ); expect(frenchOnly).toContain('Ferme à 13:00'); - expect(frenchOnly).not.toContain('"·"'); + expect(frenchOnly).not.toContain('Closes at'); const englishOnly = JSON.stringify( screenTree({ ...PAYLOAD, today: { ...PAYLOAD.today, next_change_fr: '' } }), ); 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', () => { diff --git a/lib/screen/render.ts b/lib/screen/render.ts index 2732913..d5fa794 100644 --- a/lib/screen/render.ts +++ b/lib/screen/render.ts @@ -220,21 +220,55 @@ function main(payload: ScreenPayload, width: number): ElementNode { today.hours_text, ) : 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; if (!fr && !en) { return null; } + + const available = width - 2 * PADDING; + return h( 'div', - { style: { display: 'flex', alignItems: 'baseline', marginTop: 12 } }, - fr ? h('div', { style: { display: 'flex', fontSize: 24 } }, fr) : null, - fr && en ? h('div', { style: { display: 'flex', fontSize: 24, paddingLeft: 10, paddingRight: 10 } }, '·') : null, - en ? h('div', { style: { display: 'flex', fontSize: 20 } }, en) : null, + { style: { display: 'flex', flexDirection: 'column', marginTop: 12 } }, + fr + ? h( + '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, ); }