feat(chatbot): composant ChatModeToggle (radiogroup) et état partagé du mode

Toggle sobre 2 options (role radiogroup, flèches/Home/End, focus visible,
reduced-motion), étiquette de mode sur les réponses approfondies, et
useChatProfondeur (état par clé, desktop et mobile synchronisés). Libellés lus
dans utils/chatProfondeur.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vcVGz8i4WpZm2dixKUiSD
This commit is contained in:
Jules Neny
2026-10-09 03:49:56 +02:00
co-authored by Claude Opus 5.5
parent 80f69565aa
commit 4c57f30f6a
3 changed files with 114 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
<template>
<span v-if="profondeur === 'approfondi'" class="chat-mode-badge">{{ CHAT_PROFONDEUR_LIBELLES.approfondi }}</span>
</template>
<script setup lang="ts">
// Étiquette sobre sur une réponse donnée en mode approfondi (rien en mode rapide).
defineProps<{ profondeur?: string }>()
</script>
<style scoped>
.chat-mode-badge {
display: inline-block;
margin-bottom: 6px;
padding: 1px 8px;
border-radius: 9999px;
border: 1px solid var(--nav-bg-alt);
font-size: 0.66rem;
font-family: var(--nav-font);
color: var(--nav-text-muted);
}
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<div class="chat-mode" role="radiogroup" aria-label="Mode de réponse">
<button
v-for="(p, i) in CHAT_PROFONDEURS"
:key="p"
:ref="(el) => (boutons[i] = el as HTMLButtonElement | null)"
type="button"
role="radio"
class="chat-mode-option"
:class="{ active: modelValue === p }"
:aria-checked="modelValue === p"
:tabindex="modelValue === p ? 0 : -1"
:title="CHAT_PROFONDEUR_AIDES[p]"
:disabled="disabled"
@click="choisir(p)"
@keydown="onKey($event, i)"
>{{ CHAT_PROFONDEUR_LIBELLES[p] }}</button>
</div>
</template>
<script setup lang="ts">
// Toggle 2 options partagé par les 5 chatbots. Libellés : utils/chatProfondeur.ts.
// radiogroup : flèches déplacent la sélection, Tab entre/sort du groupe (roving tabindex).
const props = defineProps<{ modelValue: 'rapide' | 'approfondi'; disabled?: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [value: 'rapide' | 'approfondi'] }>()
const boutons = ref<(HTMLButtonElement | null)[]>([])
function choisir(p: ChatProfondeur) {
if (!props.disabled) emit('update:modelValue', p)
}
function onKey(e: KeyboardEvent, i: number) {
const n = CHAT_PROFONDEURS.length
let j = -1
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') j = (i + 1) % n
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') j = (i - 1 + n) % n
else if (e.key === 'Home') j = 0
else if (e.key === 'End') j = n - 1
if (j < 0) return
e.preventDefault()
choisir(CHAT_PROFONDEURS[j])
nextTick(() => boutons.value[j]?.focus())
}
</script>
<style scoped>
.chat-mode {
display: inline-flex;
gap: 2px;
padding: 2px;
border-radius: 9999px;
background: var(--nav-bg-alt);
align-self: flex-start;
}
.chat-mode-option {
border: none;
background: transparent;
color: var(--nav-text-muted);
font-family: var(--nav-font);
font-size: 0.72rem;
line-height: 1.2;
padding: 4px 10px;
border-radius: 9999px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.chat-mode-option:hover:not(.active):not(:disabled) { color: var(--nav-text); }
.chat-mode-option.active {
background: var(--nav-primary);
color: var(--nav-text-on-primary);
font-weight: 600;
}
.chat-mode-option:focus-visible { outline: 2px solid var(--nav-primary); outline-offset: 2px; }
.chat-mode-option:disabled { opacity: 0.5; cursor: not-allowed; }
@media (prefers-reduced-motion: reduce) {
.chat-mode-option { transition: none; }
}
</style>
+13
View File
@@ -0,0 +1,13 @@
/**
* Mode de profondeur courant d'un chatbot (M3). État partagé par clé via useState :
* desktop et mobile d'un même chatbot (clés 'accueil', 'agences'…) restent synchronisés.
* Volontairement non persisté : le mode est un choix par question, 'rapide' à chaque visite.
*/
export function useChatProfondeur(key: string | (() => string) = 'defaut') {
const k = typeof key === 'function' ? key : () => key
const store = useState<Record<string, ChatProfondeur>>('aep-chat-profondeur', () => ({}))
return computed<ChatProfondeur>({
get: () => store.value[k()] ?? CHAT_PROFONDEUR_DEFAUT,
set: (v) => { store.value = { ...store.value, [k()]: normaliserProfondeur(v) } },
})
}