odoro 1.0.9 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +94 -0
  2. package/client.d.ts +90 -16
  3. package/dist/build-XJIP4GNT.js +5 -0
  4. package/dist/{chunk-34RGOZFA.js → chunk-4D56Z7G3.js} +49 -50
  5. package/dist/{chunk-JMEHF3KN.js → chunk-7ZB5MAM6.js} +4 -4
  6. package/dist/chunk-DF67LADH.js +735 -0
  7. package/dist/{chunk-LHZGX5ML.js → chunk-HS7NGZLM.js} +307 -196
  8. package/dist/chunk-LIKVHWB4.js +231 -0
  9. package/dist/chunk-QOVBLN7A.js +241 -0
  10. package/dist/chunk-RTWJ5GFB.js +729 -0
  11. package/dist/chunk-TONYIKCN.js +57 -0
  12. package/dist/cli.d.ts +12 -13
  13. package/dist/cli.js +86 -63
  14. package/dist/{commands-AHMXBWQQ.js → commands-GYA7NTCX.js} +124 -124
  15. package/dist/commands-UOWN3IK4.js +181 -0
  16. package/dist/{create-CYVSVDAD.js → create-B2TLV6XI.js} +198 -160
  17. package/dist/index.d.ts +603 -151
  18. package/dist/index.js +6 -6
  19. package/dist/{package-EIDLKAA6.js → package-3ZJOEXM4.js} +6 -5
  20. package/dist/preview-3IRG7MHV.js +4 -0
  21. package/dist/registry/index.d.ts +75 -76
  22. package/dist/registry/index.js +1 -1
  23. package/dist/{server-4UGN3SFS.js → server-ZB3P3EGS.js} +3 -3
  24. package/package.json +6 -5
  25. package/templates/react-ts/README.md +71 -35
  26. package/templates/react-ts/_env.example +9 -0
  27. package/templates/react-ts/_variants/with-engine/src/background.tsx +118 -0
  28. package/templates/react-ts/_variants/without-libs/src/App.tsx +380 -0
  29. package/templates/react-ts/_variants/without-libs/src/background.tsx +24 -0
  30. package/templates/react-ts/_variants/without-libs/src/entry-server.tsx +39 -0
  31. package/templates/react-ts/_variants/without-libs/src/main.tsx +26 -0
  32. package/templates/{react-ts-server/_variantes/sans-libs/client → react-ts/_variants/without-libs}/src/styles.css +137 -137
  33. package/templates/react-ts/{_variantes/sans-routeur → _variants/without-router}/src/App.tsx +141 -141
  34. package/templates/react-ts/_variants/without-router/src/entry-server.tsx +39 -0
  35. package/templates/react-ts/index.html +4 -4
  36. package/templates/react-ts/odoro.config.ts +5 -0
  37. package/templates/react-ts/src/App.tsx +219 -209
  38. package/templates/react-ts/src/background.tsx +66 -0
  39. package/templates/react-ts/src/entry-server.tsx +85 -0
  40. package/templates/react-ts/src/main.tsx +13 -4
  41. package/templates/react-ts/src/router.tsx +65 -45
  42. package/templates/react-ts/src/styles.css +5 -5
  43. package/templates/react-ts-server/Dockerfile +9 -9
  44. package/templates/react-ts-server/README.md +97 -45
  45. package/templates/react-ts-server/_env.example +31 -33
  46. package/templates/react-ts-server/_variants/with-engine/client/src/background.tsx +118 -0
  47. package/templates/react-ts-server/_variants/without-libs/client/src/App.tsx +380 -0
  48. package/templates/react-ts-server/_variants/without-libs/client/src/background.tsx +24 -0
  49. package/templates/react-ts-server/_variants/without-libs/client/src/entry-server.tsx +39 -0
  50. package/templates/react-ts-server/_variants/without-libs/client/src/main.tsx +26 -0
  51. package/templates/{react-ts/_variantes/sans-libs → react-ts-server/_variants/without-libs/client}/src/styles.css +137 -137
  52. package/templates/react-ts-server/{_variantes/sans-routeur → _variants/without-router}/client/src/App.tsx +141 -141
  53. package/templates/react-ts-server/_variants/without-router/client/src/entry-server.tsx +39 -0
  54. package/templates/react-ts-server/client/index.html +4 -4
  55. package/templates/react-ts-server/client/src/App.tsx +228 -210
  56. package/templates/react-ts-server/client/src/account.tsx +267 -0
  57. package/templates/react-ts-server/client/src/auth.tsx +139 -0
  58. package/templates/react-ts-server/client/src/background.tsx +66 -0
  59. package/templates/react-ts-server/client/src/entry-server.tsx +92 -0
  60. package/templates/react-ts-server/client/src/main.tsx +17 -5
  61. package/templates/react-ts-server/client/src/router.tsx +77 -45
  62. package/templates/react-ts-server/client/src/styles.css +5 -5
  63. package/templates/react-ts-server/odoro.config.ts +7 -4
  64. package/templates/react-ts-server/package.json +2 -0
  65. package/templates/react-ts-server/scripts/dev.mjs +14 -14
  66. package/templates/react-ts-server/server/src/main.ts +119 -45
  67. package/templates/react-ts-server/server/src/modules/auth/index.ts +238 -0
  68. package/templates/react-ts-server/server/src/modules/auth/password.ts +122 -0
  69. package/templates/react-ts-server/server/src/modules/auth/store.ts +193 -0
  70. package/templates/react-ts-server/server/src/modules/health/index.ts +45 -46
  71. package/dist/build-JFQHODAT.js +0 -5
  72. package/dist/chunk-22KJTV2R.js +0 -380
  73. package/dist/chunk-3SZIN6VG.js +0 -64
  74. package/dist/chunk-DL3NPC4H.js +0 -113
  75. package/dist/chunk-TQUJ3MFS.js +0 -268
  76. package/dist/chunk-ZVL7EXJO.js +0 -66
  77. package/dist/commands-4JRBD55Z.js +0 -245
  78. package/dist/preview-7DKEAQOQ.js +0 -4
  79. package/templates/react-ts/_variantes/avec-moteur/src/fond.tsx +0 -118
  80. package/templates/react-ts/_variantes/sans-libs/src/App.tsx +0 -382
  81. package/templates/react-ts/_variantes/sans-libs/src/fond.tsx +0 -23
  82. package/templates/react-ts/_variantes/sans-libs/src/main.tsx +0 -17
  83. package/templates/react-ts/src/fond.tsx +0 -66
  84. package/templates/react-ts-server/_variantes/avec-moteur/client/src/fond.tsx +0 -118
  85. package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +0 -382
  86. package/templates/react-ts-server/_variantes/sans-libs/client/src/fond.tsx +0 -23
  87. package/templates/react-ts-server/_variantes/sans-libs/client/src/main.tsx +0 -17
  88. package/templates/react-ts-server/client/src/fond.tsx +0 -66
@@ -1,78 +1,76 @@
1
- # Copier ce fichier en `.env` et ajuster les valeurs.
1
+ # Copy this file to `.env` and adjust the values.
2
2
  #
3
- # Toute variable manquante ou invalide arrete le serveur au demarrage, avec un
4
- # message qui les liste toutes d'un coup : mieux vaut un echec immediat qu'une
5
- # panne a la centieme requete.
3
+ # Any missing or invalid variable stops the server at startup, with a message
4
+ # listing them all at once: an immediate failure beats an outage on the
5
+ # hundredth request.
6
6
  #
7
- # Ce fichier est versionne. `.env` ne l'est jamais.
7
+ # This file is committed. `.env` never is.
8
8
 
9
9
  # ---------------------------------------------------------------------------
10
- # Base de donnees
10
+ # Database
11
11
  # ---------------------------------------------------------------------------
12
12
 
13
- # PostgreSQL, et rien d'autre. Aucune base locale : l'URL pointe sur une base
14
- # hebergee, la votre ou celle provisionnee par `odoro db:create`.
13
+ # PostgreSQL, and nothing else. No local database: the URL points at a hosted
14
+ # one, yours or the one provisioned by `odoro db:create`.
15
15
  #
16
- # postgres://utilisateur:motdepasse@hote:5432/base?sslmode=require
16
+ # postgres://user:password@host:5432/database?sslmode=require
17
17
  #
18
- # Tant qu'elle est absente, le client demarre et le serveur repond 503 sur
19
- # /api/ready en disant ce qui manque.
18
+ # While it is missing, the client starts and the server answers 503 on
19
+ # /api/ready saying what is absent.
20
20
  DATABASE_URL=
21
21
 
22
- # Replica de lecture, optionnel. Le routage vers lui reste explicite : jamais
23
- # de bascule automatique, le decalage de replication produit des defauts
24
- # incomprehensibles.
22
+ # Read replica, optional. Routing to it stays explicit: never an automatic
23
+ # switch, replication lag produces bugs nobody can make sense of.
25
24
  # DATABASE_REPLICA_URL=
26
25
 
27
- # Taille du pool de connexions. Laisser vide pour le defaut de l'environnement.
26
+ # Connection pool size. Leave empty for the environment default.
28
27
  # DATABASE_POOL_MAX=
29
28
 
30
- # Desactive TLS. A ne poser que sur une base strictement locale a la machine,
31
- # jamais sur une base joignable par le reseau.
29
+ # Disables TLS. Only ever set this for a database strictly local to the
30
+ # machine, never for one reachable over the network.
32
31
  # DATABASE_TLS_DISABLED=false
33
32
 
34
33
  # ---------------------------------------------------------------------------
35
34
  # Session
36
35
  # ---------------------------------------------------------------------------
37
36
 
38
- # Au moins 32 caracteres. Un secret court se retrouve par force brute, et le
39
- # jour ou c'est arrive, toutes les sessions emises depuis le premier jour sont
40
- # forgeables.
37
+ # At least 32 characters. A short secret falls to brute force, and the day that
38
+ # happens, every session issued since day one can be forged.
41
39
  #
42
40
  # node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
43
41
  SESSION_SECRET=
44
42
 
45
43
  # ---------------------------------------------------------------------------
46
- # Serveur
44
+ # Server
47
45
  # ---------------------------------------------------------------------------
48
46
 
49
- # Environnement : development | test | production
47
+ # Environment: development | test | production
50
48
  NODE_ENV=development
51
49
 
52
- # Port d'ecoute de l'API. Le client de developpement ecoute sur 5180 et lui
53
- # transmet les appels commencant par /api.
50
+ # Listening port of the API. The development client listens on 5180 and
51
+ # forwards the calls starting with /api to it.
54
52
  PORT=3001
55
53
 
56
- # URL publique du serveur, employee dans les liens des courriels.
54
+ # Public URL of the server, used in the links of outgoing emails.
57
55
  APP_URL=http://localhost:3001
58
56
 
59
- # Origines autorisees, separees par des virgules. Vide = aucune origine
60
- # tierce. Jamais `*` avec des identifiants.
57
+ # Allowed origins, comma separated. Empty = no third-party origin. Never `*`
58
+ # together with credentials.
61
59
  ALLOWED_ORIGINS=
62
60
 
63
- # Plafond de la taille des corps de requete.
61
+ # Cap on the size of request bodies.
64
62
  BODY_LIMIT=1mb
65
63
 
66
- # Delai au-dela duquel l'arret propre cesse d'attendre les requetes en cours.
64
+ # Delay after which the graceful shutdown stops waiting for in-flight requests.
67
65
  SHUTDOWN_TIMEOUT=15s
68
66
 
69
- # Niveau de journalisation : fatal | error | warn | info | debug | trace
67
+ # Log level: fatal | error | warn | info | debug | trace
70
68
  LOG_LEVEL=info
71
69
 
72
70
  # ---------------------------------------------------------------------------
73
- # Files d'attente et cache
71
+ # Queues and cache
74
72
  # ---------------------------------------------------------------------------
75
73
 
76
- # Optionnel. Absente, le cache vit en memoire et les taches s'executent en
77
- # ligne — ce qui convient a une instance unique, et pas au-dela.
74
+ # Optional. Absent, the cache lives in memory and the jobs run inline — which
75
+ # suits a single instance, and nothing beyond.
78
76
  # REDIS_URL=
@@ -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
+ }