From d44003621137c3a5c24c87cc0c8f7eeca4ca6a26 Mon Sep 17 00:00:00 2001 From: Aravind Date: Thu, 17 Sep 2026 23:59:49 +0530 Subject: [PATCH] chore(ts-migration): migrate UI primitives and design system to TypeScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 6. 57 files: 23 vendored shadcn primitives, 30 design-system components, 4 charts. Plus `src/components/ds/props.ts`, which is types only. Renaming these alone took typecheck from 37 to 1008, and the reason is worth recording because it is the shape of every remaining phase. These components had NO prop contract. No PropTypes, no validation: in the JavaScript every prop was optional and every extra prop was spread onto the underlying element. TypeScript infers a destructured parameter WITHOUT a default as REQUIRED, so the moment the files became `.tsx` it invented a rule the components never had and rejected several hundred call sites that have always worked. That is the compiler describing its own inference, not a defect it found. Three mechanical fixes, each restoring a contract that already existed: - 57 JSDoc `@type {React.ForwardRefExoticComponent}` annotations become real TypeScript annotations. Those comments were the previous authors' deliberate compatibility types; JSDoc stops applying in a `.tsx` file, so converting them preserves an intent that was already written down. - 61 `React.forwardRef(...)` calls gain ``. Without generics `ref` infers `ForwardedRef`, which no element's `Ref` accepts - so every primitive that forwards a ref to a `div` failed on the ref, not the props. - 78 component signatures take `DsProps`, a documented alias for `Record`. It exists so the decision is recorded once and is greppable when someone tightens it, rather than being 78 bare `any`s with no explanation between them. The prop NAMES are not lost: every component still destructures them by name, which is where a reader looks. Four files needed real types rather than compatibility ones. `ds/toast` takes react-hot-toast's own `ToastOptions`, which narrows `position` to its `ToastPosition` union instead of widening to `string` - the widening was what made all six calls unassignable. `ds/Pagination`'s page range is genuinely `(number | string)[]`, because it interleaves page numbers with '…' markers that the renderer tests for. `ds/Field` narrows `children.props` at three reads, and `ds/Avatar` needed the ref generic. Two of my own automated passes were wrong and were caught rather than shipped. A props-interface generator dropped alternating props, because non-overlapping regex matches consume the separating comma - it made things worse (83 file errors to 146) and was reverted wholesale. A second pass missed every multi-line signature whose defaults contain a `)`, such as `onClose = () => {}`; that needed a brace matcher rather than a character class. 56 of 57 files emit byte-identical JavaScript. The one exception is `ds/toast`, where a JSDoc type CAST - `/** @type {ToastPosition} */ ('bottom-center')` - became a real annotation, so the emitted output loses a comment and a pair of now-redundant parentheses. The value is `"bottom-center"` either way; the minified outputs differ only in esbuild's choice of mangled local names. Verified: tsc 37 -> 35, set-difference showing zero introduced and two removed; zero errors remain in any Phase 6 file; npm test 1684/1691 with the same seven failures; lint 0 errors; build succeeds with the API origin inlined; baseline artifacts untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8 --- ...formance.jsx => DepartmentPerformance.tsx} | 3 ++- .../charts/{HiringFlow.jsx => HiringFlow.tsx} | 3 ++- ...ingTrendChart.jsx => HiringTrendChart.tsx} | 3 ++- .../{muiChartTheme.js => muiChartTheme.ts} | 0 .../ds/{ActivityCard.jsx => ActivityCard.tsx} | 3 ++- src/components/ds/{Alert.jsx => Alert.tsx} | 3 ++- src/components/ds/{Avatar.jsx => Avatar.tsx} | 6 ++--- ...{ChartContainer.jsx => ChartContainer.tsx} | 7 ++--- .../ds/{DataTable.jsx => DataTable.tsx} | 3 ++- src/components/ds/{Drawer.jsx => Drawer.tsx} | 3 ++- .../ds/{EmptyState.jsx => EmptyState.tsx} | 3 ++- src/components/ds/{Field.jsx => Field.tsx} | 15 +++++++---- .../ds/{FilterBar.jsx => FilterBar.tsx} | 3 ++- .../ds/{IconButton.jsx => IconButton.tsx} | 3 +-- .../ds/{InsightRow.jsx => InsightRow.tsx} | 5 ++-- .../ds/{KpiCard.jsx => KpiCard.tsx} | 5 ++-- src/components/ds/{Layout.jsx => Layout.tsx} | 7 ++--- .../ds/{LoadingState.jsx => LoadingState.tsx} | 9 ++++--- .../ds/{MetricStrip.jsx => MetricStrip.tsx} | 3 ++- src/components/ds/{Modal.jsx => Modal.tsx} | 5 ++-- .../ds/{MultiSelect.jsx => MultiSelect.tsx} | 3 ++- .../ds/{PageHeader.jsx => PageHeader.tsx} | 5 ++-- .../ds/{Pagination.jsx => Pagination.tsx} | 8 ++++-- .../ds/{Progress.jsx => Progress.tsx} | 9 ++++--- .../ds/{SearchInput.jsx => SearchInput.tsx} | 3 +-- .../{SectionHeader.jsx => SectionHeader.tsx} | 3 ++- .../ds/{Skeleton.jsx => Skeleton.tsx} | 17 ++++++------ .../ds/{StatusBadge.jsx => StatusBadge.tsx} | 3 ++- .../ds/{Surface.jsx => Surface.tsx} | 3 +-- src/components/ds/{Tabs.jsx => Tabs.tsx} | 3 ++- .../ds/{Timeline.jsx => Timeline.tsx} | 3 ++- src/components/ds/{Toggle.jsx => Toggle.tsx} | 5 ++-- src/components/ds/{index.js => index.ts} | 0 src/components/ds/props.ts | 22 +++++++++++++++ src/components/ds/{toast.js => toast.ts} | 9 +++++-- src/components/ui/{badge.jsx => badge.tsx} | 3 +-- .../ui/{breadcrumb.jsx => breadcrumb.tsx} | 20 ++++++-------- src/components/ui/{button.jsx => button.tsx} | 3 +-- src/components/ui/{card.jsx => card.tsx} | 18 +++++-------- .../ui/{checkbox.jsx => checkbox.tsx} | 3 +-- .../ui/{collapsible.jsx => collapsible.ts} | 0 src/components/ui/{dialog.jsx => dialog.tsx} | 17 +++++------- .../{dropdown-menu.jsx => dropdown-menu.tsx} | 27 +++++++------------ .../ui/{hover-card.jsx => hover-card.tsx} | 3 +-- src/components/ui/{image.jsx => image.tsx} | 9 +++---- src/components/ui/{input.jsx => input.tsx} | 3 +-- src/components/ui/{label.jsx => label.tsx} | 3 +-- .../ui/{popover.jsx => popover.tsx} | 3 +-- .../ui/{radio-group.jsx => radio-group.tsx} | 6 ++--- src/components/ui/{select.jsx => select.tsx} | 20 +++++--------- src/components/ui/{sheet.jsx => sheet.tsx} | 17 +++++------- src/components/ui/{slider.jsx => slider.tsx} | 3 +-- src/components/ui/{switch.jsx => switch.tsx} | 3 +-- .../ui/{textarea.jsx => textarea.tsx} | 3 +-- src/components/ui/{toast.jsx => toast.tsx} | 21 +++++---------- .../ui/{toaster.jsx => toaster.tsx} | 0 .../ui/{tooltip.jsx => tooltip.tsx} | 3 +-- .../ui/{use-toast.jsx => use-toast.ts} | 0 58 files changed, 191 insertions(+), 185 deletions(-) rename src/components/charts/{DepartmentPerformance.jsx => DepartmentPerformance.tsx} (99%) rename src/components/charts/{HiringFlow.jsx => HiringFlow.tsx} (98%) rename src/components/charts/{HiringTrendChart.jsx => HiringTrendChart.tsx} (96%) rename src/components/charts/{muiChartTheme.js => muiChartTheme.ts} (100%) rename src/components/ds/{ActivityCard.jsx => ActivityCard.tsx} (98%) rename src/components/ds/{Alert.jsx => Alert.tsx} (96%) rename src/components/ds/{Avatar.jsx => Avatar.tsx} (96%) rename src/components/ds/{ChartContainer.jsx => ChartContainer.tsx} (97%) rename src/components/ds/{DataTable.jsx => DataTable.tsx} (99%) rename src/components/ds/{Drawer.jsx => Drawer.tsx} (98%) rename src/components/ds/{EmptyState.jsx => EmptyState.tsx} (97%) rename src/components/ds/{Field.jsx => Field.tsx} (75%) rename src/components/ds/{FilterBar.jsx => FilterBar.tsx} (99%) rename src/components/ds/{IconButton.jsx => IconButton.tsx} (92%) rename src/components/ds/{InsightRow.jsx => InsightRow.tsx} (96%) rename src/components/ds/{KpiCard.jsx => KpiCard.tsx} (98%) rename src/components/ds/{Layout.jsx => Layout.tsx} (94%) rename src/components/ds/{LoadingState.jsx => LoadingState.tsx} (94%) rename src/components/ds/{MetricStrip.jsx => MetricStrip.tsx} (98%) rename src/components/ds/{Modal.jsx => Modal.tsx} (98%) rename src/components/ds/{MultiSelect.jsx => MultiSelect.tsx} (99%) rename src/components/ds/{PageHeader.jsx => PageHeader.tsx} (97%) rename src/components/ds/{Pagination.jsx => Pagination.tsx} (92%) rename src/components/ds/{Progress.jsx => Progress.tsx} (98%) rename src/components/ds/{SearchInput.jsx => SearchInput.tsx} (96%) rename src/components/ds/{SectionHeader.jsx => SectionHeader.tsx} (97%) rename src/components/ds/{Skeleton.jsx => Skeleton.tsx} (91%) rename src/components/ds/{StatusBadge.jsx => StatusBadge.tsx} (97%) rename src/components/ds/{Surface.jsx => Surface.tsx} (96%) rename src/components/ds/{Tabs.jsx => Tabs.tsx} (98%) rename src/components/ds/{Timeline.jsx => Timeline.tsx} (98%) rename src/components/ds/{Toggle.jsx => Toggle.tsx} (98%) rename src/components/ds/{index.js => index.ts} (100%) create mode 100644 src/components/ds/props.ts rename src/components/ds/{toast.js => toast.ts} (84%) rename src/components/ui/{badge.jsx => badge.tsx} (92%) rename src/components/ui/{breadcrumb.jsx => breadcrumb.tsx} (74%) rename src/components/ui/{button.jsx => button.tsx} (97%) rename src/components/ui/{card.jsx => card.tsx} (57%) rename src/components/ui/{checkbox.jsx => checkbox.tsx} (86%) rename src/components/ui/{collapsible.jsx => collapsible.ts} (100%) rename src/components/ui/{dialog.jsx => dialog.tsx} (83%) rename src/components/ui/{dropdown-menu.jsx => dropdown-menu.tsx} (82%) rename src/components/ui/{hover-card.jsx => hover-card.tsx} (85%) rename src/components/ui/{image.jsx => image.tsx} (96%) rename src/components/ui/{input.jsx => input.tsx} (91%) rename src/components/ui/{label.jsx => label.tsx} (79%) rename src/components/ui/{popover.jsx => popover.tsx} (86%) rename src/components/ui/{radio-group.jsx => radio-group.tsx} (80%) rename src/components/ui/{select.jsx => select.tsx} (83%) rename src/components/ui/{sheet.jsx => sheet.tsx} (84%) rename src/components/ui/{slider.jsx => slider.tsx} (87%) rename src/components/ui/{switch.jsx => switch.tsx} (89%) rename src/components/ui/{textarea.jsx => textarea.tsx} (80%) rename src/components/ui/{toast.jsx => toast.tsx} (79%) rename src/components/ui/{toaster.jsx => toaster.tsx} (100%) rename src/components/ui/{tooltip.jsx => tooltip.tsx} (87%) rename src/components/ui/{use-toast.jsx => use-toast.ts} (100%) 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 (