// Onboarding — 4 écrans au premier lancement, une seule fois.
//
// Passe après la connexion : on ne présente pas l'app à quelqu'un qui n'est
// pas encore entré. Mémorisé en localStorage sous `vasco_onboarded` ; le
// panneau Tweaks sait le remettre à zéro pour rejouer la séquence en démo.
//
// Navigation : balayage horizontal, tap sur « Suivant », ou « Passer ».
const OB_VT = window.VT || {};
const OB_INK = OB_VT.ink || '#0a0a0a';
const OB_SUB = OB_VT.sub || '#6b7280';
const OB_BG = OB_VT.paper || '#f6f5f1';
const OB_SURF = OB_VT.surface || '#ffffff';
const OB_HAIR = OB_VT.hair || 'rgba(10,10,10,0.06)';
const OB_HAIR2 = OB_VT.hair2 || 'rgba(10,10,10,0.10)';
const OB_MED = OB_VT.medBlue || '#3d6b8f';
const OB_MED_TINT = OB_VT.medBlueTint || 'rgba(61,107,143,0.10)';
const OB_OK = OB_VT.success || '#059669';
const OB_OK_TINT = OB_VT.successTint || 'rgba(5,150,105,0.10)';
const VASCO_ONBOARD_KEY = 'vasco_onboarded';
// `?onboarding` dans l'URL force la relecture — c'est le chemin de démo.
// Le panneau Tweaks ne s'ouvre que depuis un hôte d'aperçu (il attend un
// postMessage) : dans un navigateur normal son bouton est inatteignable.
const obForced = () => {
try { return /(^|[?&])onboarding(=|&|$)/.test(window.location.search); }
catch (e) { return false; }
};
// Même prudence que pour l'auth : localStorage lève en navigation privée.
const obRead = () => {
if (obForced()) return null;
try { return window.localStorage.getItem(VASCO_ONBOARD_KEY); } catch (e) { return null; }
};
const obWrite = () => {
try { window.localStorage.setItem(VASCO_ONBOARD_KEY, '1'); } catch (e) { /* sans mémoire */ }
};
const obClear = () => {
try { window.localStorage.removeItem(VASCO_ONBOARD_KEY); } catch (e) { /* rien à faire */ }
};
// ─────────────────────────────────────────────────────────────
// Illustrations — géométriques, sur la palette de l'app.
// Pas d'images : tout est vectoriel, donc net à toutes les tailles.
// ─────────────────────────────────────────────────────────────
function ObArtDictee() {
return (
{/* Halos concentriques — la même respiration que le micro du dashboard */}
{[0, 1, 2].map((i) => (
))}
{cards.map((c, i) => (
// Deux niveaux : l'extérieur anime l'entrée, l'intérieur porte la
// position et l'inclinaison. Sur un seul élément, le transform de
// l'animation écraserait le rotate et les cartes s'empileraient.
);
}
const OB_SLIDES = [
{
id: 'dictee',
art: ,
title: 'Dictez,\non rédige.',
body: "Un compte rendu opératoire complet en parlant deux minutes. Vous relisez, vous corrigez, vous validez.",
},
{
id: 'patients',
art: ,
title: 'Vos dossiers,\nen un geste.',
body: "Wallet, liste ou grille — chaque patient à portée de pouce, avec ses alertes et son prochain rendez-vous.",
},
{
id: 'assistant',
art: ,
title: 'Un assistant\nqui agit.',
body: "Courrier au correspondant, codage CCAM, contrôle à programmer : vous demandez, c'est préparé.",
},
{
id: 'pret',
art: ,
title: 'Tout est prêt,\nDocteur.',
body: "Les données affichées sont fictives. Rien ne quitte l'appareil tant que la connexion n'est pas établie.",
},
];
// ─────────────────────────────────────────────────────────────
// Écran
// ─────────────────────────────────────────────────────────────
function OnboardingScreen({ accent, onDone }) {
const [idx, setIdx] = React.useState(0);
const [drag, setDrag] = React.useState(0);
const startX = React.useRef(null);
const width = React.useRef(1);
const last = OB_SLIDES.length - 1;
const tint = accent || OB_MED;
const finish = () => { obWrite(); onDone(); };
const next = () => (idx === last ? finish() : setIdx(idx + 1));
// ── Balayage ──
const onDown = (e) => {
startX.current = e.clientX;
width.current = e.currentTarget.getBoundingClientRect().width || 1;
e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId);
};
const onMove = (e) => {
if (startX.current === null) return;
const dx = e.clientX - startX.current;
// Résistance aux deux extrémités : on sent le bout de la pile.
const overshoot = (idx === 0 && dx > 0) || (idx === last && dx < 0);
setDrag(overshoot ? dx * 0.28 : dx);
};
const onUp = () => {
if (startX.current === null) return;
const seuil = width.current * 0.18;
if (drag < -seuil && idx < last) setIdx(idx + 1);
else if (drag > seuil && idx > 0) setIdx(idx - 1);
startX.current = null;
setDrag(0);
};
const n = OB_SLIDES.length;
const dragging = startX.current !== null;
return (
{/* Passer */}
{/* Piste des écrans */}
{OB_SLIDES.map((s, i) => (
{/* L'illustration est toujours rendue — sinon l'écran voisin
serait vide pendant le balayage. Le `key` la remonte quand
elle devient active, ce qui rejoue son animation d'entrée
pile au moment où elle arrive à l'écran. */}