UIPackage
Menu

Framework

Change language

Boilerplate repo

Decision Matrix Table

blockproductivity

Product and architectural decision matrix with dynamic weighted scoring algorithms (Impact, Confidence, Effort, Risk), RICE framework toggle, interactive weight sliders, option creator, multi-dimensional SVG tradeoff radar, and ADR consensus breakdown.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/decision-matrix-table.json
Named registry:npx shadcn-vue@latest add @uipkge/decision-matrix-tableInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
classHTMLAttributes['class']optional
titlestring'Architectural Decision Matrix (RICE / Weighted Scoring)'optional
subtitlestring'Evaluate technical options against weighted business and…optional
defaultFramework
'weighted-matrix''rice'
'weighted-matrix'optional

Schema

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

CriterionDef
interface CriterionDef {
  key: 'impact' | 'confidence' | 'effort' | 'dx' | 'maintenance'
  label: string
  shortLabel: string
  description: string
  defaultWeight: number
  isInverted?: boolean
}
MatrixOption
interface MatrixOption {
  id: string
  name: string
  architecture: string
  summary: string
  keyRisk: string
  color: string
  pros: string[]
  cons: string[]
  scores: {
    impact: number // 1 to 5 (Engineering Impact)
    confidence: number // 1 to 5 (Confidence / Feasibility)
    effort: number // 1 to 5 (Implementation Simplicity / Low Effort)
    dx: number // 1 to 5 (DX & Consumer Ownership)
    maintenance: number // 1 to 5 (Low Maintenance Overhead)
  }
  rice: {
    reach: number // 0 to 100 (% of engineers / services)
    impact: number // 1 to 5 multiplier
    confidence: number // 50 to 100 (%)
    effort: number // 1 to 5 person-sprints
  }
  isCustom?: boolean
}

Files installed (1)

  • app/components/blocks/DecisionMatrixTable.vue44.8 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { computed, ref } from 'vue'
    import {
      AlertCircle,
      Award,
      BarChart3,
      Check,
      CheckCircle2,
      ChevronDown,
      ChevronUp,
      FileCode2,
      HelpCircle,
      Info,
      Plus,
      RefreshCw,
      Scale,
      ShieldCheck,
      SlidersHorizontal,
      Trash2,
      XCircle,
      PlusCircle,
    } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
    import { Input } from '@/components/ui/input'
    import { Separator } from '@/components/ui/separator'
    import { Slider } from '@/components/ui/slider'
    import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
    
    export type FrameworkType = 'weighted-matrix' | 'rice'
    
    interface Props {
      class?: HTMLAttributes['class']
      title?: string
      subtitle?: string
      defaultFramework?: FrameworkType
    }
    
    const props = withDefaults(defineProps<Props>(), {
      title: 'Architectural Decision Matrix (RICE / Weighted Scoring)',
      subtitle: 'Evaluate technical options against weighted business and engineering criteria.',
      defaultFramework: 'weighted-matrix',
    })
    
    export interface CriterionDef {
      key: 'impact' | 'confidence' | 'effort' | 'dx' | 'maintenance'
      label: string
      shortLabel: string
      description: string
      defaultWeight: number
      isInverted?: boolean
    }
    
    export interface MatrixOption {
      id: string
      name: string
      architecture: string
      summary: string
      keyRisk: string
      color: string
      pros: string[]
      cons: string[]
      scores: {
        impact: number // 1 to 5 (Engineering Impact)
        confidence: number // 1 to 5 (Confidence / Feasibility)
        effort: number // 1 to 5 (Implementation Simplicity / Low Effort)
        dx: number // 1 to 5 (DX & Consumer Ownership)
        maintenance: number // 1 to 5 (Low Maintenance Overhead)
      }
      rice: {
        reach: number // 0 to 100 (% of engineers / services)
        impact: number // 1 to 5 multiplier
        confidence: number // 50 to 100 (%)
        effort: number // 1 to 5 person-sprints
      }
      isCustom?: boolean
    }
    
    const activeFramework = ref<FrameworkType>(props.defaultFramework)
    const showWeightEditor = ref(false)
    const showAddOptionForm = ref(false)
    const selectedOptionId = ref<string>('opt-a')
    
    const criteriaList: CriterionDef[] = [
      {
        key: 'impact',
        label: 'Engineering Impact',
        shortLabel: 'Impact',
        description: 'System modularity, bundle footprint reduction, and runtime performance gains.',
        defaultWeight: 25,
      },
      {
        key: 'confidence',
        label: 'Confidence / Feasibility',
        shortLabel: 'Confidence',
        description: 'Ecosystem support, library maturity, and developer team readiness.',
        defaultWeight: 25,
      },
      {
        key: 'effort',
        label: 'Simplicity (Low Effort)',
        shortLabel: 'Low Effort',
        description: 'Frictionless adoption curve, zero monolith refactors, fast time to market.',
        defaultWeight: 15,
      },
      {
        key: 'dx',
        label: 'DX & Consumer Ownership',
        shortLabel: 'DX Ownership',
        description: 'Full source access, effortless Tailwind styling customization, zero semver lock-in.',
        defaultWeight: 20,
      },
      {
        key: 'maintenance',
        label: 'Low Maintenance',
        shortLabel: 'Low Maint.',
        description: 'Decoupled component lifecycles, zero breaking upstream transitive dependency cascades.',
        defaultWeight: 15,
      },
    ]
    
    const criterionWeights = ref<Record<string, number>>({
      impact: 25,
      confidence: 25,
      effort: 15,
      dx: 20,
      maintenance: 15,
    })
    
    const defaultOptions: MatrixOption[] = [
      {
        id: 'opt-a',
        name: 'Option A: Unbundled Registry architecture',
        architecture: 'UIPKGE / shadcn Model',
        summary:
          'Components copy directly into consumer source tree. Zero semver locks, complete styling autonomy, and unconstrained design customization.',
        keyRisk: 'Downstream drift from upstream patches managed through automated registry diffing.',
        color: '#10b981',
        pros: [
          'Zero npm package version lock-in or dependency gridlock',
          'Immediate Tailwind class-level and DOM customization',
          '100% dead-code elimination and optimal bundle footprint',
        ],
        cons: ['Manual or CLI-assisted updates when upstream security patches land'],
        scores: {
          impact: 4.8,
          confidence: 4.7,
          effort: 4.5,
          dx: 5.0,
          maintenance: 4.4,
        },
        rice: {
          reach: 96,
          impact: 5.0,
          confidence: 95,
          effort: 1.8,
        },
      },
      {
        id: 'opt-b',
        name: 'Option B: Monolithic Multi-Framework NPM',
        architecture: 'Centralized Monorepo Package',
        summary:
          'Single heavy versioned library published to private registry. Standardized tokens but painful breaking version cascades across product suites.',
        keyRisk: 'High cross-team dependency coupling and blocked semver upgrades.',
        color: '#f59e0b',
        pros: [
          'Single point of governance for security and accessibility patches',
          'Familiar `npm install` workflow for junior developers',
        ],
        cons: [
          'Massive wrapper overhead and difficult Tailwind class overrides',
          'Breaking changes in one component delay releases for entire company',
          'Bloated runtime JS bundle containing unused component code',
        ],
        scores: {
          impact: 3.2,
          confidence: 4.1,
          effort: 3.8,
          dx: 2.3,
          maintenance: 2.6,
        },
        rice: {
          reach: 82,
          impact: 3.0,
          confidence: 85,
          effort: 3.6,
        },
      },
      {
        id: 'opt-c',
        name: 'Option C: Custom Web Components Wrapper',
        architecture: 'Custom Elements v1 & Shadow DOM',
        summary:
          'Framework-agnostic web components encapsulated in Shadow DOM. Cross-platform runtime interoperability, but major SSR hydration and styling friction.',
        keyRisk: 'SSR rendering latency and challenging CSS theme token propagation through shadow boundaries.',
        color: '#6366f1',
        pros: [
          'Universal encapsulation across Vue, React, Angular, and vanilla HTML',
          'Strict CSS isolation prevents accidental style collisions',
        ],
        cons: [
          'Complex SSR and Declarative Shadow DOM hydration mechanics',
          'Difficult theme token synchronization and slot styling gymnastics',
          'Suboptimal ergonomics with React synthetic event systems',
        ],
        scores: {
          impact: 3.7,
          confidence: 3.4,
          effort: 2.8,
          dx: 3.1,
          maintenance: 3.5,
        },
        rice: {
          reach: 90,
          impact: 3.5,
          confidence: 70,
          effort: 3.2,
        },
      },
      {
        id: 'opt-d',
        name: 'Option D: Micro-Frontend Module Federation',
        architecture: 'Webpack / Vite Runtime Federation',
        summary:
          'Dynamic runtime module stitching over HTTP. Independent team deployments, but vulnerable to CSS leakage, network latency, and orchestration failures.',
        keyRisk: 'Runtime script evaluation latency and version mismatch instability in production.',
        color: '#ef4444',
        pros: ['Fully decentralized team deployments without monorepo synchronization'],
        cons: [
          'High network overhead and cumulative layout shifts on initial load',
          'Complex local development sandbox and shared singleton debugging',
          'Runtime failures when remote hosts experience temporary downtime',
        ],
        scores: {
          impact: 3.0,
          confidence: 2.5,
          effort: 1.8,
          dx: 2.6,
          maintenance: 1.6,
        },
        rice: {
          reach: 58,
          impact: 3.0,
          confidence: 55,
          effort: 4.6,
        },
      },
    ]
    
    const options = ref<MatrixOption[]>([...defaultOptions])
    
    // Add option form state
    const newOptionName = ref('')
    const newOptionParadigm = ref('')
    const newOptionSummary = ref('')
    const newOptionScores = ref({
      impact: 4.0,
      confidence: 4.0,
      effort: 3.5,
      dx: 4.0,
      maintenance: 3.5,
    })
    
    const totalWeight = computed(() => {
      return Object.values(criterionWeights.value).reduce((sum, w) => sum + (Number(w) || 0), 0)
    })
    
    function resetWeights() {
      criteriaList.forEach((c) => {
        criterionWeights.value[c.key] = c.defaultWeight
      })
    }
    
    function resetOptions() {
      options.value = [...defaultOptions]
      selectedOptionId.value = 'opt-a'
    }
    
    function handleAddOption() {
      if (!newOptionName.value.trim()) return
    
      const newId = `opt-custom-${Date.now()}`
      const created: MatrixOption = {
        id: newId,
        name: newOptionName.value.trim(),
        architecture: newOptionParadigm.value.trim() || 'Custom Technical Architecture',
        summary: newOptionSummary.value.trim() || 'Custom evaluated technical approach for this architectural decision.',
        keyRisk: 'Requires comprehensive proof-of-concept testing in staging environments.',
        color: '#06b6d4',
        pros: ['Tailored to immediate domain constraints', 'Custom architectural design'],
        cons: ['Needs dedicated long-term ownership and testing framework'],
        scores: { ...newOptionScores.value },
        rice: {
          reach: 75,
          impact: newOptionScores.value.impact,
          confidence: Number((newOptionScores.value.confidence * 20).toFixed(0)),
          effort: Number((6 - newOptionScores.value.effort).toFixed(1)),
        },
        isCustom: true,
      }
    
      options.value.push(created)
      selectedOptionId.value = newId
      newOptionName.value = ''
      newOptionParadigm.value = ''
      newOptionSummary.value = ''
      showAddOptionForm.value = false
    }
    
    function removeOption(id: string) {
      options.value = options.value.filter((opt) => opt.id !== id)
      if (selectedOptionId.value === id) {
        selectedOptionId.value = options.value[0]?.id || ''
      }
    }
    
    // Calculations
    interface ScoredOption extends MatrixOption {
      weightedScore: number
      riceScore: number
      rank: number
      verdict: 'Recommended Winner' | 'Second Choice' | 'Alternative' | 'Discarded'
      verdictVariant: 'success' | 'info' | 'warning' | 'destructive'
    }
    
    const scoredOptions = computed<ScoredOption[]>(() => {
      const sumW = totalWeight.value || 100
    
      // 1. Calculate raw scores
      const calculated = options.value.map((opt) => {
        // Weighted score out of 100
        const weightedSum =
          opt.scores.impact * criterionWeights.value.impact +
          opt.scores.confidence * criterionWeights.value.confidence +
          opt.scores.effort * criterionWeights.value.effort +
          opt.scores.dx * criterionWeights.value.dx +
          opt.scores.maintenance * criterionWeights.value.maintenance
    
        const weightedScore = Number(((weightedSum / (5 * sumW)) * 100).toFixed(1))
    
        // RICE calculation: (Reach * Impact * (Confidence / 100)) / Effort
        const reachFactor = opt.rice.reach
        const impactFactor = opt.rice.impact
        const confidenceFactor = opt.rice.confidence / 100
        const effortFactor = Math.max(0.5, opt.rice.effort)
        const riceScore = Number(((reachFactor * impactFactor * confidenceFactor) / effortFactor).toFixed(1))
    
        return {
          ...opt,
          weightedScore,
          riceScore,
          rank: 0,
          verdict: 'Discarded' as const,
          verdictVariant: 'destructive' as const,
        }
      })
    
      // 2. Sort depending on active framework
      calculated.sort((a, b) => {
        if (activeFramework.value === 'rice') {
          return b.riceScore - a.riceScore
        }
        return b.weightedScore - a.weightedScore
      })
    
      // 3. Assign ranks and verdicts
      return calculated.map((item, index) => {
        const rank = index + 1
        let verdict: ScoredOption['verdict'] = 'Discarded'
        let verdictVariant: ScoredOption['verdictVariant'] = 'destructive'
    
        if (rank === 1) {
          verdict = 'Recommended Winner'
          verdictVariant = 'success'
        } else if (rank === 2) {
          verdict = 'Second Choice'
          verdictVariant = 'info'
        } else if (rank === 3) {
          verdict = 'Alternative'
          verdictVariant = 'warning'
        } else {
          verdict = 'Discarded'
          verdictVariant = 'destructive'
        }
    
        return {
          ...item,
          rank,
          verdict,
          verdictVariant,
        }
      })
    })
    
    const winningOption = computed(() => {
      return scoredOptions.value[0] || options.value[0]
    })
    
    const activeSelectedOption = computed(() => {
      return scoredOptions.value.find((opt) => opt.id === selectedOptionId.value) || winningOption.value
    })
    
    // Radar chart SVG geometry
    const radarCenter = { x: 170, y: 155 }
    const radarRadius = 95
    const radarAxesCount = 5
    
    function getRadarPoint(axisIndex: number, scoreValue: number, maxVal = 5) {
      const angle = -Math.PI / 2 + (axisIndex * 2 * Math.PI) / radarAxesCount
      const ratio = Math.min(1, Math.max(0, scoreValue / maxVal))
      const r = radarRadius * ratio
      const x = radarCenter.x + r * Math.cos(angle)
      const y = radarCenter.y + r * Math.sin(angle)
      return { x, y }
    }
    
    function getRadarPolygon(scores: {
      impact: number
      confidence: number
      effort: number
      dx: number
      maintenance: number
    }) {
      const values = [scores.impact, scores.confidence, scores.effort, scores.dx, scores.maintenance]
      return values
        .map((val, idx) => {
          const pt = getRadarPoint(idx, val)
          return `${pt.x.toFixed(1)},${pt.y.toFixed(1)}`
        })
        .join(' ')
    }
    
    function getRingPolygon(ratio: number) {
      return Array.from({ length: radarAxesCount })
        .map((_, idx) => {
          const pt = getRadarPoint(idx, ratio * 5)
          return `${pt.x.toFixed(1)},${pt.y.toFixed(1)}`
        })
        .join(' ')
    }
    
    const radarAxisLabels = [
      { index: 0, label: 'Impact', x: 170, y: 38, anchor: 'middle' },
      { index: 1, label: 'Confidence', x: 285, y: 115, anchor: 'start' },
      { index: 2, label: 'Simplicity', x: 245, y: 258, anchor: 'middle' },
      { index: 3, label: 'DX & Own', x: 95, y: 258, anchor: 'middle' },
      { index: 4, label: 'Low Maint', x: 55, y: 115, anchor: 'end' },
    ]
    
    function getScoreBadgeVariant(score: number): 'success' | 'default' | 'secondary' | 'outline' {
      if (score >= 4.5) return 'success'
      if (score >= 3.5) return 'default'
      if (score >= 2.5) return 'secondary'
      return 'outline'
    }
    </script>
    
    <template>
      <div data-slot="decision-matrix-table" :class="cn('bg-background text-foreground w-full space-y-6', props.class)">
        <!-- Header: Title, Framework Selector & Action Toolbar -->
        <Card class="border-border bg-card shadow-xs">
          <CardHeader class="pb-4">
            <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
              <div class="space-y-1.5">
                <div class="flex flex-wrap items-center gap-2">
                  <Badge variant="outline" class="gap-1 text-xs font-semibold tracking-wide uppercase">
                    <Scale class="text-primary size-3.5" />
                    ADR-042 Evaluation
                  </Badge>
                  <Badge variant="secondary" class="text-xs"> Updated Aug 2026 </Badge>
                </div>
                <CardTitle class="text-xl font-semibold tracking-tight sm:text-2xl">
                  {{ title }}
                </CardTitle>
                <CardDescription class="text-sm">
                  {{ subtitle }}
                </CardDescription>
              </div>
    
              <!-- Framework Switcher & Quick Actions -->
              <div class="flex flex-wrap items-center gap-2.5">
                <!-- Framework Selector -->
                <div class="bg-muted border-border inline-flex items-center rounded-lg border p-1">
                  <button
                    type="button"
                    :class="
                      cn(
                        'focus-visible:ring-ring rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
                        activeFramework === 'weighted-matrix'
                          ? 'bg-background text-foreground font-semibold shadow-xs'
                          : 'text-muted-foreground hover:text-foreground',
                      )
                    "
                    @click="activeFramework = 'weighted-matrix'"
                  >
                    Weighted Value-Risk Matrix
                  </button>
                  <button
                    type="button"
                    :class="
                      cn(
                        'focus-visible:ring-ring rounded-md px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
                        activeFramework === 'rice'
                          ? 'bg-background text-foreground font-semibold shadow-xs'
                          : 'text-muted-foreground hover:text-foreground',
                      )
                    "
                    @click="activeFramework = 'rice'"
                  >
                    RICE Framework
                  </button>
                </div>
    
                <!-- Weight Adjustment Toggle -->
                <Button
                  v-if="activeFramework === 'weighted-matrix'"
                  variant="outline"
                  size="sm"
                  class="gap-1.5 text-xs"
                  @click="showWeightEditor = !showWeightEditor"
                >
                  <SlidersHorizontal class="text-muted-foreground size-3.5" />
                  <span>Criterion Weights</span>
                  <Badge
                    variant="secondary"
                    :class="
                      cn(
                        'ml-0.5 px-1.5 py-0 text-xs tabular-nums',
                        totalWeight === 100 ? 'text-foreground' : 'bg-warning/20 text-warning font-semibold',
                      )
                    "
                  >
                    {{ totalWeight }}%
                  </Badge>
                  <ChevronDown v-if="!showWeightEditor" class="text-muted-foreground size-3" />
                  <ChevronUp v-else class="text-muted-foreground size-3" />
                </Button>
    
                <!-- Add Option Button -->
                <Button size="sm" class="gap-1.5 text-xs font-medium" @click="showAddOptionForm = !showAddOptionForm">
                  <Plus class="size-3.5" />
                  <span>Add Option</span>
                </Button>
              </div>
            </div>
          </CardHeader>
    
          <!-- Expandable Criterion Weights Adjuster -->
          <div
            v-if="showWeightEditor && activeFramework === 'weighted-matrix'"
            class="border-border bg-muted/20 border-t px-6 py-5"
          >
            <div class="space-y-4">
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <h4 class="text-foreground text-sm font-semibold tracking-tight">
                    Calibrate Evaluation Criteria Weights
                  </h4>
                  <p class="text-muted-foreground text-xs">
                    Adjust criteria percentages to reflect your organization's architectural priorities.
                  </p>
                </div>
                <div class="flex items-center gap-2">
                  <span
                    :class="
                      cn(
                        'inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-semibold tabular-nums',
                        totalWeight === 100 ? 'bg-success/10 text-success' : 'bg-warning/15 text-warning font-bold',
                      )
                    "
                  >
                    <CheckCircle2 v-if="totalWeight === 100" class="size-3" />
                    <AlertCircle v-else class="size-3" />
                    Total Weight: {{ totalWeight }}%
                  </span>
                  <Button variant="ghost" size="xs" class="text-muted-foreground gap-1 text-xs" @click="resetWeights">
                    <RefreshCw class="size-3" />
                    Reset Defaults
                  </Button>
                </div>
              </div>
    
              <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
                <div
                  v-for="criterion in criteriaList"
                  :key="criterion.key"
                  class="border-border bg-card space-y-2.5 rounded-lg border p-3.5 shadow-2xs"
                >
                  <div class="flex items-center justify-between gap-1">
                    <span class="text-foreground truncate text-xs font-semibold" :title="criterion.label">
                      {{ criterion.label }}
                    </span>
                    <span class="bg-primary/10 text-primary rounded px-1.5 py-0.5 text-xs font-bold tabular-nums">
                      {{ criterionWeights[criterion.key] }}%
                    </span>
                  </div>
                  <p class="text-muted-foreground line-clamp-2 h-8 text-xs leading-tight">
                    {{ criterion.description }}
                  </p>
                  <div class="pt-1">
                    <Slider v-model="criterionWeights[criterion.key]" :min="0" :max="50" :step="5" size="small" />
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Expandable Add Custom Proposal Form -->
          <div v-if="showAddOptionForm" class="border-border bg-muted/30 border-t px-6 py-5">
            <div class="max-w-3xl space-y-4">
              <div class="flex items-center justify-between gap-x-2">
                <h4 class="text-foreground flex items-center gap-1.5 text-sm font-semibold tracking-tight">
                  <PlusCircle class="text-primary size-4" />
                  Add Technical Architecture Option to Matrix
                </h4>
                <Button variant="ghost" size="xs" @click="showAddOptionForm = false">Cancel</Button>
              </div>
    
              <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                <div class="space-y-1">
                  <label class="text-foreground text-xs font-medium">Option Proposal Name</label>
                  <Input v-model="newOptionName" placeholder="e.g. Option E: Shared UI Submodule" size="small" />
                </div>
                <div class="space-y-1">
                  <label class="text-foreground text-xs font-medium">Architectural architecture</label>
                  <Input v-model="newOptionParadigm" placeholder="e.g. Git Submodule & Monorepo Link" size="small" />
                </div>
              </div>
    
              <div class="space-y-1">
                <label class="text-foreground text-xs font-medium">Executive Architectural Summary</label>
                <Input
                  v-model="newOptionSummary"
                  placeholder="Brief summary of distribution mechanics, bundle characteristics, and DX impact..."
                  size="small"
                />
              </div>
    
              <div class="space-y-2">
                <label class="text-foreground text-xs font-medium">Initial Scores (1.0 to 5.0)</label>
                <div class="grid grid-cols-2 gap-2.5 sm:grid-cols-5">
                  <div
                    v-for="criterion in criteriaList"
                    :key="criterion.key"
                    class="border-border bg-card rounded-md border p-2 text-center"
                  >
                    <div class="text-muted-foreground truncate text-xs font-medium">{{ criterion.shortLabel }}</div>
                    <div class="text-foreground mt-0.5 text-sm font-bold tabular-nums">
                      {{ newOptionScores[criterion.key] }} / 5
                    </div>
                    <div class="pt-1.5">
                      <Slider v-model="newOptionScores[criterion.key]" :min="1" :max="5" :step="0.5" size="small" />
                    </div>
                  </div>
                </div>
              </div>
    
              <div class="flex items-center justify-end gap-2 pt-2">
                <Button variant="outline" size="sm" @click="showAddOptionForm = false">Cancel</Button>
                <Button size="sm" :disabled="!newOptionName.trim()" @click="handleAddOption"> Add to Matrix </Button>
              </div>
            </div>
          </div>
        </Card>
    
        <!-- Main Scoring Matrix Table -->
        <Card class="border-border bg-card overflow-hidden shadow-xs">
          <div class="overflow-x-auto">
            <Table>
              <TableHeader class="bg-muted/50">
                <TableRow class="hover:bg-transparent">
                  <TableHead class="text-muted-foreground w-[42px] px-3 text-center text-xs font-semibold">
                    Rank
                  </TableHead>
                  <TableHead class="text-foreground min-w-[240px] text-xs font-semibold">
                    Technical Option & architecture
                  </TableHead>
    
                  <!-- Dynamic Headers for Weighted Matrix vs RICE -->
                  <template v-if="activeFramework === 'weighted-matrix'">
                    <TableHead
                      v-for="criterion in criteriaList"
                      :key="criterion.key"
                      class="text-foreground min-w-[130px] text-center text-xs font-semibold"
                    >
                      <div class="flex flex-col items-center">
                        <span>{{ criterion.shortLabel }}</span>
                        <span class="text-muted-foreground text-xs font-normal tabular-nums">
                          {{ criterionWeights[criterion.key] }}% wt
                        </span>
                      </div>
                    </TableHead>
                    <TableHead class="text-foreground min-w-[140px] text-center text-xs font-semibold">
                      Weighted Score
                    </TableHead>
                  </template>
    
                  <template v-else>
                    <TableHead class="text-foreground min-w-[110px] text-center text-xs font-semibold">
                      <div class="flex flex-col items-center">
                        <span>Reach</span>
                        <span class="text-muted-foreground text-xs font-normal">Dev Scale %</span>
                      </div>
                    </TableHead>
                    <TableHead class="text-foreground min-w-[110px] text-center text-xs font-semibold">
                      <div class="flex flex-col items-center">
                        <span>Impact</span>
                        <span class="text-muted-foreground text-xs font-normal">Multiplier (1-5)</span>
                      </div>
                    </TableHead>
                    <TableHead class="text-foreground min-w-[110px] text-center text-xs font-semibold">
                      <div class="flex flex-col items-center">
                        <span>Confidence</span>
                        <span class="text-muted-foreground text-xs font-normal">% Certainty</span>
                      </div>
                    </TableHead>
                    <TableHead class="text-foreground min-w-[110px] text-center text-xs font-semibold">
                      <div class="flex flex-col items-center">
                        <span>Effort</span>
                        <span class="text-muted-foreground text-xs font-normal">Sprints (1-5)</span>
                      </div>
                    </TableHead>
                    <TableHead class="text-foreground min-w-[130px] text-center text-xs font-semibold">
                      RICE Score
                    </TableHead>
                  </template>
    
                  <TableHead class="text-foreground min-w-[160px] text-center text-xs font-semibold">
                    ADR Verdict
                  </TableHead>
                  <TableHead class="text-muted-foreground w-[80px] text-center text-xs font-semibold"> Inspect </TableHead>
                </TableRow>
              </TableHeader>
    
              <TableBody>
                <TableRow
                  v-for="opt in scoredOptions"
                  :key="opt.id"
                  :class="
                    cn(
                      'cursor-pointer transition-colors duration-150',
                      opt.id === selectedOptionId ? 'bg-primary/5 dark:bg-primary/10' : 'hover:bg-muted/40',
                      opt.rank === 1 && 'font-medium',
                    )
                  "
                  @click="selectedOptionId = opt.id"
                >
                  <!-- Rank Column -->
                  <TableCell class="px-3 text-center">
                    <span
                      :class="
                        cn(
                          'inline-flex size-6 items-center justify-center rounded-full text-xs font-semibold tabular-nums',
                          opt.rank === 1
                            ? 'bg-success text-white shadow-xs'
                            : opt.rank === 2
                              ? 'bg-info/15 text-info font-semibold'
                              : 'bg-muted text-muted-foreground font-normal',
                        )
                      "
                    >
                      {{ opt.rank }}
                    </span>
                  </TableCell>
    
                  <!-- Proposal Name, architecture & Description -->
                  <TableCell>
                    <div class="space-y-1 py-1">
                      <div class="flex flex-wrap items-center gap-2">
                        <span class="text-foreground text-sm font-semibold tracking-tight">
                          {{ opt.name }}
                        </span>
                        <span
                          class="size-2 shrink-0 rounded-full"
                          :style="{ backgroundColor: opt.color }"
                          :title="opt.name"
                        />
                        <Badge v-if="opt.isCustom" variant="outline" class="text-xs">Custom</Badge>
                      </div>
                      <div class="text-muted-foreground text-xs font-medium">
                        {{ opt.architecture }}
                      </div>
                      <p class="text-muted-foreground/80 line-clamp-1 max-w-[220px] text-xs">
                        {{ opt.summary }}
                      </p>
                    </div>
                  </TableCell>
    
                  <!-- Weighted Matrix Criteria Columns -->
                  <template v-if="activeFramework === 'weighted-matrix'">
                    <TableCell v-for="criterion in criteriaList" :key="criterion.key" class="text-center">
                      <div class="inline-flex flex-col items-center gap-0.5">
                        <Badge
                          :variant="getScoreBadgeVariant(opt.scores[criterion.key])"
                          class="px-2 py-0.5 text-xs font-semibold tabular-nums"
                        >
                          {{ opt.scores[criterion.key].toFixed(1) }}
                        </Badge>
                        <!-- Mini visual rating meter (5 segments) -->
                        <div class="mt-1 flex items-center gap-0.5">
                          <span
                            v-for="seg in 5"
                            :key="seg"
                            :class="
                              cn(
                                'size-1 rounded-full',
                                seg <= Math.round(opt.scores[criterion.key]) ? 'bg-primary' : 'bg-muted-foreground/20',
                              )
                            "
                          />
                        </div>
                      </div>
                    </TableCell>
    
                    <!-- Weighted Total Score Column -->
                    <TableCell class="text-center">
                      <div class="inline-flex flex-col items-center gap-1">
                        <span
                          :class="
                            cn(
                              'text-base font-semibold tracking-tight tabular-nums',
                              opt.rank === 1
                                ? 'text-success'
                                : opt.weightedScore >= 70
                                  ? 'text-foreground'
                                  : 'text-muted-foreground',
                            )
                          "
                        >
                          {{ opt.weightedScore }}
                          <span class="text-muted-foreground text-xs font-normal">/ 100</span>
                        </span>
                        <div class="bg-muted h-1.5 w-20 overflow-hidden rounded-full">
                          <div
                            class="h-full rounded-full transition-[width,background-color] duration-300"
                            :class="
                              opt.rank === 1
                                ? 'bg-success'
                                : opt.weightedScore >= 70
                                  ? 'bg-info'
                                  : opt.weightedScore >= 50
                                    ? 'bg-warning'
                                    : 'bg-destructive'
                            "
                            :style="{ width: `${opt.weightedScore}%` }"
                          />
                        </div>
                      </div>
                    </TableCell>
                  </template>
    
                  <!-- RICE Framework Columns -->
                  <template v-else>
                    <TableCell class="text-center text-xs font-semibold tabular-nums"> {{ opt.rice.reach }}% </TableCell>
                    <TableCell class="text-center text-xs font-semibold tabular-nums">
                      {{ opt.rice.impact.toFixed(1) }}x
                    </TableCell>
                    <TableCell class="text-center text-xs font-semibold tabular-nums">
                      {{ opt.rice.confidence }}%
                    </TableCell>
                    <TableCell class="text-center text-xs font-semibold tabular-nums">
                      {{ opt.rice.effort.toFixed(1) }} sprints
                    </TableCell>
                    <TableCell class="text-center">
                      <div class="inline-flex flex-col items-center">
                        <span class="text-primary text-base font-semibold tabular-nums">
                          {{ opt.riceScore }}
                        </span>
                        <span class="text-muted-foreground text-xs font-medium">RICE Pts</span>
                      </div>
                    </TableCell>
                  </template>
    
                  <!-- Verdict Column -->
                  <TableCell class="text-center">
                    <span
                      v-if="opt.rank === 1"
                      class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-semibold"
                    >
                      <Award class="size-3.5" />
                      Recommended Winner
                    </span>
                    <span
                      v-else-if="opt.rank === 2"
                      class="border-info/30 bg-info/10 text-info inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs font-semibold"
                    >
                      <CheckCircle2 class="size-3.5" />
                      Second Choice
                    </span>
                    <span
                      v-else-if="opt.rank === 3"
                      class="border-border bg-muted/60 text-muted-foreground inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs font-medium"
                    >
                      <HelpCircle class="size-3.5" />
                      Alternative
                    </span>
                    <span
                      v-else
                      class="border-destructive/20 bg-destructive/10 text-destructive inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-xs font-medium"
                    >
                      <XCircle class="size-3.5" />
                      Discarded
                    </span>
                  </TableCell>
    
                  <!-- Action / Remove Column -->
                  <TableCell class="text-center" @click.stop>
                    <Button
                      v-if="opt.isCustom"
                      variant="ghost"
                      size="icon-sm"
                      class="text-muted-foreground hover:text-destructive"
                      title="Remove Custom Option"
                      @click="removeOption(opt.id)"
                    >
                      <Trash2 class="size-3.5" />
                    </Button>
                    <Button
                      v-else
                      variant="ghost"
                      size="icon-sm"
                      class="text-muted-foreground hover:text-foreground"
                      :title="`Inspect ${opt.name}`"
                      @click="selectedOptionId = opt.id"
                    >
                      <Info class="size-3.5" />
                    </Button>
                  </TableCell>
                </TableRow>
              </TableBody>
            </Table>
          </div>
    
          <CardFooter
            class="border-border bg-muted/20 text-muted-foreground flex flex-wrap items-center justify-between gap-3 border-t px-6 py-3 text-xs"
          >
            <div class="flex items-center gap-2">
              <span class="bg-success inline-block size-2 rounded-full" />
              <span>Scores normalize dynamically to a 0–100 scale based on active criteria weights.</span>
            </div>
            <div class="flex items-center gap-3">
              <Button variant="ghost" size="xs" class="text-muted-foreground gap-1 text-xs" @click="resetOptions">
                <RefreshCw class="size-3" />
                Reset Matrix Options
              </Button>
            </div>
          </CardFooter>
        </Card>
    
        <!-- Tradeoff Visualizer & ADR Recommendation Section -->
        <div class="grid grid-cols-1 gap-6 lg:grid-cols-12">
          <!-- Radar Chart: Multi-Option Criteria Comparison -->
          <Card class="border-border bg-card flex flex-col shadow-xs lg:col-span-6">
            <CardHeader class="pb-2">
              <div class="flex items-center justify-between gap-x-2">
                <div class="space-y-0.5">
                  <CardTitle class="flex items-center gap-2 text-base font-semibold tracking-tight">
                    <BarChart3 class="text-primary size-4" />
                    Decision Tradeoff Radar
                  </CardTitle>
                  <CardDescription class="text-xs">
                    Multi-dimensional polygon overlay comparing architectural options.
                  </CardDescription>
                </div>
                <Badge variant="outline" class="text-xs font-normal">5 Axes (1-5)</Badge>
              </div>
            </CardHeader>
    
            <CardContent class="flex flex-1 flex-col items-center justify-center p-4">
              <!-- SVG Radar Canvas -->
              <div class="relative flex aspect-square w-full max-w-[340px] items-center justify-center">
                <svg viewBox="0 0 340 310" class="h-full w-full overflow-visible">
                  <!-- Background Concentric Grid Rings -->
                  <polygon
                    v-for="ring in [0.2, 0.4, 0.6, 0.8, 1.0]"
                    :key="ring"
                    :points="getRingPolygon(ring)"
                    fill="none"
                    stroke="currentColor"
                    stroke-width="1"
                    class="text-border/50"
                    :stroke-dasharray="ring === 1.0 ? 'none' : '2,2'"
                  />
    
                  <!-- Radial Axes Lines -->
                  <line
                    v-for="(_, idx) in Array.from({ length: radarAxesCount })"
                    :key="idx"
                    :x1="radarCenter.x"
                    :y1="radarCenter.y"
                    :x2="getRadarPoint(idx, 5).x"
                    :y2="getRadarPoint(idx, 5).y"
                    stroke="currentColor"
                    stroke-width="1"
                    class="text-border"
                  />
    
                  <!-- Polygons for Non-selected options (subtle) -->
                  <template v-for="opt in scoredOptions" :key="opt.id">
                    <polygon
                      v-if="opt.id !== activeSelectedOption.id"
                      :points="getRadarPolygon(opt.scores)"
                      :fill="opt.color"
                      fill-opacity="0.08"
                      :stroke="opt.color"
                      stroke-width="1.5"
                      stroke-opacity="0.45"
                      class="cursor-pointer transition-colors duration-300"
                      @click="selectedOptionId = opt.id"
                    />
                  </template>
    
                  <!-- Polygon for Selected Option (Prominent Highlight) -->
                  <polygon
                    :points="getRadarPolygon(activeSelectedOption.scores)"
                    :fill="activeSelectedOption.color"
                    fill-opacity="0.25"
                    :stroke="activeSelectedOption.color"
                    stroke-width="2.5"
                    class="transition-colors duration-300"
                  />
    
                  <!-- Vertex Dots for Selected Option -->
                  <circle
                    v-for="(val, idx) in [
                      activeSelectedOption.scores.impact,
                      activeSelectedOption.scores.confidence,
                      activeSelectedOption.scores.effort,
                      activeSelectedOption.scores.dx,
                      activeSelectedOption.scores.maintenance,
                    ]"
                    :key="idx"
                    :cx="getRadarPoint(idx, val).x"
                    :cy="getRadarPoint(idx, val).y"
                    r="4"
                    :fill="activeSelectedOption.color"
                    class="stroke-background stroke-2 transition-colors duration-300"
                  />
    
                  <!-- Axis Labels -->
                  <text
                    v-for="axis in radarAxisLabels"
                    :key="axis.index"
                    :x="axis.x"
                    :y="axis.y"
                    :text-anchor="axis.anchor"
                    class="fill-foreground text-xs font-semibold select-none"
                    style="font-size: 11px"
                  >
                    {{ axis.label }}
                  </text>
                </svg>
              </div>
    
              <!-- Radar Chart Legend -->
              <div class="border-border flex w-full flex-wrap items-center justify-center gap-3 border-t pt-3">
                <button
                  v-for="opt in scoredOptions"
                  :key="opt.id"
                  type="button"
                  :class="
                    cn(
                      'inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs transition-colors',
                      opt.id === activeSelectedOption.id
                        ? 'bg-muted text-foreground ring-border font-semibold ring-1'
                        : 'text-muted-foreground hover:text-foreground opacity-80',
                    )
                  "
                  @click="selectedOptionId = opt.id"
                >
                  <span class="size-2.5 shrink-0 rounded-full" :style="{ backgroundColor: opt.color }" />
                  <span class="max-w-[120px] truncate">{{ opt.name.split(':')[0] }}</span>
                </button>
              </div>
            </CardContent>
          </Card>
    
          <!-- Architectural Consensus & Tradeoff Detail Card -->
          <Card class="border-border bg-card flex flex-col justify-between shadow-xs lg:col-span-6">
            <CardHeader class="pb-3">
              <div class="flex flex-wrap items-start justify-between gap-3">
                <div class="min-w-0 space-y-1">
                  <div class="flex flex-wrap items-center gap-2">
                    <Badge variant="outline" class="text-xs">Selected Option Details</Badge>
                    <Badge
                      :variant="activeSelectedOption.verdictVariant === 'success' ? 'success' : 'secondary'"
                      class="text-xs font-semibold"
                    >
                      {{ activeSelectedOption.verdict }}
                    </Badge>
                  </div>
                  <CardTitle class="text-foreground text-lg font-semibold tracking-tight">
                    {{ activeSelectedOption.name }}
                  </CardTitle>
                  <CardDescription class="text-primary text-xs font-medium">
                    {{ activeSelectedOption.architecture }}
                  </CardDescription>
                </div>
                <div class="shrink-0 text-right">
                  <div class="text-foreground text-2xl font-semibold tabular-nums">
                    {{ activeFramework === 'rice' ? activeSelectedOption.riceScore : activeSelectedOption.weightedScore }}
                  </div>
                  <div class="text-muted-foreground text-xs">
                    {{ activeFramework === 'rice' ? 'RICE Score' : 'Weighted Total' }}
                  </div>
                </div>
              </div>
            </CardHeader>
    
            <CardContent class="space-y-4 text-xs">
              <!-- Summary paragraph -->
              <p class="text-muted-foreground text-sm leading-relaxed">
                {{ activeSelectedOption.summary }}
              </p>
    
              <Separator />
    
              <!-- Pros & Cons Grid -->
              <div class="grid grid-cols-1 gap-3.5 sm:grid-cols-2">
                <!-- Architectural Pros -->
                <div class="border-success/20 bg-success/5 space-y-2 rounded-lg border p-3">
                  <span class="text-success flex items-center gap-1.5 text-xs font-semibold">
                    <Check class="size-3.5" />
                    Strategic Advantages
                  </span>
                  <ul class="text-muted-foreground space-y-1.5">
                    <li v-for="(pro, idx) in activeSelectedOption.pros" :key="idx" class="flex items-start gap-1.5">
                      <span class="text-success mt-0.5"></span>
                      <span>{{ pro }}</span>
                    </li>
                  </ul>
                </div>
    
                <!-- Architectural Cons & Risks -->
                <div class="border-destructive/20 bg-destructive/5 space-y-2 rounded-lg border p-3">
                  <span class="text-destructive flex items-center gap-1.5 text-xs font-semibold">
                    <AlertCircle class="size-3.5" />
                    Compromises & Friction
                  </span>
                  <ul class="text-muted-foreground space-y-1.5">
                    <li v-for="(con, idx) in activeSelectedOption.cons" :key="idx" class="flex items-start gap-1.5">
                      <span class="text-destructive mt-0.5"></span>
                      <span>{{ con }}</span>
                    </li>
                  </ul>
                </div>
              </div>
    
              <!-- Risk Mitigation -->
              <div class="border-border bg-muted/40 space-y-1 rounded-md border p-3">
                <span class="text-foreground flex items-center gap-1.5 font-semibold">
                  <ShieldCheck class="text-primary size-3.5" />
                  Risk Mitigation & Governance Policy:
                </span>
                <p class="text-muted-foreground">
                  {{ activeSelectedOption.keyRisk }}
                </p>
              </div>
            </CardContent>
    
            <CardFooter class="border-border bg-muted/20 flex items-center justify-between gap-x-2 border-t px-6 py-3">
              <div class="flex items-center gap-2">
                <FileCode2 class="text-muted-foreground size-4" />
                <span class="text-muted-foreground text-xs font-medium">ADR Record Consensus: </span>
                <span class="text-foreground text-xs font-semibold">{{
                  winningOption.name.split(':')[1] || winningOption.name
                }}</span>
              </div>
              <Badge variant="outline" class="gap-1 text-xs">
                <Award class="text-success size-3" />
                Rank #{{ activeSelectedOption.rank }}
              </Badge>
            </CardFooter>
          </Card>
        </div>
      </div>
    </template>
    

Raw manifest:https://uipkge.dev/r/vue/decision-matrix-table.json