Skip to content
File

Blob: src/components/ui.tsx

typescript108 lines
1import type { ButtonHTMLAttributes, ReactNode } from "react";
2import { cn } from "../lib/cn";
3 
4type Variant = "primary" | "subtle" | "ghost" | "danger";
5type Size = "sm" | "md";
6 
7const VARIANTS: Record<Variant, string> = {
8 primary: "bg-accent-600 text-white hover:bg-accent-500",
9 subtle: "border border-zinc-700/60 bg-zinc-800/60 text-zinc-300 hover:bg-zinc-700/60",
10 ghost: "text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-100",
11 danger: "border border-red-500/20 bg-red-500/10 text-red-400 hover:bg-red-500/20",
12};
13 
14const SIZES: Record<Size, string> = {
15 sm: "h-8 px-2.5 text-xs gap-1.5",
16 md: "h-10 px-4 text-sm gap-2",
17};
18 
19interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
20 variant?: Variant;
21 size?: Size;
22 active?: boolean;
23}
24 
25export function Button({
26 variant = "subtle",
27 size = "md",
28 active = false,
29 className,
30 children,
31 ...props
32}: ButtonProps) {
33 return (
34 <button
35 className={cn(
36 "inline-flex select-none items-center justify-center rounded-lg font-medium transition-transform active:scale-[0.98]",
37 "disabled:cursor-not-allowed disabled:opacity-60",
38 SIZES[size],
39 VARIANTS[variant],
40 active && "bg-zinc-700 text-zinc-100 ring-2 ring-accent-500/70",
41 className,
42 )}
43 {...props}
44 >
45 {children}
46 </button>
47 );
48}
49 
50interface SectionProps {
51 title: string;
52 icon?: ReactNode;
53 action?: ReactNode;
54 children: ReactNode;
55}
56 
57export function Section({ title, icon, action, children }: SectionProps) {
58 return (
59 <section className="border-b border-zinc-800/60 px-4 py-4 last:border-b-0">
60 <header className="mb-3 flex items-center justify-between">
61 <h2 className="flex items-center gap-2 text-[0.7rem] font-semibold uppercase tracking-wider text-zinc-400">
62 {icon}
63 {title}
64 </h2>
65 {action}
66 </header>
67 {children}
68 </section>
69 );
70}
71 
72interface SegmentedOption<T extends string> {
73 value: T;
74 label: string;
75}
76 
77interface SegmentedProps<T extends string> {
78 options: SegmentedOption<T>[];
79 value: T;
80 onChange: (value: T) => void;
81 ariaLabel?: string;
82}
83 
84export function Segmented<T extends string>({ options, value, onChange, ariaLabel }: SegmentedProps<T>) {
85 return (
86 <div
87 role="tablist"
88 aria-label={ariaLabel}
89 className="flex gap-0.5 rounded-lg border border-zinc-700/60 bg-zinc-800 p-0.5"
90 >
91 {options.map((opt) => (
92 <button
93 key={opt.value}
94 role="tab"
95 aria-selected={value === opt.value}
96 onClick={() => onChange(opt.value)}
97 className={cn(
98 "flex-1 rounded-md px-2 py-1.5 text-xs font-medium transition-transform",
99 value === opt.value ? "bg-zinc-700 text-zinc-100 shadow-sm" : "text-zinc-400 hover:text-zinc-100",
100 )}
101 >
102 {opt.label}
103 </button>
104 ))}
105 </div>
106 );
107}