timeline
Timeline with pipeline steps.
Timeline with pipeline steps.
Загрузка превью…
src/timeline/c-timeline-6.vue
<!--
Расхождение с апстримом:
- `IconPlaceholder` (иконки статуса "check"/"circle", шеврон коллапса
"chevron-right") заменена инлайновым `<svg>` (lucide-react v0.545.0) —
см. docs/PORTING.md §5. `Spinner` — то же расхождение, что и в
`ui-spinner` (он сам рендерит `IconPlaceholder` внутри), здесь
используется примитив `@revueui/ui`, который уже адаптирован.
- Сетевые аватары (unsplash) заменены статичным 1x1 data-URI (PIXEL) —
сетевые запросы в headless-стенде недетерминированы.
-->
<script setup lang="ts">
import { Badge } from "@/components/reui/badge"
import { Frame, FrameHeader, FramePanel } from "@/components/reui/frame"
import { Timeline, TimelineContent, TimelineHeader, TimelineIndicator, TimelineItem, TimelineSeparator, TimelineTitle } from "@/components/reui/timeline"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Spinner } from "@/components/ui/spinner"
import { cn } from "@/lib/utils"
const PIXEL =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
const pipelineSteps = [
{
id: 1,
title: "Source Code Checkout",
duration: "12s",
status: "completed",
description: "Successfully fetched latest changes from the main branch.",
user: { name: "Alex Johnson", avatar: PIXEL },
},
{
id: 2,
title: "Dependency Installation",
duration: "1m 45s",
status: "completed",
description: "All npm packages installed and cached for future builds.",
user: { name: "Sarah Chen", avatar: PIXEL },
},
{
id: 3,
title: "Unit & Integration Tests",
duration: "Running",
status: "active",
description: "Running 142 test suites across the entire codebase...",
user: { name: "Michael Rodriguez", avatar: PIXEL },
},
{
id: 4,
title: "Production Build",
duration: "Pending",
status: "pending",
description: "Optimizing assets and generating static site pages.",
user: { name: "Emma Wilson", avatar: PIXEL },
},
]
function statusVariant(status: string) {
return status === "completed"
? "success-light"
: status === "active"
? "info-light"
: "warning-light"
}
</script>
<template>
<div class="w-full max-w-lg">
<Timeline :value="3">
<TimelineItem
v-for="step in pipelineSteps"
:key="step.id"
:step="step.id"
class="ms-10 pb-10"
>
<TimelineHeader>
<TimelineSeparator class="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-7" />
<div class="flex items-center gap-2">
<TimelineTitle class="text-sm font-semibold">{{ step.title }}</TimelineTitle>
<Badge :variant="statusVariant(step.status)" size="sm">{{ step.duration }}</Badge>
</div>
<TimelineIndicator
:class="
cn(
'bg-muted text-muted-foreground group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 items-center justify-center border-none group-data-[orientation=vertical]/timeline:-left-7',
step.status === 'active' && 'ring-primary/20 ring-2'
)
"
>
<svg
v-if="step.status === 'completed'"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-check size-3.5"
>
<path d="M20 6 9 17l-5-5" />
</svg>
<Spinner v-else-if="step.status === 'active'" class="size-3.5" />
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-circle size-3.5"
>
<circle cx="12" cy="12" r="10" />
</svg>
</TimelineIndicator>
</TimelineHeader>
<TimelineContent class="mt-2">
<Frame stacked dense spacing="sm">
<Collapsible default-open class="group/collapsible">
<CollapsibleTrigger class="flex w-full">
<FrameHeader class="flex grow flex-row items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Avatar class="size-5">
<AvatarImage :src="step.user.avatar" :alt="step.user.name" />
<AvatarFallback>{{ step.user.name.charAt(0) }}</AvatarFallback>
</Avatar>
<span class="text-muted-foreground text-xs font-medium">{{ step.user.name }}</span>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
class="lucide lucide-chevron-right text-muted-foreground size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
>
<path d="m9 18 6-6-6-6" />
</svg>
</FrameHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<FramePanel>
<p class="text-muted-foreground text-sm leading-relaxed">{{ step.description }}</p>
</FramePanel>
</CollapsibleContent>
</Collapsible>
</Frame>
</TimelineContent>
</TimelineItem>
</Timeline>
</div>
</template>