Skip to content
File

Blob: src/client/components/ui/page-loading-skeleton.tsx

typescript234 lines
1import { PageChrome } from "@/client/components/ui/page-chrome";
2import { Skeleton } from "@/client/components/ui/skeleton";
3import type { PageKind } from "@/shared/types";
4import {
5 CANVAS_PAGE_BODY_CENTERED_CLASS,
6 CANVAS_PAGE_BODY_STAGE_CLASS,
7 type CanvasStageLayout,
8 DOC_PAGE_MAIN_CLASS,
9 DOC_PAGE_RAIL_CLASS,
10 DOC_PAGE_RAIL_INNER_CLASS,
11 OUTLINE_RAIL_MEDIA_QUERY,
12 PAGE_CONTENT_COLUMN_CLASS,
13 PAGE_SHELL_CLASS,
14 PAGE_STAGE_CLASS,
15 PAGE_STAGE_TRACKS_CLASS,
16 PAGE_STAGE_WITH_TRACKS_CLASS,
17} from "@/client/components/ui/page-layout";
18import { useMediaQuery } from "@/client/hooks/use-media-query";
19 
20const OUTLINE_RAIL_ROWS = [
21 { indent: 0, width: "w-4/5" },
22 { indent: 0.875, width: "w-3/5" },
23 { indent: 0.875, width: "w-2/3" },
24 { indent: 1.75, width: "w-1/2" },
25 { indent: 0, width: "w-3/4" },
26];
27 
28type LoadingSkeletonKind = PageKind | "unknown";
29type DocumentSkeletonLayout = "rail" | "inline";
30 
31function ChromeBreadcrumbSkeleton() {
32 return (
33 <>
34 <div className="hidden items-center gap-2 md:flex">
35 <Skeleton className="h-3 w-20" />
36 <Skeleton className="h-3 w-3" />
37 <Skeleton className="h-3 w-24" />
38 </div>
39 <div className="flex items-center gap-2 md:hidden">
40 <Skeleton className="h-3 w-24" />
41 <Skeleton className="h-3 w-3" />
42 <Skeleton className="h-3 w-3" />
43 </div>
44 </>
45 );
46}
47 
48function PageChromeSkeleton() {
49 return (
50 <>
51 <PageChrome
52 cover={<Skeleton className="h-48 w-full rounded-b-lg" />}
53 breadcrumb={<ChromeBreadcrumbSkeleton />}
54 icon={<Skeleton className="h-9 w-11 rounded-md" />}
55 />
56 <div className="mb-6 pl-4 sm:pl-7">
57 <Skeleton className="h-10 w-2/3 sm:h-12" />
58 </div>
59 </>
60 );
61}
62 
63function RailSkeleton() {
64 return (
65 <>
66 <div className="mb-3 flex items-center gap-2">
67 <Skeleton className="h-3.5 w-3.5 shrink-0" />
68 <Skeleton className="h-2 w-16" />
69 </div>
70 <ul className="flex flex-col gap-[0.2rem]">
71 {OUTLINE_RAIL_ROWS.map((row, i) => (
72 <li
73 key={i}
74 className="flex items-center gap-[0.45rem] py-[0.45rem]"
75 style={{ paddingInlineStart: `${row.indent}rem` }}
76 >
77 <Skeleton className="h-3 w-3 shrink-0" />
78 <Skeleton className={`h-3 ${row.width}`} />
79 </li>
80 ))}
81 </ul>
82 </>
83 );
84}
85 
86function DocumentBodySkeleton() {
87 return (
88 <div className="space-y-3 pl-7">
89 <Skeleton className="h-4 w-full" />
90 <Skeleton className="h-4 w-5/6" />
91 <Skeleton className="h-4 w-4/6" />
92 <Skeleton className="h-4 w-full" />
93 <Skeleton className="h-4 w-3/5" />
94 </div>
95 );
96}
97 
98function CanvasBodySkeleton() {
99 return <Skeleton className="h-[70vh] w-full rounded-lg" />;
100}
101 
102function UnknownBodySkeleton() {
103 return (
104 <div className="space-y-3 pl-7">
105 <Skeleton className="h-4 w-full" />
106 <Skeleton className="h-4 w-5/6" />
107 <Skeleton className="h-4 w-4/6" />
108 </div>
109 );
110}
111 
112interface PageLoadingSkeletonProps {
113 canvasLayout?: CanvasStageLayout;
114 kind?: LoadingSkeletonKind;
115 documentLayout?: DocumentSkeletonLayout;
116}
117 
118export function PageLoadingSkeleton({
119 canvasLayout = "centered",
120 kind = "doc",
121 documentLayout = "rail",
122}: PageLoadingSkeletonProps) {
123 // Mirror DocumentPage: the rail layout is only realized in CSS at min-[1440px],
124 // so collapse to inline when the viewport can't host the side rail.
125 const railViewport = useMediaQuery(OUTLINE_RAIL_MEDIA_QUERY);
126 const effectiveDocumentLayout: DocumentSkeletonLayout = documentLayout === "rail" && railViewport ? "rail" : "inline";
127 
128 if (kind === "canvas") {
129 return (
130 <div className={PAGE_SHELL_CLASS} aria-busy="true">
131 <div className={PAGE_STAGE_CLASS}>
132 {canvasLayout === "stage" ? (
133 <>
134 <div className={PAGE_STAGE_TRACKS_CLASS}>
135 <div className="min-w-0">
136 <div className={PAGE_CONTENT_COLUMN_CLASS}>
137 <PageChromeSkeleton />
138 </div>
139 </div>
140 </div>
141 <div className={CANVAS_PAGE_BODY_STAGE_CLASS}>
142 <CanvasBodySkeleton />
143 </div>
144 </>
145 ) : (
146 <div className="min-w-0">
147 <div className={PAGE_CONTENT_COLUMN_CLASS}>
148 <PageChromeSkeleton />
149 </div>
150 <div className={CANVAS_PAGE_BODY_CENTERED_CLASS}>
151 <CanvasBodySkeleton />
152 </div>
153 </div>
154 )}
155 </div>
156 </div>
157 );
158 }
159 
160 if (kind === "unknown") {
161 const reserveTracks = canvasLayout === "stage" || effectiveDocumentLayout === "rail";
162 if (reserveTracks) {
163 return (
164 <div className={PAGE_SHELL_CLASS} aria-busy="true">
165 <div className={PAGE_STAGE_WITH_TRACKS_CLASS}>
166 <div className={DOC_PAGE_MAIN_CLASS}>
167 <div className={PAGE_CONTENT_COLUMN_CLASS}>
168 <PageChromeSkeleton />
169 <UnknownBodySkeleton />
170 </div>
171 </div>
172 <aside className={DOC_PAGE_RAIL_CLASS} aria-hidden="true">
173 <div className={DOC_PAGE_RAIL_INNER_CLASS}>
174 <RailSkeleton />
175 </div>
176 </aside>
177 </div>
178 </div>
179 );
180 }
181 return (
182 <div className={PAGE_SHELL_CLASS} aria-busy="true">
183 <div className={PAGE_STAGE_CLASS}>
184 <div className="min-w-0">
185 <div className={PAGE_CONTENT_COLUMN_CLASS}>
186 <PageChromeSkeleton />
187 <UnknownBodySkeleton />
188 <div className="mt-8">
189 <RailSkeleton />
190 </div>
191 </div>
192 </div>
193 </div>
194 </div>
195 );
196 }
197 
198 if (effectiveDocumentLayout === "inline") {
199 return (
200 <div className={PAGE_SHELL_CLASS} aria-busy="true">
201 <div className={PAGE_STAGE_CLASS}>
202 <div className="min-w-0">
203 <div className={PAGE_CONTENT_COLUMN_CLASS}>
204 <PageChromeSkeleton />
205 <DocumentBodySkeleton />
206 <div className="mt-8">
207 <RailSkeleton />
208 </div>
209 </div>
210 </div>
211 </div>
212 </div>
213 );
214 }
215 
216 return (
217 <div className={PAGE_SHELL_CLASS} aria-busy="true">
218 <div className={PAGE_STAGE_WITH_TRACKS_CLASS}>
219 <div className={DOC_PAGE_MAIN_CLASS}>
220 <div className={PAGE_CONTENT_COLUMN_CLASS}>
221 <PageChromeSkeleton />
222 <DocumentBodySkeleton />
223 </div>
224 </div>
225 <aside className={DOC_PAGE_RAIL_CLASS} aria-hidden="true">
226 <div className={DOC_PAGE_RAIL_INNER_CLASS}>
227 <RailSkeleton />
228 </div>
229 </aside>
230 </div>
231 </div>
232 );
233}