IconButton

This commit is contained in:
Oliver Bryan
2026-01-17 03:46:09 +00:00
parent d63f7c33c1
commit 8dc6291eab
7 changed files with 92 additions and 64 deletions

View File

@@ -9,13 +9,13 @@ import { StatusSelect } from "@/components/status-select";
import StatusTag from "@/components/status-tag"; import StatusTag from "@/components/status-tag";
import { TimerDisplay } from "@/components/timer-display"; import { TimerDisplay } from "@/components/timer-display";
import { TimerModal } from "@/components/timer-modal"; import { TimerModal } from "@/components/timer-modal";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { SelectTrigger } from "@/components/ui/select"; import { SelectTrigger } from "@/components/ui/select";
import { issue } from "@/lib/server"; import { issue } from "@/lib/server";
import { issueID } from "@/lib/utils"; import { issueID } from "@/lib/utils";
import SmallSprintDisplay from "./small-sprint-display"; import SmallSprintDisplay from "./small-sprint-display";
import { SprintSelect } from "./sprint-select"; import { SprintSelect } from "./sprint-select";
import { IconButton } from "./ui/icon-button";
function assigneesToStringArray(assignees: UserRecord[]): string[] { function assigneesToStringArray(assignees: UserRecord[]): string[] {
if (assignees.length === 0) return ["unassigned"]; if (assignees.length === 0) return ["unassigned"];
@@ -245,24 +245,15 @@ export function IssueDetailPane({
</p> </p>
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
<Button <IconButton onClick={handleCopyLink} title={linkCopied ? "Copied" : "Copy link"}>
variant="dummy"
onClick={handleCopyLink}
className="px-0 py-0 w-6 h-6 hover:text-foreground/70"
title={linkCopied ? "Copied" : "Copy link"}
>
{linkCopied ? <Check /> : <Link />} {linkCopied ? <Check /> : <Link />}
</Button> </IconButton>
<Button <IconButton variant="destructive" onClick={handleDelete} title={"Delete issue"}>
variant="dummy"
onClick={handleDelete}
className="px-0 py-0 w-6 h-6 text-destructive hover:text-destructive/70"
>
<Trash /> <Trash />
</Button> </IconButton>
<Button variant={"dummy"} onClick={close} className="px-0 py-0 w-6 h-6"> <IconButton onClick={close} title={"Close"}>
<X /> <X />
</Button> </IconButton>
</div> </div>
</div> </div>

View File

@@ -10,6 +10,7 @@ import { useSession } from "@/components/session-provider";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Field } from "@/components/ui/field"; import { Field } from "@/components/ui/field";
import { IconButton } from "@/components/ui/icon-button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { UploadAvatar } from "@/components/upload-avatar"; import { UploadAvatar } from "@/components/upload-avatar";
import { capitalise, cn, getServerURL, setCsrfToken } from "@/lib/utils"; import { capitalise, cn, getServerURL, setCsrfToken } from "@/lib/utils";
@@ -144,9 +145,8 @@ export default function LogInForm() {
{/* under construction warning */} {/* under construction warning */}
{showWarning && ( {showWarning && (
<div className="relative flex flex-col border p-4 items-center border-border/50 bg-border/10 gap-2 max-w-lg"> <div className="relative flex flex-col border p-4 items-center border-border/50 bg-border/10 gap-2 max-w-lg">
<Button <IconButton
variant="dummy" size="md"
size="icon"
className="absolute top-2 right-2" className="absolute top-2 right-2"
onClick={() => { onClick={() => {
localStorage.setItem("hide-under-construction", "true"); localStorage.setItem("hide-under-construction", "true");
@@ -154,7 +154,7 @@ export default function LogInForm() {
}} }}
> >
<X /> <X />
</Button> </IconButton>
<AlertTriangle className="w-16 h-16 text-yellow-500" strokeWidth={1.5} /> <AlertTriangle className="w-16 h-16 text-yellow-500" strokeWidth={1.5} />
<div className="text-center text-sm text-muted-foreground font-500"> <div className="text-center text-sm text-muted-foreground font-500">
<p> <p>

View File

@@ -1,6 +1,6 @@
import type { UserRecord } from "@sprint/shared"; import type { UserRecord } from "@sprint/shared";
import { Plus } from "lucide-react"; import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button"; import { IconButton } from "@/components/ui/icon-button";
import { UserSelect } from "@/components/user-select"; import { UserSelect } from "@/components/user-select";
export function MultiAssigneeSelect({ export function MultiAssigneeSelect({
@@ -61,15 +61,9 @@ export function MultiAssigneeSelect({
/> />
</div> </div>
{index === assigneeIds.length - 1 && canAddMore && ( {index === assigneeIds.length - 1 && canAddMore && (
<Button <IconButton onClick={handleAddAssignee} title={"Add assignee"} className="w-9 h-9">
variant="dummy"
size="icon"
className="h-7 w-7 shrink-0 h-9"
onClick={handleAddAssignee}
title="Add assignee"
>
<Plus className="h-4 w-4" /> <Plus className="h-4 w-4" />
</Button> </IconButton>
)} )}
</> </>
))} ))}

View File

@@ -28,6 +28,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { IconButton } from "@/components/ui/icon-button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -552,9 +553,7 @@ function OrganisationsDialog({
member.OrganisationMember.role !== "owner" && member.OrganisationMember.role !== "owner" &&
member.User.id !== user.id && ( member.User.id !== user.id && (
<> <>
<Button <IconButton
variant="dummy"
size="none"
onClick={() => onClick={() =>
handleRoleChange( handleRoleChange(
member.User.id, member.User.id,
@@ -563,17 +562,22 @@ function OrganisationsDialog({
.role, .role,
) )
} }
variant={
member.OrganisationMember.role ===
"admin"
? "yellow"
: "green"
}
> >
{member.OrganisationMember.role === {member.OrganisationMember.role ===
"admin" ? ( "admin" ? (
<ChevronDown className="size-5 text-yellow-500" /> <ChevronDown className="size-5" />
) : ( ) : (
<ChevronUp className="size-5 text-green-500" /> <ChevronUp className="size-5" />
)} )}
</Button> </IconButton>
<Button <IconButton
variant="dummy" variant="destructive"
size="none"
onClick={() => onClick={() =>
handleRemoveMember( handleRemoveMember(
member.User.id, member.User.id,
@@ -581,8 +585,8 @@ function OrganisationsDialog({
) )
} }
> >
<X className="size-5 text-destructive" /> <X className="size-5" />
</Button> </IconButton>
</> </>
)} )}
</div> </div>
@@ -802,9 +806,9 @@ function OrganisationsDialog({
asChild={false} asChild={false}
className="w-9 h-9" className="w-9 h-9"
/> />
<Button <IconButton
variant="outline" variant="outline"
size="icon" size="md"
onClick={() => void handleCreateStatus()} onClick={() => void handleCreateStatus()}
disabled={ disabled={
newStatusName.trim().length > newStatusName.trim().length >
@@ -812,7 +816,7 @@ function OrganisationsDialog({
} }
> >
<Plus className="size-4" /> <Plus className="size-4" />
</Button> </IconButton>
</div> </div>
{statusError && ( {statusError && (
<p className="text-xs text-destructive"> <p className="text-xs text-destructive">

View File

@@ -1,8 +1,8 @@
import { CheckIcon, ServerIcon, Undo2 } from "lucide-react"; import { CheckIcon, ServerIcon, Undo2 } from "lucide-react";
import { type ReactNode, useState } from "react"; import { type ReactNode, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { IconButton } from "@/components/ui/icon-button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { getServerURL } from "@/lib/utils"; import { getServerURL } from "@/lib/utils";
@@ -113,14 +113,13 @@ export function ServerConfigurationDialog({ trigger }: { trigger?: ReactNode })
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild> <DialogTrigger asChild>
{trigger || ( {trigger || (
<Button <IconButton
variant="ghost" size="lg"
size="icon"
className="absolute top-2 right-2" className="absolute top-2 right-2"
title="Server Configuration" title={"Server Configuration"}
> >
<ServerIcon className="size-4" /> <ServerIcon className="size-4" />
</Button> </IconButton>
)} )}
</DialogTrigger> </DialogTrigger>
@@ -147,25 +146,23 @@ export function ServerConfigurationDialog({ trigger }: { trigger?: ReactNode })
className={!isValid ? "border-destructive" : ""} className={!isValid ? "border-destructive" : ""}
spellCheck={false} spellCheck={false}
/> />
<Button <IconButton
type="button" variant={canSave ? "primary" : "outline"}
size="icon" size="md"
variant={canSave ? "default" : "outline"}
disabled={!canSave || isCheckingHealth} disabled={!canSave || isCheckingHealth}
onClick={handleSave} onClick={handleSave}
> >
<CheckIcon className="size-4" /> <CheckIcon className="size-4" />
</Button> </IconButton>
<Button <IconButton
type="button"
size="icon"
variant="secondary" variant="secondary"
size="md"
disabled={!isNotDefault || isCheckingHealth} disabled={!isNotDefault || isCheckingHealth}
onClick={handleResetToDefault} onClick={handleResetToDefault}
title="Reset to default" title="Reset to default"
> >
<Undo2 className="size-4" /> <Undo2 className="size-4" />
</Button> </IconButton>
</div> </div>
{!isValid && ( {!isValid && (
<Label className="text-destructive text-sm">Please enter a valid URL</Label> <Label className="text-destructive text-sm">Please enter a valid URL</Label>

View File

@@ -1,6 +1,6 @@
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from "lucide-react";
import { useTheme } from "@/components/theme-provider"; import { useTheme } from "@/components/theme-provider";
import { Button } from "@/components/ui/button"; import { IconButton } from "@/components/ui/icon-button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function ThemeToggle({ className }: { className?: string }) { function ThemeToggle({ className }: { className?: string }) {
@@ -14,16 +14,14 @@ function ThemeToggle({ className }: { className?: string }) {
const isDark = resolvedTheme === "dark"; const isDark = resolvedTheme === "dark";
return ( return (
<Button <IconButton
type="button" size="md"
variant="dummy"
size="icon"
className={cn("hover:text-muted-foreground", className)} className={cn("hover:text-muted-foreground", className)}
onClick={() => setTheme(isDark ? "light" : "dark")} onClick={() => setTheme(isDark ? "light" : "dark")}
title={isDark ? "Switch to light mode" : "Switch to dark mode"} title={isDark ? "Switch to light mode" : "Switch to dark mode"}
> >
{isDark ? <Sun className="size-5" /> : <Moon className="size-5" />} {isDark ? <Sun className="size-5" /> : <Moon className="size-5" />}
</Button> </IconButton>
); );
} }

View File

@@ -0,0 +1,44 @@
import { cva, type VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cn } from "@/lib/utils";
const iconButtonVariants = cva(
"cursor-pointer inline-flex items-center justify-center [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none disabled:pointer-events-none disabled:opacity-50 focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "hover:text-foreground/70",
destructive: "text-destructive hover:text-destructive/70",
yellow: "text-yellow-500 hover:text-yellow-500/70",
green: "text-green-500 hover:text-green-500/70",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
outline: "border bg-transparent dark:hover:bg-muted/40",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
},
size: {
default: "w-6 h-6",
sm: "w-5 h-5",
md: "w-9 h-9",
lg: "w-10 h-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function IconButton({
className,
variant,
size,
...props
}: React.ComponentProps<"button"> & VariantProps<typeof iconButtonVariants>) {
return (
<button type="button" className={cn(iconButtonVariants({ variant, size, className }))} {...props} />
);
}
export { IconButton, iconButtonVariants };