refactor(ui): systematize screener controls
This commit is contained in:
parent
3375dfcfce
commit
4d2f18ea45
@ -1783,12 +1783,24 @@ body {
|
||||
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 select {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.screener-filter-row > select {
|
||||
.screener-cap-select {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@ -1800,6 +1812,85 @@ body {
|
||||
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,
|
||||
.history-tab {
|
||||
width: 100%;
|
||||
|
||||
@ -65,11 +65,8 @@ describe('ScreenerView sector filters', () => {
|
||||
await user.selectOptions(moreSectors, 'Energy');
|
||||
|
||||
expect(moreSectors).toHaveValue('Energy');
|
||||
expect(moreSectors).toHaveStyle({
|
||||
background: 'var(--accent-soft)',
|
||||
color: 'var(--primary)',
|
||||
fontWeight: '700',
|
||||
});
|
||||
expect(moreSectors).toHaveClass('screener-more-select');
|
||||
expect(moreSectors).toHaveAttribute('data-active', 'true');
|
||||
await waitFor(() => expect(globalThis.fetch).toHaveBeenCalledTimes(2));
|
||||
expect(String((globalThis.fetch as any).mock.calls[1][0])).toContain('sector=Energy');
|
||||
});
|
||||
|
||||
@ -126,7 +126,7 @@ export function ScreenerView() {
|
||||
};
|
||||
|
||||
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 ? '▲' : '▼') : '⇅'}
|
||||
</span>
|
||||
);
|
||||
@ -180,23 +180,20 @@ export function ScreenerView() {
|
||||
<CardContent>
|
||||
<div className="screener-filter-row">
|
||||
<div className="screener-search-field">
|
||||
<Search size={14} style={{
|
||||
position: 'absolute', left: 10, top: '50%',
|
||||
transform: 'translateY(-50%)', color: 'var(--muted-foreground)',
|
||||
}} />
|
||||
<Search size={14} className="screener-search-icon" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Filter by name or ticker…"
|
||||
value={query}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
|
||||
style={{ paddingLeft: 32 }}
|
||||
className="screener-search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={String(capIdx)}
|
||||
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 }))}
|
||||
/>
|
||||
|
||||
@ -208,14 +205,8 @@ export function ScreenerView() {
|
||||
onClick={() => setSector(s)}
|
||||
variant={sector === s ? 'secondary' : 'outline'}
|
||||
size="sm"
|
||||
style={{
|
||||
padding: '5px 10px', borderRadius: 20,
|
||||
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',
|
||||
}}
|
||||
className="screener-sector-chip"
|
||||
data-active={sector === s}
|
||||
>
|
||||
{s}
|
||||
</Button>
|
||||
@ -228,14 +219,8 @@ export function ScreenerView() {
|
||||
{ value: '', label: 'More sectors…' },
|
||||
...SECTORS.slice(6).map(s => ({ value: s, label: s })),
|
||||
]}
|
||||
style={{
|
||||
width: 140,
|
||||
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,
|
||||
}}
|
||||
className="screener-more-select"
|
||||
data-active={moreSectorSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -249,11 +234,7 @@ export function ScreenerView() {
|
||||
)}
|
||||
|
||||
<div className="ux-data-grid">
|
||||
<div className="ux-data-grid-header" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
||||
padding: '10px 16px',
|
||||
}}>
|
||||
<div className="ux-data-grid-header screener-results-grid">
|
||||
{([
|
||||
['symbol', 'Symbol'],
|
||||
['companyName', 'Company'],
|
||||
@ -263,14 +244,16 @@ export function ScreenerView() {
|
||||
['pe', 'P/E'],
|
||||
['volume', 'Volume'],
|
||||
] as [keyof ScreenerRow, string][]).map(([key, label]) => (
|
||||
<span
|
||||
<Button
|
||||
type="button"
|
||||
key={key}
|
||||
onClick={() => handleSort(key)}
|
||||
className="ux-data-grid-head"
|
||||
style={{ cursor: 'pointer' }}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ux-data-grid-head screener-sort-button"
|
||||
>
|
||||
{label}<SortIcon k={key} />
|
||||
</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -280,23 +263,22 @@ export function ScreenerView() {
|
||||
<div
|
||||
key={row.symbol}
|
||||
onClick={() => handleRowClick(row.symbol)}
|
||||
className="ux-data-grid-row"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '100px 1fr 90px 90px 110px 80px 110px',
|
||||
padding: '11px 16px',
|
||||
alignItems: 'center',
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handleRowClick(row.symbol);
|
||||
}
|
||||
}}
|
||||
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" style={{ fontWeight: 600 }}>
|
||||
<span className="ux-data-grid-cell screener-price-cell">
|
||||
{row.price != null ? `$${row.price.toFixed(2)}` : '—'}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 12, fontWeight: 600,
|
||||
color: row.changesPercentage >= 0 ? 'var(--bl-success)' : 'var(--bl-danger)',
|
||||
}}>
|
||||
<span className={`screener-change-cell ${row.changesPercentage >= 0 ? 'is-positive' : 'is-negative'}`}>
|
||||
{row.changesPercentage >= 0 ? '+' : ''}{row.changesPercentage?.toFixed(2)}%
|
||||
</span>
|
||||
<span className="ux-data-grid-cell">
|
||||
@ -322,7 +304,7 @@ export function ScreenerView() {
|
||||
</div>
|
||||
|
||||
{!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
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user