Skip to content

폼 컨트롤 (Input · Select · Picker) ​

입력 · 드롭다운 · 체크박스 · 라디오 · 스위치의 사이즈와 상태를 확인하고, 사용 기준과 마크업을 찾아보세요.

CSS + Alpine · Web / Mobile · 진행 중(WIP) · 문서 포맷 시범 적용

Interactive playground ​

옵션을 바꾸면 미리보기와 코드가 함께 바뀌어요.
미리보기 테마

회사 이메일을 입력하면 초대 메일이 발송돼요.

실제 DS 클래스로 렌더해요. 텍스트 필드와 드롭다운은 같은 스케일이라 나란히 두면 높이가 맞습니다. 사이즈는 항상 클래스로 지정하세요 — Tailwind h-12 같은 픽셀 지정은 높이만 먹고 보더·패딩·폰트가 따라오지 않아요. 설명 줄은 색이 필드 상태를 따라갑니다(기본 muted → focus black → 에러 red) — 입력칸을 눌러 보세요.
<label class="text-b5 font-medium text-label-assistive mb-1.5">
  이메일
</label>
<input class="ds-input--40 w-full" placeholder="user@example.com" />
<p class="ds-form-help">회사 이메일을 입력하면 초대 메일이 발송돼요.</p>

종류 · 사이즈 · 상태 · 필드 슬롯을 바꾸면 실제 DS 클래스로 렌더된 컨트롤과 마크업이 함께 바뀝니다. 피커(달력 · 시간 · 날짜+시간)와 메뉴 패널을 포함한 전 종류는 「모아보기」 탭에 있습니다.

규칙 (강제) · 코딩 에이전트용

Input · Select · 필드 구성의 강제 규칙. 상세·토큰·피커는 아래 본문.

  • 기본 32px. 사이즈 클래스를 안 주면 input/select/박스버튼은 medium 32px가 자동(이전 40→32). 요소 규칙이라 클래스 없이도 전 서비스 적용됨.
  • 입력칸 모서리는 --ds-textfield-radius(입력칸 · textarea) · --ds-dropdown-radius(select), 기본 4px. 입력칸만 바꾸려면 ds-field-r-4/8/12/16 을 입력칸이나 폼 컨테이너에 얹습니다 — 입력칸과 드롭다운이 같이 바뀌고 버튼은 그대로예요(2026-10-02~). 버튼과 함께 바꾸려면 모서리 축 ds-r-*(텍스트필드는 따르지만 select 는 따르지 않음 → 모서리). rounded-* 유틸을 직접 붙이지 마세요 — rounded-lg 는 10px 로 덮여 버튼과 어긋납니다. 게이트 화면 입력칸 규격: 게이트 화면.
  • 사이즈는 ds-input--XX/ds-select--XX 스케일(52/48/40/32/28/24)로만. input·select가 같은 스케일이라 나란히 두면 높이가 flush. 크기(height/font/padding)는 native 규칙 + --XX 모디파이어가 담당하고 Tailwind는 마진·배치 유틸로만 — 기본 규칙이 :not([class*="ds-input--"])로 모디파이어를 제외해 --XX가 !important 없이 이김.
  • 필드 구성 스택: 라벨 → 필수 * → 입력칸 → 설명 → 에러(위→아래). 필수 = 라벨 끝 <span class="text-red-400">*</span> + HTML required 병행.
  • 설명 = .ds-form-help (12px·입력칸 아래 6px·항상 표시). 색은 필드 상태 자동 추종(rest muted → focus black → error red), sibling 조합자로 동작하므로 입력칸과 같은 부모의 뒤쪽 형제여야 함(JS 토글 불필요). 앱 로컬 text-b6 neutral-400/500 재조합 금지.
  • 에러 = .ds-form-error (danger red·12px·아래 6px·x-show 토글), 점프 방지 정렬 폼은 .ds-form-error-slot(min-height 17px). 입력칸 보더는 공유 ds-input--error/ds-select--error 표준(admin 로컬 .is-error 금지).
  • 숫자 = ds-input--number — 스피너를 평소 숨기고 hover/focus 시에만 노출 + tabular-nums. 숫자 처리만 담당하므로 크기(ds-input--XX)·너비(w-full)·정렬(text-center)과 조합.
  • 상태 토큰: hover border #c1c1c1 · focus border #1b1b1b(select은 ring) · disabled opacity:.45+not-allowed · error border --ds-color-danger. 이 클래스/토큰들은 공유 design-system.css가 단일 출처(앱 로컬 복제 금지).
  • 체크박스 = ds-checkbox, 라디오 = ds-radio (한 세트 — 값은 같고 모양만 사각/원형). 클래스 없는 맨 <input type="checkbox|radio">는 브라우저 기본 컨트롤(OS 파랑) 로 렌더돼 DS 팔레트를 벗어납니다 — 반드시 클래스를 붙이세요. 크기는 16px 기본 / --lg 20px 둘뿐이고, accent-color·직접 그린 가짜 체크박스는 금지. 묶음(팀 · 전체) 체크박스의 일부 선택은 x-effect 로 $el.indeterminate 를 넣어 가로 막대로 보여요 — 일부 선택. 선택지에 보조설명이 붙어 라벨이 2줄이 되면 --lg(20px) + items-start + mt-0.5(컨트롤을 첫 줄에 맞춤) · gap-2.5 · 제목 text-b5 / 설명 text-b6 text-label-muted · 항목 간 space-y-1.5 — 2줄 조합. 상세: 체크박스 · 라디오.
  • 스위치 = ds-switch (작은 자리는 --sm, 이 둘뿐). <input type="checkbox">에만 붙입니다 — <button>/<div>로 트랙+손잡이를 직접 그리지 마세요(키보드·스크린리더가 사라집니다). 켜짐 primary 주황 / 꺼짐 neutral-300 / disabled 45%는 CSS가 강제하므로 색을 따로 주지 마세요. 글자 라벨이 옆에 없으면 aria-label 필수. 용도 경계: 즉시 반영되는 기능 on/off만 스위치, 저장을 눌러야 반영되는 폼 값은 체크박스, 예/아니오 상태 선택은 불리언 select(Modal). 상세: 스위치.
  • 피커(날짜·월·시간): 커스텀 우선(dsDatePicker/dsTimePicker + include 한 줄), 네이티브는 대안. 모달 안 레이어·닫기 규칙은 Modal.
  • 메뉴 패널 = .ds-menu + .ds-menu__item(아이콘+라벨은 --row, 선택은 --selected). 패널의 padding·border·shadow는 .ds-menu가 가지므로 래퍼에 bg-white border rounded shadow-* py-*를 따로 붙이지 마세요. 파괴적 항목(삭제)은 ds-menu__item--danger — hover에서만 의미색 red. hover:bg-red-50 같은 즉석 유틸로 위험 항목을 만들지 마세요. 고를 수 없는 항목은 disabled(또는 aria-disabled="true")만 주면 됩니다 — 글자가 disable 색으로 내려가고 hover 배경이 사라집니다. bg-neutral-50 text-neutral-400 cursor-not-allowed 같은 조건부 유틸을 직접 붙이지 마세요.
  • 모바일(<768): ds-input--48/ds-select--48 이 권장 기본값(손가락 타깃 — 강제 아님, 사이즈 고르기). 사이즈는 항상 사이즈 클래스(--52/--48/--40/--32…)로 고르고 픽셀을 직접 쓰지 마세요. ⚠️ 모바일에서 입력칸을 탭하면 iOS가 화면을 자동 확대합니다(글자 16px 미만) — 아직 규칙을 정하지 않은 미해결 항목이니 임의로 정하지 말고 그 섹션을 읽으세요. DS 사이즈 클래스엔 md: 프리픽스가 안 붙으므로, 모바일에서만 크기를 바꾸려면 앱 main.css의 @media (max-width:767px)에서 스코프 잡아 덮으세요. 상세: Modal → 모바일.
  • 모바일 텍스트 필드는 다섯 경우로 나뉩니다 — 폼 입력칸(한 줄 · 선택 · 여러 줄)과 대화 입력줄(채팅 · AI 어시스턴트). 대화 입력줄은 폼 입력칸이 아닙니다: ds-input--48 을 붙이지 말고 모바일 대화 화면 캡슐형(.ds-composer--capsule + textarea.ds-input--40.ds-input-plain)을 쓰세요. 반대로 폼 사유·메모 칸에 캡슐형을 쓰지 마세요. 표: 모바일 텍스트 필드.
  • 다크 모드(2026-10-19) — 입력칸 hover 테두리 --ds-textfield-border-hover 는 다크에서 line-alternative(라이트 line-strong #c1c1c1 그대로). 입력칸이 끼는 묶음의 간격은 space-y/mt-* 가 아니라 flex flex-col gap-* — DS 입력칸 기본 규칙(레이어 밖 margin: 0)이 space-y 여백을 지워 칸이 붙는다(휴가 신청 「기간」 사례).

진행 중 (Form controls — work in progress)

  • 텍스트 필드(input/textarea)와 드롭다운(select)의 공유 사이즈 체계. PR #247에서 기본 높이 40→32px 통일 + ds-input--XX/ds-select--XX 스케일 정리.
  • !important 없이 사이즈가 적용됨 — 기본 규칙이 --XX 모디파이어를 :not()으로 제외해 모디파이어가 자연 specificity로 이김. (강제 최소화 설계)
  • ⚠️ --ds-textfield-*/--ds-dropdown-* 토큰이 raw hex로 정의 — 검토 필요 사항.

필드 구성 (Field anatomy) ​

하나의 텍스트 필드는 위에서 아래로 라벨 → (필수 *) → 입력칸 → 설명 → 에러 순서로 쌓입니다. 설명과 에러는 **같은 자리(입력칸 아래 6px·12px)**를 공유해서, 에러가 떴다 사라져도 레이아웃이 흔들리지 않습니다.

파트클래스 / 마크업스펙
라벨<label> + text-b5 font-medium text-label-assistive12px(b5) · 입력칸 위 6px(mb-1.5)
필수 표시라벨 끝에 <span class="text-red-400">*</span>red-400(#ff5f53) · 입력칸엔 HTML required도 함께
입력칸input.ds-input--40 등사이즈 스케일
입력칸 에러 보더ds-input--error / ds-select--errorborder = --ds-color-danger(focus 시 ring)
설명(description).ds-form-help12px · 입력칸 아래 6px · 항상 표시 · 색은 필드 상태 추종(rest=muted → focus=black → error=red)
에러 메시지.ds-form-error (높이 예약은 .ds-form-error-slot)danger red(#e63224) · 12px · 입력칸 아래 6px · x-show로 토글

설명 (description) — .ds-form-help ​

입력 규칙·맥락을 항상 보여 주는 보조 줄입니다(x-show 불필요). 12px·입력칸 아래 6px은 고정이고, 색은 필드 상태를 따라갑니다 — 평소엔 눈에 안 띄게, 입력 중엔 또렷하게, 에러일 땐 경고로:

필드 상태설명 색토큰
기본 (focus out)muted 회색--ds-label-fg-muted (#8f8f8f)
선택 (focus in)black--ds-label-black (#1b1b1b)
에러danger red--ds-color-danger (#e63224)
html
<input class="ds-input--40 w-full" placeholder="user@example.com" />
<p class="ds-form-help">회사 이메일을 입력하면 초대 메일이 발송돼요.</p>

상태 색은 CSS 하나로 자동 동작합니다(JS 토글 불필요) — .ds-form-help가 입력칸의 :focus·ds-input--error를 sibling 조합자로 읽습니다. 따라서 .ds-form-help는 입력칸과 같은 부모의 (뒤쪽) 형제여야 합니다(필드 구성 스택 그대로). 에러는 focus보다 우선(에러+focus여도 red).

왜 표준 클래스인가 — 그동안 설명 줄은 앱마다 text-b6 text-neutral-400/500 mt-0.5~1.5로 제각각이었습니다(색·간격 드리프트). .ds-form-help로 크기(12px)·간격(6px)·상태 색(muted→black→red)을 한 곳에 고정해 에러 줄과 세로 위치가 정확히 맞도록 통일합니다.

에러 메시지 — .ds-form-error / .ds-form-error-slot ​

유효성 에러를 입력칸 아래에 띄웁니다. danger red·12px·아래 6px. 보통 x-show로 조건부 노출하며, **입력칸 보더는 ds-input--error**로 같이 빨강 처리합니다(메시지+보더 한 쌍).

html
<input class="ds-input--40 w-full"
       :class="errors.email && 'ds-input--error'" />
<!-- 토글형: 에러일 때만 자리 차지 -->
<p x-show="errors.email" class="ds-form-error" x-text="errors.email"></p>
  • .ds-form-error — x-show로 보였다 사라지는 기본형. 평소엔 자리를 차지하지 않습니다.
  • .ds-form-error-slot — min-height:17px로 빈 상태에도 한 줄 높이를 예약합니다. 에러 토글 시 폼이 위아래로 점프하면 안 되는 정렬 폼(예: 직원 추가/수정 패널)에 사용.

단일 출처 정비 — 두 메시지 클래스는 원래 apps/admin/src/styles/main.css에만 있던 로컬 규칙이었고, 이번에 공유 design-system.css로 승격했습니다. 앞으로 전 앱이 같은 클래스를 씁니다(앱 로컬 복제 금지). 입력칸 보더는 admin의 .is-error가 아니라 **공유 ds-input--error**를 표준으로 합니다.

기본값 — 32px (PR #247) ​

사이즈 클래스를 안 주면 medium 32px가 기본입니다. (이전 40px → 32px로 변경; design-system.css가 단일 출처, Figma는 색만 미러)

요소기본 heightfont비고
input (--XX 없음)32px12pxpadding 0 12px
select (--XX 없음)32px12pxchevron 우측 16px
textareaauto (min 5rem)12pxpadding 10px 16px, resize: vertical
박스 버튼min-height 32px—.ds-tab-item(40px)·.icon-btn(정사각) 제외

강제 없이 동작: 기본 규칙은 input:not(...):not([class*="ds-input--"]) / select:not(.ds-select--24)…:not(.ds-select--52)로 사이즈 모디파이어를 제외합니다. 그래서 ds-input--48 같은 클래스를 주면 !important 없이도 기본 32px 규칙과 경쟁하지 않고 그대로 적용됩니다.

적용 범위 & 도입 현황 ​

이 컴포넌트는 DS에 등록된 변수(크기 24~52 · 상태 · 토큰)를 골라 쓰는 전역 폼 컨트롤입니다. 적용은 두 층으로 나뉩니다.

  • 전역 기본 (자동 · 전 서비스) — input/select/textarea 요소 규칙이라, 클래스를 안 붙여도 모든 입력칸이 기본 32px 룩을 자동으로 받습니다. 마이그레이션과 무관하게 이미 전 서비스에 적용됨.
  • 명시적 사이즈 전환 (도입 진행 중) — 기본 32px가 아닌 크기가 필요한 곳에만 ds-input--XX/ds-select--XX를 부여. 궁극 목표는 전 서비스 정합이며, 현재 진행 상황은 아래와 같습니다.
시점전환된 곳적용 사이즈
PR #247admin · general-affairs 폼 31개 (x-model 전량 보존)--32
모달 마이그레이션직원 추가/수정 패널 · 근태 모달 (hr-system · admin)--40
이후잔여 모달 · 앱 전역진행 예정

"사용처 목록"이 아니라 도입 진행 상황으로 기록합니다 — 기본 룩은 이미 전역이라, 추적할 가치가 있는 건 "기본과 다른 크기로 의도적으로 전환한 곳"뿐입니다. 전체로 퍼져도 이 표는 낡지 않습니다.

사이즈 스케일 ​

텍스트 필드와 드롭다운이 같은 스케일(52/48/40/32/28/24)이라, 어떤 사이즈든 나란히 두면 높이가 flush하게 맞습니다.

ds-input--XX ​

클래스heightpaddingfont
ds-input--5252px0 16px14px
ds-input--4848px0 16px14px
ds-input--4040px0 16px14px
ds-input--3232px0 12px12px
ds-input--2828px0 12px12px
ds-input--2424px0 8px11px

textarea는 위 height를 무시하고 항상 auto-height(min 4rem, padding 8px 12px).

ds-select--XX ​

클래스heightfontpadding-leftchevron
ds-select--5252px14px16px우 16px
ds-select--4848px14px16px우 16px
ds-select--4040px14px16px우 16px
ds-select--3232px12px16px우 16px
ds-select--2828px12px12px우 10px
ds-select--2424px11px8px우 8px

스타일 변형: ds-select--fill-gray · ds-select--fill-white · ds-select--submenu(11px).

어떤 사이즈를 고르나 — 포인터 vs 손가락 ​

스케일 안에서 고르는 기본 기준은 입력 장치입니다. 강제 매핑이 아니라 근거 있는 출발점이니, 화면이 다른 걸 요구하면 다른 값을 고르고 이유를 남기세요.

맥락권장 사이즈왜
데스크톱 폼·모달·필터ds-input--40 / ds-select--40 (조밀한 툴바는 ds-input--32)마우스 포인터 기준. 32가 요소 기본값
모바일 화면 · 모바일 풀페이지 모달 · 직원 앱ds-input--48 / ds-select--48손가락 타깃. 40은 포인터 기준이라 오탭이 난다
표 안 인라인 편집 · 마이크로 그리드ds-input--28 / ds-input--24행 높이에 맞춤

사이즈는 항상 이 클래스로 지정하세요. 픽셀을 직접 쓰면(Tailwind h-12 등) 높이만 먹고 보더·패딩·폰트가 함께 오지 않습니다.

폼 컨트롤은 숫자 표기(--24/28/32/40/48/52, 숫자가 곧 높이 px)이고, 버튼은 영문 표기(ds-btn--xs/sm/md/ml/lg/xl)입니다. 높이가 같아도 클래스 이름은 계열마다 다릅니다.

⚠️ 글자 크기는 아직 미해결입니다. 사이즈 클래스가 정한 글자는 14px인데, iOS Safari는 16px 미만인 입력창을 탭하면 화면을 자동 확대합니다. 즉 지금 모바일 웹에서 입력칸을 탭하면 확대가 뜹니다(알고 두는 상태). 16px로 올리는 것도, viewport로 확대를 막는 것도 각각 대가가 있어 아직 규칙을 정하지 않았습니다 — 임의로 정하지 말고 Modal → iOS 자동 확대의 정리를 먼저 읽으세요.

같은 화면에서 두 스케일을 오가는 법(마크업은 데스크톱 기준, 모바일 구간에서만 덮기)은 → Modal → 모바일.

모바일 텍스트 필드 ​

직원 앱(모바일)의 글자 입력칸은 폼 입력칸 세 가지와 대화 입력줄 두 가지입니다. 둘은 생김새부터 다릅니다 — 폼 입력칸은 선 있는 흰 칸이고, 대화 입력줄은 선 없는 연한 캡슐입니다.

① 한 줄 입력 · ds-input--48

근무 변경 알림을 받을 번호예요.

② 선택 · ds-select--48

③ 여러 줄 (사유 · 메모) · textarea rows="3"

④ 채팅 입력줄 (메신저) · 캡슐형 + 첨부

⑤ AI 어시스턴트 입력줄 · 캡슐형, 첨부 없음

①~③ 은 폼 입력칸(선 있는 흰 칸 · 48), ④⑤ 는 대화 입력줄(선 없는 캡슐 · 안쪽 칸 40 + 위아래 4 = 48)입니다. ④ 의 입력칸을 눌러 보세요 — 포커스가 빠지면 한 줄 말줄임(…)으로 접히고, 포커스면 다섯 줄까지 자랍니다. ⑤ 처럼 아무것도 안 쳤으면 보내기 버튼은 보이지 않고, 글을 치면 나타납니다.

폼 입력칸 — 신청 · 정보 수정 화면 ​

경우마크업규격쓰는 곳(직원 앱)
① 한 줄 입력<input class="ds-input--48 w-full">높이 48 · 좌우 16 · 14px휴가 · 연장근무 · 외근 신청, 서류 등록, 출퇴근 정정 요청
② 선택<select class="ds-select--48 w-full">높이 48 · 14px휴가 신청 · 외근 보고 · 근무 형태 변경 · 서류 등록의 고르기 칸
③ 여러 줄 (사유 · 메모)<textarea rows="2~3" class="w-full text-b4 resize-none">높이는 rows · 14px · 크기 조절 핸들 없음휴가 · 연장근무 · 외근 · 근무 형태 변경 사유, 정정 요청 사유
  • 라벨 · 필수 표시 · 설명 · 에러는 PC 와 같은 필드 구성을 씁니다.
  • 풀페이지 모달(.modal-sheet--full · .ds-modal--full) 안에서는 직원 앱 main.css 가 입력칸 글자를 14px 로 올립니다(DS 기본 12px). 크기 클래스를 붙이지 않은 칸도 14px 로 보이는 이유입니다.
  • 로그인 · 회원가입 입력칸은 공유 템플릿이라 마크업에 48 을 붙이지 못하고, 앱 main.css 의 .auth-form 규칙이 768 미만에서 48 로 덮습니다.

대화 입력줄 — 채팅 · AI 어시스턴트 ​

메시지를 쓰는 칸은 폼 입력칸이 아니라 모바일 대화 화면의 캡슐형 입력줄입니다. 규칙(접힘 · 자람 · Enter · 보내기 아이콘 색)의 정본은 그 페이지이고, 여기서는 두 경우의 차이만 정리합니다.

④ 채팅 입력줄 (메신저)⑤ AI 어시스턴트 입력줄
줄.ds-composer.ds-composer--multiline.ds-composer--capsule같음
입력칸textarea.ds-input--40.ds-input-plain.ds-composer__field같음
왼쪽 버튼첨부 「+」(.ds-composer__btn--ghost, 캡슐 안 흰 원)없음
오른쪽 버튼보내기(.ds-composer__btn--send, 40px 원) — 아무것도 안 쳤으면 숨음(첨부만 있으면 보임)같음(첨부 이미지만 있으면 보임)
안내 글「메시지를 입력하세요...」「예: 남은 휴가, 근무 기록, 외근 신청 방법」(composerPlaceholder)
입력줄 위에 뜨는 것첨부 미리보기예시 질문(입력칸을 눌렀을 때만) · 「바로 갈 수 있는 화면」 팝오버
같은 템플릿의 PC 모습PC 채팅 위젯 = 캡슐 아님(useHostHeader 일 때만 캡슐)관리자 AI 패널 · 도크 = 캡슐 아님(isPageMode 일 때만 캡슐)

공통 동작:

  • 크기: 캡슐 안 칸 40 + 위아래 여백 4 = 캡슐 48 — 폼 입력칸 48 과 같은 손가락 크기입니다. 버튼도 40 이라 한 줄일 때 위 · 아래 · 좌우 여백이 4 로 같습니다.
  • 접힘 / 자람: 포커스가 빠지면 한 줄로 접히고 친 글은 말줄임(…)으로 보입니다(.ds-composer__preview). 포커스면 다섯 줄까지 자라고, 직원 앱 화면 설정 「크게」는 네 줄입니다. 넘으면 칸 안에서 스크롤합니다.
  • Enter: 휴대폰 키보드에서는 줄바꿈이고 전송은 버튼입니다(PC 는 Enter 전송 · Shift+Enter 줄바꿈 — shouldSendOnEnter).
  • 보내기는 누를 수 있을 때만: 아무것도 안 쳤을 때는 보내기 버튼이 빠지고 입력칸이 그 폭을 씁니다. 버튼의 :disabled 를 CSS 가 읽으므로 x-show 를 따로 달지 마세요.
  • 선 없음: 캡슐은 연한 면(neutral-75) 하나이고, 입력칸은 ds-input-plain 이라 흰 면 · 테두리 · 포커스 링이 없습니다. 테두리나 rounded-* 를 덧붙이지 말고, 모서리는 --ds-composer-radius 로 조절하세요.

상태 ​

상태적용스타일
hover:hover:not(:disabled)border --ds-textfield-border-hover (#c1c1c1 · 다크 line-alternative #8f8f8f)
focus:focusborder --ds-textfield-border-focus (#1b1b1b), select은 focus ring
disabled:disabledopacity: 0.45, cursor: not-allowed, bg alt
error.ds-input--error / .ds-select--errorborder --ds-color-danger (+ focus 시 ring)

숫자 입력 (ds-input--number) ​

<input type="number">용 변형. 네이티브 증감 화살표(스피너)를 평소엔 숨기고 hover/focus 시에만 노출합니다 — 평소엔 가운데 정렬 숫자가 화살표에 밀리지 않고 깔끔하게, 조작 시엔 step 단위 증감(▲▼)이 가능. 숫자 정렬용 tabular-nums도 포함합니다. (키보드 ↑/↓는 화살표 노출과 무관하게 항상 동작.)

조합형 변형 — 이 클래스는 숫자 고유 처리(스피너 + tabular-nums)만 담당합니다. 크기는 ds-input--XX, 너비는 w-full, 가운데 정렬은 text-center 유틸과 함께 씁니다(정렬은 선택 — 왼쪽 정렬 금액 입력 등엔 빼면 됨).

항목처리
스피너 (기본)숨김 — webkit ::-webkit-*-spin-button { appearance: none } + Firefox appearance: textfield
스피너 (hover/focus)노출 — appearance: auto로 ▲▼ 복귀(주로 webkit에서 동작). :not(:disabled) 가드
숫자 정렬font-variant-numeric: tabular-nums — 세로로 쌓인 숫자 칸의 자리수 정렬
html
<input type="number" x-model.number="wardSettingsForm.ratioDay" min="1" step="0.5"
       class="ds-input--40 ds-input--number w-full text-center">

Playground 는 상단 Playground의 플레이그라운드에서 숫자 토글로 확인하세요.

흩어진 핵을 대체 — 그동안 숫자 인풋 스피너는 앱마다 제각각이었습니다(admin .spinner-on-hover, .patient-count-input·.sched-grid input[type=number] 항상 숨김, team-manager 인라인 style="-moz-appearance:textfield"). ds-input--number는 그중 검증된 hover/focus 노출 동작(admin .spinner-on-hover)을 공유 DS로 승격한 것으로, 앞으로 숫자 인풋은 이 단일 기준으로 통일합니다.

체크박스 · 라디오 ​

둘은 한 세트예요. 크기(16px)·테두리·선택 색·hover·focus·disabled 값이 완전히 같고, 모양만 다릅니다 — 체크박스는 사각(radius 2px) + 흰 체크, 라디오는 원형 + 가운데 흰 점. 한 화면에 섞여 나와도 어긋나지 않게 값을 공유해요.

체크박스 .ds-checkbox
라디오 .ds-radio
html
<label class="flex items-center gap-1.5">
  <input type="checkbox" class="ds-checkbox" x-model="form.shared" /> 공개
</label>

<label class="flex items-center gap-1.5">
  <input type="radio" class="ds-radio" name="rowBasis" :checked="basis === 'date'" @change="basis = 'date'" /> 날짜 기준
</label>
클래스크기비고
ds-checkbox16px사각 2px · 선택 시 검정 채움 + 흰 체크
ds-checkbox--lg20px손가락 타깃이 필요한 자리(메신저 그룹 선택 등)
ds-radio16px원형(링 12px) · 선택 시 검정 채움 + 흰 점 4px
ds-radio--lg20px링 16px · 점 6px

「선택됨 + disabled」는 채움과 체크를 그대로 둡니다

끌 수 없는 항목(예: 문서 양식의 「용도 · 항상」)은 켜진 채 잠긴 상태라, 검정 채움 + 흰 체크(흰 점)를 유지한 채 45%로만 흐립니다. 비활성 배경(--ds-color-bg-alt)은 선택 안 된 비활성에만 칠합니다(:disabled:not(:checked)). 예전에는 비활성 규칙의 background 한 줄이 체크 글리프까지 지워, 켜져 있는데도 빈 칸으로 보였어요(2026-09-17 수정).

일부 선택 — 묶음 체크박스 ​

팀 · 전체 선택처럼 여러 항목을 한꺼번에 고르는 체크박스는 상태가 셋이에요 — 전부 골랐으면 체크, 몇 개만 골랐으면 가로 막대, 하나도 안 골랐으면 빈 칸. 막대 모양은 채움 · 색이 「선택됨」과 같고 글리프만 다릅니다(메신저 그룹 대화 · 초대하기의 팀 머리 · 「전체 선택」, 연차 촉진 표 머리).

  • indeterminate 는 HTML 속성이 아니라 JS 프로퍼티예요. 마크업에 indeterminate 를 적거나 Alpine :indeterminate 로 묶어도 들어가지 않습니다 — x-effect 로 $el.indeterminate 를 직접 넣으세요.
  • 일부 선택 상태에서 누르면 전부 선택합니다(해제가 아니에요). 전부 골라진 상태에서 누를 때만 그 묶음을 해제하고, 다른 묶음에서 고른 항목은 건드리지 않아요.
html
<input type="checkbox" class="ds-checkbox ds-checkbox--lg"
       x-effect="$el.checked = selectionState(group.ids) === 'all';
                 $el.indeterminate = selectionState(group.ids) === 'some'"
       @change="toggleMembers(group.ids)" />

클래스를 빼먹으면 OS 파랑이 나옵니다

class를 안 붙인 <input type="checkbox">/<input type="radio">는 브라우저 기본 컨트롤로 렌더돼요. 선택 색이 macOS/Windows의 파랑이라 DS 팔레트 밖 색이 화면에 섞입니다. 입력 요소 자체는 DS가 :not([type="checkbox"]):not([type="radio"])로 의도적으로 제외하고 있어서, 클래스 없이는 아무것도 적용되지 않아요.

선택지에 설명이 붙는 경우 (2줄 조합) ​

선택지마다 "무엇인지"를 한 줄 더 설명해야 하면 라벨을 제목 + 보조설명 2줄로 쌓아요. 연차 정책처럼 고르는 순간 계산 규칙이 바뀌는 설정에서는 제목만으로 판단이 안 서기 때문이에요.

html
<div class="space-y-1.5">
  <label class="flex items-start gap-2.5 cursor-pointer">
    <input type="radio" name="policy" class="ds-radio ds-radio--lg mt-0.5 shrink-0" />
    <span class="min-w-0 flex-1">
      <span class="block text-b5 text-label-normal">공휴일을 연차에서 제외</span>
      <span class="block text-b6 text-label-muted mt-0.5">5인 이상 사업장은 공휴일이 법정 유급휴일이에요</span>
    </span>
  </label>
</div>
값클래스왜
컨트롤 크기--lg (20px) — 라디오·체크박스 공통선택지 한 칸이 2줄(≈36px)이라 16px 기본은 글자 덩어리에 눌려 작아 보여요. 한 줄짜리는 기본 16px 그대로
컨트롤 정렬items-start + mt-0.5 (제목 크기와 무관하게 항상)설명이 붙어 라벨이 2줄이 되므로 가운데가 아니라 첫 줄에 맞춰요(items-center면 두 줄 사이로 내려가 어긋나요). 오프셋 2px 은 광학 보정이에요 — 글자는 줄 상자 안에서 아래로 치우쳐 앉기 때문에, 컨트롤을 줄 상자 위에 딱 맞추면 글자보다 위에 뜬 것처럼 보입니다. 계산상 높이가 같아도(14px 제목 ≈ 20px 컨트롤) 빼지 마세요
컨트롤 ↔ 글자gap-2.5 (10px)한 줄짜리(6px)보다 넓혀요 — 글자 덩어리가 커진 만큼
제목 / 설명text-b5 (13) / text-b6 (12) text-label-muted · mt-0.5필드 라벨·설명과 같은 축
선택지 간격space-y-1.5 (6px)한 항목이 2줄이라 항목 사이가 줄 간격과 구분돼야 해요
글자 묶음min-w-0 flex-1없으면 긴 설명이 컨트롤을 밀어냅니다
  • 설명이 없는 선택지에는 쓰지 마세요 — 한 줄이면 기본 조합으로 충분해요.
  • 선택지마다 카드 테두리가 필요할 만큼 설명이 길고 이미지·아이콘이 붙는다면 라디오가 아니라 선택 카드예요.
  • 체크박스도 같은 조합을 씁니다(다중 선택 + 항목별 설명) — 크기도 ds-checkbox--lg 로 맞춰요. 한 화면에 둘이 섞여 나올 때 한쪽만 16px 이면 바로 티가 납니다.

체크박스 vs 라디오 — 무엇을 쓰나

여러 개를 동시에 켤 수 있으면 체크박스, 여럿 중 하나만 고르면 라디오예요. 선택지가 2개이고 서로 반대말이면(공개/비공개) 라디오 2개보다 체크박스 1개가 낫습니다. 선택지가 5개를 넘어가면 라디오 대신 드롭다운을 고려하세요. 선택지마다 설명 한 줄이 붙어야 하면 선택 카드가 맞습니다.

스위치 (.ds-switch) ​

기능을 켜고 끄는 컨트롤이에요. 체크박스와 달리 누른 즉시 반영되는 자리에 씁니다 — 저장 버튼을 눌러야 적용되는 폼 값에는 체크박스를, "출근 현황 겹쳐보기"처럼 켜자마자 화면이 바뀌는 자리에는 스위치를 쓰세요.

기본 .ds-switch 32×20
작은 --sm 26×16
html
<!-- 라벨을 <label>로 감싸면 글자를 눌러도 토글돼요 -->
<label class="flex items-center gap-2">
  <span class="text-b6 text-neutral-600">출근 현황</span>
  <input type="checkbox" class="ds-switch ds-switch--sm"
         :checked="showAttendance" @change="toggleAttendance()"
         aria-label="출근 현황 토글">
</label>
클래스트랙손잡이쓰는 곳
ds-switch32×2016px기본 — 설정 목록, 모달 안 옵션 행
ds-switch ds-switch--sm26×1612px툴바처럼 세로 공간이 빠듯한 줄
  • 켜짐은 primary 주황, 꺼짐은 neutral-300(hover neutral-400), disabled는 45% 불투명도예요. 색을 따로 지정하지 마세요.
  • <input type="checkbox">에만 붙습니다. <button>이나 <div>로 스위치 모양을 직접 그리지 마세요 — 키보드 조작과 스크린리더가 공짜로 따라옵니다.
  • 글자 라벨이 옆에 없으면 aria-label이 필수예요.

스위치 · 체크박스 · 토글 버튼 — 어떤 걸 쓰나

  • 스위치 — 기능 on/off, 즉시 반영 (출근 현황 겹쳐보기, 알림 받기)
  • 체크박스 — 폼 값, 저장을 눌러야 반영 (약관 동의, 목록 다중 선택)
  • 상태 선택(예/아니오) — 스위치가 아니라 불리언 select를 씁니다 → 모달 규칙

네이티브 select로 표현이 안 되는 커스텀 옵션 목록(트리·체크 표시·그룹 라벨/구분선)은 공유 컴포넌트 .ds-menu 패널을 씁니다. OS 기본 드롭다운 대신 직접 띄우는 플로팅 메뉴로, 실제 직원 수정 모달의 팀/역할 선택에 사용됩니다. (design-system.css에 등록된 공유 컴포넌트 — Playground 는 상단 Playground 참고)

클래스역할
.ds-menu패널 컨테이너 (흰 바탕·라인·그림자·radius)
.ds-menu--scrollable최대 12rem 높이 + 세로 스크롤
.ds-menu__item메뉴 항목 (button/a)
.ds-menu__item--row좌우 배치 (체크 아이콘 등 우측 정렬)
.ds-menu__item--selected선택 상태 — 30% 회색 틴트의 꽉 찬 사각 바 (Figma 13413:9425)
.ds-menu__divider구분선
.ds-menu__label그룹 라벨 (10px, muted)
.ds-menu__hint안내/빈 상태 텍스트
.ds-menu__search맨 위 검색 줄(ds-input--32 한 개) — 항목이 많은 피커(팀 트리 · 역할)에서 스크롤해도 위에 붙어 있음. 결과가 없으면 .ds-menu__hint로 「검색 결과가 없어요」

토큰: 배경·라인·radius는 --ds-dropdown-*, 선택 틴트는 --ds-hover-tint-30, 구분선은 --ds-line-normal — select와 같은 드롭다운 토큰 계열을 공유합니다.

커스텀 옵션 목록(트리·체크·라벨/구분선)용 플로팅 패널. 선택 항목은 회색 바로 강조됩니다.

팀 선택


선택된 항목은 회색 바로 강조됩니다

파괴적 항목은 ds-menu__item--danger — 삭제처럼 되돌리기 어려운 항목에 얹어요. 평상시엔 일반 항목과 똑같고 hover에서만 의미색 red(--color-secondary-red)로 경고해요. 버튼의 ds-btn--red와 같은 의미 축이에요. 아래 "삭제"에 마우스를 올려 보세요.

메뉴 항목에 hover 유틸을 직접 붙이지 마세요

hover:bg-red-50 hover:text-red-600 같은 즉석 유틸로 위험 항목을 만들지 마세요 — ds-menu__item--danger가 그 역할이에요. (admin 조직 트리 우클릭 메뉴가 유틸 조합에서 이 변형으로 이관됐어요.)

warning 메뉴 항목에 hover 유틸을 직접 붙이지 마세요 hover:bg-red-50 hover:text-red-600 같은 즉석 유틸로 위험 항목을 만들지 마세요 — ds-menu__item--danger가 그 역할이에요. (admin 조직 트리 우클릭 메뉴가 유틸 조합에서 이 변형으로 이관됐어요.) :::

ms-dropdown (앱 로컬 — 공유 DS 아님) ​

다중 선택용 .ms-dropdown(커스텀 트리거+패널)은 admin·team-manager의 src/styles/main.css에만 정의된 앱 로컬 패턴으로, 공유 design-system.css에는 없습니다. PR #247에서 연장근무 상태 select → ms-dropdown으로 교체. 트리거(.ms-dropdown__trigger)는 Tooltip의 compact 툴팁 대상이라 선택 라벨이 hover로 표시됩니다. 공유 DS로 끌어올릴지는 별도 검토 대상이며, **현재 공유 표준은 ds-menu**입니다.

피커 (Picker) — 날짜 · 월 · 시간 ​

폼 컨트롤의 세 번째 패밀리. 텍스트 한 줄(Input)·선택지(Select)와 달리 날짜·월·시간을 고르는 입력입니다 — 날짜 = 커스텀 달력(dsDatePicker — 네이티브 type="date"는 대안), 월 = 커스텀 팝오버(.ds-menu), 시간 = 커스텀 드롭다운(dsTimePicker). 모달·페이지 공용이며, 라이브 조작은 상단 Playground에서. 모달 안에서 쓸 때의 레이어·닫기 규칙은 Modal §7.

달력 (날짜 · 월 선택) ​

DS 정책 — 날짜는 커스텀 우선(시스템 기본은 대안), 월은 커스텀

  • 날짜 선택 = 커스텀 달력(우선) — 결합 피커와 동일한 커스텀 달력(연도 드롭다운 + 월 이동 ‹ › + 삭제·오늘 + 오늘=채움·다른 날=테두리)을 씁니다. 앞으로 날짜 입력은 커스텀으로 통일 예정이며 커스텀을 우선합니다. 시스템 기본 <input type="date">는 대안으로 유지(네이티브 지원·모바일 휠·로케일이 필요하거나 아직 미전환인 화면).
  • 월 선택 = 커스텀 컴포넌트 — 네이티브 <input type="month">는 브라우저 지원이 약해(Firefox 등 미지원), .ds-menu 팝오버 + 연도 .ds-select + 월 그리드의 커스텀 월 선택기를 씁니다.

Playground 는 상단 Playground에서 직접 조작해 보세요 — 날짜(커스텀·우선): 연도 드롭다운 + 월 이동 + 삭제·오늘, 오늘=채움·다른 날=테두리. 날짜(시스템 기본·대안): 브라우저 네이티브 달력(OS/브라우저별 외형). 월: 커스텀 팝오버(트리거 아래 6px·우측정렬).

날짜 선택기 구성 (커스텀 — dsDatePicker) ​

네이티브 type="date"를 대체하는 공유 커스텀 달력. Alpine 컴포넌트 dsDatePicker(@hereby/components, createAuthComponentsPlugin에 등록 — 전 앱 사용 가능) + 공유 마크업 partial(packages/components/html/ds-date-picker.html) + .ds-datepicker* CSS(design-system.css)로 구성. 사용처는 호스트 div + include 한 줄이면 됩니다(로직·외형·마크업 모두 공유, 중복 없음).

파트내용
트리거readonly input.ds-input--40(우측 달력 아이콘). 누르면 달력 열림
팝오버x-teleport="body"로 모달 밖에 렌더 + position:fixed로 트리거 바로 아래 배치 → 모달의 overflow-y-auto·filter를 탈출해 절대 안 잘림(모달 밖으로 넘어가도 OK). dim 없음, z = 모달 위 1레이어(--ds-z-modal-top)
그리드연도 드롭다운 + 월 이동 ‹ › + 일자. 선택&오늘 = 채움 / 선택&다른 날 = 테두리
푸터.ds-btn-text--sm — 삭제(assistive) · 오늘(primary)
닫기Esc · 바깥 클릭 · 날짜 선택 → 피커만 닫힘(모달은 유지)
값YYYY-MM-DD 문자열 — 호스트의 x-model에 x-modelable="value"로 바인딩

사용법 (사용처 마크업):

html
<div class="ds-datepicker" x-data="dsDatePicker({ max: '9999-12-31' })"
     x-modelable="value" x-model="form.someDate">
  <!-- include "../../packages/components/html/ds-date-picker.html" -->
</div>
  • 옵션: dsDatePicker({ min, max, placeholder }) — min/max는 YYYY-MM-DD(범위 밖 날짜 비활성).
  • 컴팩트 트리거: 호스트에 사이즈 모디파이어를 더해 트리거 높이를 줄입니다(기본 40px). ds-datepicker--32(32px) → 필터 줄의 ds-select--32/ds-input--32와 정렬(예: 근태 현황 탭 기간 필터). ds-datepicker--sm(28px) → 더 좁은 인라인 행(예: 스케줄러 희망일 목록).
  • 비활성: 호스트에 x-effect="disabled = (조건)".
  • 선택 시 파생 로직(예: 신고일 → 차기기한 자동계산): dsDatePicker({ onChange: () => fn() }) — 유저 선택 시에만 발화(pick/clear/today, 다음 tick에 x-model 동기화 후). 네이티브 @change와 동일하게 프로그램적 로드엔 안 뜸.

원본: completion-form.html · 컴포넌트 DsDatePicker.ts · 마크업 ds-date-picker.html.

날짜 범위 필터 (프리셋 버튼) ​

리스트/현황 화면의 기간(from ~ to) 필터는 dsDatePicker 2개(시작·종료) + 프리셋 버튼 줄로 구성합니다.

파트마크업·동작
시작·종료 피커dsDatePicker 2개, 둘 다 ds-datepicker--32(필터 줄 높이 정렬). 교차 clamp — 시작에 `x-effect="max = (to
프리셋 버튼이번주 · 지난주 · 이번달 · 지난달 · 최근 3개월 | 전체. 각 버튼 text-c1(11px)·rounded-lg, 활성 하이라이트는 공유 datePresetBtnClass(active, key)(활성=진한 라인 / 비활성=회색 라인)
초기화 버튼ds-btn ds-btn--icon ds-btn--muted ds-btn--sm(새로고침 아이콘, title="기간 초기화"). `:disabled="!(from
  • 동작: 프리셋 클릭 → resolveDatePresetRange(preset)가 from/to를 계산하고 활성 key(certDatePreset/docMgmtDatePreset 등 상태 필드 1개)를 저장 후 재조회. 피커에서 날짜를 직접 고르면 onChange에서 프리셋 key를 비워(수동 선택 = 프리셋 해제) 즉시 재조회. 초기화는 from/to/프리셋을 함께 비움.
  • resolveDatePresetRange·datePresetBtnClass는 앱 공유 헬퍼 — 새 필터도 이 둘을 재사용하세요(프리셋 세트·하이라이트가 자동 일치).

원본: 증명서 필터 documents.html (setCertDatePreset) · 문서관리 document-management.html (setDocMgmtDatePreset).

월 선택기 구성 (커스텀) ​

파트마크업
트리거필드 모양 div(role="button", h-32 · --ds-textfield-radius · 달력 아이콘). select가 아니라 div라 전역 버튼/인풋 규칙과 충돌 없음
팝오버 표면.ds-menu (240px, padding 12px) — 트리거 우측 정렬(right-0), top-full + 마진 6px(모달 밖 넘침 방지)
연도.ds-select.ds-select--32 (현재 −10년 ~ +1년, 12개)
월 그리드4×3 버튼. 선택 --color-label-normal(검정 채움) · 이번 달 --color-label-normal 테두리 · 기본 --color-line-normal 테두리
푸터.ds-btn-text--sm — 닫기(assistive) · 이번 달(primary)

원본: apps/admin/src/views/modals/employee-attendance.html (mpOpen / mpYear / setAttendanceMonth).

왜 커스텀을 우선하나: 날짜+시간 결합 피커가 커스텀 달력을 쓰는 이상, 단독 날짜도 같은 커스텀으로 맞춰야 외형·동작이 일관됩니다(연도 드롭다운·삭제·오늘·선택 강조 등 제품이 통제). 그래서 앞으로 커스텀으로 통일하고 커스텀을 우선합니다. 단 네이티브 type="date"도 대안으로 유지 — 접근성·모바일 휠·로케일이 공짜이고, 아직 전환 안 된 화면은 네이티브 그대로 둡니다. (전환은 점진적; 신규/시각통일 필요 화면부터 커스텀)

시간 입력 (Time input) ​

DS 정책 — 시각은 기본 네이티브, 안내형 입력은 커스텀 드롭다운

  • 기본 = 네이티브 <input type="time"> — 단순 시각 입력은 네이티브를 씁니다(ds-input--40 tabular-nums). DS 테두리·높이 자동, 모바일 휠 공짜. 예: 근태 수정 모달.
  • 커스텀 시간 드롭다운(dsTimePicker) — 안내형 12시간 입력(오전/오후 + 시/분 + 증감 스텝 + 유효성)이 필요할 때. 트리거를 누르면 인풋 아래 드롭다운이 뜹니다(날짜 피커와 동일 — x-teleport 모달 위·안 잘림·dim 없음). 근무 설정의 시작/종료시간이 이 패턴.

Playground 는 상단 Playground에서 직접 — 17 입력 → 오후 5시 자동 변환, ▲▼ 스텝(시 1↔12·분 0↔59 랩), 숫자 아님/무변경이면 확인 비활성.

구성 (커스텀 — dsTimePicker) ​

날짜 피커와 같은 형태의 공유 컴포넌트 — Alpine 팩토리 dsTimePicker + 공유 마크업 partial(packages/components/html/ds-time-picker.html) + .ds-timepicker* CSS. 사용처는 호스트 div + include 한 줄:

html
<div class="ds-timepicker" x-data="dsTimePicker()" x-modelable="value" x-model="form.startTime">
  <!-- include "../../packages/components/html/ds-time-picker.html" -->
</div>
파트마크업
트리거readonly input.ds-input--40(우측 시계 아이콘·tabular-nums) — 누르면 드롭다운 오픈. 클릭 트리거라 OS time 피커 대신 커스텀 뷰가 뜸
드롭다운x-teleport="body" + position:fixed로 트리거 바로 아래 → 모달 overflow·filter 탈출해 안 잘림, dim 없음, z = 모달 위 1레이어(--ds-z-modal-top). Esc·바깥클릭·취소 = 피커만 닫음(모달 유지)
오전/오후.ds-select--40 드롭다운(오전/오후) — 단일선택이라 드롭다운
시 · 분.ds-input--40(가운데정렬·tabular-nums·키보드 입력) + 위·아래 스텝 아이콘 버튼(간격 6px). 시 1↔12·분 0↔59 랩어라운드
푸터.ds-btn--solid --white-bk(취소) · .ds-btn--solid --dark(확인) --md. 확인은 유효 + 변경 시에만 활성

값 변환: 표시 = 12시간제(오전/오후 + 시 + 분), 저장 = 24시간제 HH:MM — 호스트의 x-model에 바인딩. 시 칸은 24시간제 입력도 허용해(17 → 오후 5시) blur 시 정규화 — 자정 = 오전 12, 정오 = 오후 12. 확인 활성 조건 = 시(0–23)·분(0–59) 순수 숫자 AND 원래 값에서 변경.

원본: shift-form.html(시작/종료) · 컴포넌트 DsTimePicker.ts · 마크업 ds-time-picker.html.

날짜 + 시간 결합 ​

결합 뷰 = 한 팝업에 [달력 + 시간]. 네이티브 <input type="datetime-local">은 OS·브라우저마다 외형/동작이 달라(공지 만료일시가 이 케이스) 쓰지 않고, 그 "한 view 결합" 경험을 DS로 재현합니다 — 트리거 클릭 → 한 팝업에 커스텀 달력 그리드(좌) + 커스텀 시간 폼(우, 오전·오후 + 시·분 + 스텝) → 확인 시 YYYY-MM-DD HH:mm 저장.

⚠️ 결합 뷰는 달력 그리드를 시간 폼과 한 팝업에 인라인으로 그립니다 — 네이티브 시스템 달력은 브라우저 자체 팝업이라 한 팝업에 못 넣기 때문. 단일 날짜는 같은 커스텀 달력을 dsDatePicker 드롭다운으로 띄웁니다(외형·동작 일관). 즉 커스텀 달력 그리드는 단독·결합 모두 쓰며, 차이는 단독=teleport 드롭다운 / 결합=한 팝업 인라인입니다.

실제 동작은 상단 Playground에서.

마이그레이션 대상: 현재 datetime-local을 쓰는 곳(예: 공지 작성 만료일시)은 이 조합(시스템 달력 + 커스텀 시간 폼)으로 교체 권장.

목표 & 로드맵 ​

항목현재목표
기본 32px 통일✅ (40→32)유지
input/select 사이즈 스케일 정합✅ 52~24 동일유지
사이즈 모디파이어 강제 제거✅ !important 없음유지
시맨틱 토큰 → primitive 연결🟡 runtime-safe literal (의도적)primitive를 전 앱 :root에 항상 emit(@theme static 등)한 뒤 일괄 연결 — 별도 DS 작업
chevron 색 토큰화🟡 data-uri 한계 (값은 on-palette #8f8f8f=neutral-400)빌드타임 색 주입(생성 단계)만 가능 — <select>는 pseudo-element 불가라 mask-image 안 됨. 보류(ROI 낮음)

변경 이력 ​

날짜내용
2026-07-07날짜 범위 필터(프리셋 버튼) 패턴 신설(#date-range-filter) — 리스트 기간 필터를 dsDatePicker 2개(시작·종료, x-effect 교차 clamp) + 프리셋 줄(이번주/지난주/이번달/지난달/최근 3개월 | 전체) + --icon --muted 초기화 버튼으로 문서화. 프리셋 계산·하이라이트는 공유 헬퍼 resolveDatePresetRange/datePresetBtnClass 재사용. 증명서 필터(admin)·문서관리(team-manager) 도입 (design/adjust-table)
2026-10-06캡슐형 입력줄의 보내기 버튼 — 비활성이면 숨김 — 아무것도 안 쳤을 때 흐린 버튼 대신 버튼이 빠지고 입력칸이 넓어집니다(④⑤).
2026-10-06모바일 텍스트 필드 절 신설 — 직원 앱 입력칸을 폼 입력칸 3종(한 줄 ds-input--48 · 선택 ds-select--48 · 여러 줄 textarea)과 대화 입력줄 2종(채팅 · AI 어시스턴트 캡슐형)으로 정리하고 라이브 모음(<DemoMobileTextFields>) 추가. 대화 입력줄의 규칙 정본은 대화 UI 에 두고 여기서는 두 경우의 차이만 표로
2026-06-30필드 구성(anatomy) 섹션 신설 — 설명·에러 표기 표준화 — 텍스트 필드의 라벨→필수→입력칸→설명→에러 스택을 필드 구성으로 문서화(라이브 <DemoFieldAnatomy>). 설명 줄 = 새 공유 클래스 .ds-form-help(12px·아래 6px·항상 표시, 색은 필드 상태 추종 rest muted→focus black→error red, sibling 조합자로 JS 없이 동작) — 그동안 text-b6 neutral-400/500 mt-0.5~1.5로 제각각이던 걸 단일 표준으로 고정. 에러 메시지 = .ds-form-error/.ds-form-error-slot을 admin main.css 로컬 → 공유 design-system.css로 승격(danger·12px·아래 6px, slot은 min-height:17px로 점프 방지). 입력칸 보더는 admin .is-error가 아니라 공유 ds-input--error 표준으로 명시
2026-06-26숫자 입력 변형(ds-input--number) 신설 — <input type=number>용 공유 변형. 네이티브 스피너를 평소 숨기고 hover/focus 시에만 노출(admin .spinner-on-hover 동작을 공유 DS로 승격) + tabular-nums. 크기·너비·정렬(ds-input--XX·w-full·text-center)과 조합. 숫자 입력 섹션·라이브 미리보기(<DemoFormControls> 숫자 토글) 추가, 흩어진 patient-count-input·sched-grid·인라인 -moz-appearance 핵을 대체할 단일 기준으로 명시. 도입: scheduler ward-settings 비율 입력(admin·tm)
2026-06-26시간 입력 피커 = teleport 드롭다운으로 전환(dsTimePicker) — 기존 dim 중앙 다이얼로그(.ds-modal-backdrop--top + .ds-mini-modal)를 날짜 피커와 동일한 인풋 아래 드롭다운(x-teleport·fixed·안 잘림·dim 없음·Esc/바깥클릭=피커만 닫힘)으로. te* 인라인 로직을 공유 dsTimePicker(팩토리+partial+.ds-timepicker* CSS)로 추출 → shift-form(시작/종료)·notice-form(만료시각) 적용. #time-input "구성" 갱신, ## 피커 인트로 "시간=다이얼로그"→"드롭다운"
2026-06-26커스텀 날짜 달력 컴포넌트(dsDatePicker) 신설 + 앱 적용 — 네이티브 type=date 대체 공유 컴포넌트(Alpine DsDatePicker + 공유 마크업 partial + .ds-datepicker* CSS)를 "날짜 선택기 구성"으로 문서화. x-teleport="body" + fixed 드롭다운이라 모달 overflow·filter 탈출해 안 잘림(dim 없음·모달 위 1레이어·Esc/바깥클릭=피커만 닫힘). 사용처는 호스트 div + include 한 줄. 데스크톱 폼 12필드 전수 적용(ga·admin·hr·inventory·tm) + 좁은 행용 --sm(28px) 변형. 정정: ## 피커 인트로 "날짜=네이티브"→"커스텀 달력", #datetime의 "단일 날짜=네이티브(커스텀 안 만듦)" 모순 제거(단독=드롭다운/결합=인라인)
2026-06-25Input · Select · Picker 3패밀리로 정리 — 제목에 Picker 추가, ## 피커 (Picker) 상위 섹션 신설하고 달력(날짜·월)·시간 입력을 그 아래로 그룹화(#calendar/#time-input 앵커 유지). 그리고 "시간 입력 (Time input)" 신설 — 커스텀 시간 다이얼로그(오전/오후 드롭다운 + 시/분 키보드 입력 + ▲▼ 스텝 + 17→오후5 정규화 + 숫자아님/무변경 시 확인 비활성) 라이브(<DemoTimeInput>)·구성·값변환·날짜+시간 결합 정책. 기본=네이티브 type=time, 안내형=커스텀 다이얼로그(ds-mini-modal). 날짜+시간 결합 라이브(<DemoDateTime>, {#datetime}) 추가 — 결합 뷰가 라이브 예시 없던 것 보완
2026-06-19라이브 미리보기 = 컴포넌트 갤러리로 확장(폼 컨트롤·메뉴 패널·달력 3종을 상단에 모으고 각 항목 설명 더보기 앵커 → 상세 섹션) · 메뉴 패널(ds-menu) 섹션 신설(__item--selected 포함) · "달력(날짜·월 선택)" 섹션 신설(날짜=시스템 기본 type=date, 월=커스텀 월 선택기 — <DemoDatePicker> 라이브) · "사용처" → "적용 범위 & 도입 현황"으로 재구성(모달 마이그레이션 반영) · ms-dropdown=앱 로컬 정정 · 미리보기에 textarea·드롭다운 fill 변형 추가 · 검토 필요 사항 정리 — ① primitive 연결 항목을 runtime-safe literal 트레이드오프로 보강(보류), ②chevron(on-palette·data-uri 한계)·③24px(일관 literal px·이슈 아님)·④!important-free(정상)는 검토 완료로 제거
2026-06-08Form controls 페이지 신규 작성 + 라이브 미리보기(<DemoFormControls>) (PR #247 기준)
PR #247폼 필드·박스 버튼 기본 40→32px 통일 · ds-input 사이즈 스케일(28/40/48/52) 추가 · admin+general-affairs 31개 폼 ds-select/ds-input 마이그레이션 · select chevron 렌더 버그 수정

사용된 토큰 ​

토큰값용도
--ds-textfield-border#e5e5e5(라이트 line-normal) · 다크 line-field(#5e5e5e)input/textarea · select 기본 테두리 — 라이트는 hover(#c1c1c1)보다 옅어야 한다(2026-10-20)
--ds-textfield-border-hover#c1c1c1(라이트 line-strong) · 다크 line-alternative(#8f8f8f)hover 테두리 — 다크 line-strong 은 흰선에 가까워 한 단계 낮춤(2026-10-19). 모바일은 탭 뒤 hover 가 남아 실제로 보인다
--ds-textfield-border-focus#1b1b1bfocus 테두리
--ds-textfield-radius4px필드 radius (중앙 조정점)
--ds-textfield-fg / --ds-textfield-placeholder#1b1b1b / #8f8f8f글자 / placeholder
--ds-dropdown-bg/border/text/radius#fff / #e5e5e5 / #1b1b1b / 4pxselect 박스
--ds-dropdown-chevronSVG data-uri (#8f8f8f stroke)select 화살표

단일 출처 ​

파트파일
스타일packages/static/styles/design-system.css (native input/select/textarea 규칙 + .ds-input--XX/.ds-select--XX)
토큰--ds-textfield-* (border/hover/focus/radius/fg/placeholder) · --ds-dropdown-* (bg/border/text/radius) · --ds-dropdown-chevron

토큰 소유권: 필드 룩(테두리·radius·글자·placeholder·chevron)은 --ds-textfield-*/--ds-dropdown-* = DS 소유. 사이즈(height/font/padding)는 native 규칙 + --XX 모디파이어에 직접 정의. 마진 등 배치는 Tailwind 유틸.

검토 필요 사항 ​

  • 🟡 --ds-textfield-*/--ds-dropdown-*가 raw hex로 정의 (primitive 미연결) — --color-neutral-100/-900 등 primitive를 var()로 참조하면 primitive 한 곳만 고쳐도 모든 입력칸에 전파되는 진짜 단일 출처가 됩니다. 단, 지금 단순 전환하면 안 되는 트레이드오프가 있습니다: --ds-*는 의도적으로 runtime-safe literal 세트(토큰 생성기가 hex만 export하고, @theme 처리·tree-shaking에 의존하지 않게 둔 집합)입니다. 실측상 일부 앱 빌드에선 primitive가 :root에서 가지치기됩니다(--color-label-white가 module-manager·inventory 빌드에서 제거됨) → bare var()는 그 앱들에서 배경이 깨지고, fallback var(--color-x, #hex)는 fallback hex가 사실상 사본이라 전파가 다시 끊깁니다. 올바른 해법 = primitive를 전 앱 :root에 항상 emit(예: @theme static)하도록 정비한 뒤 --ds-*를 일괄 연결 — DS 토큰 아키텍처 차원의 별도 작업으로 보류합니다(목표 & 로드맵).