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.
- package/dist/cli.js +3 -3
- package/dist/{commands-PT3NS65O.js → commands-AHMXBWQQ.js} +25 -5
- package/dist/{create-IOFAPDBO.js → create-LTT3SAWT.js} +11 -7
- package/dist/{package-Q2FV4K3H.js → package-LK46RV2I.js} +1 -1
- package/package.json +1 -1
- package/templates/react-ts/_variantes/avec-moteur/src/sections/Fond.tsx +118 -0
- package/templates/react-ts/_variantes/sans-libs/src/App.tsx +22 -51
- package/templates/react-ts/_variantes/sans-libs/src/composants/Marque.tsx +37 -0
- package/templates/react-ts/_variantes/sans-libs/src/sections/Cloture.tsx +60 -0
- package/templates/react-ts/_variantes/sans-libs/src/sections/Fond.tsx +23 -0
- package/templates/react-ts/_variantes/sans-libs/src/sections/Hero.tsx +92 -0
- package/templates/react-ts/_variantes/sans-libs/src/sections/Piliers.tsx +44 -0
- package/templates/react-ts/_variantes/sans-libs/src/styles.css +322 -40
- package/templates/react-ts/_variantes/sans-routeur/src/App.tsx +35 -77
- package/templates/react-ts/src/App.tsx +56 -29
- package/templates/react-ts/src/composants/Marque.tsx +50 -0
- package/templates/react-ts/src/routes/About.tsx +38 -14
- package/templates/react-ts/src/routes/Home.tsx +21 -73
- package/templates/react-ts/src/sections/Cloture.tsx +69 -0
- package/templates/react-ts/src/sections/Fond.tsx +42 -0
- package/templates/react-ts/src/sections/Hero.tsx +119 -0
- package/templates/react-ts/src/sections/Piliers.tsx +68 -0
- package/templates/react-ts/src/styles.css +4 -5
- package/templates/react-ts-server/_variantes/avec-moteur/client/src/sections/Fond.tsx +118 -0
- package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +22 -51
- package/templates/react-ts-server/_variantes/sans-libs/client/src/composants/Marque.tsx +37 -0
- package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Cloture.tsx +60 -0
- package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Fond.tsx +23 -0
- package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Hero.tsx +92 -0
- package/templates/react-ts-server/_variantes/sans-libs/client/src/sections/Piliers.tsx +44 -0
- package/templates/react-ts-server/_variantes/sans-libs/client/src/styles.css +322 -40
- package/templates/react-ts-server/_variantes/sans-routeur/client/src/App.tsx +35 -77
- package/templates/react-ts-server/client/src/App.tsx +56 -29
- package/templates/react-ts-server/client/src/composants/Marque.tsx +50 -0
- package/templates/react-ts-server/client/src/routes/About.tsx +38 -14
- package/templates/react-ts-server/client/src/routes/Home.tsx +18 -138
- package/templates/react-ts-server/client/src/sections/Cloture.tsx +69 -0
- package/templates/react-ts-server/client/src/sections/Fond.tsx +42 -0
- package/templates/react-ts-server/client/src/sections/Hero.tsx +119 -0
- package/templates/react-ts-server/client/src/sections/Piliers.tsx +68 -0
- 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 {
|
|
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 «
|
|
5
|
-
export function About() {
|
|
17
|
+
/** Page « A propos ». */
|
|
18
|
+
export function About(): ReactElement {
|
|
6
19
|
return (
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
*
|
|
2
|
+
* La page d'accueil : le fond, puis les sections.
|
|
24
3
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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’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’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
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
:
|
|
6
|
-
|
|
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;
|