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<any>][]` - the same
widening `aiEngine`'s router had, where the element becomes the union of both
positions and neither `pattern.test` nor `<Icon />` 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
This commit is contained in:
2026-09-18 11:54:53 +05:30
parent e7e1e9873f
commit 3d5f54bd5b
18 changed files with 311 additions and 92 deletions

View File

@@ -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 (
<div className={cn('space-y-6', className)}>
<header className="flex flex-wrap items-center justify-between gap-4 border-b border-border/50 pb-4">
@@ -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 (
<div className={cn('flex flex-wrap items-baseline justify-between gap-2', className)}>
{/* 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 (
<div className={cn('space-y-2', className)}>
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">

View File

@@ -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<any>][]} */
const GLYPHS = [
/* `[pattern, icon]` pairs. Without the tuple the element widens to the union of
both positions, so `pattern.test` and `<Icon />` both stop working — the same
shape as `aiEngine`'s route table. */
const GLYPHS: [RegExp, ComponentType<any>][] = [
[/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;

View File

@@ -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 (
<ol className="mb-5 flex flex-wrap items-center gap-x-1.5 gap-y-2">
{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 (
<button
type="button"
@@ -465,8 +489,16 @@ function Choice({ active, onClick, className, children }) {
}
/** A growable list of short text lines — training steps, evaluation criteria. */
/** @param {any} props */
function ListBuilder({ label, hint = '', placeholder, values, onChange, addLabel }) {
interface ListBuilderProps {
label?: string;
hint?: any;
placeholder?: any;
values?: any;
onChange?: (...args: any[]) => void;
addLabel?: any;
}
function ListBuilder({ label, hint = '', placeholder, values, onChange, addLabel }: ListBuilderProps) {
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));
@@ -510,7 +542,13 @@ function ListBuilder({ label, hint = '', placeholder, values, onChange, addLabel
}
/** One line of the final read-through. */
function Review({ label, value, meta }) {
interface ReviewProps {
label?: string;
value?: any;
meta?: any;
}
function Review({ label, value, meta }: ReviewProps) {
return (
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 border-b border-border pb-3 last:border-0">
<div className="min-w-0">

View File

@@ -1,3 +1,4 @@
import type { CourseView, ProfileView } from '@/types/views';
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Lock, Search, ShieldCheck } from 'lucide-react';
@@ -14,6 +15,21 @@ import { DIFF_TONE, TYPE_ICON, TYPE_LABEL, typeOf } from './challengeMeta';
* its own, and the row still carries everything a card did — skill, title,
* description, difficulty, proof type, duration, XP, reward, state and reason.
*/
interface ChallengeDirectoryProps {
courses: CourseView[];
results?: any;
search?: any;
onSearch?: (...args: any[]) => void;
difficulty?: any;
onDifficulty?: (...args: any[]) => void;
difficulties?: any;
loading?: boolean;
filtered?: any;
onClear?: (...args: any[]) => void;
completedIds?: any;
profile: ProfileView;
}
export function ChallengeDirectory({
courses,
results,
@@ -27,7 +43,7 @@ export function ChallengeDirectory({
onClear,
completedIds,
profile,
}) {
}: ChallengeDirectoryProps) {
return (
<section aria-labelledby="forge-directory" className="min-w-0">
<div className="flex flex-col gap-3 border-b border-border pb-3 lg:flex-row lg:items-end lg:justify-between">

View File

@@ -13,8 +13,21 @@ 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.
*/
/** @param {any} props */
export function ForgeHeader({ onCreate = null, _onBrowse = null }) {
interface ForgeHeaderProps {
/**
* Passed by `University`, ignored by this component.
*
* The signature destructures `_onBrowse` — the underscore marking it
* deliberately unused — so the caller's prop never binds. Declared because
* the caller does send it; the alternative is TypeScript objecting to a prop
* JavaScript simply dropped. Same shape as `TalentHero`'s `jobRecs`.
*/
onBrowse?: (...args: any[]) => void;
onCreate?: (...args: any[]) => void;
_onBrowse?: any;
}
export function ForgeHeader({ onCreate = null, _onBrowse = null }: ForgeHeaderProps) {
return (
<header className="flex flex-col gap-4 border-b border-border/60 pb-6 sm:flex-row sm:items-end sm:justify-between">
<div className="min-w-0 space-y-1.5">

View File

@@ -4,7 +4,15 @@ import { ArrowRight, Sparkles } from 'lucide-react';
/**
* Forge Insight — AI Career Insight Card
*/
export function ForgeInsight({ headline, detail, highlight, actionLabel, onAction }) {
interface ForgeInsightProps {
headline?: any;
detail?: any;
highlight?: any;
actionLabel?: any;
onAction?: (...args: any[]) => void;
}
export function ForgeInsight({ headline, detail, highlight, actionLabel, onAction }: ForgeInsightProps) {
return (
<aside className="relative overflow-hidden rounded-2xl border border-blue-500/30 bg-gradient-to-r from-blue-50/80 via-surface to-indigo-50/30 dark:from-blue-950/40 dark:via-surface dark:to-indigo-950/30 p-5 shadow-xs">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">

View File

@@ -73,7 +73,12 @@ const STAT_CONFIG = {
},
};
export function ForgeProgress({ figures = [], loading = false }) {
interface ForgeProgressProps {
figures?: any;
loading?: boolean;
}
export function ForgeProgress({ figures = [], loading = false }: ForgeProgressProps) {
return (
<section aria-labelledby="forge-progress" className="space-y-4 rounded-2xl border border-border/80 bg-surface p-5 sm:p-6 shadow-sm">
<div className="flex items-center justify-between">

View File

@@ -19,8 +19,14 @@ const TIPS = [
'Keep training concise.',
];
/** @param {any} props */
export function ForgeRail({ recent = [], onCreate, onOpen, onAskOwliver: _onAskOwliver }) {
interface ForgeRailProps {
recent?: any;
onCreate?: (...args: any[]) => void;
onOpen?: (...args: any[]) => void;
onAskOwliver?: (...args: any[]) => void;
}
export function ForgeRail({ recent = [], onCreate, onOpen, onAskOwliver: _onAskOwliver }: ForgeRailProps) {
return (
<aside
className="grid grid-cols-1 gap-4 sm:grid-cols-3 lg:grid-cols-3 items-stretch"
@@ -103,7 +109,14 @@ export function ForgeRail({ recent = [], onCreate, onOpen, onAskOwliver: _onAskO
);
}
function Panel({ title, icon: Icon, iconColor, children }) {
interface PanelProps {
title?: string;
icon?: any;
iconColor?: any;
children: React.ReactNode;
}
function Panel({ title, icon: Icon, iconColor, children }: PanelProps) {
return (
<section className="rounded-2xl border border-border/80 bg-surface p-5 shadow-xs transition-all hover:border-blue-500/30">
<div className="flex items-center gap-2 mb-3.5 pb-2 border-b border-border/60">

View File

@@ -1,3 +1,4 @@
import type { CourseView } from '@/types/views';
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Check, Lock } from 'lucide-react';
@@ -18,7 +19,12 @@ import { TYPE_HINT, TYPE_ICON, TYPE_LABEL, typeOf, proofSteps } from './challeng
*/
/** Progress toward a threshold: pips for a small count, a bar for a score. */
function Progress({ have, need }) {
interface ProgressProps {
have?: any;
need?: any;
}
function Progress({ have, need }: ProgressProps) {
const done = Math.min(have, need);
if (need <= 8) {
@@ -47,7 +53,15 @@ function Progress({ have, need }) {
);
}
export function NextSkill({ course, locked, requirements = [], actionLabel, index = 1 }) {
interface NextSkillProps {
course: CourseView;
locked?: boolean;
requirements?: any;
actionLabel?: any;
index?: number;
}
export function NextSkill({ course, locked, requirements = [], actionLabel, index = 1 }: NextSkillProps) {
if (!course) return null;
const type = typeOf(course);

View File

@@ -1,3 +1,4 @@
import type { CourseView } from '@/types/views';
import React from 'react';
import { ArrowRight, Clock, Sparkles, BookOpen, ShieldCheck, Flame, Wine, Users, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils';
@@ -52,7 +53,13 @@ const MODULE_STATE = {
locked: { label: 'Locked', className: 'bg-surface-sunken text-ink-4 border-border' },
};
export function SkillCard({ course, onOpen, moduleState }) {
interface SkillCardProps {
course: CourseView;
onOpen?: (...args: any[]) => void;
moduleState?: any;
}
export function SkillCard({ course, onOpen, moduleState }: SkillCardProps) {
const status = statusOf(course);
const type = typeOf(course);
const ProofIcon = TYPE_ICON[type] || TYPE_ICON.roleplay;

View File

@@ -1,3 +1,4 @@
import type { CourseView } from '@/types/views';
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Check, Sparkles } from 'lucide-react';
@@ -21,7 +22,14 @@ import {
* Sections appear only when the record has something to put in them. Reading a
* heading with nothing under it teaches the reader to distrust the rest.
*/
export function SkillDetail({ course, open, onOpenChange, usage }) {
interface SkillDetailProps {
course: CourseView;
open?: boolean;
onOpenChange?: (...args: any[]) => void;
usage?: any;
}
export function SkillDetail({ course, open, onOpenChange, usage }: SkillDetailProps) {
if (!course) return null;
const status = statusOf(course);
@@ -168,8 +176,14 @@ export function SkillDetail({ course, open, onOpenChange, usage }) {
}
/** A titled region of the record. */
/** @param {any} props */
function Block({ title, meta, icon: Icon, children }) {
interface BlockProps {
title?: string;
meta?: any;
icon?: any;
children: React.ReactNode;
}
function Block({ title, meta, icon: Icon, children }: BlockProps) {
return (
<section className="border-t border-border pt-4 first:border-0 first:pt-0">
<div className="mb-2.5 flex items-baseline gap-2">

View File

@@ -21,6 +21,26 @@ import { SkillCard } from './SkillCard';
*/
const CARD_GRID = 'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4';
interface SkillLibraryProps {
results?: any;
total?: any;
search?: any;
onSearch?: (...args: any[]) => void;
category?: any;
onCategory?: (...args: any[]) => void;
categories?: any;
status?: any;
onStatus?: (...args: any[]) => void;
statuses?: any;
loading?: boolean;
filtered?: any;
onClear?: (...args: any[]) => void;
onOpen?: (...args: any[]) => void;
onCreate?: (...args: any[]) => void;
sectionRef?: any;
moduleStateFor?: any;
}
export function SkillLibrary({
results,
total,
@@ -39,7 +59,7 @@ export function SkillLibrary({
onCreate,
sectionRef,
moduleStateFor,
}) {
}: SkillLibraryProps) {
return (
<section aria-labelledby="skill-library" className="min-w-0" ref={sectionRef}>
<div className="flex flex-col gap-3 border-b border-border pb-3 lg:flex-row lg:items-end lg:justify-between">
@@ -147,7 +167,14 @@ export function SkillLibrary({
}
/** One radio row of filter options, styled as the Forge underline control. */
function FilterGroup({ label, options, value, onChange }) {
interface FilterGroupProps {
label?: string;
options?: any;
value?: any;
onChange?: (...args: any[]) => void;
}
function FilterGroup({ label, options, value, onChange }: FilterGroupProps) {
if (options.length <= 1) return null;
return (

View File

@@ -32,8 +32,12 @@ 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.
*/
/** @param {any} props */
export function SkillLadder({ state, className = '' }) {
interface SkillLadderProps {
state?: any;
className?: string;
}
export function SkillLadder({ state, className = '' }: SkillLadderProps) {
return (
<ol className={cn('flex items-start', className)} aria-label={`${state.name} levels`}>
{state.levels.map((rung, i) => {
@@ -88,8 +92,12 @@ 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 }) {
interface SkillProgressCardProps {
state?: any;
onOpen?: (...args: any[]) => void;
}
export function SkillProgressCard({ state, onOpen }: SkillProgressCardProps) {
const status = STATUS_META[state.status] || STATUS_META.not_started;
const atTop = !state.nextLevel;
@@ -165,8 +173,14 @@ 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 }) {
interface SkillProgressDetailProps {
state?: any;
open?: boolean;
onOpenChange?: (...args: any[]) => void;
onStartTraining?: (...args: any[]) => void;
}
export function SkillProgressDetail({ state, open, onOpenChange, onStartTraining }: SkillProgressDetailProps) {
if (!state) return null;
const done = new Set(state.levels.flatMap((l) => l.completed.map((m) => m.id)));

View File

@@ -13,7 +13,14 @@ const STEP_ITEMS = [
{ step: '04', title: 'Skill verified', sub: 'Earn verified badge', icon: ShieldCheck },
];
export function YourForge({ skills = [], evidenceCount = 0, onBrowse, formatDate }) {
interface YourForgeProps {
skills?: any;
evidenceCount?: any;
onBrowse?: (...args: any[]) => void;
formatDate?: any;
}
export function YourForge({ skills = [], evidenceCount = 0, onBrowse, formatDate }: YourForgeProps) {
const empty = skills.length === 0;
return (

View File

@@ -1,3 +1,4 @@
import type { ProfileView } from '@/types/views';
import { Camera, MessageSquare, ScanLine, Video } from 'lucide-react';
/**
@@ -44,7 +45,7 @@ export const DIFF_TONE = {
* progress so the distance to unlocking is legible rather than merely stated.
* Returns [] when a challenge has no gate.
*/
export function buildRequirements(course, profile = {}) {
export function buildRequirements(course: any, profile: Partial<ProfileView> = {}) {
const req = course?.unlock_requirements;
if (!req) return [];

View File

@@ -1,55 +1,9 @@
import type { Course, WorkerProfile } from '@/types/entities';
/**
* A worker profile as the talent components read one.
* Re-export, so the folder's own imports keep working.
*
* Five of `WorkerProfile`'s columns are `jsonb`, which the generated registry
* types `unknown` — correctly, because nothing on the backend declares what is
* inside them. These components iterate and read through all five, so they are
* narrowed here to arrays. What the elements hold is still unstated, because it
* still is; every field backed by a real column keeps the registry's type.
*
* Declared once for the folder rather than per file: it is the same profile
* travelling through a card, a modal and a hero, and three copies of the same
* narrowing would be three things to keep in step.
* `ProfileView` and `CourseView` started here and moved to `@/types/views` when
* a second folder needed them — `components/forge` reads the same `jsonb`
* columns off the same courses. Two copies of one narrowing would be two things
* to keep in step.
*/
export type ProfileView = Omit<
WorkerProfile,
'experience' | 'completed_courses' | 'earned_badges' | 'capabilities' | 'score_breakdown'
> & {
/* Required, not optional: the registry declares these columns NOT NULL, so
making them optional here would make a `ProfileView` unassignable back to a
`WorkerProfile` — which `ChallengeRunner` does when it hands one to a
mutation. Narrowing the type is the point; loosening the presence is not. */
experience: any[];
completed_courses: any[];
earned_badges: any[];
capabilities: any[];
score_breakdown: any;
/**
* NOT a column, and deliberately recorded as such.
*
* `TalentPoolCard` renders a MapPin row behind `profile.location &&`, but
* `worker_profiles` has no `location` — the registry has no such field and
* the API cannot send one, so that row has never rendered. Typed optional so
* the dead branch keeps compiling unchanged; deleting it is a product
* decision, not a migration one. Same class as `user.avatar_url`.
*/
location?: string;
};
/**
* A course as the proving-ground components read one.
*
* `challenge`, `unlock_requirements` and `quiz` are `jsonb`; the runner reads
* `challenge.type`, `.prompt` and `.image_url` to decide which challenge UI to
* show. Narrowed to `any` rather than given a shape, because the shape is not
* declared anywhere the registry can see.
*/
export type CourseView = Omit<Course, 'challenge' | 'unlock_requirements' | 'quiz'> & {
/* Required for the same reason as `ProfileView`'s: these columns are NOT NULL
in the registry, so optional here would break assignment back to `Course`. */
challenge: any;
unlock_requirements: any;
quiz: any;
};
export type { CourseView, ProfileView } from '@/types/views';