BoxTab
Navigation
드래그로 순서를 바꿀 수 있는 박스형 탭.
Usage
드래그로 순서를 바꿀 수 있는 박스형 탭. 순서 변경이 필요 없으면 Tabs
import
import
import { BoxTab } from '@mildang/design-system/BoxTab';BoxTab 하나만 가져오면 BoxTab.List · BoxTab.Item · BoxTab.Panel 를 그 아래에서 쓸 수 있다.
예제를 복사해 쓸 때 필요한 준비
• @mildang/icons 를 따로 설치한다. DS 패키지에 아이콘 컴포넌트가 포함되지 않는다.
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
Anatomy
tsx
import { BoxTab } from '@mildang/design-system/BoxTab';
export default function Example() {
return (
<BoxTab>
<BoxTabList />
<BoxTabItem />
<BoxTabPanel />
</BoxTab>
);
}부품
필수 여부
반복
위치
BoxTab
BoxTabList
BoxTab 안에 둔다.
BoxTabItem
여러 개 가능
BoxTabList 안에 둔다.
BoxTabPanel
여러 개 가능
BoxTab 안에 둔다.
- BoxTab 안에 BoxTabList와 BoxTabItem을 배치하고 필요하면 BoxTabPanel을 연결한다.
API Reference
BoxTab Props
Prop
Type
Default
"manual" | "automatic"
automatic
ReactNode
지정 안 함
string
지정 안 함
boolean
false
(order: string[]) => void
지정 안 함
(value: string) => void
지정 안 함
string[]
지정 안 함
"sm" | "md" | "lg"
md
string
지정 안 함
BoxTab
탭 상태와 패널을 관리하는 루트다.
공개 Props 없음
BoxTabList
탭 항목을 감싸는 목록이다.
Prop
Type
Default
string
지정 안 함
ReactNode
지정 안 함
string
지정 안 함
BoxTabItem
하나의 선택 가능한 탭이다.
Prop
Type
Default
ReactNode
지정 안 함
string
지정 안 함
string
지정 안 함
string | number
지정 안 함
boolean
false
boolean
false
boolean
false
ReactNode
지정 안 함
string | number
지정 안 함
(value: string) => void
지정 안 함
BoxTabPanel
선택된 탭의 콘텐츠 패널이다.
Prop
Type
Default
string
지정 안 함
ReactNode
지정 안 함
string
지정 안 함
예제
기본 사용
선택 탭이 흰 박스로 뜨고 패널이 함께 바뀌는 controlled 형태다 — value 를 안 넘기면 탭이 하나도 안 뽑힌다. 순서 변경이 필요 없으면 Tabs 를 쓴다.
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import { css } from '@mildang/styled-system/css';
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const ActivationModeDemo = ({ activationMode }: { activationMode: 'automatic' | 'manual' }) => {
const [value, setValue] = useState('a1');
const [clearCount, setClearCount] = useState(0);
return (
<div>
<BoxTab
activationMode={activationMode}
value={value}
onValueChange={(next) => {
setValue(next);
// 탭 전환에 딸려오는 파괴적 부수효과를 흉내낸다 (실제 캘린더는 선택 카트를 비운다)
setClearCount((prev) => prev + 1);
}}
>
<BoxTab.List aria-label={`${activationMode} 예시`}>
{['a1', 'a2', 'a3', 'a4'].map((v, i) => (
<BoxTab.Item key={v} value={v} label={`그룹 ${i + 1}`} />
))}
</BoxTab.List>
</BoxTab>
<div className={panelStyle}>선택 카트가 비워진 횟수: {clearCount}</div>
</div>
);
};
const BoxTabActivationModeExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
<div>
<div className={captionStyle}>automatic (기본) — 화살표로 지나가기만 해도 전환된다</div>
<ActivationModeDemo activationMode="automatic" />
</div>
<div>
<div className={captionStyle}>manual — 화살표는 포커스만, Enter/Space로 확정</div>
<ActivationModeDemo activationMode="manual" />
</div>
</div>
);
export default BoxTabActivationModeExample;
크기
sm·md·lg 로 높이와 라벨 크기를 나란히 비교한다. size 를 변수로 돌리면 Panda 정적 추출에서 빠지므로 세 벌을 펼쳐 썼다.
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import Eye from '@mildang/icons/react/eye';
import { css } from '@mildang/styled-system/css';
const TABS = [
{
value: 'tab1',
label: '역질문 트리거',
body: '역질문 트리거 탭의 내용입니다. 여기에 관련 콘텐츠가 표시됩니다.',
},
{
value: 'tab2',
label: '카테고리명',
body: '카테고리명 탭의 내용입니다. 탭을 클릭하면 이 영역이 바뀝니다.',
},
{
value: 'tab3',
label: '고정된 자료',
body: '고정된 자료 탭의 내용입니다. 드래그로 탭 순서도 바꿀 수 있어요.',
},
];
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const Template = ({
size,
fill,
activationMode,
withCount,
withEndIcon,
draggable,
}: {
size?: 'sm' | 'md' | 'lg';
fill?: boolean;
activationMode?: 'automatic' | 'manual';
withCount?: boolean;
withEndIcon?: boolean;
draggable?: boolean;
}) => {
const [order, setOrder] = useState(TABS.map((t) => t.value));
const [value, setValue] = useState('tab1');
const byValue = Object.fromEntries(TABS.map((t) => [t.value, t]));
return (
<BoxTab
size={size}
fill={fill}
activationMode={activationMode}
value={value}
onValueChange={setValue}
order={order}
onReorder={setOrder}
>
<BoxTab.List aria-label="예시 탭">
{order.map((v) => (
<BoxTab.Item
key={v}
value={v}
label={byValue[v].label}
draggable={draggable}
count={withCount ? 3 : undefined}
endIcon={withEndIcon ? <Eye /> : undefined}
/>
))}
</BoxTab.List>
{TABS.map((t) => (
<BoxTab.Panel key={t.value} value={t.value}>
<div className={panelStyle}>{t.body}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const BoxTabSizesExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div>
<div className={captionStyle}>sm — 높이 36px</div>
<Template size="sm" />
</div>
<div>
<div className={captionStyle}>md (기본) — 높이 40px</div>
<Template size="md" />
</div>
<div>
<div className={captionStyle}>lg — 높이 42px</div>
<Template size="lg" />
</div>
</div>
);
export default BoxTabSizesExample;
Draggable
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import Eye from '@mildang/icons/react/eye';
import { css } from '@mildang/styled-system/css';
const TABS = [
{
value: 'tab1',
label: '역질문 트리거',
body: '역질문 트리거 탭의 내용입니다. 여기에 관련 콘텐츠가 표시됩니다.',
},
{
value: 'tab2',
label: '카테고리명',
body: '카테고리명 탭의 내용입니다. 탭을 클릭하면 이 영역이 바뀝니다.',
},
{
value: 'tab3',
label: '고정된 자료',
body: '고정된 자료 탭의 내용입니다. 드래그로 탭 순서도 바꿀 수 있어요.',
},
];
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const Template = ({
size,
fill,
activationMode,
withCount,
withEndIcon,
draggable,
}: {
size?: 'sm' | 'md' | 'lg';
fill?: boolean;
activationMode?: 'automatic' | 'manual';
withCount?: boolean;
withEndIcon?: boolean;
draggable?: boolean;
}) => {
const [order, setOrder] = useState(TABS.map((t) => t.value));
const [value, setValue] = useState('tab1');
const byValue = Object.fromEntries(TABS.map((t) => [t.value, t]));
return (
<BoxTab
size={size}
fill={fill}
activationMode={activationMode}
value={value}
onValueChange={setValue}
order={order}
onReorder={setOrder}
>
<BoxTab.List aria-label="예시 탭">
{order.map((v) => (
<BoxTab.Item
key={v}
value={v}
label={byValue[v].label}
draggable={draggable}
count={withCount ? 3 : undefined}
endIcon={withEndIcon ? <Eye /> : undefined}
/>
))}
</BoxTab.List>
{TABS.map((t) => (
<BoxTab.Panel key={t.value} value={t.value}>
<div className={panelStyle}>{t.body}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const BoxTabDraggableExample = () => (
<div>
<div className={captionStyle}>
비선택 탭에 마우스를 올리면 좌측에 핸들이 나타납니다 — 잡아 끌어 순서를 바꿔 보세요
</div>
<Template size="md" draggable />
</div>
);
export default BoxTabDraggableExample;
FillContainer
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import Eye from '@mildang/icons/react/eye';
import { css } from '@mildang/styled-system/css';
const TABS = [
{
value: 'tab1',
label: '역질문 트리거',
body: '역질문 트리거 탭의 내용입니다. 여기에 관련 콘텐츠가 표시됩니다.',
},
{
value: 'tab2',
label: '카테고리명',
body: '카테고리명 탭의 내용입니다. 탭을 클릭하면 이 영역이 바뀝니다.',
},
{
value: 'tab3',
label: '고정된 자료',
body: '고정된 자료 탭의 내용입니다. 드래그로 탭 순서도 바꿀 수 있어요.',
},
];
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const Template = ({
size,
fill,
activationMode,
withCount,
withEndIcon,
draggable,
}: {
size?: 'sm' | 'md' | 'lg';
fill?: boolean;
activationMode?: 'automatic' | 'manual';
withCount?: boolean;
withEndIcon?: boolean;
draggable?: boolean;
}) => {
const [order, setOrder] = useState(TABS.map((t) => t.value));
const [value, setValue] = useState('tab1');
const byValue = Object.fromEntries(TABS.map((t) => [t.value, t]));
return (
<BoxTab
size={size}
fill={fill}
activationMode={activationMode}
value={value}
onValueChange={setValue}
order={order}
onReorder={setOrder}
>
<BoxTab.List aria-label="예시 탭">
{order.map((v) => (
<BoxTab.Item
key={v}
value={v}
label={byValue[v].label}
draggable={draggable}
count={withCount ? 3 : undefined}
endIcon={withEndIcon ? <Eye /> : undefined}
/>
))}
</BoxTab.List>
{TABS.map((t) => (
<BoxTab.Panel key={t.value} value={t.value}>
<div className={panelStyle}>{t.body}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const BoxTabFillContainerExample = () => (
<div style={{ width: 480, border: '1px dashed #ccc' }}>
<Template size="md" fill />
</div>
);
export default BoxTabFillContainerExample;
WithCount
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import Eye from '@mildang/icons/react/eye';
import { css } from '@mildang/styled-system/css';
const TABS = [
{
value: 'tab1',
label: '역질문 트리거',
body: '역질문 트리거 탭의 내용입니다. 여기에 관련 콘텐츠가 표시됩니다.',
},
{
value: 'tab2',
label: '카테고리명',
body: '카테고리명 탭의 내용입니다. 탭을 클릭하면 이 영역이 바뀝니다.',
},
{
value: 'tab3',
label: '고정된 자료',
body: '고정된 자료 탭의 내용입니다. 드래그로 탭 순서도 바꿀 수 있어요.',
},
];
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const Template = ({
size,
fill,
activationMode,
withCount,
withEndIcon,
draggable,
}: {
size?: 'sm' | 'md' | 'lg';
fill?: boolean;
activationMode?: 'automatic' | 'manual';
withCount?: boolean;
withEndIcon?: boolean;
draggable?: boolean;
}) => {
const [order, setOrder] = useState(TABS.map((t) => t.value));
const [value, setValue] = useState('tab1');
const byValue = Object.fromEntries(TABS.map((t) => [t.value, t]));
return (
<BoxTab
size={size}
fill={fill}
activationMode={activationMode}
value={value}
onValueChange={setValue}
order={order}
onReorder={setOrder}
>
<BoxTab.List aria-label="예시 탭">
{order.map((v) => (
<BoxTab.Item
key={v}
value={v}
label={byValue[v].label}
draggable={draggable}
count={withCount ? 3 : undefined}
endIcon={withEndIcon ? <Eye /> : undefined}
/>
))}
</BoxTab.List>
{TABS.map((t) => (
<BoxTab.Panel key={t.value} value={t.value}>
<div className={panelStyle}>{t.body}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const BoxTabWithCountExample = () => <Template size="md" withCount />;
export default BoxTabWithCountExample;
WithEndIcon
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import Eye from '@mildang/icons/react/eye';
import { css } from '@mildang/styled-system/css';
const TABS = [
{
value: 'tab1',
label: '역질문 트리거',
body: '역질문 트리거 탭의 내용입니다. 여기에 관련 콘텐츠가 표시됩니다.',
},
{
value: 'tab2',
label: '카테고리명',
body: '카테고리명 탭의 내용입니다. 탭을 클릭하면 이 영역이 바뀝니다.',
},
{
value: 'tab3',
label: '고정된 자료',
body: '고정된 자료 탭의 내용입니다. 드래그로 탭 순서도 바꿀 수 있어요.',
},
];
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const Template = ({
size,
fill,
activationMode,
withCount,
withEndIcon,
draggable,
}: {
size?: 'sm' | 'md' | 'lg';
fill?: boolean;
activationMode?: 'automatic' | 'manual';
withCount?: boolean;
withEndIcon?: boolean;
draggable?: boolean;
}) => {
const [order, setOrder] = useState(TABS.map((t) => t.value));
const [value, setValue] = useState('tab1');
const byValue = Object.fromEntries(TABS.map((t) => [t.value, t]));
return (
<BoxTab
size={size}
fill={fill}
activationMode={activationMode}
value={value}
onValueChange={setValue}
order={order}
onReorder={setOrder}
>
<BoxTab.List aria-label="예시 탭">
{order.map((v) => (
<BoxTab.Item
key={v}
value={v}
label={byValue[v].label}
draggable={draggable}
count={withCount ? 3 : undefined}
endIcon={withEndIcon ? <Eye /> : undefined}
/>
))}
</BoxTab.List>
{TABS.map((t) => (
<BoxTab.Panel key={t.value} value={t.value}>
<div className={panelStyle}>{t.body}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const BoxTabWithEndIconExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div>
<div className={captionStyle}>기본 — 좌우 여백 20px</div>
<Template size="md" />
</div>
<div>
<div className={captionStyle}>endIcon 있음 — 우측 여백이 4px로 좁아진다</div>
<Template size="md" withEndIcon />
</div>
</div>
);
export default BoxTabWithEndIconExample;
Deletable
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import { css } from '@mildang/styled-system/css';
const TABS = [
{
value: 'tab1',
label: '역질문 트리거',
body: '역질문 트리거 탭의 내용입니다. 여기에 관련 콘텐츠가 표시됩니다.',
},
{
value: 'tab2',
label: '카테고리명',
body: '카테고리명 탭의 내용입니다. 탭을 클릭하면 이 영역이 바뀝니다.',
},
{
value: 'tab3',
label: '고정된 자료',
body: '고정된 자료 탭의 내용입니다. 드래그로 탭 순서도 바꿀 수 있어요.',
},
];
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const BoxTabDeletableExample = () => {
const [order, setOrder] = useState(TABS.map((t) => t.value));
const [value, setValue] = useState('tab1');
const byValue = Object.fromEntries(TABS.map((t) => [t.value, t]));
return (
<BoxTab value={value} onValueChange={setValue} order={order} onReorder={setOrder}>
<BoxTab.List aria-label="삭제 가능한 탭">
{order.map((v) => (
<BoxTab.Item
key={v}
value={v}
label={byValue[v].label}
deletable
onDelete={(dv) => {
setOrder((prev) => prev.filter((x) => x !== dv));
// 선택된 탭을 지우면 선택도 이웃으로 옮긴다.
// 안 하면 value 가 사라진 탭을 가리켜 aria-selected 전부 false + Panel 이 빈다.
if (dv === value) {
const idx = order.indexOf(dv);
const remaining = order.filter((x) => x !== dv);
const neighbor = remaining[idx] ?? remaining[idx - 1];
if (neighbor) setValue(neighbor);
}
}}
/>
))}
</BoxTab.List>
{order.map((v) => (
<BoxTab.Panel key={v} value={v}>
<div className={panelStyle}>{byValue[v].body}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
export default BoxTabDeletableExample;
MaxWidth
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import { css } from '@mildang/styled-system/css';
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const LONG = [
{ value: 'l1', label: '제목이 엄청나게 길어진 경우의 탭 라벨은 이렇게 잘립니다' },
{ value: 'l2', label: 'Verrrry Loooonng Tab Label Text That Never Ends' },
{ value: 'l3', label: '짧은 탭' },
];
const LongLabelTabs = ({ maxWidth }: { maxWidth?: number }) => {
const [order, setOrder] = useState(LONG.map((t) => t.value));
const [value, setValue] = useState('l1');
const byValue = Object.fromEntries(LONG.map((t) => [t.value, t]));
return (
<BoxTab value={value} onValueChange={setValue} order={order} onReorder={setOrder}>
<BoxTab.List aria-label="긴 라벨 탭">
{order.map((v) => (
<BoxTab.Item key={v} value={v} label={byValue[v].label} maxWidth={maxWidth} />
))}
</BoxTab.List>
{order.map((v) => (
<BoxTab.Panel key={v} value={v}>
<div className={panelStyle}>{byValue[v].label}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const BoxTabMaxWidthExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div>
<div className={captionStyle}>기본값 — maxWidth 미지정 (280px)</div>
<LongLabelTabs />
</div>
<div>
<div className={captionStyle}>maxWidth={'{160}'} 으로 좁게 덮어쓴 경우</div>
<LongLabelTabs maxWidth={160} />
</div>
</div>
);
export default BoxTabMaxWidthExample;
Overflow
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import { css } from '@mildang/styled-system/css';
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const MANY = Array.from({ length: 10 }, (_, i) => ({ value: `t${i}`, label: `탭 항목 ${i + 1}` }));
const ManyTabs = ({ fill }: { fill?: boolean }) => {
const [order, setOrder] = useState(MANY.map((t) => t.value));
const [value, setValue] = useState('t0');
const byValue = Object.fromEntries(MANY.map((t) => [t.value, t]));
return (
<BoxTab fill={fill} value={value} onValueChange={setValue} order={order} onReorder={setOrder}>
<BoxTab.List aria-label="많은 탭">
{order.map((v) => (
<BoxTab.Item key={v} value={v} label={byValue[v].label} />
))}
</BoxTab.List>
{order.map((v) => (
<BoxTab.Panel key={v} value={v}>
<div className={panelStyle}>{byValue[v].label} 콘텐츠</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const BoxTabOverflowExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div style={{ width: 360, border: '1px dashed #ccc', padding: 8 }}>
<div className={captionStyle}>fit-contents (기본)</div>
<ManyTabs />
</div>
<div style={{ width: 360, border: '1px dashed #ccc', padding: 8 }}>
<div className={captionStyle}>fill-container — 여기서도 이동 버튼이 나와야 한다</div>
<ManyTabs fill />
</div>
</div>
);
export default BoxTabOverflowExample;
KeyboardNavigation
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import { css } from '@mildang/styled-system/css';
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const KEYBOARD_TABS = [
{ value: 'k1', label: '첫 번째 탭' },
{ value: 'k2', label: '두 번째 탭' },
{ value: 'k3', label: '비활성 탭', disabled: true },
{ value: 'k4', label: '네 번째 탭' },
{ value: 'k5', label: '다섯 번째 탭' },
];
const BoxTabKeyboardNavigationExample = () => {
const [value, setValue] = useState('k1');
return (
<div>
<div className={captionStyle}>
탭을 클릭해 포커스를 준 뒤 ← / → / Home / End 를 눌러 보세요 (비활성 탭은 건너뜀 · 양끝 순환)
</div>
<BoxTab value={value} onValueChange={setValue}>
<BoxTab.List aria-label="키보드 이동 예시">
{KEYBOARD_TABS.map((t) => (
<BoxTab.Item key={t.value} value={t.value} label={t.label} disabled={t.disabled} />
))}
</BoxTab.List>
{KEYBOARD_TABS.map((t) => (
<BoxTab.Panel key={t.value} value={t.value}>
<div className={panelStyle}>{t.label} 패널입니다. Tab 키로 이 영역까지 도달할 수 있습니다.</div>
</BoxTab.Panel>
))}
</BoxTab>
<button type="button" style={{ marginTop: 24 }}>
탭바 다음 요소 (Tab 순서 확인용)
</button>
</div>
);
};
export default BoxTabKeyboardNavigationExample;
LongLabelOverflow
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
const BoxTabLongLabelOverflowExample = () => {
const [value, setValue] = useState('g1');
const groups = [
{ value: 'g1', label: '2026학년도 1학기 중등 내신대비 집중반' },
{ value: 'g2', label: '고3 수능 파이널 실전 모의고사반' },
{ value: 'g3', label: '초등 저학년 파닉스' },
{ value: 'g4', label: '중등 문법 심화 클리닉 화목반' },
{ value: 'g5', label: '전체보기' },
];
return (
<div style={{ width: 420, border: '1px dashed #ccc', padding: 8 }}>
<BoxTab size="lg" value={value} onValueChange={setValue}>
<BoxTab.List aria-label="긴 라벨 + 오버플로우">
{groups.map((g) => (
<BoxTab.Item key={g.value} value={g.value} label={g.label} count={12} />
))}
</BoxTab.List>
</BoxTab>
</div>
);
};
export default BoxTabLongLabelOverflowExample;
WithoutPanel
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import { css } from '@mildang/styled-system/css';
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const BoxTabWithoutPanelExample = () => {
const [value, setValue] = useState('w1');
return (
<div>
<BoxTab size="lg" value={value} onValueChange={setValue}>
<BoxTab.List aria-label="패널 없는 탭">
{['w1', 'w2', 'w3'].map((v, i) => (
<BoxTab.Item key={v} value={v} label={`그룹 ${i + 1}`} />
))}
</BoxTab.List>
</BoxTab>
<div className={panelStyle}>
선택된 값: <code>{value}</code> — 이 영역은 BoxTab.Panel이 아니라 화면이 직접 그린 콘텐츠입니다.
</div>
</div>
);
};
export default BoxTabWithoutPanelExample;
EdgeCases
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import { css } from '@mildang/styled-system/css';
const captionStyle = css({ pb: '8', textStyle: 'caption-lg', color: 'primary.text.lowest' });
const BoxTabEdgeCasesExample = () => {
const [single, setSingle] = useState('only');
const [unmatched, setUnmatched] = useState('존재하지-않는-값');
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 32 }}>
<div>
<div className={captionStyle}>탭이 1개일 때 — 단일 그룹 화면</div>
<BoxTab size="lg" value={single} onValueChange={setSingle}>
<BoxTab.List aria-label="단일 탭">
<BoxTab.Item value="only" label="중등 내신대비반" count={8} />
</BoxTab.List>
</BoxTab>
</div>
<div>
<div className={captionStyle}>
선택값이 어떤 탭과도 매칭되지 않을 때 — 그래도 Tab으로 탭바에 들어갈 수 있어야 한다
</div>
<BoxTab value={unmatched} onValueChange={setUnmatched}>
<BoxTab.List aria-label="선택값 미매칭">
{['e1', 'e2', 'e3'].map((v, i) => (
<BoxTab.Item key={v} value={v} label={`탭 ${i + 1}`} />
))}
</BoxTab.List>
</BoxTab>
</div>
<div>
<div className={captionStyle}>
첫 탭이 비활성일 때 — 폴백은 첫 번째 "활성" 탭이어야 한다
</div>
<BoxTab value="없음">
<BoxTab.List aria-label="첫 탭 비활성">
<BoxTab.Item value="d1" label="비활성 탭" disabled />
<BoxTab.Item value="d2" label="두 번째 탭" />
<BoxTab.Item value="d3" label="세 번째 탭" />
</BoxTab.List>
</BoxTab>
</div>
</div>
);
};
export default BoxTabEdgeCasesExample;
Demo
import { useState } from 'react';
import { BoxTab } from '@mildang/design-system/BoxTab';
import Eye from '@mildang/icons/react/eye';
import { css } from '@mildang/styled-system/css';
const TABS = [
{
value: 'tab1',
label: '역질문 트리거',
body: '역질문 트리거 탭의 내용입니다. 여기에 관련 콘텐츠가 표시됩니다.',
},
{
value: 'tab2',
label: '카테고리명',
body: '카테고리명 탭의 내용입니다. 탭을 클릭하면 이 영역이 바뀝니다.',
},
{
value: 'tab3',
label: '고정된 자료',
body: '고정된 자료 탭의 내용입니다. 드래그로 탭 순서도 바꿀 수 있어요.',
},
];
const panelStyle = css({ pt: '16', textStyle: 'body-lg', color: 'neutral.text.base' });
const Template = ({
size,
fill,
activationMode,
withCount,
withEndIcon,
draggable,
}: {
size?: 'sm' | 'md' | 'lg';
fill?: boolean;
activationMode?: 'automatic' | 'manual';
withCount?: boolean;
withEndIcon?: boolean;
draggable?: boolean;
}) => {
const [order, setOrder] = useState(TABS.map((t) => t.value));
const [value, setValue] = useState('tab1');
const byValue = Object.fromEntries(TABS.map((t) => [t.value, t]));
return (
<BoxTab
size={size}
fill={fill}
activationMode={activationMode}
value={value}
onValueChange={setValue}
order={order}
onReorder={setOrder}
>
<BoxTab.List aria-label="예시 탭">
{order.map((v) => (
<BoxTab.Item
key={v}
value={v}
label={byValue[v].label}
draggable={draggable}
count={withCount ? 3 : undefined}
endIcon={withEndIcon ? <Eye /> : undefined}
/>
))}
</BoxTab.List>
{TABS.map((t) => (
<BoxTab.Panel key={t.value} value={t.value}>
<div className={panelStyle}>{t.body}</div>
</BoxTab.Panel>
))}
</BoxTab>
);
};
const STORY_DEFAULT_ARGS = { ...({}), ...({ size: 'md', fill: false, activationMode: 'automatic' }) } as { value?: string | undefined; onValueChange?: ((value: string) => void) | undefined; order?: string[] | undefined; onReorder?: ((order: string[]) => void) | undefined; activationMode?: "automatic" | "manual" | undefined; className?: string | undefined; children?: import("react").ReactNode; size?: import("@mildang/styled-system/types").ConditionalValue<"sm" | "md" | "lg"> | undefined; fill?: import("@mildang/styled-system/types").ConditionalValue<boolean> | undefined; };
const BoxTabDemoExampleRender = (args: { value?: string | undefined; onValueChange?: ((value: string) => void) | undefined; order?: string[] | undefined; onReorder?: ((order: string[]) => void) | undefined; activationMode?: "automatic" | "manual" | undefined; className?: string | undefined; children?: import("react").ReactNode; size?: import("@mildang/styled-system/types").ConditionalValue<"sm" | "md" | "lg"> | undefined; fill?: import("@mildang/styled-system/types").ConditionalValue<boolean> | undefined; }) => (
<Template
size={args.size as 'sm' | 'md' | 'lg'}
fill={args.fill as boolean}
activationMode={args.activationMode}
draggable
/>
);
export default function BoxTabDemoExample(props: Partial<{ value?: string | undefined; onValueChange?: ((value: string) => void) | undefined; order?: string[] | undefined; onReorder?: ((order: string[]) => void) | undefined; activationMode?: "automatic" | "manual" | undefined; className?: string | undefined; children?: import("react").ReactNode; size?: import("@mildang/styled-system/types").ConditionalValue<"sm" | "md" | "lg"> | undefined; fill?: import("@mildang/styled-system/types").ConditionalValue<boolean> | undefined; }>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as { value?: string | undefined; onValueChange?: ((value: string) => void) | undefined; order?: string[] | undefined; onReorder?: ((order: string[]) => void) | undefined; activationMode?: "automatic" | "manual" | undefined; className?: string | undefined; children?: import("react").ReactNode; size?: import("@mildang/styled-system/types").ConditionalValue<"sm" | "md" | "lg"> | undefined; fill?: import("@mildang/styled-system/types").ConditionalValue<boolean> | undefined; };
return BoxTabDemoExampleRender(mergedProps);
}