Popover
개요
Popover는 트리거 요소 근처에 추가 정보를 표시하는 컴포넌트입니다. Cals에서는 예약 상세 정보 미리보기, 시간대 정보, 서비스 설명 등을 표시하는 데 사용됩니다.
언제 사용하는가
- 예약 상세 정보 미리보기
- 시간대별 예약 정보
- 서비스 설명 및 가격 정보
- 고객 정보 요약
- 빠른 액션 메뉴
언제 사용하지 말아야 하는가
- 중요한 정보 → Dialog 사용
- 짧은 설명 → Tooltip 사용
- 영구적인 정보 → Alert 또는 Card 사용
- 복잡한 폼 → Dialog 사용
설치
npx @vortex/cli add popover기본 사용법
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
export default function PopoverDemo() {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">예약 정보</Button>
</PopoverTrigger>
<PopoverContent>
<div className="space-y-2">
<h4 className="font-semibold">예약 상세</h4>
<p className="text-sm text-muted-foreground">2024년 1월 15일 14:00</p>
</div>
</PopoverContent>
</Popover>
);
}Variants
Popover는 단일 스타일이지만, 용도에 따라 내용을 다르게 구성합니다.
Information Popover
정보를 표시하는 Popover입니다.
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<Info className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
<h4 className="font-semibold">예약 안내</h4>
<p className="text-sm text-muted-foreground">
예약은 최소 24시간 전에 해주시기 바랍니다. 당일 예약은 전화로
문의해주세요.
</p>
</div>
</PopoverContent>
</Popover>Action Popover
액션 버튼을 포함하는 Popover입니다.
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">빠른 액션</Button>
</PopoverTrigger>
<PopoverContent className="w-48 p-2">
<div className="space-y-1">
<Button variant="ghost" size="sm" className="w-full justify-start">
예약 수정
</Button>
<Button variant="ghost" size="sm" className="w-full justify-start">
예약 취소
</Button>
<Button variant="ghost" size="sm" className="w-full justify-start">
상세 보기
</Button>
</div>
</PopoverContent>
</Popover>Form Popover
간단한 폼을 포함하는 Popover입니다.
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">메모 추가</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-4">
<div className="space-y-2">
<h4 className="font-semibold">예약 메모</h4>
<p className="text-sm text-muted-foreground">
이 예약에 대한 메모를 추가하세요.
</p>
</div>
<div className="space-y-2">
<Textarea placeholder="메모를 입력하세요" />
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm">
취소
</Button>
<Button size="sm">저장</Button>
</div>
</div>
</PopoverContent>
</Popover>Cals 브랜딩
브랜드 컬러
Cals의 Primary Pink를 활용한 브랜드 Popover 스타일입니다.
<Popover>
<PopoverTrigger asChild>
<Button className="bg-cals-primary hover:bg-cals-primary/90">
Cals 정보
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 border-cals-primary">
<div className="space-y-2">
<h4 className="font-semibold text-cals-primary">Cals 예약 시스템</h4>
<p className="text-sm text-muted-foreground">
간편하고 효율적인 예약 관리 시스템
</p>
</div>
</PopoverContent>
</Popover>예약 상태 컬러
예약 상태별 Popover 스타일을 제공합니다.
// Available - 예약 가능
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="border-green-500 text-green-700">
예약 가능
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 border-green-500">
<div className="space-y-2">
<h4 className="font-semibold text-green-900">예약 가능</h4>
<p className="text-sm text-muted-foreground">
해당 시간대에 예약하실 수 있습니다.
</p>
<Button size="sm" className="bg-green-500 hover:bg-green-600 w-full">
예약하기
</Button>
</div>
</PopoverContent>
</Popover>
// Pending - 승인 대기
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="border-orange-500 text-orange-700">
승인 대기
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 border-orange-500">
<div className="space-y-2">
<h4 className="font-semibold text-orange-900">승인 대기</h4>
<p className="text-sm text-muted-foreground">
예약 요청이 접수되었습니다. 승인까지 최대 24시간 소요됩니다.
</p>
<div className="flex gap-2">
<Button size="sm" variant="outline" className="flex-1">
거절
</Button>
<Button size="sm" className="bg-orange-500 hover:bg-orange-600 flex-1">
승인
</Button>
</div>
</div>
</PopoverContent>
</Popover>
// Confirmed - 예약 확정
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="border-blue-500 text-blue-700">
예약 확정
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 border-blue-500">
<div className="space-y-3">
<h4 className="font-semibold text-blue-900">예약 확정</h4>
<div className="space-y-1">
<p className="text-sm font-medium">날짜</p>
<p className="text-sm text-muted-foreground">2024년 1월 15일 14:00</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium">고객명</p>
<p className="text-sm text-muted-foreground">홍길동</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium">서비스</p>
<p className="text-sm text-muted-foreground">컨설팅 (60분)</p>
</div>
<Button size="sm" variant="outline" className="w-full">
상세 보기
</Button>
</div>
</PopoverContent>
</Popover>
// Cancelled - 예약 취소
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="border-red-500 text-red-700">
예약 취소
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 border-red-500">
<div className="space-y-2">
<h4 className="font-semibold text-red-900">예약 취소</h4>
<p className="text-sm text-muted-foreground">
이 예약은 취소되었습니다. 취소 수수료 5,000원이 부과됩니다.
</p>
<div className="pt-2 border-t">
<p className="text-xs text-muted-foreground">
취소일: 2024년 1월 10일 10:30
</p>
</div>
</div>
</PopoverContent>
</Popover>브랜드 비교표
| 속성 | Foundation | iCignal | Cals |
|---|---|---|---|
| Primary Color | Neutral Gray | Corporate Blue #0066cc | Primary Pink #e91e63 |
| Use Case | 범용 정보 | 기업 데이터 | 예약 상세 정보 |
| Trigger | Button/Icon | Icon (통일) | Status Badge/Button |
| Content | Simple | Detailed (차트) | Reservation Details |
| Width | Default (256px) | Wide (400px) | Medium (320px) |
| Actions | Optional | Required | Quick Actions |
Props API
Popover
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | - | Popover 열림 상태 (controlled) |
| onOpenChange | (open: boolean) => void | - | 열림 상태 변경 핸들러 |
| defaultOpen | boolean | false | 초기 열림 상태 (uncontrolled) |
| modal | boolean | false | 모달 동작 활성화 |
PopoverTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | 자식 요소를 트리거로 사용 |
PopoverContent
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "start" | "center" | "end" | "center" | 정렬 위치 |
| side | "top" | "right" | "bottom" | "left" | "bottom" | 표시 위치 |
| sideOffset | number | 4 | 트리거와의 간격 (px) |
| className | string | - | 커스텀 CSS 클래스 |
접근성
- Role:
role="dialog"자동 적용 - ARIA:
aria-labelledby,aria-describedby연결 - Focus Management: 열릴 때 내부로 포커스 이동
- ESC Key: ESC 키로 닫기
- Outside Click: 외부 클릭으로 닫기
- Keyboard Navigation: Tab으로 내부 요소 이동
- Return Focus: 닫힐 때 트리거로 포커스 복귀
예제
예약 상세 정보
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Calendar, Clock, User, Phone } from "lucide-react";
export default function ReservationDetailPopover() {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="border-blue-500 text-blue-700">
예약 #1234
</Button>
</PopoverTrigger>
<PopoverContent className="w-96 border-blue-500">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-blue-900">예약 상세</h4>
<Badge className="bg-blue-500">확정</Badge>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">날짜 및 시간</p>
<p className="text-sm text-muted-foreground">
2024년 1월 15일 14:00 - 15:00
</p>
</div>
</div>
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">고객 정보</p>
<p className="text-sm text-muted-foreground">홍길동</p>
</div>
</div>
<div className="flex items-center gap-2">
<Phone className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">연락처</p>
<p className="text-sm text-muted-foreground">010-1234-5678</p>
</div>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">서비스</p>
<p className="text-sm text-muted-foreground">컨설팅 (60분)</p>
</div>
</div>
</div>
<div className="pt-2 border-t">
<Button size="sm" variant="outline" className="w-full">
상세 보기
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
}시간대별 예약 정보
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
export default function TimeSlotPopover() {
return (
<div className="space-x-2">
{/* Available */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="border-green-500 text-green-700"
>
14:00
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 border-green-500">
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-green-900">14:00 - 15:00</h4>
<Badge className="bg-green-500">예약 가능</Badge>
</div>
<p className="text-sm text-muted-foreground">
이 시간대는 예약 가능합니다.
</p>
<div className="space-y-1">
<p className="text-sm font-medium">서비스</p>
<p className="text-sm text-muted-foreground">컨설팅 (60분)</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium">가격</p>
<p className="text-sm text-muted-foreground">50,000원</p>
</div>
<Button
size="sm"
className="bg-green-500 hover:bg-green-600 w-full"
>
예약하기
</Button>
</div>
</PopoverContent>
</Popover>
{/* Confirmed */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="border-blue-500 text-blue-700"
>
15:00
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 border-blue-500">
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-blue-900">15:00 - 16:00</h4>
<Badge className="bg-blue-500">예약됨</Badge>
</div>
<p className="text-sm text-muted-foreground">
이 시간대는 이미 예약되었습니다.
</p>
<div className="space-y-1">
<p className="text-sm font-medium">고객명</p>
<p className="text-sm text-muted-foreground">홍길동</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium">서비스</p>
<p className="text-sm text-muted-foreground">컨설팅 (60분)</p>
</div>
</div>
</PopoverContent>
</Popover>
</div>
);
}빠른 액션 메뉴
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { MoreVertical, Edit, Trash2, Eye, Copy } from "lucide-react";
export default function QuickActionPopover() {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-48 p-2" align="end">
<div className="space-y-1">
<Button variant="ghost" size="sm" className="w-full justify-start">
<Eye className="h-4 w-4 mr-2" />
상세 보기
</Button>
<Button variant="ghost" size="sm" className="w-full justify-start">
<Edit className="h-4 w-4 mr-2" />
수정
</Button>
<Button variant="ghost" size="sm" className="w-full justify-start">
<Copy className="h-4 w-4 mr-2" />
복제
</Button>
<div className="my-1 border-t" />
<Button
variant="ghost"
size="sm"
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50"
>
<Trash2 className="h-4 w-4 mr-2" />
삭제
</Button>
</div>
</PopoverContent>
</Popover>
);
}관련 컴포넌트
- Tooltip - 짧은 설명
- Dialog - 복잡한 정보 또는 폼
- Dropdown Menu - 액션 메뉴
- Card - 영구적인 정보
Last updated on