feat: cleanup button + add correct focus rings

This commit is contained in:
Aarnav Tale
2026-03-09 00:17:39 -04:00
parent 0ce411e92b
commit 37d0080cba
16 changed files with 94 additions and 81 deletions
+25 -13
View File
@@ -4,7 +4,7 @@ import { type AriaButtonOptions, useButton } from "react-aria";
import cn from "~/utils/cn"; import cn from "~/utils/cn";
export interface ButtonProps extends AriaButtonOptions<"button"> { export interface ButtonProps extends AriaButtonOptions<"button"> {
variant?: "heavy" | "light" | "danger"; variant?: "heavy" | "light" | "danger" | "ghost";
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
ref?: React.RefObject<HTMLButtonElement | null>; ref?: React.RefObject<HTMLButtonElement | null>;
@@ -20,22 +20,34 @@ export default function Button({ variant = "light", ...props }: ButtonProps) {
ref={ref} ref={ref}
{...buttonProps} {...buttonProps}
className={cn( className={cn(
"w-fit text-sm rounded-md px-3 py-2", "w-fit rounded-lg px-3.5 py-2 text-sm leading-tight",
"focus:outline-hidden focus:ring-3", "transition-colors duration-100",
props.isDisabled && "opacity-60 cursor-not-allowed", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
props.isDisabled && "pointer-events-none opacity-50",
...(variant === "heavy" ...(variant === "heavy"
? [ ? [
"bg-mist-900 dark:bg-mist-50 font-semibold", "bg-indigo-500 font-semibold text-white",
"hover:bg-mist-900/90 dark:hover:bg-mist-50/90", "hover:bg-indigo-500/90",
"text-mist-200 dark:text-mist-800", "dark:bg-indigo-500/90 dark:hover:bg-indigo-500/80",
] ]
: variant === "danger" : variant === "danger"
? ["bg-red-500 text-white font-semibold", "hover:bg-red-500/90"] ? [
: [ "bg-red-600 font-semibold text-white",
"bg-mist-100 dark:bg-mist-800/50 font-medium", "hover:bg-red-600/90",
"border border-mist-200 dark:border-mist-700", "dark:bg-red-500 dark:hover:bg-red-500/90",
"hover:bg-mist-200/90 dark:hover:bg-mist-700/50", ]
]), : variant === "ghost"
? [
"font-medium text-indigo-600 dark:text-indigo-400",
"hover:bg-indigo-50 dark:hover:bg-indigo-500/10",
]
: [
"border border-mist-200 bg-white font-medium",
"hover:bg-mist-50",
"dark:border-mist-700 dark:bg-mist-800/50",
"dark:hover:bg-mist-700/50",
]),
props.className, props.className,
)} )}
> >
+2 -1
View File
@@ -42,7 +42,8 @@ export default function Footer({ url, debug, healthy }: FooterProps) {
tabIndex={0} // Allows keyboard focus tabIndex={0} // Allows keyboard focus
className={cn( className={cn(
"blur-sm hover:blur-none focus:blur-none transition", "blur-sm hover:blur-none focus:blur-none transition",
"focus:outline-hidden focus:ring-2 rounded-xs", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 rounded-xs",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
)} )}
> >
{url} {url}
+4 -2
View File
@@ -42,7 +42,8 @@ function TabLink({ name, to, icon }: TabLinkProps) {
"after:absolute after:bottom-0 after:left-3 after:right-3", "after:absolute after:bottom-0 after:left-3 after:right-3",
"after:h-0.5 after:bg-mist-900 dark:after:bg-mist-200", "after:h-0.5 after:bg-mist-900 dark:after:bg-mist-200",
"hover:bg-mist-200 dark:hover:bg-mist-900", "hover:bg-mist-200 dark:hover:bg-mist-900",
"focus:outline-hidden focus:ring-3", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
isActive ? "after:visible" : "after:invisible", isActive ? "after:visible" : "after:invisible",
) )
} }
@@ -60,7 +61,8 @@ function Link({ href, text }: LinkProps) {
<a <a
className={cn( className={cn(
"hidden sm:block hover:underline text-sm", "hidden sm:block hover:underline text-sm",
"focus:outline-hidden focus:ring-3 rounded-md", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 rounded-md",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
)} )}
href={href} href={href}
rel="noreferrer" rel="noreferrer"
+9 -10
View File
@@ -22,19 +22,18 @@ export default function IconButton({ variant = "light", ...props }: IconButtonPr
{...buttonProps} {...buttonProps}
aria-label={props.label} aria-label={props.label}
className={cn( className={cn(
"rounded-full flex items-center justify-center p-1", "flex items-center justify-center rounded-full p-1",
"focus:outline-hidden focus:ring-3", "transition-colors duration-100",
props.isDisabled && "opacity-60 cursor-not-allowed", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
props.isDisabled && "pointer-events-none opacity-50",
...(variant === "heavy" ...(variant === "heavy"
? [ ? [
"bg-mist-900 dark:bg-mist-50 font-semibold", "bg-indigo-500 font-semibold text-white",
"hover:bg-mist-900/90 dark:hover:bg-mist-50/90", "hover:bg-indigo-500/90",
"text-mist-200 dark:text-mist-800", "dark:bg-indigo-500/90 dark:hover:bg-indigo-500/80",
] ]
: [ : ["bg-mist-100 dark:bg-mist-700/30", "hover:bg-mist-200/90 dark:hover:bg-mist-800/30"]),
"bg-mist-100 dark:bg-mist-700/30 font-medium",
"hover:bg-mist-200/90 dark:hover:bg-mist-800/30",
]),
props.className, props.className,
)} )}
> >
+2 -1
View File
@@ -54,7 +54,8 @@ export default function Input(props: InputProps) {
{...inputProps} {...inputProps}
className={cn( className={cn(
"rounded-md px-3 py-2", "rounded-md px-3 py-2",
"focus:outline-hidden focus:ring-3", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
"bg-white dark:bg-mist-900", "bg-white dark:bg-mist-900",
"border border-mist-100 dark:border-mist-800", "border border-mist-100 dark:border-mist-800",
className, className,
+27 -30
View File
@@ -1,35 +1,32 @@
import { ExternalLink } from 'lucide-react'; import { ExternalLink } from "lucide-react";
import cn from '~/utils/cn';
import cn from "~/utils/cn";
export interface LinkProps { export interface LinkProps {
to: string; to: string;
name: string; name: string;
children: string; children: string;
className?: string; className?: string;
} }
export default function Link({ export default function Link({ to, name: alt, children, className }: LinkProps) {
to, return (
name: alt, <a
children, href={to}
className, aria-label={alt}
}: LinkProps) { target="_blank"
return ( rel="noreferrer"
<a className={cn(
href={to} "inline-flex items-center gap-x-0.5",
aria-label={alt} "text-blue-500 hover:text-blue-700",
target="_blank" "dark:text-blue-400 dark:hover:text-blue-300",
rel="noreferrer" "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 rounded-md",
className={cn( "dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
'inline-flex items-center gap-x-0.5', className,
'text-blue-500 hover:text-blue-700', )}
'dark:text-blue-400 dark:hover:text-blue-300', >
'focus:outline-hidden focus:ring-3 rounded-md', {children}
className, <ExternalLink className="w-3.5" />
)} </a>
> );
{children}
<ExternalLink className="w-3.5" />
</a>
);
} }
+2 -1
View File
@@ -43,7 +43,8 @@ export default function NumberInput(props: InputProps) {
{...groupProps} {...groupProps}
className={cn( className={cn(
"flex items-center gap-1 rounded-md pr-1", "flex items-center gap-1 rounded-md pr-1",
"focus-within:outline-hidden focus-within:ring-3", "focus-within:outline-hidden focus-within:ring-2 focus-within:ring-indigo-500/40 focus-within:ring-offset-1",
"dark:focus-within:ring-indigo-400/40 dark:focus-within:ring-offset-mist-900",
"bg-white dark:bg-mist-900", "bg-white dark:bg-mist-900",
"border border-mist-100 dark:border-mist-800", "border border-mist-100 dark:border-mist-800",
)} )}
+2 -1
View File
@@ -43,7 +43,8 @@ function Option({ item, state }: OptionsOptionProps) {
className={cn( className={cn(
"pl-0.5 pr-2 py-0.5 rounded-md cursor-pointer", "pl-0.5 pr-2 py-0.5 rounded-md cursor-pointer",
"aria-selected:bg-mist-100 dark:aria-selected:bg-mist-950", "aria-selected:bg-mist-100 dark:aria-selected:bg-mist-950",
"focus:outline-hidden focus:ring-3 z-10", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 z-10",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
"border border-mist-100 dark:border-mist-800", "border border-mist-100 dark:border-mist-800",
)} )}
> >
+3 -1
View File
@@ -62,7 +62,9 @@ function Radio({ children, label, className, ...props }: RadioProps) {
className={cn( className={cn(
"w-5 h-5 aspect-square rounded-full p-1 border-2", "w-5 h-5 aspect-square rounded-full p-1 border-2",
"border border-mist-600 dark:border-mist-300", "border border-mist-600 dark:border-mist-300",
isFocusVisible ? "ring-4" : "", isFocusVisible
? "ring-2 ring-indigo-500/40 ring-offset-1 dark:ring-indigo-400/40 dark:ring-offset-mist-900"
: "",
isDisabled ? "opacity-50 cursor-not-allowed" : "", isDisabled ? "opacity-50 cursor-not-allowed" : "",
isSelected ? "border-[6px] border-mist-900 dark:border-mist-100" : "", isSelected ? "border-[6px] border-mist-900 dark:border-mist-100" : "",
className, className,
+2 -1
View File
@@ -58,7 +58,8 @@ function Select(props: SelectProps) {
</label> </label>
<div <div
className={cn( className={cn(
"flex rounded-md focus:outline-hidden focus-within:ring-3", "flex rounded-md focus:outline-hidden focus-within:ring-2 focus-within:ring-indigo-500/40 focus-within:ring-offset-1",
"dark:focus-within:ring-indigo-400/40 dark:focus-within:ring-offset-mist-900",
"bg-white dark:bg-mist-900", "bg-white dark:bg-mist-900",
"border border-mist-100 dark:border-mist-800", "border border-mist-100 dark:border-mist-800",
props.isInvalid && "ring-red-400", props.isInvalid && "ring-red-400",
+2 -1
View File
@@ -35,7 +35,8 @@ export default function Switch(props: SwitchProps) {
"bg-mist-300 dark:bg-mist-700", "bg-mist-300 dark:bg-mist-700",
"border border-transparent dark:border-mist-800", "border border-transparent dark:border-mist-800",
state.isSelected && "bg-mist-900 dark:bg-mist-950", state.isSelected && "bg-mist-900 dark:bg-mist-950",
isFocusVisible && "ring-2", isFocusVisible &&
"ring-2 ring-indigo-500/40 ring-offset-1 dark:ring-indigo-400/40 dark:ring-offset-mist-900",
props.isDisabled && "opacity-50", props.isDisabled && "opacity-50",
props.className, props.className,
)} )}
+2 -1
View File
@@ -50,7 +50,8 @@ function Tab({ item, state }: TabsTabProps) {
className={cn( className={cn(
"pl-2 pr-3 py-2.5", "pl-2 pr-3 py-2.5",
"aria-selected:bg-mist-100 dark:aria-selected:bg-mist-950", "aria-selected:bg-mist-100 dark:aria-selected:bg-mist-950",
"focus:outline-hidden focus:ring-3 z-10", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 z-10",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
"border-r border-mist-100 dark:border-mist-800", "border-r border-mist-100 dark:border-mist-800",
"first:rounded-tl-lg last:rounded-tr-lg last:border-r-0", "first:rounded-tl-lg last:rounded-tr-lg last:border-r-0",
)} )}
+2 -1
View File
@@ -34,7 +34,8 @@ function Tooltip(props: TooltipProps) {
{...triggerProps} {...triggerProps}
className={cn( className={cn(
"flex items-center justify-center", "flex items-center justify-center",
"focus:outline-hidden focus:ring-3 rounded-md", "focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 rounded-md",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
)} )}
> >
{component} {component}
+3 -8
View File
@@ -120,12 +120,11 @@ export default function MachineMenu({
) : ( ) : (
<Button <Button
className={cn( className={cn(
"py-0.5 w-fit bg-transparent border-transparent", "py-0.5",
"border group-hover:border-mist-200",
"dark:group-hover:border-mist-700",
"opacity-0 pointer-events-none group-hover:opacity-100", "opacity-0 pointer-events-none group-hover:opacity-100",
"group-hover:pointer-events-auto", "group-hover:pointer-events-auto",
)} )}
variant="ghost"
onPress={() => { onPress={() => {
// We need to use JS to open the SSH URL // We need to use JS to open the SSH URL
// in a new WINDOW since href can only // in a new WINDOW since href can only
@@ -149,11 +148,7 @@ export default function MachineMenu({
</Menu.Button> </Menu.Button>
) : ( ) : (
<Menu.IconButton <Menu.IconButton
className={cn( className="w-10 bg-transparent py-0.5 hover:bg-mist-100 dark:hover:bg-mist-800"
"py-0.5 w-10 bg-transparent border-transparent",
"border group-hover:border-mist-200",
"dark:group-hover:border-mist-700",
)}
label="Machine Options" label="Machine Options"
> >
<Ellipsis className="h-5" /> <Ellipsis className="h-5" />
+6 -3
View File
@@ -164,8 +164,9 @@ export default function Page({
)} )}
</div> </div>
<Button <Button
className={cn("px-1.5 py-0.5 rounded-md mt-1.5", "text-blue-500 dark:text-blue-400")} className="mt-1.5 px-1.5 py-0.5"
onPress={() => setShowRouting(true)} onPress={() => setShowRouting(true)}
variant="ghost"
> >
Edit Edit
</Button> </Button>
@@ -193,8 +194,9 @@ export default function Page({
)} )}
</div> </div>
<Button <Button
className={cn("px-1.5 py-0.5 rounded-md mt-1.5", "text-blue-500 dark:text-blue-400")} className="mt-1.5 px-1.5 py-0.5"
onPress={() => setShowRouting(true)} onPress={() => setShowRouting(true)}
variant="ghost"
> >
Edit Edit
</Button> </Button>
@@ -225,8 +227,9 @@ export default function Page({
)} )}
</div> </div>
<Button <Button
className={cn("px-1.5 py-0.5 rounded-md mt-1.5", "text-blue-500 dark:text-blue-400")} className="mt-1.5 px-1.5 py-0.5"
onPress={() => setShowRouting(true)} onPress={() => setShowRouting(true)}
variant="ghost"
> >
Edit Edit
</Button> </Button>
+1 -6
View File
@@ -3,7 +3,6 @@ import { useState } from "react";
import Menu from "~/components/Menu"; import Menu from "~/components/Menu";
import type { Machine, User } from "~/types"; import type { Machine, User } from "~/types";
import cn from "~/utils/cn";
import Delete from "../dialogs/delete-user"; import Delete from "../dialogs/delete-user";
import LinkUser from "../dialogs/link-user"; import LinkUser from "../dialogs/link-user";
@@ -77,11 +76,7 @@ export default function UserMenu({ user, headscaleUsers, currentLink }: MenuProp
<Menu disabledKeys={disabledKeys}> <Menu disabledKeys={disabledKeys}>
<Menu.IconButton <Menu.IconButton
className={cn( className="w-10 bg-transparent py-0.5 hover:bg-mist-100 dark:hover:bg-mist-800"
"w-10 border-transparent bg-transparent py-0.5",
"border group-hover:border-mist-200",
"dark:group-hover:border-mist-700",
)}
label="User Options" label="User Options"
> >
<Ellipsis className="h-5" /> <Ellipsis className="h-5" />