@idbrnd/design-system 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @idbrnd/design-system
2
2
 
3
- ![Version](https://img.shields.io/badge/version-3.2.0-4B5FE1?style=flat-square)
3
+ ![Version](https://img.shields.io/badge/version-4.0.0?style=flat-square) <!-- x-release-please-version -->
4
4
  ![React](https://img.shields.io/badge/React-18.3.1-61DAFB?style=flat-square&logo=react&logoColor=white)
5
5
  ![Vite](https://img.shields.io/badge/Vite-7.3.0-646CFF?style=flat-square&logo=vite&logoColor=white)
6
6
  ![TypeScript](https://img.shields.io/badge/TypeScript-5.9.3-3178C6?style=flat-square&logo=typescript&logoColor=white)
@@ -40,6 +40,7 @@ export default function App() {
40
40
  - [OutlineButton](#outlinebutton)
41
41
  - [TextButton](#textbutton)
42
42
  - [WeakButton](#weakbutton)
43
+ - [TertiaryButton](#tertiarybutton)
43
44
  - [아이콘 버튼](#아이콘-버튼)
44
45
  - [BasicIconButton](#basiciconbutton)
45
46
  - [FillIconButton](#filliconbutton)
@@ -56,9 +57,13 @@ export default function App() {
56
57
  - [Toast](#toast)
57
58
  - [Snackbar](#snackbar)
58
59
  - [PushBadge](#pushbadge)
60
+ - [EmptyState](#emptystate)
59
61
  - [Tooltip](#tooltip)
62
+ - [Modal](#modal)
60
63
  - [Select](#select)
61
64
  - [Dropdown](#dropdown)
65
+ - [DatePicker](#datepicker)
66
+ - [TimePicker](#timepicker)
62
67
  - [Content](#content)
63
68
  - [Avatar](#avatar)
64
69
  - [AvatarStack](#avatarstack)
@@ -161,6 +166,35 @@ import { WeakButton } from "@idbrnd/design-system";
161
166
  | `keepFocusOnClick` | `boolean` | `false` | 클릭 후 포커스 유지 |
162
167
  | `customStyle` | `CSSProperties` | — | 추가 인라인 스타일 |
163
168
 
169
+ ### TertiaryButton
170
+
171
+ 아이콘 슬롯을 갖는 보조 버튼입니다. 다른 버튼과 달리 `widthType`이 없고, `leadingIcon` / `trailingIcon`으로 텍스트 앞뒤에 아이콘을 배치할 수 있습니다.
172
+
173
+ ```tsx
174
+ import { TertiaryButton } from "@idbrnd/design-system";
175
+ import { ChevronDownIcon, PlusIcon } from "@idbrnd/design-system";
176
+
177
+ <TertiaryButton variant="fill">확인</TertiaryButton>
178
+ <TertiaryButton variant="primary" leadingIcon={<PlusIcon size={16} />}>추가</TertiaryButton>
179
+ <TertiaryButton variant="gray" size="small" trailingIcon={<ChevronDownIcon size={16} />}>더보기</TertiaryButton>
180
+ ```
181
+
182
+ | Prop | 타입 | 기본값 | 설명 |
183
+ | ------------------ | ----------------------------------------------------- | ---------- | ------------------- |
184
+ | `variant` | `"fill"` \| `"tinted"` \| `"gray"` \| `"primary"` | `"fill"` | 색상 변형 |
185
+ | `size` | `"small"` \| `"medium"` \| `"large"` | `"medium"` | 크기 |
186
+ | `leadingIcon` | `ReactNode` | — | 텍스트 앞 아이콘 |
187
+ | `trailingIcon` | `ReactNode` | — | 텍스트 뒤 아이콘 |
188
+ | `loading` | `boolean` | `false` | 로딩 스피너 표시 |
189
+ | `disabled` | `boolean` | `false` | 비활성화 |
190
+ | `keepFocusOnClick` | `boolean` | `false` | 클릭 후 포커스 유지 |
191
+ | `customStyle` | `CSSProperties` | — | 추가 인라인 스타일 |
192
+
193
+ 유의사항:
194
+
195
+ - `loading` 중에는 아이콘과 children 대신 스피너만 표시되며 클릭이 동작하지 않습니다.
196
+ - 다른 버튼들과 달리 `widthType`이 없어 항상 콘텐츠 크기를 따릅니다.
197
+
164
198
  ---
165
199
 
166
200
  ## 아이콘 버튼
@@ -645,6 +679,64 @@ import { PushBadge } from "@idbrnd/design-system";
645
679
  | `maxCount` | `number` | `999` | `count`가 초과하면 `{maxCount}+`로 표시 |
646
680
  | `customStyle` | `CSSProperties` | — | 추가 인라인 스타일 |
647
681
 
682
+ ### EmptyState
683
+
684
+ 사용자가 특정 페이지에 처음 접속했을 때나 표시할 수 있는 데이터가 없을 때 노출하는 컴포넌트입니다.
685
+
686
+ ```tsx
687
+ import { EmptyState, FillButton } from "@idbrnd/design-system";
688
+ import { FolderIcon } from "@idbrnd/design-system";
689
+
690
+ <EmptyState
691
+ size="medium"
692
+ visualContent={<FolderIcon size={36} />}
693
+ headingContent="등록된 프로젝트가 없습니다."
694
+ descriptionContent={"첫 번째 프로젝트를 생성하고\n모니터링을 시작해 보세요."}
695
+ buttonContent={
696
+ <FillButton variant="primary" size="xsmall">
697
+ 프로젝트 생성
698
+ </FillButton>
699
+ }
700
+ />;
701
+ ```
702
+
703
+ **텍스트만 사용 (위젯·테이블 내부)**
704
+
705
+ ```tsx
706
+ <EmptyState
707
+ size="small"
708
+ headingContent="검색 결과가 없습니다."
709
+ descriptionContent="다른 키워드로 다시 검색해 보세요."
710
+ />
711
+ ```
712
+
713
+ | Prop | 타입 | 기본값 | 설명 |
714
+ | -------------------- | -------------------------------------- | --------- | --------------------------------------------------------------- |
715
+ | `size` | `"large"` \| `"medium"` \| `"small"` | `"small"` | 배치 구역의 크기·위계에 따른 사이즈 |
716
+ | `visualContent` | `ReactNode` | — | 상단 시각 요소. 일러스트·아이콘·이미지·임의 컴포넌트 모두 가능 |
717
+ | `headingContent` | `ReactNode` | — | 타이틀. 상황을 짧게 요약하며 단일 행 권장 |
718
+ | `descriptionContent` | `ReactNode` | — | 설명. 개행(`\n`)이 그대로 반영되며 줄 수 제한 없음 |
719
+ | `buttonContent` | `ReactNode` | — | CTA 버튼 슬롯 |
720
+ | `customStyle` | `CSSProperties` | — | 루트에 적용할 추가 인라인 스타일 |
721
+ | `className` | `string` | — | 루트에 추가할 클래스명 |
722
+
723
+ **size별 스펙**
724
+
725
+ | | `large` | `medium` | `small` |
726
+ | ---------------- | -------------------------------- | -------------------------------- | --------------------------------- |
727
+ | 시각 요소 슬롯 | 80×80 | 48×48 | 32×32 |
728
+ | 타이틀 | `--text-heading-2-semibold` | `--text-heading-2-semibold` | `--text-body-2-normal-semibold` |
729
+ | 설명 | `--text-body-1-reading-regular` | `--text-body-1-reading-regular` | `--text-label-2-reading-regular` |
730
+ | 권장 버튼 | `FillButton` `assistive`/`small` | `FillButton` `assistive`/`xsmall`| `FillButton` `assistive`/`xsmall` |
731
+
732
+ 유의사항:
733
+
734
+ - **네 슬롯 모두 넘긴 것만 렌더됩니다.** 넘기지 않으면 해당 영역과 인접 간격이 함께 사라지므로, 화면 상황에 맞게 가볍거나 묵직하게 조합할 수 있습니다.
735
+ - 루트는 부모를 채우고(`width`/`height: 100%`) 콘텐츠를 중앙 정렬합니다. 상하 여백은 80px입니다.
736
+ - 전체 화면 단위에는 `size="large"` 또는 `"medium"`에 시각 요소와 버튼을 함께 두어 액션을 유도하고, 카드·테이블 내부에는 `size="small"`로 컴팩트하게 노출하는 것을 권장합니다.
737
+ - 텍스트는 `[상황 설명 + 해결책 제안]` 구조로 작성합니다. 설명은 두 줄 이내를 권장하지만 강제되지는 않습니다.
738
+ - `visualContent`에 넣은 노드는 슬롯 크기에 맞춰 `max-width`/`max-height: 100%`가 적용됩니다.
739
+
648
740
  ### Tooltip
649
741
 
650
742
  트리거 엘리먼트에 마우스를 올리거나 클릭하면 말풍선 형태의 안내 텍스트를 표시하는 컴포넌트입니다. `trigger="always"`로 항상 표시되는 고정 안내 형태로도 사용할 수 있습니다.
@@ -721,6 +813,101 @@ import { Tooltip } from "@idbrnd/design-system";
721
813
  - 툴팁은 `document.body`에 포탈로 렌더링됩니다.
722
814
  - 화살표는 회전된 정사각형(`rotate(45deg)`) 방식으로 렌더링됩니다. 툴팁 본체와 겹쳐 배치해 브라우저 줌 축소 시 발생하는 서브픽셀 갭을 방지하며, 개별 `rgba` 대신 부모 `opacity`를 사용해 겹침 영역의 투명도 이중 적용을 방지합니다.
723
815
 
816
+ ### Modal
817
+
818
+ 데스크톱(Web) 환경에서 중요한 정보 전달이나 사용자 액션 유도가 필요할 때 화면 중앙에 노출되는 컴포넌트입니다. 배경을 어둡게 처리(Dim)하여 서비스 흐름을 일시적으로 차단하고 사용자의 시선을 집중시킵니다. `open`/`onClose`로 제어하는 controlled 컴포넌트이며, `document.body`에 포털로 렌더링됩니다.
819
+
820
+ ```tsx
821
+ import { useState } from "react";
822
+ import { Modal, FillButton } from "@idbrnd/design-system";
823
+
824
+ const [open, setOpen] = useState(false);
825
+
826
+ <Modal
827
+ open={open}
828
+ onClose={() => setOpen(false)}
829
+ heading="정말 삭제하시겠어요?"
830
+ mainAction={{ label: "삭제", onClick: handleDelete }}
831
+ subAction={{ label: "취소", onClick: () => setOpen(false) }}
832
+ >
833
+ 삭제한 항목은 복구할 수 없습니다.
834
+ </Modal>;
835
+ ```
836
+
837
+ **size / type 조합**
838
+
839
+ ```tsx
840
+ {/* fixed: size별 고정 너비(440 / 840 / 1440) */}
841
+ <Modal open={open} onClose={onClose} size="small" type="fixed">...</Modal>
842
+
843
+ {/* hug: 콘텐츠에 맞춰 size별 min~max 범위에서 결정 */}
844
+ <Modal open={open} onClose={onClose} size="large" type="hug">...</Modal>
845
+ ```
846
+
847
+ **액션 영역 직접 구성**
848
+
849
+ ```tsx
850
+ <Modal
851
+ open={open}
852
+ onClose={onClose}
853
+ heading="필터"
854
+ actionSlot={
855
+ <>
856
+ <TextButton onClick={handleReset}>초기화</TextButton>
857
+ <FillButton variant="primary" onClick={handleApply}>적용</FillButton>
858
+ </>
859
+ }
860
+ >
861
+ ...
862
+ </Modal>
863
+ ```
864
+
865
+ | Prop | 타입 | 기본값 | 설명 |
866
+ | ------------------ | ------------------------------------ | ---------- | ---------------------------------------------------------------- |
867
+ | `open` | `boolean` | — | **(필수)** 모달 표시 여부 (controlled) |
868
+ | `onClose` | `() => void` | — | **(필수)** ESC 키·닫기 버튼·딤 클릭으로 닫힐 때 호출 |
869
+ | `size` | `"small"` \| `"medium"` \| `"large"` | `"medium"` | 모달 너비 프리셋 |
870
+ | `type` | `"fixed"` \| `"hug"` | `"fixed"` | 너비 결정 방식 |
871
+ | `heading` | `ReactNode` | — | 헤더 타이틀 |
872
+ | `navBar` | `boolean` | `true` | 헤더(Nav Bar) 노출 여부 |
873
+ | `showClose` | `boolean` | `true` | 헤더 우측 닫기(X) 버튼 노출 여부 |
874
+ | `navLeadingSlot` | `ReactNode` | — | 헤더 좌측 슬롯 |
875
+ | `navTrailingSlot` | `ReactNode` | — | 헤더 우측 슬롯 (닫기 버튼 왼쪽에 배치) |
876
+ | `mainAction` | `ModalAction` | — | 메인 액션 — `FillButton`(primary/medium)으로 렌더링 |
877
+ | `subAction` | `ModalAction` | — | 서브 액션 — `OutlineButton`(assistive/medium)으로 렌더링, 메인 액션 좌측 |
878
+ | `actionSlot` | `ReactNode` | — | 지정 시 `mainAction`/`subAction` 대신 액션 영역을 통째로 교체 |
879
+ | `closeOnEsc` | `boolean` | `true` | ESC 키로 닫기 허용 |
880
+ | `closeOnDimClick` | `boolean` | `false` | 딤 클릭으로 닫기 허용 |
881
+ | `customStyle` | `CSSProperties` | — | 다이얼로그 루트에 적용할 추가 인라인 스타일 |
882
+ | `className` | `string` | — | 다이얼로그 루트에 추가할 클래스명 |
883
+ | `children` | `ReactNode` | — | 콘텐츠 영역 |
884
+
885
+ **`ModalAction`**
886
+
887
+ | 필드 | 타입 | 설명 |
888
+ | ---------- | ------------ | ----------------------------- |
889
+ | `label` | `string` | **(필수)** 버튼 레이블 |
890
+ | `onClick` | `() => void` | **(필수)** 클릭 콜백 |
891
+ | `loading` | `boolean` | 로딩 스피너 표시 여부 |
892
+ | `disabled` | `boolean` | 비활성화 여부 |
893
+
894
+ **size별 스펙**
895
+
896
+ | | `small` | `medium` | `large` |
897
+ | ------------ | --------- | --------- | ----------- |
898
+ | `fixed` 너비 | 440px | 840px | 1440px |
899
+ | `hug` 범위 | 320~470px | 560~840px | 1100~1440px |
900
+ | radius | `--radius-medium` | `--radius-large` | `--radius-xlarge` |
901
+
902
+ 유의사항:
903
+
904
+ - 헤더·액션 영역은 `flex-shrink: 0`으로 고정되고 콘텐츠 영역만 스크롤됩니다(`max-height: min(864px, 100vh - 48px)`). 콘텐츠 내부 스크롤이 끝에 닿아도 배경 페이지로 전파되지 않습니다.
905
+ - 모달이 열려 있는 동안 배경 스크롤을 차단합니다. `overflow`를 건드리지 않고 wheel·touch·키보드 스크롤 키 이벤트를 가로채는 방식이라 스크롤바가 유지되어 레이아웃 시프트가 없습니다.
906
+ - 여러 모달을 동시에 띄우는 중첩도 지원합니다. 열린 모달을 스택으로 관리하며 ESC는 항상 최상단 모달만 닫습니다.
907
+ - `closeOnDimClick`은 기본값이 `false`입니다. 딤에서 누르기 시작해 모달 내부로 드래그하다 놓는 경우는 닫기로 처리하지 않습니다(누르기·떼기가 모두 딤 위에서 일어나야 함).
908
+ - `navBar={false}`로 헤더 전체를 숨길 수 있고, `actionSlot`/`mainAction`/`subAction`을 모두 생략하면 액션 영역 자체가 렌더링되지 않습니다.
909
+ - 닫힐 때 150ms 페이드+스케일 애니메이션 후 언마운트되며, `prefers-reduced-motion: reduce` 환경에서는 애니메이션이 비활성화됩니다.
910
+
724
911
  ---
725
912
 
726
913
  ## Select
@@ -951,6 +1138,265 @@ const options: DropdownOption[] = [
951
1138
 
952
1139
  ---
953
1140
 
1141
+ ## DatePicker
1142
+
1143
+ 날짜(및 시간)를 선택하는 팝오버 기반 컴포넌트입니다. 외부 날짜 라이브러리 없이 네이티브 `Date`로 동작하며,
1144
+ `normal`(단일 날짜) / `period`(시작·종료, 캘린더 1개) / `range`(시작·종료, 연속된 2개월 동시 노출) 3가지 variant를 지원합니다.
1145
+
1146
+ ```tsx
1147
+ import { useState } from "react";
1148
+ import { DatePicker } from "@idbrnd/design-system";
1149
+
1150
+ function DatePickerExample() {
1151
+ const [value, setValue] = useState<Date | null>(null);
1152
+
1153
+ return <DatePicker heading="날짜 선택" required value={value} onChange={setValue} />;
1154
+ }
1155
+ ```
1156
+
1157
+ **Period / Range**
1158
+
1159
+ ```tsx
1160
+ import type { DatePickerDateRange } from "@idbrnd/design-system";
1161
+
1162
+ function RangeExample() {
1163
+ const [range, setRange] = useState<DatePickerDateRange>({ start: null, end: null });
1164
+
1165
+ return <DatePicker variant="range" value={range} onChange={setRange} />;
1166
+ }
1167
+ ```
1168
+
1169
+ **시간 선택 (hasTime)**
1170
+
1171
+ `hasTime`을 켜면 날짜 하단에 시작(및 종료) 시간 필드가 노출됩니다. 이 필드는 내부적으로
1172
+ [TimePicker](#timepicker)를 `trigger="compact"`로 그대로 재사용하며, `timeVariant`로 시간 표시 형식을
1173
+ 바꿀 수 있습니다.
1174
+
1175
+ ```tsx
1176
+ <DatePicker variant="period" hasTime value={range} onChange={setRange} />
1177
+
1178
+ // 24시간제(오전/오후 없음)로 표시하고 싶다면
1179
+ <DatePicker variant="period" hasTime timeVariant="hh-mm" value={range} onChange={setRange} />
1180
+ ```
1181
+
1182
+ **특정 날짜 비활성화**
1183
+
1184
+ ```tsx
1185
+ function isDateDisabled(date: Date) {
1186
+ const day = date.getDay();
1187
+ return day === 0 || day === 6; // 주말 비활성화
1188
+ }
1189
+
1190
+ <DatePicker value={value} onChange={setValue} isDateDisabled={isDateDisabled} />;
1191
+ ```
1192
+
1193
+ **Action area 커스텀 (actionSubContent / actionMainContent)**
1194
+
1195
+ ```tsx
1196
+ <DatePicker
1197
+ value={value}
1198
+ onChange={setValue}
1199
+ actionSubContent={({ onClick }) => (
1200
+ <TextButton variant="assistive" size="medium" onClick={onClick}>
1201
+ Today
1202
+ </TextButton>
1203
+ )}
1204
+ actionMainContent={({ onClick }) => (
1205
+ <FillButton size="xsmall" onClick={onClick}>
1206
+ Apply
1207
+ </FillButton>
1208
+ )}
1209
+ />
1210
+ ```
1211
+
1212
+ **i18n (다국어 대응)**
1213
+
1214
+ 화면에 노출되는 문자열은 모두 optional prop으로 override할 수 있습니다. 미지정 시 한국어 기본값이 그대로 노출됩니다.
1215
+
1216
+ ```tsx
1217
+ <DatePicker
1218
+ variant="range"
1219
+ hasTime
1220
+ value={range}
1221
+ onChange={setRange}
1222
+ startLabel="Start"
1223
+ endLabel="End"
1224
+ amLabel="AM"
1225
+ pmLabel="PM"
1226
+ weekdayLabels={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]}
1227
+ formatMonthYear={(year, month) => `${monthNames[month - 1]} ${year}`}
1228
+ formatTime={(hours, minutes) =>
1229
+ `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`
1230
+ }
1231
+ />
1232
+ ```
1233
+
1234
+ | Prop | 타입 | 기본값 | 설명 |
1235
+ | -------------------- | ------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------- |
1236
+ | `variant` | `"normal"` \| `"period"` \| `"range"` | `"normal"` | `period`/`range`는 `value`/`onChange`가 `DatePickerDateRange` 타입 |
1237
+ | `value` | `Date \| null` (normal) / `DatePickerDateRange` (period/range) | — | **(필수)** 현재 선택된 값 |
1238
+ | `onChange` | `(value) => void` | — | **(필수)** 값 변경 시 호출되는 콜백 |
1239
+ | `trigger` | `"icon"` \| `"field"` | `"icon"` | `icon`은 텍스트 직접 입력 가능, `field`는 클릭으로만 팝오버 열림 |
1240
+ | `heading` | `string` | — | 상단 라벨 텍스트. 미지정 시 라벨 미표시 |
1241
+ | `required` | `boolean` | `false` | 라벨 우측 `*` 표시 여부 |
1242
+ | `placeholder` | `string` | `"YYYY-MM-DD"` | 트리거 placeholder |
1243
+ | `disabled` | `boolean` | `false` | 전체 비활성화 |
1244
+ | `hasTime` | `boolean` | `false` | `true` 시 날짜 하단에 시간 선택([TimePicker](#timepicker) `compact` 트리거) 노출 |
1245
+ | `timeVariant` | `"aa-hh-mm"` \| `"aa-hh"` \| `"hh-mm"` | `"aa-hh-mm"` | `hasTime`일 때 시간 선택의 표시 형식. [TimePicker](#timepicker)의 `variant`와 동일 |
1246
+ | `isDateDisabled` | `(date: Date) => boolean` | — | 특정 날짜를 선택 불가 처리하는 판별 함수 |
1247
+ | `actionArea` | `boolean` | `true` | 팝오버 하단 액션 영역 노출 여부. `false`면 날짜(및 시간) 선택 즉시 `onChange` 호출 |
1248
+ | `actionSub` | `boolean` | `true` | Action area 보조 영역("오늘") 노출 여부. `hasTime`일 때 시간 선택의 보조 영역에도 그대로 전달됨 |
1249
+ | `actionSubContent` | `ReactNode \| (props: { onClick }) => ReactNode` | — | 보조 영역 커스텀 콘텐츠. 함수형은 기본 동작 핸들러를 재사용 가능 |
1250
+ | `actionMain` | `boolean` | `true` | Action area 메인 영역("적용") 노출 여부 |
1251
+ | `actionMainType` | `"fill"` \| `"text"` | `"text"` | 메인 버튼 스타일. `actionMainContent` 지정 시 무시됨. `hasTime`일 때 시간 선택에도 그대로 전달됨 |
1252
+ | `actionMainContent` | `ReactNode \| (props: { onClick }) => ReactNode` | — | 메인 영역 커스텀 콘텐츠 |
1253
+ | `monthYearActionArea`| `boolean` | `true` | 헤더의 연/월 라벨 클릭 시 빠른 선택 드롭다운 노출 여부 |
1254
+ | `startLabel` | `string` | `"시작"` | `hasTime`일 때 시작 시간 행의 라벨 |
1255
+ | `endLabel` | `string` | `"종료"` | `hasTime && (period\|range)`일 때 종료 시간 행의 라벨 |
1256
+ | `weekdayLabels` | `readonly string[]` | `["월",…,"일"]` | 요일 라벨. 월요일부터 순서대로 7개 전달 |
1257
+ | `amLabel` / `pmLabel`| `string` | `"오전"` / `"오후"` | 시간 선택 휠의 오전/오후 라벨 |
1258
+ | `formatMonthYear` | `(year: number, month: number) => string` | — | "2026년 8월" 헤더 포맷 override. `month`는 1~12로 전달 |
1259
+ | `formatTime` | `(hours: number, minutes: number) => string` | — | "오전 08:06" 시간 표시 포맷 override. `hours`는 0~23(24시간제) |
1260
+ | `timeNowLabel` | `string` | `"현재"` | 시간 휠 팝오버의 "현재" 버튼 라벨 |
1261
+ | `monthYearResetLabel`| `string` | `"초기화"` | 연/월 드롭다운의 "초기화" 버튼 라벨 |
1262
+ | `wheelApplyLabel` | `string` | `"적용"` | 시간/연·월 휠 팝오버 공용 "적용" 버튼 라벨 |
1263
+ | `timePlaceholder` | `string` | `"AA HH:MM"` | 시간 미선택 시 시간 선택 박스에 노출되는 placeholder |
1264
+ | `prevMonthLabel` / `nextMonthLabel` | `string` | `"이전 달"` / `"다음 달"` | 캘린더 이전/다음 달 이동 버튼 aria-label |
1265
+ | `openCalendarLabel` | `string` | `"달력 열기"` | 트리거 달력 아이콘 버튼 aria-label |
1266
+ | `customStyle` | `CSSProperties` | — | 루트 엘리먼트 인라인 스타일 |
1267
+
1268
+ **`DatePickerDateRange`**
1269
+
1270
+ | 필드 | 타입 | 설명 |
1271
+ | ------- | ------------- | --------- |
1272
+ | `start` | `Date \| null` | 시작 날짜 |
1273
+ | `end` | `Date \| null` | 종료 날짜 |
1274
+
1275
+ 유의사항:
1276
+
1277
+ - `variant`에 따라 `value`/`onChange` 타입이 달라지는 discriminated union입니다 (`normal`: `Date \| null`, `period`/`range`: `DatePickerDateRange`).
1278
+ - `trigger="icon"`이면서 `hasTime`이 `false`일 때만 텍스트 필드에 날짜를 직접 입력할 수 있습니다. 입력 시 `YYYYMMDD` → `YYYY-MM-DD`로 "-"가 자동으로 붙습니다.
1279
+ - `actionArea`가 `false`이면 적용 버튼 없이 날짜(또는 시작·종료 모두 선택 시 범위) 선택 즉시 `onChange`가 호출됩니다.
1280
+ - `range` variant는 연속된 2개월 캘린더를 동시에 보여줍니다.
1281
+ - 팝오버는 `document.body`에 포탈로 렌더링되며, 뷰포트 하단 공간이 부족하면 트리거 위쪽으로 자동 전환(flip)됩니다.
1282
+ - `actionSubContent`/`actionMainContent`를 함수로 전달하면 인자로 받는 `onClick`이 기본 동작(오늘로 이동 / 적용)을 그대로 실행하므로, 라벨이나 마크업만 바꾼 커스텀 요소에도 기존 동작을 재사용할 수 있습니다. `ReactNode`를 직접 전달하면 완전히 다른 요소로 교체되며 동작은 직접 구현해야 합니다.
1283
+ - `hasTime`일 때 시간 필드는 [TimePicker](#timepicker)를 그대로 임베드한 것으로, `actionArea`/`actionSub`/`actionMainType`이 동일하게 전달됩니다. 예를 들어 `actionArea={false}`면 날짜뿐 아니라 시간도 휠에서 값을 고르는 즉시 커밋됩니다.
1284
+
1285
+ ---
1286
+
1287
+ ## TimePicker
1288
+
1289
+ 시간을 선택하는 팝오버 기반 컴포넌트입니다. `DatePicker`의 시작/종료 시간 필드가 내부적으로 이 컴포넌트를
1290
+ `trigger="compact"`로 재사용합니다. `aa-hh-mm`(오전/오후+시+분) / `aa-hh`(오전/오후+시) / `hh-mm`(24시간제 시+분)
1291
+ 3가지 variant를 지원합니다.
1292
+
1293
+ ```tsx
1294
+ import { useState } from "react";
1295
+ import { TimePicker, type TimePickerValue } from "@idbrnd/design-system";
1296
+
1297
+ function TimePickerExample() {
1298
+ const [value, setValue] = useState<TimePickerValue | null>(null);
1299
+
1300
+ return <TimePicker heading="시간 선택" required value={value} onChange={setValue} />;
1301
+ }
1302
+ ```
1303
+
1304
+ **Variant**
1305
+
1306
+ ```tsx
1307
+ <TimePicker variant="aa-hh" value={value} onChange={setValue} /> {/* 오전/오후 08 */}
1308
+ <TimePicker variant="hh-mm" value={value} onChange={setValue} /> {/* 14:30 */}
1309
+ ```
1310
+
1311
+ **Trigger**
1312
+
1313
+ ```tsx
1314
+ {/* icon: 아이콘 버튼 트리거. hh-mm variant에서만 텍스트 필드에 직접 입력 가능 */}
1315
+ <TimePicker trigger="icon" value={value} onChange={setValue} />
1316
+
1317
+ {/* field: 텍스트 필드 트리거. 클릭으로만 팝오버가 열리고 직접 입력 불가 */}
1318
+ <TimePicker trigger="field" value={value} onChange={setValue} />
1319
+
1320
+ {/* compact: heading 없이 값/placeholder만 노출하는 작은 셀렉트박스. DatePicker가 내부적으로 사용하는 임베드 전용 모드 */}
1321
+ <TimePicker trigger="compact" value={value} onChange={setValue} />
1322
+ ```
1323
+
1324
+ **Action area 커스텀 (actionSubContent / actionMainContent)**
1325
+
1326
+ ```tsx
1327
+ <TimePicker
1328
+ value={value}
1329
+ onChange={setValue}
1330
+ actionSubContent={({ onClick }) => (
1331
+ <TextButton variant="assistive" size="medium" onClick={onClick}>
1332
+ Now
1333
+ </TextButton>
1334
+ )}
1335
+ actionMainContent={({ onClick }) => (
1336
+ <FillButton size="xsmall" onClick={onClick}>
1337
+ Apply
1338
+ </FillButton>
1339
+ )}
1340
+ />
1341
+ ```
1342
+
1343
+ **i18n (다국어 대응)**
1344
+
1345
+ ```tsx
1346
+ <TimePicker
1347
+ value={value}
1348
+ onChange={setValue}
1349
+ amLabel="AM"
1350
+ pmLabel="PM"
1351
+ nowLabel="Now"
1352
+ applyLabel="Apply"
1353
+ formatTime={(hours, minutes) => {
1354
+ const period = hours < 12 ? "AM" : "PM";
1355
+ const hour12 = hours % 12 === 0 ? 12 : hours % 12;
1356
+ return `${period} ${String(hour12).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
1357
+ }}
1358
+ />
1359
+ ```
1360
+
1361
+ | Prop | 타입 | 기본값 | 설명 |
1362
+ | ------------------- | --------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------- |
1363
+ | `value` | `TimePickerValue \| null` | — | **(필수)** 현재 선택된 값 |
1364
+ | `onChange` | `(value: TimePickerValue) => void` | — | **(필수)** 값 변경 시 호출되는 콜백 |
1365
+ | `variant` | `"aa-hh-mm"` \| `"aa-hh"` \| `"hh-mm"` | `"aa-hh-mm"` | `aa-hh`는 분 없이 시만, `hh-mm`은 24시간제(오전/오후 없음) |
1366
+ | `trigger` | `"icon"` \| `"field"` \| `"compact"` | `"icon"` | `compact`는 heading 없이 값/placeholder만 노출하는 임베드 전용 트리거 |
1367
+ | `heading` | `string` | — | 상단 라벨 텍스트. 미지정 시 라벨 미표시 |
1368
+ | `required` | `boolean` | `false` | 라벨 우측 `*` 표시 여부 |
1369
+ | `placeholder` | `string` | variant별 형식 | 트리거 placeholder. 미지정 시 `"AA HH:MM"` 등 variant에 맞는 형식 |
1370
+ | `disabled` | `boolean` | `false` | 전체 비활성화 |
1371
+ | `actionArea` | `boolean` | `true` | 팝오버 하단 액션 영역 노출 여부. `false`면 값 선택 즉시 `onChange` 호출 |
1372
+ | `actionSub` | `boolean` | `true` | Action area 보조 영역("현재") 노출 여부 |
1373
+ | `actionSubContent` | `ReactNode \| (props: { onClick }) => ReactNode` | — | 보조 영역 커스텀 콘텐츠. 함수형은 기본 동작 핸들러를 재사용 가능 |
1374
+ | `actionMain` | `boolean` | `true` | Action area 메인 영역("적용") 노출 여부 |
1375
+ | `actionMainType` | `"fill"` \| `"text"` | `"text"` | 메인 버튼 스타일. `actionMainContent` 지정 시 무시됨 |
1376
+ | `actionMainContent` | `ReactNode \| (props: { onClick }) => ReactNode` | — | 메인 영역 커스텀 콘텐츠 |
1377
+ | `nowLabel` | `string` | `"현재"` | 휠 팝오버의 "현재" 버튼 라벨 |
1378
+ | `applyLabel` | `string` | `"적용"` | 휠 팝오버의 "적용" 버튼 라벨 |
1379
+ | `amLabel` / `pmLabel` | `string` | `"오전"` / `"오후"` | 오전/오후 라벨 |
1380
+ | `formatTime` | `(hours: number, minutes: number) => string` | — | "오전 08:06" 시간 표시 포맷 override. `hours`는 0~23(24시간제) |
1381
+ | `openLabel` | `string` | `"시간 선택 열기"` | 트리거 시간 선택 아이콘 버튼 aria-label |
1382
+ | `customStyle` | `CSSProperties` | — | 루트 엘리먼트 인라인 스타일 |
1383
+
1384
+ **`TimePickerValue`**
1385
+
1386
+ | 필드 | 타입 | 설명 |
1387
+ | --------- | -------- | --------------- |
1388
+ | `hours` | `number` | 0~23(24시간제) |
1389
+ | `minutes` | `number` | 0~59 |
1390
+
1391
+ 유의사항:
1392
+
1393
+ - `amLabel`/`pmLabel`(기본 `"오전"`/`"오후"`)을 그대로 입력받아야 하는 `aa-hh-mm`/`aa-hh` variant는 `trigger="icon"`이어도 텍스트 직접 입력이 국제화가 불가능해 비활성화되고 `field`처럼 클릭으로만 팝오버가 열립니다. `hh-mm`(24시간제) variant에서만 실제로 직접 입력이 가능합니다.
1394
+ - `actionArea`가 `false`이면 적용 버튼 없이 휠에서 값을 고르는 즉시 `onChange`가 호출됩니다.
1395
+ - 팝오버는 `document.body`에 포탈로 렌더링되며, 뷰포트 하단 공간이 부족하면 트리거 위쪽으로 자동 전환(flip)됩니다.
1396
+ - `actionSubContent`/`actionMainContent`를 함수로 전달하면 인자로 받는 `onClick`이 기본 동작(현재 시각으로 이동 / 적용)을 그대로 실행하므로, 라벨이나 마크업만 바꾼 커스텀 요소에도 기존 동작을 재사용할 수 있습니다. `ReactNode`를 직접 전달하면 완전히 다른 요소로 교체되며 동작은 직접 구현해야 합니다.
1397
+
1398
+ ---
1399
+
954
1400
  ## Content
955
1401
 
956
1402
  ### Avatar
@@ -1941,6 +2387,9 @@ import type {
1941
2387
  WeakButtonSize,
1942
2388
  WeakButtonVariant,
1943
2389
  WeakButtonWidthType,
2390
+ TertiaryButtonProps,
2391
+ TertiaryButtonSize,
2392
+ TertiaryButtonVariant,
1944
2393
 
1945
2394
  // IconButton
1946
2395
  BasicIconButtonProps,
@@ -1961,6 +2410,11 @@ import type {
1961
2410
  SearchBarSize,
1962
2411
  SearchBarVariant,
1963
2412
 
2413
+ // Textarea
2414
+ TextareaProps,
2415
+ TextareaSize,
2416
+ TextareaVariant,
2417
+
1964
2418
  // Control
1965
2419
  CheckBoxProps,
1966
2420
  CheckBoxVariant,
@@ -1980,6 +2434,8 @@ import type {
1980
2434
  SnackbarVariant,
1981
2435
  PushBadgeProps,
1982
2436
  PushBadgeVariant,
2437
+ EmptyStateProps,
2438
+ EmptyStateSize,
1983
2439
  ClipProps,
1984
2440
  CustomSpinnerProps,
1985
2441
  FadeSpinnerProps,
@@ -2015,6 +2471,35 @@ import type {
2015
2471
  DropdownType,
2016
2472
  DropdownAlign,
2017
2473
 
2474
+ // DatePicker
2475
+ DatePickerProps,
2476
+ DatePickerVariant,
2477
+ DatePickerTrigger,
2478
+ DatePickerActionMainType,
2479
+ DatePickerActionContent,
2480
+ DatePickerActionRenderProps,
2481
+ DatePickerDateRange,
2482
+
2483
+ // TimePicker
2484
+ TimePickerProps,
2485
+ TimePickerVariant,
2486
+ TimePickerTrigger,
2487
+ TimePickerValue,
2488
+ TimePickerActionMainType,
2489
+ TimePickerActionContent,
2490
+ TimePickerActionRenderProps,
2491
+
2492
+ // Chip
2493
+ ChipProps,
2494
+ ChipVariant,
2495
+ ChipSize,
2496
+ ChipGroupProps,
2497
+ ChipGroupType,
2498
+ ChipGroupSize,
2499
+ FilterChipProps,
2500
+ FilterBarProps,
2501
+ FilterBarSize,
2502
+
2018
2503
  // SegmentedControl
2019
2504
  SegmentedControlProps,
2020
2505
  SegmentedControlVariant,
@@ -0,0 +1,37 @@
1
+ export type CalendarNavPosition = 'both' | 'prev-only' | 'next-only' | 'none';
2
+ interface CalendarMonth {
3
+ year: number;
4
+ month: number;
5
+ }
6
+ interface CalendarProps {
7
+ year: number;
8
+ month: number;
9
+ navPosition?: CalendarNavPosition;
10
+ onNavigate: (delta: number) => void;
11
+ /** 전달하지 않으면 연/월 라벨이 클릭 불가능한 정적 텍스트로 렌더링된다 */
12
+ onMonthYearChange?: (year: number, month: number) => void;
13
+ /** 함께 노출 중인 반대쪽 달력의 연/월. 겹치는 채움 날짜는 빈 칸으로 처리한다 */
14
+ neighborMonth?: CalendarMonth | null;
15
+ /** "2026년 8월" 헤더 라벨 포맷을 재정의한다. month는 1~12 값으로 전달된다 */
16
+ formatMonthYear?: (year: number, month: number) => string;
17
+ /** 연/월 선택 드롭다운의 "초기화" 버튼 라벨. @defaultValue '초기화' */
18
+ monthYearResetLabel?: string;
19
+ /** 연/월 선택 드롭다운의 "적용" 버튼 라벨. @defaultValue '적용' */
20
+ monthYearApplyLabel?: string;
21
+ /** 요일 라벨. buildMonthGrid가 월요일을 첫 컬럼으로 배치하므로 월요일부터 순서대로 7개를 전달해야 한다 */
22
+ weekdayLabels?: readonly string[];
23
+ /** 이전 달 이동 버튼의 aria-label. @defaultValue '이전 달' */
24
+ prevMonthLabel?: string;
25
+ /** 다음 달 이동 버튼의 aria-label. @defaultValue '다음 달' */
26
+ nextMonthLabel?: string;
27
+ selectionMode: 'single' | 'range';
28
+ selectedDate?: Date | null;
29
+ rangeStart?: Date | null;
30
+ rangeEnd?: Date | null;
31
+ hoverDate?: Date | null;
32
+ onHoverDate?: (date: Date | null) => void;
33
+ isDateDisabled?: (date: Date) => boolean;
34
+ onSelectDate: (date: Date) => void;
35
+ }
36
+ declare function Calendar({ year, month, navPosition, onNavigate, onMonthYearChange, neighborMonth, formatMonthYear, monthYearResetLabel, monthYearApplyLabel, weekdayLabels, prevMonthLabel, nextMonthLabel, selectionMode, selectedDate, rangeStart, rangeEnd, hoverDate, onHoverDate, isDateDisabled, onSelectDate, }: CalendarProps): import("react/jsx-runtime").JSX.Element;
37
+ export default Calendar;
@@ -0,0 +1,4 @@
1
+ import type { DatePickerProps } from "./DatePicker.types";
2
+ export type { DatePickerActionContent, DatePickerActionMainType, DatePickerActionRenderProps, DatePickerDateRange, DatePickerProps, DatePickerTrigger, DatePickerVariant, } from "./DatePicker.types";
3
+ declare function DatePicker(props: DatePickerProps): import("react/jsx-runtime").JSX.Element;
4
+ export default DatePicker;