diff --git a/src/components/charts/DepartmentPerformance.jsx b/src/components/charts/DepartmentPerformance.tsx similarity index 99% rename from src/components/charts/DepartmentPerformance.jsx rename to src/components/charts/DepartmentPerformance.tsx index d54d3b4..1b24dd4 100644 --- a/src/components/charts/DepartmentPerformance.jsx +++ b/src/components/charts/DepartmentPerformance.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from '@/components/ds/props'; import React, { useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { @@ -104,7 +105,7 @@ function getDeptColor(index, avgScore) { } /** @param {any} props */ -export function DepartmentPerformance({ items = [], className = '' }) { +export function DepartmentPerformance({ items = [], className = '' }: DsProps) { const [viewMode, setViewMode] = useState('flowchart'); // 'flowchart' | 'table' const [activeDept, setActiveDept] = useState(null); diff --git a/src/components/charts/HiringFlow.jsx b/src/components/charts/HiringFlow.tsx similarity index 98% rename from src/components/charts/HiringFlow.jsx rename to src/components/charts/HiringFlow.tsx index 56b05f2..1b313d4 100644 --- a/src/components/charts/HiringFlow.jsx +++ b/src/components/charts/HiringFlow.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from '@/components/ds/props'; import React from 'react'; import { BarChart } from '@mui/x-charts'; import { ChevronRight } from 'lucide-react'; @@ -19,7 +20,7 @@ import { chartSx, TONES, tooltipSx } from './muiChartTheme'; * component derives no figures of its own. */ /** @param {any} props */ -export function HiringFlow({ stages = [], transitions = [], weakestKey, className = '' }) { +export function HiringFlow({ stages = [], transitions = [], weakestKey, className = '' }: DsProps) { if (!stages.length) return null; const total = stages[0]?.count || 1; diff --git a/src/components/charts/HiringTrendChart.jsx b/src/components/charts/HiringTrendChart.tsx similarity index 96% rename from src/components/charts/HiringTrendChart.jsx rename to src/components/charts/HiringTrendChart.tsx index 0e8ab55..31fc801 100644 --- a/src/components/charts/HiringTrendChart.jsx +++ b/src/components/charts/HiringTrendChart.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from '@/components/ds/props'; import React from 'react'; import { LineChart } from '@mui/x-charts'; import { cn } from '@/lib/utils'; @@ -15,7 +16,7 @@ import { chartSx, TONES, tooltipSx } from './muiChartTheme'; * `points: [{ label, hires, cumulative }]` — supplied by the page. */ /** @param {any} props */ -export function HiringTrendChart({ points = [], emptyState, className = '' }) { +export function HiringTrendChart({ points = [], emptyState, className = '' }: DsProps) { const enough = points.length >= 2; return ( diff --git a/src/components/charts/muiChartTheme.js b/src/components/charts/muiChartTheme.ts similarity index 100% rename from src/components/charts/muiChartTheme.js rename to src/components/charts/muiChartTheme.ts diff --git a/src/components/ds/ActivityCard.jsx b/src/components/ds/ActivityCard.tsx similarity index 98% rename from src/components/ds/ActivityCard.jsx rename to src/components/ds/ActivityCard.tsx index eb3ef88..2c8a3fb 100644 --- a/src/components/ds/ActivityCard.jsx +++ b/src/components/ds/ActivityCard.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { cn } from '@/lib/utils'; import { Avatar } from '@/components/ds/Avatar'; @@ -30,7 +31,7 @@ export function ActivityCard({ variant = 'row', onClick, className, -}) { +}: DsProps) { const iconTones = { soft: 'bg-krow-blue-tint text-krow-blue', brand: 'bg-krow-blue text-white', diff --git a/src/components/ds/Alert.jsx b/src/components/ds/Alert.tsx similarity index 96% rename from src/components/ds/Alert.jsx rename to src/components/ds/Alert.tsx index e251778..c9ba1b7 100644 --- a/src/components/ds/Alert.jsx +++ b/src/components/ds/Alert.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { AlertCircle, CheckCircle2, Info, TriangleAlert, X } from 'lucide-react'; import { cva } from 'class-variance-authority'; @@ -40,7 +41,7 @@ const ICON_COLORS = { * that persists is noise. */ /** @param {any} props */ -export function Alert({ tone = 'info', title, children, icon, onDismiss, action, className, ...props }) { +export function Alert({ tone = 'info', title, children, icon, onDismiss, action, className, ...props }: DsProps) { const Icon = icon || ICONS[tone]; return ( diff --git a/src/components/ds/Avatar.jsx b/src/components/ds/Avatar.tsx similarity index 96% rename from src/components/ds/Avatar.jsx rename to src/components/ds/Avatar.tsx index 8eae976..ea5e73b 100644 --- a/src/components/ds/Avatar.jsx +++ b/src/components/ds/Avatar.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { cva } from 'class-variance-authority'; import { cn } from '@/lib/utils'; @@ -41,8 +42,7 @@ export function initialsFrom(name = '') { * first-class state rather than a fallback afterthought. A failed image load * degrades to initials instead of a broken-image icon. */ -/** @type {React.ForwardRefExoticComponent} */ -export const Avatar = React.forwardRef( +export const Avatar: React.ForwardRefExoticComponent = React.forwardRef( ({ name, src, size = 'default', shape, tone, status, className, ...props }, ref) => { const [failed, setFailed] = React.useState(false); const showImage = src && !failed; @@ -91,7 +91,7 @@ Avatar.displayName = 'Avatar'; * people in a tight row (applicants on a position, endorsers on a skill). */ /** @param {any} props */ -export function AvatarGroup({ people = [], max = 4, size = 'sm', className }) { +export function AvatarGroup({ people = [], max = 4, size = 'sm', className }: DsProps) { const visible = people.slice(0, max); const overflow = people.length - visible.length; diff --git a/src/components/ds/ChartContainer.jsx b/src/components/ds/ChartContainer.tsx similarity index 97% rename from src/components/ds/ChartContainer.jsx rename to src/components/ds/ChartContainer.tsx index 444150b..10321d7 100644 --- a/src/components/ds/ChartContainer.jsx +++ b/src/components/ds/ChartContainer.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { ResponsiveContainer } from 'recharts'; import { cn } from '@/lib/utils'; @@ -38,7 +39,7 @@ export const AXIS_PROPS = { * white box that ignores the token set. */ /** @param {any} props */ -export function ChartTooltip({ active, payload, label, valueFormatter, labelFormatter }) { +export function ChartTooltip({ active, payload, label, valueFormatter, labelFormatter }: DsProps) { if (!active || !payload?.length) return null; return ( @@ -71,7 +72,7 @@ export function ChartTooltip({ active, payload, label, valueFormatter, labelForm * A legend that reads as part of the card rather than as chart furniture. * Items: `{ label, color, value? }`. */ -export function ChartLegend({ items = [], className }) { +export function ChartLegend({ items = [], className }: DsProps) { return (
{items.map((item) => ( @@ -118,7 +119,7 @@ export function ChartContainer({ raw = false, footer, className, -}) { +}: DsProps) { if (loading) return ; return ( diff --git a/src/components/ds/DataTable.jsx b/src/components/ds/DataTable.tsx similarity index 99% rename from src/components/ds/DataTable.jsx rename to src/components/ds/DataTable.tsx index 200bb4d..59a65b3 100644 --- a/src/components/ds/DataTable.jsx +++ b/src/components/ds/DataTable.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { ArrowDown, ArrowUp, ChevronsUpDown, Inbox } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -60,7 +61,7 @@ export function DataTable({ className, rowClassName, stickyHeader = false, -}) { +}: DsProps) { /* ── Sorting ─────────────────────────────────────────────────────────── */ /* Row height follows the account's density preference. Only the vertical padding changes — same columns, same type, same behaviour. */ diff --git a/src/components/ds/Drawer.jsx b/src/components/ds/Drawer.tsx similarity index 98% rename from src/components/ds/Drawer.jsx rename to src/components/ds/Drawer.tsx index ec6f883..60a4cb6 100644 --- a/src/components/ds/Drawer.jsx +++ b/src/components/ds/Drawer.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { cn } from '@/lib/utils'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'; @@ -41,7 +42,7 @@ export function Drawer({ className, /** Hides the header for a fully custom panel (the mobile nav does this). */ bare = false, -}) { +}: DsProps) { const isVertical = side === 'top' || side === 'bottom'; return ( diff --git a/src/components/ds/EmptyState.jsx b/src/components/ds/EmptyState.tsx similarity index 97% rename from src/components/ds/EmptyState.jsx rename to src/components/ds/EmptyState.tsx index 136a51f..f6dd5bd 100644 --- a/src/components/ds/EmptyState.jsx +++ b/src/components/ds/EmptyState.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; @@ -25,7 +26,7 @@ export function EmptyState({ variant = 'empty', size = 'default', className, -}) { +}: DsProps) { const renderAction = (spec, buttonVariant) => { if (!spec) return null; if (React.isValidElement(spec)) return spec; diff --git a/src/components/ds/Field.jsx b/src/components/ds/Field.tsx similarity index 75% rename from src/components/ds/Field.jsx rename to src/components/ds/Field.tsx index dbbf3f4..bba1761 100644 --- a/src/components/ds/Field.jsx +++ b/src/components/ds/Field.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { cn } from '@/lib/utils'; @@ -19,7 +20,7 @@ export function Field({ inline = false, className, children, -}) { +}: DsProps) { const reactId = React.useId(); const id = htmlFor || reactId; const hintId = hint ? `${id}-hint` : undefined; @@ -27,11 +28,15 @@ export function Field({ const describedBy = [errorId, hintId].filter(Boolean).join(' ') || undefined; // Only clone when the child is a single element that can accept the wiring. + /* `isValidElement` narrows to `ReactElement`, whose `props` is + `unknown` — so reading `children.props.id` to preserve a caller's own id + does not compile. The cast says what the guard has already established and + what the runtime relies on: this is an element with props. */ const control = React.isValidElement(children) - ? React.cloneElement(children, { - id: children.props.id || id, - 'aria-describedby': children.props['aria-describedby'] || describedBy, - 'aria-invalid': error ? true : children.props['aria-invalid'], + ? React.cloneElement(children as React.ReactElement>, { + id: (children.props as Record).id || id, + 'aria-describedby': (children.props as Record)['aria-describedby'] || describedBy, + 'aria-invalid': error ? true : (children.props as Record)['aria-invalid'], }) : children; diff --git a/src/components/ds/FilterBar.jsx b/src/components/ds/FilterBar.tsx similarity index 99% rename from src/components/ds/FilterBar.jsx rename to src/components/ds/FilterBar.tsx index d16a26c..440c24d 100644 --- a/src/components/ds/FilterBar.jsx +++ b/src/components/ds/FilterBar.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { SlidersHorizontal, X } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -37,7 +38,7 @@ export function FilterBar({ /** Value that means "no filter" for select-type filters. */ allValue = 'all', className = '', -}) { +}: DsProps) { const [expanded, setExpanded] = React.useState(false); const isActive = (filter) => { diff --git a/src/components/ds/IconButton.jsx b/src/components/ds/IconButton.tsx similarity index 92% rename from src/components/ds/IconButton.jsx rename to src/components/ds/IconButton.tsx index 8410cdb..00e1a8d 100644 --- a/src/components/ds/IconButton.jsx +++ b/src/components/ds/IconButton.tsx @@ -9,8 +9,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp * `label`: an icon-only control with no accessible name is a bug, so the API * makes it impossible to omit. The label doubles as the tooltip. */ -/** @type {React.ForwardRefExoticComponent} */ -export const IconButton = React.forwardRef( +export const IconButton: React.ForwardRefExoticComponent = React.forwardRef( ({ icon: Icon, label, tooltip = true, size = 'default', side = 'top', ...props }, ref) => { const sizeMap = { xs: 'icon-xs', diff --git a/src/components/ds/InsightRow.jsx b/src/components/ds/InsightRow.tsx similarity index 96% rename from src/components/ds/InsightRow.jsx rename to src/components/ds/InsightRow.tsx index 37d15c3..bdd26c0 100644 --- a/src/components/ds/InsightRow.jsx +++ b/src/components/ds/InsightRow.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { AlertTriangle, ChevronRight, Info, ShieldAlert, TrendingDown } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -58,7 +59,7 @@ export function InsightRow({ onClick = null, actionLabel = 'Review', className = '', -}) { +}: DsProps) { const s = SEVERITY[severity] || SEVERITY.info; const Comp = onClick ? 'button' : 'div'; @@ -107,6 +108,6 @@ export function InsightRow({ /** The list container — dividers between rows, nothing else. */ /** @param {any} props */ -export function InsightList({ children, className = '' }) { +export function InsightList({ children, className = '' }: DsProps) { return
{children}
; } diff --git a/src/components/ds/KpiCard.jsx b/src/components/ds/KpiCard.tsx similarity index 98% rename from src/components/ds/KpiCard.jsx rename to src/components/ds/KpiCard.tsx index eca5794..477eb6a 100644 --- a/src/components/ds/KpiCard.jsx +++ b/src/components/ds/KpiCard.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { motion } from 'framer-motion'; import { ArrowDownRight, ArrowUpRight, Minus } from 'lucide-react'; @@ -32,7 +33,7 @@ export function KpiCard({ loading = false, onClick, className, -}) { +}: DsProps) { const t = TONES[tone] || TONES.brand; if (loading) { @@ -112,7 +113,7 @@ export function MetricCard({ loading = false, onClick, className, -}) { +}: DsProps) { const t = TONES[tone] || TONES.neutral; if (loading) { diff --git a/src/components/ds/Layout.jsx b/src/components/ds/Layout.tsx similarity index 94% rename from src/components/ds/Layout.jsx rename to src/components/ds/Layout.tsx index fcc9f5f..c2912c7 100644 --- a/src/components/ds/Layout.jsx +++ b/src/components/ds/Layout.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { cn } from '@/lib/utils'; @@ -19,7 +20,7 @@ const GAPS = { sm: 'gap-3', default: 'gap-4', lg: 'gap-6' }; * chosen to avoid orphan cards (6 → 2/3/6 rather than 1/3/6). */ /** @param {any} props */ -export function Grid({ cols = 3, gap = 'default', className, children, ...props }) { +export function Grid({ cols = 3, gap = 'default', className, children, ...props }: DsProps) { const columns = { 1: 'grid-cols-1', 2: 'grid-cols-1 sm:grid-cols-2', @@ -38,7 +39,7 @@ export function Grid({ cols = 3, gap = 'default', className, children, ...props /** Stack — vertical rhythm. `space` maps to the two spacings pages should use. */ /** @param {any} props */ -export function Stack({ space = 'default', className, children, ...props }) { +export function Stack({ space = 'default', className, children, ...props }: DsProps) { const spacing = { xs: 'space-y-2', sm: 'space-y-3', @@ -59,7 +60,7 @@ export function Stack({ space = 'default', className, children, ...props }) { * with its content at consistent spacing. */ /** @param {any} props */ -export function PageSection({ title, subtitle, actions, level = 'overline', className, children }) { +export function PageSection({ title, subtitle, actions, level = 'overline', className, children }: DsProps) { return (
{title && ( diff --git a/src/components/ds/LoadingState.jsx b/src/components/ds/LoadingState.tsx similarity index 94% rename from src/components/ds/LoadingState.jsx rename to src/components/ds/LoadingState.tsx index bdde408..2404d17 100644 --- a/src/components/ds/LoadingState.jsx +++ b/src/components/ds/LoadingState.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -6,7 +7,7 @@ import { cn } from '@/lib/utils'; * Spinner — the only spinner in the system. */ /** @param {any} props */ -export function Spinner({ size = 'default', className = '', label = 'Loading' }) { +export function Spinner({ size = 'default', className = '', label = 'Loading' }: DsProps) { const sizes = { xs: 'w-3.5 h-3.5', sm: 'w-4 h-4', default: 'w-6 h-6', lg: 'w-8 h-8' }; return ( @@ -59,7 +60,7 @@ export function InlineLoading({ message = 'Loading…', className = '' }) { * height stable so the page does not jump. Used by DataTable when refetching. */ /** @param {any} props */ -export function LoadingOverlay({ show = false, message = '', className = '' }) { +export function LoadingOverlay({ show = false, message = '', className = '' }: DsProps) { if (!show) return null; return (
2; const showRightEllipsis = right < total - 1; - const pages = [1]; + /* Page numbers with '…' markers between them — a real mixed array, which is + why the renderer tests `entry === '…'` before treating it as a number. + Inference took the seed `[1]` as `number[]` and rejected the markers. */ + const pages: (number | string)[] = [1]; if (showLeftEllipsis) pages.push('…'); for (let p = Math.max(2, left); p <= Math.min(total - 1, right); p += 1) pages.push(p); if (showRightEllipsis) pages.push('…'); @@ -47,7 +51,7 @@ export function Pagination({ onPageSizeChange, pageSizeOptions = [10, 25, 50, 100], className, -}) { +}: DsProps) { if (pageCount <= 1 && !onPageSizeChange) return null; const from = totalItems != null && pageSize ? (page - 1) * pageSize + 1 : null; diff --git a/src/components/ds/Progress.jsx b/src/components/ds/Progress.tsx similarity index 98% rename from src/components/ds/Progress.jsx rename to src/components/ds/Progress.tsx index efa4f1f..a7cadf3 100644 --- a/src/components/ds/Progress.jsx +++ b/src/components/ds/Progress.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { Check } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -45,7 +46,7 @@ export function ProgressBar({ valueSuffix = '', className, trackClassName, -}) { +}: DsProps) { const pct = clamp((Number(value) / max) * 100); const resolvedTone = tone === 'score' ? toneForScore(pct) : tone; @@ -110,7 +111,7 @@ export function ProgressRing({ /** Small text below the label. */ footnote, className, -}) { +}: DsProps) { const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const ratio = Math.max(0, Math.min(1, (Number(value) - min) / (max - min))); @@ -178,7 +179,7 @@ export function ProgressRing({ * at a glance without relying on colour. */ /** @param {any} props */ -export function ProgressSteps({ steps = [], current = 0, className = '', onStepClick = null }) { +export function ProgressSteps({ steps = [], current = 0, className = '', onStepClick = null }: DsProps) { const normalized = steps.map((s) => (typeof s === 'string' ? { label: s } : s)); return ( @@ -243,7 +244,7 @@ export function ProgressSteps({ steps = [], current = 0, className = '', onStepC * dimensions are listed with a weight and a value. */ /** @param {any} props */ -export function ScoreMeter({ label, value, weight, tone = 'score', className }) { +export function ScoreMeter({ label, value, weight, tone = 'score', className }: DsProps) { return (
diff --git a/src/components/ds/SearchInput.jsx b/src/components/ds/SearchInput.tsx similarity index 96% rename from src/components/ds/SearchInput.jsx rename to src/components/ds/SearchInput.tsx index e35d025..ef79c87 100644 --- a/src/components/ds/SearchInput.jsx +++ b/src/components/ds/SearchInput.tsx @@ -10,8 +10,7 @@ import { inputVariants } from '@/components/ui/input'; * without re-running expensive work, while the field itself stays instantly * responsive. Pass `debounce={0}` for immediate updates. */ -/** @type {React.ForwardRefExoticComponent} */ -export const SearchInput = React.forwardRef( +export const SearchInput: React.ForwardRefExoticComponent = React.forwardRef( ( { value, diff --git a/src/components/ds/SectionHeader.jsx b/src/components/ds/SectionHeader.tsx similarity index 97% rename from src/components/ds/SectionHeader.jsx rename to src/components/ds/SectionHeader.tsx index 2fb256b..5327e3b 100644 --- a/src/components/ds/SectionHeader.jsx +++ b/src/components/ds/SectionHeader.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { ChevronRight } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -21,7 +22,7 @@ export function SectionHeader({ onViewAll, viewAllLabel = 'View all', className, -}) { +}: DsProps) { if (level === 'overline') { return (
diff --git a/src/components/ds/Skeleton.jsx b/src/components/ds/Skeleton.tsx similarity index 91% rename from src/components/ds/Skeleton.jsx rename to src/components/ds/Skeleton.tsx index 22040c1..777baa6 100644 --- a/src/components/ds/Skeleton.jsx +++ b/src/components/ds/Skeleton.tsx @@ -1,3 +1,4 @@ +import type { DsProps } from './props'; import * as React from 'react'; import { cn } from '@/lib/utils'; @@ -10,7 +11,7 @@ import { cn } from '@/lib/utils'; * `prefers-reduced-motion` (see index.css). */ /** @param {any} props */ -export function Skeleton({ className = '', radius = 'md', ...props }) { +export function Skeleton({ className = '', radius = 'md', ...props }: DsProps) { return (