feat: redesign tooltips
This commit is contained in:
parent
c9d8052e39
commit
68babd7fe9
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 machine’s permissions match its creator’s.
|
By default, a machine’s permissions match its creator’s.
|
||||||
</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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user