Menu

Change language

GitHub repo

Skill Assessment Radar

blockhr

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.

Installation

$npx uipkge-ng@latest add skill-assessment-radar
Manifest:https://uipkge.dev/r/angular/skill-assessment-radar.jsonInstalls to:src/app/components/blocks/skill-assessment-radar/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

DomainBenchmark
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
}
CompetencyMetric
interface CompetencyMetric {
  id: string
  name: string
  description: string
  userScore: number
  benchmarkScore: number
  masteryLevel: 'Expert' | 'Advanced' | 'Proficient'
  masteryVariant: 'success' | 'default' | 'secondary'
  deltaLabel: string
  percentile: string
}
LearningCourse
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
}
RadarAxis
interface 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

Raw manifest:https://uipkge.dev/r/angular/skill-assessment-radar.json