@idbrnd/design-system 3.3.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.3.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)
@@ -59,8 +59,11 @@ export default function App() {
59
59
  - [PushBadge](#pushbadge)
60
60
  - [EmptyState](#emptystate)
61
61
  - [Tooltip](#tooltip)
62
+ - [Modal](#modal)
62
63
  - [Select](#select)
63
64
  - [Dropdown](#dropdown)
65
+ - [DatePicker](#datepicker)
66
+ - [TimePicker](#timepicker)
64
67
  - [Content](#content)
65
68
  - [Avatar](#avatar)
66
69
  - [AvatarStack](#avatarstack)
@@ -810,6 +813,101 @@ import { Tooltip } from "@idbrnd/design-system";
810
813
  - 툴팁은 `document.body`에 포탈로 렌더링됩니다.
811
814
  - 화살표는 회전된 정사각형(`rotate(45deg)`) 방식으로 렌더링됩니다. 툴팁 본체와 겹쳐 배치해 브라우저 줌 축소 시 발생하는 서브픽셀 갭을 방지하며, 개별 `rgba` 대신 부모 `opacity`를 사용해 겹침 영역의 투명도 이중 적용을 방지합니다.
812
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
+
813
911
  ---
814
912
 
815
913
  ## Select
@@ -1040,6 +1138,265 @@ const options: DropdownOption[] = [
1040
1138
 
1041
1139
  ---
1042
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
+
1043
1400
  ## Content
1044
1401
 
1045
1402
  ### Avatar
@@ -2114,6 +2471,24 @@ import type {
2114
2471
  DropdownType,
2115
2472
  DropdownAlign,
2116
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
+
2117
2492
  // Chip
2118
2493
  ChipProps,
2119
2494
  ChipVariant,
@@ -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;
@@ -0,0 +1,118 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+ import type { TimePickerVariant } from '../TimePicker/TimePicker.types';
3
+ /**
4
+ * - `normal`: 단일 날짜 선택, 달력 1개월 노출
5
+ * - `period`: 시작/종료 날짜 선택, 달력 1개월 노출
6
+ * - `range`: 시작/종료 날짜 선택, 연속된 2개월 동시 노출
7
+ * @defaultValue `'normal'`
8
+ */
9
+ export type DatePickerVariant = 'normal' | 'period' | 'range';
10
+ /**
11
+ * - `icon`: 아이콘 버튼 트리거. 텍스트 필드에 날짜 직접 입력 가능
12
+ * - `field`: 텍스트 필드 트리거. 클릭 시에만 달력이 열리고 직접 입력 불가
13
+ * @defaultValue `'icon'`
14
+ */
15
+ export type DatePickerTrigger = 'icon' | 'field';
16
+ /** Action area의 Main 버튼(적용) 스타일. @defaultValue `'fill'` */
17
+ export type DatePickerActionMainType = 'fill' | 'text';
18
+ export interface DatePickerDateRange {
19
+ start: Date | null;
20
+ end: Date | null;
21
+ }
22
+ /** Action area 커스텀 콘텐츠 함수형에 전달되는 인자 */
23
+ export interface DatePickerActionRenderProps {
24
+ /** 기본 동작(오늘로 이동 / 적용)을 그대로 실행하는 핸들러. 커스텀 요소에 연결해 재사용할 수 있다 */
25
+ onClick: () => void;
26
+ }
27
+ /**
28
+ * Action area 슬롯 콘텐츠.
29
+ * - 미지정: 기본 콘텐츠(오늘 텍스트 버튼 / 적용 버튼) 노출
30
+ * - ReactNode: 완전히 다른 요소(체크박스, 아이콘 버튼 등)로 교체. 동작은 직접 구현해야 한다
31
+ * - 함수: 기본 동작 핸들러(`onClick`)를 전달받아, i18n 라벨 등 텍스트만 바꾼 커스텀 요소에 그대로 재사용할 수 있다
32
+ */
33
+ export type DatePickerActionContent = ReactNode | ((props: DatePickerActionRenderProps) => ReactNode);
34
+ interface DatePickerBaseProps {
35
+ /** 상단 heading(라벨) 텍스트. 미지정 시 라벨 영역이 노출되지 않는다. */
36
+ heading?: string;
37
+ /** 필수 입력 여부. heading 노출 시 `*`를 함께 표시한다. @defaultValue `false` */
38
+ required?: boolean;
39
+ /** 트리거 placeholder. @defaultValue `'YYYY-MM-DD'` */
40
+ placeholder?: string;
41
+ /** @defaultValue `'normal'` */
42
+ variant?: DatePickerVariant;
43
+ /** @defaultValue `'icon'` */
44
+ trigger?: DatePickerTrigger;
45
+ /** 전체 비활성화 여부. @defaultValue `false` */
46
+ disabled?: boolean;
47
+ /** 날짜 하단에 시간 입력(HH:MM)을 함께 노출할지 여부. @defaultValue `false` */
48
+ hasTime?: boolean;
49
+ /** hasTime일 때 시작/종료 시간 선택에 사용하는 TimePicker의 variant. @defaultValue `'aa-hh-mm'` */
50
+ timeVariant?: TimePickerVariant;
51
+ /** 팝오버 하단 액션 영역 노출 여부. `false`면 날짜 선택 즉시 onChange가 호출된다. @defaultValue `true` */
52
+ actionArea?: boolean;
53
+ /** Action area 내 보조 영역(기본: "오늘" 텍스트 버튼) 노출 여부. @defaultValue `true` */
54
+ actionSub?: boolean;
55
+ /** Action area 보조 영역 커스텀 콘텐츠. 미지정 시 기본 "오늘" 텍스트 버튼이 노출된다 */
56
+ actionSubContent?: DatePickerActionContent;
57
+ /** Action area 내 메인 영역(기본: "적용" 버튼) 노출 여부. @defaultValue `true` */
58
+ actionMain?: boolean;
59
+ /** Action area 메인 버튼 스타일. actionMainContent 지정 시 무시된다. @defaultValue `'text'` */
60
+ actionMainType?: DatePickerActionMainType;
61
+ /** Action area 메인 영역 커스텀 콘텐츠. 미지정 시 actionMainType에 따른 기본 "적용" 버튼이 노출된다 */
62
+ actionMainContent?: DatePickerActionContent;
63
+ /** 헤더의 년/월 라벨 클릭 시 년/월 빠른 선택 뷰로 전환할지 여부. @defaultValue `true` */
64
+ monthYearActionArea?: boolean;
65
+ /** 특정 날짜를 선택 불가 처리하는 판별 함수 */
66
+ isDateDisabled?: (date: Date) => boolean;
67
+ /** hasTime일 때 시작 시간 행의 라벨. @defaultValue `'시작'` */
68
+ startLabel?: string;
69
+ /** hasTime && (period|range)일 때 종료 시간 행의 라벨. @defaultValue `'종료'` */
70
+ endLabel?: string;
71
+ /**
72
+ * 캘린더 헤더/월·년 선택 라벨("2026년 8월") 포맷을 재정의한다. i18n 등에 사용.
73
+ * month는 1~12 값으로 전달된다. 미지정 시 `${year}년 ${month}월` 형식을 사용한다.
74
+ */
75
+ formatMonthYear?: (year: number, month: number) => string;
76
+ /**
77
+ * 트리거/시간 선택 박스에 표시되는 시간("오전 08:06") 포맷을 재정의한다. i18n 등에 사용.
78
+ * hours는 0~23(24시간제) 값으로 전달된다. 미지정 시 `오전/오후 HH:MM` 형식을 사용한다.
79
+ */
80
+ formatTime?: (hours: number, minutes: number) => string;
81
+ /** 시간 선택 휠 팝오버의 "현재" 버튼 라벨. @defaultValue `'현재'` */
82
+ timeNowLabel?: string;
83
+ /** 연/월 선택 드롭다운의 "초기화" 버튼 라벨. @defaultValue `'초기화'` */
84
+ monthYearResetLabel?: string;
85
+ /** 시간/연·월 선택 휠 팝오버 공용 "적용" 버튼 라벨. @defaultValue `'적용'` */
86
+ wheelApplyLabel?: string;
87
+ /**
88
+ * 캘린더 요일 라벨. 월요일을 첫 컬럼으로 배치하므로 월요일부터 순서대로 7개를 전달해야 한다.
89
+ * 미지정 시 `['월', '화', '수', '목', '금', '토', '일']`을 사용한다.
90
+ */
91
+ weekdayLabels?: readonly string[];
92
+ /** 시간 선택 휠의 오전(AM) 라벨. @defaultValue `'오전'` */
93
+ amLabel?: string;
94
+ /** 시간 선택 휠의 오후(PM) 라벨. @defaultValue `'오후'` */
95
+ pmLabel?: string;
96
+ /** 이전 달 이동 버튼의 aria-label. @defaultValue `'이전 달'` */
97
+ prevMonthLabel?: string;
98
+ /** 다음 달 이동 버튼의 aria-label. @defaultValue `'다음 달'` */
99
+ nextMonthLabel?: string;
100
+ /** 트리거의 달력 열기 아이콘 버튼 aria-label. @defaultValue `'달력 열기'` */
101
+ openCalendarLabel?: string;
102
+ /** hasTime일 때 시간 선택 박스에 값이 없을 때 노출되는 placeholder. @defaultValue `'AA HH:MM'` */
103
+ timePlaceholder?: string;
104
+ /** 루트 컨테이너에 적용할 추가 style */
105
+ customStyle?: CSSProperties;
106
+ }
107
+ export interface DatePickerNormalProps extends DatePickerBaseProps {
108
+ variant?: 'normal';
109
+ value: Date | null;
110
+ onChange: (value: Date | null) => void;
111
+ }
112
+ export interface DatePickerPeriodProps extends DatePickerBaseProps {
113
+ variant: 'period' | 'range';
114
+ value: DatePickerDateRange;
115
+ onChange: (value: DatePickerDateRange) => void;
116
+ }
117
+ export type DatePickerProps = DatePickerNormalProps | DatePickerPeriodProps;
118
+ export {};
@@ -0,0 +1,15 @@
1
+ interface MonthYearSelectProps {
2
+ year: number;
3
+ month: number;
4
+ onChange: (year: number, month: number) => void;
5
+ /** false면 클릭해도 열리지 않는 정적 라벨로 렌더링한다 */
6
+ disabled?: boolean;
7
+ /** "2026년 8월" 라벨 포맷을 재정의한다. month는 1~12 값으로 전달된다 */
8
+ formatLabel?: (year: number, month: number) => string;
9
+ /** 드롭다운의 "초기화" 버튼 라벨. @defaultValue '초기화' */
10
+ resetLabel?: string;
11
+ /** 드롭다운의 "적용" 버튼 라벨. @defaultValue '적용' */
12
+ applyLabel?: string;
13
+ }
14
+ declare function MonthYearSelect({ year, month, onChange, disabled, formatLabel, resetLabel, applyLabel, }: MonthYearSelectProps): import("react/jsx-runtime").JSX.Element;
15
+ export default MonthYearSelect;
@@ -0,0 +1,33 @@
1
+ /** buildMonthGrid가 월요일을 첫 컬럼으로 배치하므로 라벨도 월요일부터 시작해야 한다 */
2
+ export declare const WEEKDAY_LABELS: readonly ["월", "화", "수", "목", "금", "토", "일"];
3
+ export interface CalendarDay {
4
+ date: Date;
5
+ inCurrentMonth: boolean;
6
+ }
7
+ export declare function startOfDay(date: Date): Date;
8
+ export declare function isSameDay(a: Date | null, b: Date | null): boolean;
9
+ export declare function isSameMonth(a: Date, b: Date): boolean;
10
+ export declare function isBefore(a: Date, b: Date): boolean;
11
+ export declare function isAfter(a: Date, b: Date): boolean;
12
+ export declare function isWithinRange(date: Date, start: Date | null, end: Date | null): boolean;
13
+ export declare function addMonths(year: number, month: number, delta: number): {
14
+ year: number;
15
+ month: number;
16
+ };
17
+ /** 항상 6주(6x7)로 구성된 달력 그리드를 생성한다 (앞/뒤 달 날짜 포함) */
18
+ export declare function buildMonthGrid(year: number, month: number): CalendarDay[][];
19
+ export declare function formatDate(date: Date | null): string;
20
+ export declare function parseDate(value: string): Date | null;
21
+ export declare function parseTime(value: string): {
22
+ hours: number;
23
+ minutes: number;
24
+ } | null;
25
+ export declare function withTime(date: Date, hours: number, minutes: number): Date;
26
+ export declare function today(): Date;
27
+ /**
28
+ * 날짜 직접 입력 필드에 숫자를 입력하면 4번째, 6번째 자리 뒤에 "-"를 자동으로 붙인다.
29
+ * "~"로 구분된 두 번째 날짜(기간 선택)에도 동일하게 적용된다.
30
+ * 자동으로 붙은 "-"를 backspace로 지웠을 때 아무 반응이 없어 보이는 걸 막기 위해,
31
+ * 직전 값과 비교해 실제로는 숫자 하나가 지워진 것으로 처리한다.
32
+ */
33
+ export declare function formatDateInputMask(nextRaw: string, prevRaw: string): string;
@@ -0,0 +1,3 @@
1
+ import type { ModalProps } from "./Modal.types";
2
+ declare function Modal({ open, onClose, size, type, heading, navBar, showClose, navLeadingSlot, navTrailingSlot, mainAction, subAction, actionSlot, closeOnEsc, closeOnDimClick, customStyle, className, children, }: ModalProps): import("react").ReactPortal | null;
3
+ export default Modal;