diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..ca6a6f7 --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,8 @@ +# Palette's Journal + +Critical UX and Accessibility Learnings + +## 2024-05-15 - [Loading Skeletons Lack ARIA Roles] + +**Learning:** Generic `div` elements used for visual loading skeletons are invisible to screen readers, leaving users unaware of active background loading processes or structure. +**Action:** When implementing generic loading skeletons, wrap the visual primitive in a container with `role='status'` and `aria-busy='true'` to expose the loading state properly. Additionally, apply an `aria-label="Loading"` to give more context. diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 337a8ee..556d63c 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -12,30 +12,36 @@ import { cn } from '@/lib/utils' * - Card/image */ -interface SkeletonProps { - className?: string - variant?: 'text' | 'circular' | 'rectangular' - animation?: 'pulse' | 'wave' | 'none' +interface SkeletonProps extends React.HTMLAttributes { + className?: string + variant?: 'text' | 'circular' | 'rectangular' + animation?: 'pulse' | 'wave' | 'none' } export function Skeleton({ - className, - variant = 'text', - animation = 'pulse', + className, + variant = 'text', + animation = 'pulse', + ...props }: SkeletonProps) { - return ( -
- ) + return ( +
+ ) } // ============================================================================ @@ -43,82 +49,75 @@ export function Skeleton({ // ============================================================================ interface SkeletonTextProps { - lines?: number - className?: string - lastLineWidth?: string + lines?: number + className?: string + lastLineWidth?: string } export function SkeletonText({ lines = 3, className, lastLineWidth = '60%' }: SkeletonTextProps) { - return ( -
- {Array.from({ length: lines }).map((_, i) => ( -
- -
- ))} + return ( +
+ {Array.from({ length: lines }).map((_, i) => ( +
+
- ) + ))} +
+ ) } interface SkeletonCardProps { - showImage?: boolean - showTitle?: boolean - showDescription?: boolean - showFooter?: boolean - className?: string + showImage?: boolean + showTitle?: boolean + showDescription?: boolean + showFooter?: boolean + className?: string } export function SkeletonCard({ - showImage = true, - showTitle = true, - showDescription = true, - showFooter = false, - className, + showImage = true, + showTitle = true, + showDescription = true, + showFooter = false, + className, }: SkeletonCardProps) { - return ( -
- {showImage && ( - - )} - {showTitle && ( - - )} - {showDescription && ( -
- - -
- )} - {showFooter && ( -
- - -
- )} + return ( +
+ {showImage && } + {showTitle && } + {showDescription && ( +
+ +
- ) + )} + {showFooter && ( +
+ + +
+ )} +
+ ) } export function SkeletonAvatar({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' | 'xl' }) { - const sizeClasses = { - sm: 'h-6 w-6', - md: 'h-10 w-10', - lg: 'h-12 w-12', - xl: 'h-16 w-16', - } - return + const sizeClasses = { + sm: 'h-6 w-6', + md: 'h-10 w-10', + lg: 'h-12 w-12', + xl: 'h-16 w-16', + } + return } export function SkeletonButton({ variant = 'md' }: { variant?: 'sm' | 'md' | 'lg' }) { - const sizeClasses = { - sm: 'h-8 w-20', - md: 'h-10 w-24', - lg: 'h-12 w-32', - } - return + const sizeClasses = { + sm: 'h-8 w-20', + md: 'h-10 w-24', + lg: 'h-12 w-32', + } + return } // ============================================================================ @@ -126,102 +125,104 @@ export function SkeletonButton({ variant = 'md' }: { variant?: 'sm' | 'md' | 'lg // ============================================================================ export function SkeletonMetricCard({ className }: { className?: string }) { - return ( -
- - - -
- ) + return ( +
+ + + +
+ ) } export function SkeletonEventRow({ className }: { className?: string }) { - return ( -
- -
- - -
- -
- ) + return ( +
+ +
+ + +
+ +
+ ) } export function SkeletonChart({ className }: { className?: string }) { - return ( -
-
- -
- - -
-
- + return ( +
+
+ +
+ +
- ) +
+ +
+ ) } export function SkeletonTable({ rows = 5, cols = 4 }: { rows?: number; cols?: number }) { - return ( -
- {/* Header */} -
- {Array.from({ length: cols }).map((_, i) => ( - - ))} -
- {/* Rows */} - {Array.from({ length: rows }).map((_, rowIdx) => ( -
- {Array.from({ length: cols }).map((_, colIdx) => ( - - ))} -
- ))} + return ( +
+ {/* Header */} +
+ {Array.from({ length: cols }).map((_, i) => ( + + ))} +
+ {/* Rows */} + {Array.from({ length: rows }).map((_, rowIdx) => ( +
+ {Array.from({ length: cols }).map((_, colIdx) => ( + + ))}
- ) + ))} +
+ ) } export function SkeletonDashboard() { - return ( -
- {/* Header */} -
-
- - -
-
- - -
-
- - {/* Metrics Row */} -
- - - - -
- - {/* Main Content */} -
-
- -
-
- - - - -
-
+ return ( +
+ {/* Header */} +
+
+ + +
+
+ + +
+
+ + {/* Metrics Row */} +
+ + + + +
+ + {/* Main Content */} +
+
+ +
+
+ + + +
- ) +
+
+ ) }