refactor(ui): systematize screener controls

This commit is contained in:
Saravana Achu Mac 2026-05-08 21:37:58 -07:00
parent 3375dfcfce
commit 4d2f18ea45
3 changed files with 121 additions and 51 deletions

View File

@ -1783,12 +1783,24 @@ body {
min-width: 0; min-width: 0;
} }
.screener-search-icon {
position: absolute;
top: 50%;
left: 10px;
color: var(--muted-foreground);
transform: translateY(-50%);
}
.screener-search-input {
padding-left: 32px !important;
}
.screener-filter-row input, .screener-filter-row input,
.screener-filter-row select { .screener-filter-row select {
min-height: 40px; min-height: 40px;
} }
.screener-filter-row > select { .screener-cap-select {
width: 100% !important; width: 100% !important;
} }
@ -1800,6 +1812,85 @@ body {
gap: 7px; gap: 7px;
} }
.screener-sector-chip {
min-height: 32px !important;
border-radius: 999px !important;
padding: 0 12px !important;
font-size: 11px !important;
font-weight: 700 !important;
}
.screener-sector-chip[data-active="true"] {
border-color: var(--accent) !important;
background: var(--accent-soft) !important;
color: var(--accent) !important;
}
.screener-more-select {
width: 150px;
border-radius: 999px !important;
font-weight: 650;
}
.screener-more-select[data-active="true"] {
border-color: var(--accent) !important;
background: var(--accent-soft) !important;
color: var(--accent) !important;
}
.screener-results-grid {
display: grid;
grid-template-columns: 100px minmax(220px, 1fr) 90px 90px 110px 80px 110px;
align-items: center;
gap: 0;
padding: 10px 16px;
}
.screener-sort-button {
justify-content: flex-start !important;
min-height: 28px !important;
padding: 0 !important;
border: 0 !important;
background: transparent !important;
color: var(--muted-foreground) !important;
box-shadow: none !important;
}
.screener-sort-icon {
margin-left: 4px;
color: var(--muted-foreground);
font-size: 10px;
}
.screener-sort-icon[data-active="true"] {
color: var(--accent);
}
.screener-symbol-cell,
.screener-price-cell {
color: var(--foreground);
font-weight: 750;
}
.screener-change-cell {
font-size: 12px;
font-weight: 750;
}
.screener-change-cell.is-positive {
color: var(--bl-success);
}
.screener-change-cell.is-negative {
color: var(--bl-danger);
}
.screener-results-summary {
margin-top: 8px;
color: var(--muted-foreground);
font-size: 11px;
}
.positions-tab, .positions-tab,
.history-tab { .history-tab {
width: 100%; width: 100%;

View File

@ -65,11 +65,8 @@ describe('ScreenerView sector filters', () => {
await user.selectOptions(moreSectors, 'Energy'); await user.selectOptions(moreSectors, 'Energy');
expect(moreSectors).toHaveValue('Energy'); expect(moreSectors).toHaveValue('Energy');
expect(moreSectors).toHaveStyle({ expect(moreSectors).toHaveClass('screener-more-select');
background: 'var(--accent-soft)', expect(moreSectors).toHaveAttribute('data-active', 'true');
color: 'var(--primary)',
fontWeight: '700',
});
await waitFor(() => expect(globalThis.fetch).toHaveBeenCalledTimes(2)); await waitFor(() => expect(globalThis.fetch).toHaveBeenCalledTimes(2));
expect(String((globalThis.fetch as any).mock.calls[1][0])).toContain('sector=Energy'); expect(String((globalThis.fetch as any).mock.calls[1][0])).toContain('sector=Energy');
}); });

View File

@ -126,7 +126,7 @@ export function ScreenerView() {
}; };
const SortIcon = ({ k }: { k: keyof ScreenerRow }) => ( const SortIcon = ({ k }: { k: keyof ScreenerRow }) => (
<span style={{ color: sortKey === k ? 'var(--primary)' : 'var(--muted-foreground)', marginLeft: 3, fontSize: 10 }}> <span className="screener-sort-icon" data-active={sortKey === k}>
{sortKey === k ? (sortAsc ? '▲' : '▼') : '⇅'} {sortKey === k ? (sortAsc ? '▲' : '▼') : '⇅'}
</span> </span>
); );
@ -180,23 +180,20 @@ export function ScreenerView() {
<CardContent> <CardContent>
<div className="screener-filter-row"> <div className="screener-filter-row">
<div className="screener-search-field"> <div className="screener-search-field">
<Search size={14} style={{ <Search size={14} className="screener-search-icon" />
position: 'absolute', left: 10, top: '50%',
transform: 'translateY(-50%)', color: 'var(--muted-foreground)',
}} />
<Input <Input
type="text" type="text"
placeholder="Filter by name or ticker…" placeholder="Filter by name or ticker…"
value={query} value={query}
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)} onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
style={{ paddingLeft: 32 }} className="screener-search-input"
/> />
</div> </div>
<Select <Select
value={String(capIdx)} value={String(capIdx)}
onChange={(e: ChangeEvent<HTMLSelectElement>) => setCapIdx(Number(e.target.value))} onChange={(e: ChangeEvent<HTMLSelectElement>) => setCapIdx(Number(e.target.value))}
style={{ width: 180 }} className="screener-cap-select"
options={CAP_OPTIONS.map((c, i) => ({ value: String(i), label: c.label }))} options={CAP_OPTIONS.map((c, i) => ({ value: String(i), label: c.label }))}
/> />
@ -208,14 +205,8 @@ export function ScreenerView() {
onClick={() => setSector(s)} onClick={() => setSector(s)}
variant={sector === s ? 'secondary' : 'outline'} variant={sector === s ? 'secondary' : 'outline'}
size="sm" size="sm"
style={{ className="screener-sector-chip"
padding: '5px 10px', borderRadius: 20, data-active={sector === s}
border: '1px solid', fontSize: 11, fontWeight: 600,
borderColor: sector === s ? 'var(--primary)' : 'var(--border)',
background: sector === s ? 'var(--accent-soft)' : 'var(--card)',
color: sector === s ? 'var(--primary)' : 'var(--muted-foreground)',
fontFamily: 'inherit',
}}
> >
{s} {s}
</Button> </Button>
@ -228,14 +219,8 @@ export function ScreenerView() {
{ value: '', label: 'More sectors…' }, { value: '', label: 'More sectors…' },
...SECTORS.slice(6).map(s => ({ value: s, label: s })), ...SECTORS.slice(6).map(s => ({ value: s, label: s })),
]} ]}
style={{ className="screener-more-select"
width: 140, data-active={moreSectorSelected}
borderRadius: 999,
borderColor: moreSectorSelected ? 'var(--primary)' : 'var(--border)',
background: moreSectorSelected ? 'var(--accent-soft)' : 'var(--card)',
color: moreSectorSelected ? 'var(--primary)' : 'var(--muted-foreground)',
fontWeight: moreSectorSelected ? 700 : 500,
}}
/> />
</div> </div>
</div> </div>
@ -249,11 +234,7 @@ export function ScreenerView() {
)} )}
<div className="ux-data-grid"> <div className="ux-data-grid">
<div className="ux-data-grid-header" style={{ <div className="ux-data-grid-header screener-results-grid">
display: 'grid',
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
padding: '10px 16px',
}}>
{([ {([
['symbol', 'Symbol'], ['symbol', 'Symbol'],
['companyName', 'Company'], ['companyName', 'Company'],
@ -263,14 +244,16 @@ export function ScreenerView() {
['pe', 'P/E'], ['pe', 'P/E'],
['volume', 'Volume'], ['volume', 'Volume'],
] as [keyof ScreenerRow, string][]).map(([key, label]) => ( ] as [keyof ScreenerRow, string][]).map(([key, label]) => (
<span <Button
type="button"
key={key} key={key}
onClick={() => handleSort(key)} onClick={() => handleSort(key)}
className="ux-data-grid-head" variant="ghost"
style={{ cursor: 'pointer' }} size="sm"
className="ux-data-grid-head screener-sort-button"
> >
{label}<SortIcon k={key} /> {label}<SortIcon k={key} />
</span> </Button>
))} ))}
</div> </div>
@ -280,23 +263,22 @@ export function ScreenerView() {
<div <div
key={row.symbol} key={row.symbol}
onClick={() => handleRowClick(row.symbol)} onClick={() => handleRowClick(row.symbol)}
className="ux-data-grid-row" onKeyDown={(event) => {
style={{ if (event.key === 'Enter' || event.key === ' ') {
display: 'grid', event.preventDefault();
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px', handleRowClick(row.symbol);
padding: '11px 16px', }
alignItems: 'center',
}} }}
className="ux-data-grid-row screener-results-grid"
role="button"
tabIndex={0}
> >
<span className="ux-data-grid-cell" style={{ fontWeight: 700, color: 'var(--primary)' }}>{row.symbol}</span> <span className="ux-data-grid-cell screener-symbol-cell">{row.symbol}</span>
<span className="ux-data-grid-cell">{row.companyName}</span> <span className="ux-data-grid-cell">{row.companyName}</span>
<span className="ux-data-grid-cell" style={{ fontWeight: 600 }}> <span className="ux-data-grid-cell screener-price-cell">
{row.price != null ? `$${row.price.toFixed(2)}` : '—'} {row.price != null ? `$${row.price.toFixed(2)}` : '—'}
</span> </span>
<span style={{ <span className={`screener-change-cell ${row.changesPercentage >= 0 ? 'is-positive' : 'is-negative'}`}>
fontSize: 12, fontWeight: 600,
color: row.changesPercentage >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)',
}}>
{row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}% {row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}%
</span> </span>
<span className="ux-data-grid-cell"> <span className="ux-data-grid-cell">
@ -322,7 +304,7 @@ export function ScreenerView() {
</div> </div>
{!loading && filtered.length > 0 && ( {!loading && filtered.length > 0 && (
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--muted-foreground)' }}> <div className="screener-results-summary">
{filtered.length} companies · Click any row to view chart & research {filtered.length} companies · Click any row to view chart & research
</div> </div>
)} )}