-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
463 lines (443 loc) · 28.2 KB
/
Copy pathindex.html
File metadata and controls
463 lines (443 loc) · 28.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
<meta name="theme-color" content="#4F46BA" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="icon" type="image/png" href="icons/icon-192.png" />
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png" />
<link rel="manifest" href="manifest.webmanifest" />
<!-- Fontes self-hostées (cf. scripts/vendor-fonts.mjs). Précachées par
le SW, donc 0 réseau dès la 2e visite et identité visuelle conservée
offline. Préchargement des 2 fontes critiques (Nunito normal pour le
body, Fraunces normal pour le H1 LCP) pour aplatir le waterfall
CSS → woff2 sur la 1re visite. Les data-* sont lus par
vendor-fonts.mjs pour réécrire les hrefs si Google republie. -->
<link rel="preload" as="font" type="font/woff2" crossorigin
data-family="Nunito" data-style="normal"
href="/fonts/nunito-normal-aBTMnFcQ.woff2" />
<link rel="preload" as="font" type="font/woff2" crossorigin
data-family="Fraunces" data-style="normal"
href="/fonts/fraunces-normal-TeP2Xz5c.woff2" />
<link rel="stylesheet" href="/fonts/fonts.css" />
<!-- Anti-FOUC : couleur de fond + fonte fallback appliquées dès le premier
paint, avant que la feuille de styles principale ne soit injectée. -->
<style>
html, body { background: #FBF6EC; color: #1E1A2E; }
body { font-family: 'Nunito', 'SF Pro Text', system-ui, sans-serif; margin: 0; }
/* Sur installation PWA / profil existant / "essayer dans le navigateur"
déjà cliqué : l'app va monter directement sur Home/Welcome, pas la
landing. On masque la landing statique pour éviter qu'elle flash. */
html.skip-static-landing #static-landing { display: none; }
/* Texte bilingue de la landing : chaque chaîne existe en deux éléments
(data-lang fr/en). On affiche celui de la langue de <html lang>, fixée
avant le 1er paint par le script inline du <head> — donc pas de flash.
`revert` rend à l'élément son display naturel (span inline, li
list-item), pour pouvoir taguer aussi bien des spans que des <li>. */
[data-lang="en"] { display: none; }
html[lang="en"] [data-lang="fr"] { display: none; }
html[lang="en"] [data-lang="en"] { display: revert; }
</style>
<!-- Décide AVANT le 1er paint si la landing statique doit être affichée
(et marque iOS, pour choisir le bon panneau d'aide à l'installation).
Inline + sync pour tourner avant le rendu du <body>. -->
<script>
(function() {
try {
var standalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|| window.navigator.standalone === true;
var profile = false, skipped = false, hasBootFragment = false;
// Deux clés : `multiplix-profiles` (index multi-profils) et
// `multiplix-profile` (ancien schéma mono-profil, migré au boot).
try { profile = !!(localStorage.getItem('multiplix-profiles') || localStorage.getItem('multiplix-profile')); } catch (e) {}
try { skipped = localStorage.getItem('multiplix-skip-install') === '1'; } catch (e) {}
// Fragment d'URL porteur d'un profil ou d'un appairage → on boote
// l'app directement (pas la landing) : #import= (migration
// cross-origin depuis l'ancien domaine), #transfer= (transfert depuis
// un autre appareil, QR de l'espace parent), ou #watch= (suivi à
// distance : un parent scanne le QR de l'appareil de son enfant, et
// n'a rien à faire de la landing), ou #recap (clic sur la notification
// hebdomadaire, qui ouvre l'espace parent). Le traitement réel a lieu
// dans main.tsx / App.tsx.
try { hasBootFragment = /(import|transfer|watch)=|^#recap$/.test(location.hash); } catch (e) {}
if (standalone || profile || skipped || hasBootFragment) {
document.documentElement.classList.add('skip-static-landing');
}
// Langue de l'interface (globale) : préférence explicite stockée,
// sinon langue du navigateur. ⚠ Même clé/logique que detectLang() dans
// src/i18n/lang.ts — garder les deux en phase. Fixé AVANT le 1er paint
// pour que la landing statique (texte dual fr/en piloté par CSS sur
// html[lang]) s'affiche directement dans la bonne langue, sans flash.
var lang = 'fr';
try {
var stored = localStorage.getItem('multiplix-lang');
if (stored === 'fr' || stored === 'en') lang = stored;
else if ((navigator.language || '').toLowerCase().indexOf('en') === 0) lang = 'en';
} catch (e) {}
document.documentElement.lang = lang;
if (lang === 'en') {
document.title = 'Tablito — Practise the times tables (free, no ads)';
var desc = document.querySelector('meta[name="description"]');
if (desc) desc.setAttribute('content', 'Free, ad-free app to learn the multiplication tables gently, from age 7. Leitner method, no grades or rankings, works offline. Data stays on the device by default.');
// Manifest localisé : le standard n'a pas de description multilingue,
// donc un fichier par langue, basculé ici (le <link rel="manifest">
// existe déjà plus haut). Seule la `description` diffère ; le nom et
// les icônes sont identiques. Pointé avant le 1er paint → c'est ce
// manifest que le navigateur lira pour le prompt d'installation.
var mani = document.querySelector('link[rel="manifest"]');
if (mani) mani.setAttribute('href', 'manifest.en.webmanifest');
}
var ua = navigator.userAgent;
var isIOS = /iPhone|iPod|iPad/.test(ua)
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (isIOS) {
// Sert au handler "Installer" à choisir le bon panneau d'aide
// (iOS share-sheet vs desktop) quand il n'y a pas de prompt natif.
document.documentElement.classList.add('is-ios');
// Hors Safari (Chrome/Firefox/Edge… iOS), le partage n'expose pas
// « Sur l'écran d'accueil » : on affichera un préambule "ouvre dans
// Safari". Détection fail-safe : par défaut on suppose Safari (le
// visiteur n'a pas à se voir dire d'ouvrir Safari s'il y est déjà).
// ⚠ Même denylist que isIOSSafari() dans src/lib/install.ts — ce
// script inline tourne pré-paint (avant tout module), d'où la copie.
if (/CriOS|FxiOS|EdgiOS|OPiOS|YaBrowser/.test(ua)) {
document.documentElement.classList.add('ios-other-browser');
}
}
} catch (e) {}
})();
</script>
<!-- Splash screens iOS (PWA) — un PNG par couple (taille × DPR).
Couvre iPhone 12/13/14/15/16 (standard, Pro, Pro Max) + iPad Pro 11".
Les autres appareils iOS auront un splash vide. Régénérer via
`npm run splash`. -->
<link rel="apple-touch-startup-image" href="splash/iphone-1170x2532.png"
media="(device-width: 390px) and (device-height: 844px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="splash/iphone-1179x2556.png"
media="(device-width: 393px) and (device-height: 852px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="splash/iphone-1290x2796.png"
media="(device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" />
<link rel="apple-touch-startup-image" href="splash/ipad-1668x2388.png"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" />
<title>Tablito — Réviser les tables de multiplication (gratuit, sans pub)</title>
<meta name="description" content="Appli gratuite et sans publicité pour apprendre les tables de multiplication en douceur, dès le CE1. Méthode Leitner, sans notes ni classement, fonctionne hors-ligne. Les données restent sur l'appareil par défaut." />
<link rel="canonical" href="https://tablito.app/" />
<!-- Open Graph / Twitter : aperçu riche quand l'URL est partagée (groupes
WhatsApp/Facebook de parents, iMessage, Slack enseignants…). Sans ça, le
lien s'affiche nu et personne ne clique. URLs en absolu obligatoire pour
les crawlers (ils ne voient que la prod). og:image régénérée par
`node scripts/generate-og-image.mjs`. -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Tablito" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:url" content="https://tablito.app/" />
<meta property="og:title" content="Tablito — Apprendre les tables de multiplication, en douceur" />
<meta property="og:description" content="Une appli gratuite et sans pub pour réviser les tables de multiplication dès le CE1. Méthode Leitner, sans notes ni classement, hors-ligne." />
<meta property="og:image" content="https://tablito.app/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Tablito — apprendre les tables de multiplication, dès le CE1, gratuit et sans pub." />
<!-- twitter:title/description/image retombent sur leurs équivalents og: quand
absents (spec Twitter Card) — on ne déclare que la carte pour éviter de
dupliquer (et désynchroniser) titre/description/image. -->
<meta name="twitter:card" content="summary_large_image" />
<script type="importmap">
{
"imports": {
"preact": "/vendor/preact/preact.module.js",
"preact/hooks": "/vendor/preact/hooks.module.js",
"preact/compat": "/vendor/preact/compat.module.js",
"preact/jsx-runtime": "/vendor/preact/jsx-runtime.module.js",
"react": "/vendor/preact/compat.module.js",
"react-dom": "/vendor/preact/compat.module.js",
"react-dom/client": "/vendor/preact/compat-client.mjs",
"lean-qr": "/vendor/lean-qr/index.mjs",
"qr-scanner": "/vendor/qr-scanner/qr-scanner.min.js",
"virtual:pwa-register":"/scripts/pwa-register-noop.js"
}
}
</script>
</head>
<body>
<!--
Landing 100 % statique : approche "static-first" pour FCP/LCP rapide
(esprit DHH/Rails sur les apps no-bundle). Le contenu visible est
directement dans l'HTML, aucun module JS chargé tant que l'utilisateur
n'interagit pas — si on quitte la page sans cliquer, 0 KB de JS app
téléchargé.
Source de vérité : ce markup + l'inline script en bas de body. Pas de
version Preact équivalente dans src/. App.tsx reprend la main quand
l'utilisateur clique "Essayer dans le navigateur" ou quand la PWA est
déjà installée (cf. inline script `skip-static-landing` dans <head>).
-->
<div id="root">
<div id="static-landing" class="landing">
<div class="landing-hero">
<div class="landing-mascot">
<!-- ⚠ SVG dupliqué de src/components/Mascot.tsx (l'inliner ici
permet à la landing de rester 100 % HTML, sans charger Preact
pour afficher Piou). 3e copie dans landing-video/composition/
index.html (vidéo hero). Si tu modifies le SVG, mets à jour les
trois fichiers. Aucun outil ne le détectera automatiquement. -->
<div class="mascot happy" role="img" aria-label="Mascotte Piou, humeur: happy">
<svg viewBox="0 0 100 100" class="mascot-svg" xmlns="http://www.w3.org/2000/svg">
<g class="mascot-feet">
<path d="M40 68 L 40 94 M 36 94 L 44 94" />
<path d="M60 68 L 60 94 M 56 94 L 64 94" />
</g>
<g class="mascot-body">
<ellipse cx="50" cy="60" rx="28" ry="28" class="mascot-fill-body" />
<path d="M32 60 Q 50 82 68 60" class="mascot-fill-belly" />
<g class="mascot-eye mascot-eye-left">
<circle cx="43" cy="54" r="3" class="mascot-eye-iris" />
<circle cx="44" cy="53" r="0.9" class="mascot-eye-shine" />
</g>
<g class="mascot-eye mascot-eye-right">
<circle cx="57" cy="54" r="3" class="mascot-eye-iris" />
<circle cx="58" cy="53" r="0.9" class="mascot-eye-shine" />
</g>
<path d="M46 63 L 54 63 L 50 68 Z" class="mascot-beak" />
<circle cx="38" cy="62" r="2.5" class="mascot-cheek" />
<circle cx="62" cy="62" r="2.5" class="mascot-cheek" />
<path d="M30 58 Q 26 70 36 74 Q 40 68 38 60 Z" class="mascot-wing" />
<path d="M48 34 L 50 28 L 52 34" class="mascot-tuft" />
</g>
</svg>
</div>
</div>
<h1 class="landing-title">Tablito</h1>
<p class="landing-tagline">
<span data-lang="fr">Apprendre les tables de multiplication, en douceur.</span>
<span data-lang="en">Learn the times tables, gently.</span>
</p>
<ul class="landing-badges">
<li class="landing-badge">
<span data-lang="fr">100 % gratuit</span>
<span data-lang="en">100% free</span>
</li>
<li class="landing-badge">
<span data-lang="fr">dès le CE1 · 7–11 ans</span>
<span data-lang="en">ages 7–11</span>
</li>
<li class="landing-badge">
<span data-lang="fr">tables & division</span>
<span data-lang="en">tables & division</span>
</li>
</ul>
</div>
<ul class="landing-principles">
<li>
<span class="landing-principle-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M3 12a9 9 0 1 0 3-6.7M3 4v5h5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
</span>
<span data-lang="fr"><strong>Méthode Leitner.</strong> Chaque multiplication revient juste avant que l'enfant l'oublie — la mémoire s'ancre durablement.</span>
<span data-lang="en"><strong>Leitner method.</strong> Each multiplication comes back right before your child forgets it — memory sticks for good.</span>
</li>
<li>
<span class="landing-principle-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 21s-7-4.5-7-11a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 6.5-7 11-7 11z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
</span>
<span data-lang="fr"><strong>Pas de notes, pas de classement.</strong> On célèbre le progrès, pas la performance. Aucun message négatif.</span>
<span data-lang="en"><strong>No grades, no rankings.</strong> We celebrate progress, not performance. Never a negative message.</span>
</li>
<li>
<span class="landing-principle-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.8" /><path d="M8 12l3 3 5-6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
</span>
<span data-lang="fr"><strong>Hors-ligne, sans pub, sans compte.</strong> Une fois installé, plus besoin de connexion. Les données restent sur l'appareil par défaut.</span>
<span data-lang="en"><strong>Offline, no ads, no account.</strong> Once installed, no connection needed. Data stays on the device by default.</span>
</li>
</ul>
<!-- Démo vidéo : un aperçu 9:16 de l'app (hero.<lang>.mp4, généré par
landing-video/). Poster-first + lazy : le poster (~45 KB) s'affiche,
la vidéo (~470 KB) n'est chargée qu'à l'approche du bloc, et jamais
en prefers-reduced-motion (cf. bootstrap inline en bas de body). La
vidéo n'est PAS précachée par le SW (inutile à la PWA installée). -->
<section class="landing-demo" aria-labelledby="landing-demo-title">
<h2 class="landing-demo-title" id="landing-demo-title">
<span data-lang="fr">Voir l'app en action</span>
<span data-lang="en">See the app in action</span>
</h2>
<div class="landing-demo-frame">
<!-- Pas d'attribut `poster` en dur : il serait fetché eagerly (même
avec preload="none") dans la langue FR avant que le script ne le
corrige → un visiteur EN paierait le poster FR pour rien. Le
script ci-dessous le pose dans la bonne langue ; en attendant, le
fond crème du cadre (.landing-demo-frame) tient lieu de repli. -->
<video class="landing-demo-video" id="hero-video"
muted loop playsinline preload="none">
</video>
</div>
</section>
<div class="landing-cta">
<button type="button" class="landing-install-btn">
<span data-lang="fr">Installer Tablito</span>
<span data-lang="en">Install Tablito</span>
</button>
<p class="landing-install-hint">
<span data-lang="fr">L'app s'installera comme n'importe quelle autre application.</span>
<span data-lang="en">The app installs just like any other app.</span>
</p>
<button type="button" class="landing-skip-btn">
<span data-lang="fr">Essayer dans le navigateur</span>
<span data-lang="en">Try it in the browser</span>
</button>
<p class="landing-skip-warning">
<span data-lang="fr">Attention : si tu installes Tablito plus tard sur iPhone ou iPad, la progression du navigateur ne sera pas conservée.</span>
<span data-lang="en">Heads up: if you install Tablito later on an iPhone or iPad, your browser progress won't carry over.</span>
</p>
<!-- Guide bilingue (cf. scripts/generate-user-guide.mjs) : un <a> par
langue, togglé par le CSS data-lang comme le reste de la landing.
Liens relatifs → valables sous n'importe quelle BASE (prod + previews). -->
<a class="landing-guide-link" data-lang="fr" href="guide/">Découvrir l'app en images →</a>
<a class="landing-guide-link" data-lang="en" href="guide/en/">See the app in pictures →</a>
</div>
<!-- Instructions iOS, montrées au clic "Installer" si pas de prompt natif.
Une seule liste, centrée sur l'ICÔNE de partage plutôt que sur sa
position : le bouton est en bas (iPhone) ou en haut (iPad), et son
emplacement dépend aussi du réglage « barre d'adresse » — rien de
tout ça n'est détectable côté web. Un repli ••• couvre le cas où le
partage est replié dans le menu (Safari compact). -->
<div id="ios-install-help" class="install-help">
<h2>
<span data-lang="fr">Installer sur iPhone / iPad</span>
<span data-lang="en">Install on iPhone / iPad</span>
</h2>
<!-- Préambule montré uniquement hors Safari (cf. html.ios-other-browser) :
en Safari, inutile de dire au visiteur d'ouvrir Safari. -->
<p class="ios-open-in-safari">
<span data-lang="fr">Tu es dans Chrome ou Firefox ? Ouvre d'abord cette page dans <strong>Safari</strong> (le menu de ton navigateur propose <strong>« Ouvrir dans Safari »</strong>), puis :</span>
<span data-lang="en">In Chrome or Firefox? First open this page in <strong>Safari</strong> (your browser menu has <strong>“Open in Safari”</strong>), then:</span>
</p>
<p class="ios-intro">
<span data-lang="fr">Ajoute l'app à ton écran d'accueil en 3 touches :</span>
<span data-lang="en">Add the app to your Home Screen in 3 taps:</span>
</p>
<ol>
<li data-lang="fr">
Touche l'icône <strong>Partager</strong>
<span class="ios-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 14V4"/><path d="M8.5 7.5 12 4l3.5 3.5"/><path d="M7 11H6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-1"/></svg></span>
dans la barre d'outils de Safari (<strong>en bas</strong> sur iPhone, <strong>en haut</strong> sur iPad).
<span class="ios-step-hint">Tu ne la vois pas ? Touche d'abord <strong>•••</strong>, puis <strong>« Partager »</strong>.</span>
</li>
<li data-lang="en">
Tap the <strong>Share</strong> icon
<span class="ios-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 14V4"/><path d="M8.5 7.5 12 4l3.5 3.5"/><path d="M7 11H6a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-1"/></svg></span>
in Safari's toolbar (<strong>at the bottom</strong> on iPhone, <strong>at the top</strong> on iPad).
<span class="ios-step-hint">Can't see it? Tap <strong>•••</strong> first, then <strong>“Share”</strong>.</span>
</li>
<li data-lang="fr">Fais défiler, puis choisis <strong>« Sur l'écran d'accueil »</strong> <span class="ios-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="4.5"/><path d="M12 8.5v7M8.5 12h7"/></svg></span>.</li>
<li data-lang="en">Scroll down, then choose <strong>“Add to Home Screen”</strong> <span class="ios-icon"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="4.5"/><path d="M12 8.5v7M8.5 12h7"/></svg></span>.</li>
<li data-lang="fr">Touche <strong>« Ajouter »</strong> en haut à droite. L'icône Tablito apparaît sur ton écran !</li>
<li data-lang="en">Tap <strong>“Add”</strong> at the top right. The Tablito icon appears on your screen!</li>
</ol>
</div>
<!-- Desktop sans prompt natif (Safari macOS, Firefox…). Montré au clic
"Installer" quand on n'est ni en prompt natif (Chrome/Edge) ni sur
iOS. Volontairement court et honnête : sur ordinateur l'install est
un bonus, le vrai CTA utile est "Essayer dans le navigateur". -->
<div id="desktop-install-help" class="install-help">
<h2>
<span data-lang="fr">Installer sur ordinateur</span>
<span data-lang="en">Install on desktop</span>
</h2>
<p>
<span data-lang="fr">Selon ton navigateur :</span>
<span data-lang="en">Depending on your browser:</span>
</p>
<ol>
<li>
<span data-lang="fr">Avec <strong>Chrome</strong> ou <strong>Edge</strong>, une icône d'installation apparaît à droite de la barre d'adresse — clique dessus.</span>
<span data-lang="en">In <strong>Chrome</strong> or <strong>Edge</strong>, an install icon appears at the right of the address bar — click it.</span>
</li>
<li>
<span data-lang="fr">Sur <strong>Safari (Mac)</strong>, ouvre le menu <strong>Fichier</strong> puis <strong>« Ajouter au Dock »</strong>.</span>
<span data-lang="en">In <strong>Safari (Mac)</strong>, open the <strong>File</strong> menu, then <strong>“Add to Dock”</strong>.</span>
</li>
</ol>
<p class="install-note">
<span data-lang="fr">Pas d'installation possible ? Clique sur <strong>« Essayer dans le navigateur »</strong> ci-dessus pour commencer tout de suite.</span>
<span data-lang="en">Can't install? Click <strong>“Try it in the browser”</strong> above to start right away.</span>
</p>
</div>
</div>
</div>
<!--
Bootstrap inline. Pas de <script type="module" src="..."> par défaut :
un visiteur qui ne fait que lire la landing ne télécharge PAS les
90+ modules ESM de l'app. Conforme à l'esprit "static-first".
Trois cas :
- skip-static-landing déjà posé (PWA installée, profil, skip flag) →
boot Preact immédiatement (l'utilisateur n'a pas besoin de la landing).
- Visiteur 1re fois → wire les boutons. Le boot Preact n'arrive qu'au
clic sur "Essayer dans le navigateur" ou "Installer".
- Capture beforeinstallprompt en amont pour pouvoir le réutiliser au
clic du bouton "Installer".
-->
<script>
(function() {
var deferredPrompt = null;
window.addEventListener('beforeinstallprompt', function(e) {
e.preventDefault();
deferredPrompt = e;
});
function bootApp() {
// Le path .tsx est rewrité en .js par scripts/build.mjs pour la prod.
// En dev, dev.mjs sert le .tsx via esbuild.transform.
// createRoot remplace les enfants de #root, donc la landing statique
// disparaît automatiquement quand App mount — pas besoin de la
// remove manuellement (et ça garde le contenu visible jusqu'à ce
// que l'app soit prête, plutôt que d'afficher un écran blanc
// pendant le download de main.js sur réseau lent).
return import('/src/main.tsx').catch(function(err) {
console.error('[boot] failed to load app', err);
});
}
if (document.documentElement.classList.contains('skip-static-landing')) {
bootApp();
return;
}
document.querySelector('.landing-install-btn').addEventListener('click', function() {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt = null;
return;
}
// Pas de prompt natif : iOS → instructions share-sheet ; sinon
// (desktop Safari/Firefox…) → aide desktop, et non les étapes iPhone.
var isIOS = document.documentElement.classList.contains('is-ios');
var help = document.getElementById(isIOS ? 'ios-install-help' : 'desktop-install-help');
help.classList.add('visible');
help.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
document.querySelector('.landing-skip-btn').addEventListener('click', function() {
try { localStorage.setItem('multiplix-skip-install', '1'); } catch (e) {}
bootApp();
});
// Démo vidéo : poster-first + lazy. On sert le poster/MP4 de la langue
// (html[lang] est fixé par le script du <head>), et on ne charge le MP4
// qu'à l'approche du bloc — un visiteur qui repart en haut paie 0 KB de
// vidéo. prefers-reduced-motion : on reste sur le poster, pas d'autoplay.
var heroVideo = document.getElementById('hero-video');
if (heroVideo) {
var vlang = document.documentElement.lang === 'en' ? 'en' : 'fr';
heroVideo.poster = 'img/hero-poster.' + vlang + '.jpg';
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce && 'IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
heroVideo.src = 'video/hero.' + vlang + '.mp4';
heroVideo.play().catch(function () {});
io.disconnect();
});
}, { rootMargin: '200px' });
io.observe(heroVideo);
}
}
})();
</script>
</body>
</html>