first commit

This commit is contained in:
2026-09-07 20:36:18 +05:30
commit fd366f7e4a
231 changed files with 52631 additions and 0 deletions

120
src/components/ds/Tabs.jsx Normal file
View File

@@ -0,0 +1,120 @@
import * as React from 'react';
import { motion } from 'framer-motion';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
/**
* Tabs — switches between views of the same page.
*
* Two visual styles:
* - `pill` — matches the app's header navigation, with a sliding indicator
* - `underline` — quieter, for tabs inside a card
*
* The sliding indicator uses a shared `layoutId`, so the highlight animates
* between tabs the same way the main nav pill does. `layoutGroupId` must be
* unique when two tab sets are on screen at once, otherwise the indicator
* animates between them.
*/
/** @param {any} props */
export function Tabs({
tabs = [],
value,
onChange,
variant = 'pill',
size = 'default',
layoutGroupId,
className,
ariaLabel = 'Views',
}) {
const normalized = tabs.map((t) => (typeof t === 'string' ? { value: t, label: t } : t));
const fallbackId = React.useId();
const groupId = layoutGroupId || fallbackId;
if (variant === 'underline') {
return (
<div role="tablist" aria-label={ariaLabel} className={cn('flex items-center gap-1 border-b border-border', className)}>
{normalized.map((tab) => {
const active = tab.value === value;
return (
<button
key={tab.value}
type="button"
role="tab"
aria-selected={active}
onClick={() => onChange?.(tab.value)}
className={cn(
'relative inline-flex items-center gap-2 px-3 pb-2.5 pt-1.5 font-medium transition-colors duration-base focus-ring-inset rounded-t-md',
size === 'sm' ? 'text-body-sm' : 'text-body',
active ? 'text-krow-blue' : 'text-ink-3 hover:text-ink-1'
)}
>
{tab.icon && <tab.icon className="w-4 h-4" aria-hidden="true" />}
{tab.label}
{tab.count != null && (
<Badge variant={active ? 'soft' : 'neutral'} size="sm">{tab.count}</Badge>
)}
{active && (
<motion.span
layoutId={`tab-underline-${groupId}`}
className="absolute left-0 right-0 -bottom-px h-0.5 rounded-full bg-krow-blue"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
</button>
);
})}
</div>
);
}
return (
<div
role="tablist"
aria-label={ariaLabel}
className={cn(
'inline-flex items-center gap-1 rounded-full bg-white/60 backdrop-blur-xl p-1 border border-white/50 shadow-sm',
className
)}
>
{normalized.map((tab) => {
const active = tab.value === value;
return (
<button
key={tab.value}
type="button"
role="tab"
aria-selected={active}
onClick={() => onChange?.(tab.value)}
className={cn(
'relative inline-flex items-center gap-1.5 rounded-full font-medium transition-colors duration-base focus-ring',
size === 'sm' ? 'px-3 py-1 text-caption' : 'px-4 py-1.5 text-body-sm',
active ? 'text-white' : 'text-ink-3 hover:text-ink-1'
)}
>
{active && (
<motion.span
layoutId={`tab-pill-${groupId}`}
className="absolute inset-0 rounded-full bg-krow-blue"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
<span className="relative z-10 inline-flex items-center gap-1.5">
{tab.icon && <tab.icon className="w-3.5 h-3.5" aria-hidden="true" />}
{tab.label}
{tab.count != null && (
<span
className={cn(
'rounded-full px-1.5 text-[10px] font-bold tabular-nums',
active ? 'bg-white/25 text-white' : 'bg-surface-sunken text-ink-3'
)}
>
{tab.count}
</span>
)}
</span>
</button>
);
})}
</div>
);
}