feat: begin redesign and component unification

This commit is contained in:
Aarnav Tale 2025-01-19 15:24:03 +00:00
parent ed0cdbdf4d
commit ec36876b9f
No known key found for this signature in database
10 changed files with 124 additions and 131 deletions

View File

@ -1,38 +1,40 @@
import type { Dispatch, SetStateAction } from 'react'; import type { Dispatch, SetStateAction } from 'react';
import React, { useRef } from 'react';
import { Button as AriaButton } from 'react-aria-components'; import { Button as AriaButton } from 'react-aria-components';
import { useButton } from 'react-aria';
import { cn } from '~/utils/cn'; import { cn } from '~/utils/cn';
type Props = Parameters<typeof AriaButton>[0] & { export interface ButtonProps extends React.HTMLProps<HTMLButtonElement> {
readonly control?: [boolean, Dispatch<SetStateAction<boolean>>]; variant?: 'heavy'
readonly variant?: 'heavy' | 'light'; isDisabled?: boolean
}; children?: React.ReactNode
}
export default function Button({ variant = 'light', ...props }: Props) {
const ref = useRef<HTMLButtonElement | null>(null);
const { buttonProps } = useButton(props, ref);
export default function Button(props: Props) {
return ( return (
<AriaButton <button
{...props} ref={ref}
{...buttonProps}
className={cn( className={cn(
'w-fit text-sm rounded-lg px-4 py-2', 'w-fit text-sm rounded-xl px-3 py-2',
props.variant === 'heavy' 'focus:outline-none focus:ring',
? 'bg-main-700 dark:bg-main-800' props.isDisabled && 'opacity-60 cursor-not-allowed',
: 'bg-main-200 dark:bg-main-700/30', ...(variant === 'heavy'
props.variant === 'heavy' ? [
? 'hover:bg-main-800 dark:hover:bg-main-700' 'bg-headplane-900 dark:bg-headplane-50 font-semibold',
: 'hover:bg-main-300 dark:hover:bg-main-600/30', 'hover:bg-headplane-900/90 dark:hover:bg-headplane-50/90',
props.variant === 'heavy' 'text-headplane-200 dark:text-headplane-800'
? 'text-white' ] : [
: 'text-ui-700 dark:text-ui-300', 'bg-headplane-100 dark:bg-headplane-700/30 font-medium',
props.isDisabled && 'opacity-50 cursor-not-allowed', 'hover:bg-headplane-200/90 dark:hover:bg-headplane-800/30',
]),
props.className, props.className,
)} )}
// If control is passed, set the state value >
onPress={ {props.children}
props.control </button>
? () => { )
props.control?.[1](true);
}
: props.onPress
}
/>
);
} }

View File

@ -1,37 +1,31 @@
import type { HTMLProps } from 'react'; import React from 'react';
import { Heading as AriaHeading } from 'react-aria-components'; import Title from '~/components/Title';
import { cn } from '~/utils/cn'; import { cn } from '~/utils/cn';
function Title(props: Parameters<typeof AriaHeading>[0]) {
return (
<AriaHeading
{...props}
slot="title"
className={cn('text-lg font-semibold leading-6 mb-5', props.className)}
/>
);
}
function Text(props: React.HTMLProps<HTMLParagraphElement>) { function Text(props: React.HTMLProps<HTMLParagraphElement>) {
return ( return (
<p {...props} className={cn('text-base leading-6 my-0', props.className)} /> <p {...props} className={cn('text-base leading-6 my-0', props.className)} />
); );
} }
type Props = HTMLProps<HTMLDivElement> & { type Props = React.HTMLProps<HTMLDivElement> & {
variant?: 'raised' | 'flat'; variant?: 'raised' | 'flat';
}; };
function Card(props: Props) { interface Props extends React.HTMLProps<HTMLDivElement> {
variant?: 'raised' | 'flat';
}
function Card({ variant = 'raised', ...props }: Props) {
return ( return (
<div <div
{...props} {...props}
className={cn( className={cn(
'w-full max-w-md overflow-hidden rounded-xl p-4', 'w-full max-w-md overflow-hidden rounded-3xl p-5',
props.variant === 'flat' variant === 'flat'
? 'bg-transparent shadow-none' ? 'bg-transparent shadow-none'
: 'bg-ui-50 dark:bg-ui-900 shadow-sm', : 'bg-headplane-50/50 dark:bg-headplane-950/50 shadow-sm',
'border border-ui-200 dark:border-ui-700', 'border border-headplane-100 dark:border-headplane-800',
props.className, props.className,
)} )}
> >

View File

@ -1,4 +1,6 @@
import type { Dispatch, ReactNode, SetStateAction } from 'react'; import React, { Dispatch, ReactNode, SetStateAction } from 'react';
import Button, { ButtonProps } from '~/components/Button';
import Title from '~/components/Title';
import { import {
Button as AriaButton, Button as AriaButton,
Dialog as AriaDialog, Dialog as AriaDialog,
@ -9,64 +11,16 @@ import {
} from 'react-aria-components'; } from 'react-aria-components';
import { cn } from '~/utils/cn'; import { cn } from '~/utils/cn';
type ButtonProps = Parameters<typeof AriaButton>[0] & { interface ActionProps extends ButtonProps {
readonly control?: [boolean, Dispatch<SetStateAction<boolean>>]; variant: 'cancel' | 'confirm';
};
function Button(props: ButtonProps) {
return (
<AriaButton
{...props}
aria-label="Dialog"
className={cn(
'w-fit text-sm rounded-lg px-4 py-2',
'bg-main-700 dark:bg-main-800 text-white',
'hover:bg-main-800 dark:hover:bg-main-700',
props.isDisabled && 'opacity-50 cursor-not-allowed',
props.className,
)}
// If control is passed, set the state value
onPress={
props.control
? () => {
props.control?.[1](true);
}
: undefined
}
/>
);
} }
type ActionProps = Parameters<typeof AriaButton>[0] & {
readonly variant: 'cancel' | 'confirm';
};
function Action(props: ActionProps) { function Action(props: ActionProps) {
return ( return (
<AriaButton <Button
{...props} {...props}
type={props.variant === 'confirm' ? 'submit' : 'button'} type={props.variant === 'confirm' ? 'submit' : 'button'}
className={cn( variant={props.variant === 'cancel' ? 'light' : 'heavy'}
'px-4 py-2 rounded-lg',
props.isDisabled && 'opacity-50 cursor-not-allowed',
props.variant === 'cancel'
? 'text-ui-700 dark:text-ui-300'
: 'text-ui-300 dark:text-ui-300',
props.variant === 'confirm'
? 'bg-main-700 dark:bg-main-700 pressed:bg-main-800 dark:pressed:bg-main-800'
: 'bg-ui-200 dark:bg-ui-800 pressed:bg-ui-300 dark:pressed:bg-ui-700',
props.className,
)}
/>
);
}
function Title(props: Parameters<typeof AriaHeading>[0]) {
return (
<AriaHeading
{...props}
slot="title"
className={cn('text-lg font-semibold leading-6 mb-5', props.className)}
/> />
); );
} }
@ -100,7 +54,7 @@ function Panel({ children, control, className }: PanelProps) {
> >
<Modal <Modal
className={cn( className={cn(
'w-full max-w-md overflow-hidden rounded-xl p-4', 'w-full max-w-md overflow-hidden rounded-2xl p-4',
'bg-ui-50 dark:bg-ui-900 shadow-lg', 'bg-ui-50 dark:bg-ui-900 shadow-lg',
'entering:animate-in exiting:animate-out', 'entering:animate-in exiting:animate-out',
'dark:border dark:border-ui-700', 'dark:border dark:border-ui-700',

View File

@ -86,14 +86,9 @@ export default function Header(data: Props) {
<Link href="https://github.com/juanfont/headscale" text="Headscale" /> <Link href="https://github.com/juanfont/headscale" text="Headscale" />
{data.user ? ( {data.user ? (
<Menu> <Menu>
<Menu.Button <Menu.IconButton className="p-0">
className={cn( <CircleUser />
'rounded-full h-8 w-8', </Menu.IconButton>
'hover:bg-headplane-200 dark:hover:bg-headplane-800',
)}
>
<CircleUser className="w-full h-full" />
</Menu.Button>
<Menu.Items> <Menu.Items>
<Menu.Item className="text-right"> <Menu.Item className="text-right">
<p className="font-bold">{data.user.name}</p> <p className="font-bold">{data.user.name}</p>

View File

@ -0,0 +1,42 @@
import type { Dispatch, SetStateAction } from 'react';
import React, { useRef } from 'react';
import { Button as AriaButton } from 'react-aria-components';
import { useButton } from 'react-aria';
import { cn } from '~/utils/cn';
interface Props extends React.HTMLProps<HTMLButtonElement> {
variant?: 'heavy'
isDisabled?: boolean
children: React.ReactNode
label: string
}
export default function IconButton({ variant = 'light', ...props }: Props) {
const ref = useRef<HTMLButtonElement | null>(null);
const { buttonProps } = useButton(props, ref);
return (
<button
ref={ref}
{...buttonProps}
aria-label={props.label}
className={cn(
'rounded-full flex items-center justify-center p-1',
'focus:outline-none focus:ring',
props.isDisabled && 'opacity-60 cursor-not-allowed',
...(variant === 'heavy'
? [
'bg-headplane-900 dark:bg-headplane-50 font-semibold',
'hover:bg-headplane-900/90 dark:hover:bg-headplane-50/90',
'text-headplane-200 dark:text-headplane-800'
] : [
'bg-headplane-100 dark:bg-headplane-700/30 font-medium',
'hover:bg-headplane-200/90 dark:hover:bg-headplane-800/30',
]),
props.className,
)}
>
{props.children}
</button>
)
}

View File

@ -1,4 +1,6 @@
import type { Dispatch, ReactNode, SetStateAction } from 'react'; import type { Dispatch, ReactNode, SetStateAction } from 'react';
import Button from '~/components/Button';
import IconButton from '~/components/IconButton';
import { import {
Button as AriaButton, Button as AriaButton,
Menu as AriaMenu, Menu as AriaMenu,
@ -8,16 +10,6 @@ import {
} from 'react-aria-components'; } from 'react-aria-components';
import { cn } from '~/utils/cn'; import { cn } from '~/utils/cn';
function Button(props: Parameters<typeof AriaButton>[0]) {
return (
<AriaButton
{...props}
className={cn('outline-none', props.className)}
aria-label="Menu"
/>
);
}
function Items(props: Parameters<typeof AriaMenu>[0]) { function Items(props: Parameters<typeof AriaMenu>[0]) {
return ( return (
<Popover <Popover
@ -88,4 +80,10 @@ function Menu({ children }: { children: ReactNode }) {
return <MenuTrigger>{children}</MenuTrigger>; return <MenuTrigger>{children}</MenuTrigger>;
} }
export default Object.assign(Menu, { Button, Item, ItemButton, Items }); export default Object.assign(Menu, {
IconButton,
Button,
Item,
ItemButton,
Items
});

13
app/components/Title.tsx Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
export interface TitleProps {
children: React.ReactNode;
}
export default function Title({ children }: TitleProps) {
return (
<h3 className="text-2xl font-bold mb-6">
{children}
</h3>
);
}

View File

@ -1,6 +1,7 @@
import type { LoaderFunctionArgs, LinksFunction, MetaFunction } from 'react-router'; import type { LoaderFunctionArgs, LinksFunction, MetaFunction } from 'react-router';
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useNavigation } from 'react-router'; import { Links, Meta, Outlet, Scripts, ScrollRestoration, useNavigation } from 'react-router';
import { loadContext } from '~/utils/config/headplane'; import { loadContext } from '~/utils/config/headplane';
import '@fontsource-variable/inter'
import { ProgressBar } from 'react-aria-components'; import { ProgressBar } from 'react-aria-components';
import { ErrorPopup } from '~/components/Error'; import { ErrorPopup } from '~/components/Error';
@ -30,7 +31,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
<Meta /> <Meta />
<Links /> <Links />
</head> </head>
<body className="overscroll-none dark:bg-ui-950 dark:text-ui-50"> <body className="overscroll-none dark:bg-headplane-900 dark:text-headplane-50">
{children} {children}
<Toaster /> <Toaster />
<ScrollRestoration /> <ScrollRestoration />

View File

@ -96,7 +96,7 @@ export default function Page() {
return ( return (
<div className="flex min-h-screen items-center justify-center"> <div className="flex min-h-screen items-center justify-center">
<Card className="max-w-sm m-4 sm:m-0 rounded-2xl"> <Card className="max-w-sm m-4 sm:m-0" variant="raised">
<Card.Title>Welcome to Headplane</Card.Title> <Card.Title>Welcome to Headplane</Card.Title>
{data.apiKey ? ( {data.apiKey ? (
<Form method="post"> <Form method="post">
@ -117,7 +117,7 @@ export default function Page() {
type="password" type="password"
/> />
<Button className="w-full mt-2.5" variant="heavy" type="submit"> <Button className="w-full mt-2.5" variant="heavy" type="submit">
Login Sign In
</Button> </Button>
</Form> </Form>
) : undefined} ) : undefined}
@ -131,8 +131,8 @@ export default function Page() {
{data.oidc ? ( {data.oidc ? (
<Form method="POST"> <Form method="POST">
<input type="hidden" name="oidc-start" value="true" /> <input type="hidden" name="oidc-start" value="true" />
<Button className="w-full" variant="heavy" type="submit"> <Button className="w-full" type="submit">
Login with SSO Single Sign-On
</Button> </Button>
</Form> </Form>
) : undefined} ) : undefined}

View File

@ -103,13 +103,7 @@ export default function New(data: NewProps) {
</Dialog.Panel> </Dialog.Panel>
</Dialog> </Dialog>
<Menu> <Menu>
<Menu.Button <Menu.Button variant="heavy">
className={cn(
'w-fit text-sm rounded-lg px-4 py-2',
'bg-main-700 dark:bg-main-800 text-white',
'hover:bg-main-800 dark:hover:bg-main-700',
)}
>
Add Device Add Device
</Menu.Button> </Menu.Button>
<Menu.Items> <Menu.Items>