The Coline design system: accessible React components for Coline Apps, themed live by the host workspace, and for Coline Sites in light and dark. Tailwind v4 stylesheet included. MIT.
| 5 | import { Tabs as TabsPrimitive } from "radix-ui"; |
| 6 | import { motion, useReducedMotion } from "framer-motion"; |
| 7 | import { cn } from "../lib/utils"; |
| 8 |
| 9 | /** Same raised selection and spring as Coline and Upstream navigation. */ |
| 10 | export const TABS_LAYOUT_SPRING = { type: "spring" as const, stiffness: 300, damping: 26, mass: 1 }; |
| 11 | const TabsContext = React.createContext<{ value: string | undefined; id: string } | null>(null); |
| 12 | const ListVariantContext = React.createContext<"default" | "line">("default"); |
| 13 |
| 14 | function Tabs({ className, orientation = "horizontal", value, defaultValue, onValueChange, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) { |
| 15 | const [localValue, setLocalValue] = React.useState(defaultValue); |
| 16 | const id = React.useId(); |
| 17 | const activeValue = value ?? localValue; |
| 18 | const context = React.useMemo(() => ({ value: activeValue, id }), [activeValue, id]); |
| 19 | return ( |
| 20 | <TabsContext.Provider value={context}> |
| 21 | <TabsPrimitive.Root |
| 22 | {...props} |
| 23 | {...(activeValue === undefined ? {} : { value: activeValue })} |
| 24 | orientation={orientation} |
| 25 | onValueChange={(next) => { if (value === undefined) setLocalValue(next); onValueChange?.(next); }} |
| 26 | data-slot="tabs" |
| 27 | className={cn("coline-tabs", className)} |
| 28 | /> |
| 29 | </TabsContext.Provider> |
| 30 | ); |
| 31 | } |
| 32 |
| 33 | const tabsListVariants = cva("coline-tabs-list", { |
| 34 | variants: { variant: { default: "coline-tabs-raised", line: "coline-tabs-line" } }, |
| 35 | defaultVariants: { variant: "default" }, |
| 36 | }); |
| 37 |
| 38 | function TabsList({ className, variant = "default", ...props }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) { |
| 39 | return ( |
| 40 | <ListVariantContext.Provider value={variant ?? "default"}> |
| 41 | <TabsPrimitive.List {...props} data-slot="tabs-list" data-variant={variant} className={cn(tabsListVariants({ variant }), className)} /> |
| 42 | </ListVariantContext.Provider> |
| 43 | ); |
| 44 | } |
| 45 |
| 46 | // Keep the existing explicit indicator API working without adding a second pill. |
| 47 | function hasIndicator(children: React.ReactNode): boolean { |
| 48 | return React.Children.toArray(children).some((child) => React.isValidElement<{ children?: React.ReactNode }>(child) |
| 49 | && (child.type === TabsIndicator || hasIndicator(child.props.children))); |
| 50 | } |
| 51 |
| 52 | function TabsTrigger({ className, children, value, asChild = false, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { |
| 53 | const tabs = React.useContext(TabsContext); |
| 54 | const variant = React.useContext(ListVariantContext); |
| 55 | const indicator = tabs?.value === value && variant === "default" && !hasIndicator(children) ? <TabsIndicator /> : null; |
| 56 | const content = asChild && React.isValidElement<{ children?: React.ReactNode }>(children) |
| 57 | ? React.cloneElement(children, undefined, indicator, children.props.children) |
| 58 | : <>{indicator}{children}</>; |
| 59 | return ( |
| 60 | <TabsPrimitive.Trigger {...props} value={value} asChild={asChild} data-slot="tabs-trigger" className={cn("coline-tabs-trigger", className)}> |
| 61 | {content} |
| 62 | </TabsPrimitive.Trigger> |
| 63 | ); |
| 64 | } |
| 65 |
| 66 | /** Optional customization; the raised indicator is included automatically. */ |
| 67 | function TabsIndicator({ className, layoutId = "selection" }: { className?: string; layoutId?: string }) { |
| 68 | const tabs = React.useContext(TabsContext); |
| 69 | const fallbackId = React.useId(); |
| 70 | const reducedMotion = useReducedMotion(); |
| 71 | return ( |
| 72 | <motion.div |
| 73 | aria-hidden="true" |
| 74 | data-slot="tabs-indicator" |
| 75 | layout |
| 76 | layoutId={`${tabs?.id ?? fallbackId}-${layoutId}`} |
| 77 | initial={false} |
| 78 | className={cn("coline-tabs-indicator", className)} |
| 79 | transition={reducedMotion ? { duration: 0 } : TABS_LAYOUT_SPRING} |
| 80 | /> |
| 81 | ); |
| 82 | } |
| 83 |
| 84 | function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) { |
| 85 | return <TabsPrimitive.Content {...props} data-slot="tabs-content" className={cn("coline-tabs-content", className)} />; |
| 86 | } |
| 87 |
| 88 | export { Tabs, TabsList, TabsTrigger, TabsIndicator, TabsContent, tabsListVariants }; |
| 89 |