From 49907ef9991d2f13d53bb02fa648c8a06fbfe80a Mon Sep 17 00:00:00 2001
From: Tyler Hoang <tyler@tylerhoang.xyz>
Date: Mon, 18 May 2026 01:35:04 -0700
Subject: feat: add ValuationCard component and psm-val-* CSS

---
 frontend/components/prism/ValuationCard.tsx | 190 ++++++++++++++++++++++++++++
 1 file changed, 190 insertions(+)
 create mode 100644 frontend/components/prism/ValuationCard.tsx

(limited to 'frontend/components/prism/ValuationCard.tsx')

diff --git a/frontend/components/prism/ValuationCard.tsx b/frontend/components/prism/ValuationCard.tsx
new file mode 100644
index 0000000..7329ff5
--- /dev/null
+++ b/frontend/components/prism/ValuationCard.tsx
@@ -0,0 +1,190 @@
+"use client";
+import type { ValuationResponse } from "@/types/api";
+import { deltaClass, fmtCurrency, fmtLarge, fmtPct } from "@/lib/format";
+
+type Props = { data: ValuationResponse };
+
+function pctVsCurrent(implied?: number | null, current?: number | null): number | null {
+  if (implied == null || current == null || current === 0) return null;
+  return (implied - current) / current;
+}
+
+function SummaryChip({
+  label,
+  price,
+  current,
+  accent = false,
+}: {
+  label: string;
+  price?: number | null;
+  current?: number | null;
+  accent?: boolean;
+}) {
+  const pct = pctVsCurrent(price, current);
+  return (
+    <div className={`psm-val-chip${accent ? " accent" : ""}`}>
+      <span className="psm-val-chip-label">{label}</span>
+      <span className="psm-val-chip-price">{price != null ? fmtCurrency(price) : "—"}</span>
+      {pct != null && (
+        <span className={`psm-val-chip-pct ${deltaClass(pct)}`}>{fmtPct(pct, 1, true)}</span>
+      )}
+    </div>
+  );
+}
+
+function MultipleRow({
+  label,
+  multiple,
+  price,
+  current,
+}: {
+  label: string;
+  multiple?: number | null;
+  price?: number | null;
+  current?: number | null;
+}) {
+  const pct = pctVsCurrent(price, current);
+  return (
+    <div className="psm-val-mult-row">
+      <span className="psm-val-mult-label">{label}</span>
+      <span className="psm-val-mult-x">{multiple != null ? `${multiple.toFixed(1)}×` : "—"}</span>
+      <span className="psm-val-mult-price">{price != null ? fmtCurrency(price) : "—"}</span>
+      <span className={`psm-val-mult-pct ${pct != null ? deltaClass(pct) : "neutral"}`}>
+        {pct != null ? fmtPct(pct, 1, true) : "—"}
+      </span>
+    </div>
+  );
+}
+
+export function ValuationCard({ data }: Props) {
+  const { dcf, ev_ebitda, ev_revenue, price_to_book, current_price } = data;
+  const dcfPrice = dcf.available && !dcf.error ? dcf.intrinsic_value_per_share : null;
+  const hasMultiples = ev_ebitda.available || ev_revenue.available || price_to_book.available;
+
+  return (
+    <section className="psm-card psm-val-card">
+      {/* Summary strip */}
+      <div className="psm-val-strip">
+        <div className="psm-val-chip accent">
+          <span className="psm-val-chip-label">Market Price</span>
+          <span className="psm-val-chip-price">
+            {current_price != null ? fmtCurrency(current_price) : "—"}
+          </span>
+        </div>
+        <SummaryChip label="DCF" price={dcfPrice} current={current_price} />
+        <SummaryChip
+          label="EV / EBITDA"
+          price={ev_ebitda.available ? ev_ebitda.implied_price_per_share : null}
+          current={current_price}
+        />
+        <SummaryChip
+          label="EV / Revenue"
+          price={ev_revenue.available ? ev_revenue.implied_price_per_share : null}
+          current={current_price}
+        />
+        <SummaryChip
+          label="P / Book"
+          price={price_to_book.available ? price_to_book.implied_price_per_share : null}
+          current={current_price}
+        />
+      </div>
+
+      {/* DCF detail */}
+      <div className="psm-val-section">
+        <div className="psm-val-section-head">
+          <span className="psm-eyebrow">Discounted Cash Flow</span>
+          <span className="psm-val-wacc-note">
+            WACC {(dcf.wacc * 100).toFixed(1)}% · Terminal {(dcf.terminal_growth * 100).toFixed(1)}%
+          </span>
+        </div>
+
+        {!dcf.available && (
+          <p className="psm-muted-copy">Insufficient free cash flow history for DCF.</p>
+        )}
+        {dcf.available && dcf.error && (
+          <p className="psm-val-dcf-error">{dcf.error}</p>
+        )}
+        {dcf.available && !dcf.error && (
+          <div className="psm-val-dcf-body">
+            <div className="psm-val-kv-list">
+              <div className="psm-val-kv-row">
+                <span className="psm-val-kv-label">Base FCF (TTM)</span>
+                <span className="psm-val-kv-val">{dcf.base_fcf != null ? fmtLarge(dcf.base_fcf) : "—"}</span>
+              </div>
+              <div className="psm-val-kv-row">
+                <span className="psm-val-kv-label">Historical growth</span>
+                <span className="psm-val-kv-val">
+                  {dcf.growth_rate_used != null ? fmtPct(dcf.growth_rate_used) : "—"}
+                </span>
+              </div>
+              <div className="psm-val-kv-row is-divider">
+                <span className="psm-val-kv-label">Enterprise Value</span>
+                <span className="psm-val-kv-val">
+                  {dcf.enterprise_value != null ? fmtLarge(dcf.enterprise_value) : "—"}
+                </span>
+              </div>
+              <div className="psm-val-kv-row">
+                <span className="psm-val-kv-label">Net Debt</span>
+                <span className="psm-val-kv-val">
+                  {dcf.net_debt != null ? fmtLarge(dcf.net_debt) : "—"}
+                </span>
+              </div>
+              <div className="psm-val-kv-row is-total">
+                <span className="psm-val-kv-label">Equity Value</span>
+                <span className="psm-val-kv-val">
+                  {dcf.equity_value != null ? fmtLarge(dcf.equity_value) : "—"}
+                </span>
+              </div>
+            </div>
+            <div className="psm-val-intrinsic">
+              <span className="psm-val-intrinsic-label">Intrinsic Value</span>
+              <span className="psm-val-intrinsic-price">
+                {dcf.intrinsic_value_per_share != null ? fmtCurrency(dcf.intrinsic_value_per_share) : "—"}
+              </span>
+              {data.shares_outstanding != null && (
+                <span className="psm-val-intrinsic-shares">
+                  {fmtLarge(data.shares_outstanding)} shares
+                </span>
+              )}
+            </div>
+          </div>
+        )}
+      </div>
+
+      {/* Multiples */}
+      {hasMultiples && (
+        <div className="psm-val-section">
+          <div className="psm-val-section-head">
+            <span className="psm-eyebrow">Multiples — at current market multiple</span>
+          </div>
+          <div className="psm-val-mult-list">
+            {ev_ebitda.available && (
+              <MultipleRow
+                label="EV / EBITDA"
+                multiple={ev_ebitda.multiple_used}
+                price={ev_ebitda.implied_price_per_share}
+                current={current_price}
+              />
+            )}
+            {ev_revenue.available && (
+              <MultipleRow
+                label="EV / Revenue"
+                multiple={ev_revenue.multiple_used}
+                price={ev_revenue.implied_price_per_share}
+                current={current_price}
+              />
+            )}
+            {price_to_book.available && (
+              <MultipleRow
+                label="P / Book"
+                multiple={price_to_book.multiple_used}
+                price={price_to_book.implied_price_per_share}
+                current={current_price}
+              />
+            )}
+          </div>
+        </div>
+      )}
+    </section>
+  );
+}
-- 
cgit v1.3-2-g0d8e

