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,80 +1,28 @@
1
- import { useState } from 'react'
1
+ /**
2
+ * La page d'accueil : le fond, puis les sections.
3
+ *
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
9
+ */
2
10
 
3
- import { Reveal, Stagger, useAnimate } from '@odoro-cli/libs/motion'
4
- import { Link } from '@odoro-cli/libs/router'
5
- import { Button } from '@odoro-cli/libs/ui'
11
+ import type { ReactElement } from 'react'
6
12
 
7
- /** Trois arguments mis en avant sur la page d'accueil. */
8
- const FEATURES = [
9
- {
10
- title: 'Routeur',
11
- body: 'Segments dynamiques, routes imbriquees, chargement paresseux et transitions de page.',
12
- },
13
- {
14
- title: 'Animations',
15
- body: "Une couche mince sur le moteur du navigateur. Les animations reduites sont respectees d'office.",
16
- },
17
- {
18
- title: 'Styles',
19
- body: 'Des tokens en source de verite, une feuille statique, aucun scan a l execution.',
20
- },
21
- ]
13
+ import { Cloture } from '@/sections/Cloture'
14
+ import { Fond } from '@/sections/Fond'
15
+ import { Hero } from '@/sections/Hero'
16
+ import { Piliers } from '@/sections/Piliers'
22
17
 
23
18
  /** Page d'accueil. */
24
- export function Home() {
25
- const [ref, controls] = useAnimate<HTMLSpanElement>()
26
- const [count, setCount] = useState(0)
27
-
19
+ export function Home(): ReactElement {
28
20
  return (
29
- <div className="o-flex o-flex-col o-gap-12">
30
- <Reveal>
31
- <h1 className="o-text-4xl o-font-bold o-tracking-tight">
32
- Un point de depart maitrise.
33
- </h1>
34
- <p className="o-mt-4 o-text-lg o-text-zinc-500 dark:o-text-zinc-400 o-max-w-prose">
35
- Ce projet a ete genere par <code className="o-font-mono">odoro create</code>. Le
36
- routeur, le moteur d animation et le systeme de style viennent tous de la meme
37
- librairie.
38
- </p>
39
- </Reveal>
40
-
41
- <Stagger step={80} className="o-grid o-grid-cols-1 md:o-grid-cols-3 o-gap-4">
42
- {FEATURES.map((feature) => (
43
- <article
44
- key={feature.title}
45
- 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"
46
- >
47
- <h2 className="o-font-semibold">{feature.title}</h2>
48
- <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
49
- {feature.body}
50
- </p>
51
- </article>
52
- ))}
53
- </Stagger>
54
-
55
- <div className="o-flex o-items-center o-gap-4">
56
- <Button
57
- onClick={() => {
58
- setCount((value) => value + 1)
59
- void controls.play(
60
- [
61
- { transform: 'translateY(0)' },
62
- { transform: 'translateY(-0.4rem)' },
63
- { transform: 'translateY(0)' },
64
- ],
65
- { duration: 'fast', easing: 'emphasized' },
66
- )
67
- }}
68
- >
69
- Compter
70
- </Button>
71
- <span ref={ref} className="o-inline-block o-text-lg o-tabular-nums">
72
- {count}
73
- </span>
74
- <Link to="/a-propos" className="o-text-sm">
75
- En savoir plus
76
- </Link>
77
- </div>
78
- </div>
21
+ <>
22
+ <Fond />
23
+ <Hero />
24
+ <Piliers />
25
+ <Cloture />
26
+ </>
79
27
  )
80
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;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Le fond decoratif, en surface WebGL.
3
+ *
4
+ * Cette version remplace le degre statique parce que le moteur a ete retenu a
5
+ * la creation. Le reste de la page est inchange : elle place `<Fond />` et ne
6
+ * sait pas ce qu'il y a derriere.
7
+ *
8
+ * ## Ce que le moteur apporte, et qu'un degrade ne peut pas
9
+ *
10
+ * **Un arbitre.** La surface est demandee au gestionnaire du moteur, qui la
11
+ * refuse si le navigateur n'a pas WebGL, si trop de contextes sont deja
12
+ * ouverts, ou si l'appareil ne suivrait pas. Le refus est une valeur, pas une
13
+ * exception : on affiche alors le degrade, et la page tient.
14
+ *
15
+ * **Le mouvement reduit.** Sous cette preference, rien n'est monte du tout :
16
+ * le fond n'apportait que son mouvement, et la carte graphique n'a pas a
17
+ * s'allumer pour un visiteur qui a demande le calme.
18
+ *
19
+ * **Les couleurs du theme.** Les teintes sont lues dans les jetons, pas
20
+ * ecrites dans le nuanceur. Une bascule clair / sombre les relit, et changer
21
+ * `--o-palette-brand-500` repeint le fond sans toucher a ce fichier.
22
+ *
23
+ * ## Pourquoi des styles en ligne
24
+ *
25
+ * Ce fichier est pose quel que soit le reste : un projet qui a decoche les
26
+ * bibliotheques n'a pas les classes `o-*`, et une classe absente ne peint
27
+ * rien — le fond aurait alors une hauteur nulle, sans que rien ne le signale.
28
+ * Les quelques regles de placement sont donc ecrites ou elles sont sures.
29
+ *
30
+ * @module
31
+ */
32
+
33
+ import {
34
+ AURORA_FRAGMENT,
35
+ readTokenColour,
36
+ useMotionState,
37
+ useShaderSurface,
38
+ type ShaderColour,
39
+ } from '@odoro-cli/engine'
40
+ import { useEffect, useMemo, useState, type ReactElement } from 'react'
41
+
42
+ /** Les jetons lus pour teinter la nappe. */
43
+ const TEINTES = ['--o-palette-brand-500', '--o-palette-brand-300', '--o-theme-bg'] as const
44
+
45
+ /** Le degrade servi tant que la surface n'est pas la — ou pour toujours. */
46
+ function Degrade(): ReactElement {
47
+ return (
48
+ <div
49
+ style={{
50
+ position: 'absolute',
51
+ inset: 0,
52
+ opacity: 0.28,
53
+ filter: 'blur(64px)',
54
+ background:
55
+ 'radial-gradient(60% 60% at 50% 0%, var(--o-palette-brand-500, #f97316), transparent)',
56
+ }}
57
+ />
58
+ )
59
+ }
60
+
61
+ /** Une nappe animee derriere le haut de la page. */
62
+ export function Fond(): ReactElement {
63
+ const { quality, reduced } = useMotionState()
64
+ const [hote, setHote] = useState<HTMLElement | null>(null)
65
+ const [teintes, setTeintes] = useState<readonly ShaderColour[]>([])
66
+
67
+ // Relues quand l'hote apparait et quand le theme bascule — ce dernier cas
68
+ // passe par la politique de mouvement, qui se renouvelle a chaque etat.
69
+ useEffect(() => {
70
+ if (hote === null) return
71
+ setTeintes(TEINTES.map((jeton) => readTokenColour(jeton, hote)))
72
+ }, [hote, reduced, quality])
73
+
74
+ const uniforms = useMemo(() => {
75
+ const [a, b, c] = teintes
76
+ if (a === undefined || b === undefined || c === undefined) return undefined
77
+ // Le fond du theme domine, la marque n'est qu'un reflet : ces trois
78
+ // couleurs vont dans un melange, et deux teintes de marque sur trois
79
+ // donneraient une nappe orange en pleine page plutot qu'un halo.
80
+ return { uColorA: c, uColorB: c, uColorC: a, uSpeed: 0.1, uScale: 1.4, uOctaves: 3 }
81
+ }, [teintes])
82
+
83
+ const { ref, ready, refused } = useShaderSurface<HTMLDivElement>({
84
+ fragment: AURORA_FRAGMENT,
85
+ uniforms: uniforms ?? {},
86
+ name: 'fond-accueil',
87
+ })
88
+
89
+ const repli = !ready || refused !== undefined || uniforms === undefined
90
+
91
+ return (
92
+ <div
93
+ aria-hidden="true"
94
+ ref={(element) => {
95
+ setHote(element)
96
+ ref.current = element
97
+ }}
98
+ style={{
99
+ position: 'absolute',
100
+ insetInline: 0,
101
+ top: 0,
102
+ height: '42rem',
103
+ overflow: 'hidden',
104
+ pointerEvents: 'none',
105
+ // Un fond decoratif se remarque quand on le cherche, pas avant. A
106
+ // pleine intensite la nappe passe devant le titre qu'elle est censee
107
+ // porter ; a un cinquieme, elle le souligne.
108
+ opacity: 0.2,
109
+ // Le bas se dissout dans la page : une surface qui s'arrete net
110
+ // dessine une ligne horizontale que rien ne justifie.
111
+ maskImage: 'linear-gradient(to bottom, black 45%, transparent)',
112
+ WebkitMaskImage: 'linear-gradient(to bottom, black 45%, transparent)',
113
+ }}
114
+ >
115
+ {repli ? <Degrade /> : null}
116
+ </div>
117
+ )
118
+ }