feat(website): enhance transitions and styles

This commit is contained in:
Bjorn Lammers 2025-04-17 05:29:49 +02:00 committed by Thomas Camlong
parent 0e22539f06
commit 6e3a39a4cf
No known key found for this signature in database
GPG Key ID: A678F374F428457B
10 changed files with 126 additions and 86 deletions

View File

@ -64,7 +64,7 @@
}
:root {
--radius: 0.3rem;
--radius: 0.75rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
@ -140,3 +140,21 @@
@apply bg-background text-foreground;
}
}
@layer utilities {
.hover-lift {
@apply transition-transform duration-300 hover:-translate-y-1;
}
.soft-shadow {
@apply shadow-[0_8px_30px_rgba(0,0,0,0.06)];
}
.card-hover {
@apply transition-all duration-300 hover:shadow-md;
}
.glass-effect {
@apply backdrop-blur-sm;
}
}

View File

@ -46,11 +46,11 @@ export function IconSearch({ icons, initialQuery = "" }: IconSearchProps) {
return (
<>
<div className="relative w-full max-w-md">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground transition-all duration-300" />
<Input
type="search"
placeholder="Search icons by name, aliases, or categories..."
className="w-full pl-8"
className="w-full pl-8 transition-all duration-300 text-sm md:text-base"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>

View File

@ -109,11 +109,11 @@ export function IconSearch({ icons }: IconSearchProps) {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground transition-all duration-300" />
<Input
type="search"
placeholder="Search icons by name, aliases, or categories..."
className="w-full pl-8 cursor-text"
className="w-full pl-8 cursor-text transition-all duration-300 text-sm md:text-base"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
/>

View File

@ -16,7 +16,7 @@ interface IconCardProps {
function IconCard({ name, imageUrl }: IconCardProps) {
return (
<Card className="p-4 hover:shadow-md transition-shadow duration-300 flex flex-col items-center gap-2 cursor-pointer group">
<Card className="p-4 flex flex-col items-center gap-2 cursor-pointer group hover-lift card-hover">
<div className="w-16 h-16 flex items-center justify-center">
<img src={imageUrl} alt={name} className="max-w-full max-h-full" />
</div>
@ -202,7 +202,7 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) {
<Link prefetch href="https://github.com/homarr-labs" target="_blank" rel="noopener noreferrer" className="mx-auto">
<motion.div variants={fadeUpVariants} custom={0} initial="hidden" animate="visible" whileHover="hover">
<motion.div
className="overflow-hidden rounded-md relative"
className="overflow-hidden rounded-xl relative"
variants={{
hover: {
scale: 1.05,
@ -232,7 +232,7 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) {
},
}}
/>
<Card className="p-2 flex flex-row items-center gap-2 border-rose-200 dark:border-rose-900/30 shadow-sm bg-background z-10 relative">
<Card className="p-2 flex flex-row items-center gap-2 border-rose-200 dark:border-rose-900/30 shadow-sm bg-background/80 z-10 relative glass-effect">
<motion.div
variants={{
hover: {
@ -303,11 +303,11 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) {
name="q"
type="search"
placeholder={`Search ${totalIcons} icons...`}
className="pl-10 h-10 md:h-12 rounded-lg border-muted-foreground/20 focus:border-rose-500 focus:ring-rose-500/20 transition-all"
className="pl-10 h-10 md:h-12 rounded-lg border-muted-foreground/20 focus:border-rose-500 focus:ring-rose-500/20 transition-all bg-background/95 dark:bg-background/90 backdrop-blur-sm text-sm md:text-base"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 md:h-5 w-4 md:w-5 text-muted-foreground group-focus-within:text-rose-500 transition-colors" />
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 md:h-5 w-4 md:w-5 text-muted-foreground group-focus-within:text-rose-500 transition-all duration-300" />
<motion.span
className="absolute inset-0 -z-10 rounded-lg bg-rose-500/5 opacity-0 transition-opacity group-hover:opacity-100"
initial={{ scale: 0.95 }}
@ -323,7 +323,7 @@ export function HeroSection({ totalIcons }: { totalIcons: number }) {
</Button>
<Button
variant="outline"
className="h-9 md:h-10 px-4 gap-2 border-rose-200 dark:border-rose-900/30 hover:bg-rose-50 dark:hover:bg-rose-900/20 hover:border-rose-300 dark:hover:border-rose-800"
className="h-9 md:h-10 px-4 gap-2 border-rose-200 dark:border-rose-900/30 hover:bg-rose-50 dark:hover:bg-rose-900/20 hover:border-rose-300 dark:hover:border-rose-800 bg-background/95 dark:bg-background/90 backdrop-blur-sm"
asChild
>
<Link

View File

@ -2,6 +2,7 @@
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { REPO_PATH } from "@/constants"
import { DialogDescription } from "@radix-ui/react-dialog"
import { ExternalLink, PlusCircle } from "lucide-react"
@ -68,14 +69,23 @@ export function IconSubmissionForm() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
className="hidden md:inline-flex cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 hover:border-rose-500 dark:hover:border-rose-500 transition-colors duration-200"
>
<PlusCircle className="h-4 w-4" /> Suggest new icon
</Button>
</DialogTrigger>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button
variant="outline"
className="hidden md:inline-flex cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 hover:border-rose-500 dark:hover:border-rose-500 transition-all duration-300"
>
<PlusCircle className="h-4 w-4 transition-all duration-300" /> Suggest new icon
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Suggest a new icon</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DialogContent className="md:max-w-4xl backdrop-blur-2xl">
<DialogHeader>
<DialogTitle>Suggest a new icon</DialogTitle>

View File

@ -83,10 +83,10 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) {
>
<Link
href="/icons"
className="text-rose-500 dark:text-rose-400 hover:text-rose-600 dark:hover:text-rose-300 font-medium inline-flex items-center py-2 px-4 rounded-full border border-rose-200 dark:border-rose-800/30 hover:bg-rose-50 dark:hover:bg-rose-900/20 transition-all duration-200 group"
className="text-rose-500 dark:text-rose-400 hover:text-rose-600 dark:hover:text-rose-300 font-medium inline-flex items-center py-2 px-4 rounded-full border border-rose-200 dark:border-rose-800/30 hover:bg-rose-50 dark:hover:bg-rose-900/20 transition-all duration-200 group hover-lift soft-shadow"
>
View all icons
<ArrowRight className="w-4 h-4 ml-1 transition-transform duration-200 group-hover:translate-x-1" />
<ArrowRight className="w-4 h-4 ml-1.5 transition-transform duration-200 group-hover:translate-x-1" />
</Link>
</motion.div>
</div>
@ -131,36 +131,36 @@ function RecentIconCard({ name, data, getIconVariant }: {
exit="exit"
variants={variants}
className="will-change-transform"
>
<Link
prefetch={false}
href={`/icons/${name}`}
className="group flex flex-col items-center p-3 sm:p-4 rounded-lg border border-border bg-background/95 dark:bg-background/80 hover:border-rose-500 hover:bg-rose-500/10 dark:hover:bg-rose-900/30 dark:hover:border-rose-500 transition-all duration-300 hover:shadow-lg hover:shadow-rose-500/5 relative overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-br from-rose-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
>
<Link
prefetch={false}
href={`/icons/${name}`}
className="group flex flex-col items-center p-3 sm:p-4 rounded-xl border border-border bg-background/95 dark:bg-background/80 hover:border-rose-500 hover:bg-rose-500/10 dark:hover:bg-rose-900/30 dark:hover:border-rose-500 transition-all duration-300 hover:shadow-lg hover:shadow-rose-500/5 relative overflow-hidden hover-lift"
>
<div className="absolute inset-0 bg-gradient-to-br from-rose-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2">
<Image
src={`${BASE_URL}/${data.base}/${getIconVariant(name, data)}.${data.base}`}
alt={`${name} icon`}
fill
className="object-contain p-1 group-hover:scale-110 transition-transform duration-300"
/>
</div>
<span className="text-xs sm:text-sm text-center truncate w-full capitalize group-hover:text-rose-600 dark:group-hover:text-rose-400 transition-colors duration-200 font-medium">
{name.replace(/-/g, " ")}
</span>
<div className="flex items-center justify-center mt-2 w-full">
<span className="text-[10px] sm:text-xs text-muted-foreground flex items-center whitespace-nowrap group-hover:text-rose-500/70 transition-colors duration-200">
<Clock className="w-3 h-3 mr-1 shrink-0" />
{formatIconDate(data.update.timestamp)}
</span>
</div>
<div className="relative h-12 w-12 sm:h-16 sm:w-16 mb-2">
<Image
src={`${BASE_URL}/${data.base}/${getIconVariant(name, data)}.${data.base}`}
alt={`${name} icon`}
fill
className="object-contain p-1 group-hover:scale-110 transition-transform duration-300"
/>
</div>
<span className="text-xs sm:text-sm text-center truncate w-full capitalize group-hover:text-rose-600 dark:group-hover:text-rose-400 transition-colors duration-200 font-medium">
{name.replace(/-/g, " ")}
</span>
<div className="flex items-center justify-center mt-2 w-full">
<span className="text-[10px] sm:text-xs text-muted-foreground flex items-center whitespace-nowrap group-hover:text-rose-500/70 transition-colors duration-200">
<Clock className="w-3 h-3 mr-1.5 shrink-0" />
{formatIconDate(data.update.timestamp)}
</span>
</div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<ExternalLink className="w-3 h-3 text-rose-500" />
</div>
</Link>
</motion.div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<ExternalLink className="w-3 h-3 text-rose-500" />
</div>
</Link>
</motion.div>
);
}

View File

@ -5,34 +5,46 @@ import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { useState } from "react"
export function ThemeSwitcher() {
const { setTheme } = useTheme()
const [open, setOpen] = useState(false)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
className="rounded-lg cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200"
variant="ghost"
size="icon"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")} className="cursor-pointer">
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")} className="cursor-pointer">
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")} className="cursor-pointer">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<TooltipProvider>
<DropdownMenu open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
className="rounded-md cursor-pointer hover:bg-rose-500/10 dark:hover:bg-rose-900/30 transition-colors duration-200 group"
variant="ghost"
size="icon"
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0 group-hover:text-rose-500" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 group-hover:text-rose-500" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Change theme</p>
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")} className="cursor-pointer">
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")} className="cursor-pointer">
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")} className="cursor-pointer">
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TooltipProvider>
)
}

View File

@ -5,27 +5,27 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-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",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-300 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 [&_svg]:transition-all [&_svg]:duration-300 outline-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 hover:scale-[1.02]",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow-md",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-white shadow-sm hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 hover:shadow-md",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"border bg-background shadow-sm hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 hover:shadow-md",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 hover:shadow-md",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
default: "h-10 px-5 py-2 has-[>svg]:px-3",
sm: "h-9 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-11 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9 rounded-md",
},
},
defaultVariants: {

View File

@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm transition-all duration-300 hover:shadow-md",
className
)}
{...props}

View File

@ -8,8 +8,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-10 w-full min-w-0 rounded-lg border bg-transparent px-4 py-2 text-base shadow-sm transition-all duration-300 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] focus-visible:shadow-md",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}