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