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
@@ -0,0 +1,118 @@
1
+ /**
2
+ * The decorative background, as a WebGL surface.
3
+ *
4
+ * This version replaces the static gradient because the engine was picked at
5
+ * creation. The rest of the page is unchanged: it places `<Background />` and
6
+ * does not know what is behind it.
7
+ *
8
+ * ## What the engine brings that a gradient cannot
9
+ *
10
+ * **An arbiter.** The surface is asked of the engine manager, which refuses it
11
+ * if the browser has no WebGL, if too many contexts are already open, or if
12
+ * the device would not keep up. The refusal is a value, not an exception: the
13
+ * gradient is shown instead, and the page holds.
14
+ *
15
+ * **Reduced motion.** Under that preference nothing is mounted: the background
16
+ * brought only its motion, and the graphics card has no business waking up for
17
+ * a visitor who asked for calm.
18
+ *
19
+ * **The theme colours.** The hues are read from the tokens, not written into
20
+ * the shader. A light / dark switch re-reads them, and changing
21
+ * `--o-palette-brand-500` repaints the background without touching this file.
22
+ *
23
+ * ## Why inline styles
24
+ *
25
+ * This file is laid down whatever the rest: a project that unticked the
26
+ * libraries has no `o-*` classes, and a missing class paints nothing — the
27
+ * background would then be zero pixels tall, with nothing to report it. So the
28
+ * few placement rules are written where they are safe.
29
+ *
30
+ * @module
31
+ */
32
+
33
+ import {
34
+ AURORA_FRAGMENT,
35
+ readTokenColour,
36
+ useMotionState,
37
+ useShaderSurface,
38
+ type ShaderColour,
39
+ } from '@odoro-cli/engine'
40
+ import { useEffect, useMemo, useState, type ReactElement } from 'react'
41
+
42
+ /** The tokens read to tint the glow. */
43
+ const HUES = ['--o-palette-brand-500', '--o-palette-brand-300', '--o-theme-bg'] as const
44
+
45
+ /** The gradient served while the surface is not there — or forever. */
46
+ function Gradient(): ReactElement {
47
+ return (
48
+ <div
49
+ style={{
50
+ position: 'absolute',
51
+ inset: 0,
52
+ opacity: 0.28,
53
+ filter: 'blur(64px)',
54
+ background:
55
+ 'radial-gradient(60% 60% at 50% 0%, var(--o-palette-brand-500, #3b82f6), transparent)',
56
+ }}
57
+ />
58
+ )
59
+ }
60
+
61
+ /** An animated glow behind the top of the page. */
62
+ export function Background(): ReactElement {
63
+ const { quality, reduced } = useMotionState()
64
+ const [host, setHost] = useState<HTMLElement | null>(null)
65
+ const [hues, setHues] = useState<readonly ShaderColour[]>([])
66
+
67
+ // Re-read when the host appears and when the theme switches — the latter
68
+ // goes through the motion policy, which is renewed on every state.
69
+ useEffect(() => {
70
+ if (host === null) return
71
+ setHues(HUES.map((token) => readTokenColour(token, host)))
72
+ }, [host, reduced, quality])
73
+
74
+ const uniforms = useMemo(() => {
75
+ const [a, b, c] = hues
76
+ if (a === undefined || b === undefined || c === undefined) return undefined
77
+ // The theme background dominates, the brand is only a reflection: these
78
+ // three colours go into a mix, and two brand hues out of three would give
79
+ // a full-page glow rather than a halo.
80
+ return { uColorA: c, uColorB: c, uColorC: a, uSpeed: 0.1, uScale: 1.4, uOctaves: 3 }
81
+ }, [hues])
82
+
83
+ const { ref, ready, refused } = useShaderSurface<HTMLDivElement>({
84
+ fragment: AURORA_FRAGMENT,
85
+ uniforms: uniforms ?? {},
86
+ name: 'home-background',
87
+ })
88
+
89
+ const fallback = !ready || refused !== undefined || uniforms === undefined
90
+
91
+ return (
92
+ <div
93
+ aria-hidden="true"
94
+ ref={(element) => {
95
+ setHost(element)
96
+ ref.current = element
97
+ }}
98
+ style={{
99
+ position: 'absolute',
100
+ insetInline: 0,
101
+ top: 0,
102
+ height: '42rem',
103
+ overflow: 'hidden',
104
+ pointerEvents: 'none',
105
+ // A decorative background is noticed when you look for it, not before.
106
+ // At full strength the glow comes in front of the title it is supposed
107
+ // to carry; at a fifth, it underlines it.
108
+ opacity: 0.2,
109
+ // The bottom dissolves into the page: a surface that stops dead draws
110
+ // a horizontal line that nothing justifies.
111
+ maskImage: 'linear-gradient(to bottom, black 45%, transparent)',
112
+ WebkitMaskImage: 'linear-gradient(to bottom, black 45%, transparent)',
113
+ }}
114
+ >
115
+ {fallback ? <Gradient /> : null}
116
+ </div>
117
+ )
118
+ }
@@ -0,0 +1,380 @@
1
+ /**
2
+ * The whole page, in one file.
3
+ *
4
+ * ## Without the libraries
5
+ *
6
+ * They were not picked at creation: no `o-*` classes, no tokens, no router.
7
+ * The design is that of the full version — same pill bar, same title, same
8
+ * cards — carried by `styles.css` in plain CSS.
9
+ *
10
+ * To add them later: `npm i @odoro-cli/libs`, then import
11
+ * `@odoro-cli/libs/styles.css` in `main.tsx` before this stylesheet.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import { useEffect, useState, type ReactElement } from 'react'
17
+
18
+ import { Background } from '@/background'
19
+
20
+ /* -------------------------------------------------------------------------- */
21
+ /* Brand */
22
+ /* -------------------------------------------------------------------------- */
23
+
24
+ /** The brand mark, at the requested size. */
25
+ function Mark({ size = '1.75rem' }: { readonly size?: string }): ReactElement {
26
+ return (
27
+ <svg
28
+ viewBox="0 0 100 100"
29
+ fill="none"
30
+ aria-hidden="true"
31
+ style={{ width: size, height: size, flexShrink: 0 }}
32
+ >
33
+ <path
34
+ d="M20.25 20.25H50a29.75 29.75 0 1 1-29.75 29.75Z"
35
+ stroke="currentColor"
36
+ strokeWidth="10.5"
37
+ strokeLinejoin="miter"
38
+ />
39
+ </svg>
40
+ )
41
+ }
42
+
43
+ /* -------------------------------------------------------------------------- */
44
+ /* Navigation bar */
45
+ /* -------------------------------------------------------------------------- */
46
+
47
+ /** The bar: the brand in a pill, and the link to the site. */
48
+ function Nav(): ReactElement {
49
+ return (
50
+ <header className="nav">
51
+ <div className="nav-content">
52
+ <span className="pill brand">
53
+ <span className="brand-mark">
54
+ <Mark />
55
+ </span>
56
+ <span className="brand-word">ODORO</span>
57
+ </span>
58
+
59
+ <div className="nav-end">
60
+ <a
61
+ href="https://odoro.dev"
62
+ target="_blank"
63
+ rel="noreferrer"
64
+ className="pill pill-link"
65
+ >
66
+ odoro.dev
67
+ </a>
68
+ <ThemeToggle />
69
+ </div>
70
+ </div>
71
+ </header>
72
+ )
73
+ }
74
+
75
+ /* -------------------------------------------------------------------------- */
76
+ /* Theme */
77
+ /* -------------------------------------------------------------------------- */
78
+
79
+ /** The three states of the toggle. */
80
+ type Theme = 'system' | 'light' | 'dark'
81
+
82
+ /** Where the choice is stored. The script in `index.html` reads the same key. */
83
+ const THEME_KEY = 'odoro-theme'
84
+
85
+ /** The order of the cycle, on click. */
86
+ const CYCLE: readonly Theme[] = ['system', 'light', 'dark']
87
+
88
+ /**
89
+ * Applies the theme to the root of the document.
90
+ *
91
+ * `system` removes the attribute rather than setting a third value: the
92
+ * stylesheet listens to `data-theme`, and its absence hands control back to
93
+ * the browser preference.
94
+ */
95
+ function applyTheme(theme: Theme): void {
96
+ if (theme === 'system') delete document.documentElement.dataset.theme
97
+ else document.documentElement.dataset.theme = theme
98
+ }
99
+
100
+ /** The stored theme, or `system`. */
101
+ function storedTheme(): Theme {
102
+ try {
103
+ const value = localStorage.getItem(THEME_KEY)
104
+ if (value === 'light') return 'light'
105
+ if (value === 'dark') return 'dark'
106
+ return 'system'
107
+ } catch {
108
+ // Storage refused — private window, cookies blocked. The system theme is
109
+ // still a valid answer.
110
+ return 'system'
111
+ }
112
+ }
113
+
114
+ /** The three glyphs, drawn inline. */
115
+ function Glyph({ theme }: { readonly theme: Theme }): ReactElement {
116
+ const common = {
117
+ viewBox: '0 0 24 24',
118
+ fill: 'none',
119
+ stroke: 'currentColor',
120
+ strokeWidth: 1.8,
121
+ strokeLinecap: 'round' as const,
122
+ strokeLinejoin: 'round' as const,
123
+ 'aria-hidden': true,
124
+ style: { width: '1.15rem', height: '1.15rem' },
125
+ }
126
+
127
+ if (theme === 'light') {
128
+ return (
129
+ <svg {...common}>
130
+ <circle cx="12" cy="12" r="4" />
131
+ <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" />
132
+ </svg>
133
+ )
134
+ }
135
+
136
+ if (theme === 'dark') {
137
+ return (
138
+ <svg {...common}>
139
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
140
+ </svg>
141
+ )
142
+ }
143
+
144
+ return (
145
+ <svg {...common}>
146
+ <rect x="2" y="4" width="20" height="13" rx="2" />
147
+ <path d="M8 21h8M12 17v4" />
148
+ </svg>
149
+ )
150
+ }
151
+
152
+ /** The theme toggle: system, light, dark. */
153
+ function ThemeToggle(): ReactElement {
154
+ const [theme, setTheme] = useState<Theme>(() => storedTheme())
155
+
156
+ useEffect(() => {
157
+ applyTheme(theme)
158
+ try {
159
+ if (theme === 'system') localStorage.removeItem(THEME_KEY)
160
+ else localStorage.setItem(THEME_KEY, theme)
161
+ } catch {
162
+ // The theme stays applied for the session, even without storage.
163
+ }
164
+ }, [theme])
165
+
166
+ return (
167
+ <button
168
+ type="button"
169
+ aria-label={`Theme: ${theme}`}
170
+ title={`Theme: ${theme}`}
171
+ className="pill toggle"
172
+ onClick={() => {
173
+ setTheme((current) => CYCLE[(CYCLE.indexOf(current) + 1) % CYCLE.length] ?? 'system')
174
+ }}
175
+ >
176
+ <Glyph theme={theme} />
177
+ </button>
178
+ )
179
+ }
180
+
181
+ /* -------------------------------------------------------------------------- */
182
+ /* Sections */
183
+ /* -------------------------------------------------------------------------- */
184
+
185
+ /** Three facts, not three slogans. */
186
+ const FACTS = ['Zero configuration', 'Strict TypeScript', 'Hot reloading']
187
+
188
+ /** What the product window shows. */
189
+ const SNIPPET = `export function App() {
190
+ return (
191
+ <h1 className="hero-title">
192
+ Your first page.
193
+ </h1>
194
+ )
195
+ }`
196
+
197
+ /** A miniature browser window. */
198
+ function Window(): ReactElement {
199
+ return (
200
+ <div className="window">
201
+ <div className="window-bar">
202
+ <span className="window-dots" aria-hidden="true">
203
+ <span />
204
+ <span />
205
+ <span />
206
+ </span>
207
+ <span className="window-path">src/App.tsx</span>
208
+ </div>
209
+ <pre className="window-code">{SNIPPET}</pre>
210
+ </div>
211
+ )
212
+ }
213
+
214
+ /** The hero: the promise, the actions, and the product window. */
215
+ function Hero(): ReactElement {
216
+ return (
217
+ <section className="hero">
218
+ <div className="content">
219
+ <span className="brand-dot">
220
+ <Mark size="0.9rem" />
221
+ Generated by odoro create
222
+ </span>
223
+
224
+ <h1 className="hero-title">
225
+ Your first page, <span className="accent">already alive</span>.
226
+ </h1>
227
+
228
+ <p className="hero-eyebrow">
229
+ The development server, the build and the hot reloading all come from{' '}
230
+ <code>odoro</code>. The style system is yours.
231
+ </p>
232
+
233
+ <div className="hero-actions">
234
+ <a href="#pillars" className="button button-primary">
235
+ See what is included
236
+ </a>
237
+ <a
238
+ href="https://odoro.dev/docs"
239
+ target="_blank"
240
+ rel="noreferrer"
241
+ className="button button-secondary"
242
+ >
243
+ Documentation
244
+ </a>
245
+ </div>
246
+
247
+ <div className="facts">
248
+ {FACTS.map((fact) => (
249
+ <span key={fact} className="fact">
250
+ {fact}
251
+ </span>
252
+ ))}
253
+ </div>
254
+
255
+ <div className="hero-window">
256
+ <Window />
257
+ </div>
258
+ </div>
259
+ </section>
260
+ )
261
+ }
262
+
263
+ /** What the project already has at hand. */
264
+ const PILLARS = [
265
+ {
266
+ title: 'Build',
267
+ text: 'A development server with hot reloading, and a production build.',
268
+ },
269
+ {
270
+ title: 'TypeScript',
271
+ text: 'Strict types, path aliases, and checking without emitting.',
272
+ },
273
+ {
274
+ title: 'Over to you',
275
+ text: 'No style system imposed: yours takes all the room.',
276
+ },
277
+ ]
278
+
279
+ /** The pillars section. */
280
+ function Pillars(): ReactElement {
281
+ return (
282
+ <section id="pillars" className="section">
283
+ <div className="content">
284
+ <h2 className="overline">What is already here</h2>
285
+ <div className="grid">
286
+ {PILLARS.map((pillar, index) => (
287
+ <article key={pillar.title} className="card">
288
+ <span className="card-number">{String(index + 1).padStart(2, '0')}</span>
289
+ <h3>{pillar.title}</h3>
290
+ <p>{pillar.text}</p>
291
+ </article>
292
+ ))}
293
+ </div>
294
+ </div>
295
+ </section>
296
+ )
297
+ }
298
+
299
+ /** The command people copy the most. */
300
+ const COMMAND = 'npm i @odoro-cli/libs'
301
+
302
+ /** The closing: one command, and the door to the documentation. */
303
+ function Closing(): ReactElement {
304
+ const [copied, setCopied] = useState(false)
305
+
306
+ return (
307
+ <section className="section section-centered">
308
+ <div className="content-narrow">
309
+ <h2 className="closing-title">Add the style system whenever you like.</h2>
310
+ <p className="closing-eyebrow">
311
+ The tokens, the utilities and the components live in a separate package. This
312
+ project runs without them, and takes them in without breaking anything.
313
+ </p>
314
+
315
+ <div className="command">
316
+ <code>{COMMAND}</code>
317
+ <button
318
+ type="button"
319
+ className="button button-ghost"
320
+ onClick={() => {
321
+ // The clipboard can be refused — outside a secure context, or
322
+ // permission withdrawn. The failure does not break the page: the
323
+ // label simply does not change.
324
+ void navigator.clipboard
325
+ ?.writeText(COMMAND)
326
+ .then(() => {
327
+ setCopied(true)
328
+ setTimeout(() => {
329
+ setCopied(false)
330
+ }, 1600)
331
+ })
332
+ .catch(() => undefined)
333
+ }}
334
+ >
335
+ {copied ? 'Copied' : 'Copy'}
336
+ </button>
337
+ </div>
338
+
339
+ <a
340
+ href="https://odoro.dev/docs/installation"
341
+ target="_blank"
342
+ rel="noreferrer"
343
+ className="quiet-link"
344
+ >
345
+ Read the walkthrough
346
+ </a>
347
+ </div>
348
+ </section>
349
+ )
350
+ }
351
+
352
+ /* -------------------------------------------------------------------------- */
353
+ /* Root */
354
+ /* -------------------------------------------------------------------------- */
355
+
356
+ /** Root of the application. */
357
+ export function App(): ReactElement {
358
+ return (
359
+ // The background sits here and not in the content: anchored lower, it
360
+ // started below the bar and left a darker strip behind it.
361
+ <div className="app-shell app-shell-background">
362
+ <Background />
363
+ <Nav />
364
+ <main className="main">
365
+ <Hero />
366
+ <Pillars />
367
+ <Closing />
368
+ </main>
369
+ <footer className="footer">
370
+ <span className="brand">
371
+ <span className="brand-mark">
372
+ <Mark size="1.1rem" />
373
+ </span>
374
+ <span className="brand-word">ODORO</span>
375
+ </span>
376
+ <span>Built with Odoro.</span>
377
+ </footer>
378
+ </div>
379
+ )
380
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The decorative background of the page.
3
+ *
4
+ * Two glows in the brand colour, heavily diluted. Nothing moves: it is a
5
+ * gradient, not an animation.
6
+ *
7
+ * If the engine was picked, this file is replaced by a version that opens an
8
+ * animated WebGL surface. `App.tsx` places `<Background />` and asks for
9
+ * nothing more.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import type { ReactElement } from 'react'
15
+
16
+ /** Two brand glows behind the top of the page. */
17
+ export function Background(): ReactElement {
18
+ return (
19
+ <div className="background" aria-hidden="true">
20
+ <div className="background-glow background-glow-top" />
21
+ <div className="background-glow background-glow-right" />
22
+ </div>
23
+ )
24
+ }
@@ -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
+ }
@@ -0,0 +1,26 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot, hydrateRoot } from 'react-dom/client'
3
+
4
+ import { App } from '@/App'
5
+
6
+ import '@/styles.css'
7
+
8
+ const container = document.getElementById('root')
9
+ if (container === null) {
10
+ throw new Error('Root element "#root" not found in index.html.')
11
+ }
12
+
13
+ const tree = (
14
+ <StrictMode>
15
+ <App />
16
+ </StrictMode>
17
+ )
18
+
19
+ // A container that is already filled comes from prerendering: it has to be
20
+ // **hydrated**, that is, taken over as it stands with the events attached.
21
+ // Rebuilding it would throw away the page the visitor already sees only to draw
22
+ // it again identically — a flicker, and the whole benefit of prerendering lost.
23
+ //
24
+ // Empty, it is an ordinary render.
25
+ if (container.firstElementChild === null) createRoot(container).render(tree)
26
+ else hydrateRoot(container, tree)