Réglages : préférences (langue, notifications push + test), gestion complète des tokens d'accès (liste/création/révocation, garde anti lock-out sur le dernier token), URL Gitea configurable, infos serveur en lecture seule (port/bind/origines/VAPID + flags CLI). Aide : documentation bilingue EN/FR de toutes les fonctionnalités, avec recherche. Icône Gitea (lien externe) dans la nav (sidebar + MoreSheet mobile). Backend : routes /api/v1/auth/tokens (GET/POST/DELETE) + tokenId dans /me ; routes/settings.ts (GET/PATCH, allow-list stricte gitea_url, aucun secret exposé, URL validée http/https anti-XSS) ; AuthService.listTokens/revokeToken (transaction). Front : NavItem gère les liens externes, nav primaire/secondaire, store settings, vues SettingsView/HelpView. 236 tests verts (+15 nouveaux : auth-tokens, settings-routes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2.2 KiB
Vue
59 lines
2.2 KiB
Vue
<template>
|
|
<!-- RouterLink pour une route interne ; <a target=_blank> pour un lien externe (ex. Gitea). -->
|
|
<component
|
|
:is="href ? 'a' : RouterLink"
|
|
:to="href ? undefined : to"
|
|
:href="href"
|
|
:target="href ? '_blank' : undefined"
|
|
:rel="href ? 'noopener noreferrer' : undefined"
|
|
:aria-current="!href && active ? 'page' : undefined"
|
|
:class="orientation === 'col' ? colClass : rowClass"
|
|
>
|
|
<span class="relative">
|
|
<component :is="icon" :size="orientation === 'col' ? 20 : 18" :stroke-width="1.75" />
|
|
<span
|
|
v-if="badge"
|
|
class="absolute -right-1.5 -top-1.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-amber-500 px-1 text-[9px] font-semibold text-amber-950"
|
|
>
|
|
{{ badge }}
|
|
</span>
|
|
</span>
|
|
<span :class="orientation === 'col' ? 'text-[10px]' : 'flex-1'">{{ label }}</span>
|
|
<span
|
|
v-if="badge && orientation === 'row'"
|
|
class="rounded-full bg-amber-500/20 px-1.5 text-[11px] font-semibold text-amber-300"
|
|
>
|
|
{{ badge }}
|
|
</span>
|
|
</component>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, type Component } from 'vue';
|
|
import { RouterLink, type RouteLocationRaw } from 'vue-router';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
// `| undefined` requis par exactOptionalPropertyTypes : les items de nav passent `to`/`href`
|
|
// potentiellement undefined (l'un ou l'autre selon route interne vs lien externe).
|
|
to?: RouteLocationRaw | undefined;
|
|
href?: string | undefined;
|
|
icon: Component;
|
|
label: string;
|
|
badge?: number;
|
|
active?: boolean;
|
|
orientation?: 'row' | 'col';
|
|
}>(),
|
|
{ orientation: 'row' },
|
|
);
|
|
|
|
const rowClass = computed(() => [
|
|
'flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/70',
|
|
props.active ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800/50 hover:text-zinc-200',
|
|
]);
|
|
const colClass = computed(() => [
|
|
'flex flex-1 flex-col items-center gap-0.5 py-2 transition-colors focus-visible:outline-none',
|
|
props.active ? 'text-emerald-400' : 'text-zinc-500 hover:text-zinc-300',
|
|
]);
|
|
</script>
|