import { defineRecipe } from '@pandacss/dev' // Scoped port: this project drives tabs via a controlled `active` prop // (real JS state) rather than the original's hidden input[radio]/label CSS-only // trick, so all the `:is(input:checked, label:has(:checked))` selector // branches in the real source are dropped in favor of a single // .tab-active/[aria-selected] check. The `lift` variant also skips // the original's radial-gradient trick that rounds the outer corners where the // active tab meets its content panel — kept the core "file folder tab" // border effect, dropped that specific polish detail. const activeSelector = '&.tab-active, &[aria-selected="true"], &[aria-current="true"]' const activeBeforeSelector = '&.tab-active:before, &[aria-selected="true"]:before, &[aria-current="true"]:before' const inactiveSelector = '&:not(.tab-active, [aria-selected="true"], [aria-current="true"], :hover)' export const tabsRecipe = defineRecipe({ className: 'tabs', base: { display: 'flex', flexWrap: 'wrap', '--tab-height': 'token(spacing.10)', '--tab-p': 'token(spacing.3)', // Tab styles via descendant '& .tabs-tab': { position: 'relative', display: 'inline-flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'center', textAlign: 'center', cursor: 'pointer', userSelect: 'none', appearance: 'none', border: 'none', background: 'none', height: 'var(--tab-height)', fontSize: 'token(fontSizes.md)', paddingInline: 'var(--tab-p)', color: 'inherit', fontFamily: 'inherit', // Smooth color change between the dimmed (inactive) and full states '@media (prefers-reduced-motion: no-preference)': { transition: 'color 0.2s ease', }, [inactiveSelector]: { color: 'color-mix(in oklab, token(colors.base-content) 50%, transparent)', }, '&:disabled': { pointerEvents: 'none', opacity: '0.4', }, '&:focus-visible': { outline: '2px solid currentColor', outlineOffset: '-5px', }, }, // Content styles via descendant '& .tabs-content': { order: '1', display: 'none', width: '100%', padding: 'token(spacing.4)', borderWidth: 'var(--border, 1px)', borderStyle: 'solid', borderColor: 'transparent', }, // Active content '& .tabs-content.active': { display: 'block', // Fade the incoming panel in (the outgoing one leaves instantly via // display:none — not transitionable without JS). '@media (prefers-reduced-motion: no-preference)': { animation: 'tab-fade-in 0.25s ease', }, }, }, variants: { variant: { box: { backgroundColor: 'base-200', padding: 'token(spacing.1)', borderRadius: 'calc(min(var(--tab-height) / 2, var(--radius-field)) + 0.25rem)', '& .tabs-tab': { borderRadius: 'var(--radius-field)', // Fade the highlight in on activation and out on deactivation '@media (prefers-reduced-motion: no-preference)': { transition: 'color 0.2s ease, background-color 0.25s ease, box-shadow 0.25s ease', }, [activeSelector]: { backgroundColor: 'base-100', boxShadow: '0 1px 3px color-mix(in oklab, black 15%, transparent)', }, }, '& .tabs-content': {}, }, border: { borderBottomWidth: '1px', borderColor: 'base-300', '& .tabs-tab': { '@media (prefers-reduced-motion: no-preference)': { transition: 'color 0.2s ease', }, '&:before': { content: '""', position: 'absolute', bottom: '0', left: 'var(--tab-p)', width: 'calc(100% - var(--tab-p) * 2)', height: '3px', borderRadius: 'var(--radius-field)', backgroundColor: 'transparent', transformOrigin: 'center', '@media (prefers-reduced-motion: no-preference)': { transition: 'background-color 0.2s ease', }, }, [activeBeforeSelector]: { backgroundColor: 'currentColor', // Underline grows from the center when the tab becomes active '@media (prefers-reduced-motion: no-preference)': { animation: 'tab-indicator 0.25s ease', }, }, }, '& .tabs-content': {}, }, lift: { '& .tabs-tab': { borderStyle: 'solid', // Constant 1px border on ALL sides for every state: the geometry // (content box) never changes, so the label text doesn't shift when // a tab becomes active. Only border-COLOR changes (transparent where // there's no line), which snaps — no layout impact. borderWidth: 'var(--border, 1px)', borderColor: 'transparent transparent token(colors.base-300) transparent', borderStartStartRadius: 'var(--radius-field)', borderStartEndRadius: 'var(--radius-field)', paddingTop: '0', // Only the "lift" fill animates: background (and label color) fade '@media (prefers-reduced-motion: no-preference)': { transition: 'color 0.2s ease, background-color 0.25s ease', }, [activeSelector]: { borderColor: 'token(colors.base-300) token(colors.base-300) transparent token(colors.base-300)', backgroundColor: 'base-100', }, }, '& .tabs-content': { marginTop: 'calc(-1 * var(--border, 1px))', borderColor: 'base-300', borderStartStartRadius: '0', borderStartEndRadius: 'var(--radius-box)', borderEndStartRadius: 'var(--radius-box)', borderEndEndRadius: 'var(--radius-box)', // This border is the visible bottom line of the active tab's frame // (the active tab's own bottom border is transparent and the panel // overlaps it via margin-top). The frame's top/left/right appear // instantly, so this line must too — the panel does NOT fade in // here, otherwise the bottom of the frame lags behind. '&.active': { animation: 'none', }, }, }, }, size: { xs: { '--tab-height': 'token(spacing.6)', '--tab-p': 'token(spacing.1.5)' }, sm: { '--tab-height': 'token(spacing.8)', '--tab-p': 'token(spacing.2)' }, md: { '--tab-height': 'token(spacing.10)', '--tab-p': 'token(spacing.3)' }, lg: { '--tab-height': 'token(spacing.12)', '--tab-p': 'token(spacing.4)' }, xl: { '--tab-height': 'token(spacing.14)', '--tab-p': 'token(spacing.5)' }, }, }, defaultVariants: { size: 'md', }, })