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.
- package/README.md +9 -9
- package/dist/build-KI7IEVRJ.js +4 -0
- package/dist/{chunk-R6PMAMXA.js → chunk-2EE5QUCW.js} +16 -3
- package/dist/{chunk-CJIUP7A3.js → chunk-FJYUQALD.js} +1 -1
- package/dist/cli.js +4 -4
- package/dist/{create-GD6O2TPL.js → create-LVNBZOGV.js} +21 -5
- package/dist/index.js +2 -2
- package/dist/{package-VMTZDORI.js → package-5KU4PW5P.js} +7 -5
- package/dist/{server-ATGFRVSW.js → server-DXOQAPVX.js} +1 -1
- package/package.json +6 -4
- package/templates/react-ts/_variantes/sans-libs/src/App.tsx +122 -11
- package/templates/react-ts/_variantes/sans-libs/src/styles.css +49 -5
- package/templates/react-ts/_variantes/sans-routeur/src/App.tsx +132 -12
- package/templates/react-ts/index.html +11 -0
- package/templates/react-ts/src/App.tsx +177 -22
- package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +122 -11
- package/templates/react-ts-server/_variantes/sans-libs/client/src/styles.css +49 -5
- package/templates/react-ts-server/_variantes/sans-routeur/client/src/App.tsx +132 -12
- package/templates/react-ts-server/client/index.html +11 -0
- package/templates/react-ts-server/client/src/App.tsx +177 -22
- package/dist/build-GIPMPYJ3.js +0 -4
|
@@ -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
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
/**
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
.
|
|
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
|
}
|