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)