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,20 +1,20 @@
1
1
  /**
2
- * Toute la page, en un fichier.
2
+ * The whole page, in one file.
3
3
  *
4
- * ## Une seule page
4
+ * ## A single page
5
5
  *
6
- * Le routeur n'a pas ete retenu a la creation : il n'y a donc ni `router.tsx`
7
- * ni navigation entre pages, et les sections se suivent. Le dessin est le meme
8
- * que dans la version routee.
6
+ * The router was not picked at creation: there is therefore no `router.tsx`
7
+ * and no navigation between pages, and the sections simply follow one another.
8
+ * The design is the same as in the routed version.
9
9
  *
10
- * Pour en ajouter un plus tard, `@odoro-cli/libs/router` est deja installe : il
11
- * vient avec les bibliotheques.
10
+ * To add one later, `@odoro-cli/libs/router` is already installed: it comes
11
+ * with the libraries.
12
12
  *
13
- * ## La barre de navigation
13
+ * ## The navigation bar
14
14
  *
15
- * Elle ne peint pas de bandeau : ce sont des gelules posees sur ce qu'il y a
16
- * derriere, avec un flou d'arriere-plan. Le fond decoratif continue donc de
17
- * passer dessous, au lieu d'etre coupe par une bande opaque.
15
+ * It paints no band: these are pills laid over whatever is behind, with a
16
+ * backdrop blur. The decorative background therefore keeps showing through,
17
+ * instead of being cut off by an opaque strip.
18
18
  *
19
19
  * @module
20
20
  */
@@ -23,26 +23,26 @@ import { Reveal, Stagger } from '@odoro-cli/libs/motion'
23
23
  import { buttonClasses } from '@odoro-cli/libs/ui'
24
24
  import { useEffect, useState, type CSSProperties, type ReactElement } from 'react'
25
25
 
26
- import { Fond } from '@/fond'
26
+ import { Background } from '@/background'
27
27
 
28
28
  /* -------------------------------------------------------------------------- */
29
- /* Marque */
29
+ /* Brand */
30
30
  /* -------------------------------------------------------------------------- */
31
31
 
32
32
  /**
33
- * Le signe de la marque.
33
+ * The brand mark.
34
34
  *
35
- * La taille est en style et non en classe : une classe utilitaire a valeur
36
- * arbitraire n'est emise que si le compilateur l'a vue passer, et une classe
37
- * absente ne peint rien — le signe serait sans dimensions, donc invisible.
35
+ * The size is a style and not a class: an arbitrary-value utility class is
36
+ * only emitted if the compiler saw it go by, and a missing class paints
37
+ * nothing — the mark would have no dimensions, so it would be invisible.
38
38
  */
39
- function Signe({ taille = '1.75rem' }: { readonly taille?: string }): ReactElement {
39
+ function Mark({ size = '1.75rem' }: { readonly size?: string }): ReactElement {
40
40
  return (
41
41
  <svg
42
42
  viewBox="0 0 100 100"
43
43
  fill="none"
44
44
  aria-hidden="true"
45
- style={{ width: taille, height: taille, flexShrink: 0 }}
45
+ style={{ width: size, height: size, flexShrink: 0 }}
46
46
  >
47
47
  <path
48
48
  d="M20.25 20.25H50a29.75 29.75 0 1 1-29.75 29.75Z"
@@ -55,32 +55,32 @@ function Signe({ taille = '1.75rem' }: { readonly taille?: string }): ReactEleme
55
55
  }
56
56
 
57
57
  /* -------------------------------------------------------------------------- */
58
- /* Barre de navigation */
58
+ /* Navigation bar */
59
59
  /* -------------------------------------------------------------------------- */
60
60
 
61
61
  /**
62
- * La surface d'une gelule.
62
+ * The surface of a pill.
63
63
  *
64
- * Le systeme decline les echelles de noir et de blanc, mais pas leurs variantes
65
- * sombres en alpha. Le melange se fait donc en style, ou il est exact et suit
66
- * le theme sans qu'on ait a redire chaque couleur.
64
+ * The system ships the black and white scales, but not their dark alpha
65
+ * variants. So the mix is done as a style, where it is exact and follows the
66
+ * theme without every colour having to be restated.
67
67
  */
68
- const GELULE: CSSProperties = {
68
+ const PILL: CSSProperties = {
69
69
  backgroundColor: 'color-mix(in oklab, var(--o-theme-bg) 72%, transparent)',
70
70
  borderColor: 'color-mix(in oklab, var(--o-theme-fg) 12%, transparent)',
71
71
  }
72
72
 
73
- /** La barre : la marque, et le lien vers le site. */
74
- function Barre(): ReactElement {
73
+ /** The bar: the brand, and the link to the site. */
74
+ function Nav(): ReactElement {
75
75
  return (
76
76
  <header className="o-sticky o-top-0 o-z-20 o-px-5 md:o-px-8">
77
77
  <div className="o-mx-auto o-flex o-h-20 o-w-full o-max-w-5xl o-items-center o-gap-3">
78
78
  <span
79
79
  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-backdrop-blur-xl"
80
- style={GELULE}
80
+ style={PILL}
81
81
  >
82
82
  <span className="o-text-brand-500">
83
- <Signe />
83
+ <Mark />
84
84
  </span>
85
85
  <span className="o-text-lg o-font-bold o-tracking-wide o-text-zinc-950 dark:o-text-white">
86
86
  ODORO
@@ -93,11 +93,11 @@ function Barre(): ReactElement {
93
93
  target="_blank"
94
94
  rel="noreferrer"
95
95
  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"
96
- style={GELULE}
96
+ style={PILL}
97
97
  >
98
98
  odoro.dev
99
99
  </a>
100
- <BasculeTheme />
100
+ <ThemeToggle />
101
101
  </div>
102
102
  </div>
103
103
  </header>
@@ -108,45 +108,45 @@ function Barre(): ReactElement {
108
108
  /* Theme */
109
109
  /* -------------------------------------------------------------------------- */
110
110
 
111
- /** Les trois etats de la bascule. */
112
- type Theme = 'systeme' | 'clair' | 'sombre'
111
+ /** The three states of the toggle. */
112
+ type Theme = 'system' | 'light' | 'dark'
113
113
 
114
- /** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
115
- const CLE_THEME = 'odoro-theme'
114
+ /** Where the choice is stored. The script in `index.html` reads the same key. */
115
+ const THEME_KEY = 'odoro-theme'
116
116
 
117
- /** L'ordre du cycle, au clic. */
118
- const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
117
+ /** The order of the cycle, on click. */
118
+ const CYCLE: readonly Theme[] = ['system', 'light', 'dark']
119
119
 
120
120
  /**
121
- * Pose le theme sur la racine du document.
121
+ * Applies the theme to the root of the document.
122
122
  *
123
- * `systeme` retire l'attribut plutot que d'en poser un troisieme : la feuille
124
- * de style ecoute `data-theme`, et son absence rend la main a la preference du
125
- * navigateur. C'est ce qui permet a une page laissee en « systeme » de suivre
126
- * le passage en nuit sans etre rouverte.
123
+ * `system` removes the attribute rather than setting a third value: the
124
+ * stylesheet listens to `data-theme`, and its absence hands control back to
125
+ * the browser preference. That is what lets a page left on "system" follow the
126
+ * switch to night without being reopened.
127
127
  */
128
- function poserTheme(theme: Theme): void {
129
- if (theme === 'systeme') delete document.documentElement.dataset.theme
130
- else document.documentElement.dataset.theme = theme === 'clair' ? 'light' : 'dark'
128
+ function applyTheme(theme: Theme): void {
129
+ if (theme === 'system') delete document.documentElement.dataset.theme
130
+ else document.documentElement.dataset.theme = theme
131
131
  }
132
132
 
133
- /** Le theme memorise, ou `systeme`. */
134
- function themeMemorise(): Theme {
133
+ /** The stored theme, or `system`. */
134
+ function storedTheme(): Theme {
135
135
  try {
136
- const valeur = localStorage.getItem(CLE_THEME)
137
- if (valeur === 'light') return 'clair'
138
- if (valeur === 'dark') return 'sombre'
139
- return 'systeme'
136
+ const value = localStorage.getItem(THEME_KEY)
137
+ if (value === 'light') return 'light'
138
+ if (value === 'dark') return 'dark'
139
+ return 'system'
140
140
  } catch {
141
- // Stockage refuse — fenetre privee, cookies bloques. Le theme du systeme
142
- // reste une reponse valable ; echouer ici priverait la page de son rendu.
143
- return 'systeme'
141
+ // Storage refused — private window, cookies blocked. The system theme is
142
+ // still a valid answer; failing here would deprive the page of its render.
143
+ return 'system'
144
144
  }
145
145
  }
146
146
 
147
- /** Les trois pictogrammes, dessines plutot qu'importes : les icones sont optionnelles. */
148
- function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
149
- const commun = {
147
+ /** The three glyphs, drawn rather than imported: the icons are optional. */
148
+ function Glyph({ theme }: { readonly theme: Theme }): ReactElement {
149
+ const common = {
150
150
  viewBox: '0 0 24 24',
151
151
  fill: 'none',
152
152
  stroke: 'currentColor',
@@ -157,25 +157,25 @@ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
157
157
  style: { width: '1.15rem', height: '1.15rem' },
158
158
  }
159
159
 
160
- if (theme === 'clair') {
160
+ if (theme === 'light') {
161
161
  return (
162
- <svg {...commun}>
162
+ <svg {...common}>
163
163
  <circle cx="12" cy="12" r="4" />
164
164
  <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" />
165
165
  </svg>
166
166
  )
167
167
  }
168
168
 
169
- if (theme === 'sombre') {
169
+ if (theme === 'dark') {
170
170
  return (
171
- <svg {...commun}>
171
+ <svg {...common}>
172
172
  <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
173
173
  </svg>
174
174
  )
175
175
  }
176
176
 
177
177
  return (
178
- <svg {...commun}>
178
+ <svg {...common}>
179
179
  <rect x="2" y="4" width="20" height="13" rx="2" />
180
180
  <path d="M8 21h8M12 17v4" />
181
181
  </svg>
@@ -183,37 +183,37 @@ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
183
183
  }
184
184
 
185
185
  /**
186
- * La bascule de theme.
186
+ * The theme toggle.
187
187
  *
188
- * Trois etats et non deux : « systeme » est un choix a part entiere, et le
189
- * retirer obligerait un visiteur a re-choisir a chaque changement de sa
190
- * preference.
188
+ * Three states and not two: "system" is a choice in its own right, and
189
+ * removing it would force a visitor to choose again every time their
190
+ * preference changes.
191
191
  */
192
- function BasculeTheme(): ReactElement {
193
- const [theme, setTheme] = useState<Theme>(() => themeMemorise())
192
+ function ThemeToggle(): ReactElement {
193
+ const [theme, setTheme] = useState<Theme>(() => storedTheme())
194
194
 
195
195
  useEffect(() => {
196
- poserTheme(theme)
196
+ applyTheme(theme)
197
197
  try {
198
- if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
199
- else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
198
+ if (theme === 'system') localStorage.removeItem(THEME_KEY)
199
+ else localStorage.setItem(THEME_KEY, theme)
200
200
  } catch {
201
- // Le theme reste applique pour la session, meme sans stockage.
201
+ // The theme stays applied for the session, even without storage.
202
202
  }
203
203
  }, [theme])
204
204
 
205
205
  return (
206
206
  <button
207
207
  type="button"
208
- aria-label={`Theme : ${theme}`}
209
- title={`Theme : ${theme}`}
208
+ aria-label={`Theme: ${theme}`}
209
+ title={`Theme: ${theme}`}
210
210
  onClick={() => {
211
- setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
211
+ setTheme((current) => CYCLE[(CYCLE.indexOf(current) + 1) % CYCLE.length] ?? 'system')
212
212
  }}
213
213
  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"
214
- style={GELULE}
214
+ style={PILL}
215
215
  >
216
- <Pictogramme theme={theme} />
216
+ <Glyph theme={theme} />
217
217
  </button>
218
218
  )
219
219
  }
@@ -222,22 +222,22 @@ function BasculeTheme(): ReactElement {
222
222
  /* Sections */
223
223
  /* -------------------------------------------------------------------------- */
224
224
 
225
- /** Trois faits, pas trois slogans. */
226
- const FAITS = ['Zero configuration', 'Theme clair et sombre', 'Mouvement reduit respecte']
225
+ /** Three facts, not three slogans. */
226
+ const FACTS = ['Zero configuration', 'Light and dark themes', 'Reduced motion respected']
227
227
 
228
- /** Ce que la fenetre du produit montre. */
229
- const EXTRAIT = `export function App() {
228
+ /** What the product window shows. */
229
+ const SNIPPET = `export function App() {
230
230
  return (
231
231
  <Reveal>
232
232
  <h1 className="o-text-6xl o-font-bold">
233
- Votre premiere page.
233
+ Your first page.
234
234
  </h1>
235
235
  </Reveal>
236
236
  )
237
237
  }`
238
238
 
239
- /** Une fenetre de navigateur miniature. */
240
- function Fenetre(): ReactElement {
239
+ /** A miniature browser window. */
240
+ function Window(): ReactElement {
241
241
  return (
242
242
  <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">
243
243
  <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">
@@ -249,39 +249,39 @@ function Fenetre(): ReactElement {
249
249
  <span className="o-ml-2 o-font-mono o-text-xs o-text-zinc-400">src/App.tsx</span>
250
250
  </div>
251
251
  <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">
252
- {EXTRAIT}
252
+ {SNIPPET}
253
253
  </pre>
254
254
  </div>
255
255
  )
256
256
  }
257
257
 
258
- /** Le hero : la promesse, les actions, et la fenetre du produit. */
258
+ /** The hero: the promise, the actions, and the product window. */
259
259
  function Hero(): ReactElement {
260
260
  return (
261
261
  <section className="o-relative o-px-6 o-pb-20 o-pt-16 md:o-pt-24">
262
262
  <div className="o-mx-auto o-w-full o-max-w-5xl">
263
263
  <Reveal>
264
264
  <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">
265
- <Signe taille="0.9rem" />
266
- Genere par odoro create
265
+ <Mark size="0.9rem" />
266
+ Generated by odoro create
267
267
  </span>
268
268
 
269
269
  <h1 className="o-mt-6 o-max-w-3xl o-text-5xl o-font-bold o-tracking-tight md:o-text-6xl">
270
- Votre premiere page, <span className="o-text-brand-500">deja vivante</span>.
270
+ Your first page, <span className="o-text-brand-500">already alive</span>.
271
271
  </h1>
272
272
 
273
273
  <p className="o-mt-5 o-max-w-xl o-text-lg o-text-zinc-500 dark:o-text-zinc-400">
274
- Le moteur d animation et le systeme de style viennent de la meme librairie.
275
- Modifiez cette page : elle se recharge sans perdre son etat.
274
+ The motion engine and the style system come from the same library.
275
+ Edit this page: it reloads without losing its state.
276
276
  </p>
277
277
  </Reveal>
278
278
 
279
279
  <Stagger step={70} className="o-mt-8 o-flex o-flex-wrap o-items-center o-gap-3">
280
280
  <a
281
- href="#piliers"
281
+ href="#pillars"
282
282
  className={`o-no-underline ${buttonClasses({ tone: 'primary' })}`}
283
283
  >
284
- Voir ce qui est inclus
284
+ See what is included
285
285
  </a>
286
286
  <a
287
287
  href="https://odoro.dev/docs"
@@ -294,19 +294,19 @@ function Hero(): ReactElement {
294
294
  </Stagger>
295
295
 
296
296
  <Stagger step={60} className="o-mt-6 o-flex o-flex-wrap o-gap-2">
297
- {FAITS.map((fait) => (
297
+ {FACTS.map((fact) => (
298
298
  <span
299
- key={fait}
299
+ key={fact}
300
300
  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"
301
301
  >
302
- {fait}
302
+ {fact}
303
303
  </span>
304
304
  ))}
305
305
  </Stagger>
306
306
 
307
307
  <Reveal>
308
308
  <div className="o-mt-14">
309
- <Fenetre />
309
+ <Window />
310
310
  </div>
311
311
  </Reveal>
312
312
  </div>
@@ -314,49 +314,49 @@ function Hero(): ReactElement {
314
314
  )
315
315
  }
316
316
 
317
- /** Ce que le projet a deja sous la main. */
318
- const PILIERS = [
317
+ /** What the project already has at hand. */
318
+ const PILLARS = [
319
319
  {
320
- titre: 'Animations',
321
- texte:
322
- "Une couche mince sur le moteur du navigateur. Le mouvement reduit est respecte d'office.",
320
+ title: 'Animations',
321
+ text:
322
+ "A thin layer over the browser's own engine. Reduced motion is respected out of the box.",
323
323
  },
324
324
  {
325
- titre: 'Styles',
326
- texte: 'Des jetons en source de verite, une feuille statique, aucun scan a l execution.',
325
+ title: 'Styles',
326
+ text: 'Tokens as the source of truth, a static stylesheet, no scanning at runtime.',
327
327
  },
328
328
  {
329
- titre: 'Interface',
330
- texte: 'Des composants qui lisent les memes jetons que votre code : les rethemer suffit.',
329
+ title: 'Interface',
330
+ text: 'Components that read the same tokens as your code: rethemed, they follow.',
331
331
  },
332
332
  ]
333
333
 
334
- /** La section des piliers. */
335
- function Piliers(): ReactElement {
334
+ /** The pillars section. */
335
+ function Pillars(): ReactElement {
336
336
  return (
337
337
  <section
338
- id="piliers"
338
+ id="pillars"
339
339
  className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-20"
340
340
  >
341
341
  <div className="o-mx-auto o-w-full o-max-w-5xl">
342
342
  <h2 className="o-text-sm o-font-semibold o-uppercase o-tracking-widest o-text-zinc-400">
343
- Ce qui est deja la
343
+ What is already here
344
344
  </h2>
345
345
 
346
346
  <Stagger step={90} className="o-mt-8 o-grid o-gap-4 md:o-grid-cols-3">
347
- {PILIERS.map((pilier, index) => (
347
+ {PILLARS.map((pillar, index) => (
348
348
  <article
349
- key={pilier.titre}
349
+ key={pillar.title}
350
350
  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"
351
351
  >
352
352
  <span className="o-font-mono o-text-xs o-text-brand-500">
353
353
  {String(index + 1).padStart(2, '0')}
354
354
  </span>
355
355
  <h3 className="o-mt-3 o-text-lg o-font-semibold o-tracking-tight">
356
- {pilier.titre}
356
+ {pillar.title}
357
357
  </h3>
358
358
  <p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
359
- {pilier.texte}
359
+ {pillar.text}
360
360
  </p>
361
361
  </article>
362
362
  ))}
@@ -366,45 +366,45 @@ function Piliers(): ReactElement {
366
366
  )
367
367
  }
368
368
 
369
- /** La commande que l'on copie le plus souvent. */
370
- const COMMANDE = 'odoro add text/count-up'
369
+ /** The command people copy the most. */
370
+ const COMMAND = 'odoro add text/count-up'
371
371
 
372
- /** La cloture : une commande, et la porte vers le catalogue. */
373
- function Cloture(): ReactElement {
374
- const [copie, setCopie] = useState(false)
372
+ /** The closing: one command, and the door to the catalogue. */
373
+ function Closing(): ReactElement {
374
+ const [copied, setCopied] = useState(false)
375
375
 
376
376
  return (
377
377
  <section className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-24">
378
378
  <div className="o-mx-auto o-w-full o-max-w-3xl o-text-center">
379
379
  <Reveal>
380
380
  <h2 className="o-text-3xl o-font-bold o-tracking-tight md:o-text-4xl">
381
- Ajoutez un composant, sans quitter le terminal.
381
+ Add a component without leaving the terminal.
382
382
  </h2>
383
383
  <p className="o-mx-auto o-mt-4 o-max-w-prose o-text-zinc-500 dark:o-text-zinc-400">
384
- Le registre copie le code dans votre projet. Il vous appartient : on le lit,
385
- on le modifie, il ne se met pas a jour dans votre dos.
384
+ The registry copies the code into your project. It is yours: you read it,
385
+ you change it, it never updates behind your back.
386
386
  </p>
387
387
 
388
388
  <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">
389
- <code className="o-flex-1 o-text-left o-font-mono o-text-sm">{COMMANDE}</code>
389
+ <code className="o-flex-1 o-text-left o-font-mono o-text-sm">{COMMAND}</code>
390
390
  <button
391
391
  type="button"
392
392
  className={buttonClasses({ tone: 'ghost', size: 'sm' })}
393
393
  onClick={() => {
394
- // Le presse-papiers peut etre refuse — hors contexte sur, ou
395
- // permission retiree. L'echec ne casse pas la page.
394
+ // The clipboard can be refused — outside a secure context, or
395
+ // permission withdrawn. The failure does not break the page.
396
396
  void navigator.clipboard
397
- ?.writeText(COMMANDE)
397
+ ?.writeText(COMMAND)
398
398
  .then(() => {
399
- setCopie(true)
399
+ setCopied(true)
400
400
  setTimeout(() => {
401
- setCopie(false)
401
+ setCopied(false)
402
402
  }, 1600)
403
403
  })
404
404
  .catch(() => undefined)
405
405
  }}
406
406
  >
407
- {copie ? 'Copie' : 'Copier'}
407
+ {copied ? 'Copied' : 'Copy'}
408
408
  </button>
409
409
  </div>
410
410
 
@@ -414,7 +414,7 @@ function Cloture(): ReactElement {
414
414
  rel="noreferrer"
415
415
  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"
416
416
  >
417
- Parcourir le catalogue
417
+ Browse the catalogue
418
418
  </a>
419
419
  </Reveal>
420
420
  </div>
@@ -423,32 +423,32 @@ function Cloture(): ReactElement {
423
423
  }
424
424
 
425
425
  /* -------------------------------------------------------------------------- */
426
- /* Racine */
426
+ /* Root */
427
427
  /* -------------------------------------------------------------------------- */
428
428
 
429
- /** Racine de l'application. */
429
+ /** Root of the application. */
430
430
  export function App(): ReactElement {
431
431
  return (
432
- // `relative` porte le fond, pose ici et non dans le contenu : ancre plus
433
- // bas, il commencait sous la barre et laissait une bande plus sombre
434
- // derriere elle en haut de page.
432
+ // `relative` carries the background, placed here and not in the content:
433
+ // anchored lower, it started below the bar and left a darker strip behind
434
+ // it at the top of the page.
435
435
  <div className="app-shell o-relative">
436
- <Fond />
437
- <Barre />
436
+ <Background />
437
+ <Nav />
438
438
  <main className="o-relative">
439
439
  <Hero />
440
- <Piliers />
441
- <Cloture />
440
+ <Pillars />
441
+ <Closing />
442
442
  </main>
443
443
  <footer className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-8">
444
444
  <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">
445
445
  <span className="o-inline-flex o-items-center o-gap-2">
446
446
  <span className="o-text-brand-500">
447
- <Signe taille="1.1rem" />
447
+ <Mark size="1.1rem" />
448
448
  </span>
449
449
  <span className="o-font-semibold o-tracking-wide">ODORO</span>
450
450
  </span>
451
- <span>Construit avec Odoro.</span>
451
+ <span>Built with Odoro.</span>
452
452
  </div>
453
453
  </footer>
454
454
  </div>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The prerendering entry point.
3
+ *
4
+ * Without a router there is only one page: the list of routes comes down to
5
+ * the root. The rest is identical — `odoro build` renders this HTML at build
6
+ * time, and `src/main.tsx` hydrates it on load.
7
+ *
8
+ * To do without: remove `prerender` from `odoro.config.ts`, then this file.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import { StrictMode } from 'react'
14
+ import { renderToString } from 'react-dom/server'
15
+
16
+ import { App } from '@/App'
17
+
18
+ /** The routes rendered at build time. */
19
+ export const routes = ['/']
20
+
21
+ /**
22
+ * Renders the page.
23
+ *
24
+ * @returns The HTML of the container, and the tags to add to the head.
25
+ */
26
+ export function render(): { html: string; head: string } {
27
+ return {
28
+ // `StrictMode` on both sides: a tree rendered without it here and with it
29
+ // in the browser does not always produce the same markup, and hydration
30
+ // would report a difference that is not one.
31
+ html: renderToString(
32
+ <StrictMode>
33
+ <App />
34
+ </StrictMode>,
35
+ ),
36
+ head:
37
+ '<meta name="description" content="An Odoro project: in-house engine, generated styles.">',
38
+ }
39
+ }
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="fr">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -7,9 +7,9 @@
7
7
  <meta name="theme-color" content="#3b82f6" />
8
8
  <title>Odoro</title>
9
9
  <script>
10
- // Applique le theme memorise avant la premiere peinture. Le faire depuis
11
- // React arriverait trop tard : un visiteur en theme force verrait un
12
- // eclair de l'autre theme, le temps que le script se charge.
10
+ // Applies the stored theme before the first paint. Doing it from React
11
+ // would come too late: a visitor on a forced theme would see a flash of
12
+ // the other one, for as long as the script takes to load.
13
13
  try {
14
14
  var theme = localStorage.getItem('odoro-theme')
15
15
  if (theme === 'light' || theme === 'dark')