reui
Timeline
Timeline — кастомный компонент, портированный из ReUI (keenthemes/reui, MIT).
Загрузка превью…
src/reui/timeline/Timeline.vue
<script setup lang="ts">
/**
* Порт ReUI Timeline (registry-reui/bases/radix/reui/timeline.tsx, MIT).
*
* React `defaultValue`/`value`/`onValueChange` сведены в один `defineModel`
* с именем `value` и дефолтом `1`: без `v-model:value` компонент ведёт себя
* как неконтролируемый (внутреннее состояние, инициализированное дефолтом),
* с `v-model:value` — как полностью контролируемый. Это то же самое
* поведение, что и merge `value ?? internalState` в оригинале.
*/
import type { HTMLAttributes } from "vue"
import { provide } from "vue"
import { cn } from "@/lib/utils"
import { TimelineContextKey } from "./context"
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
orientation?: "horizontal" | "vertical"
}>(),
{
orientation: "vertical",
}
)
const activeStep = defineModel<number>("value", { default: 1 })
function setActiveStep(step: number) {
activeStep.value = step
}
provide(TimelineContextKey, { activeStep, setActiveStep })
</script>
<template>
<div
:class="
cn(
'group/timeline flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col',
props.class
)
"
:data-orientation="props.orientation"
data-slot="timeline"
>
<slot />
</div>
</template>
src/reui/timeline/TimelineContent.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
data-slot="timeline-content"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</div>
</template>
src/reui/timeline/TimelineDate.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
/** Соответствие Radix `asChild`: Reka UI `Primitive` с `as-child`. */
asChild?: boolean
}>(),
{
asChild: false,
}
)
</script>
<template>
<Primitive
as="time"
:as-child="props.asChild"
data-slot="timeline-date"
:class="
cn(
'text-muted-foreground mb-1 block text-xs font-medium group-data-[orientation=vertical]/timeline:max-sm:h-4',
props.class
)
"
>
<slot />
</Primitive>
</template>
src/reui/timeline/TimelineHeader.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div data-slot="timeline-header" :class="cn(props.class)">
<slot />
</div>
</template>
src/reui/timeline/TimelineIndicator.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(
defineProps<{
class?: HTMLAttributes["class"]
/** Соответствие Radix `asChild`: Reka UI `Primitive` с `as-child`. */
asChild?: boolean
}>(),
{
asChild: false,
}
)
</script>
<template>
<Primitive
as="div"
:as-child="props.asChild"
aria-hidden="true"
:class="
cn(
'border-primary/20 group-data-completed/timeline-item:border-primary absolute size-4 rounded-full border-2 group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:left-0 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:top-0 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:-translate-x-1/2',
props.class
)
"
data-slot="timeline-indicator"
>
<slot />
</Primitive>
</template>
src/reui/timeline/TimelineItem.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
import { useTimeline } from "./context"
const props = defineProps<{
class?: HTMLAttributes["class"]
step: number
}>()
const { activeStep } = useTimeline()
</script>
<template>
<div
:data-completed="props.step <= activeStep || undefined"
data-slot="timeline-item"
:class="
cn(
'group/timeline-item has-[+[data-completed]]:**:data-[slot=timeline-separator]:bg-primary relative flex flex-1 flex-col gap-0.5 group-data-[orientation=horizontal]/timeline:mt-8 group-data-[orientation=horizontal]/timeline:not-last:pe-8 group-data-[orientation=vertical]/timeline:ms-8 group-data-[orientation=vertical]/timeline:not-last:pb-6',
props.class
)
"
>
<slot />
</div>
</template>
src/reui/timeline/TimelineSeparator.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
aria-hidden="true"
data-slot="timeline-separator"
:class="
cn(
'bg-primary/10 absolute self-start group-last/timeline-item:hidden group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:h-0.5 group-data-[orientation=horizontal]/timeline:w-[calc(100%-1rem-0.25rem)] group-data-[orientation=horizontal]/timeline:translate-x-4.5 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:h-[calc(100%-1rem-0.25rem)] group-data-[orientation=vertical]/timeline:w-0.5 group-data-[orientation=vertical]/timeline:-translate-x-1/2 group-data-[orientation=vertical]/timeline:translate-y-4.5',
props.class
)
"
/>
</template>
src/reui/timeline/TimelineTitle.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<h3
data-slot="timeline-title"
:class="cn('text-sm font-medium', props.class)"
>
<slot />
</h3>
</template>
src/reui/timeline/context.ts
import type { InjectionKey, Ref } from "vue"
import { inject } from "vue"
/**
* Соответствие React `createContext`/`useContext` из timeline.tsx.
* `activeStep` — модель (см. `defineModel` в Timeline.vue), поэтому здесь
* это `Ref`, а не голое число.
*/
export interface TimelineContextValue {
activeStep: Ref<number>
setActiveStep: (step: number) => void
}
export const TimelineContextKey: InjectionKey<TimelineContextValue> =
Symbol("TimelineContext")
export function useTimeline(): TimelineContextValue {
const context = inject(TimelineContextKey)
if (!context) {
throw new Error("useTimeline must be used within a Timeline")
}
return context
}
src/reui/timeline/index.ts
export { default as Timeline } from "./Timeline.vue"
export { default as TimelineContent } from "./TimelineContent.vue"
export { default as TimelineDate } from "./TimelineDate.vue"
export { default as TimelineHeader } from "./TimelineHeader.vue"
export { default as TimelineIndicator } from "./TimelineIndicator.vue"
export { default as TimelineItem } from "./TimelineItem.vue"
export { default as TimelineSeparator } from "./TimelineSeparator.vue"
export { default as TimelineTitle } from "./TimelineTitle.vue"
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/timeline.jsonЗависимости реестра
npm-зависимости
- reka-ui
Источник: порт из ReUI (Keenthemes, MIT)