lumir-design-system-02
Version:
Lumir Design System 02 - Modern & Friendly
1,913 lines • 94.7 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { Surface, Frame, Icon, Text, Sizing, Asterisk, Divider } from 'lumir-design-system-shared';
import React, { forwardRef, useState, useRef, useEffect, createContext, useContext } from 'react';
const Badge = ({ color, size = 'md', layout = 'only text', style = 'filled', type = 'round-square', text, iconName, iconType = 'line', className, disabled, }) => {
// 크기별 텍스트 variant 매핑
const getTextVariant = (size) => {
switch (size) {
case 'lg':
return 'label-1'; // label-1 medium
case 'md':
return 'label-2'; // label-2 medium
case 'sm':
return 'caption-2'; // caption-2 medium
}
};
// 크기별 아이콘-텍스트 간격 매핑 (Frame gap) - System-02: Modern & Friendly
const getContentGap = (size) => {
switch (size) {
case 'lg': return 'xs'; // xxs → xs (더 넓은 간격)
case 'md': return 'xs'; // xxs → xs (더 넓은 간격)
case 'sm': return 'xxs'; // 작은 크기는 유지
}
};
// 스타일별 배경색 결정
const getBackgroundColor = () => {
if (style === 'filled') {
switch (color) {
case 'primary': return 'primary-system02-1-rest';
case 'secondary': return 'secondary-system02-1-rest';
case 'cta': return 'cta-system02-1-rest';
case 'error': return 'error';
case 'warning': return 'warning';
default: return 'primary-system02-1-rest';
}
}
return undefined; // outlined, transparent 스타일은 배경 투명
};
// 스타일별 테두리 색상 결정
const getBorderColor = () => {
if (style === 'outlined') {
switch (color) {
case 'primary': return 'primary-system02-1-rest';
case 'secondary': return 'secondary-system02-1-rest';
case 'cta': return 'cta-system02-1-rest';
case 'error': return 'error';
case 'warning': return 'warning';
default: return 'primary-system02-1-rest';
}
}
return undefined; // filled, transparent 스타일은 테두리 없음
};
// 스타일별 전경색 결정 (텍스트와 아이콘 모두 적용)
const getForegroundColor = () => {
// disabled 상태 처리
if (disabled)
return 'secondary-system02-2-disabled';
if (style === 'filled') {
switch (color) {
case 'primary':
case 'cta':
// primary, cta filled일 때는 oncolor 사용
return 'primary-system02-oncolor';
case 'error':
case 'warning':
return 'secondary-system02-oncolor';
case 'secondary': return 'secondary-system02-2-rest';
default: return 'primary-system02-oncolor';
}
}
else {
// outlined, transparent 스타일
switch (color) {
case 'primary': return 'primary-system02-1-rest';
case 'secondary': return 'secondary-system02-1-rest';
case 'cta': return 'cta-system02-1-rest';
case 'error': return 'error';
case 'warning': return 'warning';
default: return 'secondary-system02-1-rest';
}
}
};
// 형태별 border radius 매핑 (System-02: 더 둥근 모서리)
const getBorderRadius = (type) => {
return type === 'circle' ? 'circular' : 'xl'; // System-02 가이드라인: xl 사용 (더 친근한 느낌)
};
// 크기별 아이콘 크기 매핑
const getIconSize = (size) => {
switch (size) {
case 'lg': return 'sm'; // 16px
case 'md': return 'sm'; // 16px
case 'sm': return 'xs'; // 12px
}
};
const backgroundColor = getBackgroundColor();
const borderColor = getBorderColor();
const foregroundColor = getForegroundColor();
const textVariant = getTextVariant(size);
const iconSize = getIconSize(size);
const contentGap = getContentGap(size);
const borderRadius = getBorderRadius(type);
return (jsx(Surface, { background: backgroundColor, foreground: foregroundColor, borderColor: borderColor, borderWidth: borderColor ? 'thin' : undefined, borderStyle: borderColor ? 'solid' : 'none', borderRadius: borderRadius, className: className, style: { width: 'fit-content', height: 'fit-content' }, children: jsxs(Frame, { display: "flex", direction: "row", align: "center", justify: "center", gap: contentGap, padding: "xxs" // System-02: Badge는 컴팩트하게 유지 (xxs)
, children: [(layout === 'text+icon' || layout === 'only icon') && iconName && (jsx(Icon, { name: iconName, size: iconSize })), (layout === 'text+icon' || layout === 'only text') && text && (jsx(Text, { variant: textVariant, weight: "regular" // System-02: medium → regular (더 부드러운 느낌)
, children: text }))] }) }));
};
/**
* Button 컴포넌트는 사용자 상호작용을 위한 기본적인 요소입니다.
* Lumir Design System의 스타일링을 따르며, 시맨틱 토큰을 사용하여 일관된 디자인을 제공합니다.
*/
const Button = forwardRef(({ variant = 'filled', buttonType = 'text-only', colorScheme = 'primary', size = 'md', isFullWidth = false, textAlign = 'center', isLoading = false, isSelected = false, disabled = false, leftIcon, rightIcon, className, children, ...rest }, ref) => {
// 마우스 상태 관리 추가
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
// 마우스 이벤트 핸들러들
const handleMouseEnter = () => {
if (!disabled && !isLoading) {
setIsHovered(true);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
setIsPressed(false);
};
const handleMouseDown = () => {
if (!disabled && !isLoading) {
setIsPressed(true);
}
};
const handleMouseUp = () => {
setIsPressed(false);
};
const resolveButtonType = () => {
if (buttonType === 'icon-only' && (leftIcon || rightIcon)) {
return 'icon-only';
}
if (buttonType === 'text-icon' && (leftIcon || rightIcon)) {
return 'text-icon';
}
return 'text-only';
};
const actualButtonType = resolveButtonType();
// 크기별 높이 토큰 결정 (Sizing 컴포넌트용)
const getSizingHeight = () => {
switch (size) {
case 'xs': return 'sm'; // 24px
case 'sm': return 'md'; // 28px
case 'md': return 'lg'; // 32px
case 'lg': return 'xl'; // 36px
case 'xl': return 'xxl'; // 40px
default: return 'lg';
}
};
// 크기별 텍스트 variant 결정 (텍스트: xl(16), lg(16), md(14), sm(14), xs(12))
const getTextVariant = () => {
switch (size) {
case 'xs': return 'label-2'; // 12px
case 'sm': return 'body-2'; // 14px
case 'md': return 'body-2'; // 14px
case 'lg': return 'body-1'; // 16px
case 'xl': return 'body-1'; // 16px
default: return 'body-2';
}
};
// 크기별 아이콘 크기 결정 (System-02: 더 큰 아이콘 사용)
const getIconSize = () => {
switch (size) {
case 'xs': return 'xs'; // 12px
case 'sm': return 'sm'; // 16px (xs → sm)
case 'md': return 'md'; // 20px (xs → md)
case 'lg': return 'lg'; // 24px (sm → lg)
case 'xl': return 'xl'; // 28px (md → xl)
default: return 'md';
}
};
// 크기별 패딩 결정 (System-02: Modern & Friendly - 더 여유로운 패딩)
const getPadding = () => {
switch (size) {
case 'xs': return 'sm'; // xs → sm (더 여유로운)
case 'sm': return 'sm'; // xs → sm
case 'md': return 'md'; // sm → md
case 'lg': return 'lg'; // md → lg
case 'xl': return 'xl'; // lg → xl
default: return 'md';
}
};
// 배경색 결정 (hover/active 상태 포함)
const getBackgroundColor = () => {
const currentState = getCurrentState();
if (variant === 'filled') {
if (colorScheme === 'primary') {
if (currentState === 'disabled')
return 'primary-system02-1-disabled';
if (currentState === 'pressed')
return 'primary-system02-1-pressed';
if (currentState === 'hovered')
return 'primary-system02-1-hovered';
return 'primary-system02-1-rest';
}
if (colorScheme === 'secondary') {
if (currentState === 'disabled')
return 'secondary-system02-2-disabled';
if (currentState === 'pressed')
return 'secondary-system02-2-pressed';
if (currentState === 'hovered')
return 'secondary-system02-2-hovered';
return 'secondary-system02-2-rest';
}
if (colorScheme === 'cta') {
if (currentState === 'disabled')
return 'cta-system02-1-disabled';
if (currentState === 'pressed')
return 'cta-system02-1-pressed';
if (currentState === 'hovered')
return 'cta-system02-1-hovered';
return 'cta-system02-1-rest';
}
}
if (variant === 'outlined') {
// outlined는 연한 배경색 위계 사용 (background-3 = 더 subtle)
if (currentState === 'rest' || currentState === 'disabled')
return undefined;
if (currentState === 'hovered') {
if (colorScheme === 'primary')
return 'secondary-system02-3-hovered'; // 연한 배경
if (colorScheme === 'secondary')
return 'secondary-system02-3-hovered';
if (colorScheme === 'cta')
return 'secondary-system02-3-hovered';
}
if (currentState === 'pressed') {
if (colorScheme === 'primary')
return 'secondary-system02-3-pressed'; // 연한 배경
if (colorScheme === 'secondary')
return 'secondary-system02-3-pressed';
if (colorScheme === 'cta')
return 'secondary-system02-3-pressed';
}
return undefined;
}
if (variant === 'transparent') {
// transparent는 항상 배경색 없음 (텍스트 색상만 변경)
return undefined;
}
return 'primary-system02-1-rest';
};
// 전경색 결정 (hover/active 상태 포함)
const getForegroundColor = () => {
const currentState = getCurrentState();
if (variant === 'filled') {
// filled는 onColor 사용 (배경색 위에 올라가는 텍스트)
if (colorScheme === 'primary') {
if (currentState === 'disabled')
return 'secondary-system02-3-disabled'; // 대비되는 disabled 색상 사용
return 'primary-system02-oncolor'; // onColor 사용
}
if (colorScheme === 'secondary') {
if (currentState === 'disabled')
return 'secondary-system02-3-disabled'; // 대비되는 disabled 색상 사용
return 'secondary-system02-oncolor'; // onColor 사용
}
if (colorScheme === 'cta') {
if (currentState === 'disabled')
return 'secondary-system02-3-disabled'; // 대비되는 disabled 색상 사용
return 'cta-system02-oncolor'; // onColor 사용
}
}
if (variant === 'outlined' || variant === 'transparent') {
// outlined/transparent는 일반 foreground 색상 사용
if (currentState === 'disabled') {
return 'secondary-system02-2-disabled';
}
// hover/pressed 상태에서는 더 진한 색상 사용
if (currentState === 'hovered') {
if (colorScheme === 'primary')
return 'primary-system02-1-hovered'; // 연한 색상 위계
if (colorScheme === 'secondary')
return 'secondary-system02-1-hovered';
if (colorScheme === 'cta')
return 'cta-system02-1-hovered';
}
if (currentState === 'pressed') {
if (colorScheme === 'primary')
return 'primary-system02-1-pressed';
if (colorScheme === 'secondary')
return 'secondary-system02-1-pressed';
if (colorScheme === 'cta')
return 'cta-system02-1-pressed';
}
// rest 상태
if (colorScheme === 'primary')
return 'primary-system02-1-rest';
if (colorScheme === 'secondary')
return 'secondary-system02-1-rest';
if (colorScheme === 'cta')
return 'cta-system02-1-rest';
return 'secondary-system02-2-disabled';
}
return 'primary-system02-1-rest';
};
// 테두리 속성 결정 (outlined 버튼용)
const getBorderProps = () => {
if (variant !== 'outlined')
return {};
const currentState = getCurrentState();
if (colorScheme === 'primary') {
let borderColor = 'secondary-system02-2-rest';
if (currentState === 'disabled')
borderColor = 'secondary-system02-3-disabled';
else if (currentState === 'pressed')
borderColor = 'primary-system02-1-pressed';
else if (currentState === 'hovered')
borderColor = 'primary-system02-1-hovered';
else
borderColor = 'primary-system02-1-rest';
return { borderWidth: 'thin', borderColor };
}
if (colorScheme === 'secondary') {
let borderColor = 'secondary-system02-2-rest';
if (currentState === 'disabled')
borderColor = 'secondary-system02-2-disabled';
else if (currentState === 'pressed')
borderColor = 'secondary-system02-1-pressed';
else if (currentState === 'hovered')
borderColor = 'secondary-system02-1-hovered';
else
borderColor = 'secondary-system02-1-rest';
return { borderWidth: 'thin', borderColor };
}
if (colorScheme === 'cta') {
let borderColor = 'secondary-system02-2-rest';
if (currentState === 'disabled')
borderColor = 'secondary-system02-3-disabled';
else if (currentState === 'pressed')
borderColor = 'cta-system02-1-pressed';
else if (currentState === 'hovered')
borderColor = 'cta-system02-1-hovered';
else
borderColor = 'cta-system02-1-rest';
return { borderWidth: 'thin', borderColor };
}
return { borderWidth: 'thin', borderColor: 'secondary-system02-2-rest' };
};
// 현재 상태 결정
const getCurrentState = () => {
if (disabled || isLoading)
return 'disabled';
if (isPressed)
return 'pressed';
if (isHovered)
return 'hovered';
return 'rest';
};
// full width일 때 정렬 방식 결정
const getJustifyContent = () => {
if (!isFullWidth)
return 'center';
switch (textAlign) {
case 'left': return 'flex-start';
case 'right': return 'flex-end';
case 'center':
default: return 'center';
}
};
// onClick과 style을 제외한 나머지 props
const { onClick, style: customStyle, color, ...otherProps } = rest;
const borderProps = getBorderProps();
return (jsx(Surface, { ref: ref, background: getBackgroundColor(), foreground: getForegroundColor(), borderRadius: "xl" // System-02: md → lg (더 둥근 모서리)
, borderWidth: borderProps.borderWidth, borderColor: borderProps.borderColor, borderStyle: borderProps.borderWidth ? 'solid' : 'none', boxShadow: "20" // System-02: 부드러운 그림자 추가
, className: className, onClick: disabled || isLoading ? undefined : onClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, style: {
cursor: disabled || isLoading ? 'not-allowed' : 'pointer',
...customStyle
}, ...otherProps, children: jsx(Sizing, { height: getSizingHeight(), width: isFullWidth ? '100%' : 'fit-content', children: jsxs(Frame, { display: "flex", direction: "row", align: "center", justify: getJustifyContent(), gap: "md" // System-02: sm → md (더 넓은 간격)
, fill: true, padding: getPadding(), children: [isLoading && (jsx(Icon, { name: "LineIconsMenuMenuIcon", size: getIconSize(), "aria-hidden": true })), leftIcon && !isLoading && (jsx(Icon, { name: leftIcon, size: getIconSize(), "aria-hidden": true })), (actualButtonType !== 'icon-only' && children) && (jsx(Text, { as: "span", variant: getTextVariant(), weight: "regular" // System-02: medium → regular (더 부드러운 느낌)
, children: children })), rightIcon && !isLoading && (jsx(Icon, { name: rightIcon, size: getIconSize(), "aria-hidden": true }))] }) }) }));
});
Button.displayName = 'Button';
/**
* TextDisplay 컴포넌트는 다양한 텍스트 요소들을 조합하여 표시하는 컴파운드 컴포넌트입니다.
* 라벨, 메인 텍스트, 설명, 캡션을 선택적으로 표시할 수 있으며, 아이콘도 함께 표시할 수 있습니다.
*
* @example
* ```tsx
* // 기본 사용법
* <TextDisplay
* primaryText="Hello World"
* />
*
* // 모든 요소 포함
* <TextDisplay
* size="lg"
* style="center"
* iconPosition="above"
* iconName="star-filled"
* iconType="flat"
* showLabel={true}
* labelText="Status"
* primaryText="Success"
* showDescription={true}
* descriptionText="Your operation completed successfully"
* showCaption={true}
* captionText="Last updated: 2024-01-01"
* />
*
* // 좌측 정렬 + 앞쪽 아이콘
* <TextDisplay
* style="left"
* iconPosition="before"
* iconName="info"
* primaryText="Information"
* showDescription={true}
* descriptionText="Additional details here"
* />
* ```
*/
const TextDisplay = forwardRef(({ size = 'md', style = 'left', iconPosition = 'none', iconName = 'LineIconsStarFilledIcon', iconType = 'line', showLabel = false, labelText = 'label', primaryText, showDescription = false, descriptionText = 'description text', showCaption = false, captionText = 'caption', className, ...rest }, ref) => {
// 크기별 텍스트 variant 매핑 (피그마 실제 크기 기준 - 수정됨)
const getTextVariants = (size) => {
switch (size) {
case 'xlg':
return {
label: 'body-2', // 14px (피그마는 15px이지만 가장 근접한 토큰)
primary: 'title-2', // 24px (lg의 20px에서 자연스러운 다음 단계)
description: 'heading-3', // 16px (피그마는 18px이지만 가장 근접한 토큰)
caption: 'body-2', // 14px (피그마는 15px이지만 가장 근접한 토큰)
};
case 'lg':
return {
label: 'label-1', // 14px (정확히 일치)
primary: 'heading-1', // 20px (md와 lg 사이의 자연스러운 중간 단계)
description: 'body-1', // 16px (정확히 일치)
caption: 'label-1', // 14px (정확히 일치)
};
case 'md':
return {
label: 'label-2', // 12px (피그마는 13px이지만 가장 근접한 토큰)
primary: 'heading-3', // 16px (피그마는 18px이지만 가장 근접한 토큰)
description: 'label-1', // 14px (정확히 일치)
caption: 'label-2', // 12px (피그마는 13px이지만 가장 근접한 토큰)
};
case 'sm':
return {
label: 'caption-1', // 12px (정확히 일치)
primary: 'body-1', // 16px (정확히 일치)
description: 'label-1', // 14px (정확히 일치)
caption: 'caption-1', // 12px (정확히 일치)
};
case 'xsm':
return {
label: 'caption-1', // 12px (정확히 일치)
primary: 'label-1', // 14px (정확히 일치)
description: 'caption-1', // 12px (정확히 일치)
caption: 'caption-3', // 11px (정확히 일치)
};
}
};
// 크기별 font weight 매핑 (피그마 기준)
const getTextWeights = (size) => {
switch (size) {
case 'xlg':
return {
label: 'regular', // 400
primary: 'bold', // 700
description: 'regular', // 400
caption: 'regular', // 400
};
case 'lg':
return {
label: 'regular', // 400
primary: 'bold', // 700
description: 'regular', // 400
caption: 'regular', // 400
};
case 'md':
return {
label: 'regular', // 400
primary: 'bold', // 700
description: 'regular', // 400
caption: 'regular', // 400
};
case 'sm':
return {
label: 'regular', // 400
primary: 'bold', // 700
description: 'regular', // 400
caption: 'regular', // 400
};
case 'xsm':
return {
label: 'regular', // 400
primary: 'bold', // 700
description: 'regular', // 400
caption: 'regular', // 400
};
}
};
// 크기별 아이콘 크기 매핑
const getIconSize = (size) => {
switch (size) {
case 'xlg': return 'lg';
case 'lg': return 'md';
case 'md': return 'sm';
case 'sm': return 'xs';
case 'xsm': return 'xs';
}
};
// Surface foreground 색상 결정
const getForegroundColors = () => {
return {
label: 'secondary-system02-2-rest',
primary: 'secondary-system02-1-rest',
description: 'secondary-system02-2-rest',
caption: 'secondary-system02-3-rest',
};
};
const textVariants = getTextVariants(size);
const textWeights = getTextWeights(size);
const iconSize = getIconSize(size);
const foregroundColors = getForegroundColors();
// 아이콘 렌더링
const renderIcon = () => {
if (iconPosition === 'none' || !iconName)
return null;
return (jsx(Icon, { name: iconName, size: iconSize }));
};
// 텍스트 콘텐츠 렌더링
const renderTextContent = () => {
const alignItems = style === 'center' ? 'center' : 'flex-start';
const textAlign = style === 'center' ? 'center' : 'left';
return (jsxs(Frame, { display: "flex", direction: "column", gap: "xxxs", align: alignItems, children: [showLabel && labelText && (jsx(Surface, { foreground: foregroundColors.label, children: jsx(Text, { variant: textVariants.label, weight: textWeights.label, textAlign: textAlign, children: labelText }) })), primaryText && (jsx(Surface, { foreground: foregroundColors.primary, children: jsx(Text, { variant: textVariants.primary, weight: textWeights.primary, textAlign: textAlign, children: primaryText }) })), showDescription && descriptionText && (jsx(Surface, { foreground: foregroundColors.description, children: jsx(Text, { variant: textVariants.description, weight: textWeights.description, textAlign: textAlign, children: descriptionText }) })), showCaption && captionText && (jsx(Surface, { foreground: foregroundColors.caption, children: jsx(Text, { variant: textVariants.caption, weight: textWeights.caption, textAlign: textAlign, children: captionText }) }))] }));
};
// 레이아웃 구성
if (iconPosition === 'above') {
// 아이콘이 위에 있는 경우 (세로 레이아웃)
return (jsx("div", { ref: ref, className: className, ...rest, children: jsxs(Frame, { display: "flex", direction: "column", gap: "sm", align: "center", children: [renderIcon(), renderTextContent()] }) }));
}
else if (iconPosition === 'before' || iconPosition === 'after') {
// 아이콘이 앞뒤에 있는 경우 (가로 레이아웃)
return (jsx("div", { ref: ref, className: className, ...rest, children: jsxs(Frame, { display: "flex", direction: "row", gap: "sm", align: "center", children: [iconPosition === 'before' && renderIcon(), renderTextContent(), iconPosition === 'after' && renderIcon()] }) }));
}
else {
// 아이콘이 없는 경우
return (jsx("div", { ref: ref, className: className, ...rest, children: renderTextContent() }));
}
});
TextDisplay.displayName = 'TextDisplay';
const Card = ({ variant = "filled", title, badge, showBadge = true, description, actionButton, showActionButton = true, imageUrl, showImage = true, imageHeight = "242", imageObjectFit = "cover", imageObjectPosition = "center", imageAspectRatio, size, textStyle, showLabel, labelText, showDescription, showCaption, captionText, onClick, className, width = '100%', minWidth, maxWidth, // maxWidth 제한 제거 - Grid에서 자유롭게 확장 가능
badgeText, badgeColor, badgeIconName, badgeSize, badgeStyle, badgeLayout, badgeType,
// Button props
buttonVariant = "filled", buttonType = "icon-only", buttonColorScheme = "primary", buttonSize = "md", buttonPosition = "top-right", buttonText, buttonIcon, buttonRightIcon, buttonIsFullWidth = false, buttonIsSelected = false, }) => {
const handleClick = () => {
if (onClick) {
onClick();
}
};
const handleActionClick = (e) => {
e.stopPropagation();
if (actionButton === null || actionButton === void 0 ? void 0 : actionButton.onClick) {
actionButton.onClick(e);
}
};
// Variant에 따른 스타일 설정
const getCardStyles = () => {
switch (variant) {
case 'filled':
return {
background: "secondary-system02-1-rest",
borderColor: "secondary-system02-2-rest",
borderWidth: "thin",
borderStyle: "solid",
boxShadow: "20", // Foundation shadow 토큰 사용
};
case 'outlined':
return {
background: undefined,
borderColor: "secondary-system02-2-rest",
borderWidth: "thin",
borderStyle: "solid",
boxShadow: undefined,
};
case 'transparent':
return {
background: undefined,
borderColor: undefined,
borderWidth: undefined,
borderStyle: undefined,
boxShadow: undefined,
};
default:
return {
background: "secondary-system02-1-rest",
borderColor: "secondary-system02-2-rest",
borderWidth: "thin",
borderStyle: "solid",
boxShadow: "20", // 더 자연스러운 그림자를 위해 20 사용
};
}
};
const cardStyles = getCardStyles();
return (jsx(Sizing, { width: width, minWidth: minWidth, maxWidth: maxWidth, className: className, children: jsx(Surface, { borderRadius: variant === 'transparent' ? undefined : "xl", background: cardStyles.background, borderColor: cardStyles.borderColor, borderWidth: cardStyles.borderWidth, borderStyle: cardStyles.borderStyle, boxShadow: cardStyles.boxShadow, onClick: handleClick, children: jsxs(Frame, { display: "flex", direction: "column", gap: "xxl", padding: variant === 'transparent' ? undefined : "lg", children: [showImage && (jsx(Sizing, { height: imageHeight, aspectRatio: imageAspectRatio, children: jsx(Surface, { borderRadius: "lg" // 10px (피그마와 동일)
, children: imageUrl ? (jsx(Sizing, { width: "100%", height: "100%", objectFit: imageObjectFit, objectPosition: imageObjectPosition, children: jsx("img", { src: imageUrl, alt: title, style: {
width: '100%',
height: '100%',
display: 'block'
} }) })) : (jsx(Frame, { display: "flex", align: "center", justify: "center", fill: true, children: jsx(TextDisplay, { size: "lg", style: "center", primaryText: "Product" }) })) }) })), buttonPosition === 'bottom-full' ? (
/* bottom-full: 텍스트 아래에 full-width 버튼 */
jsxs(Frame, { display: "flex", direction: "column", gap: "lg", children: [jsxs(Frame, { display: "flex", direction: "column", gap: "xs", children: [showBadge && (badge || badgeText) && (jsx(Badge, { text: badgeText || (badge === null || badge === void 0 ? void 0 : badge.text) || 'Badge', color: badgeColor || (badge === null || badge === void 0 ? void 0 : badge.color) || 'primary', iconName: badgeIconName || (badge === null || badge === void 0 ? void 0 : badge.iconName), size: badgeSize || (badge === null || badge === void 0 ? void 0 : badge.size) || 'sm', style: badgeStyle || (badge === null || badge === void 0 ? void 0 : badge.style) || 'filled', layout: badgeLayout || (badge === null || badge === void 0 ? void 0 : badge.layout) || 'only text', type: badgeType || (badge === null || badge === void 0 ? void 0 : badge.type) || 'round-square' })), jsx(TextDisplay, { size: size || 'md', style: textStyle || 'left', primaryText: title, showLabel: showLabel, labelText: labelText, showDescription: showDescription && !!description, descriptionText: description, showCaption: showCaption, captionText: captionText })] }), showActionButton && (jsx(Button, { variant: buttonVariant, buttonType: buttonType, colorScheme: buttonColorScheme, size: buttonSize, leftIcon: buttonIcon || (actionButton === null || actionButton === void 0 ? void 0 : actionButton.iconName) || 'LineIconsPlusPlusIcon', rightIcon: buttonRightIcon, isFullWidth: true, isSelected: buttonIsSelected, disabled: actionButton === null || actionButton === void 0 ? void 0 : actionButton.disabled, isLoading: actionButton === null || actionButton === void 0 ? void 0 : actionButton.isLoading, onClick: handleActionClick, ...actionButton === null || actionButton === void 0 ? void 0 : actionButton.buttonProps, children: buttonText }))] })) : buttonPosition === 'bottom-right' ? (
/* bottom-right: 전체를 세로로 배치하고 마지막에 우측 정렬 버튼 */
jsxs(Frame, { display: "flex", direction: "column", gap: "lg", children: [jsxs(Frame, { display: "flex", direction: "column", gap: "xs", children: [showBadge && (badge || badgeText) && (jsx(Badge, { text: badgeText || (badge === null || badge === void 0 ? void 0 : badge.text) || 'Badge', color: badgeColor || (badge === null || badge === void 0 ? void 0 : badge.color) || 'primary', iconName: badgeIconName || (badge === null || badge === void 0 ? void 0 : badge.iconName), size: badgeSize || (badge === null || badge === void 0 ? void 0 : badge.size) || 'sm', style: badgeStyle || (badge === null || badge === void 0 ? void 0 : badge.style) || 'filled', layout: badgeLayout || (badge === null || badge === void 0 ? void 0 : badge.layout) || 'only text', type: badgeType || (badge === null || badge === void 0 ? void 0 : badge.type) || 'round-square' })), jsx(TextDisplay, { size: size || 'md', style: textStyle || 'left', primaryText: title, showLabel: showLabel, labelText: labelText, showDescription: showDescription && !!description, descriptionText: description, showCaption: showCaption, captionText: captionText })] }), showActionButton && (jsx(Frame, { display: "flex", justify: "flex-end", children: jsx(Button, { variant: buttonVariant, buttonType: buttonType, colorScheme: buttonColorScheme, size: buttonSize, leftIcon: buttonIcon || (actionButton === null || actionButton === void 0 ? void 0 : actionButton.iconName) || 'LineIconsPlusPlusIcon', rightIcon: buttonRightIcon, isFullWidth: buttonIsFullWidth, isSelected: buttonIsSelected, disabled: actionButton === null || actionButton === void 0 ? void 0 : actionButton.disabled, isLoading: actionButton === null || actionButton === void 0 ? void 0 : actionButton.isLoading, onClick: handleActionClick, ...actionButton === null || actionButton === void 0 ? void 0 : actionButton.buttonProps, children: buttonText }) }))] })) : (
/* top-right (기본): 기존 가로 배치 */
jsxs(Frame, { display: "flex", direction: "row", justify: "space-between", align: "flex-start", gap: "xxl", children: [jsxs(Frame, { display: "flex", direction: "column", gap: "xs", children: [showBadge && (badge || badgeText) && (jsx(Badge, { text: badgeText || (badge === null || badge === void 0 ? void 0 : badge.text) || 'Badge', color: badgeColor || (badge === null || badge === void 0 ? void 0 : badge.color) || 'primary', iconName: badgeIconName || (badge === null || badge === void 0 ? void 0 : badge.iconName), size: badgeSize || (badge === null || badge === void 0 ? void 0 : badge.size) || 'sm', style: badgeStyle || (badge === null || badge === void 0 ? void 0 : badge.style) || 'filled', layout: badgeLayout || (badge === null || badge === void 0 ? void 0 : badge.layout) || 'only text', type: badgeType || (badge === null || badge === void 0 ? void 0 : badge.type) || 'round-square' })), jsx(TextDisplay, { size: size || 'md', style: textStyle || 'left', primaryText: title, showLabel: showLabel, labelText: labelText, showDescription: showDescription && !!description, descriptionText: description, showCaption: showCaption, captionText: captionText })] }), showActionButton && (jsx(Button, { variant: buttonVariant, buttonType: buttonType, colorScheme: buttonColorScheme, size: buttonSize, leftIcon: buttonIcon || (actionButton === null || actionButton === void 0 ? void 0 : actionButton.iconName) || 'LineIconsPlusPlusIcon', rightIcon: buttonRightIcon, isFullWidth: buttonIsFullWidth, isSelected: buttonIsSelected, disabled: actionButton === null || actionButton === void 0 ? void 0 : actionButton.disabled, isLoading: actionButton === null || actionButton === void 0 ? void 0 : actionButton.isLoading, onClick: handleActionClick, ...actionButton === null || actionButton === void 0 ? void 0 : actionButton.buttonProps, children: buttonText }))] }))] }) }) }));
};
const Label = React.forwardRef(({ size = 'md', type = 'regular', disabled = false, asterisk = false, align = 'top', interactionState, className, children, ...props }, ref) => {
// 현재 토큰 시스템에 맞춰 Text variant 매핑
// 피그마 원본: lg=15px, md=14px, sm=13px
// 토큰 시스템: lg=16px, md=14px, sm=12px (가장 가까운 값)
const getTextVariant = () => {
if (size === 'lg') {
// 16px - heading-3 계열 사용 (bold=600, medium=500, regular=400)
return 'heading-3';
}
else if (size === 'md') {
// 14px - body-2 계열 사용 (bold=700, medium=500, regular=400)
return 'body-2';
}
else {
// 12px - label-2 계열 사용 (bold=700, medium=500, regular=400)
return 'label-2';
}
};
// 피그마 기준 fontWeight 매핑
const getFontWeight = () => {
switch (type) {
case 'bold': return 'bold';
case 'medium': return 'medium';
case 'regular': return 'regular';
default: return 'regular';
}
};
// 색상 결정 (피그마 기준)
const getTextColor = () => {
return disabled ? 'secondary-system02-1-disabled' : 'secondary-system02-1-rest';
};
return (jsx("label", { ref: ref, className: className, ...props, children: jsx(Surface, { foreground: getTextColor(), children: jsxs(Frame, { display: "flex", align: align === 'center' ? 'center' : 'flex-start', direction: "row", gap: "xxxs", children: [jsx(Text, { as: "span", variant: getTextVariant(), weight: getFontWeight(), children: children }), asterisk && (jsx(Asterisk, { disabled: disabled }))] }) }) }));
});
Label.displayName = 'Label';
const Checkbox = forwardRef(({ checked = false, indeterminate = false, disabled = false, label, showLabel = true, name, value, onChange, onClick, onFocus, onBlur, className, style, ...props }, ref) => {
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const handleChange = (event) => {
if (!disabled && onChange) {
onChange(event.target.checked);
}
};
const handleClick = (event) => {
if (!disabled && onChange) {
onChange(!checked);
}
if (onClick) {
onClick(event);
}
};
const handleFocus = (event) => {
setIsFocused(true);
if (onFocus) {
onFocus(event);
}
};
const handleBlur = (event) => {
setIsFocused(false);
if (onBlur) {
onBlur(event);
}
};
const handleMouseEnter = () => {
if (!disabled) {
setIsHovered(true);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
setIsPressed(false);
};
const handleMouseDown = () => {
if (!disabled) {
setIsPressed(true);
}
};
const handleMouseUp = () => {
setIsPressed(false);
};
// 배경색 결정
const getBackgroundColor = () => {
if (!checked && !indeterminate) {
// 체크되지 않은 상태는 투명 배경
if (disabled)
return 'secondary-system02-1-disabled';
if (isPressed)
return 'secondary-system02-1-pressed';
if (isHovered)
return 'secondary-system02-1-hovered';
return 'secondary-system02-1-rest'; // 체크되지 않은 상태는 secondary 색상 사용
}
// 체크된 상태는 primary 색상
if (disabled)
return 'primary-system02-1-disabled';
if (isPressed)
return 'primary-system02-1-pressed';
if (isHovered)
return 'primary-system02-1-hovered';
return 'primary-system02-1-rest';
};
// 테두리 색상 결정
const getBorderColor = () => {
if (disabled)
return 'secondary-system02-2-disabled'; // disabled일 때만 연한 색
return 'secondary-system02-1-rest'; // 기본 상태는 더 진한 색
};
const renderCheckIcon = () => {
if (indeterminate) {
return (jsx(Surface, { background: "primary-system02-1-rest", borderRadius: "xl", children: jsx(Sizing, { style: { width: '8px', height: '2px' } }) }));
}
if (checked) {
return (jsx(Icon, { name: "LineIconsCheckmarkCheckmarkIcon", size: "xs" }));
}
return null;
};
return (jsxs(Frame, { display: "flex", direction: "row", align: "center", gap: "sm", className: className, style: {
cursor: disabled ? 'not-allowed' : 'pointer',
...style
}, children: [jsx("input", { ref: ref, type: "checkbox", checked: checked, disabled: disabled, name: name, value: value, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, style: {
position: 'absolute',
opacity: 0,
width: 0,
height: 0,
margin: 0,
padding: 0,
border: 'none'
}, ...props }), jsx(Surface, { background: getBackgroundColor(), foreground: checked || indeterminate ? "primary-system02-oncolor" : "secondary-system02-1-rest", borderRadius: "xl", borderWidth: "thin", borderStyle: "solid", borderColor: getBorderColor(), onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, style: {
cursor: disabled ? 'not-allowed' : 'pointer',
}, children: jsx(Sizing, { width: "sm", height: "xxs", children: jsx(Frame, { display: "flex", align: "center", justify: "center", fill: true, children: renderCheckIcon() }) }) }), showLabel && label && (jsx(Label, { htmlFor: name, disabled: disabled, onClick: !disabled ? (() => handleClick({})) : undefined, children: label }))] }));
});
Checkbox.displayName = 'Checkbox';
/**
* Chip 컴포넌트는 선택된 항목이나 태그를 표시하는 컴포넌트입니다.
* ComboBox에서 선택된 항목을 표시하거나 태그 입력에서 사용할 수 있습니다.
*/
const Chip = forwardRef(({ children, variant = 'filled', size = 'md', state, disabled = false, removable = false, selectable = false, selected = false, onClick, onRemove, ...rest }, ref) => {
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
// 실제 상태 결정
const getActualState = () => {
if (disabled)
return 'disabled';
if (state && state !== 'default')
return state;
if (isPressed)
return 'pressed';
if (isHovered)
return 'hovered';
return 'default';
};
// 크기별 설정 - 시멘틱 토큰 활용
const sizeConfig = {
sm: {
padding: 'xxs', // 4px
variant: 'caption-1', // 12px/16px
iconSize: 'xs',
},
md: {
padding: 'xs', // 6px
variant: 'body-2', // 14px/20px
iconSize: 'sm',
}
};
const config = sizeConfig[size];
const actualState = getActualState();
// 배경색 결정
const getBackgroundColor = () => {
if (variant === 'filled') {
// Filled variant 색상 로직
if (disabled)
return 'secondary-system02-2-disabled';
if (selected) {
if (actualState === 'pressed')
return 'primary-system02-1-pressed';
if (actualState === 'hovered')
return 'primary-system02-1-hovered';
return 'primary-system02-1-rest';
}
if (actualState === 'pressed')
return 'secondary-system02-1-pressed';
if (actualState === 'hovered')
return 'secondary-system02-1-hovered';
return 'secondary-system02-1-rest';
}
else {
// Ghost variant 색상 로직
if (disabled)
return 'secondary-system02-1-disabled';
if (selected)
return 'primary-system02-1-rest';
return 'secondary-system02-2-rest';
}
};
// 테두리 색상 결정
const getBorderColor = () => {
if (variant === 'filled')
return undefined; // filled는 테두리 없음
// outlined variant
if (disabled)
return 'secondary-system02-2-disabled';
if (selected)
return 'primary-system02-1-rest';
return 'secondary-system02-1-rest';
};
// 전경색 결정
const getForegroundColor = () => {
if (disabled)
return 'secondary-system02-1-disabled';
if (variant === 'outlined') {
if (selected)
return 'primary-system02-1-rest';
return 'secondary-system02-2-rest';
}
// filled variant
if (selected)
return 'primary-system02-oncolor'; // primary 배경에서 잘 보이도록 onColor 사용
return 'secondary-system02-2-rest';
};
// 이벤트 핸들러
const handleMouseEnter = () => {
if (!disabled && (!state || state === 'default')) {
setIsHovered(true);
}
};
const handleMouseLeave = () => {
if (!disabled && (!state || state === 'default')) {
setIsHovered(false);
setIsPressed(false);
}
};
const handleMouseDown = () => {
if (!disabled && (!state || state === 'default')) {
setIsPressed(true);
}
};
const handleMouseUp = () => {
if (!disabled && (!state || state === 'default')) {
setIsPressed(false);
}
};
const handleClick = () => {
if (disabled)
return;
if (selectable && onClick) {
onClick();
}
};
const handleRemoveClick = (event) => {
event.stopPropagation();
if (disabled)
return;
if (onRemove) {
onRemove();
}
};
return (jsx(Surface, { ref: ref, background: getBackgroundColor(), foreground: getForegroundColor(), borderRadius: "pill", borderWidth: variant === 'outlined' ? 'thin' : undefined, borderStyle: variant === 'outlined' ? 'solid' : undefined, borderColor: getBorderColor(), style: {
cursor: disabled ? 'not-allowed' : selectable ? 'pointer' : 'default',
userSelect: 'none',
display: 'inline-flex',
width: 'fit-content',
}, onClick: selectable ? handleClick : undefined, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, ...rest, children: jsxs(Frame, { display: "flex", direction: "row", gap: "xxs", align: "center", padding: config.padding, fill: true, children: [jsx(Text, { variant: config.variant, color: undefined, style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
}, children: children }), removable && (jsx(Surface, { borderRadius: "xl", onClick: handleRemoveClick, style: {
cursor: disabled ? 'not-allowed' : 'pointer',
}, children: jsx(Frame, { display: "flex", align: "center", justify: "center", padding: "xxxs", style: {
flexShrink: 0,
}, children: jsx(Icon, { name: "LineIconsCloseCloseIcon", size: config.iconSize }) }) }))] }) }));
});
Chip.displayName = 'Chip';
/**
* Field 컴포넌트는 사용자 입력을 받기 위한 텍스트 필드입니다.
* outlined, transparent, filled 스타일을 지원하며, 라벨 위치를 다양하게 설정할 수 있습니다.
*/
const Field = forwardRef(({ variant = 'outlined', labelPosition = 'above', size = 'md', fieldWidth, minWidth, truncateOnFit, label, required = false, disabled = false, error = false, errorMessage, helperText, placeholder, value, type = 'text', leftIcon, rightIcon, onChange, onFocus, onBlur, onClick, className, style, ...rest }, ref) => {
// 상태 관리 추가
const [isHovered, setIsHovered] = useState(false);
const [isFocused, setIsFocused] = useState(false);
// 이벤트 핸들러들
const handleMouseEnter = () => {
if (!disabled) {
setIsHovered(true);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
};
const handleFocus = (event) => {
setIsFocused(true);
if (onFocus) {
onFocus(event);
}
};
const handleBlur = (event) => {
setIsFocused(false);
if (onBlur) {
onBlur(event);
}
};
// 현재 상태 결정
const getCurrentState = () => {
if (disabled)
return 'disabled';
if (isFocused)
return 'pressed'; // 포커스된 상태를 pressed로 처리
if (isHovered)
return 'hovered';
return 'rest';
};
// 필드 너비 설정
const getFieldWidth = () => {
if (fieldWidth === 'fit-content') {
return 'fit-content';
}
if (fieldWidth === 'fill-width') {
return '100%';
}
if (fieldWidth) {
return fieldWidth;
}
return '100%'; // 기본값
};
// 최소 너비 설정
const getMinWidth = () => {
return minWidth || '80px';
};
// 텍스트 색상 결정 (Surface foreground용)
const getTextColor = () => {
if (error)
return 'error';
if (variant === 'transparent') {
const state = getCurrentState();
switch (state) {
case 'disabled':
return 'secondary-system01-1-disabled';
case 'hovered':
return 'secondary-system01-1-hovered';
case 'pressed':
return 'secondary-system01-1-pressed';
default:
return 'secondary-system01-1-rest';
}
}
return 'secondary-system01-1-rest';
};
// input 스타일 결정
const getInputStyle = () => {
const baseStyle = {
border: 'none',
background: 'transparent',
outline: 'none',
color: 'inherit', // Surface foreground 상속
fontSize: '14px', // body-2와 동일
fontFamily: 'inherit',
lineHeight: '1.4',
fontWeight: '400', // regular
width: '100%',
height: '100%'
};
if (fieldWidth === 'fit-content') {
// 현재 값이나 placeholder의 길이에 따라 너비 계산
const displayText = value || placeholder || '';
const textLength = displayText.length;
const dynamicWidth = Math.max(textLength * 0.6 + 2, 4); // 최소 4ch
const fitContentStyle = {
...baseStyle,
width: `${dynamicWidth}ch`,
minWidth: '4ch', // 최소 4글자 너비
maxWidth: truncateOnFit ? '25ch' : 'none',
};
// truncate 옵션이 활성화된 경우 input에 직접 적용
if (truncateOnFit) {
return {
...fitContentStyle,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '25ch',
};
}
return fitContentStyle;
}
return baseStyle;
};
// 필드 배경색 결정 (상태별)
const getFieldBackgroundColor = () => {
if (variant !== 'filled')
return undefined;
const state = getCurrentState();
switch (state) {
case 'disabled':
return 'secondary-system01-1-disabled';
case 'hovered':
return 'secondary-system01-1-hovered';
case 'pressed':
return 'secondary-system01-1-pressed';
default:
return 'secondary-system01-1-rest';
}
};
// 필드 테두리 설정 (상태별)
const getFieldBorderProps = () => {
if (variant === 'transparent') {
return {
borderWidth: undefined,
borderColor: undefined
};
}
if (error) {
return {
borderWidth: 'thin',
borderColor: 'error'
};
}
// outlined의 경우 상태별 테두리 색상
const state = getCurrentState();
let borderColor = 'secondary-system01-1-rest';
switch (state) {
case 'disabled':
borderColor = 'secondary-system01-1-disabled';
break;
case 'hovered':
borderColor = 'secondary-system01-1-hovered';
break;
case 'pressed':
borderColor = 'secondary-system01-1-pressed';
break;
default:
borderColor = 'secondary-system01-1-rest';
}
return {
borderWidth: 'thin',
borderColor
};
};
// 사이즈별 높이 토큰 매핑
const getSizingHeight = () => {
switch (size) {
case 'md':
return 'lg'; // 32px
case 'lg':
return 'xl'; // 36px
case 'xlg':
return 'xxl'; // 40px
default:
return 'lg';
}
};
// 라벨 컴포넌트 렌더링
const renderLabel = () => {
if (!label || labelPosition === 'none')
return null;
return (jsx(Label, { size: "md", type: "regular", align: "top", asterisk: required, disabled: disabled, children: label }));
};
// 입력 필드 렌더링
const renderInputField = () => {
const borderProps = getFieldBorderProps();
return (jsx(Surface, { background: getFieldBackgroundColor(), foreground: getTextColor(), borderWidth: borderProps.borderWidth, borderColor: borderProps.borderColor, borderStyle: borderProps.borderWidth ? 'solid' : 'none', borderRadius: "xl", onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onClick: onClick, className: className, style: {
width: getFieldWidth(),
minWidth: getMinWidth(),
...style
}, children: jsx(Sizing, { height: getSizingHeight(), children: jsxs(Frame, { display: "flex", direction: "row", align: "center", justify: "flex-start", gap: "sm", fill: true, padding: "md", style: {
cursor: onClick ? 'pointer' : 'text'
}, children: [leftIcon && (jsx(Icon, { name: leftIcon, size: "sm" })), jsx(Frame, { style: {
flex: 1,
display: 'flex',
alignItems: 'center'
}, children: jsx("input", { ref: ref, type: type, value: value, placeholder: placeholder, disabled: disabled, onChange: onChange, onFocus: handleFocus, onBlur: handleBlur, style: getInputStyle(), ...rest }) }), rightIcon && (jsx(Icon, { name: rightIcon, size: "sm" }))] }) }) }));
};
// 도움말/에러 메시지 렌더링
const renderMessage = () => {
const message = error ? errorMessage : helperText;
if (!message)
return null;
// 헬퍼 텍스트 색상 결정 (연한 secondary 색상 사용)
const getHelperTextColor = () => {
if (disabled)
return 'secondary-system01-3-disabled';
if (error)
return 'error';
return 'secondary-system01-3-rest'; // 기본 텍스트보다 연한 색상
};
return (jsx(Surface, { foreground: getHelperTextColor(), children: jsx(Text, { variant: "caption-1", children: message }) }));
};
// 라벨 위치에 따른 레이아웃 결정
if (labelPosition === 'before') {
return (jsxs(Frame, { display: "flex", direction: "row", gap: "md", align: "flex-start", children: [renderLabel(), jsxs(Frame, { display: "flex", direction: "column", gap: "xxs", flex: 1, children: [renderInputField(), renderMessage()] })] }));
}
// labelPosition이 'above' 또는 'none'인 경우
return (jsxs(Frame, { display: "flex", direction: "column", gap: "xxs", children: [renderLabel(), renderInputField(), renderMessage()] }));
});
Field.displayName = 'Field';
const Radio = forwardRef(({ checked = false, disabled = false, label, showLabel = true, name, value, variant = 'radio', onChange, onClick, onFocus, onBlur, className, style, ...props }, ref) => {
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const handleChange = (event) => {
if (!disabled && onChange && value) {
onChange(value);
}
};
const handleClick = (event) => {
if (!disabled && onChange && value) {
onChange(value);
}
if (onClick) {
onClick(event);
}
};
const handleFocus = (event) => {
setIsFocused(true);
if (onFocus) {
onFocus(event);
}
};
const handleBlur = (event) => {
setIsFocused(false);
if (onBlur) {
onBlur(event);
}
};
const handleMouseEnter = () => {
if (!disabled) {
setIsHovered(true);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
setIsPressed(false);
};
const handleMouseDown = () => {
if (!disabled) {
setIsPressed(true);
}
};
const handleMouseUp = () => {
setIsPressed(false);
};
// 현재 상태 결정
const getCurrentState = () => {
if (disabled)
return 'disabled';
if (isPressed)
return 'pressed';
if (isHovered)
return 'hovered';
return 'rest';
};
// 라디오 버튼 배경색 결정 (기존 secondary-1 로직 유지)
const getBackgroundColor = () => {
const state = getCurrentState();
switch (state) {
case 'disabled':
return 'secondary-system02-1-disabled';
case 'pressed':
return 'secondary-system02-1-pressed';
case 'hovered':
return 'secondary-system02-1-hovered';
default:
return 'secondary-system02-1-rest';
}
};
// 라디오 버튼 테두리 색상 결정 (기존 secondary-stroke-1 로직 유지)
const getBorderColor = () => {
const state = getCurrentState();
switch (state) {
case 'disabled':
return 'secondary-system02-1-disabled';
case 'pressed':
return 'secondary-system02-1-pressed';
case 'hovered':
return 'secondary-system02-1-hovered';
default:
return 'secondary-system02-1-rest';
}
};
// 내부 원/체크마크 배경색 결정 (기존 primary-1 로직 유지)
const getInnerBackgroundColor = () => {
const state = getCurrentState();
switch (state) {
case 'disabled':
return 'primary-system02-1-disabled';
case 'pressed':
return 'primary-system02-1-pressed';
case 'hovered':
return 'primary-system02-1-hovered';
default:
return 'primary-system02-1-rest';
}
};
// 내부 표시 렌더링
const renderInnerContent = () => {
if (!checked)
return null;
if (variant === 'checkmark') {
return (jsx(Icon, { name: "LineIconsRadioButtonOnIcon", size: "xs" }));
}
// radio 스타일: 내부 원 (기존 6px x 6px 크기 유지)
return (jsx(Sizing, { width: "6px", height: "6px", children: jsx(Surface, { background: getInnerBackgroundColor(), borderRadius: "circular", fill: true }) }));
};
return (jsxs(Frame, { display: "flex", direction: "row", align: "center", gap: "sm", className: className, style: style, children: [jsx("input", { ref: ref, type: "radio", checked: checked, disabled: disabled, name: name, value: value, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, style: {
position: 'absolute',
opacity: 0,
width: 0,
height: 0,
margin: 0,
padding: 0,
border: 'none'
}, ...props }), jsx(Sizing, { width: "16px", height: "16px", children: jsx(Surface, { background: getBackgroundColor(), borderRadius: variant === 'checkmark' ? 'xl' : 'circular', borderWidth: "thin", borderColor: getBorderColor(), borderStyle: "solid", onClick: handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, style: { cursor: disabled ? 'not-allowed' : 'pointer' }, fill: true, children: jsx(Frame, { display: "flex", direction: "column", align: "center", justify: "center", fill: true, children: renderInnerContent() }) }) }), showLabel && label && (jsx(Label, { htmlFor: name, disabled: disabled, interactionState: getCurrentState(), style: { cursor: disabled ? 'not-allowed' : 'pointer' }, onClick: !disabled ? (() => handleClick({})) : undefined, children: label }))] }));
});
Radio.displayName = 'Radio';
/**
* OptionList 컴포넌트는 선택 가능한 옵션 아이템을 나타냅니다.
* ComboBox, 드롭다운 메뉴, 선택 목록 등에서 사용할 수 있는 범용적인 컴포넌트입니다.
*/
const OptionList = forwardRef(({ type = 'single-select', state = 'default', selected = false, disabled = false, children, onClick, onMouseEnter, onMouseLeave,
// Radio props
radioName, radioValue, radioChecked, onRadioChange,
// Checkbox props
checkboxName, checkboxValue, checkboxChecked, checkboxIndeterminate, onCheckboxChange, ...rest }, ref) => {
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
// 실제 상태 결정 (외부 state prop이 있으면 우선, 없으면 실시간 상태 사용)
const getActualState = () => {
if (disabled)
return 'disabled';
if (state && state !== 'default')
return state;
if (isPressed)
return 'pressed';
if (isHovered)
return 'hovered';
if (selected)
return 'selected';
return 'default';
};
// 상태에 따른 배경색 결정
const getBackgroundColor = () => {
const actualState = getActualState();
if (actualState === 'disabled')
return 'secondary-system02-1-disabled';
if (actualState === 'pressed')
return 'secondary-system02-1-pressed';
if (actualState === 'hovered')
return 'secondary-system02-1-hovered';
if (actualState === 'selected')
return 'secondary-system02-1-selected';
return 'secondary-system02-1-rest';
};
// 상태에 따른 텍스트 색상 결정
const getForegroundColor = () => {
const actualState = getActualState();
if (actualState === 'disabled')
return 'secondary-system02-1-disabled';
return 'secondary-system02-1-rest';
};
// 이벤트 핸들러들
const handleMouseEnter = () => {
if (!disabled && (!state || state === 'default')) {
setIsHovered(true);
}
if (onMouseEnter)
onMouseEnter();
};
const handleMouseLeave = () => {
if (!disabled && (!state || state === 'default')) {
setIsHovered(false);
setIsPressed(false);
}
if (onMouseLeave)
onMouseLeave();
};
const handleMouseDown = () => {
if (!disabled && (!state || state === 'default')) {
setIsPressed(true);
}
};
const handleMouseUp = () => {
if (!disabled && (!state || state === 'default')) {
setIsPressed(false);
}
};
const handleClick = () => {
if (disabled)
return;
// Radio 컴포넌트 타입일 때
if (type === 'radio-component' && onRadioChange && radioValue) {
onRadioChange(radioValue);
}
// Checkbox 컴포넌트 타입일 때
if (type === 'checkbox-component' && onCheckboxChange) {
onCheckboxChange(!checkboxChecked);
}
// 기본 클릭 핸들러
if (onClick) {
onClick();
}
};
// 텍스트 스타일 결정
const getTextVariant = () => {
if (type === 'menu-header')
return 'caption-1';
return 'body-2';
};
// 텍스트 굵기 결정
const getFontWeight = () => {
if (type === 'menu-header')
return 'bold';
return 'regular';
};
// 패딩 결정
const getPadding = () => {
if (type === 'menu-header')
return 'xs';
if (type === 'radio-component' || type === 'checkbox-component')
return 'sm';
return 'sm';
};
// 테두리 스타일
const getBorderProps = () => {
if (state === 'focused') {
return {
borderWidth: 'thin',
borderColor: 'primary-system02-1-rest',
borderStyle: 'solid'
};
}
return {};
};
const borderProps = getBorderProps();
// 컨텐츠 렌더링
const renderContent = () => {
// Radio 컴포넌트 렌더링
if (type === 'radio-component') {
return (jsx(Radio, { name: radioName, value: radioValue, label: children, checked: radioChecked || false, disabled: disabled, onChange: onRadioChange, showLabel: true }));
}
// Checkbox 컴포넌트 렌더링
if (type === 'checkbox-component') {
return (jsx(Checkbox, { name: checkboxName, value: checkboxValue, label: children, checked: checkboxChecked || false, indeterminate: checkboxIndeterminate || false, disabled: disabled, onChange: onCheckboxChange, showLabel: true }));
}
// 기본 텍스트 렌더링
return (jsx(Text, { variant: getTextVariant(), weight: getFontWeight(), color: undefined, children: children }));
};
return (jsx(Surface, { ref: ref, background: getBackgroundColor(), foreground: getForegroundColor(), borderRadius: "xl", ...borderProps, style: {
cursor: disabled ? 'not-allowed' : 'pointer',
userSelect: 'none'
}, onClick: disabled ? undefined : handleClick, onMouseEnter: disabled ? undefined : handleMouseEnter, onMouseLeave: disabled ? undefined : handleMouseLeave, onMouseDown: disabled ? undefined : handleMouseDown, onMouseUp: disabled ? undefined : handleMouseUp, ...rest, children: jsxs(Sizing, { height: "lg", width: "100%", children: [" ", jsx(Frame, { display: "flex", align: "center", padding: getPadding(), fill: true, children: renderContent() })] }) }));
});
OptionList.displayName = 'OptionList';
/**
* ComboBox 컴포넌트는 Field를 기반으로 하는 선택 가능한 드롭다운 메뉴입니다.
* 검색 기능, 단일/다중 선택을 지원하며 선택된 항목을 Chip으로 표시합니다.
*/
const ComboBox = forwardRef(({ options, value, multiple = false, searchable = false, placeholder = '선택해주세요', label, disabled = false, errorMessage, helperText, required = false, direction = 'down', fieldVariant, fieldSize, fieldWidth, labelPosition, leftIcon, onChange, onSearch, onToggle, ...rest }, ref) => {
// 기본값 처리
const selectedValues = multiple
? (Array.isArray(value) ? value : [])
: (typeof value === 'string' ? [value] : []);
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [filteredOptions, setFilteredOptions] = useState(options);
const containerRef = useRef(null);
const fieldRef = useRef(null);
// 검색어에 따른 옵션 필터링
useEffect(() => {
if (!searchable || !searchTerm) {
setFilteredOptions(options);
return;
}
const filtered = options.filter(option => option.label.toLowerCase().includes(searchTerm.toLowerCase()));
setFilteredOptions(filtered);
if (onSearch) {
onSearch(searchTerm);
}
}, [searchTerm, options, searchable, onSearch]);
// 외부 클릭 감지로 드롭다운 닫기
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setIsOpen(false);
setSearchTerm(''); // 드롭다운이 닫힐 때 검색어 초기화
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
// 드롭다운 토글
const handleToggle = () => {
if (disabled)
return;
const newIsOpen = !isOpen;
setIsOpen(newIsOpen);
if (newIsOpen && searchable && fieldRef.current) {
fieldRef.current.focus();
}
if (!newIsOpen) {
setSearchTerm(''); // 드롭다운이 닫힐 때 검색어 초기화
}
if (onToggle) {
onToggle(newIsOpen);
}
};
// 옵션 선택 처리
const handleOptionSelect = (optionValue) => {
if (disabled)
return;
if (multiple) {
const newValues = selectedValues.includes(optionValue)
? selectedValues.filter(v => v !== optionValue)
: [...selectedValues, optionValue];
if (onChange) {
onChange(newValues);
}
}
else {
if (onChange) {
onChange(optionValue);
}
setIsOpen(false);
setSearchTerm('');
}
};
// 칩 제거 처리
const handleChipRemove = (valueToRemove) => {
if (disabled)
return;
const newValues = selectedValues.filter(v => v !== valueToRemove);
if (onChange) {
onChange(multiple ? newValues : '');
}
};
// 선택된 값들의 라벨 표시
const getDisplayValue = () => {
if (selectedValues.length === 0)
return '';
if (selectedValues.length === 1) {
const option = options.find(opt => opt.value === selectedValues[0]);
return (option === null || option === void 0 ? void 0 : option.label) || '';
}
return `${selectedValues.length}개 선택됨`;
};
// 검색 입력 처리
const handleSearchChange = (event) => {
setSearchTerm(event.target.value);
};
// Field 클릭 처리
const handleFieldClick = (event) => {
// 검색 가능한 상태에서 input을 직접 클릭한 경우에는 토글하지 않음
if (searchable && event.target instanceof HTMLInputElement) {
return;
}
handleToggle();
};
// 키보드 이벤트 처리
const handleKeyDown = (event) => {
if (event.key === 'Enter' || (event.key === ' ' && !searchable)) {
event.preventDefault();
handleToggle();
}
else if (event.key === 'Escape' && isOpen) {
setIsOpen(false);
setSearchTerm('');
}
};
// 옵션들을 그룹별로 정리
const groupedOptions = filteredOptions.reduce((groups, option) => {
const groupName = option.group || 'default';
if (!groups[groupName]) {
groups[groupName] = [];
}
groups[groupName].push(option);
return groups;
}, {});
// 선택된 칩들 렌더링
const renderSelectedChips = () => {
if (!multiple || selectedValues.length === 0)
return null;
return (jsx(Frame, { display: "flex", direction: "row", wrap: "wrap", gap: "xs", children: selectedValues.map((selectedValue) => {
const option = options.find(opt => opt.value === selectedValue);
if (!option)
return null;
return (jsx(Chip, { size: "sm", removable: true, disabled: disabled, onRemove: () => handleChipRemove(selectedValue), children: option.label }, selectedValue));
}) }));
};
return (jsx(Surface, { style: { position: 'relative', width: '100%' }, ...rest, children: jsxs("div", { ref: containerRef, children: [jsxs(Frame, { display: "flex", direction: "column", gap: "sm", children: [jsx(Field, { ref: fieldRef, variant: fieldVariant, size: fieldSize, fieldWidth: fieldWidth, minWidth: "80px", truncateOnFit: fieldWidth === 'fit-content', labelPosition: labelPosition, leftIcon: leftIcon, label: label, placeholder: placeholder, value: searchable && isOpen ? searchTerm : getDisplayValue(), disabled: disabled, error: !!errorMessage, errorMessage: errorMessage, helperText: helperText, required: required, readOnly: !searchable, rightIcon: "LineIconsArrowChevronDownIcon", onClick: handleFieldClick, onChange: searchable ? handleSearchChange : undefined, onKeyDown: handleKeyDown, style: {
cursor: disabled ? 'not-allowed' : 'pointer',
} }), renderSelectedChips()] }), isOpen && (jsx(Surface, { background: "secondary-system01-1-rest", borderRadius: "xl", borderColor: "secondary-system01-1-rest", borderWidth: "thin", borderStyle: "solid", style: {
position: 'absolute',
top: direction === 'down' ? '100%' : 'auto',
bottom: direction === 'up' ? '100%' : 'auto',
left: 0,
right: 0,
zIndex: 1000,
marginTop: direction === 'down' ? 'var(--semantic-gap-global-xxs)' : '0',
marginBottom: direction === 'up' ? 'var(--semantic-gap-global-xxs)' : '0',
maxHeight: '200px',
overflowY: 'auto'
}, children: jsxs(Frame, { display: "flex", direction: "column", padding: "xs", children: [Object.entries(groupedOptions).map(([groupName, groupOptions], groupIndex) => (jsxs(React.Fragment, { children: [groupName !== 'default' && (jsxs(Fragment, { children: [groupIndex > 0 && (jsx(Frame, { display: "flex", gap: "xxs", children: jsx(Divider, { orientation: "horizontal", thickness: "thin" }) })), jsx(OptionList, { type: "menu-header", children: groupName })] })), groupOptions.map((option) => {
const isSelected = selectedValues.includes(option.value);
return (jsx(OptionList, { type: multiple ? 'checkbox-component' : 'single-select', selected: !multiple ? isSelected : undefined, disabled: option.disabled, onClick: () => handleOptionSelect(option.value), checkboxChecked: multiple ? isSelected : undefined, onCheckboxChange: multiple ? () => handleOptionSelect(option.value) : undefined, children: option.label }, option.value));
})] }, groupName))), filteredOptions.length === 0 && (jsx(OptionList, { type: "menu-header", disabled: true, children: "\uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4" }))] }) }))] }) }));
});
ComboBox.displayName = 'ComboBox';
/**
* Link 컴포넌트는 다른 페이지나 외부 사이트로 이동하는 컴파운드 컴포넌트입니다.
* Text와 Icon 프리미티브를 조합하여 만들어지며, 다양한 크기, 색상, 상태를 지원하고 접근성을 고려한 설계가 적용되어 있습니다.
*/
const Link = forwardRef(({ variant = 'default', size = 'body-1', color = 'primary-foreground-1', interactionState = 'rest', underline = 'hover', external = false, disabled = false, children, className, target, rel, onClick, style, ...rest }, ref) => {
// 현재 상태에 따른 색상 결정
const getCurrentColor = () => {
if (disabled)
return `${color}--disabled`;
return `${color}--${interactionState}`;
};
// 텍스트 스타일 결정
const getTextStyle = () => {
const baseStyle = {
textDecoration: underline === 'always' ? 'underline' :
underline === 'hover' ? undefined : 'none',
opacity: variant === 'subtle' ? 0.7 : 1,
fontWeight: variant === 'prominent' ? 600 : undefined,
cursor: disabled ? 'not-allowed' : 'pointer',
...style
};
return baseStyle;
};
// 외부 링크 props 처리
const externalProps = external ? {
target: target || '_blank',
rel: rel || 'noopener noreferrer'
} : { target, rel };
// 클릭 핸들러
const handleClick = (event) => {
if (disabled) {
event.preventDefault();
return;
}
if (onClick) {
onClick(event);
}
};
return (jsxs("a", { ref: ref, className: className, onClick: handleClick, "aria-disabled": disabled, tabIndex: disabled ? -1 : undefined, style: {
display: 'inline-flex',
alignItems: 'center',
gap: 'var(--semantic-spacingHor-global-xxxs)',
textDecoration: 'none',
outline: 'none',
transition: 'all var(--semantic-animation-duration-transition-button) var(--semantic-animation-easing-interaction-button)',
...style
}, ...externalProps, ...rest, children: [jsx(Text, { variant: size, color: getCurrentColor(), style: getTextStyle(), children: children }), external && (jsx(Icon, { name: "ExternalLinkIcon", size: "xs", style: { opacity: 0.7 } }))] }));
});
Link.displayName = 'Link';
/**
* MenuHeader 컴포넌트는 아코디언이나 GNB 메뉴에서 사용할 수 있는 헤더 역할을 합니다.
* 클릭 가능한 버튼 형태로 구현되어 메뉴 확장/축소 등의 동작을 처리할 수 있습니다.
*
*/
const MenuHeader = forwardRef(({ size = 'lg', state, children, leftIcon, rightIcon, expanded = false, showChevron = true, disabled = false, className, onClick, color, ...rest }, ref) => {
// 실시간 인터랙션 상태 관리
const [isHovered, setIsHovered] = useState(false);
const [isPressed, setIsPressed] = useState(false);
const [isFocused, setIsFocused] = useState(false);
// 실제 상태 결정 (외부 state prop이 있으면 우선, 없으면 실시간 상태 사용)
const getActualState = () => {
if (disabled)
return 'disabled';
if (state)
return state; // 외부에서 명시적으로 상태를 지정한 경우
if (isPressed)
return 'pressed';
if (isHovered)
return 'hovered';
if (isFocused)
return 'focused';
return 'default';
};
const actualState = getActualState();
// 크기별 높이 토큰 매핑
const heightMap = {
'md': 'lg', // 32px
'lg': 'xl', // 36px
'xlg': 'xxl' // 40px
};
// 크기별 아이콘 사이즈 매핑 - Button 컴포넌트와 일치
const iconSizeMap = {
'md': 'xs', // 16px (Button md와 동일)
'lg': 'sm', // 20px (Button lg와 동일)
'xlg': 'md' // 24px (Button xl과 동일)
};
// 상태별 배경색 - 선택된 상태와 비활성화 상태는 배경색 없음
const getBackgroundColor = () => {
if (actualState === 'disabled')
return undefined; // 비활성화시 배경색 없음
if (actualState === 'selected')
return undefined; // 선택된 상태에서 배경색 없음
if (actualState === 'pressed')
return 'secondary-system02-1-pressed';
if (actualState === 'hovered')
return 'secondary-system02-1-hovered';
if (actualState === 'focused')
return undefined; // 포커스시 배경색 없음
return undefined; // 기본 상태도 배경색 없음
};
// 상태별 전경색
const getForegroundColor = () => {
if (actualState === 'disabled')
return 'secondary-system02-1-disabled';
if (actualState === 'selected')
return 'primary-system02-1-rest';
return 'secondary-system02-1-rest';
};
// 포커스 상태 보더 색상
const getBorderProps = () => {
if (actualState === 'focused') {
return {
borderWidth: 'thin',
borderStyle: 'solid',
borderColor: 'focused'
};
}
return {};
};
// 이벤트 핸들러
const handleMouseEnter = (event) => {
if (!disabled && !state)
setIsHovered(true);
};
const handleMouseLeave = (event) => {
if (!disabled && !state) {
setIsHovered(false);
setIsPressed(false);
}
};
const handleMouseDown = (event) => {
if (!disabled && !state)
setIsPressed(true);
};
const handleMouseUp = (event) => {
if (!disabled && !state)
setIsPressed(false);
};
const handleFocus = (event) => {
if (!disabled && !state)
setIsFocused(true);
};
const handleBlur = (event) => {
if (!disabled && !state)
setIsFocused(false);
};
const handleClick = (event) => {
if (disabled)
return;
if (onClick) {
onClick(event);
}
};
return (jsx(Surface, { ref: ref, background: getBackgroundColor(), foreground: getForegroundColor(), borderRadius: "xl", ...getBorderProps(), style: {
cursor: disabled ? 'not-allowed' : 'pointer',
width: '100%',
...rest.style
}, onClick: disabled ? undefined : handleClick, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onFocus: handleFocus, onBlur: handleBlur, tabIndex: disabled ? -1 : 0, "aria-disabled": disabled, "aria-expanded": expanded, className: className, ...rest, children: jsx(Sizing, { height: heightMap[size], children: jsxs(Frame, { display: "flex", align: "center", justify: "space-between", paddingX: "sm", paddingY: "xs", fill: true, children: [jsxs(Frame, { display: "flex", align: "center", gap: "xs", children: [leftIcon && (jsx(Icon, { name: leftIcon, size: iconSizeMap[size] })), jsx(Text, { variant: "heading-3", weight: "bold", children: children }), rightIcon && (jsx(Icon, { name: rightIcon, size: iconSizeMap[size] }))] }), showChevron && (jsx(Button, { variant: "transparent", buttonType: "icon-only", colorScheme: actualState === 'selected' ? 'primary' : 'secondary', size: size === 'xlg' ? 'md' : size === 'lg' ? 'sm' : 'xs', rightIcon: expanded ? "LineIconsArrowChevronUpIcon" : "LineIconsArrowChevronDownIcon", disabled: disabled, onClick: (e) => {
e.stopPropagation();
if (onClick) {
onClick(e);
}
} }))] }) }) }));
});
MenuHeader.displayName = 'MenuHeader';
const SegmentButtonContext = createContext(null);
const SegmentButton = ({ mode, selectedValues, defaultSelectedValues = [], size = 'md', variant = 'primary', onChange, children, ...rest }) => {
// Internal state for uncontrolled component
const [internalSelectedValues, setInternalSelectedValues] = useState(defaultSelectedValues);
// Use controlled values if provided, otherwise use internal state
const currentSelectedValues = selectedValues !== null && selectedValues !== void 0 ? selectedValues : internalSelectedValues;
const handleItemClick = (value) => {
let newSelectedValues;
if (mode === 'single') {
// Single select: replace current selection
newSelectedValues = [value];
}
else {
// Multi select: toggle selection
if (currentSelectedValues.includes(value)) {
newSelectedValues = currentSelectedValues.filter(v => v !== value);
}
else {
newSelectedValues = [...currentSelectedValues, value];
}
}
// Update internal state if uncontrolled
if (selectedValues === undefined) {
setInternalSelectedValues(newSelectedValues);
}
// Call onChange callback
onChange === null || onChange === void 0 ? void 0 : onChange(newSelectedValues);
};
// Surface props 분리
const { style, ...surfaceProps } = rest;
const contextValue = {
selectedValues: currentSelectedValues,
mode,
size,
variant,
onItemClick: handleItemClick,
children,
};
return (jsx(SegmentButtonContext.Provider, { value: contextValue, children: jsx(Surface, { background: "secondary-system02-1-rest", borderColor: "secondary-system02-1-rest", borderWidth: "thin", borderStyle: "solid", borderRadius: "xl", style: { width: 'fit-content', ...style }, ...surfaceProps, children: jsx(Frame, { display: "flex", direction: "row", align: "stretch", justify: "flex-start", gap: "none", fill: true, children: children }) }) }));
};
const SegmentButtonItem = ({ value, disabled = false, children }) => {
const context = useContext(SegmentButtonContext);
if (!context) {
throw new Error('SegmentButton.Item must be used within SegmentButton');
}
const { selectedValues, onItemClick, size, variant } = context;
const selected = selectedValues.includes(value);
// 현재 아이템의 위치 정보 계산
const items = React.Children.toArray(context.children);
const currentIndex = items.findIndex((child) => React.isValidElement(child) &&
child.props &&
child.props.value === value);
const isFirst = currentIndex === 0;
const isLast = currentIndex === items.length - 1;
const isSingle = items.length === 1;
// 위치에 따른 스타일 결정 (구분선과 radius)
const getButtonStyle = () => {
const baseStyle = {};
if (isSingle) {
// 단일 버튼은 기본 radius
return baseStyle;
}
if (isFirst) {
// 첫 번째 버튼: 왼쪽만 radius
baseStyle.borderTopRightRadius = '0';
baseStyle.borderBottomRightRadius = '0';
}
else if (isLast) {
// 마지막 버튼: 오른쪽만 radius
baseStyle.borderTopLeftRadius = '0';
baseStyle.borderBottomLeftRadius = '0';
}
else {
// 가운데 버튼: radius 없음
baseStyle.borderRadius = '0';
}
// 구분선 추가 (마지막 버튼 제외) - System-02 색상 사용
if (!isLast) {
baseStyle.borderRight = '1px solid var(--foundation-foundation-color-grey-light-80)';
}
return baseStyle;
};
// 선택 상태별 variant 결정
const getButtonVariant = () => {
if (selected) {
return 'filled'; // 선택된 상태는 filled
}
return 'transparent'; // 비선택 상태는 transparent
};
// 선택 상태별 colorScheme 결정 (Tab처럼)
const getButtonColorScheme = () => {
if (selected) {
return 'primary'; // 선택된 상태는 primary (System-02: purple)
}
return 'secondary'; // 비선택 상태는 secondary
};
return (jsx(Button, { variant: getButtonVariant(), colorScheme: getButtonColorScheme(), size: size, disabled: disabled, onClick: () => !disabled && onItemClick(value), style: getButtonStyle(), "aria-pressed": selected, children: children }));
};
// Attach the Item component to the main component
SegmentButton.Item = SegmentButtonItem;
const Tab = ({ items, selectedId: controlledSelectedId, defaultSelectedId, size = 'lg', showBorder = true, onChange, ...surfaceProps }) => {
var _a;
const [internalSelectedId, setInternalSelectedId] = useState(defaultSelectedId || ((_a = items[0]) === null || _a === void 0 ? void 0 : _a.id) || '');
// 마우스 상태 관리
const [hoveredId, setHoveredId] = useState(null);
const [pressedId, setPressedId] = useState(null);
// controlled vs uncontrolled 지원
const selectedId = controlledSelectedId !== undefined ? controlledSelectedId : internalSelectedId;
const handleTabClick = (id) => {
if (selectedId !== id) {
if (controlledSelectedId === undefined) {
setInternalSelectedId(id);
}
onChange === null || onChange === void 0 ? void 0 : onChange(id);
}
};
// 현재 상태 결정
const getCurrentState = (itemId) => {
const item = items.find(item => item.id === itemId);
if (item === null || item === void 0 ? void 0 : item.disabled)
return 'disabled';
if (pressedId === itemId)
return 'pressed';
if (hoveredId === itemId)
return 'hovered';
return 'rest';
};
// 크기별 설정 - System-02는 패딩을 줄여서 간격을 좁힘
const getSizeConfig = () => {
if (size === 'md') {
return {
padding: 'sm', // md -> sm으로 변경
textVariant: 'body-2',
iconSize: 'xs',
minHeight: '40px'
};
}
return {
padding: 'md', // lg -> md로 변경
textVariant: 'body-1',
iconSize: 'sm',
minHeight: '48px'
};
};
const config = getSizeConfig();
return (jsx(Surface, { ...surfaceProps, children: jsx(Frame, { display: "flex", direction: "row", gap: "none", align: "flex-start", children: items.map((item) => {
const isSelected = selectedId === item.id;
const isDisabled = item.disabled;
const currentState = getCurrentState(item.id);
// Surface 색상 결정 - 더 연한 호버 색상 사용 (System-02: Purple 계열)
const getSurfaceColors = () => {
if (isDisabled) {
return {
foreground: 'secondary-system02-1-disabled',
borderColor: 'secondary-system02-1-disabled'
};
}
if (isSelected) {
// 선택된 탭은 primary 색상 사용
if (currentState === 'pressed') {
return {
foreground: 'primary-system02-1-pressed',
borderColor: 'primary-system02-1-pressed'
};
}
if (currentState === 'hovered') {
return {
foreground: 'primary-system02-1-hovered',
borderColor: 'primary-system02-1-hovered'
};
}
return {
foreground: 'primary-system02-1-rest',
borderColor: 'primary-system02-1-rest'
};
}
// 비선택 탭은 secondary 색상 사용
if (currentState === 'pressed') {
return {
foreground: 'secondary-system02-1-pressed',
borderColor: 'secondary-system02-1-pressed'
};
}
if (currentState === 'hovered') {
return {
foreground: 'secondary-system02-1-hovered',
borderColor: 'secondary-system02-1-hovered'
};
}
return {
foreground: 'secondary-system02-1-rest',
borderColor: 'secondary-system02-1-rest'
};
};
const surfaceColors = getSurfaceColors();
return (jsx(Surface, { background: undefined, foreground: surfaceColors.foreground, borderRadius: "none", borderWidth: isSelected && showBorder ? "medium" : undefined, borderColor: isSelected && showBorder ? surfaceColors.borderColor : undefined, borderStyle: isSelected && showBorder ? "solid" : undefined, style: {
...(isSelected && showBorder ? {
borderTop: 'none',
borderLeft: 'none',
borderRight: 'none'
} : {}),
cursor: isDisabled ? "not-allowed" : "pointer",
minHeight: config.minHeight
}, onClick: () => !isDisabled && handleTabClick(item.id), onMouseEnter: () => !isDisabled && setHoveredId(item.id), onMouseLeave: () => {
setHoveredId(null);
setPressedId(null);
}, onMouseDown: () => !isDisabled && setPressedId(item.id), onMouseUp: () => setPressedId(null), children: jsxs(Frame, { display: "flex", direction: "row", align: "center", justify: "center", gap: "sm", padding: config.padding, children: [item.icon && (jsx(Icon, { name: item.icon, size: config.iconSize })), jsx(Text, { variant: config.textVariant, weight: isSelected ? 'bold' : 'regular', children: item.label })] }) }, item.id));
}) }) }));
};
export { Badge, Button, Card, Checkbox, Chip, ComboBox, Field, Label, Link, MenuHeader, OptionList, Radio, SegmentButton, Tab, TextDisplay };