feat: update some components

This commit is contained in:
Aarnav Tale
2026-03-19 14:33:12 -04:00
parent 873e6005d9
commit 54f236a332
14 changed files with 176 additions and 227 deletions
+1 -2
View File
@@ -24,9 +24,8 @@ export default function Attribute({ name, value, tooltip, isCopyable }: Attribut
>
{name}
{tooltip ? (
<Tooltip>
<Tooltip content={tooltip}>
<Info className="size-4" />
<Tooltip.Body>{tooltip}</Tooltip.Body>
</Tooltip>
) : undefined}
</dt>
+1 -1
View File
@@ -24,7 +24,7 @@ export default function Code({ isCopyable, children, className }: CodeProps) {
<button
className="absolute right-0 bottom-0"
onClick={async (event) => {
const text = Array.isArray(children) ? children.join("") : children;
const text = Array.isArray(children) ? children.join("") : String(children);
const svgs = event.currentTarget.querySelectorAll("svg");
for (const svg of svgs) {
+55 -52
View File
@@ -1,86 +1,89 @@
import { useRef } from "react";
import { AriaTabListProps, AriaTabPanelProps, useTab, useTabList, useTabPanel } from "react-aria";
import { Item, Node, TabListState, useTabListState } from "react-stately";
import { Tabs as BaseTabs } from "@base-ui/react/tabs";
import type { ComponentProps, ReactNode } from "react";
import cn from "~/utils/cn";
export interface TabsProps extends AriaTabListProps<object> {
export interface TabsProps {
label: string;
className?: string;
defaultValue?: string | number;
value?: string | number;
onValueChange?: (value: string | number) => void;
children: ReactNode;
}
function Tabs({ label, className, ...props }: TabsProps) {
const state = useTabListState(props);
const ref = useRef<HTMLDivElement | null>(null);
const { tabListProps } = useTabList(props, state, ref);
function Tabs({ label, className, children, ...props }: TabsProps) {
return (
<div className={cn("flex flex-col", className)}>
<div
{...tabListProps}
className={cn(
"flex items-center rounded-t-lg w-fit max-w-full overflow-x-auto",
"border-mist-100 dark:border-mist-800",
"border-t border-x",
)}
ref={ref}
>
{[...state.collection].map((item) => (
<Tab item={item} key={item.key} state={state} />
))}
</div>
<TabsPanel key={state.selectedItem?.key} state={state} />
</div>
<BaseTabs.Root
{...props}
defaultValue={props.defaultValue ?? 0}
aria-label={label}
className={cn("flex flex-col", className)}
>
{children}
</BaseTabs.Root>
);
}
export interface TabsTabProps {
item: Node<object>;
state: TabListState<object>;
function TabList({ children, className }: { children: ReactNode; className?: string }) {
return (
<BaseTabs.List
className={cn(
"flex items-center rounded-t-lg w-fit max-w-full",
"border-mist-100 dark:border-mist-800",
"border-t border-x",
className,
)}
>
{children}
</BaseTabs.List>
);
}
function Tab({ item, state }: TabsTabProps) {
const { key, rendered } = item;
const ref = useRef<HTMLDivElement | null>(null);
const { tabProps } = useTab({ key }, state, ref);
function Tab({
value,
children,
className,
...props
}: ComponentProps<typeof BaseTabs.Tab> & { className?: string }) {
return (
<div
{...tabProps}
<BaseTabs.Tab
value={value}
{...props}
className={cn(
"pl-2 pr-3 py-2.5",
"aria-selected:bg-mist-100 dark:aria-selected:bg-mist-950",
"data-[selected]:bg-mist-100 dark:data-[selected]:bg-mist-950",
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 z-10",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
"border-r border-mist-100 dark:border-mist-800",
"first:rounded-tl-lg last:rounded-tr-lg last:border-r-0",
className,
)}
ref={ref}
>
{rendered}
</div>
{children}
</BaseTabs.Tab>
);
}
export interface TabsPanelProps extends AriaTabPanelProps {
state: TabListState<object>;
}
function TabsPanel({ state, ...props }: TabsPanelProps) {
const ref = useRef<HTMLDivElement | null>(null);
const { tabPanelProps } = useTabPanel(props, state, ref);
function Panel({
value,
children,
className,
...props
}: ComponentProps<typeof BaseTabs.Panel> & { className?: string }) {
return (
<div
{...tabPanelProps}
<BaseTabs.Panel
value={value}
{...props}
className={cn(
"w-full overflow-clip rounded-b-lg rounded-r-lg",
"border border-mist-100 dark:border-mist-800",
className,
)}
ref={ref}
>
{state.selectedItem?.props.children}
</div>
{children}
</BaseTabs.Panel>
);
}
export default Object.assign(Tabs, { Item });
export { Tabs, TabList as TabsList, Tab as TabsTab, Panel as TabsPanel };
+11 -10
View File
@@ -11,22 +11,23 @@ export interface ExitNodeTagProps {
export function ExitNodeTag({ isEnabled }: ExitNodeTagProps) {
return (
<Tooltip>
<Chip
text="Exit Node"
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
/>
<Tooltip.Body>
{isEnabled ? (
<Tooltip
content={
isEnabled ? (
<>This machine is acting as an exit node.</>
) : (
<>
This machine is requesting to be used as an exit node. Review this from the "Edit route
settings..." option in the machine's menu.
</>
)}
</Tooltip.Body>
)
}
>
<Chip
text="Exit Node"
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
/>
</Tooltip>
);
}
+12 -11
View File
@@ -14,23 +14,24 @@ export function ExpiryTag({ variant, expiry }: ExpiryTagProps) {
});
return (
<Tooltip>
<Chip
text={
variant === "expired" ? `Expired ${formatter.format(new Date(expiry!))}` : "No expiry"
}
className="bg-mist-200 text-mist-800 dark:bg-mist-800 dark:text-mist-200"
/>
<Tooltip.Body>
{variant === "expired" ? (
<Tooltip
content={
variant === "expired" ? (
<>
This machine is expired and will not be able to connect to the network. Re-authenticate
with Tailscale on the machine to re-enable it.
</>
) : (
<>This machine has key expiry disabled and will never need to re-authenticate.</>
)}
</Tooltip.Body>
)
}
>
<Chip
text={
variant === "expired" ? `Expired ${formatter.format(new Date(expiry!))}` : "No expiry"
}
className="bg-mist-200 text-mist-800 dark:bg-mist-800 dark:text-mist-200"
/>
</Tooltip>
);
}
+1 -5
View File
@@ -5,15 +5,11 @@ import Tooltip from "../tooltip";
export function HeadplaneAgentTag() {
return (
<Tooltip>
<Tooltip content="This machine is running the Headplane agent, which allows it to provide host information in the web UI.">
<Chip
text="Headplane Agent"
className={cn("bg-purple-300 text-purple-900 dark:bg-purple-900 dark:text-purple-300")}
/>
<Tooltip.Body>
This machine is running the Headplane agent, which allows it to provide host information in
the web UI.
</Tooltip.Body>
</Tooltip>
);
}
+11 -10
View File
@@ -11,22 +11,23 @@ export interface SubnetTagProps {
export function SubnetTag({ isEnabled }: SubnetTagProps) {
return (
<Tooltip>
<Chip
text="Subnets"
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
/>
<Tooltip.Body>
{isEnabled ? (
<Tooltip
content={
isEnabled ? (
<>This machine advertises subnet routes.</>
) : (
<>
This machine has unadvertised subnet routes. Review this from the "Edit route
settings..." option in the machine's menu.
</>
)}
</Tooltip.Body>
)
}
>
<Chip
text="Subnets"
className={cn("bg-blue-300 text-blue-900 dark:bg-blue-900 dark:text-blue-300")}
rightIcon={isEnabled ? undefined : <Info className="h-full w-fit" />}
/>
</Tooltip>
);
}
+1 -5
View File
@@ -5,15 +5,11 @@ import Tooltip from "../tooltip";
export function TailscaleSSHTag() {
return (
<Tooltip>
<Tooltip content="This machine advertises Tailscale SSH, which allows you to authenticate SSH credentials using your Tailscale account and via the Headplane web UI.">
<Chip
text="Tailscale SSH"
className={cn("bg-lime-500 text-lime-900 dark:bg-lime-900 dark:text-lime-500")}
/>
<Tooltip.Body>
This machine advertises Tailscale SSH, which allows you to authenticate SSH credentials
using your Tailscale account and via the Headplane web UI.
</Tooltip.Body>
</Tooltip>
);
}
+36 -71
View File
@@ -1,80 +1,45 @@
import React, { cloneElement, useRef } from "react";
import { AriaTooltipProps, mergeProps, useTooltip, useTooltipTrigger } from "react-aria";
import { TooltipTriggerState, useTooltipTriggerState } from "react-stately";
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
import type { ReactNode } from "react";
import cn from "~/utils/cn";
export interface TooltipProps extends AriaTooltipProps {
children: [React.ReactElement, React.ReactElement<TooltipBodyProps>];
}
function Tooltip(props: TooltipProps) {
const state = useTooltipTriggerState({
...props,
delay: 0,
closeDelay: 0,
});
const ref = useRef<HTMLButtonElement | null>(null);
const { triggerProps, tooltipProps } = useTooltipTrigger(
{
...props,
delay: 0,
closeDelay: 0,
},
state,
ref,
);
const [component, body] = props.children;
return (
<span className="relative">
<button
ref={ref}
{...triggerProps}
className={cn(
"flex items-center justify-center",
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1 rounded-md",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
)}
>
{component}
</button>
{state.isOpen &&
cloneElement(body, {
...tooltipProps,
state,
})}
</span>
);
}
interface TooltipBodyProps extends AriaTooltipProps {
children: React.ReactNode;
state?: TooltipTriggerState;
export interface TooltipProps {
children: ReactNode;
content: ReactNode;
className?: string;
}
function Body({ state, className, ...props }: TooltipBodyProps) {
const { tooltipProps } = useTooltip(props, state);
export default function Tooltip({ children, content, className }: TooltipProps) {
return (
<span
{...mergeProps(props, tooltipProps)}
className={cn(
"absolute z-50 p-3 top-full mt-1",
"outline-hidden rounded-lg text-sm w-48",
"bg-white dark:bg-mist-950",
"text-black dark:text-white",
"shadow-overlay",
"border border-mist-100 dark:border-mist-800",
className,
)}
>
{props.children}
</span>
<BaseTooltip.Root>
<BaseTooltip.Trigger
delay={0}
closeDelay={0}
className={cn(
"inline-flex items-center justify-center rounded-md",
"focus:outline-hidden focus:ring-2 focus:ring-indigo-500/40 focus:ring-offset-1",
"dark:focus:ring-indigo-400/40 dark:focus:ring-offset-mist-900",
)}
>
{children}
</BaseTooltip.Trigger>
<BaseTooltip.Portal>
<BaseTooltip.Positioner sideOffset={4}>
<BaseTooltip.Popup
className={cn(
"z-50 rounded-lg p-3 text-sm w-48",
"outline-hidden",
"bg-white dark:bg-mist-950",
"text-black dark:text-white",
"shadow-overlay",
"border border-mist-100 dark:border-mist-800",
className,
)}
>
{content}
</BaseTooltip.Popup>
</BaseTooltip.Positioner>
</BaseTooltip.Portal>
</BaseTooltip.Root>
);
}
export default Object.assign(Tooltip, {
Body,
});