chore(ts-migration): migrate the non-agent pages to TypeScript
Phase 9, first batch: 18 pages. Seventeen emit byte-identical JavaScript, the
eighteenth differs only by a JSDoc cast becoming a real annotation, and the
production bundle is byte-identical to be49184.
SCOPE, decided by imports. Twenty-two of the forty pages import `lib/skills`,
`lib/agents`, `ai-assistant`, `components/skills`, `ui-tree` or `ui-editor`, and
are deferred to Phase 11 with the rest of that region. `Owliver.jsx` is in THIS
batch despite the name: it imports only `krowHooks`, `krowScore`, `krowAi` and
`ui/button`, and is a profile-building conversation page rather than any part of
the agent runtime.
`DesignSystem` is the headline result. It was the single worst file in Phase 6 -
111 errors when the design system was first renamed - and it arrived here with
none, because those were never its errors: they were the primitives' inferred
props, and fixing them at the source fixed every consumer.
Three pages needed real work, and each was the author's own intent made
explicit:
- `admin/Login` carried `useState(/** @type {{email?: string, password?: string}} */ ({}))`.
JSDoc casts stop applying in a `.tsx` file, so that became a real type
argument, and `validate`'s accumulator - built empty and filled per failed
rule - needed the same shape. The login flow itself is untouched: the
generic 401 message, the 429 branch and the `remember` field all stand.
- `Owliver` gets `new Promise<void>`, because its `resolve()` takes no
argument.
- `Candidates` names the element type of a `Set` built from `any[]`, which
otherwise infers `Set<unknown>` and makes every option a `ReactNode` error.
LINT CAUGHT A REGRESSION THE OTHER CHECKS DID NOT. The generator adds an entity
import when it sees the type NAME anywhere in the file, which for four pages
with no props at all left an import nothing used - four `unused-imports` ERRORS,
taking `npm run lint` from exit 0 to exit 1 while typecheck, tests, the bundle
and seventeen of eighteen emit comparisons all stayed green. Removed. The
generator's import rule is too eager and wants narrowing before the next batch.
Verified: tsc 21 -> 21, set-difference showing zero introduced and zero removed;
zero errors in any of the 18 pages; lint back to 0 errors and 289 warnings;
npm test 1684/1691 with the same seven failures; Owliver baseline 59/59;
production bundle byte-identical; baseline artifacts untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
@@ -43,7 +43,9 @@ export default function Candidates() {
|
|||||||
|
|
||||||
const sorted = [...applications].sort((a, b) => (b.ai_score || 0) - (a.ai_score || 0));
|
const sorted = [...applications].sort((a, b) => (b.ai_score || 0) - (a.ai_score || 0));
|
||||||
|
|
||||||
const positionOptions = Array.from(new Set(applications.map(resolveTitle).filter(Boolean))).sort();
|
/* `new Set(x)` where x is `any[]` infers `Set<unknown>`; naming the element
|
||||||
|
type is the whole fix. The values put into it are unchanged. */
|
||||||
|
const positionOptions = Array.from(new Set<string>(applications.map(resolveTitle).filter(Boolean))).sort();
|
||||||
|
|
||||||
const filtered = sorted.filter(a => {
|
const filtered = sorted.filter(a => {
|
||||||
const title = resolveTitle(a).toLowerCase();
|
const title = resolveTitle(a).toLowerCase();
|
||||||
@@ -30,8 +30,14 @@ import { AXIS_PROPS, CHART_TONES } from '@/components/ds';
|
|||||||
* place. Not linked from the product navigation — reach it at /design-system.
|
* place. Not linked from the product navigation — reach it at /design-system.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @param {any} props */
|
interface SpecProps {
|
||||||
function Spec({ title, description, children, className = '' }) {
|
title?: string;
|
||||||
|
description?: any;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Spec({ title, description, children, className = '' }: SpecProps) {
|
||||||
return (
|
return (
|
||||||
<Surface variant="solid" radius="xl" padding="lg" className={className}>
|
<Surface variant="solid" radius="xl" padding="lg" className={className}>
|
||||||
<SectionHeader title={title} subtitle={description} />
|
<SectionHeader title={title} subtitle={description} />
|
||||||
@@ -40,8 +46,12 @@ function Spec({ title, description, children, className = '' }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {any} props */
|
interface RowProps {
|
||||||
function Row({ label = '', children }) {
|
label?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Row({ label = '', children }: RowProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{label && <span className="text-caption text-ink-4 w-24 shrink-0">{label}</span>}
|
{label && <span className="text-caption text-ink-4 w-24 shrink-0">{label}</span>}
|
||||||
@@ -23,7 +23,12 @@ function formatDate(d) {
|
|||||||
} catch { return ''; }
|
} catch { return ''; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Stars({ count, size = 14 }) {
|
interface StarsProps {
|
||||||
|
count?: any;
|
||||||
|
size?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Stars({ count, size = 14 }: StarsProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-0.5">
|
<div className="flex items-center gap-0.5">
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
@@ -215,8 +220,13 @@ export default function HiredHistory() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {any} props */
|
interface StatPillProps {
|
||||||
function StatPill({ label, value, accent = false }) {
|
label?: string;
|
||||||
|
value?: any;
|
||||||
|
accent?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatPill({ label, value, accent = false }: StatPillProps) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-[#F0F4F8] border border-[#E5E7EB]">
|
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-[#F0F4F8] border border-[#E5E7EB]">
|
||||||
<span className="text-[11px] font-semibold text-[#1E56C1]">{label}</span>
|
<span className="text-[11px] font-semibold text-[#1E56C1]">{label}</span>
|
||||||
@@ -177,7 +177,13 @@ export default function KrowIdentity() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Chips({ items, onRemove, tone = 'gray' }) {
|
interface ChipsProps {
|
||||||
|
items?: any;
|
||||||
|
onRemove?: (...args: any[]) => void;
|
||||||
|
tone?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Chips({ items, onRemove, tone = 'gray' }: ChipsProps) {
|
||||||
if (!items?.length) return null;
|
if (!items?.length) return null;
|
||||||
const cls = tone === 'blue' ? 'bg-[#EEF3FE] text-[#1E40AF] border-[#DBEAFE]' : 'bg-[#F3F4F6] text-[#374151]';
|
const cls = tone === 'blue' ? 'bg-[#EEF3FE] text-[#1E40AF] border-[#DBEAFE]' : 'bg-[#F3F4F6] text-[#374151]';
|
||||||
return (
|
return (
|
||||||
@@ -42,7 +42,12 @@ function matchedCerts(profile, job) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function Section({ title, children }) {
|
interface SectionProps {
|
||||||
|
title?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Section({ title, children }: SectionProps) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
<div className="rounded-2xl bg-white border border-[#E5E7EB] p-6">
|
||||||
<h2 className="text-[15px] font-bold text-[#0F172A] mb-3">{title}</h2>
|
<h2 className="text-[15px] font-bold text-[#0F172A] mb-3">{title}</h2>
|
||||||
@@ -16,7 +16,16 @@ const METRICS = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
function MetricCard({ icon: Icon, value, label, bg, glow, index }) {
|
interface MetricCardProps {
|
||||||
|
icon?: any;
|
||||||
|
value?: any;
|
||||||
|
label?: string;
|
||||||
|
bg?: any;
|
||||||
|
glow?: any;
|
||||||
|
index?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricCard({ icon: Icon, value, label, bg, glow, index }: MetricCardProps) {
|
||||||
const isYellow = bg === 'bg-[#F9E547]';
|
const isYellow = bg === 'bg-[#F9E547]';
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -83,7 +83,11 @@ const UI = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Owliver({ compact = false }) {
|
interface OwliverProps {
|
||||||
|
compact?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Owliver({ compact = false }: OwliverProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: profile, isLoading } = useWorkerProfile();
|
const { data: profile, isLoading } = useWorkerProfile();
|
||||||
const updateProfile = useUpdateWorkerProfile();
|
const updateProfile = useUpdateWorkerProfile();
|
||||||
@@ -113,7 +117,7 @@ export default function Owliver({ compact = false }) {
|
|||||||
|
|
||||||
// Warm owl voice — slightly higher pitch, slower, gentle
|
// Warm owl voice — slightly higher pitch, slower, gentle
|
||||||
const speak = useCallback((text) => {
|
const speak = useCallback((text) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
if (!ttsEnabled || !window.speechSynthesis) { resolve(); return; }
|
if (!ttsEnabled || !window.speechSynthesis) { resolve(); return; }
|
||||||
window.speechSynthesis.cancel();
|
window.speechSynthesis.cancel();
|
||||||
const u = new SpeechSynthesisUtterance(text);
|
const u = new SpeechSynthesisUtterance(text);
|
||||||
@@ -529,8 +533,14 @@ export default function Owliver({ compact = false }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {any} props */
|
interface SummaryFieldProps {
|
||||||
function SummaryField({ label, value, full = false, empty = '—' }) {
|
label?: string;
|
||||||
|
value?: any;
|
||||||
|
full?: any;
|
||||||
|
empty?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SummaryField({ label, value, full = false, empty = '—' }: SummaryFieldProps) {
|
||||||
const has = value && String(value).trim();
|
const has = value && String(value).trim();
|
||||||
return (
|
return (
|
||||||
<div className={cn('rounded-lg bg-[#F9FAFB] p-2.5', full && 'col-span-2')}>
|
<div className={cn('rounded-lg bg-[#F9FAFB] p-2.5', full && 'col-span-2')}>
|
||||||
@@ -540,7 +550,12 @@ function SummaryField({ label, value, full = false, empty = '—' }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DnaScore({ label, value }) {
|
interface DnaScoreProps {
|
||||||
|
label?: string;
|
||||||
|
value?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DnaScore({ label, value }: DnaScoreProps) {
|
||||||
const v = typeof value === 'number' ? value : 0;
|
const v = typeof value === 'number' ? value : 0;
|
||||||
const tone = v >= 75 ? 'text-[#16A34A]' : v >= 50 ? 'text-[#0838E0]' : v > 0 ? 'text-[#D97706]' : 'text-[#9CA3AF]';
|
const tone = v >= 75 ? 'text-[#16A34A]' : v >= 50 ? 'text-[#0838E0]' : v > 0 ? 'text-[#D97706]' : 'text-[#9CA3AF]';
|
||||||
return (
|
return (
|
||||||
@@ -33,12 +33,14 @@ export default function AdminLogin() {
|
|||||||
|
|
||||||
const [form, setForm] = useState({ email: DEMO_EMAIL, password: '', remember: true });
|
const [form, setForm] = useState({ email: DEMO_EMAIL, password: '', remember: true });
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [errors, setErrors] = useState(/** @type {{ email?: string, password?: string }} */ ({}));
|
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
||||||
const [authError, setAuthError] = useState('');
|
const [authError, setAuthError] = useState('');
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
const next = {};
|
/* Same shape the state carries; built empty and filled per failed rule, so
|
||||||
|
inference alone fixes it at `{}` and neither assignment compiles. */
|
||||||
|
const next: { email?: string; password?: string } = {};
|
||||||
if (!form.email.trim()) next.email = 'Enter your work email';
|
if (!form.email.trim()) next.email = 'Enter your work email';
|
||||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) next.email = 'Enter a valid email address';
|
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) next.email = 'Enter a valid email address';
|
||||||
// Only "did you type anything". A client-side length rule here would be a
|
// Only "did you type anything". A client-side length rule here would be a
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ProfileView } from '@/types/views';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { ArrowLeft, Briefcase, CalendarDays, MapPin, Plus, Sparkles } from 'lucide-react';
|
import { ArrowLeft, Briefcase, CalendarDays, MapPin, Plus, Sparkles } from 'lucide-react';
|
||||||
@@ -36,7 +37,11 @@ const stageLabel = (application, interviews) => {
|
|||||||
return 'Applied';
|
return 'Applied';
|
||||||
};
|
};
|
||||||
|
|
||||||
function Facts({ profile }) {
|
interface FactsProps {
|
||||||
|
profile: ProfileView;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Facts({ profile }: FactsProps) {
|
||||||
const facts = [
|
const facts = [
|
||||||
profile.current_position && { icon: Briefcase, label: profile.current_position },
|
profile.current_position && { icon: Briefcase, label: profile.current_position },
|
||||||
profile.address && { icon: MapPin, label: profile.address },
|
profile.address && { icon: MapPin, label: profile.address },
|
||||||
@@ -56,7 +61,12 @@ function Facts({ profile }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tags({ title, values = [] }) {
|
interface TagsProps {
|
||||||
|
title?: string;
|
||||||
|
values?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tags({ title, values = [] }: TagsProps) {
|
||||||
if (!values.length) return null;
|
if (!values.length) return null;
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
Reference in New Issue
Block a user