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.
138 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|