/** * Renders the panel locally, so the layout can be iterated on without a device. * * npm run screen:preview * * Writes `.preview/screen.svg`, `.preview/screen.bmp`, `.preview/screen.png` * and an `index.html` that shows the real 1-bit image at 1:1 next to a * magnified view, which is where clipping and thin strokes give themselves up. */ import { mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import type { ScreenPayload } from '../lib/screen/contract'; import { encodeScreen } from '../lib/screen/encode'; import { SCREEN_HEIGHT, SCREEN_WIDTH } from '../lib/screen/layout'; import { renderScreenSvg } from '../lib/screen/render'; import { buildScreenPayload } from '../lib/screen/viewmodel'; import type { ScheduleContext } from '../lib/schedule/types'; const ZURICH = 'Europe/Zurich'; /** A deliberately busy week: a lunch break, a late opening and a holiday. */ const CONTEXT: ScheduleContext = { timezone: ZURICH, weekly: [ { dayOfWeek: 0, isClosed: true, slots: [] }, { dayOfWeek: 1, isClosed: true, slots: [] }, { dayOfWeek: 2, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] }, { dayOfWeek: 3, isClosed: false, slots: [ { open: '10:00', close: '13:00' }, { open: '14:00', close: '18:30' }, ], }, { dayOfWeek: 4, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] }, { dayOfWeek: 5, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] }, { dayOfWeek: 6, isClosed: false, slots: [{ open: '10:00', close: '18:30' }] }, ], exceptions: [ { date: '2026-09-24', isClosed: false, slots: [{ open: '14:00', close: '18:30' }], reason: 'TEMPORARY', noteFr: 'Ouverture retardée', noteEn: 'Late opening', source: 'MANUAL', }, ], vacations: [], holidays: [ { date: '2026-09-25', nameFr: 'Jeûne genevois', nameEn: 'Geneva Fast', isAutoClosed: true }, ], }; function payload(): ScreenPayload { return buildScreenPayload({ now: new Date('2026-09-23T10:30:00Z'), ctx: CONTEXT, shopName: 'ITA ITO', logoUrl: 'https://horaires.ita-ito.com/brand/logo-eink.png', messages: [ { textFr: 'Fermeture exceptionnelle jeudi matin', textEn: 'Exceptionally closed on Thursday morning', startsAt: null, endsAt: null, priority: 0, isActive: true, }, ], }); } async function main(): Promise { const directory = path.join(process.cwd(), '.preview'); await mkdir(directory, { recursive: true }); const screen = payload(); const svg = await renderScreenSvg(screen); await writeFile(path.join(directory, 'screen.svg'), svg); await writeFile(path.join(directory, 'screen.bmp'), encodeScreen(svg, 'bmp')); await writeFile(path.join(directory, 'screen.png'), encodeScreen(svg, 'png')); await writeFile(path.join(directory, 'payload.json'), `${JSON.stringify(screen, null, 2)}\n`); await writeFile(path.join(directory, 'index.html'), page()); console.log(`Preview written to ${path.relative(process.cwd(), directory)}/index.html`); console.log('Open it in a browser, or look at screen.png directly.'); } function page(): string { return ` Aperçu écran — ITA ITO

Aperçu de l'écran — ${SCREEN_WIDTH} × ${SCREEN_HEIGHT}, 1 bit

Image réelle envoyée à l'appareil, après seuillage. Aucun gris n'est possible ici.

Taille réelle

Aperçu à l'échelle 1:1

Agrandi ×2 — pour repérer les coupures et les traits trop fins

Aperçu agrandi
`; } main().catch((error: unknown) => { console.error(error); process.exitCode = 1; });