ui-polish 7: hardening pass — skeletons, ErrorBoundary, auth font fix

- Skeleton loading system: .skeleton shimmer primitives in CSS (theme-
  aware via --border) plus a SkeletonRows component, adopted at the
  main-content loading states of Jobs, Candidates, Inbox, Interviews,
  Offers, Assessments, Tasks, Managers, Notifications. Small card-level
  loading states keep their EmptyState text.
- ErrorBoundary around the route outlet, keyed by pathname: a throw in
  one lazy screen shows a recoverable error state instead of blanking
  the whole app; navigating away resets it.
- auth.css stops hardcoding the Belleza stack twice; both sites use
  var(--font-display) so the display face has one definition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/29/head
Talha Ahmed 2026-08-25 21:58:34 +05:00
parent 967c1acc5c
commit 066346af80
14 changed files with 101 additions and 23 deletions

View File

@ -7,6 +7,7 @@ import AiDock from './AiDock'
import { ROUTE_BY_PATH } from './routes'
import { useBadges, useHotkeys, useNavOpen, useRouteMeta, useSidebarCollapsed } from './useShell'
import Icon from '../ui/icons'
import ErrorBoundary from '../components/ErrorBoundary'
import Spinner from '../components/Spinner'
export default function AppLayout() {
@ -49,9 +50,12 @@ export default function AppLayout() {
<div className="main-wrap">
<Topbar onOpenNav={() => setNavOpen((o) => !o)} searchRef={searchRef} />
<main className="content" id="main-content" ref={contentRef}>
<Suspense fallback={<div className="route-loading"><Spinner label="Loading" /></div>}>
<Outlet />
</Suspense>
{/* Keyed by pathname: navigating away from a crashed screen resets it. */}
<ErrorBoundary key={location.pathname}>
<Suspense fallback={<div className="route-loading"><Spinner label="Loading" /></div>}>
<Outlet />
</Suspense>
</ErrorBoundary>
</main>
</div>

View File

@ -0,0 +1,39 @@
/* ============================================================
ErrorBoundary a throw in any lazy screen used to blank the whole app.
Mounted around the route outlet in AppLayout, keyed by pathname so simply
navigating away resets it.
============================================================ */
import { Component } from 'react'
import { EmptyState } from '../ui/primitives'
export default class ErrorBoundary extends Component {
state = { error: null }
static getDerivedStateFromError(error) {
return { error }
}
componentDidCatch(error, info) {
console.error('Screen crashed:', error, info?.componentStack)
}
render() {
if (this.state.error) {
return (
<div className="page">
<EmptyState icon="alert" title="Something went wrong">
This screen hit an unexpected error. The rest of the app is fine
try again, or head back to the dashboard.
</EmptyState>
<div style={{ textAlign: 'center' }}>
<button className="btn btn-secondary" onClick={() => this.setState({ error: null })}>
Try again
</button>
</div>
</div>
)
}
return this.props.children
}
}

View File

@ -14,7 +14,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import DataTable from '../ui/DataTable'
import Modal from '../ui/Modal'
import PageHeader from '../ui/PageHeader'
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard, ProgressBar, ScoreChip } from '../ui/primitives'
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard, ProgressBar, ScoreChip, SkeletonRows } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { useAuth } from '../auth/AuthContext'
import { useFormState } from '../components/AuthLayout'
@ -212,7 +212,7 @@ export default function Assessments() {
<div className="card">
{listQuery.isPending && (
<div className="card-body">
<EmptyState icon="check-square" title="Loading…">Fetching assessments from the server.</EmptyState>
<SkeletonRows rows={6} />
</div>
)}
{listQuery.isError && (

View File

@ -16,7 +16,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import Modal from '../ui/Modal'
import { DataTableHead, Pagination, useDataTable } from '../ui/DataTable'
import PageHeader from '../ui/PageHeader'
import { Avatar, Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
import { Avatar, Badge, EmptyState, FieldError, Icon, SkeletonRows } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import CandidateProfile from './CandidateProfile'
import { useJobTitles } from './ScoredCandidateProfile'
@ -324,7 +324,7 @@ export default function Candidates() {
{candidatesQuery.isPending && (
<div className="card-body">
<EmptyState icon="users" title="Loading…">Fetching candidates from the server.</EmptyState>
<SkeletonRows rows={6} />
</div>
)}
{candidatesQuery.isError && (

View File

@ -12,7 +12,7 @@ import EmailBody, { looksLikeHtml } from '../ui/EmailBody'
import PageHeader from '../ui/PageHeader'
import { Tabs } from '../ui/Tabs'
import { Pagination, pageWindow } from '../ui/DataTable'
import { Avatar, Badge, EmptyState, Icon, ScoreChip } from '../ui/primitives'
import { Avatar, Badge, EmptyState, Icon, ScoreChip, SkeletonRows } from '../ui/primitives'
import { JobCard, PickRoleModal } from '../ui/SuggestedRoles'
import { useToast } from '../ui/Toast'
import { useAuth } from '../auth/AuthContext'
@ -780,7 +780,7 @@ export default function Inbox() {
</div>
<div>
{applicationsQuery.isPending && (
<EmptyState icon="inbox" title="Loading…">Fetching applications from the server.</EmptyState>
<SkeletonRows rows={6} />
)}
{applicationsQuery.isError && (
<EmptyState icon="inbox" title="Couldn't load applications">

View File

@ -27,7 +27,7 @@ import DataTable from '../ui/DataTable'
import Modal from '../ui/Modal'
import PageHeader from '../ui/PageHeader'
import { Tabs } from '../ui/Tabs'
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard, Stars } from '../ui/primitives'
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard, SkeletonRows, Stars } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { qk } from '../lib/queryKeys'
import { friendlyAuthError } from '../lib/errors'
@ -306,7 +306,7 @@ export default function Interviews() {
{listQuery.isPending && (
<div className="card-body">
<EmptyState icon="calendar" title="Loading…">Fetching interviews from the server.</EmptyState>
<SkeletonRows rows={6} />
</div>
)}
{listError && (

View File

@ -15,7 +15,7 @@ import AiFieldAssist from '../ui/AiFieldAssist'
import DataTable from '../ui/DataTable'
import Modal from '../ui/Modal'
import PageHeader from '../ui/PageHeader'
import { Badge, EmptyState, FieldError, Icon } from '../ui/primitives'
import { Badge, EmptyState, FieldError, Icon, SkeletonRows } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { useAuth } from '../auth/AuthContext'
import { useFormState } from '../components/AuthLayout'
@ -252,7 +252,7 @@ export default function Jobs() {
<div className="card">
{jobsQuery.isPending && (
<div className="card-body">
<EmptyState icon="briefcase" title="Loading…">Fetching requisitions from the server.</EmptyState>
<SkeletonRows rows={6} />
</div>
)}
{jobsQuery.isError && (

View File

@ -4,7 +4,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'
import Modal from '../ui/Modal'
import PageHeader from '../ui/PageHeader'
import { Avatar, Badge, EmptyState, Icon } from '../ui/primitives'
import { Avatar, Badge, EmptyState, Icon, SkeletonRows } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { useAuth } from '../auth/AuthContext'
import { qk } from '../lib/queryKeys'
@ -53,7 +53,7 @@ export default function Managers() {
/>
{managersQuery.isPending && (
<EmptyState icon="managers" title="Loading…">Fetching hiring managers.</EmptyState>
<div className="card"><div className="card-body"><SkeletonRows rows={4} /></div></div>
)}
{managersQuery.isError && (
<EmptyState icon="managers" title="Couldnt load hiring managers">

View File

@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import PageHeader from '../ui/PageHeader'
import { EmptyState, Icon } from '../ui/primitives'
import { EmptyState, Icon, SkeletonRows } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { qk } from '../lib/queryKeys'
import { friendlyAuthError } from '../lib/errors'
@ -68,7 +68,7 @@ export default function Notifications() {
<div className="card">
{query.isPending && (
<div className="card-body">
<EmptyState icon="bell" title="Loading…">Fetching notifications.</EmptyState>
<SkeletonRows rows={5} />
</div>
)}
{query.isError && (

View File

@ -25,7 +25,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import DataTable from '../ui/DataTable'
import Modal from '../ui/Modal'
import PageHeader from '../ui/PageHeader'
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard } from '../ui/primitives'
import { Avatar, Badge, EmptyState, FieldError, Icon, KpiCard, SkeletonRows } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { qk } from '../lib/queryKeys'
import { friendlyAuthError } from '../lib/errors'
@ -283,7 +283,7 @@ export default function Offers() {
{offersQuery.isPending && (
<div className="card-body">
<EmptyState icon="file" title="Loading…">Fetching offers from the server.</EmptyState>
<SkeletonRows rows={6} />
</div>
)}
{offersQuery.isError && (

View File

@ -18,7 +18,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import Modal from '../ui/Modal'
import PageHeader from '../ui/PageHeader'
import { Badge, EmptyState, FieldError, Icon, PRIORITY_CLASS } from '../ui/primitives'
import { Badge, EmptyState, FieldError, Icon, PRIORITY_CLASS, SkeletonRows } from '../ui/primitives'
import { useToast } from '../ui/Toast'
import { useAuth } from '../auth/AuthContext'
import { useFormState } from '../components/AuthLayout'
@ -185,7 +185,7 @@ export default function Tasks() {
<div className="card-body">
<div className="list-tight">
{tasksQuery.isPending ? (
<EmptyState icon="check-square" title="Loading…">Fetching tasks from the server.</EmptyState>
<SkeletonRows rows={5} />
) : tasksQuery.isError ? (
<EmptyState icon="alert" title="Couldnt load tasks">
{friendlyAuthError(tasksQuery.error, 'Request failed')}

View File

@ -50,7 +50,7 @@
}
.auth-aside-copy h1 {
font-family: 'Belleza', Georgia, serif;
font-family: var(--font-display);
font-size: clamp(32px, 4vw, 44px);
line-height: 1.15;
font-weight: 400;
@ -115,7 +115,7 @@
}
.auth-brand .brand-name {
font-family: 'Belleza', Georgia, serif;
font-family: var(--font-display);
font-weight: 400;
letter-spacing: 0.2px;
}

View File

@ -866,6 +866,24 @@ canvas { width: 100%; max-width: 100%; display: block; }
border-radius: 20px; background: var(--bg-sunken); color: var(--text-2); }
.tab.active .tab-count { background: var(--primary-soft); color: var(--primary); }
/* Skeleton loading (primitives.jsx SkeletonRows). The sweep uses --border,
which tracks the theme, so no per-theme rules are needed. */
.skeleton {
position: relative; overflow: hidden;
background: var(--bg-sunken); border-radius: var(--radius-sm);
}
.skeleton::after {
content: ''; position: absolute; inset: 0; transform: translateX(-100%);
background: linear-gradient(90deg, transparent, var(--border), transparent);
animation: skeletonSweep 1.4s infinite;
}
@keyframes skeletonSweep { to { transform: translateX(100%); } }
.skeleton-row { display: flex; align-items: center; gap: 12px; padding: 13px 0; }
.skeleton-row + .skeleton-row { border-top: 1px solid var(--border); }
.skeleton-avatar { width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0; }
.skeleton-line { display: block; height: 11px; }
.skeleton-line + .skeleton-line { margin-top: 7px; }
/* Keyboard users jump straight past the 20+ sidebar links. */
.skip-link { position: absolute; left: -9999px; z-index: 1000; }
.skip-link:focus {

View File

@ -76,6 +76,23 @@ export function ProgressBar({ pct, className }) {
)
}
/** Shimmer placeholder for a loading table/list — one row per record slot. */
export function SkeletonRows({ rows = 5 }) {
return (
<div role="status" aria-label="Loading">
{Array.from({ length: rows }, (_, i) => (
<div className="skeleton-row" aria-hidden="true" key={i}>
<span className="skeleton skeleton-avatar" />
<div className="flex-1">
<span className="skeleton skeleton-line" style={{ width: `${52 - (i % 3) * 9}%` }} />
<span className="skeleton skeleton-line" style={{ width: `${34 - (i % 3) * 6}%` }} />
</div>
</div>
))}
</div>
)
}
export function EmptyState({ icon = 'search', title = 'No results found', children }) {
return (
<div className="empty-state">