odoro 1.0.1 → 1.0.2

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.
Files changed (46) hide show
  1. package/README.md +73 -0
  2. package/dist/cli.js +2 -2
  3. package/dist/{create-LTT3SAWT.js → create-GD6O2TPL.js} +107 -63
  4. package/dist/{package-LK46RV2I.js → package-VMTZDORI.js} +1 -1
  5. package/dist/registry/index.d.ts +2 -2
  6. package/package.json +1 -1
  7. package/templates/{react-ts-server/_variantes/avec-moteur/client/src/sections/Fond.tsx → react-ts/_variantes/avec-moteur/src/fond.tsx} +13 -13
  8. package/templates/react-ts/_variantes/sans-libs/src/App.tsx +250 -21
  9. package/templates/react-ts/_variantes/sans-libs/src/{sections/Fond.tsx → fond.tsx} +1 -1
  10. package/templates/react-ts/_variantes/sans-libs/src/styles.css +68 -35
  11. package/templates/react-ts/_variantes/sans-routeur/src/App.tsx +309 -24
  12. package/templates/react-ts/public/favicon.svg +2 -3
  13. package/templates/react-ts/src/App.tsx +422 -49
  14. package/templates/react-ts/src/{sections/Fond.tsx → fond.tsx} +9 -9
  15. package/templates/react-ts/src/router.tsx +72 -0
  16. package/templates/{react-ts/_variantes/avec-moteur/src/sections/Fond.tsx → react-ts-server/_variantes/avec-moteur/client/src/fond.tsx} +13 -13
  17. package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +250 -21
  18. package/templates/react-ts-server/_variantes/sans-libs/client/src/{sections/Fond.tsx → fond.tsx} +1 -1
  19. package/templates/react-ts-server/_variantes/sans-libs/client/src/styles.css +68 -35
  20. package/templates/react-ts-server/_variantes/sans-routeur/client/src/App.tsx +309 -24
  21. package/templates/react-ts-server/client/public/favicon.svg +2 -3
  22. package/templates/react-ts-server/client/src/App.tsx +422 -49
  23. package/templates/react-ts-server/client/src/{sections/Fond.tsx → fond.tsx} +9 -9
  24. package/templates/react-ts-server/client/src/router.tsx +72 -0
  25. package/templates/react-ts/_variantes/sans-libs/src/composants/Marque.tsx +0 -37
  26. package/templates/react-ts/_variantes/sans-libs/src/sections/Cloture.tsx +0 -60
  27. package/templates/react-ts/_variantes/sans-libs/src/sections/Hero.tsx +0 -92
  28. package/templates/react-ts/_variantes/sans-libs/src/sections/Piliers.tsx +0 -44
  29. package/templates/react-ts/src/composants/Marque.tsx +0 -50
  30. package/templates/react-ts/src/routes/About.tsx +0 -43
  31. package/templates/react-ts/src/routes/Home.tsx +0 -28
  32. package/templates/react-ts/src/routes/NotFound.tsx +0 -14
  33. package/templates/react-ts/src/sections/Cloture.tsx +0 -69
  34. package/templates/react-ts/src/sections/Hero.tsx +0 -119
  35. package/templates/react-ts/src/sections/Piliers.tsx +0 -68
  36. package/templates/react-ts-server/_variantes/sans-libs/client/src/composants/Marque.tsx +0 -37
  37. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Cloture.tsx +0 -60
  38. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Hero.tsx +0 -92
  39. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Piliers.tsx +0 -44
  40. package/templates/react-ts-server/client/src/composants/Marque.tsx +0 -50
  41. package/templates/react-ts-server/client/src/routes/About.tsx +0 -43
  42. package/templates/react-ts-server/client/src/routes/Home.tsx +0 -28
  43. package/templates/react-ts-server/client/src/routes/NotFound.tsx +0 -14
  44. package/templates/react-ts-server/client/src/sections/Cloture.tsx +0 -69
  45. package/templates/react-ts-server/client/src/sections/Hero.tsx +0 -119
  46. package/templates/react-ts-server/client/src/sections/Piliers.tsx +0 -68
@@ -1,10 +1,82 @@
1
- import { Link, Outlet, Route, Router, Routes, useLocation } from '@odoro-cli/libs/router'
2
- import type { ReactElement } from 'react'
1
+ /**
2
+ * Toute la page, en un fichier.
3
+ *
4
+ * ## Pourquoi tout ici
5
+ *
6
+ * Un projet qui demarre se lit mieux d'un seul tenant : on voit la navigation,
7
+ * les sections et le pied de page sans ouvrir huit fichiers. Chaque bloc est
8
+ * une fonction ; le jour ou l'un d'eux grossit, il se deplace dans son propre
9
+ * fichier d'un couper-coller.
10
+ *
11
+ * Deux choses vivent a part, et pour une raison : le **routeur**, qui est la
12
+ * seule dependance de routage et se change sans toucher a la page ; et le
13
+ * **fond**, qui est le seul morceau a dependre du moteur.
14
+ *
15
+ * ## La barre de navigation
16
+ *
17
+ * Elle ne peint pas de bandeau : ce sont des gelules posees sur ce qu'il y a
18
+ * derriere, avec un flou d'arriere-plan. Le fond decoratif continue donc de
19
+ * passer dessous, au lieu d'etre coupe par une bande opaque.
20
+ *
21
+ * @module
22
+ */
23
+
24
+ import { Reveal, Stagger } from '@odoro-cli/libs/motion'
25
+ import { buttonClasses } from '@odoro-cli/libs/ui'
26
+ import { useState, type CSSProperties, type ReactElement, type ReactNode } from 'react'
27
+
28
+ import { Fond } from '@/fond'
29
+ import { Link, Routeur, useLocation } from '@/router'
3
30
 
4
- import { Marque } from '@/composants/Marque'
5
- import { About } from '@/routes/About'
6
- import { Home } from '@/routes/Home'
7
- import { NotFound } from '@/routes/NotFound'
31
+ /* -------------------------------------------------------------------------- */
32
+ /* Marque */
33
+ /* -------------------------------------------------------------------------- */
34
+
35
+ /**
36
+ * Le signe de la marque.
37
+ *
38
+ * Dessine dans le flux plutot que charge depuis `public/` : il prend la couleur
39
+ * de son parent par `currentColor`, donc la teinte de marque en clair comme en
40
+ * sombre, sans qu'il y ait deux fichiers a tenir.
41
+ *
42
+ * La taille est en style et non en classe : une classe utilitaire a valeur
43
+ * arbitraire n'est emise que si le compilateur l'a vue passer, et une classe
44
+ * absente ne peint rien — le signe serait sans dimensions, donc invisible.
45
+ */
46
+ function Signe({ taille = '1.75rem' }: { readonly taille?: string }): ReactElement {
47
+ return (
48
+ <svg
49
+ viewBox="0 0 100 100"
50
+ fill="none"
51
+ aria-hidden="true"
52
+ style={{ width: taille, height: taille, flexShrink: 0 }}
53
+ >
54
+ <path
55
+ d="M20.25 20.25H50a29.75 29.75 0 1 1-29.75 29.75Z"
56
+ stroke="currentColor"
57
+ strokeWidth="10.5"
58
+ strokeLinejoin="miter"
59
+ />
60
+ </svg>
61
+ )
62
+ }
63
+
64
+ /* -------------------------------------------------------------------------- */
65
+ /* Barre de navigation */
66
+ /* -------------------------------------------------------------------------- */
67
+
68
+ /**
69
+ * La surface d'une gelule.
70
+ *
71
+ * Le systeme decline les echelles de noir et de blanc, mais pas leurs variantes
72
+ * sombres en alpha : `o-bg-white/70` et son pendant `dark:` n'existent pas en
73
+ * une seule classe. Le melange se fait donc en style, ou il est exact et suit
74
+ * le theme sans qu'on ait a redire chaque couleur.
75
+ */
76
+ const GELULE: CSSProperties = {
77
+ backgroundColor: 'color-mix(in oklab, var(--o-theme-bg) 72%, transparent)',
78
+ borderColor: 'color-mix(in oklab, var(--o-theme-fg) 12%, transparent)',
79
+ }
8
80
 
9
81
  /** Les entrees de la navigation. */
10
82
  const LIENS = [
@@ -12,64 +84,368 @@ const LIENS = [
12
84
  { to: '/a-propos', label: 'A propos' },
13
85
  ]
14
86
 
15
- /**
16
- * La barre de navigation.
17
- *
18
- * Elle flotte sur le fond plutot que de le couper : un filet clair et un flou
19
- * d'arriere-plan suffisent a la detacher, et la nappe de couleur continue de
20
- * passer dessous.
21
- */
22
- function Nav(): ReactElement {
87
+ /** Un lien de la gelule de navigation, actif sur la route courante. */
88
+ function LienHaut({ to, label }: { readonly to: string; readonly label: string }): ReactElement {
23
89
  const { pathname } = useLocation()
90
+ const actif = pathname === to
24
91
 
25
92
  return (
26
- <nav className="o-sticky o-top-0 o-z-20 o-border-b o-border-zinc-200/70 dark:o-border-zinc-800/70 o-bg-white/70 dark:o-bg-zinc-950/70 o-backdrop-blur">
27
- <div className="o-mx-auto o-flex o-w-full o-max-w-5xl o-items-center o-gap-6 o-px-6 o-py-4">
28
- <Link to="/" className="o-no-underline o-text-zinc-900 dark:o-text-zinc-50">
29
- <Marque />
93
+ <Link
94
+ to={to}
95
+ className={[
96
+ 'o-rounded-full o-px-4 o-py-2 o-text-sm o-no-underline o-transition-colors',
97
+ actif
98
+ ? 'o-font-medium o-text-zinc-950 dark:o-text-white'
99
+ : 'o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white',
100
+ ].join(' ')}
101
+ style={actif ? GELULE : undefined}
102
+ >
103
+ {label}
104
+ </Link>
105
+ )
106
+ }
107
+
108
+ /** La barre : la marque, la navigation, et le lien vers le site. */
109
+ function Barre(): ReactElement {
110
+ return (
111
+ <header className="o-sticky o-top-0 o-z-20 o-px-5 md:o-px-8">
112
+ <div className="o-mx-auto o-flex o-h-20 o-w-full o-max-w-5xl o-items-center o-gap-3">
113
+ <Link
114
+ to="/"
115
+ className="o-inline-flex o-h-14 o-shrink-0 o-items-center o-gap-2.5 o-rounded-full o-border-w-1 o-pl-5 o-pr-6 o-no-underline o-backdrop-blur-xl"
116
+ style={GELULE}
117
+ >
118
+ <span className="o-text-brand-500">
119
+ <Signe />
120
+ </span>
121
+ <span className="o-text-lg o-font-bold o-tracking-wide o-text-zinc-950 dark:o-text-white">
122
+ ODORO
123
+ </span>
30
124
  </Link>
31
125
 
32
- <div className="o-flex o-items-center o-gap-5 o-text-sm">
126
+ <nav
127
+ aria-label="Navigation"
128
+ className="o-inline-flex o-h-14 o-items-center o-gap-1 o-rounded-full o-border-w-1 o-px-2 o-backdrop-blur-xl"
129
+ style={GELULE}
130
+ >
33
131
  {LIENS.map((lien) => (
34
- <Link
35
- key={lien.to}
36
- to={lien.to}
37
- className={
38
- pathname === lien.to
39
- ? 'o-font-medium o-text-zinc-900 dark:o-text-zinc-50 o-no-underline'
40
- : 'o-text-zinc-500 dark:o-text-zinc-400 o-no-underline hover:o-text-zinc-900 dark:hover:o-text-zinc-50 o-transition'
41
- }
42
- >
43
- {lien.label}
44
- </Link>
132
+ <LienHaut key={lien.to} to={lien.to} label={lien.label} />
45
133
  ))}
46
- </div>
134
+ </nav>
47
135
 
48
136
  <a
49
137
  href="https://odoro.dev"
50
138
  target="_blank"
51
139
  rel="noreferrer"
52
- className="o-ml-auto o-text-sm o-text-zinc-500 dark:o-text-zinc-400 o-no-underline hover:o-text-brand-500"
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}
53
142
  >
54
143
  odoro.dev
55
144
  </a>
56
145
  </div>
57
- </nav>
146
+ </header>
58
147
  )
59
148
  }
60
149
 
61
- /** L'enveloppe commune a toutes les pages. */
62
- function Layout(): ReactElement {
150
+ /* -------------------------------------------------------------------------- */
151
+ /* Sections de la page d'accueil */
152
+ /* -------------------------------------------------------------------------- */
153
+
154
+ /** Trois faits, pas trois slogans. */
155
+ const FAITS = ['Zero configuration', 'Theme clair et sombre', 'Mouvement reduit respecte']
156
+
157
+ /** Ce que la fenetre du produit montre. */
158
+ const EXTRAIT = `export function Accueil() {
159
+ return (
160
+ <Reveal>
161
+ <h1 className="o-text-6xl o-font-bold">
162
+ Votre premiere page.
163
+ </h1>
164
+ </Reveal>
165
+ )
166
+ }`
167
+
168
+ /**
169
+ * Une fenetre de navigateur miniature.
170
+ *
171
+ * Les trois pastilles sont decoratives : elles disent « navigateur » sans rien
172
+ * ajouter a ce qu'un lecteur d'ecran doit entendre.
173
+ */
174
+ function Fenetre(): ReactElement {
175
+ return (
176
+ <div className="o-overflow-hidden o-rounded-2xl o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-zinc-50 dark:o-bg-zinc-900 o-shadow-2xl">
177
+ <div className="o-flex o-items-center o-gap-2 o-border-b o-border-zinc-200 dark:o-border-zinc-800 o-px-4 o-py-2.5">
178
+ <span className="o-flex o-gap-1.5" aria-hidden="true">
179
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
180
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
181
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
182
+ </span>
183
+ <span className="o-ml-2 o-font-mono o-text-xs o-text-zinc-400">src/App.tsx</span>
184
+ </div>
185
+ <pre className="o-m-0 o-overflow-x-auto o-p-5 o-font-mono o-text-xs o-leading-relaxed o-text-zinc-600 dark:o-text-zinc-300">
186
+ {EXTRAIT}
187
+ </pre>
188
+ </div>
189
+ )
190
+ }
191
+
192
+ /** Le hero : la promesse, les actions, et la fenetre du produit. */
193
+ function Hero(): ReactElement {
194
+ return (
195
+ <section className="o-relative o-px-6 o-pb-20 o-pt-16 md:o-pt-24">
196
+ <div className="o-mx-auto o-w-full o-max-w-5xl">
197
+ <Reveal>
198
+ <span className="o-inline-flex o-items-center o-gap-2 o-whitespace-nowrap o-rounded-full o-border-w-1 o-border-brand-500/30 o-bg-brand-500/10 o-px-3 o-py-1 o-text-xs o-font-medium o-text-brand-600 dark:o-text-brand-400">
199
+ <Signe taille="0.9rem" />
200
+ Genere par odoro create
201
+ </span>
202
+
203
+ <h1 className="o-mt-6 o-max-w-3xl o-text-5xl o-font-bold o-tracking-tight md:o-text-6xl">
204
+ Votre premiere page, <span className="o-text-brand-500">deja vivante</span>.
205
+ </h1>
206
+
207
+ <p className="o-mt-5 o-max-w-xl o-text-lg o-text-zinc-500 dark:o-text-zinc-400">
208
+ Le routeur, les animations et le systeme de style viennent de la meme
209
+ librairie. Modifiez cette page : elle se recharge sans perdre son etat.
210
+ </p>
211
+ </Reveal>
212
+
213
+ <Stagger step={70} className="o-mt-8 o-flex o-flex-wrap o-items-center o-gap-3">
214
+ <a
215
+ href="#piliers"
216
+ className={`o-no-underline ${buttonClasses({ tone: 'primary' })}`}
217
+ >
218
+ Voir ce qui est inclus
219
+ </a>
220
+ <a
221
+ href="https://odoro.dev/docs"
222
+ target="_blank"
223
+ rel="noreferrer"
224
+ className={`o-no-underline ${buttonClasses({ tone: 'secondary' })}`}
225
+ >
226
+ Documentation
227
+ </a>
228
+ </Stagger>
229
+
230
+ <Stagger step={60} className="o-mt-6 o-flex o-flex-wrap o-gap-2">
231
+ {FAITS.map((fait) => (
232
+ <span
233
+ key={fait}
234
+ className="o-rounded-full o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-px-3 o-py-1 o-text-xs o-text-zinc-500 dark:o-text-zinc-400"
235
+ >
236
+ {fait}
237
+ </span>
238
+ ))}
239
+ </Stagger>
240
+
241
+ <Reveal>
242
+ <div className="o-mt-14">
243
+ <Fenetre />
244
+ </div>
245
+ </Reveal>
246
+ </div>
247
+ </section>
248
+ )
249
+ }
250
+
251
+ /** Ce que le projet a deja sous la main. */
252
+ const PILIERS = [
253
+ {
254
+ titre: 'Routeur',
255
+ texte:
256
+ 'Segments dynamiques, routes imbriquees, chargement paresseux et transitions de page.',
257
+ },
258
+ {
259
+ titre: 'Animations',
260
+ texte:
261
+ "Une couche mince sur le moteur du navigateur. Le mouvement reduit est respecte d'office.",
262
+ },
263
+ {
264
+ titre: 'Styles',
265
+ texte: 'Des jetons en source de verite, une feuille statique, aucun scan a l execution.',
266
+ },
267
+ ]
268
+
269
+ /** La section des piliers. */
270
+ function Piliers(): ReactElement {
271
+ return (
272
+ <section
273
+ id="piliers"
274
+ className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-20"
275
+ >
276
+ <div className="o-mx-auto o-w-full o-max-w-5xl">
277
+ <h2 className="o-text-sm o-font-semibold o-uppercase o-tracking-widest o-text-zinc-400">
278
+ Ce qui est deja la
279
+ </h2>
280
+
281
+ <Stagger step={90} className="o-mt-8 o-grid o-gap-4 md:o-grid-cols-3">
282
+ {PILIERS.map((pilier, index) => (
283
+ <article
284
+ key={pilier.titre}
285
+ className="o-rounded-xl o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-white dark:o-bg-zinc-900 o-p-6"
286
+ >
287
+ <span className="o-font-mono o-text-xs o-text-brand-500">
288
+ {String(index + 1).padStart(2, '0')}
289
+ </span>
290
+ <h3 className="o-mt-3 o-text-lg o-font-semibold o-tracking-tight">
291
+ {pilier.titre}
292
+ </h3>
293
+ <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
294
+ {pilier.texte}
295
+ </p>
296
+ </article>
297
+ ))}
298
+ </Stagger>
299
+ </div>
300
+ </section>
301
+ )
302
+ }
303
+
304
+ /** La commande que l'on copie le plus souvent. */
305
+ const COMMANDE = 'odoro add text/count-up'
306
+
307
+ /** La cloture : une commande, et la porte vers le catalogue. */
308
+ function Cloture(): ReactElement {
309
+ const [copie, setCopie] = useState(false)
310
+
311
+ return (
312
+ <section className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-24">
313
+ <div className="o-mx-auto o-w-full o-max-w-3xl o-text-center">
314
+ <Reveal>
315
+ <h2 className="o-text-3xl o-font-bold o-tracking-tight md:o-text-4xl">
316
+ Ajoutez un composant, sans quitter le terminal.
317
+ </h2>
318
+ <p className="o-mx-auto o-mt-4 o-max-w-prose o-text-zinc-500 dark:o-text-zinc-400">
319
+ Le registre copie le code dans votre projet. Il vous appartient : on le lit,
320
+ on le modifie, il ne se met pas a jour dans votre dos.
321
+ </p>
322
+
323
+ <div className="o-mx-auto o-mt-8 o-flex o-max-w-md o-items-center o-gap-2 o-rounded-xl o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-zinc-50 dark:o-bg-zinc-900 o-p-2 o-pl-4">
324
+ <code className="o-flex-1 o-text-left o-font-mono o-text-sm">{COMMANDE}</code>
325
+ <button
326
+ type="button"
327
+ className={buttonClasses({ tone: 'ghost', size: 'sm' })}
328
+ onClick={() => {
329
+ // Le presse-papiers peut etre refuse — hors contexte sur, ou
330
+ // permission retiree. L'echec ne casse pas la page : le libelle
331
+ // ne change simplement pas.
332
+ void navigator.clipboard
333
+ ?.writeText(COMMANDE)
334
+ .then(() => {
335
+ setCopie(true)
336
+ setTimeout(() => {
337
+ setCopie(false)
338
+ }, 1600)
339
+ })
340
+ .catch(() => undefined)
341
+ }}
342
+ >
343
+ {copie ? 'Copie' : 'Copier'}
344
+ </button>
345
+ </div>
346
+
347
+ <a
348
+ href="https://odoro.dev/docs/registry"
349
+ target="_blank"
350
+ rel="noreferrer"
351
+ className="o-mt-6 o-inline-block o-text-sm o-text-zinc-500 dark:o-text-zinc-400 o-no-underline hover:o-text-brand-500"
352
+ >
353
+ Parcourir le catalogue
354
+ </a>
355
+ </Reveal>
356
+ </div>
357
+ </section>
358
+ )
359
+ }
360
+
361
+ /* -------------------------------------------------------------------------- */
362
+ /* Pages */
363
+ /* -------------------------------------------------------------------------- */
364
+
365
+ /** La page d'accueil. */
366
+ function Accueil(): ReactElement {
367
+ return (
368
+ <>
369
+ <Fond />
370
+ <Hero />
371
+ <Piliers />
372
+ <Cloture />
373
+ </>
374
+ )
375
+ }
376
+
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.dev — copie dans votre projet' },
382
+ ]
383
+
384
+ /** Une seconde page, pour montrer le routeur a l'oeuvre. */
385
+ function APropos(): ReactElement {
386
+ return (
387
+ <section className="o-px-6 o-py-24">
388
+ <div className="o-mx-auto o-w-full o-max-w-3xl">
389
+ <Reveal>
390
+ <h1 className="o-text-4xl o-font-bold o-tracking-tight">A propos</h1>
391
+ <p className="o-mt-4 o-text-zinc-500 dark:o-text-zinc-400">
392
+ Cette page existe pour montrer le routeur : la navigation ne recharge rien,
393
+ et la transition est celle du navigateur.
394
+ </p>
395
+
396
+ <dl className="o-mt-10 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
397
+ {REPERES.map((repere) => (
398
+ <div
399
+ key={repere.cle}
400
+ 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
+ >
402
+ <dt className="o-w-32 o-shrink-0 o-text-sm o-font-medium">{repere.cle}</dt>
403
+ <dd className="o-m-0 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
404
+ {repere.valeur}
405
+ </dd>
406
+ </div>
407
+ ))}
408
+ </dl>
409
+ </Reveal>
410
+ </div>
411
+ </section>
412
+ )
413
+ }
414
+
415
+ /** Ce qui s'affiche quand aucune route ne correspond. */
416
+ function Introuvable(): ReactElement {
417
+ return (
418
+ <section className="o-px-6 o-py-32 o-text-center">
419
+ <p className="o-font-mono o-text-sm o-text-brand-500">404</p>
420
+ <h1 className="o-mt-3 o-text-3xl o-font-bold o-tracking-tight">
421
+ Cette page n existe pas.
422
+ </h1>
423
+ <Link to="/" className="o-mt-6 o-inline-block o-text-sm">
424
+ Retour a l accueil
425
+ </Link>
426
+ </section>
427
+ )
428
+ }
429
+
430
+ /* -------------------------------------------------------------------------- */
431
+ /* Racine */
432
+ /* -------------------------------------------------------------------------- */
433
+
434
+ /** L'enveloppe commune : barre, contenu, pied de page. */
435
+ function Coquille(contenu: ReactNode): ReactElement {
63
436
  return (
64
437
  <div className="app-shell">
65
- <Nav />
438
+ <Barre />
66
439
  {/* `relative` porte le fond decoratif, qui se place en absolu dedans. */}
67
- <main className="o-relative o-view-transition-page">
68
- <Outlet />
69
- </main>
440
+ <main className="o-relative o-view-transition-page">{contenu}</main>
70
441
  <footer className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-8">
71
442
  <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">
72
- <Marque />
443
+ <span className="o-inline-flex o-items-center o-gap-2">
444
+ <span className="o-text-brand-500">
445
+ <Signe taille="1.1rem" />
446
+ </span>
447
+ <span className="o-font-semibold o-tracking-wide">ODORO</span>
448
+ </span>
73
449
  <span>Construit avec Odoro.</span>
74
450
  </div>
75
451
  </footer>
@@ -80,14 +456,11 @@ function Layout(): ReactElement {
80
456
  /** Racine de l'application. */
81
457
  export function App(): ReactElement {
82
458
  return (
83
- <Router>
84
- <Routes>
85
- <Route path="/" element={<Layout />}>
86
- <Route index element={<Home />} />
87
- <Route path="a-propos" element={<About />} />
88
- <Route path="*" element={<NotFound />} />
89
- </Route>
90
- </Routes>
91
- </Router>
459
+ <Routeur
460
+ enveloppe={Coquille}
461
+ accueil={<Accueil />}
462
+ apropos={<APropos />}
463
+ introuvable={<Introuvable />}
464
+ />
92
465
  )
93
466
  }
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * Le fond decoratif de la page.
3
3
  *
4
- * ## Ce qu'il est, et ce qu'il remplace
4
+ * ## Pourquoi ce n'est pas dans `App.tsx`
5
5
  *
6
- * Deux nappes de couleur de marque, tres diluees, posees en haut de page. Rien
7
- * ne bouge : c'est un degrade, pas une animation il ne coute aucune image par
8
- * seconde et se dessine avant le premier octet de JavaScript.
6
+ * C'est le seul morceau de la page qui depend de ce qui a ete coche a la
7
+ * creation : avec le moteur, ce fichier est remplace par une version qui ouvre
8
+ * une surface WebGL animee ; sans lui, c'est le degrade ci-dessous.
9
9
  *
10
- * Si le moteur a ete retenu a la creation, ce fichier est remplace par une
11
- * version qui ouvre une surface WebGL animee. Le reste de la page ne sait pas
12
- * d'ou vient son fond : elle place `<Fond />` et n'en demande pas plus.
10
+ * Le garder a part evite d'avoir deux `App.tsx` a tenir un par cas — qui
11
+ * divergeraient au premier changement de texte. `App.tsx` place `<Fond />` et
12
+ * n'en demande pas plus.
13
13
  *
14
14
  * @module
15
15
  */
16
16
 
17
17
  import type { ReactElement } from 'react'
18
18
 
19
- /** Deux nappes de marque, diluees, derriere le contenu. */
19
+ /** Deux nappes de marque, diluees, derriere le haut de la page. */
20
20
  export function Fond(): ReactElement {
21
21
  return (
22
22
  <div
@@ -31,7 +31,7 @@ export function Fond(): ReactElement {
31
31
  }}
32
32
  />
33
33
  <div
34
- className="o-absolute o-right-[-10rem] o-top-[6rem] o-size-[30rem] o-rounded-full o-blur-3xl o-opacity-20 dark:o-opacity-20"
34
+ className="o-absolute o-right-[-10rem] o-top-[6rem] o-size-[30rem] o-rounded-full o-blur-3xl o-opacity-20"
35
35
  style={{
36
36
  background:
37
37
  'radial-gradient(closest-side, var(--o-palette-brand-400), transparent)',
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Le routeur du projet.
3
+ *
4
+ * ## Pourquoi il vit seul, dans son fichier
5
+ *
6
+ * C'est le seul endroit qui nomme la dependance de routage. Changer de routeur,
7
+ * ajouter une route, en proteger une derriere une authentification : tout se
8
+ * lit ici, et `App.tsx` n'a rien a en savoir au-dela de la ligne qui l'importe.
9
+ *
10
+ * ## Pourquoi les pages sont passees, et non importees
11
+ *
12
+ * Ce fichier pourrait importer les pages depuis `App.tsx`. Les deux modules
13
+ * s'importeraient alors l'un l'autre : cela fonctionne, mais l'ordre
14
+ * d'evaluation devient une question a laquelle personne ne veut repondre le
15
+ * jour ou quelque chose s'execute au chargement du module.
16
+ *
17
+ * Les pages arrivent donc en proprietes. La dependance ne va que dans un sens —
18
+ * `App.tsx` connait le routeur, le routeur ne connait que React — et la table
19
+ * des routes reste lisible d'un coup d'oeil.
20
+ *
21
+ * @module
22
+ */
23
+
24
+ import { Outlet, Route, Router, Routes } from '@odoro-cli/libs/router'
25
+ import type { ReactElement, ReactNode } from 'react'
26
+
27
+ export { Link, useLocation } from '@odoro-cli/libs/router'
28
+
29
+ /** Les pages que le routeur place. */
30
+ export interface RouteurProps {
31
+ /** L'enveloppe commune : navigation, contenu, pied de page. */
32
+ readonly enveloppe: (contenu: ReactNode) => ReactElement
33
+ /** La page d'accueil. */
34
+ readonly accueil: ReactElement
35
+ /** La page « A propos ». */
36
+ readonly apropos: ReactElement
37
+ /** Ce qui s'affiche quand aucune route ne correspond. */
38
+ readonly introuvable: ReactElement
39
+ }
40
+
41
+ /**
42
+ * La table des routes.
43
+ *
44
+ * @example
45
+ * <Routeur
46
+ * enveloppe={(contenu) => <Coquille>{contenu}</Coquille>}
47
+ * accueil={<Accueil />}
48
+ * apropos={<APropos />}
49
+ * introuvable={<Introuvable />}
50
+ * />
51
+ */
52
+ export function Routeur({
53
+ enveloppe,
54
+ accueil,
55
+ apropos,
56
+ introuvable,
57
+ }: RouteurProps): ReactElement {
58
+ return (
59
+ <Router>
60
+ <Routes>
61
+ {/* `Outlet` marque l'endroit ou la page courante se rend : c'est ce
62
+ qui permet a la navigation et au pied de page de ne pas etre
63
+ remontes a chaque changement de route. */}
64
+ <Route path="/" element={enveloppe(<Outlet />)}>
65
+ <Route index element={accueil} />
66
+ <Route path="a-propos" element={apropos} />
67
+ <Route path="*" element={introuvable} />
68
+ </Route>
69
+ </Routes>
70
+ </Router>
71
+ )
72
+ }