Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,18 @@ export function App() {

<main className="app-content">
<ErrorBoundary>
{activeView === 'calculator' && <Calculator />}
{activeView === 'workspace' && <WorkspaceTab />}
<div
hidden={activeView !== 'workspace'}
style={{ display: activeView === 'workspace' ? undefined : 'none' }}
>
<WorkspaceTab isActive={activeView === 'workspace'} />
</div>
<div
hidden={activeView !== 'calculator'}
style={{ display: activeView === 'calculator' ? undefined : 'none' }}
>
<Calculator isActive={activeView === 'calculator'} />
</div>
{activeView === 'qfaf-test' && <QfafTestPage />}
</ErrorBoundary>
</main>
Expand Down
17 changes: 11 additions & 6 deletions src/Calculator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@
}));
};

export function Calculator() {
interface CalculatorProps {
isActive?: boolean;
}

export function Calculator({ isActive = true }: CalculatorProps) {
const [inputs, setInputs] = useState<CalculatorInputs>(DEFAULTS);
const advancedMode = useAdvancedMode();
const { isExpanded } = useScrollHeader('scroll-sentinel');
Expand Down Expand Up @@ -134,7 +138,7 @@
return calculateWithSensitivity(inputs, advancedSettings, sensitivityParams);
}
return calculate(inputs, advancedSettings);
}, [

Check warning on line 141 in src/Calculator.tsx

View workflow job for this annotation

GitHub Actions / checks

React Hook useMemo has an unnecessary dependency: 'rateVersion'. Either exclude it or remove the dependency array
inputs,
advancedSettings,
rateVersion,
Expand Down Expand Up @@ -209,6 +213,7 @@
taxRates.stateProfile,
advancedSettings.growthEnabled,
advancedSettings.defaultAnnualReturn,
inputs.collateralCostBasis,
]
);

Expand Down Expand Up @@ -266,11 +271,11 @@

// Setup keyboard shortcuts (after all handlers are defined)
useKeyboardShortcuts({
onPrint: printHandler || undefined,
onExport: exportHandler || undefined,
onShowHelp: () => setShowKeyboardHelp(true),
onEscape: () => setShowKeyboardHelp(false),
onPin: handlePinScenario,
onPrint: isActive ? printHandler || undefined : undefined,
onExport: isActive ? exportHandler || undefined : undefined,
onShowHelp: isActive ? () => setShowKeyboardHelp(true) : undefined,
onEscape: isActive ? () => setShowKeyboardHelp(false) : undefined,
onPin: isActive ? handlePinScenario : undefined,
});

// Input validation (warn-only, does not block calculation)
Expand Down
44 changes: 41 additions & 3 deletions src/ResultsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export function ResultsTable({
// Deleverage group (only when a plan is active)
if (hasDeleverage) {
cols += 1; // Extension % headline
if (expandDeleverage) cols += 3; // Unwind Gain, Unwind Tax, Fin. Saved
if (expandDeleverage) cols += 5; // Unwind Gain, ST/LT split, Unwind Tax, Fin. Saved
}

cols += 1; // Tax Savings column
Expand Down Expand Up @@ -445,6 +445,12 @@ export function ResultsTable({
<th className="col-detail">
<InfoText contentKey="col-deleverage-gain">Unwind Gain</InfoText>
</th>
<th className="col-detail">
<InfoText contentKey="col-deleverage-st-gain">Unwind ST</InfoText>
</th>
<th className="col-detail">
<InfoText contentKey="col-deleverage-lt-gain">Unwind LT</InfoText>
</th>
<th className="col-detail">
<InfoText contentKey="col-deleverage-tax">Unwind Tax</InfoText>
</th>
Expand Down Expand Up @@ -560,6 +566,8 @@ export function ResultsTable({
<td className="starting-note">—</td>
<td className="starting-note">—</td>
<td className="starting-note">—</td>
<td className="starting-note">—</td>
<td className="starting-note">—</td>
</>
)}
{hasDeleverage && <td className="starting-note">100%</td>}
Expand Down Expand Up @@ -814,6 +822,16 @@ export function ResultsTable({
? formatCurrency(year.deleverageGainRealized)
: '—'}
</td>
<td className="positive">
{year.deleverageGainSt > 0.01
? formatCurrency(year.deleverageGainSt)
: '—'}
</td>
<td className="positive">
{year.deleverageGainLt > 0.01
? formatCurrency(year.deleverageGainLt)
: '—'}
</td>
<td className="negative">
{year.deleverageTax > 0.01
? `(${formatCurrency(year.deleverageTax)})`
Expand Down Expand Up @@ -1186,15 +1204,25 @@ function TransposedTable({
sections.push({
title: 'Gain Events',
rows: [
{ label: 'Event Amount', cell: y => moneyOrDash(y.gainEventAmount) },
{ label: 'CF Shelter Applied', cell: y => moneyOrDash(y.gainEventCfShelter) },
{
label: 'Event Amount',
contentKey: 'col-gain-event-amount',
cell: y => moneyOrDash(y.gainEventAmount),
},
{
label: 'CF Shelter Applied',
contentKey: 'col-gain-event-cf-shelter',
cell: y => moneyOrDash(y.gainEventCfShelter),
},
{
label: 'Event Tax Due',
contentKey: 'col-gain-event-tax',
cell: y => (y.gainEventAmount > 0 ? formatCurrency(y.gainEventTax) : '—'),
className: () => 'negative',
},
{
label: 'Tax Without Program',
contentKey: 'col-gain-event-tax-without-program',
cell: y => moneyOrDash(y.gainEventTaxWithoutStrategy),
},
],
Expand All @@ -1215,6 +1243,16 @@ function TransposedTable({
contentKey: 'col-deleverage-gain',
cell: y => moneyOrDash(y.deleverageGainRealized),
},
{
label: 'Unwind ST Gain',
contentKey: 'col-deleverage-st-gain',
cell: y => moneyOrDash(y.deleverageGainSt),
},
{
label: 'Unwind LT Gain',
contentKey: 'col-deleverage-lt-gain',
cell: y => moneyOrDash(y.deleverageGainLt),
},
{
label: 'Tax on Unwind',
contentKey: 'col-deleverage-tax',
Expand Down
211 changes: 104 additions & 107 deletions src/components/MeetingMode/MeetingMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ function Rail({
}) {
const projYears = advancedSettings.projectionYears;
const clampYear = (raw: string) => Math.max(1, Math.min(projYears, parseInt(raw, 10) || 1));
const effectiveView = useMemo(() => getEffectiveView(inputs), [inputs]);
const splitMode = inputs.splitAllocation?.enabled === true && effectiveView.isSplit;
const filingLabel =
FILING_STATUSES.find(f => f.value === inputs.filingStatus)
?.label.replace('Married Filing Jointly', 'MFJ')
Expand Down Expand Up @@ -403,10 +405,7 @@ function Rail({
}}
>
<div style={{ fontSize: 16, fontWeight: 700, color: 'white', marginBottom: 8 }}>
{(() => {
const view = getEffectiveView(inputs);
return view.displayName;
})()}
{effectiveView.displayName}
</div>
<div
style={{
Expand All @@ -428,7 +427,7 @@ function Rail({
COLLATERAL
</div>
<div style={{ color: 'white', fontWeight: 600, fontFamily: M.mono }}>
{fmtCurrency(getEffectiveView(inputs).totalCollateral)}
{fmtCurrency(effectiveView.totalCollateral)}
</div>
</div>
<div>
Expand Down Expand Up @@ -508,116 +507,114 @@ function Rail({
Adjust scenario
</div>

{inputs.splitAllocation?.enabled === true &&
(() => {
const view = getEffectiveView(inputs);
if (!view.isSplit) return null;
return (
{splitMode && (
<div
style={{
marginBottom: 14,
padding: 10,
borderRadius: 8,
background: 'rgba(79,70,229,0.15)',
border: '1px solid rgba(79,70,229,0.35)',
fontSize: 11,
color: M.sidebarInk,
lineHeight: 1.4,
}}
>
<div style={{ fontWeight: 700, color: 'white', marginBottom: 4, fontSize: 11.5 }}>
Split allocation active
</div>
{effectiveView.legs.map(leg => (
<div
style={{
marginBottom: 14,
padding: 10,
borderRadius: 8,
background: 'rgba(79,70,229,0.15)',
border: '1px solid rgba(79,70,229,0.35)',
fontSize: 11,
color: M.sidebarInk,
lineHeight: 1.4,
}}
key={leg.strategy.id}
style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}
>
<div style={{ fontWeight: 700, color: 'white', marginBottom: 4, fontSize: 11.5 }}>
Split allocation active
</div>
{view.legs.map(leg => (
<div
key={leg.strategy.id}
style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}
>
<span>
{leg.label}: {leg.strategy.name}
</span>
<span style={{ fontFamily: M.mono }}>{fmtCurrency(leg.amount)}</span>
</div>
))}
<div
style={{
marginTop: 6,
fontSize: 10,
fontStyle: 'italic',
color: M.sidebarFaint,
}}
>
Edit split details in the main calculator view. Controls below are not active in
split mode.
</div>
<span>
{leg.label}: {leg.strategy.name}
</span>
<span style={{ fontFamily: M.mono }}>{fmtCurrency(leg.amount)}</span>
</div>
);
})()}

{/* Strategy */}
<div style={{ marginBottom: 12 }}>
<label style={railLabelStyle} htmlFor="mm-strategy">
Strategy
</label>
<select
id="mm-strategy"
value={inputs.strategyId}
onChange={e => onUpdateInput('strategyId', e.target.value)}
style={railFieldStyle}
>
<optgroup label="Core (Cash Funded)">
{STRATEGIES.filter(s => s.type === 'core').map(s => (
<option key={s.id} value={s.id} style={{ background: M.sidebar }}>
{s.name}
</option>
))}
</optgroup>
<optgroup label="Overlay (Appreciated Stock)">
{STRATEGIES.filter(s => s.type === 'overlay').map(s => (
<option key={s.id} value={s.id} style={{ background: M.sidebar }}>
{s.name}
</option>
))}
</optgroup>
</select>
</div>

{/* Collateral */}
<div style={{ marginBottom: 12 }}>
<label style={railLabelStyle} htmlFor="mm-collateral">
Collateral
</label>
<div style={{ position: 'relative' }}>
<span
))}
<div
style={{
position: 'absolute',
left: 10,
top: '50%',
transform: 'translateY(-50%)',
marginTop: 6,
fontSize: 10,
fontStyle: 'italic',
color: M.sidebarFaint,
fontSize: 12.5,
fontWeight: 600,
pointerEvents: 'none',
}}
>
$
</span>
<input
id="mm-collateral"
type="text"
inputMode="numeric"
value={formatWithCommas(inputs.collateralAmount)}
onChange={e =>
onUpdateInput('collateralAmount', parseFormattedNumber(e.target.value))
}
style={{
...railFieldStyle,
paddingLeft: 22,
fontFamily: M.mono,
}}
/>
Edit split details in the Workspace before opening Meeting Mode.
</div>
</div>
</div>
)}

{/* Strategy */}
{!splitMode && (
<div style={{ marginBottom: 12 }}>
<label style={railLabelStyle} htmlFor="mm-strategy">
Strategy
</label>
<select
id="mm-strategy"
value={inputs.strategyId}
onChange={e => onUpdateInput('strategyId', e.target.value)}
style={railFieldStyle}
>
<optgroup label="Core (Cash Funded)">
{STRATEGIES.filter(s => s.type === 'core').map(s => (
<option key={s.id} value={s.id} style={{ background: M.sidebar }}>
{s.name}
</option>
))}
</optgroup>
<optgroup label="Overlay (Appreciated Stock)">
{STRATEGIES.filter(s => s.type === 'overlay').map(s => (
<option key={s.id} value={s.id} style={{ background: M.sidebar }}>
{s.name}
</option>
))}
</optgroup>
</select>
</div>
)}

{/* Collateral */}
{!splitMode && (
<div style={{ marginBottom: 12 }}>
<label style={railLabelStyle} htmlFor="mm-collateral">
Collateral
</label>
<div style={{ position: 'relative' }}>
<span
style={{
position: 'absolute',
left: 10,
top: '50%',
transform: 'translateY(-50%)',
color: M.sidebarFaint,
fontSize: 12.5,
fontWeight: 600,
pointerEvents: 'none',
}}
>
$
</span>
<input
id="mm-collateral"
type="text"
inputMode="numeric"
value={formatWithCommas(inputs.collateralAmount)}
onChange={e =>
onUpdateInput('collateralAmount', parseFormattedNumber(e.target.value))
}
style={{
...railFieldStyle,
paddingLeft: 22,
fontFamily: M.mono,
}}
/>
</div>
</div>
)}

{/* Income */}
<div style={{ marginBottom: 12 }}>
Expand Down
Loading
Loading