- Hero : animation fade-in/fade-out + swipe (useTypewriter composable + TypewriterText) - 3 axes : Autonomie numérique, économique, citoyenne (AxisBlock + AxisGrid) - Pages gestation avec présentations (wishBounty, trustWallet, Cloud libre) - Page /decision : plateforme Décision collective (lien Glibredecision) - Bloc événement distinct en bas des axes - Nav : Numérique / Économique / Citoyenne / Événement - Dark theme éclairci (bg 7→10%, surface 12→14%) - Suppression BookSection + GrateWizardTeaser (remplacés par AxisGrid) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
2.4 KiB
TypeScript
109 lines
2.4 KiB
TypeScript
export interface TypewriterSentence {
|
|
text: string
|
|
style?: 'title' | 'citation' | 'text'
|
|
stays?: boolean
|
|
separator?: boolean
|
|
}
|
|
|
|
interface SequenceOptions {
|
|
holdMs?: number
|
|
gapMs?: number
|
|
}
|
|
|
|
export function useTypewriter(sentences: TypewriterSentence[], options: SequenceOptions = {}) {
|
|
const {
|
|
holdMs = 2400,
|
|
gapMs = 300,
|
|
} = options
|
|
|
|
const currentIndex = ref(-1)
|
|
const showActive = 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,
|
|
)
|
|
|
|
function clearTimer() {
|
|
if (holdTimer) {
|
|
clearTimeout(holdTimer)
|
|
holdTimer = null
|
|
}
|
|
}
|
|
|
|
function showNext() {
|
|
const nextIdx = currentIndex.value + 1
|
|
if (nextIdx >= sentences.length) {
|
|
isComplete.value = true
|
|
return
|
|
}
|
|
|
|
currentIndex.value = nextIdx
|
|
const sentence = sentences[nextIdx]
|
|
|
|
if (sentence.separator) {
|
|
lockedSentences.value = [...lockedSentences.value, { text: '', separator: true }]
|
|
}
|
|
|
|
showActive.value = true
|
|
}
|
|
|
|
/** Called by component via @after-enter on Transition */
|
|
function onEntered() {
|
|
holdTimer = setTimeout(() => {
|
|
showActive.value = false
|
|
}, holdMs)
|
|
}
|
|
|
|
/** 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)
|
|
}
|
|
|
|
function start() {
|
|
showNext()
|
|
}
|
|
|
|
function skipToEnd() {
|
|
clearTimer()
|
|
showActive.value = false
|
|
|
|
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
|
|
currentIndex.value = sentences.length - 1
|
|
isComplete.value = true
|
|
}
|
|
|
|
onUnmounted(clearTimer)
|
|
|
|
return {
|
|
currentIndex: readonly(currentIndex),
|
|
currentSentence,
|
|
showActive,
|
|
lockedSentences: readonly(lockedSentences),
|
|
isComplete: readonly(isComplete),
|
|
onEntered,
|
|
onLeft,
|
|
start,
|
|
skipToEnd,
|
|
}
|
|
}
|