Skip to content

Commit f6b5003

Browse files
committed
Add contextual help tooltips and improve form UX
Introduces a reusable LabelWithHelp and HelpButton component for contextual tooltips across configuration forms. Replaces static labels with LabelWithHelp in rebalance, schedule, and stock selection tabs, providing users with inline explanations for each setting. Enhances login and registration forms with show/hide password toggles. Updates default min/max position size logic and cleans up legacy rebalance settings handling. Also adds onEnterPress support to StockTickerAutocomplete for improved watchlist UX.
1 parent 0c7c572 commit f6b5003

20 files changed

+2003
-1048
lines changed

src/components/ScheduleListModal.tsx

Lines changed: 156 additions & 171 deletions
Large diffs are not rendered by default.

src/components/StandaloneWatchlist.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,7 @@ export default function StandaloneWatchlist({ onSelectStock, selectedStock }: St
665665
<StockTickerAutocomplete
666666
value={newTicker}
667667
onChange={setNewTicker}
668+
onEnterPress={addToWatchlist}
668669
placeholder="Add ticker to watchlist"
669670
/>
670671
</div>

src/components/StockTickerAutocomplete.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface StockTickerAutocompleteProps {
1717
value: string;
1818
onChange: (value: string) => void;
1919
onSelect?: (suggestion: StockSuggestion) => void;
20+
onEnterPress?: () => void;
2021
placeholder?: string;
2122
className?: string;
2223
disabled?: boolean;
@@ -28,6 +29,7 @@ export default function StockTickerAutocomplete({
2829
value,
2930
onChange,
3031
onSelect,
32+
onEnterPress,
3133
placeholder = "Enter stock symbol...",
3234
className,
3335
disabled = false,
@@ -147,9 +149,16 @@ export default function StockTickerAutocomplete({
147149
if (showSuggestions && suggestions.length > 0 && selectedIndex >= 0) {
148150
e.preventDefault();
149151
handleSelectSuggestion(suggestions[selectedIndex]);
150-
} else if (value.trim() && onSelect) {
151-
// Allow Enter to trigger onSelect even without suggestions
152-
onSelect({ symbol: value.trim(), description: '' });
152+
} else if (value.trim()) {
153+
e.preventDefault();
154+
// If onEnterPress is provided, call it (for adding to watchlist)
155+
if (onEnterPress) {
156+
onEnterPress();
157+
}
158+
// Also call onSelect if provided (for backward compatibility)
159+
else if (onSelect) {
160+
onSelect({ symbol: value.trim(), description: '' });
161+
}
153162
}
154163
break;
155164
case 'Escape':

src/components/rebalance/tabs/ConfigurationTab.tsx

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { TabsContent } from "@/components/ui/tabs";
55
import { Card } from "@/components/ui/card";
66
import { Checkbox } from "@/components/ui/checkbox";
77
import { Label } from "@/components/ui/label";
8-
import { Input } from "@/components/ui/input";
8+
import { LabelWithHelp } from "@/components/ui/help-button";
99
import { Slider } from "@/components/ui/slider";
1010
import { Progress } from "@/components/ui/progress";
1111
import { ConfigurationSummary } from "../components/ConfigurationSummary";
@@ -54,55 +54,89 @@ export function ConfigurationTab({
5454
setConfig(prev => ({ ...prev, useDefaultSettings: checked as boolean }))
5555
}
5656
/>
57-
<Label htmlFor="useDefault" className="text-sm font-medium">
58-
Use default rebalance configuration from user settings
59-
</Label>
57+
<LabelWithHelp
58+
htmlFor="useDefault"
59+
label="Use default rebalance configuration from user settings"
60+
helpContent="Automatically uses settings from Settings > Rebalance tab. When checked, all fields below become read-only"
61+
className="text-sm font-medium cursor-pointer"
62+
/>
6063
</div>
6164

6265
{/* Configuration Fields */}
6366
<div className={`space-y-6 ${config.useDefaultSettings ? 'opacity-50 pointer-events-none' : ''}`}>
6467
{/* Position Size Limits */}
65-
<div className="grid grid-cols-2 gap-6">
68+
<div className="space-y-4">
6669
<div className="space-y-2">
67-
<Label htmlFor="minPosition">Minimum Position Size ($)</Label>
68-
<Input
69-
id="minPosition"
70-
type="number"
71-
value={config.minPosition}
72-
onChange={(e) => setConfig(prev => ({
73-
...prev,
74-
minPosition: Number(e.target.value)
75-
}))}
76-
disabled={config.useDefaultSettings}
70+
<LabelWithHelp
71+
label="Min Position Size"
72+
helpContent="Minimum position size as percentage of portfolio. Prevents too many small positions. Lower values allow more positions but may increase trading costs."
7773
/>
74+
<div className="flex items-center space-x-4 py-3 min-h-[40px]">
75+
<span className="w-8 text-sm text-muted-foreground">5%</span>
76+
<Slider
77+
value={[config.minPosition]}
78+
onValueChange={(value) => {
79+
const newMin = value[0];
80+
setConfig(prev => ({
81+
...prev,
82+
minPosition: newMin,
83+
// Ensure max is always greater than min
84+
maxPosition: prev.maxPosition <= newMin ? Math.min(newMin + 5, 50) : prev.maxPosition
85+
}));
86+
}}
87+
min={5}
88+
max={25}
89+
step={5}
90+
className="flex-1"
91+
disabled={config.useDefaultSettings}
92+
/>
93+
<span className="w-12 text-sm text-muted-foreground">25%</span>
94+
<span className="w-12 text-center font-medium">{config.minPosition}%</span>
95+
</div>
7896
<p className="text-xs text-muted-foreground">
79-
Minimum dollar amount for any position
97+
Minimum percentage of portfolio per position (currently {config.minPosition}%)
8098
</p>
8199
</div>
82100

83101
<div className="space-y-2">
84-
<Label htmlFor="maxPosition">Maximum Position Size ($)</Label>
85-
<Input
86-
id="maxPosition"
87-
type="number"
88-
value={config.maxPosition}
89-
onChange={(e) => setConfig(prev => ({
90-
...prev,
91-
maxPosition: Number(e.target.value)
92-
}))}
93-
disabled={config.useDefaultSettings}
102+
<LabelWithHelp
103+
label="Max Position Size"
104+
helpContent="Maximum position size as percentage of portfolio. Ensures diversification by limiting exposure to any single stock. Recommended: 20-30% for balanced diversification."
94105
/>
106+
<div className="flex items-center space-x-4 py-3 min-h-[40px]">
107+
<span className="w-12 text-sm text-muted-foreground">{config.minPosition}%</span>
108+
<Slider
109+
value={[config.maxPosition]}
110+
onValueChange={(value) => {
111+
const newMax = value[0];
112+
setConfig(prev => ({
113+
...prev,
114+
maxPosition: newMax
115+
// Max is always at least min position size
116+
}));
117+
}}
118+
min={config.minPosition}
119+
max={50}
120+
step={5}
121+
className="flex-1"
122+
disabled={config.useDefaultSettings}
123+
/>
124+
<span className="w-12 text-sm text-muted-foreground">50%</span>
125+
<span className="w-12 text-center font-medium">{config.maxPosition}%</span>
126+
</div>
95127
<p className="text-xs text-muted-foreground">
96-
Maximum dollar amount for any position
128+
Maximum percentage of portfolio per position (currently {config.maxPosition}%)
97129
</p>
98130
</div>
99131
</div>
100132

101133
{/* Rebalance Threshold */}
102134
<div className="space-y-2">
103-
<Label htmlFor="threshold">
104-
Rebalance Threshold: {config.rebalanceThreshold}%
105-
</Label>
135+
<LabelWithHelp
136+
htmlFor="threshold"
137+
label={`Rebalance Threshold: ${config.rebalanceThreshold}%`}
138+
helpContent="Triggers rebalance when portfolio drifts by this percentage. Lower values (1-5%) result in frequent rebalancing, higher values (10-20%) result in less frequent rebalancing. Recommended: 5-10%"
139+
/>
106140
<Slider
107141
id="threshold"
108142
min={1}
@@ -136,9 +170,12 @@ export function ConfigurationTab({
136170
}}
137171
disabled={config.useDefaultSettings}
138172
/>
139-
<Label htmlFor="skipThreshold" className="text-sm font-normal cursor-pointer">
140-
Skip Threshold Check
141-
</Label>
173+
<LabelWithHelp
174+
htmlFor="skipThreshold"
175+
label="Skip Threshold Check"
176+
helpContent="When enabled, all selected stocks will be analyzed for rebalance agent regardless of rebalance threshold"
177+
className="text-sm font-normal cursor-pointer"
178+
/>
142179
</div>
143180
<p className="text-xs text-muted-foreground pl-6">
144181
When enabled, all selected stocks will be analyzed for rebalance agent regardless of rebalance threshold
@@ -159,13 +196,12 @@ export function ConfigurationTab({
159196
}}
160197
disabled={config.useDefaultSettings || config.skipThresholdCheck || !hasOppAccess}
161198
/>
162-
<Label
199+
<LabelWithHelp
163200
htmlFor="skipOpportunity"
164-
className={`text-sm font-normal ${!hasOppAccess ? 'opacity-50' : 'cursor-pointer'} ${config.skipThresholdCheck ? 'opacity-50' : ''
165-
}`}
166-
>
167-
Skip opportunity analysis (analyze all selected stocks)
168-
</Label>
201+
label="Skip opportunity analysis (analyze all selected stocks)"
202+
helpContent="Scans market for new investment opportunities when portfolio is balanced. Only activates when within rebalance threshold"
203+
className={`text-sm font-normal ${!hasOppAccess ? 'opacity-50' : 'cursor-pointer'} ${config.skipThresholdCheck ? 'opacity-50' : ''}`}
204+
/>
169205
</div>
170206
<p className="text-xs text-muted-foreground pl-6">
171207
{!hasOppAccess
@@ -180,10 +216,17 @@ export function ConfigurationTab({
180216
{/* Portfolio Allocation */}
181217
<div className="space-y-4">
182218
<div className="space-y-2">
183-
<Label>Portfolio Allocation</Label>
219+
<LabelWithHelp
220+
label="Portfolio Allocation"
221+
helpContent="Target allocation between stocks and cash"
222+
/>
184223
<div className="grid grid-cols-2 gap-4">
185224
<div className="space-y-2">
186-
<Label className="text-sm">Stock Allocation: {config.targetStockAllocation}%</Label>
225+
<LabelWithHelp
226+
label={`Stock Allocation: ${config.targetStockAllocation}%`}
227+
helpContent="Percentage to invest in stocks. Higher = more growth potential, more risk. Age-based rule: 100 minus your age = stock percentage"
228+
className="text-sm"
229+
/>
187230
<Slider
188231
min={0}
189232
max={100}
@@ -195,7 +238,11 @@ export function ConfigurationTab({
195238
/>
196239
</div>
197240
<div className="space-y-2">
198-
<Label className="text-sm">Cash Allocation: {config.targetCashAllocation}%</Label>
241+
<LabelWithHelp
242+
label={`Cash Allocation: ${config.targetCashAllocation}%`}
243+
helpContent="Percentage to keep in cash for opportunities and stability. Higher cash = more defensive, lower returns"
244+
className="text-sm"
245+
/>
199246
<Progress value={config.targetCashAllocation} className="h-2 mt-6" />
200247
</div>
201248
</div>
@@ -207,7 +254,7 @@ export function ConfigurationTab({
207254
</div>
208255

209256
{/* Workflow Explanation */}
210-
<WorkflowExplanation config={config} />
257+
<WorkflowExplanation rebalanceThreshold={config.rebalanceThreshold} />
211258

212259
{/* Summary */}
213260
<ConfigurationSummary

src/components/rebalance/tabs/StockSelectionTab.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
77
import { Badge } from "@/components/ui/badge";
88
import { Switch } from "@/components/ui/switch";
99
import { Label } from "@/components/ui/label";
10+
import { LabelWithHelp } from "@/components/ui/help-button";
1011
import { AlertCircle, Loader2, Eye, Lock } from "lucide-react";
1112
import { PortfolioComposition } from "../components/PortfolioComposition";
1213
import { StockPositionCard } from "../components/StockPositionCard";
@@ -91,9 +92,12 @@ export function StockSelectionTab({
9192
<div className="space-y-3">
9293
<div className="flex items-center justify-between">
9394
<div className="space-y-1">
94-
<Label htmlFor="include-watchlist" className="text-sm font-semibold">
95-
Include Watchlist Stocks
96-
</Label>
95+
<LabelWithHelp
96+
htmlFor="include-watchlist"
97+
label="Include Watchlist Stocks"
98+
helpContent="Add stocks from your watchlist to the rebalancing analysis. These stocks will be considered for potential new positions even though you don't currently own them."
99+
className="text-sm font-semibold"
100+
/>
97101
<p className="text-xs text-muted-foreground">
98102
Add stocks from your watchlist to the rebalancing analysis
99103
</p>

src/components/schedule-rebalance/components/TimeSelector.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Extracted from ScheduleTab.tsx to reduce file size
33

44
import { Label } from "@/components/ui/label";
5+
import { LabelWithHelp } from "@/components/ui/help-button";
56
import {
67
Select,
78
SelectContent,
@@ -38,7 +39,10 @@ export function TimeSelector({ value, onChange }: TimeSelectorProps) {
3839

3940
return (
4041
<div className="space-y-2">
41-
<Label>Time of Day</Label>
42+
<LabelWithHelp
43+
label="Time of Day"
44+
helpContent="The time when the rebalance will execute. Choose a time when markets are closed to avoid mid-day volatility. Recommended: Before market open (9:30 AM ET) or after market close (4:00 PM ET)."
45+
/>
4246
<div className="flex gap-2">
4347
{/* Hour Selection */}
4448
<Select value={hourStr} onValueChange={handleHourChange}>

src/components/schedule-rebalance/components/TimezoneSelector.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { useState } from "react";
55
import { Input } from "@/components/ui/input";
66
import { Label } from "@/components/ui/label";
7+
import { LabelWithHelp } from "@/components/ui/help-button";
78
import {
89
Select,
910
SelectContent,
@@ -24,7 +25,10 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectorProps) {
2425

2526
return (
2627
<div className="space-y-2">
27-
<Label>Timezone</Label>
28+
<LabelWithHelp
29+
label="Timezone"
30+
helpContent="Your local timezone for scheduling. The rebalance will execute at the specified time in this timezone. Market hours are in Eastern Time (ET)."
31+
/>
2832
<Select value={value} onValueChange={onChange}>
2933
<SelectTrigger>
3034
<SelectValue placeholder="Select timezone">

src/components/schedule-rebalance/tabs/ScheduleTab.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { Card } from "@/components/ui/card";
55
import { Label } from "@/components/ui/label";
6+
import { LabelWithHelp } from "@/components/ui/help-button";
67
import { Input } from "@/components/ui/input";
78
import { Checkbox } from "@/components/ui/checkbox";
89
import {
@@ -44,7 +45,10 @@ export function ScheduleTab({ loading, config, setConfig }: ScheduleTabProps) {
4445
<div className="space-y-4">
4546
{/* Interval Configuration */}
4647
<div className="space-y-2">
47-
<Label>Rebalance Frequency</Label>
48+
<LabelWithHelp
49+
label="Rebalance Frequency"
50+
helpContent="How often to automatically rebalance your portfolio. Daily for active management, Weekly for regular adjustments, Monthly for long-term investing. Your subscription determines available frequencies."
51+
/>
4852
<div className="flex gap-2">
4953
<div className="flex items-center gap-2">
5054
<Label htmlFor="interval-value" className="text-sm font-normal">
@@ -111,7 +115,10 @@ export function ScheduleTab({ loading, config, setConfig }: ScheduleTabProps) {
111115
{/* Day Selection for Weekly intervals */}
112116
{config.intervalUnit === 'weeks' && (
113117
<div className="space-y-2">
114-
<Label>On Which Day(s)</Label>
118+
<LabelWithHelp
119+
label="On Which Day(s)"
120+
helpContent="Select which day(s) of the week to run the rebalance. With higher tier subscriptions, you can select multiple days for more frequent rebalancing."
121+
/>
115122
{hasDayAccess ? (
116123
// Multi-selection for users with Day access
117124
<div className="grid grid-cols-4 gap-2">
@@ -171,7 +178,10 @@ export function ScheduleTab({ loading, config, setConfig }: ScheduleTabProps) {
171178
{/* Day of Month for Monthly intervals */}
172179
{config.intervalUnit === 'months' && (
173180
<div className="space-y-2">
174-
<Label>On Which Day(s) of the Month</Label>
181+
<LabelWithHelp
182+
label="On Which Day(s) of the Month"
183+
helpContent="Select the day of the month for rebalancing. Day 31 will automatically adjust for shorter months (e.g., will run on Feb 28/29)."
184+
/>
175185
<Select
176186
value={config.daysOfMonth[0]?.toString() || '1'}
177187
onValueChange={(value) => {

0 commit comments

Comments
 (0)