Stack
요소들을 수직 또는 수평으로 배치하는 레이아웃 컴포넌트입니다.
개요
Stack은 요소들을 일정한 간격으로 수직(VStack) 또는 수평(HStack)으로 배치하는 컴포넌트입니다. Cals 예약 관리 시스템에서 예약 정보 리스트, 폼 레이아웃, 버튼 그룹 등에 사용됩니다.
주요 특징:
- 수직/수평 배치 선택
- 간격(gap) 자동 관리
- 정렬 옵션 (start, center, end, stretch)
- 간단한 API
설치
npx @vortex/cli add stack기본 사용법
import { VStack, HStack } from "@vortex/ui";
export default function ReservationInfo() {
return (
<VStack spacing={4}>
<div>고객명: 홍길동</div>
<div>서비스: 헤어 커트</div>
<div>시간: 2024-01-15 14:00</div>
</VStack>
);
}Variants
VStack (수직 스택)
{
/* 기본 수직 스택 */
}
<VStack spacing={4}>
<div className="p-4 bg-gray-100 rounded">항목 1</div>
<div className="p-4 bg-gray-100 rounded">항목 2</div>
<div className="p-4 bg-gray-100 rounded">항목 3</div>
</VStack>;
{
/* 왼쪽 정렬 */
}
<VStack spacing={4} align="start">
<div className="p-4 bg-gray-100 rounded">왼쪽</div>
<div className="p-4 bg-gray-100 rounded w-32">정렬</div>
</VStack>;
{
/* 중앙 정렬 */
}
<VStack spacing={4} align="center">
<div className="p-4 bg-gray-100 rounded">중앙</div>
<div className="p-4 bg-gray-100 rounded w-32">정렬</div>
</VStack>;
{
/* 오른쪽 정렬 */
}
<VStack spacing={4} align="end">
<div className="p-4 bg-gray-100 rounded">오른쪽</div>
<div className="p-4 bg-gray-100 rounded w-32">정렬</div>
</VStack>;
{
/* 전체 너비 */
}
<VStack spacing={4} align="stretch">
<div className="p-4 bg-gray-100 rounded">전체</div>
<div className="p-4 bg-gray-100 rounded">너비</div>
</VStack>;HStack (수평 스택)
{
/* 기본 수평 스택 */
}
<HStack spacing={4}>
<div className="p-4 bg-gray-100 rounded">항목 1</div>
<div className="p-4 bg-gray-100 rounded">항목 2</div>
<div className="p-4 bg-gray-100 rounded">항목 3</div>
</HStack>;
{
/* 위쪽 정렬 */
}
<HStack spacing={4} align="start">
<div className="p-4 bg-gray-100 rounded h-16">위쪽</div>
<div className="p-4 bg-gray-100 rounded h-24">정렬</div>
</HStack>;
{
/* 중앙 정렬 */
}
<HStack spacing={4} align="center">
<div className="p-4 bg-gray-100 rounded h-16">중앙</div>
<div className="p-4 bg-gray-100 rounded h-24">정렬</div>
</HStack>;
{
/* 아래쪽 정렬 */
}
<HStack spacing={4} align="end">
<div className="p-4 bg-gray-100 rounded h-16">아래쪽</div>
<div className="p-4 bg-gray-100 rounded h-24">정렬</div>
</HStack>;
{
/* 전체 높이 */
}
<HStack spacing={4} align="stretch">
<div className="p-4 bg-gray-100 rounded">전체</div>
<div className="p-4 bg-gray-100 rounded">높이</div>
</HStack>;간격(Spacing) 설정
{
/* 간격 없음 */
}
<VStack spacing={0}>
<div className="p-4 bg-gray-100">간격 0</div>
<div className="p-4 bg-gray-100">간격 0</div>
</VStack>;
{
/* 작은 간격 */
}
<VStack spacing={2}>
<div className="p-4 bg-gray-100 rounded">간격 2</div>
<div className="p-4 bg-gray-100 rounded">간격 2</div>
</VStack>;
{
/* 중간 간격 (기본값) */
}
<VStack spacing={4}>
<div className="p-4 bg-gray-100 rounded">간격 4</div>
<div className="p-4 bg-gray-100 rounded">간격 4</div>
</VStack>;
{
/* 큰 간격 */
}
<VStack spacing={8}>
<div className="p-4 bg-gray-100 rounded">간격 8</div>
<div className="p-4 bg-gray-100 rounded">간격 8</div>
</VStack>;분산 정렬 (Justify)
{
/* 시작점 정렬 */
}
<HStack spacing={4} justify="start">
<div className="p-4 bg-gray-100 rounded">시작</div>
<div className="p-4 bg-gray-100 rounded">정렬</div>
</HStack>;
{
/* 중앙 정렬 */
}
<HStack spacing={4} justify="center">
<div className="p-4 bg-gray-100 rounded">중앙</div>
<div className="p-4 bg-gray-100 rounded">정렬</div>
</HStack>;
{
/* 끝점 정렬 */
}
<HStack spacing={4} justify="end">
<div className="p-4 bg-gray-100 rounded">끝점</div>
<div className="p-4 bg-gray-100 rounded">정렬</div>
</HStack>;
{
/* 균등 분산 */
}
<HStack spacing={4} justify="between">
<div className="p-4 bg-gray-100 rounded">균등</div>
<div className="p-4 bg-gray-100 rounded">분산</div>
<div className="p-4 bg-gray-100 rounded">정렬</div>
</HStack>;
{
/* 전체 균등 분산 */
}
<HStack spacing={4} justify="around">
<div className="p-4 bg-gray-100 rounded">전체</div>
<div className="p-4 bg-gray-100 rounded">균등</div>
<div className="p-4 bg-gray-100 rounded">분산</div>
</HStack>;Cals 브랜딩
예약 정보 스택 (상태별)
import { VStack } from "@vortex/ui";
{
/* Confirmed - 예약 확정 */
}
<VStack
spacing={3}
className="p-6 border-l-4 border-[#03a9f4] bg-[#03a9f4]/5 rounded-r-lg"
>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">고객</span>
<span className="font-medium">홍길동</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">서비스</span>
<span className="font-medium">헤어 커트</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">시간</span>
<span className="font-medium text-[#03a9f4]">2024-01-15 14:00</span>
</div>
</VStack>;
{
/* Pending - 예약 대기 */
}
<VStack
spacing={3}
className="p-6 border-l-4 border-[#ff9800] bg-[#ff9800]/5 rounded-r-lg"
>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">고객</span>
<span className="font-medium">김고객</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">서비스</span>
<span className="font-medium">헤어 펌</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">시간</span>
<span className="font-medium text-[#ff9800]">2024-01-15 15:00</span>
</div>
</VStack>;
{
/* Completed - 서비스 완료 */
}
<VStack
spacing={3}
className="p-6 border-l-4 border-[#9c27b0] bg-[#9c27b0]/5 rounded-r-lg"
>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">고객</span>
<span className="font-medium">박고객</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">서비스</span>
<span className="font-medium">헤어 염색</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">완료 시간</span>
<span className="font-medium text-[#9c27b0]">2024-01-14 16:00</span>
</div>
</VStack>;Primary Pink 액션 버튼 그룹
import { HStack } from '@vortex/ui'
<HStack spacing={4} justify="end">
<button className="px-6 py-2 rounded-md border border-gray-300 hover:bg-gray-100">
취소
</button>
<button className="px-6 py-2 rounded-md bg-[#e91e63] text-white hover:bg-[#c2185b]">
예약 확정
</button>
</HStack>
<HStack spacing={4} justify="center">
<button className="px-8 py-3 rounded-md border-2 border-[#e91e63] text-[#e91e63] hover:bg-[#e91e63]/10">
예약 변경
</button>
<button className="px-8 py-3 rounded-md bg-gradient-to-r from-[#e91e63] to-[#9c27b0] text-white hover:opacity-90">
예약하기
</button>
</HStack>통계 카드 수평 스택
import { HStack } from "@vortex/ui";
<HStack spacing={6}>
<div className="flex-1 p-6 rounded-lg bg-gradient-to-br from-[#e91e63] to-[#c2185b] text-white">
<p className="text-sm opacity-90 mb-1">오늘 예약</p>
<p className="text-4xl font-bold">12</p>
</div>
<div className="flex-1 p-6 rounded-lg bg-gradient-to-br from-[#03a9f4] to-[#0288d1] text-white">
<p className="text-sm opacity-90 mb-1">대기 중</p>
<p className="text-4xl font-bold">3</p>
</div>
<div className="flex-1 p-6 rounded-lg bg-gradient-to-br from-[#9c27b0] to-[#7b1fa2] text-white">
<p className="text-sm opacity-90 mb-1">이번 주</p>
<p className="text-4xl font-bold">48</p>
</div>
</HStack>;브랜드별 비교
| 특성 | Foundation | iCignal | Cals |
|---|---|---|---|
| 목적 | 범용 스택 | 제품 정보, 버튼 그룹 | 예약 정보, 폼 레이아웃 |
| 디자인 | 중립적 | 다크 테마 | 밝고 친근한 |
| Primary | - | Blue #0066cc | Pink #e91e63 |
| 기본 간격 | 4 (1rem) | 4 (1rem) | 4 (1rem) |
| 주요 사용 | 범용 레이아웃 | 제품 상세, 네비게이션 | 예약 정보, 고객 정보 |
| 특화 기능 | - | 다크 모드 | 상태별 시각적 구분 |
Props API
VStack / HStack
| Prop | Type | Default | Description |
|---|---|---|---|
| spacing | number | 4 | 항목 간 간격 (Tailwind spacing 단위) |
| align | ’start’ | ‘center’ | ‘end’ | ‘stretch' | 'stretch’ | 교차축 정렬 |
| justify | ’start’ | ‘center’ | ‘end’ | ‘between’ | ‘around' | 'start’ | 주축 정렬 |
| className | string | - | 추가 CSS 클래스 |
| children | ReactNode | - | 스택 항목들 |
Stack (제네릭)
| Prop | Type | Default | Description |
|---|---|---|---|
| direction | ’vertical’ | ‘horizontal' | 'vertical’ | 스택 방향 |
| spacing | number | 4 | 항목 간 간격 |
| align | ’start’ | ‘center’ | ‘end’ | ‘stretch' | 'stretch’ | 교차축 정렬 |
| justify | ’start’ | ‘center’ | ‘end’ | ‘between’ | ‘around' | 'start’ | 주축 정렬 |
| className | string | - | 추가 CSS 클래스 |
| children | ReactNode | - | 스택 항목들 |
접근성
- 시맨틱 HTML: 리스트 항목은
<ul>,<ol>,<li>사용 - 키보드 네비게이션: 인터랙티브 요소는 적절한
tabIndex설정 - ARIA 속성: 복잡한 스택은
role및 관련 속성 사용 - 반응형: 모바일에서는 수평 스택이 수직으로 변경될 수 있도록 고려
{
/* 접근성 개선 예제 */
}
<VStack spacing={4} as="ul" role="list" aria-label="예약 목록">
<li className="p-4 border rounded">
<span>예약 1</span>
</li>
<li className="p-4 border rounded">
<span>예약 2</span>
</li>
<li className="p-4 border rounded">
<span>예약 3</span>
</li>
</VStack>;예제
1. 예약 상세 정보 스택
import { VStack, HStack } from "@vortex/ui";
import { Calendar, Clock, User, Phone, Mail, MapPin } from "lucide-react";
export default function ReservationDetailStack() {
return (
<VStack
spacing={6}
className="p-6 border-l-4 border-[#03a9f4] bg-[#03a9f4]/5 rounded-r-lg"
>
{/* 헤더 */}
<div>
<h2 className="text-2xl font-bold text-[#03a9f4] mb-1">예약 확정</h2>
<p className="text-sm text-muted-foreground">
예약 번호: #RSV-20240115-001
</p>
</div>
{/* 서비스 정보 */}
<VStack spacing={3}>
<h3 className="text-lg font-semibold">서비스 정보</h3>
<HStack spacing={3} align="center">
<Calendar className="w-5 h-5 text-muted-foreground" />
<VStack spacing={0}>
<span className="text-sm text-muted-foreground">날짜</span>
<span className="font-medium">2024년 1월 15일</span>
</VStack>
</HStack>
<HStack spacing={3} align="center">
<Clock className="w-5 h-5 text-muted-foreground" />
<VStack spacing={0}>
<span className="text-sm text-muted-foreground">시간</span>
<span className="font-medium">오후 2:00 - 3:00</span>
</VStack>
</HStack>
<HStack spacing={3} align="center">
<MapPin className="w-5 h-5 text-muted-foreground" />
<VStack spacing={0}>
<span className="text-sm text-muted-foreground">장소</span>
<span className="font-medium">강남점</span>
</VStack>
</HStack>
</VStack>
{/* 고객 정보 */}
<VStack spacing={3}>
<h3 className="text-lg font-semibold">고객 정보</h3>
<HStack spacing={3} align="center">
<User className="w-5 h-5 text-muted-foreground" />
<VStack spacing={0}>
<span className="text-sm text-muted-foreground">이름</span>
<span className="font-medium">홍길동</span>
</VStack>
</HStack>
<HStack spacing={3} align="center">
<Phone className="w-5 h-5 text-muted-foreground" />
<VStack spacing={0}>
<span className="text-sm text-muted-foreground">연락처</span>
<span className="font-medium">010-1234-5678</span>
</VStack>
</HStack>
<HStack spacing={3} align="center">
<Mail className="w-5 h-5 text-muted-foreground" />
<VStack spacing={0}>
<span className="text-sm text-muted-foreground">이메일</span>
<span className="font-medium">hong@example.com</span>
</VStack>
</HStack>
</VStack>
{/* 결제 정보 */}
<VStack spacing={2} className="p-4 bg-white rounded-lg">
<HStack justify="between">
<span className="text-muted-foreground">서비스 금액</span>
<span className="font-medium">30,000원</span>
</HStack>
<HStack justify="between">
<span className="text-muted-foreground">할인</span>
<span className="font-medium text-[#f44336]">-3,000원</span>
</HStack>
<div className="border-t pt-2">
<HStack justify="between">
<span className="font-semibold">총 결제 금액</span>
<span className="text-xl font-bold text-[#e91e63]">27,000원</span>
</HStack>
</div>
</VStack>
{/* 액션 버튼 */}
<HStack spacing={4} justify="end">
<button className="px-6 py-2 rounded-md border border-gray-300 hover:bg-gray-100">
예약 변경
</button>
<button className="px-6 py-2 rounded-md bg-[#f44336] text-white hover:bg-[#d32f2f]">
예약 취소
</button>
</HStack>
</VStack>
);
}2. 폼 레이아웃 스택
import { VStack, HStack } from "@vortex/ui";
export default function ReservationFormStack() {
return (
<VStack spacing={6} className="max-w-2xl mx-auto p-6">
<div>
<h1 className="text-3xl font-bold text-[#e91e63] mb-2">
새 예약 만들기
</h1>
<p className="text-muted-foreground">예약 정보를 입력해주세요.</p>
</div>
{/* 고객 정보 */}
<VStack spacing={4} className="p-6 bg-white rounded-lg border">
<h2 className="text-xl font-semibold">고객 정보</h2>
<VStack spacing={2}>
<label className="text-sm font-medium">이름 *</label>
<input
type="text"
className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none"
placeholder="홍길동"
/>
</VStack>
<HStack spacing={4}>
<VStack spacing={2} className="flex-1">
<label className="text-sm font-medium">연락처 *</label>
<input
type="tel"
className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none"
placeholder="010-1234-5678"
/>
</VStack>
<VStack spacing={2} className="flex-1">
<label className="text-sm font-medium">이메일</label>
<input
type="email"
className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none"
placeholder="hong@example.com"
/>
</VStack>
</HStack>
</VStack>
{/* 예약 정보 */}
<VStack spacing={4} className="p-6 bg-white rounded-lg border">
<h2 className="text-xl font-semibold">예약 정보</h2>
<VStack spacing={2}>
<label className="text-sm font-medium">서비스 선택 *</label>
<select className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none">
<option>헤어 커트</option>
<option>헤어 펌</option>
<option>헤어 염색</option>
</select>
</VStack>
<HStack spacing={4}>
<VStack spacing={2} className="flex-1">
<label className="text-sm font-medium">날짜 *</label>
<input
type="date"
className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none"
/>
</VStack>
<VStack spacing={2} className="flex-1">
<label className="text-sm font-medium">시간 *</label>
<select className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none">
<option>10:00</option>
<option>11:00</option>
<option>14:00</option>
<option>15:00</option>
</select>
</VStack>
</HStack>
<VStack spacing={2}>
<label className="text-sm font-medium">요청사항</label>
<textarea
rows={4}
className="w-full px-4 py-2 border rounded-md focus:border-[#e91e63] focus:ring-2 focus:ring-[#e91e63]/20 outline-none resize-none"
placeholder="요청사항을 입력해주세요"
/>
</VStack>
</VStack>
{/* 제출 버튼 */}
<HStack spacing={4} justify="end">
<button className="px-8 py-3 rounded-md border border-gray-300 hover:bg-gray-100">
취소
</button>
<button className="px-8 py-3 rounded-md bg-gradient-to-r from-[#e91e63] to-[#9c27b0] text-white hover:opacity-90">
예약하기
</button>
</HStack>
</VStack>
);
}3. 예약 목록 스택
import { VStack, HStack } from "@vortex/ui";
import { Calendar, Clock, User, MoreVertical } from "lucide-react";
const reservations = [
{
id: 1,
status: "confirmed",
customer: "홍길동",
service: "헤어 커트",
time: "14:00",
date: "2024-01-15",
},
{
id: 2,
status: "pending",
customer: "김고객",
service: "헤어 펌",
time: "15:00",
date: "2024-01-15",
},
{
id: 3,
status: "confirmed",
customer: "이고객",
service: "헤어 염색",
time: "16:00",
date: "2024-01-15",
},
];
const statusConfig = {
pending: { color: "#ff9800", bg: "bg-[#ff9800]/10", label: "대기 중" },
confirmed: { color: "#03a9f4", bg: "bg-[#03a9f4]/10", label: "확정됨" },
};
export default function ReservationListStack() {
return (
<VStack spacing={4}>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">오늘의 예약</h2>
<button className="px-4 py-2 rounded-md bg-[#e91e63] text-white hover:bg-[#c2185b]">
새 예약
</button>
</div>
<VStack spacing={3}>
{reservations.map((reservation) => {
const config = statusConfig[reservation.status];
return (
<div
key={reservation.id}
className={`p-6 rounded-lg border-l-4 ${config.bg} cursor-pointer hover:shadow-md transition-shadow`}
style={{ borderLeftColor: config.color }}
>
<HStack justify="between" align="start">
<VStack spacing={3} className="flex-1">
<HStack spacing={3} align="center">
<div
className="px-3 py-1 rounded-full text-xs font-medium text-white"
style={{ backgroundColor: config.color }}
>
{config.label}
</div>
<span className="text-lg font-semibold">
{reservation.service}
</span>
</HStack>
<HStack spacing={6}>
<HStack spacing={2} align="center">
<User className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{reservation.customer}</span>
</HStack>
<HStack spacing={2} align="center">
<Calendar className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{reservation.date}</span>
</HStack>
<HStack spacing={2} align="center">
<Clock className="w-4 h-4 text-muted-foreground" />
<span
className="text-sm font-medium"
style={{ color: config.color }}
>
{reservation.time}
</span>
</HStack>
</HStack>
</VStack>
<button className="p-2 hover:bg-gray-100 rounded-md">
<MoreVertical className="w-5 h-5 text-muted-foreground" />
</button>
</HStack>
</div>
);
})}
</VStack>
</VStack>
);
}관련 컴포넌트
Last updated on