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.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { REPO } from '../lib/links';
|
||||
import LangToggle from './LangToggle.vue';
|
||||
@@ -7,26 +8,50 @@ import IconGitea from './icons/IconGitea.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Les sections #launch (« Démarrer le projet ») et #remotegit (services git distants) existaient sans
|
||||
// aucun lien de navigation : elles n'étaient atteignables qu'en scrollant à l'aveugle.
|
||||
/**
|
||||
* Navigation du site. `tier` = largeur à partir de laquelle le lien apparaît dans la barre :
|
||||
* 1 = dès 900px, 2 = à partir de 1024px, 3 = à partir de 1180px.
|
||||
*
|
||||
* Les huit liens ne tiennent qu'au-delà de ~1180px. En dessous, la barre flex les compressait au lieu
|
||||
* de les masquer, cassant « Start project », « Git services » et « How it works » sur deux ou trois
|
||||
* lignes, chevauchant le logo et expulsant le bouton Gitea. Le menu compact, lui, montre toujours la
|
||||
* liste complète : aucun lien n'est perdu, il change juste de place.
|
||||
*/
|
||||
const navLinks = [
|
||||
{ href: '#features', key: 'navFeatures' },
|
||||
{ href: '#workspace', key: 'navWorkspace' },
|
||||
{ href: '#launch', key: 'navLaunch' },
|
||||
{ href: '#remotegit', key: 'navRemoteGit' },
|
||||
{ href: '#download', key: 'navDownload' },
|
||||
{ href: '#how', key: 'navHow' },
|
||||
{ href: '#security', key: 'navSecurity' },
|
||||
{ href: '#faq', key: 'navFaq' },
|
||||
{ href: '#features', key: 'navFeatures', tier: 1 },
|
||||
{ href: '#workspace', key: 'navWorkspace', tier: 1 },
|
||||
{ href: '#launch', key: 'navLaunch', tier: 2 },
|
||||
{ href: '#remotegit', key: 'navRemoteGit', tier: 3 },
|
||||
{ href: '#download', key: 'navDownload', tier: 1 },
|
||||
{ href: '#how', key: 'navHow', tier: 3 },
|
||||
{ href: '#security', key: 'navSecurity', tier: 2 },
|
||||
{ href: '#faq', key: 'navFaq', tier: 1 },
|
||||
] as const;
|
||||
|
||||
/** Tailwind ne génère que des classes littérales : la table évite toute classe construite à la volée. */
|
||||
const TIER_CLASS: Record<number, string> = {
|
||||
1: '',
|
||||
2: 'hidden min-[1024px]:inline',
|
||||
3: 'hidden min-[1180px]:inline',
|
||||
};
|
||||
|
||||
const menuOpen = ref(false);
|
||||
const closeMenu = (): void => {
|
||||
menuOpen.value = false;
|
||||
};
|
||||
|
||||
function onKey(e: KeyboardEvent): void {
|
||||
if (e.key === 'Escape') closeMenu();
|
||||
}
|
||||
onMounted(() => window.addEventListener('keydown', onKey));
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKey));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="sticky top-0 z-50 border-b border-border-soft bg-surface-0/72 backdrop-blur-[14px]"
|
||||
>
|
||||
<div class="mx-auto flex h-16 max-w-[1200px] items-center justify-between gap-6 px-6">
|
||||
<a href="#top" class="flex items-center gap-2.5 text-fg no-underline">
|
||||
<header class="sticky top-0 z-50 border-b border-border-soft bg-surface-0/72 backdrop-blur-[14px]">
|
||||
<div class="mx-auto flex h-16 max-w-[1200px] items-center gap-4 px-6">
|
||||
<!-- shrink-0 : le logo ne doit jamais être rogné ni recouvert par la nav. -->
|
||||
<a href="#top" class="flex shrink-0 items-center gap-2.5 text-fg no-underline" @click="closeMenu">
|
||||
<img
|
||||
src="/assets/arboretum-mark.png"
|
||||
alt="Arboretum"
|
||||
@@ -37,31 +62,83 @@ const navLinks = [
|
||||
<span class="font-mono text-[17px] font-semibold tracking-[-0.01em]">Arboretum</span>
|
||||
</a>
|
||||
|
||||
<nav class="hidden items-center gap-[30px] min-[900px]:flex">
|
||||
<nav class="hidden min-w-0 flex-1 items-center justify-center gap-6 min-[900px]:flex min-[1180px]:gap-[30px]">
|
||||
<a
|
||||
v-for="link in navLinks"
|
||||
:key="link.href"
|
||||
:href="link.href"
|
||||
class="text-[14.5px] text-fg-muted no-underline transition-colors hover:text-fg"
|
||||
class="shrink-0 whitespace-nowrap text-[14.5px] text-fg-muted no-underline transition-colors hover:text-fg"
|
||||
:class="TIER_CLASS[link.tier]"
|
||||
>
|
||||
{{ t(link.key) }}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3.5">
|
||||
<div class="ml-auto flex shrink-0 items-center gap-2 min-[900px]:ml-0 min-[900px]:gap-3.5">
|
||||
<ThemeToggle />
|
||||
<LangToggle />
|
||||
<!-- Sous 900px, Gitea vit dans le panneau : à 390px, logo + 3 contrôles + menu débordaient et
|
||||
le bouton menu se retrouvait tronqué au bord de l'écran. -->
|
||||
<a
|
||||
:href="REPO"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label="Gitea"
|
||||
class="inline-flex items-center gap-[7px] rounded-lg border border-border px-[13px] py-[7px] text-[13.5px] font-medium text-fg-muted no-underline transition-colors hover:border-accent hover:text-accent"
|
||||
class="hidden items-center gap-[7px] rounded-lg border border-border px-[13px] py-[7px] text-[13.5px] font-medium text-fg-muted no-underline transition-colors hover:border-accent hover:text-accent min-[900px]:inline-flex"
|
||||
>
|
||||
<IconGitea :size="16" />
|
||||
Gitea
|
||||
<!-- Libellé masqué tant que la barre est serrée : l'icône suffit, l'aria-label reste. -->
|
||||
<span class="hidden min-[1180px]:inline">Gitea</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-lg border border-border p-[7px] text-fg-muted transition-colors hover:border-accent hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent/70 min-[900px]:hidden"
|
||||
:aria-label="t('navMenu')"
|
||||
:aria-expanded="menuOpen"
|
||||
aria-controls="site-mobile-nav"
|
||||
@click="menuOpen = !menuOpen"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
||||
<template v-if="menuOpen">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<path d="M4 7h16" />
|
||||
<path d="M4 12h16" />
|
||||
<path d="M4 17h16" />
|
||||
</template>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panneau compact sous 900px : liste complète, une entrée par ligne, refermée au choix d'un lien. -->
|
||||
<nav
|
||||
v-if="menuOpen"
|
||||
id="site-mobile-nav"
|
||||
class="border-t border-border-soft bg-surface-0/95 px-6 py-2 backdrop-blur-[14px] min-[900px]:hidden"
|
||||
>
|
||||
<a
|
||||
v-for="link in navLinks"
|
||||
:key="link.href"
|
||||
:href="link.href"
|
||||
class="block py-2 text-[15px] text-fg-muted no-underline transition-colors hover:text-fg"
|
||||
@click="closeMenu"
|
||||
>
|
||||
{{ t(link.key) }}
|
||||
</a>
|
||||
<a
|
||||
:href="REPO"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="mt-1 flex items-center gap-2 border-t border-border-soft py-2.5 text-[15px] text-fg-muted no-underline transition-colors hover:text-accent"
|
||||
@click="closeMenu"
|
||||
>
|
||||
<IconGitea :size="16" />
|
||||
Gitea
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user