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.Content | max-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 가용 폭 중 작은 값을 채택합니다. fallback400px은 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 }}로 덮어쓸 수 있습니다 —$css는ft-utilities레이어라 기본 클램프(ft-components)보다 우선합니다
Base UI Positioner 컨텍스트 밖에서 popup class를 직접 재사용하면 var(--available-*)가 비어 있어 클램프가 무효해집니다. 정상 사용 경로에서는 문제 없습니다. 참고: Base UI Select Positioner
Modal maxHeight를 100dvh 기준으로 (모바일 동적 주소창 대응)
Modal.Popup의 max-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.Root | white-space: nowrap (flex-shrink: 0은 기존) |
StatusBadge.Root | white-space: nowrap (flex-shrink: 0은 기존) |
Tag.Root | white-space: nowrap (flex-shrink: 0은 기존) |
Tabs.Tab | white-space: nowrap + flex-shrink: 0 |
Pagination 페이지 버튼 (itemButtonClass) | white-space: nowrap + flex-shrink: 0 |
Pagination 트리거 버튼 (triggerButtonClass, prev/next/first/last) | flex-shrink: 0 |
Dropdown.Trigger | white-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은 반대로 wrap이 디폴트 (Dropdown / Select / Menu)
popup 내부 item은 좁은 popup에서 가로 스크롤을 만들지 않도록 wrap이 디폴트입니다.
/* Dropdown.Item (Select.Item / Menu.Item이 동일 스타일 상속) */
white-space: normal;
word-break: break-word;popup의 max-width가 var(--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.Title과 Toast.Description에 white-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)**입니다.
| 셀렉터 | specificity | color |
|---|---|---|
.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-5for day,text-4for month)으로 떨어뜨려 "선택 불가" 의미를 명확히 함 - today emphasis(밑줄)은 유지해 "오늘은 여전히 today지만 선택 불가"임을 시각적으로 알림 — 폰트 크기·굵기도 그대로
| 상태 | day | month |
|---|---|---|
| Today (active) | primary-60 + underline | primary-60 + underline |
| Disabled | text-5 | text-4 |
| Today + Disabled (이번 fix) | text-5 + underline | text-4 + underline |
정리
- 모든 변경이 추가/수정 only — breaking 없음
- 소비자가 별도 마이그레이션할 필요 없음. 좁은 viewport / 긴 라벨 / 모바일 동적 주소창 시나리오에서 컴포넌트가 자연스러워집니다
- 컴포넌트의 책임은 "박스 형태 유지 + popup 클램프 + 내부 wrap 정책"으로 한정. ellipsis는 소비자 디자인 판단으로 위임됩니다 (
$cssopt-in 패턴, 새 문서/스토리에 상세 가이드) - 동기화: docs 22개 페이지에 Viewport Constraint / Ellipsis Overflow (소비자 opt-in) / Toast multiline / today+disabled 절이 추가됐고, storybook에는
ViewportConstraint,EllipsisOverflow,Multiline,TodayDisabled/TodayMonthDisabled스토리가 추가됐습니다
0.1.20
$css 프롭의 staticProperties enum을 MDN 38개 페이지로 재검증해 확장합니다. display(list-item/flow-root/table*), overflow(clip), align/justify(start/end/anchor-center), textWrap, overscrollBehavior, touchAction, contain, contentVisibility 등 모던 CSS 속성을 strict union 타입으로 추가합니다.
0.1.18
TextArea Root/Input compound 분해와 Select.ItemIndicator 제거 두 갈래 breaking과 함께, focusableWhenDisabled 모드 / Field.required ctx 전파 / Item ctx propagation / leaf own override 일괄을 도입합니다.