Files
OmniRoute/src/shared/components/Loading.tsx
diegosouzapw 052eb8d330 refactor: replace any types with generics and add Zod validation schemas
Eliminate `any` usage across the codebase by introducing proper generics,
typed interfaces (StatementLike, DbLike, PromptRow, etc.), and helper
conversion functions (toNumber, toString, parseVariables). Add
comprehensive Zod validation schemas for API endpoint inputs to enforce
runtime type safety alongside compile-time checks.
2026-03-04 18:59:27 -03:00

125 lines
2.9 KiB
TypeScript

"use client";
import type { HTMLAttributes } from "react";
import { cn } from "@/shared/utils/cn";
type SpinnerSize = "sm" | "md" | "lg" | "xl";
type LoadingType = "spinner" | "page" | "skeleton" | "card";
const spinnerSizes: Record<SpinnerSize, string> = {
sm: "size-4",
md: "size-6",
lg: "size-8",
xl: "size-12",
};
interface SpinnerProps {
size?: SpinnerSize;
className?: string;
label?: string;
}
interface PageLoadingProps {
message?: string;
className?: string;
}
interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
className?: string;
}
interface LoadingProps extends HTMLAttributes<HTMLDivElement> {
type?: LoadingType;
className?: string;
message?: string;
size?: SpinnerSize;
label?: string;
}
// Spinner loading
export function Spinner({ size = "md", className, label = "Loading" }: SpinnerProps) {
return (
<span
role="status"
aria-live="polite"
aria-label={label}
className={cn("inline-flex", className)}
>
<span className="sr-only">{label}</span>
<span
aria-hidden="true"
className={cn(
"material-symbols-outlined text-primary animate-spin motion-reduce:animate-none",
spinnerSizes[size]
)}
>
progress_activity
</span>
</span>
);
}
// Full page loading
export function PageLoading({ message = "Loading...", className }: PageLoadingProps) {
return (
<div
className={cn(
"fixed inset-0 z-50 flex flex-col items-center justify-center bg-bg px-6",
className
)}
role="status"
aria-live="polite"
aria-busy="true"
>
<Spinner size="xl" />
<p className="mt-4 text-text-muted text-center">{message}</p>
</div>
);
}
// Skeleton loading
export function Skeleton({ className, ...props }: SkeletonProps) {
return (
<div
aria-hidden="true"
className={cn("animate-pulse motion-reduce:animate-none rounded-lg bg-border", className)}
{...props}
/>
);
}
// Card skeleton
export function CardSkeleton() {
return (
<div className="p-6 rounded-xl border border-border bg-surface" aria-hidden="true">
<div className="flex items-center justify-between mb-4 gap-4">
<Skeleton className="h-4 w-24" />
<Skeleton className="size-10 rounded-lg" />
</div>
<Skeleton className="h-8 w-16 mb-2" />
<Skeleton className="h-3 w-20" />
</div>
);
}
// Default export
export default function Loading({
type = "spinner",
className,
message,
size,
label,
...props
}: LoadingProps) {
switch (type) {
case "page":
return <PageLoading message={message} className={className} />;
case "skeleton":
return <Skeleton className={className} {...props} />;
case "card":
return <CardSkeleton />;
default:
return <Spinner size={size} className={className} label={label} />;
}
}