Files
arboretum/packages/site/src/components/BackToTop.vue
T
johanleroy c8bf6534e0 fix(site): barre de navigation lisible à toutes les largeurs, et bouton de remontée
Les huit liens de la barre ne tiennent qu'au-delà de ~1180px. En dessous, la barre flex les compressait
au lieu de les masquer : « Start project », « Git services » et « How it works » se cassaient sur deux
ou trois lignes, les libellés recouvraient le logo et le bouton Gitea sortait du cadre. Sous 900px, il
n'y avait par ailleurs AUCUNE navigation : les sections n'étaient atteignables qu'en scrollant.

- Aucun libellé ne peut plus se casser (`whitespace-nowrap` + `shrink-0`), et le logo ne se rogne plus.
- Les liens apparaissent par palier de largeur : 4 dès 900px, 6 à partir de 1024, les 8 à partir de
  1180. Pur CSS, aucun JS, aucune mesure au runtime.
- Sous 900px, un bouton menu ouvre un panneau listant TOUS les liens (plus Gitea), refermé au choix
  d'un lien ou par Échap. Le bouton Gitea quitte la barre à ces largeurs, sinon le bouton menu se
  retrouvait tronqué au bord de l'écran sur un mobile de 390px.
- Le libellé « Gitea » n'apparaît qu'au-delà de 1180px ; en dessous l'icône suffit et l'aria-label reste.

Ajout d'un bouton « remonter en haut » en bas à droite : mêmes tokens que le reste du site, visible
seulement après 600px de défilement, cible de 44px, respecte prefers-reduced-motion.

Vérifié au rendu à 390, 760, 900, 1024, 1180 et 1440px, dans les deux thèmes, menu ouvert compris.
2026-08-04 15:43:59 +02:00

66 lines
2.2 KiB
Vue

<script setup lang="ts">
// Bouton « remonter en haut », en bas à droite. Reprend les tokens existants (bordure `border`, fond
// `surface-1`, accent au survol, radius 12px, ombre `shadow-card`) : rien de neuf visuellement.
// Il n'apparaît qu'après un vrai défilement et disparaît en haut de page, pour ne jamais recouvrir le
// contenu sans raison.
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
/** Au-delà d'un écran de défilement, remonter rend un vrai service. */
const SHOW_AFTER = 600;
const visible = ref(false);
const onScroll = (): void => {
visible.value = window.scrollY > SHOW_AFTER;
};
function toTop(): void {
// Respecte la préférence système : pas de défilement animé si l'utilisateur les a réduites.
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
window.scrollTo({ top: 0, behavior: reduce ? 'auto' : 'smooth' });
}
onMounted(() => {
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
});
onBeforeUnmount(() => window.removeEventListener('scroll', onScroll));
</script>
<template>
<Transition name="btt">
<button
v-if="visible"
type="button"
class="fixed right-5 bottom-5 z-40 inline-flex h-11 w-11 items-center justify-center rounded-[12px] border border-border bg-surface-1/90 text-fg-muted shadow-card backdrop-blur-[10px] transition-colors hover:border-accent hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent/70 sm:right-7 sm:bottom-7"
:aria-label="t('backToTop')"
:title="t('backToTop')"
@click="toTop"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m18 15-6-6-6 6" />
</svg>
</button>
</Transition>
</template>
<style scoped>
.btt-enter-active,
.btt-leave-active {
transition: opacity 0.18s ease, transform 0.18s ease;
}
.btt-enter-from,
.btt-leave-to {
opacity: 0;
transform: translateY(6px);
}
@media (prefers-reduced-motion: reduce) {
.btt-enter-active,
.btt-leave-active {
transition: none;
}
}
</style>