Skip to content

Commit 8821757

Browse files
committed
feat: added timeline component
1 parent 0b9f816 commit 8821757

27 files changed

+1803
-5
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<template>
2+
<Primitive
3+
data-slot="timeline-content"
4+
v-bind="forwarded"
5+
:class="styles({ class: props.class })"
6+
>
7+
<slot />
8+
</Primitive>
9+
</template>
10+
11+
<script lang="ts" setup>
12+
import { reactiveOmit } from "@vueuse/core";
13+
import { Primitive, useForwardProps } from "radix-vue";
14+
import type { PrimitiveProps } from "radix-vue";
15+
16+
const styles = tv({
17+
base: "text-sm text-muted-foreground",
18+
});
19+
const props = defineProps<
20+
PrimitiveProps & {
21+
class?: any;
22+
}
23+
>();
24+
25+
const forwarded = useForwardProps(reactiveOmit(props, ["class"]));
26+
</script>

app/components/Ui/Timeline/Date.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<Primitive data-slot="timeline-date" v-bind="forwarded" :class="styles({ class: props.class })">
3+
<slot />
4+
</Primitive>
5+
</template>
6+
7+
<script lang="ts" setup>
8+
import { reactiveOmit } from "@vueuse/core";
9+
import { Primitive, useForwardProps } from "radix-vue";
10+
import type { PrimitiveProps } from "radix-vue";
11+
12+
const styles = tv({
13+
base: "mb-1 block text-xs font-medium text-muted-foreground sm:max-sm:group-data-[orientation=vertical]/timeline:h-4",
14+
});
15+
const props = withDefaults(
16+
defineProps<
17+
PrimitiveProps & {
18+
class?: any;
19+
}
20+
>(),
21+
{
22+
as: "time",
23+
}
24+
);
25+
26+
const forwarded = useForwardProps(reactiveOmit(props, ["class"]));
27+
</script>

app/components/Ui/Timeline/Header.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<template>
2+
<Primitive data-slot="timeline-header" :as :as-child>
3+
<slot />
4+
</Primitive>
5+
</template>
6+
7+
<script lang="ts" setup>
8+
import { Primitive } from "radix-vue";
9+
import type { PrimitiveProps } from "radix-vue";
10+
11+
defineProps<PrimitiveProps>();
12+
</script>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<Primitive
3+
data-slot="timeline-indicator"
4+
aria-hidden="true"
5+
v-bind="forwarded"
6+
:class="styles({ class: props.class })"
7+
>
8+
<slot />
9+
</Primitive>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
import { reactiveOmit } from "@vueuse/core";
14+
import { Primitive, useForwardProps } from "radix-vue";
15+
import type { PrimitiveProps } from "radix-vue";
16+
17+
const styles = tv({
18+
base: "absolute size-4 rounded-full border-2 border-primary/20 group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:left-0 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:top-0 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=vertical]/timeline:-translate-x-1/2 group-data-[completed=true]/timeline-item:border-primary",
19+
});
20+
const props = defineProps<
21+
PrimitiveProps & {
22+
class?: any;
23+
}
24+
>();
25+
26+
const forwarded = useForwardProps(reactiveOmit(props, ["class"]));
27+
</script>

app/components/Ui/Timeline/Item.vue

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<template>
2+
<Primitive
3+
:data-completed="timelineData?.model?.value && step <= timelineData?.model?.value"
4+
:data-step="step"
5+
data-slot="timeline-item"
6+
aria-hidden="true"
7+
v-bind="forwarded"
8+
:class="styles({ class: props.class })"
9+
>
10+
<slot />
11+
</Primitive>
12+
</template>
13+
14+
<script lang="ts" setup>
15+
import { reactiveOmit } from "@vueuse/core";
16+
import { Primitive, useForwardProps } from "radix-vue";
17+
import type { TimelineData } from "./Timeline.vue";
18+
import type { PrimitiveProps } from "radix-vue";
19+
20+
import { timelineDataSymbol } from "./Timeline.vue";
21+
22+
const timelineData = inject<TimelineData>(timelineDataSymbol);
23+
24+
const styles = tv({
25+
base: "group/timeline-item relative flex flex-1 flex-col gap-0.5 group-data-[orientation=horizontal]/timeline:mt-8 group-data-[orientation=vertical]/timeline:ml-8 group-data-[orientation=horizontal]/timeline:[&:not(:last-child)]:pe-8 group-data-[orientation=vertical]/timeline:[&:not(:last-child)]:pb-12 [&_[data-slot=timeline-separator]]:has-[+[data-completed=true]]:bg-primary",
26+
});
27+
const props = defineProps<
28+
PrimitiveProps & {
29+
class?: any;
30+
step: number;
31+
}
32+
>();
33+
34+
const forwarded = useForwardProps(reactiveOmit(props, ["class", "step"]));
35+
</script>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<Primitive
3+
data-slot="timeline-separator"
4+
aria-hidden="true"
5+
v-bind="forwarded"
6+
:class="styles({ class: props.class })"
7+
>
8+
<slot />
9+
</Primitive>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
import { reactiveOmit } from "@vueuse/core";
14+
import { Primitive, useForwardProps } from "radix-vue";
15+
import type { PrimitiveProps } from "radix-vue";
16+
17+
const styles = tv({
18+
base: "absolute self-start bg-primary/10 group-last/timeline-item:hidden group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=horizontal]/timeline:h-0.5 group-data-[orientation=vertical]/timeline:h-[calc(100%-1rem-0.25rem)] group-data-[orientation=horizontal]/timeline:w-[calc(100%-1rem-0.25rem)] group-data-[orientation=vertical]/timeline:w-0.5 group-data-[orientation=horizontal]/timeline:-translate-y-1/2 group-data-[orientation=horizontal]/timeline:translate-x-[1.125rem] group-data-[orientation=vertical]/timeline:-translate-x-1/2 group-data-[orientation=vertical]/timeline:translate-y-[1.125rem]",
19+
});
20+
const props = defineProps<
21+
PrimitiveProps & {
22+
class?: any;
23+
}
24+
>();
25+
26+
const forwarded = useForwardProps(reactiveOmit(props, ["class"]));
27+
</script>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<template>
2+
<Primitive
3+
:data-orientation="orientation"
4+
data-slot="timeline"
5+
v-bind="forwarded"
6+
:class="styles({ class: props.class })"
7+
>
8+
<slot />
9+
</Primitive>
10+
</template>
11+
12+
<script lang="ts">
13+
import { reactiveOmit } from "@vueuse/core";
14+
import { Primitive, useForwardProps } from "radix-vue";
15+
import type { PrimitiveProps } from "radix-vue";
16+
import type { ModelRef } from "vue";
17+
18+
export type TimelineData = {
19+
model: ModelRef<number | undefined, string, number | undefined, number | undefined>;
20+
orientation: "horizontal" | "vertical";
21+
};
22+
export type TimelineProps = PrimitiveProps & {
23+
class?: any;
24+
orientation?: "horizontal" | "vertical";
25+
modelValue?: number | undefined;
26+
};
27+
export const timelineDataSymbol = Symbol("timeline-data");
28+
</script>
29+
30+
<script lang="ts" setup>
31+
const styles = tv({
32+
base: "group/timeline flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col",
33+
});
34+
const model = defineModel<number | undefined>({ default: 1 });
35+
const props = withDefaults(defineProps<TimelineProps>(), {
36+
orientation: "vertical",
37+
});
38+
const forwarded = useForwardProps(reactiveOmit(props, ["modelValue", "class", "orientation"]));
39+
provide<TimelineData>(timelineDataSymbol, {
40+
model,
41+
orientation: props.orientation,
42+
});
43+
</script>

app/components/Ui/Timeline/Title.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<template>
2+
<Primitive
3+
data-slot="timeline-title"
4+
aria-hidden="true"
5+
v-bind="forwarded"
6+
:class="styles({ class: props.class })"
7+
>
8+
<slot />
9+
</Primitive>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
import { reactiveOmit } from "@vueuse/core";
14+
import { Primitive, useForwardProps } from "radix-vue";
15+
import type { PrimitiveProps } from "radix-vue";
16+
17+
const styles = tv({
18+
base: "text-sm font-medium",
19+
});
20+
const props = defineProps<
21+
PrimitiveProps & {
22+
class?: any;
23+
}
24+
>();
25+
26+
const forwarded = useForwardProps(reactiveOmit(props, ["class"]));
27+
</script>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div class="flex justify-center">
3+
<UiTimeline :model-value="3">
4+
<template v-for="(item, i) of items" :key="i">
5+
<UiTimelineItem :step="item.id">
6+
<UiTimelineHeader>
7+
<UiTimelineSeparator />
8+
<UiTimelineTitle class="-mt-0.5">{{ item.title }}</UiTimelineTitle>
9+
<UiTimelineIndicator />
10+
</UiTimelineHeader>
11+
</UiTimelineItem>
12+
</template>
13+
</UiTimeline>
14+
</div>
15+
</template>
16+
17+
<script lang="ts" setup>
18+
const items = [
19+
{
20+
id: 1,
21+
date: "Mar 15, 2024",
22+
title: "Project Kickoff",
23+
description:
24+
"Initial team meeting and project scope definition. Established key milestones and resource allocation.",
25+
},
26+
{
27+
id: 2,
28+
date: "Mar 22, 2024",
29+
title: "Design Phase",
30+
description:
31+
"Completed wireframes and user interface mockups. Stakeholder review and feedback incorporated.",
32+
},
33+
{
34+
id: 3,
35+
date: "Apr 5, 2024",
36+
title: "Development Sprint",
37+
description: "Backend API implementation and frontend component development in progress.",
38+
},
39+
{
40+
id: 4,
41+
date: "Apr 19, 2024",
42+
title: "Testing & Deployment",
43+
description:
44+
"Quality assurance testing, performance optimization, and production deployment preparation.",
45+
},
46+
];
47+
</script>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<template>
2+
<div class="flex justify-center">
3+
<div class="space-y-3">
4+
<div class="text-xs font-medium text-muted-foreground">Activity</div>
5+
<UiTimeline>
6+
<UiTimelineItem
7+
v-for="item in items"
8+
:key="item.id"
9+
:step="item.id"
10+
class="!m-0 flex-row items-center gap-3 !py-2.5"
11+
>
12+
<Icon :name="getActionIcon(item.action)" class="size-4 text-muted-foreground/80" />
13+
<UiAvatar :src="item.image" :alt="item.user" class="size-6" />
14+
<UiTimelineContent class="flex items-center gap-2 text-foreground">
15+
<a class="font-medium hover:underline" href="#">
16+
{{ item.user }}
17+
</a>
18+
<span class="font-normal">
19+
{{ getActionText(item.action) }}
20+
<a class="hover:underline" href="#">
21+
{{ useTimeAgo(item.date).value }}
22+
</a>
23+
</span>
24+
</UiTimelineContent>
25+
</UiTimelineItem>
26+
</UiTimeline>
27+
</div>
28+
</div>
29+
</template>
30+
31+
<script lang="ts" setup>
32+
const items: { id: number; user: string; image: string; action: ActionType; date: Date }[] = [
33+
{
34+
id: 1,
35+
user: "Matt",
36+
image: "https://randomuser.me/api/portraits/med/men/75.jpg",
37+
action: "post",
38+
date: new Date(Date.now() - 59000), // 59 seconds ago
39+
},
40+
{
41+
id: 2,
42+
user: "Matt",
43+
image: "https://randomuser.me/api/portraits/med/men/75.jpg",
44+
action: "reply",
45+
date: new Date(Date.now() - 180000), // 3 minutes ago
46+
},
47+
{
48+
id: 3,
49+
user: "Matt",
50+
image: "https://randomuser.me/api/portraits/med/men/75.jpg",
51+
action: "edit",
52+
date: new Date(Date.now() - 300000), // 5 minutes ago
53+
},
54+
{
55+
id: 4,
56+
user: "Matt",
57+
image: "https://randomuser.me/api/portraits/med/men/75.jpg",
58+
action: "create",
59+
date: new Date(Date.now() - 600000), // 10 minutes ago
60+
},
61+
];
62+
63+
type ActionType = "post" | "reply" | "edit" | "create";
64+
65+
function getActionIcon(action: ActionType): string {
66+
const icons: Record<ActionType, string> = {
67+
post: "lucide:book-open",
68+
reply: "lucide:message-circle",
69+
edit: "lucide:pencil",
70+
create: "lucide:plus",
71+
};
72+
return icons[action];
73+
}
74+
75+
function getActionText(action: ActionType): string {
76+
const texts: Record<ActionType, string> = {
77+
post: "wrote a new post",
78+
reply: "replied to a comment",
79+
edit: "edited a post",
80+
create: "created a new project",
81+
};
82+
return texts[action];
83+
}
84+
</script>

0 commit comments

Comments
 (0)