From 3d5f54bd5b85a69b4be5f1dba356fd01d4e2f86a Mon Sep 17 00:00:00 2001 From: Aravind Date: Fri, 18 Sep 2026 11:54:53 +0530 Subject: [PATCH] chore(ts-migration): migrate components/forge and components/admin to TypeScript Phase 8, second batch: 16 files. All 16 emit byte-identical JavaScript and the production bundle is byte-identical to e7e1e98. `ProfileView` and `CourseView` move from `components/krow/types.ts` to `@/types/views`, because `components/forge` reads the same `jsonb` columns off the same courses and profiles. `components/krow/types.ts` stays as a re-export so that folder's imports are untouched. Two copies of one narrowing would be two things to keep in step. SCOPE, decided by imports rather than by folder name. `components/skills` was in the batch as requested and is DEFERRED: it imports `lib/skills/registry`, `lib/agents/runtime`, `ai-assistant/PageContext` and `ai-assistant/AgentContext`, which makes it agent region by the same test this batch used. `ui-tree` and `ui-editor` are deferred for the same reason - all eleven files drive the UI-node system, which is Owliver's "move this card" capability. `forge` was checked and kept: despite the name, it imports nothing from `lib/skills`, `lib/agents` or `ai-assistant`. It is the worker learning product. Twenty-six components now declare real props. Two corrections to the pattern came out of this batch, both from call sites: - Optionality. Batch 1 made a prop required when it had no default. That is wrong here: `AdminPage` has seventeen call sites and most pass only `title`. Required is now reserved for entity-typed props and `children` - what a component genuinely cannot render without - and everything else is optional, which is what the JavaScript always allowed. - Callback arity. `() => void` was too strict: `onOpen`, `onCreate` and `onBrowse` are called WITH arguments, and `University` passes a `useState` setter straight through, which has one parameter and is therefore not assignable to a zero-parameter type. Callbacks take `(...args: any[])`. `RoleGlyph`'s `GLYPHS` table gets `[RegExp, ComponentType][]` - the same widening `aiEngine`'s router had, where the element becomes the union of both positions and neither `pattern.test` nor `` works. The author had already written that exact type as a JSDoc comment; it is now the real annotation and the comment is gone. `ForgeHeader` receives `onBrowse` and destructures `_onBrowse`, so the prop is passed and silently dropped - the same shape as `TalentHero`'s `jobRecs` in the previous batch. Recorded rather than changed. Two automated passes were reverted rather than shipped. One added `?` to object members inside component bodies, not just interface fields, producing `TS1162: An object member cannot be declared optional` - it was rerun scoped to `interface XProps` blocks. The other was the generator itself, which annotated only the FIRST component in each file and so missed `SectionTitle` in `PageShell`; rewritten to walk every match and splice in reverse, it went from 14 components to 26 and took the error count from 93 to 38. Two runtime imports were caught by the emitted-JavaScript check and would not have been caught any other way. The generator added `import * as React` to `RoleGlyph` for a type-only reference - a real import in the bundle - now `import type { ComponentType }`. Fixing that, I then removed the React import `PageShell` genuinely had; restored. Verified: tsc 22 -> 22, set-difference showing zero introduced and zero removed; zero errors in any of the 16 files; all 16 emit byte-identical JavaScript; production bundle byte-identical; npm test 1684/1691 with the same seven failures; Owliver baseline 59/59; lint 0 errors; baseline artifacts untouched. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8 --- .../admin/{PageShell.jsx => PageShell.tsx} | 35 +++++++++-- .../admin/{RoleGlyph.jsx => RoleGlyph.tsx} | 18 ++++-- ...SkillTraining.jsx => AddSkillTraining.tsx} | 50 ++++++++++++++-- ...geDirectory.jsx => ChallengeDirectory.tsx} | 18 +++++- .../{ForgeHeader.jsx => ForgeHeader.tsx} | 17 +++++- .../{ForgeInsight.jsx => ForgeInsight.tsx} | 10 +++- .../{ForgeProgress.jsx => ForgeProgress.tsx} | 7 ++- .../forge/{ForgeRail.jsx => ForgeRail.tsx} | 19 +++++- .../forge/{NextSkill.jsx => NextSkill.tsx} | 18 +++++- .../forge/{SkillCard.jsx => SkillCard.tsx} | 9 ++- .../{SkillDetail.jsx => SkillDetail.tsx} | 20 ++++++- .../{SkillLibrary.jsx => SkillLibrary.tsx} | 31 +++++++++- ...llProgression.jsx => SkillProgression.tsx} | 26 +++++++-- .../forge/{YourForge.jsx => YourForge.tsx} | 9 ++- .../{challengeMeta.js => challengeMeta.ts} | 3 +- src/components/forge/{index.js => index.ts} | 0 src/components/krow/types.ts | 58 ++----------------- src/types/views.ts | 55 ++++++++++++++++++ 18 files changed, 311 insertions(+), 92 deletions(-) rename src/components/admin/{PageShell.jsx => PageShell.tsx} (89%) rename src/components/admin/{RoleGlyph.jsx => RoleGlyph.tsx} (84%) rename src/components/forge/{AddSkillTraining.jsx => AddSkillTraining.tsx} (95%) rename src/components/forge/{ChallengeDirectory.jsx => ChallengeDirectory.tsx} (94%) rename src/components/forge/{ForgeHeader.jsx => ForgeHeader.tsx} (81%) rename src/components/forge/{ForgeInsight.jsx => ForgeInsight.tsx} (90%) rename src/components/forge/{ForgeProgress.jsx => ForgeProgress.tsx} (97%) rename src/components/forge/{ForgeRail.jsx => ForgeRail.tsx} (91%) rename src/components/forge/{NextSkill.jsx => NextSkill.tsx} (96%) rename src/components/forge/{SkillCard.jsx => SkillCard.tsx} (97%) rename src/components/forge/{SkillDetail.jsx => SkillDetail.tsx} (95%) rename src/components/forge/{SkillLibrary.jsx => SkillLibrary.tsx} (90%) rename src/components/forge/{SkillProgression.jsx => SkillProgression.tsx} (96%) rename src/components/forge/{YourForge.jsx => YourForge.tsx} (97%) rename src/components/forge/{challengeMeta.js => challengeMeta.ts} (97%) rename src/components/forge/{index.js => index.ts} (100%) create mode 100644 src/types/views.ts diff --git a/src/components/admin/PageShell.jsx b/src/components/admin/PageShell.tsx similarity index 89% rename from src/components/admin/PageShell.jsx rename to src/components/admin/PageShell.tsx index de82471..d8a9f83 100644 --- a/src/components/admin/PageShell.jsx +++ b/src/components/admin/PageShell.tsx @@ -17,8 +17,17 @@ import { cn } from '@/lib/utils'; * Controls live in the page rather than in the global chrome, which is why the * Admin header only needs one 56px bar. */ -/** @param {any} props */ -export function AdminPage({ title, subtitle, meta, actions, tabs, children, className }) { +interface AdminPageProps { + title?: string; + subtitle?: string; + meta?: any; + actions?: any; + tabs?: any; + children: React.ReactNode; + className?: string; +} + +export function AdminPage({ title, subtitle, meta, actions, tabs, children, className }: AdminPageProps) { return (
@@ -57,8 +66,15 @@ export function AdminPage({ title, subtitle, meta, actions, tabs, children, clas * Sentence-case and small rather than a heavy all-caps overline: on a page with * four or five sections, loud labels compete with the data they introduce. */ -/** @param {any} props */ -export function SectionTitle({ id, title, meta, action, className }) { +interface SectionTitleProps { + id?: any; + title?: string; + meta?: any; + action?: any; + className?: string; +} + +export function SectionTitle({ id, title, meta, action, className }: SectionTitleProps) { return (
{/* The count wraps under the section name rather than competing with it @@ -91,8 +107,15 @@ export function SectionTitle({ id, title, meta, action, className }) { * primary action is pushed to the end, so every management page has the same * control geometry. */ -/** @param {any} props */ -export function Toolbar({ search = null, filters = null, actions = null, meta = null, className = '' }) { +interface ToolbarProps { + search?: any; + filters?: any; + actions?: any; + meta?: any; + className?: string; +} + +export function Toolbar({ search = null, filters = null, actions = null, meta = null, className = '' }: ToolbarProps) { return (
diff --git a/src/components/admin/RoleGlyph.jsx b/src/components/admin/RoleGlyph.tsx similarity index 84% rename from src/components/admin/RoleGlyph.jsx rename to src/components/admin/RoleGlyph.tsx index 351834f..84e16cc 100644 --- a/src/components/admin/RoleGlyph.jsx +++ b/src/components/admin/RoleGlyph.tsx @@ -1,3 +1,4 @@ +import type { ComponentType } from 'react'; import { Briefcase, ChefHat, ConciergeBell, HardHat, PackageSearch, ShieldCheck, Sparkles, Truck, Wine, } from 'lucide-react'; @@ -19,8 +20,10 @@ import { cn } from '@/lib/utils'; */ /** Category to icon. Matched loosely, so a new "Head Chef" still gets the hat. */ -/** @type {[RegExp, React.ComponentType][]} */ -const GLYPHS = [ +/* `[pattern, icon]` pairs. Without the tuple the element widens to the union of + both positions, so `pattern.test` and `` both stop working — the same + shape as `aiEngine`'s route table. */ +const GLYPHS: [RegExp, ComponentType][] = [ [/chef|cook|kitchen/i, ChefHat], [/bartend|bar|mixolog/i, Wine], [/server|wait|host|banquet|concierge/i, ConciergeBell], @@ -42,8 +45,15 @@ const SIZES = { lg: { box: 'h-14 w-14 rounded-xl', icon: 'h-6 w-6' }, }; -/** @param {any} props */ -export function RoleGlyph({ category, title, size = 'md', aiGenerated = false, className = '' }) { +interface RoleGlyphProps { + category?: any; + title?: string; + size?: any; + aiGenerated?: any; + className?: string; +} + +export function RoleGlyph({ category, title, size = 'md', aiGenerated = false, className = '' }: RoleGlyphProps) { const Icon = iconForRole(category, title); const s = SIZES[size] || SIZES.md; diff --git a/src/components/forge/AddSkillTraining.jsx b/src/components/forge/AddSkillTraining.tsx similarity index 95% rename from src/components/forge/AddSkillTraining.jsx rename to src/components/forge/AddSkillTraining.tsx index 3dcb474..ca72d5f 100644 --- a/src/components/forge/AddSkillTraining.jsx +++ b/src/components/forge/AddSkillTraining.tsx @@ -49,9 +49,21 @@ const EMPTY = { badge_reward: '', }; +interface AddSkillTrainingProps { + open?: boolean; + onOpenChange?: (...args: any[]) => void; + categories?: any; + onCreate?: (...args: any[]) => void; + onUpdate?: (...args: any[]) => void; + busy?: any; + prefill?: any; + startStep?: any; + courseId?: any; +} + export function AddSkillTraining({ open, onOpenChange, categories = [], onCreate, onUpdate, busy, prefill, startStep, courseId, -}) { +}: AddSkillTrainingProps) { const [step, setStep] = React.useState(0); const [form, setForm] = React.useState(EMPTY); const [confirming, setConfirming] = React.useState(false); @@ -409,7 +421,12 @@ export function AddSkillTraining({ /* ── Pieces ─────────────────────────────────────────────────────────────── */ /** Where the reader is, and how to step back to anything already answered. */ -function Stepper({ step, onStep }) { +interface StepperProps { + step?: any; + onStep?: (...args: any[]) => void; +} + +function Stepper({ step, onStep }: StepperProps) { return (
    {STEPS.map((s, i) => { @@ -443,7 +460,14 @@ function Stepper({ step, onStep }) { } /** One option in a small single-choice row. */ -function Choice({ active, onClick, className, children }) { +interface ChoiceProps { + active?: any; + onClick?: (...args: any[]) => void; + className?: string; + children: React.ReactNode; +} + +function Choice({ active, onClick, className, children }: ChoiceProps) { return (