- 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>
110 lines
2.4 KiB
TypeScript
110 lines
2.4 KiB
TypeScript
export interface TypewriterSentence {
|
|
text: string
|
|
style?: 'title' | 'citation' | 'text'
|
|
stays?: boolean
|
|
separator?: boolean
|
|
}
|
|
|
|
interface SequenceOptions {
|
|
fadeMs?: number
|
|
holdMs?: number
|
|
gapMs?: number
|
|
}
|
|
|
|
export function useTypewriter(sentences: TypewriterSentence[], options: SequenceOptions = {}) {
|
|
const {
|
|
fadeMs = 1000,
|
|
holdMs = 2800,
|
|
gapMs = 300,
|
|
} = options
|
|
|
|
const currentText = ref('')
|
|
const currentStyle = ref<string>('title')
|
|
const isVisible = ref(false)
|
|
const lockedSentences = ref<TypewriterSentence[]>([])
|
|
const isComplete = ref(false)
|
|
|
|
let currentIdx = -1
|
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
function clearTimer() {
|
|
if (timer) {
|
|
clearTimeout(timer)
|
|
timer = null
|
|
}
|
|
}
|
|
|
|
function next() {
|
|
currentIdx++
|
|
if (currentIdx >= sentences.length) {
|
|
currentText.value = ''
|
|
isComplete.value = true
|
|
return
|
|
}
|
|
|
|
const sentence = sentences[currentIdx]
|
|
|
|
if (sentence.separator) {
|
|
lockedSentences.value = [...lockedSentences.value, { text: '', separator: true }]
|
|
}
|
|
|
|
// Set text while invisible
|
|
currentText.value = sentence.text
|
|
currentStyle.value = sentence.style || 'title'
|
|
|
|
// Fade in on next frame
|
|
requestAnimationFrame(() => {
|
|
isVisible.value = true
|
|
})
|
|
|
|
// 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() {
|
|
next()
|
|
}
|
|
|
|
function skipToEnd() {
|
|
clearTimer()
|
|
isVisible.value = false
|
|
currentText.value = ''
|
|
|
|
const locked: TypewriterSentence[] = []
|
|
for (const sentence of sentences) {
|
|
if (sentence.separator) {
|
|
locked.push({ text: '', separator: true })
|
|
}
|
|
if (sentence.stays) {
|
|
locked.push({ ...sentence })
|
|
}
|
|
}
|
|
|
|
lockedSentences.value = locked
|
|
currentIdx = sentences.length - 1
|
|
isComplete.value = true
|
|
}
|
|
|
|
onUnmounted(clearTimer)
|
|
|
|
return {
|
|
currentText: readonly(currentText),
|
|
currentStyle: readonly(currentStyle),
|
|
isVisible,
|
|
lockedSentences: readonly(lockedSentences),
|
|
isComplete: readonly(isComplete),
|
|
start,
|
|
skipToEnd,
|
|
}
|
|
}
|