@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 +21 -0
- package/README.md +245 -0
- package/bin/cli.mjs +283 -0
- package/package.json +29 -0
- package/react/AccessibilityButton.tsx +51 -0
- package/react/AccessibilityFab.tsx +99 -0
- package/react/AccessibilityModal.tsx +328 -0
- package/react/engine.ts +319 -0
- package/react/icons.tsx +19 -0
- package/react/index.ts +32 -0
- package/react/package.json +17 -0
- package/vanilla/acsblt-accessibility-head.js +17 -0
- package/vanilla/acsblt-accessibility.css +159 -0
- package/vanilla/acsblt-accessibility.js +383 -0
- package/vanilla/wordpress-example.php +53 -0
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
|
+
}
|