mirror of
https://github.com/hex248/sprint.git
synced 2026-02-08 02:33:01 +00:00
IconButton
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
packages/frontend/src/components/ui/icon-button.tsx
Normal file
44
packages/frontend/src/components/ui/icon-button.tsx
Normal 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 };
|
||||||
Reference in New Issue
Block a user