Files
familysync/apps/pwa/src/components/AuthSplash.tsx
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

138 lines
4.4 KiB
TypeScript

/**
* AuthSplash — full-screen auth interstitial (D-10 + D-11, Plan 06-05).
*
* Replaces the optimistic calendar/skeleton/alert renders while auth state is
* unknown (loading) or failing (redirecting), and surfaces the session-expired
* interstitial for mid-use 401s.
*
* States:
* loading — initial cold load; meQuery is still pending. Spin + "Signing you in".
* redirecting — meQuery failed (cold load OR session-expiry); maybeRedirectToLogin()
* already fired (or the session-expiry handler fired it). Spin +
* heading/body driven by `copy` prop (defaults to cold-load copy).
* dead-end — one-shot guard has already fired; redirect failed or guard is exhausted.
* No spinner; "Sign-in required. Tap here to try again."
*
* Copy contract (UI-SPEC §Copywriting Contract §Surface 1 + §Surface 2):
* loading: heading="Signing you in" body="Taking you to the sign-in page…"
* redirecting: same defaults (or "Session expired" / "Signing you back in…" via props)
* dead-end: "Sign-in required. Tap here to try again." (tap calls clearLoginRedirect + maybeRedirectToLogin)
*
* Layout analog: SkeletonCalendar.tsx — full-screen centered column, inline styles only.
*/
import { Loader2 } from 'lucide-react';
import { clearLoginRedirect, maybeRedirectToLogin } from '../lib/loginRedirect.js';
export type AuthSplashState = 'loading' | 'redirecting' | 'dead-end';
export interface AuthSplashProps {
state: AuthSplashState;
/**
* Override the heading for the loading/redirecting states.
* Defaults to "Signing you in" (cold-load copy per UI-SPEC §Surface 1).
*/
heading?: string;
/**
* Override the body text for the loading/redirecting states.
* Defaults to "Taking you to the sign-in page…" (cold-load copy per UI-SPEC §Surface 1).
*/
body?: string;
/**
* When true, renders with position:fixed inset:0 z-index:999 so the splash
* covers the entire viewport including the persistent AppNav (FIX 3).
* Use this when CalendarShell (a child of the app shell) needs to show an
* auth interstitial that hides the nav chrome.
*/
overlay?: boolean;
}
export function AuthSplash({
state,
heading = 'Signing you in',
body = 'Taking you to the sign-in page…',
overlay = false,
}: AuthSplashProps) {
const isDeadEnd = state === 'dead-end';
const showSpinner = !isDeadEnd;
return (
<div
role="status"
aria-label="Signing you in"
style={{
...(overlay ? { position: 'fixed', inset: 0, zIndex: 999 } : { height: '100dvh' }),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 'var(--space-4)',
background: 'var(--color-surface)',
fontFamily: 'var(--font-family-base)',
padding: 'var(--space-6)',
textAlign: 'center',
}}
>
{showSpinner && (
<Loader2
size={24}
aria-hidden="true"
style={{
color: 'var(--color-member-0)',
animation: 'spin 1s linear infinite',
}}
/>
)}
{isDeadEnd ? (
// Dead-end: one-shot guard has already fired — show tap-to-retry
<button
onClick={() => {
clearLoginRedirect();
maybeRedirectToLogin();
}}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
fontFamily: 'var(--font-family-base)',
fontSize: '15px',
fontWeight: 400,
lineHeight: 1.5,
color: 'var(--color-text-secondary)',
textDecoration: 'underline',
}}
>
Sign-in required. Tap here to try again.
</button>
) : (
<>
<h1
style={{
margin: 0,
fontSize: '18px',
fontWeight: 600,
lineHeight: 1.25,
color: 'var(--color-text-primary)',
}}
>
{heading}
</h1>
<p
style={{
margin: 0,
fontSize: '15px',
fontWeight: 400,
lineHeight: 1.5,
color: 'var(--color-text-secondary)',
}}
>
{body}
</p>
</>
)}
</div>
);
}