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-foreground: oklch(0.92 0 0);
--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-foreground: oklch(0.88 0.06 254.13);
--destructive: oklch(0.64 0.21 25.33);

View File

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

View File

@ -54,7 +54,7 @@ export default async function IconsPage() {
return (
<div className="isolate overflow-hidden">
<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>
<h1 className="text-3xl font-bold">Icons</h1>

View File

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

View File

@ -205,7 +205,7 @@ export function HeroSection({ totalIcons, stars }: { totalIcons: number; stars:
/>
</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 ">
<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

View File

@ -207,6 +207,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
size="icon"
className="h-8 w-8 rounded-lg cursor-pointer"
onClick={(e) => handleDownload(e, imageUrl, `${iconName}.${format}`)}
aria-label={`Download ${iconName} in ${format} format${theme ? ` (${theme} theme)` : ""}`}
>
<Download className="w-4 h-4" />
</Button>
@ -223,6 +224,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
size="icon"
className="h-8 w-8 rounded-lg cursor-pointer"
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" />}
</Button>
@ -234,8 +236,18 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" className="h-8 w-8 rounded-lg" asChild>
<Link href={githubUrl} target="_blank" rel="noopener noreferrer">
<Button
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" />
</Link>
</Button>
@ -252,7 +264,7 @@ export function IconDetails({ icon, iconData, authorData }: IconDetailsProps) {
}
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">
{/* Left Column: Icon Info and Author */}
<div className="lg:col-span-1">

View File

@ -30,7 +30,7 @@ export function RecentlyAddedIcons({ icons }: { icons: IconWithName[] }) {
{/* 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="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">
<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

View File

@ -21,7 +21,7 @@ function Command({
<CommandPrimitive
data-slot="command"
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
)}
{...props}
@ -30,8 +30,8 @@ function Command({
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
title = "Command Menu",
description = "Search for icons...",
children,
...props
}: React.ComponentProps<typeof Dialog> & {
@ -44,8 +44,8 @@ function CommandDialog({
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<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">
<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 [&_[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}
</Command>
</DialogContent>
@ -60,13 +60,13 @@ function CommandInput({
return (
<div
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
data-slot="command-input"
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
)}
{...props}
@ -92,12 +92,16 @@ function CommandList({
}
function CommandEmpty({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.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}
/>
)
@ -111,7 +115,7 @@ function CommandGroup({
<CommandPrimitive.Group
data-slot="command-group"
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
)}
{...props}
@ -140,7 +144,7 @@ function CommandItem({
<CommandPrimitive.Item
data-slot="command-item"
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
)}
{...props}