Fix Select trigger display values and add TMDB attribution

Base UI Select.Value renders raw values by default — add children render
functions to map values to human-readable labels (e.g. "this_month" →
"This Month", "0" → "unlimited"). Switch inline select underlines from
border-bottom to text-decoration for proper baseline alignment. Add TMDB
attribution with logo and disclaimer to settings footer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 19:15:52 -05:00
co-authored by Claude Opus 4.6
parent 4773243cda
commit 436f3d7fad
21 changed files with 351 additions and 321 deletions
+57 -46
View File
@@ -6,6 +6,10 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { SofaLogo } from "@/components/sofa-logo";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { authClient, signIn, signUp } from "@/lib/auth/client";
export interface AuthConfig {
@@ -15,6 +19,9 @@ export interface AuthConfig {
registrationOpen?: boolean;
}
const authInputClass =
"h-11 rounded-lg border-border/50 bg-background/50 px-4 py-0 placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:ring-ring md:text-sm";
const fieldVariants = {
hidden: { opacity: 0, y: 10 },
visible: {
@@ -117,19 +124,20 @@ export function AuthForm({
visible: { transition: { staggerChildren: 0.08 } },
}}
>
<motion.button
type="button"
onClick={handleOidcLogin}
disabled={oidcLoading}
variants={fieldVariants}
whileTap={{ scale: 0.98 }}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-border/50 bg-background/50 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
>
<IconKey aria-hidden={true} className="size-4" />
{oidcLoading
? "Redirecting\u2026"
: `Sign in with ${authConfig?.oidcProviderName || "SSO"}`}
</motion.button>
<motion.div variants={fieldVariants}>
<Button
type="button"
variant="outline"
onClick={handleOidcLogin}
disabled={oidcLoading}
className="h-11 w-full gap-2 rounded-lg border-border/50 bg-background/50 text-sm hover:bg-accent hover:text-foreground"
>
<IconKey aria-hidden={true} className="size-4" />
{oidcLoading
? "Redirecting"
: `Sign in with ${authConfig?.oidcProviderName || "SSO"}`}
</Button>
</motion.div>
</motion.div>
)}
@@ -154,33 +162,33 @@ export function AuthForm({
>
{isRegister && (
<motion.div variants={fieldVariants} className="space-y-1.5">
<label
<Label
htmlFor="name"
className="text-xs font-medium uppercase tracking-wider text-muted-foreground"
className="uppercase tracking-wider text-muted-foreground"
>
Name
</label>
<input
</Label>
<Input
id="name"
type="text"
required
autoComplete="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="flex h-11 w-full rounded-lg border border-border/50 bg-background/50 px-4 text-sm transition-colors placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Your name\u2026"
className={authInputClass}
placeholder="Your name"
/>
</motion.div>
)}
<motion.div variants={fieldVariants} className="space-y-1.5">
<label
<Label
htmlFor="email"
className="text-xs font-medium uppercase tracking-wider text-muted-foreground"
className="uppercase tracking-wider text-muted-foreground"
>
Email
</label>
<input
</Label>
<Input
id="email"
type="email"
required
@@ -188,19 +196,19 @@ export function AuthForm({
spellCheck={false}
value={email}
onChange={(e) => setEmail(e.target.value)}
className="flex h-11 w-full rounded-lg border border-border/50 bg-background/50 px-4 text-sm transition-colors placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className={authInputClass}
placeholder="wwhite@graymatter.biz"
/>
</motion.div>
<motion.div variants={fieldVariants} className="space-y-1.5">
<label
<Label
htmlFor="password"
className="text-xs font-medium uppercase tracking-wider text-muted-foreground"
className="uppercase tracking-wider text-muted-foreground"
>
Password
</label>
<input
</Label>
<Input
id="password"
type="password"
required
@@ -210,37 +218,40 @@ export function AuthForm({
}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="flex h-11 w-full rounded-lg border border-border/50 bg-background/50 px-4 text-sm transition-colors placeholder:text-muted-foreground/50 focus-visible:border-primary/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Min 8 characters\u2026"
className={authInputClass}
placeholder="Min 8 characters"
/>
</motion.div>
<motion.button
type="submit"
disabled={loading}
variants={fieldVariants}
whileTap={{ scale: 0.98 }}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-primary font-medium text-primary-foreground transition-shadow hover:shadow-lg hover:shadow-primary/20 disabled:pointer-events-none disabled:opacity-50"
>
{loading
? "Loading\u2026"
: isRegister
? "Create account"
: "Sign in"}
</motion.button>
<motion.div variants={fieldVariants}>
<Button
type="submit"
disabled={loading}
className="h-11 w-full rounded-lg text-sm hover:shadow-lg hover:shadow-primary/20"
>
{loading
? "Loading…"
: isRegister
? "Create account"
: "Sign in"}
</Button>
</motion.div>
</motion.form>
)}
<AnimatePresence>
{error && (
<motion.div
role="alert"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="overflow-hidden rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive"
className="overflow-hidden"
>
{error}
<Alert variant="destructive" className="bg-destructive/10">
<AlertDescription className="text-destructive text-sm">
{error}
</AlertDescription>
</Alert>
</motion.div>
)}
</AnimatePresence>
+1 -1
View File
@@ -152,7 +152,7 @@ export function CommandPalette() {
>
<Command shouldFilter={false}>
<CommandInput
placeholder="Search movies & TV shows\u2026"
placeholder="Search movies & TV shows"
value={query}
onValueChange={setQuery}
/>
+5 -1
View File
@@ -7,6 +7,7 @@ import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { SofaLogo } from "@/components/sofa-logo";
import { Kbd } from "@/components/ui/kbd";
import { Separator } from "@/components/ui/separator";
import {
Tooltip,
TooltipContent,
@@ -86,7 +87,10 @@ export function NavBar() {
<span>Search</span>
<Kbd className="ml-1">K</Kbd>
</button>
<div className="mx-1.5 hidden h-4 w-px bg-border/50 sm:block" />
<Separator
orientation="vertical"
className="mx-1.5 hidden h-4 bg-border/50 sm:block"
/>
<Tooltip>
<TooltipTrigger
render={<Link href="/settings" />}
-55
View File
@@ -1,55 +0,0 @@
import { IconSelector } from "@tabler/icons-react";
import type * as React from "react";
import { cn } from "@/lib/utils";
type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
size?: "sm" | "default";
};
function NativeSelect({
className,
size = "default",
...props
}: NativeSelectProps) {
return (
<div
className={cn(
"group/native-select relative w-fit has-[select:disabled]:opacity-50",
className,
)}
data-slot="native-select-wrapper"
data-size={size}
>
<select
data-slot="native-select"
data-size={size}
className="text-foreground border-input bg-input/20 placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground 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 h-7 w-full min-w-0 appearance-none rounded-md border py-0.5 pr-6 pl-2 text-xs/relaxed transition-colors outline-none select-none focus-visible:ring-2 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:ring-2 data-[size=sm]:h-6 data-[size=sm]:text-[0.625rem]"
{...props}
/>
<IconSelector
className="text-muted-foreground pointer-events-none absolute top-1/2 right-1.5 size-3.5 -translate-y-1/2 select-none group-data-[size=sm]/native-select:size-3 group-data-[size=sm]/native-select:-translate-y-[calc(--spacing(1.25))]"
aria-hidden="true"
data-slot="native-select-icon"
/>
</div>
);
}
function NativeSelectOption({ ...props }: React.ComponentProps<"option">) {
return <option data-slot="native-select-option" {...props} />;
}
function NativeSelectOptGroup({
className,
...props
}: React.ComponentProps<"optgroup">) {
return (
<optgroup
data-slot="native-select-optgroup"
className={cn(className)}
{...props}
/>
);
}
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption };