Carousel
Layout
여러 항목을 좌우로 넘겨 보는 캐러셀.
Usage
같은 크기의 카드나 이미지를 한 영역에서 좌우로 넘겨 탐색할 때 사용한다.
import
import
import { Carousel } from '@mildang/design-system/Carousel';예제를 복사해 쓸 때 필요한 준비
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
API Reference
Carousel Props
Prop
Type
Default
ReactNode
지정 안 함
(cssText: string, containerSelector: string) => string
지정 안 함
boolean | CarouselAutoplayOptions
지정 안 함
number
지정 안 함
number[]
지정 안 함
(api: EmblaCarouselType) => void
지정 안 함
Partial<OptionsType>
지정 안 함
SystemStyleObject
지정 안 함
CreatePluginType<LoosePluginType, {}>[]
지정 안 함
Partial<Record<"sm" | "md" | "lg" | "xl", { size?: string; gap?: string; maxWidth?: string; }>>
지정 안 함
string
지정 안 함
string
지정 안 함
string
지정 안 함
number[]
지정 안 함
탐색과 사용자 제어
같은 크기의 카드나 이미지를 한 영역에서 좌우로 탐색할 때 사용한다. Dots나 이전·다음 버튼을 함께 제공해 현재 위치와 이동 방법을 분명히 한다.\n\nloop가 필요한 경우에도 사용자가 직접 멈추고 이동할 수 있는 controls를 유지한다. 자동 재생을 추가할 때는 포인터·포커스 일시정지와 사용자 상호작용 후 정지 옵션을 함께 고려한다.
기본 사용
슬라이드 세 장과 현재 위치를 알려 주는 Dots 를 함께 둡니다. 다음 장이 살짝 걸쳐 보이게 해 넘길 수 있음을 드러냅니다.
import { Box } from '@mildang/styled-system/jsx';
import { css } from '@mildang/styled-system/css';
import { Carousel } from '@mildang/design-system/Carousel';
const CarouselBasicExample = () => {
return (
<Box p="24px" w="100%">
<Box
p="16px"
border="1px solid"
borderColor="neutral.border.low"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: '0 1px 2px rgba(16, 24, 40, 0.06)',
})}
>
<Carousel slideGap="12px">
<Carousel.Viewport panda={{ borderRadius: '14px' }}>
<Carousel.Container>
{Array.from({ length: 5 }).map((_, index) => (
<Carousel.Slide key={index}>
<div
className={css({
h: '160px',
borderRadius: '12px',
border: '1px solid',
borderColor: 'neutral.border.low',
backgroundColor: index % 2 === 0 ? 'neutral.surface.high' : 'neutral.surface.highest',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
fontWeight: 700,
})}
>
{index + 1}
</div>
</Carousel.Slide>
))}
</Carousel.Container>
</Carousel.Viewport>
<Box mt="12px" display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" gap="8px">
<Carousel.PrevButton
className={css({
px: '12px',
py: '8px',
borderRadius: '8px',
backgroundColor: 'neutral.surface.high',
border: '1px solid',
borderColor: 'neutral.border.low',
_disabled: { opacity: 0.4 },
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: '12px',
py: '8px',
borderRadius: '8px',
backgroundColor: 'neutral.surface.high',
border: '1px solid',
borderColor: 'neutral.border.low',
_disabled: { opacity: 0.4 },
})}
>
Next
</Carousel.NextButton>
</Box>
<Carousel.Dots />
</Box>
</Carousel>
</Box>
</Box>
);
};
export default CarouselBasicExample;
수동 컨트롤과 loop
이전·다음 버튼으로 이동하고 마지막 슬라이드에서 처음으로 순환한다.
import { Box } from '@mildang/styled-system/jsx';
import { css } from '@mildang/styled-system/css';
import { Carousel } from '@mildang/design-system/Carousel';
const CarouselFabNavButtonExample = () => {
return (
<Box p="24px" w="100%">
<Box
p="16px"
border="1px solid"
borderColor="neutral.border.low"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: '0 1px 2px rgba(16, 24, 40, 0.06)',
})}
>
<Carousel slideGap="12px">
<Carousel.Viewport panda={{ borderRadius: '14px' }}>
<Carousel.Container>
{Array.from({ length: 5 }).map((_, index) => (
<Carousel.Slide key={index}>
<div
className={css({
h: '160px',
borderRadius: '12px',
border: '1px solid',
borderColor: 'neutral.border.low',
backgroundColor: index % 2 === 0 ? 'neutral.surface.high' : 'neutral.surface.highest',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
fontWeight: 700,
})}
>
{index + 1}
</div>
</Carousel.Slide>
))}
</Carousel.Container>
</Carousel.Viewport>
<Carousel.FabNavButton position="prev" />
<Carousel.FabNavButton position="next" />
<Box mt="12px" display="flex" justifyContent="center">
<Carousel.Dots />
</Box>
</Carousel>
</Box>
</Box>
);
};
export default CarouselFabNavButtonExample;
예제
Default
기본 캐러셀 (스냅 + 버튼 + 닷츠)
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var DefaultStoryExample = () => {
const slides = createNumberSlides(8);
return <ExampleFrame title="Default" description="기본 캐러셀 (스냅 + 버튼 + 닷츠)">
<Carousel2 aria-roledescription="carousel" aria-label="Default carousel" slideGap="12px">
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselDefault.example.tsx
function CarouselDefaultExample() {
return <DefaultStoryExample />;
}
export {
CarouselDefaultExample as default
};
Loop
loop 옵션을 사용한 무한 루프
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var LoopStoryExample = () => {
const slides = createNumberSlides(8);
return <ExampleFrame title="Loop" description="loop 옵션을 사용한 무한 루프">
<Carousel2
aria-roledescription="carousel"
aria-label="Loop carousel"
options={{ loop: true }}
slideGap="12px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselLoop.example.tsx
function CarouselLoopExample() {
return <LoopStoryExample />;
}
export {
CarouselLoopExample as default
};
Right To Left
direction: 'rtl' 옵션으로 RTL 스크롤
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var RightToLeftStoryExample = () => {
const slides = createNumberSlides(8);
return <ExampleFrame title="Right To Left" description="direction: 'rtl' 옵션으로 RTL 스크롤">
<Carousel2
aria-roledescription="carousel"
aria-label="RTL carousel"
options={{ direction: "rtl" }}
slideGap="12px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselRightToLeft.example.tsx
function CarouselRightToLeftExample() {
return <RightToLeftStoryExample />;
}
export {
CarouselRightToLeftExample as default
};
Slides To Scroll
한 번에 여러 슬라이드를 이동 (slidesToScroll: 2)
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var SlidesToScrollStoryExample = () => {
const slides = createNumberSlides(10);
return <ExampleFrame title="Slides To Scroll" description="한 번에 여러 슬라이드를 이동 (slidesToScroll: 2)">
<Carousel2
aria-roledescription="carousel"
aria-label="Slides to scroll carousel"
options={{ slidesToScroll: 2 }}
slideGap="12px"
slideSize="50%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselSlidesToScroll.example.tsx
function CarouselSlidesToScrollExample() {
return <SlidesToScrollStoryExample />;
}
export {
CarouselSlidesToScrollExample as default
};
Drag Free
dragFree: true 옵션으로 관성/프리 드래그
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var DragFreeStoryExample = () => {
const slides = createNumberSlides(10);
return <ExampleFrame title="Drag Free" description="dragFree: true 옵션으로 관성/프리 드래그">
<Carousel2
aria-roledescription="carousel"
aria-label="Drag free carousel"
options={{ dragFree: true }}
slideGap="12px"
slideSize="70%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselDragFree.example.tsx
function CarouselDragFreeExample() {
return <DragFreeStoryExample />;
}
export {
CarouselDragFreeExample as default
};
Align
align 옵션(start/center/end)을 비교
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var AlignStoryExample = () => {
const slides = createNumberSlides(8);
const renderAlign = (align) => <Box2 display="grid" gap="12px">
<Box2 as="h4" fontSize="14px" fontWeight={700} color="neutral.text.lowest">
align: {align}
</Box2>
<Carousel2
aria-roledescription="carousel"
aria-label={`Align ${align} carousel`}
options={{ align }}
slideGap="12px"
slideSize="60%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={`${align}-${slide.id}`}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</Box2>;
return <ExampleFrame title="Align" description="align 옵션(start/center/end)을 비교">
<Box2 display="grid" gap="24px">
{renderAlign("start")}
{renderAlign("center")}
{renderAlign("end")}
</Box2>
</ExampleFrame>;
};
// CarouselAlign.example.tsx
function CarouselAlignExample() {
return <AlignStoryExample />;
}
export {
CarouselAlignExample as default
};
VariableWidths
슬라이드마다 서로 다른 너비를 갖도록 구성
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var VariableWidthsStoryExample = () => {
const widths = ["40%", "70%", "55%", "85%", "60%", "45%"];
return <ExampleFrame title="Variable Widths" description="슬라이드마다 서로 다른 너비를 갖도록 구성">
<Carousel2 aria-roledescription="carousel" aria-label="Variable width carousel" slideGap="12px">
<Carousel2.Viewport>
<Carousel2.Container>
{widths.map((width, index) => <Carousel2.Slide
key={String(index)}
panda={{ flex: `0 0 ${width}` }}
aria-roledescription="slide"
aria-label={`Slide ${index + 1}`}
>
<DemoCard
panda={{
backgroundColor: index % 2 === 0 ? "purple.light.100" : "blue.light.100"
}}
>
{width}
</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselVariableWidths.example.tsx
function CarouselVariableWidthsExample() {
return <VariableWidthsStoryExample />;
}
export {
CarouselVariableWidthsExample as default
};
Y-axis
axis: 'y' 옵션으로 세로 캐러셀
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var YAxisStoryExample = () => {
const slides = createNumberSlides(10);
const height = 360;
return <ExampleFrame title="Y-axis" description="axis: 'y' 옵션으로 세로 캐러셀">
<Carousel2
aria-roledescription="carousel"
aria-label="Y axis carousel"
options={{ axis: "y" }}
slideGap="12px"
slideSize="33.333%"
panda={{ h: `${height}px` }}
>
<Carousel2.Viewport panda={{ h: `${height}px` }}>
<Carousel2.Container
panda={{
flexDirection: "column",
// @ts-expect-error Panda's generated union omits this valid compound CSS value.
touchAction: "pan-x pinch-zoom",
marginLeft: "0px",
marginTop: "calc(var(--carousel-slide-gap, 0px) * -1)"
}}
>
{slides.map((slide) => <Carousel2.Slide
key={slide.id}
panda={{
paddingLeft: "0px",
paddingTop: "var(--carousel-slide-gap, 0px)"
}}
>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselYAxis.example.tsx
function CarouselYAxisExample() {
return <YAxisStoryExample />;
}
export {
CarouselYAxisExample as default
};
Slides Per View
한 화면에 여러 슬라이드를 보이도록 구성 (3 per view)
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-basic.tsx
var SlidesPerViewStoryExample = () => {
const slides = createNumberSlides(12);
return <ExampleFrame title="Slides Per View" description="한 화면에 여러 슬라이드를 보이도록 구성 (3 per view)">
<Carousel2
aria-roledescription="carousel"
aria-label="Slides per view carousel"
slideGap="12px"
slideSize="33.333%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselSlidesPerView.example.tsx
function CarouselSlidesPerViewExample() {
return <SlidesPerViewStoryExample />;
}
export {
CarouselSlidesPerViewExample as default
};
Thumbnails
메인 캐러셀과 썸네일 캐러셀을 동기화
// _predefined-basic.tsx
import * as React2 from "react";
import { css as css2, cx as cx2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
var useSelectedIndex = (emblaApi) => {
const [selectedIndex, setSelectedIndex] = React.useState(0);
const handleUpdate = React.useCallback((api) => {
setSelectedIndex(api.selectedSnap());
}, []);
useEmblaApiEvent({ emblaApi, eventName: "reinit", handler: handleUpdate });
useEmblaApiEvent({ emblaApi, eventName: "select", handler: handleUpdate });
React.useEffect(() => {
if (!emblaApi) return;
handleUpdate(emblaApi);
}, [emblaApi, handleUpdate]);
return selectedIndex;
};
// _predefined-basic.tsx
var ThumbnailsExample = () => {
const slides = createNumberSlides(8);
const [mainApi, setMainApi] = React2.useState();
const [thumbsApi, setThumbsApi] = React2.useState();
const selectedIndex = useSelectedIndex(mainApi);
React2.useEffect(() => {
if (!mainApi || !thumbsApi) return;
thumbsApi.goTo(selectedIndex);
}, [mainApi, selectedIndex, thumbsApi]);
const handleThumbClick = React2.useCallback(
(index) => {
if (!mainApi) return;
mainApi.goTo(index);
},
[mainApi]
);
return <Box2 display="grid" gap="12px">
<Carousel2 aria-roledescription="carousel" aria-label="Main carousel" onApi={setMainApi} slideGap="12px">
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide, index) => <Carousel2.Slide
key={`main-${slide.id}`}
aria-roledescription="slide"
aria-label={`Slide ${index + 1}`}
>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav showDots={false} />
</Carousel2>
<Carousel2
aria-roledescription="carousel"
aria-label="Thumbnails carousel"
onApi={setThumbsApi}
options={{ dragFree: true, containScroll: "keepSnaps" }}
slideGap="8px"
slideSize="20%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide, index) => {
const selected = index === selectedIndex;
return <Carousel2.Slide key={`thumb-${slide.id}`}>
<button
type="button"
aria-label={`Go to slide ${index + 1}`}
onClick={() => handleThumbClick(index)}
className={cx2(
css2({
width: "100%",
borderRadius: "10px",
overflow: "hidden",
border: "2px solid",
borderColor: selected ? "neutral.border.highest" : "transparent",
outline: "none",
_focusVisible: {
outline: "2px solid",
outlineColor: "primary",
outlineOffset: "2px"
}
})
)}
>
<DemoCard
panda={{
h: "72px",
fontSize: "14px",
...slide.background ?? {},
opacity: selected ? 1 : 0.7
}}
>
{slide.label}
</DemoCard>
</button>
</Carousel2.Slide>;
})}
</Carousel2.Container>
</Carousel2.Viewport>
</Carousel2>
</Box2>;
};
var ThumbnailsStoryExample = () => {
return <ExampleFrame title="Thumbnails" description="메인 캐러셀과 썸네일 캐러셀을 동기화">
<ThumbnailsExample />
</ExampleFrame>;
};
// CarouselThumbnails.example.tsx
function CarouselThumbnailsExample() {
return <ThumbnailsStoryExample />;
}
export {
CarouselThumbnailsExample as default
};
Progress
scrollProgress()로 진행률 표시
// _predefined-misc.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
// _predefined-misc.tsx
var ProgressStoryExample = () => {
const slides = createNumberSlides(10);
const [emblaApi, setEmblaApi] = React2.useState();
const [progress, setProgress] = React2.useState(0);
useEmblaApiEvent({
emblaApi,
eventName: "scroll",
handler: (api) => setProgress(api.scrollProgress())
});
useEmblaApiEvent({
emblaApi,
eventName: "reinit",
handler: (api) => setProgress(api.scrollProgress())
});
return <ExampleFrame title="Progress" description="scrollProgress()로 진행률 표시">
<Carousel2
aria-roledescription="carousel"
aria-label="Progress carousel"
onApi={setEmblaApi}
slideGap="12px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<Box2 mt="12px" display="grid" gap="12px">
<Box2
aria-label="Progress bar"
className={css2({
height: "6px",
borderRadius: "999px",
backgroundColor: "neutral.surface.highest",
overflow: "hidden"
})}
>
<Box2
className={css2({
height: "100%",
backgroundColor: "neutral.fill.high",
width: `${Math.max(0, Math.min(1, progress)) * 100}%`,
transition: "width 80ms linear"
})}
/>
</Box2>
<DemoNav />
</Box2>
</Carousel2>
</ExampleFrame>;
};
// CarouselProgress.example.tsx
function CarouselProgressExample() {
return <ProgressStoryExample />;
}
export {
CarouselProgressExample as default
};
Lazy Load
slidesInView() 기준으로 이미지 src를 지연 설정
// _predefined-misc.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
// _predefined-misc.tsx
var createImageUrl = (index) => {
const id = 200 + index;
return `https://picsum.photos/id/${id}/900/600`;
};
var LazyLoadStoryExample = () => {
const slideCount = 12;
const [emblaApi, setEmblaApi] = React2.useState();
const [loaded, setLoaded] = React2.useState(() => /* @__PURE__ */ new Set());
const handleLoadInView = React2.useCallback((api) => {
const inView = api.slidesInView();
setLoaded((prev) => {
const next = new Set(prev);
inView.forEach((index) => next.add(index));
return next;
});
}, []);
useEmblaApiEvent({ emblaApi, eventName: "reinit", handler: handleLoadInView });
useEmblaApiEvent({ emblaApi, eventName: "select", handler: handleLoadInView });
return <ExampleFrame title="Lazy Load" description="slidesInView() 기준으로 이미지 src를 지연 설정">
<Carousel2
aria-roledescription="carousel"
aria-label="Lazy load carousel"
onApi={setEmblaApi}
slideGap="12px"
slideSize="90%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{Array.from({ length: slideCount }).map((_, index) => {
const isLoaded = loaded.has(index);
const src = isLoaded ? createImageUrl(index) : void 0;
return <Carousel2.Slide
key={String(index)}
aria-roledescription="slide"
aria-label={`Image ${index + 1}`}
>
<div
className={css2({
height: "240px",
borderRadius: "16px",
overflow: "hidden",
backgroundColor: "neutral.surface.high",
position: "relative",
display: "grid",
placeItems: "center"
})}
>
{src ? <img
alt={`Demo image ${index + 1}`}
src={src}
className={css2({
width: "100%",
height: "100%",
objectFit: "cover",
display: "block"
})}
loading="lazy"
/> : <Box2 fontSize="14px" color="neutral.text.lowest" fontWeight={700}>
Loading…
</Box2>}
<Box2
className={css2({
position: "absolute",
left: "12px",
bottom: "12px",
px: "10px",
py: "6px",
borderRadius: "999px",
backgroundColor: "rgba(0,0,0,0.55)",
color: "white",
fontSize: "12px",
fontWeight: 700
})}
>
{index + 1}
</Box2>
</div>
</Carousel2.Slide>;
})}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselLazyLoad.example.tsx
function CarouselLazyLoadExample() {
return <LazyLoadStoryExample />;
}
export {
CarouselLazyLoadExample as default
};
Scroll Bar
진행률과 클릭을 이용해 스크롤바 UI를 구성 (스냅 기반)
// _predefined-misc.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
// _predefined-misc.tsx
var ScrollBarStoryExample = () => {
const slides = createNumberSlides(10);
const [emblaApi, setEmblaApi] = React2.useState();
const [progress, setProgress] = React2.useState(0);
useEmblaApiEvent({ emblaApi, eventName: "scroll", handler: (api) => setProgress(api.scrollProgress()) });
useEmblaApiEvent({ emblaApi, eventName: "reinit", handler: (api) => setProgress(api.scrollProgress()) });
const clampedProgress = Math.max(0, Math.min(1, progress));
const handleTrackClick = React2.useCallback(
(e) => {
if (!emblaApi) return;
const rect = e.currentTarget.getBoundingClientRect();
const ratio = (e.clientX - rect.left) / rect.width;
const snaps = emblaApi.snapList().length;
if (snaps <= 1) return;
const index = Math.round(Math.max(0, Math.min(1, ratio)) * (snaps - 1));
emblaApi.goTo(index);
},
[emblaApi]
);
return <ExampleFrame title="Scroll Bar" description="진행률과 클릭을 이용해 스크롤바 UI를 구성 (스냅 기반)">
<Carousel2
aria-roledescription="carousel"
aria-label="Scroll bar carousel"
onApi={setEmblaApi}
slideGap="12px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<Box2 mt="12px" display="grid" gap="12px">
<div
role="presentation"
aria-label="Scrollbar track"
onClick={handleTrackClick}
className={css2({
height: "10px",
borderRadius: "999px",
backgroundColor: "neutral.surface.highest",
position: "relative",
cursor: "pointer"
})}
>
<div
aria-label="Scrollbar thumb"
className={css2({
position: "absolute",
top: "1px",
left: "0px",
transform: `translateX(calc(${clampedProgress * 100}% - ${clampedProgress * 56}px))`,
width: "56px",
height: "8px",
borderRadius: "999px",
backgroundColor: "neutral.fill.high",
transition: "transform 80ms linear",
pointerEvents: "none"
})}
/>
</div>
<DemoNav />
</Box2>
</Carousel2>
</ExampleFrame>;
};
// CarouselScrollBar.example.tsx
function CarouselScrollBarExample() {
return <ScrollBarStoryExample />;
}
export {
CarouselScrollBarExample as default
};
Infinite Scroll
끝에 가까워지면 아이템을 추가하고 reInit()
// _predefined-misc.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
var useSelectedIndex = (emblaApi) => {
const [selectedIndex, setSelectedIndex] = React.useState(0);
const handleUpdate = React.useCallback((api) => {
setSelectedIndex(api.selectedSnap());
}, []);
useEmblaApiEvent({ emblaApi, eventName: "reinit", handler: handleUpdate });
useEmblaApiEvent({ emblaApi, eventName: "select", handler: handleUpdate });
React.useEffect(() => {
if (!emblaApi) return;
handleUpdate(emblaApi);
}, [emblaApi, handleUpdate]);
return selectedIndex;
};
// _predefined-misc.tsx
var InfiniteScrollExample = () => {
const [items, setItems] = React2.useState(() => Array.from({ length: 10 }).map((_, i) => i + 1));
const [emblaApi, setEmblaApi] = React2.useState();
const selectedIndex = useSelectedIndex(emblaApi);
const appendMore = React2.useCallback(() => {
setItems((prev) => {
const start = prev.length + 1;
const next = Array.from({ length: 6 }).map((_, i) => start + i);
return [...prev, ...next];
});
}, []);
React2.useEffect(() => {
if (!emblaApi) return;
if (selectedIndex < items.length - 3) return;
appendMore();
}, [appendMore, emblaApi, items.length, selectedIndex]);
React2.useEffect(() => {
if (!emblaApi) return;
emblaApi.reInit();
emblaApi.goTo(selectedIndex, true);
}, [emblaApi, items.length, selectedIndex]);
return <Carousel2
aria-roledescription="carousel"
aria-label="Infinite scroll carousel"
onApi={setEmblaApi}
slideGap="12px"
slideSize="70%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{items.map((n) => <Carousel2.Slide key={String(n)}>
<DemoCard panda={{ backgroundColor: n % 2 === 0 ? "orange.100" : "yellow.100" }}>{n}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>;
};
var InfiniteScrollStoryExample = () => {
return <ExampleFrame title="Infinite Scroll" description="끝에 가까워지면 아이템을 추가하고 reInit()">
<InfiniteScrollExample />
</ExampleFrame>;
};
// CarouselInfiniteScroll.example.tsx
function CarouselInfiniteScrollExample() {
return <InfiniteScrollStoryExample />;
}
export {
CarouselInfiniteScrollExample as default
};
IOS Style Picker Default
세로 축 + 가운데 정렬로 iOS picker 스타일을 구성
// _predefined-misc.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
var useSelectedIndex = (emblaApi) => {
const [selectedIndex, setSelectedIndex] = React.useState(0);
const handleUpdate = React.useCallback((api) => {
setSelectedIndex(api.selectedSnap());
}, []);
useEmblaApiEvent({ emblaApi, eventName: "reinit", handler: handleUpdate });
useEmblaApiEvent({ emblaApi, eventName: "select", handler: handleUpdate });
React.useEffect(() => {
if (!emblaApi) return;
handleUpdate(emblaApi);
}, [emblaApi, handleUpdate]);
return selectedIndex;
};
// _predefined-misc.tsx
var IosPicker = ({ loop }) => {
const options = React2.useMemo(() => ({ axis: "y", loop, align: "center" }), [loop]);
const [emblaApi, setEmblaApi] = React2.useState();
const selectedIndex = useSelectedIndex(emblaApi);
const height = 240;
const items = React2.useMemo(
() => [
"Apple",
"Banana",
"Cherry",
"Date",
"Fig",
"Grape",
"Kiwi",
"Lime",
"Mango",
"Peach",
"Pear",
"Plum"
],
[]
);
return <div
className={css2({
position: "relative",
width: "320px",
maxWidth: "100%",
borderRadius: "16px",
border: "1px solid",
borderColor: "neutral.border.low",
overflow: "hidden",
backgroundColor: "white"
})}
>
<Carousel2
aria-roledescription="carousel"
aria-label={loop ? "iOS picker loop" : "iOS picker"}
onApi={setEmblaApi}
options={options}
panda={{ h: `${height}px` }}
slideGap="0px"
slideSize="33.333%"
>
<Carousel2.Viewport panda={{ h: `${height}px` }}>
<Carousel2.Container
panda={{
flexDirection: "column",
// @ts-expect-error Panda's generated union omits this valid compound CSS value.
touchAction: "pan-x pinch-zoom",
marginLeft: "0px",
marginTop: "0px"
}}
>
{items.map((text, index) => {
const selected = index === selectedIndex;
return <Carousel2.Slide
key={text}
panda={{
paddingLeft: "0px",
paddingTop: "0px",
display: "flex",
alignItems: "center",
justifyContent: "center",
height: `${height / 3}px`,
"& > div": { transition: "transform 160ms ease, opacity 160ms ease" }
}}
>
<div
className={css2({
fontSize: selected ? "20px" : "16px",
fontWeight: selected ? 800 : 600,
color: selected ? "neutral.text.base" : "neutral.text.lowest",
transform: selected ? "scale(1)" : "scale(0.96)",
opacity: selected ? 1 : 0.55
})}
>
{text}
</div>
</Carousel2.Slide>;
})}
</Carousel2.Container>
</Carousel2.Viewport>
</Carousel2>
<div
aria-hidden
className={css2({
pointerEvents: "none",
position: "absolute",
inset: 0,
background: "linear-gradient(to bottom, rgba(255,255,255,0.95), rgba(255,255,255,0) 35%, rgba(255,255,255,0) 65%, rgba(255,255,255,0.95))"
})}
/>
<div
aria-hidden
className={css2({
pointerEvents: "none",
position: "absolute",
left: 0,
right: 0,
top: "50%",
transform: "translateY(-50%)",
height: `${height / 3}px`,
borderTop: "1px solid",
borderBottom: "1px solid",
borderColor: "neutral.border.low"
})}
/>
</div>;
};
var IOSStylePickerDefaultStoryExample = () => {
return <ExampleFrame
title="IOS Style Picker Default"
description="세로 축 + 가운데 정렬로 iOS picker 스타일을 구성"
>
<IosPicker loop={false} />
</ExampleFrame>;
};
// CarouselIOSStylePickerDefault.example.tsx
function CarouselIOSStylePickerDefaultExample() {
return <IOSStylePickerDefaultStoryExample />;
}
export {
CarouselIOSStylePickerDefaultExample as default
};
IOS Style Picker Loop
loop 옵션을 켠 iOS picker 스타일
// _predefined-misc.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
var useSelectedIndex = (emblaApi) => {
const [selectedIndex, setSelectedIndex] = React.useState(0);
const handleUpdate = React.useCallback((api) => {
setSelectedIndex(api.selectedSnap());
}, []);
useEmblaApiEvent({ emblaApi, eventName: "reinit", handler: handleUpdate });
useEmblaApiEvent({ emblaApi, eventName: "select", handler: handleUpdate });
React.useEffect(() => {
if (!emblaApi) return;
handleUpdate(emblaApi);
}, [emblaApi, handleUpdate]);
return selectedIndex;
};
// _predefined-misc.tsx
var IosPicker = ({ loop }) => {
const options = React2.useMemo(() => ({ axis: "y", loop, align: "center" }), [loop]);
const [emblaApi, setEmblaApi] = React2.useState();
const selectedIndex = useSelectedIndex(emblaApi);
const height = 240;
const items = React2.useMemo(
() => [
"Apple",
"Banana",
"Cherry",
"Date",
"Fig",
"Grape",
"Kiwi",
"Lime",
"Mango",
"Peach",
"Pear",
"Plum"
],
[]
);
return <div
className={css2({
position: "relative",
width: "320px",
maxWidth: "100%",
borderRadius: "16px",
border: "1px solid",
borderColor: "neutral.border.low",
overflow: "hidden",
backgroundColor: "white"
})}
>
<Carousel2
aria-roledescription="carousel"
aria-label={loop ? "iOS picker loop" : "iOS picker"}
onApi={setEmblaApi}
options={options}
panda={{ h: `${height}px` }}
slideGap="0px"
slideSize="33.333%"
>
<Carousel2.Viewport panda={{ h: `${height}px` }}>
<Carousel2.Container
panda={{
flexDirection: "column",
// @ts-expect-error Panda's generated union omits this valid compound CSS value.
touchAction: "pan-x pinch-zoom",
marginLeft: "0px",
marginTop: "0px"
}}
>
{items.map((text, index) => {
const selected = index === selectedIndex;
return <Carousel2.Slide
key={text}
panda={{
paddingLeft: "0px",
paddingTop: "0px",
display: "flex",
alignItems: "center",
justifyContent: "center",
height: `${height / 3}px`,
"& > div": { transition: "transform 160ms ease, opacity 160ms ease" }
}}
>
<div
className={css2({
fontSize: selected ? "20px" : "16px",
fontWeight: selected ? 800 : 600,
color: selected ? "neutral.text.base" : "neutral.text.lowest",
transform: selected ? "scale(1)" : "scale(0.96)",
opacity: selected ? 1 : 0.55
})}
>
{text}
</div>
</Carousel2.Slide>;
})}
</Carousel2.Container>
</Carousel2.Viewport>
</Carousel2>
<div
aria-hidden
className={css2({
pointerEvents: "none",
position: "absolute",
inset: 0,
background: "linear-gradient(to bottom, rgba(255,255,255,0.95), rgba(255,255,255,0) 35%, rgba(255,255,255,0) 65%, rgba(255,255,255,0.95))"
})}
/>
<div
aria-hidden
className={css2({
pointerEvents: "none",
position: "absolute",
left: 0,
right: 0,
top: "50%",
transform: "translateY(-50%)",
height: `${height / 3}px`,
borderTop: "1px solid",
borderBottom: "1px solid",
borderColor: "neutral.border.low"
})}
/>
</div>;
};
var IOSStylePickerLoopStoryExample = () => {
return <ExampleFrame title="IOS Style Picker Loop" description="loop 옵션을 켠 iOS picker 스타일">
<IosPicker loop />
</ExampleFrame>;
};
// CarouselIOSStylePickerLoop.example.tsx
function CarouselIOSStylePickerLoopExample() {
return <IOSStylePickerLoopStoryExample />;
}
export {
CarouselIOSStylePickerLoopExample as default
};
Accessibility
aria 속성/키보드 포커스/명확한 라벨링을 강화한 예시
// _predefined-plugins.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-plugins.tsx
var AccessibilityStoryExample = () => {
const slides = createNumberSlides(6);
return <ExampleFrame title="Accessibility" description="aria 속성/키보드 포커스/명확한 라벨링을 강화한 예시">
<Carousel2 aria-roledescription="carousel" aria-label="Accessible carousel" slideGap="12px">
<Carousel2.Viewport aria-label="Carousel viewport">
<Carousel2.Container>
{slides.map((slide, index) => <Carousel2.Slide
key={slide.id}
aria-roledescription="slide"
aria-label={`${index + 1} / ${slides.length}`}
>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav dotsAriaLabel="Slides" />
</Carousel2>
</ExampleFrame>;
};
// CarouselAccessibility.example.tsx
function CarouselAccessibilityExample() {
return <AccessibilityStoryExample />;
}
export {
CarouselAccessibilityExample as default
};
Autoplay
플러그인으로 일정 간격마다 다음 슬라이드로 이동
// _predefined-plugins.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
// _predefined-plugins.tsx
var createAutoplayPlugin = ({
delayMs,
stopOnInteraction
}) => {
let emblaApi;
let timer;
let playing = false;
const stop = () => {
playing = false;
if (!timer) return;
clearInterval(timer);
timer = void 0;
};
const play = () => {
if (!emblaApi) return;
stop();
playing = true;
timer = setInterval(() => {
if (!emblaApi) return;
if (emblaApi.canGoToNext()) {
emblaApi.goToNext();
return;
}
emblaApi.goTo(0);
}, delayMs);
};
const plugin = {
name: "autoplay-demo",
options: {},
init: (api) => {
emblaApi = api;
play();
if (!stopOnInteraction) return;
api.on("pointerdown", stop);
api.on("pointerup", play);
},
destroy: () => {
stop();
emblaApi = void 0;
},
play,
stop,
isPlaying: () => playing
};
return plugin;
};
var AutoplayExample = ({
title,
description,
createPlugin
}) => {
const slides = createNumberSlides(8);
const plugin = React2.useMemo(createPlugin, [createPlugin]);
const [emblaApi, setEmblaApi] = React2.useState();
const [playing, setPlaying] = React2.useState(plugin.isPlaying());
useEmblaApiEvent({
emblaApi,
eventName: "select",
handler: () => setPlaying(plugin.isPlaying())
});
const handleToggle = React2.useCallback(() => {
if (plugin.isPlaying()) {
plugin.stop();
setPlaying(false);
return;
}
plugin.play();
setPlaying(true);
}, [plugin]);
return <ExampleFrame title={title} description={description}>
<Carousel2
aria-roledescription="carousel"
aria-label={`${title} carousel`}
onApi={setEmblaApi}
plugins={[plugin]}
slideGap="12px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<Box2 mt="12px" display="flex" alignItems="center" justifyContent="space-between" gap="12px">
<Box2 display="flex" gap="8px">
<Carousel2.PrevButton
className={css2({
px: "12px",
py: "8px",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel2.PrevButton>
<Carousel2.NextButton
className={css2({
px: "12px",
py: "8px",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel2.NextButton>
<button
type="button"
aria-label={playing ? "Pause autoplay" : "Start autoplay"}
onClick={handleToggle}
className={css2({
px: "12px",
py: "8px",
borderRadius: "8px",
backgroundColor: playing ? "neutral.fill.high" : "neutral.surface.high",
color: playing ? "inverse.text.base" : "neutral.text.base",
fontWeight: 700
})}
>
{playing ? "Pause" : "Play"}
</button>
</Box2>
<Carousel2.Dots />
</Box2>
</Carousel2>
</ExampleFrame>;
};
var AutoplayStoryExample = () => {
return <AutoplayExample
title="Autoplay"
description="플러그인으로 일정 간격마다 다음 슬라이드로 이동"
createPlugin={() => createAutoplayPlugin({ delayMs: 1800, stopOnInteraction: true })}
/>;
};
// CarouselAutoplay.example.tsx
function CarouselAutoplayExample() {
return <AutoplayStoryExample />;
}
export {
CarouselAutoplayExample as default
};
Auto Scroll
지속적인 자동 이동(데모에서는 스냅 단위 자동 이동으로 단순화)
// _predefined-plugins.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
// _predefined-plugins.tsx
var createAutoplayPlugin = ({
delayMs,
stopOnInteraction
}) => {
let emblaApi;
let timer;
let playing = false;
const stop = () => {
playing = false;
if (!timer) return;
clearInterval(timer);
timer = void 0;
};
const play = () => {
if (!emblaApi) return;
stop();
playing = true;
timer = setInterval(() => {
if (!emblaApi) return;
if (emblaApi.canGoToNext()) {
emblaApi.goToNext();
return;
}
emblaApi.goTo(0);
}, delayMs);
};
const plugin = {
name: "autoplay-demo",
options: {},
init: (api) => {
emblaApi = api;
play();
if (!stopOnInteraction) return;
api.on("pointerdown", stop);
api.on("pointerup", play);
},
destroy: () => {
stop();
emblaApi = void 0;
},
play,
stop,
isPlaying: () => playing
};
return plugin;
};
var createAutoScrollPlugin = ({ delayMs }) => {
return createAutoplayPlugin({ delayMs, stopOnInteraction: false });
};
var AutoplayExample = ({
title,
description,
createPlugin
}) => {
const slides = createNumberSlides(8);
const plugin = React2.useMemo(createPlugin, [createPlugin]);
const [emblaApi, setEmblaApi] = React2.useState();
const [playing, setPlaying] = React2.useState(plugin.isPlaying());
useEmblaApiEvent({
emblaApi,
eventName: "select",
handler: () => setPlaying(plugin.isPlaying())
});
const handleToggle = React2.useCallback(() => {
if (plugin.isPlaying()) {
plugin.stop();
setPlaying(false);
return;
}
plugin.play();
setPlaying(true);
}, [plugin]);
return <ExampleFrame title={title} description={description}>
<Carousel2
aria-roledescription="carousel"
aria-label={`${title} carousel`}
onApi={setEmblaApi}
plugins={[plugin]}
slideGap="12px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide key={slide.id}>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<Box2 mt="12px" display="flex" alignItems="center" justifyContent="space-between" gap="12px">
<Box2 display="flex" gap="8px">
<Carousel2.PrevButton
className={css2({
px: "12px",
py: "8px",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel2.PrevButton>
<Carousel2.NextButton
className={css2({
px: "12px",
py: "8px",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel2.NextButton>
<button
type="button"
aria-label={playing ? "Pause autoplay" : "Start autoplay"}
onClick={handleToggle}
className={css2({
px: "12px",
py: "8px",
borderRadius: "8px",
backgroundColor: playing ? "neutral.fill.high" : "neutral.surface.high",
color: playing ? "inverse.text.base" : "neutral.text.base",
fontWeight: 700
})}
>
{playing ? "Pause" : "Play"}
</button>
</Box2>
<Carousel2.Dots />
</Box2>
</Carousel2>
</ExampleFrame>;
};
var AutoScrollStoryExample = () => {
return <AutoplayExample
title="Auto Scroll"
description="지속적인 자동 이동(데모에서는 스냅 단위 자동 이동으로 단순화)"
createPlugin={() => createAutoScrollPlugin({ delayMs: 700 })}
/>;
};
// CarouselAutoScroll.example.tsx
function CarouselAutoScrollExample() {
return <AutoScrollStoryExample />;
}
export {
CarouselAutoScrollExample as default
};
Auto Height
선택된 슬라이드 높이에 맞춰 viewport 높이를 자동 조정
// _predefined-plugins.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-plugins.tsx
var createAutoHeightPlugin = () => {
let emblaApi;
const updateHeight = () => {
if (!emblaApi) return;
const root = emblaApi.rootNode();
const slides = emblaApi.slideNodes();
const index = emblaApi.selectedSnap();
const slide = slides[index];
if (!root || !slide) return;
root.style.transition = "height 200ms ease";
root.style.height = `${slide.getBoundingClientRect().height}px`;
};
const plugin = {
name: "auto-height-demo",
options: {},
init: (api) => {
emblaApi = api;
updateHeight();
api.on("reinit", updateHeight);
api.on("select", updateHeight);
},
destroy: () => {
if (!emblaApi) return;
emblaApi.off("reinit", updateHeight);
emblaApi.off("select", updateHeight);
emblaApi = void 0;
}
};
return plugin;
};
var AutoHeightStoryExample = () => {
const slides = [
{ id: "1", h: 120, bg: "pink.light.100" },
{ id: "2", h: 200, bg: "green.light.100" },
{ id: "3", h: 160, bg: "blue.light.100" },
{ id: "4", h: 240, bg: "purple.light.100" }
];
const autoHeightPlugin = React2.useMemo(() => createAutoHeightPlugin(), []);
return <ExampleFrame title="Auto Height" description="선택된 슬라이드 높이에 맞춰 viewport 높이를 자동 조정">
<Carousel2
aria-roledescription="carousel"
aria-label="Auto height carousel"
plugins={[autoHeightPlugin]}
slideGap="12px"
>
<Carousel2.Viewport panda={{ height: "auto" }}>
<Carousel2.Container>
{slides.map((slide, index) => <Carousel2.Slide
key={slide.id}
aria-roledescription="slide"
aria-label={`${index + 1} / ${slides.length}`}
>
<DemoCard panda={{ h: `${slide.h}px`, backgroundColor: slide.bg }}>{slide.id}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselAutoHeight.example.tsx
function CarouselAutoHeightExample() {
return <AutoHeightStoryExample />;
}
export {
CarouselAutoHeightExample as default
};
Fade
스크롤 진행도 기반으로 opacity를 조절해 페이드 느낌을 재현
// _predefined-plugins.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
var useEmblaApiEvent = ({
emblaApi,
eventName,
handler
}) => {
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
React.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => handlerRef.current(api);
if (eventName === "init") {
handle(emblaApi);
return;
}
emblaApi.on(eventName, handle);
return () => {
emblaApi.off(eventName, handle);
};
}, [emblaApi, eventName]);
};
var useSelectedIndex = (emblaApi) => {
const [selectedIndex, setSelectedIndex] = React.useState(0);
const handleUpdate = React.useCallback((api) => {
setSelectedIndex(api.selectedSnap());
}, []);
useEmblaApiEvent({ emblaApi, eventName: "reinit", handler: handleUpdate });
useEmblaApiEvent({ emblaApi, eventName: "select", handler: handleUpdate });
React.useEffect(() => {
if (!emblaApi) return;
handleUpdate(emblaApi);
}, [emblaApi, handleUpdate]);
return selectedIndex;
};
// _predefined-plugins.tsx
var FadeStoryExample = () => {
const slides = createNumberSlides(6);
const [emblaApi, setEmblaApi] = React2.useState();
const selectedIndex = useSelectedIndex(emblaApi);
return <ExampleFrame title="Fade" description="스크롤 진행도 기반으로 opacity를 조절해 페이드 느낌을 재현">
<Carousel2
aria-roledescription="carousel"
aria-label="Fade carousel"
onApi={setEmblaApi}
options={{ loop: true }}
slideGap="0px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide, index) => {
const selected = index === selectedIndex;
return <Carousel2.Slide
key={slide.id}
panda={{
"& > div": {
transition: "opacity 220ms ease, transform 220ms ease",
opacity: selected ? 1 : 0.25,
transform: selected ? "scale(1)" : "scale(0.98)"
}
}}
>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>;
})}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselFade.example.tsx
function CarouselFadeExample() {
return <FadeStoryExample />;
}
export {
CarouselFadeExample as default
};
Class Names
선택된 슬라이드에 class를 토글하는 플러그인 예시
// _predefined-plugins.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Box as Box2 } from "@mildang/styled-system/jsx";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-plugins.tsx
var createClassNamesPlugin = ({ selectedClassName }) => {
let emblaApi;
const update = () => {
if (!emblaApi) return;
const slides = emblaApi.slideNodes();
const selected = emblaApi.selectedSnap();
slides.forEach((node, index) => {
node.classList.toggle(selectedClassName, index === selected);
});
};
const plugin = {
name: "class-names-demo",
options: {},
init: (api) => {
emblaApi = api;
update();
api.on("reinit", update);
api.on("select", update);
},
destroy: () => {
if (!emblaApi) return;
emblaApi.off("reinit", update);
emblaApi.off("select", update);
emblaApi = void 0;
}
};
return plugin;
};
var ClassNamesStoryExample = () => {
const slides = createNumberSlides(8);
const classNamesPlugin = React2.useMemo(
() => createClassNamesPlugin({ selectedClassName: "is-selected" }),
[]
);
return <ExampleFrame title="Class Names" description="선택된 슬라이드에 class를 토글하는 플러그인 예시">
<Carousel2
aria-roledescription="carousel"
aria-label="Class names carousel"
plugins={[classNamesPlugin]}
slideGap="12px"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide
key={slide.id}
panda={{
"&.is-selected > div": {
outline: "2px solid",
outlineColor: "neutral.border.highest",
outlineOffset: "2px"
}
}}
>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselClassNames.example.tsx
function CarouselClassNamesExample() {
return <ClassNamesStoryExample />;
}
export {
CarouselClassNamesExample as default
};
Parallax
스크롤에 따라 내부 레이어가 느리게 이동하는 패럴랙스
// _predefined-tween.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-tween.tsx
var useTween = ({
emblaApi,
onUpdate
}) => {
const onUpdateRef = React2.useRef(onUpdate);
onUpdateRef.current = onUpdate;
React2.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => onUpdateRef.current(api);
handle(emblaApi);
emblaApi.on("reinit", handle);
emblaApi.on("scroll", handle);
emblaApi.on("select", handle);
return () => {
emblaApi.off("reinit", handle);
emblaApi.off("scroll", handle);
emblaApi.off("select", handle);
};
}, [emblaApi]);
};
var ParallaxStoryExample = () => {
const slides = createNumberSlides(8);
const [emblaApi, setEmblaApi] = React2.useState();
const layerRefs = React2.useRef([]);
useTween({
emblaApi,
onUpdate: (api) => {
const progress = api.scrollProgress();
const snaps = api.snapList();
const slidesInView = api.slidesInView();
slidesInView.forEach((slideIndex) => {
const snap = snaps[slideIndex] ?? 0;
const diffToTarget = snap - progress;
const layer = layerRefs.current[slideIndex];
if (!layer) return;
const translateX = diffToTarget * 180;
layer.style.transform = `translate3d(${translateX}px, 0px, 0px)`;
});
}
});
const setLayerRef = React2.useCallback((index) => {
return (node) => {
layerRefs.current[index] = node;
};
}, []);
return <ExampleFrame title="Parallax" description="스크롤에 따라 내부 레이어가 느리게 이동하는 패럴랙스">
<Carousel2
aria-roledescription="carousel"
aria-label="Parallax carousel"
onApi={setEmblaApi}
slideGap="12px"
slideSize="90%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide, index) => <Carousel2.Slide key={slide.id}>
<div
className={css2({
height: "220px",
borderRadius: "16px",
overflow: "hidden",
position: "relative"
})}
>
<div
ref={setLayerRef(index)}
className={css2({
position: "absolute",
inset: "-24px",
background: index % 2 === 0 ? "linear-gradient(135deg, #B7E4C7, #74C69D)" : "linear-gradient(135deg, #A9D6E5, #468FAF)",
willChange: "transform"
})}
/>
<div
className={css2({
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "22px",
fontWeight: 800,
color: "neutral.text.base"
})}
>
{slide.label}
</div>
</div>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselParallax.example.tsx
function CarouselParallaxExample() {
return <ParallaxStoryExample />;
}
export {
CarouselParallaxExample as default
};
Scale
스크롤 진행도에 따라 슬라이드가 확대/축소
// _predefined-tween.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-tween.tsx
var clamp01 = (n) => Math.max(0, Math.min(1, n));
var useTween = ({
emblaApi,
onUpdate
}) => {
const onUpdateRef = React2.useRef(onUpdate);
onUpdateRef.current = onUpdate;
React2.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => onUpdateRef.current(api);
handle(emblaApi);
emblaApi.on("reinit", handle);
emblaApi.on("scroll", handle);
emblaApi.on("select", handle);
return () => {
emblaApi.off("reinit", handle);
emblaApi.off("scroll", handle);
emblaApi.off("select", handle);
};
}, [emblaApi]);
};
var ScaleStoryExample = () => {
const slides = createNumberSlides(10);
const [emblaApi, setEmblaApi] = React2.useState();
useTween({
emblaApi,
onUpdate: (api) => {
const progress = api.scrollProgress();
const snaps = api.snapList();
const slideNodes = api.slideNodes();
slideNodes.forEach((slideNode, index) => {
const snap = snaps[index] ?? 0;
const diff = Math.abs(snap - progress);
const tween = clamp01(1 - diff * 2.2);
const scale = 0.92 + tween * 0.08;
slideNode.style.transform = `scale(${scale})`;
slideNode.style.opacity = `${0.55 + tween * 0.45}`;
});
}
});
return <ExampleFrame title="Scale" description="스크롤 진행도에 따라 슬라이드가 확대/축소">
<Carousel2
aria-roledescription="carousel"
aria-label="Scale carousel"
onApi={setEmblaApi}
options={{ loop: false }}
slideGap="12px"
slideSize="60%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide
key={slide.id}
panda={{
transition: "transform 120ms ease, opacity 120ms ease",
willChange: "transform"
}}
>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselScale.example.tsx
function CarouselScaleExample() {
return <ScaleStoryExample />;
}
export {
CarouselScaleExample as default
};
Opacity
스크롤 진행도에 따라 슬라이드 투명도 변화
// _predefined-tween.tsx
import * as React2 from "react";
import { css as css2 } from "@mildang/styled-system/css";
import { Carousel as Carousel2 } from "@mildang/design-system/Carousel";
// _predefined-utils.tsx
import * as React from "react";
import { css, cx } from "@mildang/styled-system/css";
import { Box } from "@mildang/styled-system/jsx";
import { Carousel } from "@mildang/design-system/Carousel";
var createNumberSlides = (length) => {
return Array.from({ length }).map((_, index) => ({
id: String(index + 1),
label: String(index + 1),
background: {
// 스토리 전용 데모. 플랫 `gray.*` 는 이 프리셋에 없는 경로(raw 는 `gray.light.*`)라
// 슬라이드 배경이 아예 안 칠해졌다 — 중립 surface role 로 교체.
backgroundColor: index % 2 === 0 ? "neutral.surface.high" : "neutral.surface.highest"
}
}));
};
var ExampleFrame = ({
title,
description,
children
}) => {
return <Box
p="24"
w="100%"
display="grid"
gap="12"
className={css({
"& + &": {
borderTop: "1px solid",
borderColor: "neutral.border.base",
pt: "24"
}
})}
>
<Box display="grid" gap="4">
<Box as="h3" fontSize="18px" fontWeight={700} lineHeight="1.2">
{title}
</Box>
{description ? <Box as="p" fontSize="14px" color="neutral.text.low" lineHeight="1.4">
{description}
</Box> : null}
</Box>
<Box
p="16"
border="1px solid"
borderColor="neutral.border.base"
borderRadius="16px"
backgroundColor="white"
className={css({
boxShadow: "0 1px 2px rgba(16, 24, 40, 0.06)"
})}
>
{children}
</Box>
</Box>;
};
var DemoCard = ({
children,
className,
panda
}) => {
return <div
className={cx(
css(
{
h: "160px",
borderRadius: "12px",
border: "1px solid",
borderColor: "neutral.border.base",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "20px",
fontWeight: 700,
userSelect: "none"
},
panda
),
className
)}
>
{children}
</div>;
};
var DemoNav = ({
showDots = true,
dotsAriaLabel
}) => {
return <Box mt="12" display="flex" alignItems="center" justifyContent="space-between" gap="12">
<Box display="flex" gap="8">
<Carousel.PrevButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Prev
</Carousel.PrevButton>
<Carousel.NextButton
className={css({
px: "12",
py: "8",
borderRadius: "8px",
backgroundColor: "neutral.surface.high",
_disabled: { opacity: 0.4 }
})}
>
Next
</Carousel.NextButton>
</Box>
{showDots ? <Carousel.Dots aria-label={dotsAriaLabel ?? "Carousel pagination"} /> : null}
</Box>;
};
// _predefined-tween.tsx
var clamp01 = (n) => Math.max(0, Math.min(1, n));
var useTween = ({
emblaApi,
onUpdate
}) => {
const onUpdateRef = React2.useRef(onUpdate);
onUpdateRef.current = onUpdate;
React2.useEffect(() => {
if (!emblaApi) return;
const handle = (api) => onUpdateRef.current(api);
handle(emblaApi);
emblaApi.on("reinit", handle);
emblaApi.on("scroll", handle);
emblaApi.on("select", handle);
return () => {
emblaApi.off("reinit", handle);
emblaApi.off("scroll", handle);
emblaApi.off("select", handle);
};
}, [emblaApi]);
};
var OpacityStoryExample = () => {
const slides = createNumberSlides(10);
const [emblaApi, setEmblaApi] = React2.useState();
useTween({
emblaApi,
onUpdate: (api) => {
const progress = api.scrollProgress();
const snaps = api.snapList();
const slideNodes = api.slideNodes();
slideNodes.forEach((slideNode, index) => {
const snap = snaps[index] ?? 0;
const diff = Math.abs(snap - progress);
const tween = clamp01(1 - diff * 3);
slideNode.style.opacity = `${0.15 + tween * 0.85}`;
});
}
});
return <ExampleFrame title="Opacity" description="스크롤 진행도에 따라 슬라이드 투명도 변화">
<Carousel2
aria-roledescription="carousel"
aria-label="Opacity carousel"
onApi={setEmblaApi}
slideGap="12px"
slideSize="60%"
>
<Carousel2.Viewport>
<Carousel2.Container>
{slides.map((slide) => <Carousel2.Slide
key={slide.id}
panda={{
transition: "opacity 120ms ease"
}}
>
<DemoCard panda={slide.background}>{slide.label}</DemoCard>
</Carousel2.Slide>)}
</Carousel2.Container>
</Carousel2.Viewport>
<DemoNav />
</Carousel2>
</ExampleFrame>;
};
// CarouselOpacity.example.tsx
function CarouselOpacityExample() {
return <OpacityStoryExample />;
}
export {
CarouselOpacityExample as default
};