Skill Assessment Radar
blockhrDeveloper skill proficiency scorecard and radar matrix: domain percentile rankings, dual candidate vs industry benchmark progress bars, competency breakdown, radar telemetry polygon, and personalized learning pathways.
Installation
$pnpm dlx uipkge-ng@latest add skill-assessment-radar$npx uipkge-ng@latest add skill-assessment-radar$yarn dlx uipkge-ng@latest add skill-assessment-radar$bunx uipkge-ng@latest add skill-assessment-radarManifest:
https://uipkge.dev/r/angular/skill-assessment-radar.jsonInstalls to:src/app/components/blocks/skill-assessment-radar/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
DomainBenchmarkinterface DomainBenchmark {
id: string
title: string
score: number
percentile: string
percentileLabel: string
benchmarkScore: number
delta: string
description: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
icon: any
accentColor: string
}CompetencyMetricinterface CompetencyMetric {
id: string
name: string
description: string
userScore: number
benchmarkScore: number
masteryLevel: 'Expert' | 'Advanced' | 'Proficient'
masteryVariant: 'success' | 'default' | 'secondary'
deltaLabel: string
percentile: string
}LearningCourseinterface LearningCourse {
id: string
gapTarget: string
currentScore: string
targetScore: string
title: string
provider: string
duration: string
priority: 'High' | 'Medium' | 'Refinement'
priorityVariant: 'warning' | 'default' | 'secondary'
syllabus: string[]
buttonText: string
}RadarAxisinterface RadarAxis {
label: string
shortLabel: string
userScore: number
benchmarkScore: number
}Files installed (2)
src/app/components/blocks/skill-assessment-radar/skill-assessment-radar.component.ts34.3 kB
// Developer skill proficiency scorecard and radar matrix: domain percentile rankings, // dual candidate vs industry benchmark progress bars, competency breakdown, radar // telemetry polygon, and personalized learning pathways. Port of the Vue/React block 1:1. import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { Award, CheckCircle2, Code2, Compass, Eye, FileDown, GraduationCap, Layers, LucideAngularModule, Play, Share2, ShieldCheck, Target, TrendingUp, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' interface DomainBenchmark { id: string title: string score: number percentile: string percentileLabel: string benchmarkScore: number delta: string description: string // eslint-disable-next-line @typescript-eslint/no-explicit-any icon: any accentColor: string } interface CompetencyMetric { id: string name: string description: string userScore: number benchmarkScore: number masteryLevel: 'Expert' | 'Advanced' | 'Proficient' masteryVariant: 'success' | 'default' | 'secondary' deltaLabel: string percentile: string } interface LearningCourse { id: string gapTarget: string currentScore: string targetScore: string title: string provider: string duration: string priority: 'High' | 'Medium' | 'Refinement' priorityVariant: 'warning' | 'default' | 'secondary' syllabus: string[] buttonText: string } interface RadarAxis { label: string shortLabel: string userScore: number benchmarkScore: number } const domainBenchmarks: DomainBenchmark[] = [ { id: 'frontend-architecture', title: 'Frontend Architecture', score: 92, percentile: 'Top 5% percentile', percentileLabel: '95th Percentile', benchmarkScore: 74, delta: '+18% vs Benchmark', description: 'Component decoupling, polymorphic primitives, headless state machines, and micro-frontend isolation.', icon: Layers, accentColor: 'text-success', }, { id: 'typescript-typing', title: 'TypeScript & Typing', score: 88, percentile: 'Top 8% percentile', percentileLabel: '92nd Percentile', benchmarkScore: 68, delta: '+20% vs Benchmark', description: 'Advanced mapped types, conditional generics, discriminated unions, and compile-time contract enforcement.', icon: Code2, accentColor: 'text-info', }, { id: 'accessibility-wcag', title: 'Web Accessibility & WCAG', score: 95, percentile: 'Top 2% percentile', percentileLabel: '98th Percentile', benchmarkScore: 61, delta: '+34% vs Benchmark', description: 'WCAG 2.2 AAA compliance, keyboard focus rings, ARIA live regions, and screen reader UX verification.', icon: Eye, accentColor: 'text-chart-1', }, { id: 'performance-vitals', title: 'Performance & Core Web Vitals', score: 82, percentile: 'Top 14% percentile', percentileLabel: '86th Percentile', benchmarkScore: 65, delta: '+17% vs Benchmark', description: 'Sub-50ms INP optimization, bundle treeshaking, zero-layout-shift rendering, and island hydration.', icon: Zap, accentColor: 'text-warning', }, ] const competencyBreakdown: CompetencyMetric[] = [ { id: 'reka-primitives', name: 'Polymorphic Reka Primitives', description: 'Polymorphic asChild composition, headless accessibility, slot delegation, and forwardRef contracts.', userScore: 94, benchmarkScore: 70, masteryLevel: 'Expert', masteryVariant: 'success', deltaLabel: '+24% Lead', percentile: '96th percentile', }, { id: 'oklch-tokens', name: 'OKLCH Token Systems', description: 'Perceptually uniform color spaces, light/dark contrast mapping, and dynamic theme token cascades.', userScore: 96, benchmarkScore: 64, masteryLevel: 'Expert', masteryVariant: 'success', deltaLabel: '+32% Lead', percentile: '99th percentile', }, { id: 'ssr-hydration', name: 'SSR & Hydration Safety', description: 'Isomorphic DOM execution, hydration mismatch prevention, and cookie-aware theme providers.', userScore: 86, benchmarkScore: 68, masteryLevel: 'Advanced', masteryVariant: 'default', deltaLabel: '+18% Lead', percentile: '88th percentile', }, { id: 'headless-state', name: 'Headless State Machines', description: 'Deterministic state transitions, keyboard navigation focus rings, and roving tabindex traps.', userScore: 90, benchmarkScore: 62, masteryLevel: 'Expert', masteryVariant: 'success', deltaLabel: '+28% Lead', percentile: '93rd percentile', }, { id: 'design-governance', name: 'Design System Governance', description: 'Component deprecation lifecycles, semantic token versioning, and zero-dependency registry boundaries.', userScore: 88, benchmarkScore: 72, masteryLevel: 'Advanced', masteryVariant: 'default', deltaLabel: '+16% Lead', percentile: '91st percentile', }, { id: 'micro-frontends', name: 'Micro-Frontend Routing', description: 'Module federation, dynamic remote isolation, and zero-downtime micro-app orchestration.', userScore: 78, benchmarkScore: 66, masteryLevel: 'Proficient', masteryVariant: 'secondary', deltaLabel: '+12% Lead', percentile: '79th percentile', }, ] const radarAxes: RadarAxis[] = [ { label: 'Frontend Architecture', shortLabel: 'Architecture', userScore: 92, benchmarkScore: 74 }, { label: 'TypeScript & Typing', shortLabel: 'TypeScript', userScore: 88, benchmarkScore: 68 }, { label: 'Web Accessibility & WCAG', shortLabel: 'Accessibility', userScore: 95, benchmarkScore: 61 }, { label: 'Performance & Vitals', shortLabel: 'Performance', userScore: 82, benchmarkScore: 65 }, { label: 'Headless State Machines', shortLabel: 'State Machines', userScore: 90, benchmarkScore: 62 }, { label: 'Design System Governance', shortLabel: 'Governance', userScore: 88, benchmarkScore: 72 }, ] const learningCourses: LearningCourse[] = [ { id: 'course-1', gapTarget: 'Micro-Frontend Routing', currentScore: '78%', targetScore: '95%', title: 'Enterprise Module Federation & Micro-App Orchestration', provider: 'UIPKGE Systems Architecture Guild', duration: '4.5 Hours · 6 Deep-Dive Labs', priority: 'High', priorityVariant: 'warning', syllabus: [ 'Bidirectional dependency sharing & version collision isolation', 'Cross-micro-app state synchronization without global window leakage', 'Canary deployment pipelines with zero-downtime remote rollbacks', ], buttonText: 'Enroll Pathway', }, { id: 'course-2', gapTarget: 'Performance & Core Web Vitals', currentScore: '82%', targetScore: '96%', title: 'Sub-50ms Interaction to Next Paint (INP) & Chrome Profiling', provider: 'Web Performance & Engine Guild', duration: '3.0 Hours · 4 Profile Traces', priority: 'Medium', priorityVariant: 'default', syllabus: [ 'Main-thread cooperative yielding using scheduler.yield() primitives', 'Compositor layer diagnostics & forced synchronous layout avoidance', 'Fine-grained virtualized rendering and selective component reconciliation', ], buttonText: 'Start Module', }, { id: 'course-3', gapTarget: 'SSR & Hydration Safety', currentScore: '86%', targetScore: '98%', title: 'Isomorphic State Engines & Streaming SSR Mechanics', provider: 'Modern Frontend Systems Lab', duration: '5.0 Hours · 8 Architecture Blueprints', priority: 'Refinement', priorityVariant: 'secondary', syllabus: [ 'Zero-mismatch server state serialization & client reconciliation', 'Streaming HTML chunking & out-of-order Suspense boundaries', 'Edge worker execution models and cookie-aware cache sharding', ], buttonText: 'Explore Syllabus', }, ] const radarCenter = { x: 180, y: 160 } const radarRadius = 105 const axisLabels = [ { index: 0, x: 180, y: 32, anchor: 'middle' }, { index: 1, x: 286, y: 104, anchor: 'start' }, { index: 2, x: 286, y: 224, anchor: 'start' }, { index: 3, x: 180, y: 294, anchor: 'middle' }, { index: 4, x: 74, y: 224, anchor: 'end' }, { index: 5, x: 74, y: 104, anchor: 'end' }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-skill-assessment-radar, [ui-skill-assessment-radar]', standalone: true, host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiAvatarImageComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiSeparatorComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="skill-assessment-radar" [class]="rootClass"> <!-- Header: Profile Summary, Assessment Date, Overall Score & PDF Download --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-content class="p-6"> <div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between"> <div class="flex flex-col gap-4 sm:flex-row sm:items-center"> <ui-avatar class="border-border size-16 shrink-0 border sm:size-20"> <ui-avatar-image src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200&auto=format&fit=crop&q=80" alt="Elena Rostova" /> <ui-avatar-fallback>ER</ui-avatar-fallback> </ui-avatar> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2"> <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Elena Rostova</h2> <span ui-badge variant="secondary" class="font-medium">Senior Frontend Engineer</span> </div> <p class="text-muted-foreground text-sm"> Platform Architecture · Design Systems & Headless UI Specialization </p> <div class="flex flex-wrap items-center gap-3 pt-1"> <span ui-badge variant="outline" class="gap-1.5 text-xs font-normal"> <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" /> Verified on Aug 21, 2026 </span> <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" /> <span class="text-muted-foreground text-xs"> Proctored Evaluation ID: <strong class="text-foreground font-medium tabular-nums">#UIP-8849-FE</strong> </span> </div> </div> </div> <div class="flex flex-col gap-3 sm:flex-row sm:items-center lg:flex-col lg:items-end"> <div class="flex flex-col items-start gap-1 lg:items-end"> <span ui-badge variant="success" class="gap-1.5 px-3.5 py-1.5 text-sm font-semibold shadow-xs"> <lucide-icon [img]="Award" class="size-4 shrink-0" /> <span>Level 4 - Expert · 88 / 100</span> </span> <span class="text-muted-foreground text-xs font-medium"> Top 4% Global Engineering Percentile (n = 14,820) </span> </div> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="outline" size="sm" class="gap-2 shadow-xs"> <lucide-icon [img]="FileDown" class="size-4" /> <span>Download Skill Transcript PDF</span> </button> <button ui-button variant="ghost" size="icon-sm" class="text-muted-foreground hover:text-foreground size-8" aria-label="Share skill transcript verification link" > <lucide-icon [img]="Share2" class="size-4" /> </button> </div> </div> </div> </div> </div> <!-- 4 Domain Benchmark Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> @for (domain of domains; track domain.id) { <div ui-card class="border-border bg-card flex flex-col justify-between shadow-xs"> <div ui-card-header class="pb-2"> <div class="flex items-center justify-between gap-2"> <div class="flex min-w-0 items-center gap-2"> <div class="bg-muted border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs" aria-hidden="true" > <lucide-icon [img]="domain.icon" [class]="'size-4 ' + domain.accentColor" /> </div> <h3 ui-card-title class="text-muted-foreground truncate text-xs font-medium">{{ domain.title }}</h3> </div> <span ui-badge variant="success" class="shrink-0 text-xs font-medium">{{ domain.percentile }}</span> </div> </div> <div ui-card-content class="space-y-3 pt-1"> <div> <div class="mb-1.5 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5"> <div class="flex items-baseline gap-1.5"> <span class="text-foreground text-2xl font-bold tracking-tight tabular-nums sm:text-3xl"> {{ domain.score }}% </span> <span class="text-muted-foreground text-xs font-normal">Proficiency</span> </div> <div class="text-success flex items-center gap-1 text-xs font-medium"> <lucide-icon [img]="TrendingUp" class="size-3" /> <span class="tabular-nums">{{ domain.delta }}</span> </div> </div> <ui-progress [value]="domain.score" class="h-1.5 w-full" /> </div> <div class="border-border/60 border-t pt-2.5 text-xs"> <div class="text-muted-foreground flex items-center justify-between gap-x-2"> <span>Senior Cohort Benchmark:</span> <span class="text-foreground font-semibold tabular-nums">{{ domain.benchmarkScore }}%</span> </div> <p class="text-muted-foreground mt-1 line-clamp-2 text-xs leading-relaxed">{{ domain.description }}</p> </div> </div> </div> } </div> <!-- Skill Radar Chart & Telemetry Synthesis Strip --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <div ui-card class="border-border bg-card shadow-xs lg:col-span-7"> <div ui-card-header class="pb-2"> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-base font-semibold">Proficiency Radar Telemetry</h3> <p ui-card-description class="text-xs"> Multi-axis radial polygon comparing candidate proficiency against senior baseline across 6 dimensions. </p> </div> <span ui-badge variant="outline" class="w-fit gap-1 text-xs font-normal"> <lucide-icon [img]="Compass" class="text-primary size-3" /> <span>6 Axis Calibration</span> </span> </div> </div> <div ui-card-content class="pt-2"> <div class="relative flex w-full items-center justify-center overflow-hidden py-2"> <svg class="h-72 w-full max-w-md overflow-visible sm:h-80" viewBox="0 0 360 320" aria-label="Radar chart showing candidate scores vs industry benchmark across 6 competencies" > <defs> <radialGradient id="candidate-grad-ng" cx="50%" cy="50%" r="50%"> <stop offset="0%" stop-color="var(--color-emerald-500, #10b981)" stop-opacity="0.35" /> <stop offset="100%" stop-color="var(--color-emerald-500, #10b981)" stop-opacity="0.10" /> </radialGradient> </defs> @for (ring of gridRings; track ring) { <polygon [attr.points]="ringPolygon(ring)" fill="none" stroke="currentColor" class="text-border/60" [attr.stroke-width]="ring === 1 ? '1.5' : '1'" [attr.stroke-dasharray]="ring === 1 ? 'none' : '3 3'" /> } @for (axis of axes; track axis.label; let i = $index) { <line [attr.x1]="center.x" [attr.y1]="center.y" [attr.x2]="axisPoint(i, 100).x" [attr.y2]="axisPoint(i, 100).y" stroke="currentColor" class="text-border/60" stroke-width="1" /> } <text x="184" y="130" class="fill-muted-foreground text-xs tabular-nums">40%</text> <text x="184" y="86" class="fill-muted-foreground text-xs tabular-nums">80%</text> <polygon [attr.points]="benchmarkPolygon" fill="currentColor" class="text-muted-foreground/10" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 4" /> <polygon [attr.points]="candidatePolygon" fill="url(#candidate-grad-ng)" stroke="var(--color-emerald-500, #10b981)" stroke-width="2.5" stroke-linejoin="round" /> @for (score of candidateScores; track $index; let i = $index) { <circle [attr.cx]="axisPoint(i, score).x" [attr.cy]="axisPoint(i, score).y" r="3.5" class="stroke-background fill-success" stroke-width="1.5" /> } @for (label of labels; track label.index) { <text [attr.x]="label.x" [attr.y]="label.y" [attr.text-anchor]="label.anchor" class="fill-foreground text-xs font-medium" > {{ axes[label.index]!.shortLabel }} <tspan class="dark:fill-success fill-emerald-600 font-semibold tabular-nums"> ({{ axes[label.index]!.userScore }}%) </tspan> </text> } </svg> </div> <div class="border-border/60 mt-3 flex flex-wrap items-center justify-center gap-6 border-t pt-3 text-xs"> <div class="flex flex-wrap items-center gap-2"> <span class="bg-success size-3 rounded-full shadow-xs"></span> <span class="text-foreground font-medium">Candidate Score: Elena Rostova (88.8% Avg)</span> </div> <div class="flex flex-wrap items-center gap-2"> <span class="border-muted-foreground/60 bg-muted size-3 rounded-full border border-dashed"></span> <span class="text-muted-foreground">Industry Senior Cohort Baseline (67.0% Avg)</span> </div> </div> </div> </div> <div ui-card class="border-border bg-card flex flex-col justify-between shadow-xs lg:col-span-5"> <div ui-card-header class="pb-3"> <div class="flex items-center justify-between gap-x-2"> <h3 ui-card-title class="text-base font-semibold">Percentile & Calibration Report</h3> <span ui-badge variant="outline" class="gap-1 text-xs font-normal"> <lucide-icon [img]="Award" class="text-success size-3.5" /> <span>Staff Ready</span> </span> </div> <p ui-card-description class="text-xs"> Synthesized assessment diagnostic based on 12 proctored test suites and coding scenarios. </p> </div> <div ui-card-content class="space-y-4"> <div class="space-y-2.5"> <div class="bg-muted/40 border-border/80 flex items-center justify-between gap-x-2 rounded-lg border p-3 text-xs" > <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="Target" class="text-primary size-4 shrink-0" /> <span class="text-muted-foreground font-medium">Highest Skill Domain:</span> </div> <span class="text-foreground font-semibold">Web Accessibility (95%) · Top 2%</span> </div> <div class="bg-muted/40 border-border/80 flex items-center justify-between gap-x-2 rounded-lg border p-3 text-xs" > <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="Compass" class="text-warning size-4 shrink-0" /> <span class="text-muted-foreground font-medium">Primary Growth Delta:</span> </div> <span class="text-foreground font-semibold">Micro-Frontends (78%) · Top 21%</span> </div> <div class="bg-muted/40 border-border/80 flex items-center justify-between gap-x-2 rounded-lg border p-3 text-xs" > <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="ShieldCheck" class="text-success size-4 shrink-0" /> <span class="text-muted-foreground font-medium">Proctor Verification Integrity:</span> </div> <span class="text-success font-semibold tabular-nums">99.4% Full Trust</span> </div> </div> <div class="border-border/60 space-y-2 border-t pt-3"> <div class="flex items-center justify-between gap-x-2 text-xs"> <span class="text-muted-foreground font-medium">Global Percentile Placement</span> <span class="text-foreground font-bold tabular-nums">96th Percentile</span> </div> <div class="bg-muted border-border/80 relative h-3 w-full overflow-hidden rounded-full border"> <div class="bg-muted-foreground/15 absolute inset-y-0 left-0 w-1/2"></div> <div class="bg-info/20 absolute inset-y-0 left-1/2 w-[30%]"></div> <div class="bg-success/30 absolute inset-y-0 right-0 w-[20%]"></div> <div class="bg-success ring-success/40 absolute top-1/2 size-2.5 -translate-y-1/2 rounded-full shadow-xs ring-2" style="left: 96%" ></div> </div> <div class="text-muted-foreground flex items-center justify-between gap-x-2 text-xs tabular-nums"> <span>50th (Mid)</span> <span>80th (Senior)</span> <span class="text-success font-bold">96th (Elena)</span> <span>99th (Staff)</span> </div> </div> <div class="bg-muted/30 border-border text-muted-foreground rounded-lg border p-3 text-xs leading-relaxed"> Elena demonstrated outstanding mastery in accessibility tree construction, OKLCH theme token isolation, and deterministic state management. Enrolling in the Micro-Frontend pathway will bridge remaining Staff-level requirements. </div> </div> </div> </div> <!-- Competency Breakdown Multi-Bar Matrix --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-base font-semibold">Competency Breakdown Multi-Bar Matrix</h3> <p ui-card-description class="text-xs"> Direct comparison of candidate skill scores against industry senior baseline with mastery classifications. </p> </div> <span ui-badge variant="outline" class="w-fit text-xs font-normal tabular-nums"> 6 Technical Competencies Evaluated </span> </div> </div> <div ui-card-content class="space-y-4"> <div class="border-border overflow-hidden rounded-lg border"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header class="bg-muted/50"> <tr ui-table-row> <th ui-table-head class="text-xs">Competency & Technical Scope</th> <th ui-table-head class="w-28 text-center text-xs">Mastery Level</th> <th ui-table-head class="w-64 text-xs">Candidate vs Industry Benchmark</th> <th ui-table-head class="w-28 text-right text-xs">Score Delta</th> </tr> </thead> <tbody ui-table-body> @for (comp of competencies; track comp.id) { <tr ui-table-row> <td ui-table-cell class="py-3.5"> <div class="space-y-0.5"> <p class="text-foreground text-sm font-semibold">{{ comp.name }}</p> <p class="text-muted-foreground line-clamp-1 text-xs">{{ comp.description }}</p> </div> </td> <td ui-table-cell class="py-3.5 text-center"> <span ui-badge [variant]="comp.masteryVariant" class="text-xs font-medium"> {{ comp.masteryLevel }} </span> </td> <td ui-table-cell class="py-3.5"> <div class="space-y-1.5"> <div class="space-y-1"> <div class="flex items-center justify-between gap-x-2 text-xs"> <span class="text-muted-foreground font-medium">Elena:</span> <span class="text-foreground font-bold tabular-nums">{{ comp.userScore }}%</span> </div> <ui-progress [value]="comp.userScore" class="[&_[data-slot=progress-indicator]]:bg-success h-1.5 w-full" /> </div> <div class="space-y-1"> <div class="flex items-center justify-between gap-x-2 text-xs"> <span class="text-muted-foreground">Benchmark:</span> <span class="text-muted-foreground font-medium tabular-nums" >{{ comp.benchmarkScore }}%</span > </div> <ui-progress [value]="comp.benchmarkScore" class="bg-muted-foreground/15 [&_[data-slot=progress-indicator]]:bg-muted-foreground/60 h-1 w-full" /> </div> </div> </td> <td ui-table-cell class="py-3.5 text-right"> <div class="flex flex-col items-end gap-0.5"> <span ui-badge variant="outline" class="text-success font-semibold tabular-nums"> {{ comp.deltaLabel }} </span> <span class="text-muted-foreground text-xs tabular-nums">{{ comp.percentile }}</span> </div> </td> </tr> } </tbody> </table> </div> </div> </div> </div> <!-- Recommended Next Courses & Skill Gaps Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-0.5"> <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="GraduationCap" class="text-primary size-5" /> <h3 ui-card-title class="text-base font-semibold"> Recommended Next Courses & Personalized Learning Pathways </h3> </div> <p ui-card-description class="text-xs"> Targeted modules calibrated to close identified skill gaps and elevate proficiency into the Top 1% Staff Master tier. </p> </div> <span ui-badge variant="secondary" class="w-fit text-xs font-normal">3 High-Impact Pathways</span> </div> </div> <div ui-card-content> <div class="grid grid-cols-1 gap-4 lg:grid-cols-3"> @for (course of courses; track course.id) { <div class="bg-muted/30 border-border hover:border-border/80 flex flex-col justify-between space-y-4 rounded-lg border p-4.5 transition-colors" > <div class="space-y-3"> <div class="flex items-center justify-between gap-2"> <span ui-badge [variant]="course.priorityVariant === 'warning' ? 'warning' : 'outline'" class="text-xs font-normal" > Gap Target: {{ course.gapTarget }} </span> <span class="text-muted-foreground text-xs font-medium tabular-nums"> {{ course.currentScore }} → <strong class="text-foreground">{{ course.targetScore }}</strong> </span> </div> <div class="space-y-1"> <h3 class="text-foreground text-sm leading-snug font-semibold">{{ course.title }}</h3> <p class="text-muted-foreground text-xs">{{ course.provider }} · {{ course.duration }}</p> </div> <ui-separator /> <div class="space-y-2"> <span class="text-muted-foreground text-xs font-medium">Core Learning Objectives:</span> <ul class="text-muted-foreground space-y-1.5 text-xs"> @for (item of course.syllabus; track $index) { <li class="flex items-start gap-1.5"> <lucide-icon [img]="CheckCircle2" class="text-primary mt-0.5 size-3.5 shrink-0" /> <span class="leading-relaxed">{{ item }}</span> </li> } </ul> </div> </div> <div class="border-border/60 border-t pt-3"> <button ui-button [variant]="course.priorityVariant === 'warning' ? 'default' : 'outline'" size="sm" class="w-full gap-2 shadow-xs" > <lucide-icon [img]="Play" class="size-3.5" /> <span>{{ course.buttonText }}</span> </button> </div> </div> } </div> </div> </div> </div> `, }) export class UiSkillAssessmentRadarComponent { protected readonly Award = Award protected readonly CheckCircle2 = CheckCircle2 protected readonly Compass = Compass protected readonly FileDown = FileDown protected readonly GraduationCap = GraduationCap protected readonly Play = Play protected readonly Share2 = Share2 protected readonly ShieldCheck = ShieldCheck protected readonly Target = Target protected readonly TrendingUp = TrendingUp @Input('class') className?: string readonly domains = domainBenchmarks readonly competencies = competencyBreakdown readonly courses = learningCourses readonly axes = radarAxes readonly labels = axisLabels readonly gridRings = [0.2, 0.4, 0.6, 0.8, 1.0] readonly center = radarCenter readonly candidateScores = radarAxes.map((a) => a.userScore) readonly benchmarkScores = radarAxes.map((a) => a.benchmarkScore) readonly candidatePolygon = this.polygonString(this.candidateScores) readonly benchmarkPolygon = this.polygonString(this.benchmarkScores) get rootClass(): string { return cn('w-full space-y-6', this.className) } axisPoint(index: number, score: number, maxScore = 100): { x: number; y: number } { const total = this.axes.length const angle = -Math.PI / 2 + (index * 2 * Math.PI) / total const ratio = Math.min(1, Math.max(0, score / maxScore)) const r = radarRadius * ratio return { x: this.center.x + r * Math.cos(angle), y: this.center.y + r * Math.sin(angle) } } polygonString(scores: number[]): string { return scores .map((score, index) => { const pt = this.axisPoint(index, score) return `${pt.x.toFixed(1)},${pt.y.toFixed(1)}` }) .join(' ') } ringPolygon(ringRatio: number): string { return this.axes .map((_, index) => { const pt = this.axisPoint(index, ringRatio * 100) return `${pt.x.toFixed(1)},${pt.y.toFixed(1)}` }) .join(' ') } }src/app/components/blocks/skill-assessment-radar/index.ts0.1 kB
export { UiSkillAssessmentRadarComponent } from './skill-assessment-radar.component'
Raw manifest:https://uipkge.dev/r/angular/skill-assessment-radar.json