odoro 0.3.4 → 1.0.1

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 (41) hide show
  1. package/dist/cli.js +3 -3
  2. package/dist/{commands-PT3NS65O.js → commands-AHMXBWQQ.js} +25 -5
  3. package/dist/{create-IOFAPDBO.js → create-LTT3SAWT.js} +11 -7
  4. package/dist/{package-Q2FV4K3H.js → package-LK46RV2I.js} +1 -1
  5. package/package.json +1 -1
  6. package/templates/react-ts/_variantes/avec-moteur/src/sections/Fond.tsx +118 -0
  7. package/templates/react-ts/_variantes/sans-libs/src/App.tsx +22 -51
  8. package/templates/react-ts/_variantes/sans-libs/src/composants/Marque.tsx +37 -0
  9. package/templates/react-ts/_variantes/sans-libs/src/sections/Cloture.tsx +60 -0
  10. package/templates/react-ts/_variantes/sans-libs/src/sections/Fond.tsx +23 -0
  11. package/templates/react-ts/_variantes/sans-libs/src/sections/Hero.tsx +92 -0
  12. package/templates/react-ts/_variantes/sans-libs/src/sections/Piliers.tsx +44 -0
  13. package/templates/react-ts/_variantes/sans-libs/src/styles.css +322 -40
  14. package/templates/react-ts/_variantes/sans-routeur/src/App.tsx +35 -77
  15. package/templates/react-ts/src/App.tsx +56 -29
  16. package/templates/react-ts/src/composants/Marque.tsx +50 -0
  17. package/templates/react-ts/src/routes/About.tsx +38 -14
  18. package/templates/react-ts/src/routes/Home.tsx +21 -73
  19. package/templates/react-ts/src/sections/Cloture.tsx +69 -0
  20. package/templates/react-ts/src/sections/Fond.tsx +42 -0
  21. package/templates/react-ts/src/sections/Hero.tsx +119 -0
  22. package/templates/react-ts/src/sections/Piliers.tsx +68 -0
  23. package/templates/react-ts/src/styles.css +4 -5
  24. package/templates/react-ts-server/_variantes/avec-moteur/client/src/sections/Fond.tsx +118 -0
  25. package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +22 -51
  26. package/templates/react-ts-server/_variantes/sans-libs/client/src/composants/Marque.tsx +37 -0
  27. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Cloture.tsx +60 -0
  28. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Fond.tsx +23 -0
  29. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Hero.tsx +92 -0
  30. package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Piliers.tsx +44 -0
  31. package/templates/react-ts-server/_variantes/sans-libs/client/src/styles.css +322 -40
  32. package/templates/react-ts-server/_variantes/sans-routeur/client/src/App.tsx +35 -77
  33. package/templates/react-ts-server/client/src/App.tsx +56 -29
  34. package/templates/react-ts-server/client/src/composants/Marque.tsx +50 -0
  35. package/templates/react-ts-server/client/src/routes/About.tsx +38 -14
  36. package/templates/react-ts-server/client/src/routes/Home.tsx +18 -138
  37. package/templates/react-ts-server/client/src/sections/Cloture.tsx +69 -0
  38. package/templates/react-ts-server/client/src/sections/Fond.tsx +42 -0
  39. package/templates/react-ts-server/client/src/sections/Hero.tsx +119 -0
  40. package/templates/react-ts-server/client/src/sections/Piliers.tsx +68 -0
  41. package/templates/react-ts-server/client/src/styles.css +4 -5
@@ -0,0 +1,50 @@
1
+ /**
2
+ * La marque : le signe et le mot.
3
+ *
4
+ * ## Pourquoi le signe est dessine ici, et non charge
5
+ *
6
+ * Un fichier dans `public/` serait une image de plus a telecharger, et surtout
7
+ * une image d'une seule couleur. Le signe est dessine en SVG dans le flux, avec
8
+ * `currentColor` : il prend la couleur de son parent, donc la teinte de marque
9
+ * en clair comme en sombre, sans qu'on ait deux fichiers a tenir.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import type { ReactElement } from 'react'
15
+
16
+ /** Le signe seul, a la taille de la police qui l'entoure. */
17
+ export function Signe({ className = '' }: { readonly className?: string }): ReactElement {
18
+ return (
19
+ <svg
20
+ viewBox="0 0 100 100"
21
+ fill="none"
22
+ aria-hidden="true"
23
+ className={className}
24
+ // La taille est en style, et non en classe : elle suit la police qui
25
+ // entoure le signe, et une classe utilitaire a valeur arbitraire n'est
26
+ // emise que si le compilateur l'a vue passer. Absente, elle laisserait
27
+ // un SVG sans dimensions — donc invisible, sans rien pour le signaler.
28
+ style={{ width: '1.15em', height: '1.15em', flexShrink: 0 }}
29
+ >
30
+ <path
31
+ d="M20.25 20.25H50a29.75 29.75 0 1 1-29.75 29.75Z"
32
+ stroke="currentColor"
33
+ strokeWidth="10.5"
34
+ strokeLinejoin="miter"
35
+ />
36
+ </svg>
37
+ )
38
+ }
39
+
40
+ /** Le signe et le mot, tels qu'ils vont dans une barre de navigation. */
41
+ export function Marque(): ReactElement {
42
+ return (
43
+ <span className="o-inline-flex o-items-center o-gap-2 o-font-semibold o-tracking-tight">
44
+ <span className="o-text-brand-500">
45
+ <Signe />
46
+ </span>
47
+ Odoro
48
+ </span>
49
+ )
50
+ }
@@ -1,19 +1,43 @@
1
+ /**
2
+ * Une seconde page, pour montrer le routeur a l'oeuvre.
3
+ *
4
+ * @module
5
+ */
6
+
1
7
  import { Reveal } from '@odoro-cli/libs/motion'
2
- import { Link } from '@odoro-cli/libs/router'
8
+ import type { ReactElement } from 'react'
9
+
10
+ /** Ce que le projet embarque, tel qu'il a ete cree. */
11
+ const REPERES: readonly { readonly cle: string; readonly valeur: string }[] = [
12
+ { cle: 'Moteur', valeur: 'odoro — serveur de developpement et compilation' },
13
+ { cle: 'Styles', valeur: '@odoro-cli/libs — jetons, utilitaires, composants' },
14
+ { cle: 'Registre', valeur: 'register.odoro.dev — copie dans votre projet' },
15
+ ]
3
16
 
4
- /** Page « a propos », qui demontre la navigation entre routes. */
5
- export function About() {
17
+ /** Page « A propos ». */
18
+ export function About(): ReactElement {
6
19
  return (
7
- <Reveal className="o-flex o-flex-col o-gap-6">
8
- <h1 className="o-text-3xl o-font-bold o-tracking-tight">A propos</h1>
9
- <p className="o-text-zinc-500 dark:o-text-zinc-400 o-max-w-prose">
10
- Cette page est servie par le routeur d Odoro. La navigation ne recharge pas le
11
- document : elle passe par l historique du navigateur, restaure la position de
12
- defilement et joue une transition de page quand le navigateur la prend en charge.
13
- </p>
14
- <p>
15
- <Link to="/">Retour a l accueil</Link>
16
- </p>
17
- </Reveal>
20
+ <section className="o-px-6 o-py-24">
21
+ <div className="o-mx-auto o-w-full o-max-w-3xl">
22
+ <Reveal>
23
+ <h1 className="o-text-4xl o-font-bold o-tracking-tight">A propos</h1>
24
+ <p className="o-mt-4 o-text-zinc-500 dark:o-text-zinc-400">
25
+ Cette page existe pour montrer le routeur : la navigation ne recharge
26
+ rien, et la transition est celle du navigateur.
27
+ </p>
28
+
29
+ <dl className="o-mt-10 o-divide-y o-divide-zinc-200 dark:o-divide-zinc-800 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
30
+ {REPERES.map((repere) => (
31
+ <div key={repere.cle} className="o-flex o-flex-col md:o-flex-row o-gap-1 md:o-gap-6 o-py-4">
32
+ <dt className="o-w-32 o-shrink-0 o-text-sm o-font-medium">{repere.cle}</dt>
33
+ <dd className="o-m-0 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
34
+ {repere.valeur}
35
+ </dd>
36
+ </div>
37
+ ))}
38
+ </dl>
39
+ </Reveal>
40
+ </div>
41
+ </section>
18
42
  )
19
43
  }
@@ -1,148 +1,28 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- import { Reveal, Stagger } from '@odoro-cli/libs/motion'
4
- import { Link } from '@odoro-cli/libs/router'
5
- import { Button } from '@odoro-cli/libs/ui'
6
-
7
- /** Reponse de la route de sante du serveur. */
8
- interface Health {
9
- status: string
10
- environment: string
11
- uptime: number
12
- timestamp: string
13
- }
14
-
15
- /** Etat de l'appel a l'API. */
16
- type Fetching =
17
- | { state: 'inactif' }
18
- | { state: 'en-cours' }
19
- | { state: 'ok'; health: Health }
20
- | { state: 'echec'; message: string }
21
-
22
1
  /**
23
- * Interroge la route de sante du serveur.
2
+ * La page d'accueil : le fond, puis les sections.
24
3
  *
25
- * L'URL est relative : en developpement le moteur transmet l'appel au serveur,
26
- * en production c'est le meme serveur qui repond. Il n'y a donc jamais d'URL
27
- * d'API a configurer cote client.
4
+ * Elle ne fait que les mettre bout a bout. Chaque section vit dans son propre
5
+ * fichier, si bien qu'en retirer une, en ajouter une ou en changer l'ordre se
6
+ * lit ici en une ligne.
7
+ *
8
+ * @module
28
9
  */
29
- async function fetchHealth(signal: AbortSignal): Promise<Health> {
30
- const response = await fetch('/api/health', { signal })
31
- if (!response.ok) throw new Error(`Le serveur a repondu ${response.status}.`)
32
- return (await response.json()) as Health
33
- }
34
-
35
- /** Panneau montrant l'etat de la liaison avec l'API. */
36
- function ApiStatus() {
37
- const [result, setResult] = useState<Fetching>({ state: 'inactif' })
38
- const [attempt, setAttempt] = useState(0)
39
-
40
- useEffect(() => {
41
- const controller = new AbortController()
42
- setResult({ state: 'en-cours' })
43
10
 
44
- fetchHealth(controller.signal).then(
45
- (health) => setResult({ state: 'ok', health }),
46
- (cause: unknown) => {
47
- if (controller.signal.aborted) return
48
- setResult({
49
- state: 'echec',
50
- message: cause instanceof Error ? cause.message : String(cause),
51
- })
52
- },
53
- )
11
+ import type { ReactElement } from 'react'
54
12
 
55
- return () => controller.abort()
56
- }, [attempt])
57
-
58
- return (
59
- <section className="o-rounded-lg o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-white dark:o-bg-zinc-900 o-p-5">
60
- <div className="o-flex o-items-center o-justify-between o-gap-4">
61
- <h2 className="o-font-semibold">Liaison avec l&rsquo;API</h2>
62
- <Button
63
- size="sm"
64
- tone="secondary"
65
- loading={result.state === 'en-cours'}
66
- onClick={() => setAttempt((value) => value + 1)}
67
- >
68
- Reinterroger
69
- </Button>
70
- </div>
71
-
72
- <div className="o-mt-3 o-text-sm" role="status">
73
- {result.state === 'inactif' || result.state === 'en-cours' ? (
74
- <p className="o-text-zinc-500 dark:o-text-zinc-400">
75
- Interrogation de /api/health...
76
- </p>
77
- ) : result.state === 'echec' ? (
78
- <p className="o-text-red-600 dark:o-text-red-400">
79
- Serveur injoignable : {result.message}. Verifiez qu&rsquo;il tourne bien sur
80
- le port 3001.
81
- </p>
82
- ) : (
83
- <dl className="o-grid o-grid-cols-2 o-gap-2 o-tabular-nums">
84
- <dt className="o-text-zinc-500 dark:o-text-zinc-400">Etat</dt>
85
- <dd className="o-text-emerald-600 dark:o-text-emerald-400">
86
- {result.health.status}
87
- </dd>
88
- <dt className="o-text-zinc-500 dark:o-text-zinc-400">Environnement</dt>
89
- <dd>{result.health.environment}</dd>
90
- <dt className="o-text-zinc-500 dark:o-text-zinc-400">En ligne depuis</dt>
91
- <dd>{result.health.uptime} s</dd>
92
- </dl>
93
- )}
94
- </div>
95
- </section>
96
- )
97
- }
98
-
99
- /** Trois arguments mis en avant sur la page d'accueil. */
100
- const FEATURES = [
101
- {
102
- title: 'Une seule origine',
103
- body: 'Les appels /api sont transmis au serveur : aucune question de CORS, aucune URL a configurer.',
104
- },
105
- {
106
- title: 'Un seul deploiement',
107
- body: 'En production, le serveur sert l API et le client compile. Une image, un processus.',
108
- },
109
- {
110
- title: 'Un environnement verifie',
111
- body: 'Les variables sont validees au demarrage. Une configuration incomplete arrete le serveur tout de suite.',
112
- },
113
- ]
13
+ import { Cloture } from '@/sections/Cloture'
14
+ import { Fond } from '@/sections/Fond'
15
+ import { Hero } from '@/sections/Hero'
16
+ import { Piliers } from '@/sections/Piliers'
114
17
 
115
18
  /** Page d'accueil. */
116
- export function Home() {
19
+ export function Home(): ReactElement {
117
20
  return (
118
- <div className="o-flex o-flex-col o-gap-12">
119
- <Reveal>
120
- <h1 className="o-text-4xl o-font-bold o-tracking-tight">Client et serveur.</h1>
121
- <p className="o-mt-4 o-text-lg o-text-zinc-500 dark:o-text-zinc-400 o-max-w-prose">
122
- Ce projet reunit une application React et une API Express dans un seul depot. Le
123
- panneau ci-dessous interroge le serveur pour de vrai.
124
- </p>
125
- </Reveal>
126
-
127
- <ApiStatus />
128
-
129
- <Stagger step={80} className="o-grid o-grid-cols-1 md:o-grid-cols-3 o-gap-4">
130
- {FEATURES.map((feature) => (
131
- <article
132
- key={feature.title}
133
- className="o-rounded-lg o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-white dark:o-bg-zinc-900 o-p-5"
134
- >
135
- <h2 className="o-font-semibold">{feature.title}</h2>
136
- <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
137
- {feature.body}
138
- </p>
139
- </article>
140
- ))}
141
- </Stagger>
142
-
143
- <p>
144
- <Link to="/a-propos">En savoir plus</Link>
145
- </p>
146
- </div>
21
+ <>
22
+ <Fond />
23
+ <Hero />
24
+ <Piliers />
25
+ <Cloture />
26
+ </>
147
27
  )
148
28
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * La cloture : une commande, et la porte de sortie vers la documentation.
3
+ *
4
+ * Une page d'accueil qui s'arrete sur sa derniere carte laisse le visiteur
5
+ * sans geste suivant. Celle-ci se termine par ce qu'il y a a taper.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { Reveal } from '@odoro-cli/libs/motion'
11
+ import { buttonClasses } from '@odoro-cli/libs/ui'
12
+ import { useState, type ReactElement } from 'react'
13
+
14
+ /** La commande que l'on copie le plus souvent. */
15
+ const COMMANDE = 'odoro add text/count-up'
16
+
17
+ /** La section de cloture. */
18
+ export function Cloture(): ReactElement {
19
+ const [copie, setCopie] = useState(false)
20
+
21
+ return (
22
+ <section className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-24">
23
+ <div className="o-mx-auto o-w-full o-max-w-3xl o-text-center">
24
+ <Reveal>
25
+ <h2 className="o-text-3xl o-font-bold o-tracking-tight md:o-text-4xl">
26
+ Ajoutez un composant, sans quitter le terminal.
27
+ </h2>
28
+ <p className="o-mx-auto o-mt-4 o-max-w-prose o-text-zinc-500 dark:o-text-zinc-400">
29
+ Le registre copie le code dans votre projet. Il vous appartient : on le
30
+ lit, on le modifie, il ne se met pas a jour dans votre dos.
31
+ </p>
32
+
33
+ <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">
34
+ <code className="o-flex-1 o-text-left o-font-mono o-text-sm">{COMMANDE}</code>
35
+ <button
36
+ type="button"
37
+ className={buttonClasses({ tone: 'ghost', size: 'sm' })}
38
+ onClick={() => {
39
+ // Le presse-papiers peut etre refuse — hors contexte sur, ou
40
+ // permission retiree. L'echec ne doit pas casser la page : le
41
+ // libelle ne change simplement pas.
42
+ void navigator.clipboard
43
+ ?.writeText(COMMANDE)
44
+ .then(() => {
45
+ setCopie(true)
46
+ setTimeout(() => {
47
+ setCopie(false)
48
+ }, 1600)
49
+ })
50
+ .catch(() => undefined)
51
+ }}
52
+ >
53
+ {copie ? 'Copie' : 'Copier'}
54
+ </button>
55
+ </div>
56
+
57
+ <a
58
+ href="https://odoro.dev/docs/registry"
59
+ target="_blank"
60
+ rel="noreferrer"
61
+ className="o-mt-6 o-inline-block o-text-sm o-text-zinc-500 dark:o-text-zinc-400 hover:o-text-brand-500"
62
+ >
63
+ Parcourir le catalogue
64
+ </a>
65
+ </Reveal>
66
+ </div>
67
+ </section>
68
+ )
69
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Le fond decoratif de la page.
3
+ *
4
+ * ## Ce qu'il est, et ce qu'il remplace
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.
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.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ import type { ReactElement } from 'react'
18
+
19
+ /** Deux nappes de marque, diluees, derriere le contenu. */
20
+ export function Fond(): ReactElement {
21
+ return (
22
+ <div
23
+ aria-hidden="true"
24
+ className="o-pointer-events-none o-absolute o-inset-x-0 o-top-0 o-h-[42rem] o-overflow-hidden"
25
+ >
26
+ <div
27
+ className="o-absolute o-left-1/2 o-top-[-18rem] o-size-[46rem] o--translate-x-1/2 o-rounded-full o-blur-3xl o-opacity-30 dark:o-opacity-25"
28
+ style={{
29
+ background:
30
+ 'radial-gradient(closest-side, var(--o-palette-brand-500), transparent)',
31
+ }}
32
+ />
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"
35
+ style={{
36
+ background:
37
+ 'radial-gradient(closest-side, var(--o-palette-brand-400), transparent)',
38
+ }}
39
+ />
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Le hero : la promesse, l'installation, et la fenetre du produit.
3
+ *
4
+ * ## Le dessin est celui d'odoro.dev
5
+ *
6
+ * Meme typographie — un titre serre, une phrase de soutien plus claire —,
7
+ * memes pastilles de faits, meme fenetre de navigateur posee sous le pli. Un
8
+ * projet qui demarre ressemble donc a ce qu'il peut devenir, au lieu de partir
9
+ * d'une page blanche qu'il faudra jeter.
10
+ *
11
+ * ## Ce qui n'y est pas
12
+ *
13
+ * Aucun composant du registre. Le hero se contente de ce que les bibliotheques
14
+ * apportent : les classes, `Reveal` et `Stagger`. C'est ce qui lui permet
15
+ * d'exister quel que soit ce qui a ete coche a la creation.
16
+ *
17
+ * @module
18
+ */
19
+
20
+ import { Reveal, Stagger } from '@odoro-cli/libs/motion'
21
+ import { buttonClasses } from '@odoro-cli/libs/ui'
22
+ import type { ReactElement } from 'react'
23
+
24
+ import { Signe } from '@/composants/Marque'
25
+
26
+ /** Quatre faits, pas quatre slogans. */
27
+ const FAITS = ['Zero configuration', 'Theme clair et sombre', 'Mouvement reduit respecte']
28
+
29
+ /** Ce que la fenetre montre : une page en quelques lignes. */
30
+ const EXTRAIT = `export function Accueil() {
31
+ return (
32
+ <Reveal>
33
+ <h1 className="o-text-6xl o-font-bold">
34
+ Votre premiere page.
35
+ </h1>
36
+ </Reveal>
37
+ )
38
+ }`
39
+
40
+ /**
41
+ * Une fenetre de navigateur miniature.
42
+ *
43
+ * Les trois pastilles sont decoratives : elles disent « navigateur » sans rien
44
+ * ajouter a ce qu'un lecteur d'ecran doit entendre.
45
+ */
46
+ function Fenetre(): ReactElement {
47
+ return (
48
+ <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">
49
+ <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">
50
+ <span className="o-flex o-gap-1.5" aria-hidden="true">
51
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
52
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
53
+ <span className="o-size-2.5 o-rounded-full o-bg-zinc-300 dark:o-bg-zinc-700" />
54
+ </span>
55
+ <span className="o-ml-2 o-font-mono o-text-xs o-text-zinc-400">src/routes/Home.tsx</span>
56
+ </div>
57
+ <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">
58
+ {EXTRAIT}
59
+ </pre>
60
+ </div>
61
+ )
62
+ }
63
+
64
+ /** Le hero de la page d'accueil. */
65
+ export function Hero(): ReactElement {
66
+ return (
67
+ <section className="o-relative o-px-6 o-pb-20 o-pt-24 md:o-pt-32">
68
+ <div className="o-mx-auto o-w-full o-max-w-5xl">
69
+ <Reveal>
70
+ <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">
71
+ <Signe />
72
+ Genere par odoro create
73
+ </span>
74
+
75
+ <h1 className="o-mt-6 o-max-w-3xl o-text-5xl o-font-bold o-tracking-tight md:o-text-6xl">
76
+ Votre premiere page,{' '}
77
+ <span className="o-text-brand-500">deja vivante</span>.
78
+ </h1>
79
+
80
+ <p className="o-mt-5 o-max-w-xl o-text-lg o-text-zinc-500 dark:o-text-zinc-400">
81
+ Le routeur, les animations et le systeme de style viennent de la meme
82
+ librairie. Modifiez cette page : elle se recharge sans perdre son etat.
83
+ </p>
84
+ </Reveal>
85
+
86
+ <Stagger step={70} className="o-mt-8 o-flex o-flex-wrap o-items-center o-gap-3">
87
+ <a href="#piliers" className={`o-no-underline ${buttonClasses({ tone: 'primary' })}`}>
88
+ Voir ce qui est inclus
89
+ </a>
90
+ <a
91
+ href="https://odoro.dev/docs"
92
+ className={`o-no-underline ${buttonClasses({ tone: 'secondary' })}`}
93
+ target="_blank"
94
+ rel="noreferrer"
95
+ >
96
+ Documentation
97
+ </a>
98
+ </Stagger>
99
+
100
+ <Stagger step={60} className="o-mt-6 o-flex o-flex-wrap o-gap-2">
101
+ {FAITS.map((fait) => (
102
+ <span
103
+ key={fait}
104
+ 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"
105
+ >
106
+ {fait}
107
+ </span>
108
+ ))}
109
+ </Stagger>
110
+
111
+ <Reveal>
112
+ <div className="o-mt-14">
113
+ <Fenetre />
114
+ </div>
115
+ </Reveal>
116
+ </div>
117
+ </section>
118
+ )
119
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Trois piliers : ce que le projet a deja sous la main.
3
+ *
4
+ * Les cartes s'ecrivent avec la meme grammaire que sur odoro.dev — bordure
5
+ * fine, coin arrondi, titre serre, phrase en gris — pour qu'ajouter une section
6
+ * plus tard ne demande pas d'inventer un style.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { Stagger } from '@odoro-cli/libs/motion'
12
+ import type { ReactElement } from 'react'
13
+
14
+ /** Un pilier : un numero, un titre, une phrase. */
15
+ interface Pilier {
16
+ readonly titre: string
17
+ readonly texte: string
18
+ }
19
+
20
+ const PILIERS: readonly Pilier[] = [
21
+ {
22
+ titre: 'Routeur',
23
+ texte:
24
+ 'Segments dynamiques, routes imbriquees, chargement paresseux et transitions de page.',
25
+ },
26
+ {
27
+ titre: 'Animations',
28
+ texte:
29
+ "Une couche mince sur le moteur du navigateur. Le mouvement reduit est respecte d'office.",
30
+ },
31
+ {
32
+ titre: 'Styles',
33
+ texte:
34
+ 'Des jetons en source de verite, une feuille statique, aucun scan a l execution.',
35
+ },
36
+ ]
37
+
38
+ /** La section des piliers. */
39
+ export function Piliers(): ReactElement {
40
+ return (
41
+ <section id="piliers" className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-20">
42
+ <div className="o-mx-auto o-w-full o-max-w-5xl">
43
+ <h2 className="o-text-sm o-font-semibold o-uppercase o-tracking-widest o-text-zinc-400">
44
+ Ce qui est deja la
45
+ </h2>
46
+
47
+ <Stagger step={90} className="o-mt-8 o-grid o-gap-4 md:o-grid-cols-3">
48
+ {PILIERS.map((pilier, index) => (
49
+ <article
50
+ key={pilier.titre}
51
+ 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"
52
+ >
53
+ <span className="o-font-mono o-text-xs o-text-brand-500">
54
+ {String(index + 1).padStart(2, '0')}
55
+ </span>
56
+ <h3 className="o-mt-3 o-text-lg o-font-semibold o-tracking-tight">
57
+ {pilier.titre}
58
+ </h3>
59
+ <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
60
+ {pilier.texte}
61
+ </p>
62
+ </article>
63
+ ))}
64
+ </Stagger>
65
+ </div>
66
+ </section>
67
+ )
68
+ }
@@ -1,10 +1,9 @@
1
1
  /* Styles propres a l'application.
2
- Tout le reste vient des tokens Odoro : surcharger une variable ci-dessous
3
- suffit a rethemer l'ensemble, composants de la librairie compris. */
4
2
 
5
- :root {
6
- --o-color-primary: oklch(52.4% 0.212 275);
7
- }
3
+ Tout le reste vient des jetons Odoro : surcharger une variable ci-dessous
4
+ retheme l'ensemble, composants de la librairie compris. La teinte de marque
5
+ est `--o-palette-brand-500` ; la remplacer suffit a changer la couleur de la
6
+ page, du signe et des boutons d'un seul coup. */
8
7
 
9
8
  .app-shell {
10
9
  min-height: 100dvh;