@intside/accessibility 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Intside
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # Module d'accessibilité ACSBLT
2
+
3
+ Widget d'accessibilité réutilisable, extrait du prototype ACSBLT. Il propose :
4
+
5
+ - **Profils d'assistance** (vision réduite, dyslexie, TDAH, sensibilité aux animations, daltonisme, senior) + rappel des **standards toujours actifs** (lecteur d'écran, clavier).
6
+ - **Réglages personnalisés** : taille du texte, interligne, police lisible, contraste, mode sombre, niveaux de gris, focus renforcé, grandes cibles, curseur agrandi, etc.
7
+ - **Persistance** via `localStorage` (clé `acsblt_a11y_v1`) et application de classes sur `<html>`.
8
+ - **Autonome** : aucune dépendance UI (pas de Tailwind, pas de jQuery requis). Couleur d'accent personnalisable via `--a11y-accent`.
9
+
10
+ Deux implémentations interchangeables, **même clé de stockage** et **mêmes classes CSS** :
11
+
12
+ | Dossier | Pour | Format |
13
+ |---|---|---|
14
+ | [`react/`](./react) | Next.js (App ou Pages Router), React 18+ | TypeScript / TSX |
15
+ | [`vanilla/`](./vanilla) | WordPress, sites statiques, tout HTML | JS + CSS, zéro dépendance |
16
+
17
+ ---
18
+
19
+ ## 0. Installation rapide (CLI)
20
+
21
+ Un installeur copie le bon build dans un projet cible et affiche les étapes de
22
+ câblage. **Il ne modifie jamais votre code** (il copie des fichiers et imprime
23
+ le snippet à coller).
24
+
25
+ ```bash
26
+ # Next.js / React
27
+ npx @intside/accessibility install --target nextjs --dest ../mon-app --accent '#112337'
28
+
29
+ # WordPress (thème)
30
+ npx @intside/accessibility install --target wordpress \
31
+ --dest wp-content/themes/mon-theme --accent '#0A2240' --position left
32
+
33
+ # Sans options : mode interactif
34
+ npx @intside/accessibility install
35
+
36
+ # Forcer la récupération des fichiers depuis le dépôt git (et non la copie locale)
37
+ node bin/cli.mjs install --target nextjs --dest ../mon-app --from-git --ref main
38
+ ```
39
+
40
+ Depuis un clone du repo, sans publication : `node bin/cli.mjs install …`
41
+ (ou `npm link` puis `acsblt-a11y install …`).
42
+
43
+ **D'où viennent les fichiers copiés ?** Par défaut, des fichiers livrés *à côté*
44
+ de la CLI : ceux du clone si vous lancez `node bin/cli.mjs`, ou ceux du tarball
45
+ téléchargé si vous lancez `npx <url-git>` (npm clone le dépôt et exécute le bin
46
+ depuis ce clone — donc déjà la version du dépôt). Pour forcer une récupération
47
+ explicite depuis le dépôt distant à une réf donnée — utile si votre clone local
48
+ est ancien — passez `--from-git [--ref <branche|tag>] [--repo <url>]` : la CLI
49
+ clone le dépôt dans un dossier temporaire, copie, puis le supprime.
50
+
51
+ | Option | Défaut | Rôle |
52
+ |---|---|---|
53
+ | `--target` | *(demandé)* | `nextjs` ou `wordpress` |
54
+ | `--dest` | dossier courant | Racine du projet cible |
55
+ | `--subdir` | `components/accessibility` / `assets/a11y` | Sous-dossier de destination |
56
+ | `--accent` | `#006828` | Couleur d'accent (`--a11y-accent`) |
57
+ | `--position` | `left` | Coin du bouton (WordPress) |
58
+ | `--label` | `Accessibilité` | Libellé / aria-label |
59
+ | `--report-email` | `accessibilite@example.org` | Lien « Signaler un problème » (WordPress) |
60
+ | `--from-git` | — | Récupère les fichiers depuis le dépôt distant |
61
+ | `--ref` | `main` | Réf git à récupérer (avec `--from-git`) |
62
+ | `--repo` | GitLab `packages/accessibility` | URL du dépôt (avec `--from-git`) |
63
+ | `-y, --yes` | — | Valeurs par défaut, sans prompt |
64
+
65
+ Les deux sections ci-dessous détaillent le câblage manuel produit par la CLI.
66
+
67
+ ---
68
+
69
+ ## 1. Version React / Next.js (`react/`)
70
+
71
+ Copiez le dossier `react/` dans votre projet (ex. `src/components/accessibility/`) et importez.
72
+
73
+ ### Bouton prêt à l'emploi
74
+
75
+ ```tsx
76
+ // app/layout.tsx (ou un header/footer)
77
+ import { AccessibilityButton } from '@/components/accessibility';
78
+
79
+ export default function Layout({ children }: { children: React.ReactNode }) {
80
+ return (
81
+ <html lang="fr">
82
+ <body>
83
+ {children}
84
+ <AccessibilityButton label="Accessibilité" />
85
+ </body>
86
+ </html>
87
+ );
88
+ }
89
+ ```
90
+
91
+ `AccessibilityButton` accepte `label`, `className`, `style`, `hideIcon`. Sans `className`, il est neutre (hérite de la couleur du parent) — placez-le dans votre `top-bar`, votre footer, etc.
92
+
93
+ ### Deux déclencheurs au choix
94
+
95
+ Le module fournit **deux** points d'entrée interchangeables (ils ouvrent le même modal) — choisissez l'un **ou** l'autre :
96
+
97
+ | Composant | Usage | Rendu |
98
+ |---|---|---|
99
+ | `AccessibilityButton` | Lien inline dans une barre (top-bar, footer, menu) | Hérite du style du parent |
100
+ | `AccessibilityFab` | **Bouton flottant** ancré dans un coin | Pastille ronde autonome (icône seule) |
101
+
102
+ ```tsx
103
+ import { AccessibilityFab } from '@/components/accessibility';
104
+
105
+ // Bouton flottant rond en bas à gauche (défaut)
106
+ <AccessibilityFab />
107
+
108
+ // Autres coins + variantes
109
+ <AccessibilityFab position="bottom-right" />
110
+ <AccessibilityFab hideLabel={false} label="Accès." /> // pilule avec libellé
111
+ <AccessibilityFab offset={16} />
112
+ ```
113
+
114
+ `AccessibilityFab` accepte `position` (`'bottom-left'` défaut, `'bottom-right'`, `'top-left'`, `'top-right'`), `label`, `hideLabel` (défaut `true` → pastille ronde ; `false` → pilule avec texte), `offset` (px depuis les bords), `className`, `style`. Par défaut : pastille ronde, couleur `--a11y-accent`, léger agrandissement au survol. Avec `className`, le style par défaut est entièrement remplacé par le vôtre.
115
+
116
+ > Côté **vanilla** (WordPress), le bouton flottant est déjà géré : `window.ACSBLT_A11Y = { fab: true, position: 'left' }` l'ancre en bas à gauche.
117
+
118
+ ### Contrôle manuel (votre propre déclencheur)
119
+
120
+ ```tsx
121
+ 'use client';
122
+ import { useState } from 'react';
123
+ import { AccessibilityModal } from '@/components/accessibility';
124
+
125
+ export function MyTrigger() {
126
+ const [open, setOpen] = useState(false);
127
+ return (
128
+ <>
129
+ <button onClick={() => setOpen(true)}>Accessibilité</button>
130
+ {open && <AccessibilityModal onClose={() => setOpen(false)} />}
131
+ </>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ### Éviter le flash au chargement (recommandé)
137
+
138
+ Appliquez les préférences **avant le premier paint** avec le script exporté :
139
+
140
+ ```tsx
141
+ // app/layout.tsx — App Router
142
+ import Script from 'next/script';
143
+ import { A11Y_INIT_SCRIPT } from '@/components/accessibility';
144
+
145
+ // dans <head> ou juste après <body>
146
+ <Script id="a11y-init" strategy="beforeInteractive">
147
+ {A11Y_INIT_SCRIPT}
148
+ </Script>
149
+ ```
150
+
151
+ ### Personnalisation du thème
152
+
153
+ Le modal lit des CSS variables avec valeurs par défaut. Surchargez-les au niveau `:root` :
154
+
155
+ ```css
156
+ :root {
157
+ --a11y-accent: #0a66c2; /* couleur principale */
158
+ --a11y-accent-soft: #0a66c21a;
159
+ --a11y-surface: #fff;
160
+ --a11y-text: #111;
161
+ /* … voir A11Y_TOKENS dans react/engine.ts */
162
+ }
163
+ ```
164
+
165
+ ### API exportée
166
+
167
+ `AccessibilityModal`, `AccessibilityButton`, `A11YIcon`, et le moteur : `loadA11y`, `saveA11y`, `applyA11yClasses`, `ensureA11yStyles`, les données `ACC_PROFILES` / `ACC_SETTINGS` / `ACC_STANDARDS`, et les constantes `A11Y_CSS`, `A11Y_TOKENS`, `A11Y_INIT_SCRIPT`, `STORAGE_KEY`.
168
+
169
+ > Composants en `'use client'` : ils utilisent les hooks React, `localStorage` et `document`. SSR-safe (les accès navigateur sont dans `useEffect`).
170
+
171
+ ---
172
+
173
+ ## 2. Version WordPress / vanilla JS (`vanilla/`)
174
+
175
+ Trois fichiers, **aucune dépendance** (fonctionne avec ou sans jQuery) :
176
+
177
+ - `acsblt-accessibility.css` — effets runtime + styles du widget
178
+ - `acsblt-accessibility.js` — logique + UI (bouton flottant + modal), auto-init
179
+ - `acsblt-accessibility-head.js` — script pré-paint optionnel (anti-flash, à mettre dans `<head>`)
180
+
181
+ ### Intégration HTML minimale
182
+
183
+ ```html
184
+ <link rel="stylesheet" href="/a11y/acsblt-accessibility.css">
185
+ <!-- dans <head>, avant le rendu (optionnel mais recommandé) -->
186
+ <script src="/a11y/acsblt-accessibility-head.js"></script>
187
+ <!-- config optionnelle AVANT le script principal -->
188
+ <script>window.ACSBLT_A11Y = { fab: true, position: 'right', accent: '#006828' };</script>
189
+ <!-- en fin de <body> -->
190
+ <script src="/a11y/acsblt-accessibility.js" defer></script>
191
+ ```
192
+
193
+ Un **bouton flottant** apparaît automatiquement (bas-droite). Pour utiliser votre propre déclencheur à la place :
194
+
195
+ ```html
196
+ <script>window.ACSBLT_A11Y = { fab: false };</script>
197
+ <a href="#" data-acsblt-a11y-open>Accessibilité</a>
198
+ ```
199
+
200
+ ### WordPress
201
+
202
+ Voir [`vanilla/wordpress-example.php`](./vanilla/wordpress-example.php) : enqueue propre via `wp_enqueue_scripts`, config injectée avec `wp_add_inline_script`, et script pré-paint dans `wp_head`. À coller dans `functions.php` (thème enfant) ou dans un mu-plugin.
203
+
204
+ ### Options de configuration (`window.ACSBLT_A11Y`)
205
+
206
+ | Clé | Défaut | Description |
207
+ |---|---|---|
208
+ | `fab` | `true` | Affiche le bouton flottant automatique |
209
+ | `position` | `'right'` | `'right'` ou `'left'` |
210
+ | `label` | `'Accessibilité'` | Libellé du bouton flottant |
211
+ | `accent` | `#006828` | Couleur d'accent (équivaut à `--a11y-accent`) |
212
+ | `reportEmail` | `accessibilite@example.org` | Adresse du lien « Signaler un problème » |
213
+
214
+ ### API JS publique
215
+
216
+ ```js
217
+ AcsbltA11y.open(); // ouvre le modal
218
+ AcsbltA11y.close();
219
+ AcsbltA11y.toggle();
220
+ AcsbltA11y.reset(); // réinitialise les préférences
221
+ AcsbltA11y.getState(); // copie de l'état courant
222
+ ```
223
+
224
+ ---
225
+
226
+ ## État des actions (ce qui s'applique réellement)
227
+
228
+ Toutes les actions persistent la préférence ; voici ce qu'elles produisent visuellement :
229
+
230
+ - **Profils** (vision réduite, dyslexie, TDAH, animations, daltonisme, senior) : ✅ effet CSS immédiat.
231
+ - **Texte** : taille du texte ✅ (agit désormais seule, pas seulement avec un profil), interligne ✅, espacement ✅, police lisible ✅, alignement à gauche ✅, largeur max ✅ (cible `main` / `.entry-content` / `article`).
232
+ - **Couleurs & contraste** : contraste ✅, mode sombre ✅, niveaux de gris ✅, souligner les liens ✅, boutons en évidence ✅.
233
+ - **Mouvement** : réduire les animations ✅, désactiver les transitions ✅.
234
+ - **Navigation** : focus renforcé ✅, grandes cibles ✅, curseur agrandi ✅.
235
+ - **Dépendants de votre application** (la préférence est stockée mais l'effet exige du markup spécifique à votre site) :
236
+ - `showShortcuts` (afficher les raccourcis clavier), `fieldHints` (aides sous les champs), `errorSummary` (résumé des erreurs en haut).
237
+ - Lisez ces drapeaux dans votre code via `getState()` (vanilla) ou `loadA11y()` (React) pour les implémenter selon votre UI.
238
+
239
+ > Le mode sombre utilise `filter: invert()` sur `<html>` (approche simple, imparfaite sur images/embeds — celles-ci sont ré-inversées). Pour un vrai dark-mode à base de tokens, prévoir un chantier dédié.
240
+
241
+ ## Notes
242
+
243
+ - Les deux versions partagent la **même clé** `acsblt_a11y_v1` et les **mêmes classes** `a11y-*` sur `<html>` : un utilisateur garde ses préférences s'il passe d'un site React à un site WordPress sur le même domaine.
244
+ - Les données (profils, réglages, CSS runtime) sont **dupliquées** dans chaque version pour rester totalement autonomes. En cas de modification, répercutez-la dans `react/engine.ts` **et** `vanilla/acsblt-accessibility.js` + `.css`.
245
+ - Certains effets reposent sur des sélecteurs génériques (`main`, `button`, `a`, `[aria-invalid]`). Adaptez `A11Y_CSS` / le `.css` à vos classes spécifiques si besoin (ex. `.entry-content` pour WordPress est déjà inclus).
package/bin/cli.mjs ADDED
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env node
2
+ /* ================================================================
3
+ ACSBLT · Accessibilité — installeur
4
+ Copie le bon build (React/Next ou vanilla/WordPress) dans un projet
5
+ cible et affiche les étapes de câblage. Ne modifie jamais le code
6
+ existant. Zéro dépendance (Node >= 18).
7
+ ================================================================ */
8
+
9
+ import { execFileSync } from 'node:child_process';
10
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { dirname, join, relative, resolve } from 'node:path';
13
+ import { createInterface } from 'node:readline/promises';
14
+ import { stdin, stdout, argv, exit, cwd } from 'node:process';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { parseArgs } from 'node:util';
17
+
18
+ const HERE = dirname(fileURLToPath(import.meta.url));
19
+ const PKG_ROOT = resolve(HERE, '..');
20
+ const REACT_DIR = join(PKG_ROOT, 'react');
21
+ const VANILLA_DIR = join(PKG_ROOT, 'vanilla');
22
+
23
+ // Dépôt canonique, utilisé par --from-git pour récupérer les fichiers à la volée.
24
+ const DEFAULT_REPO = 'ssh://git@gitlab.intside.one:2424/packages/accessibility.git';
25
+ const DEFAULT_REF = 'main';
26
+
27
+ const REACT_FILES = [
28
+ 'AccessibilityButton.tsx',
29
+ 'AccessibilityFab.tsx',
30
+ 'AccessibilityModal.tsx',
31
+ 'engine.ts',
32
+ 'icons.tsx',
33
+ 'index.ts',
34
+ ];
35
+ const VANILLA_FILES = [
36
+ 'acsblt-accessibility.css',
37
+ 'acsblt-accessibility.js',
38
+ 'acsblt-accessibility-head.js',
39
+ ];
40
+
41
+ const C = {
42
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
43
+ green: '\x1b[32m', cyan: '\x1b[36m', yellow: '\x1b[33m', red: '\x1b[31m',
44
+ };
45
+ const c = (color, s) => `${C[color]}${s}${C.reset}`;
46
+
47
+ function help() {
48
+ console.log(`
49
+ ${c('bold', 'acsblt-a11y')} — installe le module d'accessibilité dans un projet
50
+
51
+ ${c('bold', 'Usage')}
52
+ npx @intside/accessibility install [options]
53
+ node bin/cli.mjs install [options]
54
+
55
+ ${c('bold', 'Options')}
56
+ --target <nextjs|wordpress> Type de projet cible (sinon demandé)
57
+ --dest <path> Racine du projet cible (défaut : dossier courant)
58
+ --subdir <path> Sous-dossier de destination
59
+ (défaut : components/accessibility | assets/a11y)
60
+ --accent <couleur> Couleur d'accent (défaut : #006828)
61
+ --position <left|right> Coin du bouton flottant — WordPress (défaut : left)
62
+ --label <texte> Libellé / aria-label (défaut : Accessibilité)
63
+ --report-email <email> Adresse du lien « Signaler un problème » — WordPress
64
+ --from-git Récupère les fichiers depuis le dépôt git distant
65
+ (au lieu des fichiers locaux livrés avec la CLI)
66
+ --repo <url> URL du dépôt (défaut : GitLab packages/accessibility)
67
+ --ref <branche|tag> Réf git à récupérer (défaut : main)
68
+ -y, --yes N'invite pas, utilise les valeurs par défaut
69
+ -h, --help Affiche cette aide
70
+
71
+ ${c('bold', 'Exemples')}
72
+ npx @intside/accessibility install --target nextjs --dest ../my-app
73
+ npx @intside/accessibility install --target wordpress --dest ./wp-content/themes/mon-theme --accent '#0A2240'
74
+ `);
75
+ }
76
+
77
+ function copyFiles(srcDir, files, destDir) {
78
+ mkdirSync(destDir, { recursive: true });
79
+ const copied = [];
80
+ for (const f of files) {
81
+ const src = join(srcDir, f);
82
+ if (!existsSync(src)) throw new Error(`Fichier source introuvable : ${src}`);
83
+ copyFileSync(src, join(destDir, f));
84
+ copied.push(f);
85
+ }
86
+ return copied;
87
+ }
88
+
89
+ /**
90
+ * Récupère le dossier source (`react` ou `vanilla`) depuis le dépôt git distant,
91
+ * dans un dossier temporaire. Renvoie le chemin + une fonction de nettoyage.
92
+ */
93
+ function cloneFromGit(repo, ref, subdir) {
94
+ const tmp = mkdtempSync(join(tmpdir(), 'acsblt-a11y-'));
95
+ const cleanup = () => rmSync(tmp, { recursive: true, force: true });
96
+ try {
97
+ execFileSync('git', ['clone', '--depth', '1', '--branch', ref, repo, tmp], { stdio: 'pipe' });
98
+ } catch (e) {
99
+ cleanup();
100
+ const detail = (e.stderr && e.stderr.toString().trim()) || e.message;
101
+ throw new Error(`clone git échoué (${repo} @ ${ref}).\n${detail}`);
102
+ }
103
+ const dir = join(tmp, subdir);
104
+ if (!existsSync(dir)) {
105
+ cleanup();
106
+ throw new Error(`Le dépôt ${repo} (${ref}) ne contient pas de dossier '${subdir}'.`);
107
+ }
108
+ return { dir, cleanup };
109
+ }
110
+
111
+ function normalizeTarget(t) {
112
+ if (!t) return null;
113
+ const v = String(t).toLowerCase();
114
+ if (['nextjs', 'next', 'react'].includes(v)) return 'nextjs';
115
+ if (['wordpress', 'wp', 'vanilla'].includes(v)) return 'wordpress';
116
+ return null;
117
+ }
118
+
119
+ async function main() {
120
+ let opts;
121
+ try {
122
+ ({ values: opts } = parseArgs({
123
+ args: argv.slice(2),
124
+ allowPositionals: true,
125
+ options: {
126
+ target: { type: 'string' },
127
+ dest: { type: 'string' },
128
+ subdir: { type: 'string' },
129
+ accent: { type: 'string' },
130
+ position: { type: 'string' },
131
+ label: { type: 'string' },
132
+ 'report-email': { type: 'string' },
133
+ 'from-git': { type: 'boolean' },
134
+ repo: { type: 'string' },
135
+ ref: { type: 'string' },
136
+ yes: { type: 'boolean', short: 'y' },
137
+ help: { type: 'boolean', short: 'h' },
138
+ },
139
+ }));
140
+ } catch (e) {
141
+ console.error(c('red', `Argument invalide : ${e.message}`));
142
+ help();
143
+ exit(1);
144
+ }
145
+
146
+ if (opts.help) { help(); return; }
147
+
148
+ const auto = !!opts.yes;
149
+ const rl = auto ? null : createInterface({ input: stdin, output: stdout });
150
+ const ask = async (q, def) => {
151
+ if (auto || !rl) return def;
152
+ const a = (await rl.question(`${q}${def ? c('dim', ` (${def})`) : ''} : `)).trim();
153
+ return a || def;
154
+ };
155
+
156
+ let cleanup = () => {};
157
+ try {
158
+ // 1. Cible
159
+ let target = normalizeTarget(opts.target);
160
+ while (!target) {
161
+ const a = await ask('Type de projet ? [nextjs / wordpress]', 'nextjs');
162
+ target = normalizeTarget(a);
163
+ if (!target) console.log(c('yellow', " Réponse attendue : 'nextjs' ou 'wordpress'."));
164
+ }
165
+
166
+ // 2. Destination
167
+ const projectRoot = resolve(opts.dest || (await ask('Racine du projet cible', cwd())));
168
+ const defaultSubdir = target === 'nextjs' ? 'components/accessibility' : 'assets/a11y';
169
+ const subdir = (opts.subdir || (await ask('Sous-dossier de destination', defaultSubdir))).replace(/^\/+|\/+$/g, '');
170
+ const destDir = join(projectRoot, subdir);
171
+
172
+ // 3. Options communes
173
+ const accent = opts.accent || (await ask('Couleur d\'accent', '#006828'));
174
+ const label = opts.label || (await ask('Libellé du bouton', 'Accessibilité'));
175
+
176
+ let position = 'left';
177
+ let reportEmail = 'accessibilite@example.org';
178
+ if (target === 'wordpress') {
179
+ position = ((opts.position || (await ask('Position du bouton [left / right]', 'left'))) === 'right') ? 'right' : 'left';
180
+ reportEmail = opts['report-email'] || (await ask('Email « Signaler un problème »', 'accessibilite@example.org'));
181
+ }
182
+
183
+ rl?.close();
184
+
185
+ // 4. Source des fichiers : locale (livrée avec la CLI) ou dépôt git distant.
186
+ const files = target === 'nextjs' ? REACT_FILES : VANILLA_FILES;
187
+ const repoSubdir = target === 'nextjs' ? 'react' : 'vanilla';
188
+ let srcDir = target === 'nextjs' ? REACT_DIR : VANILLA_DIR;
189
+ if (opts['from-git']) {
190
+ const repo = opts.repo || DEFAULT_REPO;
191
+ const ref = opts.ref || DEFAULT_REF;
192
+ console.log(c('dim', `→ Récupération de '${repoSubdir}' depuis ${repo} (${ref})…`));
193
+ ({ dir: srcDir, cleanup } = cloneFromGit(repo, ref, repoSubdir));
194
+ }
195
+
196
+ // 5. Copie
197
+ const copied = copyFiles(srcDir, files, destDir);
198
+ cleanup();
199
+
200
+ console.log(`\n${c('green', '✔')} ${copied.length} fichier(s) copié(s) dans ${c('cyan', relative(cwd(), destDir) || destDir)}`);
201
+ copied.forEach((f) => console.log(` ${c('dim', '·')} ${f}`));
202
+
203
+ // 6. Instructions
204
+ if (target === 'nextjs') printNextSteps(subdir, accent);
205
+ else printWordpressSteps(subdir, { accent, position, label, reportEmail });
206
+ } catch (e) {
207
+ rl?.close();
208
+ cleanup();
209
+ console.error(`\n${c('red', '✘ Échec :')} ${e.message}`);
210
+ exit(1);
211
+ }
212
+ }
213
+
214
+ function printNextSteps(subdir, accent) {
215
+ const importPath = `@/${subdir}`;
216
+ console.log(`
217
+ ${c('bold', 'Étapes suivantes (Next.js)')}
218
+
219
+ ${c('bold', '1.')} Monte le bouton flottant une seule fois, dans ton layout racine ou
220
+ ton provider (composant client) :
221
+
222
+ ${c('cyan', `import { AccessibilityFab } from "${importPath}";`)}
223
+ ${c('dim', '// …')}
224
+ ${c('cyan', '<AccessibilityFab />')}
225
+ ${c('dim', '// bouton rond bas-gauche par défaut ; passe className=… pour le restyler')}
226
+
227
+ ${c('bold', '2.')} ${c('dim', '(option)')} Aligne la couleur de marque dans ton CSS global :
228
+
229
+ ${c('cyan', `:root { --a11y-accent: ${accent}; }`)}
230
+
231
+ ${c('bold', '3.')} ${c('dim', '(option, projets Biome)')} Exclus le dossier vendu du lint dans biome.json :
232
+
233
+ ${c('cyan', `"files": { "includes": ["**", "!${subdir}"] }`)}
234
+
235
+ ${c('dim', `L'alias d'import (@/) dépend de ton tsconfig — ajuste si besoin.`)}
236
+ `);
237
+ }
238
+
239
+ function printWordpressSteps(subdir, { accent, position, label, reportEmail }) {
240
+ const php = `function acsblt_a11y_assets() {
241
+ \t$base = get_template_directory_uri() . '/${subdir}';
242
+ \t$ver = '1.0.0';
243
+
244
+ \twp_enqueue_style( 'acsblt-a11y', $base . '/acsblt-accessibility.css', array(), $ver );
245
+
246
+ \twp_register_script( 'acsblt-a11y', $base . '/acsblt-accessibility.js', array(), $ver, true );
247
+ \twp_add_inline_script(
248
+ \t\t'acsblt-a11y',
249
+ \t\t'window.ACSBLT_A11Y = ' . wp_json_encode( array(
250
+ \t\t\t'fab' => true,
251
+ \t\t\t'position' => '${position}',
252
+ \t\t\t'label' => '${label}',
253
+ \t\t\t'accent' => '${accent}',
254
+ \t\t\t'reportEmail' => '${reportEmail}',
255
+ \t\t) ) . ';',
256
+ \t\t'before'
257
+ \t);
258
+ \twp_enqueue_script( 'acsblt-a11y' );
259
+ }
260
+ add_action( 'wp_enqueue_scripts', 'acsblt_a11y_assets' );
261
+
262
+ function acsblt_a11y_head() {
263
+ \t$path = get_template_directory() . '/${subdir}/acsblt-accessibility-head.js';
264
+ \tif ( is_readable( $path ) ) {
265
+ \t\techo "<script>\\n" . file_get_contents( $path ) . "\\n</script>\\n";
266
+ \t}
267
+ }
268
+ add_action( 'wp_head', 'acsblt_a11y_head', 1 );`;
269
+
270
+ console.log(`
271
+ ${c('bold', 'Étapes suivantes (WordPress)')}
272
+
273
+ Ajoute ce bloc à la fin du ${c('cyan', 'functions.php')} de ton thème :
274
+
275
+ ${c('dim', '────────────────────────────────────────────────────────')}
276
+ ${php}
277
+ ${c('dim', '────────────────────────────────────────────────────────')}
278
+
279
+ ${c('dim', 'Le bouton flottant + le modal apparaissent automatiquement sur le front.')}
280
+ `);
281
+ }
282
+
283
+ main();
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@intside/accessibility",
3
+ "version": "1.0.0",
4
+ "description": "Module d'accessibilité ACSBLT (profils + réglages) avec installeur pour Next.js/React et WordPress/vanilla.",
5
+ "type": "module",
6
+ "bin": {
7
+ "acsblt-a11y": "bin/cli.mjs"
8
+ },
9
+ "scripts": {
10
+ "install:into": "node bin/cli.mjs install"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "react",
15
+ "vanilla",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+ssh://git@gitlab.intside.one:2424/packages/accessibility.git"
27
+ },
28
+ "license": "MIT"
29
+ }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { AccessibilityModal } from './AccessibilityModal';
5
+ import { ensureA11yStyles, loadA11y, applyA11yClasses } from './engine';
6
+
7
+ export interface AccessibilityButtonProps {
8
+ /** Texte du déclencheur (par défaut « Accessibilité »). */
9
+ label?: string;
10
+ /** Classe CSS optionnelle pour styler le bouton selon votre design. */
11
+ className?: string;
12
+ /** Style inline optionnel. */
13
+ style?: React.CSSProperties;
14
+ /** Masquer l'icône. */
15
+ hideIcon?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Bouton générique qui ouvre le module d'accessibilité.
20
+ * Applique aussi les préférences stockées au montage (utile si vous n'utilisez
21
+ * pas le script d'init pré-paint). Placez-le où vous voulez (header, footer…).
22
+ */
23
+ export function AccessibilityButton({ label = 'Accessibilité', className, style, hideIcon }: AccessibilityButtonProps) {
24
+ const [open, setOpen] = useState(false);
25
+
26
+ useEffect(() => {
27
+ ensureA11yStyles();
28
+ applyA11yClasses(loadA11y());
29
+ }, []);
30
+
31
+ return (
32
+ <>
33
+ <button
34
+ type="button"
35
+ onClick={() => setOpen(true)}
36
+ className={className}
37
+ aria-haspopup="dialog"
38
+ style={className ? style : { display: 'inline-flex', alignItems: 'center', gap: 6, cursor: 'pointer', background: 'none', border: 'none', font: 'inherit', color: 'inherit', ...style }}
39
+ >
40
+ {!hideIcon && (
41
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{ width: 15, height: 15 }}>
42
+ <circle cx="12" cy="4" r="1.5" />
43
+ <path d="M9 9h6M12 9v7M9 20l3-4 3 4" />
44
+ </svg>
45
+ )}
46
+ {label}
47
+ </button>
48
+ {open && <AccessibilityModal onClose={() => setOpen(false)} />}
49
+ </>
50
+ );
51
+ }