본문으로 건너뛰기
Amineslab UI

Getting started

Responsive UI

화면 폭과 입력 수단을 나누고, 같은 작업을 PC·모바일에 맞는 표면으로 표현합니다.

코어 UI와 제품 규칙의 경계

Amineslab UI는 화면 크기에 따른 표면 전환, 스크롤, 터치·키보드 조작, 포커스와 입력 상태를 담당합니다. 음력 변환·윤달 보정, 가문·가족 관계, 고인 표시처럼 특정 제품의 의미나 데이터 규칙이 필요한 기능은 소비 앱에서 구성합니다.

날짜 선택은 DatePicker, 추가 선택은 Checkbox, 변경 확인은 Confirm으로 조합할 수 있습니다. 변환 함수만 주입하더라도 업무 결정 과정을 고정하는 전용 필드는 코어 API에 포함하지 않습니다. 라우팅·업로드·알림 데이터도 앱이 콜백으로 연결합니다.

공간과 입력 수단을 나눕니다

useIsMobile은 768px 미만의 공간을 판단합니다. useHasCoarsePointer는 주 포인터가 손가락인지, useHasFinePointer는 마우스·트랙패드처럼 정밀한 포인터가 하나라도 있는지를 판단합니다. 넓은 태블릿과 좁은 데스크톱 창을 같은 조건으로 취급하지 않습니다. 패널 닫기·메뉴·Select 항목은 넓은 터치 화면에서도 최소 44px 영역을 확보합니다.

현재 환경: 좁은 화면 false · 주 포인터가 터치 true · 정밀 포인터 있음 false. 마우스를 연결한 태블릿에서는 coarse와 fine이 모두 true일 수 있습니다.

const mobile = useIsMobile() // 화면 배치
const coarse = useHasCoarsePointer() // 터치 영역·항상 보이는 힌트
const fine = useHasFinePointer() // 정밀 드래그 기능 제공 여부

// 단순 스타일은 CSS로 처리합니다.
<Button className="pointer-coarse:min-h-11">작업</Button>

훅은 미디어 쿼리 변경을 구독하고 정리합니다. SSR 초기값은 mobile=false, coarse=true, fine=false이며 hydration 후 실제 환경을 반영합니다. 입력 값이나 선택 상태를 이 초기값으로 초기화하지 마세요.

같은 작업, 화면에 맞는 표면

컴포넌트PC모바일
Dialog크기별 중앙 모달 + ScrollArea전체 화면 Drawer + native 본문 스크롤
Popover트리거 옆 Popover내용 높이 Drawer, 중첩 시 Nested Drawer
Select키보드 선택 목록큰 선택 항목이 있는 Drawer
DatePicker달력 Popover + HH:mm 직접 입력달력 Drawer + 시·분 휠, 완료 확정
DateRangePicker1·2개월 Popover한 달 Drawer, 시작·종료 시각 입력
TimeInput시·분 select Popover시·분 2열 선택 목록
ActionMenu아이콘 트리거 + 옆으로 펼치는 하위 메뉴같은 시트에서 하위 단계 이동·뒤로 가기
Combobox검색 가능한 Popover검색 고정 + 목록 스크롤 Drawer
PageHeader제목 옆 주요 작업·추가 메뉴주요 작업 FAB·액션 시트
FormSurface문서 안의 폼·푸터고정 제출 tray와 본문 하단 공간 확보
ActionMenu방향키·문자 검색 메뉴같은 작업 목록의 액션 시트
Confirm작은 중앙 확인 카드작은 중앙 확인 카드
Toaster컴팩트 알림safe area를 고려한 스와이프 알림

Popover는 모바일에서 자동으로 BottomSheet로 전환합니다. 메뉴에는 ActionMenu를 사용하세요. 터치에서도 눌러야 하는 도움말을 Tooltip에만 넣지 않습니다.

선택과 확정을 구분합니다

날짜만 고르는 DatePicker는 선택 즉시 반영하고 닫습니다. 모바일 날짜+시간은 달력과 휠을 여러 번 조정하므로 편집값을 내부에 보관하고 완료에서 확정합니다. X·아래로 끌기·바깥 영역으로 닫으면 원래 값을 유지합니다. DateRangePicker와 TimeInput은 선택 즉시 반영하는 계약입니다. 같은 Bottomsheet라고 모두 같은 확정 정책을 갖지는 않습니다.

<DatePicker value={value} onValueChange={setValue} showTime />
<DateRangePicker value={range} onValueChange={setRange} numberOfMonths={2} />
<TimeInput value={time} onValueChange={setTime} minTime="09:00" maxTime="18:00" step={15} />

날짜 문자열은 YYYY-MM-DD, 날짜·시간은 YYYY-MM-DDTHH:mm, 시각은 HH:mm입니다. 이 값은 시간대 없는 로컬 입력입니다. 서버의 UTC 저장값으로 바꾸는 일과 시작·종료의 업무 규칙 검증은 소비 앱에서 수행합니다. 시간 휠의 반복 항목은 Tab 순서에 중복으로 들어가지 않으며, 휠 조작은 부모 시트의 닫기 드래그와 분리됩니다.

글자 크기와 움직임 설정

FontSizeProvider는 14·16·18·20px(기본 16px)을 지원합니다. useFontSize가 반환하는 fontSize·setFontSize·labels로 설정 화면을 구성합니다. storageKey로 앱별 저장 공간을 나누며 기본 키는 amineslab:font-size입니다. 루트의 --font-size-base를 조정하므로 rem 기반 글자·아이콘·간격이 함께 확대됩니다. provider를 제거하면 이전 루트 설정을 복원합니다.

<FontSizeProvider defaultFontSize={16} storageKey="my-app:font-size">
  <App />
</FontSizeProvider>

const { fontSize, setFontSize, labels } = useFontSize()
<Button onClick={() => setFontSize(18)}>{labels[18]}</Button>

usePrefersReducedMotion은 OS의 움직임 줄이기를 구독합니다. fadeIn·fadeInUp·scaleIn과 staggerContainer는 Motion 호환 recipe이며 사용하는 애니메이션 라이브러리에 전달합니다. reduced motion이면 이동·크기 애니메이션을 생략하세요. 서버 초기값은 true입니다.

앱과 연결하는 경계

AppLinkProvider에 앱의 링크 컴포넌트를 한 번 주입하면 to 문자열을 받는 작업들이 클라이언트 라우팅을 사용합니다. 서버 컴포넌트는 함수나 clone할 요소 대신 목적지 문자열을 전달하므로 RSC 경계에서도 안전합니다. provider 밖에서는 native anchor로 동작합니다.

// 앱의 client providers
import Link from 'next/link'
import { AppLinkProvider, FontSizeProvider, TooltipProvider } from '@amineslab/ui'
import { ThemeProvider } from '@amineslab/ui/theme/next'

<ThemeProvider>
  <FontSizeProvider>
    <AppLinkProvider component={Link}>
      <TooltipProvider>{children}</TooltipProvider>
    </AppLinkProvider>
  </FontSizeProvider>
</ThemeProvider>

공통 UI는 업로드 서버·권한 카탈로그·라우터·알림 구독 서비스를 고정하지 않습니다. 파일의 local/remote 모델, 업로드·삭제 콜백, 알림 fetch/read/push adapter로 연결합니다. 같은 인터랙션을 다른 앱에서도 재현하면서 각 앱의 데이터 계약은 유지할 수 있습니다. React Hook Form과 Sonner 연결은 각각 명시적 optional subpath에서 가져옵니다.

import { withMutationToast } from '@amineslab/ui/toast/sonner'

const callbacks = withMutationToast({
  success: '저장했습니다.',
  error: '저장하지 못했습니다.',
  errorMessage: (error, fallback) => resolveAppError(error) ?? fallback,
  onSuccess: refreshData,
})

닫힘·포커스·상태의 경계

ActionMenu의 onSelect는 PC 포커스 복원 또는 모바일 Drawer 닫힘 완료 뒤에 실행됩니다. 그 안에서 편집 모달·Confirm을 열면 두 focus trap과 scroll lock이 겹치지 않습니다. 링크는 실제 a 요소로 바로 이동하므로 모바일 새 탭이 지연 콜백의 팝업 차단에 걸리지 않습니다.

<ActionMenu groups={[[
  { label: '편집', onSelect: () => setEditing(true) },
  { label: '문서', href: '/docs', target: '_blank' },
]]} />

// 직접 Drawer를 조합할 때
const { queueAfterClose, drawerProps } = useDrawerCloseAction(setOpen)
<Drawer open={open} {...drawerProps}>
  <DrawerContent>
    <DrawerPanelHeader title="작업" />
    <DrawerPanelBody padding="sm">
      <DrawerClose asChild>
        <Button onClick={() => queueAfterClose(() => setEditing(true))}>편집</Button>
      </DrawerClose>
    </DrawerPanelBody>
  </DrawerContent>
</Drawer>

닫힘 중 다시 열거나 컴포넌트가 사라지면 예약한 작업은 취소됩니다. animation duration을 setTimeout으로 복제하지 않습니다. 폼의 값·선택 상태는 전환되는 Dialog/Drawer 바깥에서 소유하고 controlled props로 전달하면 표면이 교체돼도 유지됩니다.

본문과 여백

header/footer는 스크롤 영역 밖에 둡니다. 텍스트 본문은 md(16px), 자체 여백이 있는 선택 항목은 sm(8px), 직접 배치하는 본문은 none 또는 raw를 사용합니다. raw에서는 자식 하나가 min-h-0·flex-1·overflow-y-auto로 스크롤을 소유하도록 구성합니다. 전체 화면의 safe area는 패널이 담당합니다.

사용 기준

권장하는 사용
  • 작업 상태와 데이터는 표면 바깥에서 소유하고 컴포넌트가 레이아웃·키보드·터치 동작을 담당하게 구성합니다.
피해야 할 사용
  • 겉모양만으로 사용을 결정하지 마세요.
  • 의미와 키보드 동작, 모바일에서의 흐름을 함께 확인하세요.

확인 기준

  • 공개 import 경로와 타입 선언에 맞춰 사용했는지 확인합니다.
  • 라이트·다크 모드에서 정보와 포커스를 구분할 수 있어야 합니다.
  • 390px 화면, 200% 확대, 키보드 탐색에서도 작업을 마칠 수 있어야 합니다.