fix: issue in galley mode
This commit is contained in:
+13
-59
@@ -4,24 +4,23 @@ export interface GalleryModel {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of 3D models available in the gallery.
|
||||
* Only includes models that exist in `/public/models/`.
|
||||
*/
|
||||
export const galleryModels: GalleryModel[] = [
|
||||
{ id: "arbre", name: "Arbre", path: "/models/arbre/model.gltf" },
|
||||
{
|
||||
id: "arbre-animated",
|
||||
name: "Arbre animé",
|
||||
path: "/models/arbre-animated/model.gltf",
|
||||
},
|
||||
{ id: "blocking", name: "Blocking", path: "/models/blocking/model.gltf" },
|
||||
{
|
||||
id: "boiteauxlettres",
|
||||
name: "Boîte aux lettres",
|
||||
path: "/models/boiteauxlettres/model.gltf",
|
||||
},
|
||||
{ id: "blocking", name: "Blocking", path: "/models/blocking/terrain.gltf" },
|
||||
{
|
||||
id: "boiteimmeuble",
|
||||
name: "Boîte immeuble",
|
||||
path: "/models/boiteimmeuble/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "boitesimple",
|
||||
name: "Boîte simple",
|
||||
path: "/models/boitesimple/model.gltf",
|
||||
},
|
||||
{ id: "buisson", name: "Buisson", path: "/models/buisson/model.gltf" },
|
||||
{
|
||||
id: "buisson-animated",
|
||||
@@ -30,38 +29,7 @@ export const galleryModels: GalleryModel[] = [
|
||||
},
|
||||
{ id: "cable1", name: "Câble 1", path: "/models/cable1/model.gltf" },
|
||||
{ id: "cable2", name: "Câble 2", path: "/models/cable2/model.gltf" },
|
||||
{
|
||||
id: "champdeble",
|
||||
name: "Champ de blé",
|
||||
path: "/models/champdeble/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "champdeble-animated",
|
||||
name: "Champ de blé animé",
|
||||
path: "/models/champdeble-animated/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "champdesoja",
|
||||
name: "Champ de soja",
|
||||
path: "/models/champdesoja/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "champdesoja-animated",
|
||||
name: "Champ de soja animé",
|
||||
path: "/models/champdesoja-animated/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "champsdetournesol",
|
||||
name: "Champ de tournesol",
|
||||
path: "/models/champsdetournesol/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "champsdetournesol-animated",
|
||||
name: "Champ de tournesol animé",
|
||||
path: "/models/champsdetournesol-animated/model.gltf",
|
||||
},
|
||||
{ id: "chemins", name: "Chemins", path: "/models/chemins/model.gltf" },
|
||||
{ id: "cloud", name: "Nuage", path: "/models/cloud/model.glb" },
|
||||
{
|
||||
id: "createurdepluie",
|
||||
name: "Créateur de pluie",
|
||||
@@ -112,9 +80,9 @@ export const galleryModels: GalleryModel[] = [
|
||||
},
|
||||
{ id: "gerant", name: "Gérant", path: "/models/gerant/model.gltf" },
|
||||
{
|
||||
id: "gerant-animated",
|
||||
id: "gerant_anim",
|
||||
name: "Gérant animé",
|
||||
path: "/models/gerant-animated/model.gltf",
|
||||
path: "/models/gerant_anim/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "habitant1",
|
||||
@@ -159,16 +127,6 @@ export const galleryModels: GalleryModel[] = [
|
||||
name: "Panneau flèche",
|
||||
path: "/models/panneaufleche/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "panneausolaire",
|
||||
name: "Panneau solaire",
|
||||
path: "/models/panneausolaire/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "parcebike",
|
||||
name: "Parc e-bike",
|
||||
path: "/models/parcebike/model.gltf",
|
||||
},
|
||||
{
|
||||
id: "persoprincipal",
|
||||
name: "Personnage principal",
|
||||
@@ -188,11 +146,7 @@ export const galleryModels: GalleryModel[] = [
|
||||
path: "/models/refroidisseur/model.gltf",
|
||||
},
|
||||
{ id: "sapin", name: "Sapin", path: "/models/sapin/model.gltf" },
|
||||
{
|
||||
id: "sapin-animated",
|
||||
name: "Sapin animé",
|
||||
path: "/models/sapin-animated/model.gltf",
|
||||
},
|
||||
{ id: "skybox", name: "Skybox", path: "/models/skybox/skybox.gltf" },
|
||||
{ id: "talkie", name: "Talkie", path: "/models/talkie/model.gltf" },
|
||||
{ id: "terrain", name: "Terrain", path: "/models/terrain/model.gltf" },
|
||||
{
|
||||
|
||||
+211
@@ -299,6 +299,217 @@ canvas {
|
||||
.gallery-light-panel {
|
||||
top: 78px;
|
||||
}
|
||||
|
||||
.gallery-keyboard-hints {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gallery - Header */
|
||||
.gallery-header {
|
||||
position: absolute;
|
||||
top: clamp(18px, 3vw, 34px);
|
||||
right: clamp(18px, 3vw, 38px);
|
||||
z-index: 2;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.gallery-header .gallery-title {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.gallery-subtitle {
|
||||
margin: 6px 0 0;
|
||||
color: #a9a196;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Gallery - Loading */
|
||||
.gallery-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #f4efe7;
|
||||
}
|
||||
|
||||
.gallery-loading-spinner {
|
||||
animation: gallery-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.gallery-loading-text {
|
||||
font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@keyframes gallery-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Gallery - Empty state */
|
||||
.gallery-page--empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.gallery-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 32px;
|
||||
color: #a9a196;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gallery-empty-state h1 {
|
||||
margin: 0;
|
||||
color: #f4efe7;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.gallery-empty-state p {
|
||||
margin: 0;
|
||||
max-width: 320px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Gallery - Error state */
|
||||
.gallery-viewer-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 360px;
|
||||
padding: 24px;
|
||||
color: #fecaca;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gallery-viewer-error span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Gallery - Navigation buttons */
|
||||
.gallery-nav-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #f4efe7;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 160ms ease,
|
||||
color 160ms ease,
|
||||
transform 100ms ease;
|
||||
}
|
||||
|
||||
.gallery-nav-button:hover,
|
||||
.gallery-nav-button:focus-visible {
|
||||
background: #f4efe7;
|
||||
color: #050505;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.gallery-nav-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Gallery - Model info */
|
||||
.gallery-model-name {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #f4efe7;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gallery-model-counter {
|
||||
margin-top: 2px;
|
||||
color: #a9a196;
|
||||
font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Gallery - Texture status spinner */
|
||||
.gallery-texture-status-spinner {
|
||||
animation: gallery-spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Gallery - Keyboard hints */
|
||||
.gallery-keyboard-hints {
|
||||
position: absolute;
|
||||
top: clamp(18px, 3vw, 34px);
|
||||
left: clamp(18px, 3vw, 38px);
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #a9a196;
|
||||
font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gallery-keyboard-hints kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #a9a196;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #f4efe7;
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.gallery-keyboard-hints-separator {
|
||||
margin: 0 4px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.gallery-header {
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gallery-subtitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gallery-nav-button {
|
||||
width: 48px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Docs layout */
|
||||
|
||||
+204
-29
@@ -1,14 +1,17 @@
|
||||
import {
|
||||
Bounds,
|
||||
Center,
|
||||
Html,
|
||||
OrbitControls,
|
||||
useAnimations,
|
||||
useGLTF,
|
||||
useProgress,
|
||||
} from "@react-three/drei";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import {
|
||||
Component,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
Loader2,
|
||||
SlidersHorizontal,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
@@ -26,9 +30,24 @@ import * as THREE from "three";
|
||||
import { SkyModel } from "@/components/three/world/SkyModel";
|
||||
import { galleryModels, type GalleryModel } from "@/data/galleryModels";
|
||||
import {
|
||||
AMBIENT_INTENSITY_MAX,
|
||||
AMBIENT_INTENSITY_MIN,
|
||||
AMBIENT_INTENSITY_STEP,
|
||||
AMBIENT_LIGHT_COLOR,
|
||||
LIGHTING_DEFAULTS,
|
||||
SUN_INTENSITY_MAX,
|
||||
SUN_INTENSITY_MIN,
|
||||
SUN_INTENSITY_STEP,
|
||||
SUN_LIGHT_COLOR,
|
||||
SUN_X_MAX,
|
||||
SUN_X_MIN,
|
||||
SUN_X_STEP,
|
||||
SUN_Y_MAX,
|
||||
SUN_Y_MIN,
|
||||
SUN_Y_STEP,
|
||||
SUN_Z_MAX,
|
||||
SUN_Z_MIN,
|
||||
SUN_Z_STEP,
|
||||
} from "@/data/world/lightingConfig";
|
||||
import {
|
||||
GAME_SCENE_FALLBACK_SKY_MODEL_PATH,
|
||||
@@ -104,13 +123,62 @@ const LOADING_TEXTURE_DIAGNOSTIC: TextureDiagnostic = {
|
||||
};
|
||||
|
||||
const GALLERY_LIGHT_CONTROLS: GalleryLightControl[] = [
|
||||
{ key: "ambientIntensity", label: "Ambiance", min: 0, max: 5, step: 0.1 },
|
||||
{ key: "sunIntensity", label: "Soleil", min: 0, max: 8, step: 0.1 },
|
||||
{ key: "sunX", label: "Soleil X", min: -100, max: 100, step: 1 },
|
||||
{ key: "sunY", label: "Soleil Y", min: -100, max: 150, step: 1 },
|
||||
{ key: "sunZ", label: "Soleil Z", min: -100, max: 100, step: 1 },
|
||||
{
|
||||
key: "ambientIntensity",
|
||||
label: "Ambiance",
|
||||
min: AMBIENT_INTENSITY_MIN,
|
||||
max: AMBIENT_INTENSITY_MAX,
|
||||
step: AMBIENT_INTENSITY_STEP,
|
||||
},
|
||||
{
|
||||
key: "sunIntensity",
|
||||
label: "Soleil",
|
||||
min: SUN_INTENSITY_MIN,
|
||||
max: SUN_INTENSITY_MAX,
|
||||
step: SUN_INTENSITY_STEP,
|
||||
},
|
||||
{
|
||||
key: "sunX",
|
||||
label: "Soleil X",
|
||||
min: SUN_X_MIN,
|
||||
max: SUN_X_MAX,
|
||||
step: SUN_X_STEP,
|
||||
},
|
||||
{
|
||||
key: "sunY",
|
||||
label: "Soleil Y",
|
||||
min: SUN_Y_MIN,
|
||||
max: SUN_Y_MAX,
|
||||
step: SUN_Y_STEP,
|
||||
},
|
||||
{
|
||||
key: "sunZ",
|
||||
label: "Soleil Z",
|
||||
min: SUN_Z_MIN,
|
||||
max: SUN_Z_MAX,
|
||||
step: SUN_Z_STEP,
|
||||
},
|
||||
];
|
||||
|
||||
function GalleryLoadingIndicator(): React.JSX.Element {
|
||||
const { progress } = useProgress();
|
||||
|
||||
return (
|
||||
<Html center>
|
||||
<div className="gallery-loading">
|
||||
<Loader2
|
||||
className="gallery-loading-spinner"
|
||||
size={32}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<span className="gallery-loading-text">
|
||||
{progress < 100 ? `${Math.round(progress)}%` : "Préparation..."}
|
||||
</span>
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
class GalleryViewerErrorBoundary extends Component<
|
||||
GalleryViewerErrorBoundaryProps,
|
||||
GalleryViewerErrorBoundaryState
|
||||
@@ -134,7 +202,8 @@ class GalleryViewerErrorBoundary extends Component<
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="gallery-viewer-error" role="status">
|
||||
Ce modèle ne peut pas être affiché pour le moment.
|
||||
<TriangleAlert size={24} strokeWidth={1.8} />
|
||||
<span>Ce modèle ne peut pas être affiché pour le moment.</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -321,7 +390,23 @@ function TextureStatusBadge({
|
||||
}: {
|
||||
diagnostic: TextureDiagnostic;
|
||||
}): React.JSX.Element {
|
||||
const isLoading = diagnostic.status === "loading";
|
||||
const hasWarning = diagnostic.status === "warning";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="gallery-texture-status gallery-texture-status--loading">
|
||||
<Loader2
|
||||
className="gallery-texture-status-spinner"
|
||||
aria-hidden="true"
|
||||
size={14}
|
||||
strokeWidth={2.2}
|
||||
/>
|
||||
<span>{diagnostic.summary}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = hasWarning ? TriangleAlert : CheckCircle2;
|
||||
|
||||
return (
|
||||
@@ -390,6 +475,18 @@ function GalleryLightingPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function GalleryEmptyState(): React.JSX.Element {
|
||||
return (
|
||||
<main className="gallery-page gallery-page--empty">
|
||||
<div className="gallery-empty-state">
|
||||
<TriangleAlert size={48} strokeWidth={1.5} />
|
||||
<h1>Aucun modèle disponible</h1>
|
||||
<p>La galerie ne contient aucun modèle à afficher pour le moment.</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function getTextureDiagnostic(
|
||||
modelId: string,
|
||||
modelScene: THREE.Object3D,
|
||||
@@ -462,38 +559,114 @@ export function GalleryPage(): React.JSX.Element {
|
||||
const [textureDiagnostic, setTextureDiagnostic] = useState<TextureDiagnostic>(
|
||||
LOADING_TEXTURE_DIAGNOSTIC,
|
||||
);
|
||||
const activeModel = galleryModels[activeModelIndex] ?? galleryModels[0]!;
|
||||
|
||||
const modelCount = galleryModels.length;
|
||||
const activeModel = galleryModels[activeModelIndex] ?? galleryModels[0];
|
||||
|
||||
const activeTextureDiagnostic =
|
||||
textureDiagnostic.modelId === activeModel.id
|
||||
activeModel && textureDiagnostic.modelId === activeModel.id
|
||||
? textureDiagnostic
|
||||
: LOADING_TEXTURE_DIAGNOSTIC;
|
||||
|
||||
const goToPreviousModel = (): void => {
|
||||
// Preload adjacent models for smoother navigation
|
||||
useEffect(() => {
|
||||
if (modelCount <= 1) return;
|
||||
|
||||
const prevIndex =
|
||||
activeModelIndex === 0 ? modelCount - 1 : activeModelIndex - 1;
|
||||
const nextIndex =
|
||||
activeModelIndex === modelCount - 1 ? 0 : activeModelIndex + 1;
|
||||
|
||||
const prevModel = galleryModels[prevIndex];
|
||||
const nextModel = galleryModels[nextIndex];
|
||||
|
||||
if (prevModel) {
|
||||
useGLTF.preload(prevModel.path);
|
||||
}
|
||||
if (nextModel) {
|
||||
useGLTF.preload(nextModel.path);
|
||||
}
|
||||
}, [activeModelIndex, modelCount]);
|
||||
|
||||
// Memoized callbacks to prevent unnecessary re-renders
|
||||
const goToPreviousModel = useCallback((): void => {
|
||||
setActiveModelIndex((currentIndex) =>
|
||||
currentIndex === 0 ? modelCount - 1 : currentIndex - 1,
|
||||
);
|
||||
};
|
||||
}, [modelCount]);
|
||||
|
||||
const goToNextModel = (): void => {
|
||||
const goToNextModel = useCallback((): void => {
|
||||
setActiveModelIndex((currentIndex) =>
|
||||
currentIndex === modelCount - 1 ? 0 : currentIndex + 1,
|
||||
);
|
||||
};
|
||||
}, [modelCount]);
|
||||
|
||||
const handleLightChange = (
|
||||
key: keyof GalleryLightingConfig,
|
||||
value: number,
|
||||
): void => {
|
||||
setLighting((currentLighting) => ({
|
||||
...currentLighting,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
const handleLightChange = useCallback(
|
||||
(key: keyof GalleryLightingConfig, value: number): void => {
|
||||
setLighting((currentLighting) => ({
|
||||
...currentLighting,
|
||||
[key]: value,
|
||||
}));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const resetLighting = (): void => {
|
||||
const resetLighting = useCallback((): void => {
|
||||
setLighting({ ...LIGHTING_DEFAULTS });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleLightPanel = useCallback((): void => {
|
||||
setLightPanelOpen((open) => !open);
|
||||
}, []);
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
// Ignore if user is typing in an input
|
||||
if (
|
||||
event.target instanceof HTMLInputElement ||
|
||||
event.target instanceof HTMLTextAreaElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowLeft":
|
||||
event.preventDefault();
|
||||
goToPreviousModel();
|
||||
break;
|
||||
case "ArrowRight":
|
||||
event.preventDefault();
|
||||
goToNextModel();
|
||||
break;
|
||||
case "l":
|
||||
case "L":
|
||||
event.preventDefault();
|
||||
toggleLightPanel();
|
||||
break;
|
||||
case "r":
|
||||
case "R":
|
||||
if (!lightPanelOpen) return;
|
||||
event.preventDefault();
|
||||
resetLighting();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [
|
||||
goToPreviousModel,
|
||||
goToNextModel,
|
||||
toggleLightPanel,
|
||||
resetLighting,
|
||||
lightPanelOpen,
|
||||
]);
|
||||
|
||||
// Guard against empty gallery (after all hooks)
|
||||
if (modelCount === 0 || !activeModel) {
|
||||
return <GalleryEmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="gallery-page">
|
||||
@@ -502,7 +675,7 @@ export function GalleryPage(): React.JSX.Element {
|
||||
<div className="gallery-canvas-frame" aria-label="Viewer 3D">
|
||||
<GalleryViewerErrorBoundary resetKey={activeModel.id}>
|
||||
<Canvas camera={{ position: [3.5, 2.4, 4.5], fov: 45 }} dpr={[1, 2]}>
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={<GalleryLoadingIndicator />}>
|
||||
<GalleryScene
|
||||
lighting={lighting}
|
||||
model={activeModel}
|
||||
@@ -516,21 +689,23 @@ export function GalleryPage(): React.JSX.Element {
|
||||
<nav className="gallery-bottom-bar" aria-label="Navigation des modèles">
|
||||
<button
|
||||
type="button"
|
||||
className="gallery-nav-button"
|
||||
onClick={goToPreviousModel}
|
||||
aria-label="Modèle précédent"
|
||||
aria-label="Modèle précédent (flèche gauche)"
|
||||
>
|
||||
<ArrowLeft aria-hidden="true" size={22} strokeWidth={1.8} />
|
||||
</button>
|
||||
<div className="gallery-model-info">
|
||||
<span>{activeModel.name}</span>
|
||||
<small>
|
||||
<span className="gallery-model-name">{activeModel.name}</span>
|
||||
<small className="gallery-model-counter">
|
||||
{activeModelIndex + 1} / {modelCount}
|
||||
</small>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="gallery-nav-button"
|
||||
onClick={goToNextModel}
|
||||
aria-label="Modèle suivant"
|
||||
aria-label="Modèle suivant (flèche droite)"
|
||||
>
|
||||
<ArrowRight aria-hidden="true" size={22} strokeWidth={1.8} />
|
||||
</button>
|
||||
@@ -541,7 +716,7 @@ export function GalleryPage(): React.JSX.Element {
|
||||
lighting={lighting}
|
||||
onChange={handleLightChange}
|
||||
onReset={resetLighting}
|
||||
onToggle={() => setLightPanelOpen((open) => !open)}
|
||||
onToggle={toggleLightPanel}
|
||||
open={lightPanelOpen}
|
||||
/>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user