feat: redesign tooltips

This commit is contained in:
Aarnav Tale 2025-02-01 15:28:04 -05:00
parent c9d8052e39
commit 68babd7fe9
No known key found for this signature in database
9 changed files with 103 additions and 70 deletions

View File

@ -12,7 +12,7 @@ function Card({ variant = 'raised', ...props }: Props) {
<div <div
{...props} {...props}
className={cn( className={cn(
'w-full max-w-md overflow-hidden rounded-3xl p-5', 'w-full max-w-md rounded-3xl p-5',
variant === 'flat' variant === 'flat'
? 'bg-transparent shadow-none' ? 'bg-transparent shadow-none'
: 'bg-headplane-50/50 dark:bg-headplane-950/50 shadow-sm', : 'bg-headplane-50/50 dark:bg-headplane-950/50 shadow-sm',

View File

@ -1,21 +1,32 @@
import React from 'react';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
export interface ChipProps { export interface ChipProps {
text: string; text: string;
className?: string; className?: string;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
} }
export default function Chip({ text, className }: ChipProps) { export default function Chip({
text,
className,
leftIcon,
rightIcon,
}: ChipProps) {
return ( return (
<span <span
className={cn( className={cn(
'text-xs px-2 py-0.5 rounded-full', 'text-xs px-2 py-0.5 rounded-full',
'text-headplane-700 dark:text-headplane-100', 'text-headplane-700 dark:text-headplane-100',
'bg-headplane-100 dark:bg-headplane-700', 'bg-headplane-100 dark:bg-headplane-700',
leftIcon || rightIcon ? 'inline-flex items-center gap-x-1' : '',
className, className,
)} )}
> >
{leftIcon}
{text} {text}
{rightIcon}
</span> </span>
); );
} }

View File

@ -156,7 +156,7 @@ function DModal(props: DModalProps) {
{...underlayProps} {...underlayProps}
aria-hidden="true" aria-hidden="true"
className={cn( className={cn(
'fixed inset-0 h-screen w-screen z-50', 'fixed inset-0 h-screen w-screen z-20',
'flex items-center justify-center', 'flex items-center justify-center',
'bg-headplane-900/15 dark:bg-headplane-900/30', 'bg-headplane-900/15 dark:bg-headplane-900/30',
'entering:animate-in exiting:animate-out', 'entering:animate-in exiting:animate-out',
@ -167,7 +167,7 @@ function DModal(props: DModalProps) {
<div <div
{...modalProps} {...modalProps}
className={cn( className={cn(
'fixed inset-0 h-screen w-screen z-50', 'fixed inset-0 h-screen w-screen z-20',
'flex items-center justify-center', 'flex items-center justify-center',
)} )}
> >

View File

@ -1,5 +1,4 @@
import Link from '~/components/Link'; import Link from '~/components/Link';
import Tooltip from '~/components/Tooltip';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
declare global { declare global {

View File

@ -34,7 +34,7 @@ export default function Popover(props: PopoverProps) {
{...popoverProps} {...popoverProps}
ref={ref} ref={ref}
className={cn( className={cn(
'z-10 shadow-sm rounded-xl overflow-hidden', 'z-10 shadow-sm rounded-xl',
'bg-white dark:bg-headplane-900', 'bg-white dark:bg-headplane-900',
'border border-headplane-200 dark:border-headplane-800', 'border border-headplane-200 dark:border-headplane-800',
className, className,

View File

@ -1,37 +1,80 @@
import type { ReactNode } from 'react'; import React, { cloneElement, useRef } from 'react';
import { import {
Button as AriaButton, AriaTooltipProps,
Tooltip as AriaTooltip, mergeProps,
TooltipTrigger, useTooltip,
} from 'react-aria-components'; useTooltipTrigger,
} from 'react-aria';
import { TooltipTriggerState, useTooltipTriggerState } from 'react-stately';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
interface Props { export interface TooltipProps extends AriaTooltipProps {
children: ReactNode; children: [React.ReactElement, React.ReactElement<TooltipBodyProps>];
className?: string;
} }
function Tooltip({ children }: Props) { function Tooltip(props: TooltipProps) {
return <TooltipTrigger delay={0}>{children}</TooltipTrigger>; const state = useTooltipTriggerState({
} ...props,
delay: 0,
closeDelay: 0,
});
function Button(props: Parameters<typeof AriaButton>[0]) { const ref = useRef<HTMLButtonElement | null>(null);
return <AriaButton {...props} />; const { triggerProps, tooltipProps } = useTooltipTrigger(
} {
...props,
delay: 0,
closeDelay: 0,
},
state,
ref,
);
function Body({ children, className }: Props) { const [component, body] = props.children;
return ( return (
<AriaTooltip <span className="relative">
className={cn( <button
'text-sm max-w-xs p-2 rounded-lg mb-2', ref={ref}
'bg-white dark:bg-ui-900 drop-shadow-sm', {...triggerProps}
'border border-gray-200 dark:border-zinc-700', className="flex items-center justify-center"
className, >
)} {component}
> </button>
{children} {state.isOpen &&
</AriaTooltip> cloneElement(body, {
...tooltipProps,
state,
})}
</span>
); );
} }
export default Object.assign(Tooltip, { Button, Body }); interface TooltipBodyProps extends AriaTooltipProps {
children: React.ReactNode;
state?: TooltipTriggerState;
className?: string;
}
function Body({ state, className, ...props }: TooltipBodyProps) {
const { tooltipProps } = useTooltip(props, state);
return (
<span
{...mergeProps(props, tooltipProps)}
className={cn(
'absolute z-50 p-3 top-full mt-1',
'outline-none rounded-3xl text-sm w-48',
'bg-white dark:bg-headplane-950',
'text-black dark:text-white',
'shadow-lg dark:shadow-md rounded-xl',
'border border-headplane-100 dark:border-headplane-800',
className,
)}
>
{props.children}
</span>
);
}
export default Object.assign(Tooltip, {
Body,
});

View File

@ -1,6 +1,7 @@
import { RepoForkedIcon } from '@primer/octicons-react'; import { RepoForkedIcon } from '@primer/octicons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useSubmit } from 'react-router'; import { useSubmit } from 'react-router';
import Chip from '~/components/Chip';
import Dialog from '~/components/Dialog'; import Dialog from '~/components/Dialog';
import Input from '~/components/Input'; import Input from '~/components/Input';
@ -82,16 +83,11 @@ export default function AddNameserver({ nameservers }: Props) {
Restrict to domain Restrict to domain
</Dialog.Text> </Dialog.Text>
<Tooltip> <Tooltip>
<Tooltip.Button <Chip
className={cn( text="Split DNS"
'text-xs rounded-md px-1.5 py-0.5', leftIcon={<RepoForkedIcon className="w-4 h-4 mr-0.5" />}
'bg-ui-200 dark:bg-ui-800', className={cn('inline-flex items-center')}
'text-ui-600 dark:text-ui-300', />
)}
>
<RepoForkedIcon className="w-4 h-4 mr-0.5" />
Split DNS
</Tooltip.Button>
<Tooltip.Body> <Tooltip.Body>
Only clients that support split DNS (Tailscale v1.8 or later Only clients that support split DNS (Tailscale v1.8 or later
for most platforms) will use this nameserver. Older clients for most platforms) will use this nameserver. Older clients

View File

@ -150,9 +150,7 @@ export default function Page() {
<span className="text-sm text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-sm text-ui-600 dark:text-ui-300 flex items-center gap-x-1">
Managed by Managed by
<Tooltip> <Tooltip>
<Tooltip.Button> <InfoIcon className="w-3.5 h-3.5" />
<InfoIcon className="w-3.5 h-3.5" />
</Tooltip.Button>
<Tooltip.Body> <Tooltip.Body>
By default, a machines permissions match its creators. By default, a machines permissions match its creators.
</Tooltip.Body> </Tooltip.Body>
@ -209,9 +207,7 @@ export default function Page() {
<span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1">
Approved Approved
<Tooltip> <Tooltip>
<Tooltip.Button> <InfoIcon className="w-3.5 h-3.5" />
<InfoIcon className="w-3.5 h-3.5" />
</Tooltip.Button>
<Tooltip.Body> <Tooltip.Body>
Traffic to these routes are being routed through this machine. Traffic to these routes are being routed through this machine.
</Tooltip.Body> </Tooltip.Body>
@ -242,9 +238,7 @@ export default function Page() {
<span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1">
Awaiting Approval Awaiting Approval
<Tooltip> <Tooltip>
<Tooltip.Button> <InfoIcon className="w-3.5 h-3.5" />
<InfoIcon className="w-3.5 h-3.5" />
</Tooltip.Button>
<Tooltip.Body> <Tooltip.Body>
This machine is advertising these routes, but they must be This machine is advertising these routes, but they must be
approved before traffic will be routed to them. approved before traffic will be routed to them.
@ -276,9 +270,7 @@ export default function Page() {
<span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1"> <span className="text-ui-600 dark:text-ui-300 flex items-center gap-x-1">
Exit Node Exit Node
<Tooltip> <Tooltip>
<Tooltip.Button> <InfoIcon className="w-3.5 h-3.5" />
<InfoIcon className="w-3.5 h-3.5" />
</Tooltip.Button>
<Tooltip.Body> <Tooltip.Body>
Whether this machine can act as an exit node for your tailnet. Whether this machine can act as an exit node for your tailnet.
</Tooltip.Body> </Tooltip.Body>

View File

@ -1,5 +1,4 @@
import { InfoIcon } from '@primer/octicons-react'; import { InfoIcon } from '@primer/octicons-react';
import { Button, Tooltip, TooltipTrigger } from 'react-aria-components';
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router'; import { useLoaderData } from 'react-router';
@ -15,6 +14,7 @@ import { getSession } from '~/utils/sessions.server';
import { useLiveData } from '~/utils/useLiveData'; import { useLiveData } from '~/utils/useLiveData';
import { initAgentSocket, queryAgent } from '~/utils/ws-agent'; import { initAgentSocket, queryAgent } from '~/utils/ws-agent';
import Tooltip from '~/components/Tooltip';
import { menuAction } from './action'; import { menuAction } from './action';
import MachineRow from './components/machine'; import MachineRow from './components/machine';
import NewMachine from './dialogs/new'; import NewMachine from './dialogs/new';
@ -81,31 +81,23 @@ export default function Page() {
</div> </div>
<table className="table-auto w-full rounded-lg"> <table className="table-auto w-full rounded-lg">
<thead className="text-gray-500 dark:text-gray-400"> <thead className="text-gray-500 dark:text-gray-400">
<tr className="text-left uppercase text-xs font-bold px-0.5"> <tr className="text-left px-0.5">
<th className="pb-2">Name</th> <th className="pb-2">Name</th>
<th className="pb-2 w-1/4"> <th className="pb-2 w-1/4">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
Addresses <p className="uppercase text-xs font-bold">Addresses</p>
{data.magic ? ( {data.magic ? (
<TooltipTrigger delay={0}> <Tooltip>
<Button> <InfoIcon className="w-4 h-4" />
<InfoIcon className="w-4 h-4" /> <Tooltip.Body className="font-normal">
</Button>
<Tooltip
className={cn(
'text-sm max-w-xs p-2 rounded-lg mb-2',
'bg-white dark:bg-zinc-800',
'border border-gray-200 dark:border-zinc-700',
)}
>
Since MagicDNS is enabled, you can access devices based on Since MagicDNS is enabled, you can access devices based on
their name and also at{' '} their name and also at{' '}
<Code> <Code>
[name]. [name].
{data.magic} {data.magic}
</Code> </Code>
</Tooltip> </Tooltip.Body>
</TooltipTrigger> </Tooltip>
) : undefined} ) : undefined}
</div> </div>
</th> </th>