Files
hapi/spinner.md
T
weishu 7f2a36117c 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
2025-12-24 17:23:39 +08:00

5.0 KiB
Raw Blame History

Loading 元素精简优化计划

一、当前 Loading 元素汇总

位置 文件 当前样式 展示时机
同步横幅 SyncingBanner.tsx 顶部固定横幅 + 旋转符号 isSyncing && isOnline
权限按钮 PermissionFooter.tsx:66-73 SVG SpinnerIcon loading 状态
认证加载 App.tsx:132-146 纯文本 "Loading…" / "Authorizing…" isAuthSourceLoading / isAuthLoading
会话加载 router.tsx:100-106 纯文本 "Loading session…" !session
消息加载 HappyThread.tsx:235-258 纯文本 "Loading..." + 按钮文本 isLoadingMessages / isLoadingMoreMessages
Git 状态 files.tsx:313-318 纯文本 "Loading Git status..." / "Loading files..." gitLoading / searchResults.isLoading
文件内容 file.tsx:231-232 纯文本 "Loading file..." diffQuery.isLoading || fileQuery.isLoading
登录按钮 LoginPrompt.tsx:77 按钮文本 "Signing in..." isLoading
创建会话 NewSession.tsx:119-122, 216 "Loading machines..." / "Creating..." isPending
问卷提交 AskUserQuestionFooter.tsx:385 按钮文本 "Submitting…" loading

二、问题

  1. 样式不统一 - 纯文本 vs SpinnerIcon vs 旋转符号
  2. SpinnerIcon 无法复用 - 定义在 PermissionFooter 内部
  3. 省略号不一致 - "..." vs "…"

三、精简方案

核心原则:Spinner 做基础元件,LoadingState 负责语义化加载

创建唯一的 Spinner 组件(可访问性就绪)

// web/src/components/Spinner.tsx
export function Spinner({
    size = 'md',
    className,
    label = 'Loading',
}: {
    size?: 'sm' | 'md' | 'lg'
    className?: string
    label?: string
}) {
    const sizeClasses = { sm: 'h-4 w-4', md: 'h-5 w-5', lg: 'h-6 w-6' }
    return (
        <svg
            className={cn(sizeClasses[size], 'animate-spin text-[var(--app-hint)]', className)}
            viewBox="0 0 24 24"
            fill="none"
            role="status"
            aria-label={label}
        >
            <circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2.5" opacity="0.25" />
            <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" opacity="0.75" />
        </svg>
    )
}

新增 LoadingStateSpinner + 可选文案)

// web/src/components/LoadingState.tsx
export function LoadingState({
    label = 'Loading…',
    className,
    spinnerSize = 'md',
}: {
    label?: string
    className?: string
    spinnerSize?: 'sm' | 'md' | 'lg'
}) {
    return (
        <div className={cn('inline-flex items-center gap-2 text-[var(--app-hint)]', className)} role="status" aria-live="polite">
            <Spinner size={spinnerSize} label={label} />
            <span>{label}</span>
        </div>
    )
}

SyncingBanner 处理

方案:不改,避免打扰用户

  • 保留顶部横幅与 useSyncingState 逻辑
  • 若需要统一视觉,仅替换内部图标为新 Spinner

四、实施步骤

Step 1: 创建 Spinner 组件(可访问性)

  • 创建 web/src/components/Spinner.tsx,包含 role="status"aria-label

Step 2: 新增 LoadingState 组件

  • 创建 web/src/components/LoadingState.tsx
  • 统一使用 Loading…(省略号用

Step 3: 替换加载场景(保留语义)

  • App.tsx - 认证相关使用 <LoadingState label="Authorizing…" />
  • router.tsx - 会话加载使用 <LoadingState label="Loading session…" />
  • HappyThread.tsx - 消息加载用 <LoadingState label="Loading…" />
  • files.tsx - Git/文件列表加载用 <LoadingState label="Loading files…" />
  • file.tsx - 文件内容加载用 <LoadingState label="Loading file…" />
  • PermissionFooter.tsx - 使用新 Spinner,删除内部 SpinnerIcon

Step 4: 按钮加载保持可读标签

  • LoginPrompt.tsx / NewSession.tsx / AskUserQuestionFooter.tsx - 保留文字,旁边加 Spinner size="sm"
  • 按钮添加 aria-busy,避免替换成纯图标

Step 5: 列表/内容区优先 Skeleton 或占位

  • HappyThread.tsx - 消息列表加载改为 skeleton/placeholder
  • files.tsx - 文件列表加载改为 skeleton/placeholder
  • file.tsx - 文件内容加载改为 skeleton/placeholder

Step 6: SyncingBanner

  • 保留 SyncingBanner.tsx
  • 可选:内部图标替换为新 Spinner,不改交互与布局

五、关键文件

新建:

  • web/src/components/Spinner.tsx
  • web/src/components/LoadingState.tsx

修改:

  • web/src/App.tsx
  • web/src/router.tsx
  • web/src/components/AssistantChat/HappyThread.tsx
  • web/src/routes/sessions/files.tsx
  • web/src/routes/sessions/file.tsx
  • web/src/components/ToolCard/PermissionFooter.tsx
  • web/src/components/LoginPrompt.tsx
  • web/src/components/NewSession.tsx
  • web/src/components/ToolCard/AskUserQuestionFooter.tsx
  • web/src/components/SyncingBanner.tsx