fbd6df33b4
Adds System Preferences, Dark and Light themes, gated by the `hasThemeSwitcher` feature flag (off by default — dark stays the default theme for everyone). Old theme is now "Classic"and set as default. "System preferences" theme has both Light and Dark modes and uses your laptop settings to use a correct one. It has less color accents (specifically less colored text), and they are the same for both modes, only grayscale values change between them. And Light/Dark themes can be used separately. New Contrast setting is available for System Preferences, Dark and Light themes - it changes the contrast for the whole app. All new visual Settings live in Account.
159 lines
4.3 KiB
TypeScript
159 lines
4.3 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-background-raised",
|
|
active: "text-text-bright hover:bg-background-hover/50",
|
|
inactive: "text-text-dimmed transition hover:text-text-bright",
|
|
selected: "absolute inset-0 rounded-[2px] outline-solid outline-3 outline-primary",
|
|
},
|
|
secondary: {
|
|
base: "bg-transparent dark:bg-background-raised/50",
|
|
active: "text-text-bright",
|
|
inactive: "text-text-dimmed transition hover:text-text-bright",
|
|
selected:
|
|
"absolute inset-0 rounded bg-white border border-grid-bright dark:bg-background-raised dark:border-border-bright",
|
|
},
|
|
};
|
|
|
|
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;
|
|
/** Override the control's outer styling (e.g. a height that matches an adjacent input). */
|
|
className?: string;
|
|
};
|
|
|
|
export default function SegmentedControl({
|
|
name,
|
|
value,
|
|
defaultValue,
|
|
options,
|
|
variant = "secondary/medium",
|
|
fullWidth,
|
|
onChange,
|
|
className,
|
|
}: 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",
|
|
className
|
|
)}
|
|
>
|
|
<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>
|
|
);
|
|
}
|