feat: begin redesign and component unification
This commit is contained in:
parent
ed0cdbdf4d
commit
ec36876b9f
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
42
app/components/IconButton.tsx
Normal file
42
app/components/IconButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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
13
app/components/Title.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 />
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user