# Select 현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Select 고유의 API·Token·상태·결정만 기록한다. ## 1. 상태 | 항목 | 내용 | |---|---| | 구현 상태 | HM 스타일 적용 완료 (`hm-select-trigger` class 없음) | | 기준일 | 2026-10-02 | | 기반 | shadcn/ui Select, Radix `Select` | | 대표 Story | `Components/Form/Select` → `All` (이름 "Select") | | 보류 | Item Disabled, 일반 Focus 두께 등 (§11) | ## 2. 적용 근거 - **ui_viz Confirmed**: size/높이(select.md §1.1 D-004, §8), 색·border(§10, §1.3, D-005, M06), indicator glyph(§7, 사용자 제공 Source), Trigger/Content 연결 윤곽(§8). - **ui_viz BASELINE_VALUE**: padding-x 12, gap 12, option 높이 42(§8, §17) — "현재 적용 계약"으로 표기된 값. - **HM 사용자 결정**: Trigger Disabled opacity `0.6`(2026-10-02, §10). - **shadcn/Radix 유지**: 구조·접근성·positioning(§7). - **HM 사용자 결정(2026-10-06)**: Error는 Destructive border, Keyboard Focus는 독립 Focus Semantic ring. ## 3. 기본 규격 | Size | 외곽 | Trigger 내부 높이 | Trigger 글자 | |---|---|---|---| | `medium` | 34 | 32 | 14 / 20 / 400 | | `large` (기본) | 44 | 42 | 16 / 24 / 400 | - 높이는 `min-height = 내부 높이 + 1px border × 2`. - Padding-x 12, gap 12, border 1px `--hm-sem-stroke-input-default`, radius `--hm-radius-input`, face `--hm-sem-bg-surface`. - Placeholder(`[data-placeholder]`): `--hm-sem-fg-placeholder`. - Trigger와 Value는 `min-width: 0`; 긴 placeholder/선택값은 한 줄 ellipsis로 줄고 18px indicator를 밀지 않는다. ui_viz 공개 Story에 없던 현재 HM 복원력 규칙이며 parity 판정 근거로 쓰지 않는다. - Icon: HM glyph, 18×18 `viewBox`, `M5 7 L9 11 L13 7`, stroke 1.50542, 색 `#2d2d2d`(`--hm-select-indicator-color`). - Content: Trigger와 같은 폭(`--radix-select-trigger-width`), border 1px focus 색, `bottom`일 때 아래쪽만 radius. - Option: Trigger의 size를 따른다. min-height 42(두 size 공통, ui_viz §8), padding-x 12, 글자 Medium 14/20/400(Medium Trigger와 동일) · Large 16/24/400, radius 0. ui_viz는 Medium Option 글자 Role을 TBD로 남겼고, 2026-10-06 Trigger 값에 맞추기로 했다(사용자 요청). 높이·폭은 size로 바꾸지 않는다(Content = Trigger 외곽 폭). - size 전달: `SelectTrigger size`가 유일한 지정처다. `Select`가 내부 context로 받아 Portal된 `SelectContent`의 `data-size`로 노출하고, `select.css`가 `[data-slot="select-content"][data-size]`로 Option profile을 정한다. `SelectContent`·`SelectItem`에는 size prop이 없다. ## 4. 상태 규격 | 상태 | 값 | 근거 | |---|---|---| | Default | 위 기본 규격 | ui_viz Confirmed | | Focus (`:focus`) | border 색 → `--hm-sem-stroke-input-focus`, 두께 1px 유지 | ui_viz Confirmed (1.5px 두께는 ui_viz FOLLOW_UP으로 보류) | | Open (`[data-state="open"]`) | border 색 → focus 색, 위쪽 모서리만 round(리스트와 이어짐) | ui_viz Confirmed | | Selected (Item `[data-state="checked"]`) | Hover와 같은 배경 면(`--hm-sem-surface-highlight`, Accent 45%), border·check icon 없음 | 표현: ui_viz Confirmed / 색: shadcn Token(`accent`) | | Highlighted (Item `focus:`/`data-highlighted`) | Accent 45%(`--hm-sem-surface-highlight`) | 위와 동일 | | Disabled (Trigger `:disabled`) | Default paint 유지, `opacity: 0.6`, `cursor: not-allowed` | opacity: 사용자 결정 | | Disabled (Item `data-disabled`) | shadcn 기본 `opacity-50`, 포인터 차단 | **미검증**(§11) | | Invalid (`aria-invalid="true"`) | border 1px `--hm-sem-action-destructive`, Error ring 없음 | 2026-10-06 사용자 결정 | | Invalid + Keyboard Focus (`:focus-visible`) | Destructive border 유지 + Focus Semantic 3px / 50% outer ring | 위와 동일 | | Invalid + Open | Trigger Destructive border, 연결 seam은 Neutral. Content의 기존 Surface / Focus outer border / Accent 45% Option 유지 | 위와 동일 | | Disabled Invalid | Destructive border + 기존 opacity 0.6, focus 불가 | 위와 동일 | ## 5. API ``` SelectTrigger size: "sm" | "default" → "medium" | "large" 기본 "large" SelectContent sideOffset: shadcn 기본 4 → 기본 -1 (Trigger와 1px border를 공유) SelectItem: children 앞의 ItemIndicator(check icon) 제거. children만 ItemText로 렌더. ``` - `Select`, `SelectValue`, `SelectGroup`, `SelectLabel`, `SelectSeparator`, 스크롤 버튼은 shadcn API 그대로. - 출력 속성: `data-slot="select-trigger"`, `data-size`. HM 전용 `error`/`open`/`focused` prop은 없다. 상태는 Radix 속성(`data-state`, `data-placeholder`, `data-highlighted`, `data-side`)과 `:disabled`로 적용한다. ## 6. Token Component Mapping 위치: `packages/ui/src/theme/select.css` (`--hm-select-*`). - 재사용한 공통 Semantic: `--hm-radius-input`(radius), `--hm-sem-bg-surface`, `--hm-sem-fg-primary`, `--hm-sem-fg-placeholder`, `--hm-sem-stroke-input-default`, `--hm-sem-stroke-input-focus`. - Select 전용 값: `--hm-select-control-height-{md,lg}`(32/42), `--hm-select-padding-x`, `--hm-select-gap`, `--hm-select-option-height`, `--hm-select-font-size-*`, `--hm-select-disabled-opacity`, `--hm-select-indicator-color`. - indicator는 `--hm-select-indicator-color` → `--hm-sem-palette-indicator`를 참조한다(Default `#2d2d2d`). 신규 Primitive 없음. - Input·Button과 값(42 등)이 겹쳐도 alias하지 않는다. 각 ui_viz 규격이 따로 확정한 값이다. - `--hm-select-disabled-opacity: 0.6`은 Select 전용 Component Token이며 Input의 Disabled와 공유하지 않는다. ## 7. shadcn / Radix에서 유지한 항목 Radix Root/Trigger/Portal/Content/Viewport/Item/ItemText 구조, 키보드 조작·typeahead·focus 관리, Portal, popper positioning(`position="popper"`, `align="start"`), 스크롤 버튼, 열림/닫힘 animation class, `data-slot`, `cn(..., className)`, Trigger의 native `aria-invalid` 전달, Item의 `data-disabled` 처리. ## 8. HM에서 변경한 항목 - Size API(`sm|default` → `medium|large`), 기본 `large`, `data-size`. - `sideOffset` 기본 `-1`, Content 폭을 Trigger 폭에 맞춤, Trigger/Content 연결형 윤곽. - chevron 아이콘을 HM glyph(inline SVG)로 교체. - Item check indicator 제거, Selected를 배경 면으로 표현. - shadcn Trigger의 높이·padding·border·focus ring class 제거 → theme CSS가 담당. - `hm-select-trigger` class 제거. - 변경하지 않음: 스크롤 버튼의 lucide chevron(ChevronUp/Down). ## 9. Storybook - 대표 Story `All`("Select"): Sizes(Medium / Large 비교, 각 Trigger로 실제 목록 열기) → States(`selected`, `disabled`, `disabled-placeholder`) → Invalid / Focus(`invalid`, `invalid-disabled`). 2026-10-22 Overview의 `default`·로드 시 열린 `open` 예시는 대표 화면에서 뺐다. 열린 목록은 숨김 `Open`, 크기별 수치 목록은 숨김 `Sizes`("Compare Sizes")에서 확인한다. size 생략 시 Large(§5)는 그대로다. - verification(`tags: ["verification"]`): `Default`, `Open`, `Selected`, `Disabled`, `Sizes`("Compare Sizes"), `Medium`, `Large`, `LongContent`, `Scroll`, `Playground`(HM 전용). - `Disabled` Story 구성: Trigger 비활성 상태들(placeholder 포함). - `InvalidFocus` verification Story: 앞/뒤 버튼 사이의 Invalid Trigger와 Disabled Invalid Trigger. Tab / Shift+Tab, Enter / Space, Escape 및 focus return을 실제 키보드로 확인한다. - `LongContent`와 `Scroll`은 ui_viz 공개 Story parity가 아니라 좁은 폭·긴 값·Radix scroll 회귀를 확인하는 HM 전용 Story다. - sidebar는 `All` 하나만 보이는 1뎁스. 운영 규칙은 지침 §6.3, §7 참조. ## 10. 사용자 확정 결정 **2026-10-02** - Select Trigger Disabled opacity `0.6` 확정(`select.css`의 `--hm-select-disabled-opacity`). - 같은 날 Input Disabled `0.6`도 확정되었다(input.md 참조). 현재 적용 범위는 Input / Select이며 전체 디자인 시스템 Disabled opacity로 자동 확대하지 않는다. - **Select Item의 Disabled opacity는 이 결정에 포함하지 않는다.** Trigger와 같은 규격이라고 판단하지 않는다. ## 11. 미확정 / 보류 - **Item Disabled**: 현재 shadcn 기본 `opacity-50`. 사용자 결정·ui_viz 근거 없음. 미검증. - **FOLLOW_UP(ui_viz)**: Focus/Open 상태의 border 1.5px 두께. - **ui_viz 미정의**: Group/Label/Separator 규격, 스크롤 버튼 규격, `item-aligned` position. Error 표현은 2026-10-06 HM 사용자 결정이 우선한다. - **BASELINE_VALUE**: padding-x 12, gap 12, option 42는 "현재 적용 계약"이며 별도 Confirmed 결정은 아니다. - Selected/Highlighted는 현재 공통 Accent 45% 정책을 쓴다. ui_viz Temporary 표기는 도입 당시 근거다. ## 12. 관련 파일 ``` packages/ui/src/components/ui/select.tsx packages/ui/src/theme/select.css src/components/select.stories.tsx src/components/story-board.tsx ``` ## 13. 검증 상태 - 2026-10-05: `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 generated CSS의 `bg-[var(...)]` 경고와 500kB chunk warning은 유지된다. - Storybook computed style: Medium/Large Trigger 34/44px, padding-x 12px, radius 4px, border 1px, Option 42px·16/24/400, Disabled Trigger opacity `0.6` 확인. - Open: Trigger와 Content 폭 동일, `sideOffset=-1` 접점, Focus/Open `#123328`, Content border/radius/shadow와 Portal listbox 확인. - Interaction: click open, ArrowUp/Down, Home/End, typeahead, Enter select, Escape, focus return, Disabled open 차단, 32개 Option의 wheel/keyboard scroll 확인. - Responsive: 1280/768/390px에서 popup clipping·문서 가로 overflow 없음. 390px의 280px Trigger에서 긴 placeholder/선택값 ellipsis와 indicator 비중첩 확인. - Story: `All`의 공개 구역 4개와 숨김 `LongContent`/`Scroll` verification Story 확인. - 미확인/보류: Item Disabled 시각, collision으로 `side="top"`이 된 연결 윤곽의 별도 디자인(원 ui_viz도 viewport edge flip TBD), 브라우저별 렌더 차이. ### Invalid + Focus 검증 (2026-10-07) - Focus ring: `--ring` → `--hm-sem-stroke-input-focus` → `--hm-color-border-focus`. Invalid border: 기존 `--hm-sem-action-destructive`. 새 Runtime Role / Production Error prop / 메시지 컴포넌트 없음. - Chromium computed style: Invalid border `rgb(211, 47, 47)`, Invalid + Keyboard Focus box-shadow `color(srgb 0.14902 0.4 0.317647 / 0.5) 0px 0px 0px 3px`, `outline-style: none`. Invalid 단독은 `box-shadow: none`. - Theme Designer UI에서 Focus `#c75b00`, Destructive `#d41466`, Active `#007f73`, Accent `#ffd36d`로 분리. Invalid border `rgb(212, 20, 102)` + Focus shadow `color(srgb 0.780392 0.356863 0 / 0.5) 0px 0px 0px 3px` 확인. - `npm run typecheck`, `npm run build`, `npm run build-storybook` 통과. 기존 문서 예시 CSS warning 및 Storybook chunk 크기 / plugin timing warning 유지. - 검증 범위: Chromium 환경. 다른 브라우저의 렌더링은 별도 확인이 필요하다. - 숨김 `InvalidFocus`에서 Closed Invalid / Tab Focus / Invalid Open / 방향키 강조 / Disabled Invalid 확인. Invalid border computed 1px; Tab 전후 shadow none → 3px Focus ring. Tab은 Disabled를 건너뛰고, Shift+Tab으로 복귀. Enter / Space open, ArrowDown 이동, Escape close 및 Trigger focus return 확인. - Invalid + Open은 Trigger Destructive border와 Neutral seam `rgb(222, 219, 210)`을 동시에 유지. Popup white Surface, 외곽 `rgb(199, 91, 0)`(Focus), Selected / Highlighted `color(srgb 1 0.827451 0.427451 / 0.45)`(Accent) 확인. Popup에 Error 색 없음. 대표 `All` 회귀 확인.