Files
familysync/apps/pwa/src/components/SkeletonCalendar.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

134 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* SkeletonCalendar — shimmer loading placeholder.
*
* UI-SPEC §SkeletonCalendar:
* - Month variant: 6×7 grid of rounded rect placeholders, animated shimmer
* - Agenda variant: 4 date-group blocks, 23 rows each, varying widths (6090%)
* - No spinner; shimmer only (matches Fantastical-style)
* - aria-busy="true", aria-label="Loading calendar" on root
*
* Shimmer keyframe declared in tokens.css (@keyframes shimmer).
* The shimmer animation uses the gradient from tokens.css:
* background: linear-gradient(90deg, --color-surface-dim, --color-border-subtle, --color-surface-dim)
*/
type SkeletonVariant = 'month' | 'agenda';
interface SkeletonCalendarProps {
variant?: SkeletonVariant;
}
/** Shimmer inline style — gradient + animation referencing the keyframe in tokens.css */
const shimmerStyle: React.CSSProperties = {
background:
'linear-gradient(90deg, var(--color-surface-dim), var(--color-border-subtle), var(--color-surface-dim))',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
borderRadius: 'var(--space-1)',
};
export function SkeletonCalendar({ variant = 'month' }: SkeletonCalendarProps) {
return (
<div
aria-busy="true"
aria-label="Loading calendar"
style={{
padding: 'var(--space-4)',
fontFamily: 'var(--font-family-base)',
width: '100%',
}}
>
{variant === 'month' ? <MonthSkeleton /> : <AgendaSkeleton />}
</div>
);
}
/** 6×7 month grid of shimmer cells */
function MonthSkeleton() {
return (
<div>
{/* Day-of-week header row */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 'var(--space-1)',
marginBottom: 'var(--space-2)',
}}
>
{Array.from({ length: 7 }).map((_, i) => (
<div
key={i}
style={{
...shimmerStyle,
height: '16px',
opacity: 0.5,
}}
/>
))}
</div>
{/* 6 rows × 7 columns */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(7, 1fr)',
gap: 'var(--space-1)',
}}
>
{Array.from({ length: 42 }).map((_, i) => (
<div
key={i}
style={{
height: '80px',
...shimmerStyle,
}}
/>
))}
</div>
</div>
);
}
/** 4 date-group blocks, 23 event rows each, varying widths */
function AgendaSkeleton() {
// Each group has: a date header + 23 event rows with varying widths
const groups = [
{ rows: [80, 65, 90] },
{ rows: [75, 60] },
{ rows: [85, 70, 65] },
{ rows: [60, 80] },
];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-6)' }}>
{groups.map((group, gi) => (
<div key={gi}>
{/* Date group header placeholder */}
<div
style={{
...shimmerStyle,
height: '20px',
width: '40%',
marginBottom: 'var(--space-3)',
}}
/>
{/* Event rows */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
{group.rows.map((widthPct, ri) => (
<div
key={ri}
style={{
...shimmerStyle,
height: '44px',
width: `${widthPct}%`,
}}
/>
))}
</div>
</div>
))}
</div>
);
}