DocDoeAI / src /components /shared /SharedPill.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
8.31 kB
"use client";
import Link from "next/link";
import { HTMLAttributes } from "react";
import { clsx } from "clsx";
// =============================================================================
// Pill Types
// =============================================================================
export interface PillProps extends HTMLAttributes<HTMLSpanElement> {
active?: boolean;
disabled?: boolean;
href?: string;
onClick?: () => void;
onRemove?: () => void;
removable?: boolean;
size?: "xs" | "sm" | "md" | "lg";
variant?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | "info" | "violet" | "blue" | "amber" | "emerald";
}
// =============================================================================
// SharedPill Component
// =============================================================================
export function SharedPill({
children,
active = false,
disabled = false,
href,
onClick,
onRemove,
removable = false,
size = "sm",
variant = "secondary",
className,
...props
}: PillProps) {
// Size classes
const sizeClasses = {
xs: "px-2 py-0.5 text-[10px]",
sm: "px-2.5 py-1 text-[11px]",
md: "px-3 py-1.5 text-[12px]",
lg: "px-4 py-2 text-[13px]",
};
// Variant classes
const variantClasses = {
primary: {
normal: "bg-[var(--accent-primary)]/10 text-[var(--accent-primary)] border-[var(--accent-primary)]/20",
active: "bg-[var(--accent-primary)]/20 text-[var(--accent-primary)] border-[var(--accent-primary)]/30",
},
secondary: {
normal: "bg-[var(--bg-elevated)] text-[var(--text-secondary)] border-[var(--border-subtle)]",
active: "bg-[var(--bg-glass)] text-[var(--text-primary)] border-[var(--border-medium)]",
},
success: {
normal: "bg-[var(--accent-emerald)]/10 text-[var(--accent-emerald)] border-[var(--accent-emerald)]/20",
active: "bg-[var(--accent-emerald)]/20 text-[var(--accent-emerald)] border-[var(--accent-emerald)]/30",
},
warning: {
normal: "bg-[var(--accent-amber)]/10 text-[var(--accent-amber)] border-[var(--accent-amber)]/20",
active: "bg-[var(--accent-amber)]/20 text-[var(--accent-amber)] border-[var(--accent-amber)]/30",
},
danger: {
normal: "bg-[var(--accent-red)]/10 text-[var(--accent-red)] border-[var(--accent-red)]/20",
active: "bg-[var(--accent-red)]/20 text-[var(--accent-red)] border-[var(--accent-red)]/30",
},
neutral: {
normal: "bg-[var(--bg-glass)] text-[var(--text-muted)] border-[var(--border-subtle)]",
active: "bg-[var(--bg-elevated)] text-[var(--text-secondary)] border-[var(--border-medium)]",
},
// Additional variants
info: {
normal: "bg-blue-400/10 text-blue-400 border-blue-400/20",
active: "bg-blue-400/20 text-blue-400 border-blue-400/30",
},
violet: {
normal: "bg-violet-400/10 text-violet-400 border-violet-400/20",
active: "bg-violet-400/20 text-violet-400 border-violet-400/30",
},
blue: {
normal: "bg-blue-400/10 text-blue-400 border-blue-400/20",
active: "bg-blue-400/20 text-blue-400 border-blue-400/30",
},
amber: {
normal: "bg-amber-400/10 text-amber-400 border-amber-400/20",
active: "bg-amber-400/20 text-amber-400 border-amber-400/30",
},
emerald: {
normal: "bg-emerald-400/10 text-emerald-400 border-emerald-400/20",
active: "bg-emerald-400/20 text-emerald-400 border-emerald-400/30",
},
};
const v = variantClasses[variant];
const stateClasses = active ? v.active : v.normal;
const s = sizeClasses[size];
// Build the pill content
const pillContent = (
<span
className={clsx(
"inline-flex items-center gap-1.5",
"rounded-full",
"border",
"transition-colors",
"duration-150",
stateClasses,
s,
disabled && "opacity-50 cursor-not-allowed",
className,
)}
{...props}
>
{children}
{removable && !disabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove?.();
}}
className="flex size-4 shrink-0 items-center justify-center rounded-full hover:bg-[var(--bg-elevated)] transition"
aria-label="Remove"
>
<span className="text-[10px] font-bold" aria-hidden="true">
×
</span>
</button>
)}
</span>
);
// Decide how to render
if (href && !disabled) {
return (
<Link href={href} className="inline-flex">
{pillContent}
</Link>
);
}
if (onClick && !disabled) {
return (
<button
type="button"
onClick={onClick}
className="inline-flex cursor-pointer"
disabled={disabled}
>
{pillContent}
</button>
);
}
return pillContent;
}
// =============================================================================
// Convenience Pill Components
// =============================================================================
// Filter pill (for syllabus filters)
export function FilterPill({
children,
active,
onClick,
className,
...props
}: Omit<PillProps, "variant" | "removable">) {
return (
<SharedPill
active={active}
onClick={onClick}
variant="secondary"
size="sm"
className={clsx("font-medium", className)}
{...props}
>
{children}
</SharedPill>
);
}
// Topic pill (for chapter topics)
export function TopicPill({ children, className, ...props }: Omit<PillProps, "variant">) {
return (
<SharedPill
variant="neutral"
size="xs"
className={clsx("font-medium text-[var(--text-ghost)]", className)}
{...props}
>
{children}
</SharedPill>
);
}
// Priority pill (for exam priority)
export type PriorityLevel = "critical" | "high" | "moderate" | "optional";
const priorityConfig: Record<PriorityLevel, { variant: PillProps["variant"]; label: string }> = {
critical: { variant: "danger", label: "Exam critical" },
high: { variant: "warning", label: "High priority" },
moderate: { variant: "success", label: "Moderate" },
optional: { variant: "neutral", label: "Optional" },
};
export function PriorityPill({
level,
showLabel = false,
className,
...props
}: { level: PriorityLevel; showLabel?: boolean } & Omit<PillProps, "variant" | "children">) {
const config = priorityConfig[level];
return (
<SharedPill
variant={config.variant}
size="xs"
className={className}
{...props}
>
{showLabel ? config.label : level}
</SharedPill>
);
}
// Status pill
export type StatusType = "completed" | "in-progress" | "not-started" | "weak";
const statusConfig: Record<StatusType, { variant: PillProps["variant"]; label: string }> = {
completed: { variant: "success", label: "Completed" },
"in-progress": { variant: "warning", label: "In Progress" },
"not-started": { variant: "neutral", label: "Not Started" },
weak: { variant: "danger", label: "Weak" },
};
export function StatusPill({
status,
showLabel = false,
className,
...props
}: { status: StatusType; showLabel?: boolean } & Omit<PillProps, "variant" | "children">) {
const config = statusConfig[status];
return (
<SharedPill
variant={config.variant}
size="xs"
className={className}
{...props}
>
{showLabel ? config.label : status}
</SharedPill>
);
}
// Confidence pill (for teach page)
export type ConfidenceLevel = "high" | "medium" | "low" | "none";
const confidenceConfig: Record<ConfidenceLevel, { variant: PillProps["variant"]; label: string }> = {
high: { variant: "success", label: "High confidence" },
medium: { variant: "warning", label: "Likely match" },
low: { variant: "danger", label: "Best guess" },
none: { variant: "neutral", label: "No clear match" },
};
export function ConfidencePill({
level,
className,
...props
}: { level: ConfidenceLevel } & Omit<PillProps, "variant" | "children">) {
const config = confidenceConfig[level];
return (
<SharedPill
variant={config.variant}
size="xs"
className={clsx("font-semibold", className)}
{...props}
>
{config.label}
</SharedPill>
);
}