feat: add localized docs pages
This commit is contained in:
Generated
+10
@@ -15,6 +15,7 @@
|
|||||||
"@tanstack/react-router": "^1.168.25",
|
"@tanstack/react-router": "^1.168.25",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
"lil-gui": "^0.21.0",
|
"lil-gui": "^0.21.0",
|
||||||
|
"lucide-react": "^1.11.0",
|
||||||
"r3f-perf": "^7.2.3",
|
"r3f-perf": "^7.2.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
@@ -3331,6 +3332,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-UOhjdztXCgdBReRcIhsvz2siIBogfv/lhJEIViCpLt924dO+GDms9T7DNoucI23s6kEPpe988m5N0D2ajnzb2g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/maath": {
|
"node_modules/maath": {
|
||||||
"version": "0.10.8",
|
"version": "0.10.8",
|
||||||
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@tanstack/react-router": "^1.168.25",
|
"@tanstack/react-router": "^1.168.25",
|
||||||
"gsap": "^3.15.0",
|
"gsap": "^3.15.0",
|
||||||
"lil-gui": "^0.21.0",
|
"lil-gui": "^0.21.0",
|
||||||
|
"lucide-react": "^1.11.0",
|
||||||
"r3f-perf": "^7.2.3",
|
"r3f-perf": "^7.2.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
|||||||
+192
-58
@@ -1,6 +1,8 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
font-family: Inter;
|
font-family: "Helvetica Neue", Helvetica, Inter, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -29,131 +31,257 @@ canvas {
|
|||||||
|
|
||||||
.docs-page {
|
.docs-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
grid-template-columns: 300px minmax(0, 1fr);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background:
|
background: #050505;
|
||||||
radial-gradient(
|
color: #f4efe7;
|
||||||
circle at top left,
|
font-family: "Helvetica Neue", Helvetica, Inter, Arial, sans-serif;
|
||||||
rgba(77, 146, 217, 0.18),
|
|
||||||
transparent 34rem
|
|
||||||
),
|
|
||||||
#071019;
|
|
||||||
color: rgba(255, 255, 255, 0.88);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-sidebar {
|
.docs-sidebar {
|
||||||
padding: 32px 24px;
|
border-right: 2px solid #d8d0c4;
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
background: #050505;
|
||||||
background: rgba(4, 7, 13, 0.72);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-sidebar h1 {
|
.docs-sidebar__header,
|
||||||
margin: 24px 0 18px;
|
.docs-content__header {
|
||||||
font-size: 26px;
|
display: flex;
|
||||||
line-height: 1.1;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 78px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border-bottom: 2px solid #d8d0c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-sidebar__header h1,
|
||||||
|
.docs-content__header span {
|
||||||
|
margin: 0;
|
||||||
|
color: #f4efe7;
|
||||||
|
font-size: 21px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-sidebar nav {
|
.docs-sidebar nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-sidebar a,
|
.docs-sidebar a {
|
||||||
.docs-back-link {
|
color: #f4efe7;
|
||||||
color: rgba(255, 255, 255, 0.78);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-sidebar nav a {
|
.docs-nav-item {
|
||||||
padding: 10px 12px;
|
display: flex;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
align-items: center;
|
||||||
border-radius: 10px;
|
justify-content: space-between;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
min-height: 46px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 2px solid #d8d0c4;
|
||||||
|
color: #f4efe7;
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-home-link {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-item span:first-child {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-item strong {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-nav-item small,
|
||||||
|
.docs-nav-item__meta {
|
||||||
|
color: #a9a196;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-sidebar a:hover,
|
.docs-sidebar a:hover,
|
||||||
.docs-sidebar a:focus-visible {
|
.docs-sidebar a:focus-visible,
|
||||||
color: white;
|
.docs-nav-item--active {
|
||||||
border-color: rgba(255, 255, 255, 0.24);
|
background: #f4efe7;
|
||||||
|
color: #050505;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-back-link {
|
.docs-sidebar a:hover small,
|
||||||
display: inline-flex;
|
.docs-sidebar a:hover .docs-nav-item__meta,
|
||||||
font-size: 14px;
|
.docs-sidebar a:focus-visible small,
|
||||||
|
.docs-sidebar a:focus-visible .docs-nav-item__meta,
|
||||||
|
.docs-nav-item--active small,
|
||||||
|
.docs-nav-item--active .docs-nav-item__meta {
|
||||||
|
color: #050505;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-content {
|
.docs-content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
padding: 48px clamp(20px, 5vw, 72px);
|
background: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-content__header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
padding: 2px;
|
||||||
|
border: 2px solid #d8d0c4;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
color: #f4efe7;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle span {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 36px;
|
||||||
|
min-height: 26px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #a9a196;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle .is-active {
|
||||||
|
background: #f4efe7;
|
||||||
|
color: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-language-toggle:hover,
|
||||||
|
.docs-language-toggle:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(216, 208, 196, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section {
|
.docs-section {
|
||||||
max-width: 980px;
|
max-width: 920px;
|
||||||
margin: 0 auto 48px;
|
margin: 0 auto;
|
||||||
padding: clamp(24px, 4vw, 44px);
|
padding: 34px clamp(18px, 4vw, 56px) 48px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
}
|
||||||
border-radius: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.055);
|
.docs-section__eyebrow {
|
||||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.26);
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
color: #a9a196;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section h1,
|
.docs-section h1,
|
||||||
.docs-section h2,
|
.docs-section h2,
|
||||||
.docs-section h3 {
|
.docs-section h3 {
|
||||||
color: white;
|
color: #f4efe7;
|
||||||
line-height: 1.15;
|
letter-spacing: -0.06em;
|
||||||
|
line-height: 1.05;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section h1 {
|
.docs-section h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
font-size: clamp(32px, 5vw, 56px);
|
margin-bottom: 20px;
|
||||||
|
font-size: clamp(46px, 7vw, 88px);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section h2 {
|
.docs-section h2 {
|
||||||
margin-top: 34px;
|
margin-top: 44px;
|
||||||
font-size: clamp(24px, 3vw, 34px);
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #d8d0c4;
|
||||||
|
font-size: clamp(28px, 4vw, 44px);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section h3 {
|
.docs-section h3 {
|
||||||
margin-top: 26px;
|
margin-top: 30px;
|
||||||
font-size: 20px;
|
margin-bottom: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section p,
|
.docs-section p,
|
||||||
.docs-section li {
|
.docs-section li {
|
||||||
|
color: #d8d0c4;
|
||||||
|
font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-section ul,
|
||||||
|
.docs-section ol {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.docs-section a {
|
.docs-section a {
|
||||||
color: #8bd3ff;
|
color: #f4efe7;
|
||||||
|
text-underline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section code {
|
.docs-section code {
|
||||||
border-radius: 6px;
|
border: 0;
|
||||||
padding: 2px 6px;
|
border-radius: 2px;
|
||||||
background: rgba(255, 255, 255, 0.12);
|
padding: 2px 5px;
|
||||||
color: #d7f2ff;
|
background: rgba(216, 208, 196, 0.22);
|
||||||
|
color: #f4efe7;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 0.92em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section pre {
|
.docs-section pre {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
border-radius: 16px;
|
border: 0;
|
||||||
background: rgba(0, 0, 0, 0.34);
|
border-radius: 0;
|
||||||
|
background: #0d0d0d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section pre code {
|
.docs-section pre code {
|
||||||
|
display: block;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
color: #f4efe7;
|
||||||
|
line-height: 1.45;
|
||||||
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-section table {
|
.docs-section table {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
@@ -161,15 +289,21 @@ canvas {
|
|||||||
.docs-section th,
|
.docs-section th,
|
||||||
.docs-section td {
|
.docs-section td {
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
border: 2px solid #d8d0c4;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-section th {
|
||||||
|
background: #111;
|
||||||
|
color: #f4efe7;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.docs-section blockquote {
|
.docs-section blockquote {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding-left: 18px;
|
padding-left: 18px;
|
||||||
border-left: 3px solid rgba(139, 211, 255, 0.72);
|
border-left: 2px solid #d8d0c4;
|
||||||
color: rgba(255, 255, 255, 0.72);
|
color: #a9a196;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
@@ -180,7 +314,7 @@ canvas {
|
|||||||
|
|
||||||
.docs-sidebar {
|
.docs-sidebar {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
border-bottom: 2px solid #d8d0c4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.docs-content {
|
.docs-content {
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import remarkGfm from "remark-gfm";
|
|
||||||
import readme from "../../README.md?raw";
|
|
||||||
import architecture from "../../docs/technical/architecture.md?raw";
|
|
||||||
import targetArchitecture from "../../docs/technical/target-architecture.md?raw";
|
|
||||||
import features from "../../docs/user/features.md?raw";
|
|
||||||
|
|
||||||
interface DocSection {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const docSections: DocSection[] = [
|
|
||||||
{
|
|
||||||
id: "readme",
|
|
||||||
title: "README",
|
|
||||||
content: readme,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "architecture",
|
|
||||||
title: "Architecture actuelle",
|
|
||||||
content: architecture,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "target-architecture",
|
|
||||||
title: "Architecture cible",
|
|
||||||
content: targetArchitecture,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "features",
|
|
||||||
title: "Fonctionnalites",
|
|
||||||
content: features,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function DocsPage(): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<main className="docs-page">
|
|
||||||
<aside className="docs-sidebar" aria-label="Documentation">
|
|
||||||
<Link className="docs-back-link" to="/">
|
|
||||||
Retour a l'experience 3D
|
|
||||||
</Link>
|
|
||||||
<h1>Documentation</h1>
|
|
||||||
<nav>
|
|
||||||
{docSections.map((section) => (
|
|
||||||
<a key={section.id} href={`#${section.id}`}>
|
|
||||||
{section.title}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<div className="docs-content">
|
|
||||||
{docSections.map((section) => (
|
|
||||||
<article key={section.id} id={section.id} className="docs-section">
|
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
||||||
{section.content}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { useDocsLanguage } from "@/pages/docs/useDocsLanguage";
|
||||||
|
|
||||||
|
interface DocsDocumentProps {
|
||||||
|
title: string;
|
||||||
|
meta: string;
|
||||||
|
content: string;
|
||||||
|
frContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsDocument({
|
||||||
|
title,
|
||||||
|
meta,
|
||||||
|
content,
|
||||||
|
frContent,
|
||||||
|
}: DocsDocumentProps): React.JSX.Element {
|
||||||
|
const { language, toggleLanguage } = useDocsLanguage();
|
||||||
|
const translatedContent = language === "fr" ? frContent : content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="docs-content">
|
||||||
|
<header className="docs-content__header">
|
||||||
|
<span>{title}</span>
|
||||||
|
<button
|
||||||
|
className="docs-language-toggle"
|
||||||
|
type="button"
|
||||||
|
onClick={toggleLanguage}
|
||||||
|
aria-label="Changer la langue de la documentation"
|
||||||
|
>
|
||||||
|
<span className={language === "fr" ? "is-active" : undefined}>
|
||||||
|
FR
|
||||||
|
</span>
|
||||||
|
<span className={language === "en" ? "is-active" : undefined}>
|
||||||
|
EN
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article className="docs-section">
|
||||||
|
<div className="docs-section__eyebrow">
|
||||||
|
<span>{title}</span>
|
||||||
|
<span>{meta}</span>
|
||||||
|
</div>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{translatedContent}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
DocsLanguageContext,
|
||||||
|
type DocsLanguage,
|
||||||
|
} from "@/pages/docs/docsLanguageContext";
|
||||||
|
|
||||||
|
interface DocsLanguageProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocsLanguageProvider({
|
||||||
|
children,
|
||||||
|
}: DocsLanguageProviderProps): React.JSX.Element {
|
||||||
|
const [language, setLanguage] = useState<DocsLanguage>("en");
|
||||||
|
|
||||||
|
function toggleLanguage(): void {
|
||||||
|
setLanguage((currentLanguage) => (currentLanguage === "en" ? "fr" : "en"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocsLanguageContext value={{ language, toggleLanguage }}>
|
||||||
|
{children}
|
||||||
|
</DocsLanguageContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Link, Outlet } from "@tanstack/react-router";
|
||||||
|
import { Home } from "lucide-react";
|
||||||
|
import { DocsLanguageProvider } from "@/pages/docs/DocsLanguageProvider";
|
||||||
|
import { docSections } from "@/pages/docs/docsSections";
|
||||||
|
|
||||||
|
export function DocsLayout(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsLanguageProvider>
|
||||||
|
<main className="docs-page">
|
||||||
|
<aside className="docs-sidebar" aria-label="Documentation">
|
||||||
|
<header className="docs-sidebar__header">
|
||||||
|
<h1>Folders</h1>
|
||||||
|
<Link
|
||||||
|
className="docs-home-link"
|
||||||
|
to="/"
|
||||||
|
aria-label="Retour a l'accueil"
|
||||||
|
>
|
||||||
|
<Home size={18} strokeWidth={2.25} aria-hidden="true" />
|
||||||
|
</Link>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
{docSections.map((section) => (
|
||||||
|
<Link
|
||||||
|
activeProps={{
|
||||||
|
className: "docs-nav-item docs-nav-item--active",
|
||||||
|
}}
|
||||||
|
activeOptions={{ exact: true }}
|
||||||
|
className="docs-nav-item"
|
||||||
|
key={section.path}
|
||||||
|
to={section.path}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<strong>{section.title}</strong>
|
||||||
|
<small>{section.subtitle}</small>
|
||||||
|
</span>
|
||||||
|
<span className="docs-nav-item__meta">{section.meta}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</DocsLanguageProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import architecture from "../../../../docs/technical/architecture.md?raw";
|
||||||
|
import { DocsDocument } from "@/pages/docs/DocsDocument";
|
||||||
|
import { architectureFr } from "@/pages/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsArchitecturePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={architecture}
|
||||||
|
frContent={architectureFr}
|
||||||
|
meta="02"
|
||||||
|
title="Architecture actuelle"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export type DocsLanguage = "en" | "fr";
|
||||||
|
|
||||||
|
export interface DocsLanguageContextValue {
|
||||||
|
language: DocsLanguage;
|
||||||
|
toggleLanguage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocsLanguageContext =
|
||||||
|
createContext<DocsLanguageContextValue | null>(null);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export interface DocSection {
|
||||||
|
path: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
meta: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const docSections: DocSection[] = [
|
||||||
|
{
|
||||||
|
path: "/docs",
|
||||||
|
title: "README",
|
||||||
|
subtitle: "Project overview",
|
||||||
|
meta: "01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/architecture",
|
||||||
|
title: "Architecture actuelle",
|
||||||
|
subtitle: "Runtime structure",
|
||||||
|
meta: "02",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/target-architecture",
|
||||||
|
title: "Architecture cible",
|
||||||
|
subtitle: "Next direction",
|
||||||
|
meta: "03",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/docs/features",
|
||||||
|
title: "Fonctionnalites",
|
||||||
|
subtitle: "Implemented scope",
|
||||||
|
meta: "04",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
export const readmeFr = `# La-Fabrik
|
||||||
|
|
||||||
|
Une expérience web 3D interactive pour La Fabrik Durable, un service low-tech de réparation et de transformation situé à Altera, une ville post-capitaliste reconstruite en 2039. Les joueurs incarnent un technicien fraîchement intégré et vivent une journée de service : réparer un vélo électrique, remettre en état un réseau d'énergie et améliorer le système d'irrigation d'une ferme verticale.
|
||||||
|
|
||||||
|
Construit avec React, Three.js et Vite. Fonctionne dans le navigateur, sans installation côté utilisateur.
|
||||||
|
|
||||||
|
## Stack technique
|
||||||
|
|
||||||
|
### Build et langage
|
||||||
|
|
||||||
|
| Package |
|
||||||
|
| -------------------------------------------------- |
|
||||||
|
| [TypeScript](https://www.typescriptlang.org/docs/) |
|
||||||
|
| [React](https://react.dev/learn) |
|
||||||
|
| [Vite](https://vite.dev/guide/) |
|
||||||
|
| [ESLint](https://eslint.org/docs/latest/) |
|
||||||
|
| [Prettier](https://prettier.io/docs/) |
|
||||||
|
|
||||||
|
### Moteur 3D
|
||||||
|
|
||||||
|
| Package |
|
||||||
|
| ----------------------------------------------------------------------------------------- |
|
||||||
|
| [Three.js](https://threejs.org/docs/) |
|
||||||
|
| [@react-three/fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) |
|
||||||
|
| [@react-three/drei](https://pmndrs.github.io/drei) |
|
||||||
|
| [@react-three/rapier](https://rapier.rs/docs/) |
|
||||||
|
| [@react-three/postprocessing](https://github.com/pmndrs/postprocessing) |
|
||||||
|
| [GSAP](https://gsap.com/docs/v3/Installation/) |
|
||||||
|
|
||||||
|
### Performance et effets
|
||||||
|
|
||||||
|
| Package |
|
||||||
|
| --------------------------------------------------------------------------- |
|
||||||
|
| [r3f-perf](https://github.com/utsuboco/r3f-perf) |
|
||||||
|
| [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer) |
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
la-fabrik/
|
||||||
|
├── public/
|
||||||
|
│ ├── models/
|
||||||
|
│ │ ├── map/ # Carte de base, chargée au démarrage
|
||||||
|
│ │ ├── workshop/
|
||||||
|
│ │ ├── powerGrid/
|
||||||
|
│ │ └── farm/
|
||||||
|
│ ├── textures/
|
||||||
|
│ └── sounds/
|
||||||
|
│
|
||||||
|
└── src/
|
||||||
|
├── world/ # Monde 3D persistant
|
||||||
|
│ ├── World.tsx # Composition principale de la scène
|
||||||
|
│ ├── Map.tsx # Carte de base, toujours montée
|
||||||
|
│ ├── Lighting.tsx # Lumières ambiante, directionnelle et ponctuelles
|
||||||
|
│ ├── Environment.tsx # HDRI, brouillard, ciel
|
||||||
|
│ ├── PostFX.tsx # Bloom, SSAO, aberration chromatique
|
||||||
|
│ ├── zones/ # Zones spatiales, LOD par zone
|
||||||
|
│ └── player/ # Contrôleur joueur et caméra
|
||||||
|
│
|
||||||
|
├── components/
|
||||||
|
│ ├── 3d/ # Éléments 3D réutilisables
|
||||||
|
│ └── ui/ # Overlays HTML hors Canvas
|
||||||
|
│
|
||||||
|
├── stateManager/ # Logique, état et orchestration
|
||||||
|
├── hooks/ # Hooks React autour des managers
|
||||||
|
├── data/ # Configuration statique
|
||||||
|
├── shaders/ # Shaders GLSL
|
||||||
|
└── utils/ # Utilitaires partagés et debug
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Démarrage
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
git clone https://github.com/La-Fabrik-Durable/La-Fabrik.git
|
||||||
|
cd La-Fabrik
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
- application : \`http://localhost:5173\`
|
||||||
|
- mode debug : \`http://localhost:5173?debug\`
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Voir le fichier [LICENSE](./LICENSE).
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const architectureFr = `# Architecture actuelle
|
||||||
|
|
||||||
|
Ce document décrit le code réellement présent aujourd'hui dans le dépôt.
|
||||||
|
|
||||||
|
## Structure runtime
|
||||||
|
|
||||||
|
- \`src/App.tsx\` monte le \`Canvas\`, le \`World\` 3D, l'overlay de performance debug et les overlays HTML.
|
||||||
|
- \`src/world/World.tsx\` compose la scène active avec :
|
||||||
|
- l'environnement et l'éclairage
|
||||||
|
- les helpers debug et le mode caméra debug
|
||||||
|
- soit la carte principale, soit la scène de test physique debug
|
||||||
|
- le rig joueur quand le mode caméra actif est \`player\`
|
||||||
|
- \`src/world/Map.tsx\` charge le modèle principal de la carte et construit l'octree de collision.
|
||||||
|
- \`src/world/debug/TestScene.tsx\` fournit une scène orientée debug pour les interactions et la physique.
|
||||||
|
- \`src/world/player/PlayerComponent.tsx\` monte la caméra et le contrôleur.
|
||||||
|
- \`src/world/player/PlayerController.tsx\` gère le mouvement pointer lock, le saut et les inputs d'interaction.
|
||||||
|
|
||||||
|
## Modèle d'interaction
|
||||||
|
|
||||||
|
- \`src/stateManager/InteractionManager.ts\` est la source d'état actuelle des interactions.
|
||||||
|
- \`src/components/3d/InteractableObject.tsx\` gère la détection de focus par distance et raycasting.
|
||||||
|
- \`src/components/3d/TriggerObject.tsx\` implémente les interactions de type trigger.
|
||||||
|
- \`src/components/3d/GrabbableObject.tsx\` implémente les interactions saisir / relâcher.
|
||||||
|
- \`src/hooks/useInteraction.ts\` expose un snapshot d'interaction à l'UI React.
|
||||||
|
- \`src/components/ui/InteractPrompt.tsx\` affiche le prompt \`E\` pour les interactions trigger.
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
- \`src/stateManager/AudioManager.ts\` fournit actuellement une lecture de sons one-shot avec pool.
|
||||||
|
- Les interactions trigger peuvent lancer directement un son via \`AudioManager\`.
|
||||||
|
|
||||||
|
## Système debug
|
||||||
|
|
||||||
|
- Le mode debug est activé avec \`?debug\`.
|
||||||
|
- \`src/utils/debug/Debug.ts\` possède l'instance \`lil-gui\` et les contrôles debug.
|
||||||
|
- \`src/hooks/debug/useCameraMode.ts\` et \`src/hooks/debug/useSceneMode.ts\` s'abonnent à l'état debug.
|
||||||
|
- \`src/utils/debug/DebugPerf.tsx\` monte \`r3f-perf\` en lazy uniquement en mode debug.
|
||||||
|
- \`src/utils/debug/scene/DebugHelpers.tsx\` monte les helpers debug.
|
||||||
|
- \`src/utils/debug/scene/DebugCameraControls.tsx\` monte la caméra libre debug.
|
||||||
|
|
||||||
|
## Limites actuelles
|
||||||
|
|
||||||
|
- Le dépôt est encore un prototype, pas le runtime complet du jeu.
|
||||||
|
- \`src/world/debug/TestScene.tsx\` fait encore partie de la composition active.
|
||||||
|
- Il n'existe pas encore d'orchestrateur gameplay central comme \`GameManager\`.
|
||||||
|
- Les systèmes de missions, zones, cinématiques et dialogues ne sont pas implémentés.
|
||||||
|
- Le joueur utilise une collision octree et des règles simples, pas une pile physique gameplay complète.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const targetArchitectureFr = `# Architecture cible
|
||||||
|
|
||||||
|
Ce document décrit l'architecture visée à moyen terme pour le projet.
|
||||||
|
|
||||||
|
## Relation avec le code actuel
|
||||||
|
|
||||||
|
- \`docs/technical/architecture.md\` reste la source de vérité de ce qui existe maintenant.
|
||||||
|
- Ce document est volontairement aspirational.
|
||||||
|
- Si ce document contredit l'implémentation actuelle, l'implémentation actuelle gagne.
|
||||||
|
|
||||||
|
## Objectifs
|
||||||
|
|
||||||
|
- Garder \`App.tsx\` petit et centré sur l'orchestration.
|
||||||
|
- Séparer le code de production du monde des chemins runtime uniquement debug.
|
||||||
|
- Garder une source de vérité claire par responsabilité.
|
||||||
|
- Faire grandir les systèmes gameplay progressivement, sans préconstruire une architecture vide.
|
||||||
|
|
||||||
|
## Couches prévues
|
||||||
|
|
||||||
|
### Couche App
|
||||||
|
|
||||||
|
- \`App.tsx\` monte la scène canvas et les overlays HTML de premier niveau.
|
||||||
|
- Il doit rester fin et éviter la logique gameplay.
|
||||||
|
|
||||||
|
### Couche World
|
||||||
|
|
||||||
|
- \`src/world/\` doit contenir la composition de scène de production et les objets de scène de production.
|
||||||
|
- Responsabilités attendues :
|
||||||
|
- composition du monde
|
||||||
|
- carte, environnement, éclairage
|
||||||
|
- contrôleur joueur
|
||||||
|
- ancres d'interaction de production
|
||||||
|
- post-processing de production si nécessaire
|
||||||
|
|
||||||
|
### Couche Debug
|
||||||
|
|
||||||
|
- Les scènes et outils uniquement debug doivent être isolés du chemin de production.
|
||||||
|
- Responsabilités attendues :
|
||||||
|
- \`lil-gui\`
|
||||||
|
- overlay de performance
|
||||||
|
- helpers de scène
|
||||||
|
- caméra libre et contrôles de calibration
|
||||||
|
- scènes temporaires de test utilisées pendant le développement
|
||||||
|
|
||||||
|
### Couche UI
|
||||||
|
|
||||||
|
- \`src/components/ui/\` doit contenir les overlays HTML visibles par le joueur.
|
||||||
|
- Exemples futurs :
|
||||||
|
- crosshair
|
||||||
|
- flow de chargement
|
||||||
|
- HUD de mission
|
||||||
|
- overlays narratifs
|
||||||
|
|
||||||
|
### Couche Gameplay
|
||||||
|
|
||||||
|
- À mesure que le projet grandit, l'état gameplay peut évoluer vers une couche d'orchestration plus claire.
|
||||||
|
- Sujets probables :
|
||||||
|
- missions
|
||||||
|
- zones
|
||||||
|
- cinématiques
|
||||||
|
- dialogues
|
||||||
|
- audio
|
||||||
|
- interactions
|
||||||
|
|
||||||
|
## Règles
|
||||||
|
|
||||||
|
- Préférer du code direct et fonctionnel plutôt qu'un échafaudage spéculatif.
|
||||||
|
- Les types partagés doivent rester proches de leur domaine jusqu'à avoir plusieurs vrais consommateurs.
|
||||||
|
- Éviter de créer de nouveaux managers ou services sans besoin runtime actif.
|
||||||
|
- Les chemins runtime uniquement debug doivent être clairement marqués et faciles à retirer plus tard.
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const featuresFr = `# Fonctionnalités implémentées
|
||||||
|
|
||||||
|
Ce document liste les fonctionnalités présentes dans le code actuel.
|
||||||
|
|
||||||
|
## Scène
|
||||||
|
|
||||||
|
- Scène React Three Fiber plein écran
|
||||||
|
- Carte principale chargée depuis \`public/models/map/model.gltf\`
|
||||||
|
- Scène de test physique debug sélectionnable depuis le panneau debug
|
||||||
|
- Éclairage ambiant et directionnel
|
||||||
|
- Configuration de l'environnement de fond
|
||||||
|
|
||||||
|
## Joueur
|
||||||
|
|
||||||
|
- Mode caméra joueur
|
||||||
|
- Orientation souris avec pointer lock
|
||||||
|
- Déplacement avec \`ZQSD\`
|
||||||
|
- Saut
|
||||||
|
- Collision basée sur une octree contre la carte chargée
|
||||||
|
|
||||||
|
## Interactions
|
||||||
|
|
||||||
|
- Détection de focus par distance et raycast
|
||||||
|
- Interactions trigger activées avec \`E\`
|
||||||
|
- Interactions grab activées avec le bouton principal de la souris
|
||||||
|
- Prompt d'interaction affiché pour les interactions trigger
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
- Lecture de sons one-shot pour les interactions trigger
|
||||||
|
- Pool simple par son via \`AudioManager\`
|
||||||
|
|
||||||
|
## Outils debug
|
||||||
|
|
||||||
|
- Le paramètre \`?debug\` active le panneau debug
|
||||||
|
- Contrôles \`lil-gui\` pour le mode caméra, le mode scène et les sphères d'interaction
|
||||||
|
- Helpers de scène debug
|
||||||
|
- Caméra libre debug
|
||||||
|
- Overlay \`r3f-perf\`
|
||||||
|
|
||||||
|
## Pas encore implémenté
|
||||||
|
|
||||||
|
- système de missions
|
||||||
|
- système de zones
|
||||||
|
- système de cinématiques
|
||||||
|
- système de dialogues
|
||||||
|
- flow de chargement
|
||||||
|
- minimap et HUD de mission
|
||||||
|
- séparation complète production / debug pour les scènes gameplay
|
||||||
|
`;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import features from "../../../../docs/user/features.md?raw";
|
||||||
|
import { DocsDocument } from "@/pages/docs/DocsDocument";
|
||||||
|
import { featuresFr } from "@/pages/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsFeaturesPage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={features}
|
||||||
|
frContent={featuresFr}
|
||||||
|
meta="04"
|
||||||
|
title="Fonctionnalites"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import readme from "../../../README.md?raw";
|
||||||
|
import { DocsDocument } from "@/pages/docs/DocsDocument";
|
||||||
|
import { readmeFr } from "@/pages/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsReadmePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={readme}
|
||||||
|
frContent={readmeFr}
|
||||||
|
meta="01"
|
||||||
|
title="README"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import targetArchitecture from "../../../../docs/technical/target-architecture.md?raw";
|
||||||
|
import { DocsDocument } from "@/pages/docs/DocsDocument";
|
||||||
|
import { targetArchitectureFr } from "@/pages/docs/docsTranslations";
|
||||||
|
|
||||||
|
export function DocsTargetArchitecturePage(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<DocsDocument
|
||||||
|
content={targetArchitecture}
|
||||||
|
frContent={targetArchitectureFr}
|
||||||
|
meta="03"
|
||||||
|
title="Architecture cible"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { DocsLanguageContext } from "@/pages/docs/docsLanguageContext";
|
||||||
|
|
||||||
|
export function useDocsLanguage() {
|
||||||
|
const context = useContext(DocsLanguageContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useDocsLanguage must be used inside DocsLanguageProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
+39
-3
@@ -4,8 +4,12 @@ import {
|
|||||||
createRoute,
|
createRoute,
|
||||||
createRouter,
|
createRouter,
|
||||||
} from "@tanstack/react-router";
|
} from "@tanstack/react-router";
|
||||||
import { DocsPage } from "@/pages/DocsPage";
|
|
||||||
import { HomePage } from "@/pages/HomePage";
|
import { HomePage } from "@/pages/HomePage";
|
||||||
|
import { DocsArchitecturePage } from "@/pages/docs/architecture/page";
|
||||||
|
import { DocsLayout } from "@/pages/docs/DocsLayout";
|
||||||
|
import { DocsFeaturesPage } from "@/pages/docs/features/page";
|
||||||
|
import { DocsReadmePage } from "@/pages/docs/page";
|
||||||
|
import { DocsTargetArchitecturePage } from "@/pages/docs/target-architecture/page";
|
||||||
|
|
||||||
const rootRoute = createRootRoute({
|
const rootRoute = createRootRoute({
|
||||||
component: Outlet,
|
component: Outlet,
|
||||||
@@ -20,10 +24,42 @@ const indexRoute = createRoute({
|
|||||||
const docsRoute = createRoute({
|
const docsRoute = createRoute({
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
path: "/docs",
|
path: "/docs",
|
||||||
component: DocsPage,
|
component: DocsLayout,
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeTree = rootRoute.addChildren([indexRoute, docsRoute]);
|
const docsIndexRoute = createRoute({
|
||||||
|
getParentRoute: () => docsRoute,
|
||||||
|
path: "/",
|
||||||
|
component: DocsReadmePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docsArchitectureRoute = createRoute({
|
||||||
|
getParentRoute: () => docsRoute,
|
||||||
|
path: "/architecture",
|
||||||
|
component: DocsArchitecturePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docsTargetArchitectureRoute = createRoute({
|
||||||
|
getParentRoute: () => docsRoute,
|
||||||
|
path: "/target-architecture",
|
||||||
|
component: DocsTargetArchitecturePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docsFeaturesRoute = createRoute({
|
||||||
|
getParentRoute: () => docsRoute,
|
||||||
|
path: "/features",
|
||||||
|
component: DocsFeaturesPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([
|
||||||
|
indexRoute,
|
||||||
|
docsRoute.addChildren([
|
||||||
|
docsIndexRoute,
|
||||||
|
docsArchitectureRoute,
|
||||||
|
docsTargetArchitectureRoute,
|
||||||
|
docsFeaturesRoute,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
export const router = createRouter({ routeTree });
|
export const router = createRouter({ routeTree });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user