odoro 1.0.1 → 1.0.3

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 (54) hide show
  1. package/README.md +73 -0
  2. package/dist/build-KI7IEVRJ.js +4 -0
  3. package/dist/{chunk-R6PMAMXA.js → chunk-2EE5QUCW.js} +16 -3
  4. package/dist/{chunk-CJIUP7A3.js → chunk-FJYUQALD.js} +1 -1
  5. package/dist/cli.js +4 -4
  6. package/dist/{create-LTT3SAWT.js → create-VUSKHXBN.js} +109 -65
  7. package/dist/index.js +2 -2
  8. package/dist/{package-LK46RV2I.js → package-2JOBLFXE.js} +3 -2
  9. package/dist/registry/index.d.ts +2 -2
  10. package/dist/{server-ATGFRVSW.js → server-DXOQAPVX.js} +1 -1
  11. package/package.json +3 -2
  12. package/templates/react-ts/_variantes/avec-moteur/src/{sections/Fond.tsx → fond.tsx} +13 -13
  13. package/templates/react-ts/_variantes/sans-libs/src/App.tsx +363 -23
  14. package/templates/{react-ts-server/_variantes/sans-libs/client/src/sections/Fond.tsx → react-ts/_variantes/sans-libs/src/fond.tsx} +1 -1
  15. package/templates/react-ts/_variantes/sans-libs/src/styles.css +115 -38
  16. package/templates/react-ts/_variantes/sans-routeur/src/App.tsx +427 -22
  17. package/templates/react-ts/index.html +11 -0
  18. package/templates/react-ts/public/favicon.svg +2 -3
  19. package/templates/react-ts/src/App.tsx +585 -57
  20. package/templates/{react-ts-server/client/src/sections/Fond.tsx → react-ts/src/fond.tsx} +9 -9
  21. package/templates/react-ts/src/router.tsx +72 -0
  22. package/templates/react-ts-server/_variantes/avec-moteur/client/src/{sections/Fond.tsx → fond.tsx} +13 -13
  23. package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +363 -23
  24. package/templates/{react-ts/_variantes/sans-libs/src/sections/Fond.tsx → react-ts-server/_variantes/sans-libs/client/src/fond.tsx} +1 -1
  25. package/templates/react-ts-server/_variantes/sans-libs/client/src/styles.css +115 -38
  26. package/templates/react-ts-server/_variantes/sans-routeur/client/src/App.tsx +427 -22
  27. package/templates/react-ts-server/client/index.html +11 -0
  28. package/templates/react-ts-server/client/public/favicon.svg +2 -3
  29. package/templates/react-ts-server/client/src/App.tsx +585 -57
  30. package/templates/{react-ts/src/sections/Fond.tsx → react-ts-server/client/src/fond.tsx} +9 -9
  31. package/templates/react-ts-server/client/src/router.tsx +72 -0
  32. package/dist/build-GIPMPYJ3.js +0 -4
  33. package/templates/react-ts/_variantes/sans-libs/src/composants/Marque.tsx +0 -37
  34. package/templates/react-ts/_variantes/sans-libs/src/sections/Cloture.tsx +0 -60
  35. package/templates/react-ts/_variantes/sans-libs/src/sections/Hero.tsx +0 -92
  36. package/templates/react-ts/_variantes/sans-libs/src/sections/Piliers.tsx +0 -44
  37. package/templates/react-ts/src/composants/Marque.tsx +0 -50
  38. package/templates/react-ts/src/routes/About.tsx +0 -43
  39. package/templates/react-ts/src/routes/Home.tsx +0 -28
  40. package/templates/react-ts/src/routes/NotFound.tsx +0 -14
  41. package/templates/react-ts/src/sections/Cloture.tsx +0 -69
  42. package/templates/react-ts/src/sections/Hero.tsx +0 -119
  43. package/templates/react-ts/src/sections/Piliers.tsx +0 -68
  44. package/templates/react-ts-server/_variantes/sans-libs/client/src/composants/Marque.tsx +0 -37
  45. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Cloture.tsx +0 -60
  46. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Hero.tsx +0 -92
  47. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Piliers.tsx +0 -44
  48. package/templates/react-ts-server/client/src/composants/Marque.tsx +0 -50
  49. package/templates/react-ts-server/client/src/routes/About.tsx +0 -43
  50. package/templates/react-ts-server/client/src/routes/Home.tsx +0 -28
  51. package/templates/react-ts-server/client/src/routes/NotFound.tsx +0 -14
  52. package/templates/react-ts-server/client/src/sections/Cloture.tsx +0 -69
  53. package/templates/react-ts-server/client/src/sections/Hero.tsx +0 -119
  54. package/templates/react-ts-server/client/src/sections/Piliers.tsx +0 -68
@@ -1,10 +1,92 @@
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 {
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'
37
+
38
+ import { Fond } from '@/fond'
39
+ import { Link, Routeur, useLocation } from '@/router'
40
+
41
+ /* -------------------------------------------------------------------------- */
42
+ /* Marque */
43
+ /* -------------------------------------------------------------------------- */
44
+
45
+ /**
46
+ * Le signe de la marque.
47
+ *
48
+ * Dessine dans le flux plutot que charge depuis `public/` : il prend la couleur
49
+ * de son parent par `currentColor`, donc la teinte de marque en clair comme en
50
+ * sombre, sans qu'il y ait deux fichiers a tenir.
51
+ *
52
+ * La taille est en style et non en classe : une classe utilitaire a valeur
53
+ * arbitraire n'est emise que si le compilateur l'a vue passer, et une classe
54
+ * absente ne peint rien — le signe serait sans dimensions, donc invisible.
55
+ */
56
+ function Signe({ taille = '1.75rem' }: { readonly taille?: string }): ReactElement {
57
+ return (
58
+ <svg
59
+ viewBox="0 0 100 100"
60
+ fill="none"
61
+ aria-hidden="true"
62
+ style={{ width: taille, height: taille, flexShrink: 0 }}
63
+ >
64
+ <path
65
+ d="M20.25 20.25H50a29.75 29.75 0 1 1-29.75 29.75Z"
66
+ stroke="currentColor"
67
+ strokeWidth="10.5"
68
+ strokeLinejoin="miter"
69
+ />
70
+ </svg>
71
+ )
72
+ }
3
73
 
4
- import { Marque } from '@/composants/Marque'
5
- import { About } from '@/routes/About'
6
- import { Home } from '@/routes/Home'
7
- import { NotFound } from '@/routes/NotFound'
74
+ /* -------------------------------------------------------------------------- */
75
+ /* Barre de navigation */
76
+ /* -------------------------------------------------------------------------- */
77
+
78
+ /**
79
+ * La surface d'une gelule.
80
+ *
81
+ * Le systeme decline les echelles de noir et de blanc, mais pas leurs variantes
82
+ * sombres en alpha : `o-bg-white/70` et son pendant `dark:` n'existent pas en
83
+ * une seule classe. Le melange se fait donc en style, ou il est exact et suit
84
+ * le theme sans qu'on ait a redire chaque couleur.
85
+ */
86
+ const GELULE: CSSProperties = {
87
+ backgroundColor: 'color-mix(in oklab, var(--o-theme-bg) 72%, transparent)',
88
+ borderColor: 'color-mix(in oklab, var(--o-theme-fg) 12%, transparent)',
89
+ }
8
90
 
9
91
  /** Les entrees de la navigation. */
10
92
  const LIENS = [
@@ -12,64 +94,513 @@ const LIENS = [
12
94
  { to: '/a-propos', label: 'A propos' },
13
95
  ]
14
96
 
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 {
97
+ /** Un lien de la gelule de navigation, actif sur la route courante. */
98
+ function LienHaut({ to, label }: { readonly to: string; readonly label: string }): ReactElement {
23
99
  const { pathname } = useLocation()
100
+ const actif = pathname === to
101
+
102
+ return (
103
+ <Link
104
+ to={to}
105
+ className={[
106
+ 'o-rounded-full o-px-4 o-py-2 o-text-sm o-no-underline o-transition-colors',
107
+ actif
108
+ ? 'o-font-medium o-text-zinc-950 dark:o-text-white'
109
+ : 'o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white',
110
+ ].join(' ')}
111
+ style={actif ? GELULE : undefined}
112
+ >
113
+ {label}
114
+ </Link>
115
+ )
116
+ }
24
117
 
118
+ /** La barre : la marque, la navigation, et le lien vers le site. */
119
+ function Barre(): ReactElement {
25
120
  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 />
121
+ <header className="o-sticky o-top-0 o-z-20 o-px-5 md:o-px-8">
122
+ <div className="o-mx-auto o-flex o-h-20 o-w-full o-max-w-5xl o-items-center o-gap-3">
123
+ <Link
124
+ to="/"
125
+ 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"
126
+ style={GELULE}
127
+ >
128
+ <span className="o-text-brand-500">
129
+ <Signe />
130
+ </span>
131
+ <span className="o-text-lg o-font-bold o-tracking-wide o-text-zinc-950 dark:o-text-white">
132
+ ODORO
133
+ </span>
30
134
  </Link>
31
135
 
32
- <div className="o-flex o-items-center o-gap-5 o-text-sm">
136
+ <nav
137
+ aria-label="Navigation"
138
+ 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"
139
+ style={GELULE}
140
+ >
33
141
  {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>
142
+ <LienHaut key={lien.to} to={lien.to} label={lien.label} />
45
143
  ))}
144
+ </nav>
145
+
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 />
46
157
  </div>
158
+ </div>
159
+ </header>
160
+ )
161
+ }
47
162
 
48
- <a
49
- href="https://odoro.dev"
50
- target="_blank"
51
- 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"
53
- >
54
- odoro.dev
55
- </a>
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
+
277
+ /* -------------------------------------------------------------------------- */
278
+ /* Sections de la page d'accueil */
279
+ /* -------------------------------------------------------------------------- */
280
+
281
+ /** Trois faits, pas trois slogans. */
282
+ const FAITS = ['Zero configuration', 'Theme clair et sombre', 'Mouvement reduit respecte']
283
+
284
+ /** Ce que la fenetre du produit montre. */
285
+ const EXTRAIT = `export function Accueil() {
286
+ return (
287
+ <Reveal>
288
+ <h1 className="o-text-6xl o-font-bold">
289
+ Votre premiere page.
290
+ </h1>
291
+ </Reveal>
292
+ )
293
+ }`
294
+
295
+ /**
296
+ * Une fenetre de navigateur miniature.
297
+ *
298
+ * Les trois pastilles sont decoratives : elles disent « navigateur » sans rien
299
+ * ajouter a ce qu'un lecteur d'ecran doit entendre.
300
+ */
301
+ function Fenetre(): ReactElement {
302
+ return (
303
+ <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">
304
+ <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">
305
+ <span className="o-flex o-gap-1.5" aria-hidden="true">
306
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
307
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
308
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
309
+ </span>
310
+ <span className="o-ml-2 o-font-mono o-text-xs o-text-zinc-400">src/App.tsx</span>
311
+ </div>
312
+ <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">
313
+ {EXTRAIT}
314
+ </pre>
315
+ </div>
316
+ )
317
+ }
318
+
319
+ /** Le hero : la promesse, les actions, et la fenetre du produit. */
320
+ function Hero(): ReactElement {
321
+ return (
322
+ <section className="o-relative o-px-6 o-pb-20 o-pt-16 md:o-pt-24">
323
+ <div className="o-mx-auto o-w-full o-max-w-5xl">
324
+ <Reveal>
325
+ <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">
326
+ <Signe taille="0.9rem" />
327
+ Genere par odoro create
328
+ </span>
329
+
330
+ <h1 className="o-mt-6 o-max-w-3xl o-text-5xl o-font-bold o-tracking-tight md:o-text-6xl">
331
+ Votre premiere page, <span className="o-text-brand-500">deja vivante</span>.
332
+ </h1>
333
+
334
+ <p className="o-mt-5 o-max-w-xl o-text-lg o-text-zinc-500 dark:o-text-zinc-400">
335
+ Le routeur, les animations et le systeme de style viennent de la meme
336
+ librairie. Modifiez cette page : elle se recharge sans perdre son etat.
337
+ </p>
338
+ </Reveal>
339
+
340
+ <Stagger step={70} className="o-mt-8 o-flex o-flex-wrap o-items-center o-gap-3">
341
+ <a
342
+ href="#piliers"
343
+ className={`o-no-underline ${buttonClasses({ tone: 'primary' })}`}
344
+ >
345
+ Voir ce qui est inclus
346
+ </a>
347
+ <a
348
+ href="https://odoro.dev/docs"
349
+ target="_blank"
350
+ rel="noreferrer"
351
+ className={`o-no-underline ${buttonClasses({ tone: 'secondary' })}`}
352
+ >
353
+ Documentation
354
+ </a>
355
+ </Stagger>
356
+
357
+ <Stagger step={60} className="o-mt-6 o-flex o-flex-wrap o-gap-2">
358
+ {FAITS.map((fait) => (
359
+ <span
360
+ key={fait}
361
+ 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"
362
+ >
363
+ {fait}
364
+ </span>
365
+ ))}
366
+ </Stagger>
367
+
368
+ <Reveal>
369
+ <div className="o-mt-14">
370
+ <Fenetre />
371
+ </div>
372
+ </Reveal>
56
373
  </div>
57
- </nav>
374
+ </section>
58
375
  )
59
376
  }
60
377
 
61
- /** L'enveloppe commune a toutes les pages. */
62
- function Layout(): ReactElement {
378
+ /** Ce que le projet a deja sous la main. */
379
+ const PILIERS = [
380
+ {
381
+ titre: 'Routeur',
382
+ texte:
383
+ 'Segments dynamiques, routes imbriquees, chargement paresseux et transitions de page.',
384
+ },
385
+ {
386
+ titre: 'Animations',
387
+ texte:
388
+ "Une couche mince sur le moteur du navigateur. Le mouvement reduit est respecte d'office.",
389
+ },
390
+ {
391
+ titre: 'Styles',
392
+ texte: 'Des jetons en source de verite, une feuille statique, aucun scan a l execution.',
393
+ },
394
+ ]
395
+
396
+ /** La section des piliers. */
397
+ function Piliers(): ReactElement {
398
+ return (
399
+ <section
400
+ id="piliers"
401
+ className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-20"
402
+ >
403
+ <div className="o-mx-auto o-w-full o-max-w-5xl">
404
+ <h2 className="o-text-sm o-font-semibold o-uppercase o-tracking-widest o-text-zinc-400">
405
+ Ce qui est deja la
406
+ </h2>
407
+
408
+ <Stagger step={90} className="o-mt-8 o-grid o-gap-4 md:o-grid-cols-3">
409
+ {PILIERS.map((pilier, index) => (
410
+ <article
411
+ key={pilier.titre}
412
+ 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"
413
+ >
414
+ <span className="o-font-mono o-text-xs o-text-brand-500">
415
+ {String(index + 1).padStart(2, '0')}
416
+ </span>
417
+ <h3 className="o-mt-3 o-text-lg o-font-semibold o-tracking-tight">
418
+ {pilier.titre}
419
+ </h3>
420
+ <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
421
+ {pilier.texte}
422
+ </p>
423
+ </article>
424
+ ))}
425
+ </Stagger>
426
+ </div>
427
+ </section>
428
+ )
429
+ }
430
+
431
+ /** La commande que l'on copie le plus souvent. */
432
+ const COMMANDE = 'odoro add text/count-up'
433
+
434
+ /** La cloture : une commande, et la porte vers le catalogue. */
435
+ function Cloture(): ReactElement {
436
+ const [copie, setCopie] = useState(false)
437
+
438
+ return (
439
+ <section className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-24">
440
+ <div className="o-mx-auto o-w-full o-max-w-3xl o-text-center">
441
+ <Reveal>
442
+ <h2 className="o-text-3xl o-font-bold o-tracking-tight md:o-text-4xl">
443
+ Ajoutez un composant, sans quitter le terminal.
444
+ </h2>
445
+ <p className="o-mx-auto o-mt-4 o-max-w-prose o-text-zinc-500 dark:o-text-zinc-400">
446
+ Le registre copie le code dans votre projet. Il vous appartient : on le lit,
447
+ on le modifie, il ne se met pas a jour dans votre dos.
448
+ </p>
449
+
450
+ <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">
451
+ <code className="o-flex-1 o-text-left o-font-mono o-text-sm">{COMMANDE}</code>
452
+ <button
453
+ type="button"
454
+ className={buttonClasses({ tone: 'ghost', size: 'sm' })}
455
+ onClick={() => {
456
+ // Le presse-papiers peut etre refuse — hors contexte sur, ou
457
+ // permission retiree. L'echec ne casse pas la page : le libelle
458
+ // ne change simplement pas.
459
+ void navigator.clipboard
460
+ ?.writeText(COMMANDE)
461
+ .then(() => {
462
+ setCopie(true)
463
+ setTimeout(() => {
464
+ setCopie(false)
465
+ }, 1600)
466
+ })
467
+ .catch(() => undefined)
468
+ }}
469
+ >
470
+ {copie ? 'Copie' : 'Copier'}
471
+ </button>
472
+ </div>
473
+
474
+ <a
475
+ href="https://odoro.dev/docs/registry"
476
+ target="_blank"
477
+ rel="noreferrer"
478
+ 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"
479
+ >
480
+ Parcourir le catalogue
481
+ </a>
482
+ </Reveal>
483
+ </div>
484
+ </section>
485
+ )
486
+ }
487
+
488
+ /* -------------------------------------------------------------------------- */
489
+ /* Pages */
490
+ /* -------------------------------------------------------------------------- */
491
+
492
+ /** La page d'accueil. */
493
+ function Accueil(): ReactElement {
494
+ return (
495
+ <>
496
+ <Hero />
497
+ <Piliers />
498
+ <Cloture />
499
+ </>
500
+ )
501
+ }
502
+
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] ?? '' }))
529
+
530
+ /** Une seconde page, pour montrer le routeur a l'oeuvre. */
531
+ function APropos(): ReactElement {
532
+ return (
533
+ <section className="o-px-6 o-py-24">
534
+ <div className="o-mx-auto o-w-full o-max-w-3xl">
535
+ <Reveal>
536
+ <h1 className="o-text-4xl o-font-bold o-tracking-tight">A propos</h1>
537
+ <p className="o-mt-4 o-text-zinc-500 dark:o-text-zinc-400">
538
+ Cette page existe pour montrer le routeur : la navigation ne recharge rien,
539
+ et la transition est celle du navigateur.
540
+ </p>
541
+
542
+ <dl className="o-mt-10 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
543
+ {REPERES.map((repere) => (
544
+ <div
545
+ key={repere.nom}
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"
547
+ >
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>
554
+ <dd className="o-m-0 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
555
+ {repere.role}
556
+ </dd>
557
+ </div>
558
+ ))}
559
+ </dl>
560
+ </Reveal>
561
+ </div>
562
+ </section>
563
+ )
564
+ }
565
+
566
+ /** Ce qui s'affiche quand aucune route ne correspond. */
567
+ function Introuvable(): ReactElement {
568
+ return (
569
+ <section className="o-px-6 o-py-32 o-text-center">
570
+ <p className="o-font-mono o-text-sm o-text-brand-500">404</p>
571
+ <h1 className="o-mt-3 o-text-3xl o-font-bold o-tracking-tight">
572
+ Cette page n existe pas.
573
+ </h1>
574
+ <Link to="/" className="o-mt-6 o-inline-block o-text-sm">
575
+ Retour a l accueil
576
+ </Link>
577
+ </section>
578
+ )
579
+ }
580
+
581
+ /* -------------------------------------------------------------------------- */
582
+ /* Racine */
583
+ /* -------------------------------------------------------------------------- */
584
+
585
+ /** L'enveloppe commune : barre, contenu, pied de page. */
586
+ function Coquille(contenu: ReactNode): ReactElement {
63
587
  return (
64
- <div className="app-shell">
65
- <Nav />
66
- {/* `relative` porte le fond decoratif, qui se place en absolu dedans. */}
67
- <main className="o-relative o-view-transition-page">
68
- <Outlet />
69
- </main>
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 />
594
+ <Barre />
595
+ <main className="o-relative o-view-transition-page">{contenu}</main>
70
596
  <footer className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-8">
71
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">
72
- <Marque />
598
+ <span className="o-inline-flex o-items-center o-gap-2">
599
+ <span className="o-text-brand-500">
600
+ <Signe taille="1.1rem" />
601
+ </span>
602
+ <span className="o-font-semibold o-tracking-wide">ODORO</span>
603
+ </span>
73
604
  <span>Construit avec Odoro.</span>
74
605
  </div>
75
606
  </footer>
@@ -80,14 +611,11 @@ function Layout(): ReactElement {
80
611
  /** Racine de l'application. */
81
612
  export function App(): ReactElement {
82
613
  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>
614
+ <Routeur
615
+ enveloppe={Coquille}
616
+ accueil={<Accueil />}
617
+ apropos={<APropos />}
618
+ introuvable={<Introuvable />}
619
+ />
92
620
  )
93
621
  }