feat(oki): intro publique, chimen, PWA offline, contribuer, kréyòl, ton OKI
- Landing : intro « Pourquoi ce site ? » (Chat Control expliqué au grand public) + sommaire-chimen en 3 arcs (Fondations 🟢 / Reprendre le contrôle 🟡 / Autodéfense avancée 🔴) avec compte d'outils par catégorie - PWA : service worker offline-first (précache coquille, network-first avec repli cache pour les fiches consultées), badge hors-ligne discret - Page /contribuer (3 voies + forge labola) + template d'issue Forgejo .gitea/ISSUE_TEMPLATE/nouvo-zouti.yaml ; liens GitHub → labola.o-k-i.net - Fiches jumelles créoles : chargeur content/outils/gcf/, affichage selon la langue avec bandeaux « an chantyé » / « póko tradui », 6 pilotes (signal, protonmail, brave, bitwarden, ublock, quad9) — À FAIRE RELIRE par un locuteur natif - Ton OKI : les 6 mêmes fiches réécrites (tutoiement, registre militant-chaleureux) ; le script de migration n'écrase plus les fiches retravaillées (--force pour tout régénérer) - UI : View Transitions (respect reduced-motion), aide clavier « ? » (dialog natif, raccourcis / h a), progression locale « jès fèt » (localStorage uniquement, compteur visible après le premier geste) - docs/DEPLOYMENT.md : guide VPS (Caddy, nginx, YunoHost, Docker, rsync) - Audits : axe-core 0 violation (5 pages), Lighthouse 99-98/100/100/100 ; corrections contraste monogrammes + ordre des titres - Budgets : JS 46 Ko gzip, CSS 3,5 Ko, 59 pages prérendues Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
name: "Nouvo zouti — Proposer un outil"
|
||||||
|
description: "Pwopozé on zouti lib pou lis-la / Proposer un outil libre pour l'annuaire"
|
||||||
|
title: "[zouti] "
|
||||||
|
labels:
|
||||||
|
- "nouvo-zouti"
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: nom
|
||||||
|
attributes:
|
||||||
|
label: "Non a zouti-la / Nom de l'outil"
|
||||||
|
placeholder: "ex. Signal"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: url
|
||||||
|
attributes:
|
||||||
|
label: "Sit ofisyèl / Site officiel"
|
||||||
|
placeholder: "https://…"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: licence
|
||||||
|
attributes:
|
||||||
|
label: "Licence (identifiant SPDX si tu le connais)"
|
||||||
|
placeholder: "ex. AGPL-3.0"
|
||||||
|
- type: dropdown
|
||||||
|
id: difficulte
|
||||||
|
attributes:
|
||||||
|
label: "Difikilté / Difficulté d'installation"
|
||||||
|
options:
|
||||||
|
- "🟢 Débitan / Débutant"
|
||||||
|
- "🟡 Entèmédia / Intermédiaire"
|
||||||
|
- "🔴 Avansé / Avancé"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: pourquoi
|
||||||
|
attributes:
|
||||||
|
label: "Poukisa i konté ? / Pourquoi il compte ?"
|
||||||
|
description: "En quoi cet outil sert la souveraineté numérique ? Qui devrait s'en servir ?"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: verifs
|
||||||
|
attributes:
|
||||||
|
label: "Vérifications"
|
||||||
|
options:
|
||||||
|
- label: "L'outil est libre / open-source (code source publié)"
|
||||||
|
required: true
|
||||||
|
- label: "J'ai vérifié qu'il n'est pas déjà dans l'annuaire"
|
||||||
|
required: true
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "ia",
|
"slug": "ia",
|
||||||
"title": "\"L'IA conversationnelle\"",
|
"title": "L'IA conversationnelle",
|
||||||
"order": 15,
|
"order": 15,
|
||||||
"part": 9
|
"part": 9
|
||||||
},
|
},
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"slug": "os",
|
"slug": "os",
|
||||||
"title": "\"Systèmes d'exploitation libres\"",
|
"title": "Systèmes d'exploitation libres",
|
||||||
"order": 19,
|
"order": 19,
|
||||||
"part": 12
|
"part": 12
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,30 +14,20 @@ lang: "fr"
|
|||||||
|
|
||||||
## what
|
## what
|
||||||
|
|
||||||
Un coffre-fort à mots de passe qui les mémorise, les génère et les remplit pour vous, sur tous
|
Un coffre-fort à mots de passe qui les mémorise, les génère et les remplit pour toi, sur tous tes appareils.
|
||||||
vos appareils.
|
|
||||||
|
|
||||||
## why
|
## why
|
||||||
|
|
||||||
Gratuit, audité, multiplateforme, et chiffré de bout en bout. Vous n'avez plus qu'un seul mot de
|
Gratuit, audité, multiplateforme, chiffré de bout en bout. Tu n'as plus qu'un seul mot de passe fort à retenir — et le jour où tu veux ne dépendre de personne, il est <b>auto-hébergeable</b> (via Vaultwarden).
|
||||||
passe fort à retenir. Il est même <b>auto-hébergeable</b> (via Vaultwarden) pour ne dépendre de
|
|
||||||
personne.
|
|
||||||
|
|
||||||
## who
|
## who
|
||||||
|
|
||||||
Tout le monde. C'est le point de départ de toute la sécurité : un mot de passe unique et fort
|
Tout le monde. C'est la fondation de toute ta sécurité : un mot de passe unique et fort par service, sans effort de mémoire.
|
||||||
par service.
|
|
||||||
|
|
||||||
## install
|
## install
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Crée un compte sur bitwarden.com et choisis un mot de passe maître <b>long</b> — une phrase entière. Ne l'oublie jamais : il n'est pas récupérable, et c'est voulu.</li>
|
||||||
Créez un compte sur bitwarden.com et choisissez un mot de passe maître <b>long</b> (une
|
<li>Installe l'extension de navigateur et l'application mobile.</li>
|
||||||
phrase). Ne l'oubliez jamais : il n'est pas récupérable.
|
<li>Au fil de tes connexions, laisse Bitwarden enregistrer puis remplacer tes vieux mots de passe par des mots de passe générés.</li>
|
||||||
</li>
|
|
||||||
<li>Installez l'extension de navigateur et l'application mobile.</li>
|
|
||||||
<li>
|
|
||||||
Au fil de vos connexions, laissez Bitwarden enregistrer puis remplacer vos anciens mots de
|
|
||||||
passe par des mots de passe générés.
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
+6
-17
@@ -14,31 +14,20 @@ lang: "fr"
|
|||||||
|
|
||||||
## what
|
## what
|
||||||
|
|
||||||
Un navigateur basé sur Chromium qui bloque publicités et traqueurs par défaut, avec en prime une
|
Un navigateur basé sur Chromium qui bloque publicités et traqueurs par défaut — avec en prime une fenêtre privée connectée à Tor.
|
||||||
fenêtre privée connectée à Tor.
|
|
||||||
|
|
||||||
## why
|
## why
|
||||||
|
|
||||||
C'est l'option la plus simple pour quelqu'un qui vient de Chrome : même ergonomie, mais sans le
|
C'est le changement le plus indolore quand tu viens de Chrome : même ergonomie, sans le pistage. Il bloque les pubs YouTube nativement, propose une visio chiffrée (Brave Talk, basé sur Jitsi) et un accès Tor en un clic — pratique pour récupérer un site bloqué.
|
||||||
pistage. Il bloque les pubs YouTube nativement, propose une visio chiffrée (Brave Talk, basée
|
|
||||||
sur Jitsi) et un accès Tor en un clic, pratique pour récupérer un site bloqué.
|
|
||||||
|
|
||||||
## who
|
## who
|
||||||
|
|
||||||
Les débutants qui veulent un changement indolore depuis Chrome. (Ignorez les fonctions
|
Celles et ceux qui veulent quitter Chrome sans rien réapprendre. (Les fonctions crypto et pubs récompensées ? Ignore-les, elles se désactivent.)
|
||||||
crypto/pubs récompensées si elles ne vous intéressent pas.)
|
|
||||||
|
|
||||||
## install
|
## install
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Installe Brave depuis <b>brave.com</b>.</li>
|
||||||
Installez Brave depuis <b>brave.com</b>.
|
<li>Règle les « Boucliers » (Shields) sur « agressif », et désactive Brave Rewards si tu ne veux pas de pubs.</li>
|
||||||
</li>
|
<li>Pour Tor : menu > « Nouvelle fenêtre privée avec Tor », puis attends « Tor connecté ».</li>
|
||||||
<li>
|
|
||||||
Réglez les « Boucliers » (Shields) sur « agressif », et désactivez Brave Rewards si vous ne
|
|
||||||
voulez pas de pubs.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Pour Tor : menu > « Nouvelle fenêtre privée avec Tor », puis attendez « Tor connecté ».
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: "Bitwarden"
|
||||||
|
slug: "bitwarden"
|
||||||
|
lang: "gcf"
|
||||||
|
---
|
||||||
|
|
||||||
|
## what
|
||||||
|
|
||||||
|
On kòf a modpas ki ka sonjé-yo, jénéré-yo é ranpli-yo pou vou, asi tout apawèy a-w.
|
||||||
|
|
||||||
|
## why
|
||||||
|
|
||||||
|
Gratis, odité, miltiplatfòwm, chifré bout-an-bout. Ou ni yenki on sèl modpas fò pou sonjé — é jou ou vlé pa dépann di pèsonn, ou pé <b>oto-ébèjé'y</b> (èvè Vaultwarden).
|
||||||
|
|
||||||
|
## who
|
||||||
|
|
||||||
|
Tout moun. Sé fondasyon a tout sékirité a-w : on modpas inik é fò pou chak sèwvis, san fòsé mémwa a-w.
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Kréyé on kont asi bitwarden.com é chwazi on modpas mèt <b>long</b> — on fraz antyé. Pa janmé oublié'y : i pa rékipérab, é sé fè èsprè.</li>
|
||||||
|
<li>Enstalé èkstansyon navigatè-la é aplikasyon mobil-la.</li>
|
||||||
|
<li>O fi a konèksyon a-w, kité Bitwarden anrèjistré é apré ranplasé vyé modpas a-w pa modpas jénéré.</li>
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: "Brave"
|
||||||
|
slug: "brave"
|
||||||
|
lang: "gcf"
|
||||||
|
---
|
||||||
|
|
||||||
|
## what
|
||||||
|
|
||||||
|
On navigatè basé asi Chromium ki ka bloké piblisité é trakè pa défo — èvè an plis on fenèt privé konèkté asi Tor.
|
||||||
|
|
||||||
|
## why
|
||||||
|
|
||||||
|
Sé chanjman pi dous-la lè ou ka sòti an Chrome : menm èwgonomi, san pistaj-la. I ka bloké pib YouTube dirèk, i ka pwopozé on vizyo chifré (Brave Talk, basé asi Jitsi) é on aksè Tor an on klik — pratik pou ratrapé on sit bloké.
|
||||||
|
|
||||||
|
## who
|
||||||
|
|
||||||
|
Moun ki vlé kité Chrome san riaprann ayen. (Fonksyon crypto é pib rékonpansé ? Inyoré-yo, yo ka tenn.)
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Enstalé Brave dépi <b>brave.com</b>.</li>
|
||||||
|
<li>Réglé « Bouklyé »-la (Shields) asi « agrésif », é tenn Brave Rewards si ou pa vlé pib.</li>
|
||||||
|
<li>Pou Tor : mèni > « Nouvo fenèt privé èvè Tor », apré atann « Tor konèkté ».</li>
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: "Proton Mail"
|
||||||
|
slug: "protonmail"
|
||||||
|
lang: "gcf"
|
||||||
|
---
|
||||||
|
|
||||||
|
## what
|
||||||
|
|
||||||
|
On bwèt imèl chifré ki ka ranplasé Gmail oben Outlook — é kont pivo a tout ékosistèm Proton-la.
|
||||||
|
|
||||||
|
## why
|
||||||
|
|
||||||
|
Basé an Swis, andéwò a jiridiksyon « Five Eyes »-la. Konpatib OpenPGP : chifraj-la otomatik ant kont Proton é lòt sèwvis PGP. Kouryé a-w, sé korèspondans a-w — pa on matyè prèmyé pou piblisité.
|
||||||
|
|
||||||
|
## who
|
||||||
|
|
||||||
|
Tout moun. Sé pi bon pòt pou sòti an Google : louvè kont-la, rès a suit-la (ajanda, drive, VPN) ka suiv tousèl.
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Kréyé on kont asi <b>proton.me</b> (òf gratis-la ka sifi pou koumansé).</li>
|
||||||
|
<li>Rapatriyé vyé mésaj é kontak a-w èvè <b>Easy Switch</b> (entégré).</li>
|
||||||
|
<li>Mèt on rédirèksyon dépi Gmail é prévien kontak a-w di nouvo adrès a-w.</li>
|
||||||
|
<li>Enstalé aplikasyon mobil-la, limé <b>doub otantifikasyon-la</b> — é Proton Bridge si ou ka rété asi Thunderbird.</li>
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: "Quad9"
|
||||||
|
slug: "quad9"
|
||||||
|
lang: "gcf"
|
||||||
|
---
|
||||||
|
|
||||||
|
## what
|
||||||
|
|
||||||
|
On rézolvè DNS chifré, swis é san bi likratif, ki ka bloké an plis domèn malfézan yo ja konnèt.
|
||||||
|
|
||||||
|
## why
|
||||||
|
|
||||||
|
Founisè aksè a-w ka vwè chak sit ou ka vizité gras a DNS-la. Quad9 ka chifré dimann-lasa yo, i pa ka gadé adrès IP a-w, é i ka pwotéjé-w kont lamèkantri an menm tan. On réglaj « fè'y é oublié'y ».
|
||||||
|
|
||||||
|
## who
|
||||||
|
|
||||||
|
Tout moun — sitou si ou vlé on vré gany san enstalé ayen anplis.
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Asi Android : Réglaj > Rézo > <b>DNS privé</b> > matjé <b>dns.quad9.net</b>.</li>
|
||||||
|
<li>Asi iPhone/Mac : enstalé profil DoH-la dépi quad9.net. Asi PC : konfiguré DNS-over-HTTPS an navigatè-la oben an sistèm-la.</li>
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: "Signal"
|
||||||
|
slug: "signal"
|
||||||
|
lang: "gcf"
|
||||||
|
---
|
||||||
|
|
||||||
|
## what
|
||||||
|
|
||||||
|
On mésajri chifré pou tèks, apèl, vizyo é group a-w. Sé ranplasan dirèk a WhatsApp — menm senplisité, san Meta dèyè.
|
||||||
|
|
||||||
|
## why
|
||||||
|
|
||||||
|
Chifraj bout-an-bout pa défo, san bi likratif, protokòl odité ki ka sèwvi référans toupatou. Prèské pon métadoné pa ka rété, é ékip-la ja di : yo ké kité Léwòp olyé yo enstalé on skannaj. Chak moun ou ka fè pasé, sé on konvèsasyon anplis ki ka sòti an survéyans-la.
|
||||||
|
|
||||||
|
## who
|
||||||
|
|
||||||
|
Tout moun, jòdi-la menm. Sé prèmyé jès-la, é pi itil-la : koumansé èvè group a fanmi a-w. Yenki on kontrent : on niméwo téléfòn — ou pé séré'y dèyè on non itilizatè.
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Enstalé dépi <b>signal.org</b> (App Store, Google Play, oben APK dirèk asi Android).</li>
|
||||||
|
<li>Konfirmé niméwo a-w pa SMS, apré chwazi on kòd PIN.</li>
|
||||||
|
<li>Réglaj > Konfidansyalité : kréyé on <b>non itilizatè</b> pou séré niméwo a-w, é limé <b>vèwou enskripsyon-la</b>.</li>
|
||||||
|
<li>Envité moun a-w — group fanmi-la avan. Asi òwdinatè, lyanné aplikasyon biwo-la èvè téléfòn a-w.</li>
|
||||||
|
</ol>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: "uBlock Origin"
|
||||||
|
slug: "ublock"
|
||||||
|
lang: "gcf"
|
||||||
|
---
|
||||||
|
|
||||||
|
## what
|
||||||
|
|
||||||
|
Èkstansyon-la ki ka bloké piblisité, trakè é èskrip èspyon. Jès pi rantab a tout gid-lasa : dis ségonn, éfè imedya.
|
||||||
|
|
||||||
|
## why
|
||||||
|
|
||||||
|
Piblisité an liny sé prensipal chimen a survéyans é profilaj. Bloké'y, sé koupé pi gwo moso a pistaj-la — é an menm balan paj a-w ka chajé pi vit é fòfé data a-w ka respiré.
|
||||||
|
|
||||||
|
## who
|
||||||
|
|
||||||
|
Tout moun, asi Firefox kon asi navigatè Chromium. Si ou ka fè yenki on sèl jès jòdi-la, fè ta-lasa.
|
||||||
|
|
||||||
|
## install
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Louvè magazen èkstansyon a navigatè a-w é chèché « uBlock Origin » (orijinal-la, pa Raymond Hill).</li>
|
||||||
|
<li>Enstalé'y. I ka travay lamenm, san pon réglaj.</li>
|
||||||
|
</ol>
|
||||||
@@ -13,31 +13,21 @@ lang: "fr"
|
|||||||
|
|
||||||
## what
|
## what
|
||||||
|
|
||||||
Une boîte mail chiffrée qui remplace Gmail ou Outlook, et le compte pivot de tout l'écosystème
|
Une boîte mail chiffrée qui remplace Gmail ou Outlook — et le compte pivot de tout l'écosystème Proton.
|
||||||
Proton.
|
|
||||||
|
|
||||||
## why
|
## why
|
||||||
|
|
||||||
Basé en Suisse, hors juridiction des « Five Eyes ». Compatible OpenPGP : chiffrement automatique
|
Basée en Suisse, hors juridiction des « Five Eyes ». Compatible OpenPGP : le chiffrement est automatique entre comptes Proton et vers les autres services PGP. Ton courrier, c'est ta correspondance — pas une matière première publicitaire.
|
||||||
entre comptes Proton et vers les autres services PGP.
|
|
||||||
|
|
||||||
## who
|
## who
|
||||||
|
|
||||||
Tout le monde. C'est le meilleur point de départ pour quitter Google (voir section 05 pour le
|
Tout le monde. C'est la meilleure porte de sortie de Google : ouvre le compte, le reste de la suite (agenda, drive, VPN) suit tout seul.
|
||||||
reste de la suite).
|
|
||||||
|
|
||||||
## install
|
## install
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Crée un compte sur <b>proton.me</b> (l'offre gratuite suffit largement pour commencer).</li>
|
||||||
Créez un compte sur <b>proton.me</b> (offre gratuite suffisante pour commencer).
|
<li>Rapatrie tes anciens messages et contacts avec <b>Easy Switch</b> (intégré).</li>
|
||||||
</li>
|
<li>Mets une redirection depuis Gmail et préviens tes contacts de ta nouvelle adresse.</li>
|
||||||
<li>
|
<li>Installe l'application mobile, active la <b>double authentification</b> — et Proton Bridge si tu tiens à Thunderbird.</li>
|
||||||
Importez vos anciens messages et contacts avec <b>Easy Switch</b> (intégré).
|
|
||||||
</li>
|
|
||||||
<li>Mettez une redirection depuis Gmail et prévenez vos contacts de votre nouvelle adresse.</li>
|
|
||||||
<li>
|
|
||||||
Installez l'application mobile, activez la <b>double authentification</b>, et utilisez Proton
|
|
||||||
Bridge si vous tenez à Thunderbird.
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
+5
-12
@@ -13,26 +13,19 @@ lang: "fr"
|
|||||||
|
|
||||||
## what
|
## what
|
||||||
|
|
||||||
Un résolveur DNS chiffré, suisse et à but non lucratif, qui bloque en plus les domaines
|
Un résolveur DNS chiffré, suisse et à but non lucratif, qui bloque en plus les domaines malveillants connus.
|
||||||
malveillants connus.
|
|
||||||
|
|
||||||
## why
|
## why
|
||||||
|
|
||||||
Juridiction suisse, pas de conservation de votre adresse IP, protection contre l'hameçonnage et
|
Ton fournisseur d'accès voit chaque site que tu visites via le DNS. Quad9 chiffre ces requêtes, ne conserve pas ton adresse IP, et te protège de l'hameçonnage au passage. Un réglage « fais-le et oublie-le ».
|
||||||
les logiciels malveillants. Un excellent réglage « installer et oublier ».
|
|
||||||
|
|
||||||
## who
|
## who
|
||||||
|
|
||||||
Tout le monde, en particulier ceux qui veulent la simplicité et une bonne juridiction.
|
Tout le monde — surtout si tu veux un gain réel sans rien installer de nouveau.
|
||||||
|
|
||||||
## install
|
## install
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Sur Android : Réglages > Réseau > <b>DNS privé</b> > saisis <b>dns.quad9.net</b>.</li>
|
||||||
Sur Android : Réglages > Réseau > <b>DNS privé</b> > saisissez <b>dns.quad9.net</b>.
|
<li>Sur iPhone/Mac : installe le profil DoH depuis quad9.net. Sur PC : configure le DNS-over-HTTPS dans le navigateur ou le système.</li>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Sur iPhone/Mac : installez le profil DoH depuis quad9.net. Sur PC : configurez le
|
|
||||||
DNS-over-HTTPS dans le navigateur ou le système.
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -14,30 +14,21 @@ lang: "fr"
|
|||||||
|
|
||||||
## what
|
## what
|
||||||
|
|
||||||
Une messagerie chiffrée pour vos textos, appels, visios et groupes. C'est le remplaçant direct
|
Une messagerie chiffrée pour tes textos, appels, visios et groupes. C'est le remplaçant direct de WhatsApp — aussi simple, sans Meta derrière.
|
||||||
de WhatsApp, en aussi simple.
|
|
||||||
|
|
||||||
## why
|
## why
|
||||||
|
|
||||||
Chiffrement de bout en bout par défaut, à but non lucratif, protocole audité qui sert de
|
Chiffrement de bout en bout par défaut, à but non lucratif, protocole audité qui sert de référence mondiale. Presque aucune métadonnée gardée, et l'équipe a promis de quitter l'UE plutôt que d'installer un scan. Chaque proche que tu fais basculer, c'est une conversation de plus qui sort de la surveillance.
|
||||||
référence mondiale. Presque aucune métadonnée conservée, et l'équipe a promis de quitter l'UE
|
|
||||||
plutôt que d'installer un scan.
|
|
||||||
|
|
||||||
## who
|
## who
|
||||||
|
|
||||||
Tout le monde, dès aujourd'hui. C'est le premier geste et le plus utile. Seule contrainte : un
|
Tout le monde, dès aujourd'hui. C'est le premier geste, et le plus utile : commence par ton groupe de famille. Seule contrainte : un numéro de téléphone — que tu peux cacher derrière un nom d'utilisateur.
|
||||||
numéro de téléphone, que l'on peut masquer derrière un nom d'utilisateur.
|
|
||||||
|
|
||||||
## install
|
## install
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Installe depuis <b>signal.org</b> (App Store, Google Play, ou l'APK direct sur Android).</li>
|
||||||
Installez depuis <b>signal.org</b> (App Store, Google Play, ou l'APK direct sur Android).
|
<li>Confirme ton numéro par SMS, puis choisis un code PIN.</li>
|
||||||
</li>
|
<li>Réglages > Confidentialité : crée un <b>nom d'utilisateur</b> pour cacher ton numéro, et active le <b>verrou d'inscription</b>.</li>
|
||||||
<li>Confirmez votre numéro par SMS, puis choisissez un code PIN.</li>
|
<li>Invite tes proches — le groupe famille d'abord. Sur ordinateur, lie l'application de bureau à ton téléphone.</li>
|
||||||
<li>
|
|
||||||
Réglages > Confidentialité : créez un <b>nom d'utilisateur</b> pour cacher votre numéro, et
|
|
||||||
activez le <b>verrou d'inscription</b>.
|
|
||||||
</li>
|
|
||||||
<li>Invitez vos proches. Sur ordinateur, liez l'application de bureau à votre téléphone.</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -13,24 +13,19 @@ lang: "fr"
|
|||||||
|
|
||||||
## what
|
## what
|
||||||
|
|
||||||
L'extension de navigateur qui bloque publicités, traqueurs et scripts espions. Le geste le plus
|
L'extension qui bloque publicités, traqueurs et scripts espions. Le geste le plus rentable de tout ce guide : dix secondes, effet immédiat.
|
||||||
rentable, en dix secondes.
|
|
||||||
|
|
||||||
## why
|
## why
|
||||||
|
|
||||||
La publicité en ligne est le principal vecteur de surveillance et de profilage. La bloquer coupe
|
La publicité en ligne est le principal vecteur de surveillance et de profilage. La bloquer, c'est couper l'essentiel du pistage — et au passage tes pages chargent plus vite et ton forfait data respire.
|
||||||
l'essentiel du pistage, accélère les pages et réduit les risques de logiciels malveillants.
|
|
||||||
|
|
||||||
## who
|
## who
|
||||||
|
|
||||||
Tout le monde, sur Firefox comme sur les navigateurs Chromium.
|
Tout le monde, sur Firefox comme sur les navigateurs Chromium. Si tu ne fais qu'un seul geste aujourd'hui, fais celui-là.
|
||||||
|
|
||||||
## install
|
## install
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>
|
<li>Ouvre le magasin d'extensions de ton navigateur et cherche « uBlock Origin » (l'original, par Raymond Hill).</li>
|
||||||
Ouvrez le magasin d'extensions de votre navigateur et cherchez « uBlock Origin » (l'original,
|
<li>Installe-le. Il travaille aussitôt, sans aucun réglage.</li>
|
||||||
par Raymond Hill).
|
|
||||||
</li>
|
|
||||||
<li>Installez-le. Il fonctionne aussitôt, sans réglage nécessaire.</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
+18
-4
@@ -46,6 +46,17 @@ le contrat de validation (ADR-001).
|
|||||||
- Exemples et localisation ancrés territoire — jamais de Lorem ipsum
|
- Exemples et localisation ancrés territoire — jamais de Lorem ipsum
|
||||||
hexagonal (doctrine §4.2).
|
hexagonal (doctrine §4.2).
|
||||||
|
|
||||||
|
### Fiches de référence pour le ton
|
||||||
|
|
||||||
|
Six fiches sont réécrites au registre OKI et servent de modèle : `signal`,
|
||||||
|
`protonmail`, `brave`, `bitwarden`, `ublock`, `quad9`. Avant d'écrire ou de
|
||||||
|
réviser une fiche, relis-en deux. Ce qui change par rapport au ton d'origine :
|
||||||
|
tutoiement systématique, le geste avant la technique (« dix secondes, effet
|
||||||
|
immédiat »), l'enjeu politique dit sans détour (« ta correspondance — pas une
|
||||||
|
matière première publicitaire »), et un ancrage collectif (« commence par ton
|
||||||
|
groupe de famille »). Le script de migration ne régénère jamais une fiche
|
||||||
|
existante (`--force` pour tout écraser) : les réécritures sont protégées.
|
||||||
|
|
||||||
## Créole guadeloupéen (gcf)
|
## Créole guadeloupéen (gcf)
|
||||||
|
|
||||||
- Les **micro-textes de l'interface** (boutons, filtres, états vides, marqueur
|
- Les **micro-textes de l'interface** (boutons, filtres, états vides, marqueur
|
||||||
@@ -55,10 +66,13 @@ le contrat de validation (ADR-001).
|
|||||||
- ⚠️ **Relecture par un locuteur natif requise** : les chaînes gcf actuelles
|
- ⚠️ **Relecture par un locuteur natif requise** : les chaînes gcf actuelles
|
||||||
sont un premier jet à valider avant mise en production. La graphie de
|
sont un premier jet à valider avant mise en production. La graphie de
|
||||||
référence à trancher (GEREC-F ou autre) doit être notée ici une fois choisie.
|
référence à trancher (GEREC-F ou autre) doit être notée ici une fois choisie.
|
||||||
- La traduction intégrale des fiches est un chantier éditorial séparé :
|
- Chaque fiche peut avoir sa **jumelle créole** : `content/outils/gcf/<slug>.md`
|
||||||
le format prévu est un fichier jumeau `content/outils/gcf/<slug>.md` avec
|
(frontmatter minimal : `name`, `slug`, `lang: "gcf"` ; mêmes sections
|
||||||
`lang: "gcf"` (le chargeur l'ignorera tant que la locale de contenu n'est
|
`## what/why/who/install`). Elle s'affiche automatiquement quand la personne
|
||||||
pas activée).
|
choisit Kréyòl, avec un bandeau « tradiksyon an chantyé » ; sans jumelle,
|
||||||
|
la version française s'affiche avec un bandeau « póko tradui ». Six jumelles
|
||||||
|
pilotes existent (mêmes slugs que les fiches de référence ci-dessus) —
|
||||||
|
**premier jet non relu**, à valider avant d'en faire des modèles.
|
||||||
- Termes fixés : « zouti » (outil), « Ou pa manké ayen » (fin de liste),
|
- Termes fixés : « zouti » (outil), « Ou pa manké ayen » (fin de liste),
|
||||||
« Chwazi zouti ou » (héros), niveaux « Débitan / Entèmédia / Avansé ».
|
« Chwazi zouti ou » (héros), niveaux « Débitan / Entèmédia / Avansé ».
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Déployer lage-chat-control sur un VPS
|
||||||
|
|
||||||
|
Le site est **100 % statique** (ADR-001) : le déploiement consiste à poser le
|
||||||
|
contenu du dossier `frontend/build/` derrière un serveur web. Pas de base de
|
||||||
|
données, pas de service Node à maintenir, pas de tâche cron. Un miroir du site
|
||||||
|
= une copie du dossier.
|
||||||
|
|
||||||
|
## 0. Prérequis
|
||||||
|
|
||||||
|
- Un VPS (1 vCPU / 512 Mo suffisent très largement) sous Debian/Ubuntu
|
||||||
|
- Un nom de domaine pointant sur le VPS (enregistrement A/AAAA)
|
||||||
|
- En local : Node ≥ 22.12 et pnpm (le build se fait **sur ta machine**,
|
||||||
|
le VPS n'a besoin ni de Node ni de pnpm)
|
||||||
|
|
||||||
|
## 1. Construire le site
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://labola.o-k-i.net/cyber-mawonaj/lage-chat-control.git
|
||||||
|
cd lage-chat-control/frontend
|
||||||
|
pnpm install
|
||||||
|
pnpm build # produit frontend/build/ (~60 pages statiques)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Option A — Caddy (recommandé : TLS automatique)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# sur le VPS
|
||||||
|
sudo apt install caddy
|
||||||
|
sudo mkdir -p /srv/lage-chat-control
|
||||||
|
```
|
||||||
|
|
||||||
|
`/etc/caddy/Caddyfile` :
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
exemple.o-k-i.net {
|
||||||
|
root * /srv/lage-chat-control
|
||||||
|
file_server
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# SvelteKit adapter-static : pages pré-générées + fallback
|
||||||
|
try_files {path} {path}.html {path}/index.html /404.html
|
||||||
|
|
||||||
|
header {
|
||||||
|
# le service worker doit pouvoir se mettre à jour
|
||||||
|
Cache-Control "no-cache" /service-worker.js
|
||||||
|
# assets immuables (noms hachés)
|
||||||
|
Cache-Control "public, max-age=31536000, immutable" /_app/immutable/*
|
||||||
|
# en-têtes de sécurité
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
Referrer-Policy "no-referrer"
|
||||||
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||||
|
# CSP stricte : le site n'a aucune dépendance externe
|
||||||
|
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le TLS (Let's Encrypt) est automatique, rien d'autre à faire.
|
||||||
|
|
||||||
|
## 3. Option B — nginx
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install nginx certbot python3-certbot-nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
`/etc/nginx/sites-available/lage-chat-control` :
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
server_name exemple.o-k-i.net;
|
||||||
|
root /srv/lage-chat-control;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# pages pré-générées : /outils → outils.html, fallback 404
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri.html $uri/index.html /404.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /_app/immutable/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /service-worker.js {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'" always;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo ln -s /etc/nginx/sites-available/lage-chat-control /etc/nginx/sites-enabled/
|
||||||
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
|
sudo certbot --nginx -d exemple.o-k-i.net # TLS
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Publier (et republier)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# depuis ta machine, après chaque `pnpm build`
|
||||||
|
rsync -avz --delete frontend/build/ vps:/srv/lage-chat-control/
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est l'intégralité du « pipeline de déploiement ». Pour automatiser :
|
||||||
|
un runner Forgejo Actions sur labola peut exécuter build + rsync à chaque
|
||||||
|
push sur `main` (à ajouter quand le besoin se présente, pas avant).
|
||||||
|
|
||||||
|
## 5. Option C — YunoHost
|
||||||
|
|
||||||
|
Le site s'installe avec l'app **`my_webapp`** (site statique) :
|
||||||
|
|
||||||
|
1. Installe `my_webapp` depuis le catalogue, choisis le domaine/chemin.
|
||||||
|
2. Copie le contenu de `frontend/build/` dans le dossier `www/` de l'app
|
||||||
|
(SFTP ou rsync avec l'utilisateur dédié créé par l'app).
|
||||||
|
3. Dans la config nginx avancée de l'app, ajoute le bloc `try_files` ci-dessus.
|
||||||
|
|
||||||
|
YunoHost gère TLS, HSTS et le renouvellement des certificats.
|
||||||
|
|
||||||
|
## 6. Option D — Docker
|
||||||
|
|
||||||
|
Le `Dockerfile` et `docker/nginx.conf` du dépôt (hérités du site Astro)
|
||||||
|
servent déjà un site statique durci ; adapter le chemin de copie vers
|
||||||
|
`frontend/build/` si tu veux cette voie. Utile pour l'infra OKI si tout
|
||||||
|
le reste tourne déjà en conteneurs — sinon les options A/B sont plus simples.
|
||||||
|
|
||||||
|
## 7. Vérifier après mise en ligne
|
||||||
|
|
||||||
|
- [ ] `curl -sI https://exemple.o-k-i.net | grep -i content-security` → CSP présente
|
||||||
|
- [ ] La page `/outils` répond (et `/outils?cat=messagerie` filtre)
|
||||||
|
- [ ] DevTools > Application > Service worker : enregistré ; passer hors-ligne
|
||||||
|
et recharger une fiche déjà visitée → elle s'affiche
|
||||||
|
- [ ] observatory.mozilla.org : note A/A+ attendue avec les en-têtes ci-dessus
|
||||||
|
- [ ] Aucune requête sortante vers un domaine tiers (onglet Réseau)
|
||||||
@@ -10,14 +10,17 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"migrate": "node scripts/migrate-tools.mjs"
|
"migrate": "node scripts/migrate-tools.mjs",
|
||||||
|
"audit:a11y": "node scripts/audit-a11y.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12"
|
"node": ">=22.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@axe-core/playwright": "^4.12.1",
|
||||||
"@fontsource/atkinson-hyperlegible": "^5.2.8",
|
"@fontsource/atkinson-hyperlegible": "^5.2.8",
|
||||||
"@fontsource/fraunces": "^5.2.9",
|
"@fontsource/fraunces": "^5.2.9",
|
||||||
|
"@playwright/test": "1.61.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.0",
|
"@sveltejs/adapter-static": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
|
|||||||
Generated
+57
@@ -8,12 +8,18 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@axe-core/playwright':
|
||||||
|
specifier: ^4.12.1
|
||||||
|
version: 4.12.1(playwright-core@1.61.1)
|
||||||
'@fontsource/atkinson-hyperlegible':
|
'@fontsource/atkinson-hyperlegible':
|
||||||
specifier: ^5.2.8
|
specifier: ^5.2.8
|
||||||
version: 5.2.8
|
version: 5.2.8
|
||||||
'@fontsource/fraunces':
|
'@fontsource/fraunces':
|
||||||
specifier: ^5.2.9
|
specifier: ^5.2.9
|
||||||
version: 5.2.9
|
version: 5.2.9
|
||||||
|
'@playwright/test':
|
||||||
|
specifier: 1.61.1
|
||||||
|
version: 1.61.1
|
||||||
'@sveltejs/adapter-static':
|
'@sveltejs/adapter-static':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.10(@sveltejs/kit@2.69.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.4)(vite@7.3.6(@types/node@26.1.1)))(svelte@5.56.4)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.1)))
|
version: 3.0.10(@sveltejs/kit@2.69.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.4)(vite@7.3.6(@types/node@26.1.1)))(svelte@5.56.4)(typescript@5.9.3)(vite@7.3.6(@types/node@26.1.1)))
|
||||||
@@ -44,6 +50,11 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@axe-core/playwright@4.12.1':
|
||||||
|
resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==}
|
||||||
|
peerDependencies:
|
||||||
|
playwright-core: '>= 1.0.0'
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.28.1':
|
'@esbuild/aix-ppc64@0.28.1':
|
||||||
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -222,6 +233,11 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@playwright/test@1.61.1':
|
||||||
|
resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
@@ -432,6 +448,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
|
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
axe-core@4.12.1:
|
||||||
|
resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
axobject-query@4.1.0:
|
axobject-query@4.1.0:
|
||||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -480,6 +500,11 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -527,6 +552,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
playwright-core@1.61.1:
|
||||||
|
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.61.1:
|
||||||
|
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
postcss@8.5.16:
|
postcss@8.5.16:
|
||||||
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
|
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@@ -636,6 +671,11 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@axe-core/playwright@4.12.1(playwright-core@1.61.1)':
|
||||||
|
dependencies:
|
||||||
|
axe-core: 4.12.1
|
||||||
|
playwright-core: 1.61.1
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.28.1':
|
'@esbuild/aix-ppc64@0.28.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -737,6 +777,10 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@playwright/test@1.61.1':
|
||||||
|
dependencies:
|
||||||
|
playwright: 1.61.1
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.62.2':
|
'@rollup/rollup-android-arm-eabi@4.62.2':
|
||||||
@@ -877,6 +921,8 @@ snapshots:
|
|||||||
|
|
||||||
aria-query@5.3.1: {}
|
aria-query@5.3.1: {}
|
||||||
|
|
||||||
|
axe-core@4.12.1: {}
|
||||||
|
|
||||||
axobject-query@4.1.0: {}
|
axobject-query@4.1.0: {}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
@@ -930,6 +976,9 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.5
|
picomatch: 4.0.5
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -959,6 +1008,14 @@ snapshots:
|
|||||||
|
|
||||||
picomatch@4.0.5: {}
|
picomatch@4.0.5: {}
|
||||||
|
|
||||||
|
playwright-core@1.61.1: {}
|
||||||
|
|
||||||
|
playwright@1.61.1:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.61.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
|
||||||
postcss@8.5.16:
|
postcss@8.5.16:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.15
|
nanoid: 3.3.15
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Audit axe-core des pages clés servies sur localhost:4173
|
||||||
|
import { chromium } from '@playwright/test'
|
||||||
|
import { AxeBuilder } from '@axe-core/playwright'
|
||||||
|
|
||||||
|
const PAGES = ['/', '/outils', '/outils/signal', '/outils/tor', '/contribuer']
|
||||||
|
const browser = await chromium.launch()
|
||||||
|
const context = await browser.newContext()
|
||||||
|
const page = await context.newPage()
|
||||||
|
let total = 0
|
||||||
|
|
||||||
|
for (const path of PAGES) {
|
||||||
|
await page.goto(`http://localhost:4173${path}`, { waitUntil: 'networkidle' })
|
||||||
|
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag22aa']).analyze()
|
||||||
|
total += results.violations.length
|
||||||
|
console.log(`\n═══ ${path} — ${results.violations.length} violation(s)`)
|
||||||
|
for (const v of results.violations) {
|
||||||
|
console.log(` [${v.impact}] ${v.id}: ${v.help}`)
|
||||||
|
for (const n of v.nodes.slice(0, 3)) console.log(` → ${n.target.join(' ')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
console.log(`\nTOTAL: ${total} violation(s)`)
|
||||||
|
process.exit(total > 0 ? 1 : 0)
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
* Idempotent : relançable, écrase la sortie. Usage : pnpm migrate
|
* Idempotent : relançable, écrase la sortie. Usage : pnpm migrate
|
||||||
*/
|
*/
|
||||||
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'
|
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ function parseFrontmatter(raw) {
|
|||||||
if (i === -1) continue
|
if (i === -1) continue
|
||||||
const key = line.slice(0, i).trim()
|
const key = line.slice(0, i).trim()
|
||||||
let value = line.slice(i + 1).trim()
|
let value = line.slice(i + 1).trim()
|
||||||
if (/^'.*'$/.test(value)) value = value.slice(1, -1)
|
if (/^'.*'$/.test(value) || /^".*"$/.test(value)) value = value.slice(1, -1)
|
||||||
meta[key] = /^\d+$/.test(value) ? Number(value) : value
|
meta[key] = /^\d+$/.test(value) ? Number(value) : value
|
||||||
}
|
}
|
||||||
return [meta, raw.slice(m[0].length)]
|
return [meta, raw.slice(m[0].length)]
|
||||||
@@ -105,6 +106,12 @@ for (const file of files) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen.add(slug)
|
seen.add(slug)
|
||||||
|
// ne jamais écraser une fiche retravaillée à la main (--force pour tout régénérer)
|
||||||
|
const outPath = path.join(OUT, 'outils', `${slug}.md`)
|
||||||
|
if (existsSync(outPath) && !process.argv.includes('--force')) {
|
||||||
|
written++
|
||||||
|
continue
|
||||||
|
}
|
||||||
const dir = dirById.get(slug)
|
const dir = dirById.get(slug)
|
||||||
const nameOverride = slot(block, 'name')
|
const nameOverride = slot(block, 'name')
|
||||||
|
|
||||||
@@ -135,7 +142,7 @@ for (const file of files) {
|
|||||||
.map(([key, html]) => `## ${key}\n\n${html}`)
|
.map(([key, html]) => `## ${key}\n\n${html}`)
|
||||||
.join('\n\n')
|
.join('\n\n')
|
||||||
|
|
||||||
await writeFile(path.join(OUT, 'outils', `${slug}.md`), `---\n${front}\n---\n\n${sections}\n`)
|
await writeFile(outPath, `---\n${front}\n---\n\n${sections}\n`)
|
||||||
written++
|
written++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { browser } from '$app/environment'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* « Jès fèt » — les outils que la personne a adoptés.
|
||||||
|
* Effet Zeigarnik utilisé positivement (doctrine §1.2) : une progression
|
||||||
|
* choisie, pas un streak. Stocké uniquement sur l'appareil, jamais envoyé
|
||||||
|
* nulle part (pas de compte, pas de télémétrie — doctrine §3.3 : aucune
|
||||||
|
* métrique anxiogène ; le compteur n'apparaît qu'après le premier geste).
|
||||||
|
*/
|
||||||
|
const STORAGE_KEY = 'oki-jes'
|
||||||
|
|
||||||
|
function initial(): string[] {
|
||||||
|
if (!browser) return []
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY)
|
||||||
|
return raw ? (JSON.parse(raw) as string[]) : []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = $state({ slugs: initial() })
|
||||||
|
|
||||||
|
function persist() {
|
||||||
|
if (browser) localStorage.setItem(STORAGE_KEY, JSON.stringify(state.slugs))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAdopted(slug: string): boolean {
|
||||||
|
return state.slugs.includes(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function adoptedCount(): number {
|
||||||
|
return state.slugs.length
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleAdopted(slug: string) {
|
||||||
|
state.slugs = isAdopted(slug) ? state.slugs.filter((s) => s !== slug) : [...state.slugs, slug]
|
||||||
|
persist()
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { isAdopted, toggleAdopted } from '$lib/adopted.svelte'
|
||||||
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
|
let { slug }: { slug: string } = $props()
|
||||||
|
let adopted = $derived(isAdopted(slug))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Amélioration progressive : sans JS le bouton n'apparaît pas du tout
|
||||||
|
(rendu identique, aucune fonction cassée). -->
|
||||||
|
<button type="button" class:adopted aria-pressed={adopted} onclick={() => toggleAdopted(slug)}>
|
||||||
|
{adopted ? t('adopted.marked') : t('adopted.mark')}
|
||||||
|
<span class="visually-hidden">{t('adopted.srHint')}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
border: 2px solid var(--oki-vert);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--oki-ink);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.adopted {
|
||||||
|
background: var(--oki-vert);
|
||||||
|
color: oklch(98% 0 0);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { browser } from '$app/environment'
|
||||||
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
|
// Technologie périphérique (doctrine §3.5) : l'état réseau est une
|
||||||
|
// information non urgente — un badge discret, jamais une interruption.
|
||||||
|
let offline = $state(browser ? !navigator.onLine : false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window ononline={() => (offline = false)} onoffline={() => (offline = true)} />
|
||||||
|
|
||||||
|
{#if offline}
|
||||||
|
<p class="offline" role="status">
|
||||||
|
<strong>{t('offline.badge')}</strong> — {t('offline.detail')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.offline {
|
||||||
|
font-size: var(--text-0);
|
||||||
|
color: var(--oki-ink-2);
|
||||||
|
background: var(--oki-surface-2);
|
||||||
|
border-block: 1px solid var(--oki-line);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--accent-ink);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,8 +7,9 @@
|
|||||||
// raccourci « / » desktop (doctrine §2.2 — le clavier est roi)
|
// raccourci « / » desktop (doctrine §2.2 — le clavier est roi)
|
||||||
function onkeydown(e: KeyboardEvent) {
|
function onkeydown(e: KeyboardEvent) {
|
||||||
if (e.key === '/' && document.activeElement !== input) {
|
if (e.key === '/' && document.activeElement !== input) {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target
|
||||||
if (target.closest('input, textarea, [contenteditable]')) return
|
if (target instanceof HTMLElement && target.closest('input, textarea, [contenteditable]'))
|
||||||
|
return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
input.focus()
|
input.focus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation'
|
||||||
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
|
// Le clavier est roi sur desktop (doctrine §2.2.3) : raccourcis découvrables
|
||||||
|
// via « ? », jamais porteurs d'information exclusive (tout est aussi dans la nav).
|
||||||
|
let dialog: HTMLDialogElement
|
||||||
|
|
||||||
|
const KEYS = [
|
||||||
|
{ key: '/', label: 'shortcuts.search' },
|
||||||
|
{ key: 'h', label: 'shortcuts.home' },
|
||||||
|
{ key: 'a', label: 'shortcuts.directory' },
|
||||||
|
{ key: '?', label: 'shortcuts.help' },
|
||||||
|
{ key: 'Échap', label: 'shortcuts.close' }
|
||||||
|
]
|
||||||
|
|
||||||
|
function onkeydown(e: KeyboardEvent) {
|
||||||
|
if (e.ctrlKey || e.metaKey || e.altKey) return
|
||||||
|
const target = e.target
|
||||||
|
if (target instanceof HTMLElement && target.closest('input, textarea, select, [contenteditable]'))
|
||||||
|
return
|
||||||
|
if (e.key === '?') {
|
||||||
|
e.preventDefault()
|
||||||
|
dialog.open ? dialog.close() : dialog.showModal()
|
||||||
|
} else if (e.key === 'h') {
|
||||||
|
goto('/')
|
||||||
|
} else if (e.key === 'a') {
|
||||||
|
goto('/outils')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window {onkeydown} />
|
||||||
|
|
||||||
|
<!-- <dialog> natif : focus piégé, Échap géré par le navigateur, zéro ARIA maison -->
|
||||||
|
<dialog bind:this={dialog}>
|
||||||
|
<h2>{t('shortcuts.title')}</h2>
|
||||||
|
<dl>
|
||||||
|
{#each KEYS as { key, label } (key)}
|
||||||
|
<div>
|
||||||
|
<dt><kbd>{key}</kbd></dt>
|
||||||
|
<dd>{t(label)}</dd>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</dl>
|
||||||
|
<form method="dialog">
|
||||||
|
<button>{t('shortcuts.close')}</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
dialog {
|
||||||
|
border: 1px solid var(--oki-line);
|
||||||
|
border-top: 4px solid var(--oki-jaune);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
background: var(--oki-surface);
|
||||||
|
color: var(--oki-ink);
|
||||||
|
padding: var(--space-4);
|
||||||
|
max-inline-size: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
background: oklch(0% 0 0 / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--text-2);
|
||||||
|
margin-block-end: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
dl {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-block-end: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
dl div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
min-inline-size: 2rem;
|
||||||
|
padding: 0.15em 0.5em;
|
||||||
|
border: 1px solid var(--oki-line);
|
||||||
|
border-bottom-width: 3px;
|
||||||
|
border-radius: var(--radius-1);
|
||||||
|
background: var(--oki-surface-2);
|
||||||
|
font-size: var(--text-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import DifficultyBadge from './DifficultyBadge.svelte'
|
import DifficultyBadge from './DifficultyBadge.svelte'
|
||||||
|
import { isAdopted } from '$lib/adopted.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
interface CardTool {
|
interface CardTool {
|
||||||
@@ -22,6 +23,13 @@
|
|||||||
{tool.monogram ?? tool.name.slice(0, 2)}
|
{tool.monogram ?? tool.name.slice(0, 2)}
|
||||||
</span>
|
</span>
|
||||||
<h3><a href="/outils/{tool.slug}">{tool.name}</a></h3>
|
<h3><a href="/outils/{tool.slug}">{tool.name}</a></h3>
|
||||||
|
{#if isAdopted(tool.slug)}
|
||||||
|
<!-- information périphérique (doctrine §3.5) : discret, jamais bloquant -->
|
||||||
|
<span class="adopted" title={t('adopted.marked')}>
|
||||||
|
<span aria-hidden="true">✓</span>
|
||||||
|
<span class="visually-hidden">{t('adopted.marked')}</span>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="excerpt"><span class="visually-hidden">{t('tool.why')} : </span>{tool.excerpt}</p>
|
<p class="excerpt"><span class="visually-hidden">{t('tool.why')} : </span>{tool.excerpt}</p>
|
||||||
@@ -51,14 +59,17 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* la couleur de marque en anneau, jamais en fond de texte :
|
||||||
|
contraste AA garanti quel que soit l'outil (doctrine §4.3) */
|
||||||
.monogram {
|
.monogram {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
inline-size: 2.5rem;
|
inline-size: 2.5rem;
|
||||||
block-size: 2.5rem;
|
block-size: 2.5rem;
|
||||||
border-radius: var(--radius-2);
|
border-radius: var(--radius-2);
|
||||||
background: var(--tool-color, var(--oki-surface-2));
|
background: var(--oki-surface-2);
|
||||||
color: white;
|
border: 3px solid var(--tool-color, var(--oki-line));
|
||||||
|
color: var(--oki-ink);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: var(--text-0);
|
font-size: var(--text-0);
|
||||||
}
|
}
|
||||||
@@ -83,6 +94,18 @@
|
|||||||
border-radius: var(--radius-2);
|
border-radius: var(--radius-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.adopted {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
inline-size: 1.4rem;
|
||||||
|
block-size: 1.4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--oki-vert);
|
||||||
|
color: oklch(98% 0 0);
|
||||||
|
font-size: var(--text-0);
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.excerpt {
|
.excerpt {
|
||||||
color: var(--oki-ink-2);
|
color: var(--oki-ink-2);
|
||||||
font-size: var(--text-0);
|
font-size: var(--text-0);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Chargeur de contenu — s'exécute uniquement au build (site prérendu,
|
* Chargeur de contenu — s'exécute uniquement au build (site prérendu,
|
||||||
* ADR-001) : ni fs ni marked ne sont expédiés au client.
|
* ADR-001) : ni fs ni marked ne sont expédiés au client.
|
||||||
*/
|
*/
|
||||||
import { readFileSync, readdirSync } from 'node:fs'
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
|
|
||||||
@@ -96,7 +96,30 @@ function loadTool(file: string): Tool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache: { tools: Tool[]; categories: Category[] } | null = null
|
export interface GcfTool {
|
||||||
|
sections: ToolSections
|
||||||
|
excerpt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fiches jumelles créoles : content/outils/gcf/<slug>.md (CONTENT_GUIDE). */
|
||||||
|
function loadGcf(): Record<string, GcfTool> {
|
||||||
|
const dir = path.join(CONTENT, 'outils', 'gcf')
|
||||||
|
if (!existsSync(dir)) return {}
|
||||||
|
const out: Record<string, GcfTool> = {}
|
||||||
|
for (const file of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
||||||
|
const raw = readFileSync(path.join(dir, file), 'utf8')
|
||||||
|
const [meta, body] = parseFrontmatter(raw)
|
||||||
|
const sections = parseSections(body)
|
||||||
|
const why = stripHtml(sections.why ?? sections.what ?? '')
|
||||||
|
out[(meta.slug as string) ?? file.replace(/\.md$/, '')] = {
|
||||||
|
excerpt: why.length > 160 ? `${why.slice(0, 157).trimEnd()}…` : why,
|
||||||
|
sections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
let cache: { tools: Tool[]; categories: Category[]; gcf: Record<string, GcfTool> } | null = null
|
||||||
|
|
||||||
export function loadContent() {
|
export function loadContent() {
|
||||||
if (cache) return cache
|
if (cache) return cache
|
||||||
@@ -112,6 +135,6 @@ export function loadContent() {
|
|||||||
(order.get(a.category) ?? 99) - (order.get(b.category) ?? 99) ||
|
(order.get(a.category) ?? 99) - (order.get(b.category) ?? 99) ||
|
||||||
a.name.localeCompare(b.name, 'fr')
|
a.name.localeCompare(b.name, 'fr')
|
||||||
)
|
)
|
||||||
cache = { tools, categories }
|
cache = { tools, categories, gcf: loadGcf() }
|
||||||
return cache
|
return cache
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,21 @@
|
|||||||
"subtitle": "Reprends le contrôle de ton internet : des outils libres, expliqués simplement, du premier geste au serveur auto-hébergé.",
|
"subtitle": "Reprends le contrôle de ton internet : des outils libres, expliqués simplement, du premier geste au serveur auto-hébergé.",
|
||||||
"cta": "Voir l'annuaire"
|
"cta": "Voir l'annuaire"
|
||||||
},
|
},
|
||||||
|
"intro": {
|
||||||
|
"title": "Pourquoi ce site ?",
|
||||||
|
"p1": "L'Union européenne veut imposer le scan de toutes tes conversations privées : c'est le projet « Chat Control ». Et même quand la loi recule, l'infrastructure de surveillance, elle, continue d'avancer.",
|
||||||
|
"p2": "Ce site est un guide de terrain. Des outils libres, vérifiés, expliqués pas à pas — pour sortir des plateformes qui te surveillent et reprendre la main : toi, ta famille, ton lakou.",
|
||||||
|
"p3": "Pas besoin d'être technicien·ne. Commence par les fondations, avance à ton rythme, le reste vient après."
|
||||||
|
},
|
||||||
|
"sommaire": {
|
||||||
|
"title": "Le chimen",
|
||||||
|
"sub": "Trois étapes, du premier geste à l'autodéfense complète. Chaque catégorie est une porte d'entrée."
|
||||||
|
},
|
||||||
|
"arc": {
|
||||||
|
"fondations": "Les fondations",
|
||||||
|
"controle": "Reprendre le contrôle",
|
||||||
|
"avance": "Autodéfense avancée"
|
||||||
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"title": "Annuaire des outils",
|
"title": "Annuaire des outils",
|
||||||
"intro": "Chaque outil est libre, vérifié, et classé par difficulté. Commence par le vert.",
|
"intro": "Chaque outil est libre, vérifié, et classé par difficulté. Commence par le vert.",
|
||||||
@@ -40,7 +55,16 @@
|
|||||||
"sourceCode": "Code source",
|
"sourceCode": "Code source",
|
||||||
"license": "Licence",
|
"license": "Licence",
|
||||||
"category": "Catégorie",
|
"category": "Catégorie",
|
||||||
"backToDirectory": "Retour à l'annuaire"
|
"backToDirectory": "Retour à l'annuaire",
|
||||||
|
"notTranslated": "Fich-lasa póko tradui an kréyòl — la voici en français.",
|
||||||
|
"draftTranslation": "Tradiksyon kréyòl an chantyé — un doute ? La version française fait foi."
|
||||||
|
},
|
||||||
|
"adopted": {
|
||||||
|
"mark": "Je m'en sers",
|
||||||
|
"marked": "Je m'en sers ✓",
|
||||||
|
"progressBefore": "Ou ja fè",
|
||||||
|
"progressAfter": "gestes — chaque outil adopté est une reprise de terrain.",
|
||||||
|
"srHint": "Enregistré uniquement sur cet appareil, jamais envoyé nulle part."
|
||||||
},
|
},
|
||||||
"difficulty": {
|
"difficulty": {
|
||||||
"beginner": "Débutant",
|
"beginner": "Débutant",
|
||||||
@@ -56,6 +80,34 @@
|
|||||||
"switchLabel": "Langue",
|
"switchLabel": "Langue",
|
||||||
"switchTo": "Passer en"
|
"switchTo": "Passer en"
|
||||||
},
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"title": "Raccourcis clavier",
|
||||||
|
"search": "Rechercher",
|
||||||
|
"help": "Afficher cette aide",
|
||||||
|
"home": "Aller à l'accueil",
|
||||||
|
"directory": "Aller à l'annuaire",
|
||||||
|
"close": "Fermer"
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"badge": "Hors-ligne",
|
||||||
|
"detail": "Tu lis la copie locale — les fiches déjà visitées restent disponibles."
|
||||||
|
},
|
||||||
|
"contrib": {
|
||||||
|
"title": "Pòté mannèv — Contribuer",
|
||||||
|
"intro": "Ce guide vit par celles et ceux qui s'en servent. Trois façons d'aider, de la plus simple à la plus complète.",
|
||||||
|
"suggest": "Proposer un outil",
|
||||||
|
"suggestBody": "Tu utilises un outil libre qui manque à l'annuaire ? Ouvre une demande avec le formulaire : nom, lien, pourquoi il compte. On s'occupe de la fiche.",
|
||||||
|
"suggestCta": "Ouvrir une demande",
|
||||||
|
"fix": "Corriger une fiche",
|
||||||
|
"fixBody": "Une étape d'installation qui a changé, un lien mort, une tournure plus claire ? Chaque fiche est un simple fichier texte dans le dossier content/outils/.",
|
||||||
|
"fixCta": "Parcourir les fiches",
|
||||||
|
"translate": "Traduire en kréyòl",
|
||||||
|
"translateBody": "Le créole est la langue du cœur de ce site. Les fiches attendent leur jumelle gcf — et les traductions existantes ont besoin de relecteurs et relectrices natifs.",
|
||||||
|
"translateCta": "Lire le guide de contenu",
|
||||||
|
"repo": "Tout se passe sur la forge",
|
||||||
|
"repoBody": "Le code, le contenu et les discussions vivent sur labola, la forge auto-hébergée d'OKI. Pas de compte Microsoft requis.",
|
||||||
|
"repoCta": "Voir le dépôt"
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"license": "Contenu et code libres (MIT). Zéro tracker, zéro CDN, zéro cookie.",
|
"license": "Contenu et code libres (MIT). Zéro tracker, zéro CDN, zéro cookie.",
|
||||||
"contribute": "Proposer un outil ou une correction"
|
"contribute": "Proposer un outil ou une correction"
|
||||||
|
|||||||
@@ -15,6 +15,21 @@
|
|||||||
"subtitle": "Pran kontwòl entènèt a-w : zouti lib, èspliké senp, dépi prèmyé jès jis sèwvè a-w.",
|
"subtitle": "Pran kontwòl entènèt a-w : zouti lib, èspliké senp, dépi prèmyé jès jis sèwvè a-w.",
|
||||||
"cta": "Gadé lis-la"
|
"cta": "Gadé lis-la"
|
||||||
},
|
},
|
||||||
|
"intro": {
|
||||||
|
"title": "Poukisa sit-lasa ?",
|
||||||
|
"p1": "Linyon éwopéyèn vlé enposé skannaj a tout konvèsasyon privé a-w : sé pwojé « Chat Control ». É menm lè lwa-la ka fè bak, enfrastrikti a survéyans-la ka kontinyé vansé.",
|
||||||
|
"p2": "Sit-lasa sé on gid a téren. Zouti lib, vérifyé, èspliké pa a pa — pou sòti an platfòm ki ka survéyé-w é répran lanmen : vou, fanmi a-w, lakou a-w.",
|
||||||
|
"p3": "Pa bizwen sav enfòwmatik. Koumansé èvè fondasyon-la, vansé a vitès a-w, rès-la ka vin apré."
|
||||||
|
},
|
||||||
|
"sommaire": {
|
||||||
|
"title": "Chimen-la",
|
||||||
|
"sub": "Twa létap, dépi prèmyé jès jis otodéfans konplé. Chak katégori sé on pòt."
|
||||||
|
},
|
||||||
|
"arc": {
|
||||||
|
"fondations": "Fondasyon-la",
|
||||||
|
"controle": "Répran kontwòl",
|
||||||
|
"avance": "Otodéfans avansé"
|
||||||
|
},
|
||||||
"directory": {
|
"directory": {
|
||||||
"title": "Lis a zouti",
|
"title": "Lis a zouti",
|
||||||
"intro": "Chak zouti lib, vérifyé, é rangé pa difikilté. Koumansé èvè vèw-la.",
|
"intro": "Chak zouti lib, vérifyé, é rangé pa difikilté. Koumansé èvè vèw-la.",
|
||||||
@@ -40,7 +55,16 @@
|
|||||||
"sourceCode": "Kòd sous",
|
"sourceCode": "Kòd sous",
|
||||||
"license": "Lisans",
|
"license": "Lisans",
|
||||||
"category": "Katégori",
|
"category": "Katégori",
|
||||||
"backToDirectory": "Viré an lis-la"
|
"backToDirectory": "Viré an lis-la",
|
||||||
|
"notTranslated": "Fich-lasa póko tradui an kréyòl — mi'y an fransé.",
|
||||||
|
"draftTranslation": "Tradiksyon kréyòl an chantyé — on dout ? Vèwsyon fransé-la ka fè fwa."
|
||||||
|
},
|
||||||
|
"adopted": {
|
||||||
|
"mark": "An ka sèvi épi'y",
|
||||||
|
"marked": "An ka sèvi épi'y ✓",
|
||||||
|
"progressBefore": "Ou ja fè",
|
||||||
|
"progressAfter": "jès — chak zouti adopté sé on moso téren ou répran.",
|
||||||
|
"srHint": "Anrèjistré asi apawèy-lasa sèlman, janmé voyé okenn koté."
|
||||||
},
|
},
|
||||||
"difficulty": {
|
"difficulty": {
|
||||||
"beginner": "Débitan",
|
"beginner": "Débitan",
|
||||||
@@ -56,6 +80,34 @@
|
|||||||
"switchLabel": "Lang",
|
"switchLabel": "Lang",
|
||||||
"switchTo": "Pasé an"
|
"switchTo": "Pasé an"
|
||||||
},
|
},
|
||||||
|
"shortcuts": {
|
||||||
|
"title": "Rakousi klavyé",
|
||||||
|
"search": "Chèché",
|
||||||
|
"help": "Montré èd-lasa",
|
||||||
|
"home": "Alé an lakou-la",
|
||||||
|
"directory": "Alé an lis-la",
|
||||||
|
"close": "Fèmé"
|
||||||
|
},
|
||||||
|
"offline": {
|
||||||
|
"badge": "Dékonèkté",
|
||||||
|
"detail": "Ou ka li kopi lokal-la — fich ou ja vizité ka rété la."
|
||||||
|
},
|
||||||
|
"contrib": {
|
||||||
|
"title": "Pòté mannèv",
|
||||||
|
"intro": "Gid-lasa ka viv gras a moun ki ka sèvi épi'y. Twa mannyè pou édé, dépi pi senp jis pi konplé.",
|
||||||
|
"suggest": "Pwopozé on zouti",
|
||||||
|
"suggestBody": "Ou ka sèvi épi on zouti lib ki manké an lis-la ? Louvè on dimann èvè fòwmilè-la : non, lyen, poukisa i konté. Nou ka okipé fich-la.",
|
||||||
|
"suggestCta": "Louvè on dimann",
|
||||||
|
"fix": "Korijé on fich",
|
||||||
|
"fixBody": "On létap ki chanjé, on lyen mò, on fraz pi klè ? Chak fich sé on senp fichyé tèks an dosyé content/outils/.",
|
||||||
|
"fixCta": "Gadé fich-la yo",
|
||||||
|
"translate": "Tradui an kréyòl",
|
||||||
|
"translateBody": "Kréyòl sé lang a kè a sit-lasa. Fich-la yo ka atann jimèl gcf a-yo — é tradiksyon ki ja la bizwen lektè natif.",
|
||||||
|
"translateCta": "Li gid a kontni-la",
|
||||||
|
"repo": "Tout ka pasé asi fòwj-la",
|
||||||
|
"repoBody": "Kòd, kontni é diskisyon ka viv asi labola, fòwj oto-ébèjé a OKI. Pa bizwen kont Microsoft.",
|
||||||
|
"repoCta": "Gadé dépo-la"
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"license": "Kontni é kòd lib (MIT). Pon trakè, pon CDN, pon cookie.",
|
"license": "Kontni é kòd lib (MIT). Pon trakè, pon CDN, pon cookie.",
|
||||||
"contribute": "Pwopozé on zouti oben on koreksyon"
|
"contribute": "Pwopozé on zouti oben on koreksyon"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/** Constantes du site — un seul endroit à changer si la forge bouge. */
|
||||||
|
export const REPO_URL = 'https://labola.o-k-i.net/cyber-mawonaj/lage-chat-control'
|
||||||
|
export const NEW_TOOL_ISSUE_URL = `${REPO_URL}/issues/new?template=.gitea/ISSUE_TEMPLATE/nouvo-zouti.yaml`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les trois arcs du chimen (sommaire) : progression Fondations →
|
||||||
|
* Reprendre le contrôle → Autodéfense avancée. Une catégorie appartient
|
||||||
|
* au premier arc dont `maxPart` couvre son `part`.
|
||||||
|
*/
|
||||||
|
export const ARCS = [
|
||||||
|
{ id: 'fondations', maxPart: 4, difficulty: 'beginner' },
|
||||||
|
{ id: 'controle', maxPart: 9, difficulty: 'intermediate' },
|
||||||
|
{ id: 'avance', maxPart: 99, difficulty: 'advanced' }
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type ArcId = (typeof ARCS)[number]['id']
|
||||||
|
|
||||||
|
export function arcOf(part: number): ArcId {
|
||||||
|
return (ARCS.find((a) => part <= a.maxPart) ?? ARCS[ARCS.length - 1]).id
|
||||||
|
}
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css'
|
import '../app.css'
|
||||||
|
import { onNavigate } from '$app/navigation'
|
||||||
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte'
|
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte'
|
||||||
|
import OfflineBadge from '$lib/components/OfflineBadge.svelte'
|
||||||
|
import ShortcutsDialog from '$lib/components/ShortcutsDialog.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
import { REPO_URL } from '$lib/site'
|
||||||
|
|
||||||
let { children } = $props()
|
let { children } = $props()
|
||||||
|
|
||||||
|
// View Transitions : la navigation glisse au lieu de sauter — amélioration
|
||||||
|
// progressive pure (navigateurs sans support : rien ne change), et
|
||||||
|
// désactivée si la personne préfère réduire les animations (doctrine §4.3).
|
||||||
|
onNavigate((navigation) => {
|
||||||
|
if (!document.startViewTransition) return
|
||||||
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
document.startViewTransition(async () => {
|
||||||
|
resolve()
|
||||||
|
await navigation.complete
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="skip-link" href="#main">{t('nav.skipToContent')}</a>
|
<a class="skip-link" href="#main">{t('nav.skipToContent')}</a>
|
||||||
@@ -17,17 +35,16 @@
|
|||||||
<nav aria-label={t('nav.home')}>
|
<nav aria-label={t('nav.home')}>
|
||||||
<ul class="cluster">
|
<ul class="cluster">
|
||||||
<li><a href="/outils">{t('nav.directory')}</a></li>
|
<li><a href="/outils">{t('nav.directory')}</a></li>
|
||||||
<li>
|
<li><a href="/contribuer">{t('nav.contribute')}</a></li>
|
||||||
<a href="https://github.com/exitchatcontrol/exitchatcontrol.org" rel="external">
|
|
||||||
{t('nav.contribute')}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<OfflineBadge />
|
||||||
|
<ShortcutsDialog />
|
||||||
|
|
||||||
<main id="main" class="center stack" style="--gap: var(--space-5)">
|
<main id="main" class="center stack" style="--gap: var(--space-5)">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
@@ -35,9 +52,7 @@
|
|||||||
<footer class="center">
|
<footer class="center">
|
||||||
<p>{t('footer.license')}</p>
|
<p>{t('footer.license')}</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/exitchatcontrol/exitchatcontrol.org" rel="external">
|
<a href={REPO_URL} rel="external">{t('footer.contribute')}</a>
|
||||||
{t('footer.contribute')}
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
import { loadContent } from '$lib/content/tools.server'
|
import { loadContent } from '$lib/content/tools.server'
|
||||||
|
import { ARCS, arcOf } from '$lib/site'
|
||||||
|
|
||||||
export function load() {
|
export function load() {
|
||||||
const { tools, categories } = loadContent()
|
const { tools, categories } = loadContent()
|
||||||
|
const countBy = new Map<string, number>()
|
||||||
|
for (const tool of tools) {
|
||||||
|
countBy.set(tool.category, (countBy.get(tool.category) ?? 0) + 1)
|
||||||
|
}
|
||||||
|
// le sommaire : catégories groupées par arc (Fondations → Contrôle → Avancé)
|
||||||
|
const arcs = ARCS.map((arc) => ({
|
||||||
|
id: arc.id,
|
||||||
|
difficulty: arc.difficulty,
|
||||||
|
categories: categories
|
||||||
|
.filter((c) => arcOf(c.part) === arc.id)
|
||||||
|
.map((c) => ({ ...c, count: countBy.get(c.slug) ?? 0 }))
|
||||||
|
}))
|
||||||
return {
|
return {
|
||||||
categories,
|
arcs,
|
||||||
toolCount: tools.length,
|
toolCount: tools.length,
|
||||||
// les premiers gestes : outils débutant, un par catégorie
|
// les premiers gestes : outils débutant mis en avant
|
||||||
starters: tools.filter((t) => t.difficulty === 'beginner').slice(0, 3)
|
starters: tools
|
||||||
|
.filter((t) => t.difficulty === 'beginner')
|
||||||
|
.slice(0, 3)
|
||||||
|
.map(({ sections: _sections, ...card }) => card)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||||
|
import { adoptedCount } from '$lib/adopted.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
let { data } = $props()
|
let { data } = $props()
|
||||||
|
// n'apparaît qu'après le premier geste : jamais de « 0/55 » culpabilisant
|
||||||
|
// (doctrine §3.3 — pas de métrique anxiogène)
|
||||||
|
let jes = $derived(adoptedCount())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -16,6 +20,52 @@
|
|||||||
<h1>{t('hero.title')}</h1>
|
<h1>{t('hero.title')}</h1>
|
||||||
<p class="sub">{t('hero.subtitle')}</p>
|
<p class="sub">{t('hero.subtitle')}</p>
|
||||||
<p><a class="cta" href="/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
<p><a class="cta" href="/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
||||||
|
{#if jes > 0}
|
||||||
|
<p class="jes" role="status">
|
||||||
|
<span class="jes-mark" aria-hidden="true">✓</span>
|
||||||
|
{t('adopted.progressBefore')}
|
||||||
|
<strong>{jes}</strong>
|
||||||
|
{t('adopted.progressAfter')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- L'intro publique : de quoi il s'agit, avant toute navigation.
|
||||||
|
Motif en F (doctrine §1.1) : l'essentiel au premier mot de chaque ¶. -->
|
||||||
|
<section class="intro stack prose">
|
||||||
|
<h2>{t('intro.title')}</h2>
|
||||||
|
<p>{t('intro.p1')}</p>
|
||||||
|
<p>{t('intro.p2')}</p>
|
||||||
|
<p>{t('intro.p3')}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Le sommaire-chimen : trois arcs, une progression lisible sans lecture
|
||||||
|
(reconnaissance, doctrine §1.2) — le rail reprend vert/jaune/rouge,
|
||||||
|
doublé par texte + numérotation (jamais couleur seule, §1.1). -->
|
||||||
|
<section class="stack" aria-labelledby="sommaire-title">
|
||||||
|
<h2 id="sommaire-title">{t('sommaire.title')}</h2>
|
||||||
|
<p class="sub2">{t('sommaire.sub')}</p>
|
||||||
|
<ol class="chimen">
|
||||||
|
{#each data.arcs as arc, i (arc.id)}
|
||||||
|
<li class="arc arc-{arc.difficulty}">
|
||||||
|
<h3>
|
||||||
|
<span class="arc-num" aria-hidden="true">{i + 1}</span>
|
||||||
|
{t(`arc.${arc.id}`)}
|
||||||
|
<span class="arc-diff">{t(`difficulty.${arc.difficulty}`)}</span>
|
||||||
|
</h3>
|
||||||
|
<ul class="cluster">
|
||||||
|
{#each arc.categories as cat (cat.slug)}
|
||||||
|
<li>
|
||||||
|
<a href="/outils?cat={cat.slug}">
|
||||||
|
{cat.title}
|
||||||
|
<span class="count">{cat.count}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="stack">
|
<section class="stack">
|
||||||
@@ -29,15 +79,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="stack">
|
|
||||||
<h2>{t('directory.filterCategory')}</h2>
|
|
||||||
<ul class="cluster cats">
|
|
||||||
{#each data.categories as cat (cat.slug)}
|
|
||||||
<li><a href="/outils?cat={cat.slug}">{cat.title}</a></li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.hero {
|
.hero {
|
||||||
padding-block: var(--space-5);
|
padding-block: var(--space-5);
|
||||||
@@ -53,6 +94,10 @@
|
|||||||
max-width: var(--measure);
|
max-width: var(--measure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sub2 {
|
||||||
|
color: var(--oki-ink-2);
|
||||||
|
}
|
||||||
|
|
||||||
/* une seule action primaire par écran (Von Restorff, doctrine §1.2) */
|
/* une seule action primaire par écran (Von Restorff, doctrine §1.2) */
|
||||||
.cta {
|
.cta {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -70,14 +115,116 @@
|
|||||||
filter: brightness(1.05);
|
filter: brightness(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cats {
|
.jes {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--oki-ink-2);
|
||||||
|
font-size: var(--text-0);
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jes-mark {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
inline-size: 1.4rem;
|
||||||
|
block-size: 1.4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--oki-vert);
|
||||||
|
color: oklch(98% 0 0);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
border-inline-start: 4px solid var(--oki-rouge);
|
||||||
|
padding-inline-start: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── le chimen ─────────────────────────────────────────────────── */
|
||||||
|
.chimen {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc {
|
||||||
|
--rail: var(--oki-line);
|
||||||
|
position: relative;
|
||||||
|
padding-inline-start: var(--space-5);
|
||||||
|
padding-block-end: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* le rail vertical, coloré par arc */
|
||||||
|
.arc::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset-block: 0.4rem 0;
|
||||||
|
inset-inline-start: calc(1.1rem - 2px);
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--rail);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc:last-child::before {
|
||||||
|
inset-block-end: auto;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc-beginner {
|
||||||
|
--rail: var(--diff-beginner);
|
||||||
|
}
|
||||||
|
.arc-intermediate {
|
||||||
|
--rail: var(--diff-intermediate);
|
||||||
|
}
|
||||||
|
.arc-advanced {
|
||||||
|
--rail: var(--diff-advanced);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-block-end: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc-num {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
inline-size: 2.2rem;
|
||||||
|
block-size: 2.2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--rail);
|
||||||
|
color: var(--oki-noir);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: var(--text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc-advanced .arc-num {
|
||||||
|
color: oklch(98% 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc-diff {
|
||||||
|
font-family: var(--font-text);
|
||||||
|
font-size: var(--text-0);
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--oki-ink-2);
|
||||||
|
border: 1px solid var(--oki-line);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
padding: 0.1em 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arc ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cats a {
|
.arc a {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
min-height: var(--tap-target);
|
min-height: var(--tap-target);
|
||||||
padding-inline: var(--space-3);
|
padding-inline: var(--space-3);
|
||||||
border: 1px solid var(--oki-line);
|
border: 1px solid var(--oki-line);
|
||||||
@@ -86,7 +233,15 @@
|
|||||||
color: var(--oki-ink);
|
color: var(--oki-ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cats a:hover {
|
.arc a:hover {
|
||||||
border-color: var(--oki-jaune);
|
border-color: var(--rail);
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: var(--text-0);
|
||||||
|
color: var(--oki-ink-2);
|
||||||
|
background: var(--oki-surface-2);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
padding: 0.05em 0.5em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
import { NEW_TOOL_ISSUE_URL, REPO_URL } from '$lib/site'
|
||||||
|
|
||||||
|
// de la plus simple à la plus complète (divulgation progressive, §1.3)
|
||||||
|
const WAYS = [
|
||||||
|
{ key: 'suggest', href: NEW_TOOL_ISSUE_URL },
|
||||||
|
{ key: 'fix', href: `${REPO_URL}/src/branch/main/content/outils` },
|
||||||
|
{ key: 'translate', href: `${REPO_URL}/src/branch/main/docs/CONTENT_GUIDE.md` }
|
||||||
|
] as const
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{t('contrib.title')} — {t('site.name')}</title>
|
||||||
|
<meta name="description" content={t('contrib.intro')} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<header class="stack">
|
||||||
|
<h1>{t('contrib.title')}</h1>
|
||||||
|
<p class="prose">{t('contrib.intro')}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<ol class="ways stack" style="--gap: var(--space-4)">
|
||||||
|
{#each WAYS as way, i (way.key)}
|
||||||
|
<li class="way stack">
|
||||||
|
<h2><span class="num" aria-hidden="true">{i + 1}</span>{t(`contrib.${way.key}`)}</h2>
|
||||||
|
<p class="prose">{t(`contrib.${way.key}Body`)}</p>
|
||||||
|
<p><a class="action" href={way.href} rel="external">{t(`contrib.${way.key}Cta`)} ↗</a></p>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<section class="repo stack">
|
||||||
|
<h2>{t('contrib.repo')}</h2>
|
||||||
|
<p class="prose">{t('contrib.repoBody')}</p>
|
||||||
|
<p><a class="action" href={REPO_URL} rel="external">{t('contrib.repoCta')} ↗</a></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ways {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.way {
|
||||||
|
border: 1px solid var(--oki-line);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.way h2 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
font-size: var(--text-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.num {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
inline-size: 2.2rem;
|
||||||
|
block-size: 2.2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--oki-jaune);
|
||||||
|
color: var(--oki-noir);
|
||||||
|
font-size: var(--text-1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: var(--tap-target);
|
||||||
|
padding-inline: var(--space-4);
|
||||||
|
border: 2px solid var(--oki-jaune);
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--oki-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action:hover {
|
||||||
|
background: var(--oki-jaune);
|
||||||
|
color: var(--oki-noir);
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo {
|
||||||
|
border-inline-start: 4px solid var(--oki-vert);
|
||||||
|
padding-inline-start: var(--space-4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -74,10 +74,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="stack" aria-live="polite">
|
<section class="stack" aria-live="polite">
|
||||||
<p class="count">
|
<!-- h2 : préserve l'ordre des titres (h1 → h2 → h3 des cartes) -->
|
||||||
|
<h2 class="count">
|
||||||
{filtered.length}
|
{filtered.length}
|
||||||
{filtered.length === 1 ? t('directory.resultOne') : t('directory.results')}
|
{filtered.length === 1 ? t('directory.resultOne') : t('directory.results')}
|
||||||
</p>
|
</h2>
|
||||||
|
|
||||||
{#if filtered.length === 0}
|
{#if filtered.length === 0}
|
||||||
<p class="empty">{t('directory.empty')}</p>
|
<p class="empty">{t('directory.empty')}</p>
|
||||||
@@ -124,6 +125,8 @@
|
|||||||
.count {
|
.count {
|
||||||
color: var(--oki-ink-2);
|
color: var(--oki-ink-2);
|
||||||
font-size: var(--text-0);
|
font-size: var(--text-0);
|
||||||
|
font-family: var(--font-text);
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ export function entries() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function load({ params }: { params: { slug: string } }) {
|
export function load({ params }: { params: { slug: string } }) {
|
||||||
const { tools, categories } = loadContent()
|
const { tools, categories, gcf } = loadContent()
|
||||||
const tool = tools.find((t) => t.slug === params.slug)
|
const tool = tools.find((t) => t.slug === params.slug)
|
||||||
if (!tool) error(404)
|
if (!tool) error(404)
|
||||||
const category = categories.find((c) => c.slug === tool.category)
|
const category = categories.find((c) => c.slug === tool.category)
|
||||||
return { tool, category }
|
// la jumelle créole si elle existe (affichée selon la langue choisie)
|
||||||
|
return { tool, category, gcf: gcf[tool.slug] ?? null }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import AdoptButton from '$lib/components/AdoptButton.svelte'
|
||||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { getLocale, t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
let { data } = $props()
|
let { data } = $props()
|
||||||
let tool = $derived(data.tool)
|
let tool = $derived(data.tool)
|
||||||
|
// fiche jumelle créole : affichée quand la langue est gcf et qu'elle existe ;
|
||||||
|
// sinon repli français signalé (jamais de page vide)
|
||||||
|
let useGcf = $derived(getLocale() === 'gcf' && data.gcf !== null)
|
||||||
|
let sections = $derived(useGcf && data.gcf ? data.gcf.sections : tool.sections)
|
||||||
|
|
||||||
const SECTION_ORDER = ['what', 'why', 'who', 'install', 'extra'] as const
|
const SECTION_ORDER = ['what', 'why', 'who', 'install', 'extra'] as const
|
||||||
// « extra » n'a pas d'intitulé i18n dédié : rattaché à l'installation
|
// « extra » n'a pas d'intitulé i18n dédié : rattaché à l'installation
|
||||||
@@ -61,32 +66,41 @@
|
|||||||
<span>{tag}</span>
|
<span>{tag}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p class="cluster">
|
||||||
<a class="cta" href={tool.url} rel="external">{t('tool.officialSite')} ↗</a>
|
<a class="cta" href={tool.url} rel="external">{t('tool.officialSite')} ↗</a>
|
||||||
|
<AdoptButton slug={tool.slug} />
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#if getLocale() === 'gcf'}
|
||||||
|
<p class="translation-note" role="note">
|
||||||
|
{data.gcf ? t('tool.draftTranslation') : t('tool.notTranslated')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each SECTION_ORDER as key (key)}
|
{#each SECTION_ORDER as key (key)}
|
||||||
{#if tool.sections[key]}
|
{#if sections[key]}
|
||||||
<section class="prose stack">
|
<section class="prose stack" lang={useGcf ? 'gcf' : 'fr'}>
|
||||||
<h2>{t(HEADINGS[key])}</h2>
|
<h2>{t(HEADINGS[key])}</h2>
|
||||||
<!-- HTML produit au build par marked depuis notre propre contenu versionné -->
|
<!-- HTML produit au build par marked depuis notre propre contenu versionné -->
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
<div class="stack">{@html tool.sections[key]}</div>
|
<div class="stack">{@html sections[key]}</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* couleur de marque en anneau, texte en encre : contraste AA garanti */
|
||||||
.monogram {
|
.monogram {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
inline-size: 3.5rem;
|
inline-size: 3.5rem;
|
||||||
block-size: 3.5rem;
|
block-size: 3.5rem;
|
||||||
border-radius: var(--radius-2);
|
border-radius: var(--radius-2);
|
||||||
background: var(--tool-color, var(--oki-surface-2));
|
background: var(--oki-surface-2);
|
||||||
color: white;
|
border: 4px solid var(--tool-color, var(--oki-line));
|
||||||
|
color: var(--oki-ink);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +131,16 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.translation-note {
|
||||||
|
font-size: var(--text-0);
|
||||||
|
color: var(--oki-ink-2);
|
||||||
|
border: 1px dashed var(--oki-line);
|
||||||
|
border-inline-start: 4px solid var(--oki-jaune);
|
||||||
|
border-radius: var(--radius-1);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
max-width: var(--measure);
|
||||||
|
}
|
||||||
|
|
||||||
section h2 {
|
section h2 {
|
||||||
font-size: var(--text-3);
|
font-size: var(--text-3);
|
||||||
border-bottom: 3px solid var(--oki-jaune);
|
border-bottom: 3px solid var(--oki-jaune);
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/// <reference types="@sveltejs/kit" />
|
||||||
|
/**
|
||||||
|
* Service worker offline-first (doctrine §2.1.7) : résilience réseau
|
||||||
|
* (3G irrégulière, forfaits comptés) + souveraineté (le guide reste lisible
|
||||||
|
* même si le site est bloqué ou hors-ligne après une première visite).
|
||||||
|
*
|
||||||
|
* Stratégies :
|
||||||
|
* - assets versionnés (/_app/immutable/…) : cache-first, immuables ;
|
||||||
|
* - pages HTML : network-first avec repli cache — chaque page visitée
|
||||||
|
* devient disponible hors-ligne (« cache des fiches consultées ») ;
|
||||||
|
* - coquille (/, /outils, polices, manifest) : précachée à l'installation.
|
||||||
|
*/
|
||||||
|
import { build, files, version } from '$service-worker'
|
||||||
|
|
||||||
|
const CACHE = `oki-${version}`
|
||||||
|
const SHELL = ['/', '/outils', '/contribuer', ...build, ...files]
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
.open(CACHE)
|
||||||
|
.then((cache) => cache.addAll(SHELL))
|
||||||
|
.then(() => self.skipWaiting())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
||||||
|
.then(() => self.clients.claim())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const { request } = event
|
||||||
|
if (request.method !== 'GET') return
|
||||||
|
const url = new URL(request.url)
|
||||||
|
if (url.origin !== self.location.origin) return
|
||||||
|
|
||||||
|
// assets au nom haché : jamais modifiés, cache-first
|
||||||
|
if (url.pathname.startsWith('/_app/immutable/')) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(request).then((hit) => hit ?? fetch(request))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// pages et autres fichiers : network-first, la copie en cache est le filet
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
const cache = await caches.open(CACHE)
|
||||||
|
try {
|
||||||
|
const response = await fetch(request)
|
||||||
|
if (response.ok) cache.put(request, response.clone())
|
||||||
|
return response
|
||||||
|
} catch {
|
||||||
|
const hit = await cache.match(request, { ignoreSearch: request.mode === 'navigate' })
|
||||||
|
if (hit) return hit
|
||||||
|
// navigation vers une page jamais visitée : renvoyer l'accueil
|
||||||
|
// plutôt qu'une erreur réseau brute
|
||||||
|
if (request.mode === 'navigate') {
|
||||||
|
const home = await cache.match('/')
|
||||||
|
if (home) return home
|
||||||
|
}
|
||||||
|
throw new Error('offline')
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user