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