fix error line

This commit is contained in:
2026-08-17 17:33:56 +05:30
parent 69b38abced
commit 7fa21a4513
105 changed files with 1296 additions and 323 deletions

View File

@@ -11,6 +11,7 @@ import { StatusBadge } from '@/components/ds/StatusBadge';
* `variant="card"`. Either way the anatomy is fixed — actor, sentence, meta,
* trailing slot — so a feed built from mixed event types stays aligned.
*/
/** @param {any} props */
export function ActivityCard({
/** Person or system that acted. */
actor,

View File

@@ -39,6 +39,7 @@ const ICON_COLORS = {
* instead: an Alert that appears and disappears is easy to miss, and a toast
* that persists is noise.
*/
/** @param {any} props */
export function Alert({ tone = 'info', title, children, icon, onDismiss, action, className, ...props }) {
const Icon = icon || ICONS[tone];

View File

@@ -41,6 +41,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<any>} */
export const Avatar = React.forwardRef(
({ name, src, size = 'default', shape, tone, status, className, ...props }, ref) => {
const [failed, setFailed] = React.useState(false);
@@ -89,6 +90,7 @@ Avatar.displayName = 'Avatar';
* AvatarGroup — overlapping avatars with a "+N" remainder, for showing several
* 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 }) {
const visible = people.slice(0, max);
const overflow = people.length - visible.length;

View File

@@ -37,6 +37,7 @@ export const AXIS_PROPS = {
* A tooltip matching the design system's surfaces. Recharts' default is a plain
* white box that ignores the token set.
*/
/** @param {any} props */
export function ChartTooltip({ active, payload, label, valueFormatter, labelFormatter }) {
if (!active || !payload?.length) return null;
@@ -97,6 +98,7 @@ export function ChartLegend({ items = [], className }) {
* state, and an optional headline figure. Charts inside it are pure Recharts
* children, so swapping a bar chart for a line chart changes nothing else.
*/
/** @param {any} props */
export function ChartContainer({
title,
subtitle,

View File

@@ -23,6 +23,7 @@ import { usePreferences } from '@/lib/krowHooks';
* `hideBelow: 'md'` drops a column on small screens, which is what keeps the
* table usable on mobile without a horizontal scroll for every view.
*/
/** @param {any} props */
export function DataTable({
columns = [],
rows = [],

View File

@@ -27,6 +27,7 @@ const WIDTHS = {
* a detail view can be a Drawer on desktop and a bottom sheet on mobile without
* its contents changing.
*/
/** @param {any} props */
export function Drawer({
open,
onOpenChange,

View File

@@ -14,6 +14,7 @@ import { Button } from '@/components/ui/button';
* Pages that conflate them end up telling a user with 22 candidates that they
* have no candidates.
*/
/** @param {any} props */
export function EmptyState({
icon: Icon,
title,

View File

@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils';
* spacing, required markers and error styling are identical across the app.
* It wires `id`, `aria-describedby` and `aria-invalid` onto its child.
*/
/** @param {any} props */
export function Field({
label,
hint,

View File

@@ -18,24 +18,25 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
* On mobile the filters collapse behind a "Filters" toggle with an active count,
* because four side-by-side selects are unusable at 375px.
*/
/** @param {any} props */
export function FilterBar({
search,
onSearchChange,
search = null,
onSearchChange = null,
searchPlaceholder = 'Search...',
filters = [],
/** `{ [key]: string | string[] }` */
values = {},
onChange,
onChange = null,
/** Right-aligned actions — primary buttons, view switchers. */
actions,
actions = null,
/** Rendered under the bar, e.g. a result count. */
meta,
meta = null,
/** Value that means "no filter" for select-type filters. */
allValue = 'all',
className,
className = '',
}) {
const [expanded, setExpanded] = React.useState(false);

View File

@@ -9,6 +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<any>} */
export const IconButton = React.forwardRef(
({ icon: Icon, label, tooltip = true, size = 'default', side = 'top', ...props }, ref) => {
const sizeMap = {

View File

@@ -45,18 +45,19 @@ const SEVERITY = {
},
};
/** @param {any} props */
export function InsightRow({
severity = 'info',
title,
detail,
/** Short right-aligned figure — a count, an age, a percentage. */
metric,
metricLabel,
metric = null,
metricLabel = null,
/** Entity the item concerns, e.g. a position or candidate name. */
entity,
onClick,
entity = null,
onClick = null,
actionLabel = 'Review',
className,
className = '',
}) {
const s = SEVERITY[severity] || SEVERITY.info;
const Comp = onClick ? 'button' : 'div';
@@ -105,6 +106,7 @@ export function InsightRow({
}
/** The list container — dividers between rows, nothing else. */
export function InsightList({ children, className }) {
/** @param {any} props */
export function InsightList({ children, className = '' }) {
return <div className={cn('divide-y divide-border', className)}>{children}</div>;
}

View File

@@ -21,6 +21,7 @@ const TONES = {
* (a target, a breakdown, a sparkline) use `MetricCard`; keeping the two
* separate stops KPI rows from drifting into inconsistent heights.
*/
/** @param {any} props */
export function KpiCard({
icon: Icon,
value,
@@ -94,6 +95,7 @@ const formatDelta = (delta, suffix = '%') =>
* `invertTrend` exists because "up" is not always good — time-to-hire rising is
* bad, and colouring it green would be actively misleading.
*/
/** @param {any} props */
export function MetricCard({
label,
value,

View File

@@ -18,6 +18,7 @@ const GAPS = { sm: 'gap-3', default: 'gap-4', lg: 'gap-6' };
* `cols` is the count at the largest breakpoint; the intermediate steps are
* 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 }) {
const columns = {
1: 'grid-cols-1',
@@ -36,6 +37,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 }) {
const spacing = {
xs: 'space-y-2',
@@ -56,6 +58,7 @@ export function Stack({ space = 'default', className, children, ...props }) {
* PageSection — a titled region of a page. Pairs a SectionHeader-style label
* with its content at consistent spacing.
*/
/** @param {any} props */
export function PageSection({ title, subtitle, actions, level = 'overline', className, children }) {
return (
<section className={cn('space-y-3', className)}>

View File

@@ -5,7 +5,8 @@ import { cn } from '@/lib/utils';
/**
* Spinner — the only spinner in the system.
*/
export function Spinner({ size = 'default', className, label = 'Loading' }) {
/** @param {any} props */
export function Spinner({ size = 'default', className = '', label = 'Loading' }) {
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 (
<Loader2
@@ -23,7 +24,8 @@ export function Spinner({ size = 'default', className, label = 'Loading' }) {
* Prefer `Skeleton*` when the shape *is* known: a skeleton that matches the
* final layout prevents the content jump a spinner always causes.
*/
export function LoadingState({ message, size = 'default', className }) {
/** @param {any} props */
export function LoadingState({ message = '', size = 'default', className = '' }) {
return (
<div
className={cn(
@@ -42,7 +44,8 @@ export function LoadingState({ message, size = 'default', className }) {
* InlineLoading — a spinner and label on one line, for inside buttons rows,
* card headers, or beside a control that is refreshing.
*/
export function InlineLoading({ message = 'Loading…', className }) {
/** @param {any} props */
export function InlineLoading({ message = 'Loading…', className = '' }) {
return (
<span className={cn('inline-flex items-center gap-2 text-body-sm text-ink-3', className)}>
<Spinner size="sm" />
@@ -55,7 +58,8 @@ export function InlineLoading({ message = 'Loading…', className }) {
* LoadingOverlay — covers content that is being replaced, keeping layout
* height stable so the page does not jump. Used by DataTable when refetching.
*/
export function LoadingOverlay({ show, message, className }) {
/** @param {any} props */
export function LoadingOverlay({ show = false, message = '', className = '' }) {
if (!show) return null;
return (
<div

View File

@@ -57,6 +57,7 @@ function Delta({ delta, label, invert }) {
);
}
/** @param {any} props */
export function MetricStrip({ items = [], columns, className, loading = false }) {
const count = columns ?? Math.min(items.length, 6);

View File

@@ -24,6 +24,8 @@ const WIDTHS = {
* body scroll pushes the title and the confirm button off-screen on a laptop;
* this keeps both pinned and scrolls only the middle.
*/
/** @param {any} props */
/** @param {any} props */
export function Modal({
open,
onOpenChange,
@@ -82,6 +84,7 @@ export function Modal({
* always for, and it labels the action with the verb ("Delete") rather than
* "OK", so the button says what will happen.
*/
/** @param {any} props */
export function ConfirmModal({
open,
onOpenChange,
@@ -91,7 +94,7 @@ export function ConfirmModal({
cancelLabel = 'Cancel',
onConfirm,
tone = 'destructive',
icon,
icon = null,
busy = false,
}) {
return (

View File

@@ -12,6 +12,7 @@ import { SearchInput } from '@/components/ds/SearchInput';
* options a filter field appears, so short lists stay uncluttered and long ones
* stay usable. Options may be strings or `{ value, label, count }`.
*/
/** @param {any} props */
export function MultiSelect({
options = [],
value = [],
@@ -20,8 +21,8 @@ export function MultiSelect({
/** Chips shown before collapsing into "+N more". */
maxChips = 2,
searchThreshold = 8,
disabled,
className,
disabled = false,
className = '',
align = 'start',
emptyMessage = 'No options',
}) {

View File

@@ -7,6 +7,7 @@ import { cn } from '@/lib/utils';
* right-aligned action slot. Owning the entrance animation here is what keeps
* page transitions feeling like one product.
*/
/** @param {any} props */
export function PageHeader({
title,
subtitle,

View File

@@ -9,6 +9,12 @@ import { Button } from '@/components/ui/button';
* Always keeps the first page, the last page, and a window around the current
* one, so the control's width stays stable as the user pages through.
*/
/**
* @param {number} current
* @param {number} total
* @param {number} [siblings]
* @returns {(number | string)[]}
*/
export function buildPageRange(current, total, siblings = 1) {
const totalSlots = siblings * 2 + 5;
if (total <= totalSlots) return Array.from({ length: total }, (_, i) => i + 1);
@@ -30,6 +36,7 @@ export function buildPageRange(current, total, siblings = 1) {
* Pagination — page navigation with an optional result summary and page-size
* control. Renders nothing when there is only one page.
*/
/** @param {any} props */
export function Pagination({
page = 1,
pageCount = 1,

View File

@@ -34,6 +34,7 @@ export function toneForScore(score) {
* `tone="score"` picks the colour from the value, which is what most usages in
* this app want.
*/
/** @param {any} props */
export function ProgressBar({
value,
max = 100,
@@ -94,6 +95,7 @@ export function ProgressBar({
* score, via `min`/`max`, so the KROW ID gauge and a completion ring are the
* same component.
*/
/** @param {any} props */
export function ProgressRing({
value,
min = 0,
@@ -175,7 +177,8 @@ export function ProgressRing({
* Completed steps show a check rather than their number, so "done" is readable
* at a glance without relying on colour.
*/
export function ProgressSteps({ steps = [], current = 0, className, onStepClick }) {
/** @param {any} props */
export function ProgressSteps({ steps = [], current = 0, className = '', onStepClick = null }) {
const normalized = steps.map((s) => (typeof s === 'string' ? { label: s } : s));
return (
@@ -239,6 +242,7 @@ export function ProgressSteps({ steps = [], current = 0, className, onStepClick
* ScoreMeter — a labelled bar row used in score breakdowns, where many
* dimensions are listed with a weight and a value.
*/
/** @param {any} props */
export function ScoreMeter({ label, value, weight, tone = 'score', className }) {
return (
<div className={cn('space-y-1.5', className)}>

View File

@@ -10,6 +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<any>} */
export const SearchInput = React.forwardRef(
(
{

View File

@@ -10,6 +10,7 @@ import { cn } from '@/lib/utils';
* - `card` — a card's own title row
* - `section` — a page-level section title
*/
/** @param {any} props */
export function SectionHeader({
title,
subtitle,

View File

@@ -9,7 +9,8 @@ import { cn } from '@/lib/utils';
* progress. The shimmer is disabled automatically under
* `prefers-reduced-motion` (see index.css).
*/
export function Skeleton({ className, radius = 'md', ...props }) {
/** @param {any} props */
export function Skeleton({ className = '', radius = 'md', ...props }) {
return (
<div
aria-hidden="true"
@@ -31,7 +32,8 @@ export function Skeleton({ className, radius = 'md', ...props }) {
*/
/** Paragraph placeholder. The last line is short, as real text is. */
export function SkeletonText({ lines = 3, className }) {
/** @param {any} props */
export function SkeletonText({ lines = 3, className = '' }) {
return (
<div className={cn('space-y-2', className)} role="status" aria-label="Loading content">
{Array.from({ length: lines }).map((_, i) => (
@@ -45,7 +47,8 @@ export function SkeletonText({ lines = 3, className }) {
}
/** Matches KpiCard: icon tile, big number, caption. */
export function SkeletonKpi({ className }) {
/** @param {any} props */
export function SkeletonKpi({ className = '' }) {
return (
<div className={cn('glass-card border border-white/60 rounded-2xl p-4 shadow-sm', className)}>
<Skeleton className="w-10 h-10 rounded-xl mb-3" />
@@ -56,7 +59,8 @@ export function SkeletonKpi({ className }) {
}
/** Matches a list row: avatar, two lines of text, trailing value. */
export function SkeletonRow({ className }) {
/** @param {any} props */
export function SkeletonRow({ className = '' }) {
return (
<div className={cn('flex items-center gap-3 py-3', className)}>
<Skeleton className="w-9 h-9" radius="full" />
@@ -70,7 +74,8 @@ export function SkeletonRow({ className }) {
}
/** Matches a card in a grid: title, meta, stat row. */
export function SkeletonCard({ className }) {
/** @param {any} props */
export function SkeletonCard({ className = '' }) {
return (
<div className={cn('glass-card border border-white/60 rounded-2xl p-5 shadow-sm', className)}>
<div className="flex items-center gap-3 mb-4">
@@ -94,7 +99,8 @@ export function SkeletonCard({ className }) {
}
/** Matches DataTable: header row plus n body rows. */
export function SkeletonTable({ rows = 5, columns = 4, className }) {
/** @param {any} props */
export function SkeletonTable({ rows = 5, columns = 4, className = '' }) {
return (
<div className={cn('overflow-hidden', className)} role="status" aria-label="Loading table">
<div className="flex items-center gap-4 px-4 py-3 border-b border-border bg-surface-subtle">
@@ -114,7 +120,8 @@ export function SkeletonTable({ rows = 5, columns = 4, className }) {
}
/** Matches ChartContainer: title, legend, plot area. */
export function SkeletonChart({ className, height = 240 }) {
/** @param {any} props */
export function SkeletonChart({ className = '', height = 240 }) {
return (
<div className={cn('glass-card border border-white/60 rounded-2xl p-6 shadow-sm', className)}>
<Skeleton className="h-3.5 w-40 mb-2" />
@@ -129,7 +136,8 @@ export function SkeletonChart({ className, height = 240 }) {
}
/** n SkeletonRows inside a surface — the common "loading a list" case. */
export function SkeletonList({ rows = 5, className }) {
/** @param {any} props */
export function SkeletonList({ rows = 5, className = '' }) {
return (
<div className={cn('divide-y divide-border', className)} role="status" aria-label="Loading list">
{Array.from({ length: rows }).map((_, i) => (

View File

@@ -77,6 +77,7 @@ const DOT_COLOR = {
* `dot` adds a leading indicator for dense rows where colour alone is doing too
* much work; it also gives colour-blind users a second cue alongside the label.
*/
/** @param {any} props */
export function StatusBadge({ status, label, tone, dot = false, size = 'default', className, ...props }) {
const entry = STATUS_MAP[status] || {};
const resolvedTone = tone || entry.tone || 'neutral';

View File

@@ -55,6 +55,7 @@ const surfaceVariants = cva(
}
);
/** @type {React.ForwardRefExoticComponent<any>} */
const Surface = React.forwardRef(
({ className, variant, elevation, radius, padding, interactive, as, ...props }, ref) => {
const Comp = as || 'div';

View File

@@ -15,6 +15,7 @@ import { Badge } from '@/components/ui/badge';
* unique when two tab sets are on screen at once, otherwise the indicator
* animates between them.
*/
/** @param {any} props */
export function Tabs({
tabs = [],
value,

View File

@@ -20,7 +20,8 @@ const TONE_DOT = {
* The connecting rail is drawn per item rather than as one absolute line, so a
* timeline can be nested inside a scrolling card without the rail detaching.
*/
export function Timeline({ items = [], className, compact = false }) {
/** @param {any} props */
export function Timeline({ items = [], className = '', compact = false }) {
return (
<ol className={cn('relative', className)}>
{items.map((item, i) => {

View File

@@ -9,6 +9,7 @@ import { Switch } from '@/components/ui/switch';
* always used with, and makes the whole row a label so the hit target is the
* full width rather than the 36px control.
*/
/** @param {any} props */
export function Toggle({
checked,
onCheckedChange,
@@ -97,8 +98,15 @@ const SEGMENT_VARIANTS = {
},
};
/** @param {any} props */
export function SegmentedToggle({
options = [], value, onChange, size = 'default', variant = 'brand', className, ariaLabel,
options = [],
value = null,
onChange = null,
size = 'default',
variant = 'brand',
className = '',
ariaLabel = '',
}) {
const normalized = options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
const tone = SEGMENT_VARIANTS[variant] || SEGMENT_VARIANTS.brand;

View File

@@ -28,7 +28,7 @@ const BASE_STYLE = {
const DURATIONS = { success: 2600, error: 4200, info: 3000, loading: Infinity };
const withDefaults = (options = {}) => ({
position: 'bottom-center',
position: /** @type {import('react-hot-toast').ToastPosition} */ ('bottom-center'),
...options,
style: { ...BASE_STYLE, ...(options.style || {}) },
});