fix error line
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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',
|
||||
}) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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(
|
||||
(
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || {}) },
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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} />
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
(
|
||||
{
|
||||
|
||||
@@ -34,6 +34,7 @@ const inputVariants = cva(
|
||||
}
|
||||
)
|
||||
|
||||
/** @type {React.ForwardRefExoticComponent<any>} */
|
||||
const Input = React.forwardRef(({ className, type, size, shape, ...props }, ref) => (
|
||||
<input
|
||||
type={type}
|
||||
|
||||
@@ -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} />
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
|
||||
export function Toaster() {
|
||||
/** @param {any} [props] */
|
||||
export function Toaster(props) {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user