learning_ai_common_plat/packages/dashboard-shell/src/ProfilePage.tsx
saravanakumardb1 f1ebff5514 feat(scripts+ui): Tier 2 complete \u2014 common_plat 0 hex findings (was 59)
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)
2026-05-23 14:37:51 -07:00

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)`,
};
}