feat(oki): fondations de la migration vers la stack souveraine OKI
Architecture décidée et documentée dans ADR.md : SvelteKit natif en fichiers plats plutôt que Strapi (55 outils, données statiques, souveraineté et budget performance prioritaires — doctrine §1.2, §4.4). - frontend/ : SvelteKit 2 + Svelte 5 (runes), adapter-static, 58 pages prérendues, CSS natif en @layer (reset→tokens→base→layouts→components), tokens oklch(), container queries, typo fluide clamp(), 2 polices WOFF2 auto-hébergées (Atkinson Hyperlegible, Fraunces), zéro CDN tiers - i18n FR/gcf typé (clé manquante = erreur de build), bascule Kréyòl persistée en localStorage, micro-textes créoles (« Ou pa manké ayen ») - Composants : ToolCard, DifficultyBadge (forme+texte+couleur, jamais la couleur seule), CategoryNav (≤5 entrées, divulgation progressive), SearchBar (raccourci /), LanguageSwitcher, EndMarker - content/ : 55 fiches migrées depuis src/ (script frontend/scripts/ migrate-tools.mjs, idempotent), licences SPDX reprises de directory.json - docs/ : DESIGN_SYSTEM.md et CONTENT_GUIDE.md Budgets vérifiés : JS 41 Ko gzip, CSS 2,9 Ko gzip, svelte-check 0 erreur. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "oki-preview",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": ["--dir", "frontend", "preview", "--port", "4173"],
|
||||
"port": 4173
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,3 +5,7 @@ playwright-report/
|
||||
test-results/
|
||||
*.local
|
||||
.DS_Store
|
||||
|
||||
# SvelteKit (frontend/)
|
||||
.svelte-kit/
|
||||
frontend/build/
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# ADR-001 — Backend : SvelteKit natif (fichiers plats), pas de Strapi
|
||||
|
||||
**Statut** : accepté · **Date** : 2026-07-10 · **Décideur** : migration OKI d'exitchatcontrol.org
|
||||
**Doctrine de référence** : `ui-ux-souveraine-oki.md` (citée « doctrine §x.y » ci-dessous)
|
||||
|
||||
## Contexte
|
||||
|
||||
exitchatcontrol.org est un annuaire de ~55 outils libres répartis en 23 sections
|
||||
thématiques, actuellement généré statiquement par Astro à partir de fichiers
|
||||
versionnés (`src/data/*.json`, `src/content/sections/*/*.mdx`). Le déploiement
|
||||
cible est un conteneur nginx auto-hébergé (YunoHost/Docker), sans backend.
|
||||
|
||||
Deux options étaient sur la table (mission §2.1) :
|
||||
|
||||
- **Option A** : Strapi v5 headless, base relationnelle, admin web, API, i18n plugin.
|
||||
- **Option B** : SvelteKit full-stack, contenu en fichiers Markdown/JSON versionnés,
|
||||
parsés au build, site prérendu.
|
||||
|
||||
## Décision
|
||||
|
||||
**Option B — SvelteKit 2 + Svelte 5, contenu en fichiers plats prérendus.**
|
||||
|
||||
## Justification par la règle de décision
|
||||
|
||||
La règle imposée : minimiser la charge cognitive **extrinsèque** (doctrine §1.2,
|
||||
Sweller) pour l'utilisateur final ET l'administrateur, dans le budget performance
|
||||
(doctrine §4.4).
|
||||
|
||||
1. **Les données sont déjà statiques et versionnées.** 55 outils, mises à jour
|
||||
par pull request espacées de plusieurs jours/semaines (historique git). Le seuil
|
||||
« le volume de données justifie une base relationnelle » posé comme condition
|
||||
de l'option A n'est pas atteint — 55 fiches tiennent dans un dossier de
|
||||
fichiers Markdown lisibles à l'œil nu.
|
||||
2. **Charge d'administration.** Strapi = un service Node persistant + une base +
|
||||
des migrations de schéma + des mises à jour de sécurité + des sauvegardes.
|
||||
Pour l'admin OKI, c'est de la charge extrinsèque pure : la tâche réelle
|
||||
(« corriger une fiche ») devient « se connecter à un admin, naviguer un CMS,
|
||||
publier, invalider un cache ». En fichiers plats : éditer un fichier, commit.
|
||||
Le git devient l'historique éditorial, la revue par PR devient la modération.
|
||||
3. **Budget performance (doctrine §4.4).** Site 100 % prérendu : premier rendu
|
||||
= HTML servi par nginx, zéro requête API, JS initial minimal (hydratation
|
||||
sélective). Strapi n'aiderait pas mais ajouterait un point de latence et de
|
||||
panne si l'on cédait à la tentation du rendu à la demande.
|
||||
4. **Souveraineté et surface d'attaque.** Un site statique n'a pas de CVE de CMS,
|
||||
pas de compte admin à voler, pas de base à exfiltrer. C'est la posture
|
||||
cohérente avec le sujet même du site (OPSEC, résistance à la censure) :
|
||||
le site doit être **mirrorable** (un tarball du build suffit), ce qu'un
|
||||
Strapi rend impossible.
|
||||
5. **Loi de Tesler (doctrine §1.3)** : la complexité éditoriale n'est pas
|
||||
supprimée, elle est déplacée vers le format de fichier. On l'absorbe par un
|
||||
frontmatter strict + validation au build (le build échoue si une fiche est
|
||||
invalide — équivalent des types du CMS, sans le CMS).
|
||||
|
||||
### Ce qui ferait rebasculer vers Strapi (critères de réversibilité)
|
||||
|
||||
- Une équipe éditoriale non-technique de plusieurs personnes publiant chaque semaine ;
|
||||
- Des soumissions communautaires en volume nécessitant un workflow draft/publish ;
|
||||
- Plus de ~500 fiches ou des relations réellement complexes.
|
||||
|
||||
Dans ce cas, le modèle de données de la mission §3 s'applique tel quel ; le
|
||||
frontend SvelteKit resterait inchangé (il consommerait l'API au build).
|
||||
Étape intermédiaire possible sans backend : Decap CMS (git-based) sur le dossier
|
||||
`content/` — écarté pour l'instant, « préférer la solution la plus simple ».
|
||||
|
||||
## Décisions techniques associées
|
||||
|
||||
- **adapter-static + prerender total** : parité avec le déploiement nginx actuel
|
||||
(Dockerfile conservé dans l'esprit). Un lien fonctionne sans JS (doctrine §4.4).
|
||||
- **Markdown + frontmatter maison** : parseur frontmatter minimal (~40 lignes,
|
||||
zéro dépendance) + `marked` (MIT) pour le corps, exécuté **au build uniquement**
|
||||
— rien de tout cela n'est expédié au client.
|
||||
- **i18n maison typé** (pattern repris du projet source) plutôt que
|
||||
Paraglide/inlang : ~40 lignes, dictionnaires JSON `fr`/`gcf` vérifiés par le
|
||||
compilateur TypeScript (une clé manquante = erreur de build). Paraglide reste
|
||||
la piste si le nombre de locales croît.
|
||||
- **Langues** : `fr` (défaut) + `gcf` (créole guadeloupéen, code ISO 639-3
|
||||
enregistré BCP 47 valide). Micro-textes créoles d'abord (doctrine §4.2 :
|
||||
impact affectif disproportionné pour un coût faible) ; traduction intégrale
|
||||
des fiches = chantier éditorial séparé (voir `docs/CONTENT_GUIDE.md`).
|
||||
- **CSS natif en `@layer`** : `reset → tokens → base → layouts → components →
|
||||
utilities` (doctrine §5.2), tokens `oklch()`, container queries, `clamp()`.
|
||||
Pas de Tailwind (doctrine §5.2 option 4 : écarté en connaissance de cause).
|
||||
- **Licence du code** : MIT, comme le projet source (continuité).
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"slug": "messagerie",
|
||||
"title": "Messagerie chiffrée",
|
||||
"order": 3,
|
||||
"part": 1
|
||||
},
|
||||
{
|
||||
"slug": "email",
|
||||
"title": "E-mail chiffré & PGP",
|
||||
"order": 4,
|
||||
"part": 2
|
||||
},
|
||||
{
|
||||
"slug": "navigateur",
|
||||
"title": "Navigateur & recherche",
|
||||
"order": 5,
|
||||
"part": 3
|
||||
},
|
||||
{
|
||||
"slug": "dns",
|
||||
"title": "DNS chiffré & Cloudflare",
|
||||
"order": 6,
|
||||
"part": 3
|
||||
},
|
||||
{
|
||||
"slug": "vpn",
|
||||
"title": "VPN : la vérité",
|
||||
"order": 7,
|
||||
"part": 4
|
||||
},
|
||||
{
|
||||
"slug": "stockage",
|
||||
"title": "Stockage chiffré",
|
||||
"order": 10,
|
||||
"part": 6
|
||||
},
|
||||
{
|
||||
"slug": "motsdepasse",
|
||||
"title": "Gestionnaire de mots de passe",
|
||||
"order": 11,
|
||||
"part": 7
|
||||
},
|
||||
{
|
||||
"slug": "deuxfa",
|
||||
"title": "Double authentification & clés matérielles",
|
||||
"order": 12,
|
||||
"part": 7
|
||||
},
|
||||
{
|
||||
"slug": "social",
|
||||
"title": "Réseaux sociaux décentralisés",
|
||||
"order": 13,
|
||||
"part": 8
|
||||
},
|
||||
{
|
||||
"slug": "argent",
|
||||
"title": "Souveraineté financière",
|
||||
"order": 14,
|
||||
"part": 9
|
||||
},
|
||||
{
|
||||
"slug": "ia",
|
||||
"title": "\"L'IA conversationnelle\"",
|
||||
"order": 15,
|
||||
"part": 9
|
||||
},
|
||||
{
|
||||
"slug": "boiteaoutils",
|
||||
"title": "La boîte à outils du quotidien",
|
||||
"order": 16,
|
||||
"part": 9
|
||||
},
|
||||
{
|
||||
"slug": "selfhost",
|
||||
"title": "Auto-hébergement",
|
||||
"order": 17,
|
||||
"part": 10
|
||||
},
|
||||
{
|
||||
"slug": "tor",
|
||||
"title": "Tor",
|
||||
"order": 18,
|
||||
"part": 11
|
||||
},
|
||||
{
|
||||
"slug": "os",
|
||||
"title": "\"Systèmes d'exploitation libres\"",
|
||||
"order": 19,
|
||||
"part": 12
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Aegis · Ente Auth"
|
||||
slug: "aegis"
|
||||
category: "deuxfa"
|
||||
difficulty: "beginner"
|
||||
license: "GPL-3.0"
|
||||
url: "https://getaegis.app"
|
||||
color: "#2c5c9c"
|
||||
monogram: "Ae"
|
||||
iconSlug: "aegisauthenticator"
|
||||
tags: ["Codes TOTP","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des applications qui génèrent les codes à six chiffres qui changent toutes les 30 secondes, en
|
||||
remplacement de Google Authenticator.
|
||||
|
||||
## why
|
||||
|
||||
<b>Aegis</b> (Android) garde vos codes dans un coffre chiffré local, sans cloud.{' '}
|
||||
<b>Ente Auth</b> ajoute une synchronisation chiffrée de bout en bout entre appareils, et a été
|
||||
audité. Bien plus sûrs que le SMS, et gratuits.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde : c'est le niveau de 2FA à adopter par défaut, avant les clés matérielles.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez Aegis (F-Droid) ou Ente Auth (toutes plateformes).</li>
|
||||
<li>
|
||||
Sur chaque compte, activez la 2FA « application d'authentification » et scannez le QR code
|
||||
affiché.
|
||||
</li>
|
||||
<li>
|
||||
Notez les <b>codes de secours</b> sur papier et faites une sauvegarde chiffrée de votre
|
||||
coffre.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: "SimpleLogin · addy.io"
|
||||
slug: "alias"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://simplelogin.io"
|
||||
color: "#c0392b"
|
||||
monogram: "@"
|
||||
iconSlug: "simplelogin"
|
||||
tags: ["Alias e-mail","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des alias e-mail jetables : une adresse unique par site, qui redirige vers votre vraie boîte
|
||||
sans jamais la révéler.
|
||||
|
||||
## why
|
||||
|
||||
Vous ne donnez plus votre vraie adresse partout. Si un site est piraté ou vous spamme, vous
|
||||
coupez l'alias correspondant, et vous savez immédiatement qui a fait fuiter vos données.{' '}
|
||||
<b>SimpleLogin</b> est intégré à Proton ; <b>addy.io</b> propose des alias illimités dès l'offre
|
||||
gratuite.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, et particulièrement les comptes pseudonymes qui veulent cloisonner leurs
|
||||
inscriptions.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Créez un compte, installez l'extension de navigateur.</li>
|
||||
<li>À chaque inscription, générez un nouvel alias au lieu de saisir votre vraie adresse.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Sauvegardes chiffrées"
|
||||
slug: "backups"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "intermediate"
|
||||
url: "https://cryptomator.org"
|
||||
color: "#5a6570"
|
||||
monogram: "Bk"
|
||||
iconSlug: "cryptomator"
|
||||
tags: ["Cryptomator · restic","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
De quoi sauvegarder votre téléphone et votre ordinateur en chiffrant les données <b>avant</b>{' '}
|
||||
qu'elles ne partent dans un cloud.
|
||||
|
||||
## why
|
||||
|
||||
Les sauvegardes iCloud/Google sont souvent accessibles au fournisseur, donc à la justice.{' '}
|
||||
<b>Cryptomator</b> chiffre vos fichiers avant de les envoyer sur n'importe quel cloud ;{' '}
|
||||
<b>restic</b> fait des sauvegardes chiffrées automatiques ; <b>Proton Drive</b> est chiffré de
|
||||
bout en bout. Sur iPhone, activez la « Protection avancée des données » pour chiffrer vos
|
||||
sauvegardes iCloud.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde devrait chiffrer ses sauvegardes. Règle d'or : deux copies chiffrées, sur deux
|
||||
supports, en deux lieux.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
iPhone : Réglages > votre nom > iCloud > Protection avancée des données : activez-la.
|
||||
</li>
|
||||
<li>
|
||||
PC : installez Cryptomator, créez un coffre, placez-y vos fichiers avant de synchroniser vers
|
||||
le cloud.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "Bitchat"
|
||||
slug: "bitchat"
|
||||
category: "messagerie"
|
||||
difficulty: "advanced"
|
||||
url: "https://bitchat.free"
|
||||
color: "#e8871e"
|
||||
monogram: "bit"
|
||||
tags: ["Mesh Bluetooth","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
La messagerie mesh de Jack Dorsey : les téléphones proches se relaient les messages de proche en
|
||||
proche, sans internet ni serveur ni compte.
|
||||
|
||||
## why
|
||||
|
||||
Chiffrement de bout en bout (AES-256-GCM) et un « mode panique » qui efface tout en trois
|
||||
tapotements sur le logo.
|
||||
|
||||
## who
|
||||
|
||||
Manifestations et blackout, en complément de Briar. À utiliser avec prudence.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez depuis l'App Store (iOS) ou l'APK/GitHub (Android).</li>
|
||||
<li>Ouvrez l'application et choisissez un pseudo. Aucun compte à créer.</li>
|
||||
<li>Les appareils proches se découvrent automatiquement en Bluetooth.</li>
|
||||
<li>En cas de danger, triple-tapez le logo pour tout effacer (mode panique).</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "Bitcoin (auto-conservation)"
|
||||
slug: "bitcoin"
|
||||
category: "argent"
|
||||
difficulty: "intermediate"
|
||||
url: "https://bitcoin.org"
|
||||
color: "#f7931a"
|
||||
monogram: "₿"
|
||||
iconSlug: "bitcoin"
|
||||
tags: ["Résistant à la censure"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une monnaie numérique que vous détenez vous-même, sans passer par une banque.
|
||||
|
||||
## why
|
||||
|
||||
En <b>auto-conservation</b> (vos clés dans un portefeuille que vous seul contrôlez), personne ne
|
||||
peut geler ni bloquer vos fonds. Sa confidentialité n'est pas parfaite (le registre est public),
|
||||
mais il échappe à la censure bancaire, utile le jour où un paiement est refusé.
|
||||
|
||||
## who
|
||||
|
||||
Se prémunir contre le gel de compte ou la dé-bancarisation, et payer anonymement un service
|
||||
comme un VPN.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Procurez-vous des bitcoins (plateforme d'échange, ou entre particuliers).</li>
|
||||
<li>
|
||||
Transférez-les aussitôt vers un portefeuille que <b>vous</b> contrôlez : un portefeuille
|
||||
matériel (Trezor, ColdCard) ou une application libre. Ne les laissez pas sur la plateforme.
|
||||
</li>
|
||||
<li>Sauvegardez votre phrase de récupération sur papier, jamais en photo ni en ligne.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "Bitwarden"
|
||||
slug: "bitwarden"
|
||||
category: "motsdepasse"
|
||||
difficulty: "beginner"
|
||||
license: "GPL-3.0"
|
||||
url: "https://bitwarden.com"
|
||||
color: "#175ddc"
|
||||
monogram: "Bw"
|
||||
iconSlug: "bitwarden"
|
||||
tags: ["Le meilleur pour débuter","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un coffre-fort à mots de passe qui les mémorise, les génère et les remplit pour vous, sur tous
|
||||
vos appareils.
|
||||
|
||||
## why
|
||||
|
||||
Gratuit, audité, multiplateforme, et chiffré de bout en bout. Vous n'avez plus qu'un seul mot de
|
||||
passe fort à retenir. Il est même <b>auto-hébergeable</b> (via Vaultwarden) pour ne dépendre de
|
||||
personne.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. C'est le point de départ de toute la sécurité : un mot de passe unique et fort
|
||||
par service.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Créez un compte sur bitwarden.com et choisissez un mot de passe maître <b>long</b> (une
|
||||
phrase). Ne l'oubliez jamais : il n'est pas récupérable.
|
||||
</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>
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: "Brave"
|
||||
slug: "brave"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
license: "MPL-2.0"
|
||||
url: "https://brave.com"
|
||||
color: "#fb542b"
|
||||
monogram: "Bv"
|
||||
iconSlug: "brave"
|
||||
tags: ["Bloque tout par défaut","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
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.
|
||||
|
||||
## why
|
||||
|
||||
C'est l'option la plus simple pour quelqu'un qui vient de Chrome : même ergonomie, mais sans le
|
||||
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
|
||||
|
||||
Les débutants qui veulent un changement indolore depuis Chrome. (Ignorez les fonctions
|
||||
crypto/pubs récompensées si elles ne vous intéressent pas.)
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez Brave depuis <b>brave.com</b>.
|
||||
</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>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Briar"
|
||||
slug: "briar"
|
||||
category: "messagerie"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-3.0"
|
||||
url: "https://briarproject.org"
|
||||
color: "#c1272d"
|
||||
monogram: "Br"
|
||||
tags: ["Hors-ligne / P2P","Android · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie pair-à-pair qui fonctionne <b>sans internet</b> : par Bluetooth, Wi-Fi Direct ou
|
||||
Tor.
|
||||
|
||||
## why
|
||||
|
||||
Aucun serveur, donc rien à couper ni à saisir. Résiste aux coupures d'internet et à la censure
|
||||
réseau.
|
||||
|
||||
## who
|
||||
|
||||
Manifestations, catastrophes, zones isolées, blackout. Android uniquement, et les deux personnes
|
||||
doivent avoir l'application.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez depuis <b>briarproject.org</b> ou F-Droid (pas d'iOS).
|
||||
</li>
|
||||
<li>
|
||||
Créez un compte <b>local</b> (pseudo + mot de passe), stocké seulement sur le téléphone.
|
||||
</li>
|
||||
<li>Ajoutez un contact proche en scannant son QR code, ou à distance via un lien.</li>
|
||||
<li>
|
||||
Quand le réseau est coupé, activez le Bluetooth : les messages passent d'appareil à appareil.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "Duck.ai · Lumo (Proton)"
|
||||
slug: "cloudai"
|
||||
category: "ia"
|
||||
difficulty: "beginner"
|
||||
url: "https://duck.ai"
|
||||
color: "#de5833"
|
||||
monogram: "☁"
|
||||
iconSlug: "duckduckgo"
|
||||
tags: ["Cloud orienté vie privée"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des accès à l'IA dans le cloud, mais conçus pour la vie privée, quand une IA locale n'est pas
|
||||
possible.
|
||||
|
||||
## why
|
||||
|
||||
<b>Duck.ai</b> (DuckDuckGo) donne un accès anonymisé à plusieurs modèles. <b>Lumo</b> (Proton)
|
||||
chiffre les échanges et ne s'en sert pas pour entraîner. Bien mieux que ChatGPT grand public,
|
||||
mais cela reste un serveur distant.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent la commodité du cloud sans nourrir les géants. Gardez-y les sujets non
|
||||
sensibles.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "Cloudflare 1.1.1.1 (à nuancer)"
|
||||
slug: "cloudflare"
|
||||
category: "dns"
|
||||
difficulty: "beginner"
|
||||
url: "https://one.one.one.one"
|
||||
color: "#f38020"
|
||||
monogram: "CF"
|
||||
iconSlug: "cloudflare"
|
||||
tags: ["Rapide mais centralisé","🇺🇸"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le DNS chiffré 1.1.1.1 et l'application WARP de Cloudflare : rapides, gratuits, et un vrai
|
||||
progrès face au résolveur de votre FAI.
|
||||
|
||||
## why
|
||||
|
||||
C'est utile, avec un engagement de non-journalisation audité. Mais soyez lucide : Cloudflare se
|
||||
trouve déjà devant une immense partie du web (comme intermédiaire technique, il peut voir le
|
||||
trafic en clair vers ces sites). Y router aussi votre DNS revient à{' '}
|
||||
<b>concentrer encore plus de vos traces</b> chez une seule entreprise américaine soumise au
|
||||
droit américain.
|
||||
|
||||
## who
|
||||
|
||||
Bon pour un usage courant et le blocage des maliciels (1.1.1.1 for Families). À éviter ou à
|
||||
diversifier pour un usage sensible : préférez alors Quad9 ou Mullvad DNS.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Cryptomator"
|
||||
slug: "cryptomator"
|
||||
category: "stockage"
|
||||
difficulty: "intermediate"
|
||||
license: "GPL-3.0"
|
||||
url: "https://cryptomator.org"
|
||||
color: "#26a69a"
|
||||
monogram: "Cy"
|
||||
iconSlug: "cryptomator"
|
||||
tags: ["Chiffre avant l'envoi","🇩🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un coffre qui chiffre vos fichiers <b>avant</b> de les envoyer sur n'importe quel cloud, même
|
||||
Google Drive ou Dropbox.
|
||||
|
||||
## why
|
||||
|
||||
L'astuce parfaite si vous devez garder un cloud existant : vous conservez le service pratique,
|
||||
mais le fournisseur ne voit plus que du charabia chiffré. Vous gardez seul la clé.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui ne peuvent pas quitter un cloud grand public tout de suite, mais veulent protéger leurs
|
||||
fichiers dès maintenant.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez Cryptomator (PC et mobile) depuis cryptomator.org.</li>
|
||||
<li>
|
||||
Créez un « coffre » dans le dossier synchronisé de votre cloud, choisissez un mot de passe
|
||||
fort.
|
||||
</li>
|
||||
<li>
|
||||
Placez vos fichiers dans le coffre : ils sont chiffrés puis synchronisés automatiquement.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Effacer ses données (RGPD)"
|
||||
slug: "databrokers"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "intermediate"
|
||||
url: "https://www.cnil.fr"
|
||||
color: "#b8860b"
|
||||
monogram: "RG"
|
||||
tags: ["Courtiers en données"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Faire retirer vos informations personnelles des « courtiers en données » qui les collectent et
|
||||
les revendent.
|
||||
|
||||
## why
|
||||
|
||||
En Europe, le RGPD (article 17) vous donne un <b>droit à l'effacement</b> : vous pouvez exiger,
|
||||
gratuitement, qu'une entreprise supprime vos données, en principe sous un mois. Des services
|
||||
comme <b>Incogni</b> automatisent ces demandes en masse, moyennant paiement.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent réduire leur exposition. Attention : les données réapparaissent avec le temps,
|
||||
et cela ne touche ni les registres publics ni les réseaux sociaux. Le faire soi-même est gratuit
|
||||
mais fastidieux.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Pour une demande manuelle : écrivez au courtier en invoquant l'article 17 du RGPD et demandez
|
||||
la suppression.
|
||||
</li>
|
||||
<li>
|
||||
Pour automatiser : souscrivez à un service de retrait et laissez-le envoyer les demandes en
|
||||
votre nom.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Matrix / Element"
|
||||
slug: "element"
|
||||
category: "social"
|
||||
difficulty: "intermediate"
|
||||
url: "https://element.io"
|
||||
color: "#0dbd8b"
|
||||
monogram: "El"
|
||||
iconSlug: "element"
|
||||
tags: ["Remplace Discord / Slack","🌍 · fédéré · E2EE"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie de communautés, dans l'esprit de Discord ou Slack, mais fédérée et chiffrable de
|
||||
bout en bout. Element est l'application, Matrix le réseau.
|
||||
|
||||
## why
|
||||
|
||||
Vous pouvez héberger votre propre serveur (Synapse) et contrôler vos données. Parfait pour un
|
||||
collectif, une rédaction, une association qui veut son espace indépendant.
|
||||
|
||||
## who
|
||||
|
||||
Communautés et groupes. Voir la nuance sur les métadonnées ci-dessous.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez Element (element.io) et créez un compte sur un serveur public, ou sur le vôtre
|
||||
(section 10).
|
||||
</li>
|
||||
<li>
|
||||
Rejoignez ou créez des salons, et activez le chiffrement de bout en bout pour les
|
||||
conversations privées.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Pixelfed · PeerTube · Lemmy"
|
||||
slug: "fediverse"
|
||||
category: "social"
|
||||
difficulty: "intermediate"
|
||||
url: "https://pixelfed.org"
|
||||
color: "#ea580c"
|
||||
monogram: "Pi"
|
||||
iconSlug: "pixelfed"
|
||||
tags: ["Insta · YouTube · Reddit","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Les cousins fédérés d'Instagram (Pixelfed), de YouTube (PeerTube) et de Reddit (Lemmy).
|
||||
|
||||
## why
|
||||
|
||||
Même logique que Mastodon : pas de propriétaire unique, pas d'algorithme publicitaire, pas de
|
||||
traçage. Vous publiez photos, vidéos ou discussions sans nourrir une machine à profiler.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent quitter Instagram, YouTube ou Reddit sans renoncer au format.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Firefox (durci)"
|
||||
slug: "firefox"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
license: "MPL-2.0"
|
||||
url: "https://www.mozilla.org/firefox"
|
||||
color: "#e66000"
|
||||
monogram: "Fx"
|
||||
iconSlug: "firefoxbrowser"
|
||||
tags: ["Remplace Chrome","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le navigateur libre de référence pour un usage quotidien respectueux de la vie privée.
|
||||
|
||||
## why
|
||||
|
||||
Contrairement à Chrome (un mouchard publicitaire de Google), Firefox est développé par une
|
||||
fondation et n'a pas d'intérêt à vous profiler. Avec quelques réglages (le guide{' '}
|
||||
<em>arkenfox</em>) et l'extension uBlock Origin, il devient très solide.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, comme navigateur principal au quotidien.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez Firefox depuis <b>mozilla.org/firefox</b>.
|
||||
</li>
|
||||
<li>
|
||||
Ajoutez l'extension <b>uBlock Origin</b> (blocage des pubs et traqueurs).
|
||||
</li>
|
||||
<li>
|
||||
Dans les réglages de vie privée, choisissez « Stricte », désactivez la télémétrie, et videz
|
||||
les cookies à la fermeture. Pour aller plus loin, appliquez le guide arkenfox.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: "GrapheneOS"
|
||||
slug: "grapheneos"
|
||||
category: "os"
|
||||
difficulty: "advanced"
|
||||
license: "MIT"
|
||||
url: "https://grapheneos.org"
|
||||
color: "#1f8a70"
|
||||
monogram: "Gr"
|
||||
iconSlug: "grapheneos"
|
||||
tags: ["Mobile dégooglisé","Android · Pixel · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un Android entièrement dégooglisé, installé sur un téléphone Pixel : la souveraineté sur mobile.
|
||||
|
||||
## why
|
||||
|
||||
C'est la défense structurelle par excellence contre un OS ou un magasin d'applications qui
|
||||
voudrait vous scanner. Services Google en bac à sable optionnel,{' '}
|
||||
<b>permissions réseau par application</b>, profils cloisonnés, redémarrage automatique qui purge
|
||||
les clés de la mémoire, code PIN de contrainte qui efface le téléphone. Réputé pour sa sécurité
|
||||
au point d'être utilisé par des cibles de logiciels espions.
|
||||
|
||||
## who
|
||||
|
||||
Profils exposés, mais aussi tout citoyen prêt à acheter un Pixel pour reprendre le contrôle de
|
||||
son téléphone.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Procurez-vous un téléphone <b>Google Pixel</b> compatible (c'est le seul matériel supporté,
|
||||
pour des raisons de sécurité).
|
||||
</li>
|
||||
<li>
|
||||
Sur un ordinateur, allez sur <b>grapheneos.org/install</b> et suivez l'installeur web
|
||||
(quelques clics, aucun terminal requis).
|
||||
</li>
|
||||
<li>
|
||||
Pour vos applications, installez le magasin libre F-Droid ; ajoutez Google Play en bac à sable
|
||||
seulement si nécessaire.
|
||||
</li>
|
||||
<li>
|
||||
Réglez un long mot de passe (pas un code à 4 chiffres) et découvrez le mode « Lockdown ».
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "IVPN"
|
||||
slug: "ivpn"
|
||||
category: "vpn"
|
||||
difficulty: "intermediate"
|
||||
url: "https://www.ivpn.net"
|
||||
color: "#3b5bdb"
|
||||
monogram: "iv"
|
||||
tags: ["Sans e-mail · no-log","🇬🇮 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un petit fournisseur de VPN particulièrement rigoureux et transparent, sur le même modèle que
|
||||
Mullvad.
|
||||
|
||||
## why
|
||||
|
||||
Politique no-log auditée, pas d'e-mail obligatoire, paiement en crypto ou en espèces. Il propose
|
||||
même des options anti-traçage (blocage des trackers au niveau réseau) et une inscription sans
|
||||
aucun identifiant.
|
||||
|
||||
## who
|
||||
|
||||
Une alternative solide à Mullvad, pour les profils pseudonymes et à haut risque qui veulent
|
||||
diversifier.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Sur <b>ivpn.net</b>, générez un compte (un identifiant est créé pour vous, sans e-mail).
|
||||
</li>
|
||||
<li>Payez en cryptomonnaie ou en espèces, comme pour Mullvad.</li>
|
||||
<li>
|
||||
Installez l'application, connectez-vous hors UE, activez le kill switch et le protocole
|
||||
WireGuard.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "KeePassXC"
|
||||
slug: "keepassxc"
|
||||
category: "motsdepasse"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-2.0/3.0"
|
||||
url: "https://keepassxc.org"
|
||||
color: "#2c6b9e"
|
||||
monogram: "Kp"
|
||||
iconSlug: "keepassxc"
|
||||
tags: ["100% local","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un coffre-fort <b>local</b> : un fichier chiffré que vous seul détenez, sans aucun cloud ni
|
||||
compte.
|
||||
|
||||
## why
|
||||
|
||||
Rien ne quitte votre appareil, sauf si vous décidez de synchroniser le fichier vous-même (par
|
||||
exemple via Cryptomator). C'est le choix des puristes qui ne veulent aucun intermédiaire.
|
||||
|
||||
## who
|
||||
|
||||
Les profils avancés qui veulent le contrôle total et zéro dépendance en ligne.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez KeePassXC (PC) et une application compatible sur mobile (KeePassDX sur Android,
|
||||
Strongbox sur iOS).
|
||||
</li>
|
||||
<li>Créez un fichier de base de données protégé par un mot de passe maître.</li>
|
||||
<li>Pour l'avoir sur tous vos appareils, synchronisez ce fichier via un cloud chiffré.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: "Linux (sur ordinateur)"
|
||||
slug: "linux"
|
||||
category: "os"
|
||||
difficulty: "intermediate"
|
||||
url: "https://distrochooser.de/fr"
|
||||
color: "#2b2b2b"
|
||||
monogram: "🐧"
|
||||
iconSlug: "linux"
|
||||
tags: ["Remplace Windows / macOS","🌍 · libre & gratuit"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un système d'exploitation libre et gratuit pour votre ordinateur, en remplacement de Windows ou
|
||||
de macOS. Il en existe de nombreuses variantes, appelées « distributions ».
|
||||
|
||||
## why
|
||||
|
||||
C'est le point aveugle de beaucoup de gens : ils chiffrent leurs messages mais gardent un
|
||||
Windows qui transmet en continu de la télémétrie à Microsoft (et dont la fonction « Recall » a
|
||||
voulu photographier l'écran en permanence), ou un macOS qui signale les applications que vous
|
||||
lancez. Linux, lui, ne vous espionne pas, il est gratuit, il ressuscite les vieux ordinateurs et
|
||||
il vous rend enfin maître de votre machine. C'est plus facile que sa réputation : les
|
||||
distributions modernes ressemblent à Windows ou macOS.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde peut franchir le pas. Pour débuter, <b>Linux Mint</b> (le plus proche de Windows)
|
||||
ou <b>Fedora</b> ; <b>Debian</b> pour la stabilité. Pas besoin de tout casser : on peut d'abord
|
||||
tester, puis installer à côté de son système actuel.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Pas sûr de la distribution ? Répondez au questionnaire <b>distrochooser.de/fr</b>, qui vous
|
||||
oriente selon vos besoins.
|
||||
</li>
|
||||
<li>
|
||||
Testez-la <b>sans rien installer</b> directement dans votre navigateur sur{' '}
|
||||
<b>distrosea.com</b>.
|
||||
</li>
|
||||
<li>
|
||||
Une fois convaincu, téléchargez l'image de la distribution et créez une clé USB de démarrage
|
||||
(avec Ventoy ou Balena Etcher).
|
||||
</li>
|
||||
<li>
|
||||
Démarrez sur la clé pour essayer le système « en live », puis lancez l'installation. Activez
|
||||
le chiffrement du disque proposé.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: "Ollama · LM Studio · Jan · GPT4All"
|
||||
slug: "localai"
|
||||
category: "ia"
|
||||
difficulty: "intermediate"
|
||||
url: "https://lmstudio.ai"
|
||||
color: "#111111"
|
||||
monogram: "Ai"
|
||||
iconSlug: "ollama"
|
||||
tags: ["Remplace ChatGPT","🌍 · 100% local"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des logiciels pour faire tourner un modèle d'intelligence artificielle directement sur votre
|
||||
ordinateur.
|
||||
|
||||
## why
|
||||
|
||||
Vos questions ne quittent jamais l'appareil : aucun serveur, aucun compte, aucune télémétrie, et
|
||||
ça marche hors ligne. La qualité dépend de votre matériel, mais rien ne fuit. <b>Ollama</b> et{' '}
|
||||
<b>LM Studio</b> sont les plus simples ; <b>Jan</b> et <b>GPT4All</b> visent la vie privée
|
||||
maximale.
|
||||
|
||||
## who
|
||||
|
||||
Tous ceux qui utilisent l'IA pour des sujets un tant soit peu personnels et ne veulent pas les
|
||||
confier à un serveur américain.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Pour débuter en douceur, installez <b>LM Studio</b> ou <b>Jan</b> (interface graphique, comme
|
||||
une application classique).
|
||||
</li>
|
||||
<li>
|
||||
Téléchargez un modèle proposé dans l'application (par exemple Llama ou Mistral), adapté à la
|
||||
puissance de votre machine.
|
||||
</li>
|
||||
<li>
|
||||
Discutez : tout se passe en local. Ollama est la version en ligne de commande pour les plus
|
||||
techniques.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: "mailbox.org · Posteo"
|
||||
slug: "mailbox"
|
||||
category: "email"
|
||||
difficulty: "beginner"
|
||||
url: "https://mailbox.org"
|
||||
color: "#2470d4"
|
||||
monogram: "mx"
|
||||
tags: ["Mail classique éthique","🇩🇪 · UE · PGP"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux fournisseurs de mail européens, éthiques et peu coûteux, avec support PGP côté serveur.
|
||||
|
||||
## why
|
||||
|
||||
Respectueux de la vie privée et de l'écologie, sur des standards ouverts (IMAP, PGP). Ils
|
||||
fonctionnent avec n'importe quel logiciel de messagerie.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent un mail « classique » (relevé dans Thunderbird) mais éthique, plutôt qu'une
|
||||
messagerie chiffrée fermée.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Créez un compte (payant, de l'ordre de 1 à 3 € par mois).</li>
|
||||
<li>Activez le chiffrement PGP côté serveur (fonction « Guard » chez mailbox.org).</li>
|
||||
<li>Configurez le compte dans Thunderbird ou l'application mobile de votre choix.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: "Organic Maps · OsmAnd"
|
||||
slug: "maps"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://organicmaps.app"
|
||||
color: "#0a7d33"
|
||||
monogram: "Om"
|
||||
iconSlug: "openstreetmap"
|
||||
tags: ["Remplace Google Maps","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des applications de cartes et de navigation, fondées sur OpenStreetMap, qui fonctionnent hors
|
||||
ligne.
|
||||
|
||||
## why
|
||||
|
||||
Google Maps enregistre chacun de vos trajets et bâtit un historique de lieux d'une précision
|
||||
redoutable. <b>Organic Maps</b> ne trace rien et n'affiche aucune publicité ; <b>OsmAnd</b> vise
|
||||
les utilisateurs avancés (randonnée, courbes de niveau, navigation détaillée).
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. Le trafic en temps réel et les transports sont moins complets que chez Google,
|
||||
mais la navigation quotidienne est excellente.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez depuis F-Droid, l'App Store ou Google Play.</li>
|
||||
<li>
|
||||
Téléchargez à l'avance les cartes des régions qui vous intéressent pour l'usage hors ligne.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Mastodon"
|
||||
slug: "mastodon"
|
||||
category: "social"
|
||||
difficulty: "beginner"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://joinmastodon.org"
|
||||
color: "#6364ff"
|
||||
monogram: "Ma"
|
||||
iconSlug: "mastodon"
|
||||
tags: ["Remplace X / Twitter","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
L'alternative à X/Twitter : un réseau social fait de milliers de serveurs indépendants qui
|
||||
communiquent entre eux (le « fédiverse »).
|
||||
|
||||
## why
|
||||
|
||||
Pas de patron unique, pas d'algorithme qui décide à votre place, pas de bannissement arbitraire
|
||||
par une seule entreprise. Le fil est chronologique, sans publicité ni manipulation.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. Idéal pour les comptes d'information qui veulent une présence qu'aucune
|
||||
entreprise ne peut couper.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Sur joinmastodon.org, choisissez un serveur (ou « instance ») qui vous correspond, puis créez
|
||||
un compte.
|
||||
</li>
|
||||
<li>
|
||||
Installez une application (l'officielle, ou Ivory, Tusky…) et suivez des comptes : vous voyez
|
||||
tout le fédiverse, quel que soit leur serveur.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Molly"
|
||||
slug: "molly"
|
||||
category: "messagerie"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-3.0"
|
||||
url: "https://molly.im"
|
||||
color: "#6d4bb3"
|
||||
monogram: "M"
|
||||
tags: ["Signal durci","Android · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une version durcie de Signal pour Android, compatible avec le même réseau.
|
||||
|
||||
## why
|
||||
|
||||
Base de données chiffrée au repos, verrouillage automatique, et une variante « FOSS »
|
||||
entièrement sans services Google.
|
||||
|
||||
## who
|
||||
|
||||
Utilisateurs avancés, notamment sur GrapheneOS, avec un modèle de menace élevé. Android
|
||||
uniquement.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Ajoutez le dépôt Molly dans F-Droid, ou récupérez l'APK sur <b>molly.im</b>.
|
||||
</li>
|
||||
<li>
|
||||
Choisissez <b>Molly-FOSS</b> si vous voulez zéro dépendance Google.
|
||||
</li>
|
||||
<li>Enregistrez-vous avec votre numéro (même réseau que Signal).</li>
|
||||
<li>Définissez un mot de passe de base de données et le verrouillage automatique.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Monero (XMR)"
|
||||
slug: "monero"
|
||||
category: "argent"
|
||||
difficulty: "advanced"
|
||||
license: "BSD-3-Clause"
|
||||
url: "https://www.getmonero.org"
|
||||
color: "#ff6600"
|
||||
monogram: "ɱ"
|
||||
iconSlug: "monero"
|
||||
tags: ["Confidentiel par défaut"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
La cryptomonnaie de la vie privée : montants, expéditeur et destinataire sont masqués par
|
||||
défaut.
|
||||
|
||||
## why
|
||||
|
||||
C'est l'exact opposé de Bitcoin sur ce point : les transactions sont confidentielles par
|
||||
conception. C'est le meilleur moyen de payer un service sans laisser de trace reliable à vous.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent le maximum de confidentialité financière. Note : Monero est déjà retiré de
|
||||
plusieurs plateformes en Europe, il faut souvent passer par un échange décentralisé.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez un portefeuille (l'officiel sur getmonero.org, ou Cake Wallet sur mobile).</li>
|
||||
<li>
|
||||
Obtenez des XMR via un service d'échange décentralisé (par exemple un « swap » depuis une
|
||||
autre crypto).
|
||||
</li>
|
||||
<li>Sauvegardez votre phrase de récupération sur papier.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: "Mullvad VPN"
|
||||
slug: "mullvad"
|
||||
category: "vpn"
|
||||
difficulty: "beginner"
|
||||
url: "https://mullvad.net"
|
||||
color: "#2b4b8f"
|
||||
monogram: "Mv"
|
||||
iconSlug: "mullvad"
|
||||
tags: ["Sans compte · cash/Monero","🇸🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un VPN qui masque votre adresse IP et chiffre votre trafic jusqu'à ses serveurs, sans jamais
|
||||
vous demander la moindre information personnelle.
|
||||
|
||||
## why
|
||||
|
||||
C'est l'étalon-or de la vie privée. Pas d'e-mail, pas de nom : à l'inscription, on vous attribue
|
||||
un simple numéro de compte. Politique « no-log » auditée, prix unique et honnête (5 € par mois,
|
||||
sans palier ni fausse promesse), et paiement possible en espèces ou en Monero. C'est précisément
|
||||
ce qui compte le jour où un État tente de couper l'accès aux VPN : un compte impossible à relier
|
||||
à votre identité, et un paiement que les banques ne peuvent pas bloquer.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, du citoyen qui veut contourner une vérification d'identité au profil à haut
|
||||
risque. C'est le VPN à recommander en priorité.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Allez sur <b>mullvad.net</b>. Si le site est bloqué dans votre pays, ouvrez-le via le
|
||||
navigateur Tor ou son adresse <b>.onion</b>. Cliquez sur « Générer un numéro de compte » :
|
||||
aucun e-mail ni nom n'est demandé. Notez ce numéro dans votre gestionnaire de mots de passe et
|
||||
ne le partagez jamais, c'est votre unique clé d'accès.
|
||||
</li>
|
||||
<li>
|
||||
Choisissez la durée et le mode de paiement. Le prix est fixe. Trois voies : carte bancaire (le
|
||||
plus simple si elle n'est pas bloquée), espèces envoyées par courrier, ou cryptomonnaie.
|
||||
</li>
|
||||
<li>
|
||||
<b>Paiement anonyme en crypto :</b> cliquez sur « Créer une adresse de paiement à usage unique
|
||||
». Mullvad affiche une adresse (Bitcoin ou Monero) et un montant exact. Envoyez ce montant
|
||||
depuis votre portefeuille. Pour l'anonymat maximal, préférez <b>Monero</b> (confidentiel par
|
||||
défaut) ; Bitcoin est plus simple mais traçable. Voir la section 09 pour acheter de la crypto.
|
||||
Comptez environ 30 minutes de confirmation.
|
||||
</li>
|
||||
<li>
|
||||
Téléchargez l'application sur <b>mullvad.net/download</b> (via Tor si le site est filtré) et
|
||||
installez-la. Sur mobile, passez par l'App Store, Google Play, ou l'APK officiel si
|
||||
l'application a été retirée de votre magasin.
|
||||
</li>
|
||||
<li>
|
||||
Lancez l'application, entrez votre numéro de compte, puis connectez-vous à un serveur situé{' '}
|
||||
<b>hors de votre pays</b>, idéalement hors Union européenne (Suisse, États-Unis…). Activez le
|
||||
« kill switch » (qui coupe Internet si le VPN tombe) et le DNS de Mullvad.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Mullvad Browser"
|
||||
slug: "mullvadbrowser"
|
||||
category: "navigateur"
|
||||
difficulty: "intermediate"
|
||||
url: "https://mullvad.net/browser"
|
||||
color: "#2b4b8f"
|
||||
monogram: "Mb"
|
||||
iconSlug: "mullvad"
|
||||
tags: ["Anti-empreinte","🇸🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le navigateur du Tor Browser, mais sans le réseau Tor : à utiliser avec votre VPN pour un
|
||||
ordinateur difficile à pister.
|
||||
|
||||
## why
|
||||
|
||||
Développé avec le Tor Project, il est conçu pour que tous ses utilisateurs présentent la{' '}
|
||||
<b>même empreinte numérique</b>. Résultat : au lieu d'être unique et donc traçable, vous vous
|
||||
fondez dans la masse. C'est le meilleur choix de vie privée sur ordinateur.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent aller au-delà du simple blocage de pub, notamment les comptes pseudonymes et
|
||||
les profils exposés.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Téléchargez-le sur <b>mullvad.net/browser</b> (Windows, macOS, Linux).
|
||||
</li>
|
||||
<li>
|
||||
Utilisez-le tel quel, sans le personnaliser : chaque extension ou réglage ajouté vous rend à
|
||||
nouveau identifiable.
|
||||
</li>
|
||||
<li>Associez-le à votre VPN pour masquer aussi votre adresse IP.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Mullvad DNS · NextDNS"
|
||||
slug: "mullvaddns"
|
||||
category: "dns"
|
||||
difficulty: "intermediate"
|
||||
url: "https://mullvad.net/help/dns-over-https-and-dns-over-tls"
|
||||
color: "#2b4b8f"
|
||||
monogram: "DNS"
|
||||
iconSlug: "mullvad"
|
||||
tags: ["Filtrage réseau"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux résolveurs chiffrés qui bloquent en plus pubs et traqueurs pour tout l'appareil.
|
||||
|
||||
## why
|
||||
|
||||
<b>Mullvad DNS</b> (Suède, no-log) est gratuit et propose des listes de blocage. <b>NextDNS</b>{' '}
|
||||
est très personnalisable, avec un tableau de bord et des profils par appareil, mais il est basé
|
||||
aux États-Unis et journalise par défaut (à désactiver dans les réglages).
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent un blocage des pubs à l'échelle du système, sans installer d'application par
|
||||
navigateur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Mullvad DNS : utilisez l'adresse indiquée sur mullvad.net/help/dns comme DNS privé (même
|
||||
méthode que Quad9).
|
||||
</li>
|
||||
<li>
|
||||
NextDNS : créez un profil sur nextdns.io, <b>désactivez la journalisation</b>, puis appliquez
|
||||
la configuration à vos appareils.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Nextcloud"
|
||||
slug: "nc-storage"
|
||||
category: "stockage"
|
||||
difficulty: "advanced"
|
||||
url: "https://nextcloud.com"
|
||||
color: "#0082c9"
|
||||
monogram: "Nc"
|
||||
iconSlug: "nextcloud"
|
||||
tags: ["Votre propre cloud","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Votre propre nuage (fichiers, agenda, contacts, notes) hébergé sur votre serveur.
|
||||
|
||||
## why
|
||||
|
||||
Le degré ultime de contrôle : vos données ne quittent jamais votre matériel. Détaillé dans la
|
||||
section 10 (Auto-hébergement).
|
||||
|
||||
## who
|
||||
|
||||
Les auto-hébergeurs. Voir la fiche complète en section 10.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "Nextcloud"
|
||||
slug: "nextcloud"
|
||||
category: "selfhost"
|
||||
difficulty: "advanced"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://nextcloud.com"
|
||||
color: "#0082c9"
|
||||
monogram: "Nc"
|
||||
iconSlug: "nextcloud"
|
||||
tags: ["Remplace toute la suite Google","🇩🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Votre propre nuage : fichiers, agenda, contacts, photos, notes et documents collaboratifs, sur
|
||||
votre serveur.
|
||||
|
||||
## why
|
||||
|
||||
C'est le remplacement complet de Google Drive, Agenda et Contacts, mais chez vous. Vous décidez
|
||||
de la juridiction et personne d'autre n'a la main sur vos fichiers. À savoir : c'est la
|
||||
confidentialité « votre serveur », pas un chiffrement de bout en bout par défaut ; vous devez le
|
||||
maintenir à jour.
|
||||
|
||||
## who
|
||||
|
||||
Les auto-hébergeurs. Le plus simple est de l'installer via YunoHost ou Umbrel.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Nostr"
|
||||
slug: "nostr"
|
||||
category: "social"
|
||||
difficulty: "intermediate"
|
||||
url: "https://nostr.com"
|
||||
color: "#8e30eb"
|
||||
monogram: "No"
|
||||
tags: ["Incensurable","🌍 · protocole ouvert"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Pas une application mais un protocole : un réseau social où votre identité est une simple paire
|
||||
de clés, comme pour Bitcoin.
|
||||
|
||||
## why
|
||||
|
||||
Zéro e-mail, zéro mot de passe, aucun compte qu'on puisse supprimer. Vos messages voyagent sur
|
||||
des relais décentralisés et vous possédez vraiment votre audience : personne ne peut vous bannir
|
||||
durablement. Vous pouvez même recevoir des pourboires en Bitcoin (les « zaps ») et changer
|
||||
d'application quand vous voulez sans perdre vos abonnés.
|
||||
|
||||
## who
|
||||
|
||||
Comptes d'information et créateurs qui craignent la censure de plateforme et veulent une
|
||||
présence vraiment à eux.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez un client : Damus (iPhone), Amethyst ou Primal (Android), ou une version web.</li>
|
||||
<li>
|
||||
L'application génère votre paire de clés. <b>Sauvegardez votre clé privée (nsec)</b> comme une
|
||||
phrase de récupération Bitcoin : sur papier, jamais partagée.
|
||||
</li>
|
||||
<li>Créez votre profil, suivez des gens, activez les zaps pour recevoir des sats.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: "Standard Notes · Joplin · CryptPad"
|
||||
slug: "notes"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://standardnotes.com"
|
||||
color: "#2e7d6b"
|
||||
monogram: "No"
|
||||
tags: ["Remplace Docs / Notion","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des notes et documents chiffrés de bout en bout, pour remplacer Google Docs, Notion ou Evernote.
|
||||
|
||||
## why
|
||||
|
||||
<b>Standard Notes</b> (par Proton) chiffre vos notes par défaut. <b>Joplin</b> gère des carnets
|
||||
en Markdown avec chiffrement optionnel et la synchronisation de votre choix. <b>CryptPad</b>{' '}
|
||||
(français) offre l'équivalent de Google Docs en temps réel, entièrement chiffré.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde pour les notes ; CryptPad pour la collaboration à plusieurs.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Standard Notes / Joplin : créez un compte, installez l'application, et activez le chiffrement
|
||||
(par défaut sur Standard Notes, à activer dans Joplin).
|
||||
</li>
|
||||
<li>
|
||||
CryptPad : utilisez cryptpad.fr ou une autre instance, aucun compte requis pour commencer.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Ente · Immich"
|
||||
slug: "photos"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "intermediate"
|
||||
url: "https://ente.io"
|
||||
color: "#7b3fbf"
|
||||
monogram: "Ph"
|
||||
iconSlug: "immich"
|
||||
tags: ["Remplace Google Photos","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux façons de garder vos photos sans les confier à Google ou Apple.
|
||||
|
||||
## why
|
||||
|
||||
<b>Ente</b> est un service géré, chiffré de bout en bout, avec reconnaissance et recherche
|
||||
faites sur l'appareil : simple et sûr. <b>Immich</b> est un clone de Google Photos que vous{' '}
|
||||
<b>hébergez vous-même</b> (recherche par visages et objets incluse), à réserver à ceux qui
|
||||
savent gérer un serveur.
|
||||
|
||||
## who
|
||||
|
||||
Ente pour tous ; Immich pour les auto-hébergeurs. Attention : Immich n'est pas chiffré de bout
|
||||
en bout, sa sécurité dépend de votre serveur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Ente : installez l'application, créez un compte, activez la sauvegarde automatique de votre
|
||||
pellicule.
|
||||
</li>
|
||||
<li>
|
||||
Immich : déployez-le sur votre serveur (voir section 10) via Docker, puis connectez
|
||||
l'application mobile.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Pi-hole · AdGuard Home"
|
||||
slug: "pihole"
|
||||
category: "dns"
|
||||
difficulty: "advanced"
|
||||
url: "https://pi-hole.net"
|
||||
color: "#96060c"
|
||||
monogram: "π"
|
||||
iconSlug: "pihole"
|
||||
tags: ["Blocage tout le réseau","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un bloqueur de publicités et de traqueurs qui protège <b>tout votre réseau à la fois</b>,
|
||||
installé sur un petit ordinateur comme un Raspberry Pi.
|
||||
|
||||
## why
|
||||
|
||||
Là où uBlock Origin protège un navigateur, Pi-hole filtre <b>chaque appareil</b> de la maison, y
|
||||
compris ceux où l'on ne peut pas installer d'extension : téléphones, téléviseurs connectés,
|
||||
objets connectés. Les pubs et traqueurs sont bloqués au niveau du DNS, avant même de se charger.{' '}
|
||||
<b>AdGuard Home</b> est une alternative à l'interface plus moderne.
|
||||
|
||||
## who
|
||||
|
||||
Les foyers qui veulent nettoyer tous leurs appareils d'un coup, et qui ont (ou veulent) un
|
||||
Raspberry Pi ou un petit serveur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Sur un Raspberry Pi (ou un conteneur), lancez l'installeur officiel de Pi-hole.</li>
|
||||
<li>
|
||||
Dans les réglages de votre box/routeur, indiquez l'adresse du Pi-hole comme serveur DNS.
|
||||
</li>
|
||||
<li>Désormais, tous les appareils connectés à votre réseau sont filtrés automatiquement.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "PocketPal AI"
|
||||
slug: "pocketpal"
|
||||
category: "ia"
|
||||
difficulty: "intermediate"
|
||||
license: "MIT"
|
||||
url: "https://pocketpal.dev"
|
||||
color: "#6b4eff"
|
||||
monogram: "Pp"
|
||||
tags: ["Remplace ChatGPT sur mobile","📱 · 100% local"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une application pour faire tourner des modèles d'IA directement sur votre téléphone, entièrement
|
||||
hors ligne.
|
||||
|
||||
## why
|
||||
|
||||
Vos requêtes ne quittent jamais le téléphone : aucun compte, aucun cloud, aucune télémétrie, et
|
||||
ça marche en mode avion. Open source (MIT), l'appli télécharge des modèles <b>GGUF</b> (Gemma,
|
||||
Qwen, Phi, Llama) depuis Hugging Face et les exécute sur le CPU, le GPU ou le NPU de l'appareil.
|
||||
Le pendant mobile d'Ollama et LM Studio.
|
||||
|
||||
## who
|
||||
|
||||
Tous ceux qui dégainent ChatGPT sur leur téléphone pour des questions personnelles et ne veulent
|
||||
pas les confier à un serveur américain.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez <b>PocketPal</b> depuis l'App Store ou Google Play (ou compilez les sources).
|
||||
</li>
|
||||
<li>
|
||||
Ouvrez <b>Models</b>, choisissez un modèle adapté à votre téléphone et téléchargez-le.
|
||||
</li>
|
||||
<li>Chargez-le et discutez : tout tourne hors ligne, sur l'appareil.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "Proton Drive"
|
||||
slug: "protondrive"
|
||||
category: "stockage"
|
||||
difficulty: "beginner"
|
||||
url: "https://proton.me/drive"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pd"
|
||||
iconSlug: "protondrive"
|
||||
tags: ["Remplace Google Drive / iCloud","🇨🇭 · E2EE"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un cloud chiffré de bout en bout pour vos fichiers et vos photos, intégré à Proton.
|
||||
|
||||
## why
|
||||
|
||||
Contrairement à Google Drive ou iCloud, Proton ne peut pas lire vos fichiers : ils sont chiffrés
|
||||
avant de quitter votre appareil. Juridiction suisse, même compte que Proton Mail.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, comme remplacement direct et simple du cloud de Google ou d'Apple.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Activez Proton Drive dans votre compte Proton et installez l'application.</li>
|
||||
<li>Activez la sauvegarde automatique de vos photos, puis désactivez celle de Google/Apple.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "Proton Mail"
|
||||
slug: "protonmail"
|
||||
category: "email"
|
||||
difficulty: "beginner"
|
||||
url: "https://proton.me/mail"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pm"
|
||||
iconSlug: "protonmail"
|
||||
tags: ["Remplace Gmail","🇨🇭 · open-source · OpenPGP"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une boîte mail chiffrée qui remplace Gmail ou Outlook, et le compte pivot de tout l'écosystème
|
||||
Proton.
|
||||
|
||||
## why
|
||||
|
||||
Basé en Suisse, hors juridiction des « Five Eyes ». Compatible OpenPGP : chiffrement automatique
|
||||
entre comptes Proton et vers les autres services PGP.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. C'est le meilleur point de départ pour quitter Google (voir section 05 pour le
|
||||
reste de la suite).
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Créez un compte sur <b>proton.me</b> (offre gratuite suffisante pour commencer).
|
||||
</li>
|
||||
<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>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Proton Pass"
|
||||
slug: "protonpass"
|
||||
category: "motsdepasse"
|
||||
difficulty: "beginner"
|
||||
url: "https://proton.me/pass"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pp"
|
||||
tags: ["Avec alias e-mail","🇨🇭 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le gestionnaire de mots de passe de l'écosystème Proton, avec des alias e-mail jetables
|
||||
intégrés.
|
||||
|
||||
## why
|
||||
|
||||
Idéal si vous êtes déjà chez Proton : un seul compte pour le mail, le cloud et les mots de
|
||||
passe. Ses alias e-mail vous évitent de donner votre vraie adresse partout.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent tout regrouper chez Proton.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Proton VPN"
|
||||
slug: "protonvpn"
|
||||
category: "vpn"
|
||||
difficulty: "beginner"
|
||||
url: "https://protonvpn.com"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pv"
|
||||
iconSlug: "protonvpn"
|
||||
tags: ["Offre gratuite honnête","🇨🇭 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le VPN de l'écosystème Proton, avec une véritable offre gratuite (rare et honnête).
|
||||
|
||||
## why
|
||||
|
||||
Basé en Suisse, audité, open-source. Son offre gratuite est sans publicité, sans revente de
|
||||
données et sans limite de volume (le nombre de pays est simplement réduit). C'est la porte
|
||||
d'entrée idéale pour un débutant, surtout si vous utilisez déjà Proton Mail.
|
||||
|
||||
## who
|
||||
|
||||
Débutants, et toute personne déjà dans l'écosystème Proton qui veut un seul compte pour tout.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Rendez-vous sur <b>protonvpn.com</b> et connectez-vous avec votre compte Proton (ou créez-en
|
||||
un).
|
||||
</li>
|
||||
<li>Installez l'application (Windows, macOS, Linux, Android, iOS).</li>
|
||||
<li>
|
||||
Utilisez « Quick Connect » ou choisissez un pays, puis activez le kill switch et NetShield
|
||||
(blocage des pubs et traqueurs).
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "Quad9"
|
||||
slug: "quad9"
|
||||
category: "dns"
|
||||
difficulty: "beginner"
|
||||
url: "https://quad9.net"
|
||||
color: "#1a5fb4"
|
||||
monogram: "9"
|
||||
iconSlug: "quad9"
|
||||
tags: ["Anti-maliciel","🇨🇭 à but non lucratif"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un résolveur DNS chiffré, suisse et à but non lucratif, qui bloque en plus les domaines
|
||||
malveillants connus.
|
||||
|
||||
## why
|
||||
|
||||
Juridiction suisse, pas de conservation de votre adresse IP, protection contre l'hameçonnage et
|
||||
les logiciels malveillants. Un excellent réglage « installer et oublier ».
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, en particulier ceux qui veulent la simplicité et une bonne juridiction.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Sur Android : Réglages > Réseau > <b>DNS privé</b> > saisissez <b>dns.quad9.net</b>.
|
||||
</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>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Qubes OS · postmarketOS"
|
||||
slug: "qubes"
|
||||
category: "os"
|
||||
difficulty: "advanced"
|
||||
url: "https://www.qubes-os.org"
|
||||
color: "#3874d8"
|
||||
monogram: "Qb"
|
||||
iconSlug: "qubesos"
|
||||
tags: ["Cloisonnement","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux systèmes de niche : Qubes cloisonne votre PC en machines virtuelles étanches ; postmarketOS
|
||||
fait tourner du Linux sur d'anciens téléphones.
|
||||
|
||||
## why
|
||||
|
||||
Qubes isole chaque activité (travail, banque, navigation risquée) dans son propre compartiment :
|
||||
une compromission n'atteint pas le reste. postmarketOS prolonge la vie de téléphones abandonnés
|
||||
par leur constructeur.
|
||||
|
||||
## who
|
||||
|
||||
Utilisateurs experts avec un modèle de menace très élevé.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "DuckDuckGo · Brave Search · SearXNG"
|
||||
slug: "search"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
url: "https://duckduckgo.com"
|
||||
color: "#de5833"
|
||||
monogram: "🔎"
|
||||
iconSlug: "duckduckgo"
|
||||
tags: ["Remplace Google Search"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des moteurs de recherche qui ne profilent pas chaque requête, contrairement à Google.
|
||||
|
||||
## why
|
||||
|
||||
DuckDuckGo et Brave Search ne conservent pas d'historique lié à votre identité. Startpage vous
|
||||
sert des résultats Google sans le pistage. SearXNG va plus loin : c'est un métamoteur{' '}
|
||||
<b>auto-hébergeable</b>, la souveraineté jusque dans la barre de recherche.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. SearXNG vise ceux qui veulent héberger leur propre moteur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Dans les réglages de votre navigateur, changez le moteur par défaut pour DuckDuckGo, Brave
|
||||
Search ou Startpage.
|
||||
</li>
|
||||
<li>
|
||||
Pour SearXNG : utilisez une instance publique (annuaire sur searx.space) ou hébergez la vôtre
|
||||
(voir section 10).
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Session"
|
||||
slug: "session"
|
||||
category: "messagerie"
|
||||
difficulty: "intermediate"
|
||||
license: "GPL-3.0"
|
||||
url: "https://getsession.org"
|
||||
color: "#178a5a"
|
||||
monogram: "Se"
|
||||
iconSlug: "session"
|
||||
tags: ["Sans numéro","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un chiffrement à la Signal, mais sans numéro de téléphone et avec un routage en oignon.
|
||||
|
||||
## why
|
||||
|
||||
Votre identité est un « Session ID » aléatoire. Le trafic passe par un réseau décentralisé
|
||||
(Lokinet) qui réduit fortement les métadonnées.
|
||||
|
||||
## who
|
||||
|
||||
Quand un numéro de téléphone est un risque. Écosystème plus petit, à réserver aux échanges qui
|
||||
comptent.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez depuis <b>getsession.org</b> (toutes plateformes).
|
||||
</li>
|
||||
<li>
|
||||
« Create account » génère votre Session ID et une <b>phrase de récupération</b>.
|
||||
</li>
|
||||
<li>Notez la phrase de récupération sur papier : c'est la seule clé de votre compte.</li>
|
||||
<li>Partagez votre Session ID (ou QR) à vos contacts.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "Signal"
|
||||
slug: "signal"
|
||||
category: "messagerie"
|
||||
difficulty: "beginner"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://signal.org"
|
||||
color: "#3a76f0"
|
||||
monogram: "S"
|
||||
iconSlug: "signal"
|
||||
tags: ["Remplace WhatsApp","🇺🇸 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie chiffrée pour vos textos, appels, visios et groupes. C'est le remplaçant direct
|
||||
de WhatsApp, en aussi simple.
|
||||
|
||||
## why
|
||||
|
||||
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 conservée, et l'équipe a promis de quitter l'UE
|
||||
plutôt que d'installer un scan.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, dès aujourd'hui. C'est le premier geste et le plus utile. Seule contrainte : un
|
||||
numéro de téléphone, que l'on peut masquer derrière un nom d'utilisateur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez depuis <b>signal.org</b> (App Store, Google Play, ou l'APK direct sur Android).
|
||||
</li>
|
||||
<li>Confirmez votre numéro par SMS, puis choisissez un code PIN.</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>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "SimpleX Chat"
|
||||
slug: "simplex"
|
||||
category: "messagerie"
|
||||
difficulty: "intermediate"
|
||||
url: "https://simplex.chat"
|
||||
color: "#1e63d0"
|
||||
monogram: "SX"
|
||||
iconSlug: "simplex"
|
||||
tags: ["Sans identifiant","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie chiffrée sans aucun compte, sans numéro et sans identifiant. On se connecte en
|
||||
partageant un lien ou un QR code.
|
||||
|
||||
## why
|
||||
|
||||
Par conception, même les serveurs ignorent qui parle à qui. C'est aujourd'hui l'option la plus
|
||||
protectrice contre la corrélation des métadonnées.
|
||||
|
||||
## who
|
||||
|
||||
Comptes pseudonymes, contacts sensibles, haut risque. Un peu plus jeune que Signal, avec
|
||||
quelques aspérités.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez depuis <b>simplex.chat</b> (F-Droid, Google Play, App Store ou APK).
|
||||
</li>
|
||||
<li>
|
||||
Choisissez un nom d'affichage <b>local</b> (jamais votre vrai nom : il ne quitte pas votre
|
||||
appareil).
|
||||
</li>
|
||||
<li>
|
||||
Pour ajouter un contact, partagez un <b>lien d'invitation à usage unique</b> ou un QR code.
|
||||
</li>
|
||||
<li>Activez le verrou par code, et (avancé) configurez vos propres serveurs SMP.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Matrix Synapse"
|
||||
slug: "synapse"
|
||||
category: "selfhost"
|
||||
difficulty: "advanced"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://element.io/server"
|
||||
color: "#0dbd8b"
|
||||
monogram: "Mx"
|
||||
iconSlug: "matrix"
|
||||
tags: ["Votre serveur de chat","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Votre propre serveur de messagerie Matrix, sur lequel tournent Element et les appels chiffrés.
|
||||
|
||||
## why
|
||||
|
||||
En hébergeant le serveur, vous contrôlez aussi les <b>métadonnées</b> (qui parle à qui, quand),
|
||||
le point faible de Matrix en fédération. C'est la messagerie communautaire dont vous êtes le
|
||||
seul maître.
|
||||
|
||||
## who
|
||||
|
||||
Communautés, collectifs, rédactions qui veulent leur espace fédéré indépendant.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: "Tails"
|
||||
slug: "tails"
|
||||
category: "os"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-3.0"
|
||||
url: "https://tails.net"
|
||||
color: "#56347c"
|
||||
monogram: "Ta"
|
||||
iconSlug: "tails"
|
||||
tags: ["Clé USB amnésique","🌍 · tout via Tor"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un système qui démarre depuis une clé USB, fait passer tout le trafic par Tor et ne laisse{' '}
|
||||
<b>aucune trace</b> à l'extinction.
|
||||
|
||||
## why
|
||||
|
||||
À l'arrêt, tout est oublié : c'est « amnésique ». Idéal pour un travail sensible et ponctuel sur
|
||||
un ordinateur qui n'est pas le vôtre, sans rien y laisser.
|
||||
|
||||
## who
|
||||
|
||||
Lanceurs d'alerte, journalistes, sources. L'outil de référence pour les missions à haut risque.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Téléchargez Tails sur <b>tails.net</b> et suivez l'assistant pour créer la clé USB.
|
||||
</li>
|
||||
<li>Redémarrez l'ordinateur sur cette clé. Utilisez-le, puis éteignez : tout disparaît.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "Tailscale · Headscale · WireGuard"
|
||||
slug: "tailscale"
|
||||
category: "vpn"
|
||||
difficulty: "advanced"
|
||||
license: "BSD-3-Clause"
|
||||
url: "https://tailscale.com"
|
||||
color: "#242424"
|
||||
monogram: "Ts"
|
||||
iconSlug: "tailscale"
|
||||
tags: ["Accès privé à son serveur","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un réseau privé chiffré qui relie vos appareils à votre serveur, sans jamais l'exposer sur
|
||||
l'Internet public.
|
||||
|
||||
## why
|
||||
|
||||
Au lieu d'ouvrir des ports (et de vous faire attaquer), <b>Tailscale</b> crée un tunnel
|
||||
WireGuard entre vos seuls appareils autorisés : votre serveur n'a aucune surface d'attaque
|
||||
publique. <b>Headscale</b> est la version que vous hébergez vous-même, pour ne dépendre d'aucun
|
||||
tiers. <b>WireGuard</b> seul offre le contrôle total, au prix d'une configuration manuelle.
|
||||
|
||||
## who
|
||||
|
||||
Tout auto-hébergeur. C'est la façon sûre d'atteindre son Nextcloud ou ses photos depuis
|
||||
l'extérieur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez Tailscale sur votre serveur et sur votre téléphone/PC, connectez-les au même compte.
|
||||
</li>
|
||||
<li>
|
||||
Vos appareils se voient aussitôt sur un réseau privé ; accédez à votre serveur par son adresse
|
||||
Tailscale.
|
||||
</li>
|
||||
<li>Pour zéro dépendance, remplacez le serveur de coordination par Headscale, auto-hébergé.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: "Tor Browser"
|
||||
slug: "tor"
|
||||
category: "tor"
|
||||
difficulty: "advanced"
|
||||
license: "BSD-3-Clause"
|
||||
url: "https://www.torproject.org"
|
||||
color: "#7d4698"
|
||||
monogram: "Tor"
|
||||
iconSlug: "torbrowser"
|
||||
tags: ["Anonymat réseau","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un réseau qui sépare qui vous êtes de ce que vous faites en ligne, accessible via le Tor
|
||||
Browser.
|
||||
|
||||
## why
|
||||
|
||||
Votre trafic rebondit à travers plusieurs relais chiffrés dans le monde : aucun ne connaît à la
|
||||
fois votre identité et votre destination. C'est l'outil des journalistes et des sources, et le
|
||||
moyen d'atteindre un site bloqué (par exemple pour télécharger un VPN censuré).
|
||||
|
||||
## who
|
||||
|
||||
Profils à haut risque, et quiconque doit dissocier son activité de son adresse IP réelle. À
|
||||
utiliser ponctuellement, pas comme navigateur de tous les jours.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Téléchargez le Tor Browser sur <b>torproject.org</b> (Windows, macOS, Linux, Android).
|
||||
</li>
|
||||
<li>
|
||||
Lancez-le et cliquez « Se connecter ». Si Tor est bloqué dans votre pays, activez un « pont »
|
||||
(bridge).
|
||||
</li>
|
||||
<li>
|
||||
Naviguez sans maximiser la fenêtre, sans installer d'extension, et{' '}
|
||||
<b>sans vous connecter à vos vrais comptes</b>.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Tuta"
|
||||
slug: "tuta"
|
||||
category: "email"
|
||||
difficulty: "beginner"
|
||||
license: "GPL-3.0"
|
||||
url: "https://tuta.com"
|
||||
color: "#b01e2e"
|
||||
monogram: "Tu"
|
||||
iconSlug: "tuta"
|
||||
tags: ["Remplace Gmail","🇩🇪 · open-source · post-quantique"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie e-mail allemande, entièrement open-source et sans aucun service Google.
|
||||
|
||||
## why
|
||||
|
||||
Elle chiffre aussi <b>l'objet et une partie des métadonnées</b>, avec un chiffrement résistant à
|
||||
l'informatique quantique. Juridiction UE.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent le maximum de métadonnées chiffrées et du 100 % libre. Elle n'utilise pas PGP :
|
||||
pour écrire chiffré à l'extérieur, on protège le message par mot de passe.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Créez un compte sur <b>tuta.com</b> et installez l'application (disponible sur F-Droid).
|
||||
</li>
|
||||
<li>
|
||||
Pour écrire à un non-utilisateur de façon chiffrée : cliquez sur le cadenas et définissez un
|
||||
mot de passe, transmis à votre correspondant par un autre canal.
|
||||
</li>
|
||||
<li>Activez la double authentification.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: "uBlock Origin"
|
||||
slug: "ublock"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
url: "https://ublockorigin.com"
|
||||
color: "#7a0d0d"
|
||||
monogram: "uB"
|
||||
iconSlug: "ublockorigin"
|
||||
tags: ["Extension","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
L'extension de navigateur qui bloque publicités, traqueurs et scripts espions. Le geste le plus
|
||||
rentable, en dix secondes.
|
||||
|
||||
## why
|
||||
|
||||
La publicité en ligne est le principal vecteur de surveillance et de profilage. La bloquer coupe
|
||||
l'essentiel du pistage, accélère les pages et réduit les risques de logiciels malveillants.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, sur Firefox comme sur les navigateurs Chromium.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Ouvrez le magasin d'extensions de votre navigateur et cherchez « uBlock Origin » (l'original,
|
||||
par Raymond Hill).
|
||||
</li>
|
||||
<li>Installez-le. Il fonctionne aussitôt, sans réglage nécessaire.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "Jitsi Meet · Element Call"
|
||||
slug: "visio"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://jitsi.org"
|
||||
color: "#1d6fb8"
|
||||
monogram: "Vi"
|
||||
iconSlug: "jitsi"
|
||||
tags: ["Remplace Zoom / Meet","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des outils de visioconférence sans compte ni installation lourde, pour remplacer Zoom, Google
|
||||
Meet ou Teams.
|
||||
|
||||
## why
|
||||
|
||||
<b>Jitsi Meet</b> se lance dans le navigateur, sans compte, et peut être auto-hébergé (c'est
|
||||
aussi la base de Brave Talk). <b>Element Call</b> est chiffré de bout en bout et fédéré via
|
||||
Matrix. Pour un appel simple et totalement chiffré, les appels <b>Signal</b> font aussi le
|
||||
travail.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. Sur un serveur Jitsi public, les appels à plusieurs sont chiffrés en transport ;
|
||||
le chiffrement de bout en bout est en option.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Jitsi : allez sur meet.jit.si, créez un nom de salon, partagez le lien. Rien à installer.
|
||||
</li>
|
||||
<li>Element Call : depuis l'application Element (Matrix), lancez un appel dans un salon.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: "Votre propre site web"
|
||||
slug: "website"
|
||||
category: "selfhost"
|
||||
difficulty: "intermediate"
|
||||
url: "https://astro.build"
|
||||
color: "#7a4ddb"
|
||||
monogram: "www"
|
||||
tags: ["Résister au bannissement"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un site à vous, sur votre nom de domaine, où votre contenu ne dépend d'aucune plateforme.
|
||||
|
||||
## why
|
||||
|
||||
Le jour où un réseau supprime ou suspend votre compte, tout votre travail disparaît. Un site
|
||||
personnel est votre point d'ancrage : vos articles y restent, archivés et à jour, quoi qu'il
|
||||
arrive. C'est exactement ce que font les comptes d'information prudents. Un site statique
|
||||
(généré par Hugo, Astro ou Jekyll) suffit et se publie presque gratuitement ; pour un blog
|
||||
dynamique, un WordPress ou un Ghost auto-hébergé fait l'affaire.
|
||||
|
||||
## who
|
||||
|
||||
Comptes d'information, créateurs, militants, quiconque publie et craint la censure de
|
||||
plateforme.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Achetez un nom de domaine (idéalement chez un registraire respectueux de la vie privée).
|
||||
</li>
|
||||
<li>
|
||||
Choisissez un générateur de site statique (Astro, Hugo) et hébergez le résultat, ou installez
|
||||
WordPress/Ghost sur votre serveur.
|
||||
</li>
|
||||
<li>
|
||||
Renvoyez-y systématiquement depuis vos réseaux : votre audience vous retrouve même si un
|
||||
compte tombe.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "YubiKey · Nitrokey"
|
||||
slug: "yubikey"
|
||||
category: "deuxfa"
|
||||
difficulty: "intermediate"
|
||||
url: "https://www.yubico.com"
|
||||
color: "#84bd00"
|
||||
monogram: "Yk"
|
||||
iconSlug: "yubico"
|
||||
tags: ["Clé matérielle FIDO2"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une petite clé physique (format USB, souvent avec NFC) qui prouve votre identité d'un simple
|
||||
contact du doigt.
|
||||
|
||||
## why
|
||||
|
||||
C'est la 2FA la plus solide : elle résiste à l'hameçonnage, car la clé vérifie l'adresse réelle
|
||||
du site avant de répondre. <b>Nitrokey</b> est l'alternative à matériel et micrologiciel
|
||||
entièrement ouverts. Un faux site ne peut pas vous piéger.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent le maximum de sécurité sur leurs comptes clés. Achetez-en <b>deux</b> (une
|
||||
principale, une de secours).
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Procurez-vous deux clés sur yubico.com ou nitrokey.com.</li>
|
||||
<li>
|
||||
Dans les réglages de sécurité de chaque compte (e-mail, gestionnaire de mots de passe…),
|
||||
ajoutez une « clé de sécurité » ou « passkey ». Enregistrez vos deux clés.
|
||||
</li>
|
||||
<li>Gardez la clé de secours dans un lieu sûr, séparé.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "YunoHost · Umbrel · Start9"
|
||||
slug: "yunohost"
|
||||
category: "selfhost"
|
||||
difficulty: "advanced"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://yunohost.org"
|
||||
color: "#e29100"
|
||||
monogram: "Yh"
|
||||
tags: ["Serveur clé en main","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des systèmes qui transforment un vieux PC ou un Raspberry Pi en serveur personnel, avec un
|
||||
catalogue d'applications à installer en quelques clics.
|
||||
|
||||
## why
|
||||
|
||||
L'auto-hébergement fait peur, mais ces outils font le gros du travail. <b>YunoHost</b> gère même
|
||||
votre propre e-mail et une authentification unique. <b>Umbrel</b> offre un magasin de plus de
|
||||
300 applications, très prisé des amateurs de vie privée et de Bitcoin. <b>Start9</b> chiffre les
|
||||
sauvegardes et donne une adresse Tor à chaque service par défaut.
|
||||
|
||||
## who
|
||||
|
||||
Les profils avancés prêts à consacrer un après-midi à monter leur propre cloud. C'est la parade
|
||||
structurelle la plus radicale à Chat Control.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Récupérez un vieil ordinateur, un Raspberry Pi, ou louez un petit serveur (VPS) dans un pays
|
||||
protecteur.
|
||||
</li>
|
||||
<li>Installez YunoHost, Umbrel ou Start9 en suivant leur guide officiel.</li>
|
||||
<li>Depuis le catalogue, installez Nextcloud, un serveur Matrix, une galerie photo, etc.</li>
|
||||
<li>
|
||||
Accédez-y en privé avec Tailscale (section VPN) plutôt que d'exposer le serveur au public.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,76 @@
|
||||
# Guide de rédaction — fiches d'outils FR/créole
|
||||
|
||||
## Format d'une fiche (`content/outils/<slug>.md`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: "Signal"
|
||||
slug: "signal"
|
||||
category: "messagerie" # slug d'une catégorie de content/categories.json
|
||||
difficulty: "beginner" # beginner | intermediate | advanced
|
||||
license: "AGPL-3.0" # identifiant SPDX exact (spdx.org/licenses)
|
||||
url: "https://signal.org"
|
||||
color: "#3a76f0" # couleur du monogramme (décorative)
|
||||
monogram: "S"
|
||||
tags: ["Remplace WhatsApp","🇺🇸 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
À quoi sert l'outil, en deux phrases maximum.
|
||||
|
||||
## why
|
||||
Pourquoi il compte pour la souveraineté numérique.
|
||||
|
||||
## who
|
||||
Pour qui, et à quel moment du parcours.
|
||||
|
||||
## install
|
||||
<ol><li>Étapes concrètes, numérotées.</li></ol>
|
||||
```
|
||||
|
||||
Les valeurs du frontmatter sont encodées en JSON (guillemets doubles).
|
||||
Le corps accepte Markdown et HTML simple ; il est rendu au build, rien n'est
|
||||
interprété côté client. **Le build échoue si une fiche est invalide** — c'est
|
||||
le contrat de validation (ADR-001).
|
||||
|
||||
## Ton éditorial
|
||||
|
||||
- **Militant mais chaleureux, ancré Caraïbe.** Pas de langage corporate, pas
|
||||
de peur gratuite : on explique le geste, on donne le pouvoir d'agir.
|
||||
- Tutoiement. Phrases courtes. Le premier mot de chaque section porte le sens
|
||||
(motif en F, doctrine §1.1).
|
||||
- `what` dit le service rendu, `why` dit l'enjeu politique/technique, `who`
|
||||
donne la porte d'entrée (« commence ici si… »), `install` est actionnable
|
||||
sans autre lecture.
|
||||
- Exemples et localisation ancrés territoire — jamais de Lorem ipsum
|
||||
hexagonal (doctrine §4.2).
|
||||
|
||||
## Créole guadeloupéen (gcf)
|
||||
|
||||
- Les **micro-textes de l'interface** (boutons, filtres, états vides, marqueur
|
||||
de fin) vivent dans `frontend/src/lib/i18n/gcf.json`. C'est la couche à
|
||||
plus fort impact affectif pour le plus faible coût (doctrine §4.2) — elle
|
||||
est traduite en premier.
|
||||
- ⚠️ **Relecture par un locuteur natif requise** : les chaînes gcf actuelles
|
||||
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.
|
||||
- La traduction intégrale des fiches est un chantier éditorial séparé :
|
||||
le format prévu est un fichier jumeau `content/outils/gcf/<slug>.md` avec
|
||||
`lang: "gcf"` (le chargeur l'ignorera tant que la locale de contenu n'est
|
||||
pas activée).
|
||||
- Termes fixés : « zouti » (outil), « Ou pa manké ayen » (fin de liste),
|
||||
« Chwazi zouti ou » (héros), niveaux « Débitan / Entèmédia / Avansé ».
|
||||
|
||||
## Ajouter une catégorie
|
||||
|
||||
Ajouter une entrée à `content/categories.json` (`slug`, `title`, `order`,
|
||||
`part`). La navigation n'affiche que 4 catégories avant la divulgation
|
||||
progressive (Hick-Hyman, doctrine §1.3) : l'ordre `order` décide lesquelles.
|
||||
|
||||
## Vérifications avant merge
|
||||
|
||||
- [ ] `license` est un identifiant SPDX valide
|
||||
- [ ] `difficulty` reflète l'expérience réelle d'installation (pas le prestige)
|
||||
- [ ] Chaque section respecte sa question (what/why/who/install)
|
||||
- [ ] `pnpm build` passe (validation du frontmatter incluse)
|
||||
@@ -0,0 +1,72 @@
|
||||
# Design system OKI — exitchatcontrol.org
|
||||
|
||||
Toutes les valeurs visuelles vivent dans
|
||||
[`frontend/src/styles/tokens.css`](../frontend/src/styles/tokens.css).
|
||||
Les composants Svelte consomment les tokens ; **jamais de valeur brute**
|
||||
(couleur, taille) dans un composant (doctrine §5.2).
|
||||
|
||||
## Architecture CSS
|
||||
|
||||
Cascade explicite par `@layer` ([app.css](../frontend/src/app.css)) :
|
||||
|
||||
```
|
||||
reset → tokens → base → layouts → components → utilities
|
||||
```
|
||||
|
||||
- `reset.css` — reset moderne minimal (~40 lignes), inclut `prefers-reduced-motion`.
|
||||
- `tokens.css` — custom properties : couleurs `oklch()`, typo fluide `clamp()`, espacements, interaction.
|
||||
- `base.css` — HTML sémantique nu : typo, liens, focus visible, boutons.
|
||||
- `layouts.css` — primitives Every Layout : `.stack`, `.cluster`, `.center`, `.card-grid` (container queries, pas de media queries de largeur d'écran).
|
||||
- Le style propre à chaque composant vit dans son `<style>` Svelte (scopé à la compilation).
|
||||
|
||||
## Couleurs
|
||||
|
||||
En `oklch()` : la luminance perceptuelle rend les contrastes WCAG calculables
|
||||
(méthode modus-vivendi, doctrine §4.3). `light-dark()` + `color-scheme` gèrent
|
||||
le mode sombre sans duplication.
|
||||
|
||||
| Token | Rôle | Note contraste |
|
||||
|---|---|---|
|
||||
| `--oki-ink` / `--oki-surface` | texte / fond | ≥ 7:1 (AAA) dans les deux modes |
|
||||
| `--oki-ink-2` | texte secondaire | ≥ 4.5:1 (AA) |
|
||||
| `--oki-jaune` `--oki-vert` `--oki-rouge` `--oki-noir` | identité panafricaine | décoratif — jamais porteur d'information seul |
|
||||
| `--accent` / `--accent-ink` | accent actif / accent texte | `--accent-ink` ≥ 4.5:1 sur `--oki-surface` |
|
||||
| `--diff-*` / `--diff-*-ink` | difficulté (fond / texte) | les variantes `-ink` sont AA sur `--oki-surface` |
|
||||
|
||||
**Règle absolue** : la difficulté n'est jamais codée par la couleur seule —
|
||||
`DifficultyBadge` = forme (●/◆/▲) + texte + couleur (doctrine §1.1, ~8 % de
|
||||
déficience de vision des couleurs).
|
||||
|
||||
## Typographie
|
||||
|
||||
- Corps : **Atkinson Hyperlegible** 400 (SIL OFL, conçue pour la lisibilité).
|
||||
- Display (titres, marque) : **Fraunces** 700 (SIL OFL).
|
||||
- 2 fichiers WOFF2 auto-hébergés (`static/fonts/`), ~35 Ko au total,
|
||||
`font-display: swap` — jamais de Google Fonts (doctrine §5.3).
|
||||
- Échelle modulaire ratio 1.25, fluide par `clamp()` : `--text-0` … `--text-5`.
|
||||
- `--measure: 65ch` sur `p, li` et `.prose` (doctrine §2.2 : 45–75 caractères).
|
||||
- Tout en `rem` : la taille utilisateur est respectée.
|
||||
|
||||
## Interaction
|
||||
|
||||
- `--tap-target: 48px` : tout élément interactif (boutons, liens de nav,
|
||||
filtres, switcher) a une hauteur minimale de 48 px (Fitts, doctrine §1.3).
|
||||
- `:focus-visible` global : anneau 3 px `--accent`, offset 2 px.
|
||||
- Durées `--duration-1/2` + `--ease-out` ; annulées par `prefers-reduced-motion`.
|
||||
|
||||
## Composants
|
||||
|
||||
| Composant | Rôle | Principe doctrinal |
|
||||
|---|---|---|
|
||||
| `DifficultyBadge` | niveau (forme + texte + couleur) | §1.1 — jamais couleur seule |
|
||||
| `ToolCard` | fiche condensée, vrai `<a>` couvrant | §4.3 — pas de div cliquable |
|
||||
| `CategoryNav` | 4 catégories visibles + `<details>` | §1.3 Hick-Hyman, divulgation progressive, sans JS |
|
||||
| `SearchBar` | recherche instantanée, raccourci `/` | §2.2 — le clavier est roi |
|
||||
| `LanguageSwitcher` | FR / Kréyòl, persisté localStorage | §4.2 — bilinguisme |
|
||||
| `EndMarker` | « Ou pa manké ayen » en fin de liste | §1.5, §3 — point d'arrêt explicite |
|
||||
|
||||
## Test des 50 ms (doctrine §1.1)
|
||||
|
||||
L'identité OKI est reconnaissable avant toute lecture : bandes panafricaines
|
||||
vert/jaune/rouge dans la marque d'en-tête, soulignés et bordures jaune OKI,
|
||||
titres Fraunces, créole visible dès le héros (« Chwazi zouti ou »).
|
||||
@@ -0,0 +1,40 @@
|
||||
# exitchatcontrol × OKI — frontend
|
||||
|
||||
Annuaire souverain d'outils libres. SvelteKit 2 + Svelte 5, site 100 %
|
||||
prérendu, zéro CDN tiers, zéro tracker. Voir [`ADR.md`](../ADR.md) pour la
|
||||
décision d'architecture et `../docs/` pour le design system et le guide
|
||||
de contenu.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Node ≥ 22.12, pnpm.
|
||||
|
||||
## Commandes
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm migrate # (re)génère content/ depuis l'ancien site Astro (src/)
|
||||
pnpm dev # serveur de développement
|
||||
pnpm build # build statique dans build/ (prérend les 58 pages)
|
||||
pnpm preview # sert le build localement
|
||||
pnpm check # svelte-check (types + a11y)
|
||||
```
|
||||
|
||||
## Déploiement
|
||||
|
||||
`build/` est un site statique : n'importe quel serveur de fichiers convient
|
||||
(nginx du `docker/` existant, YunoHost `my_webapp`, etc.). Aucune dépendance
|
||||
cloud, aucun service à maintenir — un miroir = une copie du dossier.
|
||||
|
||||
## Arborescence
|
||||
|
||||
```
|
||||
src/styles/ tokens, reset, base, layouts (cascade @layer)
|
||||
src/lib/i18n/ fr.json + gcf.json typés, store de langue (runes)
|
||||
src/lib/components/ ToolCard, DifficultyBadge, CategoryNav, SearchBar,
|
||||
LanguageSwitcher, EndMarker
|
||||
src/lib/content/ chargeur build-time des fichiers ../content/
|
||||
src/routes/ / (landing), /outils (annuaire filtrable), /outils/[slug]
|
||||
static/fonts/ 2 WOFF2 auto-hébergés (Atkinson Hyperlegible, Fraunces)
|
||||
scripts/ migrate-tools.mjs
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "exitchatcontrol-oki",
|
||||
"private": true,
|
||||
"version": "4.0.0-alpha.1",
|
||||
"type": "module",
|
||||
"description": "Exit Chat Control × OKI — annuaire souverain d'outils libres. SvelteKit statique, zéro tracker, zéro CDN.",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"migrate": "node scripts/migrate-tools.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/atkinson-hyperlegible": "^5.2.8",
|
||||
"@fontsource/fraunces": "^5.2.9",
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@types/node": "^26.1.1",
|
||||
"marked": "^16.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
Generated
+1076
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Migration des données du site Astro vers les fichiers plats OKI (ADR-001,
|
||||
* mission §7). Extrait les 55 ToolCard des MDX français + métadonnées de
|
||||
* src/data/{tools,directory}.json et produit :
|
||||
* content/outils/<slug>.md (frontmatter JSON-par-ligne + sections HTML)
|
||||
* content/categories.json (catégories dérivées des sections sources)
|
||||
*
|
||||
* Idempotent : relançable, écrase la sortie. Usage : pnpm migrate
|
||||
*/
|
||||
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const SRC = path.join(ROOT, 'src')
|
||||
const OUT = path.join(ROOT, 'content')
|
||||
|
||||
const DIFFICULTY = { 1: 'beginner', 2: 'intermediate', 3: 'advanced' }
|
||||
|
||||
const tools = JSON.parse(await readFile(path.join(SRC, 'data', 'tools.json'), 'utf8'))
|
||||
const directory = JSON.parse(await readFile(path.join(SRC, 'data', 'directory.json'), 'utf8'))
|
||||
const toolById = new Map(tools.map((t) => [t.id, t]))
|
||||
const dirById = new Map(directory.map((d) => [d.id, d]))
|
||||
|
||||
function parseFrontmatter(raw) {
|
||||
const m = raw.match(/^---\n([\s\S]*?)\n---\n?/)
|
||||
if (!m) return [{}, raw]
|
||||
const meta = {}
|
||||
for (const line of m[1].split('\n')) {
|
||||
const i = line.indexOf(':')
|
||||
if (i === -1) continue
|
||||
const key = line.slice(0, i).trim()
|
||||
let value = line.slice(i + 1).trim()
|
||||
if (/^'.*'$/.test(value)) value = value.slice(1, -1)
|
||||
meta[key] = /^\d+$/.test(value) ? Number(value) : value
|
||||
}
|
||||
return [meta, raw.slice(m[0].length)]
|
||||
}
|
||||
|
||||
/** Extrait le contenu d'un slot (Fragment/ol/ul) dans un bloc ToolCard. */
|
||||
function slot(block, name) {
|
||||
const re = new RegExp(
|
||||
`<(Fragment|ol|ul)([^>]*slot="${name}"[^>]*)>([\\s\\S]*?)</\\1>`
|
||||
)
|
||||
const m = block.match(re)
|
||||
if (!m) return null
|
||||
// Les listes gardent leur balise (le HTML passe tel quel dans le markdown) ;
|
||||
// les Fragments sont déballés.
|
||||
return m[1] === 'Fragment' ? m[3].trim() : `<${m[1]}>${m[3]}</${m[1]}>`.trim()
|
||||
}
|
||||
|
||||
/** Tags = textes des <span class="tool-tag"> hors difficulté (déjà structurée). */
|
||||
function extractTags(block) {
|
||||
const html = slot(block, 'tags') ?? ''
|
||||
const tags = [...html.matchAll(/<span class="tool-tag">([\s\S]*?)<\/span>/g)]
|
||||
.map((m) => m[1].replace(/\s+/g, ' ').trim())
|
||||
.filter((t) => !/^[🟢🟡🔴]/u.test(t))
|
||||
return tags
|
||||
}
|
||||
|
||||
/** Difficulté : `level` de tools.json, sinon l'emoji du tag MDX (🟢/🟡/🔴). */
|
||||
function difficulty(tool, block) {
|
||||
if (DIFFICULTY[tool.level]) return DIFFICULTY[tool.level]
|
||||
const html = slot(block, 'tags') ?? ''
|
||||
if (html.includes('🟢')) return 'beginner'
|
||||
if (html.includes('🟡')) return 'intermediate'
|
||||
if (html.includes('🔴')) return 'advanced'
|
||||
console.warn(`⚠ ${tool.id} : difficulté introuvable — « intermediate » par défaut`)
|
||||
return 'intermediate'
|
||||
}
|
||||
|
||||
const fm = (key, value) => (value == null ? null : `${key}: ${JSON.stringify(value)}`)
|
||||
|
||||
const sectionsDir = path.join(SRC, 'content', 'sections', 'fr')
|
||||
const files = (await readdir(sectionsDir)).filter((f) => f.endsWith('.mdx')).sort()
|
||||
|
||||
await mkdir(path.join(OUT, 'outils'), { recursive: true })
|
||||
|
||||
const categories = []
|
||||
const seen = new Set()
|
||||
let written = 0
|
||||
|
||||
for (const file of files) {
|
||||
const raw = await readFile(path.join(sectionsDir, file), 'utf8')
|
||||
const [meta, body] = parseFrontmatter(raw)
|
||||
const blocks = [...body.matchAll(/<ToolCard tool="([^"]+)"[^>]*>([\s\S]*?)<\/ToolCard>/g)]
|
||||
if (blocks.length === 0) continue
|
||||
|
||||
categories.push({
|
||||
slug: meta.id,
|
||||
title: meta.title,
|
||||
order: meta.order,
|
||||
part: meta.part
|
||||
})
|
||||
|
||||
for (const [, toolId, block] of blocks) {
|
||||
const tool = toolById.get(toolId)
|
||||
if (!tool) {
|
||||
console.warn(`⚠ ${file} : ToolCard "${toolId}" absent de tools.json — ignoré`)
|
||||
continue
|
||||
}
|
||||
const slug = toolId.replace(/^t-/, '')
|
||||
if (seen.has(slug)) {
|
||||
console.warn(`⚠ ${slug} : doublon (déjà émis) — ignoré dans ${file}`)
|
||||
continue
|
||||
}
|
||||
seen.add(slug)
|
||||
const dir = dirById.get(slug)
|
||||
const nameOverride = slot(block, 'name')
|
||||
|
||||
const front = [
|
||||
fm('name', nameOverride ?? tool.name),
|
||||
fm('slug', slug),
|
||||
fm('category', meta.id),
|
||||
fm('difficulty', difficulty(tool, block)),
|
||||
fm('license', dir?.license),
|
||||
fm('url', tool.url),
|
||||
fm('color', tool.color),
|
||||
fm('monogram', tool.monogram),
|
||||
fm('iconSlug', tool.iconSlug),
|
||||
fm('tags', extractTags(block)),
|
||||
fm('lang', 'fr')
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
const sections = [
|
||||
['what', slot(block, 'what')],
|
||||
['why', slot(block, 'why')],
|
||||
['who', slot(block, 'who')],
|
||||
['install', slot(block, 'install')],
|
||||
['extra', slot(block, 'extra')]
|
||||
]
|
||||
.filter(([, html]) => html)
|
||||
.map(([key, html]) => `## ${key}\n\n${html}`)
|
||||
.join('\n\n')
|
||||
|
||||
await writeFile(path.join(OUT, 'outils', `${slug}.md`), `---\n${front}\n---\n\n${sections}\n`)
|
||||
written++
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(path.join(OUT, 'categories.json'), JSON.stringify(categories, null, 2) + '\n')
|
||||
console.log(`✔ ${written} fiches écrites dans content/outils/, ${categories.length} catégories`)
|
||||
|
||||
const missing = tools.filter((t) => !seen.has(t.id.replace(/^t-/, '')))
|
||||
if (missing.length) {
|
||||
console.warn(`⚠ outils de tools.json sans ToolCard fr : ${missing.map((t) => t.id).join(', ')}`)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* app.css — ordre de cascade explicite et auditable (doctrine §5.2).
|
||||
* Chaque couche est déclarée AVANT tout import : la position dans la
|
||||
* cascade est ici, pas dans l'ordre des fichiers.
|
||||
*/
|
||||
@layer reset, tokens, base, layouts, components, utilities;
|
||||
|
||||
/* @font-face n'est pas concerné par les couches */
|
||||
@import './styles/fonts.css';
|
||||
|
||||
@import './styles/reset.css' layer(reset);
|
||||
@import './styles/tokens.css' layer(tokens);
|
||||
@import './styles/base.css' layer(base);
|
||||
@import './styles/layouts.css' layer(layouts);
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#17181f" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { page } from '$app/state'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
interface NavCategory {
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
let { categories }: { categories: NavCategory[] } = $props()
|
||||
|
||||
// Hick-Hyman (doctrine §1.3) : ≤ 5 entrées visibles, le reste en
|
||||
// divulgation progressive (<details>, fonctionne sans JS).
|
||||
const VISIBLE = 4
|
||||
let visible = $derived(categories.slice(0, VISIBLE))
|
||||
let rest = $derived(categories.slice(VISIBLE))
|
||||
// searchParams non lisibles au prerender : garde `browser`
|
||||
let current = $derived(browser ? page.url.searchParams.get('cat') : null)
|
||||
|
||||
function href(slug: string | null): string {
|
||||
return slug ? `/outils?cat=${slug}` : '/outils'
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav aria-label={t('directory.filterCategory')} class="catnav">
|
||||
<ul class="cluster">
|
||||
<li><a href={href(null)} aria-current={current === null ? 'page' : undefined}>{t('directory.allCategories')}</a></li>
|
||||
{#each visible as cat (cat.slug)}
|
||||
<li>
|
||||
<a href={href(cat.slug)} aria-current={current === cat.slug ? 'page' : undefined}>{cat.title}</a>
|
||||
</li>
|
||||
{/each}
|
||||
{#if rest.length > 0}
|
||||
<li>
|
||||
<details>
|
||||
<summary>+ {rest.length}</summary>
|
||||
<ul class="stack">
|
||||
{#each rest as cat (cat.slug)}
|
||||
<li>
|
||||
<a href={href(cat.slug)} aria-current={current === cat.slug ? 'page' : undefined}>{cat.title}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
a[aria-current='page'] {
|
||||
background: var(--oki-jaune);
|
||||
border-color: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
details {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px dashed var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details ul {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset-inline-start: 0;
|
||||
margin-block-start: var(--space-1);
|
||||
padding: var(--space-2);
|
||||
background: var(--oki-surface);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
min-inline-size: 16rem;
|
||||
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.15);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
// Jamais la couleur seule : forme + texte + couleur (doctrine §1.1 —
|
||||
// ~8 % de déficience de vision des couleurs).
|
||||
const SHAPES = { beginner: '●', intermediate: '◆', advanced: '▲' } as const
|
||||
|
||||
let { difficulty }: { difficulty: keyof typeof SHAPES } = $props()
|
||||
</script>
|
||||
|
||||
<span class="badge badge-{difficulty}">
|
||||
<span aria-hidden="true">{SHAPES[difficulty]}</span>
|
||||
{t(`difficulty.${difficulty}`)}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-size: var(--text-0);
|
||||
font-weight: 600;
|
||||
padding: 0.15em 0.6em;
|
||||
border-radius: var(--radius-2);
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
.badge-beginner {
|
||||
color: var(--diff-beginner-ink);
|
||||
}
|
||||
.badge-intermediate {
|
||||
color: var(--diff-intermediate-ink);
|
||||
}
|
||||
.badge-advanced {
|
||||
color: var(--diff-advanced-ink);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
</script>
|
||||
|
||||
<!-- Marqueur de fin explicite : restitue le point d'arrêt naturel que le
|
||||
scroll infini supprime (doctrine §1.5 et §3.2). -->
|
||||
<p class="end" role="status">
|
||||
<strong>{t('directory.endOfList')}</strong>
|
||||
<span>{t('directory.endOfListSub')}</span>
|
||||
</p>
|
||||
|
||||
<style>
|
||||
.end {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
text-align: center;
|
||||
padding-block: var(--space-5);
|
||||
border-top: 4px double var(--oki-line);
|
||||
margin-block-start: var(--space-5);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2);
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { getLocale, setLocale, locales, localeNames, t } from '$lib/i18n/index.svelte'
|
||||
</script>
|
||||
|
||||
<!-- Bascule explicite FR / Kréyòl, persistée en localStorage (mission §4.3).
|
||||
Amélioration progressive : sans JS, le site reste lisible en français. -->
|
||||
<div class="switch" role="group" aria-label={t('lang.switchLabel')}>
|
||||
{#each locales as locale (locale)}
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={getLocale() === locale}
|
||||
onclick={() => setLocale(locale)}
|
||||
>
|
||||
{localeNames[locale]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
min-height: var(--tap-target);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
button[aria-pressed='true'] {
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { value = $bindable('') }: { value?: string } = $props()
|
||||
let input: HTMLInputElement
|
||||
|
||||
// raccourci « / » desktop (doctrine §2.2 — le clavier est roi)
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
if (e.key === '/' && document.activeElement !== input) {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest('input, textarea, [contenteditable]')) return
|
||||
e.preventDefault()
|
||||
input.focus()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window {onkeydown} />
|
||||
|
||||
<p class="search">
|
||||
<label>
|
||||
<span class="visually-hidden">{t('search.label')}</span>
|
||||
<input
|
||||
bind:this={input}
|
||||
bind:value
|
||||
type="search"
|
||||
placeholder={t('search.placeholder')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<style>
|
||||
input {
|
||||
inline-size: 100%;
|
||||
max-inline-size: 28rem;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import DifficultyBadge from './DifficultyBadge.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
interface CardTool {
|
||||
name: string
|
||||
slug: string
|
||||
difficulty: 'beginner' | 'intermediate' | 'advanced'
|
||||
license?: string
|
||||
color?: string
|
||||
monogram?: string
|
||||
tags: string[]
|
||||
excerpt: string
|
||||
}
|
||||
|
||||
let { tool }: { tool: CardTool } = $props()
|
||||
</script>
|
||||
|
||||
<article class="card">
|
||||
<header class="cluster">
|
||||
<span class="monogram" style:--tool-color={tool.color} aria-hidden="true">
|
||||
{tool.monogram ?? tool.name.slice(0, 2)}
|
||||
</span>
|
||||
<h3><a href="/outils/{tool.slug}">{tool.name}</a></h3>
|
||||
</header>
|
||||
|
||||
<p class="excerpt"><span class="visually-hidden">{t('tool.why')} : </span>{tool.excerpt}</p>
|
||||
|
||||
<footer class="cluster">
|
||||
<DifficultyBadge difficulty={tool.difficulty} />
|
||||
{#if tool.license}
|
||||
<span class="meta">{tool.license}</span>
|
||||
{/if}
|
||||
{#each tool.tags as tag (tag)}
|
||||
<span class="meta">{tag}</span>
|
||||
{/each}
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-left: 4px solid var(--oki-jaune);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monogram {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 2.5rem;
|
||||
block-size: 2.5rem;
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--tool-color, var(--oki-surface-2));
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-2);
|
||||
}
|
||||
|
||||
/* toute la carte est cliquable via le pseudo-élément du lien —
|
||||
un vrai <a>, pas de div cliquable (doctrine §4.3) */
|
||||
h3 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
h3 a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
h3 a:focus-visible::after {
|
||||
outline: var(--focus-ring);
|
||||
outline-offset: -3px;
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 0.1em 0.6em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { marked } from 'marked'
|
||||
|
||||
export type Difficulty = 'beginner' | 'intermediate' | 'advanced'
|
||||
|
||||
export interface ToolSections {
|
||||
what?: string
|
||||
why?: string
|
||||
who?: string
|
||||
install?: string
|
||||
extra?: string
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
name: string
|
||||
slug: string
|
||||
category: string
|
||||
difficulty: Difficulty
|
||||
license?: string
|
||||
url: string
|
||||
color?: string
|
||||
monogram?: string
|
||||
iconSlug?: string
|
||||
tags: string[]
|
||||
/** extrait texte brut de « why », pour les cartes */
|
||||
excerpt: string
|
||||
sections: ToolSections
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
slug: string
|
||||
title: string
|
||||
order: number
|
||||
part: number
|
||||
}
|
||||
|
||||
const CONTENT = path.resolve(process.cwd(), '..', 'content')
|
||||
const SECTION_KEYS = ['what', 'why', 'who', 'install', 'extra'] as const
|
||||
|
||||
function parseFrontmatter(raw: string): [Record<string, unknown>, string] {
|
||||
const m = raw.match(/^---\n([\s\S]*?)\n---\n?/)
|
||||
if (!m) throw new Error('frontmatter manquant')
|
||||
const meta: Record<string, unknown> = {}
|
||||
for (const line of m[1].split('\n')) {
|
||||
const i = line.indexOf(':')
|
||||
if (i === -1) continue
|
||||
// les valeurs sont encodées en JSON par le script de migration
|
||||
meta[line.slice(0, i).trim()] = JSON.parse(line.slice(i + 1).trim())
|
||||
}
|
||||
return [meta, raw.slice(m[0].length)]
|
||||
}
|
||||
|
||||
function parseSections(body: string): ToolSections {
|
||||
const sections: ToolSections = {}
|
||||
const parts = body.split(/^## (\w+)\s*$/m)
|
||||
for (let i = 1; i < parts.length; i += 2) {
|
||||
const key = parts[i] as (typeof SECTION_KEYS)[number]
|
||||
if (SECTION_KEYS.includes(key)) {
|
||||
sections[key] = marked.parse(parts[i + 1].trim(), { async: false })
|
||||
}
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
const ENTITIES: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' '
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (e) => ENTITIES[e])
|
||||
.replace(/[ \t\r\n]+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function loadTool(file: string): Tool {
|
||||
const raw = readFileSync(path.join(CONTENT, 'outils', file), 'utf8')
|
||||
const [meta, body] = parseFrontmatter(raw)
|
||||
const sections = parseSections(body)
|
||||
const why = stripHtml(sections.why ?? sections.what ?? '')
|
||||
return {
|
||||
...(meta as unknown as Omit<Tool, 'sections' | 'excerpt'>),
|
||||
excerpt: why.length > 160 ? `${why.slice(0, 157).trimEnd()}…` : why,
|
||||
sections
|
||||
}
|
||||
}
|
||||
|
||||
let cache: { tools: Tool[]; categories: Category[] } | null = null
|
||||
|
||||
export function loadContent() {
|
||||
if (cache) return cache
|
||||
const categories = (
|
||||
JSON.parse(readFileSync(path.join(CONTENT, 'categories.json'), 'utf8')) as Category[]
|
||||
).sort((a, b) => a.order - b.order)
|
||||
const order = new Map(categories.map((c, i) => [c.slug, i]))
|
||||
const tools = readdirSync(path.join(CONTENT, 'outils'))
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.map(loadTool)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(order.get(a.category) ?? 99) - (order.get(b.category) ?? 99) ||
|
||||
a.name.localeCompare(b.name, 'fr')
|
||||
)
|
||||
cache = { tools, categories }
|
||||
return cache
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"site": {
|
||||
"name": "Exit Chat Control",
|
||||
"tagline": "Reprends le contrôle de ton internet",
|
||||
"description": "Annuaire d'outils libres pour la souveraineté numérique — messagerie chiffrée, VPN, auto-hébergement, OS libres."
|
||||
},
|
||||
"nav": {
|
||||
"home": "Accueil",
|
||||
"directory": "Annuaire",
|
||||
"contribute": "Contribuer",
|
||||
"skipToContent": "Aller au contenu"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Chwazi zouti ou",
|
||||
"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"
|
||||
},
|
||||
"directory": {
|
||||
"title": "Annuaire des outils",
|
||||
"intro": "Chaque outil est libre, vérifié, et classé par difficulté. Commence par le vert.",
|
||||
"filterCategory": "Catégorie",
|
||||
"filterDifficulty": "Difficulté",
|
||||
"allCategories": "Toutes les catégories",
|
||||
"allDifficulties": "Toutes",
|
||||
"results": "outils",
|
||||
"resultOne": "outil",
|
||||
"empty": "Ayen pa maché èvè filtè-lasa — essaie d'élargir les filtres.",
|
||||
"endOfList": "Ou pa manké ayen",
|
||||
"endOfListSub": "Tu es au bout de la liste — rien ne se recharge dans ton dos.",
|
||||
"prevPage": "Page précédente",
|
||||
"nextPage": "Page suivante",
|
||||
"page": "Page"
|
||||
},
|
||||
"tool": {
|
||||
"what": "À quoi ça sert",
|
||||
"why": "Pourquoi c'est important",
|
||||
"who": "Pour qui, quand",
|
||||
"install": "Installer et utiliser",
|
||||
"officialSite": "Site officiel",
|
||||
"sourceCode": "Code source",
|
||||
"license": "Licence",
|
||||
"category": "Catégorie",
|
||||
"backToDirectory": "Retour à l'annuaire"
|
||||
},
|
||||
"difficulty": {
|
||||
"beginner": "Débutant",
|
||||
"intermediate": "Intermédiaire",
|
||||
"advanced": "Avancé"
|
||||
},
|
||||
"search": {
|
||||
"label": "Rechercher un outil",
|
||||
"placeholder": "Rechercher… (touche /)",
|
||||
"noResults": "Aucun outil trouvé"
|
||||
},
|
||||
"lang": {
|
||||
"switchLabel": "Langue",
|
||||
"switchTo": "Passer en"
|
||||
},
|
||||
"footer": {
|
||||
"license": "Contenu et code libres (MIT). Zéro tracker, zéro CDN, zéro cookie.",
|
||||
"contribute": "Proposer un outil ou une correction"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"site": {
|
||||
"name": "Exit Chat Control",
|
||||
"tagline": "Pran kontwòl entènèt a-w",
|
||||
"description": "Lis a zouti lib pou libèté niméwik — mésajri chifré, VPN, oto-ébèjman, sistèm lib."
|
||||
},
|
||||
"nav": {
|
||||
"home": "Lakou",
|
||||
"directory": "Lis a zouti",
|
||||
"contribute": "Pòté mannèv",
|
||||
"skipToContent": "Alé dirèk asi kontni-la"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Chwazi zouti ou",
|
||||
"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"
|
||||
},
|
||||
"directory": {
|
||||
"title": "Lis a zouti",
|
||||
"intro": "Chak zouti lib, vérifyé, é rangé pa difikilté. Koumansé èvè vèw-la.",
|
||||
"filterCategory": "Katégori",
|
||||
"filterDifficulty": "Difikilté",
|
||||
"allCategories": "Tout katégori",
|
||||
"allDifficulties": "Tout",
|
||||
"results": "zouti",
|
||||
"resultOne": "zouti",
|
||||
"empty": "Ayen pa maché èvè filtè-lasa — èséyé louvri filtè a-w.",
|
||||
"endOfList": "Ou pa manké ayen",
|
||||
"endOfListSub": "Ou rivé an bout a lis-la — ayen pa ka richajé dèyè do a-w.",
|
||||
"prevPage": "Paj avan",
|
||||
"nextPage": "Paj apré",
|
||||
"page": "Paj"
|
||||
},
|
||||
"tool": {
|
||||
"what": "Pou ki sa i sèwvi",
|
||||
"why": "Poukisa sa enpòwtan",
|
||||
"who": "Pou kimoun, kitan",
|
||||
"install": "Enstalé é sèwvi",
|
||||
"officialSite": "Sit ofisyèl",
|
||||
"sourceCode": "Kòd sous",
|
||||
"license": "Lisans",
|
||||
"category": "Katégori",
|
||||
"backToDirectory": "Viré an lis-la"
|
||||
},
|
||||
"difficulty": {
|
||||
"beginner": "Débitan",
|
||||
"intermediate": "Entèmédia",
|
||||
"advanced": "Avansé"
|
||||
},
|
||||
"search": {
|
||||
"label": "Chèché on zouti",
|
||||
"placeholder": "Chèché… (touch /)",
|
||||
"noResults": "Pon zouti pa touvé"
|
||||
},
|
||||
"lang": {
|
||||
"switchLabel": "Lang",
|
||||
"switchTo": "Pasé an"
|
||||
},
|
||||
"footer": {
|
||||
"license": "Kontni é kòd lib (MIT). Pon trakè, pon CDN, pon cookie.",
|
||||
"contribute": "Pwopozé on zouti oben on koreksyon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import fr from './fr.json'
|
||||
import gcf from './gcf.json'
|
||||
import { browser } from '$app/environment'
|
||||
import { defaultLocale, type Locale } from './locales'
|
||||
|
||||
export { locales, localeNames, defaultLocale, type Locale } from './locales'
|
||||
|
||||
type Dict = typeof fr
|
||||
// gcf est vérifié par TypeScript contre la forme du français : une clé
|
||||
// manquante est une erreur de build, pas un fallback silencieux (ADR-001).
|
||||
const dicts: Record<Locale, Dict> = { fr, gcf }
|
||||
|
||||
const STORAGE_KEY = 'oki-lang'
|
||||
|
||||
function initialLocale(): Locale {
|
||||
if (!browser) return defaultLocale
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
return saved === 'gcf' || saved === 'fr' ? saved : defaultLocale
|
||||
}
|
||||
|
||||
// Rune d'état partagée (Svelte 5) : la langue de l'interface.
|
||||
// Le HTML prérendu est en fr ; le créole est une amélioration progressive
|
||||
// (persistance localStorage — mission §4.3, doctrine §4.2).
|
||||
const state = $state({ locale: initialLocale() })
|
||||
|
||||
export function getLocale(): Locale {
|
||||
return state.locale
|
||||
}
|
||||
|
||||
export function setLocale(locale: Locale) {
|
||||
state.locale = locale
|
||||
if (browser) {
|
||||
localStorage.setItem(STORAGE_KEY, locale)
|
||||
document.documentElement.lang = locale
|
||||
}
|
||||
}
|
||||
|
||||
function lookup(dict: Dict, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((node, key) => {
|
||||
if (node && typeof node === 'object' && key in node) {
|
||||
return (node as Record<string, unknown>)[key]
|
||||
}
|
||||
return undefined
|
||||
}, dict)
|
||||
}
|
||||
|
||||
/** Chaîne d'interface pour `path` dans la langue courante (réactif). */
|
||||
export function t(path: string): string {
|
||||
const hit = lookup(dicts[state.locale], path) ?? lookup(dicts[defaultLocale], path)
|
||||
if (typeof hit !== 'string') {
|
||||
throw new Error(`Clé i18n manquante « ${path} » (locale : ${state.locale})`)
|
||||
}
|
||||
return hit
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// gcf = créole guadeloupéen (ISO 639-3, valide BCP 47).
|
||||
// La langue du cœur d'OKI : les micro-textes créoles activent les circuits
|
||||
// de confiance avant toute lecture consciente (doctrine §4.2).
|
||||
export const locales = ['fr', 'gcf'] as const
|
||||
export type Locale = (typeof locales)[number]
|
||||
export const defaultLocale: Locale = 'fr'
|
||||
|
||||
export const localeNames: Record<Locale, string> = {
|
||||
fr: 'Français',
|
||||
gcf: 'Kréyòl'
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import '../app.css'
|
||||
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { children } = $props()
|
||||
</script>
|
||||
|
||||
<a class="skip-link" href="#main">{t('nav.skipToContent')}</a>
|
||||
|
||||
<header class="center">
|
||||
<div class="cluster bar">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
{t('site.name')}
|
||||
</a>
|
||||
<nav aria-label={t('nav.home')}>
|
||||
<ul class="cluster">
|
||||
<li><a href="/outils">{t('nav.directory')}</a></li>
|
||||
<li>
|
||||
<a href="https://github.com/exitchatcontrol/exitchatcontrol.org" rel="external">
|
||||
{t('nav.contribute')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main" class="center stack" style="--gap: var(--space-5)">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<footer class="center">
|
||||
<p>{t('footer.license')}</p>
|
||||
<p>
|
||||
<a href="https://github.com/exitchatcontrol/exitchatcontrol.org" rel="external">
|
||||
{t('footer.contribute')}
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.bar {
|
||||
justify-content: space-between;
|
||||
padding-block: var(--space-3);
|
||||
border-bottom: 4px solid var(--oki-jaune);
|
||||
margin-block-end: var(--space-4);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: var(--text-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
min-height: var(--tap-target);
|
||||
}
|
||||
|
||||
/* marque OKI : les trois bandes panafricaines — identité du territoire
|
||||
dès le premier écran, test des 50 ms (doctrine §1.1 et §4.2) */
|
||||
.brand-mark {
|
||||
inline-size: 0.9rem;
|
||||
block-size: 1.6rem;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--oki-vert) 0 33%,
|
||||
var(--oki-jaune) 33% 66%,
|
||||
var(--oki-rouge) 66% 100%
|
||||
);
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--oki-jaune);
|
||||
text-decoration-thickness: 3px;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-block-end: var(--space-6);
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid var(--oki-line);
|
||||
padding-block: var(--space-4);
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
// Tout le site est prérendu (ADR-001) : HTML statique servi par nginx,
|
||||
// un lien fonctionne sans JS (doctrine §4.4).
|
||||
export const prerender = true
|
||||
@@ -0,0 +1,11 @@
|
||||
import { loadContent } from '$lib/content/tools.server'
|
||||
|
||||
export function load() {
|
||||
const { tools, categories } = loadContent()
|
||||
return {
|
||||
categories,
|
||||
toolCount: tools.length,
|
||||
// les premiers gestes : outils débutant, un par catégorie
|
||||
starters: tools.filter((t) => t.difficulty === 'beginner').slice(0, 3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { data } = $props()
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t('site.name')} — {t('site.tagline')}</title>
|
||||
<meta name="description" content={t('site.description')} />
|
||||
</svelte:head>
|
||||
|
||||
<!-- Valeur livrée en < 5 s (doctrine §2.1) : le héros dit quoi, pour qui,
|
||||
et donne UNE action primaire (Von Restorff, doctrine §1.2). -->
|
||||
<section class="hero stack">
|
||||
<h1>{t('hero.title')}</h1>
|
||||
<p class="sub">{t('hero.subtitle')}</p>
|
||||
<p><a class="cta" href="/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<h2>{t('directory.intro')}</h2>
|
||||
<div class="card-grid">
|
||||
<ul>
|
||||
{#each data.starters as tool (tool.slug)}
|
||||
<li><ToolCard {tool} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</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>
|
||||
.hero {
|
||||
padding-block: var(--space-5);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: var(--text-2);
|
||||
color: var(--oki-ink-2);
|
||||
max-width: var(--measure);
|
||||
}
|
||||
|
||||
/* une seule action primaire par écran (Von Restorff, doctrine §1.2) */
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cta:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.cats {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cats a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
.cats a:hover {
|
||||
border-color: var(--oki-jaune);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { loadContent } from '$lib/content/tools.server'
|
||||
|
||||
export function load() {
|
||||
const { tools, categories } = loadContent()
|
||||
return {
|
||||
categories,
|
||||
// seules les données de carte partent au client (budget JS, doctrine §4.4)
|
||||
tools: tools.map(({ sections: _sections, ...card }) => card)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { page } from '$app/state'
|
||||
import CategoryNav from '$lib/components/CategoryNav.svelte'
|
||||
import SearchBar from '$lib/components/SearchBar.svelte'
|
||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||
import EndMarker from '$lib/components/EndMarker.svelte'
|
||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { data } = $props()
|
||||
let query = $state('')
|
||||
|
||||
const DIFFICULTIES = ['beginner', 'intermediate', 'advanced'] as const
|
||||
|
||||
// Chaque filtre vit dans l'URL (query param) : partageable (mission §5.1).
|
||||
// Le HTML prérendu montre la liste complète ; le filtrage est une
|
||||
// amélioration progressive côté client (ADR-001). Les searchParams ne
|
||||
// sont pas lisibles au prerender, d'où la garde `browser`.
|
||||
let cat = $derived(browser ? page.url.searchParams.get('cat') : null)
|
||||
let diff = $derived(browser ? page.url.searchParams.get('diff') : null)
|
||||
|
||||
let filtered = $derived(
|
||||
data.tools.filter(
|
||||
(tool) =>
|
||||
(!cat || tool.category === cat) &&
|
||||
(!diff || tool.difficulty === diff) &&
|
||||
(!query ||
|
||||
`${tool.name} ${tool.tags.join(' ')} ${tool.excerpt}`
|
||||
.toLocaleLowerCase('fr')
|
||||
.includes(query.toLocaleLowerCase('fr')))
|
||||
)
|
||||
)
|
||||
|
||||
function diffHref(d: string | null): string {
|
||||
const params = new URLSearchParams(browser ? page.url.searchParams : undefined)
|
||||
if (cat) params.set('cat', cat)
|
||||
if (d) params.set('diff', d)
|
||||
else params.delete('diff')
|
||||
const qs = params.toString()
|
||||
return qs ? `/outils?${qs}` : '/outils'
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t('directory.title')} — {t('site.name')}</title>
|
||||
<meta name="description" content={t('directory.intro')} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="stack">
|
||||
<h1>{t('directory.title')}</h1>
|
||||
<p>{t('directory.intro')}</p>
|
||||
</header>
|
||||
|
||||
<div class="stack filters">
|
||||
<SearchBar bind:value={query} />
|
||||
<CategoryNav categories={data.categories} />
|
||||
<nav aria-label={t('directory.filterDifficulty')}>
|
||||
<ul class="cluster">
|
||||
<li>
|
||||
<a href={diffHref(null)} aria-current={diff === null ? 'page' : undefined}>
|
||||
{t('directory.allDifficulties')}
|
||||
</a>
|
||||
</li>
|
||||
{#each DIFFICULTIES as d (d)}
|
||||
<li>
|
||||
<a href={diffHref(d)} aria-current={diff === d ? 'page' : undefined}>
|
||||
<DifficultyBadge difficulty={d} />
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="stack" aria-live="polite">
|
||||
<p class="count">
|
||||
{filtered.length}
|
||||
{filtered.length === 1 ? t('directory.resultOne') : t('directory.results')}
|
||||
</p>
|
||||
|
||||
{#if filtered.length === 0}
|
||||
<p class="empty">{t('directory.empty')}</p>
|
||||
{:else}
|
||||
<div class="card-grid">
|
||||
<ul>
|
||||
{#each filtered as tool (tool.slug)}
|
||||
<li><ToolCard {tool} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<EndMarker />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.filters {
|
||||
--gap: var(--space-3);
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
nav a[aria-current='page'] {
|
||||
background: var(--oki-surface-2);
|
||||
border-color: var(--oki-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--space-5);
|
||||
border: 1px dashed var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-align: center;
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { error } from '@sveltejs/kit'
|
||||
import { loadContent } from '$lib/content/tools.server'
|
||||
|
||||
export function entries() {
|
||||
return loadContent().tools.map(({ slug }) => ({ slug }))
|
||||
}
|
||||
|
||||
export function load({ params }: { params: { slug: string } }) {
|
||||
const { tools, categories } = loadContent()
|
||||
const tool = tools.find((t) => t.slug === params.slug)
|
||||
if (!tool) error(404)
|
||||
const category = categories.find((c) => c.slug === tool.category)
|
||||
return { tool, category }
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<script lang="ts">
|
||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { data } = $props()
|
||||
let tool = $derived(data.tool)
|
||||
|
||||
const SECTION_ORDER = ['what', 'why', 'who', 'install', 'extra'] as const
|
||||
// « extra » n'a pas d'intitulé i18n dédié : rattaché à l'installation
|
||||
const HEADINGS: Record<string, string> = {
|
||||
what: 'tool.what',
|
||||
why: 'tool.why',
|
||||
who: 'tool.who',
|
||||
install: 'tool.install',
|
||||
extra: 'tool.install'
|
||||
}
|
||||
|
||||
let jsonLd = $derived(
|
||||
`<script type="application/ld+json">${JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: tool.name,
|
||||
url: tool.url,
|
||||
license: tool.license ? `https://spdx.org/licenses/${tool.license}` : undefined,
|
||||
applicationCategory: data.category?.title
|
||||
})}${'<'}/script>`
|
||||
)
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{tool.name} — {t('site.name')}</title>
|
||||
<meta name="description" content={tool.excerpt} />
|
||||
<meta property="og:title" content={tool.name} />
|
||||
<meta property="og:description" content={tool.excerpt} />
|
||||
<meta property="og:type" content="article" />
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — JSON-LD généré au build depuis nos propres données -->
|
||||
{@html jsonLd}
|
||||
</svelte:head>
|
||||
|
||||
<nav aria-label={t('tool.backToDirectory')}>
|
||||
<a href="/outils">← {t('tool.backToDirectory')}</a>
|
||||
</nav>
|
||||
|
||||
<article class="stack" style="--gap: var(--space-4)">
|
||||
<header class="stack">
|
||||
<div class="cluster">
|
||||
<span class="monogram" style:--tool-color={tool.color} aria-hidden="true">
|
||||
{tool.monogram ?? tool.name.slice(0, 2)}
|
||||
</span>
|
||||
<h1>{tool.name}</h1>
|
||||
</div>
|
||||
<div class="cluster meta">
|
||||
<DifficultyBadge difficulty={tool.difficulty} />
|
||||
{#if data.category}
|
||||
<a href="/outils?cat={data.category.slug}">{data.category.title}</a>
|
||||
{/if}
|
||||
{#if tool.license}
|
||||
<span>{t('tool.license')} : {tool.license}</span>
|
||||
{/if}
|
||||
{#each tool.tags as tag (tag)}
|
||||
<span>{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
<p>
|
||||
<a class="cta" href={tool.url} rel="external">{t('tool.officialSite')} ↗</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{#each SECTION_ORDER as key (key)}
|
||||
{#if tool.sections[key]}
|
||||
<section class="prose stack">
|
||||
<h2>{t(HEADINGS[key])}</h2>
|
||||
<!-- HTML produit au build par marked depuis notre propre contenu versionné -->
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div class="stack">{@html tool.sections[key]}</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/each}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.monogram {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 3.5rem;
|
||||
block-size: 3.5rem;
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--tool-color, var(--oki-surface-2));
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
.meta > * {
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 0.15em 0.6em;
|
||||
}
|
||||
|
||||
.meta a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-size: var(--text-3);
|
||||
border-bottom: 3px solid var(--oki-jaune);
|
||||
display: inline-block;
|
||||
padding-block-end: var(--space-1);
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* base.css — HTML sémantique nu (doctrine §4.3 : l'accessibilité est
|
||||
* d'abord un problème de HTML correct).
|
||||
*/
|
||||
body {
|
||||
font-family: var(--font-text);
|
||||
font-size: var(--text-1);
|
||||
line-height: var(--leading);
|
||||
color: var(--oki-ink);
|
||||
background: var(--oki-surface);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-display);
|
||||
line-height: 1.15;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--text-5);
|
||||
}
|
||||
h2 {
|
||||
font-size: var(--text-4);
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--text-2);
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
max-width: var(--measure);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-ink);
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
/* focus visible sur TOUT élément interactif (doctrine §4.3) */
|
||||
:focus-visible {
|
||||
outline: var(--focus-ring);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-1);
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: var(--tap-target); /* Fitts, doctrine §1.3 */
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.9em;
|
||||
background: var(--oki-surface-2);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: var(--radius-1);
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
padding: var(--space-3);
|
||||
background: var(--oki-surface-2);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
}
|
||||
|
||||
/* lien d'évitement clavier */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
inset-inline-start: var(--space-3);
|
||||
inset-block-start: var(--space-3);
|
||||
translate: 0 -300%;
|
||||
background: var(--oki-surface);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
translate: 0 0;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Polices auto-hébergées, servies depuis notre domaine — jamais de Google
|
||||
* Fonts (doctrine §5.3 : performance, RGPD, souveraineté).
|
||||
* Budget : 2 fichiers WOFF2 maximum. Le gras du corps est synthétisé.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Atkinson Hyperlegible';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/atkinson-hyperlegible-latin-400-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fraunces';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/fraunces-latin-700-normal.woff2') format('woff2');
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* layouts.css — primitives de composition (approche « Every Layout » :
|
||||
* des compositions, pas des pages — doctrine §5.2).
|
||||
* Le responsive passe par container queries, jamais par media queries
|
||||
* de largeur d'écran (doctrine §2.2 point 6).
|
||||
*/
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap, var(--space-3));
|
||||
}
|
||||
|
||||
.cluster {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap, var(--space-2));
|
||||
align-items: var(--align, center);
|
||||
}
|
||||
|
||||
.center {
|
||||
box-sizing: content-box;
|
||||
max-inline-size: var(--max, 70rem);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--space-3);
|
||||
}
|
||||
|
||||
.prose {
|
||||
max-inline-size: var(--measure);
|
||||
}
|
||||
|
||||
/* grille de cartes : pilotée par le conteneur, pas par l'écran */
|
||||
.card-grid {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.card-grid > ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@container (min-width: 44rem) {
|
||||
.card-grid > ul {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 68rem) {
|
||||
.card-grid > ul {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* Reset moderne minimal (doctrine §5.2 — ~30 lignes, rien de plus). */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
/* jamais de font-size en px sur html : respect de la taille utilisateur (doctrine §4.3) */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
img,
|
||||
picture,
|
||||
svg,
|
||||
video {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* tokens.css — design tokens OKI (doctrine §5.2).
|
||||
* Toute valeur visuelle du site vit ici ; les composants Svelte consomment
|
||||
* les tokens, jamais de valeur brute (couleur, taille) dans un composant.
|
||||
* Couleurs en oklch() : luminance perceptuelle maîtrisée => contraste WCAG
|
||||
* calculable (méthode modus-vivendi, doctrine §4.3).
|
||||
*/
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
/* ── Identité panafricaine OKI ─────────────────────────────────────── */
|
||||
--oki-ink: light-dark(oklch(18% 0.02 260), oklch(94% 0.01 260));
|
||||
--oki-surface: light-dark(oklch(97% 0.005 90), oklch(14% 0.02 260));
|
||||
--oki-surface-2: light-dark(oklch(93% 0.008 90), oklch(20% 0.02 260));
|
||||
--oki-line: light-dark(oklch(85% 0.01 90), oklch(32% 0.02 260));
|
||||
--oki-ink-2: light-dark(oklch(38% 0.02 260), oklch(75% 0.01 260));
|
||||
|
||||
/* Couleurs identitaires (drapeau panafricain — jaune/vert/rouge/noir) */
|
||||
--oki-jaune: oklch(75% 0.18 85);
|
||||
--oki-vert: oklch(55% 0.18 145);
|
||||
--oki-rouge: oklch(55% 0.22 25);
|
||||
--oki-noir: oklch(15% 0.01 260);
|
||||
|
||||
/* Accent principal : le jaune OKI, décliné pour rester AA sur chaque fond */
|
||||
--accent: light-dark(oklch(52% 0.14 85), var(--oki-jaune));
|
||||
--accent-ink: light-dark(oklch(35% 0.1 85), oklch(85% 0.16 85));
|
||||
|
||||
/* ── Difficulté — jamais portée par la couleur seule (doctrine §1.1) ──
|
||||
Chaque niveau = couleur + texte + icône dans DifficultyBadge. */
|
||||
--diff-beginner: oklch(65% 0.15 145);
|
||||
--diff-intermediate: oklch(75% 0.16 85);
|
||||
--diff-advanced: oklch(55% 0.18 25);
|
||||
/* variantes texte, contrastées AA sur --oki-surface */
|
||||
--diff-beginner-ink: light-dark(oklch(42% 0.14 145), oklch(78% 0.15 145));
|
||||
--diff-intermediate-ink: light-dark(oklch(48% 0.13 85), oklch(80% 0.15 85));
|
||||
--diff-advanced-ink: light-dark(oklch(46% 0.18 25), oklch(72% 0.16 25));
|
||||
|
||||
/* ── Typographie fluide (clamp, zéro media query — doctrine §5.2) ────
|
||||
Échelle modulaire ratio 1.25, corps 16px mobile → 18px desktop. */
|
||||
--text-0: clamp(0.875rem, 0.85rem + 0.15vw, 0.9375rem);
|
||||
--text-1: clamp(1rem, 0.95rem + 0.4vw, 1.125rem);
|
||||
--text-2: clamp(1.25rem, 1.17rem + 0.5vw, 1.4rem);
|
||||
--text-3: clamp(1.56rem, 1.42rem + 0.8vw, 1.75rem);
|
||||
--text-4: clamp(1.95rem, 1.7rem + 1.4vw, 2.44rem);
|
||||
--text-5: clamp(2.44rem, 2rem + 2.4vw, 3.4rem);
|
||||
|
||||
--font-text: 'Atkinson Hyperlegible', system-ui, sans-serif;
|
||||
--font-display: 'Fraunces', var(--font-text);
|
||||
|
||||
--measure: 65ch; /* largeur de lecture 45-75ch (doctrine §2.2) */
|
||||
--leading: 1.6;
|
||||
|
||||
/* ── Espacement modulaire ────────────────────────────────────────── */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 1rem;
|
||||
--space-4: 1.5rem;
|
||||
--space-5: 2.5rem;
|
||||
--space-6: 4rem;
|
||||
|
||||
/* ── Interaction ─────────────────────────────────────────────────── */
|
||||
--tap-target: 48px; /* Fitts (doctrine §1.3) */
|
||||
--radius-1: 4px;
|
||||
--radius-2: 10px;
|
||||
--duration-1: 120ms;
|
||||
--duration-2: 240ms;
|
||||
--ease-out: cubic-bezier(0.2, 0, 0, 1);
|
||||
|
||||
--focus-ring: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--oki-ink: light-dark(oklch(5% 0.01 260), oklch(99% 0 0));
|
||||
--oki-line: light-dark(oklch(40% 0.01 260), oklch(70% 0.01 260));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Exit Chat Control",
|
||||
"short_name": "ExitCC",
|
||||
"description": "Annuaire d'outils libres pour la souveraineté numérique",
|
||||
"lang": "fr",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#17181f",
|
||||
"theme_color": "#17181f",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import adapter from '@sveltejs/adapter-static'
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
// Site 100 % prérendu : servi par nginx sur l'infra OKI, mirrorable
|
||||
// (ADR-001 ; doctrine §4.4 — HTML rendu serveur, un lien marche sans JS).
|
||||
adapter: adapter({
|
||||
fallback: '404.html'
|
||||
}),
|
||||
prerender: {
|
||||
handleHttpError: 'fail'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
fs: {
|
||||
// le contenu éditorial vit à la racine du repo, hors de frontend/
|
||||
allow: ['..']
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user