feat(renderer): vertical timeline for multiple events (M10.10)

- TimelineRenderer.vue: alternating left/right on desktop, single column mobile
- Animated entries with staggered fade-up
- Auto-switches to timeline when 3+ events detected
- Individual EventCards for 1-2 events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 00:01:06 +00:00
co-authored by Claude Opus 4.6
parent 04a175171b
commit 7f67f0863d
2 changed files with 60 additions and 1 deletions
@@ -192,7 +192,12 @@
/>
</div>
<div v-if="inlineEvents.length > 0" class="mt-3 space-y-2" @click.stop>
<!-- Timeline view for 3+ events -->
<div v-if="inlineEvents.length >= 3" class="mt-3" @click.stop>
<TimelineRenderer :events="inlineEvents" />
</div>
<!-- Individual event cards for 1-2 events -->
<div v-else-if="inlineEvents.length > 0" class="mt-3 space-y-2" @click.stop>
<EventCard
v-for="(event, i) in inlineEvents"
:key="`event-${i}`"
@@ -356,6 +361,7 @@ import { extractRecipes, extractEvents } from '@/composables/contentExtraction'
import RecipeCard from '@/components/renderers/RecipeCard.vue'
import EventCard from '@/components/renderers/EventCard.vue'
import InteractiveTable from '@/components/renderers/InteractiveTable.vue'
import TimelineRenderer from '@/components/renderers/TimelineRenderer.vue'
import { extractTables } from '@/composables/useTableExtractor'
const props = withDefaults(
@@ -0,0 +1,53 @@
<template>
<div class="timeline-renderer my-4">
<div class="relative">
<!-- Vertical line -->
<div class="absolute left-4 md:left-1/2 top-0 bottom-0 w-px bg-white/10 md:-translate-x-px" />
<!-- Timeline entries -->
<div
v-for="(event, i) in events"
:key="i"
class="relative flex items-start gap-4 mb-6 animate-fade-up-fast"
:class="i % 2 === 0 ? 'md:flex-row' : 'md:flex-row-reverse'"
:style="{ animationDelay: `${i * 80}ms` }"
>
<!-- Dot -->
<div
class="absolute left-4 md:left-1/2 w-3 h-3 rounded-full bg-accent border-2 border-black z-10 md:-translate-x-1.5"
:style="{ top: '6px' }"
/>
<!-- Date (mobile: inline, desktop: left/right side) -->
<div class="hidden md:block w-[calc(50%-2rem)] text-right shrink-0" :class="i % 2 === 0 ? '' : 'order-last text-left'">
<p class="text-[10px] text-white/40 tabular-nums pt-1">{{ formatDate(event.date) }}</p>
</div>
<!-- Card -->
<div class="ml-10 md:ml-0 md:w-[calc(50%-2rem)] shrink-0">
<div class="rounded-lg bg-white/5 border border-white/10 px-3 py-2">
<p class="text-[10px] text-white/30 md:hidden tabular-nums">{{ formatDate(event.date) }}</p>
<h4 class="text-xs font-medium text-white/90">{{ event.title }}</h4>
<p v-if="event.location" class="text-[10px] text-white/40 mt-0.5">{{ event.location }}</p>
<p v-if="event.description" class="text-[10px] text-white/50 mt-1 line-clamp-2">{{ event.description }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { EventData } from '@/composables/contentExtraction'
defineProps<{
events: EventData[]
}>()
function formatDate(dateStr: string): string {
if (!dateStr) return ''
const d = new Date(dateStr)
if (isNaN(d.getTime())) return dateStr
return d.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' })
}
</script>