feat(19-04): add LoginError, fetchAuthMode/login/logout + auth fetchers, BrandSlot, brand-seam tokens

- Add LoginError class (4 codes: invalid/rate-limit/locked/server) mirroring SessionExpiredError shape
- Add hasLocalCredential to MeUser interface
- Add fetchAuthMode, fetchLocalLogin, fetchLocalLogout (pre-auth endpoints)
- Add fetchChangePassword, fetchCreateMember, fetchAdminResetPassword, fetchLinkOidc
- Create BrandSlot component with Phase-17-ready placeholder (48px circle, FS initials, h1, tagline)
- Add --brand-logo-* CSS custom properties to tokens.css (Phase 17 seam)
This commit is contained in:
Lucas Berger
2026-06-17 17:10:30 -04:00
parent 1cf572a2b9
commit 869cdc26c8
3 changed files with 276 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* BrandSlot — Phase 17 seam component for the login page brand area.
*
* Phase 19 ships a minimal shippable placeholder: a 48px circle with "FS"
* initials, the app name "FamilySync", and the tagline "Family calendar & lists".
*
* Phase 17 replaces the internals of this component (swap the placeholder div for
* an <img> with a real logo) without touching LoginPage's layout. This isolates
* the branding seam — see 19-UI-SPEC.md §Brand Slot section.
*
* CSS custom properties used (all set in tokens.css with placeholder defaults;
* Phase 17 overrides these values):
* --brand-logo-bg — logo circle background (default: var(--color-member-0))
* --brand-logo-text — initials color (default: #ffffff)
* --brand-logo-size — circle diameter (default: 48px)
* --brand-logo-border-radius — circle shape (default: 50%)
*
* Accessibility:
* <h1> contains the app name — screen readers read "FamilySync" as the page title.
* The logo circle is aria-hidden (the text is the accessible label).
* No <img> today → no broken image ref → no layout shift when Phase 17 replaces it.
*
* Security: all copy is plain-text JSX children — no dangerouslySetInnerHTML (T-05-24).
*/
export function BrandSlot() {
return (
<div style={{ textAlign: 'center' }}>
{/* Phase 17 replaces this div with <img src="..." alt="" /> */}
<div
aria-hidden="true"
style={{
width: 'var(--brand-logo-size, 48px)',
height: 'var(--brand-logo-size, 48px)',
borderRadius: 'var(--brand-logo-border-radius, 50%)',
background: 'var(--brand-logo-bg, var(--color-member-0, #4a90d9))',
color: 'var(--brand-logo-text, #ffffff)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto var(--space-2, 8px)',
fontSize: 'var(--text-display-size, 24px)',
fontWeight: 600,
fontFamily: 'var(--font-family-base)',
flexShrink: 0,
aspectRatio: '1 / 1',
}}
>
FS
</div>
{/* App name — <h1> so screen readers identify the page (UI-SPEC §Accessibility) */}
<h1
style={{
margin: 0,
marginTop: 'var(--space-2, 8px)',
marginBottom: 'var(--space-1, 4px)',
fontSize: 'var(--text-display-size, 24px)',
fontWeight: 600,
lineHeight: 'var(--text-display-line-height, 1.2)',
color: 'var(--color-text-primary, #111318)',
fontFamily: 'var(--font-family-base)',
}}
>
FamilySync
</h1>
{/* Tagline */}
<p
style={{
margin: 0,
marginBottom: 'var(--space-8, 32px)',
fontSize: 'var(--text-body-size, 15px)',
fontWeight: 400,
lineHeight: 'var(--text-body-line-height, 1.5)',
color: 'var(--color-text-secondary, #6b7280)',
fontFamily: 'var(--font-family-base)',
}}
>
Family calendar &amp; lists
</p>
</div>
);
}