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)
181 lines
4.9 KiB
TypeScript
181 lines
4.9 KiB
TypeScript
import { useState, type ReactNode } from 'react';
|
|
import type { ProfilePageProps } from './types.js';
|
|
|
|
export function ProfilePage({
|
|
user,
|
|
onUpdateProfile,
|
|
isLoading,
|
|
error,
|
|
success,
|
|
}: ProfilePageProps): ReactNode {
|
|
const [name, setName] = useState(user.name);
|
|
const [email, setEmail] = useState(user.email);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (onUpdateProfile) onUpdateProfile({ name, email });
|
|
};
|
|
|
|
return (
|
|
<div data-testid="bl-shell-profile-page" style={{ maxWidth: 600 }}>
|
|
<h1
|
|
style={{
|
|
fontSize: 24,
|
|
fontWeight: 700,
|
|
marginBottom: 24,
|
|
color: 'var(--color-foreground, #111827)',
|
|
}}
|
|
>
|
|
Profile
|
|
</h1>
|
|
|
|
{error && (
|
|
<div data-testid="bl-profile-error" style={alertStyle('var(--color-destructive, #dc2626)')}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
{success && (
|
|
<div data-testid="bl-profile-success" style={alertStyle('var(--color-success, #16a34a)')}>
|
|
{success}
|
|
</div>
|
|
)}
|
|
|
|
{/* Avatar */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 32 }}>
|
|
<div
|
|
data-testid="bl-profile-avatar"
|
|
style={{
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: '50%',
|
|
background: 'var(--bl-shell-accent, var(--color-primary, #2563eb))',
|
|
color: 'var(--bl-accent-foreground, #fff)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: 24,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
{user.avatarUrl ? (
|
|
<img
|
|
src={user.avatarUrl}
|
|
alt={user.name}
|
|
style={{ width: 64, height: 64, borderRadius: '50%' }}
|
|
/>
|
|
) : (
|
|
user.name
|
|
.split(' ')
|
|
.map(w => w[0])
|
|
.join('')
|
|
.toUpperCase()
|
|
.slice(0, 2)
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 18, fontWeight: 600 }}>{user.name}</div>
|
|
<div style={{ fontSize: 14, color: 'var(--color-muted-foreground, #6b7280)' }}>
|
|
{user.email}
|
|
</div>
|
|
{user.role && (
|
|
<div
|
|
style={{
|
|
fontSize: 12,
|
|
color: 'var(--color-muted-foreground, #6b7280)',
|
|
marginTop: 2,
|
|
}}
|
|
>
|
|
Role: {user.role}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Form */}
|
|
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
<div>
|
|
<label style={labelStyle} htmlFor="bl-profile-name">
|
|
Name
|
|
</label>
|
|
<input
|
|
id="bl-profile-name"
|
|
data-testid="bl-profile-name"
|
|
type="text"
|
|
value={name}
|
|
onChange={e => setName(e.target.value)}
|
|
required
|
|
style={inputStyle}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label style={labelStyle} htmlFor="bl-profile-email">
|
|
Email
|
|
</label>
|
|
<input
|
|
id="bl-profile-email"
|
|
data-testid="bl-profile-email"
|
|
type="email"
|
|
value={email}
|
|
onChange={e => setEmail(e.target.value)}
|
|
required
|
|
style={inputStyle}
|
|
/>
|
|
</div>
|
|
|
|
{onUpdateProfile && (
|
|
<button
|
|
data-testid="bl-profile-submit"
|
|
type="submit"
|
|
disabled={isLoading}
|
|
style={{
|
|
padding: '10px 20px',
|
|
borderRadius: 8,
|
|
border: 'none',
|
|
fontSize: 14,
|
|
fontWeight: 600,
|
|
cursor: isLoading ? 'not-allowed' : 'pointer',
|
|
background: 'var(--bl-shell-accent, var(--color-primary, #2563eb))',
|
|
color: 'var(--bl-accent-foreground, #fff)',
|
|
opacity: isLoading ? 0.6 : 1,
|
|
alignSelf: 'flex-start',
|
|
}}
|
|
>
|
|
{isLoading ? 'Saving...' : 'Save Changes'}
|
|
</button>
|
|
)}
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const labelStyle: React.CSSProperties = {
|
|
display: 'block',
|
|
fontSize: 14,
|
|
fontWeight: 500,
|
|
marginBottom: 6,
|
|
color: 'var(--color-foreground, #111827)',
|
|
};
|
|
|
|
const inputStyle: React.CSSProperties = {
|
|
width: '100%',
|
|
padding: '10px 12px',
|
|
borderRadius: 8,
|
|
border: '1px solid var(--bl-shell-border, var(--color-border, #e5e7eb))',
|
|
fontSize: 14,
|
|
background: 'var(--color-surface, #fff)',
|
|
color: 'var(--color-foreground, #111827)',
|
|
boxSizing: 'border-box',
|
|
};
|
|
|
|
function alertStyle(color: string): React.CSSProperties {
|
|
return {
|
|
padding: '10px 14px',
|
|
borderRadius: 8,
|
|
marginBottom: 16,
|
|
fontSize: 14,
|
|
color,
|
|
background: `color-mix(in srgb, ${color} 10%, transparent)`,
|
|
border: `1px solid color-mix(in srgb, ${color} 30%, transparent)`,
|
|
};
|
|
}
|