Scanner refinements:
- Exclude services/<svc>/src/ (Fastify backends, not UI)
- Exclude packages/config/ (schema/defaults, not UI)
- Exclude packages/devops/ (internal tooling)
- Exclude packages/create-app/.../templates (scaffolder templates)
- Exclude *.storybook/, /stories/, *.stories.{ts,tsx} (demo/docs)
- Exclude SVG fill=, stroke= hex (brand-mandated, e.g. Google G logo)
- Exclude ThemeEditor.tsx, theme-defaults.* (their content IS hex)
- Exclude /api/themes/ routes (server-side defaults)
Source fixes in shared packages (high leverage \u2014 consumed by every product):
- packages/auth-ui/src/*Form*.tsx + OnboardingShell + MfaChallenge (7)
- packages/dashboard-shell/src/{TopBar,ProfilePage}.tsx (3)
- dashboards/tracker-web/src/app/health/page.tsx (6)
All use the canonical var(--bl-<token>, #fallback) pattern that:
- Lets product themes override (e.g., each product sets --bl-danger differently)
- Falls back to a sensible default if tokens haven't loaded yet (defensive)
common_plat hex: 59 \u2192 0 \u2713 (Tier 2 complete)
Ecosystem total: 1569 \u2192 1402
Tier progress:
Tier 1 (critical): 13 \u2192 0 \u2713
Tier 2 (common_plat hex): 59 \u2192 0 \u2713
Tier 3 (mac_tooling, efforise): NEXT
Tier 4 (mindlyst, fastgap, flowmonk)
Tier 5 (non-hex rules)
115 lines
3.1 KiB
TypeScript
115 lines
3.1 KiB
TypeScript
import { useState, type FormEvent } from 'react';
|
|
import type { MfaChallengeProps } from './types.js';
|
|
|
|
/**
|
|
* MFA code entry form (6-digit TOTP or recovery code).
|
|
* Styled via CSS custom properties (inherits --bl-* from host app).
|
|
*/
|
|
export function MfaChallenge({
|
|
onSubmit,
|
|
onUseRecovery,
|
|
methods,
|
|
isLoading = false,
|
|
error,
|
|
className,
|
|
}: MfaChallengeProps) {
|
|
const [code, setCode] = useState('');
|
|
|
|
function handleSubmit(e: FormEvent) {
|
|
e.preventDefault();
|
|
onSubmit(code);
|
|
}
|
|
|
|
return (
|
|
<div className={className} data-testid="bl-mfa-challenge">
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}
|
|
>
|
|
<div style={{ fontSize: '14px', color: 'var(--bl-text, #333)' }}>
|
|
Enter your authentication code
|
|
</div>
|
|
|
|
{methods && methods.length > 0 && (
|
|
<div
|
|
data-testid="bl-mfa-methods"
|
|
style={{ fontSize: '12px', color: 'var(--bl-muted, #999)' }}
|
|
>
|
|
Available methods: {methods.join(', ')}
|
|
</div>
|
|
)}
|
|
|
|
<input
|
|
type="text"
|
|
inputMode="numeric"
|
|
autoComplete="one-time-code"
|
|
placeholder="000000"
|
|
value={code}
|
|
onChange={e => setCode(e.target.value)}
|
|
required
|
|
disabled={isLoading}
|
|
maxLength={8}
|
|
data-testid="bl-mfa-code"
|
|
style={{
|
|
padding: '12px',
|
|
border: '1px solid var(--bl-border, #ccc)',
|
|
borderRadius: 'var(--bl-radius, 6px)',
|
|
fontSize: '24px',
|
|
textAlign: 'center',
|
|
letterSpacing: '4px',
|
|
fontFamily: 'monospace',
|
|
}}
|
|
/>
|
|
|
|
{error && (
|
|
<div
|
|
data-testid="bl-mfa-error"
|
|
style={{ color: 'var(--bl-error, #dc3545)', fontSize: '13px' }}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || code.length < 6}
|
|
data-testid="bl-mfa-submit"
|
|
style={{
|
|
padding: '10px 16px',
|
|
border: 'none',
|
|
borderRadius: 'var(--bl-radius, 6px)',
|
|
background: 'var(--bl-primary, #0066ff)',
|
|
color: 'var(--bl-accent-foreground, #fff)',
|
|
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
fontSize: '14px',
|
|
fontWeight: 600,
|
|
opacity: isLoading || code.length < 6 ? 0.6 : 1,
|
|
}}
|
|
>
|
|
{isLoading ? 'Verifying...' : 'Verify'}
|
|
</button>
|
|
|
|
{onUseRecovery && (
|
|
<button
|
|
type="button"
|
|
onClick={onUseRecovery}
|
|
disabled={isLoading}
|
|
data-testid="bl-mfa-recovery"
|
|
style={{
|
|
padding: '8px',
|
|
border: 'none',
|
|
background: 'transparent',
|
|
color: 'var(--bl-link, #0066ff)',
|
|
cursor: 'pointer',
|
|
fontSize: '13px',
|
|
textDecoration: 'underline',
|
|
}}
|
|
>
|
|
Use a recovery code
|
|
</button>
|
|
)}
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|