Basic implementation for anime page
This commit is contained in:
@ -7,20 +7,19 @@ import { Primitive } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { badgeVariants } from "."
|
||||
|
||||
const props = defineProps<PrimitiveProps & {
|
||||
variant?: BadgeVariants["variant"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
const props = withDefaults(defineProps<PrimitiveProps & {
|
||||
variant?: BadgeVariants["variant"]
|
||||
size?: BadgeVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>(), {
|
||||
size: "default"
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="badge"
|
||||
:class="cn(badgeVariants({ variant }), props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
<Primitive data-slot="badge" :class="cn(badgeVariants({ variant, size }), props.class)" v-bind="delegatedProps">
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
@ -4,23 +4,28 @@ 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",
|
||||
},
|
||||
"inline-flex items-center justify-center rounded-full border 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",
|
||||
},
|
||||
size: {
|
||||
default: "px-2 py-0.5",
|
||||
xs: "px-1 py-0.2",
|
||||
lg: "px-3 py-1"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||
|
||||
22
app/components/ui/card/Card.vue
Normal file
22
app/components/ui/card/Card.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<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="card"
|
||||
:class="
|
||||
cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
app/components/ui/card/CardAction.vue
Normal file
17
app/components/ui/card/CardAction.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<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="card-action"
|
||||
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
app/components/ui/card/CardContent.vue
Normal file
17
app/components/ui/card/CardContent.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<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="card-content"
|
||||
:class="cn('px-6', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
app/components/ui/card/CardDescription.vue
Normal file
17
app/components/ui/card/CardDescription.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
data-slot="card-description"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
17
app/components/ui/card/CardFooter.vue
Normal file
17
app/components/ui/card/CardFooter.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<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="card-footer"
|
||||
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
app/components/ui/card/CardHeader.vue
Normal file
17
app/components/ui/card/CardHeader.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<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="card-header"
|
||||
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
app/components/ui/card/CardTitle.vue
Normal file
17
app/components/ui/card/CardTitle.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<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="card-title"
|
||||
:class="cn('leading-none font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
7
app/components/ui/card/index.ts
Normal file
7
app/components/ui/card/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export { default as Card } from "./Card.vue"
|
||||
export { default as CardAction } from "./CardAction.vue"
|
||||
export { default as CardContent } from "./CardContent.vue"
|
||||
export { default as CardDescription } from "./CardDescription.vue"
|
||||
export { default as CardFooter } from "./CardFooter.vue"
|
||||
export { default as CardHeader } from "./CardHeader.vue"
|
||||
export { default as CardTitle } from "./CardTitle.vue"
|
||||
82
app/components/ui/hoverable-image/HoverableImage.vue
Normal file
82
app/components/ui/hoverable-image/HoverableImage.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script setup lang="ts" generic="Provider extends keyof ConfiguredImageProviders = ProviderDefaults['provider']">
|
||||
import { Tooltip, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ConfiguredImageProviders, ProviderDefaults } from '@nuxt/image';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { imageVariants } from '.';
|
||||
|
||||
interface Props {
|
||||
src: string;
|
||||
alt?: string;
|
||||
title?: string;
|
||||
sizes?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
loading?: 'lazy' | 'eager';
|
||||
provider?: Provider;
|
||||
quality?: number;
|
||||
class?: HTMLAttributes['class'];
|
||||
overlayHeight?: string;
|
||||
overlayOpacity?: number;
|
||||
overlayClass?: string;
|
||||
imageClass?: string;
|
||||
variant?: 'default' | 'rounded' | 'elevated' | 'minimal';
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
alt: '',
|
||||
title: '',
|
||||
sizes: '100vw sm:50vw md:33vw lg:25vw',
|
||||
width: 400,
|
||||
height: 300,
|
||||
loading: 'lazy',
|
||||
provider: undefined,
|
||||
quality: 80,
|
||||
overlayHeight: '4em',
|
||||
overlayOpacity: 1,
|
||||
overlayClass: '',
|
||||
imageClass: '',
|
||||
variant: 'default',
|
||||
size: 'md',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
hover: [state: boolean];
|
||||
click: [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const handleHover = (state: boolean) => {
|
||||
emit('hover', state);
|
||||
};
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
emit('click', event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div :class="cn(imageVariants({ variant, size }), props.class)" @mouseenter="handleHover(true)"
|
||||
@mouseleave="handleHover(false)" @click="handleClick">
|
||||
<NuxtImg :src="src" :alt="alt" :sizes="sizes" :width="width" :height="height" :loading="loading"
|
||||
:provider="provider" :quality="quality" :class="cn('w-full h-auto object-cover', imageClass)" />
|
||||
|
||||
<div
|
||||
:class="cn('absolute bottom-0 left-0 right-0 overlay-gradient bg-linear-to-t group-hover:opacity-0 opacity-100 from-muted from-20% to-transparent flex items-end transition-opacity p-4', overlayClass)">
|
||||
<span v-if="title" class="text-white font-semibold text-sm md:text-base line-clamp-2">
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<slot />
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overlay-gradient {
|
||||
height: v-bind(overlayHeight);
|
||||
}
|
||||
</style>
|
||||
31
app/components/ui/hoverable-image/index.ts
Normal file
31
app/components/ui/hoverable-image/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export { default as HoverableImage } from "./HoverableImage.vue"
|
||||
|
||||
export const imageVariants = cva(
|
||||
"relative group cursor-pointer overflow-hidden rounded-lg",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border border-border",
|
||||
rounded: "rounded-full overflow-hidden",
|
||||
elevated: "shadow-lg hover:shadow-xl",
|
||||
minimal: "border-0",
|
||||
},
|
||||
size: {
|
||||
sm: "max-w-xs",
|
||||
md: "max-w-md",
|
||||
lg: "max-w-lg",
|
||||
xl: "max-w-xl",
|
||||
full: "w-full",
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "md",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export type HoverableImageVariants = VariantProps<typeof imageVariants>
|
||||
@ -17,10 +17,11 @@ watch(palette, () => {
|
||||
});
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
image_url: string;
|
||||
image_alt?: string;
|
||||
title: string;
|
||||
type: 'anime' | 'manga' | 'character' | 'voice_actor';
|
||||
type: string;
|
||||
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
@ -46,45 +47,46 @@ const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full overflow-hidden max-w-64">
|
||||
<div class="flex flex-col gap-2 cursor-pointer text-muted-foreground vibrant-color">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<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">
|
||||
{{ props.title }}
|
||||
</p>
|
||||
</div>
|
||||
</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>
|
||||
<NuxtLink :to="`anime/${id}`">
|
||||
<div class="h-full overflow-hidden max-w-64">
|
||||
<div class="flex flex-col gap-2 cursor-pointer text-muted-foreground vibrant-color">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<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 max-w-[12ch] 2xl:max-w-full">
|
||||
{{ 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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :sideOffset="16" :collisionBoundary="props.tooltipCollisionBoundary"
|
||||
:avoidCollisions="true" side="right" tooltipColor="muted" tooltipArrowColor="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>
|
||||
<div class="flex gap-2">
|
||||
<Badge v-for="tag in props.tooltipMetadata?.tags">
|
||||
{{ tag }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -9,7 +9,7 @@ const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('rounded-lg h-full overflow-hidden w-full max-w-64', props.class)">
|
||||
<div :class="cn('rounded-lg h-full min-h-72 overflow-hidden w-full max-w-64', props.class)">
|
||||
<div class="flex flex-col gap-2 h-full pb-6">
|
||||
<Skeleton class="h-full w-full" />
|
||||
<Skeleton class="h-6 w-3/4" />
|
||||
|
||||
24
app/components/ui/tabs/Tabs.vue
Normal file
24
app/components/ui/tabs/Tabs.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsRootEmits, TabsRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TabsRoot, useForwardPropsEmits } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<TabsRootProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<TabsRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="tabs"
|
||||
v-bind="forwarded"
|
||||
:class="cn('flex flex-col gap-2', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</TabsRoot>
|
||||
</template>
|
||||
21
app/components/ui/tabs/TabsContent.vue
Normal file
21
app/components/ui/tabs/TabsContent.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TabsContent } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<TabsContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsContent
|
||||
data-slot="tabs-content"
|
||||
:class="cn('flex-1 outline-none', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</TabsContent>
|
||||
</template>
|
||||
24
app/components/ui/tabs/TabsList.vue
Normal file
24
app/components/ui/tabs/TabsList.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsListProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TabsList } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<TabsListProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsList
|
||||
data-slot="tabs-list"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</TabsList>
|
||||
</template>
|
||||
26
app/components/ui/tabs/TabsTrigger.vue
Normal file
26
app/components/ui/tabs/TabsTrigger.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabsTriggerProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TabsTrigger, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsTrigger
|
||||
data-slot="tabs-trigger"
|
||||
:class="cn(
|
||||
'data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</TabsTrigger>
|
||||
</template>
|
||||
4
app/components/ui/tabs/index.ts
Normal file
4
app/components/ui/tabs/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as Tabs } from "./Tabs.vue"
|
||||
export { default as TabsContent } from "./TabsContent.vue"
|
||||
export { default as TabsList } from "./TabsList.vue"
|
||||
export { default as TabsTrigger } from "./TabsTrigger.vue"
|
||||
@ -4,6 +4,7 @@ import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { tooltipArrowVariants, tooltipContentVariants, type TooltipArrowVariants, type TooltipContentVariants } from "."
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
@ -13,6 +14,8 @@ interface Props {
|
||||
includeArrow?: boolean
|
||||
class?: HTMLAttributes["class"]
|
||||
arrowClass?: HTMLAttributes["class"]
|
||||
tooltipColor?: TooltipContentVariants["color"]
|
||||
tooltipArrowColor?: TooltipArrowVariants["color"]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TooltipContentProps & Props>(), {
|
||||
@ -29,11 +32,11 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
<template>
|
||||
<TooltipPortal>
|
||||
<TooltipContent data-slot="tooltip-content" 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)">
|
||||
:class="cn(tooltipContentVariants({ variant: 'default', color: tooltipColor }), props.class)">
|
||||
<slot />
|
||||
|
||||
<TooltipArrow
|
||||
:class="cn('bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]', arrowClass)"
|
||||
:class="cn(tooltipArrowVariants({ variant: 'default', color: tooltipArrowColor }), arrowClass)"
|
||||
v-if="includeArrow" />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
|
||||
@ -2,3 +2,48 @@ export { default as Tooltip } from "./Tooltip.vue"
|
||||
export { default as TooltipContent } from "./TooltipContent.vue"
|
||||
export { default as TooltipProvider } from "./TooltipProvider.vue"
|
||||
export { default as TooltipTrigger } from "./TooltipTrigger.vue"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export const tooltipContentVariants = cva(
|
||||
"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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: ""
|
||||
},
|
||||
color: {
|
||||
default: "bg-foreground",
|
||||
muted: "bg-muted text-muted-foreground"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
color: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export const tooltipArrowVariants = cva(
|
||||
"z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: ""
|
||||
},
|
||||
color: {
|
||||
default: "bg-foreground fill-foreground",
|
||||
muted: "bg-muted fill-muted"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
color: "default"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export type TooltipContentVariants = VariantProps<typeof tooltipContentVariants>
|
||||
export type TooltipArrowVariants = VariantProps<typeof tooltipArrowVariants>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user