Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

skeleton.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

skeleton.tsxBlame80 lines · 1 contributor
c63b860Claude1/**
2 * BLOCK O2 — Loading skeleton component.
3 *
4 * Replaces ad-hoc spinners. Renders N gradient-pulsing rectangles
5 * shaped like the eventual content. CSS-only animation that respects
6 * prefers-reduced-motion.
7 */
8
9import type { FC } from "hono/jsx";
10
11export interface SkeletonProps {
12 height?: string;
13 width?: string;
14 rounded?: boolean;
15 count?: number;
16 gap?: string;
17 ariaLabel?: string;
18}
19
20export const Skeleton: FC<SkeletonProps> = ({
21 height = "1em",
22 width = "100%",
23 rounded = false,
24 count = 1,
25 gap = "8px",
26 ariaLabel = "Loading",
27}) => {
28 const n = Math.max(1, Math.floor(count));
29 const bars: number[] = [];
30 for (let i = 0; i < n; i++) bars.push(i);
31 const radius = rounded ? "9999px" : "6px";
32 return (
33 <div class="skeleton-stack" role="status" aria-live="polite" aria-busy="true" aria-label={ariaLabel}
34 style={`display:flex;flex-direction:column;gap:${gap};width:100%`}>
35 {bars.map(() => (
36 <div class="skeleton-bar" style={`height:${height};width:${width};border-radius:${radius}`} />
37 ))}
38 <span class="sr-only">{ariaLabel}</span>
39 <style dangerouslySetInnerHTML={{ __html: skeletonCss }} />
40 </div>
41 );
42};
43
44export const SkeletonRow: FC<{ height?: number }> = ({ height = 48 }) => (
45 <div class="skeleton-bar"
46 style={`height:${height}px;width:100%;border-radius:8px;margin-bottom:8px`}
47 aria-hidden="true">
48 <style dangerouslySetInnerHTML={{ __html: skeletonCss }} />
49 </div>
50);
51
52export const SkeletonRepoRow: FC = () => (
53 <div class="skeleton-repo-row" style="display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--border)">
54 <div class="skeleton-bar" style="width:32px;height:32px;border-radius:50%" />
55 <div style="flex:1;display:flex;flex-direction:column;gap:6px">
56 <div class="skeleton-bar" style="height:14px;width:40%" />
57 <div class="skeleton-bar" style="height:11px;width:65%" />
58 </div>
59 <style dangerouslySetInnerHTML={{ __html: skeletonCss }} />
60 </div>
61);
62
63export const SkeletonList: FC<{ count?: number; height?: number }> = ({ count = 4 }) => {
64 const n = Math.max(1, Math.floor(count));
65 const rows: number[] = [];
66 for (let i = 0; i < n; i++) rows.push(i);
67 return (
68 <div class="skeleton-list" role="status" aria-busy="true" aria-live="polite" aria-label="Loading repositories">
69 {rows.map(() => (<SkeletonRepoRow />))}
70 </div>
71 );
72};
73
74export const skeletonCss = `
75.skeleton-bar { background: linear-gradient(90deg, var(--bg-elevated) 0%, rgba(140,109,255,0.10) 50%, var(--bg-elevated) 100%); background-size: 200% 100%; animation: skeleton-shimmer 1.4s linear infinite; border: 1px solid var(--border); display: block; }
76:root[data-theme='light'] .skeleton-bar { background: linear-gradient(90deg, var(--bg-elevated) 0%, rgba(109,77,255,0.10) 50%, var(--bg-elevated) 100%); background-size: 200% 100%; }
77@keyframes skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
78.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
79@media (prefers-reduced-motion: reduce) { .skeleton-bar { animation: none; } }
80`;