odoro 1.0.9 → 2.1.0

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 (88) hide show
  1. package/README.md +94 -0
  2. package/client.d.ts +90 -16
  3. package/dist/build-XJIP4GNT.js +5 -0
  4. package/dist/{chunk-34RGOZFA.js → chunk-4D56Z7G3.js} +49 -50
  5. package/dist/{chunk-JMEHF3KN.js → chunk-7ZB5MAM6.js} +4 -4
  6. package/dist/chunk-DF67LADH.js +735 -0
  7. package/dist/{chunk-LHZGX5ML.js → chunk-HS7NGZLM.js} +307 -196
  8. package/dist/chunk-LIKVHWB4.js +231 -0
  9. package/dist/chunk-QOVBLN7A.js +241 -0
  10. package/dist/chunk-RTWJ5GFB.js +729 -0
  11. package/dist/chunk-TONYIKCN.js +57 -0
  12. package/dist/cli.d.ts +12 -13
  13. package/dist/cli.js +86 -63
  14. package/dist/{commands-AHMXBWQQ.js → commands-GYA7NTCX.js} +124 -124
  15. package/dist/commands-UOWN3IK4.js +181 -0
  16. package/dist/{create-CYVSVDAD.js → create-B2TLV6XI.js} +198 -160
  17. package/dist/index.d.ts +603 -151
  18. package/dist/index.js +6 -6
  19. package/dist/{package-EIDLKAA6.js → package-3ZJOEXM4.js} +6 -5
  20. package/dist/preview-3IRG7MHV.js +4 -0
  21. package/dist/registry/index.d.ts +75 -76
  22. package/dist/registry/index.js +1 -1
  23. package/dist/{server-4UGN3SFS.js → server-ZB3P3EGS.js} +3 -3
  24. package/package.json +6 -5
  25. package/templates/react-ts/README.md +71 -35
  26. package/templates/react-ts/_env.example +9 -0
  27. package/templates/react-ts/_variants/with-engine/src/background.tsx +118 -0
  28. package/templates/react-ts/_variants/without-libs/src/App.tsx +380 -0
  29. package/templates/react-ts/_variants/without-libs/src/background.tsx +24 -0
  30. package/templates/react-ts/_variants/without-libs/src/entry-server.tsx +39 -0
  31. package/templates/react-ts/_variants/without-libs/src/main.tsx +26 -0
  32. package/templates/{react-ts-server/_variantes/sans-libs/client → react-ts/_variants/without-libs}/src/styles.css +137 -137
  33. package/templates/react-ts/{_variantes/sans-routeur → _variants/without-router}/src/App.tsx +141 -141
  34. package/templates/react-ts/_variants/without-router/src/entry-server.tsx +39 -0
  35. package/templates/react-ts/index.html +4 -4
  36. package/templates/react-ts/odoro.config.ts +5 -0
  37. package/templates/react-ts/src/App.tsx +219 -209
  38. package/templates/react-ts/src/background.tsx +66 -0
  39. package/templates/react-ts/src/entry-server.tsx +85 -0
  40. package/templates/react-ts/src/main.tsx +13 -4
  41. package/templates/react-ts/src/router.tsx +65 -45
  42. package/templates/react-ts/src/styles.css +5 -5
  43. package/templates/react-ts-server/Dockerfile +9 -9
  44. package/templates/react-ts-server/README.md +97 -45
  45. package/templates/react-ts-server/_env.example +31 -33
  46. package/templates/react-ts-server/_variants/with-engine/client/src/background.tsx +118 -0
  47. package/templates/react-ts-server/_variants/without-libs/client/src/App.tsx +380 -0
  48. package/templates/react-ts-server/_variants/without-libs/client/src/background.tsx +24 -0
  49. package/templates/react-ts-server/_variants/without-libs/client/src/entry-server.tsx +39 -0
  50. package/templates/react-ts-server/_variants/without-libs/client/src/main.tsx +26 -0
  51. package/templates/{react-ts/_variantes/sans-libs → react-ts-server/_variants/without-libs/client}/src/styles.css +137 -137
  52. package/templates/react-ts-server/{_variantes/sans-routeur → _variants/without-router}/client/src/App.tsx +141 -141
  53. package/templates/react-ts-server/_variants/without-router/client/src/entry-server.tsx +39 -0
  54. package/templates/react-ts-server/client/index.html +4 -4
  55. package/templates/react-ts-server/client/src/App.tsx +228 -210
  56. package/templates/react-ts-server/client/src/account.tsx +267 -0
  57. package/templates/react-ts-server/client/src/auth.tsx +139 -0
  58. package/templates/react-ts-server/client/src/background.tsx +66 -0
  59. package/templates/react-ts-server/client/src/entry-server.tsx +92 -0
  60. package/templates/react-ts-server/client/src/main.tsx +17 -5
  61. package/templates/react-ts-server/client/src/router.tsx +77 -45
  62. package/templates/react-ts-server/client/src/styles.css +5 -5
  63. package/templates/react-ts-server/odoro.config.ts +7 -4
  64. package/templates/react-ts-server/package.json +2 -0
  65. package/templates/react-ts-server/scripts/dev.mjs +14 -14
  66. package/templates/react-ts-server/server/src/main.ts +119 -45
  67. package/templates/react-ts-server/server/src/modules/auth/index.ts +238 -0
  68. package/templates/react-ts-server/server/src/modules/auth/password.ts +122 -0
  69. package/templates/react-ts-server/server/src/modules/auth/store.ts +193 -0
  70. package/templates/react-ts-server/server/src/modules/health/index.ts +45 -46
  71. package/dist/build-JFQHODAT.js +0 -5
  72. package/dist/chunk-22KJTV2R.js +0 -380
  73. package/dist/chunk-3SZIN6VG.js +0 -64
  74. package/dist/chunk-DL3NPC4H.js +0 -113
  75. package/dist/chunk-TQUJ3MFS.js +0 -268
  76. package/dist/chunk-ZVL7EXJO.js +0 -66
  77. package/dist/commands-4JRBD55Z.js +0 -245
  78. package/dist/preview-7DKEAQOQ.js +0 -4
  79. package/templates/react-ts/_variantes/avec-moteur/src/fond.tsx +0 -118
  80. package/templates/react-ts/_variantes/sans-libs/src/App.tsx +0 -382
  81. package/templates/react-ts/_variantes/sans-libs/src/fond.tsx +0 -23
  82. package/templates/react-ts/_variantes/sans-libs/src/main.tsx +0 -17
  83. package/templates/react-ts/src/fond.tsx +0 -66
  84. package/templates/react-ts-server/_variantes/avec-moteur/client/src/fond.tsx +0 -118
  85. package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +0 -382
  86. package/templates/react-ts-server/_variantes/sans-libs/client/src/fond.tsx +0 -23
  87. package/templates/react-ts-server/_variantes/sans-libs/client/src/main.tsx +0 -17
  88. package/templates/react-ts-server/client/src/fond.tsx +0 -66
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Toute la page, en un fichier.
2
+ * The whole page, in one file.
3
3
  *
4
- * ## Pourquoi tout ici
4
+ * ## Why everything is here
5
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.
6
+ * A project that is just starting reads better in one piece: you see the
7
+ * navigation, the sections and the footer without opening eight files. Each
8
+ * block is a function; the day one of them grows, it moves into its own file
9
+ * with a cut and a paste.
10
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.
11
+ * Two things live apart, and for a reason: the **router**, which is the only
12
+ * routing dependency and can be swapped without touching the page; and the
13
+ * **background**, which is the only piece that depends on the engine.
14
14
  *
15
- * ## La barre de navigation
15
+ * ## The navigation bar
16
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.
17
+ * It paints no band: these are pills laid over whatever is behind, with a
18
+ * backdrop blur. The decorative background therefore keeps showing through,
19
+ * instead of being cut off by an opaque strip.
20
20
  *
21
21
  * @module
22
22
  */
@@ -31,35 +31,39 @@ import {
31
31
  type ReactNode,
32
32
  } from 'react'
33
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'
34
+ // Named rather than imported wholesale: the rest of the manifest — the
35
+ // scripts, the metadata — has no business in the code shipped to the browser.
36
+ //
37
+ // Two levels up, and not one: in this template the client lives in `client/`,
38
+ // and the manifest is at the root of the project, alongside the server's.
39
+ import { dependencies, devDependencies } from '../../package.json'
37
40
 
38
- import { Fond } from '@/fond'
39
- import { Link, Routeur, useLocation } from '@/router'
41
+ import { ProfilePage, Register, SignIn } from '@/account'
42
+ import { Background } from '@/background'
43
+ import { Link, Router, useLocation } from '@/router'
40
44
 
41
45
  /* -------------------------------------------------------------------------- */
42
- /* Marque */
46
+ /* Brand */
43
47
  /* -------------------------------------------------------------------------- */
44
48
 
45
49
  /**
46
- * Le signe de la marque.
50
+ * The brand mark.
47
51
  *
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.
52
+ * Drawn inline rather than loaded from `public/`: it takes the colour of its
53
+ * parent through `currentColor`, hence the brand hue in light as in dark, with
54
+ * no second file to maintain.
51
55
  *
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.
56
+ * The size is a style and not a class: an arbitrary-value utility class is
57
+ * only emitted if the compiler saw it go by, and a missing class paints
58
+ * nothing — the mark would have no dimensions, so it would be invisible.
55
59
  */
56
- function Signe({ taille = '1.75rem' }: { readonly taille?: string }): ReactElement {
60
+ function Mark({ size = '1.75rem' }: { readonly size?: string }): ReactElement {
57
61
  return (
58
62
  <svg
59
63
  viewBox="0 0 100 100"
60
64
  fill="none"
61
65
  aria-hidden="true"
62
- style={{ width: taille, height: taille, flexShrink: 0 }}
66
+ style={{ width: size, height: size, flexShrink: 0 }}
63
67
  >
64
68
  <path
65
69
  d="M20.25 20.25H50a29.75 29.75 0 1 1-29.75 29.75Z"
@@ -72,61 +76,62 @@ function Signe({ taille = '1.75rem' }: { readonly taille?: string }): ReactEleme
72
76
  }
73
77
 
74
78
  /* -------------------------------------------------------------------------- */
75
- /* Barre de navigation */
79
+ /* Navigation bar */
76
80
  /* -------------------------------------------------------------------------- */
77
81
 
78
82
  /**
79
- * La surface d'une gelule.
83
+ * The surface of a pill.
80
84
  *
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
+ * The system ships the black and white scales, but not their dark alpha
86
+ * variants: `o-bg-white/70` and its `dark:` counterpart do not exist as a
87
+ * single class. So the mix is done as a style, where it is exact and follows
88
+ * the theme without every colour having to be restated.
85
89
  */
86
- const GELULE: CSSProperties = {
90
+ const PILL: CSSProperties = {
87
91
  backgroundColor: 'color-mix(in oklab, var(--o-theme-bg) 72%, transparent)',
88
92
  borderColor: 'color-mix(in oklab, var(--o-theme-fg) 12%, transparent)',
89
93
  }
90
94
 
91
- /** Les entrees de la navigation. */
92
- const LIENS = [
93
- { to: '/', label: 'Accueil' },
94
- { to: '/a-propos', label: 'A propos' },
95
+ /** The navigation entries. */
96
+ const LINKS = [
97
+ { to: '/', label: 'Home' },
98
+ { to: '/about', label: 'About' },
99
+ { to: '/profile', label: 'Account' },
95
100
  ]
96
101
 
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 {
102
+ /** A link in the navigation pill, active on the current route. */
103
+ function TopLink({ to, label }: { readonly to: string; readonly label: string }): ReactElement {
99
104
  const { pathname } = useLocation()
100
- const actif = pathname === to
105
+ const active = pathname === to
101
106
 
102
107
  return (
103
108
  <Link
104
109
  to={to}
105
110
  className={[
106
111
  'o-rounded-full o-px-4 o-py-2 o-text-sm o-no-underline o-transition-colors',
107
- actif
112
+ active
108
113
  ? 'o-font-medium o-text-zinc-950 dark:o-text-white'
109
114
  : 'o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white',
110
115
  ].join(' ')}
111
- style={actif ? GELULE : undefined}
116
+ style={active ? PILL : undefined}
112
117
  >
113
118
  {label}
114
119
  </Link>
115
120
  )
116
121
  }
117
122
 
118
- /** La barre : la marque, la navigation, et le lien vers le site. */
119
- function Barre(): ReactElement {
123
+ /** The bar: the brand, the navigation, and the link to the site. */
124
+ function Nav(): ReactElement {
120
125
  return (
121
126
  <header className="o-sticky o-top-0 o-z-20 o-px-5 md:o-px-8">
122
127
  <div className="o-mx-auto o-flex o-h-20 o-w-full o-max-w-5xl o-items-center o-gap-3">
123
128
  <Link
124
129
  to="/"
125
130
  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}
131
+ style={PILL}
127
132
  >
128
133
  <span className="o-text-brand-500">
129
- <Signe />
134
+ <Mark />
130
135
  </span>
131
136
  <span className="o-text-lg o-font-bold o-tracking-wide o-text-zinc-950 dark:o-text-white">
132
137
  ODORO
@@ -136,10 +141,10 @@ function Barre(): ReactElement {
136
141
  <nav
137
142
  aria-label="Navigation"
138
143
  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}
144
+ style={PILL}
140
145
  >
141
- {LIENS.map((lien) => (
142
- <LienHaut key={lien.to} to={lien.to} label={lien.label} />
146
+ {LINKS.map((link) => (
147
+ <TopLink key={link.to} to={link.to} label={link.label} />
143
148
  ))}
144
149
  </nav>
145
150
 
@@ -149,11 +154,11 @@ function Barre(): ReactElement {
149
154
  target="_blank"
150
155
  rel="noreferrer"
151
156
  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}
157
+ style={PILL}
153
158
  >
154
159
  odoro.dev
155
160
  </a>
156
- <BasculeTheme />
161
+ <ThemeToggle />
157
162
  </div>
158
163
  </div>
159
164
  </header>
@@ -164,45 +169,45 @@ function Barre(): ReactElement {
164
169
  /* Theme */
165
170
  /* -------------------------------------------------------------------------- */
166
171
 
167
- /** Les trois etats de la bascule. */
168
- type Theme = 'systeme' | 'clair' | 'sombre'
172
+ /** The three states of the toggle. */
173
+ type Theme = 'system' | 'light' | 'dark'
169
174
 
170
- /** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
171
- const CLE_THEME = 'odoro-theme'
175
+ /** Where the choice is stored. The script in `index.html` reads the same key. */
176
+ const THEME_KEY = 'odoro-theme'
172
177
 
173
- /** L'ordre du cycle, au clic. */
174
- const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
178
+ /** The order of the cycle, on click. */
179
+ const CYCLE: readonly Theme[] = ['system', 'light', 'dark']
175
180
 
176
181
  /**
177
- * Pose le theme sur la racine du document.
182
+ * Applies the theme to the root of the document.
178
183
  *
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.
184
+ * `system` removes the attribute rather than setting a third value: the
185
+ * stylesheet listens to `data-theme`, and its absence hands control back to
186
+ * the browser preference. That is what lets a page left on "system" follow the
187
+ * switch to night without being reopened.
183
188
  */
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'
189
+ function applyTheme(theme: Theme): void {
190
+ if (theme === 'system') delete document.documentElement.dataset.theme
191
+ else document.documentElement.dataset.theme = theme
187
192
  }
188
193
 
189
- /** Le theme memorise, ou `systeme`. */
190
- function themeMemorise(): Theme {
194
+ /** The stored theme, or `system`. */
195
+ function storedTheme(): Theme {
191
196
  try {
192
- const valeur = localStorage.getItem(CLE_THEME)
193
- if (valeur === 'light') return 'clair'
194
- if (valeur === 'dark') return 'sombre'
195
- return 'systeme'
197
+ const value = localStorage.getItem(THEME_KEY)
198
+ if (value === 'light') return 'light'
199
+ if (value === 'dark') return 'dark'
200
+ return 'system'
196
201
  } 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'
202
+ // Storage refused — private window, cookies blocked. The system theme is
203
+ // still a valid answer; failing here would deprive the page of its render.
204
+ return 'system'
200
205
  }
201
206
  }
202
207
 
203
- /** Les trois pictogrammes, dessines plutot qu'importes : les icones sont optionnelles. */
204
- function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
205
- const commun = {
208
+ /** The three glyphs, drawn rather than imported: the icons are optional. */
209
+ function Glyph({ theme }: { readonly theme: Theme }): ReactElement {
210
+ const common = {
206
211
  viewBox: '0 0 24 24',
207
212
  fill: 'none',
208
213
  stroke: 'currentColor',
@@ -213,25 +218,25 @@ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
213
218
  style: { width: '1.15rem', height: '1.15rem' },
214
219
  }
215
220
 
216
- if (theme === 'clair') {
221
+ if (theme === 'light') {
217
222
  return (
218
- <svg {...commun}>
223
+ <svg {...common}>
219
224
  <circle cx="12" cy="12" r="4" />
220
225
  <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
226
  </svg>
222
227
  )
223
228
  }
224
229
 
225
- if (theme === 'sombre') {
230
+ if (theme === 'dark') {
226
231
  return (
227
- <svg {...commun}>
232
+ <svg {...common}>
228
233
  <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
229
234
  </svg>
230
235
  )
231
236
  }
232
237
 
233
238
  return (
234
- <svg {...commun}>
239
+ <svg {...common}>
235
240
  <rect x="2" y="4" width="20" height="13" rx="2" />
236
241
  <path d="M8 21h8M12 17v4" />
237
242
  </svg>
@@ -239,66 +244,66 @@ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
239
244
  }
240
245
 
241
246
  /**
242
- * La bascule de theme.
247
+ * The theme toggle.
243
248
  *
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.
249
+ * Three states and not two: "system" is a choice in its own right, and
250
+ * removing it would force a visitor to choose again every time their
251
+ * preference changes.
247
252
  */
248
- function BasculeTheme(): ReactElement {
249
- const [theme, setTheme] = useState<Theme>(() => themeMemorise())
253
+ function ThemeToggle(): ReactElement {
254
+ const [theme, setTheme] = useState<Theme>(() => storedTheme())
250
255
 
251
256
  useEffect(() => {
252
- poserTheme(theme)
257
+ applyTheme(theme)
253
258
  try {
254
- if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
255
- else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
259
+ if (theme === 'system') localStorage.removeItem(THEME_KEY)
260
+ else localStorage.setItem(THEME_KEY, theme)
256
261
  } catch {
257
- // Le theme reste applique pour la session, meme sans stockage.
262
+ // The theme stays applied for the session, even without storage.
258
263
  }
259
264
  }, [theme])
260
265
 
261
266
  return (
262
267
  <button
263
268
  type="button"
264
- aria-label={`Theme : ${theme}`}
265
- title={`Theme : ${theme}`}
269
+ aria-label={`Theme: ${theme}`}
270
+ title={`Theme: ${theme}`}
266
271
  onClick={() => {
267
- setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
272
+ setTheme((current) => CYCLE[(CYCLE.indexOf(current) + 1) % CYCLE.length] ?? 'system')
268
273
  }}
269
274
  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}
275
+ style={PILL}
271
276
  >
272
- <Pictogramme theme={theme} />
277
+ <Glyph theme={theme} />
273
278
  </button>
274
279
  )
275
280
  }
276
281
 
277
282
  /* -------------------------------------------------------------------------- */
278
- /* Sections de la page d'accueil */
283
+ /* Home page sections */
279
284
  /* -------------------------------------------------------------------------- */
280
285
 
281
- /** Trois faits, pas trois slogans. */
282
- const FAITS = ['Zero configuration', 'Theme clair et sombre', 'Mouvement reduit respecte']
286
+ /** Three facts, not three slogans. */
287
+ const FACTS = ['Zero configuration', 'Light and dark themes', 'Reduced motion respected']
283
288
 
284
- /** Ce que la fenetre du produit montre. */
285
- const EXTRAIT = `export function Accueil() {
289
+ /** What the product window shows. */
290
+ const SNIPPET = `export function Home() {
286
291
  return (
287
292
  <Reveal>
288
293
  <h1 className="o-text-6xl o-font-bold">
289
- Votre premiere page.
294
+ Your first page.
290
295
  </h1>
291
296
  </Reveal>
292
297
  )
293
298
  }`
294
299
 
295
300
  /**
296
- * Une fenetre de navigateur miniature.
301
+ * A miniature browser window.
297
302
  *
298
- * Les trois pastilles sont decoratives : elles disent « navigateur » sans rien
299
- * ajouter a ce qu'un lecteur d'ecran doit entendre.
303
+ * The three dots are decorative: they say "browser" without adding anything a
304
+ * screen reader needs to hear.
300
305
  */
301
- function Fenetre(): ReactElement {
306
+ function Window(): ReactElement {
302
307
  return (
303
308
  <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
309
  <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">
@@ -310,39 +315,39 @@ function Fenetre(): ReactElement {
310
315
  <span className="o-ml-2 o-font-mono o-text-xs o-text-zinc-400">src/App.tsx</span>
311
316
  </div>
312
317
  <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}
318
+ {SNIPPET}
314
319
  </pre>
315
320
  </div>
316
321
  )
317
322
  }
318
323
 
319
- /** Le hero : la promesse, les actions, et la fenetre du produit. */
324
+ /** The hero: the promise, the actions, and the product window. */
320
325
  function Hero(): ReactElement {
321
326
  return (
322
327
  <section className="o-relative o-px-6 o-pb-20 o-pt-16 md:o-pt-24">
323
328
  <div className="o-mx-auto o-w-full o-max-w-5xl">
324
329
  <Reveal>
325
330
  <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
331
+ <Mark size="0.9rem" />
332
+ Generated by odoro create
328
333
  </span>
329
334
 
330
335
  <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>.
336
+ Your first page, <span className="o-text-brand-500">already alive</span>.
332
337
  </h1>
333
338
 
334
339
  <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.
340
+ The router, the animations and the style system all come from the same
341
+ library. Edit this page: it reloads without losing its state.
337
342
  </p>
338
343
  </Reveal>
339
344
 
340
345
  <Stagger step={70} className="o-mt-8 o-flex o-flex-wrap o-items-center o-gap-3">
341
346
  <a
342
- href="#piliers"
347
+ href="#pillars"
343
348
  className={`o-no-underline ${buttonClasses({ tone: 'primary' })}`}
344
349
  >
345
- Voir ce qui est inclus
350
+ See what is included
346
351
  </a>
347
352
  <a
348
353
  href="https://odoro.dev/docs"
@@ -355,19 +360,19 @@ function Hero(): ReactElement {
355
360
  </Stagger>
356
361
 
357
362
  <Stagger step={60} className="o-mt-6 o-flex o-flex-wrap o-gap-2">
358
- {FAITS.map((fait) => (
363
+ {FACTS.map((fact) => (
359
364
  <span
360
- key={fait}
365
+ key={fact}
361
366
  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
367
  >
363
- {fait}
368
+ {fact}
364
369
  </span>
365
370
  ))}
366
371
  </Stagger>
367
372
 
368
373
  <Reveal>
369
374
  <div className="o-mt-14">
370
- <Fenetre />
375
+ <Window />
371
376
  </div>
372
377
  </Reveal>
373
378
  </div>
@@ -375,50 +380,49 @@ function Hero(): ReactElement {
375
380
  )
376
381
  }
377
382
 
378
- /** Ce que le projet a deja sous la main. */
379
- const PILIERS = [
383
+ /** What the project already has at hand. */
384
+ const PILLARS = [
380
385
  {
381
- titre: 'Routeur',
382
- texte:
383
- 'Segments dynamiques, routes imbriquees, chargement paresseux et transitions de page.',
386
+ title: 'Router',
387
+ text: 'Dynamic segments, nested routes, lazy loading and page transitions.',
384
388
  },
385
389
  {
386
- titre: 'Animations',
387
- texte:
388
- "Une couche mince sur le moteur du navigateur. Le mouvement reduit est respecte d'office.",
390
+ title: 'Animations',
391
+ text:
392
+ "A thin layer over the browser's own engine. Reduced motion is respected out of the box.",
389
393
  },
390
394
  {
391
- titre: 'Styles',
392
- texte: 'Des jetons en source de verite, une feuille statique, aucun scan a l execution.',
395
+ title: 'Styles',
396
+ text: 'Tokens as the source of truth, a static stylesheet, no scanning at runtime.',
393
397
  },
394
398
  ]
395
399
 
396
- /** La section des piliers. */
397
- function Piliers(): ReactElement {
400
+ /** The pillars section. */
401
+ function Pillars(): ReactElement {
398
402
  return (
399
403
  <section
400
- id="piliers"
404
+ id="pillars"
401
405
  className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-20"
402
406
  >
403
407
  <div className="o-mx-auto o-w-full o-max-w-5xl">
404
408
  <h2 className="o-text-sm o-font-semibold o-uppercase o-tracking-widest o-text-zinc-400">
405
- Ce qui est deja la
409
+ What is already here
406
410
  </h2>
407
411
 
408
412
  <Stagger step={90} className="o-mt-8 o-grid o-gap-4 md:o-grid-cols-3">
409
- {PILIERS.map((pilier, index) => (
413
+ {PILLARS.map((pillar, index) => (
410
414
  <article
411
- key={pilier.titre}
415
+ key={pillar.title}
412
416
  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
417
  >
414
418
  <span className="o-font-mono o-text-xs o-text-brand-500">
415
419
  {String(index + 1).padStart(2, '0')}
416
420
  </span>
417
421
  <h3 className="o-mt-3 o-text-lg o-font-semibold o-tracking-tight">
418
- {pilier.titre}
422
+ {pillar.title}
419
423
  </h3>
420
424
  <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
421
- {pilier.texte}
425
+ {pillar.text}
422
426
  </p>
423
427
  </article>
424
428
  ))}
@@ -428,46 +432,46 @@ function Piliers(): ReactElement {
428
432
  )
429
433
  }
430
434
 
431
- /** La commande que l'on copie le plus souvent. */
432
- const COMMANDE = 'odoro add text/count-up'
435
+ /** The command people copy the most. */
436
+ const COMMAND = 'odoro add text/count-up'
433
437
 
434
- /** La cloture : une commande, et la porte vers le catalogue. */
435
- function Cloture(): ReactElement {
436
- const [copie, setCopie] = useState(false)
438
+ /** The closing: one command, and the door to the catalogue. */
439
+ function Closing(): ReactElement {
440
+ const [copied, setCopied] = useState(false)
437
441
 
438
442
  return (
439
443
  <section className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-24">
440
444
  <div className="o-mx-auto o-w-full o-max-w-3xl o-text-center">
441
445
  <Reveal>
442
446
  <h2 className="o-text-3xl o-font-bold o-tracking-tight md:o-text-4xl">
443
- Ajoutez un composant, sans quitter le terminal.
447
+ Add a component without leaving the terminal.
444
448
  </h2>
445
449
  <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.
450
+ The registry copies the code into your project. It is yours: you read it,
451
+ you change it, it never updates behind your back.
448
452
  </p>
449
453
 
450
454
  <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>
455
+ <code className="o-flex-1 o-text-left o-font-mono o-text-sm">{COMMAND}</code>
452
456
  <button
453
457
  type="button"
454
458
  className={buttonClasses({ tone: 'ghost', size: 'sm' })}
455
459
  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.
460
+ // The clipboard can be refused — outside a secure context, or
461
+ // permission withdrawn. The failure does not break the page:
462
+ // the label simply does not change.
459
463
  void navigator.clipboard
460
- ?.writeText(COMMANDE)
464
+ ?.writeText(COMMAND)
461
465
  .then(() => {
462
- setCopie(true)
466
+ setCopied(true)
463
467
  setTimeout(() => {
464
- setCopie(false)
468
+ setCopied(false)
465
469
  }, 1600)
466
470
  })
467
471
  .catch(() => undefined)
468
472
  }}
469
473
  >
470
- {copie ? 'Copie' : 'Copier'}
474
+ {copied ? 'Copied' : 'Copy'}
471
475
  </button>
472
476
  </div>
473
477
 
@@ -477,7 +481,7 @@ function Cloture(): ReactElement {
477
481
  rel="noreferrer"
478
482
  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
483
  >
480
- Parcourir le catalogue
484
+ Browse the catalogue
481
485
  </a>
482
486
  </Reveal>
483
487
  </div>
@@ -489,70 +493,70 @@ function Cloture(): ReactElement {
489
493
  /* Pages */
490
494
  /* -------------------------------------------------------------------------- */
491
495
 
492
- /** La page d'accueil. */
493
- function Accueil(): ReactElement {
496
+ /** The home page. */
497
+ function Home(): ReactElement {
494
498
  return (
495
499
  <>
496
500
  <Hero />
497
- <Piliers />
498
- <Cloture />
501
+ <Pillars />
502
+ <Closing />
499
503
  </>
500
504
  )
501
505
  }
502
506
 
503
507
  /**
504
- * Ce que chaque paquet de la famille apporte.
508
+ * What each package of the family brings.
505
509
  *
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.
510
+ * The table describes; it does not decide. What shows comes from the manifest
511
+ * of the project, hence from what was actually installed — and that stays true
512
+ * if you add or remove a package later.
509
513
  */
510
514
  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',
515
+ odoro: 'Development server, build and registry',
516
+ '@odoro-cli/libs': 'Tokens, utilities, components and router',
517
+ '@odoro-cli/icons': 'Five icon families, importable one by one',
518
+ '@odoro-cli/engine': 'WebGL, surfaces and motion policy',
519
+ '@odoro-cli/server': 'Modular back-end foundation',
516
520
  }
517
521
 
518
522
  /**
519
- * Les paquets de la famille presents dans ce projet.
523
+ * The packages of the family present in this project.
520
524
  *
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.
525
+ * Both fields are read, because `odoro` is a development dependency where the
526
+ * others are production dependencies. A hand-written list would start lying at
527
+ * the first box left unticked at creation.
524
528
  */
525
- const REPERES = Object.entries({ ...dependencies, ...devDependencies })
526
- .filter(([nom]) => nom in ROLES)
529
+ const LANDMARKS = Object.entries({ ...dependencies, ...devDependencies })
530
+ .filter(([name]) => name in ROLES)
527
531
  .sort(([a], [b]) => a.localeCompare(b))
528
- .map(([nom, version]) => ({ nom, version, role: ROLES[nom] ?? '' }))
532
+ .map(([name, version]) => ({ name, version, role: ROLES[name] ?? '' }))
529
533
 
530
- /** Une seconde page, pour montrer le routeur a l'oeuvre. */
531
- function APropos(): ReactElement {
534
+ /** A second page, to show the router at work. */
535
+ function About(): ReactElement {
532
536
  return (
533
537
  <section className="o-px-6 o-py-24">
534
538
  <div className="o-mx-auto o-w-full o-max-w-3xl">
535
539
  <Reveal>
536
- <h1 className="o-text-4xl o-font-bold o-tracking-tight">A propos</h1>
540
+ <h1 className="o-text-4xl o-font-bold o-tracking-tight">About</h1>
537
541
  <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.
542
+ This page exists to show the router: navigating reloads nothing, and the
543
+ transition is the one the browser provides.
540
544
  </p>
541
545
 
542
546
  <dl className="o-mt-10 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
543
- {REPERES.map((repere) => (
547
+ {LANDMARKS.map((landmark) => (
544
548
  <div
545
- key={repere.nom}
549
+ key={landmark.name}
546
550
  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
551
  >
548
552
  <dt className="o-w-56 o-shrink-0 o-font-mono o-text-sm o-font-medium">
549
- {repere.nom}
553
+ {landmark.name}
550
554
  <span className="o-ml-2 o-text-xs o-font-normal o-text-zinc-400">
551
- {repere.version}
555
+ {landmark.version}
552
556
  </span>
553
557
  </dt>
554
558
  <dd className="o-m-0 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
555
- {repere.role}
559
+ {landmark.role}
556
560
  </dd>
557
561
  </div>
558
562
  ))}
@@ -563,59 +567,73 @@ function APropos(): ReactElement {
563
567
  )
564
568
  }
565
569
 
566
- /** Ce qui s'affiche quand aucune route ne correspond. */
567
- function Introuvable(): ReactElement {
570
+ /** What shows when no route matches. */
571
+ function NotFound(): ReactElement {
568
572
  return (
569
573
  <section className="o-px-6 o-py-32 o-text-center">
570
574
  <p className="o-font-mono o-text-sm o-text-brand-500">404</p>
571
575
  <h1 className="o-mt-3 o-text-3xl o-font-bold o-tracking-tight">
572
- Cette page n existe pas.
576
+ This page does not exist.
573
577
  </h1>
574
578
  <Link to="/" className="o-mt-6 o-inline-block o-text-sm">
575
- Retour a l accueil
579
+ Back to home
576
580
  </Link>
577
581
  </section>
578
582
  )
579
583
  }
580
584
 
581
585
  /* -------------------------------------------------------------------------- */
582
- /* Racine */
586
+ /* Root */
583
587
  /* -------------------------------------------------------------------------- */
584
588
 
585
- /** L'enveloppe commune : barre, contenu, pied de page. */
586
- function Coquille(contenu: ReactNode): ReactElement {
589
+ /** The common shell: nav, content, footer. */
590
+ function Shell(content: ReactNode): ReactElement {
587
591
  return (
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
+ // `relative` carries the decorative background, which places itself
593
+ // absolutely inside it. It sits here and not in the page: anchored to the
594
+ // top of `main`, it started below the bar, and the strip left behind it
595
+ // showed at the top of the page as a darker rectangle.
592
596
  <div className="app-shell o-relative">
593
- <Fond />
594
- <Barre />
595
- <main className="o-relative o-view-transition-page">{contenu}</main>
597
+ <Background />
598
+ <Nav />
599
+ <main className="o-relative o-view-transition-page">{content}</main>
596
600
  <footer className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-8">
597
601
  <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">
598
602
  <span className="o-inline-flex o-items-center o-gap-2">
599
603
  <span className="o-text-brand-500">
600
- <Signe taille="1.1rem" />
604
+ <Mark size="1.1rem" />
601
605
  </span>
602
606
  <span className="o-font-semibold o-tracking-wide">ODORO</span>
603
607
  </span>
604
- <span>Construit avec Odoro.</span>
608
+ <span>Built with Odoro.</span>
605
609
  </div>
606
610
  </footer>
607
611
  </div>
608
612
  )
609
613
  }
610
614
 
611
- /** Racine de l'application. */
612
- export function App(): ReactElement {
615
+ /** What the application receives. */
616
+ export interface AppProps {
617
+ /**
618
+ * The address to render, during prerendering.
619
+ *
620
+ * Absent in the browser: there, the address bar is the truth.
621
+ */
622
+ readonly url?: string
623
+ }
624
+
625
+ /** Root of the application. */
626
+ export function App({ url }: AppProps = {}): ReactElement {
613
627
  return (
614
- <Routeur
615
- enveloppe={Coquille}
616
- accueil={<Accueil />}
617
- apropos={<APropos />}
618
- introuvable={<Introuvable />}
628
+ <Router
629
+ shell={Shell}
630
+ home={<Home />}
631
+ about={<About />}
632
+ signIn={<SignIn />}
633
+ register={<Register />}
634
+ profile={<ProfilePage />}
635
+ notFound={<NotFound />}
636
+ url={url}
619
637
  />
620
638
  )
621
639
  }