Add tooltip content for MediaImageCard

This commit is contained in:
2026-01-16 11:32:42 +05:00
parent 0858920f4b
commit 295c68c52a
6 changed files with 212 additions and 44 deletions

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { BadgeVariants } from "."
import { reactiveOmit } from "@vueuse/core"
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { badgeVariants } from "."
const props = defineProps<PrimitiveProps & {
variant?: BadgeVariants["variant"]
class?: HTMLAttributes["class"]
}>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot />
</Primitive>
</template>

View File

@ -0,0 +1,26 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Badge } from "./Badge.vue"
export const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import tinycolor from "tinycolor2";
import type { VNodeRef } from "vue"; import type { VNodeRef } from "vue";
import tinycolor from "tinycolor2" import Badge from "../badge/Badge.vue";
import { Dot } from "lucide-vue-next";
const imageElement = shallowRef<VNodeRef | null>(null); const imageElement = shallowRef<VNodeRef | null>(null);
@ -32,6 +34,7 @@ interface Props {
tags?: string[]; tags?: string[];
}; };
tooltipCollisionBoundary?: Element;
dynamicColor?: boolean; dynamicColor?: boolean;
@ -43,12 +46,41 @@ const props = defineProps<Props>();
</script> </script>
<template> <template>
<div class="rounded-lg h-full overflow-hidden max-w-64"> <div class="h-full overflow-hidden max-w-64">
<div class="flex flex-col gap-2 cursor-pointer text-muted-foreground vibrant-color"> <div class="flex flex-col gap-2 cursor-pointer text-muted-foreground vibrant-color">
<NuxtImg ref="imageElement" :src="props.image_url" sizes="128px md:256px" quality="100" /> <TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<NuxtImg class="rounded-lg" ref="imageElement" :src="props.image_url" sizes="128px md:256px"
quality="100" />
<p class="text-lg line-clamp-2 font-semibold"> <p class="text-lg line-clamp-2 font-semibold">
{{ props.title }} {{ props.title }}
</p> </p>
</TooltipTrigger>
<TooltipContent :sideOffset="16" :collisionBoundary="props.tooltipCollisionBoundary"
:avoidCollisions="true" side="right" class="bg-muted text-muted-foreground"
arrowClass="bg-muted fill-muted">
<div class="flex flex-col gap-1 m-4">
<div class="flex">
<p class="text-xl font-bold truncate max-w-[24ch]">{{ props.title }}</p>
</div>
<p class="text-lg font-semibold tracking-wider" :style="{ color: paletteVibrantHsl }">{{
props.tooltipMetadata?.studio }}
</p>
<div class="flex items-center text-base font-semibold">
<p>{{ props.tooltipMetadata?.type }}</p>
<Dot />
<p>{{ props.tooltipMetadata?.rating }} episodes</p>
</div>
<div class="flex gap-2">
<Badge v-for="tag in props.tooltipMetadata?.tags">
{{ tag }}
</Badge>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
</div> </div>
</template> </template>

View File

@ -9,8 +9,15 @@ defineOptions({
inheritAttrs: false, inheritAttrs: false,
}) })
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(), { interface Props {
includeArrow?: boolean
class?: HTMLAttributes["class"]
arrowClass?: HTMLAttributes["class"]
}
const props = withDefaults(defineProps<TooltipContentProps & Props>(), {
sideOffset: 4, sideOffset: 4,
includeArrow: true
}) })
const emits = defineEmits<TooltipContentEmits>() const emits = defineEmits<TooltipContentEmits>()
@ -21,14 +28,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template> <template>
<TooltipPortal> <TooltipPortal>
<TooltipContent <TooltipContent data-slot="tooltip-content" v-bind="{ ...forwarded, ...$attrs }"
data-slot="tooltip-content" :class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)">
v-bind="{ ...forwarded, ...$attrs }"
:class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)"
>
<slot /> <slot />
<TooltipArrow class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> <TooltipArrow
:class="cn('bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]', arrowClass)"
v-if="includeArrow" />
</TooltipContent> </TooltipContent>
</TooltipPortal> </TooltipPortal>
</template> </template>

View File

@ -1,5 +1,6 @@
<template> <template>
<ThemeProvider> <ThemeProvider>
<TooltipProvider>
<ClientOnly> <ClientOnly>
<Sonner /> <Sonner />
</ClientOnly> </ClientOnly>
@ -11,6 +12,7 @@
<slot /> <slot />
</main> </main>
</div> </div>
</TooltipProvider>
</ThemeProvider> </ThemeProvider>
</template> </template>
@ -19,7 +21,10 @@ import 'vue-sonner/style.css'
import Sonner from '@/components/ui/sonner/Sonner.vue'; import Sonner from '@/components/ui/sonner/Sonner.vue';
import AppHeader from '@/components/ui/internal/AppHeader.vue'; import AppHeader from '@/components/ui/internal/AppHeader.vue';
import Headroom from "headroom.js"; import ThemeProvider from '~/components/ui/provider/ThemeProvider.vue'; import Headroom from "headroom.js";
import ThemeProvider from '~/components/ui/provider/ThemeProvider.vue';
import TooltipProvider from '~/components/ui/tooltip/TooltipProvider.vue';
const header = ref(null); const header = ref(null);

View File

@ -4,6 +4,7 @@ import HeroSection from '@/components/ui/internal/HeroSection.vue';
import MediaImageCard from '~/components/ui/internal/MediaImageCard.vue'; import MediaImageCard from '~/components/ui/internal/MediaImageCard.vue';
import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageCard.vue'; import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageCard.vue';
const tooltipCollisionBoundary = ref<Element | undefined>(undefined);
</script> </script>
<template> <template>
@ -30,28 +31,100 @@ import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageC
View all View all
</Button> </Button>
</div> </div>
<div <div ref="tooltipCollisionBoundary"
class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4 gap-y-12 justify-items-center w-full"> class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4 gap-y-12 justify-items-center w-full">
<MediaImageCard image_url="example/jigokuraku.jpg" title="Jigokuraku 2nd Season" type="anime" /> <MediaImageCard image_url="example/jigokuraku.jpg" title="Jigokuraku 2nd Season" type="anime"
:tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/jujutsukaisen.jpg" <MediaImageCard image_url="example/jujutsukaisen.jpg"
title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen" type="anime" /> title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen" type="anime" :tooltipEnabled="true"
:tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/okiraku.jpg" title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei" <MediaImageCard image_url="example/okiraku.jpg" title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei"
type="anime" /> type="anime" :tooltipEnabled="true" :tooltipMetadata="{
<MediaImageCard image_url="example/oshi no ko.jpg" title="[Oshi no Ko] 3rd Season" type="anime" /> studio: 'MAPPA',
<MediaImageCard image_url="example/shibou.jpg" title="Shibou Yuugi de Meshi wo Kuu." type="anime" /> type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/oshi no ko.jpg" title="[Oshi no Ko] 3rd Season" type="anime"
:tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/shibou.jpg" title="Shibou Yuugi de Meshi wo Kuu." type="anime"
:tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/sousounofrieren.jpg" title="Sousou no Frieren 2nd Season" <MediaImageCard image_url="example/sousounofrieren.jpg" title="Sousou no Frieren 2nd Season"
type="anime" /> type="anime" :tooltipEnabled="true" :tooltipMetadata="{
<MediaImageCard image_url="example/tamonkun.jpg" title="Tamon-kun Ima Docchi!?" type="anime" /> studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/tamonkun.jpg" title="Tamon-kun Ima Docchi!?" type="anime"
:tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/yuusha party.jpg" title="Yuusha Party wo Oidasareta Kiyou Binbou" <MediaImageCard image_url="example/yuusha party.jpg" title="Yuusha Party wo Oidasareta Kiyou Binbou"
type="anime" /> type="anime" :tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/jujutsukaisen.jpg" <MediaImageCard image_url="example/jujutsukaisen.jpg"
title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen soetjsot giwjiagjwiag wgi wajig wig gwi" title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen soetjsot giwjiagjwiag wgi wajig wig gwi"
type="anime" /> type="anime" :tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/okiraku.jpg" <MediaImageCard image_url="example/okiraku.jpg"
title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei teisotsoeitj wgjiwag wgj wigwji" title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei teisotsoeitj wgjiwag wgj wigwji" type="anime"
type="anime" /> :tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<MediaImageCard image_url="example/oshi no ko.jpg" <MediaImageCard image_url="example/oshi no ko.jpg"
title="[Oshi no Ko] 3rd Season iaowjegiogjoiawgio waiogjawiog wag" type="anime" /> title="[Oshi no Ko] 3rd Season iaowjegiogjoiawgio waiogjawiog wag" type="anime"
:tooltipEnabled="true" :tooltipMetadata="{
studio: 'MAPPA',
type: 'TV Show',
episodes: 12,
rating: 78,
tags: ['action', 'adventure']
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
<SkeletonMediaImageCard /> <SkeletonMediaImageCard />
</div> </div>
</div> </div>