fix(web): convert <Button> picker cards to native <button> (UI audit #5)

The @bytelyst/ui Button primitive applies whitespace-nowrap + fixed
h-{size} which collapses multi-line stacked content (Pattern A in
docs/ui/UI_AUDIT.md). Affected sites use Button as a card-shaped
option/action picker with stacked title + description spans.

Converted to native <button> + new .card-button utility class:
- StrategyWizard:188 — risk style picker (3 cards)
- StrategyWizard:342 — trading hours picker
- SimpleView:1009 — "new short-term buy plan" card
- SimpleView:1028 — "manage existing holding" card
- MyStrategiesTab:153 — diagnostic accordion toggle

Adds layout-fixes.css §25 .card-button — resets button defaults,
preserves focus-visible ring tied to --bl-focus-ring/--bl-accent.

Skipped: ChatControl:543 (FAB) — single-icon child, not affected.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin 2026-05-10 09:02:54 +00:00
parent 1807dc0d30
commit 67c9ecb589
4 changed files with 542 additions and 513 deletions

View File

@ -185,12 +185,11 @@ export const StrategyWizard: React.FC<{
const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id); const isLocked = !isFeatureAllowed(tier, 'risk_style', style.id);
return ( return (
<div key={style.id} className="relative"> <div key={style.id} className="relative">
<Button <button
type="button" type="button"
variant="ghost"
disabled={isLocked} disabled={isLocked}
onClick={() => setState({ ...state, riskStyle: style })} onClick={() => setState({ ...state, riskStyle: style })}
className={`${optionBaseClass} ${isLocked ? 'cursor-not-allowed opacity-40 grayscale' : 'hover:scale-[1.01] active:scale-[0.99]' className={`card-button ${optionBaseClass} ${isLocked ? 'cursor-not-allowed opacity-40 grayscale' : 'hover:scale-[1.01] active:scale-[0.99]'
} ${state.riskStyle?.id === style.id } ${state.riskStyle?.id === style.id
? optionSelectedClass ? optionSelectedClass
: optionIdleClass : optionIdleClass
@ -213,7 +212,7 @@ export const StrategyWizard: React.FC<{
<p className="text-sm leading-relaxed text-[var(--muted-foreground)]">{style.description}</p> <p className="text-sm leading-relaxed text-[var(--muted-foreground)]">{style.description}</p>
</div> </div>
</div> </div>
</Button> </button>
{isLocked && ( {isLocked && (
<div className="absolute top-4 right-4 text-[9px] font-black uppercase text-amber-500/80 tracking-tighter bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20"> <div className="absolute top-4 right-4 text-[9px] font-black uppercase text-amber-500/80 tracking-tighter bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
Pro/Elite Only Pro/Elite Only
@ -339,12 +338,11 @@ export const StrategyWizard: React.FC<{
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{(['24/7', 'London + New York', 'Asia only'] as const).map(option => ( {(['24/7', 'London + New York', 'Asia only'] as const).map(option => (
<Button <button
type="button" type="button"
variant="ghost"
key={option} key={option}
onClick={() => setState({ ...state, hours: option })} onClick={() => setState({ ...state, hours: option })}
className={`rounded-2xl border-2 p-6 text-left transition-all ${state.hours === option className={`card-button rounded-2xl border-2 p-6 text-left transition-all ${state.hours === option
? optionSelectedClass ? optionSelectedClass
: optionIdleClass : optionIdleClass
}`} }`}
@ -362,7 +360,7 @@ export const StrategyWizard: React.FC<{
</span> </span>
</div> </div>
</div> </div>
</Button> </button>
))} ))}
</div> </div>

View File

@ -493,3 +493,36 @@
word-break: break-word; word-break: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* ---------------------------------------------------------------------------
Section 25 card-button utility (UI audit Pattern A)
Use on a native <button> when you need a button-shaped CARD with stacked
block content. The @bytelyst/ui Button primitive has whitespace-nowrap +
fixed h-{size} that collapses multi-line content. This class restores the
focus ring + reset behavior without those constraints.
--------------------------------------------------------------------------- */
.card-button {
appearance: none;
-webkit-appearance: none;
background: transparent;
border: 0;
font: inherit;
color: inherit;
cursor: pointer;
display: block;
width: 100%;
text-align: left;
white-space: normal;
height: auto;
transition: background-color 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
}
.card-button:disabled {
cursor: not-allowed;
opacity: 0.4;
}
.card-button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--bl-bg-canvas, #0b0f17),
0 0 0 4px var(--bl-focus-ring, var(--bl-accent, #5A8CFF));
border-radius: inherit;
}

View File

@ -150,10 +150,10 @@ const ActiveStrategyCard: React.FC<{
{/* 5. Health Diagnostic (Education Layer) */} {/* 5. Health Diagnostic (Education Layer) */}
<div style={{ marginBottom: '32px' }}> <div style={{ marginBottom: '32px' }}>
<Button <button
type="button" type="button"
onClick={() => onToggleExpand(profile.id)} onClick={() => onToggleExpand(profile.id)}
variant="ghost" className="card-button"
style={{ style={{
width: '100%', width: '100%',
padding: '16px', padding: '16px',
@ -190,7 +190,7 @@ const ActiveStrategyCard: React.FC<{
<p style={{ fontSize: '11px', color: 'var(--bl-warning)', margin: 0, fontStyle: 'italic', fontWeight: 600 }}>{explanation.recommendation}</p> <p style={{ fontSize: '11px', color: 'var(--bl-warning)', margin: 0, fontStyle: 'italic', fontWeight: 600 }}>{explanation.recommendation}</p>
</div> </div>
)} )}
</Button> </button>
</div> </div>
{/* 6. Action */} {/* 6. Action */}

View File

@ -1006,15 +1006,14 @@ export function SimpleView() {
</p> </p>
</div> </div>
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
<Button <button
type="button" type="button"
onClick={() => { onClick={() => {
dispatch({ type: 'clear-feedback' }); dispatch({ type: 'clear-feedback' });
dispatch({ type: 'set-selected-holding-trade-id', value: null }); dispatch({ type: 'set-selected-holding-trade-id', value: null });
updateDraft('side', 'buy'); updateDraft('side', 'buy');
}} }}
variant="ghost" className={`card-button h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
className={`h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
draft.side === 'buy' draft.side === 'buy'
? 'border-[var(--primary)] bg-[var(--accent-soft)]' ? 'border-[var(--primary)] bg-[var(--accent-soft)]'
: 'border-[var(--border)] bg-[var(--card-elevated)]' : 'border-[var(--border)] bg-[var(--card-elevated)]'
@ -1024,8 +1023,8 @@ export function SimpleView() {
<div className="text-sm font-bold text-[var(--foreground)]">New short-term buy plan</div> <div className="text-sm font-bold text-[var(--foreground)]">New short-term buy plan</div>
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div> <div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Arm a dip-buy trigger and let the app manage the profit exit after fill.</div>
</div> </div>
</Button> </button>
<Button <button
type="button" type="button"
onClick={() => { onClick={() => {
dispatch({ type: 'clear-feedback' }); dispatch({ type: 'clear-feedback' });
@ -1035,8 +1034,7 @@ export function SimpleView() {
updateDraft('side', 'sell'); updateDraft('side', 'sell');
} }
}} }}
variant="ghost" className={`card-button h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
className={`h-auto justify-start rounded-[1.25rem] border px-5 py-5 text-left transition ${
draft.side === 'sell' draft.side === 'sell'
? 'border-[var(--primary)] bg-[var(--accent-soft)]' ? 'border-[var(--primary)] bg-[var(--accent-soft)]'
: 'border-[var(--border)] bg-[var(--card-elevated)]' : 'border-[var(--border)] bg-[var(--card-elevated)]'
@ -1046,7 +1044,7 @@ export function SimpleView() {
<div className="text-sm font-bold text-[var(--foreground)]">Manage an existing holding</div> <div className="text-sm font-bold text-[var(--foreground)]">Manage an existing holding</div>
<div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Choose a filled holding and place it back under managed profit-taking.</div> <div className="mt-2 text-sm font-normal leading-6 text-[var(--muted-foreground)]">Choose a filled holding and place it back under managed profit-taking.</div>
</div> </div>
</Button> </button>
</div> </div>
</section> </section>