Fix accueil : hero fade doux, icônes safelist, blocs cliquables, menu, dark fort

- Hero : réécriture composable timeout pur (plus de Transition callbacks)
  Animation fade opacity 1s très douce, lisible
- Icônes : safelist UnoCSS dans nuxt.config.ts (résout pastilles vides)
- Menu : mis à jour site.yml (Numérique/Économique/Citoyenne/Événement)
- Blocs : card entière cliquable, zone actions séparée (border-top)
- Économie du don : lié à /modele-eco (page chapitres préservée)
- Tarifs de l'eau : bouton SejeteralO (localhost:3009 / collectivites.librodrome.org)
- Dark theme fort : bg 220 12% 15%, surface 19%, surface-light 24%
- Config SejeteralO + Glibredecision dans app.config.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-03 04:08:47 +01:00
parent 97ba6dd04c
commit 082a17d09b
9 changed files with 184 additions and 182 deletions

View File

@@ -6,76 +6,79 @@ export interface TypewriterSentence {
}
interface SequenceOptions {
fadeMs?: number
holdMs?: number
gapMs?: number
}
export function useTypewriter(sentences: TypewriterSentence[], options: SequenceOptions = {}) {
const {
holdMs = 2400,
fadeMs = 1000,
holdMs = 2800,
gapMs = 300,
} = options
const currentIndex = ref(-1)
const showActive = ref(false)
const currentText = ref('')
const currentStyle = ref<string>('title')
const isVisible = ref(false)
const lockedSentences = ref<TypewriterSentence[]>([])
const isComplete = ref(false)
let holdTimer: ReturnType<typeof setTimeout> | null = null
const currentSentence = computed(() =>
currentIndex.value >= 0 && currentIndex.value < sentences.length
? sentences[currentIndex.value]
: null,
)
let currentIdx = -1
let timer: ReturnType<typeof setTimeout> | null = null
function clearTimer() {
if (holdTimer) {
clearTimeout(holdTimer)
holdTimer = null
if (timer) {
clearTimeout(timer)
timer = null
}
}
function showNext() {
const nextIdx = currentIndex.value + 1
if (nextIdx >= sentences.length) {
function next() {
currentIdx++
if (currentIdx >= sentences.length) {
currentText.value = ''
isComplete.value = true
return
}
currentIndex.value = nextIdx
const sentence = sentences[nextIdx]
const sentence = sentences[currentIdx]
if (sentence.separator) {
lockedSentences.value = [...lockedSentences.value, { text: '', separator: true }]
}
showActive.value = true
}
// Set text while invisible
currentText.value = sentence.text
currentStyle.value = sentence.style || 'title'
/** Called by component via @after-enter on Transition */
function onEntered() {
holdTimer = setTimeout(() => {
showActive.value = false
}, holdMs)
}
// Fade in on next frame
requestAnimationFrame(() => {
isVisible.value = true
})
/** Called by component via @after-leave on Transition */
function onLeft() {
const sentence = sentences[currentIndex.value]
if (sentence?.stays) {
lockedSentences.value = [...lockedSentences.value, { ...sentence }]
}
setTimeout(showNext, gapMs)
// After fade-in + hold → fade out
timer = setTimeout(() => {
isVisible.value = false
// After fade-out completes → lock if stays, then next
timer = setTimeout(() => {
if (sentence.stays) {
lockedSentences.value = [...lockedSentences.value, { ...sentence }]
}
timer = setTimeout(next, gapMs)
}, fadeMs)
}, fadeMs + holdMs)
}
function start() {
showNext()
next()
}
function skipToEnd() {
clearTimer()
showActive.value = false
isVisible.value = false
currentText.value = ''
const locked: TypewriterSentence[] = []
for (const sentence of sentences) {
@@ -88,20 +91,18 @@ export function useTypewriter(sentences: TypewriterSentence[], options: Sequence
}
lockedSentences.value = locked
currentIndex.value = sentences.length - 1
currentIdx = sentences.length - 1
isComplete.value = true
}
onUnmounted(clearTimer)
return {
currentIndex: readonly(currentIndex),
currentSentence,
showActive,
currentText: readonly(currentText),
currentStyle: readonly(currentStyle),
isVisible,
lockedSentences: readonly(lockedSentences),
isComplete: readonly(isComplete),
onEntered,
onLeft,
start,
skipToEnd,
}