Skip to content

Commit 346acab

Browse files
nullcoderClaude
andauthored
feat: implement LoadingState components (#97)
* feat: implement LoadingState components (#67) - Add LoadingState component with skeleton, spinner, and progress variants - Implement EditorSkeleton for code editor loading states - Create LoadingSpinner with customizable messages - Add LoadingProgress for file processing operations - Include useDelayedLoading hook to prevent loading state flashing - Add comprehensive tests and demo page - Support fullscreen overlays for blocking operations - Implement proper ARIA attributes for accessibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <claude@ghostpaste.dev> * docs: mark LoadingStates (#67) as complete --------- Co-authored-by: Claude <claude@ghostpaste.dev>
1 parent c2dad1e commit 346acab

File tree

6 files changed

+757
-11
lines changed

6 files changed

+757
-11
lines changed

app/demo/loading-states/page.tsx

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import {
5+
LoadingState,
6+
LoadingSkeleton,
7+
LoadingSpinner,
8+
LoadingProgress,
9+
EditorSkeleton,
10+
useDelayedLoading,
11+
} from "@/components/ui/loading-state";
12+
import { Button } from "@/components/ui/button";
13+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
14+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
15+
16+
export default function LoadingStatesDemo() {
17+
const [showFullscreen, setShowFullscreen] = React.useState(false);
18+
const [progress, setProgress] = React.useState(0);
19+
const [isDelayedLoading, setIsDelayedLoading] = React.useState(false);
20+
const showDelayed = useDelayedLoading(isDelayedLoading, 500);
21+
22+
// Simulate progress
23+
React.useEffect(() => {
24+
const interval = setInterval(() => {
25+
setProgress((prev) => {
26+
if (prev >= 100) return 0;
27+
return prev + 10;
28+
});
29+
}, 500);
30+
31+
return () => clearInterval(interval);
32+
}, []);
33+
34+
return (
35+
<div className="container mx-auto py-8">
36+
<h1 className="mb-8 text-3xl font-bold">Loading States Demo</h1>
37+
38+
<Tabs defaultValue="skeleton" className="w-full">
39+
<TabsList className="grid w-full grid-cols-4">
40+
<TabsTrigger value="skeleton">Skeleton</TabsTrigger>
41+
<TabsTrigger value="spinner">Spinner</TabsTrigger>
42+
<TabsTrigger value="progress">Progress</TabsTrigger>
43+
<TabsTrigger value="delayed">Delayed</TabsTrigger>
44+
</TabsList>
45+
46+
<TabsContent value="skeleton" className="space-y-4">
47+
<Card>
48+
<CardHeader>
49+
<CardTitle>Skeleton Loading States</CardTitle>
50+
</CardHeader>
51+
<CardContent className="space-y-6">
52+
<div>
53+
<h3 className="mb-3 text-lg font-semibold">
54+
Generic Loading Skeleton
55+
</h3>
56+
<LoadingState type="skeleton" />
57+
</div>
58+
59+
<div>
60+
<h3 className="mb-3 text-lg font-semibold">
61+
Editor Loading Skeleton
62+
</h3>
63+
<EditorSkeleton />
64+
</div>
65+
66+
<div>
67+
<h3 className="mb-3 text-lg font-semibold">
68+
Direct Skeleton Component
69+
</h3>
70+
<LoadingSkeleton />
71+
</div>
72+
</CardContent>
73+
</Card>
74+
</TabsContent>
75+
76+
<TabsContent value="spinner" className="space-y-4">
77+
<Card>
78+
<CardHeader>
79+
<CardTitle>Spinner Loading States</CardTitle>
80+
</CardHeader>
81+
<CardContent className="space-y-6">
82+
<div>
83+
<h3 className="mb-3 text-lg font-semibold">Default Spinner</h3>
84+
<LoadingState type="spinner" />
85+
</div>
86+
87+
<div>
88+
<h3 className="mb-3 text-lg font-semibold">
89+
Spinner with Custom Message
90+
</h3>
91+
<LoadingState type="spinner" message="Encrypting files..." />
92+
</div>
93+
94+
<div>
95+
<h3 className="mb-3 text-lg font-semibold">
96+
Direct Spinner Component
97+
</h3>
98+
<LoadingSpinner message="Decrypting gist..." />
99+
</div>
100+
101+
<div>
102+
<h3 className="mb-3 text-lg font-semibold">
103+
Fullscreen Spinner
104+
</h3>
105+
<Button
106+
onClick={() => {
107+
setShowFullscreen(true);
108+
setTimeout(() => setShowFullscreen(false), 3000);
109+
}}
110+
>
111+
Show Fullscreen Spinner (3s)
112+
</Button>
113+
{showFullscreen && (
114+
<LoadingState
115+
type="spinner"
116+
message="Processing your request..."
117+
fullscreen
118+
/>
119+
)}
120+
</div>
121+
</CardContent>
122+
</Card>
123+
</TabsContent>
124+
125+
<TabsContent value="progress" className="space-y-4">
126+
<Card>
127+
<CardHeader>
128+
<CardTitle>Progress Loading States</CardTitle>
129+
</CardHeader>
130+
<CardContent className="space-y-6">
131+
<div>
132+
<h3 className="mb-3 text-lg font-semibold">
133+
Auto-incrementing Progress
134+
</h3>
135+
<LoadingState
136+
type="progress"
137+
message="Uploading files..."
138+
progress={progress}
139+
/>
140+
</div>
141+
142+
<div>
143+
<h3 className="mb-3 text-lg font-semibold">
144+
Static Progress Examples
145+
</h3>
146+
<div className="space-y-4">
147+
<LoadingProgress message="Processing main.js" progress={25} />
148+
<LoadingProgress
149+
message="Encrypting data.json"
150+
progress={50}
151+
/>
152+
<LoadingProgress
153+
message="Finalizing upload..."
154+
progress={90}
155+
/>
156+
</div>
157+
</div>
158+
159+
<div>
160+
<h3 className="mb-3 text-lg font-semibold">
161+
Edge Cases (Clamped Values)
162+
</h3>
163+
<div className="space-y-4">
164+
<LoadingProgress
165+
message="Negative progress (clamped to 0)"
166+
progress={-20}
167+
/>
168+
<LoadingProgress
169+
message="Overflow progress (clamped to 100)"
170+
progress={150}
171+
/>
172+
</div>
173+
</div>
174+
</CardContent>
175+
</Card>
176+
</TabsContent>
177+
178+
<TabsContent value="delayed" className="space-y-4">
179+
<Card>
180+
<CardHeader>
181+
<CardTitle>Delayed Loading States</CardTitle>
182+
</CardHeader>
183+
<CardContent className="space-y-6">
184+
<div>
185+
<h3 className="mb-3 text-lg font-semibold">
186+
useDelayedLoading Hook Demo
187+
</h3>
188+
<p className="text-muted-foreground mb-4 text-sm">
189+
The loading state only shows if the operation takes longer
190+
than 500ms. This prevents flashing loading states for quick
191+
operations.
192+
</p>
193+
<div className="space-y-4">
194+
<Button
195+
onClick={() => {
196+
setIsDelayedLoading(true);
197+
setTimeout(() => setIsDelayedLoading(false), 100);
198+
}}
199+
>
200+
Quick Operation (100ms - No Loading State)
201+
</Button>
202+
203+
<Button
204+
onClick={() => {
205+
setIsDelayedLoading(true);
206+
setTimeout(() => setIsDelayedLoading(false), 1000);
207+
}}
208+
>
209+
Slow Operation (1s - Shows Loading State)
210+
</Button>
211+
212+
<div className="mt-4">
213+
<p className="text-muted-foreground text-sm">
214+
Loading state active: {isDelayedLoading ? "Yes" : "No"}
215+
</p>
216+
<p className="text-muted-foreground text-sm">
217+
Showing loading UI: {showDelayed ? "Yes" : "No"}
218+
</p>
219+
</div>
220+
221+
{showDelayed && (
222+
<LoadingSpinner message="Processing delayed operation..." />
223+
)}
224+
</div>
225+
</div>
226+
</CardContent>
227+
</Card>
228+
</TabsContent>
229+
</Tabs>
230+
231+
<Card className="mt-8">
232+
<CardHeader>
233+
<CardTitle>Usage Examples</CardTitle>
234+
</CardHeader>
235+
<CardContent>
236+
<pre className="bg-muted overflow-x-auto rounded-lg p-4 text-sm">
237+
{`// Basic skeleton
238+
<LoadingState type="skeleton" />
239+
240+
// Spinner with message
241+
<LoadingState type="spinner" message="Encrypting files..." />
242+
243+
// Progress bar
244+
<LoadingState type="progress" message="Uploading..." progress={45} />
245+
246+
// Fullscreen overlay
247+
<LoadingState type="spinner" fullscreen={true} />
248+
249+
// Editor skeleton
250+
<EditorSkeleton />
251+
252+
// Delayed loading hook
253+
const showLoading = useDelayedLoading(isLoading, 100);
254+
{showLoading && <LoadingSpinner />}`}
255+
</pre>
256+
</CardContent>
257+
</Card>
258+
259+
<Card className="mt-8">
260+
<CardHeader>
261+
<CardTitle>Accessibility Features</CardTitle>
262+
</CardHeader>
263+
<CardContent className="space-y-2">
264+
<p>✓ Proper ARIA roles (status, progressbar)</p>
265+
<p>✓ Screen reader announcements with aria-live</p>
266+
<p>✓ Descriptive aria-labels</p>
267+
<p>✓ Progress percentage announcements</p>
268+
<p>✓ Respects prefers-reduced-motion</p>
269+
</CardContent>
270+
</Card>
271+
</div>
272+
);
273+
}

0 commit comments

Comments
 (0)