update position page design
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import React from 'react';
|
||||
import { BookOpen, Layers, ShieldCheck, Target } from 'lucide-react';
|
||||
import { BookOpen, Layers, ShieldCheck, Target, TrendingUp } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/**
|
||||
* Skill overview — the state of the library, in four figures.
|
||||
* Skill overview — progression, in counted figures.
|
||||
*
|
||||
* Each figure counts records that exist: skills in the library, skills carrying
|
||||
* training material, skills carrying a challenge, and verifications the
|
||||
* workforce has actually earned. Nothing here is a target or a projection, so
|
||||
* there is nothing on this strip that cannot be traced back to a row.
|
||||
* Every figure is a count of records or of states derived from them: skills on
|
||||
* the ladder, skills being worked on, levels held, levels one module away, and
|
||||
* modules queued next. Nothing here is a target or a projection, so there is
|
||||
* nothing on this strip that cannot be traced back to a row.
|
||||
*
|
||||
* The page supplies the figures and their order; this only styles them, which is
|
||||
* why adding a fifth needed no change to the layout.
|
||||
*/
|
||||
|
||||
const STAT_CONFIG = {
|
||||
@@ -19,6 +22,34 @@ const STAT_CONFIG = {
|
||||
border: 'border-blue-200/50 dark:border-blue-800/40 hover:border-blue-400',
|
||||
accent: 'from-blue-600 to-indigo-600',
|
||||
},
|
||||
'Skills in progress': {
|
||||
icon: BookOpen,
|
||||
bg: 'bg-indigo-50 text-indigo-600 dark:bg-indigo-950/60 dark:text-indigo-400',
|
||||
text: 'text-indigo-600 dark:text-indigo-400',
|
||||
border: 'border-indigo-200/50 dark:border-indigo-800/40 hover:border-indigo-400',
|
||||
accent: 'from-indigo-500 to-purple-500',
|
||||
},
|
||||
'Skills verified': {
|
||||
icon: ShieldCheck,
|
||||
bg: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-950/60 dark:text-emerald-400',
|
||||
text: 'text-emerald-600 dark:text-emerald-400',
|
||||
border: 'border-emerald-200/50 dark:border-emerald-800/40 hover:border-emerald-400',
|
||||
accent: 'from-emerald-500 to-teal-500',
|
||||
},
|
||||
'Next level ready': {
|
||||
icon: TrendingUp,
|
||||
bg: 'bg-amber-50 text-amber-600 dark:bg-amber-950/60 dark:text-amber-400',
|
||||
text: 'text-amber-600 dark:text-amber-400',
|
||||
border: 'border-amber-200/50 dark:border-amber-800/40 hover:border-amber-400',
|
||||
accent: 'from-amber-500 to-orange-500',
|
||||
},
|
||||
'Recommended training': {
|
||||
icon: Target,
|
||||
bg: 'bg-violet-50 text-violet-600 dark:bg-violet-950/60 dark:text-violet-400',
|
||||
text: 'text-violet-600 dark:text-violet-400',
|
||||
border: 'border-violet-200/50 dark:border-violet-800/40 hover:border-violet-400',
|
||||
accent: 'from-violet-500 to-fuchsia-500',
|
||||
},
|
||||
'Training modules': {
|
||||
icon: BookOpen,
|
||||
bg: 'bg-indigo-50 text-indigo-600 dark:bg-indigo-950/60 dark:text-indigo-400',
|
||||
@@ -54,8 +85,9 @@ export function ForgeProgress({ figures = [], loading = false }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 4 Modern Stat Cards */}
|
||||
<div className="grid grid-cols-2 gap-3.5 sm:grid-cols-4">
|
||||
{/* Sized off this column rather than the viewport: with Owliver open the
|
||||
page is wide while this strip is not. */}
|
||||
<div className="grid grid-cols-2 gap-3.5 [grid-template-columns:repeat(auto-fit,minmax(min(100%,9rem),1fr))]">
|
||||
{figures.map((f) => {
|
||||
const cfg = STAT_CONFIG[f.label] || STAT_CONFIG['Active skills'];
|
||||
const Icon = cfg.icon;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { ArrowRight, Clock, Sparkles, BookOpen, ShieldCheck, Flame, Wine, Users, CheckCircle2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ds';
|
||||
import { unlocksLabel } from '@/lib/skillGraph';
|
||||
import {
|
||||
DIFF_TONE, owliverCriteria, statusOf, STATUS_TONE, TYPE_ICON, TYPE_LABEL, trainingType, typeOf,
|
||||
} from './challengeMeta';
|
||||
@@ -41,7 +42,17 @@ function getCategoryMeta(category = '') {
|
||||
};
|
||||
}
|
||||
|
||||
export function SkillCard({ course, onOpen }) {
|
||||
/**
|
||||
* The state of this module for the person being viewed — the four states a
|
||||
* training card can be in, named the way a learner would name them.
|
||||
*/
|
||||
const MODULE_STATE = {
|
||||
completed: { label: 'Completed', className: 'bg-success-muted text-success border-success/30' },
|
||||
available: { label: 'Available', className: 'bg-krow-blue-tint text-krow-blue border-krow-blue/30' },
|
||||
locked: { label: 'Locked', className: 'bg-surface-sunken text-ink-4 border-border' },
|
||||
};
|
||||
|
||||
export function SkillCard({ course, onOpen, moduleState }) {
|
||||
const status = statusOf(course);
|
||||
const type = typeOf(course);
|
||||
const ProofIcon = TYPE_ICON[type] || TYPE_ICON.roleplay;
|
||||
@@ -49,6 +60,7 @@ export function SkillCard({ course, onOpen }) {
|
||||
const hasChallenge = Boolean(course.challenge);
|
||||
const catMeta = getCategoryMeta(course.category);
|
||||
const CatIcon = catMeta.icon;
|
||||
const unlocks = unlocksLabel(course);
|
||||
|
||||
const isPublished = status === 'published';
|
||||
|
||||
@@ -74,16 +86,30 @@ export function SkillCard({ course, onOpen }) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Badge
|
||||
variant={STATUS_TONE[status] || 'neutral'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'shrink-0 font-bold text-[10px] shadow-2xs',
|
||||
isPublished && 'bg-emerald-500/10 text-emerald-600 border-emerald-500/30 dark:bg-emerald-950/60 dark:text-emerald-400'
|
||||
)}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
{/* What this module is *to the reader* leads: completed, available or
|
||||
locked. Publication state is an authoring fact and only shows when
|
||||
there is no learner state to report. */}
|
||||
{moduleState ? (
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-bold',
|
||||
(MODULE_STATE[moduleState] || MODULE_STATE.available).className
|
||||
)}
|
||||
>
|
||||
{(MODULE_STATE[moduleState] || MODULE_STATE.available).label}
|
||||
</span>
|
||||
) : (
|
||||
<Badge
|
||||
variant={STATUS_TONE[status] || 'neutral'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'shrink-0 font-bold text-[10px] shadow-2xs',
|
||||
isPublished && 'bg-emerald-500/10 text-emerald-600 border-emerald-500/30 dark:bg-emerald-950/60 dark:text-emerald-400'
|
||||
)}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title & Difficulty Tag */}
|
||||
@@ -143,7 +169,18 @@ export function SkillCard({ course, onOpen }) {
|
||||
|
||||
{/* Footer Divider & Action Button */}
|
||||
<div className="relative z-10 mt-4 flex items-center justify-between gap-2 border-t border-border/60 pt-3 font-medium">
|
||||
{course.proof_skill ? (
|
||||
{/* What completing this does to your record — the reason to take it.
|
||||
"Verifies <skill>" is the fallback for library material that no level
|
||||
depends on, so those cards still say what they prove. */}
|
||||
{unlocks ? (
|
||||
<span
|
||||
title={`Unlocks ${unlocks}`}
|
||||
className="flex min-w-0 flex-1 items-center gap-1 truncate text-[10px] font-bold uppercase tracking-wider text-ink-4"
|
||||
>
|
||||
<CheckCircle2 className="h-3 w-3 shrink-0 text-emerald-500" />
|
||||
<span className="truncate">Unlocks {unlocks}</span>
|
||||
</span>
|
||||
) : course.proof_skill ? (
|
||||
<span
|
||||
title={`Verifies ${course.proof_skill}`}
|
||||
className="min-w-0 flex-1 truncate text-[10px] font-bold uppercase tracking-wider text-ink-4 flex items-center gap-1"
|
||||
|
||||
@@ -38,6 +38,7 @@ export function SkillLibrary({
|
||||
onOpen,
|
||||
onCreate,
|
||||
sectionRef,
|
||||
moduleStateFor,
|
||||
}) {
|
||||
return (
|
||||
<section aria-labelledby="skill-library" className="min-w-0" ref={sectionRef}>
|
||||
@@ -132,7 +133,12 @@ export function SkillLibrary({
|
||||
) : (
|
||||
<div className={CARD_GRID}>
|
||||
{results.map((course) => (
|
||||
<SkillCard key={course.id} course={course} onOpen={onOpen} />
|
||||
<SkillCard
|
||||
key={course.id}
|
||||
course={course}
|
||||
onOpen={onOpen}
|
||||
moduleState={moduleStateFor?.(course)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
285
src/components/forge/SkillProgression.jsx
Normal file
285
src/components/forge/SkillProgression.jsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import React from 'react';
|
||||
import { ArrowRight, Check, Circle, Lock, ShieldCheck, Sparkles } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge, Button, Drawer, ProgressBar } from '@/components/ds';
|
||||
import { levelLabel } from '@/lib/skillGraph';
|
||||
|
||||
/**
|
||||
* Skill progression — a skill as a ladder rather than a pile of courses.
|
||||
*
|
||||
* The old Forge answered "what training exists". These answer the question a
|
||||
* person actually arrives with: *where am I, and what is the next thing.* That
|
||||
* is one rail (the levels), one number (progress to the next level) and one
|
||||
* named module (the thing to do now) — and nothing else, because a progression
|
||||
* view that lists everything is a course catalogue again.
|
||||
*
|
||||
* Nothing here derives anything. `lib/skillGraph.js` computes the state and
|
||||
* these arrange it, which is what keeps the card, the drawer and the profile
|
||||
* page from ever disagreeing about someone's level.
|
||||
*/
|
||||
|
||||
const STATUS_META = {
|
||||
verified: { label: 'Verified', variant: 'success' },
|
||||
advancing: { label: 'Advancing', variant: 'soft' },
|
||||
in_progress: { label: 'In progress', variant: 'warning' },
|
||||
not_started: { label: 'Not started', variant: 'neutral' },
|
||||
};
|
||||
|
||||
/**
|
||||
* The level rail: ✓ earned, ● in progress, ○ not reached.
|
||||
*
|
||||
* Deliberately a single line. The distance between where someone is and the top
|
||||
* 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 }) {
|
||||
return (
|
||||
<ol className={cn('flex items-start', className)} aria-label={`${state.name} levels`}>
|
||||
{state.levels.map((rung, i) => {
|
||||
const earned = Boolean(rung.earned);
|
||||
const current = !earned && state.nextLevel === rung.level;
|
||||
const started = current && rung.completed.length > 0;
|
||||
|
||||
return (
|
||||
<li key={rung.level} className="flex min-w-0 flex-1 flex-col items-center gap-1.5">
|
||||
<div className="flex w-full items-center">
|
||||
{/* Connectors are drawn by the rungs either side, so the rail is
|
||||
continuous without a separate track element to keep aligned. */}
|
||||
<span className={cn('h-px flex-1', i === 0 ? 'bg-transparent' : earned ? 'bg-success' : 'bg-border')} aria-hidden="true" />
|
||||
<span
|
||||
className={cn(
|
||||
'grid h-5 w-5 shrink-0 place-items-center rounded-full border text-[10px] font-bold',
|
||||
earned && 'border-success bg-success text-white',
|
||||
current && 'border-krow-blue bg-krow-blue-tint text-krow-blue',
|
||||
!earned && !current && 'border-border bg-surface text-ink-4'
|
||||
)}
|
||||
>
|
||||
{earned ? <Check className="h-3 w-3" /> : current ? <Circle className="h-2 w-2 fill-current" /> : <Circle className="h-2 w-2" />}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'h-px flex-1',
|
||||
i === state.levels.length - 1 ? 'bg-transparent' : state.levels[i + 1]?.earned ? 'bg-success' : 'bg-border'
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
'w-full truncate text-center text-[10px] font-semibold uppercase tracking-wide',
|
||||
earned ? 'text-success' : current ? 'text-krow-blue' : 'text-ink-4'
|
||||
)}
|
||||
>
|
||||
{rung.label}
|
||||
</span>
|
||||
|
||||
{started && (
|
||||
<span className="text-[10px] tabular-nums text-ink-4">
|
||||
{rung.completed.length}/{rung.modules.length}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
/** One skill: where this person stands on it, and the next thing to do. */
|
||||
export function SkillProgressCard({ state, onOpen }) {
|
||||
const status = STATUS_META[state.status] || STATUS_META.not_started;
|
||||
const atTop = !state.nextLevel;
|
||||
|
||||
return (
|
||||
<article
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onOpen(state)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onOpen(state);
|
||||
}
|
||||
}}
|
||||
className="group flex h-full cursor-pointer flex-col rounded-2xl border border-border bg-surface p-5 text-left
|
||||
transition-[border-color,box-shadow] duration-base ease-out hover:border-krow-blue/40 hover:shadow-md
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate font-heading text-body font-semibold text-ink-1">{state.name}</h3>
|
||||
<p className="mt-0.5 flex items-center gap-1 text-caption text-ink-3">
|
||||
{state.verifiedLevel && <ShieldCheck className="h-3 w-3 text-success" aria-hidden="true" />}
|
||||
{state.verifiedLabel}
|
||||
{state.verifiedByOverride && <span className="text-ink-4">· admin verified</span>}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={status.variant} size="sm">{status.label}</Badge>
|
||||
</div>
|
||||
|
||||
<SkillLadder state={state} className="mt-4" />
|
||||
|
||||
<div className="mb-4 mt-4">
|
||||
{atTop ? (
|
||||
<p className="text-caption text-ink-3">Top of the ladder — every module verified.</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-1 flex items-baseline justify-between gap-2">
|
||||
<span className="truncate text-caption text-ink-2">Progress to {levelLabel(state.nextLevel)}</span>
|
||||
<span className="shrink-0 text-caption font-semibold tabular-nums text-ink-1">
|
||||
{state.completedInNext}/{state.requiredForNext}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar value={state.progressToNext} tone={state.nearlyThere ? 'success' : 'brand'} size="sm" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* `mt-auto` pins the footer down; the spacing above it comes from the
|
||||
block before, so the two never fight over a margin. */}
|
||||
<div className="mt-auto flex items-end justify-between gap-3 border-t border-border pt-3">
|
||||
<p className="min-w-0 text-caption text-ink-3">
|
||||
{state.nextRequirement ? (
|
||||
<>
|
||||
<span className="text-ink-4">Next</span>{' '}
|
||||
<span className="font-medium text-ink-2">{state.nextRequirement.title}</span>
|
||||
</>
|
||||
) : atTop ? 'Nothing outstanding' : 'No training authored for this level yet'}
|
||||
</p>
|
||||
<span className="inline-flex shrink-0 items-center gap-1 text-caption font-semibold text-krow-blue">
|
||||
View
|
||||
<ArrowRight className="h-3 w-3 transition-transform duration-base group-hover:translate-x-0.5" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The whole ladder, level by level, with every module and its state.
|
||||
*
|
||||
* Modules below the person's verified level are shown complete rather than
|
||||
* hidden: the evidence behind a level is the reason to trust the level, and
|
||||
* folding it away would leave the label standing on nothing.
|
||||
*/
|
||||
export function SkillProgressDetail({ state, open, onOpenChange, onStartTraining }) {
|
||||
if (!state) return null;
|
||||
|
||||
const done = new Set(state.levels.flatMap((l) => l.completed.map((m) => m.id)));
|
||||
const next = state.nextRequirement;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={`${state.name} skill`}
|
||||
description={`Current level: ${state.verifiedLabel}${state.nextLevel ? ` · Next: ${levelLabel(state.nextLevel)}` : ''}`}
|
||||
size="lg"
|
||||
footer={
|
||||
next ? (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>Close</Button>
|
||||
<Button size="sm" onClick={() => onStartTraining(next)}>Start training</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>Close</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="space-y-5">
|
||||
{/* Where they stand */}
|
||||
<div className="rounded-xl border border-border bg-surface-subtle/60 p-4">
|
||||
<SkillLadder state={state} />
|
||||
{state.nextLevel && (
|
||||
<div className="mt-4">
|
||||
<div className="mb-1 flex items-baseline justify-between">
|
||||
<span className="text-caption text-ink-2">Progress to {levelLabel(state.nextLevel)}</span>
|
||||
<span className="text-caption font-semibold tabular-nums text-ink-1">{state.progressToNext}%</span>
|
||||
</div>
|
||||
<ProgressBar value={state.progressToNext} tone="brand" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{state.verifiedByOverride && (
|
||||
<p className="mt-3 flex items-center gap-1.5 text-caption text-ink-3">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-krow-blue" aria-hidden="true" />
|
||||
{levelLabel(state.verifiedByOverride.level)} verified manually by {state.verifiedByOverride.verified_by || 'an administrator'}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* The ladder itself */}
|
||||
{state.levels.map((rung, i) => (
|
||||
<section key={rung.level}>
|
||||
<div className="mb-2 flex items-baseline justify-between gap-2">
|
||||
<h3 className="font-heading text-caption font-bold uppercase tracking-wide text-ink-4">
|
||||
Level {i + 1} — {rung.label}
|
||||
</h3>
|
||||
<span className="text-caption text-ink-4 tabular-nums">
|
||||
{rung.completed.length}/{rung.modules.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{rung.modules.length ? (
|
||||
<ul className="divide-y divide-border rounded-lg border border-border">
|
||||
{rung.modules.map((module) => {
|
||||
const complete = done.has(module.id);
|
||||
const isNext = next?.id === module.id;
|
||||
/* Locked means the rung below is unfinished — the same rule the
|
||||
level derivation uses, so nothing can look startable here and
|
||||
be refused there. */
|
||||
const locked = !complete && !isNext && i > 0 && !state.levels[i - 1].earned;
|
||||
|
||||
return (
|
||||
<li key={module.id} className="flex items-center gap-2.5 px-3 py-2.5">
|
||||
<span
|
||||
className={cn(
|
||||
'grid h-5 w-5 shrink-0 place-items-center rounded-full border',
|
||||
complete ? 'border-success bg-success text-white' : locked ? 'border-border text-ink-4' : 'border-krow-blue text-krow-blue'
|
||||
)}
|
||||
>
|
||||
{complete ? <Check className="h-3 w-3" /> : locked ? <Lock className="h-2.5 w-2.5" /> : <Circle className="h-2 w-2" />}
|
||||
</span>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={cn('truncate text-body-sm', complete ? 'text-ink-2' : 'font-medium text-ink-1')}>
|
||||
{module.title}
|
||||
</p>
|
||||
<p className="truncate text-[11px] text-ink-4">
|
||||
{module.estimated_minutes} min · {(module.verification_criteria || []).length} verification criteria
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{complete ? (
|
||||
<span className="shrink-0 text-[11px] font-semibold text-success">Verified</span>
|
||||
) : isNext ? (
|
||||
<Button size="xs" variant="outline" onClick={() => onStartTraining(module)}>Start</Button>
|
||||
) : (
|
||||
<span className="shrink-0 text-[11px] text-ink-4">{locked ? 'Locked' : 'Available'}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-body-sm text-ink-3">No training authored for this level yet.</p>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
|
||||
{next && (
|
||||
<div className="flex items-start gap-2.5 rounded-xl border border-krow-blue/30 bg-krow-blue-tint/50 p-4">
|
||||
<Sparkles className="mt-0.5 h-4 w-4 shrink-0 text-krow-blue" aria-hidden="true" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-body-sm font-semibold text-ink-1">Next milestone</p>
|
||||
<p className="text-caption text-ink-2">
|
||||
Complete {next.title} to reach {state.name} — {levelLabel(next.target_level)}.
|
||||
Owliver verifies the evidence against {(next.verification_criteria || []).length} criteria.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
* SkillLibrary every skill, filterable
|
||||
* SkillCard one skill and its verification journey
|
||||
* SkillDetail one skill read end to end
|
||||
* SkillProgression a skill as a ladder: levels, modules, and what is next
|
||||
* AddSkillTraining skill → training → proof → Owliver → publish
|
||||
* ForgeRail the authoring job, the rules, and the way to Owliver
|
||||
*
|
||||
@@ -31,6 +32,7 @@ export { ForgeProgress } from './ForgeProgress';
|
||||
export { SkillLibrary } from './SkillLibrary';
|
||||
export { SkillCard } from './SkillCard';
|
||||
export { SkillDetail } from './SkillDetail';
|
||||
export { SkillLadder, SkillProgressCard, SkillProgressDetail } from './SkillProgression';
|
||||
export { AddSkillTraining } from './AddSkillTraining';
|
||||
export { ForgeRail } from './ForgeRail';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user