Docs/dolt plan clarifications #1
@@ -10,39 +10,31 @@ interface FundamentalsPanelProps {
|
|||||||
data: FundamentalResponse;
|
data: FundamentalResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Positive / neutral / negative styling, always paired with the read text. */
|
/** Favorable / neutral / adverse — always paired with the read text. */
|
||||||
type Tone = 'up' | 'flat' | 'down';
|
type Tone = 'good' | 'flat' | 'bad';
|
||||||
|
|
||||||
const TONE_TEXT: Record<Tone, string> = {
|
// Horizon tokens (native to the rest of the product).
|
||||||
up: 'text-emerald-400',
|
const HZ = {
|
||||||
flat: 'text-gray-400',
|
text: '#EDEEF3',
|
||||||
down: 'text-rose-400',
|
muted: '#9AA0B0',
|
||||||
};
|
track: '#5D6373',
|
||||||
const TONE_CELL: Record<Tone, string> = {
|
fav: '#6EC9DB', // cyan
|
||||||
up: 'bg-emerald-400/10 text-emerald-200 ring-emerald-400/20',
|
adv: '#EF9182', // coral
|
||||||
flat: 'bg-white/5 text-gray-200 ring-white/10',
|
|
||||||
down: 'bg-rose-400/10 text-rose-200 ring-rose-400/20',
|
|
||||||
};
|
|
||||||
const TONE_BAR: Record<Tone, string> = {
|
|
||||||
up: 'bg-emerald-400',
|
|
||||||
flat: 'bg-gray-400',
|
|
||||||
down: 'bg-rose-400',
|
|
||||||
};
|
};
|
||||||
|
const toneColor = (t: Tone) => (t === 'good' ? HZ.fav : t === 'bad' ? HZ.adv : HZ.muted);
|
||||||
|
|
||||||
const POSITIVE_READS = new Set([
|
const POSITIVE_READS = new Set([
|
||||||
'accelerating', 'improving', 'above peers', 'buying back', 'attractively valued',
|
'accelerating', 'improving', 'above peers', 'above own average', 'buying back',
|
||||||
'conservative leverage',
|
'attractively valued', 'conservative leverage',
|
||||||
]);
|
]);
|
||||||
const NEGATIVE_READS = new Set([
|
const NEGATIVE_READS = new Set([
|
||||||
'decelerating', 'deteriorating', 'priced above peers', 'elevated leverage',
|
'decelerating', 'deteriorating', 'priced above peers', 'elevated leverage', 'below peers',
|
||||||
'below peers',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function readTone(read: string | null | undefined): Tone {
|
function readTone(read: string | null | undefined): Tone {
|
||||||
if (!read) return 'flat';
|
if (!read) return 'flat';
|
||||||
if (POSITIVE_READS.has(read)) return 'up';
|
if (POSITIVE_READS.has(read)) return 'good';
|
||||||
if (NEGATIVE_READS.has(read)) return 'down';
|
if (NEGATIVE_READS.has(read)) return 'bad';
|
||||||
if (read.includes('dilution')) return 'down';
|
if (read.includes('dilution')) return 'bad';
|
||||||
return 'flat';
|
return 'flat';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +54,11 @@ function money(v: number | null | undefined): string {
|
|||||||
if (abs >= 1e6) return `$${(v / 1e6).toFixed(1)}M`;
|
if (abs >= 1e6) return `$${(v / 1e6).toFixed(1)}M`;
|
||||||
return `$${v.toFixed(0)}`;
|
return `$${v.toFixed(0)}`;
|
||||||
}
|
}
|
||||||
|
function signedPp(v: number): string {
|
||||||
|
return `${v >= 0 ? '+' : ''}${Math.round(v * 10) / 10}pp`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse a YYYY-MM-DD string as a LOCAL calendar date (avoids the UTC-midnight
|
/** Parse YYYY-MM-DD as a LOCAL calendar date (no UTC off-by-one west of UTC). */
|
||||||
* off-by-one that shows the previous day west of UTC). */
|
|
||||||
function parseLocalDate(s: string): Date {
|
function parseLocalDate(s: string): Date {
|
||||||
const [y, m, d] = s.split('-').map(Number);
|
const [y, m, d] = s.split('-').map(Number);
|
||||||
return new Date(y, (m ?? 1) - 1, d ?? 1);
|
return new Date(y, (m ?? 1) - 1, d ?? 1);
|
||||||
@@ -81,22 +75,22 @@ export function FundamentalsPanel({ data }: FundamentalsPanelProps) {
|
|||||||
const reads = data.reads?.by_key ?? {};
|
const reads = data.reads?.by_key ?? {};
|
||||||
const val = data.valuation;
|
const val = data.valuation;
|
||||||
const earnings = data.earnings;
|
const earnings = data.earnings;
|
||||||
|
|
||||||
// per-metric provenance/freshness (all SEC snapshot metrics share the latest filing)
|
|
||||||
const provenance = (data.metrics ?? []).find((m) => m.period_end) ?? null;
|
const provenance = (data.metrics ?? []).find((m) => m.period_end) ?? null;
|
||||||
|
|
||||||
const hasAny = (data.metrics ?? []).some((m) => m.value != null) || !!val || !!earnings?.next
|
const hasAny = (data.metrics ?? []).some((m) => m.value != null) || !!val || !!earnings?.next
|
||||||
|| (earnings?.recent?.length ?? 0) > 0;
|
|| (earnings?.recent?.length ?? 0) > 0;
|
||||||
|
|
||||||
const tapeRows = [
|
// Operating trend: value vs a truthful reference (prior quarter / prior avg / zero).
|
||||||
{ key: 'revenue_growth_yoy', label: 'Revenue growth', fmt: pct },
|
const trendRows: { key: string; label: string; kind: 'growth' | 'margin' | 'share' }[] = [
|
||||||
{ key: 'eps_growth_yoy', label: 'EPS growth', fmt: pct },
|
{ key: 'revenue_growth_yoy', label: 'Revenue growth', kind: 'growth' },
|
||||||
{ key: 'operating_margin', label: 'Operating margin', fmt: pct },
|
{ key: 'eps_growth_yoy', label: 'EPS growth', kind: 'growth' },
|
||||||
{ key: 'fcf_margin', label: 'FCF margin', fmt: pct },
|
{ key: 'operating_margin', label: 'Operating margin', kind: 'margin' },
|
||||||
{ key: 'share_count_change_yoy', label: 'Share count', fmt: pct },
|
{ key: 'fcf_margin', label: 'FCF margin', kind: 'margin' },
|
||||||
|
{ key: 'share_count_change_yoy', label: 'Share count', kind: 'share' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const valuationRows: {
|
// Valuation & balance: favorable percentile vs peer median.
|
||||||
|
const valueRows: {
|
||||||
label: string; value: number | null; industry: MetricIndustry | null;
|
label: string; value: number | null; industry: MetricIndustry | null;
|
||||||
readKey: string; fmt: (v: number | null) => string;
|
readKey: string; fmt: (v: number | null) => string;
|
||||||
}[] = [
|
}[] = [
|
||||||
@@ -110,41 +104,37 @@ export function FundamentalsPanel({ data }: FundamentalsPanelProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="glass p-5" aria-label="Fundamentals">
|
<section className="glass p-5" aria-label="Fundamentals">
|
||||||
<div className="mb-1 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
<div className="mb-1 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
|
||||||
<h3 className="text-xs font-medium uppercase tracking-widest text-gray-500">Fundamentals</h3>
|
<h3 className="text-xs font-medium uppercase tracking-widest" style={{ color: HZ.muted }}>Fundamentals</h3>
|
||||||
{data.reads?.header && (
|
{data.reads?.header && (
|
||||||
<p className="text-[11.5px] text-gray-400 sm:text-right">{data.reads.header}</p>
|
<p className="text-[11.5px] sm:text-right" style={{ color: HZ.muted }}>{data.reads.header}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!hasAny ? (
|
{!hasAny ? (
|
||||||
<p className="mt-3 text-sm text-gray-500">No fundamentals reported yet.</p>
|
<p className="mt-3 text-sm" style={{ color: HZ.muted }}>No fundamentals reported yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<EarningsStrip earnings={earnings} />
|
<EarningsStrip earnings={earnings} />
|
||||||
|
|
||||||
{/* Quarter tape — the panel's signature device */}
|
<div className="mt-4 grid gap-x-8 gap-y-5 sm:grid-cols-2">
|
||||||
<div className="mt-4">
|
<div>
|
||||||
<div className="mb-1.5 flex items-baseline justify-between">
|
<SectionLabel>Operating trend</SectionLabel>
|
||||||
<span className="text-[10px] font-medium uppercase tracking-widest text-gray-500">Quarter tape</span>
|
<div className="mt-2 space-y-3.5">
|
||||||
<span className="hidden text-[10px] uppercase tracking-widest text-gray-600 sm:inline">Latest · read</span>
|
{trendRows.map((r) => (
|
||||||
</div>
|
<TrendRow key={r.key} label={r.label} kind={r.kind}
|
||||||
<div className="space-y-2 sm:space-y-1.5">
|
metric={metrics[r.key]} read={reads[r.key]} />
|
||||||
{tapeRows.map((row) => (
|
|
||||||
<TapeRow key={row.key} label={row.label} metric={metrics[row.key]}
|
|
||||||
read={reads[row.key]} fmt={row.fmt} />
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
{/* Balance & valuation — restrained peer strips */}
|
<SectionLabel>Valuation & balance</SectionLabel>
|
||||||
<div className="mt-4">
|
<div className="mt-2 space-y-3.5">
|
||||||
<span className="text-[10px] font-medium uppercase tracking-widest text-gray-500">Balance & valuation</span>
|
{valueRows.map((r) => (
|
||||||
<div className="mt-1.5 space-y-2.5 sm:space-y-2">
|
<ValueRow key={r.label} {...r} read={reads[r.readKey]} />
|
||||||
{valuationRows.map((row) => (
|
|
||||||
<ValuationRow key={row.label} {...row} read={reads[row.readKey]} />
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Provenance provenance={provenance} priceDate={val?.price_date ?? null}
|
<Provenance provenance={provenance} priceDate={val?.price_date ?? null}
|
||||||
marketCap={val?.market_cap_est ?? null} />
|
marketCap={val?.market_cap_est ?? null} />
|
||||||
@@ -154,90 +144,147 @@ export function FundamentalsPanel({ data }: FundamentalsPanelProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TapeRow({ label, metric, read, fmt }: {
|
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||||
label: string; metric: MetricItem | undefined; read: string | null | undefined;
|
return <span className="text-[10px] font-medium uppercase tracking-widest" style={{ color: HZ.muted }}>{children}</span>;
|
||||||
fmt: (v: number | null) => string;
|
}
|
||||||
|
|
||||||
|
// ---- operating-trend row: value vs reference, shown as a delta rail ----------
|
||||||
|
|
||||||
|
function TrendRow({ label, kind, metric, read }: {
|
||||||
|
label: string; kind: 'growth' | 'margin' | 'share';
|
||||||
|
metric: MetricItem | undefined; read: string | null | undefined;
|
||||||
}) {
|
}) {
|
||||||
const history = metric?.history ?? [];
|
|
||||||
const tone = readTone(read);
|
const tone = readTone(read);
|
||||||
const cells = (
|
const value = metric?.value ?? null;
|
||||||
<div className="flex justify-end gap-1" role="img"
|
const history = (metric?.history ?? []).map((h) => h.value).filter((v): v is number => v != null);
|
||||||
aria-label={`${label} last ${history.length}: ${history.map((h) => fmt(h.value)).join(', ') || 'no data'}`}>
|
|
||||||
{history.length === 0 && <span className="num text-xs text-gray-600">—</span>}
|
// reference + neutral band per the backend's deterministic rules
|
||||||
{history.map((h, i) => {
|
let ref: number | null = null;
|
||||||
const latest = i === history.length - 1;
|
let refLabel = '';
|
||||||
|
let halfRange = 8;
|
||||||
|
let neutral = 2;
|
||||||
|
if (kind === 'growth') {
|
||||||
|
ref = history.length >= 2 ? history[history.length - 2] : null;
|
||||||
|
refLabel = ref != null ? `prior ${pct(ref)}` : '';
|
||||||
|
halfRange = 8; neutral = 2;
|
||||||
|
} else if (kind === 'margin') {
|
||||||
|
const prior = history.slice(0, -1);
|
||||||
|
ref = prior.length ? prior.reduce((a, b) => a + b, 0) / prior.length : null;
|
||||||
|
refLabel = ref != null ? `avg ${pct(ref)}` : '';
|
||||||
|
halfRange = 4; neutral = 1;
|
||||||
|
} else {
|
||||||
|
ref = 0; refLabel = 'vs zero'; halfRange = 5; neutral = 1;
|
||||||
|
}
|
||||||
|
const delta = value != null && ref != null ? value - ref : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={i}
|
<div>
|
||||||
className={`num rounded px-1.5 py-0.5 text-[11px] ring-1 ${latest ? TONE_CELL[tone] : 'bg-white/5 text-gray-400 ring-white/5'}`}>
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
{fmt(h.value)}
|
<span className="truncate text-sm" style={{ color: HZ.muted }}>{label}</span>
|
||||||
</span>
|
<span className="num text-sm" style={{ color: HZ.text }}>{pct(value)}</span>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="text-[11.5px] font-medium" style={{ color: toneColor(tone) }}>{read ?? '—'}</div>
|
||||||
return (
|
{delta != null ? (
|
||||||
<div className="sm:grid sm:grid-cols-[minmax(0,7rem)_1fr_auto] sm:items-center sm:gap-3">
|
<div className="mt-1 flex items-center gap-2">
|
||||||
{/* mobile: label + read on one line, cells below (avoids narrow-width overflow) */}
|
<span className="w-20 shrink-0 text-[10px]" style={{ color: HZ.muted }}>{refLabel}</span>
|
||||||
<div className="flex items-baseline justify-between gap-2 sm:hidden">
|
<DeltaRail delta={delta} halfRange={halfRange} neutral={neutral} tone={tone}
|
||||||
<span className="text-sm text-gray-400">{label}</span>
|
ariaLabel={`${label} ${pct(value)}, ${refLabel || 'reference'}, change ${signedPp(delta)}, ${read ?? 'no read'}`} />
|
||||||
<span className={`text-[11.5px] font-medium ${TONE_TEXT[tone]}`}>{read ?? '—'}</span>
|
<span className="num w-14 shrink-0 text-right text-[10px]" style={{ color: toneColor(tone) }}>{signedPp(delta)}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden truncate text-sm text-gray-400 sm:block">{label}</span>
|
) : (
|
||||||
<div className="mt-1 sm:mt-0">{cells}</div>
|
value == null && <div className="mt-1 text-[10px]" style={{ color: HZ.track }}>n/a</div>
|
||||||
<span className={`hidden text-right text-[11.5px] font-medium sm:block sm:w-24 ${TONE_TEXT[tone]}`}>
|
)}
|
||||||
{read ?? '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ValuationRow({ label, value, industry, read, fmt }: {
|
/** A comparison rail centered on a reference line (not a progress bar): a dot at
|
||||||
|
* the current delta with a bar connecting it back to the reference. */
|
||||||
|
function DeltaRail({ delta, halfRange, neutral, tone, ariaLabel }: {
|
||||||
|
delta: number; halfRange: number; neutral: number; tone: Tone; ariaLabel: string;
|
||||||
|
}) {
|
||||||
|
const clamped = Math.max(-halfRange, Math.min(halfRange, delta));
|
||||||
|
const pos = 50 + (clamped / halfRange) * 50; // %
|
||||||
|
const barLeft = Math.min(50, pos);
|
||||||
|
const barWidth = Math.abs(pos - 50);
|
||||||
|
const bandHalf = (neutral / halfRange) * 50;
|
||||||
|
const color = toneColor(tone);
|
||||||
|
return (
|
||||||
|
<span className="relative block h-1.5 flex-1 rounded-full" role="img" aria-label={ariaLabel}
|
||||||
|
style={{ background: 'rgba(93,99,115,0.22)' }}>
|
||||||
|
<span className="absolute inset-y-0 rounded-full" aria-hidden
|
||||||
|
style={{ left: `${50 - bandHalf}%`, width: `${2 * bandHalf}%`, background: 'rgba(93,99,115,0.4)' }} />
|
||||||
|
<span className="absolute inset-y-[-2px] w-px" aria-hidden style={{ left: '50%', background: 'rgba(237,238,243,0.55)' }} />
|
||||||
|
<span className="absolute inset-y-0 rounded-full" aria-hidden style={{ left: `${barLeft}%`, width: `${barWidth}%`, background: color }} />
|
||||||
|
<span className="absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full" aria-hidden
|
||||||
|
style={{ left: `${pos}%`, background: color, boxShadow: '0 0 0 2px #11131C' }} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- valuation/balance row: favorable percentile vs peer median -------------
|
||||||
|
|
||||||
|
function ValueRow({ label, value, industry, read, fmt }: {
|
||||||
label: string; value: number | null; industry: MetricIndustry | null;
|
label: string; value: number | null; industry: MetricIndustry | null;
|
||||||
read: string | null | undefined; fmt: (v: number | null) => string;
|
read: string | null | undefined; fmt: (v: number | null) => string;
|
||||||
}) {
|
}) {
|
||||||
const tone = readTone(read);
|
const tone = readTone(read);
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-[minmax(0,8.5rem)_auto_1fr] items-center gap-x-3 gap-y-0.5">
|
<div>
|
||||||
<span className="truncate text-sm text-gray-400">{label}</span>
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
<span className="num text-sm text-gray-100">{fmt(value)}</span>
|
<span className="truncate text-sm" style={{ color: HZ.muted }}>{label}</span>
|
||||||
<div className="flex items-center justify-end gap-2 text-right">
|
<span className="num text-sm" style={{ color: HZ.text }}>{fmt(value)}</span>
|
||||||
{industry ? (
|
</div>
|
||||||
|
{value == null ? (
|
||||||
|
<div className="mt-1 text-[10px]" style={{ color: HZ.track }}>n/a</div>
|
||||||
|
) : industry ? (
|
||||||
<>
|
<>
|
||||||
{/* visible compact peer context (also the only peer info on mobile) */}
|
<div className="text-[11.5px] font-medium" style={{ color: toneColor(tone) }}>{read ?? 'in line'}</div>
|
||||||
<span className="text-[11px] text-gray-500">med {fmt(industry.median)} · {industry.peer_count}p</span>
|
<div className="mt-1 flex items-center gap-2">
|
||||||
<PercentileStrip industry={industry} tone={tone} label={label} fmt={fmt} />
|
<span className="w-3 text-[9px]" style={{ color: HZ.track }}>0</span>
|
||||||
<span className={`text-[11.5px] font-medium ${TONE_TEXT[tone]}`}>{read ?? 'in line'}</span>
|
<PercentileRail percentile={industry.favorable_percentile} tone={tone}
|
||||||
|
ariaLabel={`${label}: ${industry.favorable_percentile}th favorable percentile vs ${industry.label}, median ${fmt(industry.median)}, ${industry.peer_count} peers`} />
|
||||||
|
<span className="w-6 text-right text-[9px]" style={{ color: HZ.track }}>100</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 pl-5 text-[10px]" style={{ color: HZ.muted }}>
|
||||||
|
median {fmt(industry.median)} · {industry.peer_count} peers
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[11px] text-gray-600">peers n/a</span>
|
<div className="text-[11px]" style={{ color: HZ.track }}>peers n/a</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PercentileStrip({ industry, tone, label, fmt }: {
|
/** Percentile rail 0-100 with the peer median fixed at 50; right = more favorable
|
||||||
industry: MetricIndustry; tone: Tone; label: string; fmt: (v: number | null) => string;
|
* (the percentile is already polarity-aware). */
|
||||||
|
function PercentileRail({ percentile, tone, ariaLabel }: {
|
||||||
|
percentile: number; tone: Tone; ariaLabel: string;
|
||||||
}) {
|
}) {
|
||||||
const p = Math.max(0, Math.min(100, industry.favorable_percentile));
|
const p = Math.max(0, Math.min(100, percentile));
|
||||||
|
const barLeft = Math.min(50, p);
|
||||||
|
const barWidth = Math.abs(p - 50);
|
||||||
|
const color = toneColor(tone);
|
||||||
return (
|
return (
|
||||||
<span
|
<span className="relative block h-1.5 flex-1 rounded-full" role="img" aria-label={ariaLabel}
|
||||||
className="relative hidden h-1.5 w-16 rounded-full bg-white/10 sm:block"
|
style={{ background: 'rgba(93,99,115,0.22)' }}>
|
||||||
role="img"
|
<span className="absolute inset-y-[-2px] w-px" aria-hidden style={{ left: '50%', background: 'rgba(237,238,243,0.55)' }} />
|
||||||
aria-label={`${label}: ${industry.favorable_percentile}th favorable percentile vs ${industry.label}, median ${fmt(industry.median)}, ${industry.peer_count} peers`}
|
<span className="absolute inset-y-0 rounded-full" aria-hidden style={{ left: `${barLeft}%`, width: `${barWidth}%`, background: color }} />
|
||||||
>
|
<span className="absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full" aria-hidden
|
||||||
<span className={`absolute inset-y-0 left-0 rounded-full ${TONE_BAR[tone]}`} style={{ width: `${p}%` }} />
|
style={{ left: `${p}%`, background: color, boxShadow: '0 0 0 2px #11131C' }} />
|
||||||
<span className="absolute inset-y-[-2px] left-1/2 w-px bg-white/40" aria-hidden />
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- earnings + provenance (unchanged behavior) ----------------------------
|
||||||
|
|
||||||
function Provenance({ provenance, priceDate, marketCap }: {
|
function Provenance({ provenance, priceDate, marketCap }: {
|
||||||
provenance: MetricItem | null; priceDate: string | null; marketCap: number | null;
|
provenance: MetricItem | null; priceDate: string | null; marketCap: number | null;
|
||||||
}) {
|
}) {
|
||||||
if (!provenance && !priceDate) return null;
|
if (!provenance?.period_end && !priceDate) return null;
|
||||||
return (
|
return (
|
||||||
<p className="mt-4 border-t border-white/10 pt-2 text-[10px] leading-relaxed text-gray-600">
|
<p className="mt-4 border-t border-white/10 pt-2 text-[10px] leading-relaxed" style={{ color: HZ.track }}>
|
||||||
{provenance?.period_end && (
|
{provenance?.period_end && (
|
||||||
<>SEC filings · latest {shortDate(provenance.period_end)}
|
<>SEC filings · latest {shortDate(provenance.period_end)}
|
||||||
{provenance.filed_date && <> (filed {shortDate(provenance.filed_date)})</>}</>
|
{provenance.filed_date && <> (filed {shortDate(provenance.filed_date)})</>}</>
|
||||||
@@ -252,27 +299,25 @@ function Provenance({ provenance, priceDate, marketCap }: {
|
|||||||
function EarningsStrip({ earnings }: { earnings: FundamentalResponse['earnings'] }) {
|
function EarningsStrip({ earnings }: { earnings: FundamentalResponse['earnings'] }) {
|
||||||
const next = earnings?.next;
|
const next = earnings?.next;
|
||||||
const recent = earnings?.recent ?? [];
|
const recent = earnings?.recent ?? [];
|
||||||
const when = next
|
const when = next ? (next.days_until === 0 ? 'today' : `${next.days_until}d`) : null;
|
||||||
? next.days_until === 0 ? 'today' : `in ${next.days_until}d`
|
|
||||||
: null;
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
|
<div className="mt-2 flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
|
||||||
<span className="text-sm text-gray-300">
|
<span className="text-sm" style={{ color: HZ.text }}>
|
||||||
<span className="text-gray-500">Next earnings </span>
|
<span style={{ color: HZ.muted }}>Next earnings </span>
|
||||||
{next ? (
|
{next ? (
|
||||||
<>
|
<>
|
||||||
{shortDate(next.date)}
|
{shortDate(next.date)}
|
||||||
{' · '}
|
{' · '}
|
||||||
<span className="uppercase">{next.session === 'unknown' ? 'TBD' : next.session}</span>
|
<span className="uppercase">{next.session === 'unknown' ? 'TBD' : next.session}</span>
|
||||||
<span className="text-gray-500"> · {when}</span>
|
<span style={{ color: HZ.muted }}> · {when}</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500">no date</span>
|
<span style={{ color: HZ.muted }}>no date</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{recent.length > 0 && (
|
{recent.length > 0 && (
|
||||||
<span className="flex items-center gap-1.5">
|
<span className="flex items-center gap-1.5">
|
||||||
<span className="text-[10px] uppercase tracking-widest text-gray-600">Last {recent.length}</span>
|
<span className="text-[10px] uppercase tracking-widest" style={{ color: HZ.track }}>Last {recent.length}</span>
|
||||||
{recent.slice().reverse().map((e, i) => <EarningsBar key={i} e={e} />)}
|
{recent.slice().reverse().map((e, i) => <EarningsBar key={i} e={e} />)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -282,15 +327,13 @@ function EarningsStrip({ earnings }: { earnings: FundamentalResponse['earnings']
|
|||||||
|
|
||||||
function EarningsBar({ e }: { e: EarningsRecent }) {
|
function EarningsBar({ e }: { e: EarningsRecent }) {
|
||||||
const beat = e.eps_actual != null && e.eps_estimate != null ? e.eps_actual - e.eps_estimate : null;
|
const beat = e.eps_actual != null && e.eps_estimate != null ? e.eps_actual - e.eps_estimate : null;
|
||||||
const tone: Tone = beat == null ? 'flat' : beat > 0 ? 'up' : beat < 0 ? 'down' : 'flat';
|
const tone: Tone = beat == null ? 'flat' : beat > 0 ? 'good' : beat < 0 ? 'bad' : 'flat';
|
||||||
const label = beat == null ? 'n/a' : beat > 0 ? 'beat' : beat < 0 ? 'miss' : 'in line';
|
const label = beat == null ? 'n/a' : beat > 0 ? 'beat' : beat < 0 ? 'miss' : 'in line';
|
||||||
const arrow = beat == null ? '·' : beat > 0 ? '▲' : beat < 0 ? '▼' : '–';
|
const arrow = beat == null ? '·' : beat > 0 ? '▲' : beat < 0 ? '▼' : '–';
|
||||||
return (
|
return (
|
||||||
<span
|
<span className="num text-xs" style={{ color: toneColor(tone) }}
|
||||||
className={`num rounded px-1 text-[10px] ring-1 ${TONE_CELL[tone]}`}
|
|
||||||
title={`${e.announce_date}: ${label}${e.surprise_pct != null ? ` ${e.surprise_pct > 0 ? '+' : ''}${e.surprise_pct}%` : ''}`}
|
title={`${e.announce_date}: ${label}${e.surprise_pct != null ? ` ${e.surprise_pct > 0 ? '+' : ''}${e.surprise_pct}%` : ''}`}
|
||||||
aria-label={`${e.announce_date} ${label}`}
|
aria-label={`${e.announce_date} ${label}`}>
|
||||||
>
|
|
||||||
{arrow}
|
{arrow}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user