diff --git a/frontend/src/components/AssetSelector.module.css b/frontend/src/components/AssetSelector.module.css
index a1ecec4..090a1fa 100644
--- a/frontend/src/components/AssetSelector.module.css
+++ b/frontend/src/components/AssetSelector.module.css
@@ -44,21 +44,21 @@
opacity: 0.5;
}
-.asset-selector__selected {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
-}
-
.asset-selector__symbol {
font-weight: 700;
color: var(--text-primary);
font-size: 1rem;
}
-.asset-selector__name {
- font-size: 0.875rem;
+.asset-selector__company-label {
+ display: block;
+ font-size: 0.75rem;
color: var(--text-secondary);
+ margin-top: 0.25rem;
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.asset-selector__placeholder {
diff --git a/frontend/src/components/AssetSelector.tsx b/frontend/src/components/AssetSelector.tsx
index 71eff9f..e6c822e 100644
--- a/frontend/src/components/AssetSelector.tsx
+++ b/frontend/src/components/AssetSelector.tsx
@@ -104,16 +104,18 @@ export function AssetSelector({ assets, selectedSymbol, onSelect, disabled = fal
disabled={disabled}
>
{selectedAsset ? (
-
- {selectedAsset.symbol}
- {selectedAsset.name}
-
+ {selectedAsset.symbol}
) : (
Choose an asset...
)}
{isOpen ? '▲' : '▼'}
+ {/* Company name below trigger — keeps trigger compact */}
+ {selectedAsset && (
+ {selectedAsset.name}
+ )}
+
{isOpen && (
{/* Filter Controls */}
diff --git a/frontend/src/components/InvestmentBuilder.tsx b/frontend/src/components/InvestmentBuilder.tsx
index 0066133..e346a3c 100644
--- a/frontend/src/components/InvestmentBuilder.tsx
+++ b/frontend/src/components/InvestmentBuilder.tsx
@@ -22,6 +22,7 @@ interface InvestmentBuilderProps {
export function InvestmentBuilder({ assets, onSimulate, isSimulating }: InvestmentBuilderProps) {
const { investments, setInvestments } = useSimulationContext();
const [showValidation, setShowValidation] = useState(false);
+ const [newInvestmentIds, setNewInvestmentIds] = useState>(new Set());
// Create a new empty investment
function createEmptyInvestment(): Investment {
@@ -36,15 +37,26 @@ export function InvestmentBuilder({ assets, onSimulate, isSimulating }: Investme
// Add new investment row
const handleAddInvestment = () => {
if (investments.length < 10) {
- setInvestments([...investments, createEmptyInvestment()]);
+ const newInv = createEmptyInvestment();
+ setInvestments([...investments, newInv]);
+ setNewInvestmentIds((prev) => new Set(prev).add(newInv.id));
}
};
- // Update an investment
+ // Update an investment (also marks it as no longer "new")
const handleUpdateInvestment = (index: number, updated: Investment) => {
const newInvestments = [...investments];
newInvestments[index] = updated;
setInvestments(newInvestments);
+
+ // Once a field is interacted with, stop suppressing validation
+ if (newInvestmentIds.has(updated.id)) {
+ setNewInvestmentIds((prev) => {
+ const next = new Set(prev);
+ next.delete(updated.id);
+ return next;
+ });
+ }
};
// Remove an investment
@@ -70,8 +82,9 @@ export function InvestmentBuilder({ assets, onSimulate, isSimulating }: Investme
// Handle simulation
const handleSimulate = () => {
- // Always show validation when user clicks simulate
+ // Reveal all validation errors (including on "new" rows)
setShowValidation(true);
+ setNewInvestmentIds(new Set());
if (!canSimulate) return;
@@ -105,6 +118,7 @@ export function InvestmentBuilder({ assets, onSimulate, isSimulating }: Investme
onRemove={() => handleRemoveInvestment(index)}
canRemove={investments.length > 1}
showValidation={showValidation}
+ isNew={newInvestmentIds.has(investment.id)}
/>
))}
diff --git a/frontend/src/components/InvestmentForm.module.css b/frontend/src/components/InvestmentForm.module.css
index cf05205..4755cd0 100644
--- a/frontend/src/components/InvestmentForm.module.css
+++ b/frontend/src/components/InvestmentForm.module.css
@@ -43,6 +43,13 @@
font-family: var(--font-family);
}
+.investment-form__ipo-info {
+ margin-left: 0.375rem;
+ cursor: help;
+ font-size: 0.875rem;
+ vertical-align: middle;
+}
+
.investment-form__input {
width: 100%;
padding: 0.75rem 1rem;
diff --git a/frontend/src/components/InvestmentForm.tsx b/frontend/src/components/InvestmentForm.tsx
index 8361cf6..1a19b40 100644
--- a/frontend/src/components/InvestmentForm.tsx
+++ b/frontend/src/components/InvestmentForm.tsx
@@ -10,6 +10,16 @@ interface InvestmentFormProps {
onRemove: () => void;
canRemove: boolean;
showValidation?: boolean; // Only show errors when true (after simulate attempt)
+ isNew?: boolean; // Suppress validation on freshly-added rows
+}
+
+/**
+ * Format an ISO date string (YYYY-MM-DD) to a readable label.
+ * e.g. "1986-03-13" → "Mar 13, 1986"
+ */
+function formatIpoDate(dateStr: string): string {
+ const d = new Date(dateStr + 'T00:00:00');
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
/**
@@ -26,7 +36,8 @@ export function InvestmentForm({
onUpdate,
onRemove,
canRemove,
- showValidation = false
+ showValidation = false,
+ isNew = false
}: InvestmentFormProps) {
const [errors, setErrors] = useState>({});
const [touched, setTouched] = useState>({});
@@ -36,8 +47,9 @@ export function InvestmentForm({
setTouched(prev => ({ ...prev, [field]: true }));
};
- // Show error only if touched or showValidation is true
+ // Show error only if (touched OR showValidation) AND not a fresh "new" row
const shouldShowError = (field: string) => {
+ if (isNew) return false;
return (touched[field] || showValidation) && errors[field];
};
@@ -99,6 +111,10 @@ export function InvestmentForm({
investment.amountUsd > 0 &&
investment.purchaseDate;
+ // Resolve selected asset for IPO date tooltip
+ const selectedAsset = assets.find(a => a.symbol === investment.symbol);
+ const ipoDate = selectedAsset?.ipoDate || undefined;
+
return (