feat: refonte OKI — constellation, carte SVG déterministe, timeline interactive
- Background : shader Three.js remplacé par la constellation zetwal (DOM/SVG, zéro WebGL) - Carte : graphe 3D aléatoire remplacé par un SVG 2D au layout baké au build (scripts/build-layout.mjs, positions déterministes), nœuds focusables, pan/zoom, filtres sans re-layout - Timeline : 4 ères, reveal syncopé gwoka, liens événement→fiche catalogue (module software-modal), mois affichés, corrections factuelles (Twitter 2022, Mastodon 2016, PeerTube 2017, Bluesky 2024, 105 logiciels) - Charte OKI : tokens + bridge, Archivo/Inter self-hébergées, flag-bar, KineticText, footer fédéré - Dette : three retiré (JS initial ~828 Ko → 77 Ko), OG/Twitter/JSON-LD/robots/sitemap, licence CC BY-SA 4.0, fediverse.json mort supprimé, icônes PWA 121 Ko → 9 Ko - Lighthouse mobile : 91/97/100/100 — tests vitest 6/6, svelte-check 0/0
@@ -9,15 +9,14 @@
|
||||
"version": "4.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-force": "^3.0.0",
|
||||
"three": "^0.185.1"
|
||||
"@fontsource/archivo": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.70.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/three": "^0.185.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"svelte": "^5.56.6",
|
||||
"svelte-check": "^4.7.3",
|
||||
"typescript": "^6.0.3",
|
||||
@@ -25,13 +24,6 @@
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
|
||||
@@ -66,6 +58,15 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource/archivo": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/archivo/-/archivo-5.3.0.tgz",
|
||||
"integrity": "sha512-5DIMgPVJRi62OqdOVoogCFxP73EkNM/E0YVTSDIQlDEJfDbxqZduwM/YoNZwC9Sx1CDgpKinqf8ckRY1QNIecw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -515,13 +516,6 @@
|
||||
"vite": "^8.0.0-beta.7 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||
@@ -572,28 +566,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.185.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.1.tgz",
|
||||
"integrity": "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
@@ -601,13 +573,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
|
||||
@@ -821,6 +786,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -830,6 +796,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
||||
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
@@ -844,6 +811,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -853,6 +821,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -955,13 +924,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1275,13 +1237,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
|
||||
"integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -1558,12 +1513,6 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.185.1",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz",
|
||||
"integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "atlas-fediverse",
|
||||
"version": "4.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"dev": "vite dev",
|
||||
@@ -12,19 +11,18 @@
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"license": "CC-BY-SA-4.0",
|
||||
"type": "module",
|
||||
"private": "true",
|
||||
"dependencies": {
|
||||
"d3-force": "^3.0.0",
|
||||
"three": "^0.185.1"
|
||||
"@fontsource/archivo": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.70.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/three": "^0.185.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"svelte": "^5.56.6",
|
||||
"svelte-check": "^4.7.3",
|
||||
"typescript": "^6.0.3",
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env node
|
||||
/* build-layout.mjs — pré-calcule les positions 2D de la carte de l'atlas.
|
||||
*
|
||||
* ⚠️ REJOUER CE SCRIPT À CHAQUE MODIFICATION de `src/lib/data/network.json` :
|
||||
* node scripts/build-layout.mjs
|
||||
* puis committer `src/lib/data/network-layout.json` avec les données.
|
||||
*
|
||||
* La simulation d3-force est entièrement déterministe :
|
||||
* - positions initiales dérivées d'un hash stable de l'id (FNV-1a), pas de
|
||||
* Math.random() — même entrée ⇒ même sortie, bit pour bit ;
|
||||
* - ticks synchrones à nombre fixe (simulation.stop() + tick()), indépendants
|
||||
* de l'horloge et de l'environnement.
|
||||
*
|
||||
* Sortie : src/lib/data/network-layout.json
|
||||
* { viewBox: { width, height }, nodes: { [id]: { x, y } } }
|
||||
* coordonnées normalisées dans le viewBox, arrondies à 0.1 près.
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
forceSimulation,
|
||||
forceLink,
|
||||
forceManyBody,
|
||||
forceCollide,
|
||||
forceX,
|
||||
forceY
|
||||
} from 'd3-force';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const networkPath = join(root, 'src/lib/data/network.json');
|
||||
const outPath = join(root, 'src/lib/data/network-layout.json');
|
||||
|
||||
const network = JSON.parse(readFileSync(networkPath, 'utf8'));
|
||||
|
||||
/** Hash FNV-1a 32 bits — graine stable par id, indépendante de la plateforme. */
|
||||
function hashId(str) {
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h ^= str.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return h >>> 0;
|
||||
}
|
||||
|
||||
/* ————— Positions initiales déterministes —————
|
||||
Chaque catégorie occupe un secteur d'un grand cercle ; les nœuds sont
|
||||
dispersés autour du centre de leur catégorie selon leur hash. La simulation
|
||||
part ainsi d'un état proche de la topologie finale (convergence rapide,
|
||||
clusters lisibles). */
|
||||
const categories = [...new Set(network.nodes.map((n) => n.category))].sort();
|
||||
const catIndex = new Map(categories.map((c, i) => [c, i]));
|
||||
// Ellipse large (ratio proche du viewBox 1200×800) : les clusters de
|
||||
// catégories s'y répartissent et la carte remplit le cadre.
|
||||
const CLUSTER_RX = 340;
|
||||
const CLUSTER_RY = 210;
|
||||
const JITTER_RADIUS = 60;
|
||||
|
||||
/** Cible de placement d'une catégorie sur l'ellipse. */
|
||||
function categoryTarget(category) {
|
||||
const angle = (2 * Math.PI * catIndex.get(category)) / categories.length;
|
||||
return { x: CLUSTER_RX * Math.cos(angle), y: CLUSTER_RY * Math.sin(angle) };
|
||||
}
|
||||
|
||||
const nodes = network.nodes.map((n, i) => {
|
||||
const h = hashId(n.id);
|
||||
const target = categoryTarget(n.category);
|
||||
const jAngle = ((h % 1000) / 1000) * 2 * Math.PI;
|
||||
const jRadius = (((h >> 10) % 1000) / 1000) * JITTER_RADIUS;
|
||||
return {
|
||||
...n,
|
||||
index: i,
|
||||
x: target.x + jRadius * Math.cos(jAngle),
|
||||
y: target.y + jRadius * Math.sin(jAngle)
|
||||
};
|
||||
});
|
||||
|
||||
const links = network.links.map((l) => ({
|
||||
source: l.source,
|
||||
target: l.target,
|
||||
type: l.type
|
||||
}));
|
||||
|
||||
/* ————— Paramètres de forces —————
|
||||
- liens « strong » courts (30) pour souder les communautés, « weak »
|
||||
longs (70) pour espacer les ponts inter-catégories ;
|
||||
- répulsion modérée (-45) : le graphe est dense (873 liens / 105 nœuds),
|
||||
une charge trop forte éclaterait les clusters ;
|
||||
- collision = rayon visuel du nœud + marge, pour éviter les
|
||||
superpositions une fois rendu en SVG ;
|
||||
- forceX/forceY par catégorie vers la cible elliptique (0.08) : ancre
|
||||
chaque cluster sans écraser la structure interne. */
|
||||
const simulation = forceSimulation(nodes)
|
||||
.force(
|
||||
'link',
|
||||
forceLink(links)
|
||||
.id((d) => d.id)
|
||||
.distance((d) => (d.type === 'strong' ? 30 : 70))
|
||||
.strength((d) => (d.type === 'strong' ? 0.7 : 0.2))
|
||||
)
|
||||
.force('charge', forceManyBody().strength(-45))
|
||||
.force('collide', forceCollide().radius((d) => 4 + (d.val ?? 5) * 0.6 + 6).iterations(2))
|
||||
.force('x', forceX((d) => categoryTarget(d.category).x).strength(0.08))
|
||||
.force('y', forceY((d) => categoryTarget(d.category).y).strength(0.08))
|
||||
.alpha(1)
|
||||
.alphaDecay(0.02)
|
||||
.stop();
|
||||
|
||||
// Convergence synchrone à nombre de ticks fixe (≈ 3× le temps de stabilisation
|
||||
// théorique log(0.001)/log(1-0.02) ≈ 342) : reproductible à l'identique.
|
||||
const TICKS = 600;
|
||||
for (let i = 0; i < TICKS; i++) simulation.tick();
|
||||
|
||||
/* ————— Normalisation dans le viewBox 1200×800 (marge 60) ————— */
|
||||
const VIEW_W = 1200;
|
||||
const VIEW_H = 800;
|
||||
const MARGIN = 60;
|
||||
|
||||
const xs = nodes.map((n) => n.x);
|
||||
const ys = nodes.map((n) => n.y);
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs);
|
||||
const minY = Math.min(...ys);
|
||||
const maxY = Math.max(...ys);
|
||||
const scale = Math.min(
|
||||
(VIEW_W - 2 * MARGIN) / Math.max(maxX - minX, 1e-9),
|
||||
(VIEW_H - 2 * MARGIN) / Math.max(maxY - minY, 1e-9)
|
||||
);
|
||||
// Centrage du nuage dans le viewBox.
|
||||
const offX = MARGIN + (VIEW_W - 2 * MARGIN - (maxX - minX) * scale) / 2;
|
||||
const offY = MARGIN + (VIEW_H - 2 * MARGIN - (maxY - minY) * scale) / 2;
|
||||
|
||||
const positions = {};
|
||||
for (const n of nodes) {
|
||||
const x = Math.round((offX + (n.x - minX) * scale) * 10) / 10;
|
||||
const y = Math.round((offY + (n.y - minY) * scale) * 10) / 10;
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||||
throw new Error(`Position non finie pour « ${n.id} » — ajuster les forces.`);
|
||||
}
|
||||
positions[n.id] = { x, y };
|
||||
}
|
||||
|
||||
/* ————— Contrôles qualité ————— */
|
||||
const ids = Object.keys(positions);
|
||||
if (ids.length !== network.nodes.length) {
|
||||
throw new Error(`${ids.length} positions pour ${network.nodes.length} nœuds.`);
|
||||
}
|
||||
let minDist = Infinity;
|
||||
let closest = null;
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
for (let j = i + 1; j < ids.length; j++) {
|
||||
const a = positions[ids[i]];
|
||||
const b = positions[ids[j]];
|
||||
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
closest = [ids[i], ids[j]];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minDist < 8) {
|
||||
console.warn(
|
||||
`⚠️ distance minimale ${minDist.toFixed(1)}px entre « ${closest[0]} » et « ${closest[1]} » — envisager forceCollide plus large.`
|
||||
);
|
||||
}
|
||||
|
||||
const layout = {
|
||||
viewBox: { width: VIEW_W, height: VIEW_H },
|
||||
nodes: positions
|
||||
};
|
||||
writeFileSync(outPath, JSON.stringify(layout, null, 2) + '\n');
|
||||
|
||||
console.log(`✔ ${ids.length} positions écrites dans src/lib/data/network-layout.json`);
|
||||
console.log(` distance minimale entre nœuds : ${minDist.toFixed(1)}px (${closest[0]} ↔ ${closest[1]})`);
|
||||
console.log(` alpha résiduel après ${TICKS} ticks : ${simulation.alpha().toFixed(5)}`);
|
||||
@@ -1,8 +1,13 @@
|
||||
/* app.css — point d'entrée des styles, ordre de cascade explicite */
|
||||
/* app.css — point d'entrée des styles, ordre de cascade explicite.
|
||||
oki-tokens.css et oki-bridge.css sont hors layer : ils priment sur
|
||||
tokens.css / themes/default.css (charte OKI = seule identité). */
|
||||
@layer reset, tokens, base, layouts, components, utilities;
|
||||
|
||||
@import './styles/reset.css' layer(reset);
|
||||
@import './styles/tokens.css' layer(tokens);
|
||||
@import './styles/themes/default.css' layer(tokens);
|
||||
@import './styles/oki-tokens.css';
|
||||
@import './styles/oki-bridge.css';
|
||||
@import './styles/base.css' layer(base);
|
||||
@import './styles/oki-base.css' layer(base);
|
||||
@import './styles/layouts.css' layer(layouts);
|
||||
|
||||
@@ -8,14 +8,58 @@
|
||||
name="description"
|
||||
content="Cartographie interactive du Fédiverse : 105 logiciels, 12 protocoles, 18 ans d'histoire du réseau social décentralisé."
|
||||
/>
|
||||
<meta name="theme-color" content="#001219" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#0D0D0D" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<link rel="canonical" href="https://syel.o-k-i.net/" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://syel.o-k-i.net/" />
|
||||
<meta property="og:title" content="Le Grand Atlas du Fédiverse — 2026" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Cartographie interactive du Fédiverse : 105 logiciels, 12 protocoles, 18 ans d'histoire du réseau social décentralisé."
|
||||
/>
|
||||
<meta property="og:image" content="https://syel.o-k-i.net/icons/icon-512.png" />
|
||||
<meta property="og:image:width" content="512" />
|
||||
<meta property="og:image:height" content="512" />
|
||||
<meta property="og:image:alt" content="Logo du Grand Atlas du Fédiverse" />
|
||||
<meta property="og:locale" content="fr_FR" />
|
||||
<meta property="og:site_name" content="Le Grand Atlas du Fédiverse" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:title" content="Le Grand Atlas du Fédiverse — 2026" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Cartographie interactive du Fédiverse : 105 logiciels, 12 protocoles, 18 ans d'histoire du réseau social décentralisé."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://syel.o-k-i.net/icons/icon-512.png" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Le Grand Atlas du Fédiverse",
|
||||
"url": "https://syel.o-k-i.net/",
|
||||
"description": "Cartographie interactive du Fédiverse : 105 logiciels, 12 protocoles, 18 ans d'histoire du réseau social décentralisé.",
|
||||
"applicationCategory": "ReferenceApplication",
|
||||
"operatingSystem": "Web",
|
||||
"inLanguage": "fr",
|
||||
"license": "https://creativecommons.org/licenses/by-sa/4.0/",
|
||||
"creator": {
|
||||
"@type": "Organization",
|
||||
"name": "ORGANISATION KA INTERNATIONALE",
|
||||
"url": "https://o-k-i.net"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/icons/logo.svg" />
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/icons/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="preload"
|
||||
href="%sveltekit.assets%/fonts/space-grotesk-var.woff2"
|
||||
href="%sveltekit.assets%/fonts/archivo-latin-700-normal.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
@@ -27,25 +71,8 @@
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<style>
|
||||
/* 2 fichiers WOFF2 variables, auto-hébergés, font-display: swap.
|
||||
Dans app.html pour bénéficier de %sveltekit.assets% (base /fediverse). */
|
||||
@font-face {
|
||||
font-family: 'Space Grotesk';
|
||||
src: url('%sveltekit.assets%/fonts/space-grotesk-var.woff2') format('woff2');
|
||||
font-weight: 300 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('%sveltekit.assets%/fonts/inter-var.woff2') format('woff2');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
</style>
|
||||
<!-- Archivo 600-900 (titres) + Inter variable (corps), auto-hébergées, font-display: swap -->
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/fonts/fonts.css" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
@@ -53,7 +80,7 @@
|
||||
<div
|
||||
id="boot-warning"
|
||||
hidden
|
||||
style="position: fixed; inset-inline: 1rem; inset-block-end: 1rem; z-index: 9999; padding: 1rem 1.25rem; border: 2px solid #f72585; border-radius: 1rem; background: #001219; color: #eef4f8; font-family: system-ui, sans-serif; font-size: 0.9375rem; line-height: 1.5; box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4);"
|
||||
style="position: fixed; inset-inline: 1rem; inset-block-end: 1rem; z-index: 9999; padding: 1rem 1.25rem; border: 2px solid #FDB813; border-radius: 6px; background: #0D0D0D; color: #FFF8E7; font-family: system-ui, sans-serif; font-size: 0.9375rem; line-height: 1.5; box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4);"
|
||||
>
|
||||
<strong>L'application n'a pas pu démarrer.</strong>
|
||||
Si vous lisez ce fichier en local (<code>file://</code>), servez le dossier
|
||||
@@ -63,10 +90,10 @@
|
||||
</div>
|
||||
<noscript>
|
||||
<div
|
||||
style="padding: 1rem 1.25rem; background: #001219; color: #eef4f8; font-family: system-ui, sans-serif; text-align: center;"
|
||||
style="padding: 1rem 1.25rem; background: #0D0D0D; color: #FFF8E7; font-family: system-ui, sans-serif; text-align: center;"
|
||||
>
|
||||
JavaScript est désactivé : le contenu de l'Atlas reste lisible, mais la carte 3D et les
|
||||
fenêtres de détails sont indisponibles.
|
||||
JavaScript est désactivé : le contenu de l'Atlas reste lisible, mais la carte
|
||||
interactive et les fenêtres de détails sont indisponibles.
|
||||
</div>
|
||||
</noscript>
|
||||
<script src="%sveltekit.assets%/boot-check.js" defer></script>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
const projectLinks = [
|
||||
{ href: `${base}/#map`, labelKey: 'footer.about' },
|
||||
{ href: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', labelKey: 'footer.license' },
|
||||
{ href: 'https://github.com/', labelKey: 'footer.source' },
|
||||
{ href: `${base}/data/fediverse.json`, labelKey: 'footer.api' }
|
||||
] as const;
|
||||
|
||||
@@ -35,6 +34,7 @@
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<div class="flag-bar" aria-hidden="true"></div>
|
||||
<div class="center stack footer-stack">
|
||||
<div class="footer-grid">
|
||||
<div class="stack brand-col">
|
||||
@@ -98,6 +98,10 @@
|
||||
</div>
|
||||
|
||||
<div class="bottom-bar stack">
|
||||
<p class="small muted fede">
|
||||
Un service libre opéré par
|
||||
<a href="https://o-k-i.net" rel="external">ORGANISATION KA INTERNATIONALE · o-k-i.net</a>
|
||||
</p>
|
||||
<p class="small muted">{t('footer.rights')}</p>
|
||||
<p class="small muted">{t('footer.credits')}</p>
|
||||
</div>
|
||||
@@ -108,11 +112,13 @@
|
||||
footer {
|
||||
background: var(--abyss);
|
||||
border-block-start: 1px solid var(--border);
|
||||
padding-block: var(--space-6) var(--space-4);
|
||||
padding-block: 0 var(--space-4);
|
||||
}
|
||||
|
||||
/* La flag-bar (oki-base.css) est plaquée en haut du footer, sans padding */
|
||||
.footer-stack {
|
||||
--stack-gap: var(--space-5);
|
||||
padding-block-start: var(--space-6);
|
||||
}
|
||||
|
||||
.footer-grid {
|
||||
@@ -231,6 +237,17 @@
|
||||
padding-block-start: var(--space-3);
|
||||
}
|
||||
|
||||
/* Ligne fédérée (charte §1.11) : texte muted, lien or */
|
||||
.fede a {
|
||||
color: var(--or-oki);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
}
|
||||
|
||||
.fede a:hover {
|
||||
color: var(--or-clair);
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
.footer-grid {
|
||||
grid-template-columns: 2fr 1fr 1fr 1fr;
|
||||
|
||||
@@ -156,6 +156,8 @@
|
||||
</ul>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<div class="flag-bar" aria-hidden="true"></div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
@@ -179,6 +181,14 @@
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
/* Signature n°1 : flag-bar en bord inférieur de la nav (charte §1.5).
|
||||
La classe .flag-bar vit dans oki-base.css — ici, son ancrage. */
|
||||
header .flag-bar {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
inset-block-end: 0;
|
||||
}
|
||||
|
||||
header.scrolled {
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(16px);
|
||||
|
||||
@@ -1,11 +1,88 @@
|
||||
<script lang="ts">
|
||||
/* HeroSection — plein viewport, shader océan en fond, titre + CTA. */
|
||||
/* HeroSection — plein viewport, constellation zetwal en fond, titre + CTA.
|
||||
Fond charte §7 (portails fédivers) : étoiles zetwal du sprite OKI
|
||||
reliées par de fins filets --line, dérive lente au scroll via
|
||||
animation-timeline: scroll(), quelques étoiles en éclat --or-oki
|
||||
qui pulsent (transform/opacity, délais syncopés). Zéro JS, zéro WebGL. */
|
||||
import { base } from '$app/paths';
|
||||
import { stats } from '$lib/data';
|
||||
import HeroShader from './HeroShader.svelte';
|
||||
|
||||
interface Star {
|
||||
cx: number;
|
||||
cy: number;
|
||||
s: number;
|
||||
eclat?: boolean;
|
||||
/** Délai syncopé (3+3+2) de la pulsation — charte §1.7 */
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
/* Positions dans le viewBox 1440×900 — étoiles + maillage de filets */
|
||||
const stars: Star[] = [
|
||||
{ cx: 150, cy: 190, s: 30 },
|
||||
{ cx: 430, cy: 100, s: 22 },
|
||||
{ cx: 770, cy: 160, s: 38, eclat: true, delay: 0 },
|
||||
{ cx: 1130, cy: 120, s: 24 },
|
||||
{ cx: 1330, cy: 270, s: 30 },
|
||||
{ cx: 990, cy: 390, s: 20 },
|
||||
{ cx: 630, cy: 340, s: 26, eclat: true, delay: 300 },
|
||||
{ cx: 270, cy: 430, s: 22 },
|
||||
{ cx: 100, cy: 650, s: 26 },
|
||||
{ cx: 490, cy: 710, s: 32, eclat: true, delay: 660 },
|
||||
{ cx: 870, cy: 650, s: 22 },
|
||||
{ cx: 1240, cy: 730, s: 28 },
|
||||
{ cx: 1390, cy: 530, s: 20 }
|
||||
];
|
||||
|
||||
const links: [number, number][] = [
|
||||
[0, 1],
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 4],
|
||||
[0, 7],
|
||||
[7, 6],
|
||||
[6, 2],
|
||||
[6, 5],
|
||||
[5, 3],
|
||||
[7, 8],
|
||||
[8, 9],
|
||||
[9, 10],
|
||||
[10, 5],
|
||||
[10, 11],
|
||||
[11, 12],
|
||||
[12, 4]
|
||||
];
|
||||
</script>
|
||||
|
||||
<section id="hero" aria-labelledby="hero-title">
|
||||
<HeroShader />
|
||||
<svg
|
||||
class="constellation"
|
||||
viewBox="0 0 1440 900"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g class="drift">
|
||||
{#each links as [a, b] (`${a}-${b}`)}
|
||||
<line
|
||||
class="fil"
|
||||
x1={stars[a].cx}
|
||||
y1={stars[a].cy}
|
||||
x2={stars[b].cx}
|
||||
y2={stars[b].cy}
|
||||
/>
|
||||
{/each}
|
||||
{#each stars as star (`${star.cx}-${star.cy}`)}
|
||||
<use
|
||||
href="{base}/icons.svg#zetwal"
|
||||
class:eclat={star.eclat}
|
||||
style:animation-delay={star.delay !== undefined ? `${star.delay}ms` : undefined}
|
||||
x={star.cx - star.s / 2}
|
||||
y={star.cy - star.s / 2}
|
||||
width={star.s}
|
||||
height={star.s}
|
||||
/>
|
||||
{/each}
|
||||
</g>
|
||||
</svg>
|
||||
<div class="veil" aria-hidden="true"></div>
|
||||
|
||||
<div class="content center stack">
|
||||
@@ -47,9 +124,6 @@
|
||||
|
||||
<style>
|
||||
section {
|
||||
/* Contexte sombre local : light-dark() bascule en mode sombre ici,
|
||||
quel que soit le thème de la page. */
|
||||
color-scheme: dark;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -57,11 +131,75 @@
|
||||
min-block-size: 100svh;
|
||||
overflow: hidden;
|
||||
background: var(--abyss);
|
||||
color: var(--on-accent);
|
||||
color: var(--blanc-creme);
|
||||
scroll-margin-top: var(--header-h);
|
||||
}
|
||||
|
||||
/* Voile de lisibilité : garantit le contraste du texte sur le shader */
|
||||
/* ── Constellation zetwal (charte §7) ── */
|
||||
.constellation {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.fil {
|
||||
stroke: var(--line);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.constellation use {
|
||||
color: var(--blanc-creme);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* Étoiles en éclat : or OKI, pulsation douce transform/opacity */
|
||||
.constellation use.eclat {
|
||||
color: var(--or-oki);
|
||||
opacity: 0.8;
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.constellation use.eclat {
|
||||
animation: zetwal-pulse calc(var(--dur-phrase) * 3) var(--ease-ka) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zetwal-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dérive lente au scroll — natif uniquement, sinon fond statique */
|
||||
@supports (animation-timeline: scroll()) {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.drift {
|
||||
animation: constellation-drift linear both;
|
||||
animation-timeline: scroll();
|
||||
animation-range: 0 100svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes constellation-drift {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(5rem);
|
||||
}
|
||||
}
|
||||
|
||||
/* Voile de lisibilité : garantit le contraste du texte sur la constellation */
|
||||
.veil {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -103,7 +241,7 @@
|
||||
}
|
||||
|
||||
.lede strong {
|
||||
color: var(--on-accent);
|
||||
color: var(--blanc-creme);
|
||||
}
|
||||
|
||||
.cta {
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
<script lang="ts">
|
||||
/* HeroShader — océan WebGL (Three.js, chargé paresseusement côté client).
|
||||
Remplit son parent en absolu ; purement décoratif (aria-hidden). */
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const vertexShader = /* glsl */ `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
/* Palette cosinus type Iñigo Quilez : a + b*cos(6.28318*(c*t+d)) */
|
||||
const fragmentShader = /* glsl */ `
|
||||
uniform float u_time;
|
||||
uniform vec2 u_resolution;
|
||||
varying vec2 vUv;
|
||||
|
||||
vec3 palette(in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d) {
|
||||
return a + b * cos(6.28318 * (c * t + d));
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = (vUv - 0.5) * 2.0;
|
||||
uv.x *= u_resolution.x / u_resolution.y;
|
||||
|
||||
vec2 uv0 = uv;
|
||||
vec3 finalColor = vec3(0.0);
|
||||
|
||||
for (float i = 0.0; i < 3.0; i++) {
|
||||
uv = fract(uv * 1.5) - 0.5;
|
||||
|
||||
float d = length(uv) * exp(-length(uv0));
|
||||
|
||||
vec3 col = palette(length(uv0) + i * 0.4 + u_time * 0.4,
|
||||
vec3(0.5, 0.5, 0.9),
|
||||
vec3(0.5, 0.5, 0.4),
|
||||
vec3(1.0, 1.0, 0.9),
|
||||
vec3(0.0, 0.1, 0.2));
|
||||
|
||||
d = sin(d * 8.0 + u_time) / 8.0;
|
||||
d = abs(d);
|
||||
d = pow(0.01 / d, 1.2);
|
||||
|
||||
finalColor += col * d;
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(finalColor, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
let container: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
let cleanup: (() => void) | undefined;
|
||||
let disposed = false;
|
||||
|
||||
(async () => {
|
||||
// Import paresseux obligatoire : rien de Three au top-level (SSR).
|
||||
const THREE = await import('three');
|
||||
if (disposed || !container) return;
|
||||
|
||||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
const width = container.offsetWidth || window.innerWidth;
|
||||
const height = container.offsetHeight || window.innerHeight;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 2000);
|
||||
camera.position.z = 1;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.domElement.style.width = '100%';
|
||||
renderer.domElement.style.height = '100%';
|
||||
renderer.domElement.style.display = 'block';
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
const uniforms = {
|
||||
u_time: { value: 0.0 },
|
||||
u_resolution: { value: new THREE.Vector2(width, height) }
|
||||
};
|
||||
|
||||
const geometry = new THREE.PlaneGeometry(2, 2);
|
||||
const material = new THREE.ShaderMaterial({ vertexShader, fragmentShader, uniforms });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
|
||||
let raf = 0;
|
||||
const tick = () => {
|
||||
uniforms.u_time.value += 0.008;
|
||||
renderer.render(scene, camera);
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
if (reduceMotion) {
|
||||
// Mouvement réduit : une seule frame statique (u_time fixe), pas de boucle.
|
||||
renderer.render(scene, camera);
|
||||
} else {
|
||||
// Frugalité : la boucle ne tourne que si le canvas est visible à l'écran
|
||||
// ET si l'onglet est actif.
|
||||
let inView = false;
|
||||
let pageVisible = !document.hidden;
|
||||
|
||||
const stop = () => {
|
||||
if (raf !== 0) {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = 0;
|
||||
}
|
||||
};
|
||||
const sync = () => {
|
||||
if (inView && pageVisible) {
|
||||
if (raf === 0) raf = requestAnimationFrame(tick);
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
inView = entries[0]?.isIntersecting ?? false;
|
||||
sync();
|
||||
},
|
||||
{ threshold: 0 }
|
||||
);
|
||||
observer.observe(container);
|
||||
|
||||
const onVisibility = () => {
|
||||
pageVisible = !document.hidden;
|
||||
sync();
|
||||
};
|
||||
document.addEventListener('visibilitychange', onVisibility);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const w = container.offsetWidth;
|
||||
const h = container.offsetHeight;
|
||||
if (w === 0 || h === 0) return;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
uniforms.u_resolution.value.set(w, h);
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
|
||||
cleanup = () => {
|
||||
stop();
|
||||
observer.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
document.removeEventListener('visibilitychange', onVisibility);
|
||||
renderer.dispose();
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
if (container.contains(renderer.domElement)) {
|
||||
container.removeChild(renderer.domElement);
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Chemin reduced-motion : pas d'observateurs d'animation, juste le resize.
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const w = container.offsetWidth;
|
||||
const h = container.offsetHeight;
|
||||
if (w === 0 || h === 0) return;
|
||||
camera.aspect = w / h;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(w, h);
|
||||
uniforms.u_resolution.value.set(w, h);
|
||||
renderer.render(scene, camera);
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
|
||||
cleanup = () => {
|
||||
resizeObserver.disconnect();
|
||||
renderer.dispose();
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
if (container.contains(renderer.domElement)) {
|
||||
container.removeChild(renderer.domElement);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
cleanup?.();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="shader" bind:this={container} aria-hidden="true"></div>
|
||||
|
||||
<style>
|
||||
.shader {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
/* ProtocolSection — grille des 12 protocoles sur fond « data stream ». */
|
||||
import { protocols, type Protocol } from '$lib/data';
|
||||
import ProtocolStream from './ProtocolStream.svelte';
|
||||
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||
|
||||
const TYPES: Record<Protocol['type'], { label: string; color: string }> = {
|
||||
fediverse: { label: 'Protocole du Fédiverse', color: 'var(--accent-4-text)' },
|
||||
@@ -26,7 +27,7 @@
|
||||
<div class="content center stack">
|
||||
<header class="section-head stack">
|
||||
<p class="kicker">Standards & Protocoles</p>
|
||||
<h2 id="protocols-title">Les fondations du <span class="gradient">Fédiverse</span></h2>
|
||||
<KineticText id="protocols-title" text="Les fondations du Fédiverse" />
|
||||
<p class="subtitle">
|
||||
Comprendre les protocoles qui permettent la communication décentralisée entre les
|
||||
différentes plateformes.
|
||||
@@ -146,7 +147,7 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--ocean);
|
||||
color: var(--on-accent);
|
||||
color: var(--blanc-creme);
|
||||
padding-block: var(--space-6);
|
||||
scroll-margin-top: var(--header-h);
|
||||
}
|
||||
@@ -183,13 +184,6 @@
|
||||
color: var(--accent-2-text);
|
||||
}
|
||||
|
||||
.gradient {
|
||||
background: var(--gradient-brand);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
max-inline-size: var(--measure);
|
||||
color: var(--ink-soft);
|
||||
@@ -301,7 +295,7 @@
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-small);
|
||||
color: var(--muted);
|
||||
background: color-mix(in oklab, var(--on-accent) 6%, transparent);
|
||||
background: color-mix(in oklab, var(--blanc-creme) 6%, transparent);
|
||||
border-radius: var(--radius-1);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { categoryById, ETATS, softwares, type Category, type Software } from '$lib/data';
|
||||
import { t } from '$lib/i18n/i18n.svelte';
|
||||
import { isTypingTarget } from '$lib/keyboard';
|
||||
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||
import { consumeSoftwareDetailsRequest } from '$lib/software-modal.svelte';
|
||||
|
||||
type StatusFilter = Software['etat'] | 'all';
|
||||
|
||||
@@ -23,15 +25,15 @@
|
||||
'En développement': 'var(--compat-none)'
|
||||
};
|
||||
|
||||
/** Choisit un texte contrasté (clair ou foncé) selon la luminance WCAG d'une couleur hex. */
|
||||
/** Choisit un texte contrasté (crème ou foncé) selon la luminance WCAG d'une couleur hex. */
|
||||
function contrastingInk(hex: string | undefined): string {
|
||||
if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return 'var(--on-accent)';
|
||||
if (!hex || !/^#[0-9a-fA-F]{6}$/.test(hex)) return 'var(--blanc-creme)';
|
||||
const channels = [1, 3, 5].map((start) => {
|
||||
const c = parseInt(hex.slice(start, start + 2), 16) / 255;
|
||||
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
const luminance = 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
||||
return luminance > 0.18 ? 'oklch(22% 0.02 260)' : 'var(--on-accent)';
|
||||
return luminance > 0.18 ? 'var(--noir-oki)' : 'var(--blanc-creme)';
|
||||
}
|
||||
|
||||
function categoryOf(software: Software): Category | undefined {
|
||||
@@ -157,13 +159,20 @@
|
||||
$effect(() => {
|
||||
if (selectedSoftware) closeButton?.focus();
|
||||
});
|
||||
|
||||
// Ouverture de la fiche depuis une autre section (chronologie, etc.)
|
||||
// via l'état partagé $lib/software-modal.svelte
|
||||
$effect(() => {
|
||||
const requested = consumeSoftwareDetailsRequest();
|
||||
if (requested) openDetails(requested, null);
|
||||
});
|
||||
</script>
|
||||
|
||||
<section id="catalog" aria-labelledby="catalog-title">
|
||||
<div class="inner center">
|
||||
<header class="section-header stack">
|
||||
<p class="kicker">Répertoire</p>
|
||||
<h2 id="catalog-title">Les logiciels du <span class="gradient-text">Fédiverse</span></h2>
|
||||
<KineticText id="catalog-title" text="Les logiciels du Fédiverse" />
|
||||
<p class="subtitle">
|
||||
{softwares.length} logiciels référencés, classés par catégorie et filtrables par statut
|
||||
d'activité.
|
||||
@@ -426,13 +435,6 @@
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: var(--gradient-brand);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--ink-soft);
|
||||
max-inline-size: var(--measure);
|
||||
|
||||
@@ -1,28 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { networkData, stats } from '$lib/data';
|
||||
import { networkData, softwareById, stats, type Software } from '$lib/data';
|
||||
import { timelineEvents, type TimelineEvent } from '$lib/data/timeline';
|
||||
import { reveal } from '$lib/motion/reveal';
|
||||
import KineticText from '$lib/components/motion/KineticText.svelte';
|
||||
import { requestSoftwareDetails } from '$lib/software-modal.svelte';
|
||||
|
||||
type CategoryKey = TimelineEvent['category'];
|
||||
type FilterKey = CategoryKey | 'all';
|
||||
|
||||
/* Couleurs de catégories : tokens sémantiques (adaptés au thème clair/sombre). */
|
||||
/* Couleurs de catégories : décor uniquement (l'or reste la seule couleur d'action). */
|
||||
const categoryConfig = {
|
||||
foundation: { label: 'Fondation', color: 'var(--muted)' },
|
||||
protocol: { label: 'Protocole', color: 'var(--accent)' },
|
||||
software: { label: 'Logiciel', color: 'var(--accent-2)' },
|
||||
milestone: { label: 'Jalon', color: 'var(--status-maintenance)' },
|
||||
ecosystem: { label: 'Écosystème', color: 'var(--accent-4)' }
|
||||
protocol: { label: 'Protocole', color: 'var(--turquoise-caraibes)' },
|
||||
software: { label: 'Logiciel', color: 'var(--bleu-ocean)' },
|
||||
milestone: { label: 'Jalon', color: 'var(--rouge-oki)' },
|
||||
ecosystem: { label: 'Écosystème', color: 'var(--vert-oki)' }
|
||||
} as const satisfies Record<CategoryKey, { label: string; color: string }>;
|
||||
|
||||
const categoryFilters: readonly { key: FilterKey; label: string; color: string }[] = [
|
||||
{ key: 'all', label: 'Tout', color: 'var(--accent)' },
|
||||
{ key: 'foundation', label: 'Fondations', color: categoryConfig.foundation.color },
|
||||
{ key: 'protocol', label: 'Protocoles', color: categoryConfig.protocol.color },
|
||||
{ key: 'software', label: 'Logiciels', color: categoryConfig.software.color },
|
||||
{ key: 'milestone', label: 'Jalons', color: categoryConfig.milestone.color },
|
||||
{ key: 'ecosystem', label: 'Écosystème', color: categoryConfig.ecosystem.color }
|
||||
const categoryFilters: readonly { key: FilterKey; label: string }[] = [
|
||||
{ key: 'all', label: 'Tout' },
|
||||
{ key: 'foundation', label: 'Fondations' },
|
||||
{ key: 'protocol', label: 'Protocoles' },
|
||||
{ key: 'software', label: 'Logiciels' },
|
||||
{ key: 'milestone', label: 'Jalons' },
|
||||
{ key: 'ecosystem', label: 'Écosystème' }
|
||||
];
|
||||
|
||||
/* Marqueurs d'ère : les bornes couvrent la totalité des événements (2008 → 2026).
|
||||
« Pionniers » étendu à 2015 et « essor » à 2019 pour ne laisser aucune année orpheline. */
|
||||
const eras = [
|
||||
{ id: 'pionniers', label: 'Les pionniers', from: 2008, to: 2015 },
|
||||
{ id: 'essor', label: "L'essor d'ActivityPub", from: 2016, to: 2019 },
|
||||
{ id: 'bascules', label: 'Les grandes bascules', from: 2020, to: 2022 },
|
||||
{ id: 'consolidation', label: 'La consolidation', from: 2023, to: 2026 }
|
||||
] as const;
|
||||
|
||||
interface EraGroup {
|
||||
id: string;
|
||||
label: string;
|
||||
from: number;
|
||||
to: number;
|
||||
revealIndex: number;
|
||||
years: YearGroup[];
|
||||
}
|
||||
|
||||
const MONTHS_FR = [
|
||||
'Janv.',
|
||||
'Févr.',
|
||||
'Mars',
|
||||
'Avr.',
|
||||
'Mai',
|
||||
'Juin',
|
||||
'Juil.',
|
||||
'Août',
|
||||
'Sept.',
|
||||
'Oct.',
|
||||
'Nov.',
|
||||
'Déc.'
|
||||
] as const;
|
||||
|
||||
interface TimelineItem {
|
||||
event: TimelineEvent;
|
||||
software: Software | undefined;
|
||||
revealIndex: number;
|
||||
}
|
||||
|
||||
interface YearGroup {
|
||||
year: number;
|
||||
items: TimelineItem[];
|
||||
}
|
||||
|
||||
let activeFilter = $state<FilterKey>('all');
|
||||
|
||||
const filtered = $derived(
|
||||
@@ -31,14 +78,34 @@
|
||||
: timelineEvents.filter((event) => event.category === activeFilter)
|
||||
);
|
||||
|
||||
const yearGroups = $derived.by(() => {
|
||||
/* Regroupe les événements filtrés par ère puis par année, et attribue
|
||||
l'indice de cascade gwoka (reveal syncopé) sur l'ensemble du flux. */
|
||||
const eraGroups = $derived.by((): EraGroup[] => {
|
||||
let revealIndex = 0;
|
||||
const groups: EraGroup[] = [];
|
||||
for (const era of eras) {
|
||||
const inEra = filtered.filter((event) => event.year >= era.from && event.year <= era.to);
|
||||
if (inEra.length === 0) continue;
|
||||
const byYear = new Map<number, TimelineEvent[]>();
|
||||
for (const event of filtered) {
|
||||
for (const event of inEra) {
|
||||
const group = byYear.get(event.year);
|
||||
if (group) group.push(event);
|
||||
else byYear.set(event.year, [event]);
|
||||
}
|
||||
return [...byYear.entries()].sort((a, b) => a[0] - b[0]);
|
||||
const eraRevealIndex = revealIndex++;
|
||||
const years: YearGroup[] = [...byYear.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([year, events]) => ({
|
||||
year,
|
||||
items: events.map((event) => ({
|
||||
event,
|
||||
software: event.softwareId ? softwareById.get(event.softwareId) : undefined,
|
||||
revealIndex: revealIndex++
|
||||
}))
|
||||
}));
|
||||
groups.push({ ...era, revealIndex: eraRevealIndex, years });
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
|
||||
const finalStats = [
|
||||
@@ -47,13 +114,17 @@
|
||||
{ value: stats.totalProtocols, label: 'Protocoles' },
|
||||
{ value: networkData.links.length, label: 'Liens de fédération' }
|
||||
];
|
||||
|
||||
function formatMonth(month: number): string {
|
||||
return MONTHS_FR[month - 1] ?? '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<section id="timeline" aria-labelledby="timeline-title">
|
||||
<div class="inner center">
|
||||
<header class="section-header stack">
|
||||
<p class="kicker">🕐 Chronologie</p>
|
||||
<h2 id="timeline-title">L'évolution du <span class="gradient-text">Fédiverse</span></h2>
|
||||
<p class="kicker">Chronologie</p>
|
||||
<KineticText id="timeline-title" text="L'évolution du Fédiverse" />
|
||||
<p class="subtitle">
|
||||
De StatusNet en 2008 à l'Atlas de 2026 : {stats.yearsOfHistory} ans d'histoire du réseau
|
||||
social décentralisé.
|
||||
@@ -65,7 +136,6 @@
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
style:--cat-color={filter.color}
|
||||
aria-pressed={activeFilter === filter.key}
|
||||
onclick={() => (activeFilter = filter.key)}
|
||||
>
|
||||
@@ -75,31 +145,70 @@
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
{#each yearGroups as [year, events] (year)}
|
||||
<section class="year-group" aria-labelledby="timeline-year-{year}">
|
||||
<h3 class="year-badge" id="timeline-year-{year}">{year}</h3>
|
||||
<!-- Ligne verticale : se dessine au scroll (scaleY + animation-timeline, sous garde) -->
|
||||
<div class="timeline-line" aria-hidden="true"></div>
|
||||
|
||||
{#each eraGroups as era (era.id)}
|
||||
<section class="era" aria-labelledby="timeline-era-{era.id}">
|
||||
<header class="era-header" use:reveal={era.revealIndex}>
|
||||
<h3 class="era-title" id="timeline-era-{era.id}">{era.label}</h3>
|
||||
<p class="era-range">{era.from} — {era.to}</p>
|
||||
</header>
|
||||
|
||||
{#each era.years as yearGroup (yearGroup.year)}
|
||||
<section class="year-group" aria-labelledby="timeline-year-{yearGroup.year}">
|
||||
<h4 class="year-badge" id="timeline-year-{yearGroup.year}">{yearGroup.year}</h4>
|
||||
<ol class="events" role="list">
|
||||
{#each events as event, i (`${year}-${event.title}`)}
|
||||
{@const cat = categoryConfig[event.category]}
|
||||
<li class="event" class:left={i % 2 === 0} class:right={i % 2 === 1}>
|
||||
<article class="event-card" style:--event-color={event.color}>
|
||||
{#each yearGroup.items as item, i (`${yearGroup.year}-${item.event.title}`)}
|
||||
{@const cat = categoryConfig[item.event.category]}
|
||||
<li
|
||||
class="event"
|
||||
class:left={i % 2 === 0}
|
||||
class:right={i % 2 === 1}
|
||||
style:--event-color={item.event.color}
|
||||
use:reveal={item.revealIndex}
|
||||
>
|
||||
<article class="event-card">
|
||||
<p class="event-meta">
|
||||
<span class="event-dot" aria-hidden="true"></span>
|
||||
<span class="event-cat" style:--cat-label-color={cat.color}>{cat.label}</span>
|
||||
<span class="event-cat" style:--cat-label-color={cat.color}>{cat.label}</span
|
||||
>
|
||||
{#if item.event.month}
|
||||
<time
|
||||
class="event-date"
|
||||
datetime="{item.event.year}-{String(item.event.month).padStart(2, '0')}"
|
||||
>
|
||||
{formatMonth(item.event.month)} {item.event.year}
|
||||
</time>
|
||||
{/if}
|
||||
</p>
|
||||
<h4 class="event-title">{event.title}</h4>
|
||||
<p class="event-desc">{event.description}</p>
|
||||
<h5 class="event-title">{item.event.title}</h5>
|
||||
<p class="event-desc">{item.event.description}</p>
|
||||
{#if item.software}
|
||||
{@const software = item.software}
|
||||
<button
|
||||
type="button"
|
||||
class="event-cta"
|
||||
aria-label="Voir la fiche {software.name}"
|
||||
onclick={() => requestSoftwareDetails(software.id)}
|
||||
>
|
||||
Voir la fiche {software.name}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</button>
|
||||
{/if}
|
||||
</article>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</section>
|
||||
{/each}
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="grid stats" role="list">
|
||||
{#each finalStats as stat (stat.label)}
|
||||
<div class="stat-card" role="listitem">
|
||||
{#each finalStats as stat, i (stat.label)}
|
||||
<div class="stat-card" role="listitem" use:reveal={i}>
|
||||
<p class="stat-value gradient-text">{stat.value}</p>
|
||||
<p class="stat-label">{stat.label}</p>
|
||||
</div>
|
||||
@@ -110,8 +219,8 @@
|
||||
|
||||
<style>
|
||||
section {
|
||||
background: var(--abyss);
|
||||
padding-block: var(--space-6);
|
||||
background: var(--noir-profond);
|
||||
padding-block: var(--space-5);
|
||||
scroll-margin-top: var(--header-h);
|
||||
}
|
||||
|
||||
@@ -120,10 +229,10 @@
|
||||
}
|
||||
|
||||
.section-header {
|
||||
--stack-gap: var(--space-3);
|
||||
--stack-gap: var(--space-2);
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-block-end: var(--space-5);
|
||||
margin-block-end: var(--space-4);
|
||||
}
|
||||
|
||||
.kicker {
|
||||
@@ -131,7 +240,7 @@
|
||||
font-size: var(--text-small);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--status-maintenance);
|
||||
color: var(--or-oki);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
@@ -142,7 +251,7 @@
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--ink-soft);
|
||||
color: var(--muted);
|
||||
max-inline-size: var(--measure);
|
||||
}
|
||||
|
||||
@@ -150,35 +259,42 @@
|
||||
|
||||
.filters {
|
||||
justify-content: center;
|
||||
margin-block-end: var(--space-5);
|
||||
margin-block-end: var(--space-4);
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-block-size: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-pill);
|
||||
min-block-size: 2.75rem;
|
||||
padding-inline: var(--space-2);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--text-small);
|
||||
font-weight: 500;
|
||||
color: var(--ink-soft);
|
||||
color: var(--muted);
|
||||
transition:
|
||||
background-color var(--duration-1) var(--ease-out),
|
||||
border-color var(--duration-1) var(--ease-out),
|
||||
color var(--duration-1) var(--ease-out);
|
||||
background-color var(--dur-tanbou) var(--ease-ka),
|
||||
border-color var(--dur-tanbou) var(--ease-ka),
|
||||
color var(--dur-tanbou) var(--ease-ka);
|
||||
}
|
||||
|
||||
.pill:hover {
|
||||
border-color: var(--cat-color);
|
||||
color: var(--ink);
|
||||
border-color: var(--or-oki);
|
||||
color: var(--blanc-creme);
|
||||
}
|
||||
|
||||
.pill:focus-visible {
|
||||
outline: 2px solid var(--or-oki);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Or = seule couleur d'action : état actif des filtres */
|
||||
.pill[aria-pressed='true'] {
|
||||
background: color-mix(in oklch, var(--cat-color) 22%, transparent);
|
||||
border-color: var(--cat-color);
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
background: var(--or-oki);
|
||||
border-color: var(--or-oki);
|
||||
color: var(--noir-oki);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* --- Chronologie --- */
|
||||
@@ -188,20 +304,77 @@
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
/* Ligne verticale en dégradé : à gauche sur mobile, centrée sur desktop */
|
||||
.timeline::before {
|
||||
content: '';
|
||||
/* Ligne verticale : à gauche sur mobile, centrée sur desktop.
|
||||
Statique par défaut (fallback) ; animée au scroll sous garde. */
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
inset-inline-start: var(--space-3);
|
||||
inset-inline-start: var(--space-2);
|
||||
translate: -50% 0;
|
||||
inline-size: 2px;
|
||||
background: linear-gradient(180deg, var(--accent), var(--accent-2), var(--accent-4));
|
||||
background: linear-gradient(180deg, var(--or-oki), var(--or-clair));
|
||||
transform-origin: top;
|
||||
}
|
||||
|
||||
@keyframes timeline-line-draw {
|
||||
from {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
to {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@supports (animation-timeline: view()) {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.timeline-line {
|
||||
animation: timeline-line-draw linear both;
|
||||
animation-timeline: view();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Marqueurs d'ère --- */
|
||||
|
||||
.era {
|
||||
position: relative;
|
||||
margin-block-end: var(--space-4);
|
||||
}
|
||||
|
||||
.era-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: var(--space-1) var(--space-2);
|
||||
margin-block-end: var(--space-3);
|
||||
padding-block: var(--space-2);
|
||||
background: var(--noir-profond);
|
||||
}
|
||||
|
||||
.era-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2);
|
||||
font-weight: 800;
|
||||
color: var(--blanc-creme);
|
||||
}
|
||||
|
||||
.era-range {
|
||||
padding: 0.125rem var(--space-1);
|
||||
border: 1px solid var(--or-oki);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-small);
|
||||
color: var(--or-oki);
|
||||
}
|
||||
|
||||
/* --- Groupes annuels --- */
|
||||
|
||||
.year-group {
|
||||
position: relative;
|
||||
margin-block-end: var(--space-5);
|
||||
margin-block-end: var(--space-3);
|
||||
}
|
||||
|
||||
.year-badge {
|
||||
@@ -209,59 +382,57 @@
|
||||
z-index: 1;
|
||||
inline-size: fit-content;
|
||||
margin-inline: auto;
|
||||
margin-block-end: var(--space-4);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-pill);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
margin-block-end: var(--space-3);
|
||||
padding: 0.125rem var(--space-2);
|
||||
background: var(--noir-oki);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-1);
|
||||
color: var(--ink);
|
||||
color: var(--blanc-creme);
|
||||
}
|
||||
|
||||
.events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.event {
|
||||
position: relative;
|
||||
padding-inline-start: var(--space-5);
|
||||
padding-inline-start: var(--space-4);
|
||||
}
|
||||
|
||||
/* Pastille sur la ligne */
|
||||
/* Pastille sur la ligne : couleur propre à l'événement (donnée) */
|
||||
.event::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: var(--space-3);
|
||||
inset-block-start: var(--space-4);
|
||||
inset-inline-start: var(--space-2);
|
||||
inset-block-start: var(--space-3);
|
||||
translate: -50% 0;
|
||||
inline-size: 0.75rem;
|
||||
block-size: 0.75rem;
|
||||
border-radius: 50%;
|
||||
background: var(--event-color, var(--accent));
|
||||
border: 2px solid var(--abyss);
|
||||
box-shadow: 0 0 0 2px color-mix(in oklch, var(--event-color, var(--accent)) 45%, transparent);
|
||||
background: var(--event-color, var(--or-oki));
|
||||
border: 2px solid var(--noir-profond);
|
||||
box-shadow: 0 0 0 2px color-mix(in oklch, var(--event-color, var(--or-oki)) 45%, transparent);
|
||||
}
|
||||
|
||||
.event-card {
|
||||
inline-size: fit-content;
|
||||
max-inline-size: 100%;
|
||||
padding: var(--space-4);
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-2);
|
||||
box-shadow: var(--shadow-1);
|
||||
padding: var(--space-3);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.event-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-block-end: var(--space-2);
|
||||
gap: var(--space-1);
|
||||
margin-block-end: var(--space-1);
|
||||
}
|
||||
|
||||
.event-dot {
|
||||
@@ -269,8 +440,8 @@
|
||||
inline-size: 0.5rem;
|
||||
block-size: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--event-color, var(--accent));
|
||||
box-shadow: 0 0 0.5rem color-mix(in oklch, var(--event-color, var(--accent)) 70%, transparent);
|
||||
background: var(--event-color, var(--or-oki));
|
||||
box-shadow: 0 0 0.5rem color-mix(in oklch, var(--event-color, var(--or-oki)) 70%, transparent);
|
||||
}
|
||||
|
||||
.event-cat {
|
||||
@@ -281,24 +452,64 @@
|
||||
color: var(--cat-label-color);
|
||||
}
|
||||
|
||||
.event-date {
|
||||
margin-inline-start: auto;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-small);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-1);
|
||||
font-weight: 700;
|
||||
color: var(--blanc-creme);
|
||||
margin-block-end: var(--space-1);
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
font-size: var(--text-small);
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.6;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Lien événement → fiche logiciel du catalogue : or = action */
|
||||
.event-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-block-start: var(--space-2);
|
||||
padding: 0.375rem var(--space-1);
|
||||
border: 1px solid var(--or-oki);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-small);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--or-oki);
|
||||
transition:
|
||||
background-color var(--dur-tanbou) var(--ease-ka),
|
||||
color var(--dur-tanbou) var(--ease-ka);
|
||||
}
|
||||
|
||||
.event-cta:hover {
|
||||
background: var(--or-oki);
|
||||
color: var(--noir-oki);
|
||||
}
|
||||
|
||||
.event-cta:focus-visible {
|
||||
outline: 2px solid var(--or-oki);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Alternance gauche/droite sur desktop */
|
||||
@media (min-width: 48rem) {
|
||||
.timeline::before {
|
||||
.timeline-line {
|
||||
inset-inline-start: 50%;
|
||||
}
|
||||
|
||||
.event {
|
||||
inline-size: calc(50% - var(--space-5));
|
||||
inline-size: calc(50% - var(--space-4));
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
@@ -315,9 +526,13 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.event.left .event-date {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.event.left::before {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: calc(-1 * var(--space-5));
|
||||
inset-inline-end: calc(-1 * var(--space-4));
|
||||
translate: 50% 0;
|
||||
}
|
||||
|
||||
@@ -326,18 +541,14 @@
|
||||
}
|
||||
|
||||
.event.right::before {
|
||||
inset-inline-start: calc(-1 * var(--space-5));
|
||||
inset-inline-start: calc(-1 * var(--space-4));
|
||||
}
|
||||
}
|
||||
|
||||
/* Container query : carte compacte quand la chronologie est étroite */
|
||||
@container (max-width: 30rem) {
|
||||
.event-card {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.event-desc {
|
||||
font-size: var(--text-small);
|
||||
padding: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,14 +556,14 @@
|
||||
|
||||
.stats {
|
||||
--grid-min: 11rem;
|
||||
margin-block-start: var(--space-6);
|
||||
margin-block-start: var(--space-5);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: var(--space-3);
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-2);
|
||||
padding: var(--space-2);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -364,6 +575,6 @@
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-small);
|
||||
color: var(--ink-soft);
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* KineticText — titrage au scroll, split par mots (charte §2.6).
|
||||
* Chemin unique : scrub CSS via animation-timeline: view() ; sans
|
||||
* support ou en reduced-motion, le titre reste statique et visible
|
||||
* (pas de fallback GSAP : zéro dépendance JS dans ce projet).
|
||||
*/
|
||||
import { syncopatedDelay } from '$lib/motion/tokens';
|
||||
|
||||
interface Props {
|
||||
/** Chaîne intacte — restituée via aria-label. */
|
||||
text: string;
|
||||
/** Balise du titre (h1-h4). */
|
||||
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'span';
|
||||
class?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let { text, as = 'h2', class: className = '', id }: Props = $props();
|
||||
|
||||
/**
|
||||
* Split par mots (jamais par caractères — langues à apostrophes).
|
||||
* Sur un titre (h1-h4) : aria-label avec la chaîne intacte + mots
|
||||
* aria-hidden. Sur span/p : aria-label est prohibé — les mots
|
||||
* restent lisibles naturellement par les lecteurs d'écran.
|
||||
*/
|
||||
const words = $derived(text.split(/\s+/).filter(Boolean));
|
||||
const named = $derived(['h1', 'h2', 'h3', 'h4'].includes(as));
|
||||
|
||||
function rangeFor(index: number): { start: number; end: number; delay: number } {
|
||||
const delay = syncopatedDelay(index);
|
||||
const start = Math.min(5 + (delay / 960) * 25, 55);
|
||||
return { start, end: start + 30, delay };
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={as}
|
||||
{id}
|
||||
class="kinetic-text {className}"
|
||||
aria-label={named ? text : undefined}
|
||||
>
|
||||
{#each words as word, i (i)}
|
||||
{@const range = rangeFor(i)}
|
||||
<span
|
||||
class="kt-word"
|
||||
aria-hidden={named ? true : undefined}
|
||||
style="--kt-start: {range.start}%; --kt-end: {range.end}%; --kt-delay: {range.delay}ms"
|
||||
>{word}{#if i < words.length - 1} {/if}</span
|
||||
>
|
||||
{/each}
|
||||
</svelte:element>
|
||||
|
||||
<style>
|
||||
.kinetic-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.kt-word {
|
||||
display: inline-block;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Chemin natif : scrub via animation-timeline: view() (playbook §5.2).
|
||||
L'état caché initial n'existe QUE dans ce bloc @supports :
|
||||
sans JS ni support, le contenu reste intégralement visible. */
|
||||
@supports (animation-timeline: view()) {
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.kt-word {
|
||||
animation: kt-rise linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: entry var(--kt-start) entry var(--kt-end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kt-rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(60%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,428 @@
|
||||
{
|
||||
"viewBox": {
|
||||
"width": 1200,
|
||||
"height": 800
|
||||
},
|
||||
"nodes": {
|
||||
"Mastodon": {
|
||||
"x": 705.1,
|
||||
"y": 112.8
|
||||
},
|
||||
"GoToSocial": {
|
||||
"x": 677.1,
|
||||
"y": 107.1
|
||||
},
|
||||
"Akkoma": {
|
||||
"x": 713.6,
|
||||
"y": 143.4
|
||||
},
|
||||
"Pleroma": {
|
||||
"x": 745.5,
|
||||
"y": 180.9
|
||||
},
|
||||
"Misskey": {
|
||||
"x": 670,
|
||||
"y": 180.9
|
||||
},
|
||||
"Sharkey": {
|
||||
"x": 702.3,
|
||||
"y": 218.2
|
||||
},
|
||||
"Friendica": {
|
||||
"x": 724,
|
||||
"y": 91.1
|
||||
},
|
||||
"Hubzilla": {
|
||||
"x": 739.9,
|
||||
"y": 66.3
|
||||
},
|
||||
"Streams": {
|
||||
"x": 752.6,
|
||||
"y": 91.9
|
||||
},
|
||||
"Iceshrimp": {
|
||||
"x": 636.6,
|
||||
"y": 147.1
|
||||
},
|
||||
"GNU social": {
|
||||
"x": 713.1,
|
||||
"y": 65.1
|
||||
},
|
||||
"Bonfire": {
|
||||
"x": 755.2,
|
||||
"y": 136.2
|
||||
},
|
||||
"microblog.pub": {
|
||||
"x": 685.8,
|
||||
"y": 241.5
|
||||
},
|
||||
"PixelFed": {
|
||||
"x": 783.6,
|
||||
"y": 138.9
|
||||
},
|
||||
"Vernissage": {
|
||||
"x": 682.3,
|
||||
"y": 269.8
|
||||
},
|
||||
"PeerTube": {
|
||||
"x": 767,
|
||||
"y": 162.2
|
||||
},
|
||||
"Owncast": {
|
||||
"x": 839.6,
|
||||
"y": 192.8
|
||||
},
|
||||
"Tube (Framasoft)": {
|
||||
"x": 798.8,
|
||||
"y": 232.9
|
||||
},
|
||||
"Castopod": {
|
||||
"x": 777.8,
|
||||
"y": 218.2
|
||||
},
|
||||
"Funkwhale": {
|
||||
"x": 718.5,
|
||||
"y": 171.5
|
||||
},
|
||||
"Loops": {
|
||||
"x": 686.1,
|
||||
"y": 134.2
|
||||
},
|
||||
"Ghost": {
|
||||
"x": 841.4,
|
||||
"y": 355.3
|
||||
},
|
||||
"WriteFreely": {
|
||||
"x": 733.3,
|
||||
"y": 290
|
||||
},
|
||||
"Plume": {
|
||||
"x": 778.2,
|
||||
"y": 245
|
||||
},
|
||||
"WordPress + ActivityPub": {
|
||||
"x": 849.1,
|
||||
"y": 395.8
|
||||
},
|
||||
"Drupal + ActivityPub": {
|
||||
"x": 874.1,
|
||||
"y": 359.8
|
||||
},
|
||||
"Lemmy": {
|
||||
"x": 723.9,
|
||||
"y": 199.5
|
||||
},
|
||||
"Mbin": {
|
||||
"x": 691.5,
|
||||
"y": 162.2
|
||||
},
|
||||
"NodeBB": {
|
||||
"x": 594.1,
|
||||
"y": 284.7
|
||||
},
|
||||
"Mobilizon": {
|
||||
"x": 655.4,
|
||||
"y": 260.5
|
||||
},
|
||||
"Gancio": {
|
||||
"x": 714.2,
|
||||
"y": 244.2
|
||||
},
|
||||
"Forgejo": {
|
||||
"x": 666.9,
|
||||
"y": 342.1
|
||||
},
|
||||
"Gitea": {
|
||||
"x": 638.9,
|
||||
"y": 329.6
|
||||
},
|
||||
"Nextcloud": {
|
||||
"x": 744.9,
|
||||
"y": 728.8
|
||||
},
|
||||
"ownCloud": {
|
||||
"x": 686.6,
|
||||
"y": 654.8
|
||||
},
|
||||
"BookWyrm": {
|
||||
"x": 752.5,
|
||||
"y": 237.4
|
||||
},
|
||||
"Wikidata": {
|
||||
"x": 1031.9,
|
||||
"y": 348.1
|
||||
},
|
||||
"Diaspora*": {
|
||||
"x": 688.5,
|
||||
"y": 60
|
||||
},
|
||||
"Element": {
|
||||
"x": 713.7,
|
||||
"y": 646
|
||||
},
|
||||
"Fedify": {
|
||||
"x": 642,
|
||||
"y": 175.2
|
||||
},
|
||||
"Mitra": {
|
||||
"x": 696,
|
||||
"y": 85.7
|
||||
},
|
||||
"Socialhome": {
|
||||
"x": 784.5,
|
||||
"y": 89.4
|
||||
},
|
||||
"kbin": {
|
||||
"x": 638,
|
||||
"y": 217.1
|
||||
},
|
||||
"Epicyon": {
|
||||
"x": 696.9,
|
||||
"y": 190.2
|
||||
},
|
||||
"Takahē": {
|
||||
"x": 733.1,
|
||||
"y": 118.2
|
||||
},
|
||||
"Smithereen": {
|
||||
"x": 800.8,
|
||||
"y": 187.2
|
||||
},
|
||||
"Kroeg": {
|
||||
"x": 792.7,
|
||||
"y": 162.9
|
||||
},
|
||||
"Lotide": {
|
||||
"x": 710.7,
|
||||
"y": 272.5
|
||||
},
|
||||
"Sublinks": {
|
||||
"x": 658.8,
|
||||
"y": 232.1
|
||||
},
|
||||
"RoadHouse": {
|
||||
"x": 651.7,
|
||||
"y": 198.9
|
||||
},
|
||||
"Pieoneers": {
|
||||
"x": 739,
|
||||
"y": 156.1
|
||||
},
|
||||
"WordPress MU + ActivityPub": {
|
||||
"x": 813.3,
|
||||
"y": 389.7
|
||||
},
|
||||
"Emissary": {
|
||||
"x": 664.6,
|
||||
"y": 152.9
|
||||
},
|
||||
"Hometown": {
|
||||
"x": 811.4,
|
||||
"y": 145.4
|
||||
},
|
||||
"GlitchSoc": {
|
||||
"x": 771.8,
|
||||
"y": 113
|
||||
},
|
||||
"Akkoma-FE": {
|
||||
"x": 772.4,
|
||||
"y": 190.2
|
||||
},
|
||||
"Postmarks": {
|
||||
"x": 655.5,
|
||||
"y": 125.8
|
||||
},
|
||||
"AodeRelay": {
|
||||
"x": 675.4,
|
||||
"y": 208.9
|
||||
},
|
||||
"Coracle": {
|
||||
"x": 315.6,
|
||||
"y": 234.6
|
||||
},
|
||||
"Nostrudel": {
|
||||
"x": 318,
|
||||
"y": 174.1
|
||||
},
|
||||
"Lume": {
|
||||
"x": 364.1,
|
||||
"y": 272.9
|
||||
},
|
||||
"Gossip": {
|
||||
"x": 388.1,
|
||||
"y": 203.4
|
||||
},
|
||||
"Current": {
|
||||
"x": 394.7,
|
||||
"y": 231.1
|
||||
},
|
||||
"Plebstr": {
|
||||
"x": 298.4,
|
||||
"y": 194.9
|
||||
},
|
||||
"Nostrpels": {
|
||||
"x": 362.4,
|
||||
"y": 203.6
|
||||
},
|
||||
"ZapStream": {
|
||||
"x": 341.6,
|
||||
"y": 255.3
|
||||
},
|
||||
"NostrCheck.me": {
|
||||
"x": 326.2,
|
||||
"y": 201.5
|
||||
},
|
||||
"NostrMarket": {
|
||||
"x": 337.9,
|
||||
"y": 284.2
|
||||
},
|
||||
"Prosody": {
|
||||
"x": 240.4,
|
||||
"y": 469.9
|
||||
},
|
||||
"ejabberd": {
|
||||
"x": 220.3,
|
||||
"y": 514.3
|
||||
},
|
||||
"Openfire": {
|
||||
"x": 211.6,
|
||||
"y": 486.4
|
||||
},
|
||||
"Tigase": {
|
||||
"x": 217.1,
|
||||
"y": 412
|
||||
},
|
||||
"Conversations": {
|
||||
"x": 224.9,
|
||||
"y": 439.8
|
||||
},
|
||||
"Dino": {
|
||||
"x": 168.1,
|
||||
"y": 448.8
|
||||
},
|
||||
"Gajim": {
|
||||
"x": 196.3,
|
||||
"y": 453
|
||||
},
|
||||
"Monal": {
|
||||
"x": 172.8,
|
||||
"y": 478.8
|
||||
},
|
||||
"Snikket": {
|
||||
"x": 267.5,
|
||||
"y": 480.6
|
||||
},
|
||||
"Movim": {
|
||||
"x": 188.3,
|
||||
"y": 503.6
|
||||
},
|
||||
"Jabber": {
|
||||
"x": 266.1,
|
||||
"y": 448.9
|
||||
},
|
||||
"Bluesky": {
|
||||
"x": 800.2,
|
||||
"y": 115.7
|
||||
},
|
||||
"Graysky": {
|
||||
"x": 768.3,
|
||||
"y": 68.1
|
||||
},
|
||||
"Tokimeki": {
|
||||
"x": 668.1,
|
||||
"y": 80
|
||||
},
|
||||
"Briar": {
|
||||
"x": 249.3,
|
||||
"y": 424.1
|
||||
},
|
||||
"Jami": {
|
||||
"x": 248.1,
|
||||
"y": 504.1
|
||||
},
|
||||
"CryptPad": {
|
||||
"x": 703.9,
|
||||
"y": 702.7
|
||||
},
|
||||
"HedgeDoc": {
|
||||
"x": 742.1,
|
||||
"y": 651.8
|
||||
},
|
||||
"Immich": {
|
||||
"x": 762.5,
|
||||
"y": 675.8
|
||||
},
|
||||
"PhotoPrism": {
|
||||
"x": 728.6,
|
||||
"y": 684.6
|
||||
},
|
||||
"Synapse": {
|
||||
"x": 670.4,
|
||||
"y": 678.3
|
||||
},
|
||||
"Dendrite": {
|
||||
"x": 762.8,
|
||||
"y": 706.6
|
||||
},
|
||||
"Conduit": {
|
||||
"x": 668.4,
|
||||
"y": 706.8
|
||||
},
|
||||
"Cinny": {
|
||||
"x": 718.7,
|
||||
"y": 740
|
||||
},
|
||||
"FluffyChat": {
|
||||
"x": 687.7,
|
||||
"y": 732.6
|
||||
},
|
||||
"FediBridge": {
|
||||
"x": 730.4,
|
||||
"y": 224.3
|
||||
},
|
||||
"ActivityPub Relay": {
|
||||
"x": 737,
|
||||
"y": 261.3
|
||||
},
|
||||
"Sepia Search": {
|
||||
"x": 750.9,
|
||||
"y": 208.9
|
||||
},
|
||||
"SimpleX Chat": {
|
||||
"x": 187.6,
|
||||
"y": 421.8
|
||||
},
|
||||
"Nostr Protocol": {
|
||||
"x": 343.6,
|
||||
"y": 224.2
|
||||
},
|
||||
"Damus": {
|
||||
"x": 367.6,
|
||||
"y": 240.2
|
||||
},
|
||||
"Amethyst": {
|
||||
"x": 315.5,
|
||||
"y": 266.6
|
||||
},
|
||||
"Iris": {
|
||||
"x": 345.9,
|
||||
"y": 179.9
|
||||
},
|
||||
"Primal": {
|
||||
"x": 374.4,
|
||||
"y": 178.3
|
||||
},
|
||||
"Habla.news": {
|
||||
"x": 289.9,
|
||||
"y": 222.1
|
||||
},
|
||||
"Nostr.band": {
|
||||
"x": 291.8,
|
||||
"y": 250.6
|
||||
},
|
||||
"Snort": {
|
||||
"x": 389.1,
|
||||
"y": 259.1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,24 +77,16 @@ export const timelineEvents: TimelineEvent[] = [
|
||||
softwareId: 'misskey',
|
||||
color: '#4CC9F0',
|
||||
},
|
||||
// 2015
|
||||
// 2016
|
||||
{
|
||||
year: 2015,
|
||||
title: 'PeerTube',
|
||||
description: "Framasoft lance PeerTube, une plateforme vidéo fédérée et décentralisée. Alternative libre à YouTube.",
|
||||
category: 'software',
|
||||
softwareId: 'peertube',
|
||||
color: '#F72585',
|
||||
},
|
||||
{
|
||||
year: 2015,
|
||||
year: 2016,
|
||||
month: 10,
|
||||
title: 'Mastodon',
|
||||
description: "Eugen Rochko lance Mastodon. Le début de l'explosion du Fédiverse moderne. Utilise OStatus, migrera plus tard vers ActivityPub.",
|
||||
category: 'software',
|
||||
softwareId: 'mastodon',
|
||||
color: '#4CC9F0',
|
||||
},
|
||||
// 2016
|
||||
{
|
||||
year: 2016,
|
||||
title: 'Pleroma',
|
||||
@@ -126,6 +118,14 @@ export const timelineEvents: TimelineEvent[] = [
|
||||
category: 'protocol',
|
||||
color: '#F72585',
|
||||
},
|
||||
{
|
||||
year: 2017,
|
||||
title: 'PeerTube',
|
||||
description: "Framasoft lance PeerTube, une plateforme vidéo fédérée et décentralisée. Alternative libre à YouTube.",
|
||||
category: 'software',
|
||||
softwareId: 'peertube',
|
||||
color: '#F72585',
|
||||
},
|
||||
{
|
||||
year: 2017,
|
||||
title: 'PixelFed',
|
||||
@@ -145,6 +145,7 @@ export const timelineEvents: TimelineEvent[] = [
|
||||
// 2018
|
||||
{
|
||||
year: 2018,
|
||||
month: 1,
|
||||
title: 'ActivityPub — W3C Recommendation',
|
||||
description: "ActivityPub devient une recommandation officielle du W3C. Le standard est désormais finalisé.",
|
||||
category: 'milestone',
|
||||
@@ -248,13 +249,6 @@ export const timelineEvents: TimelineEvent[] = [
|
||||
color: '#06D6A0',
|
||||
},
|
||||
// 2021
|
||||
{
|
||||
year: 2021,
|
||||
title: 'Twitter → Mastodon',
|
||||
description: "Vague massive de migrations de Twitter vers Mastodon après l'acquisition par Elon Musk. Le Fédiverse passe de 2M à 10M+ d'utilisateurs.",
|
||||
category: 'milestone',
|
||||
color: '#F72585',
|
||||
},
|
||||
{
|
||||
year: 2021,
|
||||
title: 'GoToSocial',
|
||||
@@ -264,6 +258,14 @@ export const timelineEvents: TimelineEvent[] = [
|
||||
color: '#4CC9F0',
|
||||
},
|
||||
// 2022
|
||||
{
|
||||
year: 2022,
|
||||
month: 10,
|
||||
title: 'Twitter → Mastodon',
|
||||
description: "Vague massive de migrations de Twitter vers Mastodon après l'acquisition par Elon Musk. Le Fédiverse passe de 2M à 10M+ d'utilisateurs.",
|
||||
category: 'milestone',
|
||||
color: '#F72585',
|
||||
},
|
||||
{
|
||||
year: 2022,
|
||||
title: 'Akkoma',
|
||||
@@ -352,14 +354,15 @@ export const timelineEvents: TimelineEvent[] = [
|
||||
softwareId: 'ghost',
|
||||
color: '#7209B7',
|
||||
},
|
||||
// 2024
|
||||
{
|
||||
year: 2023,
|
||||
year: 2024,
|
||||
month: 2,
|
||||
title: 'Bluesky ouvre au public',
|
||||
description: "Bluesky (AT Protocol) ouvre ses portes. L'écosystème des réseaux décentralisés gagne en visibilité.",
|
||||
category: 'ecosystem',
|
||||
color: '#4CC9F0',
|
||||
},
|
||||
// 2024
|
||||
{
|
||||
year: 2024,
|
||||
title: 'Loops',
|
||||
@@ -411,7 +414,7 @@ export const timelineEvents: TimelineEvent[] = [
|
||||
{
|
||||
year: 2026,
|
||||
title: 'Le Grand Atlas du Fédiverse v4.0',
|
||||
description: "Publication de cette cartographie complète recensant 55+ logiciels, 12 protocoles et 18 ans d'histoire du réseau fédéré.",
|
||||
description: "Publication de cette cartographie complète recensant 105 logiciels, 12 protocoles et 18 ans d'histoire du réseau fédéré.",
|
||||
category: 'milestone',
|
||||
color: '#F72585',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { syncopatedDelay } from './tokens';
|
||||
|
||||
/**
|
||||
* Action `use:reveal` — reveal syncopé à l'entrée dans le viewport.
|
||||
*
|
||||
* L'état caché initial n'existe que si JS est actif (classe `js` sur
|
||||
* <html>, posée par theme.js) ET sans prefers-reduced-motion (media
|
||||
* query dans base.css) : sans JS ou en reduced-motion, le contenu est
|
||||
* intégralement visible.
|
||||
*
|
||||
* @param index position dans la cascade — délai gwoka 3+3+2 (charte §3)
|
||||
*/
|
||||
export function reveal(node: HTMLElement, index: number = 0) {
|
||||
function apply(i: number) {
|
||||
node.dataset.reveal = '';
|
||||
node.style.setProperty('--d', `${syncopatedDelay(i)}ms`);
|
||||
}
|
||||
|
||||
apply(index);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
node.classList.add('in');
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.15, rootMargin: '0px 0px -5% 0px' }
|
||||
);
|
||||
observer.observe(node);
|
||||
|
||||
return {
|
||||
update(i: number) {
|
||||
apply(i);
|
||||
},
|
||||
destroy() {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
/** Gate unique prefers-reduced-motion (playbook §6). */
|
||||
export function prefersReducedMotion(): boolean {
|
||||
return browser && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stagger syncopé de marque OKI (3+3+2) — charte §3.
|
||||
* Délais en ms pour l'index donné (cycle de 8 éléments).
|
||||
*/
|
||||
const SYNCOPATED_DELAYS = [0, 120, 300, 360, 600, 660, 900, 960];
|
||||
|
||||
export function syncopatedDelay(index: number): number {
|
||||
const cycle = Math.floor(index / 8);
|
||||
return SYNCOPATED_DELAYS[index % 8] + cycle * 1200;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* État partagé pour ouvrir la fiche détail d'un logiciel du catalogue
|
||||
depuis une autre section (ex. la chronologie).
|
||||
SoftwareCatalog consomme la demande et affiche sa modale existante. */
|
||||
|
||||
import { softwareById, type Software } from '$lib/data';
|
||||
|
||||
let pendingRequest = $state<Software | null>(null);
|
||||
|
||||
/**
|
||||
* Demande l'ouverture de la fiche d'un logiciel dans le catalogue.
|
||||
* Sans effet si l'identifiant n'existe pas dans software.json.
|
||||
*/
|
||||
export function requestSoftwareDetails(softwareId: string): void {
|
||||
const software = softwareById.get(softwareId);
|
||||
if (software) pendingRequest = software;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit et consomme la dernière demande d'ouverture (appelé par SoftwareCatalog).
|
||||
* Retourne `null` s'il n'y a aucune demande en attente.
|
||||
*/
|
||||
export function consumeSoftwareDetailsRequest(): Software | null {
|
||||
const software = pendingRequest;
|
||||
pendingRequest = null;
|
||||
return software;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const worker = self as unknown as ServiceWorkerGlobalScope;
|
||||
const PRECACHE = `atlas-precache-${version}`;
|
||||
const RUNTIME = `atlas-runtime-${version}`;
|
||||
|
||||
// Les listes $service-worker incluent le base path (/fediverse) :
|
||||
// Listes $service-worker : build = chunks Vite, files = static/, prerendered = pages HTML.
|
||||
// build = chunks Vite, files = static/, prerendered = pages HTML prérendues.
|
||||
const PRECACHE_URLS = [...build, ...files, ...prerendered];
|
||||
|
||||
@@ -67,7 +67,7 @@ async function networkFirst(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
// Candidates : copie de la visite, version canonique avec slash final
|
||||
// (trailingSlash: 'always' → pages en dossiers), puis la racine du scope
|
||||
// (registration.scope inclut le base path, ex. https://o-k-i.net/fediverse/).
|
||||
// (registration.scope, ex. https://syel.o-k-i.net/).
|
||||
const candidates = [
|
||||
request.url,
|
||||
`${url.origin}${url.pathname.replace(/\/?$/, '/')}`,
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/* ============================================================
|
||||
OKI BASE — primitives charte OKI (o-k-i.net base.css), fusionnées
|
||||
avec le socle du projet : reset, corps de texte, focus visible,
|
||||
skip-link, visually-hidden et kill-switch prefers-reduced-motion
|
||||
vivent déjà dans reset.css / base.css — non repris ici.
|
||||
============================================================ */
|
||||
|
||||
@layer base {
|
||||
/* Titres : Archivo, capitales, crénage serré (charte §1.3) */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* États initiaux des reveals : uniquement si JS actif (attribut
|
||||
data-js posé par +layout.svelte) ET motion autorisée — sans JS ou
|
||||
en reduced-motion, tout le contenu est visible (playbook §2.6) */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html[data-js] [data-reveal] {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
transition:
|
||||
opacity var(--dur-mesure) var(--ease-ka) var(--d, 0ms),
|
||||
transform var(--dur-mesure) var(--ease-ka) var(--d, 0ms);
|
||||
}
|
||||
|
||||
html[data-js] [data-reveal].in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Variante « pop » des pictogrammes : scale(0.6) → 1 */
|
||||
html[data-js] [data-reveal].reveal-pop {
|
||||
transform: scale(0.6);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
html[data-js] [data-reveal].reveal-pop.in {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Signature n°1 : la flag-bar, 4 segments francs, 6 px ── */
|
||||
.flag-bar {
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#0d0d0d 0 25%,
|
||||
var(--or-oki) 25% 50%,
|
||||
var(--vert-oki) 50% 75%,
|
||||
var(--rouge-oki) 75% 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Bouton OKI canonique : Archivo 700, uppercase, bordure 2 px ── */
|
||||
.btn {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: var(--border-btn);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--or-oki);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color var(--dur-tanbou) var(--ease-ka),
|
||||
border-color var(--dur-tanbou) var(--ease-ka),
|
||||
transform var(--dur-tanbou) var(--ease-ka);
|
||||
}
|
||||
|
||||
/* Balayage or qui glisse depuis la gauche (hover craft) */
|
||||
.btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: var(--or-oki);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
transition: transform var(--dur-tanbou) var(--ease-syncope);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: var(--noir-oki);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn:hover::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.btn-solid {
|
||||
background: var(--or-oki);
|
||||
color: var(--noir-oki);
|
||||
}
|
||||
|
||||
.btn-solid::after {
|
||||
background: var(--or-clair);
|
||||
}
|
||||
|
||||
.btn-solid:hover {
|
||||
border-color: var(--or-clair);
|
||||
}
|
||||
|
||||
/* ── Carte OKI canonique : fond 3 %, liseré gauche 4 px, survol ── */
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border: var(--border-card);
|
||||
border-left: 4px solid var(--or-oki);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-4);
|
||||
transition:
|
||||
transform var(--dur-tanbou) var(--ease-ka),
|
||||
border-color var(--dur-tanbou) var(--ease-ka);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: var(--or-oki);
|
||||
}
|
||||
|
||||
/* ── Tag OKI canonique : or 8 % + bordure or 1 px ── */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
background: color-mix(in srgb, var(--or-oki) 8%, transparent);
|
||||
border: 1px solid var(--or-oki);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.15em 0.6em;
|
||||
font-size: 0.85rem;
|
||||
color: var(--or-oki);
|
||||
}
|
||||
|
||||
/* ── Pictogrammes SVG (set OKI, stroke 2 px, currentColor) ── */
|
||||
.icon {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: square;
|
||||
stroke-linejoin: miter;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-fill {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/* ============================================================
|
||||
OKI BRIDGE — alias des anciens tokens du projet vers la charte
|
||||
OKI (oki-tokens.css). Règles hors layer : elles priment sur les
|
||||
définitions d'origine (tokens.css, themes/default.css, @layer tokens).
|
||||
Thème sombre OKI = seule identité : color-scheme dark et valeurs
|
||||
sombres fixes — les light-dark() d'origine sont court-circuités,
|
||||
les sélecteurs existants ne changent pas.
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
/* ── Fonds & surfaces ── */
|
||||
--surface: var(--noir-oki);
|
||||
--surface-2: var(--noir-profond);
|
||||
--surface-3: var(--gris-sombre);
|
||||
--abyss: var(--noir-oki); /* fond le plus profond (hero, footer) */
|
||||
--ocean: var(--noir-profond); /* fond intermédiaire */
|
||||
|
||||
/* ── Texte ── */
|
||||
--ink: var(--blanc-creme);
|
||||
--ink-soft: var(--muted); /* crème 70 % */
|
||||
--on-accent: var(--noir-oki); /* texte sur bouton or */
|
||||
|
||||
/* ── Bordures & verre — filets blanc 10 %, jamais de gris plein ── */
|
||||
--border: var(--line);
|
||||
--border-strong: rgba(255, 255, 255, 0.25);
|
||||
--glass: color-mix(in srgb, var(--noir-oki) 82%, transparent);
|
||||
--glass-border: var(--line);
|
||||
|
||||
/* ── Accents — or = SEULE couleur d'action (liens, CTA, focus) ── */
|
||||
--accent: var(--or-oki);
|
||||
--accent-text: var(--or-oki);
|
||||
--accent-strong: var(--or-oki);
|
||||
--accent-2: var(--turquoise-caraibes); /* décor uniquement */
|
||||
--accent-2-text: var(--or-oki); /* ses usages texte (liens) → or */
|
||||
--accent-3: var(--orange-flamme); /* couleur protocolaire Nostr */
|
||||
--accent-3-text: var(--orange-flamme);
|
||||
--accent-3-strong: var(--orange-flamme);
|
||||
--accent-4: var(--vert-oki); /* couleur protocolaire SimpleX */
|
||||
--accent-4-text: var(--vert-oki);
|
||||
--accent-4-strong: var(--vert-oki);
|
||||
|
||||
/* ── États sémantiques — rouge = signaler, vert = valider ── */
|
||||
--status-active: var(--vert-oki);
|
||||
--status-maintenance: var(--jaune-soleil);
|
||||
--status-experimental: var(--rouge-oki);
|
||||
--compat-full: var(--vert-oki);
|
||||
--compat-partial: var(--jaune-soleil);
|
||||
--compat-extension: var(--turquoise-caraibes);
|
||||
--compat-none: var(--rouge-oki);
|
||||
|
||||
/* ── Rayons — angles nets partout (charte §1.4) ── */
|
||||
--radius-1: var(--radius-sm); /* 3px */
|
||||
--radius-2: var(--radius-md); /* 6px */
|
||||
--radius-3: var(--radius-md);
|
||||
--radius-pill: var(--radius-md);
|
||||
|
||||
/* ── Ombres & halos ── */
|
||||
--shadow-1: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.45);
|
||||
--shadow-2: 0 0.5rem 2rem rgba(0, 0, 0, 0.55);
|
||||
--glow-accent: 0 0 1.5rem color-mix(in srgb, var(--or-oki) 45%, transparent);
|
||||
--glow-accent-2: 0 0 1.5rem color-mix(in srgb, var(--or-oki) 30%, transparent);
|
||||
|
||||
/* ── Dégradés de marque (texte en background-clip) ── */
|
||||
--gradient-brand: linear-gradient(135deg, var(--or-oki), var(--or-clair));
|
||||
--gradient-abyss: linear-gradient(180deg, var(--noir-profond), var(--noir-oki));
|
||||
|
||||
/* ── Transitions — cadences gwoka (charte §1.7) ── */
|
||||
--ease-out: var(--ease-ka);
|
||||
--duration-1: var(--dur-tanbou);
|
||||
--duration-2: var(--dur-mesure);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/* ============================================================
|
||||
OKI TOKENS — charte-oki-design-system.md §2 et §3 (canonique)
|
||||
Thème sombre = identité par défaut. Le clair est un opt-in
|
||||
(html.light-theme) qui assombrit les accents pour WCAG AA.
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
/* ── Noyau (thème sombre = défaut) ── */
|
||||
--noir-oki: #0d0d0d; /* background */
|
||||
--noir-profond: #1a0f1a; /* surface */
|
||||
--blanc-creme: #fff8e7; /* foreground */
|
||||
--line: rgba(255, 255, 255, 0.1); /* filets — jamais de gris plein */
|
||||
--or-oki: #fdb813; /* accent : SEULE couleur d'action */
|
||||
--rouge-oki: #ff1654; /* signal, ponctuation — jamais un lien/bouton */
|
||||
|
||||
/* ── Étendue (parcimonie : décorations et sémantique) ── */
|
||||
--vert-oki: #00d66c; /* succès, validation, dons mensuels */
|
||||
--turquoise-caraibes: #00ced1;
|
||||
--jaune-soleil: #ffd700;
|
||||
--orange-flamme: #ff6b35;
|
||||
--violet-nuit: #6b2d5c;
|
||||
--bleu-ocean: #0077be;
|
||||
--or-clair: #ffe066; /* survol des boutons */
|
||||
--vert-clair: #66ffaa;
|
||||
--rouge-clair: #ff6b9d;
|
||||
--gris-sombre: #2d1b2e;
|
||||
|
||||
/* ── Sémantique dérivée ── */
|
||||
--muted: color-mix(in srgb, var(--blanc-creme) 70%, transparent);
|
||||
--card-bg: rgba(255, 255, 255, 0.03);
|
||||
|
||||
/* ── Typographie ── */
|
||||
--font-display: 'Archivo', 'Arial Black', sans-serif; /* 600-900 : TITRES + BOUTONS */
|
||||
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; /* 400-800 */
|
||||
|
||||
/* ── Formes, espacements, layout — angles nets partout ── */
|
||||
--radius-sm: 3px; /* boutons, tags, badges */
|
||||
--radius-md: 6px; /* cartes — jamais plus */
|
||||
--border-card: 1px solid var(--line);
|
||||
--border-btn: 2px solid var(--or-oki);
|
||||
--space-1: 0.75rem;
|
||||
--space-2: 1rem;
|
||||
--space-3: 1.5rem;
|
||||
--space-4: 2rem;
|
||||
--space-5: 4rem;
|
||||
--container: 1200px;
|
||||
|
||||
/* ── Motion — cadences gwoka (charte §3) ── */
|
||||
--ease-ka: cubic-bezier(0.22, 1, 0.36, 1); /* temps fort : attaque franche, longue tenue */
|
||||
--ease-syncope: cubic-bezier(0.65, 0, 0.35, 1); /* contretemps : symétrique, urgent */
|
||||
--dur-tanbou: 120ms; /* double-croche — micro-interactions, hovers */
|
||||
--dur-mesure: 480ms; /* une mesure — entrées de section */
|
||||
--dur-phrase: 960ms; /* deux mesures — transitions de page */
|
||||
}
|
||||
|
||||
/* Thème clair — opt-in, accents assombris pour WCAG AA */
|
||||
html.light-theme {
|
||||
--noir-oki: #fff8e7;
|
||||
--blanc-creme: #0d0d0d;
|
||||
--noir-profond: #f5f0e8;
|
||||
--gris-sombre: #e8ddd0;
|
||||
--or-oki: #b87a00;
|
||||
--vert-oki: #006b3d;
|
||||
--rouge-oki: #a01030;
|
||||
--turquoise-caraibes: #006b75;
|
||||
--violet-nuit: #6b2d5c;
|
||||
--bleu-ocean: #004b7f;
|
||||
--or-clair: #d99000;
|
||||
--vert-clair: #008c4f;
|
||||
--rouge-clair: #c01545;
|
||||
--jaune-soleil: #b87a00;
|
||||
--orange-flamme: #c85000;
|
||||
--line: rgba(0, 0, 0, 0.12);
|
||||
--card-bg: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/* Polices auto-hébergées — charte OKI §1.3.
|
||||
Archivo 600-900 : titres + boutons · Inter variable : corps.
|
||||
URLs relatives au fichier (servi depuis /fonts/). */
|
||||
|
||||
@font-face {
|
||||
font-family: 'Archivo';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('archivo-latin-600-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Archivo';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('archivo-latin-700-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Archivo';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url('archivo-latin-800-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Archivo';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('archivo-latin-900-normal.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('inter-var.woff2') format('woff2');
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Set iconographique OKI — charte §4 : viewBox 24×24, stroke 2 px, angles nets, currentColor -->
|
||||
<symbol id="ka" viewBox="0 0 24 24">
|
||||
<!-- tambour ka : collerette hexagonale, fût conique, sangles en zigzag -->
|
||||
<path d="M4 7 L20 7 L22 9 L20 11 L4 11 L2 9 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M4 11 L7 21 L17 21 L20 11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M8 11 L10.5 16 L9 21 M16 11 L13.5 16 L15 21" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="lambi" viewBox="0 0 24 24">
|
||||
<!-- conque lambi : dome en spirale, lèvre angulaire -->
|
||||
<path d="M4 14 C4 8 9 4 14 4 C18 4 21 7 21 11 L17 14 L20 18 L13 20 C8 21 4 18 4 14 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M11 14 C11 12.5 13.5 12.5 13.5 14 C13.5 16 9.5 16 9.5 13 C9.5 9.5 14 8.5 16 10.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="zetwal" viewBox="0 0 24 24">
|
||||
<!-- étoile 4 branches -->
|
||||
<path d="M12 3 L14.5 9.5 L21 12 L14.5 14.5 L12 21 L9.5 14.5 L3 12 L9.5 9.5 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="mawon" viewBox="0 0 24 24">
|
||||
<!-- brisure de chaîne : deux demi-maillons octogonaux, connexion rompue -->
|
||||
<path d="M9 7 L5 7 L3 9 L3 15 L5 17 L9 17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M15 7 L19 7 L21 9 L21 15 L19 17 L15 17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M12 9.5 L12 11 M12 13 L12 14.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="lakanmou" viewBox="0 0 24 24">
|
||||
<!-- flamme : contour angulaire à base courbe, languette interne -->
|
||||
<path d="M12 3 L15 8 L18 12 C19 15 18 18 15.5 20 L12 21 L8.5 20 C6 18 5 15 6 12 L9 8 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M12 12 L14 14.5 C14.3 16.5 13 18 12 18 C11 18 9.7 16.5 10 14.5 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="jaden" viewBox="0 0 24 24">
|
||||
<!-- pousse : tige et deux feuilles angulaires en cerf-volant -->
|
||||
<path d="M12 21 L12 10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M12 13 L3 5 L6 12 L10 16 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M12 13 L21 5 L18 12 L14 16 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="kannen" viewBox="0 0 24 24">
|
||||
<!-- canne à sucre : tige segmentée, trois lames au sommet -->
|
||||
<path d="M10 21 L10 6 L14 6 L14 21 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M10 17 L14 17 M10 13 L14 13 M10 9 L14 9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M10 6 L5 3 M14 6 L19 3 M12 6 L12 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="lanme" viewBox="0 0 24 24">
|
||||
<!-- vague angulaire : trois lignes brisées superposées -->
|
||||
<path d="M2 8 L7 4 L12 8 L17 4 L22 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M2 14 L7 10 L12 14 L17 10 L22 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M2 20 L7 16 L12 20 L17 16 L22 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="glo" viewBox="0 0 24 24">
|
||||
<!-- poing levé géométrique : bloc poing + avant-bras, doigts segmentés, pouce -->
|
||||
<path d="M8 20 L8 15 L6 15 L6 6 L8 4 L18 4 L18 15 L16 15 L16 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M6 9 L18 9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M9 4 L9 9 M12 4 L12 9 M15 4 L15 9 M6 12 L10 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="kle" viewBox="0 0 24 24">
|
||||
<!-- cadenas ouvert : corps rectangulaire, anse angulaire levée -->
|
||||
<path d="M5 11 L19 11 L19 21 L5 21 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M8 11 L8 6 L10 4 L14 4 L16 6 L16 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M12 14.5 L12 17.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="pawol" viewBox="0 0 24 24">
|
||||
<!-- bulle de parole angulaire : rectangle à queue, deux lignes de texte -->
|
||||
<path d="M3 4 L21 4 L21 15 L13 15 L8 20 L8 15 L3 15 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M7 8 L17 8 M7 11.5 L13 11.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
<symbol id="mizik-note" viewBox="0 0 24 24">
|
||||
<!-- double croche : deux tiges reliées par un croc, têtes en losange -->
|
||||
<path d="M9 6 L9 19 M19 4 L19 17 M9 6 L19 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M9 16.5 L11.5 19 L9 21.5 L6.5 19 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
<path d="M19 14.5 L21.5 17 L19 19.5 L16.5 17 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 7.0 KiB |
@@ -7,8 +7,8 @@
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#001219",
|
||||
"theme_color": "#001219",
|
||||
"background_color": "#0D0D0D",
|
||||
"theme_color": "#0D0D0D",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://syel.o-k-i.net/sitemap.xml
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://syel.o-k-i.net/</loc>
|
||||
<lastmod>2026-07-21</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://syel.o-k-i.net/nostr/</loc>
|
||||
<lastmod>2026-07-21</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://syel.o-k-i.net/simplex/</loc>
|
||||
<lastmod>2026-07-21</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
</urlset>
|
||||