odoro 1.0.2 → 1.0.3
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 +9 -9
- package/dist/build-KI7IEVRJ.js +4 -0
- package/dist/{chunk-R6PMAMXA.js → chunk-2EE5QUCW.js} +16 -3
- package/dist/{chunk-CJIUP7A3.js → chunk-FJYUQALD.js} +1 -1
- package/dist/cli.js +4 -4
- package/dist/{create-GD6O2TPL.js → create-VUSKHXBN.js} +4 -4
- package/dist/index.js +2 -2
- package/dist/{package-VMTZDORI.js → package-2JOBLFXE.js} +3 -2
- package/dist/{server-ATGFRVSW.js → server-DXOQAPVX.js} +1 -1
- package/package.json +3 -2
- package/templates/react-ts/_variantes/sans-libs/src/App.tsx +122 -11
- package/templates/react-ts/_variantes/sans-libs/src/styles.css +49 -5
- package/templates/react-ts/_variantes/sans-routeur/src/App.tsx +132 -12
- package/templates/react-ts/index.html +11 -0
- package/templates/react-ts/src/App.tsx +177 -22
- package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +122 -11
- package/templates/react-ts-server/_variantes/sans-libs/client/src/styles.css +49 -5
- package/templates/react-ts-server/_variantes/sans-routeur/client/src/App.tsx +132 -12
- package/templates/react-ts-server/client/index.html +11 -0
- package/templates/react-ts-server/client/src/App.tsx +177 -22
- package/dist/build-GIPMPYJ3.js +0 -4
package/README.md
CHANGED
|
@@ -14,15 +14,15 @@ package is what it installs.
|
|
|
14
14
|
|
|
15
15
|
| Command | What it does |
|
|
16
16
|
| ---------------- | --------------------------------------------------------- |
|
|
17
|
-
| `odoro dev` | Development server with hot module replacement.
|
|
18
|
-
| `odoro build` | Production build into `dist/`.
|
|
19
|
-
| `odoro preview` | Serves `dist/` the way a static host would.
|
|
20
|
-
| `odoro create` | Scaffolds a project — same thing as `npm create odoro`.
|
|
21
|
-
| `odoro init` | Writes `odoro.json` so `odoro add` knows where to write.
|
|
22
|
-
| `odoro add <id>` | Copies a registry component into your project.
|
|
23
|
-
| `odoro list` | Prints the registry catalogue.
|
|
24
|
-
| `odoro diff` | Compares what is installed with what the registry serves.
|
|
25
|
-
| `odoro doctor` | Checks that the project is in a fit state.
|
|
17
|
+
| `odoro dev` | Development server with hot module replacement. |
|
|
18
|
+
| `odoro build` | Production build into `dist/`. |
|
|
19
|
+
| `odoro preview` | Serves `dist/` the way a static host would. |
|
|
20
|
+
| `odoro create` | Scaffolds a project — same thing as `npm create odoro`. |
|
|
21
|
+
| `odoro init` | Writes `odoro.json` so `odoro add` knows where to write. |
|
|
22
|
+
| `odoro add <id>` | Copies a registry component into your project. |
|
|
23
|
+
| `odoro list` | Prints the registry catalogue. |
|
|
24
|
+
| `odoro diff` | Compares what is installed with what the registry serves. |
|
|
25
|
+
| `odoro doctor` | Checks that the project is in a fit state. |
|
|
26
26
|
|
|
27
27
|
Run `odoro --help` for the full list, including the database commands.
|
|
28
28
|
|
|
@@ -34,7 +34,9 @@ async function fournisseurDe(root) {
|
|
|
34
34
|
// `generate` enumere les classes du palier courant. Absent d'un paquet
|
|
35
35
|
// plus ancien : le champ reste alors indefini, et le developpement
|
|
36
36
|
// retombe sur la feuille telle qu'elle est livree.
|
|
37
|
-
...typeof generate === "function" ? {
|
|
37
|
+
...typeof generate === "function" ? {
|
|
38
|
+
classesConnues: () => new Set(generate("core").classNames)
|
|
39
|
+
} : {}
|
|
38
40
|
};
|
|
39
41
|
} catch {
|
|
40
42
|
return void 0;
|
|
@@ -720,6 +722,11 @@ async function applyReactRefresh(code, file) {
|
|
|
720
722
|
// La carte de source produite par la compilation precedente est reprise et
|
|
721
723
|
// fusionnee : sans cela, les numeros de ligne du debogueur designeraient le
|
|
722
724
|
// code instrumente plutot que la source.
|
|
725
|
+
//
|
|
726
|
+
// `true` demande a Babel de lire la carte inline du code recu. Il l'accepte
|
|
727
|
+
// — verifie : la source reprise est bien le fichier d'origine — mais les
|
|
728
|
+
// types de la 7.x ne decrivent que la forme objet. L'annotation est
|
|
729
|
+
// incomplete, pas la valeur.
|
|
723
730
|
inputSourceMap: true,
|
|
724
731
|
sourceMaps: "inline",
|
|
725
732
|
// `skipEnvCheck` leve un garde-fou destine aux configurations globales,
|
|
@@ -934,7 +941,9 @@ async function startDevServer(config) {
|
|
|
934
941
|
const fournisseur = await fournisseurDe(config.root);
|
|
935
942
|
const utilitaires = fournisseur?.classesConnues === void 0 ? void 0 : fournisseur.renderUtilitairesPour(fournisseur.classesConnues());
|
|
936
943
|
if (utilitaires !== void 0) {
|
|
937
|
-
info(
|
|
944
|
+
info(
|
|
945
|
+
`utilitaires de developpement : ${String(Math.round(utilitaires.length / 1024))} Ko`
|
|
946
|
+
);
|
|
938
947
|
}
|
|
939
948
|
const entries = extractEntries(await readFile(indexFile, "utf8"), config.root);
|
|
940
949
|
const specifiers = await scanDependencies(config, entries);
|
|
@@ -1005,7 +1014,11 @@ async function startDevServer(config) {
|
|
|
1005
1014
|
};
|
|
1006
1015
|
const serveHtml = async (response) => {
|
|
1007
1016
|
const html = await readFile(indexFile, "utf8");
|
|
1008
|
-
send(
|
|
1017
|
+
send(
|
|
1018
|
+
response,
|
|
1019
|
+
injectClient(html, utilitaires !== void 0),
|
|
1020
|
+
MIME[".html"]
|
|
1021
|
+
);
|
|
1009
1022
|
};
|
|
1010
1023
|
const forward = (incoming, response, target) => {
|
|
1011
1024
|
const url2 = new URL(incoming.url ?? "/", target);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { extractEntries, applyAlias, isBareSpecifier, fournisseurDe } from './chunk-
|
|
2
|
+
import { extractEntries, applyAlias, isBareSpecifier, fournisseurDe } from './chunk-2EE5QUCW.js';
|
|
3
3
|
import { info, colors, size, success, duration } from './chunk-JMEHF3KN.js';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { readFile, rm, mkdir, writeFile, cp, stat } from 'fs/promises';
|
package/dist/cli.js
CHANGED
|
@@ -131,7 +131,7 @@ function rootFrom(flags, positional) {
|
|
|
131
131
|
async function run(argv) {
|
|
132
132
|
const { command, positional, flags } = parseArgs(argv);
|
|
133
133
|
if (flags["version"] === true) {
|
|
134
|
-
const manifest = await import('./package-
|
|
134
|
+
const manifest = await import('./package-2JOBLFXE.js');
|
|
135
135
|
console.log(manifest.default.version);
|
|
136
136
|
return 0;
|
|
137
137
|
}
|
|
@@ -142,7 +142,7 @@ async function run(argv) {
|
|
|
142
142
|
switch (command) {
|
|
143
143
|
case "create":
|
|
144
144
|
case "new": {
|
|
145
|
-
const { createCommand } = await import('./create-
|
|
145
|
+
const { createCommand } = await import('./create-VUSKHXBN.js');
|
|
146
146
|
const options = {};
|
|
147
147
|
if (positional[0] !== void 0) options.name = positional[0];
|
|
148
148
|
if (typeof flags["template"] === "string") options.template = flags["template"];
|
|
@@ -156,13 +156,13 @@ async function run(argv) {
|
|
|
156
156
|
return createCommand(options);
|
|
157
157
|
}
|
|
158
158
|
case "dev": {
|
|
159
|
-
const { startDevServer } = await import('./server-
|
|
159
|
+
const { startDevServer } = await import('./server-DXOQAPVX.js');
|
|
160
160
|
const config = await loadConfig(rootFrom(flags, positional), overridesFrom(flags));
|
|
161
161
|
await startDevServer(config);
|
|
162
162
|
return new Promise(() => void 0);
|
|
163
163
|
}
|
|
164
164
|
case "build": {
|
|
165
|
-
const { buildProject, reportBuild } = await import('./build-
|
|
165
|
+
const { buildProject, reportBuild } = await import('./build-KI7IEVRJ.js');
|
|
166
166
|
const config = await loadConfig(rootFrom(flags, positional), overridesFrom(flags));
|
|
167
167
|
const output = await buildProject(config);
|
|
168
168
|
reportBuild(output, process.cwd());
|
|
@@ -444,9 +444,7 @@ async function createCommand(options) {
|
|
|
444
444
|
})
|
|
445
445
|
));
|
|
446
446
|
if (!templates.includes(template)) {
|
|
447
|
-
prompts.cancel(
|
|
448
|
-
`Unknown template: "${template}". Available: ${templates.join(", ")}.`
|
|
449
|
-
);
|
|
447
|
+
prompts.cancel(`Unknown template: "${template}". Available: ${templates.join(", ")}.`);
|
|
450
448
|
return 1;
|
|
451
449
|
}
|
|
452
450
|
const modules = await askModules(options);
|
|
@@ -485,7 +483,9 @@ async function createCommand(options) {
|
|
|
485
483
|
if (overwrite !== void 0) scaffoldOptions.overwrite = overwrite;
|
|
486
484
|
scaffoldOptions.modules = modules;
|
|
487
485
|
const { files } = await scaffold(scaffoldOptions);
|
|
488
|
-
spinner2.stop(
|
|
486
|
+
spinner2.stop(
|
|
487
|
+
`${String(files.length)} files written to ${colors.cyan(basename(target))}`
|
|
488
|
+
);
|
|
489
489
|
if (withGit && !existsSync(resolve(target, ".git"))) {
|
|
490
490
|
try {
|
|
491
491
|
execSync("git init -q", { cwd: target, stdio: "ignore" });
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export { startPreviewServer } from './chunk-34ES2ICQ.js';
|
|
3
3
|
export { defineConfig, loadConfig } from './chunk-DL3NPC4H.js';
|
|
4
4
|
import './chunk-T6RLHSCW.js';
|
|
5
|
-
export { buildProject, reportBuild } from './chunk-
|
|
6
|
-
export { ModuleGraph, depFileName, detectSelfAccepting, optimizeDeps, scanDependencies, startDevServer } from './chunk-
|
|
5
|
+
export { buildProject, reportBuild } from './chunk-FJYUQALD.js';
|
|
6
|
+
export { ModuleGraph, depFileName, detectSelfAccepting, optimizeDeps, scanDependencies, startDevServer } from './chunk-2EE5QUCW.js';
|
|
7
7
|
import './chunk-JMEHF3KN.js';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "odoro",
|
|
5
|
-
version: "1.0.
|
|
5
|
+
version: "1.0.3",
|
|
6
6
|
type: "module",
|
|
7
7
|
license: "UNLICENSED",
|
|
8
8
|
author: "BouBouw",
|
|
@@ -44,13 +44,14 @@ var package_default = {
|
|
|
44
44
|
prepublishOnly: "pnpm run build"
|
|
45
45
|
},
|
|
46
46
|
dependencies: {
|
|
47
|
-
"@babel/core": "^
|
|
47
|
+
"@babel/core": "^7.29.7",
|
|
48
48
|
"@clack/prompts": "^0.11.0",
|
|
49
49
|
esbuild: "^0.28.2",
|
|
50
50
|
picocolors: "^1.1.1",
|
|
51
51
|
"react-refresh": "^0.18.0"
|
|
52
52
|
},
|
|
53
53
|
devDependencies: {
|
|
54
|
+
"@types/babel__core": "^7.20.5",
|
|
54
55
|
"@types/node": "^22.18.8",
|
|
55
56
|
tsup: "^8.5.0",
|
|
56
57
|
typescript: "^5.9.3",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "odoro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "BouBouw",
|
|
@@ -35,13 +35,14 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@babel/core": "^
|
|
38
|
+
"@babel/core": "^7.29.7",
|
|
39
39
|
"@clack/prompts": "^0.11.0",
|
|
40
40
|
"esbuild": "^0.28.2",
|
|
41
41
|
"picocolors": "^1.1.1",
|
|
42
42
|
"react-refresh": "^0.18.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@types/babel__core": "^7.20.5",
|
|
45
46
|
"@types/node": "^22.18.8",
|
|
46
47
|
"tsup": "^8.5.0",
|
|
47
48
|
"typescript": "^5.9.3",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { useState, type ReactElement } from 'react'
|
|
17
|
+
import { useEffect, useState, type ReactElement } from 'react'
|
|
18
18
|
|
|
19
19
|
import { Fond } from '@/fond'
|
|
20
20
|
|
|
@@ -57,19 +57,128 @@ function Barre(): ReactElement {
|
|
|
57
57
|
<span className="marque-mot">ODORO</span>
|
|
58
58
|
</span>
|
|
59
59
|
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
<div className="barre-fin">
|
|
61
|
+
<a
|
|
62
|
+
href="https://odoro.dev"
|
|
63
|
+
target="_blank"
|
|
64
|
+
rel="noreferrer"
|
|
65
|
+
className="gelule gelule-lien"
|
|
66
|
+
>
|
|
67
|
+
odoro.dev
|
|
68
|
+
</a>
|
|
69
|
+
<BasculeTheme />
|
|
70
|
+
</div>
|
|
68
71
|
</div>
|
|
69
72
|
</header>
|
|
70
73
|
)
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
/* -------------------------------------------------------------------------- */
|
|
77
|
+
/* Theme */
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
|
|
80
|
+
/** Les trois etats de la bascule. */
|
|
81
|
+
type Theme = 'systeme' | 'clair' | 'sombre'
|
|
82
|
+
|
|
83
|
+
/** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
|
|
84
|
+
const CLE_THEME = 'odoro-theme'
|
|
85
|
+
|
|
86
|
+
/** L'ordre du cycle, au clic. */
|
|
87
|
+
const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pose le theme sur la racine du document.
|
|
91
|
+
*
|
|
92
|
+
* `systeme` retire l'attribut plutot que d'en poser un troisieme : la feuille
|
|
93
|
+
* de style ecoute `data-theme`, et son absence rend la main a la preference du
|
|
94
|
+
* navigateur.
|
|
95
|
+
*/
|
|
96
|
+
function poserTheme(theme: Theme): void {
|
|
97
|
+
if (theme === 'systeme') delete document.documentElement.dataset.theme
|
|
98
|
+
else document.documentElement.dataset.theme = theme === 'clair' ? 'light' : 'dark'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Le theme memorise, ou `systeme`. */
|
|
102
|
+
function themeMemorise(): Theme {
|
|
103
|
+
try {
|
|
104
|
+
const valeur = localStorage.getItem(CLE_THEME)
|
|
105
|
+
if (valeur === 'light') return 'clair'
|
|
106
|
+
if (valeur === 'dark') return 'sombre'
|
|
107
|
+
return 'systeme'
|
|
108
|
+
} catch {
|
|
109
|
+
// Stockage refuse — fenetre privee, cookies bloques. Le theme du systeme
|
|
110
|
+
// reste une reponse valable.
|
|
111
|
+
return 'systeme'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Les trois pictogrammes, dessines dans le flux. */
|
|
116
|
+
function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
|
|
117
|
+
const commun = {
|
|
118
|
+
viewBox: '0 0 24 24',
|
|
119
|
+
fill: 'none',
|
|
120
|
+
stroke: 'currentColor',
|
|
121
|
+
strokeWidth: 1.8,
|
|
122
|
+
strokeLinecap: 'round' as const,
|
|
123
|
+
strokeLinejoin: 'round' as const,
|
|
124
|
+
'aria-hidden': true,
|
|
125
|
+
style: { width: '1.15rem', height: '1.15rem' },
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (theme === 'clair') {
|
|
129
|
+
return (
|
|
130
|
+
<svg {...commun}>
|
|
131
|
+
<circle cx="12" cy="12" r="4" />
|
|
132
|
+
<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" />
|
|
133
|
+
</svg>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (theme === 'sombre') {
|
|
138
|
+
return (
|
|
139
|
+
<svg {...commun}>
|
|
140
|
+
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
|
|
141
|
+
</svg>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<svg {...commun}>
|
|
147
|
+
<rect x="2" y="4" width="20" height="13" rx="2" />
|
|
148
|
+
<path d="M8 21h8M12 17v4" />
|
|
149
|
+
</svg>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** La bascule de theme : systeme, clair, sombre. */
|
|
154
|
+
function BasculeTheme(): ReactElement {
|
|
155
|
+
const [theme, setTheme] = useState<Theme>(() => themeMemorise())
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
poserTheme(theme)
|
|
159
|
+
try {
|
|
160
|
+
if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
|
|
161
|
+
else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
|
|
162
|
+
} catch {
|
|
163
|
+
// Le theme reste applique pour la session, meme sans stockage.
|
|
164
|
+
}
|
|
165
|
+
}, [theme])
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
aria-label={`Theme : ${theme}`}
|
|
171
|
+
title={`Theme : ${theme}`}
|
|
172
|
+
className="gelule bascule"
|
|
173
|
+
onClick={() => {
|
|
174
|
+
setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<Pictogramme theme={theme} />
|
|
178
|
+
</button>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
73
182
|
/* -------------------------------------------------------------------------- */
|
|
74
183
|
/* Sections */
|
|
75
184
|
/* -------------------------------------------------------------------------- */
|
|
@@ -249,10 +358,12 @@ function Cloture(): ReactElement {
|
|
|
249
358
|
/** Racine de l'application. */
|
|
250
359
|
export function App(): ReactElement {
|
|
251
360
|
return (
|
|
252
|
-
|
|
361
|
+
// Le fond est pose ici et non dans le contenu : ancre plus bas, il
|
|
362
|
+
// commencait sous la barre et laissait une bande plus sombre derriere elle.
|
|
363
|
+
<div className="app-shell app-shell-fond">
|
|
364
|
+
<Fond />
|
|
253
365
|
<Barre />
|
|
254
366
|
<main className="principal">
|
|
255
|
-
<Fond />
|
|
256
367
|
<Hero />
|
|
257
368
|
<Piliers />
|
|
258
369
|
<Cloture />
|
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
color-scheme: light dark;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/*
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/* Les valeurs du theme sombre, ecrites une fois et reprises deux fois.
|
|
25
|
+
|
|
26
|
+
La bascule pose `data-theme` sur la racine ; sans attribut, la preference du
|
|
27
|
+
navigateur decide. Les deux regles sont donc necessaires : la seconde sans la
|
|
28
|
+
premiere ignorerait le choix du visiteur, la premiere sans la seconde
|
|
29
|
+
rendrait un fond blanc au milieu de la nuit. */
|
|
27
30
|
@media (prefers-color-scheme: dark) {
|
|
28
|
-
:root {
|
|
31
|
+
:root:not([data-theme='light']) {
|
|
29
32
|
--fond: #09090b;
|
|
30
33
|
--encre: #fafafa;
|
|
31
34
|
--doux: #a1a1aa;
|
|
@@ -34,6 +37,25 @@
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
:root[data-theme='dark'] {
|
|
41
|
+
--fond: #09090b;
|
|
42
|
+
--encre: #fafafa;
|
|
43
|
+
--doux: #a1a1aa;
|
|
44
|
+
--filet: #27272a;
|
|
45
|
+
--surface: #18181b;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Le choix explicite est dit au navigateur : sans cela, `light-dark()`, les
|
|
49
|
+
controles natifs et les barres de defilement continuent de suivre un systeme
|
|
50
|
+
en nuit alors que le visiteur a demande le clair. */
|
|
51
|
+
:root[data-theme='light'] {
|
|
52
|
+
color-scheme: light;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:root[data-theme='dark'] {
|
|
56
|
+
color-scheme: dark;
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
* {
|
|
38
60
|
box-sizing: border-box;
|
|
39
61
|
}
|
|
@@ -93,12 +115,30 @@ pre {
|
|
|
93
115
|
text-decoration: none;
|
|
94
116
|
}
|
|
95
117
|
|
|
96
|
-
.
|
|
118
|
+
.barre-fin {
|
|
97
119
|
margin-left: auto;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 0.75rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.gelule-lien {
|
|
98
126
|
color: var(--doux);
|
|
99
127
|
font-size: 0.875rem;
|
|
100
128
|
}
|
|
101
129
|
|
|
130
|
+
.bascule {
|
|
131
|
+
width: 3.5rem;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
padding: 0;
|
|
134
|
+
color: var(--doux);
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.bascule:hover {
|
|
139
|
+
color: var(--encre);
|
|
140
|
+
}
|
|
141
|
+
|
|
102
142
|
.gelule-lien:hover {
|
|
103
143
|
color: var(--encre);
|
|
104
144
|
}
|
|
@@ -125,6 +165,10 @@ pre {
|
|
|
125
165
|
|
|
126
166
|
/* ----- Coquille ----------------------------------------------------------- */
|
|
127
167
|
|
|
168
|
+
.app-shell-fond {
|
|
169
|
+
position: relative;
|
|
170
|
+
}
|
|
171
|
+
|
|
128
172
|
.principal {
|
|
129
173
|
position: relative;
|
|
130
174
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { Reveal, Stagger } from '@odoro-cli/libs/motion'
|
|
23
23
|
import { buttonClasses } from '@odoro-cli/libs/ui'
|
|
24
|
-
import { useState, type CSSProperties, type ReactElement } from 'react'
|
|
24
|
+
import { useEffect, useState, type CSSProperties, type ReactElement } from 'react'
|
|
25
25
|
|
|
26
26
|
import { Fond } from '@/fond'
|
|
27
27
|
|
|
@@ -87,20 +87,137 @@ function Barre(): ReactElement {
|
|
|
87
87
|
</span>
|
|
88
88
|
</span>
|
|
89
89
|
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
<div className="o-ml-auto o-flex o-items-center o-gap-3">
|
|
91
|
+
<a
|
|
92
|
+
href="https://odoro.dev"
|
|
93
|
+
target="_blank"
|
|
94
|
+
rel="noreferrer"
|
|
95
|
+
className="o-inline-flex o-h-14 o-shrink-0 o-items-center o-rounded-full o-border-w-1 o-px-5 o-text-sm o-no-underline o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
|
|
96
|
+
style={GELULE}
|
|
97
|
+
>
|
|
98
|
+
odoro.dev
|
|
99
|
+
</a>
|
|
100
|
+
<BasculeTheme />
|
|
101
|
+
</div>
|
|
99
102
|
</div>
|
|
100
103
|
</header>
|
|
101
104
|
)
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
/* Theme */
|
|
109
|
+
/* -------------------------------------------------------------------------- */
|
|
110
|
+
|
|
111
|
+
/** Les trois etats de la bascule. */
|
|
112
|
+
type Theme = 'systeme' | 'clair' | 'sombre'
|
|
113
|
+
|
|
114
|
+
/** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
|
|
115
|
+
const CLE_THEME = 'odoro-theme'
|
|
116
|
+
|
|
117
|
+
/** L'ordre du cycle, au clic. */
|
|
118
|
+
const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pose le theme sur la racine du document.
|
|
122
|
+
*
|
|
123
|
+
* `systeme` retire l'attribut plutot que d'en poser un troisieme : la feuille
|
|
124
|
+
* de style ecoute `data-theme`, et son absence rend la main a la preference du
|
|
125
|
+
* navigateur. C'est ce qui permet a une page laissee en « systeme » de suivre
|
|
126
|
+
* le passage en nuit sans etre rouverte.
|
|
127
|
+
*/
|
|
128
|
+
function poserTheme(theme: Theme): void {
|
|
129
|
+
if (theme === 'systeme') delete document.documentElement.dataset.theme
|
|
130
|
+
else document.documentElement.dataset.theme = theme === 'clair' ? 'light' : 'dark'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Le theme memorise, ou `systeme`. */
|
|
134
|
+
function themeMemorise(): Theme {
|
|
135
|
+
try {
|
|
136
|
+
const valeur = localStorage.getItem(CLE_THEME)
|
|
137
|
+
if (valeur === 'light') return 'clair'
|
|
138
|
+
if (valeur === 'dark') return 'sombre'
|
|
139
|
+
return 'systeme'
|
|
140
|
+
} catch {
|
|
141
|
+
// Stockage refuse — fenetre privee, cookies bloques. Le theme du systeme
|
|
142
|
+
// reste une reponse valable ; echouer ici priverait la page de son rendu.
|
|
143
|
+
return 'systeme'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Les trois pictogrammes, dessines plutot qu'importes : les icones sont optionnelles. */
|
|
148
|
+
function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
|
|
149
|
+
const commun = {
|
|
150
|
+
viewBox: '0 0 24 24',
|
|
151
|
+
fill: 'none',
|
|
152
|
+
stroke: 'currentColor',
|
|
153
|
+
strokeWidth: 1.8,
|
|
154
|
+
strokeLinecap: 'round' as const,
|
|
155
|
+
strokeLinejoin: 'round' as const,
|
|
156
|
+
'aria-hidden': true,
|
|
157
|
+
style: { width: '1.15rem', height: '1.15rem' },
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (theme === 'clair') {
|
|
161
|
+
return (
|
|
162
|
+
<svg {...commun}>
|
|
163
|
+
<circle cx="12" cy="12" r="4" />
|
|
164
|
+
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
|
|
165
|
+
</svg>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (theme === 'sombre') {
|
|
170
|
+
return (
|
|
171
|
+
<svg {...commun}>
|
|
172
|
+
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
|
|
173
|
+
</svg>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<svg {...commun}>
|
|
179
|
+
<rect x="2" y="4" width="20" height="13" rx="2" />
|
|
180
|
+
<path d="M8 21h8M12 17v4" />
|
|
181
|
+
</svg>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* La bascule de theme.
|
|
187
|
+
*
|
|
188
|
+
* Trois etats et non deux : « systeme » est un choix a part entiere, et le
|
|
189
|
+
* retirer obligerait un visiteur a re-choisir a chaque changement de sa
|
|
190
|
+
* preference.
|
|
191
|
+
*/
|
|
192
|
+
function BasculeTheme(): ReactElement {
|
|
193
|
+
const [theme, setTheme] = useState<Theme>(() => themeMemorise())
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
poserTheme(theme)
|
|
197
|
+
try {
|
|
198
|
+
if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
|
|
199
|
+
else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
|
|
200
|
+
} catch {
|
|
201
|
+
// Le theme reste applique pour la session, meme sans stockage.
|
|
202
|
+
}
|
|
203
|
+
}, [theme])
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
aria-label={`Theme : ${theme}`}
|
|
209
|
+
title={`Theme : ${theme}`}
|
|
210
|
+
onClick={() => {
|
|
211
|
+
setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
|
|
212
|
+
}}
|
|
213
|
+
className="o-inline-flex o-size-14 o-shrink-0 o-cursor-pointer o-items-center o-justify-center o-rounded-full o-border-w-1 o-backdrop-blur-xl o-text-zinc-600 dark:o-text-zinc-300 hover:o-text-zinc-950 dark:hover:o-text-white"
|
|
214
|
+
style={GELULE}
|
|
215
|
+
>
|
|
216
|
+
<Pictogramme theme={theme} />
|
|
217
|
+
</button>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
104
221
|
/* -------------------------------------------------------------------------- */
|
|
105
222
|
/* Sections */
|
|
106
223
|
/* -------------------------------------------------------------------------- */
|
|
@@ -312,10 +429,13 @@ function Cloture(): ReactElement {
|
|
|
312
429
|
/** Racine de l'application. */
|
|
313
430
|
export function App(): ReactElement {
|
|
314
431
|
return (
|
|
315
|
-
|
|
432
|
+
// `relative` porte le fond, pose ici et non dans le contenu : ancre plus
|
|
433
|
+
// bas, il commencait sous la barre et laissait une bande plus sombre
|
|
434
|
+
// derriere elle en haut de page.
|
|
435
|
+
<div className="app-shell o-relative">
|
|
436
|
+
<Fond />
|
|
316
437
|
<Barre />
|
|
317
438
|
<main className="o-relative">
|
|
318
|
-
<Fond />
|
|
319
439
|
<Hero />
|
|
320
440
|
<Piliers />
|
|
321
441
|
<Cloture />
|
|
@@ -4,7 +4,18 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<meta name="theme-color" content="#3b82f6" />
|
|
7
8
|
<title>Odoro</title>
|
|
9
|
+
<script>
|
|
10
|
+
// Applique le theme memorise avant la premiere peinture. Le faire depuis
|
|
11
|
+
// React arriverait trop tard : un visiteur en theme force verrait un
|
|
12
|
+
// eclair de l'autre theme, le temps que le script se charge.
|
|
13
|
+
try {
|
|
14
|
+
var theme = localStorage.getItem('odoro-theme')
|
|
15
|
+
if (theme === 'light' || theme === 'dark')
|
|
16
|
+
document.documentElement.dataset.theme = theme
|
|
17
|
+
} catch (error) {}
|
|
18
|
+
</script>
|
|
8
19
|
</head>
|
|
9
20
|
<body>
|
|
10
21
|
<div id="root"></div>
|