Files
James Ritchie b4e08bddec Fix(webapp): metrics UI improvements (#3063)
- Lots of small UI improvements
- Toggle full screen charts with "V" shortcut

<img width="3504" height="2286" alt="CleanShot 2026-02-14 at 20 06
30@2x"
src="https://github.com/user-attachments/assets/32403661-06b3-4f6c-a074-243b3f43197d"
/>

<img width="362" height="222" alt="CleanShot 2026-02-14 at 20 06 58@2x"
src="https://github.com/user-attachments/assets/ee4f5859-4b71-482e-9636-5abd78d9f623"
/>

<img width="434" height="360" alt="CleanShot 2026-02-14 at 20 06 53@2x"
src="https://github.com/user-attachments/assets/44339348-f3f3-425b-bbf3-bb41ed6a8e59"
/>

<img width="460" height="310" alt="CleanShot 2026-02-14 at 20 06 48@2x"
src="https://github.com/user-attachments/assets/eb3b6af6-b88a-4677-8a48-acfa6b768770"
/>

<img width="502" height="332" alt="CleanShot 2026-02-14 at 20 06 45@2x"
src="https://github.com/user-attachments/assets/b788d78b-8f4a-4b18-94c8-487673f3ec7b"
/>

<img width="370" height="257" alt="CleanShot 2026-02-14 at 20 10 29@2x"
src="https://github.com/user-attachments/assets/a81e8b14-b2eb-402c-bf3b-affd0d4fde26"
/>
2026-02-14 21:26:25 +00:00

154 lines
4.1 KiB
TypeScript

import { RadioGroup } from "@headlessui/react";
import { motion } from "framer-motion";
import type { ReactNode } from "react";
import { cn } from "~/utils/cn";
const sizes = {
small: {
control: "h-6",
option: "px-2 text-xs",
container: "gap-x-0.5",
},
medium: {
control: "h-10",
option: "px-3 py-[0.13rem] text-sm",
container: "p-1 gap-x-0.5",
},
};
const theme = {
primary: {
base: "bg-charcoal-700",
active: "text-text-bright hover:bg-charcoal-750/50",
inactive: "text-text-dimmed transition hover:text-text-bright",
selected: "absolute inset-0 rounded-[2px] outline outline-3 outline-primary",
},
secondary: {
base: "bg-charcoal-700/50",
active: "text-text-bright",
inactive: "text-text-dimmed transition hover:text-text-bright",
selected: "absolute inset-0 rounded bg-charcoal-700 border border-charcoal-600",
},
};
type Size = keyof typeof sizes;
type Theme = keyof typeof theme;
type VariantStyle = {
base: string;
active: string;
inactive: string;
option: string;
container: string;
selected: string;
};
function createVariant(sizeName: Size, themeName: Theme): VariantStyle {
return {
base: cn(sizes[sizeName].control, theme[themeName].base),
active: theme[themeName].active,
inactive: theme[themeName].inactive,
option: sizes[sizeName].option,
container: sizes[sizeName].container,
selected: theme[themeName].selected,
};
}
const variants = {
"primary/small": createVariant("small", "primary"),
"primary/medium": createVariant("medium", "primary"),
"secondary/small": createVariant("small", "secondary"),
"secondary/medium": createVariant("medium", "secondary"),
} as const;
type VariantType = keyof typeof variants;
type Options = {
label: ReactNode;
value: string;
};
type SegmentedControlProps = {
name: string;
value?: string;
defaultValue?: string;
options: Options[];
variant?: VariantType;
fullWidth?: boolean;
onChange?: (value: string) => void;
};
export default function SegmentedControl({
name,
value,
defaultValue,
options,
variant = "secondary/medium",
fullWidth,
onChange,
}: SegmentedControlProps) {
const variantStyle = variants[variant];
const isPrimary = variant.startsWith("primary");
return (
<div
className={cn(
"flex rounded text-text-bright",
variantStyle.base,
fullWidth ? "w-full" : "w-fit"
)}
>
<RadioGroup
value={value}
defaultValue={defaultValue ?? options[0].value}
name={name}
onChange={(c: string) => {
if (onChange) {
onChange(c);
}
}}
className="w-full"
>
<div
className={cn("flex h-full w-full items-center justify-between", variantStyle.container)}
>
{options.map((option) => (
<RadioGroup.Option
key={option.value}
value={option.value}
className={({ checked }) =>
cn(
"relative flex h-full grow cursor-pointer text-center font-normal focus-custom",
checked ? variantStyle.active : variantStyle.inactive
)
}
>
{({ checked }) => (
<>
<div
className={cn(
"relative flex h-full w-full items-center justify-between",
variantStyle.option
)}
>
<div className="z-10 flex h-full w-full items-center justify-center">
<RadioGroup.Label as="p">{option.label}</RadioGroup.Label>
</div>
{checked && (
<motion.div
layoutId={`segmented-control-${name}`}
transition={{ duration: 0.4, type: "spring" }}
className={variantStyle.selected}
/>
)}
</div>
</>
)}
</RadioGroup.Option>
))}
</div>
</RadioGroup>
</div>
);
}