mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
feat: add Spinner and LoadingState components for unified loading UX
Introduces two reusable loading components to standardize loading states: - Spinner: Base spinner with accessibility support (role, aria-label) - LoadingState: Semantic loading indicator combining spinner and label Updates loading patterns across the app: - Full-screen/block loading now uses LoadingState (App, router, files, file) - Inline button loading states show spinner + text with aria-busy - Message list loading replaced with MessageSkeleton component - Removes duplicate SpinnerIcon definition from PermissionFooter Improves UX and accessibility: - Loading screens centered and full-height - Consistent ellipsis character (…) - Semantic accessibility (role="status", aria-live, aria-busy) - Adds CSS variables for banner styling
This commit is contained in:
@@ -80,6 +80,21 @@ function DiffDisplay(props: { diffContent: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function FileContentSkeleton() {
|
||||
const widths = ['w-full', 'w-11/12', 'w-5/6', 'w-3/4', 'w-2/3', 'w-4/5']
|
||||
|
||||
return (
|
||||
<div role="status" aria-live="polite">
|
||||
<span className="sr-only">Loading file…</span>
|
||||
<div className="animate-pulse space-y-2 rounded-md border border-[var(--app-border)] bg-[var(--app-code-bg)] p-3">
|
||||
{Array.from({ length: 12 }).map((_, index) => (
|
||||
<div key={`file-skeleton-${index}`} className={`h-3 ${widths[index % widths.length]} rounded bg-[var(--app-subtle-bg)]`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function resolveLanguage(path: string): string | undefined {
|
||||
const parts = path.split('.')
|
||||
if (parts.length <= 1) return undefined
|
||||
@@ -229,7 +244,7 @@ export default function FilePage() {
|
||||
{missingPath ? (
|
||||
<div className="text-sm text-[var(--app-hint)]">No file path provided.</div>
|
||||
) : loading ? (
|
||||
<div className="text-sm text-[var(--app-hint)]">Loading file...</div>
|
||||
<FileContentSkeleton />
|
||||
) : fileError ? (
|
||||
<div className="text-sm text-[var(--app-hint)]">{fileError}</div>
|
||||
) : binaryFile ? (
|
||||
|
||||
@@ -210,6 +210,27 @@ function SearchResultRow(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function FileListSkeleton(props: { label: string; rows?: number }) {
|
||||
const titleWidths = ['w-1/3', 'w-1/2', 'w-2/3', 'w-2/5', 'w-3/5']
|
||||
const subtitleWidths = ['w-1/2', 'w-2/3', 'w-3/4', 'w-1/3']
|
||||
const rows = props.rows ?? 6
|
||||
|
||||
return (
|
||||
<div className="p-3 animate-pulse space-y-3" role="status" aria-live="polite">
|
||||
<span className="sr-only">{props.label}</span>
|
||||
{Array.from({ length: rows }).map((_, index) => (
|
||||
<div key={`skeleton-row-${index}`} className="flex items-center gap-3">
|
||||
<div className="h-6 w-6 rounded bg-[var(--app-subtle-bg)]" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className={`h-3 ${titleWidths[index % titleWidths.length]} rounded bg-[var(--app-subtle-bg)]`} />
|
||||
<div className={`h-2 ${subtitleWidths[index % subtitleWidths.length]} rounded bg-[var(--app-subtle-bg)]`} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FilesPage() {
|
||||
const { api } = useAppContext()
|
||||
const navigate = useNavigate()
|
||||
@@ -311,10 +332,10 @@ export default function FilesPage() {
|
||||
</div>
|
||||
) : null}
|
||||
{gitLoading ? (
|
||||
<div className="p-6 text-sm text-[var(--app-hint)]">Loading Git status...</div>
|
||||
<FileListSkeleton label="Loading Git status…" />
|
||||
) : shouldSearch ? (
|
||||
searchResults.isLoading ? (
|
||||
<div className="p-6 text-sm text-[var(--app-hint)]">Loading files...</div>
|
||||
<FileListSkeleton label="Loading files…" />
|
||||
) : searchResults.error ? (
|
||||
<div className="p-6 text-sm text-[var(--app-hint)]">{searchResults.error}</div>
|
||||
) : searchResults.files.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user