Menu

Change language

GitHub repo

Matrix Swot Analysis

blockproductivity

Strategic planning 2x2 SWOT analysis matrix (Strengths, Weaknesses, Opportunities, Threats) with dynamic card management, quadrant counters, TOWS strategic initiatives, and PDF/share toolbar.

Installation

$npx uipkge-ng@latest add matrix-swot-analysis
Manifest:https://uipkge.dev/r/angular/matrix-swot-analysis.jsonInstalls to:src/app/components/blocks/matrix-swot-analysis/

Variants

Loading interactive previews…

Schema

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

SwotItem
interface SwotItem {
  id: string
  title: string
  description: string
  impact: 'high' | 'medium' | 'low'
  tag: string
}
StrategicInitiative
interface StrategicInitiative {
  id: string
  title: string
  description: string
  strategyType: 'SO' | 'WO' | 'ST' | 'WT'
  strategyLabel: string
  priority: 'Critical' | 'High' | 'Strategic'
  timeframe: string
  targetMetric: string
}

npm dependencies

Files installed (2)

  • src/app/components/blocks/matrix-swot-analysis/matrix-swot-analysis.component.ts38.1 kB
    // Strategic planning 2x2 SWOT analysis matrix (Strengths, Weaknesses, Opportunities,
    // Threats) with dynamic card management, quadrant counters, TOWS strategic initiatives,
    // and PDF/share toolbar. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      AlertTriangle,
      Calendar,
      Check,
      Clock,
      Download,
      Layers,
      LucideAngularModule,
      Plus,
      Share2,
      ShieldAlert,
      ShieldCheck,
      Trash2,
      TrendingUp,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardFooterComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    
    export interface SwotItem {
      id: string
      title: string
      description: string
      impact: 'high' | 'medium' | 'low'
      tag: string
    }
    
    export interface StrategicInitiative {
      id: string
      title: string
      description: string
      strategyType: 'SO' | 'WO' | 'ST' | 'WT'
      strategyLabel: string
      priority: 'Critical' | 'High' | 'Strategic'
      timeframe: string
      targetMetric: string
    }
    
    const defaultStrengths: SwotItem[] = [
      {
        id: 's-1',
        title: 'Zero npm dependency overhead',
        description:
          'Components are copied directly into consumer project codebases, eliminating semver dependency locks, version skew, and transitive package bloat.',
        impact: 'high',
        tag: 'Core Architecture',
      },
      {
        id: 's-2',
        title: '100% code ownership for consumers',
        description:
          'Engineering teams retain complete control over markup, styles, accessibility props, and internal behaviors with zero upstream constraints.',
        impact: 'high',
        tag: 'Developer Autonomy',
      },
      {
        id: 's-3',
        title: 'Dual-framework Vue & React parity',
        description:
          'Strict visual, token, and functional symmetry between Vue 3 (Reka UI) and React (Radix UI) enables cross-stack design system alignment.',
        impact: 'high',
        tag: 'Market Breadth',
      },
      {
        id: 's-4',
        title: 'Tailwind v4 OKLCH token architecture',
        description:
          'CSS-first theme configuration using semantic surface variables, calibrated dark-mode layering, and WCAG AA compliant contrast ratios.',
        impact: 'medium',
        tag: 'Design System',
      },
    ]
    
    const defaultWeaknesses: SwotItem[] = [
      {
        id: 'w-1',
        title: 'Manual upgrade path without semver',
        description:
          'Consuming codebases must manually pull code diffs or re-execute CLI add commands when library enhancements or bug fixes are published.',
        impact: 'high',
        tag: 'Maintenance Model',
      },
      {
        id: 'w-2',
        title: 'Initial learning curve for monorepos',
        description:
          'Junior developers accustomed to single-command npm package installs may face an initial cognitive hurdle managing raw unbundled components.',
        impact: 'medium',
        tag: 'Developer DX',
      },
      {
        id: 'w-3',
        title: 'Build time for 1,300+ SSG pages',
        description:
          'Synthesizing static island previews, AST code extraction, and multi-format LLM documentation across dual registries requires high memory in CI.',
        impact: 'medium',
        tag: 'Tooling Scale',
      },
    ]
    
    const defaultOpportunities: SwotItem[] = [
      {
        id: 'o-1',
        title: 'AI Copilot workflows demanding raw source code',
        description:
          'LLM coding assistants (Claude, Cursor, Copilot) perform substantially better when refactoring transparent local SFCs than navigating opaque compiled dependencies.',
        impact: 'high',
        tag: 'AI Ergonomics',
      },
      {
        id: 'o-2',
        title: 'Enterprise design system white-labeling',
        description:
          'B2B organizations seeking unbranded, custom-styled design systems can adopt UIPKGE as their modular, open-source architectural baseline.',
        impact: 'high',
        tag: 'Enterprise B2B',
      },
      {
        id: 'o-3',
        title: 'E-Commerce and Healthcare template verticals',
        description:
          'Surging industry appetite for pre-built reference architectures (HRMS, Hospital Management, Logistics tracking) powered by composable blocks.',
        impact: 'high',
        tag: 'Vertical Growth',
      },
      {
        id: 'o-4',
        title: 'Community contributions of domain blocks',
        description:
          'Open-source contributors can rapidly publish specialized workflow blocks (radar charts, KYC wizards, audit logs) without semver packaging bottlenecks.',
        impact: 'medium',
        tag: 'Community',
      },
    ]
    
    const defaultThreats: SwotItem[] = [
      {
        id: 't-1',
        title: 'Official shadcn expanding into Vue ecosystem',
        description:
          'Potential first-party Vue support by upstream shadcn could consolidate developer attention away from independent unbundled registries.',
        impact: 'high',
        tag: 'Competitive Risk',
      },
      {
        id: 't-2',
        title: 'Upstream Reka UI headless breaking changes',
        description:
          'Substantial architectural shifts or breaking release cycles in upstream headless libraries require immediate parity synchronization.',
        impact: 'high',
        tag: 'Upstream Drift',
      },
      {
        id: 't-3',
        title: 'Browser CSS specification shifts',
        description:
          'Rapid evolution in CSS @scope, anchor positioning, and native light-dark() CSS functions may require recurring baseline refactoring.',
        impact: 'medium',
        tag: 'Web Standards',
      },
    ]
    
    const defaultInitiatives: StrategicInitiative[] = [
      {
        id: 'init-1',
        title: 'Automate Upstream Sync & Breaking Change Detection',
        description:
          'Implement automated CI regression monitoring against Reka UI and upstream shadcn tokens to maintain 100% runtime compatibility without breaking consumer projects.',
        strategyType: 'SO',
        strategyLabel: 'SO Strategy (Strengths × Opportunities)',
        priority: 'Critical',
        timeframe: 'Q4 2026',
        targetMetric: '< 24h Upstream Parity Turnaround',
      },
      {
        id: 'init-2',
        title: 'AI-Native Context & CLI Smart Upgrade Engine',
        description:
          'Ship an intelligent CLI diff command and fine-tuned llms.txt vector context to let AI coding agents auto-patch and upgrade local consumer components directly.',
        strategyType: 'WO',
        strategyLabel: 'WO Strategy (Weaknesses × Opportunities)',
        priority: 'High',
        timeframe: 'Q4 2026',
        targetMetric: 'Zero-Friction AST Upgrade Patches',
      },
      {
        id: 'init-3',
        title: 'Curated Vertical Template Packs & Enterprise Baseline',
        description:
          'Deliver production-grade Nuxt 4 and Next.js vertical boilerplate templates (HRMS, HMS, Logistics) to eliminate monorepo onboarding friction for enterprise builders.',
        strategyType: 'ST',
        strategyLabel: 'ST Strategy (Strengths × Threats)',
        priority: 'Strategic',
        timeframe: 'Q1 2027',
        targetMetric: '4 Turnkey Vertical Reference Apps',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-matrix-swot-analysis, [ui-matrix-swot-analysis]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardFooterComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
      ],
      template: `
        <div data-slot="matrix-swot-analysis" [class]="rootClass">
          <!-- Header Surface -->
          <div
            class="bg-card text-card-foreground flex flex-col gap-5 rounded-xl border p-5 shadow-xs sm:p-6 lg:flex-row lg:items-center lg:justify-between"
          >
            <div class="space-y-1.5">
              <div class="flex flex-wrap items-center gap-2">
                <h2 class="text-xl font-bold tracking-tight sm:text-2xl">{{ titleText() }}</h2>
                <span ui-badge variant="outline" class="bg-primary/5 text-primary border-primary/20 text-xs font-medium">
                  2×2 TOWS Matrix
                </span>
                <span ui-badge variant="secondary" class="text-xs font-normal"> {{ totalCards() }} Strategic Factors </span>
              </div>
    
              <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 text-xs sm:text-sm">
                <div class="flex items-center gap-1.5">
                  <lucide-icon [img]="Layers" class="size-3.5" />
                  <span class="text-foreground font-medium">{{ projectText() }}</span>
                </div>
                <span class="hidden sm:inline">·</span>
                <div class="flex items-center gap-1.5">
                  <lucide-icon [img]="Calendar" class="size-3.5" />
                  <span>Updated {{ lastUpdatedText() }}</span>
                </div>
              </div>
            </div>
    
            <!-- Action Toolbar -->
            <div class="flex flex-wrap items-center gap-2.5">
              <button
                ui-button
                aria-label="Download attachment"
                variant="outline"
                size="sm"
                class="h-9 gap-2 text-xs shadow-xs"
                [disabled]="isExporting()"
                (click)="handleExportPdf()"
              >
                @if (!isExporting()) {
                  <lucide-icon [img]="Download" class="size-3.5" />
                } @else {
                  <lucide-icon [img]="Clock" class="size-3.5 animate-spin" />
                }
                <span>{{ isExporting() ? 'Generating PDF...' : 'Export Canvas PDF' }}</span>
              </button>
    
              <button ui-button variant="default" size="sm" class="h-9 gap-2 text-xs shadow-xs" (click)="handleShare()">
                @if (isCopied()) {
                  <lucide-icon [img]="Check" class="size-3.5" />
                } @else {
                  <lucide-icon [img]="Share2" class="size-3.5" />
                }
                <span>{{ isCopied() ? 'Link Copied!' : 'Share Strategy' }}</span>
              </button>
            </div>
          </div>
    
          <!-- 2x2 Matrix Axis Guide Legend -->
          <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
            <div
              class="border-success/20 bg-success/5 text-success flex items-center justify-between rounded-lg border px-3.5 py-2.5 text-xs font-medium shadow-xs"
            >
              <div class="flex items-center gap-2">
                <lucide-icon [img]="ShieldCheck" class="text-success size-4 shrink-0" />
                <span>Internal Attributes</span>
              </div>
              <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-xs">Helpful (+)</span>
            </div>
    
            <div
              class="border-destructive/20 bg-destructive/5 text-destructive flex items-center justify-between rounded-lg border px-3.5 py-2.5 text-xs font-medium shadow-xs"
            >
              <div class="flex items-center gap-2">
                <lucide-icon [img]="AlertTriangle" class="text-destructive size-4 shrink-0" />
                <span>Internal Attributes</span>
              </div>
              <span ui-badge variant="outline" class="border-destructive/30 bg-destructive/10 text-xs">Harmful (−)</span>
            </div>
    
            <div
              class="border-info/20 bg-info/5 text-info flex items-center justify-between rounded-lg border px-3.5 py-2.5 text-xs font-medium shadow-xs"
            >
              <div class="flex items-center gap-2">
                <lucide-icon [img]="TrendingUp" class="text-info size-4 shrink-0" />
                <span>External Dynamics</span>
              </div>
              <span ui-badge variant="outline" class="border-info/30 bg-info/10 text-xs">Helpful (+)</span>
            </div>
    
            <div
              class="border-warning/20 bg-warning/5 text-warning flex items-center justify-between rounded-lg border px-3.5 py-2.5 text-xs font-medium shadow-xs"
            >
              <div class="flex items-center gap-2">
                <lucide-icon [img]="ShieldAlert" class="text-warning size-4 shrink-0" />
                <span>External Dynamics</span>
              </div>
              <span ui-badge variant="outline" class="border-warning/30 bg-warning/10 text-warning text-xs">
                Harmful (−)
              </span>
            </div>
          </div>
    
          <!-- 2x2 SWOT Grid -->
          <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
            <!-- QUADRANT 1: STRENGTHS -->
            <div ui-card class="border-success/30 border-success/20 flex flex-col justify-between shadow-xs">
              <div ui-card-header class="border-border/60 bg-success/5 dark:bg-success/10 border-b pb-4">
                <div class="flex items-start justify-between gap-3">
                  <div class="flex items-center gap-3">
                    <div
                      class="border-success/20 bg-success/10 text-success flex size-9 items-center justify-center rounded-lg border shadow-xs"
                    >
                      <lucide-icon [img]="ShieldCheck" class="size-5" />
                    </div>
                    <div>
                      <div class="flex items-center gap-2">
                        <h3 ui-card-title class="text-base font-bold sm:text-lg">Strengths</h3>
                        <span ui-badge class="border-success/20 bg-success/15 text-success text-xs font-semibold">
                          {{ strengths().length }}
                        </span>
                      </div>
                      <p ui-card-description class="text-xs">Internal attributes &amp; distinct technical capabilities</p>
                    </div>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <span ui-badge variant="outline" class="border-success/30 text-success hidden text-xs sm:inline-flex">
                      Internal
                    </span>
                    <span ui-badge variant="outline" class="border-success/30 text-success text-xs"> Positive </span>
                  </div>
                </div>
              </div>
    
              <div ui-card-content class="flex-1 space-y-3 p-4 sm:p-5">
                @for (item of strengths(); track item.id) {
                  <div
                    class="group bg-card hover:border-success/40 relative rounded-lg border p-3.5 shadow-xs transition-[border-color,box-shadow]"
                  >
                    <div class="flex items-start justify-between gap-2">
                      <div class="space-y-1">
                        <div class="flex flex-wrap items-center gap-2">
                          <h4 class="text-foreground text-sm leading-snug font-semibold">{{ item.title }}</h4>
                          <span
                            ui-badge
                            variant="outline"
                            class="border-success/20 bg-success/10 text-success text-xs font-normal"
                          >
                            {{ item.tag }}
                          </span>
                        </div>
                        <p class="text-muted-foreground text-xs leading-relaxed">{{ item.description }}</p>
                      </div>
                      @if (!isReadonly()) {
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="text-muted-foreground hover:text-destructive hover:bg-destructive/10 -mt-1 -mr-1 size-7 shrink-0 p-0 opacity-80 group-hover:opacity-100"
                          aria-label="Remove strength item"
                          (click)="removeStrength(item.id)"
                        >
                          <lucide-icon [img]="Trash2" class="size-3.5" />
                        </button>
                      }
                    </div>
                  </div>
                }
                @if (strengths().length === 0) {
                  <div class="text-muted-foreground rounded-lg border border-dashed py-8 text-center text-xs">
                    No strength items listed. Add a core advantage below.
                  </div>
                }
              </div>
    
              @if (!isReadonly()) {
                <div ui-card-footer class="border-border/60 bg-muted/20 border-t p-3 sm:p-4">
                  <form class="flex w-full items-center gap-2" (submit)="addStrength($event)">
                    <ui-input
                      [value]="inputStrength()"
                      (valueChange)="inputStrength.set($event)"
                      type="text"
                      placeholder="Add key strength... (Press Enter)"
                      class="h-8.5 text-xs shadow-xs"
                    />
                    <button
                      ui-button
                      type="submit"
                      size="sm"
                      class="bg-success hover:bg-success/90 h-8.5 shrink-0 gap-1.5 text-xs text-white shadow-xs"
                    >
                      <lucide-icon [img]="Plus" class="size-3.5" />
                      <span>Add</span>
                    </button>
                  </form>
                </div>
              }
            </div>
    
            <!-- QUADRANT 2: WEAKNESSES -->
            <div ui-card class="border-destructive/30 border-destructive/20 flex flex-col justify-between shadow-xs">
              <div ui-card-header class="border-border/60 bg-destructive/5 dark:bg-destructive/10 border-b pb-4">
                <div class="flex items-start justify-between gap-3">
                  <div class="flex items-center gap-3">
                    <div
                      class="border-destructive/20 bg-destructive/10 text-destructive flex size-9 items-center justify-center rounded-lg border shadow-xs"
                    >
                      <lucide-icon [img]="AlertTriangle" class="size-5" />
                    </div>
                    <div>
                      <div class="flex items-center gap-2">
                        <h3 ui-card-title class="text-base font-bold sm:text-lg">Weaknesses</h3>
                        <span
                          ui-badge
                          class="border-destructive/20 bg-destructive/15 text-destructive text-xs font-semibold"
                        >
                          {{ weaknesses().length }}
                        </span>
                      </div>
                      <p ui-card-description class="text-xs">Internal limitations &amp; operational friction points</p>
                    </div>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <span
                      ui-badge
                      variant="outline"
                      class="border-destructive/30 text-destructive hidden text-xs sm:inline-flex"
                    >
                      Internal
                    </span>
                    <span ui-badge variant="outline" class="border-destructive/30 text-destructive text-xs">
                      Negative
                    </span>
                  </div>
                </div>
              </div>
    
              <div ui-card-content class="flex-1 space-y-3 p-4 sm:p-5">
                @for (item of weaknesses(); track item.id) {
                  <div
                    class="group bg-card hover:border-destructive/40 relative rounded-lg border p-3.5 shadow-xs transition-[border-color,box-shadow]"
                  >
                    <div class="flex items-start justify-between gap-2">
                      <div class="space-y-1">
                        <div class="flex flex-wrap items-center gap-2">
                          <h4 class="text-foreground text-sm leading-snug font-semibold">{{ item.title }}</h4>
                          <span
                            ui-badge
                            variant="outline"
                            class="border-destructive/20 bg-destructive/10 text-destructive text-xs font-normal"
                          >
                            {{ item.tag }}
                          </span>
                        </div>
                        <p class="text-muted-foreground text-xs leading-relaxed">{{ item.description }}</p>
                      </div>
                      @if (!isReadonly()) {
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="text-muted-foreground hover:text-destructive hover:bg-destructive/10 -mt-1 -mr-1 size-7 shrink-0 p-0 opacity-80 group-hover:opacity-100"
                          aria-label="Remove weakness item"
                          (click)="removeWeakness(item.id)"
                        >
                          <lucide-icon [img]="Trash2" class="size-3.5" />
                        </button>
                      }
                    </div>
                  </div>
                }
                @if (weaknesses().length === 0) {
                  <div class="text-muted-foreground rounded-lg border border-dashed py-8 text-center text-xs">
                    No weakness items listed. Add an area for improvement below.
                  </div>
                }
              </div>
    
              @if (!isReadonly()) {
                <div ui-card-footer class="border-border/60 bg-muted/20 border-t p-3 sm:p-4">
                  <form class="flex w-full items-center gap-2" (submit)="addWeakness($event)">
                    <ui-input
                      [value]="inputWeakness()"
                      (valueChange)="inputWeakness.set($event)"
                      type="text"
                      placeholder="Add key weakness... (Press Enter)"
                      class="h-8.5 text-xs shadow-xs"
                    />
                    <button
                      ui-button
                      type="submit"
                      size="sm"
                      class="bg-destructive hover:bg-destructive/90 h-8.5 shrink-0 gap-1.5 text-xs text-white shadow-xs"
                    >
                      <lucide-icon [img]="Plus" class="size-3.5" />
                      <span>Add</span>
                    </button>
                  </form>
                </div>
              }
            </div>
    
            <!-- QUADRANT 3: OPPORTUNITIES -->
            <div ui-card class="border-info/30 border-info/20 flex flex-col justify-between shadow-xs">
              <div ui-card-header class="border-border/60 bg-info/5 dark:bg-info/10 border-b pb-4">
                <div class="flex items-start justify-between gap-3">
                  <div class="flex items-center gap-3">
                    <div
                      class="border-info/20 bg-info/10 text-info flex size-9 items-center justify-center rounded-lg border shadow-xs"
                    >
                      <lucide-icon [img]="TrendingUp" class="size-5" />
                    </div>
                    <div>
                      <div class="flex items-center gap-2">
                        <h3 ui-card-title class="text-base font-bold sm:text-lg">Opportunities</h3>
                        <span ui-badge class="border-info/20 bg-info/15 text-info text-xs font-semibold">
                          {{ opportunities().length }}
                        </span>
                      </div>
                      <p ui-card-description class="text-xs">External tailwinds &amp; strategic expansion opportunities</p>
                    </div>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <span ui-badge variant="outline" class="border-info/30 text-info hidden text-xs sm:inline-flex">
                      External
                    </span>
                    <span ui-badge variant="outline" class="border-info/30 text-info text-xs"> Positive </span>
                  </div>
                </div>
              </div>
    
              <div ui-card-content class="flex-1 space-y-3 p-4 sm:p-5">
                @for (item of opportunities(); track item.id) {
                  <div
                    class="group bg-card hover:border-info/40 relative rounded-lg border p-3.5 shadow-xs transition-[border-color,box-shadow]"
                  >
                    <div class="flex items-start justify-between gap-2">
                      <div class="space-y-1">
                        <div class="flex flex-wrap items-center gap-2">
                          <h4 class="text-foreground text-sm leading-snug font-semibold">{{ item.title }}</h4>
                          <span ui-badge variant="outline" class="border-info/20 bg-info/10 text-info text-xs font-normal">
                            {{ item.tag }}
                          </span>
                        </div>
                        <p class="text-muted-foreground text-xs leading-relaxed">{{ item.description }}</p>
                      </div>
                      @if (!isReadonly()) {
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="text-muted-foreground hover:text-destructive hover:bg-destructive/10 -mt-1 -mr-1 size-7 shrink-0 p-0 opacity-80 group-hover:opacity-100"
                          aria-label="Remove opportunity item"
                          (click)="removeOpportunity(item.id)"
                        >
                          <lucide-icon [img]="Trash2" class="size-3.5" />
                        </button>
                      }
                    </div>
                  </div>
                }
                @if (opportunities().length === 0) {
                  <div class="text-muted-foreground rounded-lg border border-dashed py-8 text-center text-xs">
                    No opportunity items listed. Add a market growth vector below.
                  </div>
                }
              </div>
    
              @if (!isReadonly()) {
                <div ui-card-footer class="border-border/60 bg-muted/20 border-t p-3 sm:p-4">
                  <form class="flex w-full items-center gap-2" (submit)="addOpportunity($event)">
                    <ui-input
                      [value]="inputOpportunity()"
                      (valueChange)="inputOpportunity.set($event)"
                      type="text"
                      placeholder="Add key opportunity... (Press Enter)"
                      class="h-8.5 text-xs shadow-xs"
                    />
                    <button
                      ui-button
                      type="submit"
                      size="sm"
                      class="bg-info hover:bg-info/90 h-8.5 shrink-0 gap-1.5 text-xs text-white shadow-xs"
                    >
                      <lucide-icon [img]="Plus" class="size-3.5" />
                      <span>Add</span>
                    </button>
                  </form>
                </div>
              }
            </div>
    
            <!-- QUADRANT 4: THREATS -->
            <div ui-card class="border-warning/30 dark:border-warning/20 flex flex-col justify-between shadow-xs">
              <div ui-card-header class="border-border/60 bg-warning/5 dark:bg-warning/10 border-b pb-4">
                <div class="flex items-start justify-between gap-3">
                  <div class="flex items-center gap-3">
                    <div
                      class="border-warning/20 bg-warning/10 text-warning flex size-9 items-center justify-center rounded-lg border shadow-xs"
                    >
                      <lucide-icon [img]="ShieldAlert" class="size-5" />
                    </div>
                    <div>
                      <div class="flex items-center gap-2">
                        <h3 ui-card-title class="text-base font-bold sm:text-lg">Threats</h3>
                        <span ui-badge class="border-warning/20 bg-warning/15 text-warning text-xs font-semibold">
                          {{ threats().length }}
                        </span>
                      </div>
                      <p ui-card-description class="text-xs">External risks, competitive forces &amp; ecosystem shifts</p>
                    </div>
                  </div>
                  <div class="flex items-center gap-1.5">
                    <span ui-badge variant="outline" class="border-warning/30 text-warning hidden text-xs sm:inline-flex">
                      External
                    </span>
                    <span ui-badge variant="outline" class="border-warning/30 text-warning text-xs"> Negative </span>
                  </div>
                </div>
              </div>
    
              <div ui-card-content class="flex-1 space-y-3 p-4 sm:p-5">
                @for (item of threats(); track item.id) {
                  <div
                    class="bg-card hover:border-warning/40 relative rounded-lg border p-3.5 shadow-xs transition-[border-color,box-shadow]"
                  >
                    <div class="flex items-start justify-between gap-2">
                      <div class="space-y-1">
                        <div class="flex flex-wrap items-center gap-2">
                          <h4 class="text-foreground text-sm leading-snug font-semibold">{{ item.title }}</h4>
                          <span
                            ui-badge
                            variant="outline"
                            class="border-warning/20 bg-warning/10 text-warning text-xs font-normal"
                          >
                            {{ item.tag }}
                          </span>
                        </div>
                        <p class="text-muted-foreground text-xs leading-relaxed">{{ item.description }}</p>
                      </div>
                      @if (!isReadonly()) {
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="text-muted-foreground hover:text-destructive hover:bg-destructive/10 -mt-1 -mr-1 size-7 shrink-0 p-0 opacity-80 group-hover:opacity-100"
                          aria-label="Remove threat item"
                          (click)="removeThreat(item.id)"
                        >
                          <lucide-icon [img]="Trash2" class="size-3.5" />
                        </button>
                      }
                    </div>
                  </div>
                }
                @if (threats().length === 0) {
                  <div class="text-muted-foreground rounded-lg border border-dashed py-8 text-center text-xs">
                    No threat items listed. Add an external risk factor below.
                  </div>
                }
              </div>
    
              @if (!isReadonly()) {
                <div ui-card-footer class="border-border/60 bg-muted/20 border-t p-3 sm:p-4">
                  <form class="flex w-full items-center gap-2" (submit)="addThreat($event)">
                    <ui-input
                      [value]="inputThreat()"
                      (valueChange)="inputThreat.set($event)"
                      type="text"
                      placeholder="Add key threat... (Press Enter)"
                      class="h-8.5 text-xs shadow-xs"
                    />
                    <button
                      ui-button
                      type="submit"
                      size="sm"
                      class="bg-warning text-warning-foreground hover:bg-warning/90 h-8.5 shrink-0 gap-1.5 text-xs shadow-xs"
                    >
                      <lucide-icon [img]="Plus" class="size-3.5" />
                      <span>Add</span>
                    </button>
                  </form>
                </div>
              }
            </div>
          </div>
    
          <!-- Strategic Synthesis & Key Next Actions Card (TOWS Strategy) -->
          <div ui-card class="shadow-xs">
            <div ui-card-header class="border-border/60 bg-muted/15 border-b pb-4">
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                <div class="space-y-1">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="Zap" class="text-primary size-4" />
                    <h3 ui-card-title class="text-base font-bold sm:text-lg">Strategic Synthesis &amp; Key Next Actions</h3>
                  </div>
                  <p ui-card-description class="text-xs">
                    Actionable strategic initiatives bridging internal capabilities with external market dynamics (TOWS
                    matrix formulation).
                  </p>
                </div>
                <span ui-badge variant="outline" class="w-fit text-xs font-medium">
                  {{ strategicInitiatives().length }} Active Initiatives
                </span>
              </div>
            </div>
    
            <div ui-card-content class="p-4 sm:p-6">
              <div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
                @for (initiative of strategicInitiatives(); track initiative.id) {
                  <div
                    class="bg-muted/30 hover:border-primary/40 flex flex-col justify-between rounded-xl border p-4 shadow-xs transition-[border-color,box-shadow]"
                  >
                    <div class="space-y-3">
                      <div class="flex items-center justify-between gap-2">
                        <span ui-badge variant="outline" [class]="strategyBadgeClass(initiative.strategyType)">
                          {{ initiative.strategyType }} Strategy
                        </span>
                        <span
                          ui-badge
                          [variant]="initiative.priority === 'Critical' ? 'destructive' : 'secondary'"
                          class="text-xs font-medium whitespace-normal"
                        >
                          {{ initiative.priority }}
                        </span>
                      </div>
                      <div class="space-y-1.5">
                        <h4 class="text-foreground text-sm leading-snug font-semibold">{{ initiative.title }}</h4>
                        <p class="text-muted-foreground text-xs leading-relaxed">{{ initiative.description }}</p>
                      </div>
                    </div>
                    <div class="border-border/50 mt-4 flex items-center justify-between gap-2 border-t pt-3 text-xs">
                      <div class="text-muted-foreground flex items-center gap-1.5">
                        <lucide-icon [img]="Clock" class="size-3.5" />
                        <span>{{ initiative.timeframe }}</span>
                      </div>
                      <span class="bg-primary/10 text-primary rounded px-2 py-0.5 text-xs font-medium">
                        {{ initiative.targetMetric }}
                      </span>
                    </div>
                  </div>
                }
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiMatrixSwotAnalysisComponent {
      protected readonly AlertTriangle = AlertTriangle
      protected readonly Calendar = Calendar
      protected readonly Check = Check
      protected readonly Clock = Clock
      protected readonly Download = Download
      protected readonly Layers = Layers
      protected readonly Plus = Plus
      protected readonly Share2 = Share2
      protected readonly ShieldAlert = ShieldAlert
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Trash2 = Trash2
      protected readonly TrendingUp = TrendingUp
      protected readonly Zap = Zap
    
      @Input() set title(v: string | undefined) {
        this.titleText.set(v ?? 'SWOT Strategic Analysis Canvas')
      }
      @Input() set project(v: string | undefined) {
        this.projectText.set(v ?? 'UIPKGE Unbundled UI Registry Strategy · Q3 2026')
      }
      @Input() set lastUpdated(v: string | undefined) {
        this.lastUpdatedText.set(v ?? 'Aug 21, 2026')
      }
      @Input() set initialStrengths(v: SwotItem[] | undefined) {
        if (v) this.strengths.set([...v])
      }
      @Input() set initialWeaknesses(v: SwotItem[] | undefined) {
        if (v) this.weaknesses.set([...v])
      }
      @Input() set initialOpportunities(v: SwotItem[] | undefined) {
        if (v) this.opportunities.set([...v])
      }
      @Input() set initialThreats(v: SwotItem[] | undefined) {
        if (v) this.threats.set([...v])
      }
      @Input() set initiatives(v: StrategicInitiative[] | undefined) {
        if (v) this.strategicInitiatives.set([...v])
      }
      @Input() set readonly(v: boolean) {
        this.isReadonly.set(!!v)
      }
      @Input('class') className?: string
    
      readonly titleText = signal('SWOT Strategic Analysis Canvas')
      readonly projectText = signal('UIPKGE Unbundled UI Registry Strategy · Q3 2026')
      readonly lastUpdatedText = signal('Aug 21, 2026')
      readonly isReadonly = signal(false)
    
      readonly strengths = signal<SwotItem[]>([...defaultStrengths])
      readonly weaknesses = signal<SwotItem[]>([...defaultWeaknesses])
      readonly opportunities = signal<SwotItem[]>([...defaultOpportunities])
      readonly threats = signal<SwotItem[]>([...defaultThreats])
      readonly strategicInitiatives = signal<StrategicInitiative[]>([...defaultInitiatives])
    
      readonly inputStrength = signal('')
      readonly inputWeakness = signal('')
      readonly inputOpportunity = signal('')
      readonly inputThreat = signal('')
    
      readonly isCopied = signal(false)
      readonly isExporting = signal(false)
    
      readonly totalCards = computed(
        () => this.strengths().length + this.weaknesses().length + this.opportunities().length + this.threats().length,
      )
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      strategyBadgeClass(t: StrategicInitiative['strategyType']): string {
        return cn(
          'text-xs font-semibold',
          t === 'SO' && 'border-success/20 bg-success/10 text-success',
          t === 'WO' && 'border-info/20 bg-info/10 text-info',
          t === 'ST' && 'border-warning/20 bg-warning/10 text-warning',
          t === 'WT' && 'border-destructive/20 bg-destructive/10 text-destructive',
        )
      }
    
      addStrength(e?: Event): void {
        e?.preventDefault()
        const text = this.inputStrength().trim()
        if (!text) return
        this.strengths.update((prev) => [
          ...prev,
          {
            id: `s-${Date.now()}`,
            title: text,
            description: 'Custom strategic strength added during canvas session.',
            impact: 'high',
            tag: 'Internal Strategic Advantage',
          },
        ])
        this.inputStrength.set('')
      }
    
      removeStrength(id: string): void {
        this.strengths.update((prev) => prev.filter((item) => item.id !== id))
      }
    
      addWeakness(e?: Event): void {
        e?.preventDefault()
        const text = this.inputWeakness().trim()
        if (!text) return
        this.weaknesses.update((prev) => [
          ...prev,
          {
            id: `w-${Date.now()}`,
            title: text,
            description: 'Custom strategic vulnerability identified for remediation.',
            impact: 'medium',
            tag: 'Internal Remediation',
          },
        ])
        this.inputWeakness.set('')
      }
    
      removeWeakness(id: string): void {
        this.weaknesses.update((prev) => prev.filter((item) => item.id !== id))
      }
    
      addOpportunity(e?: Event): void {
        e?.preventDefault()
        const text = this.inputOpportunity().trim()
        if (!text) return
        this.opportunities.update((prev) => [
          ...prev,
          {
            id: `o-${Date.now()}`,
            title: text,
            description: 'External market dynamic or technological growth tailwind.',
            impact: 'high',
            tag: 'External Opportunity',
          },
        ])
        this.inputOpportunity.set('')
      }
    
      removeOpportunity(id: string): void {
        this.opportunities.update((prev) => prev.filter((item) => item.id !== id))
      }
    
      addThreat(e?: Event): void {
        e?.preventDefault()
        const text = this.inputThreat().trim()
        if (!text) return
        this.threats.update((prev) => [
          ...prev,
          {
            id: `t-${Date.now()}`,
            title: text,
            description: 'External industry risk factor requiring proactive defensive mitigation.',
            impact: 'high',
            tag: 'External Threat',
          },
        ])
        this.inputThreat.set('')
      }
    
      removeThreat(id: string): void {
        this.threats.update((prev) => prev.filter((item) => item.id !== id))
      }
    
      handleShare(): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(window.location.href)
          this.isCopied.set(true)
          setTimeout(() => this.isCopied.set(false), 2000)
        }
      }
    
      handleExportPdf(): void {
        this.isExporting.set(true)
        setTimeout(() => this.isExporting.set(false), 1500)
      }
    }
    
  • src/app/components/blocks/matrix-swot-analysis/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/matrix-swot-analysis.json