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.
- package/README.md +94 -0
- package/client.d.ts +90 -16
- package/dist/build-LZAUDJST.js +5 -0
- package/dist/{chunk-34RGOZFA.js → chunk-4D56Z7G3.js} +49 -50
- package/dist/{chunk-JMEHF3KN.js → chunk-7ZB5MAM6.js} +4 -4
- package/dist/{chunk-LHZGX5ML.js → chunk-OMHEQVIW.js} +295 -160
- package/dist/{chunk-3SZIN6VG.js → chunk-P3DJITWE.js} +10 -10
- package/dist/{chunk-ZVL7EXJO.js → chunk-PRWVZ2SM.js} +22 -14
- package/dist/chunk-Q2KCMTB5.js +741 -0
- package/dist/chunk-QOVBLN7A.js +241 -0
- package/dist/chunk-WWTDEV4Y.js +604 -0
- package/dist/cli.d.ts +12 -13
- package/dist/cli.js +84 -63
- package/dist/{commands-4JRBD55Z.js → commands-EUGVLQWL.js} +44 -45
- package/dist/{commands-AHMXBWQQ.js → commands-GYA7NTCX.js} +124 -124
- package/dist/{create-CYVSVDAD.js → create-QX5LEWMS.js} +158 -154
- package/dist/index.d.ts +603 -151
- package/dist/index.js +6 -6
- package/dist/{package-EIDLKAA6.js → package-T4Z7OBOH.js} +5 -5
- package/dist/preview-P4WY5GEY.js +4 -0
- package/dist/registry/index.d.ts +75 -76
- package/dist/registry/index.js +1 -1
- package/dist/{server-4UGN3SFS.js → server-2WP562EV.js} +3 -3
- package/package.json +5 -5
- package/templates/react-ts/README.md +71 -35
- package/templates/react-ts/_env.example +9 -0
- package/templates/react-ts/_variants/with-engine/src/background.tsx +118 -0
- package/templates/react-ts/_variants/without-libs/src/App.tsx +380 -0
- package/templates/react-ts/_variants/without-libs/src/background.tsx +24 -0
- package/templates/react-ts/_variants/without-libs/src/entry-server.tsx +39 -0
- package/templates/react-ts/_variants/without-libs/src/main.tsx +26 -0
- package/templates/{react-ts-server/_variantes/sans-libs/client → react-ts/_variants/without-libs}/src/styles.css +137 -137
- package/templates/react-ts/{_variantes/sans-routeur → _variants/without-router}/src/App.tsx +141 -141
- package/templates/react-ts/_variants/without-router/src/entry-server.tsx +39 -0
- package/templates/react-ts/index.html +4 -4
- package/templates/react-ts/odoro.config.ts +5 -0
- package/templates/react-ts/src/App.tsx +219 -209
- package/templates/react-ts/src/background.tsx +66 -0
- package/templates/react-ts/src/entry-server.tsx +85 -0
- package/templates/react-ts/src/main.tsx +13 -4
- package/templates/react-ts/src/router.tsx +65 -45
- package/templates/react-ts/src/styles.css +5 -5
- package/templates/react-ts-server/Dockerfile +9 -9
- package/templates/react-ts-server/README.md +97 -45
- package/templates/react-ts-server/_env.example +31 -33
- package/templates/react-ts-server/_variants/with-engine/client/src/background.tsx +118 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/App.tsx +380 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/background.tsx +24 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/entry-server.tsx +39 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/main.tsx +26 -0
- package/templates/{react-ts/_variantes/sans-libs → react-ts-server/_variants/without-libs/client}/src/styles.css +137 -137
- package/templates/react-ts-server/{_variantes/sans-routeur → _variants/without-router}/client/src/App.tsx +141 -141
- package/templates/react-ts-server/_variants/without-router/client/src/entry-server.tsx +39 -0
- package/templates/react-ts-server/client/index.html +4 -4
- package/templates/react-ts-server/client/src/App.tsx +228 -210
- package/templates/react-ts-server/client/src/account.tsx +267 -0
- package/templates/react-ts-server/client/src/auth.tsx +139 -0
- package/templates/react-ts-server/client/src/background.tsx +66 -0
- package/templates/react-ts-server/client/src/entry-server.tsx +92 -0
- package/templates/react-ts-server/client/src/main.tsx +17 -5
- package/templates/react-ts-server/client/src/router.tsx +77 -45
- package/templates/react-ts-server/client/src/styles.css +5 -5
- package/templates/react-ts-server/odoro.config.ts +7 -4
- package/templates/react-ts-server/package.json +2 -0
- package/templates/react-ts-server/scripts/dev.mjs +14 -14
- package/templates/react-ts-server/server/src/main.ts +119 -45
- package/templates/react-ts-server/server/src/modules/auth/index.ts +238 -0
- package/templates/react-ts-server/server/src/modules/auth/password.ts +122 -0
- package/templates/react-ts-server/server/src/modules/auth/store.ts +193 -0
- package/templates/react-ts-server/server/src/modules/health/index.ts +45 -46
- package/dist/build-JFQHODAT.js +0 -5
- package/dist/chunk-22KJTV2R.js +0 -380
- package/dist/chunk-DL3NPC4H.js +0 -113
- package/dist/chunk-TQUJ3MFS.js +0 -268
- package/dist/preview-7DKEAQOQ.js +0 -4
- package/templates/react-ts/_variantes/avec-moteur/src/fond.tsx +0 -118
- package/templates/react-ts/_variantes/sans-libs/src/App.tsx +0 -382
- package/templates/react-ts/_variantes/sans-libs/src/fond.tsx +0 -23
- package/templates/react-ts/_variantes/sans-libs/src/main.tsx +0 -17
- package/templates/react-ts/src/fond.tsx +0 -66
- package/templates/react-ts-server/_variantes/avec-moteur/client/src/fond.tsx +0 -118
- package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +0 -382
- package/templates/react-ts-server/_variantes/sans-libs/client/src/fond.tsx +0 -23
- package/templates/react-ts-server/_variantes/sans-libs/client/src/main.tsx +0 -17
- package/templates/react-ts-server/client/src/fond.tsx +0 -66
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The whole page, in one file.
|
|
3
3
|
*
|
|
4
|
-
* ##
|
|
4
|
+
* ## A single page
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* To add one later, `@odoro-cli/libs/router` is already installed: it comes
|
|
11
|
+
* with the libraries.
|
|
12
12
|
*
|
|
13
|
-
* ##
|
|
13
|
+
* ## The navigation bar
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 {
|
|
26
|
+
import { Background } from '@/background'
|
|
27
27
|
|
|
28
28
|
/* -------------------------------------------------------------------------- */
|
|
29
|
-
/*
|
|
29
|
+
/* Brand */
|
|
30
30
|
/* -------------------------------------------------------------------------- */
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* The brand mark.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
/*
|
|
58
|
+
/* Navigation bar */
|
|
59
59
|
/* -------------------------------------------------------------------------- */
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* The surface of a pill.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
74
|
-
function
|
|
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={
|
|
80
|
+
style={PILL}
|
|
81
81
|
>
|
|
82
82
|
<span className="o-text-brand-500">
|
|
83
|
-
<
|
|
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={
|
|
96
|
+
style={PILL}
|
|
97
97
|
>
|
|
98
98
|
odoro.dev
|
|
99
99
|
</a>
|
|
100
|
-
<
|
|
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
|
-
/**
|
|
112
|
-
type Theme = '
|
|
111
|
+
/** The three states of the toggle. */
|
|
112
|
+
type Theme = 'system' | 'light' | 'dark'
|
|
113
113
|
|
|
114
|
-
/**
|
|
115
|
-
const
|
|
114
|
+
/** Where the choice is stored. The script in `index.html` reads the same key. */
|
|
115
|
+
const THEME_KEY = 'odoro-theme'
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
const CYCLE: readonly Theme[] = ['
|
|
117
|
+
/** The order of the cycle, on click. */
|
|
118
|
+
const CYCLE: readonly Theme[] = ['system', 'light', 'dark']
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
*
|
|
121
|
+
* Applies the theme to the root of the document.
|
|
122
122
|
*
|
|
123
|
-
* `
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
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
|
|
129
|
-
if (theme === '
|
|
130
|
-
else document.documentElement.dataset.theme = theme
|
|
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
|
-
/**
|
|
134
|
-
function
|
|
133
|
+
/** The stored theme, or `system`. */
|
|
134
|
+
function storedTheme(): Theme {
|
|
135
135
|
try {
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
if (
|
|
139
|
-
return '
|
|
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
|
-
//
|
|
142
|
-
//
|
|
143
|
-
return '
|
|
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
|
-
/**
|
|
148
|
-
function
|
|
149
|
-
const
|
|
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 === '
|
|
160
|
+
if (theme === 'light') {
|
|
161
161
|
return (
|
|
162
|
-
<svg {...
|
|
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 === '
|
|
169
|
+
if (theme === 'dark') {
|
|
170
170
|
return (
|
|
171
|
-
<svg {...
|
|
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 {...
|
|
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
|
-
*
|
|
186
|
+
* The theme toggle.
|
|
187
187
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
|
193
|
-
const [theme, setTheme] = useState<Theme>(() =>
|
|
192
|
+
function ThemeToggle(): ReactElement {
|
|
193
|
+
const [theme, setTheme] = useState<Theme>(() => storedTheme())
|
|
194
194
|
|
|
195
195
|
useEffect(() => {
|
|
196
|
-
|
|
196
|
+
applyTheme(theme)
|
|
197
197
|
try {
|
|
198
|
-
if (theme === '
|
|
199
|
-
else localStorage.setItem(
|
|
198
|
+
if (theme === 'system') localStorage.removeItem(THEME_KEY)
|
|
199
|
+
else localStorage.setItem(THEME_KEY, theme)
|
|
200
200
|
} catch {
|
|
201
|
-
//
|
|
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
|
|
209
|
-
title={`Theme
|
|
208
|
+
aria-label={`Theme: ${theme}`}
|
|
209
|
+
title={`Theme: ${theme}`}
|
|
210
210
|
onClick={() => {
|
|
211
|
-
setTheme((
|
|
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={
|
|
214
|
+
style={PILL}
|
|
215
215
|
>
|
|
216
|
-
<
|
|
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
|
-
/**
|
|
226
|
-
const
|
|
225
|
+
/** Three facts, not three slogans. */
|
|
226
|
+
const FACTS = ['Zero configuration', 'Light and dark themes', 'Reduced motion respected']
|
|
227
227
|
|
|
228
|
-
/**
|
|
229
|
-
const
|
|
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
|
-
|
|
233
|
+
Your first page.
|
|
234
234
|
</h1>
|
|
235
235
|
</Reveal>
|
|
236
236
|
)
|
|
237
237
|
}`
|
|
238
238
|
|
|
239
|
-
/**
|
|
240
|
-
function
|
|
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
|
-
{
|
|
252
|
+
{SNIPPET}
|
|
253
253
|
</pre>
|
|
254
254
|
</div>
|
|
255
255
|
)
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
/**
|
|
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
|
-
<
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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="#
|
|
281
|
+
href="#pillars"
|
|
282
282
|
className={`o-no-underline ${buttonClasses({ tone: 'primary' })}`}
|
|
283
283
|
>
|
|
284
|
-
|
|
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
|
-
{
|
|
297
|
+
{FACTS.map((fact) => (
|
|
298
298
|
<span
|
|
299
|
-
key={
|
|
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
|
-
{
|
|
302
|
+
{fact}
|
|
303
303
|
</span>
|
|
304
304
|
))}
|
|
305
305
|
</Stagger>
|
|
306
306
|
|
|
307
307
|
<Reveal>
|
|
308
308
|
<div className="o-mt-14">
|
|
309
|
-
<
|
|
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
|
-
/**
|
|
318
|
-
const
|
|
317
|
+
/** What the project already has at hand. */
|
|
318
|
+
const PILLARS = [
|
|
319
319
|
{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"
|
|
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
|
-
|
|
326
|
-
|
|
325
|
+
title: 'Styles',
|
|
326
|
+
text: 'Tokens as the source of truth, a static stylesheet, no scanning at runtime.',
|
|
327
327
|
},
|
|
328
328
|
{
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
title: 'Interface',
|
|
330
|
+
text: 'Components that read the same tokens as your code: rethemed, they follow.',
|
|
331
331
|
},
|
|
332
332
|
]
|
|
333
333
|
|
|
334
|
-
/**
|
|
335
|
-
function
|
|
334
|
+
/** The pillars section. */
|
|
335
|
+
function Pillars(): ReactElement {
|
|
336
336
|
return (
|
|
337
337
|
<section
|
|
338
|
-
id="
|
|
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
|
-
|
|
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
|
-
{
|
|
347
|
+
{PILLARS.map((pillar, index) => (
|
|
348
348
|
<article
|
|
349
|
-
key={
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
/**
|
|
370
|
-
const
|
|
369
|
+
/** The command people copy the most. */
|
|
370
|
+
const COMMAND = 'odoro add text/count-up'
|
|
371
371
|
|
|
372
|
-
/**
|
|
373
|
-
function
|
|
374
|
-
const [
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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">{
|
|
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
|
-
//
|
|
395
|
-
// permission
|
|
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(
|
|
397
|
+
?.writeText(COMMAND)
|
|
398
398
|
.then(() => {
|
|
399
|
-
|
|
399
|
+
setCopied(true)
|
|
400
400
|
setTimeout(() => {
|
|
401
|
-
|
|
401
|
+
setCopied(false)
|
|
402
402
|
}, 1600)
|
|
403
403
|
})
|
|
404
404
|
.catch(() => undefined)
|
|
405
405
|
}}
|
|
406
406
|
>
|
|
407
|
-
{
|
|
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
|
-
|
|
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
|
-
/*
|
|
426
|
+
/* Root */
|
|
427
427
|
/* -------------------------------------------------------------------------- */
|
|
428
428
|
|
|
429
|
-
/**
|
|
429
|
+
/** Root of the application. */
|
|
430
430
|
export function App(): ReactElement {
|
|
431
431
|
return (
|
|
432
|
-
// `relative`
|
|
433
|
-
//
|
|
434
|
-
//
|
|
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
|
-
<
|
|
437
|
-
<
|
|
436
|
+
<Background />
|
|
437
|
+
<Nav />
|
|
438
438
|
<main className="o-relative">
|
|
439
439
|
<Hero />
|
|
440
|
-
<
|
|
441
|
-
<
|
|
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
|
-
<
|
|
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>
|
|
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="
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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')
|