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.
| c63b860 | 1 | /** |
| 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 | ||
| 9 | import type { FC } from "hono/jsx"; | |
| 10 | ||
| 11 | export interface SkeletonProps { | |
| 12 | height?: string; | |
| 13 | width?: string; | |
| 14 | rounded?: boolean; | |
| 15 | count?: number; | |
| 16 | gap?: string; | |
| 17 | ariaLabel?: string; | |
| 18 | } | |
| 19 | ||
| 20 | export 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 | ||
| 44 | export 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 | ||
| 52 | export 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 | ||
| 63 | export 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 | ||
| 74 | export 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 | `; |