refactor(web): enhance consistency and accessibility across components

This commit is contained in:
Bjorn Lammers 2025-04-23 00:06:26 +02:00
parent d6cb15aab0
commit df3c53818a
8 changed files with 79 additions and 45 deletions

View File

@ -199,7 +199,7 @@
--secondary: oklch(0.31 0.03 266.71); --secondary: oklch(0.31 0.03 266.71);
--secondary-foreground: oklch(0.92 0 0); --secondary-foreground: oklch(0.92 0 0);
--muted: oklch(0.31 0.03 266.71); --muted: oklch(0.31 0.03 266.71);
--muted-foreground: oklch(0.72 0 0); --muted-foreground: oklch(0.78 0 0);
--accent: oklch(0.34 0.06 267.59); --accent: oklch(0.34 0.06 267.59);
--accent-foreground: oklch(0.88 0.06 254.13); --accent-foreground: oklch(0.88 0.06 254.13);
--destructive: oklch(0.64 0.21 25.33); --destructive: oklch(0.64 0.21 25.33);

View File

@ -277,14 +277,14 @@ export function IconSearch({ icons }: IconSearchProps) {
{/* Filter dropdown */} {/* Filter dropdown */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="flex-1 sm:flex-none cursor-pointer bg-background border-border shadow-sm "> <Button
variant="outline"
size="sm"
className="flex-1 sm:flex-none cursor-pointer bg-background border-border shadow-sm"
aria-label="Filter icons"
>
<Filter className="h-4 w-4 mr-2" /> <Filter className="h-4 w-4 mr-2" />
<span>Categories</span> <span>{selectedCategories.length > 0 ? `Filters (${selectedCategories.length})` : "Filter"}</span>
{selectedCategories.length > 0 && (
<Badge variant="secondary" className="ml-2 px-1.5">
{selectedCategories.length}
</Badge>
)}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-64 sm:w-56"> <DropdownMenuContent align="start" className="w-64 sm:w-56">
@ -353,7 +353,13 @@ export function IconSearch({ icons }: IconSearchProps) {
{/* Clear all button */} {/* Clear all button */}
{(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && ( {(searchQuery || selectedCategories.length > 0 || sortOption !== "relevance") && (
<Button variant="outline" size="sm" onClick={clearFilters} className="flex-1 sm:flex-none cursor-pointer bg-background"> <Button
variant="outline"
size="sm"
onClick={clearFilters}
className="flex-1 sm:flex-none cursor-pointer bg-background"
aria-label="Reset all filters"
>
<X className="h-4 w-4 mr-2" /> <X className="h-4 w-4 mr-2" />
<span>Reset</span> <span>Reset</span>
</Button> </Button>
@ -406,15 +412,15 @@ export function IconSearch({ icons }: IconSearchProps) {
<div className="flex flex-col gap-4 items-center w-full"> <div className="flex flex-col gap-4 items-center w-full">
<IconSubmissionContent /> <IconSubmissionContent />
<div className="mt-4 flex items-center gap-2 justify-center"> <div className="mt-4 flex items-center gap-2 justify-center">
<span className="text-sm text-muted-foreground">Need help?</span> <span className="text-sm text-muted-foreground">Can't submit it yourself?</span>
<Button <Button
className="cursor-pointer" className="cursor-pointer"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => { onClick={() => {
setIsLazyRequestSubmitted(true) setIsLazyRequestSubmitted(true)
toast("Request submitted", { toast("Request received!", {
description: `We've added "${searchQuery || "this icon"}" to our request list.`, description: `We've noted your request for "${searchQuery || "this icon"}". Thanks for your suggestion.`,
}) })
posthog.capture("lazy icon request", { posthog.capture("lazy icon request", {
query: searchQuery, query: searchQuery,
@ -423,7 +429,7 @@ export function IconSearch({ icons }: IconSearchProps) {
}} }}
disabled={isLazyRequestSubmitted} disabled={isLazyRequestSubmitted}
> >
Submit request Request this icon
</Button> </Button>
</div> </div>
</div> </div>
@ -432,7 +438,8 @@ export function IconSearch({ icons }: IconSearchProps) {
<> <>
<div className="flex justify-between items-center pb-2"> <div className="flex justify-between items-center pb-2">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{filteredIcons.length} {filteredIcons.length === 1 ? "icon" : "icons"} found Found {filteredIcons.length} icon
{filteredIcons.length !== 1 ? "s" : ""}.
</p> </p>
<div className="flex items-center gap-1 text-xs text-muted-foreground"> <div className="flex items-center gap-1 text-xs text-muted-foreground">
{getSortIcon(sortOption)} {getSortIcon(sortOption)}

View File

@ -54,7 +54,7 @@ export default async function IconsPage() {
return ( return (
<div className="isolate overflow-hidden"> <div className="isolate overflow-hidden">
<div className="py-8"> <div className="py-8">
<div className="space-y-4 mb-8 mx-auto max-w-7xl"> <div className="space-y-4 mb-8 mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<h1 className="text-3xl font-bold">Icons</h1> <h1 className="text-3xl font-bold">Icons</h1>

View File

@ -103,9 +103,14 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
return ( return (
<CommandDialog open={isOpen} onOpenChange={setIsOpen}> <CommandDialog open={isOpen} onOpenChange={setIsOpen}>
<CommandInput placeholder="Search for icons by name, category, or purpose..." value={query} onValueChange={setQuery} /> <CommandInput placeholder="Search icons by name, category, or alias..." value={query} onValueChange={setQuery} />
<CommandList> <CommandList className="max-h-[350px]">
<CommandEmpty>No matching icons found. Try a different search term or browse all icons.</CommandEmpty> <CommandEmpty>
<div className="py-6 text-center">
<p className="text-sm text-muted-foreground">No matching icons found.</p>
<p className="text-xs text-muted-foreground mt-1">Try a different search term or browse all icons.</p>
</div>
</CommandEmpty>
<CommandGroup heading="Icons"> <CommandGroup heading="Icons">
{filteredIcons.map(({ name, data }) => { {filteredIcons.map(({ name, data }) => {
// Find matched alias for display if available // Find matched alias for display if available
@ -115,19 +120,25 @@ export function CommandMenu({ icons, open: externalOpen, onOpenChange: externalO
: null : null
return ( return (
<CommandItem key={name} value={name} onSelect={() => handleSelect(name)} className="flex items-center gap-2 cursor-pointer"> <CommandItem key={name} value={name} onSelect={() => handleSelect(name)} className="flex items-center gap-3 cursor-pointer">
<div className="flex-shrink-0 h-5 w-5 relative"> <div className="flex-shrink-0 h-8 w-8 relative">
<div className="h-5 w-5 bg-rose-100 dark:bg-rose-900/30 rounded-md flex items-center justify-center"> <div className="h-8 w-8 bg-accent/40 dark:bg-accent/25 rounded-lg border border-border/60 flex items-center justify-center shadow-sm backdrop-blur-sm">
<span className="text-[10px] font-medium text-rose-800 dark:text-rose-300">{name.substring(0, 2).toUpperCase()}</span> <span className="text-[10px] font-medium">{name.substring(0, 2).toUpperCase()}</span>
</div> </div>
</div> </div>
<span className="flex-grow capitalize">{name.replace(/-/g, " ")}</span> <div className="flex flex-col gap-0.5">
{matchedAlias && <span className="text-xs text-primary-500 truncate max-w-[100px]">alias: {matchedAlias}</span>} <span className="capitalize text-sm font-medium">{name.replace(/-/g, " ")}</span>
{matchedAlias && (
<span className="text-xs text-muted-foreground truncate max-w-[180px]">
Alias: {matchedAlias}
</span>
)}
{!matchedAlias && data.categories && data.categories.length > 0 && ( {!matchedAlias && data.categories && data.categories.length > 0 && (
<span className="text-xs text-muted-foreground truncate max-w-[100px]"> <span className="text-xs text-muted-foreground truncate max-w-[180px]">
{data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())} {data.categories[0].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
</span> </span>
)} )}
</div>
</CommandItem> </CommandItem>
) )
})} })}

View File

@ -205,7 +205,7 @@ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars:
/> />
</div> </div>
<div className="relative z-10 container mx-auto px-4 md:px-6 mt-4 py-20"> <div className="relative z-10 container mx-auto px-4 sm:px-6 lg:px-8 mt-4 py-20">
<div className="max-w-4xl mx-auto text-center flex flex-col gap-4 "> <div className="max-w-4xl mx-auto text-center flex flex-col gap-4 ">
<h1 className="relative text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-preset-slide-up motion-duration-500 "> <h1 className="relative text-3xl sm:text-5xl md:text-7xl font-bold mb-4 md:mb-8 tracking-tight motion-preset-slide-up motion-duration-500 ">
Your definitive source for Your definitive source for

View File

@ -207,6 +207,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
size="icon" size="icon"
className="h-8 w-8 rounded-lg cursor-pointer" className="h-8 w-8 rounded-lg cursor-pointer"
onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)} onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)}
aria-label={`Download ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
</Button> </Button>
@ -223,6 +224,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
size="icon" size="icon"
className="h-8 w-8 rounded-lg cursor-pointer" className="h-8 w-8 rounded-lg cursor-pointer"
onClick={(e) => handleCopy(imageUrl, `btn-${variantKey}`, e)} onClick={(e) => handleCopy(imageUrl, `btn-${variantKey}`, e)}
aria-label={`Copy URL for ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
> >
{copiedVariants[`btn-${variantKey}`] ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />} {copiedVariants[`btn-${variantKey}`] ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</Button> </Button>
@ -234,8 +236,18 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild> <Button
<Link href={githubUrl} target="_blank" rel="noopener noreferrer"> variant="outline"
size="icon"
className="h-8 w-8 rounded-lg"
asChild
>
<Link
href={githubUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`View ${iconName} ${format} file on GitHub`}
>
<Github className="w-4 h-4" /> <Github className="w-4 h-4" />
</Link> </Link>
</Button> </Button>
@ -252,7 +264,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
} }
return ( return (
<div className="container mx-auto pt-12 pb-14"> <div className="container mx-auto pt-12 pb-14 px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Left Column: Icon Info and Author */} {/* Left Column: Icon Info and Author */}
<div className="lg:col-span-1"> <div className="lg:col-span-1">

View File

@ -30,7 +30,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) {
{/* Background glow */} {/* Background glow */}
<div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true" /> <div className="absolute inset-x-0 -top-40 -z-10 transform-gpu overflow-hidden blur-3xl sm:-top-80" aria-hidden="true" />
<div className="mx-auto px-6 lg:px-8"> <div className="mx-auto px-4 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl text-center my-4"> <div className="mx-auto max-w-2xl text-center my-4">
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl bg-clip-text text-transparent bg-gradient-to-r from-rose-600 to-rose-500 motion-safe:motion-preset-fade-lg motion-duration-500"> <h2 className="text-3xl font-bold tracking-tight sm:text-4xl bg-clip-text text-transparent bg-gradient-to-r from-rose-600 to-rose-500 motion-safe:motion-preset-fade-lg motion-duration-500">
Recently Added Icons Recently Added Icons

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 h-full w-full flex-col overflow-hidden rounded-md", "bg-transparent text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className className
)} )}
{...props} {...props}
@ -30,8 +30,8 @@ function Command({
} }
function CommandDialog({ function CommandDialog({
title = "Command Palette", title = "Command Menu",
description = "Search for a command to run...", description = "Search for icons...",
children, children,
...props ...props
}: React.ComponentProps<typeof Dialog> & { }: React.ComponentProps<typeof Dialog> & {
@ -44,8 +44,8 @@ function CommandDialog({
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription> <DialogDescription>{description}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogContent className="overflow-hidden p-0"> <DialogContent className="overflow-hidden p-0 border-border bg-background/80 backdrop-blur-xl shadow-lg sm:max-w-md rounded-xl transition-all duration-200 ease-in-out">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> <Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
@ -60,13 +60,13 @@ function CommandInput({
return ( return (
<div <div
data-slot="command-input-wrapper" data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3" className="flex h-12 items-center gap-3 border-b border-border/60 px-4 bg-transparent"
> >
<SearchIcon className="size-4 shrink-0 opacity-50" /> <SearchIcon className="size-4 shrink-0 text-muted-foreground" />
<CommandPrimitive.Input <CommandPrimitive.Input
data-slot="command-input" data-slot="command-input"
className={cn( className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50", "placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none focus:outline-none focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}
@ -92,12 +92,16 @@ function CommandList({
} }
function CommandEmpty({ function CommandEmpty({
className,
...props ...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) { }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return ( return (
<CommandPrimitive.Empty <CommandPrimitive.Empty
data-slot="command-empty" data-slot="command-empty"
className="py-6 text-center text-sm" className={cn(
"py-6 text-center text-sm text-muted-foreground",
className
)}
{...props} {...props}
/> />
) )
@ -111,7 +115,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 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", "text-foreground overflow-hidden p-1.5 px-2 [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className className
)} )}
{...props} {...props}
@ -140,7 +144,7 @@ function CommandItem({
<CommandPrimitive.Item <CommandPrimitive.Item
data-slot="command-item" data-slot="command-item"
className={cn( className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "data-[selected=true]:bg-accent/70 data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-sm outline-none select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 transition-colors duration-200",
className className
)} )}
{...props} {...props}