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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { CheckIcon, ServerIcon, Undo2 } from "lucide-react";
import { type ReactNode, useState } from "react";
import { createPortal } from "react-dom";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { IconButton } from "@/components/ui/icon-button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getServerURL } from "@/lib/utils";
@@ -113,14 +113,13 @@ export function ServerConfigurationDialog({ trigger }: { trigger?: ReactNode })
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
{trigger || (
<Button
variant="ghost"
size="icon"
<IconButton
size="lg"
className="absolute top-2 right-2"
title="Server Configuration"
title={"Server Configuration"}
>
<ServerIcon className="size-4" />
</Button>
</IconButton>
)}
</DialogTrigger>
@@ -147,25 +146,23 @@ export function ServerConfigurationDialog({ trigger }: { trigger?: ReactNode })
className={!isValid ? "border-destructive" : ""}
spellCheck={false}
/>
<Button
type="button"
size="icon"
variant={canSave ? "default" : "outline"}
<IconButton
variant={canSave ? "primary" : "outline"}
size="md"
disabled={!canSave || isCheckingHealth}
onClick={handleSave}
>
<CheckIcon className="size-4" />
</Button>
<Button
type="button"
size="icon"
</IconButton>
<IconButton
variant="secondary"
size="md"
disabled={!isNotDefault || isCheckingHealth}
onClick={handleResetToDefault}
title="Reset to default"
>
<Undo2 className="size-4" />
</Button>
</IconButton>
</div>
{!isValid && (
<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 { useTheme } from "@/components/theme-provider";
import { Button } from "@/components/ui/button";
import { IconButton } from "@/components/ui/icon-button";
import { cn } from "@/lib/utils";
function ThemeToggle({ className }: { className?: string }) {
@@ -14,16 +14,14 @@ function ThemeToggle({ className }: { className?: string }) {
const isDark = resolvedTheme === "dark";
return (
<Button
type="button"
variant="dummy"
size="icon"
<IconButton
size="md"
className={cn("hover:text-muted-foreground", className)}
onClick={() => setTheme(isDark ? "light" : "dark")}
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
>
{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 };