odoro 1.0.2 → 1.0.4

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.
@@ -21,7 +21,7 @@
21
21
 
22
22
  import { Reveal, Stagger } from '@odoro-cli/libs/motion'
23
23
  import { buttonClasses } from '@odoro-cli/libs/ui'
24
- import { useState, type CSSProperties, type ReactElement } from 'react'
24
+ import { useEffect, useState, type CSSProperties, type ReactElement } from 'react'
25
25
 
26
26
  import { Fond } from '@/fond'
27
27
 
@@ -87,20 +87,137 @@ function Barre(): ReactElement {
87
87
  </span>
88
88
  </span>
89
89
 
90
- <a
91
- href="https://odoro.dev"
92
- target="_blank"
93
- rel="noreferrer"
94
- className="o-ml-auto o-inline-flex o-h-14 o-shrink-0 o-items-center o-rounded-full o-border-w-1 o-px-5 o-text-sm o-no-underline o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
95
- style={GELULE}
96
- >
97
- odoro.dev
98
- </a>
90
+ <div className="o-ml-auto o-flex o-items-center o-gap-3">
91
+ <a
92
+ href="https://odoro.dev"
93
+ target="_blank"
94
+ rel="noreferrer"
95
+ className="o-inline-flex o-h-14 o-shrink-0 o-items-center o-rounded-full o-border-w-1 o-px-5 o-text-sm o-no-underline o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
96
+ style={GELULE}
97
+ >
98
+ odoro.dev
99
+ </a>
100
+ <BasculeTheme />
101
+ </div>
99
102
  </div>
100
103
  </header>
101
104
  )
102
105
  }
103
106
 
107
+ /* -------------------------------------------------------------------------- */
108
+ /* Theme */
109
+ /* -------------------------------------------------------------------------- */
110
+
111
+ /** Les trois etats de la bascule. */
112
+ type Theme = 'systeme' | 'clair' | 'sombre'
113
+
114
+ /** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
115
+ const CLE_THEME = 'odoro-theme'
116
+
117
+ /** L'ordre du cycle, au clic. */
118
+ const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
119
+
120
+ /**
121
+ * Pose le theme sur la racine du document.
122
+ *
123
+ * `systeme` retire l'attribut plutot que d'en poser un troisieme : la feuille
124
+ * de style ecoute `data-theme`, et son absence rend la main a la preference du
125
+ * navigateur. C'est ce qui permet a une page laissee en « systeme » de suivre
126
+ * le passage en nuit sans etre rouverte.
127
+ */
128
+ function poserTheme(theme: Theme): void {
129
+ if (theme === 'systeme') delete document.documentElement.dataset.theme
130
+ else document.documentElement.dataset.theme = theme === 'clair' ? 'light' : 'dark'
131
+ }
132
+
133
+ /** Le theme memorise, ou `systeme`. */
134
+ function themeMemorise(): Theme {
135
+ try {
136
+ const valeur = localStorage.getItem(CLE_THEME)
137
+ if (valeur === 'light') return 'clair'
138
+ if (valeur === 'dark') return 'sombre'
139
+ return 'systeme'
140
+ } catch {
141
+ // Stockage refuse — fenetre privee, cookies bloques. Le theme du systeme
142
+ // reste une reponse valable ; echouer ici priverait la page de son rendu.
143
+ return 'systeme'
144
+ }
145
+ }
146
+
147
+ /** Les trois pictogrammes, dessines plutot qu'importes : les icones sont optionnelles. */
148
+ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
149
+ const commun = {
150
+ viewBox: '0 0 24 24',
151
+ fill: 'none',
152
+ stroke: 'currentColor',
153
+ strokeWidth: 1.8,
154
+ strokeLinecap: 'round' as const,
155
+ strokeLinejoin: 'round' as const,
156
+ 'aria-hidden': true,
157
+ style: { width: '1.15rem', height: '1.15rem' },
158
+ }
159
+
160
+ if (theme === 'clair') {
161
+ return (
162
+ <svg {...commun}>
163
+ <circle cx="12" cy="12" r="4" />
164
+ <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
165
+ </svg>
166
+ )
167
+ }
168
+
169
+ if (theme === 'sombre') {
170
+ return (
171
+ <svg {...commun}>
172
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
173
+ </svg>
174
+ )
175
+ }
176
+
177
+ return (
178
+ <svg {...commun}>
179
+ <rect x="2" y="4" width="20" height="13" rx="2" />
180
+ <path d="M8 21h8M12 17v4" />
181
+ </svg>
182
+ )
183
+ }
184
+
185
+ /**
186
+ * La bascule de theme.
187
+ *
188
+ * Trois etats et non deux : « systeme » est un choix a part entiere, et le
189
+ * retirer obligerait un visiteur a re-choisir a chaque changement de sa
190
+ * preference.
191
+ */
192
+ function BasculeTheme(): ReactElement {
193
+ const [theme, setTheme] = useState<Theme>(() => themeMemorise())
194
+
195
+ useEffect(() => {
196
+ poserTheme(theme)
197
+ try {
198
+ if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
199
+ else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
200
+ } catch {
201
+ // Le theme reste applique pour la session, meme sans stockage.
202
+ }
203
+ }, [theme])
204
+
205
+ return (
206
+ <button
207
+ type="button"
208
+ aria-label={`Theme : ${theme}`}
209
+ title={`Theme : ${theme}`}
210
+ onClick={() => {
211
+ setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
212
+ }}
213
+ className="o-inline-flex o-size-14 o-shrink-0 o-cursor-pointer o-items-center o-justify-center o-rounded-full o-border-w-1 o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
214
+ style={GELULE}
215
+ >
216
+ <Pictogramme theme={theme} />
217
+ </button>
218
+ )
219
+ }
220
+
104
221
  /* -------------------------------------------------------------------------- */
105
222
  /* Sections */
106
223
  /* -------------------------------------------------------------------------- */
@@ -312,10 +429,13 @@ function Cloture(): ReactElement {
312
429
  /** Racine de l'application. */
313
430
  export function App(): ReactElement {
314
431
  return (
315
- <div className="app-shell">
432
+ // `relative` porte le fond, pose ici et non dans le contenu : ancre plus
433
+ // bas, il commencait sous la barre et laissait une bande plus sombre
434
+ // derriere elle en haut de page.
435
+ <div className="app-shell o-relative">
436
+ <Fond />
316
437
  <Barre />
317
438
  <main className="o-relative">
318
- <Fond />
319
439
  <Hero />
320
440
  <Piliers />
321
441
  <Cloture />
@@ -4,7 +4,18 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
+ <meta name="theme-color" content="#3b82f6" />
7
8
  <title>Odoro</title>
9
+ <script>
10
+ // Applique le theme memorise avant la premiere peinture. Le faire depuis
11
+ // React arriverait trop tard : un visiteur en theme force verrait un
12
+ // eclair de l'autre theme, le temps que le script se charge.
13
+ try {
14
+ var theme = localStorage.getItem('odoro-theme')
15
+ if (theme === 'light' || theme === 'dark')
16
+ document.documentElement.dataset.theme = theme
17
+ } catch (error) {}
18
+ </script>
8
19
  </head>
9
20
  <body>
10
21
  <div id="root"></div>
@@ -23,7 +23,17 @@
23
23
 
24
24
  import { Reveal, Stagger } from '@odoro-cli/libs/motion'
25
25
  import { buttonClasses } from '@odoro-cli/libs/ui'
26
- import { useState, type CSSProperties, type ReactElement, type ReactNode } from 'react'
26
+ import {
27
+ useEffect,
28
+ useState,
29
+ type CSSProperties,
30
+ type ReactElement,
31
+ type ReactNode,
32
+ } from 'react'
33
+
34
+ // Nommes plutot qu'importes en bloc : le reste du manifeste — les scripts,
35
+ // les metadonnees — n'a rien a faire dans le code livre au navigateur.
36
+ import { dependencies, devDependencies } from '../package.json'
27
37
 
28
38
  import { Fond } from '@/fond'
29
39
  import { Link, Routeur, useLocation } from '@/router'
@@ -133,20 +143,137 @@ function Barre(): ReactElement {
133
143
  ))}
134
144
  </nav>
135
145
 
136
- <a
137
- href="https://odoro.dev"
138
- target="_blank"
139
- rel="noreferrer"
140
- className="o-ml-auto o-inline-flex o-h-14 o-shrink-0 o-items-center o-rounded-full o-border-w-1 o-px-5 o-text-sm o-no-underline o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
141
- style={GELULE}
142
- >
143
- odoro.dev
144
- </a>
146
+ <div className="o-ml-auto o-flex o-items-center o-gap-3">
147
+ <a
148
+ href="https://odoro.dev"
149
+ target="_blank"
150
+ rel="noreferrer"
151
+ className="o-inline-flex o-h-14 o-shrink-0 o-items-center o-rounded-full o-border-w-1 o-px-5 o-text-sm o-no-underline o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
152
+ style={GELULE}
153
+ >
154
+ odoro.dev
155
+ </a>
156
+ <BasculeTheme />
157
+ </div>
145
158
  </div>
146
159
  </header>
147
160
  )
148
161
  }
149
162
 
163
+ /* -------------------------------------------------------------------------- */
164
+ /* Theme */
165
+ /* -------------------------------------------------------------------------- */
166
+
167
+ /** Les trois etats de la bascule. */
168
+ type Theme = 'systeme' | 'clair' | 'sombre'
169
+
170
+ /** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
171
+ const CLE_THEME = 'odoro-theme'
172
+
173
+ /** L'ordre du cycle, au clic. */
174
+ const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
175
+
176
+ /**
177
+ * Pose le theme sur la racine du document.
178
+ *
179
+ * `systeme` retire l'attribut plutot que d'en poser un troisieme : la feuille
180
+ * de style ecoute `data-theme`, et son absence rend la main a la preference du
181
+ * navigateur. C'est ce qui permet a une page laissee en « systeme » de suivre
182
+ * le passage en nuit sans etre rouverte.
183
+ */
184
+ function poserTheme(theme: Theme): void {
185
+ if (theme === 'systeme') delete document.documentElement.dataset.theme
186
+ else document.documentElement.dataset.theme = theme === 'clair' ? 'light' : 'dark'
187
+ }
188
+
189
+ /** Le theme memorise, ou `systeme`. */
190
+ function themeMemorise(): Theme {
191
+ try {
192
+ const valeur = localStorage.getItem(CLE_THEME)
193
+ if (valeur === 'light') return 'clair'
194
+ if (valeur === 'dark') return 'sombre'
195
+ return 'systeme'
196
+ } catch {
197
+ // Stockage refuse — fenetre privee, cookies bloques. Le theme du systeme
198
+ // reste une reponse valable ; echouer ici priverait la page de son rendu.
199
+ return 'systeme'
200
+ }
201
+ }
202
+
203
+ /** Les trois pictogrammes, dessines plutot qu'importes : les icones sont optionnelles. */
204
+ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
205
+ const commun = {
206
+ viewBox: '0 0 24 24',
207
+ fill: 'none',
208
+ stroke: 'currentColor',
209
+ strokeWidth: 1.8,
210
+ strokeLinecap: 'round' as const,
211
+ strokeLinejoin: 'round' as const,
212
+ 'aria-hidden': true,
213
+ style: { width: '1.15rem', height: '1.15rem' },
214
+ }
215
+
216
+ if (theme === 'clair') {
217
+ return (
218
+ <svg {...commun}>
219
+ <circle cx="12" cy="12" r="4" />
220
+ <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
221
+ </svg>
222
+ )
223
+ }
224
+
225
+ if (theme === 'sombre') {
226
+ return (
227
+ <svg {...commun}>
228
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
229
+ </svg>
230
+ )
231
+ }
232
+
233
+ return (
234
+ <svg {...commun}>
235
+ <rect x="2" y="4" width="20" height="13" rx="2" />
236
+ <path d="M8 21h8M12 17v4" />
237
+ </svg>
238
+ )
239
+ }
240
+
241
+ /**
242
+ * La bascule de theme.
243
+ *
244
+ * Trois etats et non deux : « systeme » est un choix a part entiere, et le
245
+ * retirer obligerait un visiteur a re-choisir a chaque changement de sa
246
+ * preference.
247
+ */
248
+ function BasculeTheme(): ReactElement {
249
+ const [theme, setTheme] = useState<Theme>(() => themeMemorise())
250
+
251
+ useEffect(() => {
252
+ poserTheme(theme)
253
+ try {
254
+ if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
255
+ else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
256
+ } catch {
257
+ // Le theme reste applique pour la session, meme sans stockage.
258
+ }
259
+ }, [theme])
260
+
261
+ return (
262
+ <button
263
+ type="button"
264
+ aria-label={`Theme : ${theme}`}
265
+ title={`Theme : ${theme}`}
266
+ onClick={() => {
267
+ setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
268
+ }}
269
+ className="o-inline-flex o-size-14 o-shrink-0 o-cursor-pointer o-items-center o-justify-center o-rounded-full o-border-w-1 o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
270
+ style={GELULE}
271
+ >
272
+ <Pictogramme theme={theme} />
273
+ </button>
274
+ )
275
+ }
276
+
150
277
  /* -------------------------------------------------------------------------- */
151
278
  /* Sections de la page d'accueil */
152
279
  /* -------------------------------------------------------------------------- */
@@ -366,7 +493,6 @@ function Cloture(): ReactElement {
366
493
  function Accueil(): ReactElement {
367
494
  return (
368
495
  <>
369
- <Fond />
370
496
  <Hero />
371
497
  <Piliers />
372
498
  <Cloture />
@@ -374,12 +500,32 @@ function Accueil(): ReactElement {
374
500
  )
375
501
  }
376
502
 
377
- /** Ce que le projet embarque, tel qu'il a ete cree. */
378
- const REPERES = [
379
- { cle: 'Moteur', valeur: 'odoro — serveur de developpement et compilation' },
380
- { cle: 'Styles', valeur: '@odoro-cli/libs jetons, utilitaires, composants' },
381
- { cle: 'Registre', valeur: 'register.odoro.devcopie dans votre projet' },
382
- ]
503
+ /**
504
+ * Ce que chaque paquet de la famille apporte.
505
+ *
506
+ * La table decrit ; elle ne decide pas. Ce qui s'affiche vient du manifeste du
507
+ * projet, donc de ce qui a reellement ete installe et cela reste vrai si vous
508
+ * ajoutez ou retirez un paquet plus tard.
509
+ */
510
+ const ROLES: Readonly<Record<string, string>> = {
511
+ odoro: 'Serveur de developpement, compilation et registre',
512
+ '@odoro-cli/libs': 'Jetons, utilitaires, composants et routeur',
513
+ '@odoro-cli/icons': 'Cinq familles d icones, importables une a une',
514
+ '@odoro-cli/engine': 'WebGL, surfaces et politique de mouvement',
515
+ '@odoro-cli/server': 'Socle back-end modulaire',
516
+ }
517
+
518
+ /**
519
+ * Les paquets de la famille presents dans ce projet.
520
+ *
521
+ * Les deux champs sont lus, car `odoro` est une dependance de developpement
522
+ * quand les autres sont des dependances de production. Une liste ecrite a la
523
+ * main mentirait des la premiere case decochee a la creation.
524
+ */
525
+ const REPERES = Object.entries({ ...dependencies, ...devDependencies })
526
+ .filter(([nom]) => nom in ROLES)
527
+ .sort(([a], [b]) => a.localeCompare(b))
528
+ .map(([nom, version]) => ({ nom, version, role: ROLES[nom] ?? '' }))
383
529
 
384
530
  /** Une seconde page, pour montrer le routeur a l'oeuvre. */
385
531
  function APropos(): ReactElement {
@@ -396,12 +542,17 @@ function APropos(): ReactElement {
396
542
  <dl className="o-mt-10 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
397
543
  {REPERES.map((repere) => (
398
544
  <div
399
- key={repere.cle}
545
+ key={repere.nom}
400
546
  className="o-flex o-flex-col md:o-flex-row o-gap-1 md:o-gap-6 o-border-b o-border-zinc-200 dark:o-border-zinc-800 o-py-4"
401
547
  >
402
- <dt className="o-w-32 o-shrink-0 o-text-sm o-font-medium">{repere.cle}</dt>
548
+ <dt className="o-w-56 o-shrink-0 o-font-mono o-text-sm o-font-medium">
549
+ {repere.nom}
550
+ <span className="o-ml-2 o-text-xs o-font-normal o-text-zinc-400">
551
+ {repere.version}
552
+ </span>
553
+ </dt>
403
554
  <dd className="o-m-0 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
404
- {repere.valeur}
555
+ {repere.role}
405
556
  </dd>
406
557
  </div>
407
558
  ))}
@@ -434,9 +585,13 @@ function Introuvable(): ReactElement {
434
585
  /** L'enveloppe commune : barre, contenu, pied de page. */
435
586
  function Coquille(contenu: ReactNode): ReactElement {
436
587
  return (
437
- <div className="app-shell">
588
+ // `relative` porte le fond decoratif, qui se place en absolu dedans. Il est
589
+ // pose ici et non dans la page : ancre au haut de `main`, il commencait
590
+ // sous la barre, et la bande restee derriere elle se voyait en haut de
591
+ // page comme un rectangle plus sombre.
592
+ <div className="app-shell o-relative">
593
+ <Fond />
438
594
  <Barre />
439
- {/* `relative` porte le fond decoratif, qui se place en absolu dedans. */}
440
595
  <main className="o-relative o-view-transition-page">{contenu}</main>
441
596
  <footer className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-8">
442
597
  <div className="o-mx-auto o-flex o-w-full o-max-w-5xl o-items-center o-justify-between o-gap-4 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
@@ -14,7 +14,7 @@
14
14
  * @module
15
15
  */
16
16
 
17
- import { useState, type ReactElement } from 'react'
17
+ import { useEffect, useState, type ReactElement } from 'react'
18
18
 
19
19
  import { Fond } from '@/fond'
20
20
 
@@ -57,19 +57,128 @@ function Barre(): ReactElement {
57
57
  <span className="marque-mot">ODORO</span>
58
58
  </span>
59
59
 
60
- <a
61
- href="https://odoro.dev"
62
- target="_blank"
63
- rel="noreferrer"
64
- className="gelule gelule-lien"
65
- >
66
- odoro.dev
67
- </a>
60
+ <div className="barre-fin">
61
+ <a
62
+ href="https://odoro.dev"
63
+ target="_blank"
64
+ rel="noreferrer"
65
+ className="gelule gelule-lien"
66
+ >
67
+ odoro.dev
68
+ </a>
69
+ <BasculeTheme />
70
+ </div>
68
71
  </div>
69
72
  </header>
70
73
  )
71
74
  }
72
75
 
76
+ /* -------------------------------------------------------------------------- */
77
+ /* Theme */
78
+ /* -------------------------------------------------------------------------- */
79
+
80
+ /** Les trois etats de la bascule. */
81
+ type Theme = 'systeme' | 'clair' | 'sombre'
82
+
83
+ /** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
84
+ const CLE_THEME = 'odoro-theme'
85
+
86
+ /** L'ordre du cycle, au clic. */
87
+ const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
88
+
89
+ /**
90
+ * Pose le theme sur la racine du document.
91
+ *
92
+ * `systeme` retire l'attribut plutot que d'en poser un troisieme : la feuille
93
+ * de style ecoute `data-theme`, et son absence rend la main a la preference du
94
+ * navigateur.
95
+ */
96
+ function poserTheme(theme: Theme): void {
97
+ if (theme === 'systeme') delete document.documentElement.dataset.theme
98
+ else document.documentElement.dataset.theme = theme === 'clair' ? 'light' : 'dark'
99
+ }
100
+
101
+ /** Le theme memorise, ou `systeme`. */
102
+ function themeMemorise(): Theme {
103
+ try {
104
+ const valeur = localStorage.getItem(CLE_THEME)
105
+ if (valeur === 'light') return 'clair'
106
+ if (valeur === 'dark') return 'sombre'
107
+ return 'systeme'
108
+ } catch {
109
+ // Stockage refuse — fenetre privee, cookies bloques. Le theme du systeme
110
+ // reste une reponse valable.
111
+ return 'systeme'
112
+ }
113
+ }
114
+
115
+ /** Les trois pictogrammes, dessines dans le flux. */
116
+ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
117
+ const commun = {
118
+ viewBox: '0 0 24 24',
119
+ fill: 'none',
120
+ stroke: 'currentColor',
121
+ strokeWidth: 1.8,
122
+ strokeLinecap: 'round' as const,
123
+ strokeLinejoin: 'round' as const,
124
+ 'aria-hidden': true,
125
+ style: { width: '1.15rem', height: '1.15rem' },
126
+ }
127
+
128
+ if (theme === 'clair') {
129
+ return (
130
+ <svg {...commun}>
131
+ <circle cx="12" cy="12" r="4" />
132
+ <path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
133
+ </svg>
134
+ )
135
+ }
136
+
137
+ if (theme === 'sombre') {
138
+ return (
139
+ <svg {...commun}>
140
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
141
+ </svg>
142
+ )
143
+ }
144
+
145
+ return (
146
+ <svg {...commun}>
147
+ <rect x="2" y="4" width="20" height="13" rx="2" />
148
+ <path d="M8 21h8M12 17v4" />
149
+ </svg>
150
+ )
151
+ }
152
+
153
+ /** La bascule de theme : systeme, clair, sombre. */
154
+ function BasculeTheme(): ReactElement {
155
+ const [theme, setTheme] = useState<Theme>(() => themeMemorise())
156
+
157
+ useEffect(() => {
158
+ poserTheme(theme)
159
+ try {
160
+ if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
161
+ else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
162
+ } catch {
163
+ // Le theme reste applique pour la session, meme sans stockage.
164
+ }
165
+ }, [theme])
166
+
167
+ return (
168
+ <button
169
+ type="button"
170
+ aria-label={`Theme : ${theme}`}
171
+ title={`Theme : ${theme}`}
172
+ className="gelule bascule"
173
+ onClick={() => {
174
+ setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
175
+ }}
176
+ >
177
+ <Pictogramme theme={theme} />
178
+ </button>
179
+ )
180
+ }
181
+
73
182
  /* -------------------------------------------------------------------------- */
74
183
  /* Sections */
75
184
  /* -------------------------------------------------------------------------- */
@@ -249,10 +358,12 @@ function Cloture(): ReactElement {
249
358
  /** Racine de l'application. */
250
359
  export function App(): ReactElement {
251
360
  return (
252
- <div className="app-shell">
361
+ // Le fond est pose ici et non dans le contenu : ancre plus bas, il
362
+ // commencait sous la barre et laissait une bande plus sombre derriere elle.
363
+ <div className="app-shell app-shell-fond">
364
+ <Fond />
253
365
  <Barre />
254
366
  <main className="principal">
255
- <Fond />
256
367
  <Hero />
257
368
  <Piliers />
258
369
  <Cloture />
@@ -21,11 +21,14 @@
21
21
  color-scheme: light dark;
22
22
  }
23
23
 
24
- /* La preference du systeme, faute d'un selecteur de theme : c'est ce que les
25
- jetons apporteraient, et l'absence de bibliotheque ne doit pas se payer d'un
26
- fond blanc au milieu de la nuit. */
24
+ /* Les valeurs du theme sombre, ecrites une fois et reprises deux fois.
25
+
26
+ La bascule pose `data-theme` sur la racine ; sans attribut, la preference du
27
+ navigateur decide. Les deux regles sont donc necessaires : la seconde sans la
28
+ premiere ignorerait le choix du visiteur, la premiere sans la seconde
29
+ rendrait un fond blanc au milieu de la nuit. */
27
30
  @media (prefers-color-scheme: dark) {
28
- :root {
31
+ :root:not([data-theme='light']) {
29
32
  --fond: #09090b;
30
33
  --encre: #fafafa;
31
34
  --doux: #a1a1aa;
@@ -34,6 +37,25 @@
34
37
  }
35
38
  }
36
39
 
40
+ :root[data-theme='dark'] {
41
+ --fond: #09090b;
42
+ --encre: #fafafa;
43
+ --doux: #a1a1aa;
44
+ --filet: #27272a;
45
+ --surface: #18181b;
46
+ }
47
+
48
+ /* Le choix explicite est dit au navigateur : sans cela, `light-dark()`, les
49
+ controles natifs et les barres de defilement continuent de suivre un systeme
50
+ en nuit alors que le visiteur a demande le clair. */
51
+ :root[data-theme='light'] {
52
+ color-scheme: light;
53
+ }
54
+
55
+ :root[data-theme='dark'] {
56
+ color-scheme: dark;
57
+ }
58
+
37
59
  * {
38
60
  box-sizing: border-box;
39
61
  }
@@ -93,12 +115,30 @@ pre {
93
115
  text-decoration: none;
94
116
  }
95
117
 
96
- .gelule-lien {
118
+ .barre-fin {
97
119
  margin-left: auto;
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 0.75rem;
123
+ }
124
+
125
+ .gelule-lien {
98
126
  color: var(--doux);
99
127
  font-size: 0.875rem;
100
128
  }
101
129
 
130
+ .bascule {
131
+ width: 3.5rem;
132
+ justify-content: center;
133
+ padding: 0;
134
+ color: var(--doux);
135
+ cursor: pointer;
136
+ }
137
+
138
+ .bascule:hover {
139
+ color: var(--encre);
140
+ }
141
+
102
142
  .gelule-lien:hover {
103
143
  color: var(--encre);
104
144
  }
@@ -125,6 +165,10 @@ pre {
125
165
 
126
166
  /* ----- Coquille ----------------------------------------------------------- */
127
167
 
168
+ .app-shell-fond {
169
+ position: relative;
170
+ }
171
+
128
172
  .principal {
129
173
  position: relative;
130
174
  }