diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index a7f54e76..f443aab4 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -25,7 +25,7 @@ import { OpenClawPanel } from '@/components/ai/openclaw-panel' import { HostGrid, type HostInfo, type HostService } from '@/components/infra/host-grid' import { AppLayout } from '@/components/layout' import { PageTabs, type TabConfig } from '@/components/layout/page-tabs' -import { LobsterLoading } from '@/components/shared/lobster-loading' +import { PulseSkeleton, CardSkeleton } from '@/components/shared/pulse-skeleton' import { ServiceTopology } from '@/components/topology' import { BarChart3, Flame, Telescope, FlaskConical, Activity, GitBranch } from 'lucide-react' import { DispositionMini } from '@/components/shared/disposition-mini' @@ -56,7 +56,7 @@ function AlertsAndApprovalsTab() { }).finally(() => setLoading(false)) }, []) - if (loading) return + if (loading) return return (
@@ -172,7 +172,7 @@ function DispositionTab() { .finally(() => setLoading(false)) }, []) - if (loading) return + if (loading) return const s = data?.summary if (!s || s.total === 0) return ( @@ -730,7 +730,7 @@ export default function Home({ params }: { params: { locale: string } }) {
{isIncidentsLoading ? ( - + ) : incidentsError ? (
{incidentsError}
) : (incidents?.length ?? 0) === 0 ? ( diff --git a/apps/web/src/components/shared/pulse-skeleton.tsx b/apps/web/src/components/shared/pulse-skeleton.tsx new file mode 100644 index 00000000..a0cbdd85 --- /dev/null +++ b/apps/web/src/components/shared/pulse-skeleton.tsx @@ -0,0 +1,50 @@ +'use client' + +/** + * PulseSkeleton — 脈動骨架屏 (取代「載入中...」文字) + * Sprint 5R G1: Gemini 建議 — AI 呼吸感知 + * @created 2026-04-09 Claude Opus 4.6 Asia/Taipei + */ + +export function PulseSkeleton({ width = '100%', height = 16, borderRadius = 4, count = 1 }: { + width?: string | number + height?: number + borderRadius?: number + count?: number +}) { + return ( + <> + +
+ {Array.from({ length: count }).map((_, i) => ( +
+ ))} +
+ + ) +} + +/** 卡片級骨架屏 — 模擬 .card 結構 */ +export function CardSkeleton({ lines = 3 }: { lines?: number }) { + return ( +
+ +
+ +
+
+ ) +}