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

@@ -37,7 +37,9 @@ export function ThinkingIndicator() {
* with tables and KPI tiles, and a chat bubble would waste a third of a 380px
* column on padding around them.
*/
export const Message = React.memo(({ role, text, blocks, streaming, stopped, onPrompt }) => {
export const Message = React.memo(
/** @param {any} props */
({ role, text, blocks, streaming, stopped, onPrompt }) => {
if (role === 'user') {
return (
<div className="flex justify-end">

View File

@@ -15,7 +15,8 @@ import { useAssistantPanel } from './AssistantPanelContext';
* It self-hides on pages with no assistant, so a page can render it
* unconditionally and never needs to know the placement rules.
*/
export function AssistantTrigger({ className, size = 'default' }) {
/** @param {any} props */
export function AssistantTrigger({ className = '', size = 'default' }) {
const { supported, isOpen, toggle, context } = useAssistantPanel();
if (!supported) return null;

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import {
ArrowLeft, History, Maximize2, Minimize2, PanelRightClose, RotateCcw, Sparkles, Trash2,
ArrowLeft, History, Maximize2, Minimize2, PanelRightClose, RotateCcw, Trash2,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Surface } from '@/components/ds/Surface';
@@ -13,7 +13,6 @@ import {
usePreferences, useRoleCategories,
} from '@/lib/krowHooks';
import { ROLE_CATEGORIES } from '@/lib/roleCategories';
import { AddSkillDialog } from '@/components/skills/AddSkillDialog';
import { runAction } from '@/lib/skills/actions';
import { skillsForContext } from '@/lib/skills/registry';
import { owliverSuggestions } from '@/lib/skills/owliverResolver';
@@ -286,7 +285,6 @@ export default function KrowAssistant({
activity: facts.activity || [],
trainingPaths,
}), [pageContext, facts, assignments, trainingPaths]);
const [addSkillOpen, setAddSkillOpen] = React.useState(false);
/**
* Which face of the panel the body is showing.
@@ -500,12 +498,33 @@ export default function KrowAssistant({
const ready = declared.filter((c) => !c.deferred);
const asking = declared.filter((c) => c.deferred);
/**
* One chip per *intent*, not per wording.
*
* De-duplicating on the label alone let two chips through whenever the same
* answer was worded twice — a skill's "Show hiring activity" and its
* "Summarize hiring activity" both resolved to that skill's `summary`
* capability, so the reader was offered the same reading under two names and
* had no way to tell them apart. What a chip *resolves to* is the thing that
* must be unique: a skill capability, the page capability, or, for a chip
* that is neither, the question it sends. The label is compared too, so two
* differently-routed chips still cannot arrive reading identically.
*/
const seen = new Set();
const intentOf = (chip) => {
if (chip?.skillId && chip?.skillCapability) return `skill:${chip.skillId}:${chip.skillCapability}`;
if (chip?.capability) return `page:${chip.capability}`;
return `ask:${String(chip?.prompt ?? '').trim().toLowerCase()}`;
};
return [...ready, ...skillPrompts, ...buildPrompts(context.id, facts, workforce), ...asking]
.filter((chip) => {
const key = String(chip?.label ?? '').trim().toLowerCase();
if (!key || seen.has(key)) return false;
seen.add(key);
const label = String(chip?.label ?? '').trim().toLowerCase();
if (!label) return false;
const intent = intentOf(chip);
if (seen.has(intent) || seen.has(`label:${label}`)) return false;
seen.add(intent);
seen.add(`label:${label}`);
return true;
});
/* `pageContext` decides which suggestions can answer without asking, so the
@@ -603,17 +622,16 @@ export default function KrowAssistant({
<p className="truncate text-caption leading-tight text-ink-3">{context.page}</p>
</div>
{/* Window controls */}
{/* Window controls.
Authoring a skill is deliberately not among them. Skills are a
registry with a lifecycle — authored, enabled, edited, removed — and
Workspace → Skills is where that lifecycle lives. A second entry
point here meant the panel you *use* Owliver from was also a place
you *configured* it from, and the two lists could be reached from
different places with different affordances. The panel does the
first job only; the registry behind it is unchanged. */}
<div className="flex items-center gap-0.5">
{/* Opens the same dialog Settings does; there is one registry behind
both, so a skill added here is the skill added there. */}
<IconButton
icon={Sparkles}
label="Add an Owliver skill"
variant="ghost"
size="sm"
onClick={() => setAddSkillOpen(true)}
/>
{/* History lives with the other window controls rather than in the
body, so the layout of the panel is unchanged whether or not
there is anything to show. It toggles: pressing it again returns
@@ -729,8 +747,6 @@ export default function KrowAssistant({
)}
</div>
<AddSkillDialog open={addSkillOpen} onOpenChange={setAddSkillOpen} />
{/* ── Composer: fixed to the bottom in both states ───────────────── */}
<div className="shrink-0 space-y-2 border-t border-white/60 px-3 pb-2.5 pt-2.5">
{/* All suggestions on the landing screen, where they teach what can be

View File

@@ -15,9 +15,16 @@ import { cn } from '@/lib/utils';
* `max` caps how many are offered in a conversation, where vertical space belongs
* to the thread rather than to suggestions.
*
* A chip is shrink-to-fit up to the panel's width, so a long suggestion spends the
* whole row before it is allowed to take a second line. When it does take one, the
* radius stays at the single-line pill's 14px rather than following the height:
* `rounded-full` on a two-line chip resolves to half of 46px, and that corner is
* what turns the same component into something that reads as a card.
*
* Arrow keys move between chips, so the whole set is one tab stop.
*/
export function PromptChips({ prompts = [], onSelect, max, align = 'center', className }) {
/** @param {any} props */
export function PromptChips({ prompts = [], onSelect, max = 0, align = 'center', className = '' }) {
const chipRefs = React.useRef([]);
const visible = max ? prompts.slice(0, max) : prompts;
@@ -54,15 +61,21 @@ export function PromptChips({ prompts = [], onSelect, max, align = 'center', cla
onKeyDown={(e) => onKeyDown(e, i)}
title={prompt.prompt}
className={cn(
'rounded-full border border-border bg-surface px-2.5 py-1',
'text-caption font-medium text-ink-2 shadow-xs',
/* 14px is the radius `rounded-full` already produces on a one-line
chip, pinned so a wrapped chip keeps the same corner. */
'flex max-w-full items-center rounded-[14px] border border-border bg-surface px-2.5 py-1',
'text-caption font-medium text-ink-2 shadow-xs text-left',
'transition-all duration-base ease-out',
'hover:-translate-y-px hover:border-krow-blue/40 hover:bg-krow-blue-tint hover:text-krow-blue hover:shadow-sm',
'active:translate-y-0 active:shadow-xs',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/60 focus-visible:ring-offset-1'
)}
>
{prompt.label}
{/* `pretty` rather than `balance`: the row should be spent before a
second line starts, and balancing would leave the pill wide with
two short lines inside it. Clamped so no suggestion can grow the
chip into a paragraph — the full text stays in `title`. */}
<span className="line-clamp-3 text-pretty">{prompt.label}</span>
</button>
))}
</div>

View File

@@ -17,6 +17,7 @@ import { cn } from '@/lib/utils';
* gain — the suggestion chips already teach the range of what can be asked, and
* they do it without a timer or a rerender every four seconds.
*/
/** @param {any} props */
export function PromptInput({
value,
onChange,
@@ -26,7 +27,7 @@ export function PromptInput({
placeholders = ['Ask anything…'],
autoFocus = false,
align = 'left',
className,
className = '',
}) {
const textareaRef = React.useRef(null);
const [focused, setFocused] = React.useState(false);

View File

@@ -62,14 +62,14 @@ const TONE_BADGE = {
/* ── Individual blocks ──────────────────────────────────────────────────── */
const TextBlock = React.memo(({ block }) => (
const TextBlock = React.memo(/** @param {any} props */ ({ block }) => (
<p className="text-body-sm leading-relaxed text-ink-2">
<Inline value={block.text} />
</p>
));
TextBlock.displayName = 'TextBlock';
const HeadingBlock = React.memo(({ block }) => (
const HeadingBlock = React.memo(/** @param {any} props */ ({ block }) => (
<div className="space-y-0.5">
<h4 className="font-heading text-body-sm font-semibold text-ink-1">{block.text}</h4>
{block.sub && <p className="text-caption text-ink-3">{block.sub}</p>}
@@ -78,7 +78,7 @@ const HeadingBlock = React.memo(({ block }) => (
HeadingBlock.displayName = 'HeadingBlock';
/** Headline figures. Two columns keeps the numbers large in a 380px panel. */
const KpisBlock = React.memo(({ block }) => (
const KpisBlock = React.memo(/** @param {any} props */ ({ block }) => (
<div className="grid grid-cols-2 gap-2 [[data-wide]_&]:grid-cols-3">
{block.items.map((item) => (
<div
@@ -113,7 +113,7 @@ const KpisBlock = React.memo(({ block }) => (
KpisBlock.displayName = 'KpisBlock';
/** Pass/warn checks. The icon carries the state so colour is not the only cue. */
const StatusBlock = React.memo(({ block }) => (
const StatusBlock = React.memo(/** @param {any} props */ ({ block }) => (
<ul className="space-y-1.5">
{block.items.map((item) => (
<li key={item.label} className="flex items-start gap-2">
@@ -145,7 +145,7 @@ const StatusBlock = React.memo(({ block }) => (
));
StatusBlock.displayName = 'StatusBlock';
const MetersBlock = React.memo(({ block }) => (
const MetersBlock = React.memo(/** @param {any} props */ ({ block }) => (
<div className="space-y-2.5">
{block.items.map((item) => (
<div key={item.label} className="space-y-1">
@@ -167,7 +167,7 @@ MetersBlock.displayName = 'MetersBlock';
* A table. Scrolls horizontally inside its own container so a wide comparison
* never widens the panel.
*/
const TableBlock = React.memo(({ block }) => {
const TableBlock = React.memo(/** @param {any} props */ ({ block }) => {
const cell = (value) => {
if (value == null || value === '') return <span className="text-ink-4">—</span>;
if (typeof value === 'object') {
@@ -228,7 +228,7 @@ const TableBlock = React.memo(({ block }) => {
TableBlock.displayName = 'TableBlock';
/** Stage progression, widths relative to the largest stage. */
const FunnelBlock = React.memo(({ block }) => {
const FunnelBlock = React.memo(/** @param {any} props */ ({ block }) => {
const max = Math.max(...block.steps.map((s) => s.count), 1);
return (
@@ -255,7 +255,7 @@ const FunnelBlock = React.memo(({ block }) => {
});
FunnelBlock.displayName = 'FunnelBlock';
const ListBlock = React.memo(({ block }) => {
const ListBlock = React.memo(/** @param {any} props */ ({ block }) => {
const Tag = block.ordered ? 'ol' : 'ul';
return (
<Tag className="space-y-1.5">
@@ -298,7 +298,7 @@ ListBlock.displayName = 'ListBlock';
* clickable and doing nothing. An item may carry a hint *and* an action: the
* hint explains the record, the action opens it.
*/
const InsightsBlock = React.memo(({ block, onPrompt }) => (
const InsightsBlock = React.memo(/** @param {any} props */ ({ block, onPrompt }) => (
<div className="space-y-2">
{block.items.map((item, i) => {
const surface = cn(
@@ -375,7 +375,7 @@ const InsightsBlock = React.memo(({ block, onPrompt }) => (
InsightsBlock.displayName = 'InsightsBlock';
/** Recommended steps — numbered, because order is the recommendation. */
const ActionsBlock = React.memo(({ block }) => (
const ActionsBlock = React.memo(/** @param {any} props */ ({ block }) => (
<ol className="space-y-2">
{block.items.map((item, i) => (
<li key={i} className="flex gap-2.5 rounded-xl border border-border bg-surface px-3 py-2.5 shadow-xs">
@@ -394,7 +394,7 @@ const ActionsBlock = React.memo(({ block }) => (
));
ActionsBlock.displayName = 'ActionsBlock';
const BadgesBlock = React.memo(({ block }) => (
const BadgesBlock = React.memo(/** @param {any} props */ ({ block }) => (
<div className="flex flex-wrap gap-1.5">
{block.items.map((item) => (
<Badge key={item.label} variant={TONE_BADGE[item.tone] || 'soft'} size="lg">
@@ -405,7 +405,7 @@ const BadgesBlock = React.memo(({ block }) => (
));
BadgesBlock.displayName = 'BadgesBlock';
const TimelineBlock = React.memo(({ block }) => (
const TimelineBlock = React.memo(/** @param {any} props */ ({ block }) => (
<ol className="relative space-y-3">
{block.items.map((item, i) => (
<li key={i} className="flex gap-3">
@@ -438,7 +438,7 @@ const TimelineBlock = React.memo(({ block }) => (
));
TimelineBlock.displayName = 'TimelineBlock';
const NoteBlock = React.memo(({ block }) => (
const NoteBlock = React.memo(/** @param {any} props */ ({ block }) => (
<p className="border-l-2 border-border pl-2.5 text-caption italic leading-relaxed text-ink-3">
<Inline value={block.text} />
</p>
@@ -455,7 +455,7 @@ NoteBlock.displayName = 'NoteBlock';
* shape the table does not carry renders nothing rather than something
* improvised.
*/
const SkillSectionBlock = React.memo(({ block }) => {
const SkillSectionBlock = React.memo(/** @param {any} props */ ({ block }) => {
const section = block.section;
const Component = SECTION_COMPONENTS[section?.shape || section?.type];
@@ -515,7 +515,7 @@ const RENDERERS = {
* blocks has consistent rhythm — a heading hugs what follows it, everything
* else breathes.
*/
export const ResponseDocument = React.memo(({ blocks = [], streaming = false, onPrompt }) => (
export const ResponseDocument = React.memo(/** @param {any} props */ ({ blocks = [], streaming = false, onPrompt }) => (
<div className="space-y-3">
{blocks.map((block, i) => {
const Renderer = RENDERERS[block.type];

View File

@@ -459,7 +459,7 @@ const PROMPTS = {
prompt: `Continue the ${drafts[0].title} draft`,
}
: {
label: `${drafts.length} drafts to finish`,
label: `Which ${plural(drafts.length, 'position')} ${verb(drafts.length, 'is', 'are')} still in draft?`,
prompt: 'Which positions are still in draft?',
capability: 'position-drafts',
});
@@ -496,8 +496,9 @@ const PROMPTS = {
if (withStrong.length) {
const best = withStrong.sort((a, b) => b.status.strong.length - a.status.strong.length)[0];
const n = best.status.strong.length;
out.push({
label: `${best.status.strong.length} strong for ${best.position.title}`,
label: `Show the ${plural(n, 'strongest candidate')} for ${best.position.title}`,
prompt: `Who matches ${best.position.title}?`,
});
}
@@ -505,10 +506,16 @@ const PROMPTS = {
out.push({ label: 'Which position needs people first?', prompt: 'Which position needs people first?' });
}
if (newToday) {
out.push({ label: `${newToday} applied today`, prompt: 'Who applied today?' });
out.push({
label: `Which ${plural(newToday, 'candidate')} applied today?`,
prompt: 'Who applied today?',
});
}
if (readyCount) {
out.push({ label: `${readyCount} ready for interview`, prompt: 'Who is ready for interview?' });
out.push({
label: `Which ${plural(readyCount, 'candidate')} ${verb(readyCount, 'is', 'are')} ready for interview?`,
prompt: 'Who is ready for interview?',
});
}
}
@@ -516,13 +523,25 @@ const PROMPTS = {
ones — they are still the right answer to "how is hiring going". */
out.push(
f.starvedPositions.length
? { label: `${f.starvedPositions.length} roles need attention`, prompt: 'What needs attention across my positions?', capability: 'positions-attention' }
? {
label: `Which ${plural(f.starvedPositions.length, 'role')} ${verb(f.starvedPositions.length, 'needs', 'need')} attention?`,
prompt: 'What needs attention across my positions?',
capability: 'positions-attention',
}
: { label: 'What needs attention?', prompt: 'What needs attention across my positions?', capability: 'positions-attention' },
{ label: 'Which position has the strongest pipeline?', prompt: 'Which position has the strongest pipeline?', capability: 'position-strength' },
f.unscreened.length
? { label: `${f.unscreened.length} waiting for review`, prompt: 'Show candidates waiting for review', capability: 'candidates-waiting' }
? {
label: `Show the ${plural(f.unscreened.length, 'application')} waiting for review`,
prompt: 'Show candidates waiting for review',
capability: 'candidates-waiting',
}
: { label: 'Show candidates waiting for review', prompt: 'Show candidates waiting for review', capability: 'candidates-waiting' },
{ label: 'Summarize hiring activity', prompt: 'Summarize hiring activity across my positions', capability: 'hiring-operations' }
/* Named for the whole board. The Hiring Activity skill answers the same
question *for one position*, and the two used to arrive worded
identically — see the intent de-duplication in `KrowAssistant`, which
now also compares what a chip resolves to rather than only its words. */
{ label: 'Summarize hiring activity across all positions', prompt: 'Summarize hiring activity across my positions', capability: 'hiring-operations' }
);
return out;

View File

@@ -42,7 +42,7 @@ export function readHistory() {
return list
.filter((r) => r && r.id && Array.isArray(r.messages) && r.messages.length)
.filter((r) => new Date(r.updatedAt || 0).getTime() >= cutoff)
.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
.sort((a, b) => new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime());
} catch {
/* Corrupt or unavailable storage is an empty history, not an error the
reader has to deal with. */

View File

@@ -60,7 +60,7 @@ export function buildFacts({
/* Time-to-hire — the created→updated span ImpactMetrics uses. */
const timeToHire = hired.length
? Math.round(avg(hired.map((a) =>
Math.max(1, (new Date(a.updated_date) - new Date(a.created_date)) / DAY_MS))))
Math.max(1, (new Date(a.updated_date).getTime() - new Date(a.created_date).getTime()) / DAY_MS))))
: 0;
/* Open roles attracting nobody. */

View File

@@ -103,7 +103,8 @@ function getDeptColor(index, avgScore) {
};
}
export function DepartmentPerformance({ items = [], className }) {
/** @param {any} props */
export function DepartmentPerformance({ items = [], className = '' }) {
const [viewMode, setViewMode] = useState('flowchart'); // 'flowchart' | 'table'
const [activeDept, setActiveDept] = useState(null);

View File

@@ -18,7 +18,8 @@ import { chartSx, TONES, tooltipSx } from './muiChartTheme';
* `stages` and `transitions` come from the page's own fact sheet — this
* component derives no figures of its own.
*/
export function HiringFlow({ stages = [], transitions = [], weakestKey, className }) {
/** @param {any} props */
export function HiringFlow({ stages = [], transitions = [], weakestKey, className = '' }) {
if (!stages.length) return null;
const total = stages[0]?.count || 1;

View File

@@ -14,7 +14,8 @@ import { chartSx, TONES, tooltipSx } from './muiChartTheme';
*
* `points: [{ label, hires, cumulative }]` — supplied by the page.
*/
export function HiringTrendChart({ points = [], emptyState, className }) {
/** @param {any} props */
export function HiringTrendChart({ points = [], emptyState, className = '' }) {
const enough = points.length >= 2;
return (

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 || {}) },
});

View File

@@ -465,7 +465,8 @@ function Choice({ active, onClick, className, children }) {
}
/** A growable list of short text lines — training steps, evaluation criteria. */
function ListBuilder({ label, hint, placeholder, values, onChange, addLabel }) {
/** @param {any} props */
function ListBuilder({ label, hint = '', placeholder, values, onChange, addLabel }) {
const setAt = (i, v) => onChange(values.map((x, j) => (j === i ? v : x)));
const removeAt = (i) => onChange(values.length === 1 ? [''] : values.filter((_, j) => j !== i));

View File

@@ -13,7 +13,8 @@ import { Plus, Sparkles } from 'lucide-react';
* where a worker actually is one; putting it here made the console read as
* somebody's personal training dashboard.
*/
export function ForgeHeader({ onCreate }) {
/** @param {any} props */
export function ForgeHeader({ onCreate = null, _onBrowse = null }) {
return (
<header className="flex flex-col gap-4 border-b border-border/60 pb-6 sm:flex-row sm:items-end sm:justify-between">
<div className="min-w-0 space-y-1.5">

View File

@@ -19,7 +19,8 @@ const TIPS = [
'Keep training concise.',
];
export function ForgeRail({ recent = [], onCreate, onOpen, onAskOwliver }) {
/** @param {any} props */
export function ForgeRail({ recent = [], onCreate, onOpen, onAskOwliver: _onAskOwliver }) {
return (
<aside
className="grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-3 items-stretch"

View File

@@ -168,6 +168,7 @@ export function SkillDetail({ course, open, onOpenChange, usage }) {
}
/** A titled region of the record. */
/** @param {any} props */
function Block({ title, meta, icon: Icon, children }) {
return (
<section className="border-t border-border pt-4 first:border-0 first:pt-0">

View File

@@ -32,7 +32,8 @@ const STATUS_META = {
* of their ladder is the one thing this component exists to make obvious, and a
* stack of rows makes that a reading exercise instead of a glance.
*/
export function SkillLadder({ state, className }) {
/** @param {any} props */
export function SkillLadder({ state, className = '' }) {
return (
<ol className={cn('flex items-start', className)} aria-label={`${state.name} levels`}>
{state.levels.map((rung, i) => {
@@ -87,6 +88,7 @@ export function SkillLadder({ state, className }) {
}
/** One skill: where this person stands on it, and the next thing to do. */
/** @param {any} props */
export function SkillProgressCard({ state, onOpen }) {
const status = STATUS_META[state.status] || STATUS_META.not_started;
const atTop = !state.nextLevel;
@@ -163,6 +165,7 @@ export function SkillProgressCard({ state, onOpen }) {
* hidden: the evidence behind a level is the reason to trust the level, and
* folding it away would leave the label standing on nothing.
*/
/** @param {any} props */
export function SkillProgressDetail({ state, open, onOpenChange, onStartTraining }) {
if (!state) return null;

View File

@@ -95,13 +95,14 @@ export default function AIInterviewModal({ open, onClose, application, job, cond
setMessages(newMessages);
lastUserMessageTime.current = Date.now();
await speak(question);
} catch (e) {
} catch (_e) {
setError('Interview connection issue. Please retry.');
}
}, [jobTitle, speak, language]);
const startListening = useCallback(() => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const win = /** @type {any} */ (window);
const SpeechRecognition = win.SpeechRecognition || win.webkitSpeechRecognition;
if (!SpeechRecognition) {
setError('Voice input not supported in this browser. Use text mode below.');
return;
@@ -181,7 +182,7 @@ export default function AIInterviewModal({ open, onClose, application, job, cond
} });
setEvaluation(result);
setPhase('done');
} catch (e) {
} catch (_e) {
setError('Evaluation failed. Please try again.');
setPhase('active');
}

View File

@@ -20,7 +20,7 @@ const STATUS_STYLES = {
rejected: { badge: 'bg-[#FEE2E2] text-[#991B1B]', label: 'Rejected', card: 'bg-white border-[#E5E7EB]' },
};
function getInitials(name) {
function _getInitials(name) {
return name?.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase() || '??';
}
@@ -67,6 +67,7 @@ function ScoreRing({ score }) {
);
}
/** @param {any} props */
export default function CandidateCard({ application, jobTitle, rank, onAction, onMessage, onCall, onSchedule, onDecline, onDelete, onHire }) {
const navigate = useNavigate();
const position = jobTitle || application.job_title || '';
@@ -116,7 +117,7 @@ export default function CandidateCard({ application, jobTitle, rank, onAction, o
try {
await base44.entities.JobApplication.update(application.id, { status: 'rejected' });
toast.success(`${application.applicant_name} declined`);
} catch (err) {
} catch (_err) {
toast.error('Failed to decline candidate');
}
}
@@ -130,7 +131,7 @@ export default function CandidateCard({ application, jobTitle, rank, onAction, o
try {
await base44.entities.JobApplication.delete(application.id);
toast.success(`${application.applicant_name} deleted`);
} catch (err) {
} catch (_err) {
toast.error('Failed to delete candidate');
}
}

View File

@@ -21,7 +21,8 @@ const METRIC_CONFIG = [
{ key: 'employer_requirements', label: 'Req Match', Icon: Check },
];
function MetricPill({ label, value, Icon }) {
/** @param {any} props */
function _MetricPill({ label, value, Icon }) {
return (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white border border-[#E5E7EB]">
<Icon className="w-3 h-3 text-[#9CA3AF]" strokeWidth={2} />

View File

@@ -2,7 +2,8 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
function Stage({ label, value, sub, accent, to }) {
/** @param {any} props */
function Stage({ label, value, sub = '', accent = false, to = null }) {
const body = (
<div className="text-center sm:text-left">
<div className="text-[11px] font-medium uppercase tracking-wider text-[#94A3B8]">{label}</div>

View File

@@ -332,7 +332,8 @@ export function MatchedCandidates({ job, profiles = [], courses = [], applicatio
* `MatchedCandidates` rather than inside it: two audiences, two components, one
* scoring function that cannot drift between them.
*/
export function YourSkillFit({ job, held, className }) {
/** @param {any} props */
export function YourSkillFit({ job, held, className = '' }) {
const match = matchPosition(job, held);
if (!match) return null;
const tone = TONE[match.tone] || TONE.warning;

View File

@@ -83,6 +83,7 @@ function ScoreFactor({ label, value }) {
);
}
/** @param {any} props */
export default function TalentDetailModal({ open, onClose, profile, onContact, onSchedule, onHire, matchScore, matchReasons }) {
if (!profile) return null;
const badges = profile.earned_badges || [];

View File

@@ -92,9 +92,17 @@ function SkillPath({ state }) {
);
}
/** @param {any} props */
export function VerifiedSkills({
pageId, profile, profiles = [], courses = [], positions = [], email, customSources = [],
onOpenTraining, className,
pageId = '',
profile = null,
profiles = [],
courses = [],
positions = [],
email = '',
customSources = [],
onOpenTraining = null,
className = '',
}) {
/* Accepts a profile or the email of one, so a candidate record — which is an
application, not a worker profile — can be matched to the person behind it. */
@@ -126,7 +134,7 @@ export function VerifiedSkills({
if (!worker) return [];
const byId = new Map(courses.map((c) => [c.id, c]));
return [...(worker.completed_courses || [])]
.sort((a, b) => new Date(b.completed_date) - new Date(a.completed_date))
.sort((a, b) => new Date(b.completed_date).getTime() - new Date(a.completed_date).getTime())
.slice(0, 3)
.map((entry) => {
const course = byId.get(entry.course_id);

View File

@@ -172,7 +172,8 @@ const initialsFor = (name) => {
* hierarchy this card is meant to establish. Values share one ink colour so no
* single signal shouts louder than the rest.
*/
function SupportingSignal({ label, value, suffix, icon: Icon }) {
/** @param {any} props */
function SupportingSignal({ label, value, suffix = '', icon: Icon = null }) {
const shown = value != null && value !== '' ? value : '—';
return (
<div className="flex items-baseline justify-between gap-3">

View File

@@ -96,7 +96,8 @@ function FactorPill({ label, value }) {
);
}
function ReportRow({ icon: Icon, label, value, tone }) {
/** @param {any} props */
function ReportRow({ icon: Icon, label, value, tone = 'neutral' }) {
const valColor = tone === 'verified' ? 'text-[#10B981]' : tone === 'amber' ? 'text-[#D97706]' : 'text-[#0F172A]';
return (
<div className="flex items-center justify-between gap-3 py-2 border-b border-white/60 last:border-0">
@@ -117,7 +118,7 @@ export default function IdentityCareerCard({ profile }) {
const score = getTalentScore(profile);
const band = getScoreBand(score);
const growth = getGrowth(profile);
const _growth = getGrowth(profile);
const esat = getESAT(profile);
const fico = toFICO(score);
const shortId = (profile.id || '').slice(-6).toUpperCase();

View File

@@ -29,7 +29,8 @@ function marketRange(profile = {}) {
return `$${low}-$${low + 4}/hr`;
}
function Field({ label, value, sub, accent }) {
/** @param {any} props */
function Field({ label, value, sub = '', accent = false }) {
return (
<div>
<p className="text-[11px] uppercase tracking-wide text-[#6B7280]">{label}</p>
@@ -68,7 +69,8 @@ function Factor({ label, value, weight }) {
);
}
export default function TalentHero({ profile, jobRecs = [], nextCourse, openOwliver }) {
/** @param {any} props */
export default function TalentHero({ profile, _jobRecs = [], nextCourse = null, openOwliver = null }) {
const score = getTalentScore(profile);
const factors = getFactors(profile);
const first = (profile.full_name || 'there').split(' ')[0];

View File

@@ -160,7 +160,21 @@ function SkillSection({ skill, section, context }) {
);
}
export function SkillSurface({ page, placement, context = null, className }) {
/** @param {any} props */
export function SkillSurface({
page = '',
placement = '',
context = null,
className = '',
_pageId = '',
_profile = null,
_profiles = [],
_courses = [],
_positions = [],
_customSources = [],
_email = '',
_onOpenTraining = null,
}) {
const sections = useSkillSections(page, placement);
const resolved = useSkillDataContext(context);

View File

@@ -42,6 +42,7 @@ const badgeVariants = cva(
}
)
/** @type {React.ForwardRefExoticComponent<any>} */
const Badge = React.forwardRef(({ className, variant, size, shape, ...props }, ref) => (
<div ref={ref} className={cn(badgeVariants({ variant, size, shape }), className)} {...props} />
))

View File

@@ -4,11 +4,13 @@ import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
/** @type {React.ForwardRefExoticComponent<any>} */
const Breadcrumb = React.forwardRef(
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
)
Breadcrumb.displayName = "Breadcrumb"
/** @type {React.ForwardRefExoticComponent<any>} */
const BreadcrumbList = React.forwardRef(({ className, ...props }, ref) => (
<ol
ref={ref}
@@ -20,6 +22,7 @@ const BreadcrumbList = React.forwardRef(({ className, ...props }, ref) => (
))
BreadcrumbList.displayName = "BreadcrumbList"
/** @type {React.ForwardRefExoticComponent<any>} */
const BreadcrumbItem = React.forwardRef(({ className, ...props }, ref) => (
<li
ref={ref}
@@ -28,6 +31,7 @@ const BreadcrumbItem = React.forwardRef(({ className, ...props }, ref) => (
))
BreadcrumbItem.displayName = "BreadcrumbItem"
/** @type {React.ForwardRefExoticComponent<any>} */
const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
@@ -40,6 +44,7 @@ const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
/** @type {React.ForwardRefExoticComponent<any>} */
const BreadcrumbPage = React.forwardRef(({ className, ...props }, ref) => (
<span
ref={ref}
@@ -51,6 +56,7 @@ const BreadcrumbPage = React.forwardRef(({ className, ...props }, ref) => (
))
BreadcrumbPage.displayName = "BreadcrumbPage"
/** @param {any} props */
const BreadcrumbSeparator = ({
children,
className,
@@ -66,6 +72,7 @@ const BreadcrumbSeparator = ({
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
/** @param {any} props */
const BreadcrumbEllipsis = ({
className,
...props

View File

@@ -82,6 +82,7 @@ const buttonVariants = cva(
}
)
/** @type {React.ForwardRefExoticComponent<any>} */
const Button = React.forwardRef((
{ className, variant, size, shape, block, asChild = false, loading = false, disabled, children, ...props },
ref

View File

@@ -2,6 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
/** @type {React.ForwardRefExoticComponent<any>} */
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
@@ -10,6 +11,7 @@ const Card = React.forwardRef(({ className, ...props }, ref) => (
))
Card.displayName = "Card"
/** @type {React.ForwardRefExoticComponent<any>} */
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
@@ -18,6 +20,7 @@ const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
))
CardHeader.displayName = "CardHeader"
/** @type {React.ForwardRefExoticComponent<any>} */
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
@@ -26,6 +29,7 @@ const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
))
CardTitle.displayName = "CardTitle"
/** @type {React.ForwardRefExoticComponent<any>} */
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
@@ -34,11 +38,13 @@ const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
))
CardDescription.displayName = "CardDescription"
/** @type {React.ForwardRefExoticComponent<any>} */
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
/** @type {React.ForwardRefExoticComponent<any>} */
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}

View File

@@ -4,6 +4,7 @@ import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
/** @type {React.ForwardRefExoticComponent<any>} */
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}

View File

@@ -14,6 +14,7 @@ const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
/** @type {React.ForwardRefExoticComponent<any>} */
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
@@ -25,6 +26,7 @@ const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
@@ -46,8 +48,9 @@ const DialogContent = React.forwardRef(({ className, children, ...props }, ref)
))
DialogContent.displayName = DialogPrimitive.Content.displayName
/** @param {any} props */
const DialogHeader = ({
className,
className = '',
...props
}) => (
<div
@@ -56,8 +59,9 @@ const DialogHeader = ({
)
DialogHeader.displayName = "DialogHeader"
/** @param {any} props */
const DialogFooter = ({
className,
className = '',
...props
}) => (
<div
@@ -66,6 +70,7 @@ const DialogFooter = ({
)
DialogFooter.displayName = "DialogFooter"
/** @type {React.ForwardRefExoticComponent<any>} */
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
@@ -74,6 +79,7 @@ const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}

View File

@@ -16,6 +16,7 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
@@ -32,6 +33,7 @@ const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, .
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
@@ -44,6 +46,7 @@ const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) =
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
@@ -59,6 +62,7 @@ const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...pr
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
@@ -71,6 +75,7 @@ const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref)
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
@@ -91,9 +96,11 @@ const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checke
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuRadioItem = React.forwardRef(({ className, children, value, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
value={value}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
@@ -109,6 +116,7 @@ const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
@@ -117,6 +125,7 @@ const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref)
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}

View File

@@ -9,6 +9,7 @@ const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
/** @type {React.ForwardRefExoticComponent<any>} */
const HoverCardContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}

View File

@@ -72,6 +72,7 @@ function buildSrcSet(parsed, options) {
).join(", ")
}
/** @type {React.ForwardRefExoticComponent<any>} */
const ImageWrapper = React.forwardRef(({ aspectRatio, className, style, children }, ref) => (
<span
ref={ref}
@@ -83,6 +84,7 @@ const ImageWrapper = React.forwardRef(({ aspectRatio, className, style, children
))
ImageWrapper.displayName = "ImageWrapper"
/** @type {React.ForwardRefExoticComponent<any>} */
const ResponsiveImage = React.forwardRef(
({ parsed, fittingType, focalPoint, quality, className, style, aspectRatio, onLoad, ...props }, parentRef) => {
const wrapperRef = React.useRef(null)
@@ -172,6 +174,7 @@ ResponsiveImage.displayName = "ResponsiveImage"
* server-side, optionally anchored at a focal point. Other URLs render as a
* plain <img>. Failed loads swap to a fallback image.
*/
/** @type {React.ForwardRefExoticComponent<any>} */
const Image = React.forwardRef(
(
{

View File

@@ -34,6 +34,7 @@ const inputVariants = cva(
}
)
/** @type {React.ForwardRefExoticComponent<any>} */
const Input = React.forwardRef(({ className, type, size, shape, ...props }, ref) => (
<input
type={type}

View File

@@ -8,6 +8,7 @@ const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
/** @type {React.ForwardRefExoticComponent<any>} */
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))

View File

@@ -9,6 +9,7 @@ const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
/** @type {React.ForwardRefExoticComponent<any>} */
const PopoverContent = React.forwardRef(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content

View File

@@ -4,15 +4,18 @@ import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
/** @type {React.ForwardRefExoticComponent<any>} */
const RadioGroup = React.forwardRef(({ className, ...props }, ref) => {
return (<RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />);
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef(({ className, ...props }, ref) => {
/** @type {React.ForwardRefExoticComponent<any>} */
const RadioGroupItem = React.forwardRef(({ className = '', value, ...props }, ref) => {
return (
(<RadioGroupPrimitive.Item
ref={ref}
value={value}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-border data-[state=checked]:border-krow-blue text-krow-blue shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-krow-blue/60 disabled:cursor-not-allowed disabled:opacity-50",
className

View File

@@ -12,6 +12,7 @@ const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
/** @type {React.ForwardRefExoticComponent<any>} */
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
@@ -28,6 +29,7 @@ const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref)
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
@@ -38,6 +40,7 @@ const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) =>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
@@ -49,6 +52,7 @@ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) =
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
@@ -73,6 +77,7 @@ const SelectContent = React.forwardRef(({ className, children, position = "poppe
))
SelectContent.displayName = SelectPrimitive.Content.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
@@ -81,9 +86,17 @@ const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
/** @type {React.ForwardRefExoticComponent<any>} */
/* `value` is named and forwarded explicitly — the props type here is `any`, so a
value arriving only through `...props` is invisible to the checker and it flags
the required prop as missing. Naming it without passing it back down is the
trap: Radix then has no value to match against, no item is ever `isSelected`,
the trigger's value node never receives any item text, and every closed select
renders as an empty pill with nothing but a chevron. */
const SelectItem = React.forwardRef(({ className = '', children, value, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
value={value}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
@@ -99,6 +112,7 @@ const SelectItem = React.forwardRef(({ className, children, ...props }, ref) =>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}

View File

@@ -14,6 +14,7 @@ const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
/** @type {React.ForwardRefExoticComponent<any>} */
const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
@@ -44,6 +45,7 @@ const sheetVariants = cva(
}
)
/** @type {React.ForwardRefExoticComponent<any>} */
const SheetContent = React.forwardRef(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
@@ -79,6 +81,7 @@ const SheetFooter = ({
)
SheetFooter.displayName = "SheetFooter"
/** @type {React.ForwardRefExoticComponent<any>} */
const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
@@ -87,6 +90,7 @@ const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const SheetDescription = React.forwardRef(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}

View File

@@ -3,6 +3,7 @@ import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
/** @type {React.ForwardRefExoticComponent<any>} */
const Slider = React.forwardRef(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}

View File

@@ -3,6 +3,7 @@ import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
/** @type {React.ForwardRefExoticComponent<any>} */
const Switch = React.forwardRef(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(

View File

@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
/** @type {React.ForwardRefExoticComponent<any>} */
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
@@ -16,6 +17,7 @@ const TabsList = React.forwardRef(({ className, ...props }, ref) => (
))
TabsList.displayName = TabsPrimitive.List.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
@@ -27,6 +29,7 @@ const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
/** @type {React.ForwardRefExoticComponent<any>} */
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}

View File

@@ -2,6 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
/** @type {React.ForwardRefExoticComponent<any>} */
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
(<textarea

View File

@@ -3,6 +3,7 @@ import { cva } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
/** @type {React.ForwardRefExoticComponent<any>} */
const ToastProvider = React.forwardRef(({ ...props }, ref) => (
<div
ref={ref}
@@ -12,6 +13,7 @@ const ToastProvider = React.forwardRef(({ ...props }, ref) => (
));
ToastProvider.displayName = "ToastProvider";
/** @type {React.ForwardRefExoticComponent<any>} */
const ToastViewport = React.forwardRef(({ ...props }, ref) => (
<div
ref={ref}
@@ -37,6 +39,7 @@ const toastVariants = cva(
}
);
/** @type {React.ForwardRefExoticComponent<any>} */
const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
return (
<div
@@ -48,6 +51,7 @@ const Toast = React.forwardRef(({ className, variant, ...props }, ref) => {
});
Toast.displayName = "Toast";
/** @type {React.ForwardRefExoticComponent<any>} */
const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
@@ -60,6 +64,7 @@ const ToastAction = React.forwardRef(({ className, ...props }, ref) => (
));
ToastAction.displayName = "ToastAction";
/** @type {React.ForwardRefExoticComponent<any>} */
const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
<button
ref={ref}
@@ -75,6 +80,7 @@ const ToastClose = React.forwardRef(({ className, ...props }, ref) => (
));
ToastClose.displayName = "ToastClose";
/** @type {React.ForwardRefExoticComponent<any>} */
const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
@@ -84,6 +90,7 @@ const ToastTitle = React.forwardRef(({ className, ...props }, ref) => (
));
ToastTitle.displayName = "ToastTitle";
/** @type {React.ForwardRefExoticComponent<any>} */
const ToastDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}

View File

@@ -8,7 +8,8 @@ import {
ToastViewport,
} from "@/components/ui/toast";
export function Toaster() {
/** @param {any} [props] */
export function Toaster(props) {
const { toasts } = useToast();
return (

View File

@@ -11,6 +11,7 @@ const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
/** @type {React.ForwardRefExoticComponent<any>} */
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content