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.
@@ -23,7 +23,17 @@
23
23
 
24
24
  import { Reveal, Stagger } from '@odoro-cli/libs/motion'
25
25
  import { buttonClasses } from '@odoro-cli/libs/ui'
26
- import { useState, type CSSProperties, type ReactElement, type ReactNode } from 'react'
26
+ import {
27
+ useEffect,
28
+ useState,
29
+ type CSSProperties,
30
+ type ReactElement,
31
+ type ReactNode,
32
+ } from 'react'
33
+
34
+ // Nommes plutot qu'importes en bloc : le reste du manifeste — les scripts,
35
+ // les metadonnees — n'a rien a faire dans le code livre au navigateur.
36
+ import { dependencies, devDependencies } from '../package.json'
27
37
 
28
38
  import { Fond } from '@/fond'
29
39
  import { Link, Routeur, useLocation } from '@/router'
@@ -133,20 +143,137 @@ function Barre(): ReactElement {
133
143
  ))}
134
144
  </nav>
135
145
 
136
- <a
137
- href="https://odoro.dev"
138
- target="_blank"
139
- rel="noreferrer"
140
- className="o-ml-auto 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"
141
- style={GELULE}
142
- >
143
- odoro.dev
144
- </a>
146
+ <div className="o-ml-auto o-flex o-items-center o-gap-3">
147
+ <a
148
+ href="https://odoro.dev"
149
+ target="_blank"
150
+ rel="noreferrer"
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={GELULE}
153
+ >
154
+ odoro.dev
155
+ </a>
156
+ <BasculeTheme />
157
+ </div>
145
158
  </div>
146
159
  </header>
147
160
  )
148
161
  }
149
162
 
163
+ /* -------------------------------------------------------------------------- */
164
+ /* Theme */
165
+ /* -------------------------------------------------------------------------- */
166
+
167
+ /** Les trois etats de la bascule. */
168
+ type Theme = 'systeme' | 'clair' | 'sombre'
169
+
170
+ /** Ou le choix est memorise. Le script de `index.html` lit la meme cle. */
171
+ const CLE_THEME = 'odoro-theme'
172
+
173
+ /** L'ordre du cycle, au clic. */
174
+ const CYCLE: readonly Theme[] = ['systeme', 'clair', 'sombre']
175
+
176
+ /**
177
+ * Pose le theme sur la racine du document.
178
+ *
179
+ * `systeme` retire l'attribut plutot que d'en poser un troisieme : la feuille
180
+ * de style ecoute `data-theme`, et son absence rend la main a la preference du
181
+ * navigateur. C'est ce qui permet a une page laissee en « systeme » de suivre
182
+ * le passage en nuit sans etre rouverte.
183
+ */
184
+ function poserTheme(theme: Theme): void {
185
+ if (theme === 'systeme') delete document.documentElement.dataset.theme
186
+ else document.documentElement.dataset.theme = theme === 'clair' ? 'light' : 'dark'
187
+ }
188
+
189
+ /** Le theme memorise, ou `systeme`. */
190
+ function themeMemorise(): Theme {
191
+ try {
192
+ const valeur = localStorage.getItem(CLE_THEME)
193
+ if (valeur === 'light') return 'clair'
194
+ if (valeur === 'dark') return 'sombre'
195
+ return 'systeme'
196
+ } catch {
197
+ // Stockage refuse — fenetre privee, cookies bloques. Le theme du systeme
198
+ // reste une reponse valable ; echouer ici priverait la page de son rendu.
199
+ return 'systeme'
200
+ }
201
+ }
202
+
203
+ /** Les trois pictogrammes, dessines plutot qu'importes : les icones sont optionnelles. */
204
+ function Pictogramme({ theme }: { readonly theme: Theme }): ReactElement {
205
+ const commun = {
206
+ viewBox: '0 0 24 24',
207
+ fill: 'none',
208
+ stroke: 'currentColor',
209
+ strokeWidth: 1.8,
210
+ strokeLinecap: 'round' as const,
211
+ strokeLinejoin: 'round' as const,
212
+ 'aria-hidden': true,
213
+ style: { width: '1.15rem', height: '1.15rem' },
214
+ }
215
+
216
+ if (theme === 'clair') {
217
+ return (
218
+ <svg {...commun}>
219
+ <circle cx="12" cy="12" r="4" />
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
+ </svg>
222
+ )
223
+ }
224
+
225
+ if (theme === 'sombre') {
226
+ return (
227
+ <svg {...commun}>
228
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />
229
+ </svg>
230
+ )
231
+ }
232
+
233
+ return (
234
+ <svg {...commun}>
235
+ <rect x="2" y="4" width="20" height="13" rx="2" />
236
+ <path d="M8 21h8M12 17v4" />
237
+ </svg>
238
+ )
239
+ }
240
+
241
+ /**
242
+ * La bascule de theme.
243
+ *
244
+ * Trois etats et non deux : « systeme » est un choix a part entiere, et le
245
+ * retirer obligerait un visiteur a re-choisir a chaque changement de sa
246
+ * preference.
247
+ */
248
+ function BasculeTheme(): ReactElement {
249
+ const [theme, setTheme] = useState<Theme>(() => themeMemorise())
250
+
251
+ useEffect(() => {
252
+ poserTheme(theme)
253
+ try {
254
+ if (theme === 'systeme') localStorage.removeItem(CLE_THEME)
255
+ else localStorage.setItem(CLE_THEME, theme === 'clair' ? 'light' : 'dark')
256
+ } catch {
257
+ // Le theme reste applique pour la session, meme sans stockage.
258
+ }
259
+ }, [theme])
260
+
261
+ return (
262
+ <button
263
+ type="button"
264
+ aria-label={`Theme : ${theme}`}
265
+ title={`Theme : ${theme}`}
266
+ onClick={() => {
267
+ setTheme((actuel) => CYCLE[(CYCLE.indexOf(actuel) + 1) % CYCLE.length] ?? 'systeme')
268
+ }}
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={GELULE}
271
+ >
272
+ <Pictogramme theme={theme} />
273
+ </button>
274
+ )
275
+ }
276
+
150
277
  /* -------------------------------------------------------------------------- */
151
278
  /* Sections de la page d'accueil */
152
279
  /* -------------------------------------------------------------------------- */
@@ -366,7 +493,6 @@ function Cloture(): ReactElement {
366
493
  function Accueil(): ReactElement {
367
494
  return (
368
495
  <>
369
- <Fond />
370
496
  <Hero />
371
497
  <Piliers />
372
498
  <Cloture />
@@ -374,12 +500,32 @@ function Accueil(): ReactElement {
374
500
  )
375
501
  }
376
502
 
377
- /** Ce que le projet embarque, tel qu'il a ete cree. */
378
- const REPERES = [
379
- { cle: 'Moteur', valeur: 'odoro — serveur de developpement et compilation' },
380
- { cle: 'Styles', valeur: '@odoro-cli/libs jetons, utilitaires, composants' },
381
- { cle: 'Registre', valeur: 'register.odoro.devcopie dans votre projet' },
382
- ]
503
+ /**
504
+ * Ce que chaque paquet de la famille apporte.
505
+ *
506
+ * La table decrit ; elle ne decide pas. Ce qui s'affiche vient du manifeste du
507
+ * projet, donc de ce qui a reellement ete installe et cela reste vrai si vous
508
+ * ajoutez ou retirez un paquet plus tard.
509
+ */
510
+ const ROLES: Readonly<Record<string, string>> = {
511
+ odoro: 'Serveur de developpement, compilation et registre',
512
+ '@odoro-cli/libs': 'Jetons, utilitaires, composants et routeur',
513
+ '@odoro-cli/icons': 'Cinq familles d icones, importables une a une',
514
+ '@odoro-cli/engine': 'WebGL, surfaces et politique de mouvement',
515
+ '@odoro-cli/server': 'Socle back-end modulaire',
516
+ }
517
+
518
+ /**
519
+ * Les paquets de la famille presents dans ce projet.
520
+ *
521
+ * Les deux champs sont lus, car `odoro` est une dependance de developpement
522
+ * quand les autres sont des dependances de production. Une liste ecrite a la
523
+ * main mentirait des la premiere case decochee a la creation.
524
+ */
525
+ const REPERES = Object.entries({ ...dependencies, ...devDependencies })
526
+ .filter(([nom]) => nom in ROLES)
527
+ .sort(([a], [b]) => a.localeCompare(b))
528
+ .map(([nom, version]) => ({ nom, version, role: ROLES[nom] ?? '' }))
383
529
 
384
530
  /** Une seconde page, pour montrer le routeur a l'oeuvre. */
385
531
  function APropos(): ReactElement {
@@ -396,12 +542,17 @@ function APropos(): ReactElement {
396
542
  <dl className="o-mt-10 o-border-t o-border-zinc-200 dark:o-border-zinc-800">
397
543
  {REPERES.map((repere) => (
398
544
  <div
399
- key={repere.cle}
545
+ key={repere.nom}
400
546
  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"
401
547
  >
402
- <dt className="o-w-32 o-shrink-0 o-text-sm o-font-medium">{repere.cle}</dt>
548
+ <dt className="o-w-56 o-shrink-0 o-font-mono o-text-sm o-font-medium">
549
+ {repere.nom}
550
+ <span className="o-ml-2 o-text-xs o-font-normal o-text-zinc-400">
551
+ {repere.version}
552
+ </span>
553
+ </dt>
403
554
  <dd className="o-m-0 o-text-sm o-text-zinc-500 dark:o-text-zinc-400">
404
- {repere.valeur}
555
+ {repere.role}
405
556
  </dd>
406
557
  </div>
407
558
  ))}
@@ -434,9 +585,13 @@ function Introuvable(): ReactElement {
434
585
  /** L'enveloppe commune : barre, contenu, pied de page. */
435
586
  function Coquille(contenu: ReactNode): ReactElement {
436
587
  return (
437
- <div className="app-shell">
588
+ // `relative` porte le fond decoratif, qui se place en absolu dedans. Il est
589
+ // pose ici et non dans la page : ancre au haut de `main`, il commencait
590
+ // sous la barre, et la bande restee derriere elle se voyait en haut de
591
+ // page comme un rectangle plus sombre.
592
+ <div className="app-shell o-relative">
593
+ <Fond />
438
594
  <Barre />
439
- {/* `relative` porte le fond decoratif, qui se place en absolu dedans. */}
440
595
  <main className="o-relative o-view-transition-page">{contenu}</main>
441
596
  <footer className="o-border-t o-border-zinc-200 dark:o-border-zinc-800 o-px-6 o-py-8">
442
597
  <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">
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- export { buildProject, reportBuild } from './chunk-CJIUP7A3.js';
3
- import './chunk-R6PMAMXA.js';
4
- import './chunk-JMEHF3KN.js';