diff --git a/index.html b/index.html index f013251..eaecf38 100644 --- a/index.html +++ b/index.html @@ -4,8 +4,33 @@ Clément Saillant — LE BUS + + +
+

+

+
+ +
+ +
+ +
+
+ +
+

+ Version animée : ce site est une demo 3D (un bus sur un PCB). + Activez JavaScript / les animations pour monter dedans. +

+
diff --git a/package-lock.json b/package-lock.json index d082371..f028890 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@playwright/test": "^1.53", + "@types/node": "^26.1.1", "@types/react": "^19", "@types/react-dom": "^19", "@types/three": "^0.177.0", @@ -1415,6 +1416,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", @@ -2758,6 +2769,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", diff --git a/package.json b/package.json index 2840ba0..dc55d60 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ }, "devDependencies": { "@playwright/test": "^1.53", + "@types/node": "^26.1.1", "@types/react": "^19", "@types/react-dom": "^19", "@types/three": "^0.177.0", diff --git a/src/boot.ts b/src/boot.ts new file mode 100644 index 0000000..84a783a --- /dev/null +++ b/src/boot.ts @@ -0,0 +1,3 @@ +export function boot(): void { + console.log('demo boot placeholder — replaced by Task 5') +} diff --git a/src/fallback/inject.test.ts b/src/fallback/inject.test.ts new file mode 100644 index 0000000..4135ad9 --- /dev/null +++ b/src/fallback/inject.test.ts @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { renderFallbackHtml } from './inject' +import { LINKS } from '../content/links' + +const template = readFileSync('index.html', 'utf8') + +describe('renderFallbackHtml', () => { + it('injects every identity link into the raw HTML', () => { + const out = renderFallbackHtml(template) + for (const l of LINKS) expect(out).toContain(`href="${l.href}"`) + }) + it('injects the rel=me anchor for Mastodon', () => { + const out = renderFallbackHtml(template) + expect(out).toMatch( + /]*href="https:\/\/mastodon\.saillant\.cc\/@clement"[^>]*rel="me"/, + ) + }) + it('leaves no marker comments behind', () => { + const out = renderFallbackHtml(template) + expect(out).not.toMatch(//) + }) +}) diff --git a/src/fallback/inject.ts b/src/fallback/inject.ts new file mode 100644 index 0000000..a8a2948 --- /dev/null +++ b/src/fallback/inject.ts @@ -0,0 +1,23 @@ +import { BIO_LINES, NAME, TAGLINE } from '../content/bio' +import { GREETINGS } from '../content/greetings' +import { LINKS } from '../content/links' +import { MUSIC } from '../content/music' + +export function renderFallbackHtml(html: string): string { + const nav = LINKS.map( + (l) => + `${l.label}`, + ).join('\n ') + const bio = BIO_LINES.map((line) => `

${line}

`).join('\n ') + const music = + `

${MUSIC.title}

${MUSIC.blurb}

` + + `

${MUSIC.listenLabel}

` + const greetings = `

Greetings to ${GREETINGS.join(', ')}.

` + return html + .replace('', NAME) + .replace('', TAGLINE) + .replace('', nav) + .replace('', bio) + .replace('', music) + .replace('', greetings) +} diff --git a/src/main.tsx b/src/main.tsx index 3d4c112..1bca0e5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,15 @@ import './style.css' -console.log('LE BUS — scaffold OK') +function canRunDemo(): boolean { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return false + const gl = document.createElement('canvas').getContext('webgl2') + return gl !== null +} + +if (canRunDemo()) { + document.body.classList.add('demo-on') + // App is mounted in Task 5; for now the gate is in place. + import('./boot').then((m) => m.boot()).catch(() => { + document.body.classList.remove('demo-on') + }) +} diff --git a/src/style.css b/src/style.css index 0fcd296..4f83ce5 100644 --- a/src/style.css +++ b/src/style.css @@ -1,2 +1,32 @@ :root { color-scheme: dark; } body { margin: 0; background: #05010d; color: #ffd23f; font-family: "Courier New", monospace; } + +#fallback { + max-width: 42rem; + margin: 0 auto; + padding: 2rem 1rem 4rem; + line-height: 1.5; +} +#fallback h1 { color: #ff5c00; font-size: 2.2rem; margin-bottom: 0; } +#fallback .tagline { margin-top: 0.2rem; color: #9be564; } +#fallback nav a { + display: block; + padding: 0.6rem 0.8rem; + margin: 0.4rem 0; + border: 3px solid #ffd23f; + color: #ffd23f; + text-decoration: none; + font-weight: bold; +} +#fallback nav a:hover, #fallback nav a:focus { background: #ffd23f; color: #05010d; } +#fallback a { color: #9be564; } +#fallback .hint { color: #666; font-size: 0.85rem; } +/* When the 3D demo is on, keep the fallback for screen readers only */ +body.demo-on #fallback { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} diff --git a/vite.config.ts b/vite.config.ts index 9ffcc67..def65cb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,14 @@ -import { defineConfig } from 'vite' +import { defineConfig, type Plugin } from 'vite' import react from '@vitejs/plugin-react' +import { renderFallbackHtml } from './src/fallback/inject' + +function fallbackPlugin(): Plugin { + return { + name: 'inject-fallback', + transformIndexHtml: (html) => renderFallbackHtml(html), + } +} export default defineConfig({ - plugins: [react()], + plugins: [react(), fallbackPlugin()], })