@epfl-sti/poesis 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"poesis.js","names":[],"sources":["../src/components/ui/Avatar.tsx","../src/components/ui/Badge.tsx","../src/components/ui/Spinner.tsx","../src/components/ui/Button.tsx","../src/components/ui/IconButton.tsx","../src/components/ui/Pagination.tsx","../src/components/ui/FileUpload.tsx","../src/components/ui/ProgressBar.tsx","../src/components/ui/Checkbox.tsx","../src/components/ui/Input.tsx","../src/components/ui/RadioGroup.tsx","../src/theme/reactSelectStyles.ts","../src/components/ui/Select.tsx","../src/components/ui/Switch.tsx","../src/components/ui/Textarea.tsx","../src/components/ui/DropdownMenu.tsx","../src/components/ui/Popover.tsx","../src/components/ui/Tabs.tsx","../src/components/ui/Tooltip.tsx","../src/hooks/useLanguage.ts","../src/components/ui/LanguageSwitcher.tsx","../src/hooks/useTheme.ts","../src/components/ui/ThemeToggle.tsx","../src/components/layout/TopNav.tsx","../src/components/layout/SideNav.tsx","../src/components/layout/BurgerDrawer.tsx","../src/components/layout/Footer.tsx","../src/components/layout/UserMenu.tsx","../src/components/layout/PageShell.tsx","../src/components/data-display/Card.tsx","../src/components/data-display/DescriptionList.tsx","../src/components/data-display/EmptyState.tsx","../src/components/data-display/Table.tsx","../src/components/feedback/Alert.tsx","../src/components/feedback/Dialog.tsx","../src/components/feedback/ConfirmDialog.tsx","../src/components/feedback/toastContext.ts","../src/components/feedback/Toast.tsx","../src/hooks/useAuth.tsx"],"sourcesContent":["import { type ComponentPropsWithoutRef, useState } from \"react\";\n\n/* ── Size token maps ────────────────────────────────────── */\n\nconst sizeClasses = {\n sm: \"size-8 text-caption\",\n md: \"size-10 text-small\",\n lg: \"size-14 text-body\",\n} as const;\n\nexport type AvatarSize = keyof typeof sizeClasses;\n\nexport interface AvatarProps extends ComponentPropsWithoutRef<\"span\"> {\n /** Image URL. Falls back to initials when absent or broken. */\n src?: string | null;\n /** Full name used to derive initials. */\n name: string;\n /** Size preset. @default \"md\" */\n size?: AvatarSize;\n /** Alt text for the image. Defaults to `name`. */\n alt?: string;\n}\n\n/** Extract up to two uppercase initials from a name. */\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0][0].toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\n/**\n * User avatar with an image that falls back to initials.\n *\n * Always renders a circular element. Sizes: `sm` (32 px), `md` (40 px), `lg` (56 px).\n */\nexport function Avatar({ src, name, size = \"md\", alt, className = \"\", ...rest }: AvatarProps) {\n const [imgFailed, setImgFailed] = useState(false);\n\n const showImage = !!src && !imgFailed;\n\n return (\n <span\n role=\"img\"\n aria-label={alt ?? name}\n className={[\n \"inline-flex items-center justify-center rounded-full overflow-hidden\",\n \"bg-primary text-white font-semibold select-none shrink-0\",\n sizeClasses[size],\n className,\n ].join(\" \")}\n {...rest}\n >\n {showImage ? (\n <img\n src={src}\n alt={alt ?? name}\n className=\"size-full object-cover\"\n onError={() => setImgFailed(true)}\n />\n ) : (\n <span aria-hidden=\"true\">{getInitials(name)}</span>\n )}\n </span>\n );\n}\n","import type { ComponentPropsWithoutRef } from \"react\";\n\n/* ── Color token maps ───────────────────────────────────── */\n\nconst colorClasses = {\n default: \"bg-bg-tertiary text-text-primary\",\n success: \"bg-success-bg text-success\",\n warning: \"bg-warning-bg text-warning\",\n error: \"bg-error-bg text-error\",\n info: \"bg-info-bg text-info\",\n} as const;\n\nconst dotColorClasses = {\n default: \"bg-text-muted\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n error: \"bg-error\",\n info: \"bg-info\",\n} as const;\n\nexport type BadgeColor = keyof typeof colorClasses;\n\n/* ── Dot Badge ──────────────────────────────────────────── */\n\nexport interface DotBadgeProps extends ComponentPropsWithoutRef<\"span\"> {\n /** Semantic color. @default \"default\" */\n color?: BadgeColor;\n /** Render as a small status dot instead of a text pill. */\n dot: true;\n children?: never;\n}\n\n/* ── Text Badge ─────────────────────────────────────────── */\n\nexport interface TextBadgeProps extends ComponentPropsWithoutRef<\"span\"> {\n /** Semantic color. @default \"default\" */\n color?: BadgeColor;\n dot?: false;\n children: React.ReactNode;\n}\n\nexport type BadgeProps = DotBadgeProps | TextBadgeProps;\n\n/**\n * Status indicator — either a small colored dot or a text pill.\n *\n * ```tsx\n * <Badge color=\"success\" dot /> // status dot\n * <Badge color=\"error\">Failed</Badge> // text pill\n * ```\n */\nexport function Badge({ color = \"default\", dot, className = \"\", children, ...rest }: BadgeProps) {\n if (dot) {\n return (\n <span\n role=\"status\"\n className={`inline-block size-2.5 rounded-full ${dotColorClasses[color]} ${className}`}\n {...rest}\n />\n );\n }\n\n return (\n <span\n className={[\n \"inline-flex items-center rounded-sm px-2 py-0.5\",\n \"text-caption font-medium leading-none\",\n colorClasses[color],\n className,\n ].join(\" \")}\n {...rest}\n >\n {children}\n </span>\n );\n}\n","import type { ComponentPropsWithoutRef } from \"react\";\n\nconst sizeMap = {\n sm: \"size-4\",\n md: \"size-6\",\n lg: \"size-8\",\n} as const;\n\nexport type SpinnerSize = keyof typeof sizeMap;\n\nexport interface SpinnerProps extends ComponentPropsWithoutRef<\"svg\"> {\n /** Visual size of the spinner. @default \"md\" */\n size?: SpinnerSize;\n}\n\n/**\n * Animated loading indicator.\n *\n * Inherits `currentColor` so it adapts to its parent's text color.\n */\nexport function Spinner({ size = \"md\", className = \"\", ...rest }: SpinnerProps) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={`animate-spin ${sizeMap[size]} ${className}`}\n role=\"status\"\n aria-label=\"Loading\"\n {...rest}\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"3\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8v3a5 5 0 00-5 5H4z\"\n />\n </svg>\n );\n}\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\nimport { Spinner } from \"./Spinner\";\n\n/* ── Variant × size token maps ─────────────────────────── */\n\nconst variantClasses = {\n primary: \"bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary\",\n secondary:\n \"border border-border text-text-primary bg-bg-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n ghost: \"text-text-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n danger: \"bg-error text-white hover:opacity-90 focus-visible:ring-error\",\n} as const;\n\nconst sizeClasses = {\n sm: \"text-small px-3 py-1 gap-1.5\",\n md: \"text-body px-4 py-2 gap-2\",\n lg: \"text-body px-5 py-2.5 gap-2\",\n} as const;\n\nconst spinnerSizeMap = {\n sm: \"sm\",\n md: \"sm\",\n lg: \"md\",\n} as const;\n\nexport type ButtonVariant = keyof typeof variantClasses;\nexport type ButtonSize = keyof typeof sizeClasses;\n\nexport interface ButtonProps extends ComponentPropsWithoutRef<\"button\"> {\n /** Visual style variant. @default \"primary\" */\n variant?: ButtonVariant;\n /** Size preset. @default \"md\" */\n size?: ButtonSize;\n /** Show a spinner and disable interaction. */\n loading?: boolean;\n /** Optional icon before children. */\n iconLeft?: ReactNode;\n /** Optional icon after children. */\n iconRight?: ReactNode;\n}\n\n/**\n * Primary action element.\n *\n * Supports four variants (`primary`, `secondary`, `ghost`, `danger`),\n * three sizes (`sm`, `md`, `lg`), and a `loading` state.\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = \"primary\",\n size = \"md\",\n loading = false,\n disabled,\n iconLeft,\n iconRight,\n className = \"\",\n children,\n ...rest\n },\n ref,\n) {\n const isDisabled = disabled || loading;\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={isDisabled}\n aria-busy={loading || undefined}\n className={[\n // base\n \"inline-flex items-center justify-center font-medium rounded-md\",\n \"transition-colors duration-150 cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n // variant + size\n variantClasses[variant],\n sizeClasses[size],\n // disabled / loading\n isDisabled && \"opacity-50 pointer-events-none\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {loading ? <Spinner size={spinnerSizeMap[size]} aria-hidden=\"true\" /> : iconLeft}\n {children}\n {!loading && iconRight}\n </button>\n );\n});\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\nimport { Spinner } from \"./Spinner\";\n\n/* ── Variant token maps ─────────────────────────────────── */\n\nconst variantClasses = {\n primary: \"bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary\",\n secondary:\n \"border border-border text-text-primary bg-bg-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n ghost: \"text-text-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n danger: \"bg-error text-white hover:opacity-90 focus-visible:ring-error\",\n} as const;\n\nconst sizeClasses = {\n sm: \"size-7 text-small\",\n md: \"size-9 text-body\",\n lg: \"size-11 text-body\",\n} as const;\n\nexport type IconButtonVariant = keyof typeof variantClasses;\nexport type IconButtonSize = keyof typeof sizeClasses;\n\nexport interface IconButtonProps extends ComponentPropsWithoutRef<\"button\"> {\n /** Visual style variant. @default \"ghost\" */\n variant?: IconButtonVariant;\n /** Size preset. @default \"md\" */\n size?: IconButtonSize;\n /** Show a spinner and disable interaction. */\n loading?: boolean;\n /** The icon element to render. */\n icon: ReactNode;\n /** Accessible label (required since there's no visible text). */\n \"aria-label\": string;\n}\n\n/**\n * Square icon-only button for toolbar actions.\n *\n * `aria-label` is required to ensure accessibility.\n */\nexport const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(\n { variant = \"ghost\", size = \"md\", loading = false, disabled, icon, className = \"\", ...rest },\n ref,\n) {\n const isDisabled = disabled || loading;\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={isDisabled}\n aria-busy={loading || undefined}\n className={[\n // base\n \"inline-flex items-center justify-center rounded-md\",\n \"transition-colors duration-150 cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n // variant + size\n variantClasses[variant],\n sizeClasses[size],\n // disabled / loading\n isDisabled && \"opacity-50 pointer-events-none\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {loading ? <Spinner size=\"sm\" aria-hidden=\"true\" /> : icon}\n </button>\n );\n});\n","import { type ComponentPropsWithoutRef, forwardRef, useCallback, useState } from \"react\";\nimport {\n ChevronBarLeft,\n ChevronBarRight,\n ChevronLeft,\n ChevronRight,\n} from \"react-bootstrap-icons\";\nimport { useTranslation } from \"react-i18next\";\n\n/* ── Size token map ───────────────────────────────────────── */\n\nconst sizeClasses = {\n sm: \"h-7 min-w-7 px-1.5 text-small\",\n md: \"h-9 min-w-9 px-2 text-body\",\n lg: \"h-11 min-w-11 px-3 text-body\",\n} as const;\n\nconst iconSizeMap = { sm: 14, md: 16, lg: 18 } as const;\n\nexport type PaginationSize = keyof typeof sizeClasses;\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface PaginationProps extends Omit<ComponentPropsWithoutRef<\"nav\">, \"onChange\" | \"children\"> {\n /** Total number of pages (≥ 1). */\n totalPages: number;\n /** Current page (controlled). 1-based. */\n page?: number;\n /** Default page (uncontrolled). 1-based. @default 1 */\n defaultPage?: number;\n /** Callback when page changes. */\n onChange?: (page: number) => void;\n /** Number of pages to show on each side of current page. @default 1 */\n siblingCount?: number;\n /** Number of pages always shown at start and end. @default 1 */\n boundaryCount?: number;\n /** Show first/last page jump buttons. @default false */\n showFirstLast?: boolean;\n /** Size preset. @default \"md\" */\n size?: PaginationSize;\n /** Disable all interaction. */\n disabled?: boolean;\n}\n\n/* ── Range computation ────────────────────────────────────── */\n\ntype PageItem = number | \"ellipsis-start\" | \"ellipsis-end\";\n\nfunction computeRange(\n currentPage: number,\n totalPages: number,\n siblingCount: number,\n boundaryCount: number,\n): PageItem[] {\n // Total page buttons that can be displayed (boundaries + siblings + current + 2 ellipsis slots)\n const totalSlots = boundaryCount * 2 + siblingCount * 2 + 3; // +3 = current + 2 ellipsis positions\n\n // If everything fits, show all pages\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const startBoundary = Array.from({ length: boundaryCount }, (_, i) => i + 1);\n const endBoundary = Array.from({ length: boundaryCount }, (_, i) => totalPages - boundaryCount + 1 + i);\n\n const siblingStart = Math.max(boundaryCount + 2, currentPage - siblingCount);\n const siblingEnd = Math.min(totalPages - boundaryCount - 1, currentPage + siblingCount);\n\n const showStartEllipsis = siblingStart > boundaryCount + 2;\n const showEndEllipsis = siblingEnd < totalPages - boundaryCount - 1;\n\n const items: PageItem[] = [];\n\n // Start boundary\n items.push(...startBoundary);\n\n // Start ellipsis or bridging page\n if (showStartEllipsis) {\n items.push(\"ellipsis-start\");\n } else {\n // Fill pages between boundary and sibling start\n for (let i = boundaryCount + 1; i < siblingStart; i++) {\n items.push(i);\n }\n }\n\n // Sibling range (includes current page)\n for (let i = siblingStart; i <= siblingEnd; i++) {\n items.push(i);\n }\n\n // End ellipsis or bridging page\n if (showEndEllipsis) {\n items.push(\"ellipsis-end\");\n } else {\n for (let i = siblingEnd + 1; i <= totalPages - boundaryCount; i++) {\n items.push(i);\n }\n }\n\n // End boundary\n items.push(...endBoundary);\n\n return items;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Page navigation with ellipsis truncation, prev/next buttons,\n * and optional first/last jump buttons.\n *\n * Supports controlled (`page`/`onChange`) and uncontrolled (`defaultPage`) usage.\n */\nexport const Pagination = forwardRef<HTMLElement, PaginationProps>(function Pagination(\n {\n totalPages,\n page: controlledPage,\n defaultPage = 1,\n onChange,\n siblingCount = 1,\n boundaryCount = 1,\n showFirstLast = false,\n size = \"md\",\n disabled = false,\n className = \"\",\n ...rest\n },\n ref,\n) {\n const { t } = useTranslation();\n\n /* Controlled / uncontrolled */\n const isControlled = controlledPage !== undefined;\n const [internalPage, setInternalPage] = useState(defaultPage);\n const currentPage = Math.min(Math.max(isControlled ? controlledPage : internalPage, 1), totalPages);\n\n const setPage = useCallback(\n (p: number) => {\n const clamped = Math.min(Math.max(p, 1), totalPages);\n if (!isControlled) setInternalPage(clamped);\n onChange?.(clamped);\n },\n [isControlled, onChange, totalPages],\n );\n\n const items = computeRange(currentPage, totalPages, siblingCount, boundaryCount);\n const iconSize = iconSizeMap[size];\n\n const isFirst = currentPage <= 1;\n const isLast = currentPage >= totalPages;\n\n /* Shared button base classes */\n const btnBase = [\n \"inline-flex items-center justify-center rounded-md font-medium\",\n \"transition-colors duration-150 cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-epfl-canard\",\n sizeClasses[size],\n ].join(\" \");\n\n const pageBtn = (active: boolean) =>\n [\n btnBase,\n active\n ? \"bg-epfl-canard text-white\"\n : \"text-text-primary hover:bg-bg-tertiary\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n const navBtn = (isDisabledEdge: boolean) =>\n [\n btnBase,\n \"text-text-secondary hover:bg-bg-tertiary\",\n (disabled || isDisabledEdge) && \"opacity-40 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n if (totalPages <= 0) return null;\n\n return (\n <nav\n ref={ref}\n aria-label={t(\"common.pagination\")}\n className={[\"inline-flex items-center gap-1\", className].filter(Boolean).join(\" \")}\n {...rest}\n >\n {/* First */}\n {showFirstLast && (\n <button\n type=\"button\"\n aria-label={t(\"common.first\")}\n disabled={disabled || isFirst}\n className={navBtn(isFirst)}\n onClick={() => setPage(1)}\n >\n <ChevronBarLeft size={iconSize} aria-hidden />\n </button>\n )}\n\n {/* Previous */}\n <button\n type=\"button\"\n aria-label={t(\"common.previous\")}\n disabled={disabled || isFirst}\n className={navBtn(isFirst)}\n onClick={() => setPage(currentPage - 1)}\n >\n <ChevronLeft size={iconSize} aria-hidden />\n </button>\n\n {/* Page items */}\n {items.map((item) => {\n if (typeof item === \"string\") {\n return (\n <span\n key={item}\n aria-hidden\n className={[\n \"inline-flex items-center justify-center select-none text-text-secondary\",\n sizeClasses[size],\n ].join(\" \")}\n >\n …\n </span>\n );\n }\n\n const isActive = item === currentPage;\n return (\n <button\n key={item}\n type=\"button\"\n aria-current={isActive ? \"page\" : undefined}\n aria-label={t(\"common.goToPage\", { page: item })}\n disabled={disabled}\n className={[\n pageBtn(isActive),\n disabled && \"opacity-40 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n onClick={() => setPage(item)}\n >\n {item}\n </button>\n );\n })}\n\n {/* Next */}\n <button\n type=\"button\"\n aria-label={t(\"common.next\")}\n disabled={disabled || isLast}\n className={navBtn(isLast)}\n onClick={() => setPage(currentPage + 1)}\n >\n <ChevronRight size={iconSize} aria-hidden />\n </button>\n\n {/* Last */}\n {showFirstLast && (\n <button\n type=\"button\"\n aria-label={t(\"common.last\")}\n disabled={disabled || isLast}\n className={navBtn(isLast)}\n onClick={() => setPage(totalPages)}\n >\n <ChevronBarRight size={iconSize} aria-hidden />\n </button>\n )}\n </nav>\n );\n});\n","import {\n type ChangeEvent,\n type DragEvent,\n type ReactNode,\n useCallback,\n useRef,\n useState,\n} from \"react\";\nimport { CloudArrowUp, X } from \"react-bootstrap-icons\";\n\nexport interface FileUploadProps {\n /** Accepted file types (e.g. `\"image/*,.pdf\"`). Maps to the `<input accept>` attribute. */\n accept?: string;\n /** Allow selecting multiple files. */\n multiple?: boolean;\n /** Maximum file size in bytes. Files exceeding this are rejected silently via `onReject`. */\n maxSize?: number;\n /** Callback fired with accepted files after drop or selection. */\n onFilesSelected?: (files: File[]) => void;\n /** Callback fired with rejected files (wrong type or over `maxSize`). */\n onReject?: (files: File[]) => void;\n /** Disable the dropzone. */\n disabled?: boolean;\n /** Label text shown inside the dropzone. */\n label?: string;\n /** Helper text shown below the label (e.g. \"PDF, PNG, up to 10 MB\"). */\n helperText?: string;\n /** Error message. When set, the border turns red. */\n error?: string;\n /** Custom illustration / icon rendered above the label. */\n illustration?: ReactNode;\n /** Additional class names on the outer wrapper. */\n className?: string;\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function FileUpload({\n accept,\n multiple = false,\n maxSize,\n onFilesSelected,\n onReject,\n disabled = false,\n label = \"Drop files here or click to browse\",\n helperText,\n error,\n illustration,\n className = \"\",\n}: FileUploadProps) {\n const [isDragOver, setIsDragOver] = useState(false);\n const [selectedFiles, setSelectedFiles] = useState<File[]>([]);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const hasError = Boolean(error);\n\n const filterFiles = useCallback(\n (fileList: FileList) => {\n const all = Array.from(fileList);\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const file of all) {\n if (maxSize && file.size > maxSize) {\n rejected.push(file);\n } else {\n accepted.push(file);\n }\n }\n\n if (accepted.length > 0) {\n const next = multiple\n ? [...selectedFiles, ...accepted]\n : accepted.slice(0, 1);\n setSelectedFiles(next);\n onFilesSelected?.(next);\n }\n\n if (rejected.length > 0) {\n onReject?.(rejected);\n }\n },\n [maxSize, multiple, onFilesSelected, onReject, selectedFiles],\n );\n\n const handleDragOver = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n if (!disabled) setIsDragOver(true);\n },\n [disabled],\n );\n\n const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n setIsDragOver(false);\n }, []);\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n setIsDragOver(false);\n if (disabled || !e.dataTransfer.files.length) return;\n filterFiles(e.dataTransfer.files);\n },\n [disabled, filterFiles],\n );\n\n const handleInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (e.target.files?.length) {\n filterFiles(e.target.files);\n }\n },\n [filterFiles],\n );\n\n const handleClick = useCallback(() => {\n if (!disabled) inputRef.current?.click();\n }, [disabled]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (!disabled && (e.key === \"Enter\" || e.key === \" \")) {\n e.preventDefault();\n inputRef.current?.click();\n }\n },\n [disabled],\n );\n\n const removeFile = useCallback(\n (index: number) => {\n const next = selectedFiles.filter((_, i) => i !== index);\n setSelectedFiles(next);\n onFilesSelected?.(next);\n },\n [selectedFiles, onFilesSelected],\n );\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {/* Dropzone area */}\n <div\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n className={[\n \"flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-lg\",\n \"transition-colors cursor-pointer select-none\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\",\n disabled\n ? \"opacity-50 pointer-events-none bg-bg-secondary\"\n : isDragOver\n ? \"border-primary bg-primary/5\"\n : hasError\n ? \"border-error hover:border-error bg-bg-primary\"\n : \"border-border hover:border-primary bg-bg-primary\",\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-disabled={disabled}\n >\n {illustration ?? (\n <CloudArrowUp\n size={32}\n className={\n isDragOver\n ? \"text-primary\"\n : \"text-text-muted\"\n }\n aria-hidden\n />\n )}\n <span className=\"text-body font-medium text-text-primary text-center\">\n {label}\n </span>\n {helperText && (\n <span className=\"text-caption text-text-secondary text-center\">\n {helperText}\n </span>\n )}\n </div>\n\n {/* Hidden file input */}\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n onChange={handleInputChange}\n className=\"hidden\"\n tabIndex={-1}\n aria-hidden\n />\n\n {/* Error message */}\n {error && (\n <p className=\"text-caption text-error\">{error}</p>\n )}\n\n {/* Selected files list */}\n {selectedFiles.length > 0 && (\n <ul className=\"flex flex-col gap-1 mt-1\">\n {selectedFiles.map((file, i) => (\n <li\n key={`${file.name}-${file.size}-${i}`}\n className=\"flex items-center gap-2 rounded-md bg-bg-secondary px-3 py-1.5 text-small text-text-primary\"\n >\n <span className=\"truncate flex-1\">{file.name}</span>\n <span className=\"shrink-0 text-text-secondary\">\n {formatFileSize(file.size)}\n </span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n removeFile(i);\n }}\n className=\"shrink-0 p-0.5 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={14} />\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n","import { type ComponentPropsWithoutRef, forwardRef } from \"react\";\n\nconst sizeClasses = {\n sm: \"h-1.5\",\n md: \"h-2.5\",\n lg: \"h-4\",\n} as const;\n\nconst variantClasses = {\n primary: \"bg-primary\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n error: \"bg-error\",\n info: \"bg-info\",\n} as const;\n\nexport type ProgressBarSize = keyof typeof sizeClasses;\nexport type ProgressBarVariant = keyof typeof variantClasses;\n\nexport interface ProgressBarProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** Current progress value (0–100). Clamped internally. */\n value: number;\n /** Visual color variant. */\n variant?: ProgressBarVariant;\n /** Height of the bar. */\n size?: ProgressBarSize;\n /** Show the percentage label next to the bar. */\n showLabel?: boolean;\n /** Override the label text. Receives the clamped value. */\n formatLabel?: (value: number) => string;\n}\n\nexport const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(\n function ProgressBar(\n {\n value,\n variant = \"primary\",\n size = \"md\",\n showLabel = false,\n formatLabel,\n className = \"\",\n ...rest\n },\n ref,\n ) {\n const clamped = Math.round(Math.min(100, Math.max(0, value)));\n const label = formatLabel ? formatLabel(clamped) : `${clamped}%`;\n\n return (\n <div\n ref={ref}\n className={`flex items-center gap-2 ${className}`}\n {...rest}\n >\n <div\n className={[\n \"flex-1 overflow-hidden rounded-full bg-bg-tertiary\",\n sizeClasses[size],\n ].join(\" \")}\n role=\"progressbar\"\n aria-valuenow={clamped}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div\n className={[\n \"h-full rounded-full transition-all duration-300 ease-out\",\n variantClasses[variant],\n ].join(\" \")}\n style={{ width: `${clamped}%` }}\n />\n </div>\n {showLabel && (\n <span className=\"text-small font-medium text-text-secondary tabular-nums shrink-0\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nexport interface CheckboxProps extends Omit<ComponentPropsWithoutRef<\"input\">, \"type\"> {\n /** Label displayed next to the checkbox. */\n label?: ReactNode;\n /** Put the checkbox in an indeterminate state. */\n indeterminate?: boolean;\n /** Helper text below the label. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n}\n\n/**\n * Custom-styled checkbox with indeterminate support.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n {\n label,\n indeterminate = false,\n helperText,\n error,\n className = \"\",\n id: idProp,\n disabled,\n ...rest\n },\n forwardedRef,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n const hasError = Boolean(error);\n const internalRef = useRef<HTMLInputElement | null>(null);\n\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n internalRef.current = node;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(node);\n } else if (forwardedRef) {\n forwardedRef.current = node;\n }\n },\n [forwardedRef],\n );\n\n useEffect(() => {\n if (internalRef.current) {\n internalRef.current.indeterminate = indeterminate;\n }\n }, [indeterminate]);\n\n return (\n <div className={`flex flex-col gap-1 ${className}`}>\n <div className=\"flex items-start gap-2\">\n <input\n ref={setRef}\n id={id}\n type=\"checkbox\"\n disabled={disabled}\n aria-invalid={hasError || undefined}\n aria-describedby={helperText || error ? helperId : undefined}\n className={[\n \"mt-0.5 size-4 shrink-0 cursor-pointer rounded-sm border appearance-none\",\n \"bg-bg-primary transition-colors\",\n \"checked:bg-primary checked:border-primary\",\n \"indeterminate:bg-primary indeterminate:border-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n hasError ? \"border-error\" : \"border-border hover:border-border-strong\",\n // Checkmark via background SVG\n \"checked:bg-[url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22white%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%22%2F%3E%3C%2Fsvg%3E')] checked:bg-center checked:bg-no-repeat\",\n // Indeterminate dash\n \"indeterminate:bg-[url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22white%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%223%22%20y%3D%227%22%20width%3D%2210%22%20height%3D%222%22%20rx%3D%221%22%2F%3E%3C%2Fsvg%3E')] indeterminate:bg-center indeterminate:bg-no-repeat\",\n ].join(\" \")}\n {...rest}\n />\n\n {label && (\n <label\n htmlFor={id}\n className={`text-body select-none ${disabled ? \"text-text-muted cursor-not-allowed\" : \"text-text-primary cursor-pointer\"}`}\n >\n {label}\n </label>\n )}\n </div>\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption pl-6 ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n});\n","import { forwardRef, useId, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nexport interface InputProps extends Omit<ComponentPropsWithoutRef<\"input\">, \"size\"> {\n /** Visible label above the input. */\n label?: string;\n /** Helper text shown below the input. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Icon or element rendered inside the input on the left. */\n leftIcon?: ReactNode;\n /** Icon or element rendered inside the input on the right. */\n rightIcon?: ReactNode;\n /** Input size variant. */\n inputSize?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst sizeClasses = {\n sm: \"h-8 text-small px-2.5\",\n md: \"h-10 text-body px-3\",\n lg: \"h-12 text-body px-3.5\",\n} as const;\n\nconst iconPaddingLeft = {\n sm: \"pl-8\",\n md: \"pl-10\",\n lg: \"pl-11\",\n} as const;\n\nconst iconPaddingRight = {\n sm: \"pr-8\",\n md: \"pr-10\",\n lg: \"pr-11\",\n} as const;\n\nconst iconSizeClasses = {\n sm: \"[&>svg]:size-4\",\n md: \"[&>svg]:size-5\",\n lg: \"[&>svg]:size-5\",\n} as const;\n\n/**\n * Text input with label, helper text, error state, and icon slots.\n * Supports text, email, password, and search types.\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n label,\n helperText,\n error,\n leftIcon,\n rightIcon,\n inputSize = \"md\",\n className = \"\",\n id: idProp,\n disabled,\n ...rest\n },\n ref,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n const hasError = Boolean(error);\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {label && (\n <label htmlFor={id} className=\"text-small font-medium text-text-primary\">\n {label}\n </label>\n )}\n\n <div className=\"relative\">\n {leftIcon && (\n <span\n className={`pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-text-secondary ${iconSizeClasses[inputSize]}`}\n aria-hidden=\"true\"\n >\n {leftIcon}\n </span>\n )}\n\n <input\n ref={ref}\n id={id}\n disabled={disabled}\n aria-invalid={hasError || undefined}\n aria-describedby={helperText || error ? helperId : undefined}\n className={[\n \"w-full rounded-md border bg-bg-primary text-text-primary placeholder:text-text-muted\",\n \"outline-none transition-colors\",\n \"focus:border-primary focus:ring-1 focus:ring-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-bg-secondary\",\n hasError\n ? \"border-error focus:border-error focus:ring-error\"\n : \"border-border hover:border-border-strong\",\n sizeClasses[inputSize],\n leftIcon ? iconPaddingLeft[inputSize] : \"\",\n rightIcon ? iconPaddingRight[inputSize] : \"\",\n ].join(\" \")}\n {...rest}\n />\n\n {rightIcon && (\n <span\n className={`pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-text-secondary ${iconSizeClasses[inputSize]}`}\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n </div>\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n});\n","import { createContext, useContext, useId, type ReactNode } from \"react\";\n\n/* ── Context ─────────────────────────────────────────────── */\n\ninterface RadioGroupContextValue {\n name: string;\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n hasError?: boolean;\n}\n\nconst RadioGroupContext = createContext<RadioGroupContextValue | null>(null);\n\nfunction useRadioGroup() {\n const ctx = useContext(RadioGroupContext);\n if (!ctx) throw new Error(\"RadioGroup.Item must be used within RadioGroup\");\n return ctx;\n}\n\n/* ── RadioGroup ──────────────────────────────────────────── */\n\nexport interface RadioGroupProps {\n /** Group name for all radio inputs. */\n name?: string;\n /** Currently selected value (controlled). */\n value?: string;\n /** Default selected value (uncontrolled). */\n defaultValue?: string;\n /** Called when the selected value changes. */\n onChange?: (value: string) => void;\n /** Visible label for the group. */\n label?: string;\n /** Helper text below the group. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Layout direction. */\n orientation?: \"vertical\" | \"horizontal\";\n /** Disable all options. */\n disabled?: boolean;\n /** Radio items. */\n children: ReactNode;\n className?: string;\n}\n\n/**\n * Radio group with vertical or horizontal layout.\n * Use `RadioGroup.Item` for individual options.\n */\nexport function RadioGroup({\n name: nameProp,\n value,\n onChange,\n label,\n helperText,\n error,\n orientation = \"vertical\",\n disabled = false,\n children,\n className = \"\",\n}: RadioGroupProps) {\n const autoId = useId();\n const name = nameProp ?? autoId;\n const groupId = `${name}-group`;\n const helperId = `${groupId}-helper`;\n const hasError = Boolean(error);\n\n return (\n <RadioGroupContext.Provider value={{ name, value, onChange, disabled, hasError }}>\n <fieldset\n className={`flex flex-col gap-2 ${className}`}\n aria-describedby={helperText || error ? helperId : undefined}\n disabled={disabled}\n >\n {label && (\n <legend className=\"text-small font-medium text-text-primary mb-1\">\n {label}\n </legend>\n )}\n\n <div\n className={`flex gap-3 ${orientation === \"horizontal\" ? \"flex-row flex-wrap\" : \"flex-col\"}`}\n role=\"radiogroup\"\n >\n {children}\n </div>\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </fieldset>\n </RadioGroupContext.Provider>\n );\n}\n\n/* ── RadioGroup.Item ─────────────────────────────────────── */\n\nexport interface RadioItemProps {\n /** Value for this option. */\n value: string;\n /** Label displayed next to the radio. */\n label: ReactNode;\n /** Description shown below the label. */\n description?: string;\n /** Disable only this option. */\n disabled?: boolean;\n}\n\nfunction RadioItem({ value, label, description, disabled: itemDisabled }: RadioItemProps) {\n const {\n name,\n value: groupValue,\n onChange,\n disabled: groupDisabled,\n hasError,\n } = useRadioGroup();\n const id = useId();\n const disabled = groupDisabled || itemDisabled;\n const checked = groupValue !== undefined ? groupValue === value : undefined;\n\n return (\n <div className=\"flex items-start gap-2\">\n <input\n id={id}\n type=\"radio\"\n name={name}\n value={value}\n checked={checked}\n disabled={disabled}\n onChange={() => onChange?.(value)}\n className={[\n \"mt-0.5 size-4 shrink-0 cursor-pointer appearance-none rounded-full border\",\n \"bg-bg-primary transition-colors\",\n \"checked:border-primary checked:border-[5px]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n hasError ? \"border-error\" : \"border-border hover:border-border-strong\",\n ].join(\" \")}\n />\n\n <div className=\"flex flex-col\">\n <label\n htmlFor={id}\n className={`text-body select-none ${disabled ? \"text-text-muted cursor-not-allowed\" : \"text-text-primary cursor-pointer\"}`}\n >\n {label}\n </label>\n {description && (\n <span className=\"text-caption text-text-secondary\">{description}</span>\n )}\n </div>\n </div>\n );\n}\n\nRadioGroup.Item = RadioItem;\n","import type { ClassNamesConfig, ThemeConfig } from \"react-select\";\n\n/**\n * Custom react-select theme that maps to EPFL design tokens.\n * Uses CSS custom properties so it automatically adapts to light/dark mode.\n */\nexport const epflSelectTheme: ThemeConfig = (theme) => ({\n ...theme,\n borderRadius: 8,\n colors: {\n ...theme.colors,\n primary: \"var(--color-primary)\", // focused border, selected option\n primary75: \"var(--color-primary-hover)\",\n primary50: \"var(--color-bg-tertiary)\", // option hover\n primary25: \"var(--color-bg-secondary)\", // option light hover\n neutral0: \"var(--color-bg-primary)\", // control background\n neutral5: \"var(--color-bg-secondary)\",\n neutral10: \"var(--color-bg-tertiary)\", // multi-value tag bg\n neutral20: \"var(--color-border)\", // control border\n neutral30: \"var(--color-border-strong)\", // hover border\n neutral40: \"var(--color-text-muted)\", // placeholder if no search\n neutral50: \"var(--color-text-muted)\", // placeholder\n neutral60: \"var(--color-text-secondary)\",\n neutral70: \"var(--color-text-primary)\",\n neutral80: \"var(--color-text-primary)\", // selected text, input text\n neutral90: \"var(--color-text-primary)\",\n danger: \"var(--color-error)\",\n dangerLight: \"var(--color-error-bg)\",\n },\n});\n\n/**\n * Tailwind-based classNames for react-select components.\n * Applies EPFL design system styling via utility classes.\n */\nexport const epflSelectClassNames: ClassNamesConfig = {\n control: ({ isFocused }) =>\n `!shadow-sm !rounded-md !border-border ${isFocused ? \"!border-primary !ring-1 !ring-primary\" : \"\"}`,\n\n menu: () => \"!rounded-lg !shadow-lg !border !border-border\",\n option: ({ isFocused, isSelected }) =>\n `${isSelected ? \"!bg-primary !text-white\" : isFocused ? \"!bg-bg-tertiary\" : \"\"}`,\n\n multiValue: () => \"!rounded-sm !bg-bg-tertiary\",\n};\n","import { useId } from \"react\";\nimport ReactSelect, { type ClassNamesConfig, type GroupBase, type Props } from \"react-select\";\nimport { epflSelectClassNames, epflSelectTheme } from \"../../theme/reactSelectStyles\";\n\nconst selectSizeClasses = {\n sm: \"!min-h-8 !text-small\",\n md: \"!min-h-10 !text-body\",\n lg: \"!min-h-12 !text-body\",\n} as const;\n\nconst selectSizeHeight = {\n sm: 32,\n md: 40,\n lg: 48,\n} as const;\n\nexport interface SelectProps<\n Option = unknown,\n IsMulti extends boolean = false,\n Group extends GroupBase<Option> = GroupBase<Option>,\n> extends Props<Option, IsMulti, Group> {\n /** Visible label above the select. */\n label?: string;\n /** Helper text shown below the select. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Select size variant. */\n selectSize?: \"sm\" | \"md\" | \"lg\";\n}\n\n/**\n * Wrapper around react-select pre-configured with the EPFL design system theme.\n * Accepts all react-select props plus label, helperText, and error.\n */\nexport function Select<\n Option = unknown,\n IsMulti extends boolean = false,\n Group extends GroupBase<Option> = GroupBase<Option>,\n>({ label, helperText, error, selectSize = \"md\", className = \"\", ...rest }: SelectProps<Option, IsMulti, Group>) {\n const autoId = useId();\n const inputId = rest.inputId ?? autoId;\n const helperId = `${inputId}-helper`;\n const hasError = Boolean(error);\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {label && (\n <label htmlFor={inputId} className=\"text-small font-medium text-text-primary\">\n {label}\n </label>\n )}\n\n <ReactSelect<Option, IsMulti, Group>\n inputId={inputId}\n theme={epflSelectTheme}\n menuPortalTarget={document.body}\n styles={{\n control: (base, state) => ({\n ...base,\n minHeight: selectSizeHeight[selectSize],\n height: state.isMulti ? undefined : selectSizeHeight[selectSize],\n }),\n valueContainer: (base) => ({\n ...base,\n height: rest.isMulti ? undefined : selectSizeHeight[selectSize] - 2,\n padding: \"0 6px\",\n }),\n input: (base) => ({\n ...base,\n margin: 0,\n paddingTop: 0,\n paddingBottom: 0,\n }),\n indicatorsContainer: (base) => ({\n ...base,\n height: rest.isMulti ? undefined : selectSizeHeight[selectSize] - 2,\n }),\n menuPortal: (base) => ({ ...base, zIndex: 50 }),\n ...rest.styles,\n }}\n classNames={{\n ...(epflSelectClassNames as unknown as ClassNamesConfig<\n Option,\n IsMulti,\n Group\n >),\n control: (state) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const base = (epflSelectClassNames.control as any)?.(state) ?? \"\";\n const sizeClass = selectSizeClasses[selectSize];\n const styled = `${base} ${sizeClass}`;\n return hasError ? `${styled} !border-error !focus:ring-error` : styled;\n },\n }}\n aria-invalid={hasError || undefined}\n aria-errormessage={hasError ? helperId : undefined}\n {...rest}\n />\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n}\n","import { forwardRef, useId, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nexport interface SwitchProps extends Omit<ComponentPropsWithoutRef<\"input\">, \"type\"> {\n /** Label displayed next to the switch. */\n label?: ReactNode;\n /** Helper text below the switch. */\n helperText?: string;\n}\n\n/**\n * Toggle switch with label. Uses a hidden checkbox for accessibility.\n */\nexport const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(\n { label, helperText, className = \"\", id: idProp, disabled, ...rest },\n ref,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n\n return (\n <div className={`flex flex-col gap-1 ${className}`}>\n <div className=\"flex items-center gap-3\">\n {/* Hidden native checkbox for a11y */}\n <div className=\"relative inline-flex items-center\">\n <input\n ref={ref}\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n disabled={disabled}\n aria-describedby={helperText ? helperId : undefined}\n className=\"peer sr-only\"\n {...rest}\n />\n\n {/* Track + Thumb (thumb via ::after pseudo-element) */}\n <label\n htmlFor={id}\n className={[\n \"relative h-6 w-11 rounded-full transition-colors\",\n \"bg-border-strong\",\n \"peer-checked:bg-primary\",\n \"peer-focus-visible:ring-2 peer-focus-visible:ring-primary peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-bg-primary\",\n \"peer-disabled:opacity-50 peer-disabled:cursor-not-allowed\",\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer\",\n // Thumb\n \"after:content-[''] after:absolute after:top-0.5 after:left-0.5\",\n \"after:size-5 after:rounded-full after:bg-white after:shadow-sm\",\n \"after:transition-transform peer-checked:after:translate-x-5\",\n ].join(\" \")}\n aria-hidden=\"true\"\n />\n </div>\n\n {label && (\n <label\n htmlFor={id}\n className={`text-body select-none ${disabled ? \"text-text-muted cursor-not-allowed\" : \"text-text-primary cursor-pointer\"}`}\n >\n {label}\n </label>\n )}\n </div>\n\n {helperText && (\n <p id={helperId} className=\"text-caption text-text-secondary pl-14\">\n {helperText}\n </p>\n )}\n </div>\n );\n});\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n type ComponentPropsWithoutRef,\n} from \"react\";\n\nexport interface TextareaProps extends ComponentPropsWithoutRef<\"textarea\"> {\n /** Visible label above the textarea. */\n label?: string;\n /** Helper text shown below the textarea. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Automatically grow height to fit content. */\n autoGrow?: boolean;\n /** Minimum number of visible rows (default: 3). */\n minRows?: number;\n}\n\n/**\n * Textarea with label, helper text, error state, and optional auto-grow.\n */\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n {\n label,\n helperText,\n error,\n autoGrow = false,\n minRows = 3,\n className = \"\",\n id: idProp,\n disabled,\n onChange,\n ...rest\n },\n forwardedRef,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n const hasError = Boolean(error);\n const internalRef = useRef<HTMLTextAreaElement | null>(null);\n\n const resize = useCallback(() => {\n const el = internalRef.current;\n if (!el || !autoGrow) return;\n el.style.height = \"auto\";\n el.style.height = `${el.scrollHeight}px`;\n }, [autoGrow]);\n\n useEffect(() => {\n resize();\n }, [resize, rest.value, rest.defaultValue]);\n\n // Merge refs\n const setRef = useCallback(\n (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(node);\n } else if (forwardedRef) {\n forwardedRef.current = node;\n }\n },\n [forwardedRef],\n );\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {label && (\n <label htmlFor={id} className=\"text-small font-medium text-text-primary\">\n {label}\n </label>\n )}\n\n <textarea\n ref={setRef}\n id={id}\n rows={minRows}\n disabled={disabled}\n aria-invalid={hasError || undefined}\n aria-describedby={helperText || error ? helperId : undefined}\n onChange={(e) => {\n onChange?.(e);\n resize();\n }}\n className={[\n \"w-full rounded-md border bg-bg-primary text-text-primary placeholder:text-text-muted\",\n \"px-3 py-2 text-body outline-none transition-colors\",\n \"focus:border-primary focus:ring-1 focus:ring-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-bg-secondary\",\n autoGrow ? \"resize-none overflow-hidden\" : \"resize-y\",\n hasError\n ? \"border-error focus:border-error focus:ring-error\"\n : \"border-border hover:border-border-strong\",\n ].join(\" \")}\n {...rest}\n />\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n});\n","import {\n autoUpdate,\n flip,\n FloatingFocusManager,\n FloatingPortal,\n offset,\n type Placement,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useInteractions,\n useListNavigation,\n useRole,\n} from \"@floating-ui/react\";\nimport {\n createContext,\n forwardRef,\n type ReactNode,\n useCallback,\n useContext,\n useRef,\n useState,\n} from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface DropdownMenuProps {\n /** The trigger element that opens the menu. */\n trigger: ReactNode;\n /** Menu items. Use `DropdownItem` and `DropdownDivider`. */\n children: ReactNode;\n /** Preferred placement of the floating menu. @default \"bottom-start\" */\n placement?: Placement;\n /** Additional CSS classes on the menu panel. */\n className?: string;\n}\n\nexport interface DropdownItemProps {\n /** Item label or content. */\n children: ReactNode;\n /** Optional icon rendered before the label. */\n icon?: ReactNode;\n /** Danger / destructive styling. */\n danger?: boolean;\n /** Whether the item is disabled. */\n disabled?: boolean;\n /** Click handler. */\n onClick?: () => void;\n /** Additional CSS classes. */\n className?: string;\n}\n\n/* ── Internal context ─────────────────────────────────────── */\n\nconst DropdownCtx = createContext<{\n close: () => void;\n getItemProps: (userProps?: Record<string, unknown>) => Record<string, unknown>;\n activeIndex: number | null;\n setActiveIndex: (i: number | null) => void;\n}>({\n close: () => {},\n getItemProps: () => ({}),\n activeIndex: null,\n setActiveIndex: () => {},\n});\n\n/* ── DropdownDivider ──────────────────────────────────────── */\n\nexport function DropdownDivider() {\n return <div role=\"separator\" className=\"my-1 border-t border-border\" />;\n}\n\n/* ── DropdownItem ─────────────────────────────────────────── */\n\nexport const DropdownItem = forwardRef<HTMLButtonElement, DropdownItemProps>(function DropdownItem(\n { children, icon, danger = false, disabled = false, onClick, className = \"\", ...rest },\n ref,\n) {\n const { close } = useContext(DropdownCtx);\n\n const handleClick = () => {\n if (disabled) return;\n onClick?.();\n close();\n };\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"menuitem\"\n disabled={disabled}\n onClick={handleClick}\n className={[\n \"flex w-full items-center gap-2 px-3 py-2 text-small rounded-md\",\n \"text-left transition-colors outline-none cursor-pointer\",\n danger\n ? \"text-error hover:bg-error-bg focus:bg-error-bg\"\n : \"text-text-primary hover:bg-bg-tertiary focus:bg-bg-tertiary\",\n disabled && \"opacity-50 pointer-events-none\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {icon && <span className=\"shrink-0 size-4\">{icon}</span>}\n {children}\n </button>\n );\n});\n\n/* ── DropdownMenu ─────────────────────────────────────────── */\n\nexport function DropdownMenu({\n trigger,\n children,\n placement = \"bottom-start\",\n className = \"\",\n}: DropdownMenuProps) {\n const [isOpen, setIsOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const listRef = useRef<(HTMLElement | null)[]>([]);\n\n const { refs, floatingStyles, context } = useFloating({\n open: isOpen,\n onOpenChange: setIsOpen,\n placement,\n middleware: [offset(4), flip(), shift({ padding: 8 })],\n whileElementsMounted: autoUpdate,\n });\n\n const click = useClick(context);\n const dismiss = useDismiss(context);\n const role = useRole(context, { role: \"menu\" });\n const listNavigation = useListNavigation(context, {\n listRef,\n activeIndex,\n onNavigate: setActiveIndex,\n loop: true,\n });\n\n const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([\n click,\n dismiss,\n role,\n listNavigation,\n ]);\n\n const close = useCallback(() => setIsOpen(false), []);\n\n return (\n <>\n {/* Wrapper to attach floating-ui ref + interaction props */}\n <span ref={refs.setReference} className=\"inline-flex\" {...getReferenceProps()}>\n {trigger}\n </span>\n\n {isOpen && (\n <FloatingPortal>\n <FloatingFocusManager context={context} modal={false}>\n <div\n // eslint-disable-next-line react-hooks/refs -- floating-ui callback ref setter, not a .current read\n ref={refs.setFloating}\n style={floatingStyles}\n className={[\n \"z-50 min-w-[10rem] p-1 rounded-lg border border-border\",\n \"bg-bg-primary shadow-lg animate-floating-in\",\n className,\n ].join(\" \")}\n {...getFloatingProps()}\n >\n <DropdownCtx.Provider\n value={{ close, getItemProps, activeIndex, setActiveIndex }}\n >\n {children}\n </DropdownCtx.Provider>\n </div>\n </FloatingFocusManager>\n </FloatingPortal>\n )}\n </>\n );\n}\n","import {\n autoUpdate,\n flip,\n FloatingFocusManager,\n FloatingPortal,\n offset,\n type Placement,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useInteractions,\n useRole,\n} from \"@floating-ui/react\";\nimport { type ReactNode, useState } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface PopoverProps {\n /** The trigger element that toggles the popover. */\n trigger: ReactNode;\n /** Popover body content. */\n children: ReactNode;\n /** Preferred placement. @default \"bottom\" */\n placement?: Placement;\n /** Whether the popover is controlled externally. */\n open?: boolean;\n /** Callback when open state changes (controlled mode). */\n onOpenChange?: (open: boolean) => void;\n /** Additional CSS classes on the popover panel. */\n className?: string;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Popover({\n trigger,\n children,\n placement = \"bottom\",\n open: controlledOpen,\n onOpenChange,\n className = \"\",\n}: PopoverProps) {\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = controlledOpen !== undefined;\n const isOpen = isControlled ? controlledOpen : uncontrolledOpen;\n const setOpen = isControlled ? (v: boolean) => onOpenChange?.(v) : setUncontrolledOpen;\n\n const { refs, floatingStyles, context } = useFloating({\n open: isOpen,\n onOpenChange: setOpen,\n placement,\n middleware: [offset(8), flip(), shift({ padding: 8 })],\n whileElementsMounted: autoUpdate,\n });\n\n const click = useClick(context);\n const dismiss = useDismiss(context);\n const role = useRole(context);\n\n const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);\n\n return (\n <>\n {/* Wrapper to attach floating-ui ref + interaction props */}\n <span ref={refs.setReference} className=\"inline-flex\" {...getReferenceProps()}>\n {trigger}\n </span>\n\n {isOpen && (\n <FloatingPortal>\n <FloatingFocusManager context={context} modal={false}>\n <div\n // eslint-disable-next-line react-hooks/refs -- floating-ui callback ref setter, not a .current read\n ref={refs.setFloating}\n style={floatingStyles}\n className={[\n \"z-50 rounded-lg border border-border p-md\",\n \"bg-bg-primary shadow-lg animate-floating-in\",\n className,\n ].join(\" \")}\n {...getFloatingProps()}\n >\n {children}\n </div>\n </FloatingFocusManager>\n </FloatingPortal>\n )}\n </>\n );\n}\n","import {\n createContext,\n type KeyboardEvent,\n type ReactNode,\n useCallback,\n useContext,\n useId,\n useRef,\n useState,\n} from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface TabItem {\n /** Unique key for the tab. */\n value: string;\n /** Label displayed in the tab button. */\n label: ReactNode;\n /** Optional icon before the label. */\n icon?: ReactNode;\n /** Whether the tab is disabled. */\n disabled?: boolean;\n}\n\nexport interface TabsProps {\n /** Tab definitions. */\n items: TabItem[];\n /** The currently active tab value (controlled). */\n value?: string;\n /** Default active tab (uncontrolled). */\n defaultValue?: string;\n /** Callback when the active tab changes. */\n onChange?: (value: string) => void;\n /** Tab panel content — keyed by tab value. */\n children?: ReactNode;\n /** Additional CSS classes on the root. */\n className?: string;\n}\n\nexport interface TabPanelProps {\n /** Must match a `TabItem.value`. */\n value: string;\n /** Panel content. */\n children: ReactNode;\n /** Additional CSS classes. */\n className?: string;\n}\n\n/* ── Internal context ─────────────────────────────────────── */\n\nconst TabsCtx = createContext<{ activeValue: string; idPrefix: string }>({\n activeValue: \"\",\n idPrefix: \"\",\n});\n\n/* ── TabPanel ─────────────────────────────────────────────── */\n\nexport function TabPanel({ value, children, className = \"\" }: TabPanelProps) {\n const { activeValue, idPrefix } = useContext(TabsCtx);\n const isActive = activeValue === value;\n\n return (\n <div\n id={`${idPrefix}-panel-${value}`}\n role=\"tabpanel\"\n aria-labelledby={`${idPrefix}-tab-${value}`}\n hidden={!isActive}\n tabIndex={0}\n className={className}\n >\n {isActive && children}\n </div>\n );\n}\n\n/* ── Tabs ─────────────────────────────────────────────────── */\n\nexport function Tabs({\n items,\n value: controlledValue,\n defaultValue,\n onChange,\n children,\n className = \"\",\n}: TabsProps) {\n const idPrefix = useId().replace(/:/g, \"\");\n const tabListRef = useRef<HTMLDivElement>(null);\n\n /* Controlled / uncontrolled */\n const isControlled = controlledValue !== undefined;\n const [internal, setInternal] = useState(\n () => defaultValue ?? items.find((t) => !t.disabled)?.value ?? items[0]?.value ?? \"\",\n );\n const activeValue = isControlled ? controlledValue : internal;\n\n const setActive = useCallback(\n (v: string) => {\n if (!isControlled) setInternal(v);\n onChange?.(v);\n },\n [isControlled, onChange],\n );\n\n /* Keyboard navigation (arrow keys + Home/End) */\n const enabledItems = items.filter((t) => !t.disabled);\n\n const handleKeyDown = (e: KeyboardEvent) => {\n const currentIdx = enabledItems.findIndex((t) => t.value === activeValue);\n let nextIdx = currentIdx;\n\n switch (e.key) {\n case \"ArrowRight\":\n case \"ArrowDown\":\n e.preventDefault();\n nextIdx = (currentIdx + 1) % enabledItems.length;\n break;\n case \"ArrowLeft\":\n case \"ArrowUp\":\n e.preventDefault();\n nextIdx = (currentIdx - 1 + enabledItems.length) % enabledItems.length;\n break;\n case \"Home\":\n e.preventDefault();\n nextIdx = 0;\n break;\n case \"End\":\n e.preventDefault();\n nextIdx = enabledItems.length - 1;\n break;\n default:\n return;\n }\n\n const next = enabledItems[nextIdx];\n if (next) {\n setActive(next.value);\n /* Focus the new tab button */\n const btn = tabListRef.current?.querySelector<HTMLElement>(\n `[data-tab-value=\"${next.value}\"]`,\n );\n btn?.focus();\n }\n };\n\n return (\n <TabsCtx.Provider value={{ activeValue, idPrefix }}>\n <div className={className}>\n {/* Tab list */}\n <div\n ref={tabListRef}\n role=\"tablist\"\n aria-orientation=\"horizontal\"\n onKeyDown={handleKeyDown}\n className=\"flex border-b border-border\"\n >\n {items.map((tab) => {\n const isActive = tab.value === activeValue;\n return (\n <button\n key={tab.value}\n id={`${idPrefix}-tab-${tab.value}`}\n role=\"tab\"\n type=\"button\"\n data-tab-value={tab.value}\n aria-selected={isActive}\n aria-controls={`${idPrefix}-panel-${tab.value}`}\n tabIndex={isActive ? 0 : -1}\n disabled={tab.disabled}\n onClick={() => setActive(tab.value)}\n className={[\n \"inline-flex items-center gap-1.5 px-4 py-2.5 text-small font-medium\",\n \"border-b-2 -mb-px transition-colors outline-none cursor-pointer\",\n \"focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\",\n isActive\n ? \"border-primary text-primary\"\n : \"border-transparent text-text-secondary hover:text-text-primary hover:border-border-strong\",\n tab.disabled && \"opacity-50 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {tab.icon && <span className=\"shrink-0 size-4\">{tab.icon}</span>}\n {tab.label}\n </button>\n );\n })}\n </div>\n\n {/* Panels */}\n {children}\n </div>\n </TabsCtx.Provider>\n );\n}\n","import {\n FloatingPortal,\n arrow,\n autoUpdate,\n flip,\n offset,\n shift,\n useDismiss,\n useFloating,\n useFocus,\n useHover,\n useInteractions,\n useRole,\n} from \"@floating-ui/react\";\nimport { type ReactNode, useRef, useState } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type TooltipPlacement = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface TooltipProps {\n /** The element the tooltip is anchored to. */\n children: ReactNode;\n /** Tooltip text. */\n content: ReactNode;\n /** Placement relative to the trigger. @default \"top\" */\n placement?: TooltipPlacement;\n /** Delay in ms before showing. @default 200 */\n delay?: number;\n /** Additional CSS classes on the tooltip. */\n className?: string;\n}\n\n/* ── Arrow side mapping ───────────────────────────────────── */\n\nconst arrowSide: Record<string, string> = {\n top: \"bottom\",\n bottom: \"top\",\n left: \"right\",\n right: \"left\",\n};\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Tooltip({\n children,\n content,\n placement = \"top\",\n delay = 200,\n className = \"\",\n}: TooltipProps) {\n const [isOpen, setIsOpen] = useState(false);\n const arrowRef = useRef<HTMLDivElement>(null);\n\n const {\n refs,\n floatingStyles,\n context,\n middlewareData,\n placement: actualPlacement,\n } = useFloating({\n open: isOpen,\n onOpenChange: setIsOpen,\n placement,\n middleware: [\n offset(8),\n flip(),\n shift({ padding: 8 }),\n // eslint-disable-next-line react-hooks/refs -- floating-ui arrow middleware needs ref object\n arrow({ element: arrowRef }),\n ],\n whileElementsMounted: autoUpdate,\n });\n\n const hover = useHover(context, { delay, move: false });\n const focus = useFocus(context);\n const dismiss = useDismiss(context);\n const role = useRole(context, { role: \"tooltip\" });\n\n const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);\n\n /* Wrap child to attach floating-ui ref + interaction props */\n\n /* Arrow positioning */\n const side = actualPlacement.split(\"-\")[0];\n const arrowX = middlewareData.arrow?.x;\n const arrowY = middlewareData.arrow?.y;\n\n return (\n <>\n <span ref={refs.setReference} className=\"inline-flex\" {...getReferenceProps()}>\n {children}\n </span>\n\n {isOpen && content && (\n <FloatingPortal>\n <div\n // eslint-disable-next-line react-hooks/refs -- floating-ui callback ref setter, not a .current read\n ref={refs.setFloating}\n style={floatingStyles}\n role=\"tooltip\"\n className={[\n \"z-50 max-w-[var(--container-xs)] px-3 py-1.5 rounded-md text-small\",\n \"bg-text-primary text-bg-primary shadow-md\",\n \"pointer-events-none animate-floating-in\",\n className,\n ].join(\" \")}\n {...getFloatingProps()}\n >\n {content}\n {/* Arrow */}\n <div\n ref={arrowRef}\n className=\"absolute size-2 bg-text-primary rotate-45\"\n style={{\n left: arrowX != null ? `${arrowX}px` : \"\",\n top: arrowY != null ? `${arrowY}px` : \"\",\n [arrowSide[side]]: \"-4px\",\n }}\n />\n </div>\n </FloatingPortal>\n )}\n </>\n );\n}\n","import { useCallback } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport type Language = \"en\" | \"fr\";\n\n/**\n * Hook providing the current language and a toggle / setter.\n *\n * ```tsx\n * const { language, toggleLanguage, setLanguage } = useLanguage();\n * ```\n */\nexport function useLanguage() {\n const { i18n } = useTranslation();\n\n const language = (i18n.language ?? \"en\") as Language;\n\n const setLanguage = useCallback(\n (lng: Language) => {\n i18n.changeLanguage(lng);\n },\n [i18n],\n );\n\n const toggleLanguage = useCallback(() => {\n setLanguage(language === \"en\" ? \"fr\" : \"en\");\n }, [language, setLanguage]);\n\n return { language, toggleLanguage, setLanguage } as const;\n}\n","import { useLanguage, type Language } from \"../../hooks/useLanguage\";\n\nexport interface LanguageSwitcherProps {\n /** Additional CSS classes. */\n className?: string;\n}\n\nconst languages: { value: Language; label: string }[] = [\n { value: \"en\", label: \"EN\" },\n { value: \"fr\", label: \"FR\" },\n];\n\n/**\n * Pill-style toggle for switching between EN and FR.\n *\n * Designed to sit inside the TopNav `actions` slot.\n */\nexport function LanguageSwitcher({ className = \"\" }: LanguageSwitcherProps) {\n const { language, setLanguage } = useLanguage();\n\n return (\n <div\n role=\"radiogroup\"\n aria-label=\"Language\"\n className={[\n \"inline-flex items-center rounded-full border border-border bg-bg-secondary p-0.5\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {languages.map(({ value, label }) => {\n const isActive = language === value;\n return (\n <button\n key={value}\n type=\"button\"\n role=\"radio\"\n aria-checked={isActive}\n onClick={() => setLanguage(value)}\n className={[\n \"rounded-full px-2.5 py-0.5 text-caption font-medium transition-colors cursor-pointer\",\n isActive\n ? \"bg-primary text-white shadow-sm\"\n : \"text-text-secondary hover:text-text-primary\",\n ].join(\" \")}\n >\n {label}\n </button>\n );\n })}\n </div>\n );\n}\n","import { useCallback, useEffect, useSyncExternalStore } from \"react\";\n\nconst STORAGE_KEY = \"poesis-theme\";\ntype Theme = \"light\" | \"dark\";\n\n/** Listeners for useSyncExternalStore */\nconst listeners = new Set<() => void>();\n\nfunction notify() {\n listeners.forEach((l) => l());\n}\n\nfunction subscribe(callback: () => void) {\n listeners.add(callback);\n return () => listeners.delete(callback);\n}\n\nfunction getSnapshot(): Theme {\n return document.documentElement.classList.contains(\"dark\") ? \"dark\" : \"light\";\n}\n\nfunction getServerSnapshot(): Theme {\n return \"light\";\n}\n\n/**\n * Apply theme class to <html> and persist to localStorage.\n */\nfunction applyTheme(theme: Theme) {\n if (theme === \"dark\") {\n document.documentElement.classList.add(\"dark\");\n } else {\n document.documentElement.classList.remove(\"dark\");\n }\n localStorage.setItem(STORAGE_KEY, theme);\n notify();\n}\n\n/**\n * Initialise theme from localStorage or system preference.\n * Call once at app startup (e.g. in main.tsx or a top-level effect).\n */\nexport function initTheme() {\n const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;\n if (stored) {\n applyTheme(stored);\n return;\n }\n const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n applyTheme(prefersDark ? \"dark\" : \"light\");\n}\n\n/**\n * Hook providing the current theme and a toggle function.\n *\n * ```tsx\n * const { theme, toggleTheme } = useTheme();\n * ```\n */\nexport function useTheme() {\n const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n // Listen for system preference changes when no stored preference\n useEffect(() => {\n const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = (e: MediaQueryListEvent) => {\n if (!localStorage.getItem(STORAGE_KEY)) {\n applyTheme(e.matches ? \"dark\" : \"light\");\n }\n };\n mq.addEventListener(\"change\", handler);\n return () => mq.removeEventListener(\"change\", handler);\n }, []);\n\n const toggleTheme = useCallback(() => {\n applyTheme(theme === \"dark\" ? \"light\" : \"dark\");\n }, [theme]);\n\n const setTheme = useCallback((t: Theme) => {\n applyTheme(t);\n }, []);\n\n return { theme, toggleTheme, setTheme, isDark: theme === \"dark\" } as const;\n}\n","import { MoonFill, SunFill } from \"react-bootstrap-icons\";\nimport { useTranslation } from \"react-i18next\";\nimport { useTheme } from \"../../hooks/useTheme\";\nimport { IconButton, type IconButtonSize, type IconButtonVariant } from \"./IconButton\";\n\nexport interface ThemeToggleProps {\n /** Visual style. @default \"ghost\" */\n variant?: IconButtonVariant;\n /** Size preset. @default \"md\" */\n size?: IconButtonSize;\n /** Additional CSS classes. */\n className?: string;\n}\n\n/**\n * Sun/moon icon button that toggles between light and dark mode.\n *\n * Designed to sit inside the TopNav `actions` slot.\n */\nexport function ThemeToggle({ variant = \"ghost\", size = \"md\", className }: ThemeToggleProps) {\n const { isDark, toggleTheme } = useTheme();\n const { t } = useTranslation();\n\n return (\n <IconButton\n variant={variant}\n size={size}\n icon={isDark ? <SunFill size={16} /> : <MoonFill size={16} />}\n aria-label={t(\"theme.toggleTheme\")}\n onClick={toggleTheme}\n className={className}\n />\n );\n}\n","import { type ReactNode } from \"react\";\nimport { List } from \"react-bootstrap-icons\";\nimport type { NavCategory } from \"./types\";\n\nconst EPFL_LOGO_URL =\n \"https://www.epfl.ch/campus/services/website//wp-content/themes/wp-theme-2018/assets/svg/epfl-logo.svg?refresh=now\";\n\nexport interface TopNavProps {\n /** Full custom logo element — takes precedence over logoUrl */\n logo?: ReactNode;\n /** URL of a logo image (defaults to the official EPFL SVG) */\n logoUrl?: string;\n /** Top-level navigation categories */\n categories?: NavCategory[];\n /** Currently active category id */\n activeCategoryId?: string;\n /** Called when a category tab is clicked */\n onCategoryChange?: (id: string) => void;\n /** Slot for right-side actions (language switcher, theme toggle, avatar) */\n actions?: ReactNode;\n /** Called when the mobile burger button is clicked */\n onBurgerClick?: () => void;\n}\n\n/**\n * Fixed top navigation bar.\n *\n * Desktop: Logo + category tabs + actions.\n * Mobile (<lg): Logo + burger + actions.\n */\nexport function TopNav({\n logo,\n logoUrl,\n categories = [],\n activeCategoryId,\n onCategoryChange,\n actions,\n onBurgerClick,\n}: TopNavProps) {\n return (\n <header className=\"fixed inset-x-0 top-0 z-40 flex h-14 items-center border-b border-border bg-bg-primary px-md\">\n {/* ── Burger (mobile only) ── */}\n <button\n type=\"button\"\n onClick={onBurgerClick}\n className=\"mr-sm flex size-9 items-center justify-center rounded-md text-text-secondary hover:bg-bg-tertiary hover:text-text-primary poesis-mobile-only\"\n aria-label=\"Open menu\"\n >\n <List size={22} />\n </button>\n\n {/* ── Logo ── */}\n <div className=\"mr-lg flex shrink-0 items-center\">\n {logo ?? (\n <img\n src={logoUrl ?? EPFL_LOGO_URL}\n alt=\"EPFL\"\n className=\"h-[30px] w-auto\"\n />\n )}\n </div>\n\n {/* ── Category tabs (desktop) ── */}\n <nav className=\"poesis-desktop-flex flex-1 items-center gap-xs\" aria-label=\"Main categories\">\n {categories.map((cat) => {\n const isActive = cat.id === activeCategoryId;\n return (\n <button\n key={cat.id}\n type=\"button\"\n onClick={() => onCategoryChange?.(cat.id)}\n className={[\n \"flex items-center gap-xs rounded-md px-sm py-xs text-small font-medium transition-colors\",\n isActive\n ? \"bg-primary/10 text-primary\"\n : \"text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={isActive ? \"page\" : undefined}\n >\n {cat.icon && <cat.icon size={16} />}\n {cat.label}\n </button>\n );\n })}\n </nav>\n\n {/* ── Spacer (mobile — push actions right) ── */}\n <div className=\"flex-1 poesis-mobile-only\" />\n\n {/* ── Actions slot ── */}\n {actions && <div className=\"flex items-center gap-xs\">{actions}</div>}\n </header>\n );\n}\n","import { useState } from \"react\";\nimport { ChevronDown, ChevronRight } from \"react-bootstrap-icons\";\nimport { Badge } from \"../ui/Badge\";\nimport { Tooltip } from \"../ui/Tooltip\";\nimport type { IconComponent, NavLinkCounter, NavSection } from \"./types\";\n\n// ── Fold state persistence ──────────────────────────────────\n\nconst FOLD_KEY = \"sidebar-fold-state\";\n\nconst slugify = (text: string): string =>\n text\n .toString()\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .trim()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n\nconst readFoldState = (key: string): boolean => {\n try {\n const raw = localStorage.getItem(FOLD_KEY);\n if (raw) return JSON.parse(raw)[key] === true;\n } catch {\n /* ignore */\n }\n return false;\n};\n\nconst writeFoldState = (key: string, open: boolean) => {\n try {\n const raw = localStorage.getItem(FOLD_KEY);\n const state = raw ? JSON.parse(raw) : {};\n state[key] = open;\n localStorage.setItem(FOLD_KEY, JSON.stringify(state));\n } catch {\n /* ignore */\n }\n};\n\nfunction useFoldState(title: string | undefined, foldable: boolean) {\n const storageKey = `sidebar-${title ? slugify(title) : \"fallback\"}-open`;\n const [isOpen, setIsOpen] = useState(foldable ? readFoldState(storageKey) : true);\n\n const toggle = () => {\n if (!foldable) return;\n const next = !isOpen;\n setIsOpen(next);\n writeFoldState(storageKey, next);\n };\n\n return { isOpen, toggle };\n}\n\n// ── Types ───────────────────────────────────────────────────\n\nexport interface HomeLinkConfig {\n label: string;\n href: string;\n icon?: IconComponent;\n}\n\nexport interface SideNavProps {\n /** Grouped navigation sections for the active category */\n sections?: NavSection[];\n /** Currently active link id (for highlight) */\n activeLinkId?: string;\n /** Called when a link is clicked */\n onLinkClick?: (href: string) => void;\n /** Whether the sidebar is collapsed to icon-only mode */\n collapsed?: boolean;\n /** Called to toggle collapsed state */\n onToggleCollapse?: () => void;\n /** Whether sections can be folded and fold state persisted to localStorage */\n foldable?: boolean;\n /** Optional home link rendered at the top of the sidebar */\n homeLink?: HomeLinkConfig;\n}\n\n/**\n * Sidebar navigation with grouped, collapsible sections.\n *\n * - 256px wide (or 64px collapsed)\n * - Hidden on mobile (content moves into BurgerDrawer)\n * - Fixed height, scrollable when content overflows\n */\nexport function SideNav({\n sections = [],\n activeLinkId,\n onLinkClick,\n collapsed = false,\n foldable = false,\n homeLink,\n}: SideNavProps) {\n return (\n <aside\n className={[\n \"poesis-desktop-flex flex-col border-r border-border bg-bg-secondary overflow-y-auto transition-[width] duration-200\",\n collapsed ? \"w-16\" : \"w-64\",\n ].join(\" \")}\n >\n <nav aria-label=\"Page navigation\">\n <ul className=\"list-none m-0 p-0\">\n {/* Home link */}\n {homeLink && (\n <li className=\"border-b border-border\">\n <button\n type=\"button\"\n onClick={() => onLinkClick?.(homeLink.href)}\n title={collapsed ? (typeof homeLink.label === \"string\" ? homeLink.label : undefined) : undefined}\n className={[\n \"flex w-full items-center gap-3 px-4 py-3.5 text-sm no-underline font-medium transition-colors\",\n collapsed ? \"justify-center\" : \"\",\n activeLinkId === homeLink.href\n ? \"bg-bg-tertiary text-text-primary border-r-2 border-red-600\"\n : \"text-text-secondary hover:bg-bg-secondary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={activeLinkId === homeLink.href ? \"page\" : undefined}\n >\n {homeLink.icon && <homeLink.icon size={18} className=\"shrink-0\" />}\n {!collapsed && <span>{homeLink.label}</span>}\n </button>\n </li>\n )}\n\n {/* Menu sections */}\n {sections.map((section) => (\n <SideNavSection\n key={section.id}\n section={section}\n activeLinkId={activeLinkId}\n onLinkClick={onLinkClick}\n collapsed={collapsed}\n foldable={foldable}\n />\n ))}\n </ul>\n </nav>\n </aside>\n );\n}\n\n/* ── Counter badges ───────────────────────────────────────── */\n\nfunction NavLinkCounters({ counters }: { counters?: NavLinkCounter[] }) {\n const visible = counters?.filter((c) => c.count > 0);\n if (!visible?.length) return null;\n\n return (\n <span className=\"ml-auto flex items-center gap-1\">\n {visible.map((counter, i) => (\n <Tooltip key={i} content={counter.tooltip} placement=\"top\">\n <Badge color={counter.color}>\n {counter.count > 99 ? \"99+\" : counter.count}\n </Badge>\n </Tooltip>\n ))}\n </span>\n );\n}\n\n/* ── Collapsible section ──────────────────────────────────── */\n\nfunction SideNavSection({\n section,\n activeLinkId,\n onLinkClick,\n collapsed,\n foldable,\n}: {\n section: NavSection;\n activeLinkId?: string;\n onLinkClick?: (href: string) => void;\n collapsed: boolean;\n foldable: boolean;\n}) {\n const { isOpen, toggle } = useFoldState(section.title, foldable);\n\n return (\n <li className=\"border-b border-border last:border-b-0\">\n {/* Section heading */}\n {section.title && !collapsed && (\n foldable ? (\n <button\n type=\"button\"\n onClick={toggle}\n className={[\n \"flex items-center gap-2 w-full text-left px-5 py-4 text-xs font-bold uppercase tracking-wider hover:text-text-primary hover:bg-bg-secondary transition-colors cursor-pointer bg-transparent border-none\",\n section.titleClassName ?? \"text-text-muted\",\n ].join(\" \")}\n >\n {isOpen ? (\n <ChevronDown className=\"w-3 h-3 shrink-0\" />\n ) : (\n <ChevronRight className=\"w-3 h-3 shrink-0\" />\n )}\n <span>{section.title}</span>\n </button>\n ) : (\n <span className={[\n \"block px-5 py-4 text-xs font-bold uppercase tracking-wider\",\n section.titleClassName ?? \"text-text-muted\",\n ].join(\" \")}>\n {section.title}\n </span>\n )\n )}\n\n {/* Links */}\n {(isOpen || collapsed) && (\n <ul className=\"list-none m-0 p-0 pb-1\">\n {section.links.map((link) => {\n const isActive = link.id === activeLinkId;\n return (\n <li key={link.id}>\n <button\n type=\"button\"\n onClick={() => onLinkClick?.(link.href)}\n title={collapsed ? (link.titleText ?? (typeof link.label === \"string\" ? link.label : undefined)) : undefined}\n className={[\n \"flex w-full items-center gap-sm px-4 py-3 text-sm no-underline transition-colors\",\n collapsed ? \"justify-center\" : \"\",\n isActive\n ? \"bg-bg-tertiary text-text-primary font-medium border-r-2 border-red-600\"\n : \"text-text-secondary hover:bg-bg-secondary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={isActive ? \"page\" : undefined}\n >\n {link.icon && <link.icon size={18} />}\n {!collapsed && <span>{link.label}</span>}\n {!collapsed && <NavLinkCounters counters={link.counters} />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </li>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { ChevronDown, XLg } from \"react-bootstrap-icons\";\nimport { Badge } from \"../ui/Badge\";\nimport { Tooltip } from \"../ui/Tooltip\";\nimport type { NavCategory, NavLinkCounter, NavSection } from \"./types\";\n\nexport interface BurgerDrawerProps {\n /** Whether the drawer is open */\n open: boolean;\n /** Called to close the drawer */\n onClose: () => void;\n /** Top-level categories */\n categories?: NavCategory[];\n /** Active category id */\n activeCategoryId?: string;\n /** Called when a category is selected */\n onCategoryChange?: (id: string) => void;\n /** Sections for the active category */\n sections?: NavSection[];\n /** Active link id */\n activeLinkId?: string;\n /** Called when a link is clicked */\n onLinkClick?: (href: string) => void;\n}\n\n/**\n * Mobile slide-over drawer that merges TopNav categories and SideNav links.\n * Slides in from the left with a backdrop overlay.\n */\nexport function BurgerDrawer({\n open,\n onClose,\n categories = [],\n activeCategoryId,\n onCategoryChange,\n sections = [],\n activeLinkId,\n onLinkClick,\n}: BurgerDrawerProps) {\n const drawerRef = useRef<HTMLDivElement>(null);\n\n // Close on Escape\n useEffect(() => {\n if (!open) return;\n const handler = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [open, onClose]);\n\n // Trap focus inside drawer when open\n useEffect(() => {\n if (!open) return;\n const el = drawerRef.current;\n el?.focus();\n }, [open]);\n\n // Prevent body scroll when open\n useEffect(() => {\n if (open) {\n document.body.style.overflow = \"hidden\";\n } else {\n document.body.style.overflow = \"\";\n }\n return () => {\n document.body.style.overflow = \"\";\n };\n }, [open]);\n\n return (\n <>\n {/* ── Backdrop ── */}\n <div\n className={[\n \"fixed inset-0 z-50 bg-black/40 transition-opacity poesis-mobile-only\",\n open ? \"opacity-100\" : \"pointer-events-none opacity-0\",\n ].join(\" \")}\n onClick={onClose}\n aria-hidden=\"true\"\n />\n\n {/* ── Drawer panel ── */}\n <div\n ref={drawerRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Navigation menu\"\n className={[\n \"fixed inset-y-0 left-0 z-50 flex w-72 flex-col bg-bg-primary shadow-xl transition-transform duration-200 poesis-mobile-only\",\n open ? \"translate-x-0\" : \"-translate-x-full\",\n ].join(\" \")}\n >\n {/* ── Header ── */}\n <div className=\"flex h-14 items-center justify-between border-b border-border px-md\">\n <span className=\"text-h3 font-bold text-epfl-red\">EPFL</span>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"flex size-9 items-center justify-center rounded-md text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\"\n aria-label=\"Close menu\"\n >\n <XLg size={18} />\n </button>\n </div>\n\n {/* ── Scrollable content ── */}\n <div className=\"flex-1 overflow-y-auto p-sm\">\n {/* Category pills */}\n {categories.length > 0 && (\n <div className=\"mb-md flex flex-col gap-xs\">\n <span className=\"px-sm text-caption font-semibold uppercase tracking-wide text-text-muted\">\n Categories\n </span>\n {categories.map((cat) => {\n const isActive = cat.id === activeCategoryId;\n return (\n <button\n key={cat.id}\n type=\"button\"\n onClick={() => {\n onCategoryChange?.(cat.id);\n }}\n className={[\n \"flex items-center gap-sm rounded-md px-sm py-xs text-small font-medium transition-colors\",\n isActive\n ? \"bg-primary/10 text-primary\"\n : \"text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\",\n ].join(\" \")}\n >\n {cat.icon && <cat.icon size={16} />}\n {cat.label}\n </button>\n );\n })}\n </div>\n )}\n\n {/* Page links by section */}\n {sections.map((section) => (\n <DrawerSection\n key={section.id}\n section={section}\n activeLinkId={activeLinkId}\n onLinkClick={(href) => {\n onLinkClick?.(href);\n onClose();\n }}\n />\n ))}\n </div>\n </div>\n </>\n );\n}\n\n/* ── Counter badges for drawer links ─────────────────────── */\n\nfunction DrawerLinkCounters({ counters }: { counters?: NavLinkCounter[] }) {\n const visible = counters?.filter((c) => c.count > 0);\n if (!visible?.length) return null;\n\n return (\n <span className=\"ml-auto flex items-center gap-1\">\n {visible.map((counter, i) => (\n <Tooltip key={i} content={counter.tooltip} placement=\"top\">\n <Badge color={counter.color}>\n {counter.count > 99 ? \"99+\" : counter.count}\n </Badge>\n </Tooltip>\n ))}\n </span>\n );\n}\n\n/* ── Collapsible section inside drawer ───────────────────── */\n\nfunction DrawerSection({\n section,\n activeLinkId,\n onLinkClick,\n}: {\n section: NavSection;\n activeLinkId?: string;\n onLinkClick?: (href: string) => void;\n}) {\n const [open, setOpen] = useState(true);\n\n return (\n <div className=\"mb-xs flex flex-col\">\n {section.title && (\n <button\n type=\"button\"\n onClick={() => setOpen((o) => !o)}\n className=\"flex items-center justify-between rounded-md px-sm py-xs text-caption font-semibold uppercase tracking-wide text-text-muted hover:text-text-secondary\"\n >\n <span>{section.title}</span>\n <ChevronDown\n size={14}\n className={`transition-transform ${open ? \"\" : \"-rotate-90\"}`}\n />\n </button>\n )}\n\n {open && (\n <ul className=\"flex flex-col gap-px\">\n {section.links.map((link) => {\n const isActive = link.id === activeLinkId;\n return (\n <li key={link.id}>\n <button\n type=\"button\"\n onClick={() => onLinkClick?.(link.href)}\n className={[\n \"flex w-full items-center gap-sm rounded-md px-sm py-xs text-small transition-colors\",\n isActive\n ? \"bg-primary/10 font-medium text-primary\"\n : \"text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={isActive ? \"page\" : undefined}\n >\n {link.icon && <link.icon size={18} />}\n <span>{link.label}</span>\n <DrawerLinkCounters counters={link.counters} />\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n","const LOGO_URL =\n \"https://www.epfl.ch/campus/services/website//wp-content/themes/wp-theme-2018/assets/svg/epfl-logo.svg?refresh=now\";\n\nconst getCurrentYear = () => new Date().getFullYear();\n\nexport function Footer() {\n const scrollToTop = (e: React.MouseEvent<HTMLButtonElement>) => {\n const scrollable = e.currentTarget.closest(\"[data-scrollable]\");\n if (scrollable) {\n scrollable.scrollTo({ top: 0, behavior: \"smooth\" });\n } else {\n window.scrollTo({ top: 0, behavior: \"smooth\" });\n }\n };\n\n return (\n <footer role=\"contentinfo\" className=\"bg-bg-secondary border-t border-border mt-auto\">\n <div className=\"mx-auto max-w-[var(--container-6xl)] px-lg py-xl\">\n <div className=\"flex flex-wrap gap-lg\">\n {/* Logo */}\n <div className=\"shrink-0\">\n <a href=\"https://www.epfl.ch/en/\">\n <img\n src={LOGO_URL}\n alt=\"Logo EPFL\"\n className=\"h-8 w-auto\"\n style={{ filter: \"var(--logo-filter)\", opacity: \"var(--logo-opacity)\" }}\n />\n </a>\n </div>\n\n {/* Contact & Legal */}\n <div className=\"flex-1 min-w-[280px]\">\n <div className=\"flex flex-wrap gap-sm items-center mb-sm text-small\">\n <span className=\"font-medium text-text-primary\">Contact</span>\n <span className=\"text-text-secondary\">EPFL CH-1015 Lausanne</span>\n <span className=\"text-text-secondary\">+41 21 693 11 11</span>\n </div>\n\n {/* Legal */}\n <div className=\"flex flex-wrap gap-sm text-small mt-sm border-t border-border pt-sm\">\n <div className=\"flex gap-sm\">\n <a\n href=\"https://www.epfl.ch/about/overview/regulations-and-guidelines/disclaimer/\"\n className=\"text-text-secondary no-underline hover:text-primary transition-colors\"\n >\n Accessibility\n </a>\n <a\n href=\"https://www.epfl.ch/about/overview/regulations-and-guidelines/disclaimer/\"\n className=\"text-text-secondary no-underline hover:text-primary transition-colors\"\n >\n Disclaimer\n </a>\n <a\n href=\"https://go.epfl.ch/privacy-policy/\"\n className=\"text-text-secondary no-underline hover:text-primary transition-colors\"\n >\n Privacy policy\n </a>\n </div>\n <p className=\"m-0 text-text-muted text-small\">\n &copy; {getCurrentYear()} EPFL, all rights reserved\n </p>\n </div>\n </div>\n </div>\n\n {/* Back to top */}\n <div className=\"flex justify-end pt-md\">\n <button\n type=\"button\"\n onClick={scrollToTop}\n aria-label=\"Back to top\"\n className=\"bg-transparent border border-border-strong text-text-muted rounded-full w-9 h-9 cursor-pointer flex items-center justify-center hover:bg-bg-tertiary hover:text-text-primary transition-colors\"\n >\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"w-4 h-4\"\n >\n <polyline points=\"18 15 12 9 6 15\" />\n </svg>\n </button>\n </div>\n </div>\n </footer>\n );\n}\n","import { BoxArrowInRight, BoxArrowRight } from \"react-bootstrap-icons\";\nimport { useTranslation } from \"react-i18next\";\nimport type { AuthUser } from \"../../hooks/useAuth\";\nimport { Avatar } from \"../ui/Avatar\";\nimport { Button } from \"../ui/Button\";\nimport { DropdownDivider, DropdownItem, DropdownMenu } from \"../ui/DropdownMenu\";\n\n/* ------------------------------------------------------------------ */\n/* LoginButton (shown when not authenticated) */\n/* ------------------------------------------------------------------ */\n\nexport interface LoginButtonProps {\n /** Called when the user clicks the login button. */\n onLogin: () => void;\n}\n\nexport function LoginButton({ onLogin }: LoginButtonProps) {\n const { t } = useTranslation();\n return (\n <Button\n variant=\"primary\"\n size=\"sm\"\n iconLeft={<BoxArrowInRight size={14} />}\n onClick={onLogin}\n >\n {t(\"auth.login\")}\n </Button>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* UserMenu (shown when authenticated) */\n/* ------------------------------------------------------------------ */\n\nexport interface UserMenuProps {\n /** The authenticated user. */\n user: AuthUser;\n /** Called when the user clicks the logout item. */\n onLogout: () => void;\n}\n\nexport function UserMenu({ user, onLogout }: UserMenuProps) {\n const { t } = useTranslation();\n\n return (\n <DropdownMenu\n trigger={\n <Avatar\n src={user.avatarUrl}\n name={user.name}\n size=\"sm\"\n className=\"cursor-pointer\"\n />\n }\n placement=\"bottom-end\"\n >\n <div className=\"px-3 py-2\">\n <p className=\"text-small font-medium text-text-primary m-0\">{user.name}</p>\n <p className=\"text-caption text-text-secondary m-0\">{user.email}</p>\n </div>\n <DropdownDivider />\n <DropdownItem icon={<BoxArrowRight size={14} />} onClick={onLogout}>\n {t(\"auth.logout\")}\n </DropdownItem>\n </DropdownMenu>\n );\n}\n","import { useState, type ReactNode } from \"react\";\nimport { BurgerDrawer } from \"./BurgerDrawer\";\nimport { Footer } from \"./Footer\";\nimport { SideNav, type HomeLinkConfig } from \"./SideNav\";\nimport { TopNav } from \"./TopNav\";\nimport type { NavCategory, NavSection } from \"./types\";\n\nexport interface PageShellProps {\n /** Page content */\n children: ReactNode;\n /** Custom logo element for TopNav */\n logo?: ReactNode;\n /** URL of a logo image for TopNav (defaults to EPFL SVG) */\n logoUrl?: string;\n /** Top-level categories */\n categories?: NavCategory[];\n /** Active category id */\n activeCategoryId?: string;\n /** Called when a category tab is clicked */\n onCategoryChange?: (id: string) => void;\n /** Sidebar sections for the active category */\n sections?: NavSection[];\n /** Active link id for sidebar highlight */\n activeLinkId?: string;\n /** Called when a sidebar link is clicked */\n onLinkClick?: (href: string) => void;\n /** Slot for TopNav right-side actions */\n actions?: ReactNode;\n /** Whether the sidebar is collapsed */\n sideNavCollapsed?: boolean;\n /** Whether sidebar sections can be folded with localStorage persistence */\n sideNavFoldable?: boolean;\n /** Optional home link at the top of the sidebar */\n sideNavHomeLink?: HomeLinkConfig;\n /** Whether to show the footer (defaults to true) */\n showFooter?: boolean;\n}\n\n/**\n * Full application shell composing TopNav + SideNav + content area.\n *\n * - Desktop: fixed top bar + sidebar + scrollable main\n * - Mobile: fixed top bar + burger drawer + full-width main\n */\nexport function PageShell({\n children,\n logo,\n logoUrl,\n categories = [],\n activeCategoryId,\n onCategoryChange,\n sections = [],\n activeLinkId,\n onLinkClick,\n actions,\n sideNavCollapsed = false,\n sideNavFoldable = false,\n sideNavHomeLink,\n showFooter = true,\n}: PageShellProps) {\n const [drawerOpen, setDrawerOpen] = useState(false);\n\n return (\n <div className=\"flex h-screen flex-col bg-bg-primary\">\n {/* ── Top Navigation ── */}\n <TopNav\n logo={logo}\n logoUrl={logoUrl}\n categories={categories}\n activeCategoryId={activeCategoryId}\n onCategoryChange={onCategoryChange}\n actions={actions}\n onBurgerClick={() => setDrawerOpen(true)}\n />\n\n {/* ── Body: SideNav + Main ── */}\n <div className=\"flex flex-1 overflow-hidden pt-14\">\n {/* Sidebar (desktop only — hidden via CSS on mobile) */}\n <SideNav\n sections={sections}\n activeLinkId={activeLinkId}\n onLinkClick={onLinkClick}\n collapsed={sideNavCollapsed}\n foldable={sideNavFoldable}\n homeLink={sideNavHomeLink}\n />\n\n {/* Main content */}\n <main data-scrollable className=\"flex-1 overflow-y-auto flex flex-col\">\n <div className=\"flex-1 px-md py-lg lg:px-lg\">\n <div className=\"mx-auto max-w-[var(--container-6xl)]\">{children}</div>\n </div>\n {showFooter && <Footer />}\n </main>\n </div>\n\n {/* ── Mobile Drawer ── */}\n <BurgerDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n categories={categories}\n activeCategoryId={activeCategoryId}\n onCategoryChange={(id) => {\n onCategoryChange?.(id);\n setDrawerOpen(false);\n }}\n sections={sections}\n activeLinkId={activeLinkId}\n onLinkClick={(href) => {\n onLinkClick?.(href);\n setDrawerOpen(false);\n }}\n />\n </div>\n );\n}\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface CardProps extends ComponentPropsWithoutRef<\"div\"> {\n /** Optional header content — rendered above the body with a bottom border. */\n header?: ReactNode;\n /** Optional footer content — rendered below the body with a top border. */\n footer?: ReactNode;\n /** Optional actions aligned to the right inside the footer area. */\n actions?: ReactNode;\n /** Remove the default padding on the body section. */\n noPadding?: boolean;\n /** Card body content. */\n children?: ReactNode;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Elevated surface container with optional header, footer, and actions slots.\n *\n * Uses `radius-md` and `shadow-md` from the design tokens.\n */\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n { header, footer, actions, noPadding = false, className = \"\", children, ...rest },\n ref,\n) {\n const hasFooter = footer || actions;\n\n return (\n <div\n ref={ref}\n className={[\n \"rounded-md shadow-md border border-border bg-bg-secondary\",\n \"flex flex-col overflow-hidden\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {/* Header */}\n {header && (\n <div className=\"px-lg py-md border-b border-border\">\n <div className=\"font-semibold text-body text-text-primary\">{header}</div>\n </div>\n )}\n\n {/* Body */}\n <div className={noPadding ? \"\" : \"p-lg\"}>{children}</div>\n\n {/* Footer / Actions */}\n {hasFooter && (\n <div className=\"px-lg py-md border-t border-border flex items-center gap-3\">\n {footer && <div className=\"flex-1 min-w-0\">{footer}</div>}\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n </div>\n );\n});\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface DescriptionItem {\n /** Label / key text. */\n label: ReactNode;\n /** Value / description text. */\n value: ReactNode;\n}\n\nexport type DescriptionListLayout = \"vertical\" | \"horizontal\";\n\nexport interface DescriptionListProps extends ComponentPropsWithoutRef<\"dl\"> {\n /** Array of key-value items. */\n items: DescriptionItem[];\n /** Layout direction. @default \"horizontal\" */\n layout?: DescriptionListLayout;\n /** Show a light divider between items. @default true */\n dividers?: boolean;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Key-value pair layout for detail views.\n *\n * Supports horizontal (label left, value right) and vertical (label above value) layouts.\n */\nexport const DescriptionList = forwardRef<HTMLDListElement, DescriptionListProps>(\n function DescriptionList(\n { items, layout = \"horizontal\", dividers = true, className = \"\", ...rest },\n ref,\n ) {\n const isHorizontal = layout === \"horizontal\";\n\n return (\n <dl\n ref={ref}\n className={[\"text-body\", dividers && \"divide-y divide-border\", className]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {items.map((item, idx) => (\n <div\n key={idx}\n className={[\n \"py-sm\",\n isHorizontal\n ? \"flex flex-col sm:flex-row sm:gap-lg\"\n : \"flex flex-col gap-xs\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n <dt\n className={[\n \"font-medium text-text-secondary text-small shrink-0\",\n isHorizontal && \"sm:w-1/3\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {item.label}\n </dt>\n <dd className=\"text-text-primary\">{item.value}</dd>\n </div>\n ))}\n </dl>\n );\n },\n);\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface EmptyStateProps extends ComponentPropsWithoutRef<\"div\"> {\n /** Optional illustration or icon element. */\n illustration?: ReactNode;\n /** Primary message. */\n title: string;\n /** Optional secondary description. */\n description?: string;\n /** Optional call-to-action element (e.g. a Button). */\n action?: ReactNode;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Placeholder shown when a view has no content.\n *\n * Provides an illustration slot, message, and optional CTA.\n */\nexport const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(\n { illustration, title, description, action, className = \"\", ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={[\n \"flex flex-col items-center justify-center text-center py-2xl px-lg\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {/* Illustration / Icon */}\n {illustration && <div className=\"mb-lg text-text-secondary\">{illustration}</div>}\n\n {/* Title */}\n <h3 className=\"text-h3 font-semibold text-text-primary\">{title}</h3>\n\n {/* Description */}\n {description && (\n <p className=\"mt-xs text-body text-text-secondary max-w-[var(--container-md)]\">\n {description}\n </p>\n )}\n\n {/* CTA */}\n {action && <div className=\"mt-lg\">{action}</div>}\n </div>\n );\n});\n","import {\n type ComponentPropsWithoutRef,\n type ReactNode,\n forwardRef,\n useCallback,\n useMemo,\n useState,\n} from \"react\";\nimport {\n Sliders,\n SortAlphaDown,\n SortAlphaDownAlt,\n SortDown,\n SortNumericDown,\n SortNumericDownAlt,\n} from \"react-bootstrap-icons\";\nimport { Checkbox } from \"../ui/Checkbox\";\nimport { Popover } from \"../ui/Popover\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface SortState {\n /** Column key currently sorted. */\n column: string;\n /** Sort direction. */\n direction: SortDirection;\n}\n\nexport interface TableColumn<T> {\n /** Unique key used for sorting & React keying. */\n key: string;\n /** Column header label. */\n header: ReactNode;\n /** Render the cell content for a given row. */\n cell: (row: T, index: number) => ReactNode;\n /** Enable sorting on this column. @default false */\n sortable?: boolean;\n /**\n * Extract a sortable value from a row.\n * Required for built-in (uncontrolled) sorting.\n * The return type is also used to auto-detect the sort icon style.\n */\n sortValue?: (row: T) => string | number | null | undefined;\n /** Optional header cell className. */\n headerClassName?: string;\n /** Optional body cell className. */\n cellClassName?: string;\n /**\n * Whether this column can be hidden via the column-visibility popover.\n * @default true (when `columnVisibility` is enabled on the table)\n */\n hideable?: boolean;\n}\n\nexport interface TableProps<T> extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** Column definitions. */\n columns: TableColumn<T>[];\n /** Row data array. */\n data: T[];\n /** Unique key extractor for each row. */\n rowKey: (row: T, index: number) => string | number;\n /** Show striped rows. @default true */\n striped?: boolean;\n /** Controlled sort state. */\n sort?: SortState;\n /** Called when a sortable column header is clicked. */\n onSortChange?: (sort: SortState) => void;\n /** Content shown when data is empty. */\n emptyContent?: ReactNode;\n /** Make the table header row sticky when scrolling vertically. @default false */\n stickyHeader?: boolean;\n\n /* ── Toolbar ────────────────────────────────── */\n\n /**\n * Content rendered on the left side of the toolbar row (e.g. title, counters).\n * The toolbar row is shown when `toolbar` or `columnVisibility` is set.\n */\n toolbar?: ReactNode;\n\n /* ── Column visibility ─────────────────────── */\n\n /** Show a toolbar with a column-visibility toggle popover. @default false */\n columnVisibility?: boolean;\n /**\n * localStorage key for persisting hidden-column state.\n * When set, hidden columns are saved to / restored from localStorage.\n */\n columnVisibilityKey?: string;\n /** Controlled hidden-column keys. */\n hiddenColumns?: string[];\n /** Callback when hidden columns change (controlled mode). */\n onHiddenColumnsChange?: (hiddenColumns: string[]) => void;\n /** Initially hidden column keys (uncontrolled mode, ignored when `columnVisibilityKey` restores saved state). */\n defaultHiddenColumns?: string[];\n\n /* ── Row selection ─────────────────────────── */\n\n /**\n * Key of the column whose values identify each row for selection.\n * Setting this enables the selection checkbox column.\n */\n selectionKey?: keyof T & string;\n /** Controlled selected values (values of the `selectionKey` column). */\n selectedValues?: unknown[];\n /** Called when the set of selected values changes. */\n onSelectionChange?: (selectedValues: unknown[]) => void;\n}\n\n/* ── Sort icon helper ─────────────────────────────────────── */\n\ntype SortColumnType = \"text\" | \"numeric\";\n\nfunction detectColumnType<T>(column: TableColumn<T>, data: T[]): SortColumnType {\n if (!column.sortValue || data.length === 0) return \"text\";\n for (const row of data) {\n const v = column.sortValue(row);\n if (v != null) return typeof v === \"number\" ? \"numeric\" : \"text\";\n }\n return \"text\";\n}\n\nfunction SortIcon({\n active,\n direction,\n columnType,\n}: {\n active: boolean;\n direction?: SortDirection;\n columnType: SortColumnType;\n}) {\n const size = 14;\n if (!active) {\n return <SortDown size={size} className=\"ml-1 inline opacity-30\" aria-hidden />;\n }\n if (columnType === \"numeric\") {\n return direction === \"asc\" ? (\n <SortNumericDown size={size} className=\"ml-1 inline\" aria-hidden />\n ) : (\n <SortNumericDownAlt size={size} className=\"ml-1 inline\" aria-hidden />\n );\n }\n return direction === \"asc\" ? (\n <SortAlphaDown size={size} className=\"ml-1 inline\" aria-hidden />\n ) : (\n <SortAlphaDownAlt size={size} className=\"ml-1 inline\" aria-hidden />\n );\n}\n\n/* ── Column-visibility toolbar ────────────────────────────── */\n\nfunction ColumnVisibilityPopover<T>({\n columns,\n hiddenSet,\n onToggle,\n}: {\n columns: TableColumn<T>[];\n hiddenSet: Set<string>;\n onToggle: (key: string) => void;\n}) {\n const hideableColumns = columns.filter((c) => c.hideable !== false);\n const visibleCount = hideableColumns.filter((c) => !hiddenSet.has(c.key)).length;\n\n return (\n <Popover\n placement=\"bottom-end\"\n trigger={\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-small font-medium text-text-secondary hover:text-text-primary hover:bg-bg-secondary border border-border transition-colors\"\n aria-label=\"Toggle column visibility\"\n >\n <Sliders size={14} aria-hidden />\n Columns\n </button>\n }\n className=\"min-w-48\"\n >\n <div className=\"flex flex-col gap-1\">\n <p className=\"text-caption font-semibold text-text-secondary mb-1\">\n Visible columns\n </p>\n {hideableColumns.map((col) => {\n const isVisible = !hiddenSet.has(col.key);\n // Prevent hiding the last visible column\n const isLastVisible = isVisible && visibleCount <= 1;\n return (\n <Checkbox\n key={col.key}\n label={col.header}\n checked={isVisible}\n disabled={isLastVisible}\n onChange={() => onToggle(col.key)}\n />\n );\n })}\n </div>\n </Popover>\n );\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Data table with sortable headers, striped rows, column visibility toggling,\n * row selection, and a responsive scroll wrapper.\n *\n * Accepts generic row type `T` for full type safety.\n */\nexport const Table = forwardRef<HTMLDivElement, TableProps<unknown>>(function Table(\n {\n columns,\n data,\n rowKey,\n striped = false,\n sort,\n onSortChange,\n emptyContent,\n stickyHeader = false,\n toolbar,\n columnVisibility = false,\n columnVisibilityKey,\n hiddenColumns: controlledHidden,\n onHiddenColumnsChange,\n defaultHiddenColumns,\n selectionKey,\n selectedValues: controlledSelected,\n onSelectionChange,\n className = \"\",\n ...rest\n },\n ref,\n) {\n /* ── Sort state ───────────────────────────── */\n const [internalSort, setInternalSort] = useState<SortState | undefined>();\n const activeSort = sort ?? internalSort;\n\n const handleSort = useCallback(\n (columnKey: string) => {\n const next: SortState =\n activeSort?.column === columnKey && activeSort.direction === \"asc\"\n ? { column: columnKey, direction: \"desc\" }\n : { column: columnKey, direction: \"asc\" };\n\n if (onSortChange) {\n onSortChange(next);\n } else {\n setInternalSort(next);\n }\n },\n [activeSort, onSortChange],\n );\n\n /* ── Column visibility state ──────────────── */\n const [internalHidden, setInternalHidden] = useState<Set<string>>(() => {\n if (columnVisibilityKey) {\n try {\n const stored = localStorage.getItem(columnVisibilityKey);\n if (stored) return new Set(JSON.parse(stored) as string[]);\n } catch { /* ignore malformed data */ }\n }\n return new Set(defaultHiddenColumns ?? []);\n });\n const hiddenSet = useMemo(\n () => (controlledHidden ? new Set(controlledHidden) : internalHidden),\n [controlledHidden, internalHidden],\n );\n\n const handleColumnToggle = useCallback(\n (key: string) => {\n const next = new Set(hiddenSet);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n const arr = Array.from(next);\n if (columnVisibilityKey) {\n try { localStorage.setItem(columnVisibilityKey, JSON.stringify(arr)); } catch { /* quota exceeded */ }\n }\n if (onHiddenColumnsChange) {\n onHiddenColumnsChange(arr);\n } else {\n setInternalHidden(next);\n }\n },\n [hiddenSet, onHiddenColumnsChange, columnVisibilityKey],\n );\n\n /* ── Selection state ──────────────────────── */\n const [internalSelected, setInternalSelected] = useState<Set<unknown>>(() => new Set());\n const selectedSet = useMemo(\n () => (controlledSelected ? new Set(controlledSelected) : internalSelected),\n [controlledSelected, internalSelected],\n );\n\n const fireSelectionChange = useCallback(\n (next: Set<unknown>) => {\n const arr = Array.from(next);\n if (onSelectionChange) {\n onSelectionChange(arr);\n } else {\n setInternalSelected(next);\n }\n },\n [onSelectionChange],\n );\n\n const toggleRow = useCallback(\n (value: unknown) => {\n const next = new Set(selectedSet);\n if (next.has(value)) {\n next.delete(value);\n } else {\n next.add(value);\n }\n fireSelectionChange(next);\n },\n [selectedSet, fireSelectionChange],\n );\n\n const toggleAll = useCallback(\n (allValues: unknown[]) => {\n const allSelected = allValues.length > 0 && allValues.every((v) => selectedSet.has(v));\n fireSelectionChange(allSelected ? new Set() : new Set(allValues));\n },\n [selectedSet, fireSelectionChange],\n );\n\n /* ── Visible columns ──────────────────────── */\n const visibleColumns = useMemo(\n () =>\n columnVisibility ? columns.filter((c) => !hiddenSet.has(c.key)) : columns,\n [columns, hiddenSet, columnVisibility],\n );\n\n /* ── Sort data in uncontrolled mode ────────── */\n const sortedData = useMemo(() => {\n if (!activeSort || onSortChange) return data;\n const col = columns.find((c) => c.key === activeSort.column);\n if (!col?.sortValue) return data;\n const getValue = col.sortValue;\n return [...data].sort((a, b) => {\n const aVal = getValue(a);\n const bVal = getValue(b);\n if (aVal == null && bVal == null) return 0;\n if (aVal == null) return 1;\n if (bVal == null) return -1;\n const cmp =\n typeof aVal === \"number\" && typeof bVal === \"number\"\n ? aVal - bVal\n : String(aVal).localeCompare(String(bVal));\n return activeSort.direction === \"asc\" ? cmp : -cmp;\n });\n }, [data, activeSort, columns, onSortChange]);\n\n /* ── Selection helpers for current data ────── */\n const allSelectionValues = useMemo(() => {\n if (!selectionKey) return [];\n return sortedData.map((row) => (row as Record<string, unknown>)[selectionKey]);\n }, [sortedData, selectionKey]);\n\n const allSelected = allSelectionValues.length > 0 && allSelectionValues.every((v) => selectedSet.has(v));\n const someSelected = !allSelected && allSelectionValues.some((v) => selectedSet.has(v));\n\n /* Detect column types for icons */\n const columnTypes = useMemo(() => {\n const map = new Map<string, SortColumnType>();\n for (const col of columns) {\n if (col.sortable) {\n map.set(col.key, detectColumnType(col, data));\n }\n }\n return map;\n }, [columns, data]);\n\n const totalColumns = visibleColumns.length + (selectionKey ? 1 : 0);\n const isEmpty = sortedData.length === 0;\n\n return (\n <div\n ref={ref}\n className={[\"overflow-x-auto rounded-md border border-border\", className]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {/* ── Toolbar row ──── */}\n {(toolbar || columnVisibility) && (\n <div className=\"flex items-center justify-between gap-md px-md py-sm border-b border-border bg-bg-primary\">\n <div className=\"flex items-center gap-sm min-w-0\">{toolbar}</div>\n {columnVisibility && (\n <ColumnVisibilityPopover\n columns={columns}\n hiddenSet={hiddenSet}\n onToggle={handleColumnToggle}\n />\n )}\n </div>\n )}\n\n <table className=\"w-full text-body text-left\">\n {/* Head */}\n <thead className={stickyHeader ? \"sticky top-0 z-10\" : undefined}>\n <tr className=\"bg-bg-secondary border-b border-border\">\n {/* Selection header */}\n {selectionKey && (\n <th className=\"w-10 px-sm py-sm text-center\">\n <Checkbox\n checked={allSelected}\n indeterminate={someSelected}\n onChange={() => toggleAll(allSelectionValues)}\n aria-label=\"Select all rows\"\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n const isSorted = activeSort?.column === col.key;\n return (\n <th\n key={col.key}\n className={[\n \"px-md py-sm font-semibold text-small text-text-secondary whitespace-nowrap\",\n col.sortable &&\n \"cursor-pointer select-none hover:text-text-primary\",\n col.headerClassName,\n ]\n .filter(Boolean)\n .join(\" \")}\n onClick={col.sortable ? () => handleSort(col.key) : undefined}\n aria-sort={\n isSorted\n ? activeSort!.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : col.sortable\n ? \"none\"\n : undefined\n }\n >\n <span className=\"inline-flex items-center\">\n {col.header}\n {col.sortable && (\n <SortIcon\n active={isSorted}\n direction={\n isSorted ? activeSort!.direction : undefined\n }\n columnType={columnTypes.get(col.key) ?? \"text\"}\n />\n )}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n\n {/* Body */}\n <tbody>\n {isEmpty ? (\n <tr>\n <td\n colSpan={totalColumns}\n className=\"px-md py-xl text-center text-text-secondary\"\n >\n {emptyContent ?? \"No data available.\"}\n </td>\n </tr>\n ) : (\n sortedData.map((row, idx) => {\n const rowVal = selectionKey\n ? (row as Record<string, unknown>)[selectionKey]\n : undefined;\n return (\n <tr\n key={rowKey(row, idx)}\n className={[\n \"border-b border-border last:border-b-0 transition-colors\",\n striped && idx % 2 === 1 && \"bg-bg-tertiary/50\",\n \"hover:bg-bg-tertiary/70\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {/* Selection cell */}\n {selectionKey && (\n <td className=\"w-10 px-sm py-sm text-center\">\n <Checkbox\n checked={selectedSet.has(rowVal)}\n onChange={() => toggleRow(rowVal)}\n aria-label={`Select row ${rowVal}`}\n />\n </td>\n )}\n {visibleColumns.map((col) => (\n <td\n key={col.key}\n className={[\n \"px-md py-sm text-text-primary\",\n col.cellClassName,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {col.cell(row, idx)}\n </td>\n ))}\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n );\n}) as <T>(props: TableProps<T> & { ref?: React.Ref<HTMLDivElement> }) => React.ReactElement | null;\n","import { useState, type ReactNode } from \"react\";\nimport {\n CheckCircleFill,\n ExclamationTriangleFill,\n InfoCircleFill,\n X,\n XCircleFill,\n} from \"react-bootstrap-icons\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type AlertVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface AlertProps {\n /** Visual variant — controls icon, color, and background. */\n variant: AlertVariant;\n /** Optional bold title above the body. */\n title?: string;\n /** Alert body content. */\n children: ReactNode;\n /** Show a dismiss (×) button. */\n dismissible?: boolean;\n /** Callback fired when the dismiss button is clicked. */\n onDismiss?: () => void;\n /** Additional CSS classes on the root element. */\n className?: string;\n}\n\n/* ── Variant config ───────────────────────────────────────── */\n\nconst variantStyles: Record<\n AlertVariant,\n { bg: string; border: string; text: string; icon: typeof CheckCircleFill }\n> = {\n success: {\n bg: \"bg-success-bg\",\n border: \"border-success/30\",\n text: \"text-success\",\n icon: CheckCircleFill,\n },\n warning: {\n bg: \"bg-warning-bg\",\n border: \"border-warning/30\",\n text: \"text-warning\",\n icon: ExclamationTriangleFill,\n },\n error: {\n bg: \"bg-error-bg\",\n border: \"border-error/30\",\n text: \"text-error\",\n icon: XCircleFill,\n },\n info: {\n bg: \"bg-info-bg\",\n border: \"border-info/30\",\n text: \"text-info\",\n icon: InfoCircleFill,\n },\n};\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Alert({\n variant,\n title,\n children,\n dismissible = false,\n onDismiss,\n className = \"\",\n}: AlertProps) {\n const [visible, setVisible] = useState(true);\n const { bg, border, text, icon: Icon } = variantStyles[variant];\n\n if (!visible) return null;\n\n const handleDismiss = () => {\n setVisible(false);\n onDismiss?.();\n };\n\n return (\n <div\n role=\"alert\"\n className={`flex gap-3 rounded-md border p-md ${bg} ${border} ${className}`}\n >\n {/* Icon */}\n <Icon size={20} className={`shrink-0 mt-0.5 ${text}`} aria-hidden />\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {title && <p className={`font-semibold text-body ${text}`}>{title}</p>}\n <div className=\"text-body text-text-primary\">{children}</div>\n </div>\n\n {/* Dismiss */}\n {dismissible && (\n <button\n type=\"button\"\n onClick={handleDismiss}\n className=\"shrink-0 p-1 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label=\"Dismiss alert\"\n >\n <X size={16} />\n </button>\n )}\n </div>\n );\n}\n","import { useEffect, useRef, type KeyboardEvent, type ReactNode } from \"react\";\nimport { X } from \"react-bootstrap-icons\";\nimport { createPortal } from \"react-dom\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type DialogSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface DialogProps {\n /** Whether the dialog is open. */\n open: boolean;\n /** Callback to close the dialog. */\n onClose: () => void;\n /** Optional dialog title shown in the header. */\n title?: string;\n /** Width preset. Default: \"md\". */\n size?: DialogSize;\n /** Dialog body content. */\n children: ReactNode;\n /** Optional footer (buttons, etc.). */\n footer?: ReactNode;\n /** Additional CSS classes on the panel. */\n className?: string;\n}\n\n/* ── Size map ─────────────────────────────────────────────── */\n\nconst sizeClasses: Record<DialogSize, string> = {\n sm: \"max-w-[24rem]\",\n md: \"max-w-[32rem]\",\n lg: \"max-w-[42rem]\",\n};\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Dialog({\n open,\n onClose,\n title,\n size = \"md\",\n children,\n footer,\n className = \"\",\n}: DialogProps) {\n const panelRef = useRef<HTMLDivElement>(null);\n\n /* ── Focus trap: focus the panel when opened ── */\n useEffect(() => {\n if (!open) return;\n\n const prevActive = document.activeElement as HTMLElement | null;\n panelRef.current?.focus();\n\n return () => {\n prevActive?.focus();\n };\n }, [open]);\n\n /* ── Lock body scroll while open ── */\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n /* ── Keyboard: Esc to close, Tab trap ── */\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n onClose();\n return;\n }\n\n // Focus trap\n if (e.key === \"Tab\" && panelRef.current) {\n const focusable = panelRef.current.querySelectorAll<HTMLElement>(\n 'a[href], button:not([disabled]), textarea, input:not([disabled]), select, [tabindex]:not([tabindex=\"-1\"])',\n );\n if (focusable.length === 0) return;\n\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n };\n\n if (!open) return null;\n\n return createPortal(\n <div\n style={{ position: \"fixed\", inset: 0, zIndex: 50, overflowY: \"auto\" }}\n aria-modal=\"true\"\n role=\"dialog\"\n aria-labelledby={title ? \"dialog-title\" : undefined}\n onKeyDown={handleKeyDown}\n >\n {/* Backdrop */}\n <div\n style={{ position: \"fixed\", inset: 0 }}\n className=\"bg-black/50\"\n aria-hidden\n onClick={onClose}\n />\n\n {/* Centering wrapper */}\n <div\n style={{\n display: \"flex\",\n minHeight: \"100%\",\n width: \"100%\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: \"var(--spacing-md, 16px)\",\n boxSizing: \"border-box\",\n }}\n >\n {/* Panel */}\n <div\n ref={panelRef}\n tabIndex={-1}\n className={`relative z-10 flex flex-col w-full ${sizeClasses[size]} rounded-lg border border-border bg-bg-primary shadow-xl outline-none animate-dialog-in ${className}`}\n >\n {/* Header */}\n {title && (\n <div className=\"flex items-center justify-between px-lg py-md border-b border-border\">\n <h2\n id=\"dialog-title\"\n className=\"text-h3 font-semibold text-text-primary\"\n >\n {title}\n </h2>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"shrink-0 p-1 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label=\"Close dialog\"\n >\n <X size={18} />\n </button>\n </div>\n )}\n\n {/* Body */}\n <div className=\"flex-1 overflow-y-auto px-lg py-md text-body text-text-primary\">\n {children}\n </div>\n\n {/* Footer */}\n {footer && (\n <div className=\"flex items-center justify-end gap-sm px-lg py-md border-t border-border\">\n {footer}\n </div>\n )}\n </div>\n </div>\n </div>,\n document.body,\n );\n}\n","import { type ReactNode } from \"react\";\nimport { ExclamationTriangleFill } from \"react-bootstrap-icons\";\nimport { Spinner } from \"../ui/Spinner\";\nimport { Dialog, type DialogSize } from \"./Dialog\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface ConfirmDialogProps {\n /** Whether the dialog is open. */\n open: boolean;\n /** Callback to close without confirming. */\n onCancel: () => void;\n /** Callback when the user confirms the action. */\n onConfirm: () => void;\n /** Dialog title. */\n title: string;\n /** Body message / description. */\n children: ReactNode;\n /** Text for the confirm button. Default: \"Confirm\". */\n confirmLabel?: string;\n /** Text for the cancel button. Default: \"Cancel\". */\n cancelLabel?: string;\n /** Danger variant — red confirm button with a warning icon. */\n danger?: boolean;\n /** Show a spinner on the confirm button and disable both buttons. */\n submitting?: boolean;\n /** Width preset. Default: \"sm\". */\n size?: DialogSize;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function ConfirmDialog({\n open,\n onCancel,\n onConfirm,\n title,\n children,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n danger = false,\n submitting = false,\n size = \"sm\",\n}: ConfirmDialogProps) {\n return (\n <Dialog open={open} onClose={onCancel} title={title} size={size}>\n {/* Body */}\n <div className=\"flex gap-3\">\n {danger && (\n <ExclamationTriangleFill\n size={22}\n className=\"shrink-0 mt-0.5 text-error\"\n aria-hidden\n />\n )}\n <div className=\"text-body text-text-secondary\">{children}</div>\n </div>\n\n {/* Actions — placed as children, Dialog body handles wrapping */}\n <div className=\"flex items-center justify-end gap-sm mt-lg\">\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={submitting}\n className={[\n \"px-4 py-2 rounded-md text-small font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border transition-colors cursor-pointer\",\n submitting && \"opacity-50 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {cancelLabel}\n </button>\n <button\n type=\"button\"\n onClick={onConfirm}\n disabled={submitting}\n className={[\n \"inline-flex items-center justify-center gap-2 px-4 py-2 rounded-md text-small font-medium text-white transition-colors cursor-pointer\",\n danger\n ? \"bg-error hover:bg-error/80\"\n : \"bg-primary hover:bg-primary-hover\",\n submitting && \"opacity-80 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {submitting && <Spinner size=\"sm\" />}\n {confirmLabel}\n </button>\n </div>\n </Dialog>\n );\n}\n","import { createContext, useContext } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastData {\n id: string;\n variant: ToastVariant;\n title?: string;\n message: string;\n /** Auto-dismiss duration in ms. 0 = persistent. Default: 5000 */\n duration?: number;\n}\n\nexport type AddToastInput = Omit<ToastData, \"id\">;\n\nexport interface ToastContextValue {\n addToast: (toast: AddToastInput) => string;\n removeToast: (id: string) => void;\n}\n\n/* ── Context ──────────────────────────────────────────────── */\n\nexport const ToastContext = createContext<ToastContextValue | null>(null);\n\nexport function useToast(): ToastContextValue {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used within a <ToastProvider>\");\n return ctx;\n}\n","import { useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n CheckCircleFill,\n ExclamationTriangleFill,\n InfoCircleFill,\n X,\n XCircleFill,\n} from \"react-bootstrap-icons\";\nimport {\n ToastContext,\n type AddToastInput,\n type ToastData,\n type ToastVariant,\n} from \"./toastContext\";\n\n/* ── Variant config ───────────────────────────────────────── */\n\nconst variantConfig: Record<\n ToastVariant,\n { bg: string; accent: string; text: string; icon: typeof CheckCircleFill }\n> = {\n success: {\n bg: \"bg-bg-primary\",\n accent: \"bg-success\",\n text: \"text-success\",\n icon: CheckCircleFill,\n },\n warning: {\n bg: \"bg-bg-primary\",\n accent: \"bg-warning\",\n text: \"text-warning\",\n icon: ExclamationTriangleFill,\n },\n error: {\n bg: \"bg-bg-primary\",\n accent: \"bg-error\",\n text: \"text-error\",\n icon: XCircleFill,\n },\n info: {\n bg: \"bg-bg-primary\",\n accent: \"bg-info\",\n text: \"text-info\",\n icon: InfoCircleFill,\n },\n};\n\n/* ── Single Toast ─────────────────────────────────────────── */\n\ninterface ToastItemProps {\n toast: ToastData;\n onRemove: (id: string) => void;\n}\n\nfunction ToastItem({ toast, onRemove }: ToastItemProps) {\n const { variant, title, message, duration = 5000 } = toast;\n const { bg, accent, text, icon: Icon } = variantConfig[variant];\n const [progress, setProgress] = useState(100);\n const startRef = useRef<number>(0);\n const rafRef = useRef<number>(0);\n\n useEffect(() => {\n if (duration <= 0) return;\n\n startRef.current = performance.now();\n\n const tick = (now: number) => {\n const elapsed = now - startRef.current;\n const remaining = Math.max(0, 100 - (elapsed / duration) * 100);\n setProgress(remaining);\n\n if (remaining <= 0) {\n onRemove(toast.id);\n return;\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(rafRef.current);\n }, [duration, toast.id, onRemove]);\n\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className={`relative overflow-hidden rounded-lg border border-border shadow-lg ${bg} w-80 animate-slide-in`}\n >\n {/* Content row */}\n <div className=\"flex gap-3 p-md\">\n <Icon size={18} className={`shrink-0 mt-0.5 ${text}`} aria-hidden />\n <div className=\"flex-1 min-w-0\">\n {title && <p className=\"font-semibold text-small text-text-primary\">{title}</p>}\n <p className=\"text-small text-text-secondary\">{message}</p>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemove(toast.id)}\n className=\"shrink-0 p-1 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label=\"Dismiss notification\"\n >\n <X size={14} />\n </button>\n </div>\n\n {/* Progress bar */}\n {duration > 0 && (\n <div className=\"h-0.5 w-full bg-bg-tertiary\">\n <div\n className={`h-full ${accent} transition-none`}\n style={{ width: `${progress}%` }}\n />\n </div>\n )}\n </div>\n );\n}\n\n/* ── Provider ─────────────────────────────────────────────── */\n\nlet nextId = 0;\n\nexport function ToastProvider({ children }: { children: ReactNode }) {\n const [toasts, setToasts] = useState<ToastData[]>([]);\n\n const addToast = useCallback((input: AddToastInput): string => {\n const id = `toast-${++nextId}`;\n setToasts((prev) => [...prev, { ...input, id }]);\n return id;\n }, []);\n\n const removeToast = useCallback((id: string) => {\n setToasts((prev) => prev.filter((t) => t.id !== id));\n }, []);\n\n return (\n <ToastContext value={{ addToast, removeToast }}>\n {children}\n\n {/* Toast stack — fixed bottom-right */}\n <div\n aria-label=\"Notifications\"\n className=\"fixed bottom-lg right-lg z-50 flex flex-col-reverse gap-sm pointer-events-none\"\n >\n {toasts.map((t) => (\n <div key={t.id} className=\"pointer-events-auto\">\n <ToastItem toast={t} onRemove={removeToast} />\n </div>\n ))}\n </div>\n </ToastContext>\n );\n}\n","import { createContext, useContext, useMemo, useState, type ReactNode } from \"react\";\nimport type { AppRole } from \"../routes/types\";\n\n/* ── User type ────────────────────────────────────────────── */\n\nexport interface AuthUser {\n name: string;\n email: string;\n role: AppRole;\n avatarUrl?: string;\n}\n\n/* ── Context ──────────────────────────────────────────────── */\n\ninterface AuthContextValue {\n user: AuthUser;\n setRole: (role: AppRole) => void;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n/* ── Provider ─────────────────────────────────────────────── */\n\nconst DEFAULT_USER: AuthUser = {\n name: \"Alice Martin\",\n email: \"alice.martin@epfl.ch\",\n role: \"admin\",\n};\n\nexport interface AuthProviderProps {\n /** Override initial user (useful for stories). */\n initialUser?: AuthUser;\n children: ReactNode;\n}\n\n/**\n * Mock auth provider — supplies a user with a switchable role.\n *\n * In production this would be replaced by a real auth layer.\n */\nexport function AuthProvider({ initialUser = DEFAULT_USER, children }: AuthProviderProps) {\n const [user, setUser] = useState<AuthUser>(initialUser);\n\n const setRole = (role: AppRole) => setUser((u) => ({ ...u, role }));\n\n const value = useMemo(() => ({ user, setRole }), [user]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n/* ── Hook ─────────────────────────────────────────────────── */\n\n/**\n * Access the current authenticated user and role setter.\n *\n * ```tsx\n * const { user, setRole } = useAuth();\n * ```\n */\nexport function useAuth(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth must be used within an AuthProvider\");\n return ctx;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"poesis.js","names":[],"sources":["../src/components/ui/Avatar.tsx","../src/components/ui/Badge.tsx","../src/components/ui/Spinner.tsx","../src/components/ui/Button.tsx","../src/components/ui/IconButton.tsx","../src/components/ui/Pagination.tsx","../src/components/ui/FileUpload.tsx","../src/components/ui/ProgressBar.tsx","../src/components/ui/Checkbox.tsx","../src/components/ui/Input.tsx","../src/components/ui/RadioGroup.tsx","../src/theme/reactSelectStyles.ts","../src/components/ui/Select.tsx","../src/components/ui/Switch.tsx","../src/components/ui/Textarea.tsx","../src/components/ui/DropdownMenu.tsx","../src/components/ui/Popover.tsx","../src/components/ui/Tabs.tsx","../src/components/ui/Tooltip.tsx","../src/hooks/useLanguage.ts","../src/components/ui/LanguageSwitcher.tsx","../src/hooks/useTheme.ts","../src/components/ui/ThemeToggle.tsx","../src/components/layout/TopNav.tsx","../src/components/layout/SideNav.tsx","../src/components/layout/BurgerDrawer.tsx","../src/components/layout/Footer.tsx","../src/components/layout/UserMenu.tsx","../src/components/layout/PageShell.tsx","../src/components/data-display/Card.tsx","../src/components/data-display/DescriptionList.tsx","../src/components/data-display/EmptyState.tsx","../src/components/data-display/Table.tsx","../src/components/feedback/Alert.tsx","../src/components/feedback/Dialog.tsx","../src/components/feedback/ConfirmDialog.tsx","../src/components/feedback/toastContext.ts","../src/components/feedback/Toast.tsx","../src/hooks/useAuth.tsx"],"sourcesContent":["import { type ComponentPropsWithoutRef, useState } from \"react\";\n\n/* ── Size token maps ────────────────────────────────────── */\n\nconst sizeClasses = {\n sm: \"size-8 text-caption\",\n md: \"size-10 text-small\",\n lg: \"size-14 text-body\",\n} as const;\n\nexport type AvatarSize = keyof typeof sizeClasses;\n\nexport interface AvatarProps extends ComponentPropsWithoutRef<\"span\"> {\n /** Image URL. Falls back to initials when absent or broken. */\n src?: string | null;\n /** Full name used to derive initials. */\n name: string;\n /** Size preset. @default \"md\" */\n size?: AvatarSize;\n /** Alt text for the image. Defaults to `name`. */\n alt?: string;\n}\n\n/** Extract up to two uppercase initials from a name. */\nfunction getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/);\n if (parts.length === 0) return \"?\";\n if (parts.length === 1) return parts[0][0].toUpperCase();\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n}\n\n/**\n * User avatar with an image that falls back to initials.\n *\n * Always renders a circular element. Sizes: `sm` (32 px), `md` (40 px), `lg` (56 px).\n */\nexport function Avatar({ src, name, size = \"md\", alt, className = \"\", ...rest }: AvatarProps) {\n const [imgFailed, setImgFailed] = useState(false);\n\n const showImage = !!src && !imgFailed;\n\n return (\n <span\n role=\"img\"\n aria-label={alt ?? name}\n className={[\n \"inline-flex items-center justify-center rounded-full overflow-hidden\",\n \"bg-primary text-white font-semibold select-none shrink-0\",\n sizeClasses[size],\n className,\n ].join(\" \")}\n {...rest}\n >\n {showImage ? (\n <img\n src={src}\n alt={alt ?? name}\n className=\"size-full object-cover\"\n onError={() => setImgFailed(true)}\n />\n ) : (\n <span aria-hidden=\"true\">{getInitials(name)}</span>\n )}\n </span>\n );\n}\n","import type { ComponentPropsWithoutRef } from \"react\";\n\n/* ── Color token maps ───────────────────────────────────── */\n\nconst colorClasses = {\n default: \"bg-bg-tertiary text-text-primary\",\n success: \"bg-success-bg text-success\",\n warning: \"bg-warning-bg text-warning\",\n error: \"bg-error-bg text-error\",\n info: \"bg-info-bg text-info\",\n} as const;\n\nconst dotColorClasses = {\n default: \"bg-text-muted\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n error: \"bg-error\",\n info: \"bg-info\",\n} as const;\n\nexport type BadgeColor = keyof typeof colorClasses;\n\n/* ── Dot Badge ──────────────────────────────────────────── */\n\nexport interface DotBadgeProps extends ComponentPropsWithoutRef<\"span\"> {\n /** Semantic color. @default \"default\" */\n color?: BadgeColor;\n /** Render as a small status dot instead of a text pill. */\n dot: true;\n children?: never;\n}\n\n/* ── Text Badge ─────────────────────────────────────────── */\n\nexport interface TextBadgeProps extends ComponentPropsWithoutRef<\"span\"> {\n /** Semantic color. @default \"default\" */\n color?: BadgeColor;\n dot?: false;\n children: React.ReactNode;\n}\n\nexport type BadgeProps = DotBadgeProps | TextBadgeProps;\n\n/**\n * Status indicator — either a small colored dot or a text pill.\n *\n * ```tsx\n * <Badge color=\"success\" dot /> // status dot\n * <Badge color=\"error\">Failed</Badge> // text pill\n * ```\n */\nexport function Badge({ color = \"default\", dot, className = \"\", children, ...rest }: BadgeProps) {\n if (dot) {\n return (\n <span\n role=\"status\"\n className={`inline-block size-2.5 rounded-full ${dotColorClasses[color]} ${className}`}\n {...rest}\n />\n );\n }\n\n return (\n <span\n className={[\n \"inline-flex items-center rounded-sm px-2 py-0.5\",\n \"text-caption font-medium leading-none\",\n colorClasses[color],\n className,\n ].join(\" \")}\n {...rest}\n >\n {children}\n </span>\n );\n}\n","import type { ComponentPropsWithoutRef } from \"react\";\n\nconst sizeMap = {\n sm: \"size-4\",\n md: \"size-6\",\n lg: \"size-8\",\n} as const;\n\nexport type SpinnerSize = keyof typeof sizeMap;\n\nexport interface SpinnerProps extends ComponentPropsWithoutRef<\"svg\"> {\n /** Visual size of the spinner. @default \"md\" */\n size?: SpinnerSize;\n}\n\n/**\n * Animated loading indicator.\n *\n * Inherits `currentColor` so it adapts to its parent's text color.\n */\nexport function Spinner({ size = \"md\", className = \"\", ...rest }: SpinnerProps) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className={`animate-spin ${sizeMap[size]} ${className}`}\n role=\"status\"\n aria-label=\"Loading\"\n {...rest}\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"3\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8v3a5 5 0 00-5 5H4z\"\n />\n </svg>\n );\n}\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\nimport { Spinner } from \"./Spinner\";\n\n/* ── Variant × size token maps ─────────────────────────── */\n\nconst variantClasses = {\n primary: \"bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary\",\n secondary:\n \"border border-border text-text-primary bg-bg-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n ghost: \"text-text-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n danger: \"bg-error text-white hover:opacity-90 focus-visible:ring-error\",\n} as const;\n\nconst sizeClasses = {\n sm: \"text-small px-3 py-1 gap-1.5\",\n md: \"text-body px-4 py-2 gap-2\",\n lg: \"text-body px-5 py-2.5 gap-2\",\n} as const;\n\nconst spinnerSizeMap = {\n sm: \"sm\",\n md: \"sm\",\n lg: \"md\",\n} as const;\n\nexport type ButtonVariant = keyof typeof variantClasses;\nexport type ButtonSize = keyof typeof sizeClasses;\n\nexport interface ButtonProps extends ComponentPropsWithoutRef<\"button\"> {\n /** Visual style variant. @default \"primary\" */\n variant?: ButtonVariant;\n /** Size preset. @default \"md\" */\n size?: ButtonSize;\n /** Show a spinner and disable interaction. */\n loading?: boolean;\n /** Optional icon before children. */\n iconLeft?: ReactNode;\n /** Optional icon after children. */\n iconRight?: ReactNode;\n}\n\n/**\n * Primary action element.\n *\n * Supports four variants (`primary`, `secondary`, `ghost`, `danger`),\n * three sizes (`sm`, `md`, `lg`), and a `loading` state.\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = \"primary\",\n size = \"md\",\n loading = false,\n disabled,\n iconLeft,\n iconRight,\n className = \"\",\n children,\n ...rest\n },\n ref,\n) {\n const isDisabled = disabled || loading;\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={isDisabled}\n aria-busy={loading || undefined}\n className={[\n // base\n \"inline-flex items-center justify-center font-medium rounded-md\",\n \"transition-colors duration-150 cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n // variant + size\n variantClasses[variant],\n sizeClasses[size],\n // disabled / loading\n isDisabled && \"opacity-50 pointer-events-none\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {loading ? <Spinner size={spinnerSizeMap[size]} aria-hidden=\"true\" /> : iconLeft}\n {children}\n {!loading && iconRight}\n </button>\n );\n});\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\nimport { Spinner } from \"./Spinner\";\n\n/* ── Variant token maps ─────────────────────────────────── */\n\nconst variantClasses = {\n primary: \"bg-primary text-white hover:bg-primary-hover focus-visible:ring-primary\",\n secondary:\n \"border border-border text-text-primary bg-bg-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n ghost: \"text-text-primary hover:bg-bg-tertiary focus-visible:ring-primary\",\n danger: \"bg-error text-white hover:opacity-90 focus-visible:ring-error\",\n} as const;\n\nconst sizeClasses = {\n sm: \"size-7 text-small\",\n md: \"size-9 text-body\",\n lg: \"size-11 text-body\",\n} as const;\n\nexport type IconButtonVariant = keyof typeof variantClasses;\nexport type IconButtonSize = keyof typeof sizeClasses;\n\nexport interface IconButtonProps extends ComponentPropsWithoutRef<\"button\"> {\n /** Visual style variant. @default \"ghost\" */\n variant?: IconButtonVariant;\n /** Size preset. @default \"md\" */\n size?: IconButtonSize;\n /** Show a spinner and disable interaction. */\n loading?: boolean;\n /** The icon element to render. */\n icon: ReactNode;\n /** Accessible label (required since there's no visible text). */\n \"aria-label\": string;\n}\n\n/**\n * Square icon-only button for toolbar actions.\n *\n * `aria-label` is required to ensure accessibility.\n */\nexport const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(\n { variant = \"ghost\", size = \"md\", loading = false, disabled, icon, className = \"\", ...rest },\n ref,\n) {\n const isDisabled = disabled || loading;\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={isDisabled}\n aria-busy={loading || undefined}\n className={[\n // base\n \"inline-flex items-center justify-center rounded-md\",\n \"transition-colors duration-150 cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2\",\n // variant + size\n variantClasses[variant],\n sizeClasses[size],\n // disabled / loading\n isDisabled && \"opacity-50 pointer-events-none\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {loading ? <Spinner size=\"sm\" aria-hidden=\"true\" /> : icon}\n </button>\n );\n});\n","import { type ComponentPropsWithoutRef, forwardRef, useCallback, useState } from \"react\";\nimport {\n ChevronBarLeft,\n ChevronBarRight,\n ChevronLeft,\n ChevronRight,\n} from \"react-bootstrap-icons\";\nimport { useTranslation } from \"react-i18next\";\n\n/* ── Size token map ───────────────────────────────────────── */\n\nconst sizeClasses = {\n sm: \"h-7 min-w-7 px-1.5 text-small\",\n md: \"h-9 min-w-9 px-2 text-body\",\n lg: \"h-11 min-w-11 px-3 text-body\",\n} as const;\n\nconst iconSizeMap = { sm: 14, md: 16, lg: 18 } as const;\n\nexport type PaginationSize = keyof typeof sizeClasses;\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface PaginationProps extends Omit<ComponentPropsWithoutRef<\"nav\">, \"onChange\" | \"children\"> {\n /** Total number of pages (≥ 1). */\n totalPages: number;\n /** Current page (controlled). 1-based. */\n page?: number;\n /** Default page (uncontrolled). 1-based. @default 1 */\n defaultPage?: number;\n /** Callback when page changes. */\n onChange?: (page: number) => void;\n /** Number of pages to show on each side of current page. @default 1 */\n siblingCount?: number;\n /** Number of pages always shown at start and end. @default 1 */\n boundaryCount?: number;\n /** Show first/last page jump buttons. @default false */\n showFirstLast?: boolean;\n /** Size preset. @default \"md\" */\n size?: PaginationSize;\n /** Disable all interaction. */\n disabled?: boolean;\n}\n\n/* ── Range computation ────────────────────────────────────── */\n\ntype PageItem = number | \"ellipsis-start\" | \"ellipsis-end\";\n\nfunction computeRange(\n currentPage: number,\n totalPages: number,\n siblingCount: number,\n boundaryCount: number,\n): PageItem[] {\n // Total page buttons that can be displayed (boundaries + siblings + current + 2 ellipsis slots)\n const totalSlots = boundaryCount * 2 + siblingCount * 2 + 3; // +3 = current + 2 ellipsis positions\n\n // If everything fits, show all pages\n if (totalPages <= totalSlots) {\n return Array.from({ length: totalPages }, (_, i) => i + 1);\n }\n\n const startBoundary = Array.from({ length: boundaryCount }, (_, i) => i + 1);\n const endBoundary = Array.from({ length: boundaryCount }, (_, i) => totalPages - boundaryCount + 1 + i);\n\n const siblingStart = Math.max(boundaryCount + 2, currentPage - siblingCount);\n const siblingEnd = Math.min(totalPages - boundaryCount - 1, currentPage + siblingCount);\n\n const showStartEllipsis = siblingStart > boundaryCount + 2;\n const showEndEllipsis = siblingEnd < totalPages - boundaryCount - 1;\n\n const items: PageItem[] = [];\n\n // Start boundary\n items.push(...startBoundary);\n\n // Start ellipsis or bridging page\n if (showStartEllipsis) {\n items.push(\"ellipsis-start\");\n } else {\n // Fill pages between boundary and sibling start\n for (let i = boundaryCount + 1; i < siblingStart; i++) {\n items.push(i);\n }\n }\n\n // Sibling range (includes current page)\n for (let i = siblingStart; i <= siblingEnd; i++) {\n items.push(i);\n }\n\n // End ellipsis or bridging page\n if (showEndEllipsis) {\n items.push(\"ellipsis-end\");\n } else {\n for (let i = siblingEnd + 1; i <= totalPages - boundaryCount; i++) {\n items.push(i);\n }\n }\n\n // End boundary\n items.push(...endBoundary);\n\n return items;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Page navigation with ellipsis truncation, prev/next buttons,\n * and optional first/last jump buttons.\n *\n * Supports controlled (`page`/`onChange`) and uncontrolled (`defaultPage`) usage.\n */\nexport const Pagination = forwardRef<HTMLElement, PaginationProps>(function Pagination(\n {\n totalPages,\n page: controlledPage,\n defaultPage = 1,\n onChange,\n siblingCount = 1,\n boundaryCount = 1,\n showFirstLast = false,\n size = \"md\",\n disabled = false,\n className = \"\",\n ...rest\n },\n ref,\n) {\n const { t } = useTranslation();\n\n /* Controlled / uncontrolled */\n const isControlled = controlledPage !== undefined;\n const [internalPage, setInternalPage] = useState(defaultPage);\n const currentPage = Math.min(Math.max(isControlled ? controlledPage : internalPage, 1), totalPages);\n\n const setPage = useCallback(\n (p: number) => {\n const clamped = Math.min(Math.max(p, 1), totalPages);\n if (!isControlled) setInternalPage(clamped);\n onChange?.(clamped);\n },\n [isControlled, onChange, totalPages],\n );\n\n const items = computeRange(currentPage, totalPages, siblingCount, boundaryCount);\n const iconSize = iconSizeMap[size];\n\n const isFirst = currentPage <= 1;\n const isLast = currentPage >= totalPages;\n\n /* Shared button base classes */\n const btnBase = [\n \"inline-flex items-center justify-center rounded-md font-medium\",\n \"transition-colors duration-150 cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-epfl-canard\",\n sizeClasses[size],\n ].join(\" \");\n\n const pageBtn = (active: boolean) =>\n [\n btnBase,\n active\n ? \"bg-epfl-canard text-white\"\n : \"text-text-primary hover:bg-bg-tertiary\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n const navBtn = (isDisabledEdge: boolean) =>\n [\n btnBase,\n \"text-text-secondary hover:bg-bg-tertiary\",\n (disabled || isDisabledEdge) && \"opacity-40 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n if (totalPages <= 0) return null;\n\n return (\n <nav\n ref={ref}\n aria-label={t(\"common.pagination\")}\n className={[\"inline-flex items-center gap-1\", className].filter(Boolean).join(\" \")}\n {...rest}\n >\n {/* First */}\n {showFirstLast && (\n <button\n type=\"button\"\n aria-label={t(\"common.first\")}\n disabled={disabled || isFirst}\n className={navBtn(isFirst)}\n onClick={() => setPage(1)}\n >\n <ChevronBarLeft size={iconSize} aria-hidden />\n </button>\n )}\n\n {/* Previous */}\n <button\n type=\"button\"\n aria-label={t(\"common.previous\")}\n disabled={disabled || isFirst}\n className={navBtn(isFirst)}\n onClick={() => setPage(currentPage - 1)}\n >\n <ChevronLeft size={iconSize} aria-hidden />\n </button>\n\n {/* Page items */}\n {items.map((item) => {\n if (typeof item === \"string\") {\n return (\n <span\n key={item}\n aria-hidden\n className={[\n \"inline-flex items-center justify-center select-none text-text-secondary\",\n sizeClasses[size],\n ].join(\" \")}\n >\n …\n </span>\n );\n }\n\n const isActive = item === currentPage;\n return (\n <button\n key={item}\n type=\"button\"\n aria-current={isActive ? \"page\" : undefined}\n aria-label={t(\"common.goToPage\", { page: item })}\n disabled={disabled}\n className={[\n pageBtn(isActive),\n disabled && \"opacity-40 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n onClick={() => setPage(item)}\n >\n {item}\n </button>\n );\n })}\n\n {/* Next */}\n <button\n type=\"button\"\n aria-label={t(\"common.next\")}\n disabled={disabled || isLast}\n className={navBtn(isLast)}\n onClick={() => setPage(currentPage + 1)}\n >\n <ChevronRight size={iconSize} aria-hidden />\n </button>\n\n {/* Last */}\n {showFirstLast && (\n <button\n type=\"button\"\n aria-label={t(\"common.last\")}\n disabled={disabled || isLast}\n className={navBtn(isLast)}\n onClick={() => setPage(totalPages)}\n >\n <ChevronBarRight size={iconSize} aria-hidden />\n </button>\n )}\n </nav>\n );\n});\n","import {\n type ChangeEvent,\n type DragEvent,\n type ReactNode,\n useCallback,\n useRef,\n useState,\n} from \"react\";\nimport { CloudArrowUp, X } from \"react-bootstrap-icons\";\n\nexport interface FileUploadProps {\n /** Accepted file types (e.g. `\"image/*,.pdf\"`). Maps to the `<input accept>` attribute. */\n accept?: string;\n /** Allow selecting multiple files. */\n multiple?: boolean;\n /** Maximum file size in bytes. Files exceeding this are rejected silently via `onReject`. */\n maxSize?: number;\n /** Callback fired with accepted files after drop or selection. */\n onFilesSelected?: (files: File[]) => void;\n /** Callback fired with rejected files (wrong type or over `maxSize`). */\n onReject?: (files: File[]) => void;\n /** Disable the dropzone. */\n disabled?: boolean;\n /** Label text shown inside the dropzone. */\n label?: string;\n /** Helper text shown below the label (e.g. \"PDF, PNG, up to 10 MB\"). */\n helperText?: string;\n /** Error message. When set, the border turns red. */\n error?: string;\n /** Custom illustration / icon rendered above the label. */\n illustration?: ReactNode;\n /** Additional class names on the outer wrapper. */\n className?: string;\n}\n\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nexport function FileUpload({\n accept,\n multiple = false,\n maxSize,\n onFilesSelected,\n onReject,\n disabled = false,\n label = \"Drop files here or click to browse\",\n helperText,\n error,\n illustration,\n className = \"\",\n}: FileUploadProps) {\n const [isDragOver, setIsDragOver] = useState(false);\n const [selectedFiles, setSelectedFiles] = useState<File[]>([]);\n const inputRef = useRef<HTMLInputElement>(null);\n\n const hasError = Boolean(error);\n\n const filterFiles = useCallback(\n (fileList: FileList) => {\n const all = Array.from(fileList);\n const accepted: File[] = [];\n const rejected: File[] = [];\n\n for (const file of all) {\n if (maxSize && file.size > maxSize) {\n rejected.push(file);\n } else {\n accepted.push(file);\n }\n }\n\n if (accepted.length > 0) {\n const next = multiple\n ? [...selectedFiles, ...accepted]\n : accepted.slice(0, 1);\n setSelectedFiles(next);\n onFilesSelected?.(next);\n }\n\n if (rejected.length > 0) {\n onReject?.(rejected);\n }\n },\n [maxSize, multiple, onFilesSelected, onReject, selectedFiles],\n );\n\n const handleDragOver = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n if (!disabled) setIsDragOver(true);\n },\n [disabled],\n );\n\n const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n setIsDragOver(false);\n }, []);\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n setIsDragOver(false);\n if (disabled || !e.dataTransfer.files.length) return;\n filterFiles(e.dataTransfer.files);\n },\n [disabled, filterFiles],\n );\n\n const handleInputChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (e.target.files?.length) {\n filterFiles(e.target.files);\n }\n },\n [filterFiles],\n );\n\n const handleClick = useCallback(() => {\n if (!disabled) inputRef.current?.click();\n }, [disabled]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent) => {\n if (!disabled && (e.key === \"Enter\" || e.key === \" \")) {\n e.preventDefault();\n inputRef.current?.click();\n }\n },\n [disabled],\n );\n\n const removeFile = useCallback(\n (index: number) => {\n const next = selectedFiles.filter((_, i) => i !== index);\n setSelectedFiles(next);\n onFilesSelected?.(next);\n },\n [selectedFiles, onFilesSelected],\n );\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {/* Dropzone area */}\n <div\n role=\"button\"\n tabIndex={disabled ? -1 : 0}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n className={[\n \"flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-lg\",\n \"transition-colors cursor-pointer select-none\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\",\n disabled\n ? \"opacity-50 pointer-events-none bg-bg-secondary\"\n : isDragOver\n ? \"border-primary bg-primary/5\"\n : hasError\n ? \"border-error hover:border-error bg-bg-primary\"\n : \"border-border hover:border-primary bg-bg-primary\",\n ]\n .filter(Boolean)\n .join(\" \")}\n aria-disabled={disabled}\n >\n {illustration ?? (\n <CloudArrowUp\n size={32}\n className={\n isDragOver\n ? \"text-primary\"\n : \"text-text-muted\"\n }\n aria-hidden\n />\n )}\n <span className=\"text-body font-medium text-text-primary text-center\">\n {label}\n </span>\n {helperText && (\n <span className=\"text-caption text-text-secondary text-center\">\n {helperText}\n </span>\n )}\n </div>\n\n {/* Hidden file input */}\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n onChange={handleInputChange}\n className=\"hidden\"\n tabIndex={-1}\n aria-hidden\n />\n\n {/* Error message */}\n {error && (\n <p className=\"text-caption text-error\">{error}</p>\n )}\n\n {/* Selected files list */}\n {selectedFiles.length > 0 && (\n <ul className=\"flex flex-col gap-1 mt-1\">\n {selectedFiles.map((file, i) => (\n <li\n key={`${file.name}-${file.size}-${i}`}\n className=\"flex items-center gap-2 rounded-md bg-bg-secondary px-3 py-1.5 text-small text-text-primary\"\n >\n <span className=\"truncate flex-1\">{file.name}</span>\n <span className=\"shrink-0 text-text-secondary\">\n {formatFileSize(file.size)}\n </span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n removeFile(i);\n }}\n className=\"shrink-0 p-0.5 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label={`Remove ${file.name}`}\n >\n <X size={14} />\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n","import { type ComponentPropsWithoutRef, forwardRef } from \"react\";\n\nconst sizeClasses = {\n sm: \"h-1.5\",\n md: \"h-2.5\",\n lg: \"h-4\",\n} as const;\n\nconst variantClasses = {\n primary: \"bg-primary\",\n success: \"bg-success\",\n warning: \"bg-warning\",\n error: \"bg-error\",\n info: \"bg-info\",\n} as const;\n\nexport type ProgressBarSize = keyof typeof sizeClasses;\nexport type ProgressBarVariant = keyof typeof variantClasses;\n\nexport interface ProgressBarProps\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\"> {\n /** Current progress value (0–100). Clamped internally. */\n value: number;\n /** Visual color variant. */\n variant?: ProgressBarVariant;\n /** Height of the bar. */\n size?: ProgressBarSize;\n /** Show the percentage label next to the bar. */\n showLabel?: boolean;\n /** Override the label text. Receives the clamped value. */\n formatLabel?: (value: number) => string;\n}\n\nexport const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(\n function ProgressBar(\n {\n value,\n variant = \"primary\",\n size = \"md\",\n showLabel = false,\n formatLabel,\n className = \"\",\n ...rest\n },\n ref,\n ) {\n const clamped = Math.round(Math.min(100, Math.max(0, value)));\n const label = formatLabel ? formatLabel(clamped) : `${clamped}%`;\n\n return (\n <div\n ref={ref}\n className={`flex items-center gap-2 ${className}`}\n {...rest}\n >\n <div\n className={[\n \"flex-1 overflow-hidden rounded-full bg-bg-tertiary\",\n sizeClasses[size],\n ].join(\" \")}\n role=\"progressbar\"\n aria-valuenow={clamped}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n <div\n className={[\n \"h-full rounded-full transition-all duration-300 ease-out\",\n variantClasses[variant],\n ].join(\" \")}\n style={{ width: `${clamped}%` }}\n />\n </div>\n {showLabel && (\n <span className=\"text-small font-medium text-text-secondary tabular-nums shrink-0\">\n {label}\n </span>\n )}\n </div>\n );\n },\n);\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from \"react\";\n\nexport interface CheckboxProps extends Omit<ComponentPropsWithoutRef<\"input\">, \"type\"> {\n /** Label displayed next to the checkbox. */\n label?: ReactNode;\n /** Put the checkbox in an indeterminate state. */\n indeterminate?: boolean;\n /** Helper text below the label. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n}\n\n/**\n * Custom-styled checkbox with indeterminate support.\n */\nexport const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(\n {\n label,\n indeterminate = false,\n helperText,\n error,\n className = \"\",\n id: idProp,\n disabled,\n ...rest\n },\n forwardedRef,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n const hasError = Boolean(error);\n const internalRef = useRef<HTMLInputElement | null>(null);\n\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n internalRef.current = node;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(node);\n } else if (forwardedRef) {\n forwardedRef.current = node;\n }\n },\n [forwardedRef],\n );\n\n useEffect(() => {\n if (internalRef.current) {\n internalRef.current.indeterminate = indeterminate;\n }\n }, [indeterminate]);\n\n return (\n <div className={`flex flex-col gap-1 ${className}`}>\n <div className=\"flex items-start gap-2\">\n <input\n ref={setRef}\n id={id}\n type=\"checkbox\"\n disabled={disabled}\n aria-invalid={hasError || undefined}\n aria-describedby={helperText || error ? helperId : undefined}\n className={[\n \"mt-0.5 size-4 shrink-0 cursor-pointer rounded-sm border appearance-none\",\n \"bg-bg-primary transition-colors\",\n \"checked:bg-primary checked:border-primary\",\n \"indeterminate:bg-primary indeterminate:border-primary\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n hasError ? \"border-error\" : \"border-border hover:border-border-strong\",\n // Checkmark via background SVG\n \"checked:bg-[url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22white%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M12.207%204.793a1%201%200%20010%201.414l-5%205a1%201%200%2001-1.414%200l-2-2a1%201%200%20011.414-1.414L6.5%209.086l4.293-4.293a1%201%200%20011.414%200z%22%2F%3E%3C%2Fsvg%3E')] checked:bg-center checked:bg-no-repeat\",\n // Indeterminate dash\n \"indeterminate:bg-[url('data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2016%2016%22%20fill%3D%22white%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20x%3D%223%22%20y%3D%227%22%20width%3D%2210%22%20height%3D%222%22%20rx%3D%221%22%2F%3E%3C%2Fsvg%3E')] indeterminate:bg-center indeterminate:bg-no-repeat\",\n ].join(\" \")}\n {...rest}\n />\n\n {label && (\n <label\n htmlFor={id}\n className={`text-body select-none ${disabled ? \"text-text-muted cursor-not-allowed\" : \"text-text-primary cursor-pointer\"}`}\n >\n {label}\n </label>\n )}\n </div>\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption pl-6 ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n});\n","import { forwardRef, useId, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nexport interface InputProps extends Omit<ComponentPropsWithoutRef<\"input\">, \"size\"> {\n /** Visible label above the input. */\n label?: string;\n /** Helper text shown below the input. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Icon or element rendered inside the input on the left. */\n leftIcon?: ReactNode;\n /** Icon or element rendered inside the input on the right. */\n rightIcon?: ReactNode;\n /** Input size variant. */\n inputSize?: \"sm\" | \"md\" | \"lg\";\n}\n\nconst sizeClasses = {\n sm: \"h-8 text-small px-2.5\",\n md: \"h-10 text-body px-3\",\n lg: \"h-12 text-body px-3.5\",\n} as const;\n\nconst iconPaddingLeft = {\n sm: \"pl-8\",\n md: \"pl-10\",\n lg: \"pl-11\",\n} as const;\n\nconst iconPaddingRight = {\n sm: \"pr-8\",\n md: \"pr-10\",\n lg: \"pr-11\",\n} as const;\n\nconst iconSizeClasses = {\n sm: \"[&>svg]:size-4\",\n md: \"[&>svg]:size-5\",\n lg: \"[&>svg]:size-5\",\n} as const;\n\n/**\n * Text input with label, helper text, error state, and icon slots.\n * Supports text, email, password, and search types.\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n label,\n helperText,\n error,\n leftIcon,\n rightIcon,\n inputSize = \"md\",\n className = \"\",\n id: idProp,\n disabled,\n ...rest\n },\n ref,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n const hasError = Boolean(error);\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {label && (\n <label htmlFor={id} className=\"text-small font-medium text-text-primary\">\n {label}\n </label>\n )}\n\n <div className=\"relative\">\n {leftIcon && (\n <span\n className={`pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-text-secondary ${iconSizeClasses[inputSize]}`}\n aria-hidden=\"true\"\n >\n {leftIcon}\n </span>\n )}\n\n <input\n ref={ref}\n id={id}\n disabled={disabled}\n aria-invalid={hasError || undefined}\n aria-describedby={helperText || error ? helperId : undefined}\n className={[\n \"w-full rounded-md border bg-bg-primary text-text-primary placeholder:text-text-muted\",\n \"outline-none transition-colors\",\n \"focus:border-primary focus:ring-1 focus:ring-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-bg-secondary\",\n hasError\n ? \"border-error focus:border-error focus:ring-error\"\n : \"border-border hover:border-border-strong\",\n sizeClasses[inputSize],\n leftIcon ? iconPaddingLeft[inputSize] : \"\",\n rightIcon ? iconPaddingRight[inputSize] : \"\",\n ].join(\" \")}\n {...rest}\n />\n\n {rightIcon && (\n <span\n className={`pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-text-secondary ${iconSizeClasses[inputSize]}`}\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n </div>\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n});\n","import { createContext, useContext, useId, type ReactNode } from \"react\";\n\n/* ── Context ─────────────────────────────────────────────── */\n\ninterface RadioGroupContextValue {\n name: string;\n value?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n hasError?: boolean;\n}\n\nconst RadioGroupContext = createContext<RadioGroupContextValue | null>(null);\n\nfunction useRadioGroup() {\n const ctx = useContext(RadioGroupContext);\n if (!ctx) throw new Error(\"RadioGroup.Item must be used within RadioGroup\");\n return ctx;\n}\n\n/* ── RadioGroup ──────────────────────────────────────────── */\n\nexport interface RadioGroupProps {\n /** Group name for all radio inputs. */\n name?: string;\n /** Currently selected value (controlled). */\n value?: string;\n /** Default selected value (uncontrolled). */\n defaultValue?: string;\n /** Called when the selected value changes. */\n onChange?: (value: string) => void;\n /** Visible label for the group. */\n label?: string;\n /** Helper text below the group. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Layout direction. */\n orientation?: \"vertical\" | \"horizontal\";\n /** Disable all options. */\n disabled?: boolean;\n /** Radio items. */\n children: ReactNode;\n className?: string;\n}\n\n/**\n * Radio group with vertical or horizontal layout.\n * Use `RadioGroup.Item` for individual options.\n */\nexport function RadioGroup({\n name: nameProp,\n value,\n onChange,\n label,\n helperText,\n error,\n orientation = \"vertical\",\n disabled = false,\n children,\n className = \"\",\n}: RadioGroupProps) {\n const autoId = useId();\n const name = nameProp ?? autoId;\n const groupId = `${name}-group`;\n const helperId = `${groupId}-helper`;\n const hasError = Boolean(error);\n\n return (\n <RadioGroupContext.Provider value={{ name, value, onChange, disabled, hasError }}>\n <fieldset\n className={`flex flex-col gap-2 ${className}`}\n aria-describedby={helperText || error ? helperId : undefined}\n disabled={disabled}\n >\n {label && (\n <legend className=\"text-small font-medium text-text-primary mb-1\">\n {label}\n </legend>\n )}\n\n <div\n className={`flex gap-3 ${orientation === \"horizontal\" ? \"flex-row flex-wrap\" : \"flex-col\"}`}\n role=\"radiogroup\"\n >\n {children}\n </div>\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </fieldset>\n </RadioGroupContext.Provider>\n );\n}\n\n/* ── RadioGroup.Item ─────────────────────────────────────── */\n\nexport interface RadioItemProps {\n /** Value for this option. */\n value: string;\n /** Label displayed next to the radio. */\n label: ReactNode;\n /** Description shown below the label. */\n description?: string;\n /** Disable only this option. */\n disabled?: boolean;\n}\n\nfunction RadioItem({ value, label, description, disabled: itemDisabled }: RadioItemProps) {\n const {\n name,\n value: groupValue,\n onChange,\n disabled: groupDisabled,\n hasError,\n } = useRadioGroup();\n const id = useId();\n const disabled = groupDisabled || itemDisabled;\n const checked = groupValue !== undefined ? groupValue === value : undefined;\n\n return (\n <div className=\"flex items-start gap-2\">\n <input\n id={id}\n type=\"radio\"\n name={name}\n value={value}\n checked={checked}\n disabled={disabled}\n onChange={() => onChange?.(value)}\n className={[\n \"mt-0.5 size-4 shrink-0 cursor-pointer appearance-none rounded-full border\",\n \"bg-bg-primary transition-colors\",\n \"checked:border-primary checked:border-[5px]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n hasError ? \"border-error\" : \"border-border hover:border-border-strong\",\n ].join(\" \")}\n />\n\n <div className=\"flex flex-col\">\n <label\n htmlFor={id}\n className={`text-body select-none ${disabled ? \"text-text-muted cursor-not-allowed\" : \"text-text-primary cursor-pointer\"}`}\n >\n {label}\n </label>\n {description && (\n <span className=\"text-caption text-text-secondary\">{description}</span>\n )}\n </div>\n </div>\n );\n}\n\nRadioGroup.Item = RadioItem;\n","import type { ClassNamesConfig, ThemeConfig } from \"react-select\";\n\n/**\n * Custom react-select theme that maps to EPFL design tokens.\n * Uses CSS custom properties so it automatically adapts to light/dark mode.\n */\nexport const epflSelectTheme: ThemeConfig = (theme) => ({\n ...theme,\n borderRadius: 8,\n colors: {\n ...theme.colors,\n primary: \"var(--color-primary)\", // focused border, selected option\n primary75: \"var(--color-primary-hover)\",\n primary50: \"var(--color-bg-tertiary)\", // option hover\n primary25: \"var(--color-bg-secondary)\", // option light hover\n neutral0: \"var(--color-bg-primary)\", // control background\n neutral5: \"var(--color-bg-secondary)\",\n neutral10: \"var(--color-bg-tertiary)\", // multi-value tag bg\n neutral20: \"var(--color-border)\", // control border\n neutral30: \"var(--color-border-strong)\", // hover border\n neutral40: \"var(--color-text-muted)\", // placeholder if no search\n neutral50: \"var(--color-text-muted)\", // placeholder\n neutral60: \"var(--color-text-secondary)\",\n neutral70: \"var(--color-text-primary)\",\n neutral80: \"var(--color-text-primary)\", // selected text, input text\n neutral90: \"var(--color-text-primary)\",\n danger: \"var(--color-error)\",\n dangerLight: \"var(--color-error-bg)\",\n },\n});\n\n/**\n * Tailwind-based classNames for react-select components.\n * Applies EPFL design system styling via utility classes.\n */\nexport const epflSelectClassNames: ClassNamesConfig = {\n control: ({ isFocused }) =>\n `!shadow-sm !rounded-md !border-border ${isFocused ? \"!border-primary !ring-1 !ring-primary\" : \"\"}`,\n\n menu: () => \"!rounded-lg !shadow-lg !border !border-border\",\n option: ({ isFocused, isSelected }) =>\n `${isSelected ? \"!bg-primary !text-white\" : isFocused ? \"!bg-bg-tertiary\" : \"\"}`,\n\n multiValue: () => \"!rounded-sm !bg-bg-tertiary\",\n};\n","import { useId } from \"react\";\nimport ReactSelect, { type ClassNamesConfig, type GroupBase, type Props } from \"react-select\";\nimport { epflSelectClassNames, epflSelectTheme } from \"../../theme/reactSelectStyles\";\n\nconst selectSizeClasses = {\n sm: \"!min-h-8 !text-small\",\n md: \"!min-h-10 !text-body\",\n lg: \"!min-h-12 !text-body\",\n} as const;\n\nconst selectSizeHeight = {\n sm: 32,\n md: 40,\n lg: 48,\n} as const;\n\nexport interface SelectProps<\n Option = unknown,\n IsMulti extends boolean = false,\n Group extends GroupBase<Option> = GroupBase<Option>,\n> extends Props<Option, IsMulti, Group> {\n /** Visible label above the select. */\n label?: string;\n /** Helper text shown below the select. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Select size variant. */\n selectSize?: \"sm\" | \"md\" | \"lg\";\n}\n\n/**\n * Wrapper around react-select pre-configured with the EPFL design system theme.\n * Accepts all react-select props plus label, helperText, and error.\n */\nexport function Select<\n Option = unknown,\n IsMulti extends boolean = false,\n Group extends GroupBase<Option> = GroupBase<Option>,\n>({ label, helperText, error, selectSize = \"md\", className = \"\", ...rest }: SelectProps<Option, IsMulti, Group>) {\n const autoId = useId();\n const inputId = rest.inputId ?? autoId;\n const helperId = `${inputId}-helper`;\n const hasError = Boolean(error);\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {label && (\n <label htmlFor={inputId} className=\"text-small font-medium text-text-primary\">\n {label}\n </label>\n )}\n\n <ReactSelect<Option, IsMulti, Group>\n inputId={inputId}\n theme={epflSelectTheme}\n menuPortalTarget={document.body}\n styles={{\n control: (base, state) => ({\n ...base,\n minHeight: selectSizeHeight[selectSize],\n height: state.isMulti ? undefined : selectSizeHeight[selectSize],\n }),\n valueContainer: (base) => ({\n ...base,\n height: rest.isMulti ? undefined : selectSizeHeight[selectSize] - 2,\n padding: \"0 6px\",\n }),\n input: (base) => ({\n ...base,\n margin: 0,\n paddingTop: 0,\n paddingBottom: 0,\n }),\n indicatorsContainer: (base) => ({\n ...base,\n height: rest.isMulti ? undefined : selectSizeHeight[selectSize] - 2,\n }),\n menuPortal: (base) => ({ ...base, zIndex: 50 }),\n ...rest.styles,\n }}\n classNames={{\n ...(epflSelectClassNames as unknown as ClassNamesConfig<\n Option,\n IsMulti,\n Group\n >),\n control: (state) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const base = (epflSelectClassNames.control as any)?.(state) ?? \"\";\n const sizeClass = selectSizeClasses[selectSize];\n const styled = `${base} ${sizeClass}`;\n return hasError ? `${styled} !border-error !focus:ring-error` : styled;\n },\n }}\n aria-invalid={hasError || undefined}\n aria-errormessage={hasError ? helperId : undefined}\n {...rest}\n />\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n}\n","import { forwardRef, useId, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nexport interface SwitchProps extends Omit<ComponentPropsWithoutRef<\"input\">, \"type\"> {\n /** Label displayed next to the switch. */\n label?: ReactNode;\n /** Helper text below the switch. */\n helperText?: string;\n}\n\n/**\n * Toggle switch with label. Uses a hidden checkbox for accessibility.\n */\nexport const Switch = forwardRef<HTMLInputElement, SwitchProps>(function Switch(\n { label, helperText, className = \"\", id: idProp, disabled, ...rest },\n ref,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n\n return (\n <div className={`flex flex-col gap-1 ${className}`}>\n <div className=\"flex items-center gap-3\">\n {/* Hidden native checkbox for a11y */}\n <div className=\"relative inline-flex items-center\">\n <input\n ref={ref}\n id={id}\n type=\"checkbox\"\n role=\"switch\"\n disabled={disabled}\n aria-describedby={helperText ? helperId : undefined}\n className=\"peer sr-only\"\n {...rest}\n />\n\n {/* Track + Thumb (thumb via ::after pseudo-element) */}\n <label\n htmlFor={id}\n className={[\n \"relative h-6 w-11 rounded-full transition-colors\",\n \"bg-border-strong\",\n \"peer-checked:bg-primary\",\n \"peer-focus-visible:ring-2 peer-focus-visible:ring-primary peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-bg-primary\",\n \"peer-disabled:opacity-50 peer-disabled:cursor-not-allowed\",\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer\",\n // Thumb\n \"after:content-[''] after:absolute after:top-0.5 after:left-0.5\",\n \"after:size-5 after:rounded-full after:bg-white after:shadow-sm\",\n \"after:transition-transform peer-checked:after:translate-x-5\",\n ].join(\" \")}\n aria-hidden=\"true\"\n />\n </div>\n\n {label && (\n <label\n htmlFor={id}\n className={`text-body select-none ${disabled ? \"text-text-muted cursor-not-allowed\" : \"text-text-primary cursor-pointer\"}`}\n >\n {label}\n </label>\n )}\n </div>\n\n {helperText && (\n <p id={helperId} className=\"text-caption text-text-secondary pl-14\">\n {helperText}\n </p>\n )}\n </div>\n );\n});\n","import {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useRef,\n type ComponentPropsWithoutRef,\n} from \"react\";\n\nexport interface TextareaProps extends ComponentPropsWithoutRef<\"textarea\"> {\n /** Visible label above the textarea. */\n label?: string;\n /** Helper text shown below the textarea. */\n helperText?: string;\n /** Error message — replaces helper text and triggers error styling. */\n error?: string;\n /** Automatically grow height to fit content. */\n autoGrow?: boolean;\n /** Minimum number of visible rows (default: 3). */\n minRows?: number;\n}\n\n/**\n * Textarea with label, helper text, error state, and optional auto-grow.\n */\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n {\n label,\n helperText,\n error,\n autoGrow = false,\n minRows = 3,\n className = \"\",\n id: idProp,\n disabled,\n onChange,\n ...rest\n },\n forwardedRef,\n) {\n const autoId = useId();\n const id = idProp ?? autoId;\n const helperId = `${id}-helper`;\n const hasError = Boolean(error);\n const internalRef = useRef<HTMLTextAreaElement | null>(null);\n\n const resize = useCallback(() => {\n const el = internalRef.current;\n if (!el || !autoGrow) return;\n el.style.height = \"auto\";\n el.style.height = `${el.scrollHeight}px`;\n }, [autoGrow]);\n\n useEffect(() => {\n resize();\n }, [resize, rest.value, rest.defaultValue]);\n\n // Merge refs\n const setRef = useCallback(\n (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof forwardedRef === \"function\") {\n forwardedRef(node);\n } else if (forwardedRef) {\n forwardedRef.current = node;\n }\n },\n [forwardedRef],\n );\n\n return (\n <div className={`flex flex-col gap-1.5 ${className}`}>\n {label && (\n <label htmlFor={id} className=\"text-small font-medium text-text-primary\">\n {label}\n </label>\n )}\n\n <textarea\n ref={setRef}\n id={id}\n rows={minRows}\n disabled={disabled}\n aria-invalid={hasError || undefined}\n aria-describedby={helperText || error ? helperId : undefined}\n onChange={(e) => {\n onChange?.(e);\n resize();\n }}\n className={[\n \"w-full rounded-md border bg-bg-primary text-text-primary placeholder:text-text-muted\",\n \"px-3 py-2 text-body outline-none transition-colors\",\n \"focus:border-primary focus:ring-1 focus:ring-primary\",\n \"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-bg-secondary\",\n autoGrow ? \"resize-none overflow-hidden\" : \"resize-y\",\n hasError\n ? \"border-error focus:border-error focus:ring-error\"\n : \"border-border hover:border-border-strong\",\n ].join(\" \")}\n {...rest}\n />\n\n {(helperText || error) && (\n <p\n id={helperId}\n className={`text-caption ${hasError ? \"text-error\" : \"text-text-secondary\"}`}\n >\n {error ?? helperText}\n </p>\n )}\n </div>\n );\n});\n","import {\n autoUpdate,\n flip,\n FloatingFocusManager,\n FloatingPortal,\n offset,\n type Placement,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useInteractions,\n useListNavigation,\n useRole,\n} from \"@floating-ui/react\";\nimport {\n createContext,\n forwardRef,\n type ReactNode,\n useCallback,\n useContext,\n useRef,\n useState,\n} from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface DropdownMenuProps {\n /** The trigger element that opens the menu. */\n trigger: ReactNode;\n /** Menu items. Use `DropdownItem` and `DropdownDivider`. */\n children: ReactNode;\n /** Preferred placement of the floating menu. @default \"bottom-start\" */\n placement?: Placement;\n /** Additional CSS classes on the menu panel. */\n className?: string;\n}\n\nexport interface DropdownItemProps {\n /** Item label or content. */\n children: ReactNode;\n /** Optional icon rendered before the label. */\n icon?: ReactNode;\n /** Danger / destructive styling. */\n danger?: boolean;\n /** Whether the item is disabled. */\n disabled?: boolean;\n /** Click handler. */\n onClick?: () => void;\n /** Additional CSS classes. */\n className?: string;\n}\n\n/* ── Internal context ─────────────────────────────────────── */\n\nconst DropdownCtx = createContext<{\n close: () => void;\n getItemProps: (userProps?: Record<string, unknown>) => Record<string, unknown>;\n activeIndex: number | null;\n setActiveIndex: (i: number | null) => void;\n}>({\n close: () => {},\n getItemProps: () => ({}),\n activeIndex: null,\n setActiveIndex: () => {},\n});\n\n/* ── DropdownDivider ──────────────────────────────────────── */\n\nexport function DropdownDivider() {\n return <div role=\"separator\" className=\"my-1 border-t border-border\" />;\n}\n\n/* ── DropdownItem ─────────────────────────────────────────── */\n\nexport const DropdownItem = forwardRef<HTMLButtonElement, DropdownItemProps>(function DropdownItem(\n { children, icon, danger = false, disabled = false, onClick, className = \"\", ...rest },\n ref,\n) {\n const { close } = useContext(DropdownCtx);\n\n const handleClick = () => {\n if (disabled) return;\n onClick?.();\n close();\n };\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"menuitem\"\n disabled={disabled}\n onClick={handleClick}\n className={[\n \"flex w-full items-center gap-2 px-3 py-2 text-small rounded-md\",\n \"text-left transition-colors outline-none cursor-pointer\",\n danger\n ? \"text-error hover:bg-error-bg focus:bg-error-bg\"\n : \"text-text-primary hover:bg-bg-tertiary focus:bg-bg-tertiary\",\n disabled && \"opacity-50 pointer-events-none\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {icon && <span className=\"shrink-0 size-4\">{icon}</span>}\n {children}\n </button>\n );\n});\n\n/* ── DropdownMenu ─────────────────────────────────────────── */\n\nexport function DropdownMenu({\n trigger,\n children,\n placement = \"bottom-start\",\n className = \"\",\n}: DropdownMenuProps) {\n const [isOpen, setIsOpen] = useState(false);\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const listRef = useRef<(HTMLElement | null)[]>([]);\n\n const { refs, floatingStyles, context } = useFloating({\n open: isOpen,\n onOpenChange: setIsOpen,\n placement,\n middleware: [offset(4), flip(), shift({ padding: 8 })],\n whileElementsMounted: autoUpdate,\n });\n\n const click = useClick(context);\n const dismiss = useDismiss(context);\n const role = useRole(context, { role: \"menu\" });\n const listNavigation = useListNavigation(context, {\n listRef,\n activeIndex,\n onNavigate: setActiveIndex,\n loop: true,\n });\n\n const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([\n click,\n dismiss,\n role,\n listNavigation,\n ]);\n\n const close = useCallback(() => setIsOpen(false), []);\n\n return (\n <>\n {/* Wrapper to attach floating-ui ref + interaction props */}\n <span ref={refs.setReference} className=\"inline-flex\" {...getReferenceProps()}>\n {trigger}\n </span>\n\n {isOpen && (\n <FloatingPortal>\n <FloatingFocusManager context={context} modal={false}>\n <div\n // eslint-disable-next-line react-hooks/refs -- floating-ui callback ref setter, not a .current read\n ref={refs.setFloating}\n style={floatingStyles}\n className={[\n \"z-50 min-w-[10rem] p-1 rounded-lg border border-border\",\n \"bg-bg-primary shadow-lg animate-floating-in\",\n className,\n ].join(\" \")}\n {...getFloatingProps()}\n >\n <DropdownCtx.Provider\n value={{ close, getItemProps, activeIndex, setActiveIndex }}\n >\n {children}\n </DropdownCtx.Provider>\n </div>\n </FloatingFocusManager>\n </FloatingPortal>\n )}\n </>\n );\n}\n","import {\n autoUpdate,\n flip,\n FloatingFocusManager,\n FloatingPortal,\n offset,\n type Placement,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useInteractions,\n useRole,\n} from \"@floating-ui/react\";\nimport { type ReactNode, useState } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface PopoverProps {\n /** The trigger element that toggles the popover. */\n trigger: ReactNode;\n /** Popover body content. */\n children: ReactNode;\n /** Preferred placement. @default \"bottom\" */\n placement?: Placement;\n /** Whether the popover is controlled externally. */\n open?: boolean;\n /** Callback when open state changes (controlled mode). */\n onOpenChange?: (open: boolean) => void;\n /** Additional CSS classes on the popover panel. */\n className?: string;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Popover({\n trigger,\n children,\n placement = \"bottom\",\n open: controlledOpen,\n onOpenChange,\n className = \"\",\n}: PopoverProps) {\n const [uncontrolledOpen, setUncontrolledOpen] = useState(false);\n\n const isControlled = controlledOpen !== undefined;\n const isOpen = isControlled ? controlledOpen : uncontrolledOpen;\n const setOpen = isControlled ? (v: boolean) => onOpenChange?.(v) : setUncontrolledOpen;\n\n const { refs, floatingStyles, context } = useFloating({\n open: isOpen,\n onOpenChange: setOpen,\n placement,\n middleware: [offset(8), flip(), shift({ padding: 8 })],\n whileElementsMounted: autoUpdate,\n });\n\n const click = useClick(context);\n const dismiss = useDismiss(context);\n const role = useRole(context);\n\n const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);\n\n return (\n <>\n {/* Wrapper to attach floating-ui ref + interaction props */}\n <span ref={refs.setReference} className=\"inline-flex\" {...getReferenceProps()}>\n {trigger}\n </span>\n\n {isOpen && (\n <FloatingPortal>\n <FloatingFocusManager context={context} modal={false}>\n <div\n // eslint-disable-next-line react-hooks/refs -- floating-ui callback ref setter, not a .current read\n ref={refs.setFloating}\n style={floatingStyles}\n className={[\n \"z-50 rounded-lg border border-border p-md\",\n \"bg-bg-primary shadow-lg animate-floating-in\",\n className,\n ].join(\" \")}\n {...getFloatingProps()}\n >\n {children}\n </div>\n </FloatingFocusManager>\n </FloatingPortal>\n )}\n </>\n );\n}\n","import {\n createContext,\n type KeyboardEvent,\n type ReactNode,\n useCallback,\n useContext,\n useId,\n useRef,\n useState,\n} from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface TabItem {\n /** Unique key for the tab. */\n value: string;\n /** Label displayed in the tab button. */\n label: ReactNode;\n /** Optional icon before the label. */\n icon?: ReactNode;\n /** Whether the tab is disabled. */\n disabled?: boolean;\n}\n\nexport interface TabsProps {\n /** Tab definitions. */\n items: TabItem[];\n /** The currently active tab value (controlled). */\n value?: string;\n /** Default active tab (uncontrolled). */\n defaultValue?: string;\n /** Callback when the active tab changes. */\n onChange?: (value: string) => void;\n /** Tab panel content — keyed by tab value. */\n children?: ReactNode;\n /** Additional CSS classes on the root. */\n className?: string;\n}\n\nexport interface TabPanelProps {\n /** Must match a `TabItem.value`. */\n value: string;\n /** Panel content. */\n children: ReactNode;\n /** Additional CSS classes. */\n className?: string;\n}\n\n/* ── Internal context ─────────────────────────────────────── */\n\nconst TabsCtx = createContext<{ activeValue: string; idPrefix: string }>({\n activeValue: \"\",\n idPrefix: \"\",\n});\n\n/* ── TabPanel ─────────────────────────────────────────────── */\n\nexport function TabPanel({ value, children, className = \"\" }: TabPanelProps) {\n const { activeValue, idPrefix } = useContext(TabsCtx);\n const isActive = activeValue === value;\n\n return (\n <div\n id={`${idPrefix}-panel-${value}`}\n role=\"tabpanel\"\n aria-labelledby={`${idPrefix}-tab-${value}`}\n hidden={!isActive}\n tabIndex={0}\n className={className}\n >\n {isActive && children}\n </div>\n );\n}\n\n/* ── Tabs ─────────────────────────────────────────────────── */\n\nexport function Tabs({\n items,\n value: controlledValue,\n defaultValue,\n onChange,\n children,\n className = \"\",\n}: TabsProps) {\n const idPrefix = useId().replace(/:/g, \"\");\n const tabListRef = useRef<HTMLDivElement>(null);\n\n /* Controlled / uncontrolled */\n const isControlled = controlledValue !== undefined;\n const [internal, setInternal] = useState(\n () => defaultValue ?? items.find((t) => !t.disabled)?.value ?? items[0]?.value ?? \"\",\n );\n const activeValue = isControlled ? controlledValue : internal;\n\n const setActive = useCallback(\n (v: string) => {\n if (!isControlled) setInternal(v);\n onChange?.(v);\n },\n [isControlled, onChange],\n );\n\n /* Keyboard navigation (arrow keys + Home/End) */\n const enabledItems = items.filter((t) => !t.disabled);\n\n const handleKeyDown = (e: KeyboardEvent) => {\n const currentIdx = enabledItems.findIndex((t) => t.value === activeValue);\n let nextIdx = currentIdx;\n\n switch (e.key) {\n case \"ArrowRight\":\n case \"ArrowDown\":\n e.preventDefault();\n nextIdx = (currentIdx + 1) % enabledItems.length;\n break;\n case \"ArrowLeft\":\n case \"ArrowUp\":\n e.preventDefault();\n nextIdx = (currentIdx - 1 + enabledItems.length) % enabledItems.length;\n break;\n case \"Home\":\n e.preventDefault();\n nextIdx = 0;\n break;\n case \"End\":\n e.preventDefault();\n nextIdx = enabledItems.length - 1;\n break;\n default:\n return;\n }\n\n const next = enabledItems[nextIdx];\n if (next) {\n setActive(next.value);\n /* Focus the new tab button */\n const btn = tabListRef.current?.querySelector<HTMLElement>(\n `[data-tab-value=\"${next.value}\"]`,\n );\n btn?.focus();\n }\n };\n\n return (\n <TabsCtx.Provider value={{ activeValue, idPrefix }}>\n <div className={className}>\n {/* Tab list */}\n <div\n ref={tabListRef}\n role=\"tablist\"\n aria-orientation=\"horizontal\"\n onKeyDown={handleKeyDown}\n className=\"flex border-b border-border\"\n >\n {items.map((tab) => {\n const isActive = tab.value === activeValue;\n return (\n <button\n key={tab.value}\n id={`${idPrefix}-tab-${tab.value}`}\n role=\"tab\"\n type=\"button\"\n data-tab-value={tab.value}\n aria-selected={isActive}\n aria-controls={`${idPrefix}-panel-${tab.value}`}\n tabIndex={isActive ? 0 : -1}\n disabled={tab.disabled}\n onClick={() => setActive(tab.value)}\n className={[\n \"inline-flex items-center gap-1.5 px-4 py-2.5 text-small font-medium\",\n \"border-b-2 -mb-px transition-colors outline-none cursor-pointer\",\n \"focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2\",\n isActive\n ? \"border-primary text-primary\"\n : \"border-transparent text-text-secondary hover:text-text-primary hover:border-border-strong\",\n tab.disabled && \"opacity-50 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {tab.icon && <span className=\"shrink-0 size-4\">{tab.icon}</span>}\n {tab.label}\n </button>\n );\n })}\n </div>\n\n {/* Panels */}\n {children}\n </div>\n </TabsCtx.Provider>\n );\n}\n","import {\n FloatingPortal,\n arrow,\n autoUpdate,\n flip,\n offset,\n shift,\n useDismiss,\n useFloating,\n useFocus,\n useHover,\n useInteractions,\n useRole,\n} from \"@floating-ui/react\";\nimport { type ReactNode, useRef, useState } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type TooltipPlacement = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface TooltipProps {\n /** The element the tooltip is anchored to. */\n children: ReactNode;\n /** Tooltip text. */\n content: ReactNode;\n /** Placement relative to the trigger. @default \"top\" */\n placement?: TooltipPlacement;\n /** Delay in ms before showing. @default 200 */\n delay?: number;\n /** Additional CSS classes on the tooltip. */\n className?: string;\n}\n\n/* ── Arrow side mapping ───────────────────────────────────── */\n\nconst arrowSide: Record<string, string> = {\n top: \"bottom\",\n bottom: \"top\",\n left: \"right\",\n right: \"left\",\n};\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Tooltip({\n children,\n content,\n placement = \"top\",\n delay = 200,\n className = \"\",\n}: TooltipProps) {\n const [isOpen, setIsOpen] = useState(false);\n const arrowRef = useRef<HTMLDivElement>(null);\n\n const {\n refs,\n floatingStyles,\n context,\n middlewareData,\n placement: actualPlacement,\n } = useFloating({\n open: isOpen,\n onOpenChange: setIsOpen,\n placement,\n middleware: [\n offset(8),\n flip(),\n shift({ padding: 8 }),\n // eslint-disable-next-line react-hooks/refs -- floating-ui arrow middleware needs ref object\n arrow({ element: arrowRef }),\n ],\n whileElementsMounted: autoUpdate,\n });\n\n const hover = useHover(context, { delay, move: false });\n const focus = useFocus(context);\n const dismiss = useDismiss(context);\n const role = useRole(context, { role: \"tooltip\" });\n\n const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);\n\n /* Wrap child to attach floating-ui ref + interaction props */\n\n /* Arrow positioning */\n const side = actualPlacement.split(\"-\")[0];\n const arrowX = middlewareData.arrow?.x;\n const arrowY = middlewareData.arrow?.y;\n\n return (\n <>\n <span ref={refs.setReference} className=\"inline-flex\" {...getReferenceProps()}>\n {children}\n </span>\n\n {isOpen && content && (\n <FloatingPortal>\n <div\n // eslint-disable-next-line react-hooks/refs -- floating-ui callback ref setter, not a .current read\n ref={refs.setFloating}\n style={floatingStyles}\n role=\"tooltip\"\n className={[\n \"z-50 max-w-[var(--container-xs)] px-3 py-1.5 rounded-md text-small\",\n \"bg-text-primary text-bg-primary shadow-md\",\n \"pointer-events-none animate-floating-in\",\n className,\n ].join(\" \")}\n {...getFloatingProps()}\n >\n {content}\n {/* Arrow */}\n <div\n ref={arrowRef}\n className=\"absolute size-2 bg-text-primary rotate-45\"\n style={{\n left: arrowX != null ? `${arrowX}px` : \"\",\n top: arrowY != null ? `${arrowY}px` : \"\",\n [arrowSide[side]]: \"-4px\",\n }}\n />\n </div>\n </FloatingPortal>\n )}\n </>\n );\n}\n","import { useCallback } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport type Language = \"en\" | \"fr\";\n\n/**\n * Hook providing the current language and a toggle / setter.\n *\n * ```tsx\n * const { language, toggleLanguage, setLanguage } = useLanguage();\n * ```\n */\nexport function useLanguage() {\n const { i18n } = useTranslation();\n\n const language = (i18n.language ?? \"en\") as Language;\n\n const setLanguage = useCallback(\n (lng: Language) => {\n i18n.changeLanguage(lng);\n },\n [i18n],\n );\n\n const toggleLanguage = useCallback(() => {\n setLanguage(language === \"en\" ? \"fr\" : \"en\");\n }, [language, setLanguage]);\n\n return { language, toggleLanguage, setLanguage } as const;\n}\n","import { useLanguage, type Language } from \"../../hooks/useLanguage\";\n\nexport interface LanguageSwitcherProps {\n /** Additional CSS classes. */\n className?: string;\n}\n\nconst languages: { value: Language; label: string }[] = [\n { value: \"en\", label: \"EN\" },\n { value: \"fr\", label: \"FR\" },\n];\n\n/**\n * Pill-style toggle for switching between EN and FR.\n *\n * Designed to sit inside the TopNav `actions` slot.\n */\nexport function LanguageSwitcher({ className = \"\" }: LanguageSwitcherProps) {\n const { language, setLanguage } = useLanguage();\n\n return (\n <div\n role=\"radiogroup\"\n aria-label=\"Language\"\n className={[\n \"inline-flex items-center rounded-full border border-border bg-bg-secondary p-0.5\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {languages.map(({ value, label }) => {\n const isActive = language === value;\n return (\n <button\n key={value}\n type=\"button\"\n role=\"radio\"\n aria-checked={isActive}\n onClick={() => setLanguage(value)}\n className={[\n \"rounded-full px-2.5 py-0.5 text-caption font-medium transition-colors cursor-pointer\",\n isActive\n ? \"bg-primary text-white shadow-sm\"\n : \"text-text-secondary hover:text-text-primary\",\n ].join(\" \")}\n >\n {label}\n </button>\n );\n })}\n </div>\n );\n}\n","import { useCallback, useEffect, useSyncExternalStore } from \"react\";\n\nconst STORAGE_KEY = \"poesis-theme\";\ntype Theme = \"light\" | \"dark\";\n\n/** Listeners for useSyncExternalStore */\nconst listeners = new Set<() => void>();\n\nfunction notify() {\n listeners.forEach((l) => l());\n}\n\nfunction subscribe(callback: () => void) {\n listeners.add(callback);\n return () => listeners.delete(callback);\n}\n\nfunction getSnapshot(): Theme {\n return document.documentElement.classList.contains(\"dark\") ? \"dark\" : \"light\";\n}\n\nfunction getServerSnapshot(): Theme {\n return \"light\";\n}\n\n/**\n * Apply theme class to <html> and persist to localStorage.\n */\nfunction applyTheme(theme: Theme) {\n if (theme === \"dark\") {\n document.documentElement.classList.add(\"dark\");\n } else {\n document.documentElement.classList.remove(\"dark\");\n }\n localStorage.setItem(STORAGE_KEY, theme);\n notify();\n}\n\n/**\n * Initialise theme from localStorage or system preference.\n * Call once at app startup (e.g. in main.tsx or a top-level effect).\n */\nexport function initTheme() {\n const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;\n if (stored) {\n applyTheme(stored);\n return;\n }\n const prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n applyTheme(prefersDark ? \"dark\" : \"light\");\n}\n\n/**\n * Hook providing the current theme and a toggle function.\n *\n * ```tsx\n * const { theme, toggleTheme } = useTheme();\n * ```\n */\nexport function useTheme() {\n const theme = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n\n // Listen for system preference changes when no stored preference\n useEffect(() => {\n const mq = window.matchMedia(\"(prefers-color-scheme: dark)\");\n const handler = (e: MediaQueryListEvent) => {\n if (!localStorage.getItem(STORAGE_KEY)) {\n applyTheme(e.matches ? \"dark\" : \"light\");\n }\n };\n mq.addEventListener(\"change\", handler);\n return () => mq.removeEventListener(\"change\", handler);\n }, []);\n\n const toggleTheme = useCallback(() => {\n applyTheme(theme === \"dark\" ? \"light\" : \"dark\");\n }, [theme]);\n\n const setTheme = useCallback((t: Theme) => {\n applyTheme(t);\n }, []);\n\n return { theme, toggleTheme, setTheme, isDark: theme === \"dark\" } as const;\n}\n","import { MoonFill, SunFill } from \"react-bootstrap-icons\";\nimport { useTranslation } from \"react-i18next\";\nimport { useTheme } from \"../../hooks/useTheme\";\nimport { IconButton, type IconButtonSize, type IconButtonVariant } from \"./IconButton\";\n\nexport interface ThemeToggleProps {\n /** Visual style. @default \"ghost\" */\n variant?: IconButtonVariant;\n /** Size preset. @default \"md\" */\n size?: IconButtonSize;\n /** Additional CSS classes. */\n className?: string;\n}\n\n/**\n * Sun/moon icon button that toggles between light and dark mode.\n *\n * Designed to sit inside the TopNav `actions` slot.\n */\nexport function ThemeToggle({ variant = \"ghost\", size = \"md\", className }: ThemeToggleProps) {\n const { isDark, toggleTheme } = useTheme();\n const { t } = useTranslation();\n\n return (\n <IconButton\n variant={variant}\n size={size}\n icon={isDark ? <SunFill size={16} /> : <MoonFill size={16} />}\n aria-label={t(\"theme.toggleTheme\")}\n onClick={toggleTheme}\n className={className}\n />\n );\n}\n","import { type ReactNode } from \"react\";\nimport { List } from \"react-bootstrap-icons\";\nimport type { NavCategory } from \"./types\";\n\nconst EPFL_LOGO_URL =\n \"https://www.epfl.ch/campus/services/website//wp-content/themes/wp-theme-2018/assets/svg/epfl-logo.svg?refresh=now\";\n\nexport interface TopNavProps {\n /** Full custom logo element — takes precedence over logoUrl */\n logo?: ReactNode;\n /** URL of a logo image (defaults to the official EPFL SVG) */\n logoUrl?: string;\n /** Top-level navigation categories */\n categories?: NavCategory[];\n /** Currently active category id */\n activeCategoryId?: string;\n /** Called when a category tab is clicked */\n onCategoryChange?: (id: string) => void;\n /** Slot for right-side actions (language switcher, theme toggle, avatar) */\n actions?: ReactNode;\n /** Called when the mobile burger button is clicked */\n onBurgerClick?: () => void;\n}\n\n/**\n * Fixed top navigation bar.\n *\n * Desktop: Logo + category tabs + actions.\n * Mobile (<lg): Logo + burger + actions.\n */\nexport function TopNav({\n logo,\n logoUrl,\n categories = [],\n activeCategoryId,\n onCategoryChange,\n actions,\n onBurgerClick,\n}: TopNavProps) {\n return (\n <header className=\"fixed inset-x-0 top-0 z-40 flex h-14 items-center border-b border-border bg-bg-primary px-md\">\n {/* ── Burger (mobile only) ── */}\n <button\n type=\"button\"\n onClick={onBurgerClick}\n className=\"mr-sm flex size-9 items-center justify-center rounded-md text-text-secondary hover:bg-bg-tertiary hover:text-text-primary poesis-mobile-only\"\n aria-label=\"Open menu\"\n >\n <List size={22} />\n </button>\n\n {/* ── Logo ── */}\n <div className=\"mr-lg flex shrink-0 items-center\">\n {logo ?? (\n <img\n src={logoUrl ?? EPFL_LOGO_URL}\n alt=\"EPFL\"\n className=\"h-[30px] w-auto\"\n />\n )}\n </div>\n\n {/* ── Category tabs (desktop) ── */}\n <nav className=\"poesis-desktop-flex flex-1 items-center gap-xs\" aria-label=\"Main categories\">\n {categories.map((cat) => {\n const isActive = cat.id === activeCategoryId;\n return (\n <button\n key={cat.id}\n type=\"button\"\n onClick={() => onCategoryChange?.(cat.id)}\n className={[\n \"flex items-center gap-xs rounded-md px-sm py-xs text-small font-medium transition-colors\",\n isActive\n ? \"bg-primary/10 text-primary\"\n : \"text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={isActive ? \"page\" : undefined}\n >\n {cat.icon && <cat.icon size={16} />}\n {cat.label}\n </button>\n );\n })}\n </nav>\n\n {/* ── Spacer (mobile — push actions right) ── */}\n <div className=\"flex-1 poesis-mobile-only\" />\n\n {/* ── Actions slot ── */}\n {actions && <div className=\"flex items-center gap-xs\">{actions}</div>}\n </header>\n );\n}\n","import { useState } from \"react\";\nimport { ChevronDown, ChevronRight } from \"react-bootstrap-icons\";\nimport { Badge } from \"../ui/Badge\";\nimport { Tooltip } from \"../ui/Tooltip\";\nimport type { IconComponent, NavLinkCounter, NavSection } from \"./types\";\n\n// ── Fold state persistence ──────────────────────────────────\n\nconst FOLD_KEY = \"sidebar-fold-state\";\n\nconst slugify = (text: string): string =>\n text\n .toString()\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .trim()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n\nconst readFoldState = (key: string): boolean => {\n try {\n const raw = localStorage.getItem(FOLD_KEY);\n if (raw) return JSON.parse(raw)[key] === true;\n } catch {\n /* ignore */\n }\n return false;\n};\n\nconst writeFoldState = (key: string, open: boolean) => {\n try {\n const raw = localStorage.getItem(FOLD_KEY);\n const state = raw ? JSON.parse(raw) : {};\n state[key] = open;\n localStorage.setItem(FOLD_KEY, JSON.stringify(state));\n } catch {\n /* ignore */\n }\n};\n\nfunction useFoldState(title: string | undefined, foldable: boolean) {\n const storageKey = `sidebar-${title ? slugify(title) : \"fallback\"}-open`;\n const [isOpen, setIsOpen] = useState(foldable ? readFoldState(storageKey) : true);\n\n const toggle = () => {\n if (!foldable) return;\n const next = !isOpen;\n setIsOpen(next);\n writeFoldState(storageKey, next);\n };\n\n return { isOpen, toggle };\n}\n\n// ── Types ───────────────────────────────────────────────────\n\nexport interface HomeLinkConfig {\n label: string;\n href: string;\n icon?: IconComponent;\n}\n\nexport interface SideNavProps {\n /** Grouped navigation sections for the active category */\n sections?: NavSection[];\n /** Currently active link id (for highlight) */\n activeLinkId?: string;\n /** Called when a link is clicked */\n onLinkClick?: (href: string) => void;\n /** Whether the sidebar is collapsed to icon-only mode */\n collapsed?: boolean;\n /** Called to toggle collapsed state */\n onToggleCollapse?: () => void;\n /** Whether sections can be folded and fold state persisted to localStorage */\n foldable?: boolean;\n /** Optional home link rendered at the top of the sidebar */\n homeLink?: HomeLinkConfig;\n}\n\n/**\n * Sidebar navigation with grouped, collapsible sections.\n *\n * - 256px wide (or 64px collapsed)\n * - Hidden on mobile (content moves into BurgerDrawer)\n * - Fixed height, scrollable when content overflows\n */\nexport function SideNav({\n sections = [],\n activeLinkId,\n onLinkClick,\n collapsed = false,\n foldable = false,\n homeLink,\n}: SideNavProps) {\n return (\n <aside\n className={[\n \"poesis-desktop-flex flex-col border-r border-border bg-bg-secondary overflow-y-auto transition-[width] duration-200\",\n collapsed ? \"w-16\" : \"w-64\",\n ].join(\" \")}\n >\n <nav aria-label=\"Page navigation\">\n <ul className=\"list-none m-0 p-0\">\n {/* Home link */}\n {homeLink && (\n <li className=\"border-b border-border\">\n <button\n type=\"button\"\n onClick={() => onLinkClick?.(homeLink.href)}\n title={collapsed ? (typeof homeLink.label === \"string\" ? homeLink.label : undefined) : undefined}\n className={[\n \"flex w-full items-center gap-3 px-4 py-3.5 text-sm no-underline font-medium transition-colors\",\n collapsed ? \"justify-center\" : \"\",\n activeLinkId === homeLink.href\n ? \"bg-bg-tertiary text-text-primary border-r-2 border-red-600\"\n : \"text-text-secondary hover:bg-bg-secondary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={activeLinkId === homeLink.href ? \"page\" : undefined}\n >\n {homeLink.icon && <homeLink.icon size={18} className=\"shrink-0\" />}\n {!collapsed && <span>{homeLink.label}</span>}\n </button>\n </li>\n )}\n\n {/* Menu sections */}\n {sections.map((section) => (\n <SideNavSection\n key={section.id}\n section={section}\n activeLinkId={activeLinkId}\n onLinkClick={onLinkClick}\n collapsed={collapsed}\n foldable={foldable}\n />\n ))}\n </ul>\n </nav>\n </aside>\n );\n}\n\n/* ── Counter badges ───────────────────────────────────────── */\n\nfunction NavLinkCounters({ counters }: { counters?: NavLinkCounter[] }) {\n const visible = counters?.filter((c) => c.count > 0);\n if (!visible?.length) return null;\n\n return (\n <span className=\"ml-auto flex items-center gap-1\">\n {visible.map((counter, i) => (\n <Tooltip key={i} content={counter.tooltip} placement=\"top\">\n <Badge color={counter.color}>\n {counter.count > 99 ? \"99+\" : counter.count}\n </Badge>\n </Tooltip>\n ))}\n </span>\n );\n}\n\n/* ── Collapsible section ──────────────────────────────────── */\n\nfunction SideNavSection({\n section,\n activeLinkId,\n onLinkClick,\n collapsed,\n foldable,\n}: {\n section: NavSection;\n activeLinkId?: string;\n onLinkClick?: (href: string) => void;\n collapsed: boolean;\n foldable: boolean;\n}) {\n const { isOpen, toggle } = useFoldState(section.title, foldable);\n\n return (\n <li className=\"border-b border-border last:border-b-0\">\n {/* Section heading */}\n {section.title && !collapsed && (\n foldable ? (\n <button\n type=\"button\"\n onClick={toggle}\n className={[\n \"flex items-center gap-2 w-full text-left px-5 py-4 text-xs font-bold uppercase tracking-wider hover:text-text-primary hover:bg-bg-secondary transition-colors cursor-pointer bg-transparent border-none\",\n section.titleClassName ?? \"text-text-muted\",\n ].join(\" \")}\n >\n {isOpen ? (\n <ChevronDown className=\"w-3 h-3 shrink-0\" />\n ) : (\n <ChevronRight className=\"w-3 h-3 shrink-0\" />\n )}\n <span>{section.title}</span>\n </button>\n ) : (\n <span className={[\n \"block px-5 py-4 text-xs font-bold uppercase tracking-wider\",\n section.titleClassName ?? \"text-text-muted\",\n ].join(\" \")}>\n {section.title}\n </span>\n )\n )}\n\n {/* Links */}\n {(isOpen || collapsed) && (\n <ul className=\"list-none m-0 p-0 pb-1\">\n {section.links.map((link) => {\n const isActive = link.id === activeLinkId;\n return (\n <li key={link.id}>\n <button\n type=\"button\"\n onClick={() => onLinkClick?.(link.href)}\n title={collapsed ? (link.titleText ?? (typeof link.label === \"string\" ? link.label : undefined)) : undefined}\n className={[\n \"flex w-full items-center gap-sm px-4 py-3 text-sm no-underline transition-colors\",\n collapsed ? \"justify-center\" : \"\",\n isActive\n ? \"bg-bg-tertiary text-text-primary font-medium border-r-2 border-red-600\"\n : \"text-text-secondary hover:bg-bg-secondary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={isActive ? \"page\" : undefined}\n >\n {link.icon && <link.icon size={18} />}\n {!collapsed && <span>{link.label}</span>}\n {!collapsed && <NavLinkCounters counters={link.counters} />}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </li>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { ChevronDown, XLg } from \"react-bootstrap-icons\";\nimport { Badge } from \"../ui/Badge\";\nimport { Tooltip } from \"../ui/Tooltip\";\nimport type { NavCategory, NavLinkCounter, NavSection } from \"./types\";\n\nexport interface BurgerDrawerProps {\n /** Whether the drawer is open */\n open: boolean;\n /** Called to close the drawer */\n onClose: () => void;\n /** Top-level categories */\n categories?: NavCategory[];\n /** Active category id */\n activeCategoryId?: string;\n /** Called when a category is selected */\n onCategoryChange?: (id: string) => void;\n /** Sections for the active category */\n sections?: NavSection[];\n /** Active link id */\n activeLinkId?: string;\n /** Called when a link is clicked */\n onLinkClick?: (href: string) => void;\n}\n\n/**\n * Mobile slide-over drawer that merges TopNav categories and SideNav links.\n * Slides in from the left with a backdrop overlay.\n */\nexport function BurgerDrawer({\n open,\n onClose,\n categories = [],\n activeCategoryId,\n onCategoryChange,\n sections = [],\n activeLinkId,\n onLinkClick,\n}: BurgerDrawerProps) {\n const drawerRef = useRef<HTMLDivElement>(null);\n\n // Close on Escape\n useEffect(() => {\n if (!open) return;\n const handler = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [open, onClose]);\n\n // Trap focus inside drawer when open\n useEffect(() => {\n if (!open) return;\n const el = drawerRef.current;\n el?.focus();\n }, [open]);\n\n // Prevent body scroll when open\n useEffect(() => {\n if (open) {\n document.body.style.overflow = \"hidden\";\n } else {\n document.body.style.overflow = \"\";\n }\n return () => {\n document.body.style.overflow = \"\";\n };\n }, [open]);\n\n return (\n <>\n {/* ── Backdrop ── */}\n <div\n className={[\n \"fixed inset-0 z-50 bg-black/40 transition-opacity poesis-mobile-only\",\n open ? \"opacity-100\" : \"pointer-events-none opacity-0\",\n ].join(\" \")}\n onClick={onClose}\n aria-hidden=\"true\"\n />\n\n {/* ── Drawer panel ── */}\n <div\n ref={drawerRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Navigation menu\"\n className={[\n \"fixed inset-y-0 left-0 z-50 flex w-72 flex-col bg-bg-primary shadow-xl transition-transform duration-200 poesis-mobile-only\",\n open ? \"translate-x-0\" : \"-translate-x-full\",\n ].join(\" \")}\n >\n {/* ── Header ── */}\n <div className=\"flex h-14 items-center justify-between border-b border-border px-md\">\n <span className=\"text-h3 font-bold text-epfl-red\">EPFL</span>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"flex size-9 items-center justify-center rounded-md text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\"\n aria-label=\"Close menu\"\n >\n <XLg size={18} />\n </button>\n </div>\n\n {/* ── Scrollable content ── */}\n <div className=\"flex-1 overflow-y-auto p-sm\">\n {/* Category pills */}\n {categories.length > 0 && (\n <div className=\"mb-md flex flex-col gap-xs\">\n <span className=\"px-sm text-caption font-semibold uppercase tracking-wide text-text-muted\">\n Categories\n </span>\n {categories.map((cat) => {\n const isActive = cat.id === activeCategoryId;\n return (\n <button\n key={cat.id}\n type=\"button\"\n onClick={() => {\n onCategoryChange?.(cat.id);\n }}\n className={[\n \"flex items-center gap-sm rounded-md px-sm py-xs text-small font-medium transition-colors\",\n isActive\n ? \"bg-primary/10 text-primary\"\n : \"text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\",\n ].join(\" \")}\n >\n {cat.icon && <cat.icon size={16} />}\n {cat.label}\n </button>\n );\n })}\n </div>\n )}\n\n {/* Page links by section */}\n {sections.map((section) => (\n <DrawerSection\n key={section.id}\n section={section}\n activeLinkId={activeLinkId}\n onLinkClick={(href) => {\n onLinkClick?.(href);\n onClose();\n }}\n />\n ))}\n </div>\n </div>\n </>\n );\n}\n\n/* ── Counter badges for drawer links ─────────────────────── */\n\nfunction DrawerLinkCounters({ counters }: { counters?: NavLinkCounter[] }) {\n const visible = counters?.filter((c) => c.count > 0);\n if (!visible?.length) return null;\n\n return (\n <span className=\"ml-auto flex items-center gap-1\">\n {visible.map((counter, i) => (\n <Tooltip key={i} content={counter.tooltip} placement=\"top\">\n <Badge color={counter.color}>\n {counter.count > 99 ? \"99+\" : counter.count}\n </Badge>\n </Tooltip>\n ))}\n </span>\n );\n}\n\n/* ── Collapsible section inside drawer ───────────────────── */\n\nfunction DrawerSection({\n section,\n activeLinkId,\n onLinkClick,\n}: {\n section: NavSection;\n activeLinkId?: string;\n onLinkClick?: (href: string) => void;\n}) {\n const [open, setOpen] = useState(true);\n\n return (\n <div className=\"mb-xs flex flex-col\">\n {section.title && (\n <button\n type=\"button\"\n onClick={() => setOpen((o) => !o)}\n className=\"flex items-center justify-between rounded-md px-sm py-xs text-caption font-semibold uppercase tracking-wide text-text-muted hover:text-text-secondary\"\n >\n <span>{section.title}</span>\n <ChevronDown\n size={14}\n className={`transition-transform ${open ? \"\" : \"-rotate-90\"}`}\n />\n </button>\n )}\n\n {open && (\n <ul className=\"flex flex-col gap-px\">\n {section.links.map((link) => {\n const isActive = link.id === activeLinkId;\n return (\n <li key={link.id}>\n <button\n type=\"button\"\n onClick={() => onLinkClick?.(link.href)}\n className={[\n \"flex w-full items-center gap-sm rounded-md px-sm py-xs text-small transition-colors\",\n isActive\n ? \"bg-primary/10 font-medium text-primary\"\n : \"text-text-secondary hover:bg-bg-tertiary hover:text-text-primary\",\n ].join(\" \")}\n aria-current={isActive ? \"page\" : undefined}\n >\n {link.icon && <link.icon size={18} />}\n <span>{link.label}</span>\n <DrawerLinkCounters counters={link.counters} />\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n","const LOGO_URL =\n \"https://www.epfl.ch/campus/services/website//wp-content/themes/wp-theme-2018/assets/svg/epfl-logo.svg?refresh=now\";\n\nconst getCurrentYear = () => new Date().getFullYear();\n\nexport function Footer() {\n const scrollToTop = (e: React.MouseEvent<HTMLButtonElement>) => {\n const scrollable = e.currentTarget.closest(\"[data-scrollable]\");\n if (scrollable) {\n scrollable.scrollTo({ top: 0, behavior: \"smooth\" });\n } else {\n window.scrollTo({ top: 0, behavior: \"smooth\" });\n }\n };\n\n return (\n <footer role=\"contentinfo\" className=\"bg-bg-secondary border-t border-border mt-auto\">\n <div className=\"mx-auto max-w-[var(--container-6xl)] px-lg py-xl\">\n <div className=\"flex flex-wrap gap-lg\">\n {/* Logo */}\n <div className=\"shrink-0\">\n <a href=\"https://www.epfl.ch/en/\">\n <img\n src={LOGO_URL}\n alt=\"Logo EPFL\"\n className=\"h-8 w-auto\"\n style={{ filter: \"var(--logo-filter)\", opacity: \"var(--logo-opacity)\" }}\n />\n </a>\n </div>\n\n {/* Contact & Legal */}\n <div className=\"flex-1 min-w-[280px]\">\n <div className=\"flex flex-wrap gap-sm items-center mb-sm text-small\">\n <span className=\"font-medium text-text-primary\">Contact</span>\n <span className=\"text-text-secondary\">EPFL CH-1015 Lausanne</span>\n <span className=\"text-text-secondary\">+41 21 693 11 11</span>\n </div>\n\n {/* Legal */}\n <div className=\"flex flex-wrap gap-sm text-small mt-sm border-t border-border pt-sm\">\n <div className=\"flex gap-sm\">\n <a\n href=\"https://www.epfl.ch/about/overview/regulations-and-guidelines/disclaimer/\"\n className=\"text-text-secondary no-underline hover:text-primary transition-colors\"\n >\n Accessibility\n </a>\n <a\n href=\"https://www.epfl.ch/about/overview/regulations-and-guidelines/disclaimer/\"\n className=\"text-text-secondary no-underline hover:text-primary transition-colors\"\n >\n Disclaimer\n </a>\n <a\n href=\"https://go.epfl.ch/privacy-policy/\"\n className=\"text-text-secondary no-underline hover:text-primary transition-colors\"\n >\n Privacy policy\n </a>\n </div>\n <p className=\"m-0 text-text-muted text-small\">\n &copy; {getCurrentYear()} EPFL, all rights reserved\n </p>\n </div>\n </div>\n </div>\n\n {/* Back to top */}\n <div className=\"flex justify-end pt-md\">\n <button\n type=\"button\"\n onClick={scrollToTop}\n aria-label=\"Back to top\"\n className=\"bg-transparent border border-border-strong text-text-muted rounded-full w-9 h-9 cursor-pointer flex items-center justify-center hover:bg-bg-tertiary hover:text-text-primary transition-colors\"\n >\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"w-4 h-4\"\n >\n <polyline points=\"18 15 12 9 6 15\" />\n </svg>\n </button>\n </div>\n </div>\n </footer>\n );\n}\n","import { BoxArrowInRight, BoxArrowRight } from \"react-bootstrap-icons\";\nimport { useTranslation } from \"react-i18next\";\nimport type { AuthUser } from \"../../hooks/useAuth\";\nimport { Avatar } from \"../ui/Avatar\";\nimport { Button } from \"../ui/Button\";\nimport { DropdownDivider, DropdownItem, DropdownMenu } from \"../ui/DropdownMenu\";\n\n/* ------------------------------------------------------------------ */\n/* LoginButton (shown when not authenticated) */\n/* ------------------------------------------------------------------ */\n\nexport interface LoginButtonProps {\n /** Called when the user clicks the login button. */\n onLogin: () => void;\n}\n\nexport function LoginButton({ onLogin }: LoginButtonProps) {\n const { t } = useTranslation();\n return (\n <Button\n variant=\"primary\"\n size=\"sm\"\n iconLeft={<BoxArrowInRight size={14} />}\n onClick={onLogin}\n >\n {t(\"auth.login\")}\n </Button>\n );\n}\n\n/* ------------------------------------------------------------------ */\n/* UserMenu (shown when authenticated) */\n/* ------------------------------------------------------------------ */\n\nexport interface UserMenuProps {\n /** The authenticated user. */\n user: AuthUser;\n /** Called when the user clicks the logout item. */\n onLogout: () => void;\n}\n\nexport function UserMenu({ user, onLogout }: UserMenuProps) {\n const { t } = useTranslation();\n\n return (\n <DropdownMenu\n trigger={\n <Avatar\n src={user.avatarUrl}\n name={user.name}\n size=\"sm\"\n className=\"cursor-pointer\"\n />\n }\n placement=\"bottom-end\"\n >\n <div className=\"px-3 py-2\">\n <p className=\"text-small font-medium text-text-primary m-0\">{user.name}</p>\n <p className=\"text-caption text-text-secondary m-0\">{user.email}</p>\n </div>\n <DropdownDivider />\n <DropdownItem icon={<BoxArrowRight size={14} />} onClick={onLogout}>\n {t(\"auth.logout\")}\n </DropdownItem>\n </DropdownMenu>\n );\n}\n","import { useState, type ReactNode } from \"react\";\nimport { BurgerDrawer } from \"./BurgerDrawer\";\nimport { Footer } from \"./Footer\";\nimport { SideNav, type HomeLinkConfig } from \"./SideNav\";\nimport { TopNav } from \"./TopNav\";\nimport type { NavCategory, NavSection } from \"./types\";\n\nexport interface PageShellProps {\n /** Page content */\n children: ReactNode;\n /** Custom logo element for TopNav */\n logo?: ReactNode;\n /** URL of a logo image for TopNav (defaults to EPFL SVG) */\n logoUrl?: string;\n /** Top-level categories */\n categories?: NavCategory[];\n /** Active category id */\n activeCategoryId?: string;\n /** Called when a category tab is clicked */\n onCategoryChange?: (id: string) => void;\n /** Sidebar sections for the active category */\n sections?: NavSection[];\n /** Active link id for sidebar highlight */\n activeLinkId?: string;\n /** Called when a sidebar link is clicked */\n onLinkClick?: (href: string) => void;\n /** Slot for TopNav right-side actions */\n actions?: ReactNode;\n /** Whether the sidebar is collapsed */\n sideNavCollapsed?: boolean;\n /** Whether sidebar sections can be folded with localStorage persistence */\n sideNavFoldable?: boolean;\n /** Optional home link at the top of the sidebar */\n sideNavHomeLink?: HomeLinkConfig;\n /** Whether to show the footer (defaults to true) */\n showFooter?: boolean;\n /**\n * CSS classes for the inner content container div.\n * Pass `false` to remove the wrapper entirely (full-bleed content).\n * Defaults to `\"mx-auto max-w-[var(--container-6xl)]\"`.\n */\n contentContainerClassName?: string | false;\n}\n\n/**\n * Full application shell composing TopNav + SideNav + content area.\n *\n * - Desktop: fixed top bar + sidebar + scrollable main\n * - Mobile: fixed top bar + burger drawer + full-width main\n */\nexport function PageShell({\n children,\n logo,\n logoUrl,\n categories = [],\n activeCategoryId,\n onCategoryChange,\n sections = [],\n activeLinkId,\n onLinkClick,\n actions,\n sideNavCollapsed = false,\n sideNavFoldable = false,\n sideNavHomeLink,\n showFooter = true,\n contentContainerClassName = \"mx-auto max-w-[var(--container-6xl)]\",\n}: PageShellProps) {\n const [drawerOpen, setDrawerOpen] = useState(false);\n\n return (\n <div className=\"flex h-screen flex-col bg-bg-primary\">\n {/* ── Top Navigation ── */}\n <TopNav\n logo={logo}\n logoUrl={logoUrl}\n categories={categories}\n activeCategoryId={activeCategoryId}\n onCategoryChange={onCategoryChange}\n actions={actions}\n onBurgerClick={() => setDrawerOpen(true)}\n />\n\n {/* ── Body: SideNav + Main ── */}\n <div className=\"flex flex-1 overflow-hidden pt-14\">\n {/* Sidebar (desktop only — hidden via CSS on mobile) */}\n <SideNav\n sections={sections}\n activeLinkId={activeLinkId}\n onLinkClick={onLinkClick}\n collapsed={sideNavCollapsed}\n foldable={sideNavFoldable}\n homeLink={sideNavHomeLink}\n />\n\n {/* Main content */}\n <main data-scrollable className=\"flex-1 overflow-y-auto flex flex-col\">\n <div className=\"flex-1 px-md py-lg lg:px-lg\">\n {contentContainerClassName === false ? (\n children\n ) : (\n <div className={contentContainerClassName}>{children}</div>\n )}\n </div>\n {showFooter && <Footer />}\n </main>\n </div>\n\n {/* ── Mobile Drawer ── */}\n <BurgerDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n categories={categories}\n activeCategoryId={activeCategoryId}\n onCategoryChange={(id) => {\n onCategoryChange?.(id);\n setDrawerOpen(false);\n }}\n sections={sections}\n activeLinkId={activeLinkId}\n onLinkClick={(href) => {\n onLinkClick?.(href);\n setDrawerOpen(false);\n }}\n />\n </div>\n );\n}\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface CardProps extends ComponentPropsWithoutRef<\"div\"> {\n /** Optional header content — rendered above the body with a bottom border. */\n header?: ReactNode;\n /** Optional footer content — rendered below the body with a top border. */\n footer?: ReactNode;\n /** Optional actions aligned to the right inside the footer area. */\n actions?: ReactNode;\n /** Remove the default padding on the body section. */\n noPadding?: boolean;\n /** Card body content. */\n children?: ReactNode;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Elevated surface container with optional header, footer, and actions slots.\n *\n * Uses `radius-md` and `shadow-md` from the design tokens.\n */\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n { header, footer, actions, noPadding = false, className = \"\", children, ...rest },\n ref,\n) {\n const hasFooter = footer || actions;\n\n return (\n <div\n ref={ref}\n className={[\n \"rounded-md shadow-md border border-border bg-bg-secondary\",\n \"flex flex-col overflow-hidden\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {/* Header */}\n {header && (\n <div className=\"px-lg py-md border-b border-border\">\n <div className=\"font-semibold text-body text-text-primary\">{header}</div>\n </div>\n )}\n\n {/* Body */}\n <div className={noPadding ? \"\" : \"p-lg\"}>{children}</div>\n\n {/* Footer / Actions */}\n {hasFooter && (\n <div className=\"px-lg py-md border-t border-border flex items-center gap-3\">\n {footer && <div className=\"flex-1 min-w-0\">{footer}</div>}\n {actions && <div className=\"ml-auto flex items-center gap-2\">{actions}</div>}\n </div>\n )}\n </div>\n );\n});\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface DescriptionItem {\n /** Label / key text. */\n label: ReactNode;\n /** Value / description text. */\n value: ReactNode;\n}\n\nexport type DescriptionListLayout = \"vertical\" | \"horizontal\";\n\nexport interface DescriptionListProps extends ComponentPropsWithoutRef<\"dl\"> {\n /** Array of key-value items. */\n items: DescriptionItem[];\n /** Layout direction. @default \"horizontal\" */\n layout?: DescriptionListLayout;\n /** Show a light divider between items. @default true */\n dividers?: boolean;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Key-value pair layout for detail views.\n *\n * Supports horizontal (label left, value right) and vertical (label above value) layouts.\n */\nexport const DescriptionList = forwardRef<HTMLDListElement, DescriptionListProps>(\n function DescriptionList(\n { items, layout = \"horizontal\", dividers = true, className = \"\", ...rest },\n ref,\n ) {\n const isHorizontal = layout === \"horizontal\";\n\n return (\n <dl\n ref={ref}\n className={[\"text-body\", dividers && \"divide-y divide-border\", className]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {items.map((item, idx) => (\n <div\n key={idx}\n className={[\n \"py-sm\",\n isHorizontal\n ? \"flex flex-col sm:flex-row sm:gap-lg\"\n : \"flex flex-col gap-xs\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n <dt\n className={[\n \"font-medium text-text-secondary text-small shrink-0\",\n isHorizontal && \"sm:w-1/3\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {item.label}\n </dt>\n <dd className=\"text-text-primary\">{item.value}</dd>\n </div>\n ))}\n </dl>\n );\n },\n);\n","import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface EmptyStateProps extends ComponentPropsWithoutRef<\"div\"> {\n /** Optional illustration or icon element. */\n illustration?: ReactNode;\n /** Primary message. */\n title: string;\n /** Optional secondary description. */\n description?: string;\n /** Optional call-to-action element (e.g. a Button). */\n action?: ReactNode;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Placeholder shown when a view has no content.\n *\n * Provides an illustration slot, message, and optional CTA.\n */\nexport const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(function EmptyState(\n { illustration, title, description, action, className = \"\", ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={[\n \"flex flex-col items-center justify-center text-center py-2xl px-lg\",\n className,\n ]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {/* Illustration / Icon */}\n {illustration && <div className=\"mb-lg text-text-secondary\">{illustration}</div>}\n\n {/* Title */}\n <h3 className=\"text-h3 font-semibold text-text-primary\">{title}</h3>\n\n {/* Description */}\n {description && (\n <p className=\"mt-xs text-body text-text-secondary max-w-[var(--container-md)]\">\n {description}\n </p>\n )}\n\n {/* CTA */}\n {action && <div className=\"mt-lg\">{action}</div>}\n </div>\n );\n});\n","import {\n type ComponentPropsWithoutRef,\n type ReactNode,\n forwardRef,\n useCallback,\n useMemo,\n useState,\n} from \"react\";\nimport {\n Sliders,\n SortAlphaDown,\n SortAlphaDownAlt,\n SortDown,\n SortNumericDown,\n SortNumericDownAlt,\n} from \"react-bootstrap-icons\";\nimport { Checkbox } from \"../ui/Checkbox\";\nimport { Popover } from \"../ui/Popover\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type SortDirection = \"asc\" | \"desc\";\n\nexport interface SortState {\n /** Column key currently sorted. */\n column: string;\n /** Sort direction. */\n direction: SortDirection;\n}\n\nexport interface TableColumn<T> {\n /** Unique key used for sorting & React keying. */\n key: string;\n /** Column header label. */\n header: ReactNode;\n /** Render the cell content for a given row. */\n cell: (row: T, index: number) => ReactNode;\n /** Enable sorting on this column. @default false */\n sortable?: boolean;\n /**\n * Extract a sortable value from a row.\n * Required for built-in (uncontrolled) sorting.\n * The return type is also used to auto-detect the sort icon style.\n */\n sortValue?: (row: T) => string | number | null | undefined;\n /** Optional header cell className. */\n headerClassName?: string;\n /** Optional body cell className. */\n cellClassName?: string;\n /**\n * Whether this column can be hidden via the column-visibility popover.\n * @default true (when `columnVisibility` is enabled on the table)\n */\n hideable?: boolean;\n}\n\nexport interface TableProps<T> extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** Column definitions. */\n columns: TableColumn<T>[];\n /** Row data array. */\n data: T[];\n /** Unique key extractor for each row. */\n rowKey: (row: T, index: number) => string | number;\n /** Show striped rows. @default true */\n striped?: boolean;\n /** Controlled sort state. */\n sort?: SortState;\n /** Called when a sortable column header is clicked. */\n onSortChange?: (sort: SortState) => void;\n /** Content shown when data is empty. */\n emptyContent?: ReactNode;\n /** Make the table header row sticky when scrolling vertically. @default false */\n stickyHeader?: boolean;\n\n /* ── Toolbar ────────────────────────────────── */\n\n /**\n * Content rendered on the left side of the toolbar row (e.g. title, counters).\n * The toolbar row is shown when `toolbar` or `columnVisibility` is set.\n */\n toolbar?: ReactNode;\n\n /* ── Column visibility ─────────────────────── */\n\n /** Show a toolbar with a column-visibility toggle popover. @default false */\n columnVisibility?: boolean;\n /**\n * localStorage key for persisting hidden-column state.\n * When set, hidden columns are saved to / restored from localStorage.\n */\n columnVisibilityKey?: string;\n /** Controlled hidden-column keys. */\n hiddenColumns?: string[];\n /** Callback when hidden columns change (controlled mode). */\n onHiddenColumnsChange?: (hiddenColumns: string[]) => void;\n /** Initially hidden column keys (uncontrolled mode, ignored when `columnVisibilityKey` restores saved state). */\n defaultHiddenColumns?: string[];\n\n /* ── Row selection ─────────────────────────── */\n\n /**\n * Key of the column whose values identify each row for selection.\n * Setting this enables the selection checkbox column.\n */\n selectionKey?: keyof T & string;\n /** Controlled selected values (values of the `selectionKey` column). */\n selectedValues?: unknown[];\n /** Called when the set of selected values changes. */\n onSelectionChange?: (selectedValues: unknown[]) => void;\n}\n\n/* ── Sort icon helper ─────────────────────────────────────── */\n\ntype SortColumnType = \"text\" | \"numeric\";\n\nfunction detectColumnType<T>(column: TableColumn<T>, data: T[]): SortColumnType {\n if (!column.sortValue || data.length === 0) return \"text\";\n for (const row of data) {\n const v = column.sortValue(row);\n if (v != null) return typeof v === \"number\" ? \"numeric\" : \"text\";\n }\n return \"text\";\n}\n\nfunction SortIcon({\n active,\n direction,\n columnType,\n}: {\n active: boolean;\n direction?: SortDirection;\n columnType: SortColumnType;\n}) {\n const size = 14;\n if (!active) {\n return <SortDown size={size} className=\"ml-1 inline opacity-30\" aria-hidden />;\n }\n if (columnType === \"numeric\") {\n return direction === \"asc\" ? (\n <SortNumericDown size={size} className=\"ml-1 inline\" aria-hidden />\n ) : (\n <SortNumericDownAlt size={size} className=\"ml-1 inline\" aria-hidden />\n );\n }\n return direction === \"asc\" ? (\n <SortAlphaDown size={size} className=\"ml-1 inline\" aria-hidden />\n ) : (\n <SortAlphaDownAlt size={size} className=\"ml-1 inline\" aria-hidden />\n );\n}\n\n/* ── Column-visibility toolbar ────────────────────────────── */\n\nfunction ColumnVisibilityPopover<T>({\n columns,\n hiddenSet,\n onToggle,\n}: {\n columns: TableColumn<T>[];\n hiddenSet: Set<string>;\n onToggle: (key: string) => void;\n}) {\n const hideableColumns = columns.filter((c) => c.hideable !== false);\n const visibleCount = hideableColumns.filter((c) => !hiddenSet.has(c.key)).length;\n\n return (\n <Popover\n placement=\"bottom-end\"\n trigger={\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-small font-medium text-text-secondary hover:text-text-primary hover:bg-bg-secondary border border-border transition-colors\"\n aria-label=\"Toggle column visibility\"\n >\n <Sliders size={14} aria-hidden />\n Columns\n </button>\n }\n className=\"min-w-48\"\n >\n <div className=\"flex flex-col gap-1\">\n <p className=\"text-caption font-semibold text-text-secondary mb-1\">\n Visible columns\n </p>\n {hideableColumns.map((col) => {\n const isVisible = !hiddenSet.has(col.key);\n // Prevent hiding the last visible column\n const isLastVisible = isVisible && visibleCount <= 1;\n return (\n <Checkbox\n key={col.key}\n label={col.header}\n checked={isVisible}\n disabled={isLastVisible}\n onChange={() => onToggle(col.key)}\n />\n );\n })}\n </div>\n </Popover>\n );\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\n/**\n * Data table with sortable headers, striped rows, column visibility toggling,\n * row selection, and a responsive scroll wrapper.\n *\n * Accepts generic row type `T` for full type safety.\n */\nexport const Table = forwardRef<HTMLDivElement, TableProps<unknown>>(function Table(\n {\n columns,\n data,\n rowKey,\n striped = false,\n sort,\n onSortChange,\n emptyContent,\n stickyHeader = false,\n toolbar,\n columnVisibility = false,\n columnVisibilityKey,\n hiddenColumns: controlledHidden,\n onHiddenColumnsChange,\n defaultHiddenColumns,\n selectionKey,\n selectedValues: controlledSelected,\n onSelectionChange,\n className = \"\",\n ...rest\n },\n ref,\n) {\n /* ── Sort state ───────────────────────────── */\n const [internalSort, setInternalSort] = useState<SortState | undefined>();\n const activeSort = sort ?? internalSort;\n\n const handleSort = useCallback(\n (columnKey: string) => {\n const next: SortState =\n activeSort?.column === columnKey && activeSort.direction === \"asc\"\n ? { column: columnKey, direction: \"desc\" }\n : { column: columnKey, direction: \"asc\" };\n\n if (onSortChange) {\n onSortChange(next);\n } else {\n setInternalSort(next);\n }\n },\n [activeSort, onSortChange],\n );\n\n /* ── Column visibility state ──────────────── */\n const [internalHidden, setInternalHidden] = useState<Set<string>>(() => {\n if (columnVisibilityKey) {\n try {\n const stored = localStorage.getItem(columnVisibilityKey);\n if (stored) return new Set(JSON.parse(stored) as string[]);\n } catch { /* ignore malformed data */ }\n }\n return new Set(defaultHiddenColumns ?? []);\n });\n const hiddenSet = useMemo(\n () => (controlledHidden ? new Set(controlledHidden) : internalHidden),\n [controlledHidden, internalHidden],\n );\n\n const handleColumnToggle = useCallback(\n (key: string) => {\n const next = new Set(hiddenSet);\n if (next.has(key)) {\n next.delete(key);\n } else {\n next.add(key);\n }\n const arr = Array.from(next);\n if (columnVisibilityKey) {\n try { localStorage.setItem(columnVisibilityKey, JSON.stringify(arr)); } catch { /* quota exceeded */ }\n }\n if (onHiddenColumnsChange) {\n onHiddenColumnsChange(arr);\n } else {\n setInternalHidden(next);\n }\n },\n [hiddenSet, onHiddenColumnsChange, columnVisibilityKey],\n );\n\n /* ── Selection state ──────────────────────── */\n const [internalSelected, setInternalSelected] = useState<Set<unknown>>(() => new Set());\n const selectedSet = useMemo(\n () => (controlledSelected ? new Set(controlledSelected) : internalSelected),\n [controlledSelected, internalSelected],\n );\n\n const fireSelectionChange = useCallback(\n (next: Set<unknown>) => {\n const arr = Array.from(next);\n if (onSelectionChange) {\n onSelectionChange(arr);\n } else {\n setInternalSelected(next);\n }\n },\n [onSelectionChange],\n );\n\n const toggleRow = useCallback(\n (value: unknown) => {\n const next = new Set(selectedSet);\n if (next.has(value)) {\n next.delete(value);\n } else {\n next.add(value);\n }\n fireSelectionChange(next);\n },\n [selectedSet, fireSelectionChange],\n );\n\n const toggleAll = useCallback(\n (allValues: unknown[]) => {\n const allSelected = allValues.length > 0 && allValues.every((v) => selectedSet.has(v));\n fireSelectionChange(allSelected ? new Set() : new Set(allValues));\n },\n [selectedSet, fireSelectionChange],\n );\n\n /* ── Visible columns ──────────────────────── */\n const visibleColumns = useMemo(\n () =>\n columnVisibility ? columns.filter((c) => !hiddenSet.has(c.key)) : columns,\n [columns, hiddenSet, columnVisibility],\n );\n\n /* ── Sort data in uncontrolled mode ────────── */\n const sortedData = useMemo(() => {\n if (!activeSort || onSortChange) return data;\n const col = columns.find((c) => c.key === activeSort.column);\n if (!col?.sortValue) return data;\n const getValue = col.sortValue;\n return [...data].sort((a, b) => {\n const aVal = getValue(a);\n const bVal = getValue(b);\n if (aVal == null && bVal == null) return 0;\n if (aVal == null) return 1;\n if (bVal == null) return -1;\n const cmp =\n typeof aVal === \"number\" && typeof bVal === \"number\"\n ? aVal - bVal\n : String(aVal).localeCompare(String(bVal));\n return activeSort.direction === \"asc\" ? cmp : -cmp;\n });\n }, [data, activeSort, columns, onSortChange]);\n\n /* ── Selection helpers for current data ────── */\n const allSelectionValues = useMemo(() => {\n if (!selectionKey) return [];\n return sortedData.map((row) => (row as Record<string, unknown>)[selectionKey]);\n }, [sortedData, selectionKey]);\n\n const allSelected = allSelectionValues.length > 0 && allSelectionValues.every((v) => selectedSet.has(v));\n const someSelected = !allSelected && allSelectionValues.some((v) => selectedSet.has(v));\n\n /* Detect column types for icons */\n const columnTypes = useMemo(() => {\n const map = new Map<string, SortColumnType>();\n for (const col of columns) {\n if (col.sortable) {\n map.set(col.key, detectColumnType(col, data));\n }\n }\n return map;\n }, [columns, data]);\n\n const totalColumns = visibleColumns.length + (selectionKey ? 1 : 0);\n const isEmpty = sortedData.length === 0;\n\n return (\n <div\n ref={ref}\n className={[\"overflow-x-auto rounded-md border border-border\", className]\n .filter(Boolean)\n .join(\" \")}\n {...rest}\n >\n {/* ── Toolbar row ──── */}\n {(toolbar || columnVisibility) && (\n <div className=\"flex items-center justify-between gap-md px-md py-sm border-b border-border bg-bg-primary\">\n <div className=\"flex items-center gap-sm min-w-0\">{toolbar}</div>\n {columnVisibility && (\n <ColumnVisibilityPopover\n columns={columns}\n hiddenSet={hiddenSet}\n onToggle={handleColumnToggle}\n />\n )}\n </div>\n )}\n\n <table className=\"w-full text-body text-left\">\n {/* Head */}\n <thead className={stickyHeader ? \"sticky top-0 z-10\" : undefined}>\n <tr className=\"bg-bg-secondary border-b border-border\">\n {/* Selection header */}\n {selectionKey && (\n <th className=\"w-10 px-sm py-sm text-center\">\n <Checkbox\n checked={allSelected}\n indeterminate={someSelected}\n onChange={() => toggleAll(allSelectionValues)}\n aria-label=\"Select all rows\"\n />\n </th>\n )}\n {visibleColumns.map((col) => {\n const isSorted = activeSort?.column === col.key;\n return (\n <th\n key={col.key}\n className={[\n \"px-md py-sm font-semibold text-small text-text-secondary whitespace-nowrap\",\n col.sortable &&\n \"cursor-pointer select-none hover:text-text-primary\",\n col.headerClassName,\n ]\n .filter(Boolean)\n .join(\" \")}\n onClick={col.sortable ? () => handleSort(col.key) : undefined}\n aria-sort={\n isSorted\n ? activeSort!.direction === \"asc\"\n ? \"ascending\"\n : \"descending\"\n : col.sortable\n ? \"none\"\n : undefined\n }\n >\n <span className=\"inline-flex items-center\">\n {col.header}\n {col.sortable && (\n <SortIcon\n active={isSorted}\n direction={\n isSorted ? activeSort!.direction : undefined\n }\n columnType={columnTypes.get(col.key) ?? \"text\"}\n />\n )}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n\n {/* Body */}\n <tbody>\n {isEmpty ? (\n <tr>\n <td\n colSpan={totalColumns}\n className=\"px-md py-xl text-center text-text-secondary\"\n >\n {emptyContent ?? \"No data available.\"}\n </td>\n </tr>\n ) : (\n sortedData.map((row, idx) => {\n const rowVal = selectionKey\n ? (row as Record<string, unknown>)[selectionKey]\n : undefined;\n return (\n <tr\n key={rowKey(row, idx)}\n className={[\n \"border-b border-border last:border-b-0 transition-colors\",\n striped && idx % 2 === 1 && \"bg-bg-tertiary/50\",\n \"hover:bg-bg-tertiary/70\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {/* Selection cell */}\n {selectionKey && (\n <td className=\"w-10 px-sm py-sm text-center\">\n <Checkbox\n checked={selectedSet.has(rowVal)}\n onChange={() => toggleRow(rowVal)}\n aria-label={`Select row ${rowVal}`}\n />\n </td>\n )}\n {visibleColumns.map((col) => (\n <td\n key={col.key}\n className={[\n \"px-md py-sm text-text-primary\",\n col.cellClassName,\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {col.cell(row, idx)}\n </td>\n ))}\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n );\n}) as <T>(props: TableProps<T> & { ref?: React.Ref<HTMLDivElement> }) => React.ReactElement | null;\n","import { useState, type ReactNode } from \"react\";\nimport {\n CheckCircleFill,\n ExclamationTriangleFill,\n InfoCircleFill,\n X,\n XCircleFill,\n} from \"react-bootstrap-icons\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type AlertVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface AlertProps {\n /** Visual variant — controls icon, color, and background. */\n variant: AlertVariant;\n /** Optional bold title above the body. */\n title?: string;\n /** Alert body content. */\n children: ReactNode;\n /** Show a dismiss (×) button. */\n dismissible?: boolean;\n /** Callback fired when the dismiss button is clicked. */\n onDismiss?: () => void;\n /** Additional CSS classes on the root element. */\n className?: string;\n}\n\n/* ── Variant config ───────────────────────────────────────── */\n\nconst variantStyles: Record<\n AlertVariant,\n { bg: string; border: string; text: string; icon: typeof CheckCircleFill }\n> = {\n success: {\n bg: \"bg-success-bg\",\n border: \"border-success/30\",\n text: \"text-success\",\n icon: CheckCircleFill,\n },\n warning: {\n bg: \"bg-warning-bg\",\n border: \"border-warning/30\",\n text: \"text-warning\",\n icon: ExclamationTriangleFill,\n },\n error: {\n bg: \"bg-error-bg\",\n border: \"border-error/30\",\n text: \"text-error\",\n icon: XCircleFill,\n },\n info: {\n bg: \"bg-info-bg\",\n border: \"border-info/30\",\n text: \"text-info\",\n icon: InfoCircleFill,\n },\n};\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Alert({\n variant,\n title,\n children,\n dismissible = false,\n onDismiss,\n className = \"\",\n}: AlertProps) {\n const [visible, setVisible] = useState(true);\n const { bg, border, text, icon: Icon } = variantStyles[variant];\n\n if (!visible) return null;\n\n const handleDismiss = () => {\n setVisible(false);\n onDismiss?.();\n };\n\n return (\n <div\n role=\"alert\"\n className={`flex gap-3 rounded-md border p-md ${bg} ${border} ${className}`}\n >\n {/* Icon */}\n <Icon size={20} className={`shrink-0 mt-0.5 ${text}`} aria-hidden />\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {title && <p className={`font-semibold text-body ${text}`}>{title}</p>}\n <div className=\"text-body text-text-primary\">{children}</div>\n </div>\n\n {/* Dismiss */}\n {dismissible && (\n <button\n type=\"button\"\n onClick={handleDismiss}\n className=\"shrink-0 p-1 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label=\"Dismiss alert\"\n >\n <X size={16} />\n </button>\n )}\n </div>\n );\n}\n","import { useEffect, useRef, type KeyboardEvent, type ReactNode } from \"react\";\nimport { X } from \"react-bootstrap-icons\";\nimport { createPortal } from \"react-dom\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type DialogSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface DialogProps {\n /** Whether the dialog is open. */\n open: boolean;\n /** Callback to close the dialog. */\n onClose: () => void;\n /** Optional dialog title shown in the header. */\n title?: string;\n /** Width preset. Default: \"md\". */\n size?: DialogSize;\n /** Dialog body content. */\n children: ReactNode;\n /** Optional footer (buttons, etc.). */\n footer?: ReactNode;\n /** Additional CSS classes on the panel. */\n className?: string;\n}\n\n/* ── Size map ─────────────────────────────────────────────── */\n\nconst sizeClasses: Record<DialogSize, string> = {\n sm: \"max-w-[24rem]\",\n md: \"max-w-[32rem]\",\n lg: \"max-w-[42rem]\",\n};\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function Dialog({\n open,\n onClose,\n title,\n size = \"md\",\n children,\n footer,\n className = \"\",\n}: DialogProps) {\n const panelRef = useRef<HTMLDivElement>(null);\n\n /* ── Focus trap: focus the panel when opened ── */\n useEffect(() => {\n if (!open) return;\n\n const prevActive = document.activeElement as HTMLElement | null;\n panelRef.current?.focus();\n\n return () => {\n prevActive?.focus();\n };\n }, [open]);\n\n /* ── Lock body scroll while open ── */\n useEffect(() => {\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n /* ── Keyboard: Esc to close, Tab trap ── */\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") {\n onClose();\n return;\n }\n\n // Focus trap\n if (e.key === \"Tab\" && panelRef.current) {\n const focusable = panelRef.current.querySelectorAll<HTMLElement>(\n 'a[href], button:not([disabled]), textarea, input:not([disabled]), select, [tabindex]:not([tabindex=\"-1\"])',\n );\n if (focusable.length === 0) return;\n\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n };\n\n if (!open) return null;\n\n return createPortal(\n <div\n style={{ position: \"fixed\", inset: 0, zIndex: 50, overflowY: \"auto\" }}\n aria-modal=\"true\"\n role=\"dialog\"\n aria-labelledby={title ? \"dialog-title\" : undefined}\n onKeyDown={handleKeyDown}\n >\n {/* Backdrop */}\n <div\n style={{ position: \"fixed\", inset: 0 }}\n className=\"bg-black/50\"\n aria-hidden\n onClick={onClose}\n />\n\n {/* Centering wrapper */}\n <div\n style={{\n display: \"flex\",\n minHeight: \"100%\",\n width: \"100%\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: \"var(--spacing-md, 16px)\",\n boxSizing: \"border-box\",\n }}\n >\n {/* Panel */}\n <div\n ref={panelRef}\n tabIndex={-1}\n className={`relative z-10 flex flex-col w-full ${sizeClasses[size]} rounded-lg border border-border bg-bg-primary shadow-xl outline-none animate-dialog-in ${className}`}\n >\n {/* Header */}\n {title && (\n <div className=\"flex items-center justify-between px-lg py-md border-b border-border\">\n <h2\n id=\"dialog-title\"\n className=\"text-h3 font-semibold text-text-primary\"\n >\n {title}\n </h2>\n <button\n type=\"button\"\n onClick={onClose}\n className=\"shrink-0 p-1 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label=\"Close dialog\"\n >\n <X size={18} />\n </button>\n </div>\n )}\n\n {/* Body */}\n <div className=\"flex-1 overflow-y-auto px-lg py-md text-body text-text-primary\">\n {children}\n </div>\n\n {/* Footer */}\n {footer && (\n <div className=\"flex items-center justify-end gap-sm px-lg py-md border-t border-border\">\n {footer}\n </div>\n )}\n </div>\n </div>\n </div>,\n document.body,\n );\n}\n","import { type ReactNode } from \"react\";\nimport { ExclamationTriangleFill } from \"react-bootstrap-icons\";\nimport { Spinner } from \"../ui/Spinner\";\nimport { Dialog, type DialogSize } from \"./Dialog\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport interface ConfirmDialogProps {\n /** Whether the dialog is open. */\n open: boolean;\n /** Callback to close without confirming. */\n onCancel: () => void;\n /** Callback when the user confirms the action. */\n onConfirm: () => void;\n /** Dialog title. */\n title: string;\n /** Body message / description. */\n children: ReactNode;\n /** Text for the confirm button. Default: \"Confirm\". */\n confirmLabel?: string;\n /** Text for the cancel button. Default: \"Cancel\". */\n cancelLabel?: string;\n /** Danger variant — red confirm button with a warning icon. */\n danger?: boolean;\n /** Show a spinner on the confirm button and disable both buttons. */\n submitting?: boolean;\n /** Width preset. Default: \"sm\". */\n size?: DialogSize;\n}\n\n/* ── Component ────────────────────────────────────────────── */\n\nexport function ConfirmDialog({\n open,\n onCancel,\n onConfirm,\n title,\n children,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n danger = false,\n submitting = false,\n size = \"sm\",\n}: ConfirmDialogProps) {\n return (\n <Dialog open={open} onClose={onCancel} title={title} size={size}>\n {/* Body */}\n <div className=\"flex gap-3\">\n {danger && (\n <ExclamationTriangleFill\n size={22}\n className=\"shrink-0 mt-0.5 text-error\"\n aria-hidden\n />\n )}\n <div className=\"text-body text-text-secondary\">{children}</div>\n </div>\n\n {/* Actions — placed as children, Dialog body handles wrapping */}\n <div className=\"flex items-center justify-end gap-sm mt-lg\">\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={submitting}\n className={[\n \"px-4 py-2 rounded-md text-small font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border transition-colors cursor-pointer\",\n submitting && \"opacity-50 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {cancelLabel}\n </button>\n <button\n type=\"button\"\n onClick={onConfirm}\n disabled={submitting}\n className={[\n \"inline-flex items-center justify-center gap-2 px-4 py-2 rounded-md text-small font-medium text-white transition-colors cursor-pointer\",\n danger\n ? \"bg-error hover:bg-error/80\"\n : \"bg-primary hover:bg-primary-hover\",\n submitting && \"opacity-80 pointer-events-none\",\n ]\n .filter(Boolean)\n .join(\" \")}\n >\n {submitting && <Spinner size=\"sm\" />}\n {confirmLabel}\n </button>\n </div>\n </Dialog>\n );\n}\n","import { createContext, useContext } from \"react\";\n\n/* ── Types ────────────────────────────────────────────────── */\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastData {\n id: string;\n variant: ToastVariant;\n title?: string;\n message: string;\n /** Auto-dismiss duration in ms. 0 = persistent. Default: 5000 */\n duration?: number;\n}\n\nexport type AddToastInput = Omit<ToastData, \"id\">;\n\nexport interface ToastContextValue {\n addToast: (toast: AddToastInput) => string;\n removeToast: (id: string) => void;\n}\n\n/* ── Context ──────────────────────────────────────────────── */\n\nexport const ToastContext = createContext<ToastContextValue | null>(null);\n\nexport function useToast(): ToastContextValue {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used within a <ToastProvider>\");\n return ctx;\n}\n","import { useCallback, useEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n CheckCircleFill,\n ExclamationTriangleFill,\n InfoCircleFill,\n X,\n XCircleFill,\n} from \"react-bootstrap-icons\";\nimport {\n ToastContext,\n type AddToastInput,\n type ToastData,\n type ToastVariant,\n} from \"./toastContext\";\n\n/* ── Variant config ───────────────────────────────────────── */\n\nconst variantConfig: Record<\n ToastVariant,\n { bg: string; accent: string; text: string; icon: typeof CheckCircleFill }\n> = {\n success: {\n bg: \"bg-bg-primary\",\n accent: \"bg-success\",\n text: \"text-success\",\n icon: CheckCircleFill,\n },\n warning: {\n bg: \"bg-bg-primary\",\n accent: \"bg-warning\",\n text: \"text-warning\",\n icon: ExclamationTriangleFill,\n },\n error: {\n bg: \"bg-bg-primary\",\n accent: \"bg-error\",\n text: \"text-error\",\n icon: XCircleFill,\n },\n info: {\n bg: \"bg-bg-primary\",\n accent: \"bg-info\",\n text: \"text-info\",\n icon: InfoCircleFill,\n },\n};\n\n/* ── Single Toast ─────────────────────────────────────────── */\n\ninterface ToastItemProps {\n toast: ToastData;\n onRemove: (id: string) => void;\n}\n\nfunction ToastItem({ toast, onRemove }: ToastItemProps) {\n const { variant, title, message, duration = 5000 } = toast;\n const { bg, accent, text, icon: Icon } = variantConfig[variant];\n const [progress, setProgress] = useState(100);\n const startRef = useRef<number>(0);\n const rafRef = useRef<number>(0);\n\n useEffect(() => {\n if (duration <= 0) return;\n\n startRef.current = performance.now();\n\n const tick = (now: number) => {\n const elapsed = now - startRef.current;\n const remaining = Math.max(0, 100 - (elapsed / duration) * 100);\n setProgress(remaining);\n\n if (remaining <= 0) {\n onRemove(toast.id);\n return;\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(rafRef.current);\n }, [duration, toast.id, onRemove]);\n\n return (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className={`relative overflow-hidden rounded-lg border border-border shadow-lg ${bg} w-80 animate-slide-in`}\n >\n {/* Content row */}\n <div className=\"flex gap-3 p-md\">\n <Icon size={18} className={`shrink-0 mt-0.5 ${text}`} aria-hidden />\n <div className=\"flex-1 min-w-0\">\n {title && <p className=\"font-semibold text-small text-text-primary\">{title}</p>}\n <p className=\"text-small text-text-secondary\">{message}</p>\n </div>\n <button\n type=\"button\"\n onClick={() => onRemove(toast.id)}\n className=\"shrink-0 p-1 rounded-sm text-text-secondary hover:text-text-primary hover:bg-bg-tertiary transition-colors cursor-pointer\"\n aria-label=\"Dismiss notification\"\n >\n <X size={14} />\n </button>\n </div>\n\n {/* Progress bar */}\n {duration > 0 && (\n <div className=\"h-0.5 w-full bg-bg-tertiary\">\n <div\n className={`h-full ${accent} transition-none`}\n style={{ width: `${progress}%` }}\n />\n </div>\n )}\n </div>\n );\n}\n\n/* ── Provider ─────────────────────────────────────────────── */\n\nlet nextId = 0;\n\nexport function ToastProvider({ children }: { children: ReactNode }) {\n const [toasts, setToasts] = useState<ToastData[]>([]);\n\n const addToast = useCallback((input: AddToastInput): string => {\n const id = `toast-${++nextId}`;\n setToasts((prev) => [...prev, { ...input, id }]);\n return id;\n }, []);\n\n const removeToast = useCallback((id: string) => {\n setToasts((prev) => prev.filter((t) => t.id !== id));\n }, []);\n\n return (\n <ToastContext value={{ addToast, removeToast }}>\n {children}\n\n {/* Toast stack — fixed bottom-right */}\n <div\n aria-label=\"Notifications\"\n className=\"fixed bottom-lg right-lg z-50 flex flex-col-reverse gap-sm pointer-events-none\"\n >\n {toasts.map((t) => (\n <div key={t.id} className=\"pointer-events-auto\">\n <ToastItem toast={t} onRemove={removeToast} />\n </div>\n ))}\n </div>\n </ToastContext>\n );\n}\n","import { createContext, useContext, useMemo, useState, type ReactNode } from \"react\";\nimport type { AppRole } from \"../routes/types\";\n\n/* ── User type ────────────────────────────────────────────── */\n\nexport interface AuthUser {\n name: string;\n email: string;\n role: AppRole;\n avatarUrl?: string;\n}\n\n/* ── Context ──────────────────────────────────────────────── */\n\ninterface AuthContextValue {\n user: AuthUser;\n setRole: (role: AppRole) => void;\n}\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\n/* ── Provider ─────────────────────────────────────────────── */\n\nconst DEFAULT_USER: AuthUser = {\n name: \"Alice Martin\",\n email: \"alice.martin@epfl.ch\",\n role: \"admin\",\n};\n\nexport interface AuthProviderProps {\n /** Override initial user (useful for stories). */\n initialUser?: AuthUser;\n children: ReactNode;\n}\n\n/**\n * Mock auth provider — supplies a user with a switchable role.\n *\n * In production this would be replaced by a real auth layer.\n */\nexport function AuthProvider({ initialUser = DEFAULT_USER, children }: AuthProviderProps) {\n const [user, setUser] = useState<AuthUser>(initialUser);\n\n const setRole = (role: AppRole) => setUser((u) => ({ ...u, role }));\n\n const value = useMemo(() => ({ user, setRole }), [user]);\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;\n}\n\n/* ── Hook ─────────────────────────────────────────────────── */\n\n/**\n * Access the current authenticated user and role setter.\n *\n * ```tsx\n * const { user, setRole } = useAuth();\n * ```\n */\nexport function useAuth(): AuthContextValue {\n const ctx = useContext(AuthContext);\n if (!ctx) throw new Error(\"useAuth must be used within an AuthProvider\");\n return ctx;\n}\n"],"mappings":"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"}