- );
-}
-
-function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
- useModalScrollLock();
- const [toolboxId, setToolboxId] = useState(selectedId);
- return (
-
-
onClose(null)} />
-
-
- );
-}
-
-function ScreenshotViewer({ shot, onClose }) {
- useModalScrollLock();
- const canAnnotate = Boolean(shot.canAnnotate && shot.onChangeMarkers);
- const [viewerMarkers, setViewerMarkers] = useState(() => Array.isArray(shot.markers) ? shot.markers : []);
- const viewerRef = useRef(null);
- const mediaRef = useRef(null);
- const hasMarkers = viewerMarkers.length > 0;
-
- useEffect(() => {
- setViewerMarkers(Array.isArray(shot.markers) ? shot.markers : []);
- }, [shot]);
-
- useEffect(() => {
- if (!viewerRef.current || !mediaRef.current) return undefined;
- const viewer = viewerRef.current;
- const frame = mediaRef.current;
-
- function syncImageHeight() {
- const maxHeight = Math.max(400, window.innerHeight * 0.8);
- const height = Math.min(maxHeight, Math.max(400, Math.round(frame.getBoundingClientRect().height)));
- viewer.style.setProperty("--viewer-image-height", `${height}px`);
- }
-
- syncImageHeight();
- const observer = new ResizeObserver(syncImageHeight);
- observer.observe(frame);
- window.addEventListener("resize", syncImageHeight);
- return () => {
- observer.disconnect();
- window.removeEventListener("resize", syncImageHeight);
- };
- }, [shot.dataUrl, canAnnotate]);
-
- useEffect(() => {
- const onKeyDown = (event) => {
- if (event.key === "Escape") onClose();
- };
- document.addEventListener("keydown", onKeyDown);
- return () => document.removeEventListener("keydown", onKeyDown);
- }, [onClose]);
-
- function updateViewerMarkers(nextMarkers) {
- setViewerMarkers(nextMarkers);
- shot.onChangeMarkers?.(nextMarkers);
- }
-
- function addViewerMarker(event) {
- if (!canAnnotate || !mediaRef.current) return;
- const rect = mediaRef.current.getBoundingClientRect();
- const nextMarkers = [
- ...viewerMarkers,
- {
- id: shot.createMarkerId?.() || uid("marker"),
- label: "",
- x: clampPercent(((event.clientX - rect.left) / rect.width) * 100),
- y: clampPercent(((event.clientY - rect.top) / rect.height) * 100)
- }
- ];
- updateViewerMarkers(nextMarkers);
- }
-
- function updateViewerMarker(markerId, updater) {
- updateViewerMarkers(viewerMarkers.map((marker) => marker.id === markerId ? updater(marker) : marker));
- }
-
- function removeViewerMarker(markerId) {
- updateViewerMarkers(viewerMarkers.filter((marker) => marker.id !== markerId));
- }
-
- return (
-
-
-
-
-
-
-
-

- {viewerMarkers.map((marker, index) => (
-
- {index + 1}
-
- ))}
-
-
- {canAnnotate && (
-
- )}
-
-
-
- );
-}
-
-function StorageQuota({ usage }) {
- const safeUsage = usage || { used: 0, limit: 0, ratio: 0 };
- const softRatio = Math.min(1, safeUsage.used / APP_STORAGE_SOFT_LIMIT_BYTES);
- const percent = Math.round(softRatio * 100);
- const state = safeUsage.used >= APP_STORAGE_SOFT_LIMIT_BYTES ? "danger" : softRatio >= APP_STORAGE_WARNING_RATIO ? "warning" : "ok";
- const browserQuota = safeUsage.limit ? `Quota navigateur estimé : ${formatBytes(safeUsage.limit)}` : "Quota navigateur estimé indisponible";
- return (
-
- Stockage local recommandé{formatBytes(safeUsage.used)} / {formatBytes(APP_STORAGE_SOFT_LIMIT_BYTES)}
-
-
- );
-}
-
-function ImportButton({ className, label, title, ariaLabel, icon, onFile }) {
- return (
-
- );
-}
-
createRoot(document.querySelector("#app")).render(
);
diff --git a/website/src/pages/AboutPage.jsx b/website/src/pages/AboutPage.jsx
new file mode 100644
index 0000000..5c51946
--- /dev/null
+++ b/website/src/pages/AboutPage.jsx
@@ -0,0 +1,102 @@
+// Rôle : présente le projet, son fonctionnement local et les repères de contribution.
+import React, { useEffect } from "react";
+import { RichText } from "../components/RichText.jsx";
+
+export function AboutPage({ siteContent }) {
+ const content = siteContent.about;
+ const limits = Array.isArray(content.limits) ? content.limits : [];
+ const storageSectionIndex = content.sections.findIndex((section) => section.title.toLowerCase().includes("stockage"));
+ const normalizeReminder = (item, index) => typeof item === "string" ? { title: `Point ${index + 1}`, text: item, icon: "toolbox" } : item;
+ useEffect(() => {
+ if (!location.hash.includes("#contribuer")) return;
+ requestAnimationFrame(() => document.querySelector("#contribuer")?.scrollIntoView({ behavior: "smooth", block: "start" }));
+ }, []);
+
+ return (
+
+
+
+
{content.eyebrow}
+
{content.title}
+
+
+
+
+
+
Principes
+
Comment ça fonctionne
+
+
+ {content.sections.map((section, index) => (
+
+ {String(index + 1).padStart(2, "0")}
+
+
{section.title}
+
+ {index === storageSectionIndex && limits.length > 0 && (
+
+
{content.limitsTitle}
+
+ {limits.map((item, itemIndex) => {
+ const reminder = normalizeReminder(item, itemIndex);
+ return (
+ -
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
Toolbox
+
{content.toolsTitle}
+
+
+
+ {content.tools.map((tool) => (
+
+
+
+
+
+
+ ))}
+
+
+ {content.contribute && (
+
+
+
{content.contribute.eyebrow}
+
{content.contribute.title}
+
+
+
+
{content.contribute.example}
+
+
{content.contribute.promptTitle}
+
+
{content.contribute.promptText}
+
+
+
+ )}
+
+ );
+}
diff --git a/website/src/pages/HomePage.jsx b/website/src/pages/HomePage.jsx
new file mode 100644
index 0000000..30864ef
--- /dev/null
+++ b/website/src/pages/HomePage.jsx
@@ -0,0 +1,49 @@
+// Rôle : affiche la page d'accueil et les contenus éditoriaux principaux.
+import React from "react";
+import { TOOLBOX_MODULES } from "../features/toolboxes/modules/index.jsx";
+import { RichText } from "../components/RichText.jsx";
+
+export function HomePage({ siteContent, toolboxes }) {
+ const content = siteContent.home;
+ const toolCount = Object.keys(TOOLBOX_MODULES).length;
+ return (
+ <>
+
+
+
{content.hero.eyebrow}
+
{content.hero.title}
+
{content.hero.description}
+
+
+
+
{toolboxes.length}{toolboxes.length > 1 ? content.stats.toolboxPlural : content.stats.toolboxSingular}
+
{toolCount}{toolCount > 1 ? content.stats.toolPlural : content.stats.toolSingular}
+
+
+
+
+
{content.origin.eyebrow}
+
{content.origin.title}
+
+ {content.origin.lines.map((line, index) => )}
+ {content.origin.caption}
+
+
+
+

+
+
+
+ >
+ );
+}
+
+function DialogueLine({ line }) {
+ return
;
+}
diff --git a/website/src/router/RouteContent.jsx b/website/src/router/RouteContent.jsx
new file mode 100644
index 0000000..f2506b2
--- /dev/null
+++ b/website/src/router/RouteContent.jsx
@@ -0,0 +1,20 @@
+// Rôle : choisit la page à rendre selon la route active.
+import React from "react";
+import { GameRoute } from "../features/games/GameRoute.jsx";
+import { GamesPage } from "../features/games/GamesPage.jsx";
+import { ToolboxesPage, ToolboxPage } from "../features/toolboxes/ToolboxPages.jsx";
+import { AboutPage } from "../pages/AboutPage.jsx";
+import { HomePage } from "../pages/HomePage.jsx";
+import { navigate } from "./hashRouter.js";
+
+export function RouteContent(props) {
+ const { route } = props;
+ if (route === "/") return
;
+ if (route === "/about") return
;
+ if (route === "/toolboxes") return
;
+ if (route.startsWith("/toolbox/")) return
;
+ if (route === "/games") return
;
+ if (route.startsWith("/games/")) return
;
+ navigate("/");
+ return null;
+}
diff --git a/website/src/router/hashRouter.js b/website/src/router/hashRouter.js
new file mode 100644
index 0000000..4759bb8
--- /dev/null
+++ b/website/src/router/hashRouter.js
@@ -0,0 +1,27 @@
+// Rôle : synchronise la navigation hash avec l'état React.
+import { useEffect, useState } from "react";
+
+function currentRoute() {
+ const hashRoute = location.hash.replace(/^#/, "");
+ if (hashRoute) return hashRoute.replace(/#.+$/, "");
+ const path = location.pathname.replace(/\/+$/, "") || "/";
+ if (path === "/mhwilds") return "/games/mhwilds";
+ if (path === "/mhwilds/monsters") return "/games/mhwilds/monsters";
+ if (path === "/mhwilds/endemic") return "/games/mhwilds/endemic";
+ if (path === "/mhwilds/lists") return "/games/mhwilds/lists";
+ return path;
+}
+
+export function navigate(path) {
+ location.hash = path;
+}
+
+export function useHashRoute() {
+ const [route, setRoute] = useState(currentRoute);
+ useEffect(() => {
+ const onHashChange = () => setRoute(currentRoute());
+ window.addEventListener("hashchange", onHashChange);
+ return () => window.removeEventListener("hashchange", onHashChange);
+ }, []);
+ return route;
+}
diff --git a/website/src/styles/_base.scss b/website/src/styles/_base.scss
index c40bd15..7bac819 100644
--- a/website/src/styles/_base.scss
+++ b/website/src/styles/_base.scss
@@ -1,3 +1,4 @@
+// Rôle : définit les bases globales HTML, body, typographie et contrôles natifs.
* {
box-sizing: border-box;
}
@@ -251,11 +252,11 @@ body.is-modal-open {
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
- .screenshot-viewer-button,
+ .image-viewer-button,
.module-edit-button,
.module-delete-button,
- .shot-annotate-button,
- .shot-delete-button,
+ .image-annotate-button,
+ .image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
) {
@@ -288,11 +289,11 @@ body.is-modal-open {
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
- .screenshot-viewer-button,
+ .image-viewer-button,
.module-edit-button,
.module-delete-button,
- .shot-annotate-button,
- .shot-delete-button,
+ .image-annotate-button,
+ .image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
) .ui-icon {
@@ -309,11 +310,11 @@ body.is-modal-open {
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
- .screenshot-viewer-button,
+ .image-viewer-button,
.module-edit-button,
.module-delete-button,
- .shot-annotate-button,
- .shot-delete-button,
+ .image-annotate-button,
+ .image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
):hover {
@@ -335,11 +336,11 @@ body.is-modal-open {
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
- .screenshot-viewer-button,
+ .image-viewer-button,
.module-edit-button,
.module-delete-button,
- .shot-annotate-button,
- .shot-delete-button,
+ .image-annotate-button,
+ .image-delete-button,
.tool-quick-add-button
).primary {
border-color: rgba(246, 196, 83, 0.34);
@@ -360,11 +361,11 @@ body.is-modal-open {
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
- .screenshot-viewer-button,
+ .image-viewer-button,
.module-edit-button,
.module-delete-button,
- .shot-annotate-button,
- .shot-delete-button,
+ .image-annotate-button,
+ .image-delete-button,
.tool-quick-add-button
).primary:hover {
border-color: rgba(246, 196, 83, 0.72);
@@ -387,11 +388,11 @@ body.is-modal-open {
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
- .screenshot-viewer-button,
+ .image-viewer-button,
.module-edit-button,
.module-delete-button,
- .shot-annotate-button,
- .shot-delete-button,
+ .image-annotate-button,
+ .image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
):hover .ui-icon {
@@ -399,12 +400,12 @@ body.is-modal-open {
filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
}
-:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger {
+:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger {
border-color: rgba(251, 113, 133, 0.2);
color: var(--color-danger);
}
-:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger:hover {
+:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger:hover {
border-color: rgba(251, 113, 133, 0.56);
background:
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
@@ -415,7 +416,7 @@ body.is-modal-open {
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
-:is(.card-icon-button, .module-delete-button, .shot-delete-button, .checklist-delete-button).danger:hover .ui-icon {
+:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger:hover .ui-icon {
background-color: #fff;
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
}
diff --git a/website/src/styles/_cards.scss b/website/src/styles/_cards.scss
index 0c8762a..01f131c 100644
--- a/website/src/styles/_cards.scss
+++ b/website/src/styles/_cards.scss
@@ -1,3 +1,4 @@
+// Rôle : regroupe les styles de cards, panels et variantes compactes.
@use "mixins";
.section-grid,
diff --git a/website/src/styles/_diablo4.scss b/website/src/styles/_diablo4.scss
index 27190e9..958bcd8 100644
--- a/website/src/styles/_diablo4.scss
+++ b/website/src/styles/_diablo4.scss
@@ -1,3 +1,4 @@
+// Rôle : contient les styles spécifiques aux affichages Diablo IV.
.game-card-placeholder {
display: grid;
width: 100%;
diff --git a/website/src/styles/_games.scss b/website/src/styles/_games.scss
index ded251d..97ac124 100644
--- a/website/src/styles/_games.scss
+++ b/website/src/styles/_games.scss
@@ -1,3 +1,4 @@
+// Rôle : regroupe les styles partagés des pages de jeux, filtres et listes.
@use "mixins";
.game-home-grid {
diff --git a/website/src/styles/_home.scss b/website/src/styles/_home.scss
index c5b1aeb..7527a6e 100644
--- a/website/src/styles/_home.scss
+++ b/website/src/styles/_home.scss
@@ -1,3 +1,4 @@
+// Rôle : contient les styles spécifiques à la page d'accueil.
@use "mixins";
.hero,
diff --git a/website/src/styles/_icons.scss b/website/src/styles/_icons.scss
index e4ed102..0024f7f 100644
--- a/website/src/styles/_icons.scss
+++ b/website/src/styles/_icons.scss
@@ -1,3 +1,4 @@
+// Rôle : définit le rendu commun des icônes SVG.
.ui-icon {
display: block;
width: 20px;
diff --git a/website/src/styles/_mhwilds.scss b/website/src/styles/_mhwilds.scss
index c00fc39..bf714a5 100644
--- a/website/src/styles/_mhwilds.scss
+++ b/website/src/styles/_mhwilds.scss
@@ -1,3 +1,4 @@
+// Rôle : contient les styles spécifiques aux pages Monster Hunter Wilds.
.monster-grid {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
diff --git a/website/src/styles/_mixins.scss b/website/src/styles/_mixins.scss
index 72882b7..d8a15a4 100644
--- a/website/src/styles/_mixins.scss
+++ b/website/src/styles/_mixins.scss
@@ -1,3 +1,4 @@
+// Rôle : fournit les mixins Sass partagés du thème.
@mixin border-mask {
mask-image:
linear-gradient(#000 0 0),
diff --git a/website/src/styles/_nebula.scss b/website/src/styles/_nebula.scss
index b73e436..d8cb1b9 100644
--- a/website/src/styles/_nebula.scss
+++ b/website/src/styles/_nebula.scss
@@ -1,3 +1,4 @@
+// Rôle : génère le fond nebula et le champ d'étoiles global.
@use "sass:list";
$bright-stars:
diff --git a/website/src/styles/_overlays.scss b/website/src/styles/_overlays.scss
index 7aabeb8..dc91036 100644
--- a/website/src/styles/_overlays.scss
+++ b/website/src/styles/_overlays.scss
@@ -1,4 +1,5 @@
-.screenshot-viewer-root {
+// Rôle : regroupe les styles des modales, viewers et notifications.
+.image-viewer-root {
position: fixed;
inset: 0;
z-index: 110;
@@ -8,14 +9,14 @@
overscroll-behavior: contain;
}
-.screenshot-viewer-backdrop {
+.image-viewer-backdrop {
position: absolute;
inset: 0;
background: rgba(2, 3, 9, 0.64);
backdrop-filter: blur(4px);
}
-.screenshot-viewer {
+.image-viewer {
--viewer-content-max-height: min(80vh, calc(100vh - 140px));
--viewer-image-height: 400px;
position: relative;
@@ -30,14 +31,14 @@
box-shadow: var(--shadow-lg);
}
-.screenshot-viewer header {
+.image-viewer header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-2);
}
-.screenshot-viewer-title {
+.image-viewer-title {
overflow: hidden;
color: var(--color-text);
font-size: var(--font-size-md);
@@ -45,30 +46,30 @@
white-space: nowrap;
}
-.screenshot-viewer-actions {
+.image-viewer-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
-.screenshot-viewer-button {
+.image-viewer-button {
flex: 0 0 40px;
}
-.screenshot-viewer-body {
+.image-viewer-body {
display: grid;
min-width: 0;
justify-items: center;
}
-.screenshot-viewer-body.has-annotation-side {
+.image-viewer-body.has-annotation-side {
grid-template-columns: minmax(0, 1fr) minmax(240px, 340px);
gap: var(--space-4);
align-items: start;
justify-items: stretch;
}
-.screenshot-viewer-media {
+.image-viewer-media {
display: flex;
min-width: 0;
max-width: 100%;
@@ -77,11 +78,11 @@
align-items: start;
}
-.screenshot-viewer-media.can-annotate {
+.image-viewer-media.can-annotate {
cursor: crosshair;
}
-.screenshot-viewer-image-frame {
+.image-viewer-image-frame {
position: relative;
display: block;
width: fit-content;
@@ -91,7 +92,7 @@
line-height: 0;
}
-.screenshot-viewer-image-frame img {
+.image-viewer-image-frame img {
display: block;
width: auto;
height: auto;
@@ -100,7 +101,7 @@
object-fit: contain;
}
-.screenshot-viewer-marker {
+.image-viewer-marker {
display: inline-grid;
width: 30px;
min-width: 30px;
@@ -123,7 +124,7 @@
pointer-events: none;
}
-.screenshot-viewer-side {
+.image-viewer-side {
display: grid;
align-content: start;
grid-template-rows: auto minmax(0, 1fr);
@@ -142,12 +143,12 @@
rgba(7, 10, 24, 0.38);
}
-.screenshot-viewer-side strong {
+.image-viewer-side strong {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
-.screenshot-viewer-side .annotation-marker-list {
+.image-viewer-side .annotation-marker-list {
min-width: 0;
max-height: 100%;
overflow-y: auto;
diff --git a/website/src/styles/_responsive.scss b/website/src/styles/_responsive.scss
index 266db3e..1741c31 100644
--- a/website/src/styles/_responsive.scss
+++ b/website/src/styles/_responsive.scss
@@ -1,3 +1,4 @@
+// Rôle : contient les adaptations responsive et préférences d'accessibilité.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
@@ -166,11 +167,11 @@
grid-template-columns: 1fr;
}
- .screenshot-viewer-body.has-annotation-side {
+ .image-viewer-body.has-annotation-side {
grid-template-columns: 1fr;
}
- .screenshot-viewer-side {
+ .image-viewer-side {
height: min(280px, 32vh);
}
diff --git a/website/src/styles/_shell.scss b/website/src/styles/_shell.scss
index beaf434..6d17f44 100644
--- a/website/src/styles/_shell.scss
+++ b/website/src/styles/_shell.scss
@@ -1,3 +1,4 @@
+// Rôle : définit le layout principal, la sidebar et la navigation.
@use "mixins";
.app-shell {
diff --git a/website/src/styles/_tokens.scss b/website/src/styles/_tokens.scss
index 148187a..1e64382 100644
--- a/website/src/styles/_tokens.scss
+++ b/website/src/styles/_tokens.scss
@@ -1,3 +1,4 @@
+// Rôle : expose les tokens CSS du thème Sokko G.
:root {
color-scheme: dark;
diff --git a/website/src/styles/_toolboxes.scss b/website/src/styles/_toolboxes.scss
index d966c3c..a19e456 100644
--- a/website/src/styles/_toolboxes.scss
+++ b/website/src/styles/_toolboxes.scss
@@ -1,3 +1,4 @@
+// Rôle : regroupe les styles des toolboxes, outils et panneau latéral.
@use "mixins";
.toolbox-head {
@@ -1701,14 +1702,14 @@ textarea:focus {
padding: var(--space-4);
}
-.shots {
+.images {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--space-3);
padding: var(--space-4);
}
-.shots figure {
+.images figure {
position: relative;
display: grid;
gap: 8px;
@@ -1719,7 +1720,7 @@ textarea:focus {
background: rgba(5, 7, 17, 0.38);
}
-.shot-preview {
+.image-preview {
display: block;
width: 100%;
min-height: 0;
@@ -1729,18 +1730,18 @@ textarea:focus {
background: transparent;
}
-.shot-preview:hover {
+.image-preview:hover {
transform: none;
box-shadow: 0 0 0 2px rgba(246, 196, 83, 0.28);
}
-.shot-label,
-.shot-label-input {
+.image-label,
+.image-label-input {
min-width: 0;
font-size: var(--font-size-sm);
}
-.shot-label {
+.image-label {
overflow: hidden;
color: var(--color-text-secondary);
font-weight: 700;
@@ -1748,7 +1749,7 @@ textarea:focus {
white-space: nowrap;
}
-.shot-label-input {
+.image-label-input {
width: 100%;
min-height: 36px;
padding: 0 10px;
@@ -1758,17 +1759,17 @@ textarea:focus {
color: var(--color-text);
}
-.shot-label-input::placeholder {
+.image-label-input::placeholder {
color: rgba(185, 194, 215, 0.48);
}
-.shot-label-input:focus {
+.image-label-input:focus {
border-color: rgba(246, 196, 83, 0.72);
outline: none;
box-shadow: 0 0 0 3px rgba(246, 196, 83, 0.14);
}
-.shots img {
+.images img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
@@ -1777,12 +1778,12 @@ textarea:focus {
background: var(--color-bg-deep);
}
-.shot-actions {
+.image-actions {
display: flex;
gap: var(--space-2);
}
-.shot-actions .shot-annotate-button,
-.shot-actions .shot-delete-button {
+.image-actions .image-annotate-button,
+.image-actions .image-delete-button {
margin-top: 0;
}
diff --git a/website/src/styles/main.scss b/website/src/styles/main.scss
index 02fa8b3..fe38483 100644
--- a/website/src/styles/main.scss
+++ b/website/src/styles/main.scss
@@ -1,3 +1,4 @@
+// Rôle : point d'entrée Sass qui assemble les modules de style.
@use "tokens";
@use "nebula";
@use "base";
diff --git a/website/src/utils/bodyScrollLock.js b/website/src/utils/bodyScrollLock.js
index bbe01c6..e90c56b 100644
--- a/website/src/utils/bodyScrollLock.js
+++ b/website/src/utils/bodyScrollLock.js
@@ -1,3 +1,4 @@
+// Rôle : bloque le scroll de page derrière les modales actives.
export function lockBodyScroll() {
const currentLocks = Number(document.body.dataset.modalLocks || 0);
document.body.dataset.modalLocks = String(currentLocks + 1);
diff --git a/website/src/utils/imageCompression.js b/website/src/utils/imageCompression.js
new file mode 100644
index 0000000..dc5ba1b
--- /dev/null
+++ b/website/src/utils/imageCompression.js
@@ -0,0 +1,11 @@
+// Rôle : compresse les images utilisateur avant stockage IndexedDB.
+export async function compressImage(file) {
+ const bitmap = await createImageBitmap(file);
+ const maxSide = 1400;
+ const ratio = Math.min(1, maxSide / Math.max(bitmap.width, bitmap.height));
+ const canvas = document.createElement("canvas");
+ canvas.width = Math.round(bitmap.width * ratio);
+ canvas.height = Math.round(bitmap.height * ratio);
+ canvas.getContext("2d").drawImage(bitmap, 0, 0, canvas.width, canvas.height);
+ return canvas.toDataURL("image/jpeg", 0.78);
+}
diff --git a/website/src/utils/indexedDbStorage.js b/website/src/utils/indexedDbStorage.js
index b1f60d9..49fa21a 100644
--- a/website/src/utils/indexedDbStorage.js
+++ b/website/src/utils/indexedDbStorage.js
@@ -1,3 +1,4 @@
+// Rôle : encapsule les opérations IndexedDB génériques utilisées par les toolboxes.
const DB_NAME = "sokkog";
const DB_VERSION = 1;
const KV_STORE = "kv";