Featuring Design System
ChangelogComponents

0.1.19

Base UI Positioner의 --available-* 변수로 popup이 viewport를 넘지 않게 자동 클램프하고, 박스/인터랙티브 컴포넌트는 nowrap 디폴트로 정합하며, Toast \n 보존과 Calendar today+disabled 색상 specificity 충돌도 함께 해결합니다.

이번 릴리즈는 시각 정합(visual contract)만 손봅니다. breaking 없이 모두 추가/수정으로, 소비자가 별도 마이그레이션할 게 없습니다. 좁은 viewport·긴 라벨 시나리오에서 컴포넌트가 자연스러워지는 4개 주제 — popup viewport 클램프, nowrap 디폴트, Toast \n 보존, Calendar today+disabled 색상 — 가 포함됩니다.

Viewport-aware popup 클램프 (Dropdown / Select / Menu / Popover / Tooltip)

좁은 viewport에서 popup이 화면 밖으로 잘리던 문제를 해결합니다. Base UI Positioner가 트리거 기준 viewport 가장자리까지의 가용 공간을 노출하는 --available-height / --available-width CSS 변수를 popup 본체의 max-height / max-width에 적용합니다.

컴포넌트클램프
Dropdown.Popup (Select.Popup / Menu.Content가 래핑)max-height: var(--available-height) + max-width: var(--available-width)
Popover.Content (popup 본체)동일
Tooltip.Contentmax-width: min(400px, var(--available-width, 400px)) + max-height: var(--available-height) + overflow-y: auto
  • 가용 공간을 넘지 않게 자동 묶이고, 내용이 넘치면 popup 내부에서 세로 스크롤됩니다 (overflow-y: auto). 기본 placement로 공간이 부족하면 Floating UI가 반대 방향으로 flip합니다
  • Tooltip디자인 상한(400px)과 viewport 가용 폭 중 작은 값을 채택합니다. fallback 400px은 Positioner 컨텍스트 바깥(스냅샷 도구 등)에서 var가 비었을 때 동작. 가로 wrap은 기존의 pre-wrap + word-break: break-all이 그대로 담당하고, 세로 클램프 시 silent clipping 방지를 위해 overflow-y: auto 추가
  • Select.Popup은 별도로 --anchor-width(트리거 폭)를 받아 너비를 트리거에 맞춥니다. max-width: var(--available-width)는 anchor-width가 viewport를 넘을 때 안전망 역할
  • 소비자가 추가 설정할 필요 없음. 더 작게 잡고 싶다면 $css={{ maxHeight, maxWidth }}로 덮어쓸 수 있습니다 — $cssft-utilities 레이어라 기본 클램프(ft-components)보다 우선합니다

Base UI Positioner 컨텍스트 에서 popup class를 직접 재사용하면 var(--available-*)가 비어 있어 클램프가 무효해집니다. 정상 사용 경로에서는 문제 없습니다. 참고: Base UI Select Positioner

Modal.Popupmax-height 계산이 다음과 같이 변경됩니다.

- max-height: calc(100vh  - var(--spacing-2000) - var(--spacing-1200));
+ max-height: calc(100dvh - var(--spacing-2000) - var(--spacing-1200));
  • 100vh는 모바일 브라우저의 동적 UI(주소창) 영역까지 포함하므로 popup이 주소창 뒤로 잘리는 문제 발생
  • 100dvh(dynamic viewport height)는 실제 가시 영역만 잡아 iOS Safari, Chrome Mobile에서도 모달이 화면 안에 들어옴
  • 컨텐츠가 max-height를 초과하면 Modal.Body가 내부 스크롤 (Title/Actions 고정) — 동작 변화 없음. 모달 전체 높이만 viewport에 정확히 묶임

지원 브라우저: Safari 15.4+, Chrome 108+, Firefox 101+. 매우 오래된 브라우저는 calc() 안의 100dvh가 무효화되어 클램프가 풀리지만, 모달 자체는 그대로 동작합니다 — 단, 매우 긴 컨텐츠에서는 viewport 초과 가능.

Drawer.Popup은 이미 100dvh / 100vw로 풀-블리드 되어 있어 이번 릴리즈에서 변경 없음.

박스/인터랙티브 컴포넌트 nowrap 디폴트

좁은 부모 컨테이너 안에서 박스 형태 컴포넌트가 wrap되거나 짜부되지 않도록 다음 컴포넌트에 white-space: nowrap + (필요 시) flex-shrink: 0이 디폴트로 들어갑니다.

컴포넌트새로 추가된 속성
Button.Rootwhite-space: nowrap (flex-shrink: 0은 기존)
StatusBadge.Rootwhite-space: nowrap (flex-shrink: 0은 기존)
Tag.Rootwhite-space: nowrap (flex-shrink: 0은 기존)
Tabs.Tabwhite-space: nowrap + flex-shrink: 0
Pagination 페이지 버튼 (itemButtonClass)white-space: nowrap + flex-shrink: 0
Pagination 트리거 버튼 (triggerButtonClass, prev/next/first/last)flex-shrink: 0
Dropdown.Triggerwhite-space: nowrap + flex-shrink: 0

이 정책으로 다음 시나리오에서 컴포넌트가 자기 크기를 잃지 않습니다.

  • flex 컨테이너 안에서 형제 요소가 늘어나도 짜부되지 않음
  • 컨테이너 폭이 부족하면 wrap되는 대신 가로 overflow (컨테이너에 overflow-x: auto 또는 flex-wrap: wrap을 부모가 직접 결정)
  • 매우 긴 라벨이 들어와도 컴포넌트의 박스 형태가 무너지지 않음

ellipsis가 필요한 소비자는 자식 텍스트 컴포넌트에 $css로 opt-in 합니다. 새 문서의 Ellipsis Overflow (소비자 opt-in) 절을 참고하세요.

<Button.Root $css={{ maxWidth: '180px' }}>
  <Button.Text $css={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
    매우 긴 동적 버튼 라벨
  </Button.Text>
</Button.Root>

popup 내부 item은 좁은 popup에서 가로 스크롤을 만들지 않도록 wrap이 디폴트입니다.

/* Dropdown.Item (Select.Item / Menu.Item이 동일 스타일 상속) */
white-space: normal;
word-break: break-word;

popup의 max-widthvar(--available-width)로 클램프된 상황에서 item이 nowrap이면 가로 스크롤이 생기는데, popup 안에서 가로 스크롤은 키보드 내비게이션과 충돌하므로 wrap이 안전한 디폴트입니다.

한 줄 ellipsis가 필요하면 <Select.Item $css={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} /> 로 opt-in 합니다.

Toast \n 줄바꿈 보존 (white-space: pre-line)

Toast.TitleToast.Descriptionwhite-space: pre-line이 적용됩니다. 소비자가 전달한 문자열의 \n이 줄바꿈으로 렌더되고, 연속 공백은 1개로 압축됩니다.

toast.add({
  status: 'success',
  title: '저장이 완료되었습니다',
  description: '1. 변경 사항이 적용됐습니다.\n2. 동기화는 최대 5분 소요됩니다.\n3. 문제 발생 시 헬프데스크로 문의 주세요.',
});

toast.add({
  status: 'error',
  title: '세션 만료 임박\n(잠시 후 자동 로그아웃)',
  description: '저장하지 않은 변경 사항이 있다면 지금 저장해 주세요.',
});
  • 명시적 라인 브레이크가 필요한 알림(여러 단계 안내, 다중 항목 표시)에 활용
  • 자동 wrap은 그대로 작동 — 박스 폭을 넘는 긴 문장은 여전히 자동 줄바꿈됩니다

의도치 않게 \n이 들어간 외부 문자열(서버 에러 메시지 등)은 그대로 줄바꿈됩니다. 사용자에게 노출되는 메시지에 \n이 섞일 가능성이 있다면 replace(/\n/g, ' ')로 정제하세요.

pre-wrap이 아닌 pre-line을 선택한 이유: 자동 wrap을 유지하면서 명시적 \n만 보존하기 위함입니다. pre-wrap은 연속 공백도 모두 보존하여 의도치 않은 정렬 깨짐 위험이 있습니다.

Calendar today + disabled 색상 specificity 충돌 해결

오늘 날짜가 minDate / maxDate 바깥에 위치해 today와 disabled가 동시 적용될 때, today 색이 disabled를 이겨 활성 톤으로 보이던 문제를 해결합니다.

원인 — react-datepicker 셀렉터의 specificity

react-datepicker가 출력하는 today 셀렉터는 내부적으로 :not()을 포함해 specificity가 **(0, 3, 0)**으로 계산됩니다. 반면 .disabled의 color 룰은 클래스 두 개라 specificity가 **(0, 2, 0)**입니다.

셀렉터specificitycolor
.today:not(.disabled):not(.outside-month) 등 (today active 톤)(0, 3, 0)primary-60
.today.disabled (color 룰 없음 → 위 룰 적용)(0, 2, 0)(위가 우선)

자연 캐스케이드로는 today가 우선하므로, today가 disabled여도 활성 톤 색이 그대로 보였습니다.

해결 — today + disabled 조합 셀렉터에 color 명시

/* day calendar */
'&.react-datepicker__day--today.react-datepicker__day--disabled': {
  color: vars.semantic.color.text[5],
  textDecorationLine: 'underline',
  textDecorationColor: vars.semantic.color.text[4],
},

/* month picker */
'&.react-datepicker__month-text--today.react-datepicker__month-text--disabled': {
  color: vars.semantic.color.text[4],
  textDecorationLine: 'underline',
  textDecorationColor: vars.semantic.color.text[4],
},
  • 색은 disabled 톤(text-5 for day, text-4 for month)으로 떨어뜨려 "선택 불가" 의미를 명확히 함
  • today emphasis(밑줄)은 유지해 "오늘은 여전히 today지만 선택 불가"임을 시각적으로 알림 — 폰트 크기·굵기도 그대로
상태daymonth
Today (active)primary-60 + underlineprimary-60 + underline
Disabledtext-5text-4
Today + Disabled (이번 fix)text-5 + underlinetext-4 + underline

정리

  • 모든 변경이 추가/수정 only — breaking 없음
  • 소비자가 별도 마이그레이션할 필요 없음. 좁은 viewport / 긴 라벨 / 모바일 동적 주소창 시나리오에서 컴포넌트가 자연스러워집니다
  • 컴포넌트의 책임은 "박스 형태 유지 + popup 클램프 + 내부 wrap 정책"으로 한정. ellipsis는 소비자 디자인 판단으로 위임됩니다 ($css opt-in 패턴, 새 문서/스토리에 상세 가이드)
  • 동기화: docs 22개 페이지에 Viewport Constraint / Ellipsis Overflow (소비자 opt-in) / Toast multiline / today+disabled 절이 추가됐고, storybook에는 ViewportConstraint, EllipsisOverflow, Multiline, TodayDisabled / TodayMonthDisabled 스토리가 추가됐습니다