Polish mobile layouts, extract ExpandableText, fix image sizing

- Stack title-hero poster/metadata vertically on mobile (flex-col md:flex-row),
  switch backdrop tall breakpoint from sm to md
- Extract bio expand/collapse logic into reusable ExpandableText component;
  use it in PersonHero and TitleHero
- Fix ContinueWatchingCard image to use fill + sizes instead of fixed
  width/height to avoid layout shift
- Add fade-out gradient on genre chip scrollbar edge on mobile
- Show scaled-down ambient glow on mobile instead of hiding it entirely
- Humanize knownForDepartment labels (Acting→Actor, Directing→Director, etc.)
- Change cast member name from truncate to line-clamp-2 for wrapping
- Hide tooltip arrow via [&>:last-child]:hidden instead of color-matching it
- Apply safe-area-inset padding to HeroBanner content on notched devices
This commit is contained in:
jake committed 2026-03-07 12:09:51 -05:00
1 parent 5d63ad699f
commit 383f0b819b
97 files changed
+767 -608

No files matched your search

+3 -3
View File
@@ -25,17 +25,17 @@ export default async function RegisterPage() {
<IconLock aria-hidden={true} className="size-6 text-primary" /> <IconLock aria-hidden={true} className="size-6 text-primary" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h1 className="font-display text-xl tracking-tight text-balance"> <h1 className="text-balance font-display text-xl tracking-tight">
Registration Closed Registration Closed
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
New accounts are not being accepted right now. Contact the admin New accounts are not being accepted right now. Contact the admin
if you need access. if you need access.
</p> </p>
</div> </div>
<Link <Link
href="/login" href="/login"
className="inline-flex h-10 items-center rounded-lg bg-primary px-6 text-sm font-medium text-primary-foreground transition-all hover:shadow-md hover:shadow-primary/20" className="inline-flex h-10 items-center rounded-lg bg-primary px-6 font-medium text-primary-foreground text-sm transition-all hover:shadow-md hover:shadow-primary/20"
> >
Sign in instead Sign in instead
</Link> </Link>
+11 -11
View File
@@ -50,7 +50,7 @@ const steps = [
</a>{" "} </a>{" "}
and request an API key. Choose &ldquo;Developer&rdquo; when asked. You and request an API key. Choose &ldquo;Developer&rdquo; when asked. You
need the{" "} need the{" "}
<span className="font-mono text-xs text-primary"> <span className="font-mono text-primary text-xs">
API Read Access Token API Read Access Token
</span>{" "} </span>{" "}
(the long one). (the long one).
@@ -123,11 +123,11 @@ export function SetupForm() {
> >
{/* Header */} {/* Header */}
<motion.div variants={sectionVariants} className="space-y-3"> <motion.div variants={sectionVariants} className="space-y-3">
<div className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-medium text-primary"> <div className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-3 py-1 font-medium text-primary text-xs">
<IconKey aria-hidden={true} className="size-3.5" /> <IconKey aria-hidden={true} className="size-3.5" />
Setup required Setup required
</div> </div>
<h1 className="font-display text-3xl tracking-tight text-balance sm:text-4xl"> <h1 className="text-balance font-display text-3xl tracking-tight sm:text-4xl">
Connect to TMDB Connect to TMDB
</h1> </h1>
<p className="max-w-lg text-muted-foreground leading-relaxed"> <p className="max-w-lg text-muted-foreground leading-relaxed">
@@ -150,13 +150,13 @@ export function SetupForm() {
{steps.map((step, i) => ( {steps.map((step, i) => (
<div key={step.number} className="flex gap-4"> <div key={step.number} className="flex gap-4">
<div className="flex shrink-0 items-start pt-0.5"> <div className="flex shrink-0 items-start pt-0.5">
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary/10 font-mono text-sm font-semibold text-primary"> <span className="flex h-7 w-7 items-center justify-center rounded-full bg-primary/10 font-mono font-semibold text-primary text-sm">
{step.number} {step.number}
</span> </span>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<h2 className="font-medium">{step.title}</h2> <h2 className="font-medium">{step.title}</h2>
<p className="text-sm leading-relaxed text-muted-foreground"> <p className="text-muted-foreground text-sm leading-relaxed">
{step.description} {step.description}
</p> </p>
@@ -168,8 +168,8 @@ export function SetupForm() {
key={snippet.label} key={snippet.label}
className="group relative overflow-hidden rounded-lg border border-border/50 bg-card/60" className="group relative overflow-hidden rounded-lg border border-border/50 bg-card/60"
> >
<div className="flex items-center justify-between border-b border-border/30 px-3 py-1.5"> <div className="flex items-center justify-between border-border/30 border-b px-3 py-1.5">
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground"> <span className="font-medium text-[11px] text-muted-foreground uppercase tracking-wider">
{snippet.label} {snippet.label}
</span> </span>
<Button <Button
@@ -194,7 +194,7 @@ export function SetupForm() {
)} )}
</Button> </Button>
</div> </div>
<pre className="overflow-x-auto p-3 font-mono text-sm text-foreground/80"> <pre className="overflow-x-auto p-3 font-mono text-foreground/80 text-sm">
{snippet.code} {snippet.code}
</pre> </pre>
</div> </div>
@@ -221,7 +221,7 @@ export function SetupForm() {
<p className="font-medium text-green-400"> <p className="font-medium text-green-400">
TMDB API key detected TMDB API key detected
</p> </p>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
Redirecting you to Sofa… Redirecting you to Sofa…
</p> </p>
</div> </div>
@@ -229,10 +229,10 @@ export function SetupForm() {
) : ( ) : (
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-0.5"> <div className="space-y-0.5">
<p className="text-sm font-medium"> <p className="font-medium text-sm">
After setting the key and restarting: After setting the key and restarting:
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-muted-foreground text-xs">
Click the button to verify your configuration Click the button to verify your configuration
</p> </p>
</div> </div>
@@ -35,16 +35,16 @@ export function ContinueWatchingCard({
return ( return (
<Link <Link
href={`/titles/${item.title.id}`} href={`/titles/${item.title.id}`}
className="group relative w-[calc(100vw-3rem)] shrink-0 overflow-hidden rounded-xl bg-card/50 ring-1 ring-white/[0.06] transition-shadow hover:shadow-lg hover:shadow-black/25 sm:w-72" className="group relative inline-block w-64 shrink-0 overflow-hidden rounded-xl bg-card/50 ring-1 ring-white/[0.06] transition-shadow hover:shadow-black/25 hover:shadow-lg sm:w-72"
> >
<div className="relative aspect-video overflow-hidden rounded-t-xl bg-muted"> <div className="relative aspect-video overflow-hidden rounded-t-xl bg-muted">
{stillUrl ? ( {stillUrl ? (
<Image <Image
src={stillUrl} src={stillUrl}
alt={item.nextEpisode?.name ?? item.title.title} alt={item.nextEpisode?.name ?? item.title.title}
width={500} fill
height={281} sizes="(min-width: 640px) 288px, 256px"
className="h-full w-full object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105" className="object-cover motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-105"
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center bg-gradient-to-br from-card via-secondary to-muted"> <div className="flex h-full items-center justify-center bg-gradient-to-br from-card via-secondary to-muted">
@@ -56,13 +56,13 @@ export function ContinueWatchingCard({
)} )}
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
{item.nextEpisode && ( {item.nextEpisode && (
<div className="absolute bottom-2.5 left-3 right-3"> <div className="absolute right-3 bottom-2.5 left-3">
<p className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-primary"> <p className="flex items-center gap-1.5 font-medium text-[10px] text-primary uppercase tracking-wider">
<span className="inline-block h-1.5 w-1.5 motion-safe:animate-pulse rounded-full bg-primary" /> <span className="inline-block h-1.5 w-1.5 rounded-full bg-primary motion-safe:animate-pulse" />
Up next Up next
</p> </p>
<p className="mt-0.5 truncate text-sm font-medium text-white"> <p className="mt-0.5 truncate font-medium text-sm text-white">
<span className="font-mono text-xs text-white/60 [word-spacing:-0.25em] mr-0.5"> <span className="mr-0.5 font-mono text-white/60 text-xs [word-spacing:-0.25em]">
S{item.nextEpisode.seasonNumber} E S{item.nextEpisode.seasonNumber} E
{item.nextEpisode.episodeNumber} {item.nextEpisode.episodeNumber}
</span>{" "} </span>{" "}
@@ -73,8 +73,8 @@ export function ContinueWatchingCard({
</div> </div>
<div className="flex items-center gap-3 p-3"> <div className="flex items-center gap-3 p-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{item.title.title}</p> <p className="truncate font-medium text-sm">{item.title.title}</p>
<p className="text-xs text-muted-foreground"> <p className="text-muted-foreground text-xs">
{item.watchedEpisodes}/{item.totalEpisodes} episodes {item.watchedEpisodes}/{item.totalEpisodes} episodes
</p> </p>
</div> </div>
@@ -83,7 +83,7 @@ export function ContinueWatchingCard({
</div> </div>
</div> </div>
{progress > 0 && ( {progress > 0 && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted"> <div className="absolute right-0 bottom-0 left-0 h-0.5 bg-muted">
<div <div
className="h-full bg-primary transition-[width]" className="h-full bg-primary transition-[width]"
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
@@ -18,7 +18,7 @@ export function ContinueWatchingList({
return ( return (
<Carousel <Carousel
opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }} opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }}
className="-mx-4 sm:-mx-0" className="-mx-4 sm:-mx-0 [&>[data-slot=carousel-content]]:px-px"
> >
<CarouselContent className="px-4 sm:px-0"> <CarouselContent className="px-4 sm:px-0">
{items.map((item, i) => ( {items.map((item, i) => (
@@ -15,7 +15,7 @@ export function FeedSection({
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span aria-hidden={true}>{icon}</span> <span aria-hidden={true}>{icon}</span>
<h2 className="font-display text-xl tracking-tight text-balance"> <h2 className="text-balance font-display text-xl tracking-tight">
{title} {title}
</h2> </h2>
</div> </div>
@@ -53,7 +53,7 @@ function StatCard({
}: StatCardProps) { }: StatCardProps) {
return ( return (
<div <div
className="animate-stagger-item relative overflow-hidden rounded-xl border border-border/30 bg-card/50 p-4" className="relative animate-stagger-item overflow-hidden rounded-xl border border-border/30 bg-card/50 p-4"
style={{ "--stagger-index": index } as React.CSSProperties} style={{ "--stagger-index": index } as React.CSSProperties}
> >
{sparklineData && <Sparkline data={sparklineData} color={color} />} {sparklineData && <Sparkline data={sparklineData} color={color} />}
@@ -63,7 +63,7 @@ function StatCard({
> >
<Icon aria-hidden={true} className={`size-[13px] ${color}`} /> <Icon aria-hidden={true} className={`size-[13px] ${color}`} />
</div> </div>
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground"> <span className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
{label} {label}
</span> </span>
</div> </div>
@@ -97,7 +97,7 @@ function PeriodSelector({
onValueChange={(v) => v && onPeriodChange(v as TimePeriod)} onValueChange={(v) => v && onPeriodChange(v as TimePeriod)}
> >
<SelectTrigger <SelectTrigger
className={`${inlineTriggerClass} uppercase text-foreground/80`} className={`${inlineTriggerClass} text-foreground/80 uppercase`}
> >
<SelectValue> <SelectValue>
{(value: TimePeriod | null) => (value ? periodLabels[value] : null)} {(value: TimePeriod | null) => (value ? periodLabels[value] : null)}
@@ -132,11 +132,11 @@ export function StatsDisplay({ stats }: { stats: DashboardStats }) {
} | null>(null); } | null>(null);
useEffect(() => { useEffect(() => {
getStatsAction("movies", moviePeriod).then(setMovieStats); void getStatsAction("movies", moviePeriod).then(setMovieStats);
}, [moviePeriod]); }, [moviePeriod]);
useEffect(() => { useEffect(() => {
getStatsAction("episodes", episodePeriod).then(setEpisodeStats); void getStatsAction("episodes", episodePeriod).then(setEpisodeStats);
}, [episodePeriod]); }, [episodePeriod]);
const movieCount = movieStats?.count ?? stats.moviesThisMonth; const movieCount = movieStats?.count ?? stats.moviesThisMonth;
@@ -16,19 +16,19 @@ export async function StatsSection({ userId }: { userId: string }) {
<> <>
<StatsDisplay stats={stats} /> <StatsDisplay stats={stats} />
{isEmpty && ( {isEmpty && (
<div className="flex flex-col items-center gap-4 rounded-xl border border-dashed border-border/50 py-16 text-center"> <div className="flex flex-col items-center gap-4 rounded-xl border border-border/50 border-dashed py-16 text-center">
<div className="rounded-full bg-primary/10 p-4"> <div className="rounded-full bg-primary/10 p-4">
<IconDeviceTv aria-hidden={true} className="size-8 text-primary" /> <IconDeviceTv aria-hidden={true} className="size-8 text-primary" />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<p className="font-medium">Your library is empty</p> <p className="font-medium">Your library is empty</p>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
Search for movies and TV shows to start tracking Search for movies and TV shows to start tracking
</p> </p>
</div> </div>
<Link <Link
href="/explore" href="/explore"
className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-md hover:shadow-primary/20" className="inline-flex h-9 items-center rounded-lg bg-primary px-4 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-md hover:shadow-primary/20"
> >
Start exploring Start exploring
</Link> </Link>
@@ -1,10 +1,10 @@
export function WelcomeHeader({ name }: { name?: string | null }) { export function WelcomeHeader({ name }: { name?: string | null }) {
return ( return (
<div> <div>
<h1 className="font-display text-3xl tracking-tight text-balance"> <h1 className="text-balance font-display text-3xl tracking-tight">
Welcome back{name ? `, ${name}` : ""} Welcome back{name ? `, ${name}` : ""}
</h1> </h1>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-muted-foreground text-sm">
Here&apos;s what&apos;s happening with your library Here&apos;s what&apos;s happening with your library
</p> </p>
</div> </div>
+3 -3
View File
@@ -26,7 +26,7 @@ export default function PagesError({
<h1 className="font-display text-2xl tracking-tight sm:text-3xl"> <h1 className="font-display text-2xl tracking-tight sm:text-3xl">
Something went wrong Something went wrong
</h1> </h1>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground"> <p className="mx-auto max-w-sm text-muted-foreground text-sm leading-relaxed">
An unexpected error occurred while loading this page. You can try An unexpected error occurred while loading this page. You can try
again or head back to the dashboard. again or head back to the dashboard.
</p> </p>
@@ -42,7 +42,7 @@ export default function PagesError({
<button <button
type="button" type="button"
onClick={reset} onClick={reset}
className="group relative inline-flex h-10 items-center justify-center gap-2 overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20" className="group relative inline-flex h-10 items-center justify-center gap-2 overflow-hidden rounded-lg bg-primary px-5 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-lg hover:shadow-primary/20"
> >
<IconRefresh className="size-4" /> <IconRefresh className="size-4" />
<span className="relative z-10">Try again</span> <span className="relative z-10">Try again</span>
@@ -50,7 +50,7 @@ export default function PagesError({
</button> </button>
<Link <Link
href="/dashboard" href="/dashboard"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5" className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 font-medium text-sm transition-colors hover:border-primary/40 hover:bg-primary/5"
> >
Dashboard Dashboard
</Link> </Link>
@@ -106,28 +106,31 @@ function FilterableTitleRowInner({
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{icon} {icon}
<h2 className="font-display text-xl tracking-tight text-balance"> <h2 className="text-balance font-display text-xl tracking-tight">
{heading} {heading}
</h2> </h2>
</div> </div>
{/* Genre chips */} {/* Genre chips */}
<div className="no-scrollbar -mx-4 flex gap-2 overflow-x-auto px-4 pb-1 sm:-mx-0 sm:flex-wrap sm:px-0"> <div className="relative">
{genres.map((genre) => ( <div className="no-scrollbar -mx-4 flex gap-2 overflow-x-auto px-4 pb-1 sm:mx-0 sm:px-0">
<Button {genres.map((genre) => (
key={genre.id} <Button
variant={selectedGenre === genre.id ? "default" : "outline"} key={genre.id}
size="xs" variant={selectedGenre === genre.id ? "default" : "outline"}
onClick={() => toggleGenre(genre.id)} size="sm"
className={`shrink-0 rounded-full ${ onClick={() => toggleGenre(genre.id)}
selectedGenre === genre.id className={`shrink-0 rounded-full ${
? "border-primary bg-primary/10 text-primary hover:bg-primary/20" selectedGenre === genre.id
: "border-border/50 bg-card/50 text-muted-foreground hover:border-primary/20 hover:text-foreground" ? "border-primary bg-primary/10 text-primary hover:bg-primary/20"
}`} : "border-border/50 bg-card/50 text-muted-foreground hover:border-primary/20 hover:text-foreground"
> }`}
{genre.name} >
</Button> {genre.name}
))} </Button>
))}
</div>
<div className="pointer-events-none absolute top-0 right-0 h-full w-8 bg-gradient-to-l from-background to-transparent sm:hidden" />
</div> </div>
{/* Loading skeleton */} {/* Loading skeleton */}
@@ -147,7 +150,7 @@ function FilterableTitleRowInner({
{/* Empty state */} {/* Empty state */}
{!loading && selectedGenre !== null && items.length === 0 && ( {!loading && selectedGenre !== null && items.length === 0 && (
<p className="py-8 text-center text-sm text-muted-foreground"> <p className="py-8 text-center text-muted-foreground text-sm">
No titles found for this genre. No titles found for this genre.
</p> </p>
)} )}
@@ -161,13 +164,13 @@ function FilterableTitleRowInner({
dragFree: true, dragFree: true,
containScroll: "trimSnaps", containScroll: "trimSnaps",
}} }}
className="-mx-6 sm:-mx-2 carousel-tilt" className="carousel-tilt -mx-6 sm:-mx-2"
> >
<CarouselContent className="px-6 sm:px-2"> <CarouselContent className="px-6 sm:px-2">
{items.slice(0, 20).map((item, i) => ( {items.slice(0, 20).map((item, i) => (
<CarouselItem <CarouselItem
key={`${item.type}-${item.tmdbId}`} key={`${item.type}-${item.tmdbId}`}
className="basis-auto pl-4 w-[140px] shrink-0 sm:w-[160px]" className="w-[140px] shrink-0 basis-auto pl-4 sm:w-[160px]"
> >
<div <div
className="animate-stagger-item" className="animate-stagger-item"
+10 -10
View File
@@ -24,8 +24,8 @@ export function HeroBanner({
const href = `/titles/tmdb-${tmdbId}-${type}`; const href = `/titles/tmdb-${tmdbId}-${type}`;
return ( return (
<div className="animate-stagger-item relative -mt-6 mb-4 ml-[calc(-50vw+50%)] mr-[calc(-50vw+50%)] overflow-hidden"> <div className="relative -mt-6 mr-[calc(-50vw+50%)] mb-4 ml-[calc(-50vw+50%)] animate-stagger-item overflow-hidden">
<div className="relative w-full min-h-[280px] max-h-[420px] aspect-[21/9]"> <div className="relative aspect-[21/9] max-h-[420px] min-h-[280px] w-full">
{backdropPath ? ( {backdropPath ? (
<Image <Image
src={backdropPath} src={backdropPath}
@@ -44,18 +44,18 @@ export function HeroBanner({
{/* Content */} {/* Content */}
<div className="absolute inset-0 flex items-end"> <div className="absolute inset-0 flex items-end">
<div className="w-full px-4 pb-8 sm:px-6"> <div className="w-full pb-8">
<div className="mx-auto max-w-6xl"> <div className="mx-auto max-w-6xl pr-[max(1rem,env(safe-area-inset-right))] pl-[max(1rem,env(safe-area-inset-left))] sm:pr-[max(1.5rem,env(safe-area-inset-right))] sm:pl-[max(1.5rem,env(safe-area-inset-left))]">
<div <div
className="animate-stagger-item" className="animate-stagger-item"
style={{ "--stagger-index": 3 } as React.CSSProperties} style={{ "--stagger-index": 3 } as React.CSSProperties}
> >
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<span className="rounded bg-primary/20 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-primary"> <span className="rounded bg-primary/20 px-2 py-0.5 font-semibold text-[10px] text-primary uppercase tracking-wider">
{type} {type}
</span> </span>
{voteAverage > 0 && ( {voteAverage > 0 && (
<span className="flex items-center gap-1 text-sm text-primary"> <span className="flex items-center gap-1 text-primary text-sm">
<IconStar <IconStar
aria-hidden={true} aria-hidden={true}
className="size-3.5 fill-primary" className="size-3.5 fill-primary"
@@ -63,21 +63,21 @@ export function HeroBanner({
{voteAverage.toFixed(1)} {voteAverage.toFixed(1)}
</span> </span>
)} )}
<span className="text-xs text-muted-foreground"> <span className="text-muted-foreground text-xs">
Trending today Trending today
</span> </span>
</div> </div>
<Link href={href} className="group/title"> <Link href={href} className="group/title">
<h2 className="font-display text-3xl tracking-tight text-balance sm:text-4xl transition-colors group-hover/title:text-primary"> <h2 className="text-balance font-display text-3xl tracking-tight transition-colors group-hover/title:text-primary sm:text-4xl">
{title} {title}
</h2> </h2>
</Link> </Link>
<p className="mt-2 line-clamp-2 max-w-2xl text-sm text-muted-foreground"> <p className="mt-2 line-clamp-2 max-w-2xl text-muted-foreground text-sm">
{overview} {overview}
</p> </p>
<Link <Link
href={href} href={href}
className="mt-4 inline-flex h-9 items-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-md hover:shadow-primary/20" className="mt-4 inline-flex h-9 items-center gap-2 rounded-lg bg-primary px-4 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-md hover:shadow-primary/20"
> >
<IconPlus aria-hidden={true} className="size-4" /> <IconPlus aria-hidden={true} className="size-4" />
Add to Library Add to Library
@@ -37,19 +37,19 @@ export function TitleRow({
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{icon} {icon}
<h2 className="font-display text-xl tracking-tight text-balance"> <h2 className="text-balance font-display text-xl tracking-tight">
{heading} {heading}
</h2> </h2>
</div> </div>
<Carousel <Carousel
opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }} opts={{ align: "start", dragFree: true, containScroll: "trimSnaps" }}
className="-mx-6 sm:-mx-2 carousel-tilt" className="carousel-tilt -mx-6 sm:-mx-2"
> >
<CarouselContent className="px-6 sm:px-2"> <CarouselContent className="px-6 sm:px-2">
{items.map((item, i) => ( {items.map((item, i) => (
<CarouselItem <CarouselItem
key={`${item.type}-${item.tmdbId}`} key={`${item.type}-${item.tmdbId}`}
className="basis-auto pl-4 w-[140px] shrink-0 sm:w-[160px]" className="w-[140px] shrink-0 basis-auto pl-4 sm:w-[160px]"
> >
<div <div
className="animate-stagger-item" className="animate-stagger-item"
+2 -2
View File
@@ -35,8 +35,8 @@ export default function ExploreLoading() {
return ( return (
<div className="space-y-10"> <div className="space-y-10">
{/* Hero skeleton — full viewport width like the actual HeroBanner */} {/* Hero skeleton — full viewport width like the actual HeroBanner */}
<div className="-mt-6 mb-4 ml-[calc(-50vw+50%)] mr-[calc(-50vw+50%)]"> <div className="-mt-6 mr-[calc(-50vw+50%)] mb-4 ml-[calc(-50vw+50%)]">
<Skeleton className="aspect-[21/9] min-h-[280px] max-h-[420px] w-full rounded-none" /> <Skeleton className="aspect-[21/9] max-h-[420px] min-h-[280px] w-full rounded-none" />
</div> </div>
<TitleRowSkeleton /> <TitleRowSkeleton />
+3 -3
View File
@@ -20,11 +20,11 @@ export default async function PagesLayout({
<ProgressProvider> <ProgressProvider>
<div className="relative z-0 min-h-screen pb-[calc(3.5rem+env(safe-area-inset-bottom))] sm:pb-0"> <div className="relative z-0 min-h-screen pb-[calc(3.5rem+env(safe-area-inset-bottom))] sm:pb-0">
<NavBar userName={session.user.name} /> <NavBar userName={session.user.name} />
{/* Ambient glow — hidden on mobile where it overwhelms the viewport */} {/* Ambient glow — smaller on mobile to add warmth without overwhelming */}
<div className="pointer-events-none fixed left-1/2 top-1/4 -translate-x-1/2 -translate-y-1/2 hidden h-[600px] w-[800px] rounded-full bg-primary/3 blur-[200px] sm:block" /> <div className="pointer-events-none fixed top-1/4 left-1/2 h-[300px] w-[300px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/3 blur-[120px] sm:h-[600px] sm:w-[800px] sm:blur-[200px]" />
<main <main
id="main-content" id="main-content"
className="relative mx-auto max-w-6xl pl-[max(1rem,env(safe-area-inset-left))] pr-[max(1rem,env(safe-area-inset-right))] py-6 sm:pl-[max(1.5rem,env(safe-area-inset-left))] sm:pr-[max(1.5rem,env(safe-area-inset-right))]" className="relative mx-auto max-w-6xl py-6 pr-[max(1rem,env(safe-area-inset-right))] pl-[max(1rem,env(safe-area-inset-left))] sm:pr-[max(1.5rem,env(safe-area-inset-right))] sm:pl-[max(1.5rem,env(safe-area-inset-left))]"
> >
{children} {children}
</main> </main>
+6 -6
View File
@@ -4,37 +4,37 @@ export default function PagesNotFound() {
return ( return (
<div className="flex flex-col items-center gap-6 py-24 text-center"> <div className="flex flex-col items-center gap-6 py-24 text-center">
{/* Ghosted 404 */} {/* Ghosted 404 */}
<h1 className="animate-stagger-item font-display text-[6rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[8rem]"> <h1 className="animate-stagger-item font-display text-[6rem] text-foreground/[0.06] leading-[0.85] tracking-tight sm:text-[8rem]">
404 404
</h1> </h1>
<div <div
className="animate-stagger-item -mt-4 space-y-2" className="-mt-4 animate-stagger-item space-y-2"
style={{ "--stagger-index": 1 } as React.CSSProperties} style={{ "--stagger-index": 1 } as React.CSSProperties}
> >
<h2 className="font-display text-2xl tracking-tight sm:text-3xl"> <h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Page not found Page not found
</h2> </h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground"> <p className="mx-auto max-w-sm text-muted-foreground text-sm leading-relaxed">
This page doesn&apos;t exist or may have been moved. Try searching for This page doesn&apos;t exist or may have been moved. Try searching for
what you&apos;re looking for instead. what you&apos;re looking for instead.
</p> </p>
</div> </div>
<div <div
className="animate-stagger-item flex items-center gap-3" className="flex animate-stagger-item items-center gap-3"
style={{ "--stagger-index": 2 } as React.CSSProperties} style={{ "--stagger-index": 2 } as React.CSSProperties}
> >
<Link <Link
href="/dashboard" href="/dashboard"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20" className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-lg hover:shadow-primary/20"
> >
<span className="relative z-10">Dashboard</span> <span className="relative z-10">Dashboard</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" /> <div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</Link> </Link>
<Link <Link
href="/explore" href="/explore"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5" className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 font-medium text-sm transition-colors hover:border-primary/40 hover:bg-primary/5"
> >
Explore Explore
</Link> </Link>
@@ -65,10 +65,10 @@ export function FilmographyGrid({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<IconMovie aria-hidden={true} className="size-5 text-primary" /> <IconMovie aria-hidden={true} className="size-5 text-primary" />
<h2 className="font-display text-xl tracking-tight text-balance"> <h2 className="text-balance font-display text-xl tracking-tight">
Filmography Filmography
</h2> </h2>
<span className="text-sm text-muted-foreground"> <span className="text-muted-foreground text-sm">
({filtered.length}) ({filtered.length})
</span> </span>
</div> </div>
@@ -105,7 +105,7 @@ export function FilmographyGrid({
<Button <Button
key={f.value} key={f.value}
variant={filter === f.value ? "default" : "secondary"} variant={filter === f.value ? "default" : "secondary"}
size="xs" size="sm"
onClick={() => setFilter(f.value)} onClick={() => setFilter(f.value)}
className="rounded-full" className="rounded-full"
> >
@@ -3,7 +3,7 @@
import { IconCalendar, IconMapPin } from "@tabler/icons-react"; import { IconCalendar, IconMapPin } from "@tabler/icons-react";
import { format, parseISO } from "date-fns"; import { format, parseISO } from "date-fns";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useRef, useState } from "react"; import { ExpandableText } from "@/components/expandable-text";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import type { ResolvedPerson } from "@/lib/types/title"; import type { ResolvedPerson } from "@/lib/types/title";
@@ -23,27 +23,13 @@ function calculateAge(birthday: string, deathday?: string | null): number {
} }
export function PersonHero({ person }: PersonHeroProps) { export function PersonHero({ person }: PersonHeroProps) {
const [bioExpanded, setBioExpanded] = useState(false);
const [isClamped, setIsClamped] = useState(false);
const bioRef = useRef<HTMLParagraphElement>(null);
useEffect(() => {
const el = bioRef.current;
if (!el) return;
const check = () => setIsClamped(el.scrollHeight > el.clientHeight);
check();
const observer = new ResizeObserver(check);
observer.observe(el);
return () => observer.disconnect();
}, []);
const age = person.birthday const age = person.birthday
? calculateAge(person.birthday, person.deathday) ? calculateAge(person.birthday, person.deathday)
: null; : null;
return ( return (
<div className="animate-stagger-item flex flex-col gap-6 sm:flex-row sm:gap-8"> <div className="flex animate-stagger-item flex-col gap-6 sm:flex-row sm:gap-8">
<div className="size-40 shrink-0 self-center overflow-hidden rounded-2xl ring-1 ring-white/10 shadow-2xl sm:size-56 sm:self-start"> <div className="size-40 shrink-0 self-center overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/10 sm:size-56 sm:self-start">
{person.profilePath ? ( {person.profilePath ? (
<Image <Image
src={person.profilePath} src={person.profilePath}
@@ -63,17 +49,27 @@ export function PersonHero({ person }: PersonHeroProps) {
</div> </div>
<div className="min-w-0 flex-1 space-y-3"> <div className="min-w-0 flex-1 space-y-3">
<h1 className="font-display text-3xl tracking-tight text-balance sm:text-5xl"> <h1 className="text-balance font-display text-3xl tracking-tight sm:text-5xl">
{person.name} {person.name}
</h1> </h1>
{person.knownForDepartment && ( {person.knownForDepartment && (
<Badge className="border-0 bg-primary/10 px-2.5 font-semibold uppercase tracking-wider text-primary"> <Badge className="border-0 bg-primary/10 px-2.5 font-semibold text-primary uppercase tracking-wider">
{person.knownForDepartment} {person.knownForDepartment === "Acting"
? "Actor"
: person.knownForDepartment === "Directing"
? "Director"
: person.knownForDepartment === "Writing"
? "Writer"
: person.knownForDepartment === "Production"
? "Producer"
: person.knownForDepartment === "Editing"
? "Editor"
: person.knownForDepartment}
</Badge> </Badge>
)} )}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground"> <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground text-sm">
{person.birthday && ( {person.birthday && (
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<IconCalendar aria-hidden={true} className="size-3.5" /> <IconCalendar aria-hidden={true} className="size-3.5" />
@@ -93,27 +89,7 @@ export function PersonHero({ person }: PersonHeroProps) {
)} )}
</div> </div>
{person.biography && ( {person.biography && <ExpandableText text={person.biography} />}
<div className="max-w-3xl">
<p
ref={bioRef}
className={`text-sm leading-relaxed text-muted-foreground ${
!bioExpanded ? "line-clamp-3" : ""
}`}
>
{person.biography}
</p>
{(isClamped || bioExpanded) && (
<button
type="button"
onClick={() => setBioExpanded(!bioExpanded)}
className="mt-1 text-xs font-medium text-primary transition-colors hover:text-primary/80"
>
{bioExpanded ? "Show less" : "Read more"}
</button>
)}
</div>
)}
</div> </div>
</div> </div>
); );
+6 -6
View File
@@ -4,39 +4,39 @@ export default function PersonNotFound() {
return ( return (
<div className="flex flex-col items-center gap-6 py-24 text-center"> <div className="flex flex-col items-center gap-6 py-24 text-center">
<h1 <h1
className="animate-stagger-item font-display text-[6rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[8rem]" className="animate-stagger-item font-display text-[6rem] text-foreground/[0.06] leading-[0.85] tracking-tight sm:text-[8rem]"
style={{ "--stagger-index": 0 } as React.CSSProperties} style={{ "--stagger-index": 0 } as React.CSSProperties}
> >
404 404
</h1> </h1>
<div <div
className="animate-stagger-item -mt-4 space-y-2" className="-mt-4 animate-stagger-item space-y-2"
style={{ "--stagger-index": 1 } as React.CSSProperties} style={{ "--stagger-index": 1 } as React.CSSProperties}
> >
<h2 className="font-display text-2xl tracking-tight sm:text-3xl"> <h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Person not found Person not found
</h2> </h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground"> <p className="mx-auto max-w-sm text-muted-foreground text-sm leading-relaxed">
The person you&apos;re looking for doesn&apos;t exist or may have been The person you&apos;re looking for doesn&apos;t exist or may have been
removed from the database. removed from the database.
</p> </p>
</div> </div>
<div <div
className="animate-stagger-item flex items-center gap-3" className="flex animate-stagger-item items-center gap-3"
style={{ "--stagger-index": 2 } as React.CSSProperties} style={{ "--stagger-index": 2 } as React.CSSProperties}
> >
<Link <Link
href="/explore" href="/explore"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20" className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-lg hover:shadow-primary/20"
> >
<span className="relative z-10">Explore titles</span> <span className="relative z-10">Explore titles</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" /> <div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</Link> </Link>
<Link <Link
href="/dashboard" href="/dashboard"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5" className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 font-medium text-sm transition-colors hover:border-primary/40 hover:bg-primary/5"
> >
Dashboard Dashboard
</Link> </Link>
@@ -29,7 +29,7 @@ export function AccountSection({
<div> <div>
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<IconUser aria-hidden={true} className="size-4 text-muted-foreground" /> <IconUser aria-hidden={true} className="size-4 text-muted-foreground" />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
Account Account
</h2> </h2>
</div> </div>
@@ -48,7 +48,7 @@ export function AccountSection({
)} )}
</CardTitle> </CardTitle>
<CardDescription>{user.email}</CardDescription> <CardDescription>{user.email}</CardDescription>
<p className="mt-0.5 text-xs text-muted-foreground/60"> <p className="mt-0.5 text-muted-foreground/60 text-xs">
Member since {memberSince} Member since {memberSince}
</p> </p>
</div> </div>
@@ -184,7 +184,7 @@ function BackupScheduleInner() {
value={String(maxRetention)} value={String(maxRetention)}
onValueChange={(v) => v && changeMaxRetention(Number(v))} onValueChange={(v) => v && changeMaxRetention(Number(v))}
> >
<SelectTrigger className="h-auto w-auto gap-0.5 rounded-none border-0 bg-transparent p-0 shadow-none underline decoration-dotted decoration-muted-foreground/50 underline-offset-4 hover:bg-transparent hover:text-foreground hover:decoration-foreground/50 focus-visible:ring-0 focus-visible:decoration-solid focus-visible:decoration-foreground dark:bg-transparent dark:hover:bg-transparent"> <SelectTrigger className="h-auto w-auto gap-0.5 rounded-none border-0 bg-transparent p-0 underline decoration-muted-foreground/50 decoration-dotted underline-offset-4 shadow-none hover:bg-transparent hover:text-foreground hover:decoration-foreground/50 focus-visible:decoration-foreground focus-visible:decoration-solid focus-visible:ring-0 dark:bg-transparent dark:hover:bg-transparent">
<SelectValue> <SelectValue>
{(value: string | null) => {(value: string | null) =>
value === "0" value === "0"
@@ -226,7 +226,7 @@ function BackupScheduleInner() {
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{enabled && ( {enabled && (
<CardContent className="border-t border-border/30 pt-4"> <CardContent className="border-border/30 border-t pt-4">
<motion.div <motion.div
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }} animate={{ opacity: 1, height: "auto" }}
@@ -237,7 +237,7 @@ function BackupScheduleInner() {
<div className="space-y-3"> <div className="space-y-3">
{/* Frequency selector */} {/* Frequency selector */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<span className="inline-block text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="inline-block font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
Frequency Frequency
</span> </span>
<ButtonGroup> <ButtonGroup>
@@ -270,7 +270,7 @@ function BackupScheduleInner() {
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
className="overflow-hidden" className="overflow-hidden"
> >
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
Day:{" "} Day:{" "}
</span> </span>
<Select <Select
@@ -279,7 +279,7 @@ function BackupScheduleInner() {
v && changeSchedule(frequency, time, Number(v)) v && changeSchedule(frequency, time, Number(v))
} }
> >
<SelectTrigger className="h-auto gap-1 border-border/50 bg-muted/30 px-2.5 py-1 text-xs text-foreground hover:bg-muted/50 dark:bg-muted/30 dark:hover:bg-muted/50"> <SelectTrigger className="h-auto gap-1 border-border/50 bg-muted/30 px-2.5 py-1 text-foreground text-xs hover:bg-muted/50 dark:bg-muted/30 dark:hover:bg-muted/50">
<SelectValue> <SelectValue>
{(value: string | null) => {(value: string | null) =>
value !== null value !== null
@@ -314,14 +314,14 @@ function BackupScheduleInner() {
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
className="overflow-hidden" className="overflow-hidden"
> >
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
{frequency === "12h" ? "Starting at" : "Time:"}{" "} {frequency === "12h" ? "Starting at" : "Time:"}{" "}
</span> </span>
<Select <Select
value={time} value={time}
onValueChange={(v) => v && changeSchedule(frequency, v)} onValueChange={(v) => v && changeSchedule(frequency, v)}
> >
<SelectTrigger className="h-auto gap-1 border-border/50 bg-muted/30 px-2.5 py-1 text-xs text-foreground hover:bg-muted/50 dark:bg-muted/30 dark:hover:bg-muted/50"> <SelectTrigger className="h-auto gap-1 border-border/50 bg-muted/30 px-2.5 py-1 text-foreground text-xs hover:bg-muted/50 dark:bg-muted/30 dark:hover:bg-muted/50">
<SelectValue> <SelectValue>
{(value: string | null) => {(value: string | null) =>
value value
@@ -127,7 +127,7 @@ export function BackupSection({
{/* Backup list */} {/* Backup list */}
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{backups.length > 0 && ( {backups.length > 0 && (
<CardContent className="border-t border-border/30 pt-4"> <CardContent className="border-border/30 border-t pt-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
{backups.map((backup) => ( {backups.map((backup) => (
<motion.div <motion.div
@@ -169,7 +169,7 @@ export function BackupSection({
</Tooltip> </Tooltip>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<span className="text-xs font-medium text-foreground"> <span className="font-medium text-foreground text-xs">
{formatBackupDate(backup.createdAt)} {formatBackupDate(backup.createdAt)}
</span> </span>
<span className="text-[11px] text-muted-foreground"> <span className="text-[11px] text-muted-foreground">
@@ -185,7 +185,7 @@ export function BackupSection({
</span> </span>
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100"> <div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100">
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ render={
@@ -135,7 +135,7 @@ export function IntegrationCard({ config }: { config: IntegrationConfig }) {
</CardContent> </CardContent>
<CollapsibleContent className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0"> <CollapsibleContent className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0">
<CardContent className="space-y-3 border-t border-border/30 pt-4"> <CardContent className="space-y-3 border-border/30 border-t pt-4">
{config.alert} {config.alert}
{!connection ? ( {!connection ? (
@@ -216,7 +216,7 @@ export function IntegrationCard({ config }: { config: IntegrationConfig }) {
)} )}
<Collapsible open={setupOpen} onOpenChange={setSetupOpen}> <Collapsible open={setupOpen} onOpenChange={setSetupOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-1.5 rounded-md py-1 text-xs text-muted-foreground transition-colors hover:text-foreground"> <CollapsibleTrigger className="flex w-full items-center gap-1.5 rounded-md py-1 text-muted-foreground text-xs transition-colors hover:text-foreground">
<IconChevronDown <IconChevronDown
aria-hidden={true} aria-hidden={true}
className={`size-3 transition-transform ${setupOpen ? "rotate-0" : "-rotate-90"}`} className={`size-3 transition-transform ${setupOpen ? "rotate-0" : "-rotate-90"}`}
@@ -224,7 +224,7 @@ export function IntegrationCard({ config }: { config: IntegrationConfig }) {
Setup instructions Setup instructions
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0"> <CollapsibleContent className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0">
<div className="mt-2 rounded-lg bg-muted/30 border border-border/50 p-3 text-xs leading-relaxed text-muted-foreground"> <div className="mt-2 rounded-lg border border-border/50 bg-muted/30 p-3 text-muted-foreground text-xs leading-relaxed">
<ol className="list-inside list-decimal space-y-1.5"> <ol className="list-inside list-decimal space-y-1.5">
{config.setupSteps} {config.setupSteps}
</ol> </ol>
@@ -232,7 +232,7 @@ export function IntegrationCard({ config }: { config: IntegrationConfig }) {
<p className="mt-2 -ml-0.5"> <p className="mt-2 -ml-0.5">
<IconBook2 <IconBook2
aria-hidden={true} aria-hidden={true}
className="inline-block size-3 translate-y-[-1px] mr-1" className="mr-1 inline-block size-3 translate-y-[-1px]"
/> />
Need more help?{" "} Need more help?{" "}
<a <a
@@ -17,7 +17,7 @@ function origin() {
/** Reusable alert banner for integrations that require a subscription. */ /** Reusable alert banner for integrations that require a subscription. */
function RequirementAlert({ children }: { children: React.ReactNode }) { function RequirementAlert({ children }: { children: React.ReactNode }) {
return ( return (
<Alert className="border-primary/20 bg-primary/5 [&>svg]:text-primary gap-0"> <Alert className="gap-0 border-primary/20 bg-primary/5 [&>svg]:text-primary">
<IconInfoCircle aria-hidden={true} className="inline-block size-3.5" /> <IconInfoCircle aria-hidden={true} className="inline-block size-3.5" />
<AlertDescription className="text-foreground/80"> <AlertDescription className="text-foreground/80">
{children} {children}
@@ -29,7 +29,7 @@ export function IntegrationsSection({
aria-hidden={true} aria-hidden={true}
className="size-4 text-muted-foreground" className="size-4 text-muted-foreground"
/> />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
Integrations Integrations
</h2> </h2>
</div> </div>
@@ -33,11 +33,11 @@ export function SettingsShell({
<motion.div variants={sectionVariants}> <motion.div variants={sectionVariants}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<IconSettings aria-hidden={true} className="size-5 text-primary" /> <IconSettings aria-hidden={true} className="size-5 text-primary" />
<h1 className="font-display text-3xl tracking-tight text-balance"> <h1 className="text-balance font-display text-3xl tracking-tight">
Settings Settings
</h1> </h1>
</div> </div>
<p className="mt-1 text-sm text-muted-foreground"> <p className="mt-1 text-muted-foreground text-sm">
Manage your account and preferences Manage your account and preferences
</p> </p>
</motion.div> </motion.div>
@@ -225,9 +225,9 @@ function SystemStatusCard() {
</CardContent> </CardContent>
{/* Database */} {/* Database */}
<CardContent className="border-t border-border/30 pt-4"> <CardContent className="border-border/30 border-t pt-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
Database Database
</span> </span>
<span className="font-mono text-[11px] text-muted-foreground"> <span className="font-mono text-[11px] text-muted-foreground">
@@ -239,22 +239,22 @@ function SystemStatusCard() {
</CardContent> </CardContent>
{/* TMDB */} {/* TMDB */}
<CardContent className="border-t border-border/30 pt-4"> <CardContent className="border-border/30 border-t pt-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
TMDB API TMDB API
</span> </span>
{!data.tmdb.tokenConfigured ? ( {!data.tmdb.tokenConfigured ? (
<> <>
<StatusDot status="error" /> <StatusDot status="error" />
<span className="text-xs text-muted-foreground/50"> <span className="text-muted-foreground/50 text-xs">
Not configured Not configured
</span> </span>
</> </>
) : data.tmdb.connected && data.tmdb.tokenValid ? ( ) : data.tmdb.connected && data.tmdb.tokenValid ? (
<> <>
<StatusDot status="ok" /> <StatusDot status="ok" />
<span className="text-xs text-muted-foreground">Connected</span> <span className="text-muted-foreground text-xs">Connected</span>
<span className="font-mono text-[11px] text-muted-foreground/80"> <span className="font-mono text-[11px] text-muted-foreground/80">
{data.tmdb.responseTimeMs}ms {data.tmdb.responseTimeMs}ms
</span> </span>
@@ -262,12 +262,12 @@ function SystemStatusCard() {
) : data.tmdb.connected && !data.tmdb.tokenValid ? ( ) : data.tmdb.connected && !data.tmdb.tokenValid ? (
<> <>
<StatusDot status="error" /> <StatusDot status="error" />
<span className="text-xs text-destructive">Invalid token</span> <span className="text-destructive text-xs">Invalid token</span>
</> </>
) : ( ) : (
<> <>
<StatusDot status="error" /> <StatusDot status="error" />
<span className="text-xs text-destructive">Unreachable</span> <span className="text-destructive text-xs">Unreachable</span>
{data.tmdb.error && ( {data.tmdb.error && (
<span className="text-[11px] text-muted-foreground/50"> <span className="text-[11px] text-muted-foreground/50">
{data.tmdb.error} {data.tmdb.error}
@@ -279,9 +279,9 @@ function SystemStatusCard() {
</CardContent> </CardContent>
{/* Environment */} {/* Environment */}
<CardContent className="border-t border-border/30 pt-4"> <CardContent className="border-border/30 border-t pt-4">
<div className="space-y-2"> <div className="space-y-2">
<span className="inline-flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="inline-flex items-center gap-1.5 font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
Environment Environment
{data.environment.dataDirWritable ? ( {data.environment.dataDirWritable ? (
<IconCheck aria-hidden={true} className="size-3 text-green-500" /> <IconCheck aria-hidden={true} className="size-3 text-green-500" />
@@ -301,7 +301,7 @@ function SystemStatusCard() {
className="flex items-baseline gap-[1px] font-mono text-[11px] leading-relaxed" className="flex items-baseline gap-[1px] font-mono text-[11px] leading-relaxed"
> >
<span className="text-muted-foreground/60">{env.name}=</span> <span className="text-muted-foreground/60">{env.name}=</span>
<span className="text-muted-foreground break-all"> <span className="break-all text-muted-foreground">
{env.value} {env.value}
</span> </span>
</div> </div>
@@ -365,23 +365,23 @@ function BackgroundJobsCard() {
<RefreshButton /> <RefreshButton />
</div> </div>
</CardContent> </CardContent>
<CardContent className="border-t border-border/30 px-0 pt-0 pb-0"> <CardContent className="border-border/30 border-t px-0 pt-0 pb-0">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="border-b-border/30 hover:bg-transparent"> <TableRow className="border-b-border/30 hover:bg-transparent">
<TableHead className="h-8 pl-5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"> <TableHead className="h-8 pl-5 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
Job Job
</TableHead> </TableHead>
<TableHead className="h-8 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"> <TableHead className="h-8 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
Schedule Schedule
</TableHead> </TableHead>
<TableHead className="h-8 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"> <TableHead className="h-8 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
Last run Last run
</TableHead> </TableHead>
<TableHead className="h-8 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"> <TableHead className="h-8 font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
Next run Next run
</TableHead> </TableHead>
<TableHead className="h-8 pr-5 text-right text-[10px] font-medium uppercase tracking-wider text-muted-foreground"> <TableHead className="h-8 pr-5 text-right font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
<span className="sr-only">Actions</span> <span className="sr-only">Actions</span>
</TableHead> </TableHead>
</TableRow> </TableRow>
@@ -413,7 +413,7 @@ function BackgroundJobsCard() {
label={job.lastError ?? "Last run failed"} label={job.lastError ?? "Last run failed"}
/> />
)} )}
<span className="text-xs text-muted-foreground"> <span className="text-muted-foreground text-xs">
{JOB_LABELS[job.jobName] ?? job.jobName} {JOB_LABELS[job.jobName] ?? job.jobName}
</span> </span>
</div> </div>
@@ -424,7 +424,7 @@ function BackgroundJobsCard() {
{job.cronPattern ? ( {job.cronPattern ? (
<Tooltip> <Tooltip>
<TooltipTrigger className="cursor-default"> <TooltipTrigger className="cursor-default">
<span className="text-xs text-muted-foreground/80"> <span className="text-muted-foreground/80 text-xs">
{cronToHuman(job.cronPattern)} {cronToHuman(job.cronPattern)}
</span> </span>
</TooltipTrigger> </TooltipTrigger>
@@ -433,7 +433,7 @@ function BackgroundJobsCard() {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
<span className="text-xs text-muted-foreground/50"> <span className="text-muted-foreground/50 text-xs">
— —
</span> </span>
)} )}
@@ -446,7 +446,7 @@ function BackgroundJobsCard() {
<TooltipTrigger className="cursor-default"> <TooltipTrigger className="cursor-default">
<div className="flex items-baseline gap-1.5"> <div className="flex items-baseline gap-1.5">
<span <span
className="text-xs text-muted-foreground/80" className="text-muted-foreground/80 text-xs"
suppressHydrationWarning suppressHydrationWarning
> >
<LiveTimeAgo date={job.lastRunAt} /> <LiveTimeAgo date={job.lastRunAt} />
@@ -469,7 +469,7 @@ function BackgroundJobsCard() {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
<span className="text-xs text-muted-foreground/50"> <span className="text-muted-foreground/50 text-xs">
Never Never
</span> </span>
)} )}
@@ -481,7 +481,7 @@ function BackgroundJobsCard() {
<Tooltip> <Tooltip>
<TooltipTrigger className="cursor-default"> <TooltipTrigger className="cursor-default">
<span <span
className="text-xs text-muted-foreground/80" className="text-muted-foreground/80 text-xs"
suppressHydrationWarning suppressHydrationWarning
> >
<LiveTimeAgo date={job.nextRunAt} /> <LiveTimeAgo date={job.nextRunAt} />
@@ -492,7 +492,7 @@ function BackgroundJobsCard() {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
<span className="text-xs text-muted-foreground/50"> <span className="text-muted-foreground/50 text-xs">
— —
</span> </span>
)} )}
@@ -561,9 +561,9 @@ function StorageCard() {
</CardContent> </CardContent>
{/* Image cache */} {/* Image cache */}
<CardContent className="border-t border-border/30 pt-4"> <CardContent className="border-border/30 border-t pt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
Image cache Image cache
</span> </span>
{data.imageCache.enabled ? ( {data.imageCache.enabled ? (
@@ -574,17 +574,17 @@ function StorageCard() {
</div> </div>
{data.imageCache.enabled ? ( {data.imageCache.enabled ? (
<> <>
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-muted-foreground text-xs">
{data.imageCache.imageCount.toLocaleString()} cached images {data.imageCache.imageCount.toLocaleString()} cached images
</p> </p>
<p className="mt-0.5 text-[10px] leading-relaxed text-muted-foreground/50"> <p className="mt-0.5 text-[10px] text-muted-foreground/50 leading-relaxed">
{Object.entries(data.imageCache.categories) {Object.entries(data.imageCache.categories)
.map(([name, cat]) => `${name} ${cat.count}`) .map(([name, cat]) => `${name} ${cat.count}`)
.join(" · ")} .join(" · ")}
</p> </p>
</> </>
) : ( ) : (
<p className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground/50"> <p className="mt-1 flex items-center gap-1.5 text-muted-foreground/50 text-xs">
<StatusDot status="inactive" /> <StatusDot status="inactive" />
Disabled Disabled
</p> </p>
@@ -592,9 +592,9 @@ function StorageCard() {
</CardContent> </CardContent>
{/* Backup summary */} {/* Backup summary */}
<CardContent className="border-t border-border/30 pt-4"> <CardContent className="border-border/30 border-t pt-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> <span className="font-medium text-[11px] text-muted-foreground/70 uppercase tracking-wider">
Backups Backups
</span> </span>
{data.backups.backupCount > 0 && ( {data.backups.backupCount > 0 && (
@@ -605,14 +605,14 @@ function StorageCard() {
</div> </div>
{data.backups.backupCount > 0 ? ( {data.backups.backupCount > 0 ? (
<p <p
className="mt-1 text-xs text-muted-foreground" className="mt-1 text-muted-foreground text-xs"
suppressHydrationWarning suppressHydrationWarning
> >
{data.backups.backupCount} backups · last{" "} {data.backups.backupCount} backups · last{" "}
<LiveTimeAgo date={data.backups.lastBackupAt} fallback="unknown" /> <LiveTimeAgo date={data.backups.lastBackupAt} fallback="unknown" />
</p> </p>
) : ( ) : (
<p className="mt-1 flex items-center gap-1.5 text-xs text-muted-foreground/50"> <p className="mt-1 flex items-center gap-1.5 text-muted-foreground/50 text-xs">
<StatusDot status="inactive" /> <StatusDot status="inactive" />
No backups yet No backups yet
</p> </p>
+12 -12
View File
@@ -110,13 +110,13 @@ export default async function SettingsPage() {
return ( return (
<SettingsShell <SettingsShell
footer={ footer={
<footer className="border-t border-border/50 pt-6 pb-2 text-center text-xs text-muted-foreground"> <footer className="border-border/50 border-t pt-6 pb-2 text-center text-muted-foreground text-xs">
<p> <p>
<a <a
href={`https://github.com/${GITHUB_REPO}`} href={`https://github.com/${GITHUB_REPO}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-primary/80 hover:text-primary transition-colors" className="font-medium text-primary/80 transition-colors hover:text-primary"
> >
Sofa Sofa
</a>{" "} </a>{" "}
@@ -129,7 +129,7 @@ export default async function SettingsPage() {
href={`https://github.com/${GITHUB_REPO}/commit/${GIT_COMMIT_SHA}`} href={`https://github.com/${GITHUB_REPO}/commit/${GIT_COMMIT_SHA}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-mono hover:text-primary transition-colors" className="font-mono transition-colors hover:text-primary"
> >
{GIT_COMMIT_SHA} {GIT_COMMIT_SHA}
</a> </a>
@@ -137,7 +137,7 @@ export default async function SettingsPage() {
</> </>
)} )}
{updateCheck?.updateAvailable && ( {updateCheck?.updateAvailable && (
<span className="ml-1.5 inline-flex items-center gap-1 rounded-full bg-primary/15 px-2 py-0.5 text-[10px] font-medium text-primary"> <span className="ml-1.5 inline-flex items-center gap-1 rounded-full bg-primary/15 px-2 py-0.5 font-medium text-[10px] text-primary">
<span className="relative flex h-1.5 w-1.5"> <span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" /> <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-primary" /> <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-primary" />
@@ -161,11 +161,11 @@ export default async function SettingsPage() {
href="https://www.themoviedb.org/" href="https://www.themoviedb.org/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hover:opacity-70 transition-opacity" className="transition-opacity hover:opacity-70"
> >
<TmdbLogo className="h-3" /> <TmdbLogo className="h-3" />
</a> </a>
<p className="text-[10px] leading-relaxed text-muted-foreground"> <p className="text-[10px] text-muted-foreground leading-relaxed">
This product uses the TMDB API but is not endorsed or certified by This product uses the TMDB API but is not endorsed or certified by
TMDB. TMDB.
</p> </p>
@@ -191,10 +191,10 @@ export default async function SettingsPage() {
aria-hidden={true} aria-hidden={true}
className="size-4 text-muted-foreground" className="size-4 text-muted-foreground"
/> />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
Server Server
</h2> </h2>
<span className="rounded-md bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"> <span className="rounded-md bg-primary/10 px-1.5 py-0.5 font-medium text-[10px] text-primary">
Admin only Admin only
</span> </span>
</div> </div>
@@ -210,10 +210,10 @@ export default async function SettingsPage() {
aria-hidden={true} aria-hidden={true}
className="size-4 text-muted-foreground" className="size-4 text-muted-foreground"
/> />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
Security Security
</h2> </h2>
<span className="rounded-md bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"> <span className="rounded-md bg-primary/10 px-1.5 py-0.5 font-medium text-[10px] text-primary">
Admin only Admin only
</span> </span>
</div> </div>
@@ -236,10 +236,10 @@ export default async function SettingsPage() {
aria-hidden={true} aria-hidden={true}
className="size-4 text-muted-foreground" className="size-4 text-muted-foreground"
/> />
<h2 className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> <h2 className="font-medium text-muted-foreground text-xs uppercase tracking-wider">
Backups Backups
</h2> </h2>
<span className="rounded-md bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"> <span className="rounded-md bg-primary/10 px-1.5 py-0.5 font-medium text-[10px] text-primary">
Admin only Admin only
</span> </span>
</div> </div>
@@ -65,7 +65,7 @@ export function CastCarousel({ actors, titleType }: CastCarouselProps) {
)} )}
</div> </div>
<div className="w-full text-center"> <div className="w-full text-center">
<p className="truncate text-xs font-medium"> <p className="line-clamp-2 font-medium text-xs leading-tight">
{member.name} {member.name}
</p> </p>
{member.character && ( {member.character && (
@@ -29,7 +29,7 @@ export function GenreCollapse({ genres }: { genres: string[] }) {
{remaining.map((genre) => ( {remaining.map((genre) => (
<span <span
key={genre} key={genre}
className="px-2 py-1 text-xs text-popover-foreground" className="px-2 py-1 text-popover-foreground text-xs"
> >
{genre} {genre}
</span> </span>
@@ -48,7 +48,7 @@ export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }} exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary/10 px-4 text-sm font-medium text-primary ring-1 ring-primary/20 transition-all hover:bg-primary/15 hover:ring-primary/30 active:scale-[0.97]" className="inline-flex h-9 items-center gap-2 rounded-lg bg-primary/10 px-4 font-medium text-primary text-sm ring-1 ring-primary/20 transition-all hover:bg-primary/15 hover:ring-primary/30 active:scale-[0.97]"
> >
<IconPlus aria-hidden={true} className="size-3.5" strokeWidth={2.5} /> <IconPlus aria-hidden={true} className="size-3.5" strokeWidth={2.5} />
Watchlist Watchlist
@@ -63,7 +63,7 @@ export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
exit={{ opacity: 0, y: -4 }} exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
title="Remove from library" title="Remove from library"
className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 text-sm font-medium ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:ring-destructive/30 hover:bg-destructive/10 hover:text-destructive`} className={`group inline-flex h-9 items-center gap-2 rounded-lg px-4 font-medium text-sm ring-1 transition-all active:scale-[0.97] ${config.class} ${config.bgClass} ${config.borderClass} hover:bg-destructive/10 hover:text-destructive hover:ring-destructive/30`}
> >
<span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1"> <span className="grid [&>svg]:col-start-1 [&>svg]:row-start-1">
<config.icon <config.icon
@@ -72,7 +72,7 @@ export function StatusButton({ currentStatus, onChange }: StatusButtonProps) {
/> />
<IconX <IconX
aria-hidden={true} aria-hidden={true}
className="size-3.5 opacity-0 text-destructive transition-opacity group-hover:opacity-100" className="size-3.5 text-destructive opacity-0 transition-opacity group-hover:opacity-100"
/> />
</span> </span>
<span className="grid [&>span]:col-start-1 [&>span]:row-start-1"> <span className="grid [&>span]:col-start-1 [&>span]:row-start-1">
@@ -30,7 +30,7 @@ export function TitleActions() {
<Button <Button
onClick={handleWatchMovie} onClick={handleWatchMovie}
size="lg" size="lg"
className="h-9 rounded-lg px-4 text-sm active:scale-[0.97] hover:shadow-md hover:shadow-primary/20" className="h-9 rounded-lg px-4 text-sm hover:shadow-md hover:shadow-primary/20 active:scale-[0.97]"
> >
<IconCheck aria-hidden={true} className="size-3.5" /> <IconCheck aria-hidden={true} className="size-3.5" />
Mark Watched Mark Watched
@@ -43,7 +43,7 @@ function ProviderBadge({
), ),
} }
: {})} : {})}
className={`flex h-10 w-10 items-center justify-center overflow-hidden rounded-lg border border-border/30 bg-card motion-safe:transition-transform motion-safe:hover:scale-105${watchUrl ? "" : " cursor-default"}`} className={`flex h-10 w-10 items-center justify-center overflow-hidden rounded-lg border border-border/30 bg-card motion-safe:transition-transform motion-safe:hover:scale-105${watchUrl ? "" : "cursor-default"}`}
> >
{logoPath ? ( {logoPath ? (
<Image <Image
@@ -54,12 +54,12 @@ function ProviderBadge({
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
) : ( ) : (
<span className="text-[8px] font-medium text-muted-foreground"> <span className="font-medium text-[8px] text-muted-foreground">
{name.slice(0, 2)} {name.slice(0, 2)}
</span> </span>
)} )}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="bg-popover px-2 py-1 text-[10px] font-medium text-popover-foreground shadow-md [&>:last-child]:bg-popover [&>:last-child]:fill-popover"> <TooltipContent className="bg-popover px-2 py-1 font-medium text-[10px] text-popover-foreground shadow-md [&>:last-child]:hidden">
{watchUrl ? `Watch on ${name}` : name} {watchUrl ? `Watch on ${name}` : name}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -78,7 +78,7 @@ function OverflowProviderIcon({ offer }: { offer: AvailabilityOffer }) {
className="h-7 w-7 object-cover" className="h-7 w-7 object-cover"
/> />
) : ( ) : (
<span className="text-[7px] font-medium text-muted-foreground"> <span className="font-medium text-[7px] text-muted-foreground">
{offer.providerName.slice(0, 2)} {offer.providerName.slice(0, 2)}
</span> </span>
)} )}
@@ -93,7 +93,7 @@ function OverflowBadge({ offers }: { offers: AvailabilityOffer[] }) {
openOnHover openOnHover
delay={0} delay={0}
closeDelay={300} closeDelay={300}
className="flex h-10 w-10 cursor-default items-center justify-center rounded-lg border border-border/30 bg-card text-xs font-semibold text-muted-foreground motion-safe:transition-transform motion-safe:hover:scale-105" className="flex h-10 w-10 cursor-default items-center justify-center rounded-lg border border-border/30 bg-card font-semibold text-muted-foreground text-xs motion-safe:transition-transform motion-safe:hover:scale-105"
> >
+{offers.length} +{offers.length}
</PopoverTrigger> </PopoverTrigger>
@@ -108,7 +108,7 @@ function OverflowBadge({ offers }: { offers: AvailabilityOffer[] }) {
className="flex items-center gap-2.5 px-2 py-1.5 hover:bg-muted/50" className="flex items-center gap-2.5 px-2 py-1.5 hover:bg-muted/50"
> >
<OverflowProviderIcon offer={offer} /> <OverflowProviderIcon offer={offer} />
<span className="truncate text-xs text-popover-foreground"> <span className="truncate text-popover-foreground text-xs">
{offer.providerName} {offer.providerName}
</span> </span>
</a> </a>
@@ -118,7 +118,7 @@ function OverflowBadge({ offers }: { offers: AvailabilityOffer[] }) {
className="flex items-center gap-2.5 px-2 py-1.5" className="flex items-center gap-2.5 px-2 py-1.5"
> >
<OverflowProviderIcon offer={offer} /> <OverflowProviderIcon offer={offer} />
<span className="truncate text-xs text-popover-foreground"> <span className="truncate text-popover-foreground text-xs">
{offer.providerName} {offer.providerName}
</span> </span>
</div> </div>
@@ -143,8 +143,8 @@ export function TitleAvailability({
if (Object.keys(availByType).length === 0) return null; if (Object.keys(availByType).length === 0) return null;
return ( return (
<div className="pt-1 space-y-2"> <div className="space-y-2 pt-1">
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground"> <h2 className="font-semibold text-muted-foreground text-xs uppercase tracking-wider">
Where to Watch Where to Watch
</h2> </h2>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
@@ -154,7 +154,7 @@ export function TitleAvailability({
return ( return (
<div key={type} className="space-y-1.5"> <div key={type} className="space-y-1.5">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60"> <span className="font-medium text-[10px] text-muted-foreground/60 uppercase tracking-wider">
{offerLabels[type] ?? type} {offerLabels[type] ?? type}
</span> </span>
<div className="flex gap-1.5"> <div className="flex gap-1.5">
@@ -8,6 +8,7 @@ import {
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import Image from "next/image"; import Image from "next/image";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { ExpandableText } from "@/components/expandable-text";
import { TmdbLogo } from "@/components/tmdb-logo"; import { TmdbLogo } from "@/components/tmdb-logo";
import type { ColorPalette, ResolvedTitle } from "@/lib/types/title"; import type { ColorPalette, ResolvedTitle } from "@/lib/types/title";
import { GenreCollapse } from "./genre-collapse"; import { GenreCollapse } from "./genre-collapse";
@@ -33,7 +34,7 @@ export function TitleHero({
<> <>
{/* Backdrop hero */} {/* Backdrop hero */}
{title.backdropPath && ( {title.backdropPath && (
<div className="relative -mt-6 ml-[calc(-50vw+50%)] mr-[calc(-50vw+50%)] h-80 overflow-hidden sm:h-[28rem]"> <div className="relative -mt-6 mr-[calc(-50vw+50%)] ml-[calc(-50vw+50%)] h-80 overflow-hidden md:h-[28rem]">
<Image <Image
src={title.backdropPath} src={title.backdropPath}
alt="" alt=""
@@ -80,12 +81,12 @@ export function TitleHero({
{/* Title header */} {/* Title header */}
<div <div
className={`flex flex-row gap-4 sm:gap-8 ${title.backdropPath ? "-mt-32 relative z-10" : ""}`} className={`flex flex-col gap-4 md:flex-row md:gap-8 ${title.backdropPath ? "relative z-10 -mt-32" : ""}`}
> >
{title.posterPath && ( {title.posterPath && (
<div className="shrink-0"> <div className="shrink-0 self-center md:self-start">
<div <div
className="overflow-hidden rounded-xl sm:rounded-2xl ring-1 ring-foreground/5 shadow-2xl transition-shadow duration-500" className="overflow-hidden rounded-xl shadow-2xl ring-1 ring-foreground/5 transition-shadow duration-500 md:rounded-2xl"
style={{ style={{
boxShadow: palette?.darkVibrant boxShadow: palette?.darkVibrant
? `0 25px 60px -12px ${palette.darkVibrant}50, 0 12px 28px -8px rgba(0,0,0,0.5)` ? `0 25px 60px -12px ${palette.darkVibrant}50, 0 12px 28px -8px rgba(0,0,0,0.5)`
@@ -97,7 +98,7 @@ export function TitleHero({
alt={title.title} alt={title.title}
width={220} width={220}
height={330} height={330}
className="h-auto w-[120px] sm:w-[220px]" className="aspect-[2/3] w-[140px] object-cover md:w-[220px]"
priority priority
/> />
</div> </div>
@@ -106,30 +107,35 @@ export function TitleHero({
<div className="flex-1 space-y-5"> <div className="flex-1 space-y-5">
<div> <div>
<h1 className="font-display text-2xl tracking-tight text-balance sm:text-5xl"> <h1 className="text-balance font-display text-2xl tracking-tight md:text-5xl">
{title.title} {title.title}
</h1> </h1>
<div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-muted-foreground"> <div className="mt-2 space-y-1.5 text-muted-foreground text-sm md:space-y-0">
<TypeBadge type={title.type} /> {/* Desktop: single row with dot separators */}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 [&>*+*]:before:mr-2 [&>*+*]:before:text-border [&>*+*]:before:content-['·']"> <div className="hidden md:flex md:items-center md:gap-x-2 md:gap-y-1">
{title.contentRating && <span>{title.contentRating}</span>} <TypeBadge type={title.type} />
{year && <span>{year}</span>} <div className="flex items-center gap-x-2 [&>*+*]:before:mr-2 [&>*+*]:before:text-border [&>*+*]:before:content-['·']">
{title.genres.length > 0 && ( {title.contentRating && <span>{title.contentRating}</span>}
<GenreCollapse genres={title.genres} /> {year && <span>{year}</span>}
)} {title.genres.length > 0 && (
{title.voteAverage != null && title.voteAverage > 0 && ( <GenreCollapse genres={title.genres} />
<span className="inline-flex items-center gap-1 text-primary"> )}
<IconStarFilled className="size-3.5" /> {title.voteAverage != null && title.voteAverage > 0 && (
{title.voteAverage.toFixed(1)} <span className="inline-flex items-center gap-1 text-primary">
</span> <IconStarFilled className="size-3.5" />
)} {title.voteAverage.toFixed(1)}
{title.status &&
!(title.type === "movie" && title.status === "Released") && (
<span className="inline-flex items-center gap-1">
<StatusIcon status={title.status} />
{title.status}
</span> </span>
)} )}
{title.status &&
!(
title.type === "movie" && title.status === "Released"
) && (
<span className="inline-flex items-center gap-1">
<StatusIcon status={title.status} />
{title.status}
</span>
)}
</div>
<a <a
href={`https://www.themoviedb.org/${title.type === "movie" ? "movie" : "tv"}/${title.tmdbId}`} href={`https://www.themoviedb.org/${title.type === "movie" ? "movie" : "tv"}/${title.tmdbId}`}
target="_blank" target="_blank"
@@ -140,14 +146,70 @@ export function TitleHero({
<TmdbLogo className="h-2.5 w-auto" /> <TmdbLogo className="h-2.5 w-auto" />
</a> </a>
</div> </div>
{/* Mobile: two clean rows */}
<div className="flex items-center gap-2 md:hidden">
<TypeBadge type={title.type} />
{title.contentRating && <span>{title.contentRating}</span>}
{year && (
<>
{title.contentRating && (
<span className="text-border">·</span>
)}
<span>{year}</span>
</>
)}
{title.genres.length > 0 && (
<>
<span className="text-border">·</span>
<GenreCollapse genres={title.genres} />
</>
)}
{title.voteAverage != null && title.voteAverage > 0 && (
<>
<span className="text-border">·</span>
<span className="inline-flex items-center gap-1 text-primary">
<IconStarFilled className="size-3.5" />
{title.voteAverage.toFixed(1)}
</span>
</>
)}
</div>
{title.status &&
!(title.type === "movie" && title.status === "Released") && (
<div className="flex items-center gap-2 md:hidden">
<span className="inline-flex items-center gap-1 text-muted-foreground/70">
<StatusIcon status={title.status} />
{title.status}
</span>
<a
href={`https://www.themoviedb.org/${title.type === "movie" ? "movie" : "tv"}/${title.tmdbId}`}
target="_blank"
rel="noopener noreferrer"
aria-label="View on TMDB"
className="inline-flex items-center opacity-70 transition-opacity hover:opacity-40"
>
<TmdbLogo className="h-2.5 w-auto" />
</a>
</div>
)}
{/* TMDB link for mobile when no status is shown */}
{(!title.status ||
(title.type === "movie" && title.status === "Released")) && (
<a
href={`https://www.themoviedb.org/${title.type === "movie" ? "movie" : "tv"}/${title.tmdbId}`}
target="_blank"
rel="noopener noreferrer"
aria-label="View on TMDB"
className="inline-flex items-center opacity-70 transition-opacity hover:opacity-40 md:hidden"
>
<TmdbLogo className="h-2.5 w-auto" />
</a>
)}
</div> </div>
</div> </div>
{title.overview && ( {title.overview && <ExpandableText text={title.overview} />}
<p className="max-w-2xl break-words leading-relaxed text-muted-foreground">
{title.overview}
</p>
)}
{actions} {actions}
@@ -176,26 +238,41 @@ function StatusIcon({ status }: { status: string }) {
function AmbientGlow({ palette }: { palette: ColorPalette | null }) { function AmbientGlow({ palette }: { palette: ColorPalette | null }) {
if (!palette) return null; if (!palette) return null;
const glowColor = palette.vibrant ?? palette.darkMuted ?? palette.muted;
return ( return (
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[800px] overflow-hidden"> <>
{palette.vibrant && ( {/* Mobile: single full-bleed color wash that flows from the backdrop edge-to-edge */}
{glowColor && (
<div <div
className="absolute -left-32 top-16 h-[500px] w-[500px] rounded-full opacity-[0.07] blur-[120px]" className="pointer-events-none absolute top-0 right-[calc(-50vw+50%)] left-[calc(-50vw+50%)] -z-10 h-[600px] md:hidden"
style={{ background: palette.vibrant }} style={{
background: `radial-gradient(ellipse 100% 60% at 50% 0%, ${glowColor}18 0%, transparent 70%)`,
}}
/> />
)} )}
{palette.darkMuted && (
<div {/* Desktop: multi-orb ambient glow with room to breathe */}
className="absolute -right-24 top-48 h-[400px] w-[600px] rounded-full opacity-[0.05] blur-[140px]" <div className="pointer-events-none absolute inset-x-0 top-0 -z-10 hidden h-[800px] overflow-hidden md:block">
style={{ background: palette.darkMuted }} {palette.vibrant && (
/> <div
)} className="absolute top-16 -left-32 h-[500px] w-[500px] rounded-full opacity-[0.07] blur-[120px]"
{palette.muted && ( style={{ background: palette.vibrant }}
<div />
className="absolute left-1/3 top-[500px] h-[300px] w-[400px] rounded-full opacity-[0.04] blur-[100px]" )}
style={{ background: palette.muted }} {palette.darkMuted && (
/> <div
)} className="absolute top-48 -right-24 h-[400px] w-[600px] rounded-full opacity-[0.05] blur-[140px]"
</div> style={{ background: palette.darkMuted }}
/>
)}
{palette.muted && (
<div
className="absolute top-[500px] left-1/3 h-[300px] w-[400px] rounded-full opacity-[0.04] blur-[100px]"
style={{ background: palette.muted }}
/>
)}
</div>
</>
); );
} }
@@ -88,7 +88,7 @@ export function TitleSeasons({
<Button <Button
variant="ghost" variant="ghost"
size="xs" size="xs"
className="uppercase tracking-wider text-muted-foreground" className="text-muted-foreground uppercase tracking-wider"
> >
<IconChecks aria-hidden={true} className="size-3.5" /> <IconChecks aria-hidden={true} className="size-3.5" />
Mark All Watched Mark All Watched
@@ -167,9 +167,13 @@ export function TitleSeasons({
e.stopPropagation(); e.stopPropagation();
handleMarkSeason(season); handleMarkSeason(season);
}} }}
className="w-24 uppercase tracking-wider text-primary hover:bg-primary/10 hover:text-primary sm:hidden sm:group-hover/season:block" className="text-primary uppercase tracking-wider hover:bg-primary/10 hover:text-primary sm:hidden sm:w-24 sm:group-hover/season:block"
> >
Watch all <IconChecks
aria-hidden={true}
className="size-3.5 sm:hidden"
/>
<span className="hidden sm:inline">Watch all</span>
</Button> </Button>
)} )}
{totalCount > 0 && watchedCount === totalCount && ( {totalCount > 0 && watchedCount === totalCount && (
@@ -180,13 +184,17 @@ export function TitleSeasons({
e.stopPropagation(); e.stopPropagation();
handleUnmarkSeason(season); handleUnmarkSeason(season);
}} }}
className="w-24 uppercase tracking-wider text-muted-foreground hover:bg-destructive/10 hover:text-destructive sm:hidden sm:group-hover/season:block" className="text-muted-foreground uppercase tracking-wider hover:bg-destructive/10 hover:text-destructive sm:hidden sm:w-24 sm:group-hover/season:block"
> >
Unwatch all <IconChecks
aria-hidden={true}
className="size-3.5 sm:hidden"
/>
<span className="hidden sm:inline">Unwatch all</span>
</Button> </Button>
)} )}
{totalCount > 0 && ( {totalCount > 0 && (
<span className="font-mono text-xs tabular-nums text-muted-foreground"> <span className="font-mono text-muted-foreground text-xs tabular-nums">
{watchedCount}/{totalCount} {watchedCount}/{totalCount}
</span> </span>
)} )}
@@ -215,7 +223,7 @@ export function TitleSeasons({
stiffness: 300, stiffness: 300,
damping: 30, damping: 30,
}} }}
className="overflow-hidden border-t border-border/50" className="overflow-hidden border-border/50 border-t"
> >
{season.episodes.map((ep) => { {season.episodes.map((ep) => {
const isWatched = watchedSet.has(ep.id); const isWatched = watchedSet.has(ep.id);
@@ -223,72 +231,99 @@ export function TitleSeasons({
return ( return (
<div <div
key={ep.id} key={ep.id}
className={`flex gap-3 border-b border-border/30 px-4 py-3 last:border-b-0 transition-colors ${isWatched ? "opacity-60" : ""}`} className={`border-border/30 border-b transition-colors last:border-b-0 ${isWatched ? "opacity-60" : ""}`}
> >
<button {/* Mobile: still banner above episode info */}
type="button"
aria-label={`Mark episode ${ep.episodeNumber} as ${isWatched ? "unwatched" : "watched"}`}
onClick={() =>
handleWatchEpisode(
ep.id,
season.seasonNumber,
ep.episodeNumber,
isWatched,
)
}
disabled={watchingEp === ep.id}
className={`mt-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 transition-all ${
isWatched
? "border-primary bg-primary text-primary-foreground"
: "border-muted-foreground/40 bg-muted-foreground/5 hover:border-primary/70 hover:bg-primary/10"
}`}
>
{isWatched && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 500,
damping: 15,
}}
>
<IconCheck className="size-3.5" />
</motion.div>
)}
</button>
{stillPath && ( {stillPath && (
<div className="hidden h-14 w-24 shrink-0 overflow-hidden rounded-md bg-muted sm:block"> <div className="relative aspect-video w-full overflow-hidden bg-muted sm:hidden">
<Image <Image
src={stillPath} src={stillPath}
alt={ep.name ?? ""} alt={ep.name ?? ""}
width={300} width={600}
height={169} height={338}
className="h-full w-full object-cover" className="h-full w-full object-cover"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" />
<span className="absolute bottom-2 left-3 font-mono text-[10px] text-white/70">
E{String(ep.episodeNumber).padStart(2, "0")}
</span>
</div> </div>
)} )}
<div className="min-w-0 flex-1"> <div className="flex gap-3 px-4 py-3">
<p className="text-sm"> <button
<span className="font-mono text-xs text-muted-foreground"> type="button"
E{String(ep.episodeNumber).padStart(2, "0")} aria-label={`Mark episode ${ep.episodeNumber} as ${isWatched ? "unwatched" : "watched"}`}
</span>{" "} onClick={() =>
<span className="font-medium"> handleWatchEpisode(
{ep.name ?? "Untitled"} ep.id,
</span> season.seasonNumber,
</p> ep.episodeNumber,
<p className="text-xs text-muted-foreground"> isWatched,
{ep.airDate )
? format(parseISO(ep.airDate), "MMM d, yyyy") }
: ""} disabled={watchingEp === ep.id}
{ep.airDate && ep.runtimeMinutes ? " · " : ""} className={`mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 transition-all ${
{ep.runtimeMinutes ? `${ep.runtimeMinutes}m` : ""} isWatched
</p> ? "border-primary bg-primary text-primary-foreground"
{ep.overview && ( : "border-muted-foreground/40 bg-muted-foreground/5 hover:border-primary/70 hover:bg-primary/10"
<p className="mt-1 hidden line-clamp-2 text-xs leading-relaxed text-muted-foreground/70 sm:block"> }`}
{ep.overview} >
</p> {isWatched && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 500,
damping: 15,
}}
>
<IconCheck className="size-3.5" />
</motion.div>
)}
</button>
{/* Desktop: inline thumbnail */}
{stillPath && (
<div className="hidden h-14 w-24 shrink-0 overflow-hidden rounded-md bg-muted sm:block">
<Image
src={stillPath}
alt={ep.name ?? ""}
width={300}
height={169}
className="h-full w-full object-cover"
/>
</div>
)} )}
<div className="min-w-0 flex-1">
<p className="text-sm">
{/* Episode number shown inline on desktop, or mobile without still */}
<span
className={`font-mono text-muted-foreground text-xs ${stillPath ? "hidden sm:inline" : ""}`}
>
E{String(ep.episodeNumber).padStart(2, "0")}
</span>
{stillPath && (
<span className="hidden sm:inline"> </span>
)}
<span className="font-medium">
{ep.name ?? "Untitled"}
</span>
</p>
<p className="text-muted-foreground text-xs">
{ep.airDate
? format(parseISO(ep.airDate), "MMM d, yyyy")
: ""}
{ep.airDate && ep.runtimeMinutes ? " · " : ""}
{ep.runtimeMinutes
? `${ep.runtimeMinutes}m`
: ""}
</p>
{ep.overview && (
<p className="mt-1 line-clamp-2 text-muted-foreground/70 text-xs leading-relaxed">
{ep.overview}
</p>
)}
</div>
</div> </div>
</div> </div>
); );
@@ -36,13 +36,13 @@ export function TrailerDialog({
<button <button
type="button" type="button"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="inline-flex h-5 items-center gap-1 rounded border border-border/50 px-2 text-xs text-muted-foreground transition-colors hover:border-border hover:text-foreground" className="inline-flex h-5 items-center gap-1 rounded border border-border/50 px-2 text-muted-foreground text-xs transition-colors hover:border-border hover:text-foreground"
> >
<IconPlayerPlayFilled aria-hidden={true} className="h-2.5 w-2.5" /> <IconPlayerPlayFilled aria-hidden={true} className="h-2.5 w-2.5" />
Trailer Trailer
</button> </button>
)} )}
<DialogContent className="sm:max-w-4xl p-0 overflow-hidden bg-black border-white/10"> <DialogContent className="overflow-hidden border-white/10 bg-black p-0 sm:max-w-4xl">
<DialogTitle className="sr-only">Trailer</DialogTitle> <DialogTitle className="sr-only">Trailer</DialogTitle>
<div className="aspect-video w-full"> <div className="aspect-video w-full">
<MediaThemeSutro <MediaThemeSutro
+6 -6
View File
@@ -4,39 +4,39 @@ export default function TitleNotFound() {
return ( return (
<div className="flex flex-col items-center gap-6 py-24 text-center"> <div className="flex flex-col items-center gap-6 py-24 text-center">
<h1 <h1
className="animate-stagger-item font-display text-[6rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[8rem]" className="animate-stagger-item font-display text-[6rem] text-foreground/[0.06] leading-[0.85] tracking-tight sm:text-[8rem]"
style={{ "--stagger-index": 0 } as React.CSSProperties} style={{ "--stagger-index": 0 } as React.CSSProperties}
> >
404 404
</h1> </h1>
<div <div
className="animate-stagger-item -mt-4 space-y-2" className="-mt-4 animate-stagger-item space-y-2"
style={{ "--stagger-index": 1 } as React.CSSProperties} style={{ "--stagger-index": 1 } as React.CSSProperties}
> >
<h2 className="font-display text-2xl tracking-tight sm:text-3xl"> <h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Title not found Title not found
</h2> </h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground"> <p className="mx-auto max-w-sm text-muted-foreground text-sm leading-relaxed">
The title you&apos;re looking for doesn&apos;t exist or may have been The title you&apos;re looking for doesn&apos;t exist or may have been
removed from the database. removed from the database.
</p> </p>
</div> </div>
<div <div
className="animate-stagger-item flex items-center gap-3" className="flex animate-stagger-item items-center gap-3"
style={{ "--stagger-index": 2 } as React.CSSProperties} style={{ "--stagger-index": 2 } as React.CSSProperties}
> >
<Link <Link
href="/explore" href="/explore"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20" className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-5 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-lg hover:shadow-primary/20"
> >
<span className="relative z-10">Explore titles</span> <span className="relative z-10">Explore titles</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" /> <div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
</Link> </Link>
<Link <Link
href="/dashboard" href="/dashboard"
className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 text-sm font-medium transition-colors hover:border-primary/40 hover:bg-primary/5" className="inline-flex h-10 items-center justify-center rounded-lg border border-border px-5 font-medium text-sm transition-colors hover:border-primary/40 hover:bg-primary/5"
> >
Dashboard Dashboard
</Link> </Link>
+1 -1
View File
@@ -49,7 +49,7 @@ export default function RootLayout({
> >
<a <a
href="#main-content" href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-md" className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-primary-foreground"
> >
Skip to main content Skip to main content
</a> </a>
+9 -9
View File
@@ -5,11 +5,11 @@ export default function NotFound() {
return ( return (
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-6"> <div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-6">
{/* Warm projector glow */} {/* Warm projector glow */}
<div className="pointer-events-none absolute left-1/2 top-[40%] h-[500px] w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/4 blur-[160px]" /> <div className="pointer-events-none absolute top-[40%] left-1/2 h-[500px] w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/4 blur-[160px]" />
{/* Subtle vertical light beam */} {/* Subtle vertical light beam */}
<div <div
className="pointer-events-none absolute left-1/2 top-0 h-[60vh] w-px -translate-x-1/2" className="pointer-events-none absolute top-0 left-1/2 h-[60vh] w-px -translate-x-1/2"
style={{ style={{
background: background:
"linear-gradient(to bottom, oklch(0.8 0.14 65 / 0.12), transparent)", "linear-gradient(to bottom, oklch(0.8 0.14 65 / 0.12), transparent)",
@@ -19,7 +19,7 @@ export default function NotFound() {
<div className="relative z-10 flex flex-col items-center text-center"> <div className="relative z-10 flex flex-col items-center text-center">
{/* Frame counter label */} {/* Frame counter label */}
<p <p
className="animate-stagger-item font-mono text-[10px] tracking-[0.5em] uppercase text-muted-foreground/40" className="animate-stagger-item font-mono text-[10px] text-muted-foreground/40 uppercase tracking-[0.5em]"
style={{ "--stagger-index": 0 } as React.CSSProperties} style={{ "--stagger-index": 0 } as React.CSSProperties}
> >
Scene not found Scene not found
@@ -27,7 +27,7 @@ export default function NotFound() {
{/* Large ghosted 404 */} {/* Large ghosted 404 */}
<h1 <h1
className="animate-stagger-item font-display text-[8rem] leading-[0.85] tracking-tight text-foreground/[0.06] sm:text-[11rem] md:text-[14rem]" className="animate-stagger-item font-display text-[8rem] text-foreground/[0.06] leading-[0.85] tracking-tight sm:text-[11rem] md:text-[14rem]"
style={{ "--stagger-index": 1 } as React.CSSProperties} style={{ "--stagger-index": 1 } as React.CSSProperties}
> >
404 404
@@ -35,13 +35,13 @@ export default function NotFound() {
{/* Message */} {/* Message */}
<div <div
className="animate-stagger-item -mt-2 space-y-3 sm:-mt-4" className="-mt-2 animate-stagger-item space-y-3 sm:-mt-4"
style={{ "--stagger-index": 2 } as React.CSSProperties} style={{ "--stagger-index": 2 } as React.CSSProperties}
> >
<h2 className="font-display text-2xl tracking-tight sm:text-3xl"> <h2 className="font-display text-2xl tracking-tight sm:text-3xl">
Lost in the credits Lost in the credits
</h2> </h2>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground"> <p className="mx-auto max-w-sm text-muted-foreground text-sm leading-relaxed">
This page was left on the cutting room floor. It may have been This page was left on the cutting room floor. It may have been
moved, removed, or never made it past the screenplay. moved, removed, or never made it past the screenplay.
</p> </p>
@@ -49,12 +49,12 @@ export default function NotFound() {
{/* Actions */} {/* Actions */}
<div <div
className="animate-stagger-item mt-8 flex items-center gap-3" className="mt-8 flex animate-stagger-item items-center gap-3"
style={{ "--stagger-index": 3 } as React.CSSProperties} style={{ "--stagger-index": 3 } as React.CSSProperties}
> >
<Link <Link
href="/" href="/"
className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-6 text-sm font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20" className="group relative inline-flex h-10 items-center justify-center overflow-hidden rounded-lg bg-primary px-6 font-medium text-primary-foreground text-sm transition-shadow hover:shadow-lg hover:shadow-primary/20"
> >
<span className="relative z-10">Return home</span> <span className="relative z-10">Return home</span>
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" /> <div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
@@ -63,7 +63,7 @@ export default function NotFound() {
{/* Logo watermark */} {/* Logo watermark */}
<div <div
className="animate-stagger-item mt-16 text-muted-foreground/20" className="mt-16 animate-stagger-item text-muted-foreground/20"
style={{ "--stagger-index": 4 } as React.CSSProperties} style={{ "--stagger-index": 4 } as React.CSSProperties}
> >
<SofaLogo className="size-8" /> <SofaLogo className="size-8" />
+16 -4
View File
@@ -22,15 +22,27 @@
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"domains": {
"next": "recommended",
"react": "recommended"
},
"rules": { "rules": {
"recommended": true, "recommended": true,
"suspicious": { "suspicious": {
"noUnknownAtRules": "off" "noUnknownAtRules": "off"
},
"nursery": {
"useSortedClasses": {
"level": "error",
"options": {
"attributes": ["classList"],
"functions": ["cn", "clsx", "cva"]
}
},
"noFloatingPromises": "error",
"noMisusedPromises": "error",
"useDestructuring": "error"
} }
},
"domains": {
"next": "recommended",
"react": "recommended"
} }
}, },
"assist": { "assist": {
+7 -7
View File
@@ -107,10 +107,10 @@ export function AuthForm({
<Link href="/" className="inline-flex justify-center text-primary"> <Link href="/" className="inline-flex justify-center text-primary">
<SofaLogo className="size-9" /> <SofaLogo className="size-9" />
</Link> </Link>
<h1 className="text-lg font-medium text-balance"> <h1 className="text-balance font-medium text-lg">
{isRegister ? "Create your account" : "Welcome back"} {isRegister ? "Create your account" : "Welcome back"}
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
{isRegister ? "Start tracking your watches" : "Sign in to continue"} {isRegister ? "Start tracking your watches" : "Sign in to continue"}
</p> </p>
</div> </div>
@@ -144,7 +144,7 @@ export function AuthForm({
{showOidc && showPasswordForm && ( {showOidc && showPasswordForm && (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-px flex-1 bg-border/50" /> <div className="h-px flex-1 bg-border/50" />
<span className="text-xs text-muted-foreground">or</span> <span className="text-muted-foreground text-xs">or</span>
<div className="h-px flex-1 bg-border/50" /> <div className="h-px flex-1 bg-border/50" />
</div> </div>
)} )}
@@ -164,7 +164,7 @@ export function AuthForm({
<motion.div variants={fieldVariants} className="space-y-1.5"> <motion.div variants={fieldVariants} className="space-y-1.5">
<Label <Label
htmlFor="name" htmlFor="name"
className="uppercase tracking-wider text-muted-foreground" className="text-muted-foreground uppercase tracking-wider"
> >
Name Name
</Label> </Label>
@@ -184,7 +184,7 @@ export function AuthForm({
<motion.div variants={fieldVariants} className="space-y-1.5"> <motion.div variants={fieldVariants} className="space-y-1.5">
<Label <Label
htmlFor="email" htmlFor="email"
className="uppercase tracking-wider text-muted-foreground" className="text-muted-foreground uppercase tracking-wider"
> >
Email Email
</Label> </Label>
@@ -204,7 +204,7 @@ export function AuthForm({
<motion.div variants={fieldVariants} className="space-y-1.5"> <motion.div variants={fieldVariants} className="space-y-1.5">
<Label <Label
htmlFor="password" htmlFor="password"
className="uppercase tracking-wider text-muted-foreground" className="text-muted-foreground uppercase tracking-wider"
> >
Password Password
</Label> </Label>
@@ -258,7 +258,7 @@ export function AuthForm({
{showPasswordForm && {showPasswordForm &&
(isRegister || authConfig?.registrationOpen !== false) && ( (isRegister || authConfig?.registrationOpen !== false) && (
<p className="text-center text-sm text-muted-foreground"> <p className="text-center text-muted-foreground text-sm">
{isRegister ? ( {isRegister ? (
<> <>
Already have an account?{" "} Already have an account?{" "}
+4 -4
View File
@@ -227,7 +227,7 @@ export function CommandPalette() {
</div> </div>
)} )}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-xs font-medium"> <p className="truncate font-medium text-xs">
{r.title} {r.title}
</p> </p>
<div className="flex items-center gap-1.5 text-[10px] text-muted-foreground"> <div className="flex items-center gap-1.5 text-[10px] text-muted-foreground">
@@ -275,7 +275,7 @@ export function CommandPalette() {
<button <button
type="button" type="button"
onClick={handleClearRecent} onClick={handleClearRecent}
className="text-[10px] font-normal text-muted-foreground transition-colors hover:text-foreground" className="font-normal text-[10px] text-muted-foreground transition-colors hover:text-foreground"
> >
Clear all Clear all
</button> </button>
@@ -363,7 +363,7 @@ export function CommandPalette() {
<div className="space-y-5 py-2"> <div className="space-y-5 py-2">
{Object.entries(groupedShortcuts).map(([scope, items]) => ( {Object.entries(groupedShortcuts).map(([scope, items]) => (
<div key={scope} className="space-y-2"> <div key={scope} className="space-y-2">
<h3 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground"> <h3 className="font-semibold text-[10px] text-muted-foreground uppercase tracking-wider">
{scope} {scope}
</h3> </h3>
<div className="space-y-1"> <div className="space-y-1">
@@ -372,7 +372,7 @@ export function CommandPalette() {
key={item.description} key={item.description}
className="flex items-center justify-between rounded-md px-2 py-1.5" className="flex items-center justify-between rounded-md px-2 py-1.5"
> >
<span className="text-xs text-foreground"> <span className="text-foreground text-xs">
{item.description} {item.description}
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
+57
View File
@@ -0,0 +1,57 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
interface ExpandableTextProps {
text: string;
/** Tailwind line-clamp class applied when collapsed (default: "line-clamp-3") */
clampClass?: string;
className?: string;
textClassName?: string;
}
export function ExpandableText({
text,
clampClass = "line-clamp-3",
className,
textClassName,
}: ExpandableTextProps) {
const [expanded, setExpanded] = useState(false);
const [clamped, setClamped] = useState(false);
const ref = useRef<HTMLParagraphElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const check = () => setClamped(el.scrollHeight > el.clientHeight + 1);
check();
const observer = new ResizeObserver(check);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<div className={className}>
<p
ref={ref}
className={cn(
"break-words text-muted-foreground leading-relaxed",
!expanded && clampClass,
textClassName,
)}
>
{text}
</p>
{(clamped || expanded) && (
<button
type="button"
onClick={() => setExpanded(!expanded)}
className="mt-1 font-medium text-primary text-xs transition-colors hover:text-primary/80"
>
{expanded ? "Show less" : "Read more"}
</button>
)}
</div>
);
}
+4 -4
View File
@@ -81,7 +81,7 @@ export function LandingPage({
{/* Warm primary glow */} {/* Warm primary glow */}
<motion.div <motion.div
className="pointer-events-none absolute left-1/2 top-1/3 h-[600px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/5 blur-[120px]" className="pointer-events-none absolute top-1/3 left-1/2 h-[600px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary/5 blur-[120px]"
animate={{ opacity: [0.4, 0.7, 0.4] }} animate={{ opacity: [0.4, 0.7, 0.4] }}
transition={{ transition={{
duration: 6, duration: 6,
@@ -93,7 +93,7 @@ export function LandingPage({
<main className="relative z-10 flex flex-col items-center gap-10 px-6 text-center"> <main className="relative z-10 flex flex-col items-center gap-10 px-6 text-center">
<div className="space-y-4"> <div className="space-y-4">
<motion.p <motion.p
className="text-sm font-medium uppercase tracking-[0.3em] text-primary" className="font-medium text-primary text-sm uppercase tracking-[0.3em]"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ transition={{
@@ -118,7 +118,7 @@ export function LandingPage({
<SofaLogo className="size-24 sm:size-28 md:size-32" /> <SofaLogo className="size-24 sm:size-28 md:size-32" />
</motion.div> </motion.div>
<motion.p <motion.p
className="mx-auto max-w-md text-lg leading-relaxed text-muted-foreground" className="mx-auto max-w-md text-lg text-muted-foreground leading-relaxed"
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ transition={{
@@ -176,7 +176,7 @@ export function LandingPage({
</main> </main>
{/* Bottom fade */} {/* Bottom fade */}
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-background to-transparent" /> <div className="pointer-events-none absolute right-0 bottom-0 left-0 h-32 bg-gradient-to-t from-background to-transparent" />
</div> </div>
); );
} }
+2 -2
View File
@@ -15,7 +15,7 @@ export function MobileTabBar() {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-border/50 bg-background/90 pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)] backdrop-blur-xl sm:hidden"> <nav className="fixed right-0 bottom-0 left-0 z-50 border-border/50 border-t bg-background/90 pr-[env(safe-area-inset-right)] pl-[env(safe-area-inset-left)] backdrop-blur-xl sm:hidden">
<div className="flex h-14 items-stretch"> <div className="flex h-14 items-stretch">
{tabs.map((tab) => { {tabs.map((tab) => {
const Icon = tab.icon; const Icon = tab.icon;
@@ -33,7 +33,7 @@ export function MobileTabBar() {
className={`size-5 ${isActive ? "text-primary" : "text-muted-foreground"}`} className={`size-5 ${isActive ? "text-primary" : "text-muted-foreground"}`}
/> />
<span <span
className={`text-[10px] font-medium ${isActive ? "text-primary" : "text-muted-foreground"}`} className={`font-medium text-[10px] ${isActive ? "text-primary" : "text-muted-foreground"}`}
> >
{tab.label} {tab.label}
</span> </span>
+6 -6
View File
@@ -27,8 +27,8 @@ export function NavBar({ userName }: { userName: string }) {
const setCommandPaletteOpen = useSetAtom(commandPaletteOpenAtom); const setCommandPaletteOpen = useSetAtom(commandPaletteOpenAtom);
return ( return (
<header className="sticky top-0 z-50 border-b border-border/50 bg-background/80 pt-[env(safe-area-inset-top)] backdrop-blur-xl"> <header className="sticky top-0 z-50 border-border/50 border-b bg-background/80 pt-[env(safe-area-inset-top)] backdrop-blur-xl">
<nav className="mx-auto flex h-14 max-w-6xl items-center justify-between gap-5 pl-[max(1rem,env(safe-area-inset-left))] pr-[max(1rem,env(safe-area-inset-right))] sm:gap-0 sm:pl-[max(1.5rem,env(safe-area-inset-left))] sm:pr-[max(1.5rem,env(safe-area-inset-right))]"> <nav className="mx-auto flex h-14 max-w-6xl items-center justify-between gap-5 pr-[max(1rem,env(safe-area-inset-right))] pl-[max(1rem,env(safe-area-inset-left))] sm:gap-0 sm:pr-[max(1.5rem,env(safe-area-inset-right))] sm:pl-[max(1.5rem,env(safe-area-inset-left))]">
<div className="flex items-center gap-3 sm:gap-6"> <div className="flex items-center gap-3 sm:gap-6">
<Link <Link
href="/dashboard" href="/dashboard"
@@ -46,7 +46,7 @@ export function NavBar({ userName }: { userName: string }) {
<Link <Link
key={link.href} key={link.href}
href={link.href} href={link.href}
className="relative inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground" className="relative inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground"
> >
{link.label} {link.label}
{isActive && ( {isActive && (
@@ -71,7 +71,7 @@ export function NavBar({ userName }: { userName: string }) {
<button <button
type="button" type="button"
onClick={() => setCommandPaletteOpen(true)} onClick={() => setCommandPaletteOpen(true)}
className="flex flex-1 items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:border-primary/20 hover:bg-card sm:hidden" className="flex flex-1 items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-muted-foreground text-sm transition-colors hover:border-primary/20 hover:bg-card sm:hidden"
> >
<IconSearch aria-hidden={true} className="size-3.5" /> <IconSearch aria-hidden={true} className="size-3.5" />
<span>Search…</span> <span>Search…</span>
@@ -80,7 +80,7 @@ export function NavBar({ userName }: { userName: string }) {
<button <button
type="button" type="button"
onClick={() => setCommandPaletteOpen(true)} onClick={() => setCommandPaletteOpen(true)}
className="hidden items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:border-primary/20 hover:bg-card sm:inline-flex" className="hidden items-center gap-2 rounded-lg border border-border/50 bg-card/50 px-3 py-1.5 text-muted-foreground text-sm transition-colors hover:border-primary/20 hover:bg-card sm:inline-flex"
> >
<IconSearch aria-hidden={true} className="size-3.5" /> <IconSearch aria-hidden={true} className="size-3.5" />
<span>Search…</span> <span>Search…</span>
@@ -93,7 +93,7 @@ export function NavBar({ userName }: { userName: string }) {
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={<Link href="/settings" />} render={<Link href="/settings" />}
className="hidden items-center gap-1.5 rounded-md px-2 py-1.5 text-sm leading-none text-muted-foreground transition-colors hover:text-foreground sm:inline-flex" className="hidden items-center gap-1.5 rounded-md px-2 py-1.5 text-muted-foreground text-sm leading-none transition-colors hover:text-foreground sm:inline-flex"
> >
{userName} {userName}
<IconSettings aria-hidden={true} className="size-3.5" /> <IconSettings aria-hidden={true} className="size-3.5" />
+2 -2
View File
@@ -13,7 +13,7 @@ export function TitleCardSkeleton() {
return ( return (
<div className="overflow-hidden rounded-xl bg-card ring-1 ring-white/[0.06]"> <div className="overflow-hidden rounded-xl bg-card ring-1 ring-white/[0.06]">
<Skeleton className="aspect-[2/3] w-full rounded-none" /> <Skeleton className="aspect-[2/3] w-full rounded-none" />
<div className="px-3 pb-3 pt-2.5"> <div className="px-3 pt-2.5 pb-3">
<Skeleton className="h-4 w-3/4" /> <Skeleton className="h-4 w-3/4" />
<Skeleton className="mt-1.5 h-3 w-1/2" /> <Skeleton className="mt-1.5 h-3 w-1/2" />
</div> </div>
@@ -174,7 +174,7 @@ export function RecommendationsSkeleton() {
export function TitleDetailSkeleton() { export function TitleDetailSkeleton() {
return ( return (
<div className="space-y-10"> <div className="space-y-10">
<Skeleton className="-mt-6 ml-[calc(-50vw+50%)] mr-[calc(-50vw+50%)] h-80 rounded-none sm:h-[28rem]" /> <Skeleton className="-mt-6 mr-[calc(-50vw+50%)] ml-[calc(-50vw+50%)] h-80 rounded-none sm:h-[28rem]" />
<div className="flex flex-row gap-4 sm:gap-8"> <div className="flex flex-row gap-4 sm:gap-8">
<Skeleton className="h-[180px] w-[120px] shrink-0 rounded-xl sm:h-[330px] sm:w-[220px]" /> <Skeleton className="h-[180px] w-[120px] shrink-0 rounded-xl sm:h-[330px] sm:w-[220px]" />
<div className="flex-1 space-y-5"> <div className="flex-1 space-y-5">
+10 -10
View File
@@ -112,7 +112,7 @@ function QuickAddButton({
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
className="absolute top-2 right-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 backdrop-blur-sm text-white cursor-default" className="absolute top-2 right-2 z-10 flex size-8 cursor-default items-center justify-center rounded-full bg-black/50 text-white backdrop-blur-sm"
render={<div />} render={<div />}
> >
<StatusIcon className="size-4" /> <StatusIcon className="size-4" />
@@ -126,7 +126,7 @@ function QuickAddButton({
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
onClick={handleClick} onClick={handleClick}
className="absolute top-2 right-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 backdrop-blur-sm text-white opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 hover:bg-black/70" className="absolute top-2 right-2 z-10 flex size-8 items-center justify-center rounded-full bg-black/50 text-white opacity-60 backdrop-blur-sm transition-opacity hover:bg-black/70 focus-visible:opacity-100 sm:opacity-0 sm:group-hover:opacity-100"
render={<button type="button" />} render={<button type="button" />}
> >
{state === "idle" && <IconPlus className="size-4" />} {state === "idle" && <IconPlus className="size-4" />}
@@ -145,7 +145,7 @@ function ProgressBar({ watched, total }: { watched: number; total: number }) {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
className="absolute bottom-0 left-0 right-0 z-10 h-1 bg-white/10 cursor-default" className="absolute right-0 bottom-0 left-0 z-10 h-1 cursor-default bg-white/10"
render={<div />} render={<div />}
> >
<div <div
@@ -179,7 +179,7 @@ function CardInner({
return ( return (
<div <div
className={`relative overflow-hidden rounded-xl bg-card ring-1 transition-[box-shadow,ring-color] duration-200 ease-out hover:ring-primary/25 hover:shadow-lg hover:shadow-primary/5 ${ringClass}`} className={`relative overflow-hidden rounded-xl bg-card ring-1 transition-[box-shadow,ring-color] duration-200 ease-out hover:shadow-lg hover:shadow-primary/5 hover:ring-primary/25 ${ringClass}`}
> >
<div className="aspect-[2/3] overflow-hidden bg-card"> <div className="aspect-[2/3] overflow-hidden bg-card">
{posterPath ? ( {posterPath ? (
@@ -202,7 +202,7 @@ function CardInner({
/> />
<div className="absolute inset-0 bg-gradient-to-t from-primary/10 via-transparent to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-primary/10 via-transparent to-transparent" />
<div className="relative px-3 text-center"> <div className="relative px-3 text-center">
<p className="font-display text-sm leading-snug tracking-tight text-foreground/70"> <p className="font-display text-foreground/70 text-sm leading-snug tracking-tight">
{title} {title}
</p> </p>
</div> </div>
@@ -220,12 +220,12 @@ function CardInner({
)} )}
</div> </div>
<div className="px-3 pb-3 pt-2.5"> <div className="px-3 pt-2.5 pb-3">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{userStatus && ( {userStatus && (
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
className="cursor-default shrink-0" className="shrink-0 cursor-default"
render={<div className="flex items-center" />} render={<div className="flex items-center" />}
> >
<span className="relative flex size-2"> <span className="relative flex size-2">
@@ -240,11 +240,11 @@ function CardInner({
<TooltipContent>{statusConfig[userStatus].label}</TooltipContent> <TooltipContent>{statusConfig[userStatus].label}</TooltipContent>
</Tooltip> </Tooltip>
)} )}
<p className="line-clamp-1 text-sm font-medium leading-snug"> <p className="line-clamp-1 font-medium text-sm leading-snug">
{title} {title}
</p> </p>
</div> </div>
<div className="mt-1.5 flex items-center gap-2 text-xs text-muted-foreground"> <div className="mt-1.5 flex items-center gap-2 text-muted-foreground text-xs">
<TypeIcon <TypeIcon
aria-hidden={true} aria-hidden={true}
className="size-3.5 shrink-0 text-primary/60" className="size-3.5 shrink-0 text-primary/60"
@@ -283,7 +283,7 @@ export function TitleCard({
const href = id ? `/titles/${id}` : `/titles/tmdb-${tmdbId}-${type}`; const href = id ? `/titles/${id}` : `/titles/tmdb-${tmdbId}-${type}`;
const tilt = useTiltEffect(); const tilt = useTiltEffect();
return ( return (
<div className="relative group"> <div className="group relative">
<QuickAddButton <QuickAddButton
tmdbId={tmdbId} tmdbId={tmdbId}
type={type as "movie" | "tv"} type={type as "movie" | "tv"}
+4 -4
View File
@@ -21,7 +21,7 @@ function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return ( return (
<AccordionPrimitive.Item <AccordionPrimitive.Item
data-slot="accordion-item" data-slot="accordion-item"
className={cn("data-open:bg-muted/50 not-last:border-b", className)} className={cn("not-last:border-b data-open:bg-muted/50", className)}
{...props} {...props}
/> />
); );
@@ -37,7 +37,7 @@ function AccordionTrigger({
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
data-slot="accordion-trigger" data-slot="accordion-trigger"
className={cn( className={cn(
"**:data-[slot=accordion-trigger-icon]:text-muted-foreground group/accordion-trigger relative flex flex-1 items-start justify-between gap-6 border border-transparent p-2 text-left text-xs/relaxed font-medium transition-all outline-none hover:underline aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4", "group/accordion-trigger relative flex flex-1 items-start justify-between gap-6 border border-transparent p-2 text-left font-medium text-xs/relaxed outline-none transition-all hover:underline aria-disabled:pointer-events-none aria-disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
className, className,
)} )}
{...props} {...props}
@@ -64,12 +64,12 @@ function AccordionContent({
return ( return (
<AccordionPrimitive.Panel <AccordionPrimitive.Panel
data-slot="accordion-content" data-slot="accordion-content"
className="data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden px-2 text-xs/relaxed" className="overflow-hidden px-2 text-xs/relaxed data-closed:animate-accordion-up data-open:animate-accordion-down"
{...props} {...props}
> >
<div <div
className={cn( className={cn(
"[&_a]:hover:text-foreground h-(--accordion-panel-height) pt-0 pb-4 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4", "h-(--accordion-panel-height) pt-0 pb-4 data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className, className,
)} )}
> >
+5 -5
View File
@@ -29,7 +29,7 @@ function AlertDialogOverlay({
<AlertDialogPrimitive.Backdrop <AlertDialogPrimitive.Backdrop
data-slot="alert-dialog-overlay" data-slot="alert-dialog-overlay"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs", "data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/80 duration-100 data-closed:animate-out data-open:animate-in supports-backdrop-filter:backdrop-blur-xs",
className, className,
)} )}
{...props} {...props}
@@ -51,7 +51,7 @@ function AlertDialogContent({
data-slot="alert-dialog-content" data-slot="alert-dialog-content"
data-size={size} data-size={size}
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl p-4 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-64 data-[size=default]:sm:max-w-sm", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-3 rounded-xl bg-background p-4 outline-none ring-1 ring-foreground/10 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-64 data-closed:animate-out data-open:animate-in data-[size=default]:sm:max-w-sm",
className, className,
)} )}
{...props} {...props}
@@ -100,7 +100,7 @@ function AlertDialogMedia({
<div <div
data-slot="alert-dialog-media" data-slot="alert-dialog-media"
className={cn( className={cn(
"bg-muted mb-2 inline-flex size-8 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-4", "mb-2 inline-flex size-8 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-4",
className, className,
)} )}
{...props} {...props}
@@ -116,7 +116,7 @@ function AlertDialogTitle({
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
data-slot="alert-dialog-title" data-slot="alert-dialog-title"
className={cn( className={cn(
"text-sm font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", "font-medium text-sm sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
className, className,
)} )}
{...props} {...props}
@@ -132,7 +132,7 @@ function AlertDialogDescription({
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
data-slot="alert-dialog-description" data-slot="alert-dialog-description"
className={cn( className={cn(
"text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", "text-balance text-muted-foreground text-xs/relaxed md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className, className,
)} )}
{...props} {...props}
+4 -4
View File
@@ -4,13 +4,13 @@ import type * as React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const alertVariants = cva( const alertVariants = cva(
"grid gap-0.5 rounded-lg border px-2 py-1.5 text-left text-xs/relaxed has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-3.5 w-full relative group/alert", "group/alert relative grid w-full gap-0.5 rounded-lg border px-2 py-1.5 text-left text-xs/relaxed has-data-[slot=alert-action]:relative has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-1.5 has-data-[slot=alert-action]:pr-18 *:[svg:not([class*='size-'])]:size-3.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-card text-card-foreground", default: "bg-card text-card-foreground",
destructive: destructive:
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -39,7 +39,7 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="alert-title" data-slot="alert-title"
className={cn( className={cn(
"[&_a]:hover:text-foreground font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3", "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
className, className,
)} )}
{...props} {...props}
@@ -55,7 +55,7 @@ function AlertDescription({
<div <div
data-slot="alert-description" data-slot="alert-description"
className={cn( className={cn(
"text-muted-foreground [&_a]:hover:text-foreground text-xs/relaxed text-balance md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4", "text-balance text-muted-foreground text-xs/relaxed md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
className, className,
)} )}
{...props} {...props}
+5 -5
View File
@@ -17,7 +17,7 @@ function Avatar({
data-slot="avatar" data-slot="avatar"
data-size={size} data-size={size}
className={cn( className={cn(
"after:border-border group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten", "group/avatar relative flex size-8 shrink-0 select-none rounded-full after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
className, className,
)} )}
{...props} {...props}
@@ -46,7 +46,7 @@ function AvatarFallback({
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
data-slot="avatar-fallback" data-slot="avatar-fallback"
className={cn( className={cn(
"bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs", "flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground text-sm group-data-[size=sm]/avatar:text-xs",
className, className,
)} )}
{...props} {...props}
@@ -59,7 +59,7 @@ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
<span <span
data-slot="avatar-badge" data-slot="avatar-badge"
className={cn( className={cn(
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none", "absolute right-0 bottom-0 z-10 inline-flex select-none items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
@@ -75,7 +75,7 @@ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="avatar-group" data-slot="avatar-group"
className={cn( className={cn(
"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2", "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
className, className,
)} )}
{...props} {...props}
@@ -91,7 +91,7 @@ function AvatarGroupCount({
<div <div
data-slot="avatar-group-count" data-slot="avatar-group-count"
className={cn( className={cn(
"bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-xs/relaxed ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3", "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground text-xs/relaxed ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className, className,
)} )}
{...props} {...props}
+3 -3
View File
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const badgeVariants = cva( const badgeVariants = cva(
"h-5 gap-1 rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-2.5! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>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 overflow-hidden group/badge", "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border border-transparent px-2 py-0.5 font-medium text-[0.625rem] transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-2.5!",
{ {
variants: { variants: {
variant: { variant: {
@@ -13,9 +13,9 @@ const badgeVariants = cva(
secondary: secondary:
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
destructive: destructive:
"bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
outline: outline:
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground bg-input/20 dark:bg-input/30", "border-border bg-input/20 text-foreground dark:bg-input/30 [a]:hover:bg-muted [a]:hover:text-muted-foreground",
ghost: ghost:
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
+3 -3
View File
@@ -20,7 +20,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
<ol <ol
data-slot="breadcrumb-list" data-slot="breadcrumb-list"
className={cn( className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-xs/relaxed wrap-break-word", "wrap-break-word flex flex-wrap items-center gap-1.5 text-muted-foreground text-xs/relaxed",
className, className,
)} )}
{...props} {...props}
@@ -47,7 +47,7 @@ function BreadcrumbLink({
defaultTagName: "a", defaultTagName: "a",
props: mergeProps<"a">( props: mergeProps<"a">(
{ {
className: cn("hover:text-foreground transition-colors", className), className: cn("transition-colors hover:text-foreground", className),
}, },
props, props,
), ),
@@ -67,7 +67,7 @@ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
role="link" role="link"
aria-disabled="true" aria-disabled="true"
aria-current="page" aria-current="page"
className={cn("text-foreground font-normal", className)} className={cn("font-normal text-foreground", className)}
{...props} {...props}
/> />
); );
+5 -5
View File
@@ -5,14 +5,14 @@ import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonGroupVariants = cva( const buttonGroupVariants = cva(
"has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md flex w-fit items-stretch *:focus-visible:z-10 *:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{ {
variants: { variants: {
orientation: { orientation: {
horizontal: horizontal:
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0 *:data-slot:rounded-r-none", "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
vertical: vertical:
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 *:data-slot:rounded-b-none", "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -48,7 +48,7 @@ function ButtonGroupText({
props: mergeProps<"div">( props: mergeProps<"div">(
{ {
className: cn( className: cn(
"bg-muted gap-2 rounded-md border px-2.5 text-xs/relaxed font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", "flex items-center gap-2 rounded-md border bg-muted px-2.5 font-medium text-xs/relaxed [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className, className,
), ),
}, },
@@ -71,7 +71,7 @@ function ButtonGroupSeparator({
data-slot="button-group-separator" data-slot="button-group-separator"
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto", "relative self-stretch bg-input data-horizontal:mx-px data-vertical:my-px data-vertical:h-auto data-horizontal:w-auto",
className, className,
)} )}
{...props} {...props}
+4 -4
View File
@@ -6,19 +6,19 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium focus-visible:ring-2 aria-invalid:ring-2 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", "group/button inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md border border-transparent bg-clip-padding font-medium text-xs/relaxed outline-none transition-colors focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80", default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline: outline:
"border-border dark:bg-input/30 hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground", "border-border hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30",
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost: ghost:
"hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive: destructive:
"bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 dark:hover:bg-destructive/30",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
+19 -19
View File
@@ -34,7 +34,7 @@ function Calendar({
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
className={cn( className={cn(
"bg-background group/calendar p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(6)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent", "group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-3 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(6)]",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className, className,
@@ -49,30 +49,30 @@ function Calendar({
classNames={{ classNames={{
root: cn("w-fit", defaultClassNames.root), root: cn("w-fit", defaultClassNames.root),
months: cn( months: cn(
"flex gap-4 flex-col md:flex-row relative", "relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months, defaultClassNames.months,
), ),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month), month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn( nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav, defaultClassNames.nav,
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous, defaultClassNames.button_previous,
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none", "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next, defaultClassNames.button_next,
), ),
month_caption: cn( month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)", "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
defaultClassNames.month_caption, defaultClassNames.month_caption,
), ),
dropdowns: cn( dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5", "flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm",
defaultClassNames.dropdowns, defaultClassNames.dropdowns,
), ),
dropdown_root: cn( dropdown_root: cn(
@@ -80,49 +80,49 @@ function Calendar({
defaultClassNames.dropdown_root, defaultClassNames.dropdown_root,
), ),
dropdown: cn( dropdown: cn(
"absolute bg-popover inset-0 opacity-0", "absolute inset-0 bg-popover opacity-0",
defaultClassNames.dropdown, defaultClassNames.dropdown,
), ),
caption_label: cn( caption_label: cn(
"select-none font-medium", "select-none font-medium",
captionLayout === "label" captionLayout === "label"
? "text-sm" ? "text-sm"
: "rounded-(--cell-radius) flex items-center gap-1 text-sm [&>svg]:text-muted-foreground [&>svg]:size-3.5", : "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
defaultClassNames.caption_label, defaultClassNames.caption_label,
), ),
table: "w-full border-collapse", table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays), weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn( weekday: cn(
"text-muted-foreground rounded-(--cell-radius) flex-1 font-normal text-[0.8rem] select-none", "flex-1 select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground",
defaultClassNames.weekday, defaultClassNames.weekday,
), ),
week: cn("flex w-full mt-2", defaultClassNames.week), week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn( week_number_header: cn(
"select-none w-(--cell-size)", "w-(--cell-size) select-none",
defaultClassNames.week_number_header, defaultClassNames.week_number_header,
), ),
week_number: cn( week_number: cn(
"text-[0.8rem] select-none text-muted-foreground", "select-none text-[0.8rem] text-muted-foreground",
defaultClassNames.week_number, defaultClassNames.week_number,
), ),
day: cn( day: cn(
"relative w-full rounded-(--cell-radius) h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius) group/day aspect-square select-none", "group/day relative aspect-square h-full w-full select-none rounded-(--cell-radius) p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
props.showWeekNumber props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
defaultClassNames.day, defaultClassNames.day,
), ),
range_start: cn( range_start: cn(
"rounded-l-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:right-0 z-0 isolate", "relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
defaultClassNames.range_start, defaultClassNames.range_start,
), ),
range_middle: cn("rounded-none", defaultClassNames.range_middle), range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn( range_end: cn(
"rounded-r-(--cell-radius) bg-muted relative after:bg-muted after:absolute after:inset-y-0 after:w-4 after:left-0 z-0 isolate", "relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted",
defaultClassNames.range_end, defaultClassNames.range_end,
), ),
today: cn( today: cn(
"bg-muted text-foreground rounded-(--cell-radius) data-[selected=true]:rounded-none", "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none",
defaultClassNames.today, defaultClassNames.today,
), ),
outside: cn( outside: cn(
@@ -215,7 +215,7 @@ function CalendarDayButton({
data-range-end={modifiers.range_end} data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle} data-range-middle={modifiers.range_middle}
className={cn( className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) [&>span]:text-xs [&>span]:opacity-70", "relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 font-normal leading-none data-[range-end=true]:rounded-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-end=true]:bg-primary data-[range-middle=true]:bg-muted data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day, defaultClassNames.day,
className, className,
)} )}
+3 -3
View File
@@ -12,7 +12,7 @@ function Card({
data-slot="card" data-slot="card"
data-size={size} data-size={size}
className={cn( className={cn(
"ring-foreground/10 bg-card text-card-foreground group/card flex flex-col gap-4 overflow-hidden rounded-lg py-4 text-xs/relaxed ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg", "group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-card-foreground text-xs/relaxed ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",
className, className,
)} )}
{...props} {...props}
@@ -25,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="card-header" data-slot="card-header"
className={cn( className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3", "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className, className,
)} )}
{...props} {...props}
@@ -37,7 +37,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="card-title" data-slot="card-title"
className={cn("text-sm font-medium", className)} className={cn("font-medium text-sm", className)}
{...props} {...props}
/> />
); );
+5 -5
View File
@@ -55,7 +55,7 @@ function ChartContainer({
data-slot="chart" data-slot="chart"
data-chart={chartId} data-chart={chartId}
className={cn( className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden", "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
className, className,
)} )}
{...props} {...props}
@@ -177,7 +177,7 @@ function ChartTooltipContent({
return ( return (
<div <div
className={cn( className={cn(
"border-border/50 bg-background grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs/relaxed shadow-xl", "grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs/relaxed shadow-xl",
className, className,
)} )}
> >
@@ -194,7 +194,7 @@ function ChartTooltipContent({
<div <div
key={item.dataKey} key={item.dataKey}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5", "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center", indicator === "dot" && "items-center",
)} )}
> >
@@ -239,7 +239,7 @@ function ChartTooltipContent({
</span> </span>
</div> </div>
{item.value && ( {item.value && (
<span className="text-foreground font-mono font-medium tabular-nums"> <span className="font-medium font-mono text-foreground tabular-nums">
{item.value.toLocaleString()} {item.value.toLocaleString()}
</span> </span>
)} )}
@@ -292,7 +292,7 @@ function ChartLegendContent({
<div <div
key={item.value} key={item.value}
className={cn( className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3", "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
)} )}
> >
{itemConfig?.icon && !hideIcon ? ( {itemConfig?.icon && !hideIcon ? (
+1 -1
View File
@@ -9,7 +9,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
data-slot="checkbox" data-slot="checkbox"
className={cn( className={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2", "peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input outline-none transition-shadow after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 group-has-disabled/field:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:bg-input/30 dark:data-checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className, className,
)} )}
{...props} {...props}
+8 -8
View File
@@ -30,7 +30,7 @@ function ComboboxTrigger({
{...props} {...props}
> >
{children} {children}
<IconChevronDown className="text-muted-foreground pointer-events-none size-3.5" /> <IconChevronDown className="pointer-events-none size-3.5 text-muted-foreground" />
</ComboboxPrimitive.Trigger> </ComboboxPrimitive.Trigger>
); );
} }
@@ -110,7 +110,7 @@ function ComboboxContent({
data-slot="combobox-content" data-slot="combobox-content"
data-chips={!!anchor} data-chips={!!anchor}
className={cn( className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 *:data-[slot=input-group]:bg-input/20 dark:bg-popover data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-7 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:shadow-none", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) min-w-[calc(var(--anchor-width)+--spacing(7))] max-w-(--available-width) origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-closed:animate-out data-open:animate-in *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-7 *:data-[slot=input-group]:border-none *:data-[slot=input-group]:bg-input/20 *:data-[slot=input-group]:shadow-none dark:bg-popover",
className, className,
)} )}
{...props} {...props}
@@ -142,7 +142,7 @@ function ComboboxItem({
<ComboboxPrimitive.Item <ComboboxPrimitive.Item
data-slot="combobox-item" data-slot="combobox-item"
className={cn( className={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground relative flex min-h-7 w-full cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "relative flex min-h-7 w-full cursor-default select-none items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-50 not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@@ -176,7 +176,7 @@ function ComboboxLabel({
return ( return (
<ComboboxPrimitive.GroupLabel <ComboboxPrimitive.GroupLabel
data-slot="combobox-label" data-slot="combobox-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} className={cn("px-2 py-1.5 text-muted-foreground text-xs", className)}
{...props} {...props}
/> />
); );
@@ -193,7 +193,7 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
<ComboboxPrimitive.Empty <ComboboxPrimitive.Empty
data-slot="combobox-empty" data-slot="combobox-empty"
className={cn( className={cn(
"text-muted-foreground hidden w-full justify-center py-2 text-center text-xs/relaxed group-data-empty/combobox-content:flex", "hidden w-full justify-center py-2 text-center text-muted-foreground text-xs/relaxed group-data-empty/combobox-content:flex",
className, className,
)} )}
{...props} {...props}
@@ -208,7 +208,7 @@ function ComboboxSeparator({
return ( return (
<ComboboxPrimitive.Separator <ComboboxPrimitive.Separator
data-slot="combobox-separator" data-slot="combobox-separator"
className={cn("bg-border/50 -mx-1 my-1 h-px", className)} className={cn("-mx-1 my-1 h-px bg-border/50", className)}
{...props} {...props}
/> />
); );
@@ -223,7 +223,7 @@ function ComboboxChips({
<ComboboxPrimitive.Chips <ComboboxPrimitive.Chips
data-slot="combobox-chips" data-slot="combobox-chips"
className={cn( className={cn(
"bg-input/20 dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/30 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-7 flex-wrap items-center gap-1 rounded-md border bg-clip-padding px-2 py-0.5 text-xs/relaxed transition-colors focus-within:ring-2 has-aria-invalid:ring-2 has-data-[slot=combobox-chip]:px-1", "flex min-h-7 flex-wrap items-center gap-1 rounded-md border border-input bg-input/20 bg-clip-padding px-2 py-0.5 text-xs/relaxed transition-colors focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30 has-aria-invalid:border-destructive has-data-[slot=combobox-chip]:px-1 has-aria-invalid:ring-2 has-aria-invalid:ring-destructive/20 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
className, className,
)} )}
{...props} {...props}
@@ -243,7 +243,7 @@ function ComboboxChip({
<ComboboxPrimitive.Chip <ComboboxPrimitive.Chip
data-slot="combobox-chip" data-slot="combobox-chip"
className={cn( className={cn(
"bg-muted-foreground/10 text-foreground flex h-[calc(--spacing(4.75))] w-fit items-center justify-center gap-1 rounded-[calc(var(--radius-sm)-2px)] px-1.5 text-xs/relaxed font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0", "flex h-[calc(--spacing(4.75))] w-fit items-center justify-center gap-1 whitespace-nowrap rounded-[calc(var(--radius-sm)-2px)] bg-muted-foreground/10 px-1.5 font-medium text-foreground text-xs/relaxed has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:opacity-50",
className, className,
)} )}
{...props} {...props}
+7 -7
View File
@@ -21,7 +21,7 @@ function Command({
<CommandPrimitive <CommandPrimitive
data-slot="command" data-slot="command"
className={cn( className={cn(
"bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-xl p-1", "flex size-full flex-col overflow-hidden rounded-xl bg-popover p-1 text-popover-foreground",
className, className,
)} )}
{...props} {...props}
@@ -68,7 +68,7 @@ function CommandInput({
}: React.ComponentProps<typeof CommandPrimitive.Input>) { }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return ( return (
<div data-slot="command-input-wrapper" className="p-1 pb-0"> <div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="bg-input/20 dark:bg-input/30 h-8!"> <InputGroup className="h-8! bg-input/20 dark:bg-input/30">
<CommandPrimitive.Input <CommandPrimitive.Input
data-slot="command-input" data-slot="command-input"
className={cn( className={cn(
@@ -93,7 +93,7 @@ function CommandList({
<CommandPrimitive.List <CommandPrimitive.List
data-slot="command-list" data-slot="command-list"
className={cn( className={cn(
"no-scrollbar max-h-72 scroll-py-1 overflow-x-hidden overflow-y-auto outline-none", "no-scrollbar max-h-72 scroll-py-1 overflow-y-auto overflow-x-hidden outline-none",
className, className,
)} )}
{...props} {...props}
@@ -122,7 +122,7 @@ function CommandGroup({
<CommandPrimitive.Group <CommandPrimitive.Group
data-slot="command-group" data-slot="command-group"
className={cn( className={cn(
"text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2.5 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium", "overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2.5 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground **:[[cmdk-group-heading]]:text-xs",
className, className,
)} )}
{...props} {...props}
@@ -137,7 +137,7 @@ function CommandSeparator({
return ( return (
<CommandPrimitive.Separator <CommandPrimitive.Separator
data-slot="command-separator" data-slot="command-separator"
className={cn("bg-border/50 -mx-1 my-1 h-px", className)} className={cn("-mx-1 my-1 h-px bg-border/50", className)}
{...props} {...props}
/> />
); );
@@ -152,7 +152,7 @@ function CommandItem({
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot="command-item"
className={cn( className={cn(
"data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground group/command-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2.5 py-1.5 text-xs/relaxed outline-hidden select-none in-data-[slot=dialog-content]:rounded-md data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "group/command-item relative flex min-h-7 cursor-default select-none items-center gap-2 in-data-[slot=dialog-content]:rounded-md rounded-md px-2.5 py-1.5 text-xs/relaxed outline-hidden data-[disabled=true]:pointer-events-none data-selected:bg-muted data-selected:text-foreground data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-selected:*:[svg]:text-foreground",
className, className,
)} )}
{...props} {...props}
@@ -171,7 +171,7 @@ function CommandShortcut({
<span <span
data-slot="command-shortcut" data-slot="command-shortcut"
className={cn( className={cn(
"text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-[0.625rem] tracking-widest", "ml-auto text-[0.625rem] text-muted-foreground tracking-widest group-data-selected/command-item:text-foreground",
className, className,
)} )}
{...props} {...props}
+8 -8
View File
@@ -52,7 +52,7 @@ function ContextMenuContent({
<ContextMenuPrimitive.Popup <ContextMenuPrimitive.Popup
data-slot="context-menu-content" data-slot="context-menu-content"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 bg-popover text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-y-auto overflow-x-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-md outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className, className,
)} )}
{...props} {...props}
@@ -80,7 +80,7 @@ function ContextMenuLabel({
data-slot="context-menu-label" data-slot="context-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"text-muted-foreground px-2 py-1.5 text-xs data-inset:pl-7.5", "px-2 py-1.5 text-muted-foreground text-xs data-inset:pl-7.5",
className, className,
)} )}
{...props} {...props}
@@ -103,7 +103,7 @@ function ContextMenuItem({
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/context-menu-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "group/context-menu-item relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive",
className, className,
)} )}
{...props} {...props}
@@ -130,7 +130,7 @@ function ContextMenuSubTrigger({
data-slot="context-menu-sub-trigger" data-slot="context-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "flex min-h-7 cursor-default select-none items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-inset:pl-7.5 data-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@@ -168,7 +168,7 @@ function ContextMenuCheckboxItem({
data-slot="context-menu-checkbox-item" data-slot="context-menu-checkbox-item"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
checked={checked} checked={checked}
@@ -208,7 +208,7 @@ function ContextMenuRadioItem({
data-slot="context-menu-radio-item" data-slot="context-menu-radio-item"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@@ -230,7 +230,7 @@ function ContextMenuSeparator({
return ( return (
<ContextMenuPrimitive.Separator <ContextMenuPrimitive.Separator
data-slot="context-menu-separator" data-slot="context-menu-separator"
className={cn("bg-border/50 -mx-1 my-1 h-px", className)} className={cn("-mx-1 my-1 h-px bg-border/50", className)}
{...props} {...props}
/> />
); );
@@ -244,7 +244,7 @@ function ContextMenuShortcut({
<span <span
data-slot="context-menu-shortcut" data-slot="context-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground group-focus/context-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest", "ml-auto text-[0.625rem] text-muted-foreground tracking-widest group-focus/context-menu-item:text-accent-foreground",
className, className,
)} )}
{...props} {...props}
+4 -4
View File
@@ -30,7 +30,7 @@ function DialogOverlay({
<DialogPrimitive.Backdrop <DialogPrimitive.Backdrop
data-slot="dialog-overlay" data-slot="dialog-overlay"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs", "data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 isolate z-50 bg-black/80 duration-100 data-closed:animate-out data-open:animate-in supports-backdrop-filter:backdrop-blur-xs",
className, className,
)} )}
{...props} {...props}
@@ -52,7 +52,7 @@ function DialogContent({
<DialogPrimitive.Popup <DialogPrimitive.Popup
data-slot="dialog-content" data-slot="dialog-content"
className={cn( className={cn(
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 text-xs/relaxed ring-1 duration-100 outline-none sm:max-w-sm", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-xs/relaxed outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in sm:max-w-sm",
className, className,
)} )}
{...props} {...props}
@@ -119,7 +119,7 @@ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return ( return (
<DialogPrimitive.Title <DialogPrimitive.Title
data-slot="dialog-title" data-slot="dialog-title"
className={cn("text-sm font-medium", className)} className={cn("font-medium text-sm", className)}
{...props} {...props}
/> />
); );
@@ -133,7 +133,7 @@ function DialogDescription({
<DialogPrimitive.Description <DialogPrimitive.Description
data-slot="dialog-description" data-slot="dialog-description"
className={cn( className={cn(
"text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed *:[a]:underline *:[a]:underline-offset-3", "text-muted-foreground text-xs/relaxed *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className, className,
)} )}
{...props} {...props}
+4 -4
View File
@@ -37,7 +37,7 @@ function DrawerOverlay({
<DrawerPrimitive.Overlay <DrawerPrimitive.Overlay
data-slot="drawer-overlay" data-slot="drawer-overlay"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/80 supports-backdrop-filter:backdrop-blur-xs", "data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/80 data-closed:animate-out data-open:animate-in supports-backdrop-filter:backdrop-blur-xs",
className, className,
)} )}
{...props} {...props}
@@ -56,12 +56,12 @@ function DrawerContent({
<DrawerPrimitive.Content <DrawerPrimitive.Content
data-slot="drawer-content" data-slot="drawer-content"
className={cn( className={cn(
"before:bg-background before:border-border group/drawer-content fixed z-50 flex h-auto flex-col bg-transparent p-2 text-xs/relaxed overscroll-contain before:absolute before:inset-2 before:-z-10 before:rounded-xl before:border data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm", "group/drawer-content fixed z-50 flex h-auto flex-col overscroll-contain bg-transparent p-2 text-xs/relaxed before:absolute before:inset-2 before:-z-10 before:rounded-xl before:border before:border-border before:bg-background data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm",
className, className,
)} )}
{...props} {...props}
> >
<div className="bg-muted mx-auto mt-4 hidden h-1.5 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" /> <div className="mx-auto mt-4 hidden h-1.5 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
@@ -98,7 +98,7 @@ function DrawerTitle({
return ( return (
<DrawerPrimitive.Title <DrawerPrimitive.Title
data-slot="drawer-title" data-slot="drawer-title"
className={cn("text-foreground text-sm font-medium", className)} className={cn("font-medium text-foreground text-sm", className)}
{...props} {...props}
/> />
); );
+9 -9
View File
@@ -41,7 +41,7 @@ function DropdownMenuContent({
<MenuPrimitive.Popup <MenuPrimitive.Popup
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 bg-popover text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-y-auto overflow-x-hidden rounded-lg bg-popover p-1 text-popover-foreground shadow-md outline-none ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in data-closed:overflow-hidden",
className, className,
)} )}
{...props} {...props}
@@ -67,7 +67,7 @@ function DropdownMenuLabel({
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"text-muted-foreground px-2 py-1.5 text-xs data-inset:pl-7.5", "px-2 py-1.5 text-muted-foreground text-xs data-inset:pl-7.5",
className, className,
)} )}
{...props} {...props}
@@ -90,7 +90,7 @@ function DropdownMenuItem({
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "group/dropdown-menu-item relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 data-[variant=destructive]:*:[svg]:text-destructive",
className, className,
)} )}
{...props} {...props}
@@ -115,7 +115,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger" data-slot="dropdown-menu-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "flex min-h-7 cursor-default select-none items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-popup-open:bg-accent data-inset:pl-7.5 data-open:text-accent-foreground data-popup-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@@ -138,7 +138,7 @@ function DropdownMenuSubContent({
<DropdownMenuContent <DropdownMenuContent
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 bg-popover text-popover-foreground w-auto min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 w-auto min-w-32 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className, className,
)} )}
align={align} align={align}
@@ -164,7 +164,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
checked={checked} checked={checked}
@@ -205,7 +205,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@@ -230,7 +230,7 @@ function DropdownMenuSeparator({
return ( return (
<MenuPrimitive.Separator <MenuPrimitive.Separator
data-slot="dropdown-menu-separator" data-slot="dropdown-menu-separator"
className={cn("bg-border/50 -mx-1 my-1 h-px", className)} className={cn("-mx-1 my-1 h-px bg-border/50", className)}
{...props} {...props}
/> />
); );
@@ -244,7 +244,7 @@ function DropdownMenuShortcut({
<span <span
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest", "ml-auto text-[0.625rem] text-muted-foreground tracking-widest group-focus/dropdown-menu-item:text-accent-foreground",
className, className,
)} )}
{...props} {...props}
+5 -5
View File
@@ -7,7 +7,7 @@ function Empty({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="empty" data-slot="empty"
className={cn( className={cn(
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance", "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 text-balance rounded-xl border-dashed p-6 text-center",
className, className,
)} )}
{...props} {...props}
@@ -31,7 +31,7 @@ const emptyMediaVariants = cva(
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
icon: "bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-md [&_svg:not([class*='size-'])]:size-4", icon: "flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -59,7 +59,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="empty-title" data-slot="empty-title"
className={cn("text-sm font-medium tracking-tight", className)} className={cn("font-medium text-sm tracking-tight", className)}
{...props} {...props}
/> />
); );
@@ -70,7 +70,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
<div <div
data-slot="empty-description" data-slot="empty-description"
className={cn( className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-xs/relaxed [&>a]:underline [&>a]:underline-offset-4", "text-muted-foreground text-xs/relaxed [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className, className,
)} )}
{...props} {...props}
@@ -83,7 +83,7 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="empty-content" data-slot="empty-content"
className={cn( className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2 text-xs/relaxed text-balance", "flex w-full min-w-0 max-w-sm flex-col items-center gap-2 text-balance text-xs/relaxed",
className, className,
)} )}
{...props} {...props}
+9 -9
View File
@@ -51,15 +51,15 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
} }
const fieldVariants = cva( const fieldVariants = cva(
"data-[invalid=true]:text-destructive gap-2 group/field flex w-full", "group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
{ {
variants: { variants: {
orientation: { orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto", vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal: horizontal:
"flex-row items-center *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive: responsive:
"flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:*:data-[slot=field-label]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", "@md/field-group:flex-row flex-col @md/field-group:items-center *:w-full @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -106,7 +106,7 @@ function FieldLabel({
<Label <Label
data-slot="field-label" data-slot="field-label"
className={cn( className={cn(
"has-data-checked:bg-primary/5 dark:has-data-checked:bg-primary/10 group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-2", "group/field-label peer/field-label flex w-fit gap-2 leading-snug has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border has-data-checked:bg-primary/5 *:data-[slot=field]:p-2 group-data-[disabled=true]/field:opacity-50 dark:has-data-checked:bg-primary/10",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className, className,
)} )}
@@ -120,7 +120,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="field-label" data-slot="field-label"
className={cn( className={cn(
"flex w-fit items-center gap-2 text-xs/relaxed leading-snug font-medium group-data-[disabled=true]/field:opacity-50", "flex w-fit items-center gap-2 font-medium text-xs/relaxed leading-snug group-data-[disabled=true]/field:opacity-50",
className, className,
)} )}
{...props} {...props}
@@ -133,8 +133,8 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
<p <p
data-slot="field-description" data-slot="field-description"
className={cn( className={cn(
"text-muted-foreground text-left text-xs/relaxed leading-normal font-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5", "text-left font-normal text-muted-foreground text-xs/relaxed leading-normal group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1", "nth-last-2:-mt-1 last:mt-0",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className, className,
)} )}
@@ -163,7 +163,7 @@ function FieldSeparator({
<Separator className="absolute inset-0 top-1/2" /> <Separator className="absolute inset-0 top-1/2" />
{children && ( {children && (
<span <span
className="text-muted-foreground bg-background relative mx-auto block w-fit px-2" className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content" data-slot="field-separator-content"
> >
{children} {children}
@@ -224,7 +224,7 @@ function FieldError({
<div <div
role="alert" role="alert"
data-slot="field-error" data-slot="field-error"
className={cn("text-destructive text-xs/relaxed font-normal", className)} className={cn("font-normal text-destructive text-xs/relaxed", className)}
{...props} {...props}
> >
{content} {content}
+1 -1
View File
@@ -38,7 +38,7 @@ function HoverCardContent({
<PreviewCardPrimitive.Popup <PreviewCardPrimitive.Popup
data-slot="hover-card-content" data-slot="hover-card-content"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 bg-popover text-popover-foreground data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-72 origin-(--transform-origin) rounded-lg p-2.5 text-xs/relaxed shadow-md ring-1 outline-hidden duration-100", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-72 origin-(--transform-origin) rounded-lg bg-popover p-2.5 text-popover-foreground text-xs/relaxed shadow-md outline-hidden ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className, className,
)} )}
{...props} {...props}
+8 -8
View File
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group" data-slot="input-group"
role="group" role="group"
className={cn( className={cn(
"border-input bg-input/20 dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 group/input-group relative flex h-7 w-full min-w-0 items-center rounded-md border transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-2 has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", "group/input-group relative flex h-7 w-full min-w-0 items-center rounded-md border border-input bg-input/20 outline-none transition-colors in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-start]]:h-auto has-[>textarea]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:flex-col has-[textarea]:rounded-md has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:bg-input/30 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className, className,
)} )}
{...props} {...props}
@@ -23,18 +23,18 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
} }
const inputGroupAddonVariants = cva( const inputGroupAddonVariants = cva(
"text-muted-foreground **:data-[slot=kbd]:bg-muted-foreground/10 h-auto gap-1 py-2 text-xs/relaxed font-medium group-data-[disabled=true]/input-group:opacity-50 **:data-[slot=kbd]:rounded-[calc(var(--radius-sm)-2px)] **:data-[slot=kbd]:px-1 **:data-[slot=kbd]:text-[0.625rem] [&>svg:not([class*='size-'])]:size-3.5 flex cursor-text items-center justify-center select-none", "flex h-auto cursor-text select-none items-center justify-center gap-1 py-2 font-medium text-muted-foreground text-xs/relaxed **:data-[slot=kbd]:rounded-[calc(var(--radius-sm)-2px)] **:data-[slot=kbd]:bg-muted-foreground/10 **:data-[slot=kbd]:px-1 **:data-[slot=kbd]:text-[0.625rem] group-data-[disabled=true]/input-group:opacity-50 [&>svg:not([class*='size-'])]:size-3.5",
{ {
variants: { variants: {
align: { align: {
"inline-start": "inline-start":
"pl-2 has-[>button]:ml-[-0.275rem] has-[>kbd]:ml-[-0.275rem] order-first", "order-first pl-2 has-[>button]:ml-[-0.275rem] has-[>kbd]:ml-[-0.275rem]",
"inline-end": "inline-end":
"pr-2 has-[>button]:mr-[-0.275rem] has-[>kbd]:mr-[-0.275rem] order-last", "order-last pr-2 has-[>button]:mr-[-0.275rem] has-[>kbd]:mr-[-0.275rem]",
"block-start": "block-start":
"px-2 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start", "order-first w-full justify-start px-2 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end": "block-end":
"px-2 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start", "order-last w-full justify-start px-2 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -68,7 +68,7 @@ function InputGroupAddon({
} }
const inputGroupButtonVariants = cva( const inputGroupButtonVariants = cva(
"gap-2 rounded-md text-xs/relaxed shadow-none flex items-center", "flex items-center gap-2 rounded-md text-xs/relaxed shadow-none",
{ {
variants: { variants: {
size: { size: {
@@ -109,7 +109,7 @@ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return ( return (
<span <span
className={cn( className={cn(
"text-muted-foreground flex items-center gap-2 text-xs/relaxed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", "flex items-center gap-2 text-muted-foreground text-xs/relaxed [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className, className,
)} )}
{...props} {...props}
+1 -1
View File
@@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type} type={type}
data-slot="input" data-slot="input"
className={cn( className={cn(
"bg-input/20 dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 file:text-foreground placeholder:text-muted-foreground h-7 w-full min-w-0 rounded-md border px-2 py-0.5 text-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs/relaxed file:font-medium focus-visible:ring-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 md:text-xs/relaxed", "h-7 w-full min-w-0 rounded-md border border-input bg-input/20 px-2 py-0.5 text-sm outline-none transition-colors file:inline-flex file:h-6 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-xs/relaxed placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className, className,
)} )}
{...props} {...props}
+6 -6
View File
@@ -35,18 +35,18 @@ function ItemSeparator({
} }
const itemVariants = cva( const itemVariants = cva(
"[a]:hover:bg-muted rounded-md border text-xs/relaxed w-full group/item focus-visible:border-ring focus-visible:ring-ring/50 flex items-center flex-wrap outline-none transition-colors duration-100 focus-visible:ring-[3px] [a]:transition-colors", "group/item flex w-full flex-wrap items-center rounded-md border text-xs/relaxed outline-none transition-colors duration-100 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted",
{ {
variants: { variants: {
variant: { variant: {
default: "border-transparent", default: "border-transparent",
outline: "border-border", outline: "border-border",
muted: "bg-muted/50 border-transparent", muted: "border-transparent bg-muted/50",
}, },
size: { size: {
default: "gap-2.5 px-3 py-2.5", default: "gap-2.5 px-3 py-2.5",
sm: "gap-2.5 px-3 py-2.5", sm: "gap-2.5 px-3 py-2.5",
xs: "gap-2.5 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0", xs: "gap-2.5 in-data-[slot=dropdown-menu-content]:p-0 px-2.5 py-2",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -81,7 +81,7 @@ function Item({
} }
const itemMediaVariants = cva( const itemMediaVariants = cva(
"gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start flex shrink-0 items-center justify-center [&_svg]:pointer-events-none", "flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
{ {
variants: { variants: {
variant: { variant: {
@@ -130,7 +130,7 @@ function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
<div <div
data-slot="item-title" data-slot="item-title"
className={cn( className={cn(
"line-clamp-1 flex w-fit items-center gap-2 text-xs/relaxed leading-snug font-medium underline-offset-4", "line-clamp-1 flex w-fit items-center gap-2 font-medium text-xs/relaxed leading-snug underline-offset-4",
className, className,
)} )}
{...props} {...props}
@@ -143,7 +143,7 @@ function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
<p <p
data-slot="item-description" data-slot="item-description"
className={cn( className={cn(
"text-muted-foreground [&>a:hover]:text-primary line-clamp-2 text-left text-xs/relaxed font-normal [&>a]:underline [&>a]:underline-offset-4", "line-clamp-2 text-left font-normal text-muted-foreground text-xs/relaxed [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className, className,
)} )}
{...props} {...props}
+1 -1
View File
@@ -5,7 +5,7 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
<kbd <kbd
data-slot="kbd" data-slot="kbd"
className={cn( className={cn(
"bg-muted text-muted-foreground in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-xs px-1 font-sans text-[0.625rem] font-medium select-none [&_svg:not([class*='size-'])]:size-3", "pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-xs bg-muted in-data-[slot=tooltip-content]:bg-background/20 px-1 font-medium font-sans in-data-[slot=tooltip-content]:text-background text-[0.625rem] text-muted-foreground dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
className, className,
)} )}
{...props} {...props}
+1 -1
View File
@@ -10,7 +10,7 @@ function Label({ className, ...props }: React.ComponentProps<"label">) {
<label <label
data-slot="label" data-slot="label"
className={cn( className={cn(
"flex items-center gap-2 text-xs/relaxed leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", "flex select-none items-center gap-2 font-medium text-xs/relaxed leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className, className,
)} )}
{...props} {...props}
+11 -11
View File
@@ -26,7 +26,7 @@ function Menubar({ className, ...props }: MenubarPrimitive.Props) {
<MenubarPrimitive <MenubarPrimitive
data-slot="menubar" data-slot="menubar"
className={cn( className={cn(
"bg-background flex h-9 items-center rounded-lg border p-1", "flex h-9 items-center rounded-lg border bg-background p-1",
className, className,
)} )}
{...props} {...props}
@@ -58,7 +58,7 @@ function MenubarTrigger({
<DropdownMenuTrigger <DropdownMenuTrigger
data-slot="menubar-trigger" data-slot="menubar-trigger"
className={cn( className={cn(
"hover:bg-muted aria-expanded:bg-muted flex items-center rounded-[calc(var(--radius-md)-2px)] px-2 py-[calc(--spacing(0.85))] text-xs/relaxed font-medium outline-hidden select-none", "flex select-none items-center rounded-[calc(var(--radius-md)-2px)] px-2 py-[calc(--spacing(0.85))] font-medium text-xs/relaxed outline-hidden hover:bg-muted aria-expanded:bg-muted",
className, className,
)} )}
{...props} {...props}
@@ -80,7 +80,7 @@ function MenubarContent({
alignOffset={alignOffset} alignOffset={alignOffset}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-open:fade-in-0 data-open:zoom-in-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 ring-foreground/10 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100", "data-open:fade-in-0 data-open:zoom-in-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 min-w-32 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-open:animate-in",
className, className,
)} )}
{...props} {...props}
@@ -100,7 +100,7 @@ function MenubarItem({
data-inset={inset} data-inset={inset}
data-variant={variant} data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive! not-data-[variant=destructive]:focus:**:text-accent-foreground group/menubar-item min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed data-disabled:opacity-50 data-inset:pl-7.5 [&_svg:not([class*='size-'])]:size-3.5", "group/menubar-item min-h-7 gap-2 rounded-md px-2 py-1 text-xs/relaxed focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-disabled:opacity-50 data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive!",
className, className,
)} )}
{...props} {...props}
@@ -122,7 +122,7 @@ function MenubarCheckboxItem({
data-slot="menubar-checkbox-item" data-slot="menubar-checkbox-item"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-2 pl-7.5 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0", "relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md py-1.5 pr-2 pl-7.5 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
checked={checked} checked={checked}
@@ -157,7 +157,7 @@ function MenubarRadioItem({
data-slot="menubar-radio-item" data-slot="menubar-radio-item"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-2 pl-7.5 text-xs outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-7.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "relative flex min-h-7 cursor-default select-none items-center gap-2 rounded-md py-1.5 pr-2 pl-7.5 text-xs outline-hidden focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-disabled:pointer-events-none data-inset:pl-7.5 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@@ -184,7 +184,7 @@ function MenubarLabel({
data-slot="menubar-label" data-slot="menubar-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"text-muted-foreground px-2 py-1.5 text-xs data-inset:pl-7.5", "px-2 py-1.5 text-muted-foreground text-xs data-inset:pl-7.5",
className, className,
)} )}
{...props} {...props}
@@ -199,7 +199,7 @@ function MenubarSeparator({
return ( return (
<DropdownMenuSeparator <DropdownMenuSeparator
data-slot="menubar-separator" data-slot="menubar-separator"
className={cn("bg-border/50 -mx-1 my-1 h-px", className)} className={cn("-mx-1 my-1 h-px bg-border/50", className)}
{...props} {...props}
/> />
); );
@@ -213,7 +213,7 @@ function MenubarShortcut({
<DropdownMenuShortcut <DropdownMenuShortcut
data-slot="menubar-shortcut" data-slot="menubar-shortcut"
className={cn( className={cn(
"text-muted-foreground group-focus/menubar-item:text-accent-foreground ml-auto text-[0.625rem] tracking-widest", "ml-auto text-[0.625rem] text-muted-foreground tracking-widest group-focus/menubar-item:text-accent-foreground",
className, className,
)} )}
{...props} {...props}
@@ -239,7 +239,7 @@ function MenubarSubTrigger({
data-slot="menubar-sub-trigger" data-slot="menubar-sub-trigger"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground min-h-7 gap-2 rounded-md px-2 py-1 text-xs data-inset:pl-7.5 [&_svg:not([class*='size-'])]:size-3.5", "min-h-7 gap-2 rounded-md px-2 py-1 text-xs focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-open:bg-accent data-inset:pl-7.5 data-open:text-accent-foreground [&_svg:not([class*='size-'])]:size-3.5",
className, className,
)} )}
{...props} {...props}
@@ -255,7 +255,7 @@ function MenubarSubContent({
<DropdownMenuSubContent <DropdownMenuSubContent
data-slot="menubar-sub-content" data-slot="menubar-sub-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 min-w-32 rounded-lg p-1 shadow-md ring-1 duration-100", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 min-w-32 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className, className,
)} )}
{...props} {...props}
+6 -6
View File
@@ -55,7 +55,7 @@ function NavigationMenuItem({
} }
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
"bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/30 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted rounded-md px-2.5 py-1.5 text-xs/relaxed font-medium transition-all focus-visible:ring-2 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center disabled:pointer-events-none outline-none", "group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-2.5 py-1.5 font-medium text-xs/relaxed outline-none transition-all hover:bg-muted focus:bg-muted focus-visible:outline-1 focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 data-open:bg-muted/50 data-popup-open:bg-muted/50 data-open:focus:bg-muted data-open:hover:bg-muted data-popup-open:hover:bg-muted",
); );
function NavigationMenuTrigger({ function NavigationMenuTrigger({
@@ -86,7 +86,7 @@ function NavigationMenuContent({
<NavigationMenuPrimitive.Content <NavigationMenuPrimitive.Content
data-slot="navigation-menu-content" data-slot="navigation-menu-content"
className={cn( className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:ring-foreground/10 data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1.5 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-xl group-data-[viewport=false]/navigation-menu:shadow-md group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", "data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 h-full w-auto p-1.5 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=navigation-menu-link]:focus:outline-none **:data-[slot=navigation-menu-link]:focus:ring-0 group-data-[viewport=false]/navigation-menu:rounded-xl group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow-md group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-open:animate-in",
className, className,
)} )}
{...props} {...props}
@@ -115,7 +115,7 @@ function NavigationMenuPositioner({
)} )}
{...props} {...props}
> >
<NavigationMenuPrimitive.Popup className="bg-popover text-popover-foreground ring-foreground/10 data-[ending-style]:easing-[ease] xs:w-(--popup-width) relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-xl shadow ring-1 transition-[opacity,transform,width,height,scale,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0"> <NavigationMenuPrimitive.Popup className="data-[ending-style]:easing-[ease] relative h-(--popup-height) w-(--popup-width) xs:w-(--popup-width) origin-(--transform-origin) rounded-xl bg-popover text-popover-foreground shadow outline-none ring-1 ring-foreground/10 transition-[opacity,transform,width,height,scale,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:scale-90 data-starting-style:scale-90 data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:duration-150">
<NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden" /> <NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden" />
</NavigationMenuPrimitive.Popup> </NavigationMenuPrimitive.Popup>
</NavigationMenuPrimitive.Positioner> </NavigationMenuPrimitive.Positioner>
@@ -131,7 +131,7 @@ function NavigationMenuLink({
<NavigationMenuPrimitive.Link <NavigationMenuPrimitive.Link
data-slot="navigation-menu-link" data-slot="navigation-menu-link"
className={cn( className={cn(
"data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 focus-visible:ring-ring/30 hover:bg-muted focus:bg-muted flex items-center gap-1.5 rounded-lg p-2 text-xs/relaxed transition-all outline-none focus-visible:ring-2 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4", "flex items-center gap-1.5 rounded-lg p-2 text-xs/relaxed outline-none transition-all hover:bg-muted focus:bg-muted focus-visible:outline-1 focus-visible:ring-2 focus-visible:ring-ring/30 data-[active=true]:bg-muted/50 data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted [&_svg:not([class*='size-'])]:size-4",
className, className,
)} )}
{...props} {...props}
@@ -147,12 +147,12 @@ function NavigationMenuIndicator({
<NavigationMenuPrimitive.Icon <NavigationMenuPrimitive.Icon
data-slot="navigation-menu-indicator" data-slot="navigation-menu-indicator"
className={cn( className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden", "data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=visible]:animate-in",
className, className,
)} )}
{...props} {...props}
> >
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" /> <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Icon> </NavigationMenuPrimitive.Icon>
); );
} }
+2 -2
View File
@@ -37,7 +37,7 @@ function PopoverContent({
<PopoverPrimitive.Popup <PopoverPrimitive.Popup
data-slot="popover-content" data-slot="popover-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-lg p-2.5 text-xs shadow-md ring-1 outline-hidden duration-100", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-lg bg-popover p-2.5 text-popover-foreground text-xs shadow-md outline-hidden ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className, className,
)} )}
{...props} {...props}
@@ -61,7 +61,7 @@ function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return ( return (
<PopoverPrimitive.Title <PopoverPrimitive.Title
data-slot="popover-title" data-slot="popover-title"
className={cn("text-sm font-medium", className)} className={cn("font-medium text-sm", className)}
{...props} {...props}
/> />
); );
+4 -4
View File
@@ -29,7 +29,7 @@ function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
return ( return (
<ProgressPrimitive.Track <ProgressPrimitive.Track
className={cn( className={cn(
"bg-muted relative flex h-1 w-full items-center overflow-x-hidden rounded-md", "relative flex h-1 w-full items-center overflow-x-hidden rounded-md bg-muted",
className, className,
)} )}
data-slot="progress-track" data-slot="progress-track"
@@ -45,7 +45,7 @@ function ProgressIndicator({
return ( return (
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
data-slot="progress-indicator" data-slot="progress-indicator"
className={cn("bg-primary h-full transition-all", className)} className={cn("h-full bg-primary transition-all", className)}
{...props} {...props}
/> />
); );
@@ -54,7 +54,7 @@ function ProgressIndicator({
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) { function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
return ( return (
<ProgressPrimitive.Label <ProgressPrimitive.Label
className={cn("text-xs/relaxed font-medium", className)} className={cn("font-medium text-xs/relaxed", className)}
data-slot="progress-label" data-slot="progress-label"
{...props} {...props}
/> />
@@ -65,7 +65,7 @@ function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
return ( return (
<ProgressPrimitive.Value <ProgressPrimitive.Value
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs/relaxed tabular-nums", "ml-auto text-muted-foreground text-xs/relaxed tabular-nums",
className, className,
)} )}
data-slot="progress-value" data-slot="progress-value"
+2 -2
View File
@@ -20,7 +20,7 @@ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
<RadioPrimitive.Root <RadioPrimitive.Root
data-slot="radio-group-item" data-slot="radio-group-item"
className={cn( className={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:border-destructive/50 group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3", "group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:bg-input/30 dark:data-checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className, className,
)} )}
{...props} {...props}
@@ -29,7 +29,7 @@ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
data-slot="radio-group-indicator" data-slot="radio-group-indicator"
className="flex size-4 items-center justify-center" className="flex size-4 items-center justify-center"
> >
<span className="bg-primary-foreground absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full" /> <span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
</RadioPrimitive.Indicator> </RadioPrimitive.Indicator>
</RadioPrimitive.Root> </RadioPrimitive.Root>
); );
+2 -2
View File
@@ -35,13 +35,13 @@ function ResizableHandle({
<ResizablePrimitive.Separator <ResizablePrimitive.Separator
data-slot="resizable-handle" data-slot="resizable-handle"
className={cn( className={cn(
"bg-border focus-visible:ring-ring ring-offset-background relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90", "relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
className, className,
)} )}
{...props} {...props}
> >
{withHandle && ( {withHandle && (
<div className="bg-border z-10 flex h-6 w-1 shrink-0 rounded-lg" /> <div className="z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border" />
)} )}
</ResizablePrimitive.Separator> </ResizablePrimitive.Separator>
); );
+3 -3
View File
@@ -19,7 +19,7 @@ function ScrollArea({
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport" data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1" className="size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:ring-ring/50"
> >
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
@@ -40,14 +40,14 @@ function ScrollBar({
data-orientation={orientation} data-orientation={orientation}
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent", "flex touch-none select-none p-px transition-colors data-horizontal:h-2.5 data-vertical:h-full data-vertical:w-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:border-l data-vertical:border-l-transparent",
className, className,
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.Thumb <ScrollAreaPrimitive.Thumb
data-slot="scroll-area-thumb" data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full" className="relative flex-1 rounded-full bg-border"
/> />
</ScrollAreaPrimitive.Scrollbar> </ScrollAreaPrimitive.Scrollbar>
); );
+8 -8
View File
@@ -45,7 +45,7 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"border-input data-placeholder:text-muted-foreground bg-input/20 dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 flex w-fit items-center justify-between gap-1.5 rounded-md border px-2 py-1.5 text-xs/relaxed whitespace-nowrap transition-colors outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 data-[size=default]:h-7 data-[size=sm]:h-6 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "flex w-fit items-center justify-between gap-1.5 whitespace-nowrap rounded-md border border-input bg-input/20 px-2 py-1.5 text-xs/relaxed outline-none transition-colors focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-[size=default]:h-7 data-[size=sm]:h-6 data-placeholder:text-muted-foreground *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className, className,
)} )}
{...props} {...props}
@@ -53,7 +53,7 @@ function SelectTrigger({
{children} {children}
<SelectPrimitive.Icon <SelectPrimitive.Icon
render={ render={
<IconSelector className="text-muted-foreground pointer-events-none size-3.5" /> <IconSelector className="pointer-events-none size-3.5 text-muted-foreground" />
} }
/> />
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
@@ -88,7 +88,7 @@ function SelectContent({
data-slot="select-content" data-slot="select-content"
data-align-trigger={alignItemWithTrigger} data-align-trigger={alignItemWithTrigger}
className={cn( className={cn(
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 ring-foreground/10 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none", "data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-y-auto overflow-x-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-closed:animate-out data-open:animate-in",
className, className,
)} )}
{...props} {...props}
@@ -109,7 +109,7 @@ function SelectLabel({
return ( return (
<SelectPrimitive.GroupLabel <SelectPrimitive.GroupLabel
data-slot="select-label" data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)} className={cn("px-2 py-1.5 text-muted-foreground text-xs", className)}
{...props} {...props}
/> />
); );
@@ -124,7 +124,7 @@ function SelectItem({
<SelectPrimitive.Item <SelectPrimitive.Item
data-slot="select-item" data-slot="select-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground relative flex min-h-7 w-full cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", "relative flex min-h-7 w-full cursor-default select-none items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className, className,
)} )}
{...props} {...props}
@@ -151,7 +151,7 @@ function SelectSeparator({
<SelectPrimitive.Separator <SelectPrimitive.Separator
data-slot="select-separator" data-slot="select-separator"
className={cn( className={cn(
"bg-border/50 pointer-events-none -mx-1 my-1 h-px", "pointer-events-none -mx-1 my-1 h-px bg-border/50",
className, className,
)} )}
{...props} {...props}
@@ -167,7 +167,7 @@ function SelectScrollUpButton({
<SelectPrimitive.ScrollUpArrow <SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button" data-slot="select-scroll-up-button"
className={cn( className={cn(
"bg-popover top-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5", "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-3.5",
className, className,
)} )}
{...props} {...props}
@@ -185,7 +185,7 @@ function SelectScrollDownButton({
<SelectPrimitive.ScrollDownArrow <SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button" data-slot="select-scroll-down-button"
className={cn( className={cn(
"bg-popover bottom-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-3.5", "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-3.5",
className, className,
)} )}
{...props} {...props}
+1 -1
View File
@@ -14,7 +14,7 @@ function Separator({
data-slot="separator" data-slot="separator"
orientation={orientation} orientation={orientation}
className={cn( className={cn(
"bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className, className,
)} )}
{...props} {...props}
+3 -3
View File
@@ -27,7 +27,7 @@ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
<SheetPrimitive.Backdrop <SheetPrimitive.Backdrop
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/80 duration-100 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs", "data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/80 duration-100 data-closed:animate-out data-open:animate-in data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
className, className,
)} )}
{...props} {...props}
@@ -52,7 +52,7 @@ function SheetContent({
data-slot="sheet-content" data-slot="sheet-content"
data-side={side} data-side={side}
className={cn( className={cn(
"bg-background data-open:animate-in data-closed:animate-out data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", "data-[side=right]:data-closed:slide-out-to-right-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=top]:data-closed:slide-out-to-top-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:fade-out-0 data-open:fade-in-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=bottom]:data-open:slide-in-from-bottom-10 fixed z-50 flex flex-col bg-background bg-clip-padding text-xs/relaxed shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=top]:inset-x-0 data-[side=left]:inset-y-0 data-[side=right]:inset-y-0 data-[side=top]:top-0 data-[side=right]:right-0 data-[side=bottom]:bottom-0 data-[side=left]:left-0 data-[side=bottom]:h-auto data-[side=left]:h-full data-[side=right]:h-full data-[side=top]:h-auto data-[side=left]:w-3/4 data-[side=right]:w-3/4 data-closed:animate-out data-open:animate-in data-[side=bottom]:border-t data-[side=left]:border-r data-[side=top]:border-b data-[side=right]:border-l data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className, className,
)} )}
{...props} {...props}
@@ -102,7 +102,7 @@ function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return ( return (
<SheetPrimitive.Title <SheetPrimitive.Title
data-slot="sheet-title" data-slot="sheet-title"
className={cn("text-foreground text-sm font-medium", className)} className={cn("font-medium text-foreground text-sm", className)}
{...props} {...props}
/> />
); );
+20 -20
View File
@@ -140,7 +140,7 @@ function SidebarProvider({
} as React.CSSProperties } as React.CSSProperties
} }
className={cn( className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
className, className,
)} )}
{...props} {...props}
@@ -171,7 +171,7 @@ function Sidebar({
<div <div
data-slot="sidebar" data-slot="sidebar"
className={cn( className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col", "flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
className, className,
)} )}
{...props} {...props}
@@ -189,7 +189,7 @@ function Sidebar({
data-sidebar="sidebar" data-sidebar="sidebar"
data-slot="sidebar" data-slot="sidebar"
data-mobile="true" data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden" className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={ style={
{ {
"--sidebar-width": SIDEBAR_WIDTH_MOBILE, "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
@@ -209,7 +209,7 @@ function Sidebar({
return ( return (
<div <div
className="group peer text-sidebar-foreground hidden md:block" className="group peer hidden text-sidebar-foreground md:block"
data-state={state} data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""} data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant} data-variant={variant}
@@ -232,7 +232,7 @@ function Sidebar({
data-slot="sidebar-container" data-slot="sidebar-container"
data-side={side} data-side={side}
className={cn( className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex", "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=right]:right-0 data-[side=left]:left-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] md:flex",
// Adjust the padding for floating and inset variants. // Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset" variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]" ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
@@ -244,7 +244,7 @@ function Sidebar({
<div <div
data-sidebar="sidebar" data-sidebar="sidebar"
data-slot="sidebar-inner" data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:ring-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1" className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
> >
{children} {children}
</div> </div>
@@ -291,10 +291,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
onClick={toggleSidebar} onClick={toggleSidebar}
title="Toggle Sidebar" title="Toggle Sidebar"
className={cn( className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2", "absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize", "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full", "group-data-[collapsible=offcanvas]:translate-x-0 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className, className,
@@ -309,7 +309,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
<main <main
data-slot="sidebar-inset" data-slot="sidebar-inset"
className={cn( className={cn(
"bg-background relative flex w-full flex-1 flex-col md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", "relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm",
className, className,
)} )}
{...props} {...props}
@@ -326,7 +326,7 @@ function SidebarInput({
data-slot="sidebar-input" data-slot="sidebar-input"
data-sidebar="input" data-sidebar="input"
className={cn( className={cn(
"bg-muted/20 dark:bg-muted/30 border-input h-8 w-full", "h-8 w-full border-input bg-muted/20 dark:bg-muted/30",
className, className,
)} )}
{...props} {...props}
@@ -364,7 +364,7 @@ function SidebarSeparator({
<Separator <Separator
data-slot="sidebar-separator" data-slot="sidebar-separator"
data-sidebar="separator" data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)} className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props} {...props}
/> />
); );
@@ -408,7 +408,7 @@ function SidebarGroupLabel({
props: mergeProps<"div">( props: mergeProps<"div">(
{ {
className: cn( className: cn(
"text-sidebar-foreground/70 ring-sidebar-ring h-8 rounded-md px-2 text-xs transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 flex shrink-0 items-center outline-hidden [&>svg]:shrink-0", "flex h-8 shrink-0 items-center rounded-md px-2 text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 [&>svg]:size-4 [&>svg]:shrink-0",
className, className,
), ),
}, },
@@ -432,7 +432,7 @@ function SidebarGroupAction({
props: mergeProps<"button">( props: mergeProps<"button">(
{ {
className: cn( className: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden group-data-[collapsible=icon]:hidden", "absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 group-data-[collapsible=icon]:hidden md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
className, className,
), ),
}, },
@@ -483,13 +483,13 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
} }
const sidebarMenuButtonVariants = cva( const sidebarMenuButtonVariants = cva(
"ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-[calc(var(--radius-sm)+2px)] p-2 text-left text-xs transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0", "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-[calc(var(--radius-sm)+2px)] p-2 text-left text-xs outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline: outline:
"bg-background hover:bg-sidebar-accent hover:text-sidebar-accent-foreground shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
}, },
size: { size: {
default: "h-8 text-xs", default: "h-8 text-xs",
@@ -572,9 +572,9 @@ function SidebarMenuAction({
props: mergeProps<"button">( props: mergeProps<"button">(
{ {
className: cn( className: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-[calc(var(--radius-sm)-2px)] p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0", "absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-[calc(var(--radius-sm)-2px)] p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
showOnHover && showOnHover &&
"peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 aria-expanded:opacity-100 md:opacity-0", "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 aria-expanded:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground md:opacity-0",
className, className,
), ),
}, },
@@ -597,7 +597,7 @@ function SidebarMenuBadge({
data-slot="sidebar-menu-badge" data-slot="sidebar-menu-badge"
data-sidebar="menu-badge" data-sidebar="menu-badge"
className={cn( className={cn(
"text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-[calc(var(--radius-sm)-2px)] px-1 text-xs font-medium tabular-nums select-none group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1", "pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-[calc(var(--radius-sm)-2px)] px-1 font-medium text-sidebar-foreground text-xs tabular-nums peer-hover/menu-button:text-sidebar-accent-foreground group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
className, className,
)} )}
{...props} {...props}
@@ -649,7 +649,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
data-slot="sidebar-menu-sub" data-slot="sidebar-menu-sub"
data-sidebar="menu-sub" data-sidebar="menu-sub"
className={cn( className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden", "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
className, className,
)} )}
{...props} {...props}
@@ -687,7 +687,7 @@ function SidebarMenuSubButton({
props: mergeProps<"a">( props: mergeProps<"a">(
{ {
className: cn( className: cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-xs data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0", "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-active:bg-sidebar-accent data-[size=md]:text-xs data-[size=sm]:text-xs data-active:text-sidebar-accent-foreground group-data-[collapsible=icon]:hidden [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
className, className,
), ),
}, },
+1 -1
View File
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="skeleton" data-slot="skeleton"
className={cn("bg-muted animate-pulse rounded-md", className)} className={cn("animate-pulse rounded-md bg-muted", className)}
{...props} {...props}
/> />
); );
+5 -5
View File
@@ -25,7 +25,7 @@ function Slider({
return ( return (
<SliderPrimitive.Root <SliderPrimitive.Root
className={cn("data-horizontal:w-full data-vertical:h-full", className)} className={cn("data-vertical:h-full data-horizontal:w-full", className)}
data-slot="slider" data-slot="slider"
defaultValue={defaultValue} defaultValue={defaultValue}
value={value} value={value}
@@ -34,14 +34,14 @@ function Slider({
thumbAlignment="edge" thumbAlignment="edge"
{...props} {...props}
> >
<SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col"> <SliderPrimitive.Control className="relative flex w-full touch-none select-none items-center data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col data-disabled:opacity-50">
<SliderPrimitive.Track <SliderPrimitive.Track
data-slot="slider-track" data-slot="slider-track"
className="bg-muted relative grow overflow-hidden rounded-md select-none data-horizontal:h-3 data-horizontal:w-full data-vertical:h-full data-vertical:w-3" className="relative grow select-none overflow-hidden rounded-md bg-muted data-horizontal:h-3 data-vertical:h-full data-horizontal:w-full data-vertical:w-3"
> >
<SliderPrimitive.Indicator <SliderPrimitive.Indicator
data-slot="slider-range" data-slot="slider-range"
className="bg-primary select-none data-horizontal:h-full data-vertical:w-full" className="select-none bg-primary data-horizontal:h-full data-vertical:w-full"
/> />
</SliderPrimitive.Track> </SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => ( {Array.from({ length: _values.length }, (_, index) => (
@@ -49,7 +49,7 @@ function Slider({
data-slot="slider-thumb" data-slot="slider-thumb"
// biome-ignore lint/suspicious/noArrayIndexKey: shadcn generated // biome-ignore lint/suspicious/noArrayIndexKey: shadcn generated
key={index} key={index}
className="border-primary ring-ring/30 block size-4 shrink-0 rounded-md border bg-white shadow-sm transition-colors select-none hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" className="block size-4 shrink-0 select-none rounded-md border border-primary bg-white shadow-sm ring-ring/30 transition-colors hover:ring-4 focus-visible:outline-hidden focus-visible:ring-4 disabled:pointer-events-none disabled:opacity-50"
/> />
))} ))}
</SliderPrimitive.Control> </SliderPrimitive.Control>
+2 -2
View File
@@ -16,14 +16,14 @@ function Switch({
data-slot="switch" data-slot="switch"
data-size={size} data-size={size}
className={cn( className={cn(
"data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-2 aria-invalid:ring-2 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-[size=default]:h-[18px] data-[size=default]:w-[30px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px]", "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent outline-none transition-all after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 data-[size=default]:h-[18px] data-[size=sm]:h-[14px] data-[size=default]:w-[30px] data-[size=sm]:w-[24px] data-disabled:cursor-not-allowed data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-50 dark:data-unchecked:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className, className,
)} )}
{...props} {...props}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb" data-slot="switch-thumb"
className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-3.5 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=default]/switch:data-unchecked:translate-x-px group-data-[size=sm]/switch:data-unchecked:translate-x-px" className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-3.5 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=default]/switch:data-unchecked:translate-x-px group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-1px)] group-data-[size=sm]/switch:data-unchecked:translate-x-px dark:data-checked:bg-primary-foreground dark:data-unchecked:bg-foreground"
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
); );
+5 -5
View File
@@ -44,7 +44,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
<tfoot <tfoot
data-slot="table-footer" data-slot="table-footer"
className={cn( className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className, className,
)} )}
{...props} {...props}
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr <tr
data-slot="table-row" data-slot="table-row"
className={cn( className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className, className,
)} )}
{...props} {...props}
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th <th
data-slot="table-head" data-slot="table-head"
className={cn( className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0", "h-10 whitespace-nowrap px-2 text-left align-middle font-medium text-foreground [&:has([role=checkbox])]:pr-0",
className, className,
)} )}
{...props} {...props}
@@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
<td <td
data-slot="table-cell" data-slot="table-cell"
className={cn( className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0", "whitespace-nowrap p-2 align-middle [&:has([role=checkbox])]:pr-0",
className, className,
)} )}
{...props} {...props}
@@ -98,7 +98,7 @@ function TableCaption({
return ( return (
<caption <caption
data-slot="table-caption" data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-xs", className)} className={cn("mt-4 text-muted-foreground text-xs", className)}
{...props} {...props}
/> />
); );
+4 -4
View File
@@ -24,7 +24,7 @@ function Tabs({
} }
const tabsListVariants = cva( const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col", "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground data-[variant=line]:rounded-none group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col",
{ {
variants: { variants: {
variant: { variant: {
@@ -58,10 +58,10 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
<TabsPrimitive.Tab <TabsPrimitive.Tab
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-xs font-medium whitespace-nowrap transition-colors group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:py-[calc(--spacing(1.25))] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5", "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-1.5 py-0.5 font-medium text-foreground/60 text-xs transition-colors hover:text-foreground focus-visible:border-ring focus-visible:outline-1 focus-visible:outline-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:py-[calc(--spacing(1.25))] dark:text-muted-foreground dark:hover:text-foreground [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground", "data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className, className,
)} )}
{...props} {...props}
+1 -1
View File
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea <textarea
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
"border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full resize-none rounded-md border px-2 py-2 text-sm transition-colors outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-2 md:text-xs/relaxed", "field-sizing-content flex min-h-16 w-full resize-none rounded-md border border-input bg-input/20 px-2 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className, className,
)} )}
{...props} {...props}
+1 -1
View File
@@ -71,7 +71,7 @@ function ToggleGroupItem({
data-size={context.size || size} data-size={context.size || size}
data-spacing={context.spacing} data-spacing={context.spacing}
className={cn( className={cn(
"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t", "shrink-0 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-[spacing=0]/toggle-group:px-2 group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md",
toggleVariants({ toggleVariants({
variant: context.variant || variant, variant: context.variant || variant,
size: context.size || size, size: context.size || size,
+2 -2
View File
@@ -6,12 +6,12 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const toggleVariants = cva( const toggleVariants = cva(
"hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-md text-xs font-medium transition-all [&_svg:not([class*='size-'])]:size-3.5 group/toggle hover:bg-muted inline-flex items-center justify-center whitespace-nowrap outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "group/toggle inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md font-medium text-xs outline-none transition-all hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-pressed:bg-muted aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
outline: "border-input hover:bg-muted border bg-transparent", outline: "border border-input bg-transparent hover:bg-muted",
}, },
size: { size: {
default: "h-7 min-w-7 px-2", default: "h-7 min-w-7 px-2",
+2 -2
View File
@@ -50,13 +50,13 @@ function TooltipContent({
<TooltipPrimitive.Popup <TooltipPrimitive.Popup
data-slot="tooltip-content" data-slot="tooltip-content"
className={cn( className={cn(
"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 bg-foreground text-background z-50 w-fit max-w-xs origin-(--transform-origin) rounded-md px-3 py-1.5 text-xs **:data-[slot=kbd]:rounded-md", "data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:fade-out-0 data-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 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-fit max-w-xs origin-(--transform-origin) rounded-md bg-foreground px-3 py-1.5 text-background text-xs data-[state=delayed-open]:animate-in data-closed:animate-out data-open:animate-in **:data-[slot=kbd]:rounded-md",
className, className,
)} )}
{...props} {...props}
> >
{children} {children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" /> <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-start]:top-1/2! data-[side=left]:top-1/2! data-[side=right]:top-1/2! data-[side=inline-start]:-right-1 data-[side=left]:-right-1 data-[side=top]:-bottom-2.5 data-[side=inline-end]:-left-1 data-[side=right]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:-translate-y-1/2 data-[side=right]:-translate-y-1/2" />
</TooltipPrimitive.Popup> </TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner> </TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
+1 -1
View File
@@ -12,7 +12,7 @@ export function UpdateToast() {
useEffect(() => { useEffect(() => {
if (shown) return; if (shown) return;
getUpdateCheckAction().then((data) => { void getUpdateCheckAction().then((data) => {
if (!data?.updateAvailable) return; if (!data?.updateAvailable) return;
setShown(true); setShown(true);
+1 -1
View File
@@ -91,7 +91,7 @@ export async function getSonarrList(
const result: { TvdbId: number; Title: string }[] = []; const result: { TvdbId: number; Title: string }[] = [];
for (const row of rows) { for (const row of rows) {
let tvdbId = row.tvdbId; let { tvdbId } = row;
if (tvdbId == null) { if (tvdbId == null) {
try { try {
+2 -2
View File
@@ -86,7 +86,7 @@ export function logEpisodeWatch(
.where(eq(seasons.id, ep.seasonId)) .where(eq(seasons.id, ep.seasonId))
.get(); .get();
if (!season) return; if (!season) return;
const titleId = season.titleId; const { titleId } = season;
// Auto-set status to in_progress if not set // Auto-set status to in_progress if not set
const existing = db const existing = db
@@ -141,7 +141,7 @@ export function logEpisodeWatchBatch(
.all(); .all();
if (seasonRows.length === 0) return; if (seasonRows.length === 0) return;
const titleId = seasonRows[0].titleId; const { titleId } = seasonRows[0];
// Set status to in_progress if not already set // Set status to in_progress if not already set
const existing = tx const existing = tx
+1 -2
View File
@@ -178,8 +178,7 @@ async function resolveEpisode(event: WebhookEvent): Promise<{
seasonNumber: number; seasonNumber: number;
episodeNumber: number; episodeNumber: number;
} | null> { } | null> {
const seasonNumber = event.seasonNumber; const { seasonNumber, episodeNumber } = event;
const episodeNumber = event.episodeNumber;
if (seasonNumber == null || episodeNumber == null) return null; if (seasonNumber == null || episodeNumber == null) return null;
// Strategy 1: Use IMDB ID to find the episode and get show_id // Strategy 1: Use IMDB ID to find the episode and get show_id