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 React, { useRef } from 'react';
import { Button as AriaButton } from 'react-aria-components';
import { useButton } from 'react-aria';
import { cn } from '~/utils/cn';
type Props = Parameters<typeof AriaButton>[0] & {
readonly control?: [boolean, Dispatch<SetStateAction<boolean>>];
readonly variant?: 'heavy' | 'light';
};
export interface ButtonProps extends React.HTMLProps<HTMLButtonElement> {
variant?: 'heavy'
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 (
<AriaButton
{...props}
<button
ref={ref}
{...buttonProps}
className={cn(
'w-fit text-sm rounded-lg px-4 py-2',
props.variant === 'heavy'
? 'bg-main-700 dark:bg-main-800'
: 'bg-main-200 dark:bg-main-700/30',
props.variant === 'heavy'
? 'hover:bg-main-800 dark:hover:bg-main-700'
: 'hover:bg-main-300 dark:hover:bg-main-600/30',
props.variant === 'heavy'
? 'text-white'
: 'text-ui-700 dark:text-ui-300',
props.isDisabled && 'opacity-50 cursor-not-allowed',
'w-fit text-sm rounded-xl px-3 py-2',
'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,
)}
// If control is passed, set the state value
onPress={
props.control
? () => {
props.control?.[1](true);
}
: props.onPress
}
/>
);
>
{props.children}
</button>
)
}

View File

@ -1,37 +1,31 @@
import type { HTMLProps } from 'react';
import { Heading as AriaHeading } from 'react-aria-components';
import React from 'react';
import Title from '~/components/Title';
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>) {
return (
<p {...props} className={cn('text-base leading-6 my-0', props.className)} />
);
}
type Props = HTMLProps<HTMLDivElement> & {
type Props = React.HTMLProps<HTMLDivElement> & {
variant?: 'raised' | 'flat';
};
function Card(props: Props) {
interface Props extends React.HTMLProps<HTMLDivElement> {
variant?: 'raised' | 'flat';
}
function Card({ variant = 'raised', ...props }: Props) {
return (
<div
{...props}
className={cn(
'w-full max-w-md overflow-hidden rounded-xl p-4',
props.variant === 'flat'
'w-full max-w-md overflow-hidden rounded-3xl p-5',
variant === 'flat'
? 'bg-transparent shadow-none'
: 'bg-ui-50 dark:bg-ui-900 shadow-sm',
'border border-ui-200 dark:border-ui-700',
: 'bg-headplane-50/50 dark:bg-headplane-950/50 shadow-sm',
'border border-headplane-100 dark:border-headplane-800',
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 {
Button as AriaButton,
Dialog as AriaDialog,
@ -9,64 +11,16 @@ import {
} from 'react-aria-components';
import { cn } from '~/utils/cn';
type ButtonProps = Parameters<typeof AriaButton>[0] & {
readonly control?: [boolean, Dispatch<SetStateAction<boolean>>];
};
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
}
/>
);
interface ActionProps extends ButtonProps {
variant: 'cancel' | 'confirm';
}
type ActionProps = Parameters<typeof AriaButton>[0] & {
readonly variant: 'cancel' | 'confirm';
};
function Action(props: ActionProps) {
return (
<AriaButton
<Button
{...props}
type={props.variant === 'confirm' ? 'submit' : 'button'}
className={cn(
'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)}
variant={props.variant === 'cancel' ? 'light' : 'heavy'}
/>
);
}
@ -100,7 +54,7 @@ function Panel({ children, control, className }: PanelProps) {
>
<Modal
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',
'entering:animate-in exiting:animate-out',
'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" />
{data.user ? (
<Menu>
<Menu.Button
className={cn(
'rounded-full h-8 w-8',
'hover:bg-headplane-200 dark:hover:bg-headplane-800',
)}
>
<CircleUser className="w-full h-full" />
</Menu.Button>
<Menu.IconButton className="p-0">
<CircleUser />
</Menu.IconButton>
<Menu.Items>
<Menu.Item className="text-right">
<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 Button from '~/components/Button';
import IconButton from '~/components/IconButton';
import {
Button as AriaButton,
Menu as AriaMenu,
@ -8,16 +10,6 @@ import {
} from 'react-aria-components';
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]) {
return (
<Popover
@ -88,4 +80,10 @@ function Menu({ children }: { children: ReactNode }) {
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 { Links, Meta, Outlet, Scripts, ScrollRestoration, useNavigation } from 'react-router';
import { loadContext } from '~/utils/config/headplane';
import '@fontsource-variable/inter'
import { ProgressBar } from 'react-aria-components';
import { ErrorPopup } from '~/components/Error';
@ -30,7 +31,7 @@ export function Layout({ children }: { readonly children: React.ReactNode }) {
<Meta />
<Links />
</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}
<Toaster />
<ScrollRestoration />

View File

@ -96,7 +96,7 @@ export default function Page() {
return (
<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>
{data.apiKey ? (
<Form method="post">
@ -117,7 +117,7 @@ export default function Page() {
type="password"
/>
<Button className="w-full mt-2.5" variant="heavy" type="submit">
Login
Sign In
</Button>
</Form>
) : undefined}
@ -131,8 +131,8 @@ export default function Page() {
{data.oidc ? (
<Form method="POST">
<input type="hidden" name="oidc-start" value="true" />
<Button className="w-full" variant="heavy" type="submit">
Login with SSO
<Button className="w-full" type="submit">
Single Sign-On
</Button>
</Form>
) : undefined}

View File

@ -103,13 +103,7 @@ export default function New(data: NewProps) {
</Dialog.Panel>
</Dialog>
<Menu>
<Menu.Button
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',
)}
>
<Menu.Button variant="heavy">
Add Device
</Menu.Button>
<Menu.Items>