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.
- package/README.md +94 -0
- package/client.d.ts +90 -16
- package/dist/build-XJIP4GNT.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-DF67LADH.js +735 -0
- package/dist/{chunk-LHZGX5ML.js → chunk-HS7NGZLM.js} +307 -196
- package/dist/chunk-LIKVHWB4.js +231 -0
- package/dist/chunk-QOVBLN7A.js +241 -0
- package/dist/chunk-RTWJ5GFB.js +729 -0
- package/dist/chunk-TONYIKCN.js +57 -0
- package/dist/cli.d.ts +12 -13
- package/dist/cli.js +86 -63
- package/dist/{commands-AHMXBWQQ.js → commands-GYA7NTCX.js} +124 -124
- package/dist/commands-UOWN3IK4.js +181 -0
- package/dist/{create-CYVSVDAD.js → create-B2TLV6XI.js} +198 -160
- package/dist/index.d.ts +603 -151
- package/dist/index.js +6 -6
- package/dist/{package-EIDLKAA6.js → package-3ZJOEXM4.js} +6 -5
- package/dist/preview-3IRG7MHV.js +4 -0
- package/dist/registry/index.d.ts +75 -76
- package/dist/registry/index.js +1 -1
- package/dist/{server-4UGN3SFS.js → server-ZB3P3EGS.js} +3 -3
- package/package.json +6 -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-3SZIN6VG.js +0 -64
- package/dist/chunk-DL3NPC4H.js +0 -113
- package/dist/chunk-TQUJ3MFS.js +0 -268
- package/dist/chunk-ZVL7EXJO.js +0 -66
- package/dist/commands-4JRBD55Z.js +0 -245
- 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,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The whole page, in one file.
|
|
3
3
|
*
|
|
4
|
-
* ##
|
|
4
|
+
* ## Why everything is here
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* A project that is just starting reads better in one piece: you see the
|
|
7
|
+
* navigation, the sections and the footer without opening eight files. Each
|
|
8
|
+
* block is a function; the day one of them grows, it moves into its own file
|
|
9
|
+
* with a cut and a paste.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* **
|
|
11
|
+
* Two things live apart, and for a reason: the **router**, which is the only
|
|
12
|
+
* routing dependency and can be swapped without touching the page; and the
|
|
13
|
+
* **background**, which is the only piece that depends on the engine.
|
|
14
14
|
*
|
|
15
|
-
* ##
|
|
15
|
+
* ## The navigation bar
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* It paints no band: these are pills laid over whatever is behind, with a
|
|
18
|
+
* backdrop blur. The decorative background therefore keeps showing through,
|
|
19
|
+
* instead of being cut off by an opaque strip.
|
|
20
20
|
*
|
|
21
21
|
* @module
|
|
22
22
|
*/
|
|
@@ -31,35 +31,39 @@ import {
|
|
|
31
31
|
type ReactNode,
|
|
32
32
|
} from 'react'
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
34
|
+
// Named rather than imported wholesale: the rest of the manifest — the
|
|
35
|
+
// scripts, the metadata — has no business in the code shipped to the browser.
|
|
36
|
+
//
|
|
37
|
+
// Two levels up, and not one: in this template the client lives in `client/`,
|
|
38
|
+
// and the manifest is at the root of the project, alongside the server's.
|
|
39
|
+
import { dependencies, devDependencies } from '../../package.json'
|
|
37
40
|
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
41
|
+
import { ProfilePage, Register, SignIn } from '@/account'
|
|
42
|
+
import { Background } from '@/background'
|
|
43
|
+
import { Link, Router, useLocation } from '@/router'
|
|
40
44
|
|
|
41
45
|
/* -------------------------------------------------------------------------- */
|
|
42
|
-
/*
|
|
46
|
+
/* Brand */
|
|
43
47
|
/* -------------------------------------------------------------------------- */
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
|
-
*
|
|
50
|
+
* The brand mark.
|
|
47
51
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
52
|
+
* Drawn inline rather than loaded from `public/`: it takes the colour of its
|
|
53
|
+
* parent through `currentColor`, hence the brand hue in light as in dark, with
|
|
54
|
+
* no second file to maintain.
|
|
51
55
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
56
|
+
* The size is a style and not a class: an arbitrary-value utility class is
|
|
57
|
+
* only emitted if the compiler saw it go by, and a missing class paints
|
|
58
|
+
* nothing — the mark would have no dimensions, so it would be invisible.
|
|
55
59
|
*/
|
|
56
|
-
function
|
|
60
|
+
function Mark({ size = '1.75rem' }: { readonly size?: string }): ReactElement {
|
|
57
61
|
return (
|
|
58
62
|
<svg
|
|
59
63
|
viewBox="0 0 100 100"
|
|
60
64
|
fill="none"
|
|
61
65
|
aria-hidden="true"
|
|
62
|
-
style={{ width:
|
|
66
|
+
style={{ width: size, height: size, flexShrink: 0 }}
|
|
63
67
|
>
|
|
64
68
|
<path
|
|
65
69
|
d="M20.25 20.25H50a29.75 29.75 0 1 1-29.75 29.75Z"
|
|
@@ -72,61 +76,62 @@ function Signe({ taille = '1.75rem' }: { readonly taille?: string }): ReactEleme
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/* -------------------------------------------------------------------------- */
|
|
75
|
-
/*
|
|
79
|
+
/* Navigation bar */
|
|
76
80
|
/* -------------------------------------------------------------------------- */
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
|
-
*
|
|
83
|
+
* The surface of a pill.
|
|
80
84
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
* The system ships the black and white scales, but not their dark alpha
|
|
86
|
+
* variants: `o-bg-white/70` and its `dark:` counterpart do not exist as a
|
|
87
|
+
* single class. So the mix is done as a style, where it is exact and follows
|
|
88
|
+
* the theme without every colour having to be restated.
|
|
85
89
|
*/
|
|
86
|
-
const
|
|
90
|
+
const PILL: CSSProperties = {
|
|
87
91
|
backgroundColor: 'color-mix(in oklab, var(--o-theme-bg) 72%, transparent)',
|
|
88
92
|
borderColor: 'color-mix(in oklab, var(--o-theme-fg) 12%, transparent)',
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
/**
|
|
92
|
-
const
|
|
93
|
-
{ to: '/', label: '
|
|
94
|
-
{ to: '/
|
|
95
|
+
/** The navigation entries. */
|
|
96
|
+
const LINKS = [
|
|
97
|
+
{ to: '/', label: 'Home' },
|
|
98
|
+
{ to: '/about', label: 'About' },
|
|
99
|
+
{ to: '/profile', label: 'Account' },
|
|
95
100
|
]
|
|
96
101
|
|
|
97
|
-
/**
|
|
98
|
-
function
|
|
102
|
+
/** A link in the navigation pill, active on the current route. */
|
|
103
|
+
function TopLink({ to, label }: { readonly to: string; readonly label: string }): ReactElement {
|
|
99
104
|
const { pathname } = useLocation()
|
|
100
|
-
const
|
|
105
|
+
const active = pathname === to
|
|
101
106
|
|
|
102
107
|
return (
|
|
103
108
|
<Link
|
|
104
109
|
to={to}
|
|
105
110
|
className={[
|
|
106
111
|
'o-rounded-full o-px-4 o-py-2 o-text-sm o-no-underline o-transition-colors',
|
|
107
|
-
|
|
112
|
+
active
|
|
108
113
|
? 'o-font-medium o-text-zinc-950 dark:o-text-white'
|
|
109
114
|
: 'o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white',
|
|
110
115
|
].join(' ')}
|
|
111
|
-
style={
|
|
116
|
+
style={active ? PILL : undefined}
|
|
112
117
|
>
|
|
113
118
|
{label}
|
|
114
119
|
</Link>
|
|
115
120
|
)
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
/**
|
|
119
|
-
function
|
|
123
|
+
/** The bar: the brand, the navigation, and the link to the site. */
|
|
124
|
+
function Nav(): ReactElement {
|
|
120
125
|
return (
|
|
121
126
|
<header className="o-sticky o-top-0 o-z-20 o-px-5 md:o-px-8">
|
|
122
127
|
<div className="o-mx-auto o-flex o-h-20 o-w-full o-max-w-5xl o-items-center o-gap-3">
|
|
123
128
|
<Link
|
|
124
129
|
to="/"
|
|
125
130
|
className="o-inline-flex o-h-14 o-shrink-0 o-items-center o-gap-2.5 o-rounded-full o-border-w-1 o-pl-5 o-pr-6 o-no-underline o-backdrop-blur-xl"
|
|
126
|
-
style={
|
|
131
|
+
style={PILL}
|
|
127
132
|
>
|
|
128
133
|
<span className="o-text-brand-500">
|
|
129
|
-
<
|
|
134
|
+
<Mark />
|
|
130
135
|
</span>
|
|
131
136
|
<span className="o-text-lg o-font-bold o-tracking-wide o-text-zinc-950 dark:o-text-white">
|
|
132
137
|
ODORO
|
|
@@ -136,10 +141,10 @@ function Barre(): ReactElement {
|
|
|
136
141
|
<nav
|
|
137
142
|
aria-label="Navigation"
|
|
138
143
|
className="o-inline-flex o-h-14 o-items-center o-gap-1 o-rounded-full o-border-w-1 o-px-2 o-backdrop-blur-xl"
|
|
139
|
-
style={
|
|
144
|
+
style={PILL}
|
|
140
145
|
>
|
|
141
|
-
{
|
|
142
|
-
<
|
|
146
|
+
{LINKS.map((link) => (
|
|
147
|
+
<TopLink key={link.to} to={link.to} label={link.label} />
|
|
143
148
|
))}
|
|
144
149
|
</nav>
|
|
145
150
|
|
|
@@ -149,11 +154,11 @@ function Barre(): ReactElement {
|
|
|
149
154
|
target="_blank"
|
|
150
155
|
rel="noreferrer"
|
|
151
156
|
className="o-inline-flex o-h-14 o-shrink-0 o-items-center o-rounded-full o-border-w-1 o-px-5 o-text-sm o-no-underline o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
|
|
152
|
-
style={
|
|
157
|
+
style={PILL}
|
|
153
158
|
>
|
|
154
159
|
odoro.dev
|
|
155
160
|
</a>
|
|
156
|
-
<
|
|
161
|
+
<ThemeToggle />
|
|
157
162
|
</div>
|
|
158
163
|
</div>
|
|
159
164
|
</header>
|
|
@@ -164,45 +169,45 @@ function Barre(): ReactElement {
|
|
|
164
169
|
/* Theme */
|
|
165
170
|
/* -------------------------------------------------------------------------- */
|
|
166
171
|
|
|
167
|
-
/**
|
|
168
|
-
type Theme = '
|
|
172
|
+
/** The three states of the toggle. */
|
|
173
|
+
type Theme = 'system' | 'light' | 'dark'
|
|
169
174
|
|
|
170
|
-
/**
|
|
171
|
-
const
|
|
175
|
+
/** Where the choice is stored. The script in `index.html` reads the same key. */
|
|
176
|
+
const THEME_KEY = 'odoro-theme'
|
|
172
177
|
|
|
173
|
-
/**
|
|
174
|
-
const CYCLE: readonly Theme[] = ['
|
|
178
|
+
/** The order of the cycle, on click. */
|
|
179
|
+
const CYCLE: readonly Theme[] = ['system', 'light', 'dark']
|
|
175
180
|
|
|
176
181
|
/**
|
|
177
|
-
*
|
|
182
|
+
* Applies the theme to the root of the document.
|
|
178
183
|
*
|
|
179
|
-
* `
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
184
|
+
* `system` removes the attribute rather than setting a third value: the
|
|
185
|
+
* stylesheet listens to `data-theme`, and its absence hands control back to
|
|
186
|
+
* the browser preference. That is what lets a page left on "system" follow the
|
|
187
|
+
* switch to night without being reopened.
|
|
183
188
|
*/
|
|
184
|
-
function
|
|
185
|
-
if (theme === '
|
|
186
|
-
else document.documentElement.dataset.theme = theme
|
|
189
|
+
function applyTheme(theme: Theme): void {
|
|
190
|
+
if (theme === 'system') delete document.documentElement.dataset.theme
|
|
191
|
+
else document.documentElement.dataset.theme = theme
|
|
187
192
|
}
|
|
188
193
|
|
|
189
|
-
/**
|
|
190
|
-
function
|
|
194
|
+
/** The stored theme, or `system`. */
|
|
195
|
+
function storedTheme(): Theme {
|
|
191
196
|
try {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
195
|
-
return '
|
|
197
|
+
const value = localStorage.getItem(THEME_KEY)
|
|
198
|
+
if (value === 'light') return 'light'
|
|
199
|
+
if (value === 'dark') return 'dark'
|
|
200
|
+
return 'system'
|
|
196
201
|
} catch {
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
return '
|
|
202
|
+
// Storage refused — private window, cookies blocked. The system theme is
|
|
203
|
+
// still a valid answer; failing here would deprive the page of its render.
|
|
204
|
+
return 'system'
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
|
|
203
|
-
/**
|
|
204
|
-
function
|
|
205
|
-
const
|
|
208
|
+
/** The three glyphs, drawn rather than imported: the icons are optional. */
|
|
209
|
+
function Glyph({ theme }: { readonly theme: Theme }): ReactElement {
|
|
210
|
+
const common = {
|
|
206
211
|
viewBox: '0 0 24 24',
|
|
207
212
|
fill: 'none',
|
|
208
213
|
stroke: 'currentColor',
|
|
@@ -213,25 +218,25 @@ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
|
|
|
213
218
|
style: { width: '1.15rem', height: '1.15rem' },
|
|
214
219
|
}
|
|
215
220
|
|
|
216
|
-
if (theme === '
|
|
221
|
+
if (theme === 'light') {
|
|
217
222
|
return (
|
|
218
|
-
<svg {...
|
|
223
|
+
<svg {...common}>
|
|
219
224
|
<circle cx="12" cy="12" r="4" />
|
|
220
225
|
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
|
|
221
226
|
</svg>
|
|
222
227
|
)
|
|
223
228
|
}
|
|
224
229
|
|
|
225
|
-
if (theme === '
|
|
230
|
+
if (theme === 'dark') {
|
|
226
231
|
return (
|
|
227
|
-
<svg {...
|
|
232
|
+
<svg {...common}>
|
|
228
233
|
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
|
|
229
234
|
</svg>
|
|
230
235
|
)
|
|
231
236
|
}
|
|
232
237
|
|
|
233
238
|
return (
|
|
234
|
-
<svg {...
|
|
239
|
+
<svg {...common}>
|
|
235
240
|
<rect x="2" y="4" width="20" height="13" rx="2" />
|
|
236
241
|
<path d="M8 21h8M12 17v4" />
|
|
237
242
|
</svg>
|
|
@@ -239,66 +244,66 @@ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
|
|
|
239
244
|
}
|
|
240
245
|
|
|
241
246
|
/**
|
|
242
|
-
*
|
|
247
|
+
* The theme toggle.
|
|
243
248
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* preference.
|
|
249
|
+
* Three states and not two: "system" is a choice in its own right, and
|
|
250
|
+
* removing it would force a visitor to choose again every time their
|
|
251
|
+
* preference changes.
|
|
247
252
|
*/
|
|
248
|
-
function
|
|
249
|
-
const [theme, setTheme] = useState<Theme>(() =>
|
|
253
|
+
function ThemeToggle(): ReactElement {
|
|
254
|
+
const [theme, setTheme] = useState<Theme>(() => storedTheme())
|
|
250
255
|
|
|
251
256
|
useEffect(() => {
|
|
252
|
-
|
|
257
|
+
applyTheme(theme)
|
|
253
258
|
try {
|
|
254
|
-
if (theme === '
|
|
255
|
-
else localStorage.setItem(
|
|
259
|
+
if (theme === 'system') localStorage.removeItem(THEME_KEY)
|
|
260
|
+
else localStorage.setItem(THEME_KEY, theme)
|
|
256
261
|
} catch {
|
|
257
|
-
//
|
|
262
|
+
// The theme stays applied for the session, even without storage.
|
|
258
263
|
}
|
|
259
264
|
}, [theme])
|
|
260
265
|
|
|
261
266
|
return (
|
|
262
267
|
<button
|
|
263
268
|
type="button"
|
|
264
|
-
aria-label={`Theme
|
|
265
|
-
title={`Theme
|
|
269
|
+
aria-label={`Theme: ${theme}`}
|
|
270
|
+
title={`Theme: ${theme}`}
|
|
266
271
|
onClick={() => {
|
|
267
|
-
setTheme((
|
|
272
|
+
setTheme((current) => CYCLE[(CYCLE.indexOf(current) + 1) % CYCLE.length] ?? 'system')
|
|
268
273
|
}}
|
|
269
274
|
className="o-inline-flex o-size-14 o-shrink-0 o-cursor-pointer o-items-center o-justify-center o-rounded-full o-border-w-1 o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
|
|
270
|
-
style={
|
|
275
|
+
style={PILL}
|
|
271
276
|
>
|
|
272
|
-
<
|
|
277
|
+
<Glyph theme={theme} />
|
|
273
278
|
</button>
|
|
274
279
|
)
|
|
275
280
|
}
|
|
276
281
|
|
|
277
282
|
/* -------------------------------------------------------------------------- */
|
|
278
|
-
/*
|
|
283
|
+
/* Home page sections */
|
|
279
284
|
/* -------------------------------------------------------------------------- */
|
|
280
285
|
|
|
281
|
-
/**
|
|
282
|
-
const
|
|
286
|
+
/** Three facts, not three slogans. */
|
|
287
|
+
const FACTS = ['Zero configuration', 'Light and dark themes', 'Reduced motion respected']
|
|
283
288
|
|
|
284
|
-
/**
|
|
285
|
-
const
|
|
289
|
+
/** What the product window shows. */
|
|
290
|
+
const SNIPPET = `export function Home() {
|
|
286
291
|
return (
|
|
287
292
|
<Reveal>
|
|
288
293
|
<h1 className="o-text-6xl o-font-bold">
|
|
289
|
-
|
|
294
|
+
Your first page.
|
|
290
295
|
</h1>
|
|
291
296
|
</Reveal>
|
|
292
297
|
)
|
|
293
298
|
}`
|
|
294
299
|
|
|
295
300
|
/**
|
|
296
|
-
*
|
|
301
|
+
* A miniature browser window.
|
|
297
302
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
303
|
+
* The three dots are decorative: they say "browser" without adding anything a
|
|
304
|
+
* screen reader needs to hear.
|
|
300
305
|
*/
|
|
301
|
-
function
|
|
306
|
+
function Window(): ReactElement {
|
|
302
307
|
return (
|
|
303
308
|
<div className="o-overflow-hidden o-rounded-2xl o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-zinc-50 dark:o-bg-zinc-900 o-shadow-2xl">
|
|
304
309
|
<div className="o-flex o-items-center o-gap-2 o-border-b o-border-zinc-200 dark:o-border-zinc-800 o-px-4 o-py-2.5">
|
|
@@ -310,39 +315,39 @@ function Fenetre(): ReactElement {
|
|
|
310
315
|
<span className="o-ml-2 o-font-mono o-text-xs o-text-zinc-400">src/App.tsx</span>
|
|
311
316
|
</div>
|
|
312
317
|
<pre className="o-m-0 o-overflow-x-auto o-p-5 o-font-mono o-text-xs o-leading-relaxed o-text-zinc-600 dark:o-text-zinc-300">
|
|
313
|
-
{
|
|
318
|
+
{SNIPPET}
|
|
314
319
|
</pre>
|
|
315
320
|
</div>
|
|
316
321
|
)
|
|
317
322
|
}
|
|
318
323
|
|
|
319
|
-
/**
|
|
324
|
+
/** The hero: the promise, the actions, and the product window. */
|
|
320
325
|
function Hero(): ReactElement {
|
|
321
326
|
return (
|
|
322
327
|
<section className="o-relative o-px-6 o-pb-20 o-pt-16 md:o-pt-24">
|
|
323
328
|
<div className="o-mx-auto o-w-full o-max-w-5xl">
|
|
324
329
|
<Reveal>
|
|
325
330
|
<span className="o-inline-flex o-items-center o-gap-2 o-whitespace-nowrap o-rounded-full o-border-w-1 o-border-brand-500/30 o-bg-brand-500/10 o-px-3 o-py-1 o-text-xs o-font-medium o-text-brand-600 dark:o-text-brand-400">
|
|
326
|
-
<
|
|
327
|
-
|
|
331
|
+
<Mark size="0.9rem" />
|
|
332
|
+
Generated by odoro create
|
|
328
333
|
</span>
|
|
329
334
|
|
|
330
335
|
<h1 className="o-mt-6 o-max-w-3xl o-text-5xl o-font-bold o-tracking-tight md:o-text-6xl">
|
|
331
|
-
|
|
336
|
+
Your first page, <span className="o-text-brand-500">already alive</span>.
|
|
332
337
|
</h1>
|
|
333
338
|
|
|
334
339
|
<p className="o-mt-5 o-max-w-xl o-text-lg o-text-zinc-500 dark:o-text-zinc-400">
|
|
335
|
-
|
|
336
|
-
|
|
340
|
+
The router, the animations and the style system all come from the same
|
|
341
|
+
library. Edit this page: it reloads without losing its state.
|
|
337
342
|
</p>
|
|
338
343
|
</Reveal>
|
|
339
344
|
|
|
340
345
|
<Stagger step={70} className="o-mt-8 o-flex o-flex-wrap o-items-center o-gap-3">
|
|
341
346
|
<a
|
|
342
|
-
href="#
|
|
347
|
+
href="#pillars"
|
|
343
348
|
className={`o-no-underline ${buttonClasses({ tone: 'primary' })}`}
|
|
344
349
|
>
|
|
345
|
-
|
|
350
|
+
See what is included
|
|
346
351
|
</a>
|
|
347
352
|
<a
|
|
348
353
|
href="https://odoro.dev/docs"
|
|
@@ -355,19 +360,19 @@ function Hero(): ReactElement {
|
|
|
355
360
|
</Stagger>
|
|
356
361
|
|
|
357
362
|
<Stagger step={60} className="o-mt-6 o-flex o-flex-wrap o-gap-2">
|
|
358
|
-
{
|
|
363
|
+
{FACTS.map((fact) => (
|
|
359
364
|
<span
|
|
360
|
-
key={
|
|
365
|
+
key={fact}
|
|
361
366
|
className="o-rounded-full o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-px-3 o-py-1 o-text-xs o-text-zinc-500 dark:o-text-zinc-400"
|
|
362
367
|
>
|
|
363
|
-
{
|
|
368
|
+
{fact}
|
|
364
369
|
</span>
|
|
365
370
|
))}
|
|
366
371
|
</Stagger>
|
|
367
372
|
|
|
368
373
|
<Reveal>
|
|
369
374
|
<div className="o-mt-14">
|
|
370
|
-
<
|
|
375
|
+
<Window />
|
|
371
376
|
</div>
|
|
372
377
|
</Reveal>
|
|
373
378
|
</div>
|
|
@@ -375,50 +380,49 @@ function Hero(): ReactElement {
|
|
|
375
380
|
)
|
|
376
381
|
}
|
|
377
382
|
|
|
378
|
-
/**
|
|
379
|
-
const
|
|
383
|
+
/** What the project already has at hand. */
|
|
384
|
+
const PILLARS = [
|
|
380
385
|
{
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
'Segments dynamiques, routes imbriquees, chargement paresseux et transitions de page.',
|
|
386
|
+
title: 'Router',
|
|
387
|
+
text: 'Dynamic segments, nested routes, lazy loading and page transitions.',
|
|
384
388
|
},
|
|
385
389
|
{
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"
|
|
390
|
+
title: 'Animations',
|
|
391
|
+
text:
|
|
392
|
+
"A thin layer over the browser's own engine. Reduced motion is respected out of the box.",
|
|
389
393
|
},
|
|
390
394
|
{
|
|
391
|
-
|
|
392
|
-
|
|
395
|
+
title: 'Styles',
|
|
396
|
+
text: 'Tokens as the source of truth, a static stylesheet, no scanning at runtime.',
|
|
393
397
|
},
|
|
394
398
|
]
|
|
395
399
|
|
|
396
|
-
/**
|
|
397
|
-
function
|
|
400
|
+
/** The pillars section. */
|
|
401
|
+
function Pillars(): ReactElement {
|
|
398
402
|
return (
|
|
399
403
|
<section
|
|
400
|
-
id="
|
|
404
|
+
id="pillars"
|
|
401
405
|
className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-20"
|
|
402
406
|
>
|
|
403
407
|
<div className="o-mx-auto o-w-full o-max-w-5xl">
|
|
404
408
|
<h2 className="o-text-sm o-font-semibold o-uppercase o-tracking-widest o-text-zinc-400">
|
|
405
|
-
|
|
409
|
+
What is already here
|
|
406
410
|
</h2>
|
|
407
411
|
|
|
408
412
|
<Stagger step={90} className="o-mt-8 o-grid o-gap-4 md:o-grid-cols-3">
|
|
409
|
-
{
|
|
413
|
+
{PILLARS.map((pillar, index) => (
|
|
410
414
|
<article
|
|
411
|
-
key={
|
|
415
|
+
key={pillar.title}
|
|
412
416
|
className="o-rounded-xl o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-white dark:o-bg-zinc-900 o-p-6"
|
|
413
417
|
>
|
|
414
418
|
<span className="o-font-mono o-text-xs o-text-brand-500">
|
|
415
419
|
{String(index + 1).padStart(2, '0')}
|
|
416
420
|
</span>
|
|
417
421
|
<h3 className="o-mt-3 o-text-lg o-font-semibold o-tracking-tight">
|
|
418
|
-
{
|
|
422
|
+
{pillar.title}
|
|
419
423
|
</h3>
|
|
420
424
|
<p className="o-mt-2 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
|
|
421
|
-
{
|
|
425
|
+
{pillar.text}
|
|
422
426
|
</p>
|
|
423
427
|
</article>
|
|
424
428
|
))}
|
|
@@ -428,46 +432,46 @@ function Piliers(): ReactElement {
|
|
|
428
432
|
)
|
|
429
433
|
}
|
|
430
434
|
|
|
431
|
-
/**
|
|
432
|
-
const
|
|
435
|
+
/** The command people copy the most. */
|
|
436
|
+
const COMMAND = 'odoro add text/count-up'
|
|
433
437
|
|
|
434
|
-
/**
|
|
435
|
-
function
|
|
436
|
-
const [
|
|
438
|
+
/** The closing: one command, and the door to the catalogue. */
|
|
439
|
+
function Closing(): ReactElement {
|
|
440
|
+
const [copied, setCopied] = useState(false)
|
|
437
441
|
|
|
438
442
|
return (
|
|
439
443
|
<section className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-24">
|
|
440
444
|
<div className="o-mx-auto o-w-full o-max-w-3xl o-text-center">
|
|
441
445
|
<Reveal>
|
|
442
446
|
<h2 className="o-text-3xl o-font-bold o-tracking-tight md:o-text-4xl">
|
|
443
|
-
|
|
447
|
+
Add a component without leaving the terminal.
|
|
444
448
|
</h2>
|
|
445
449
|
<p className="o-mx-auto o-mt-4 o-max-w-prose o-text-zinc-500 dark:o-text-zinc-400">
|
|
446
|
-
|
|
447
|
-
|
|
450
|
+
The registry copies the code into your project. It is yours: you read it,
|
|
451
|
+
you change it, it never updates behind your back.
|
|
448
452
|
</p>
|
|
449
453
|
|
|
450
454
|
<div className="o-mx-auto o-mt-8 o-flex o-max-w-md o-items-center o-gap-2 o-rounded-xl o-border-w-1 o-border-zinc-200 dark:o-border-zinc-800 o-bg-zinc-50 dark:o-bg-zinc-900 o-p-2 o-pl-4">
|
|
451
|
-
<code className="o-flex-1 o-text-left o-font-mono o-text-sm">{
|
|
455
|
+
<code className="o-flex-1 o-text-left o-font-mono o-text-sm">{COMMAND}</code>
|
|
452
456
|
<button
|
|
453
457
|
type="button"
|
|
454
458
|
className={buttonClasses({ tone: 'ghost', size: 'sm' })}
|
|
455
459
|
onClick={() => {
|
|
456
|
-
//
|
|
457
|
-
// permission
|
|
458
|
-
//
|
|
460
|
+
// The clipboard can be refused — outside a secure context, or
|
|
461
|
+
// permission withdrawn. The failure does not break the page:
|
|
462
|
+
// the label simply does not change.
|
|
459
463
|
void navigator.clipboard
|
|
460
|
-
?.writeText(
|
|
464
|
+
?.writeText(COMMAND)
|
|
461
465
|
.then(() => {
|
|
462
|
-
|
|
466
|
+
setCopied(true)
|
|
463
467
|
setTimeout(() => {
|
|
464
|
-
|
|
468
|
+
setCopied(false)
|
|
465
469
|
}, 1600)
|
|
466
470
|
})
|
|
467
471
|
.catch(() => undefined)
|
|
468
472
|
}}
|
|
469
473
|
>
|
|
470
|
-
{
|
|
474
|
+
{copied ? 'Copied' : 'Copy'}
|
|
471
475
|
</button>
|
|
472
476
|
</div>
|
|
473
477
|
|
|
@@ -477,7 +481,7 @@ function Cloture(): ReactElement {
|
|
|
477
481
|
rel="noreferrer"
|
|
478
482
|
className="o-mt-6 o-inline-block o-text-sm o-text-zinc-500 dark:o-text-zinc-400 o-no-underline hover:o-text-brand-500"
|
|
479
483
|
>
|
|
480
|
-
|
|
484
|
+
Browse the catalogue
|
|
481
485
|
</a>
|
|
482
486
|
</Reveal>
|
|
483
487
|
</div>
|
|
@@ -489,70 +493,70 @@ function Cloture(): ReactElement {
|
|
|
489
493
|
/* Pages */
|
|
490
494
|
/* -------------------------------------------------------------------------- */
|
|
491
495
|
|
|
492
|
-
/**
|
|
493
|
-
function
|
|
496
|
+
/** The home page. */
|
|
497
|
+
function Home(): ReactElement {
|
|
494
498
|
return (
|
|
495
499
|
<>
|
|
496
500
|
<Hero />
|
|
497
|
-
<
|
|
498
|
-
<
|
|
501
|
+
<Pillars />
|
|
502
|
+
<Closing />
|
|
499
503
|
</>
|
|
500
504
|
)
|
|
501
505
|
}
|
|
502
506
|
|
|
503
507
|
/**
|
|
504
|
-
*
|
|
508
|
+
* What each package of the family brings.
|
|
505
509
|
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
510
|
+
* The table describes; it does not decide. What shows comes from the manifest
|
|
511
|
+
* of the project, hence from what was actually installed — and that stays true
|
|
512
|
+
* if you add or remove a package later.
|
|
509
513
|
*/
|
|
510
514
|
const ROLES: Readonly<Record<string, string>> = {
|
|
511
|
-
odoro: '
|
|
512
|
-
'@odoro-cli/libs': '
|
|
513
|
-
'@odoro-cli/icons': '
|
|
514
|
-
'@odoro-cli/engine': 'WebGL, surfaces
|
|
515
|
-
'@odoro-cli/server': '
|
|
515
|
+
odoro: 'Development server, build and registry',
|
|
516
|
+
'@odoro-cli/libs': 'Tokens, utilities, components and router',
|
|
517
|
+
'@odoro-cli/icons': 'Five icon families, importable one by one',
|
|
518
|
+
'@odoro-cli/engine': 'WebGL, surfaces and motion policy',
|
|
519
|
+
'@odoro-cli/server': 'Modular back-end foundation',
|
|
516
520
|
}
|
|
517
521
|
|
|
518
522
|
/**
|
|
519
|
-
*
|
|
523
|
+
* The packages of the family present in this project.
|
|
520
524
|
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
525
|
+
* Both fields are read, because `odoro` is a development dependency where the
|
|
526
|
+
* others are production dependencies. A hand-written list would start lying at
|
|
527
|
+
* the first box left unticked at creation.
|
|
524
528
|
*/
|
|
525
|
-
const
|
|
526
|
-
.filter(([
|
|
529
|
+
const LANDMARKS = Object.entries({ ...dependencies, ...devDependencies })
|
|
530
|
+
.filter(([name]) => name in ROLES)
|
|
527
531
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
528
|
-
.map(([
|
|
532
|
+
.map(([name, version]) => ({ name, version, role: ROLES[name] ?? '' }))
|
|
529
533
|
|
|
530
|
-
/**
|
|
531
|
-
function
|
|
534
|
+
/** A second page, to show the router at work. */
|
|
535
|
+
function About(): ReactElement {
|
|
532
536
|
return (
|
|
533
537
|
<section className="o-px-6 o-py-24">
|
|
534
538
|
<div className="o-mx-auto o-w-full o-max-w-3xl">
|
|
535
539
|
<Reveal>
|
|
536
|
-
<h1 className="o-text-4xl o-font-bold o-tracking-tight">
|
|
540
|
+
<h1 className="o-text-4xl o-font-bold o-tracking-tight">About</h1>
|
|
537
541
|
<p className="o-mt-4 o-text-zinc-500 dark:o-text-zinc-400">
|
|
538
|
-
|
|
539
|
-
|
|
542
|
+
This page exists to show the router: navigating reloads nothing, and the
|
|
543
|
+
transition is the one the browser provides.
|
|
540
544
|
</p>
|
|
541
545
|
|
|
542
546
|
<dl className="o-mt-10 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
|
|
543
|
-
{
|
|
547
|
+
{LANDMARKS.map((landmark) => (
|
|
544
548
|
<div
|
|
545
|
-
key={
|
|
549
|
+
key={landmark.name}
|
|
546
550
|
className="o-flex o-flex-col md:o-flex-row o-gap-1 md:o-gap-6 o-border-b o-border-zinc-200 dark:o-border-zinc-800 o-py-4"
|
|
547
551
|
>
|
|
548
552
|
<dt className="o-w-56 o-shrink-0 o-font-mono o-text-sm o-font-medium">
|
|
549
|
-
{
|
|
553
|
+
{landmark.name}
|
|
550
554
|
<span className="o-ml-2 o-text-xs o-font-normal o-text-zinc-400">
|
|
551
|
-
{
|
|
555
|
+
{landmark.version}
|
|
552
556
|
</span>
|
|
553
557
|
</dt>
|
|
554
558
|
<dd className="o-m-0 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
|
|
555
|
-
{
|
|
559
|
+
{landmark.role}
|
|
556
560
|
</dd>
|
|
557
561
|
</div>
|
|
558
562
|
))}
|
|
@@ -563,59 +567,73 @@ function APropos(): ReactElement {
|
|
|
563
567
|
)
|
|
564
568
|
}
|
|
565
569
|
|
|
566
|
-
/**
|
|
567
|
-
function
|
|
570
|
+
/** What shows when no route matches. */
|
|
571
|
+
function NotFound(): ReactElement {
|
|
568
572
|
return (
|
|
569
573
|
<section className="o-px-6 o-py-32 o-text-center">
|
|
570
574
|
<p className="o-font-mono o-text-sm o-text-brand-500">404</p>
|
|
571
575
|
<h1 className="o-mt-3 o-text-3xl o-font-bold o-tracking-tight">
|
|
572
|
-
|
|
576
|
+
This page does not exist.
|
|
573
577
|
</h1>
|
|
574
578
|
<Link to="/" className="o-mt-6 o-inline-block o-text-sm">
|
|
575
|
-
|
|
579
|
+
Back to home
|
|
576
580
|
</Link>
|
|
577
581
|
</section>
|
|
578
582
|
)
|
|
579
583
|
}
|
|
580
584
|
|
|
581
585
|
/* -------------------------------------------------------------------------- */
|
|
582
|
-
/*
|
|
586
|
+
/* Root */
|
|
583
587
|
/* -------------------------------------------------------------------------- */
|
|
584
588
|
|
|
585
|
-
/**
|
|
586
|
-
function
|
|
589
|
+
/** The common shell: nav, content, footer. */
|
|
590
|
+
function Shell(content: ReactNode): ReactElement {
|
|
587
591
|
return (
|
|
588
|
-
// `relative`
|
|
589
|
-
//
|
|
590
|
-
//
|
|
591
|
-
// page
|
|
592
|
+
// `relative` carries the decorative background, which places itself
|
|
593
|
+
// absolutely inside it. It sits here and not in the page: anchored to the
|
|
594
|
+
// top of `main`, it started below the bar, and the strip left behind it
|
|
595
|
+
// showed at the top of the page as a darker rectangle.
|
|
592
596
|
<div className="app-shell o-relative">
|
|
593
|
-
<
|
|
594
|
-
<
|
|
595
|
-
<main className="o-relative o-view-transition-page">{
|
|
597
|
+
<Background />
|
|
598
|
+
<Nav />
|
|
599
|
+
<main className="o-relative o-view-transition-page">{content}</main>
|
|
596
600
|
<footer className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-8">
|
|
597
601
|
<div className="o-mx-auto o-flex o-w-full o-max-w-5xl o-items-center o-justify-between o-gap-4 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
|
|
598
602
|
<span className="o-inline-flex o-items-center o-gap-2">
|
|
599
603
|
<span className="o-text-brand-500">
|
|
600
|
-
<
|
|
604
|
+
<Mark size="1.1rem" />
|
|
601
605
|
</span>
|
|
602
606
|
<span className="o-font-semibold o-tracking-wide">ODORO</span>
|
|
603
607
|
</span>
|
|
604
|
-
<span>
|
|
608
|
+
<span>Built with Odoro.</span>
|
|
605
609
|
</div>
|
|
606
610
|
</footer>
|
|
607
611
|
</div>
|
|
608
612
|
)
|
|
609
613
|
}
|
|
610
614
|
|
|
611
|
-
/**
|
|
612
|
-
export
|
|
615
|
+
/** What the application receives. */
|
|
616
|
+
export interface AppProps {
|
|
617
|
+
/**
|
|
618
|
+
* The address to render, during prerendering.
|
|
619
|
+
*
|
|
620
|
+
* Absent in the browser: there, the address bar is the truth.
|
|
621
|
+
*/
|
|
622
|
+
readonly url?: string
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Root of the application. */
|
|
626
|
+
export function App({ url }: AppProps = {}): ReactElement {
|
|
613
627
|
return (
|
|
614
|
-
<
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
628
|
+
<Router
|
|
629
|
+
shell={Shell}
|
|
630
|
+
home={<Home />}
|
|
631
|
+
about={<About />}
|
|
632
|
+
signIn={<SignIn />}
|
|
633
|
+
register={<Register />}
|
|
634
|
+
profile={<ProfilePage />}
|
|
635
|
+
notFound={<NotFound />}
|
|
636
|
+
url={url}
|
|
619
637
|
/>
|
|
620
638
|
)
|
|
621
639
|
}
|