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