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.
134 lines
3.6 KiB
TypeScript
134 lines
3.6 KiB
TypeScript
/**
|
||
* SkeletonCalendar — shimmer loading placeholder.
|
||
*
|
||
* UI-SPEC §SkeletonCalendar:
|
||
* - Month variant: 6×7 grid of rounded rect placeholders, animated shimmer
|
||
* - Agenda variant: 4 date-group blocks, 2–3 rows each, varying widths (60–90%)
|
||
* - 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, 2–3 event rows each, varying widths */
|
||
function AgendaSkeleton() {
|
||
// Each group has: a date header + 2–3 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>
|
||
);
|
||
}
|