Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/src/components/ui/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cn } from '@/lib/utils'

// push 전환 동안 pending 화면이 빈 배경(라이트 테마 = 흰 화면)으로 슬라이드되어
// 보이지 않도록, 실제 레이아웃의 자리를 흉내 내는 shimmer 플레이스홀더 조각.
// 화면별 스켈레톤을 감싸는 쪽에서 role="status" aria-label="불러오는 중"을 달고,
// 조각 자체는 보조기기에 노출하지 않는다.
export function Skeleton({ className }: { className?: string }) {
return (
<div
aria-hidden
className={cn('relative overflow-hidden rounded-md bg-muted', className)}
>
<div className="absolute inset-0 animate-skeleton-shimmer bg-gradient-to-r from-transparent via-foreground/8 to-transparent" />
</div>
)
}
32 changes: 29 additions & 3 deletions frontend/src/stackflow/activities/event-detail-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useFlow } from '@stackflow/react'
import type { ActivityComponentType } from '@stackflow/react'
import { ActivityShell } from '@/stackflow/components/activity-shell'
import { MonogramAvatar } from '@/components/ui/monogram-avatar'
import { Skeleton } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
import { fetchEvent } from '@/lib/api/events'
import { fetchPersons } from '@/lib/api/persons'
Expand Down Expand Up @@ -47,9 +48,34 @@ export const EventDetailActivity: ActivityComponentType<'EventDetail'> = ({
if (eventQuery.isPending) {
return (
<ActivityShell>
<p className="py-20 text-center text-sm text-muted-foreground">
불러오는 중…
</p>
<header className="grid shrink-0 grid-cols-3 items-center py-1">
<button
type="button"
onClick={() => pop()}
className="text-left text-lg font-extrabold text-muted-foreground"
aria-label="뒤로 가기"
>
</button>
<h1 className="text-center text-base font-extrabold">몽글 상세</h1>
<span aria-hidden className="text-right" />
</header>
<div role="status" aria-label="불러오는 중" className="mt-6">
<div className="flex items-start justify-between gap-2">
<Skeleton className="h-7 w-3/5" />
<Skeleton className="h-7 w-16 rounded-full" />
</div>
<Skeleton className="mt-3 h-4 w-32" />
<div className="mt-4 flex gap-2">
<Skeleton className="h-8 w-24 rounded-full" />
<Skeleton className="h-8 w-20 rounded-full" />
</div>
<div className="mt-5 space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-11/12" />
<Skeleton className="h-4 w-2/3" />
</div>
</div>
</ActivityShell>
)
}
Expand Down
36 changes: 34 additions & 2 deletions frontend/src/stackflow/activities/person-edit-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ActivityShell } from '@/stackflow/components/activity-shell'
import { FormPageHeader } from '@/components/layout/form-page-header'
import { PersonForm, personToFormValues } from '@/components/person/person-form'
import { ConfirmPopup } from '@/components/ui/confirm-popup'
import { Skeleton } from '@/components/ui/skeleton'
import { fetchChips } from '@/lib/api/chips'
import { deletePerson, fetchPerson, updatePerson } from '@/lib/api/persons'
import { formatPersonName } from '@/lib/format'
Expand Down Expand Up @@ -68,16 +69,47 @@ export const PersonEditActivity: ActivityComponentType<'PersonEdit'> = ({
)?.requestSubmit()
}

if (!Number.isFinite(id) || personQuery.isPending) {
if (!Number.isFinite(id)) {
return (
<ActivityShell layout="fixed">
<p className="py-20 text-center text-sm text-muted-foreground">
{personQuery.isPending ? '불러오는 중…' : '잘못된 경로예요.'}
잘못된 경로예요.
</p>
</ActivityShell>
)
}

if (personQuery.isPending) {
return (
<ActivityShell layout="fixed" className="px-0">
<FormPageHeader
onBack={() => pop()}
title="프로필 수정"
className="px-5"
/>
<div
role="status"
aria-label="불러오는 중"
className="mt-6 space-y-6 px-5"
>
<div className="space-y-2">
<Skeleton className="h-6 w-40" />
<Skeleton className="h-4 w-52" />
</div>
<div className="flex justify-center py-2">
<Skeleton className="size-24 rounded-full" />
</div>
{[0, 1, 2].map((row) => (
<div key={row} className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-11 w-full rounded-lg" />
</div>
))}
</div>
</ActivityShell>
)
}

if (!person || personQuery.isError) {
return (
<ActivityShell layout="fixed">
Expand Down
53 changes: 51 additions & 2 deletions frontend/src/stackflow/activities/person/person-profile-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ListGroupItem,
ListGroupLabel,
} from '@/components/ui/list-group'
import { Skeleton } from '@/components/ui/skeleton'
import { coloredTagStyle, tagChipClass } from '@/components/ui/tag-chip'
import { fetchPersonTimeline } from '@/lib/api/events'
import { deletePerson, fetchPerson } from '@/lib/api/persons'
Expand Down Expand Up @@ -59,14 +60,62 @@ export function PersonProfileView({
},
})

if (!Number.isFinite(id) || personQuery.isPending) {
if (!Number.isFinite(id)) {
return (
<p className="py-20 text-center text-sm text-muted-foreground">
{personQuery.isPending ? '불러오는 중…' : '잘못된 경로예요.'}
잘못된 경로예요.
</p>
)
}

if (personQuery.isPending) {
return (
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<PersonPageHeader active="profile" onSelectView={onSelectView} />
<div
role="status"
aria-label="불러오는 중"
className="min-h-0 min-w-0 flex-1 space-y-6 overflow-y-auto pb-24"
>
<ListGroup>
<ListGroupItem withDivider={false} className="py-4">
<div className="flex items-start gap-4">
<Skeleton className="size-20 rounded-full" />
<div className="min-w-0 flex-1">
<Skeleton className="h-7 w-32" />
<Skeleton className="mt-2 h-4 w-16" />
<div className="mt-2 flex gap-1.5">
<Skeleton className="h-7 w-16 rounded-full" />
<Skeleton className="h-7 w-20 rounded-full" />
</div>
</div>
</div>
</ListGroupItem>
</ListGroup>
<ListGroup>
<ListGroupItem withDivider={false}>
<div className="grid grid-cols-2 gap-x-4 gap-y-4">
{[0, 1, 2, 3].map((cell) => (
<div key={cell}>
<Skeleton className="h-3.5 w-16" />
<Skeleton className="mt-1.5 h-5 w-20" />
</div>
))}
</div>
</ListGroupItem>
</ListGroup>
<ListGroup>
{[0, 1, 2].map((row) => (
<ListGroupItem key={row} withDivider={row < 2} className="py-3.5">
<Skeleton className="h-5 w-28" />
</ListGroupItem>
))}
</ListGroup>
</div>
</div>
)
}

if (!person) {
return (
<p className="py-20 text-center text-sm text-muted-foreground">
Expand Down
34 changes: 31 additions & 3 deletions frontend/src/stackflow/activities/person/person-timeline-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { PersonPageHeader } from '@/components/person/person-page-header'
import { MonogramAvatar } from '@/components/ui/monogram-avatar'
import { Button } from '@/components/ui/button'
import { ListGroup, ListGroupItem } from '@/components/ui/list-group'
import { Skeleton } from '@/components/ui/skeleton'
import { fetchChips } from '@/lib/api/chips'
import { fetchPersonTimeline } from '@/lib/api/events'
import { fetchPerson } from '@/lib/api/persons'
Expand Down Expand Up @@ -133,9 +134,36 @@ export function PersonTimelineView({
<PersonPageHeader active="timeline" onSelectView={onSelectView} />
}
>
<p className="py-12 text-center text-sm text-muted-foreground">
불러오는 중…
</p>
<div role="status" aria-label="불러오는 중">
<div className="mb-4">
<Skeleton className="h-7 w-32" />
<Skeleton className="mt-2 h-3.5 w-40" />
</div>
<ListGroup className="mb-4">
<ListGroupItem
withDivider={false}
className="flex items-center gap-3 py-3"
>
<Skeleton className="size-10 rounded-full" />
<div className="flex gap-6">
{[0, 1].map((stat) => (
<div key={stat}>
<Skeleton className="h-5 w-14" />
<Skeleton className="mt-1.5 h-3 w-16" />
</div>
))}
</div>
</ListGroupItem>
</ListGroup>
<ListGroup>
{[0, 1, 2].map((row) => (
<ListGroupItem key={row} withDivider={row < 2} className="py-3">
<Skeleton className="h-4 w-3/5" />
<Skeleton className="mt-2 h-3 w-24" />
</ListGroupItem>
))}
</ListGroup>
</div>
</TimelineScrollShell>
)
}
Expand Down
27 changes: 24 additions & 3 deletions frontend/src/stackflow/activities/record-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MonogramAvatar } from '@/components/ui/monogram-avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Skeleton } from '@/components/ui/skeleton'
import { Textarea } from '@/components/ui/textarea'
import { tagChipBaseClass } from '@/components/ui/tag-chip'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
Expand Down Expand Up @@ -326,9 +327,29 @@ export const RecordActivity: ActivityComponentType<'Record'> = ({ params }) => {
return (
<ActivityShell layout="fixed" className="px-0" presentation="modal">
{recordHeader}
<p className="px-5 py-20 text-center text-sm text-muted-foreground">
불러오는 중…
</p>
<div
role="status"
aria-label="불러오는 중"
className="mt-6 space-y-6 px-5"
>
<div className="space-y-2">
<Skeleton className="h-6 w-36" />
<Skeleton className="h-4 w-48" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-11 w-full rounded-lg" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-28 w-full rounded-lg" />
</div>
<div className="flex gap-2">
<Skeleton className="h-9 w-20 rounded-full" />
<Skeleton className="h-9 w-24 rounded-full" />
<Skeleton className="h-9 w-16 rounded-full" />
</div>
</div>
</ActivityShell>
)
}
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@
--font-sans: var(--font-sans);
}

@theme {
--animate-skeleton-shimmer: skeleton-shimmer 1.8s ease-in-out infinite;

@keyframes skeleton-shimmer {
from {
transform: translateX(-100%);
}
to {
transform: translateX(100%);
}
}
}

@layer base {
* {
@apply border-border outline-ring/50;
Expand Down