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