Skip to content
Merged
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
8 changes: 3 additions & 5 deletions frontend/src/components/timeline/timeline-scroll-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactNode, RefObject } from 'react'
import { ScrollBody } from '@/components/ui/scroll-body'

// 고정 헤더 + 내부 스크롤 본문. 바깥 셸(TabShell/ActivityShell의 fixed 레이아웃)이
// 세로 flex 컨테이너라는 전제로 동작한다.
Expand All @@ -17,12 +18,9 @@ export function TimelineScrollShell({
{header !== undefined ? (
<header className="shrink-0">{header}</header>
) : null}
<div
ref={scrollRef}
className="min-h-0 min-w-0 flex-1 overflow-y-auto pb-24 [scrollbar-gutter:stable] [-webkit-overflow-scrolling:touch]"
>
<ScrollBody ref={scrollRef} pad="tabbar">
{children}
</div>
</ScrollBody>
</>
)
}
73 changes: 73 additions & 0 deletions frontend/src/components/ui/scroll-body.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ScrollBody } from '@/components/ui/scroll-body'
import type { Meta, StoryObj } from '@storybook/react-vite'

const meta = {
title: 'UI/ScrollBody',
component: ScrollBody,
tags: ['autodocs'],
argTypes: {
pad: {
control: 'select',
options: ['tabbar', 'screen', 'none'],
},
},
// flex-1 본문이라, 높이가 정해진 세로 flex 컨테이너 안에서만 스크롤이 드러난다.
decorators: [
(Story) => (
<div className="flex h-96 w-72 flex-col overflow-hidden rounded-xl border border-border">
<Story />
</div>
),
],
} satisfies Meta<typeof ScrollBody>

export default meta

type Story = StoryObj<typeof meta>

const LINES = Array.from(
{ length: 24 },
(_, i) => `${i + 1}번째 줄 · 스크롤을 확인하기 위한 긴 본문 내용이에요.`,
)

function Filler() {
return (
<div className="space-y-3 px-4 py-3">
{LINES.map((line) => (
<p key={line} className="text-sm text-foreground">
{line}
</p>
))}
</div>
)
}

// 탭바가 겹치는 탭 화면. 하단 pb-24로 마지막 항목이 탭바에 가리지 않게 한다.
export const Tabbar: Story = {
args: { pad: 'tabbar' },
render: (args) => (
<ScrollBody {...args}>
<Filler />
</ScrollBody>
),
}

// 일반 전체 화면. 하단 pb-8 여백만 둔다.
export const Screen: Story = {
args: { pad: 'screen' },
render: (args) => (
<ScrollBody {...args}>
<Filler />
</ScrollBody>
),
}

// 하단 여백 없이 콘텐츠 끝까지. 하단에 별도 고정 바가 붙는 화면에 쓴다.
export const None: Story = {
args: { pad: 'none' },
render: (args) => (
<ScrollBody {...args}>
<Filler />
</ScrollBody>
),
}
31 changes: 31 additions & 0 deletions frontend/src/components/ui/scroll-body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ComponentProps } from 'react'
import { cn } from '@/lib/utils'

export type ScrollBodyPad = 'tabbar' | 'screen' | 'none'

// pad별 하단 여백: 탭바가 겹치는 탭 화면(tabbar)·일반 화면 하단 여백(screen)·여백 없음(none).
const PAD_CLASS: Record<ScrollBodyPad, string> = {
tabbar: 'pb-24',
screen: 'pb-8',
none: '',
}

// 세로 flex 셸(TabShell/ActivityShell fixed 레이아웃) 안에서 본문만 세로로 스크롤시키는 컨테이너.
// iOS 관성 스크롤과 스크롤바 자리(gutter) 예약을 함께 켜, 스크롤바가 나타날 때 본문 폭이 밀리지 않게 한다.
// 화면별 간격(space-y-* 등)은 컴포넌트에 굳히지 않고 className으로 넘긴다.
export function ScrollBody({
pad = 'tabbar',
className,
...props
}: ComponentProps<'div'> & { pad?: ScrollBodyPad }) {
return (
<div
className={cn(
'min-h-0 min-w-0 flex-1 overflow-y-auto [scrollbar-gutter:stable] [-webkit-overflow-scrolling:touch]',
PAD_CLASS[pad],
className,
)}
{...props}
/>
)
}
5 changes: 3 additions & 2 deletions frontend/src/stackflow/activities/event-detail-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ActivityComponentType } from '@stackflow/react'
import { eventQuery, personQuery } from '@/apis/queries'
import { ActivityShell } from '@/stackflow/components/activity-shell'
import { MonogramAvatar } from '@/components/ui/monogram-avatar'
import { ScrollBody } from '@/components/ui/scroll-body'
import { Badge } from '@/components/ui/badge'
import { StatusMessage } from '@/components/ui/status-message'
import { EventPhotoGallery } from '@/components/events/event-photo-gallery'
Expand Down Expand Up @@ -77,7 +78,7 @@ export const EventDetailActivity: ActivityComponentType<'EventDetail'> = ({
</button>
</header>

<div className="min-h-0 flex-1 overflow-y-auto px-5 pb-8 [-webkit-overflow-scrolling:touch]">
<ScrollBody pad="screen" className="px-5">
<div className="flex items-start gap-2">
<h2
data-amp-mask
Expand Down Expand Up @@ -159,7 +160,7 @@ export const EventDetailActivity: ActivityComponentType<'EventDetail'> = ({
) : null}

<EventPhotoGallery photoUrls={event.photoUrls} />
</div>
</ScrollBody>
</ActivityShell>
)
}
5 changes: 3 additions & 2 deletions frontend/src/stackflow/activities/person-edit-activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PersonEditForm } from '@/components/person/person-edit-form'
import { personToFormValues } from '@/components/person/person-form'
import { usePersonDelete } from '@/components/person/use-person-delete'
import { Button } from '@/components/ui/button'
import { ScrollBody } from '@/components/ui/scroll-body'
import { StatusMessage } from '@/components/ui/status-message'
import { personMutation } from '@/apis/mutations'
import { chipQuery, homeQuery, personQuery } from '@/apis/queries'
Expand Down Expand Up @@ -87,7 +88,7 @@ export const PersonEditActivity: ActivityComponentType<'PersonEdit'> = ({
className="px-5"
/>

<div className="min-h-0 min-w-0 flex-1 overflow-y-auto [scrollbar-gutter:stable] [-webkit-overflow-scrolling:touch]">
<ScrollBody pad="none">
<div className="px-5 pt-4">
<PersonEditForm
key={person.id}
Expand All @@ -105,7 +106,7 @@ export const PersonEditActivity: ActivityComponentType<'PersonEdit'> = ({
</p>
) : null}
</div>
</div>
</ScrollBody>

<div className="shrink-0 border-t border-border/70 bg-background px-5 pt-3 pb-[max(0.25rem,env(safe-area-inset-bottom))]">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { eventQuery, personQuery } from '@/apis/queries'
import { DeletePersonConfirm } from '@/components/person/delete-person-confirm'
import { usePersonDelete } from '@/components/person/use-person-delete'
import { MonogramAvatar } from '@/components/ui/monogram-avatar'
import { ScrollBody } from '@/components/ui/scroll-body'
import { ListGroup } from '@/components/ui/list-group'
import { ListGroupItem } from '@/components/ui/list-group-item'
import { ListGroupLabel } from '@/components/ui/list-group-label'
Expand Down Expand Up @@ -79,7 +80,7 @@ export function PersonProfileView({

return (
<>
<div className="min-h-0 min-w-0 flex-1 space-y-6 overflow-y-auto pb-24 [scrollbar-gutter:stable] [-webkit-overflow-scrolling:touch]">
<ScrollBody pad="tabbar" className="space-y-6">
<section>
<ListGroup>
<ListGroupItem withDivider={false} className="py-4">
Expand Down Expand Up @@ -257,7 +258,7 @@ export function PersonProfileView({
</ListGroupItem>
</ListGroup>
</section>
</div>
</ScrollBody>

<DeletePersonConfirm
open={del.open}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ActivityComponentType } from '@stackflow/react'
import { useState } from 'react'
import { HomePeriodToggle } from '@/components/home/period-toggle'
import { SettingsPageHeader } from '@/components/settings/settings-page-header'
import { ScrollBody } from '@/components/ui/scroll-body'
import { getDefaultHomePeriod, setDefaultHomePeriod } from '@/lib/home-period'
import type { HomePeriod } from '@/lib/home-period'
import { ActivityShell } from '@/stackflow/components/activity-shell'
Expand All @@ -24,15 +25,15 @@ export const HomeSettingsActivity: ActivityComponentType<
return (
<ActivityShell layout="fixed">
<SettingsPageHeader title="홈 설정" onBack={() => pop()} />
<div className="min-h-0 flex-1 overflow-y-auto pb-8">
<ScrollBody pad="screen">
<p className="mb-1 text-body font-extrabold text-foreground">
기본으로 보여줄 기간
</p>
<p className="mb-4 text-xs font-medium text-muted-foreground">
관계도에 처음 보이는 사람 범위를 정해요
</p>
<HomePeriodToggle value={period} onChange={handleChange} />
</div>
</ScrollBody>
</ActivityShell>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
personQuery,
timelineQuery,
} from '@/apis/queries'
import { ScrollBody } from '@/components/ui/scroll-body'
import { TagTypePanel } from '@/stackflow/activities/settings/tag-type-panel'

const TAG_GROUPS = [
Expand Down Expand Up @@ -40,7 +41,7 @@ export function TagSettingsPage() {
)

return (
<div className="min-h-0 flex-1 space-y-7 overflow-y-auto pb-8">
<ScrollBody pad="screen" className="space-y-7">
{TAG_GROUPS.map((group) => (
<TagTypePanel
key={group.type}
Expand All @@ -61,6 +62,6 @@ export function TagSettingsPage() {
}}
/>
))}
</div>
</ScrollBody>
)
}
5 changes: 3 additions & 2 deletions frontend/src/stackflow/tabs/people-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TabShell } from '@/stackflow/components/tab-shell'
import { MonogramAvatar } from '@/components/ui/monogram-avatar'
import { Button } from '@/components/ui/button'
import { PageTitle } from '@/components/ui/page-title'
import { ScrollBody } from '@/components/ui/scroll-body'
import {
EmptyState,
EmptyStateAction,
Expand Down Expand Up @@ -99,7 +100,7 @@ export function PeopleTab() {
</div>
</header>

<div className="min-h-0 min-w-0 flex-1 space-y-6 overflow-y-auto pb-24 [scrollbar-gutter:stable] [-webkit-overflow-scrolling:touch]">
<ScrollBody pad="tabbar" className="space-y-6">
<section>
<ListGroup>
<ListGroupItem>
Expand Down Expand Up @@ -168,7 +169,7 @@ export function PeopleTab() {
onToggleFavorite={(id) => favoriteMutation.mutate(id)}
/>
)}
</div>
</ScrollBody>
</TabShell>
)
}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/stackflow/tabs/settings-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useTheme } from '@/components/theme-provider'
import { Button } from '@/components/ui/button'
import { ConfirmPopup } from '@/components/ui/confirm-popup'
import { PageTitle } from '@/components/ui/page-title'
import { ScrollBody } from '@/components/ui/scroll-body'
import { ListGroup } from '@/components/ui/list-group'
import { ListGroupItem } from '@/components/ui/list-group-item'
import { ListGroupLabel } from '@/components/ui/list-group-label'
Expand Down Expand Up @@ -46,7 +47,7 @@ export function SettingsTab() {
<PageTitle>설정</PageTitle>
</header>

<div className="min-h-0 min-w-0 flex-1 space-y-6 overflow-y-auto pb-24 [scrollbar-gutter:stable] [-webkit-overflow-scrolling:touch]">
<ScrollBody pad="tabbar" className="space-y-6">
<ListGroup>
<SettingsNavigationItem
label="홈 설정"
Expand Down Expand Up @@ -126,7 +127,7 @@ export function SettingsTab() {
</ListGroupItem>
</ListGroup>
</section>
</div>
</ScrollBody>

<ConfirmPopup
open={confirmOpen}
Expand Down
Loading