SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.0 KB · 91 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { cn } from "@/lib/utils";45export interface SegmentedOption<T extends string> {6  value: T;7  label: React.ReactNode;8  icon?: React.ReactNode;9  disabled?: boolean;10  count?: number;11}1213/**14 * Segmented control (iOS-like). Keyboard: ←/→ move, Enter/Space select. Scrolls horizontally when it overflows.15 */16export function Segmented<T extends string>({ value, onChange, options, size = "md", className, ariaLabel, fill }: { value: T; onChange: (v: T) => void; options: SegmentedOption<T>[]; size?: "sm" | "md" | "lg"; className?: string; ariaLabel?: string; fill?: boolean }) {17  const ref = React.useRef<HTMLDivElement>(null);18  const idx = options.findIndex((o) => o.value === value);19  const onKey = (e: React.KeyboardEvent) => {20    if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;21    e.preventDefault();22    const dir = e.key === "ArrowLeft" ? -1 : 1;23    let i = idx;24    for (let n = 0; n < options.length; n++) {25      i = (i + dir + options.length) % options.length;26      if (!options[i].disabled) break;27    }28    onChange(options[i].value);29    (ref.current?.children[i] as HTMLElement | undefined)?.focus();30  };31  const h = size === "sm" ? "h-8" : size === "lg" ? "h-11" : "h-9";32  const item = size === "sm" ? "h-6 px-2.5 text-[12.5px]" : size === "lg" ? "h-9 px-4 text-[15px]" : "h-7 px-3 text-[13px]";33  return (34    <div ref={ref} role="tablist" aria-label={ariaLabel} onKeyDown={onKey} className={cn("inline-flex max-w-full items-center gap-0.5 overflow-x-auto rounded-lg bg-bg-muted p-1 scrollbar-none", h, fill && "flex w-full", className)}>35      {options.map((o) => {36        const active = o.value === value;37        return (38          <button39            key={o.value}40            role="tab"41            type="button"42            aria-selected={active}43            tabIndex={active ? 0 : -1}44            disabled={o.disabled}45            onClick={() => onChange(o.value)}46            className={cn(47              "tap inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-md font-medium transition-[background-color,color,box-shadow] duration-150 disabled:opacity-40 [&_svg]:size-3.5 [&_svg]:shrink-0",48              item,49              fill && "flex-1",50              active ? "bg-bg-elevated text-fg shadow-xs" : "text-fg-muted hover:text-fg",51            )}52          >53            {o.icon}54            {o.label}55            {o.count !== undefined ? <span className={cn("tabular-nums text-[11px]", active ? "text-fg-subtle" : "text-fg-subtle/70")}>{o.count}</span> : null}56          </button>57        );58      })}59    </div>60  );61}6263/** Pill filter chips (multi or single select). Horizontal scroll on phones. */64export function ChipRow<T extends string>({ value, onChange, options, className, multiple }: { value: T | T[] | null; onChange: (v: T) => void; options: { value: T; label: React.ReactNode; icon?: React.ReactNode; count?: number }[]; className?: string; multiple?: boolean }) {65  const isActive = (v: T) => (Array.isArray(value) ? value.includes(v) : value === v);66  return (67    <div className={cn("flex gap-1.5 overflow-x-auto scrollbar-none", className)} role={multiple ? "group" : "radiogroup"}>68      {options.map((o) => {69        const active = isActive(o.value);70        return (71          <button72            key={o.value}73            type="button"74            role={multiple ? "checkbox" : "radio"}75            aria-checked={active}76            onClick={() => onChange(o.value)}77            className={cn(78              "tap inline-flex h-8 shrink-0 items-center gap-1.5 rounded-full border px-3 text-[13px] font-medium transition-colors [&_svg]:size-3.5",79              active ? "border-fg bg-fg text-bg" : "border-border bg-bg-elevated text-fg-muted hover:border-border-strong hover:text-fg",80            )}81          >82            {o.icon}83            {o.label}84            {o.count !== undefined ? <span className={cn("tabular-nums text-[11px]", active ? "opacity-70" : "text-fg-subtle")}>{o.count}</span> : null}85          </button>86        );87      })}88    </div>89  );90}91