Calendar#
날짜를 시각적으로 탐색하고 선택할 수 있는 캘린더 컴포넌트입니다.
Live Preview#
class CalendarNoneExample extends StatefulComponent {
const CalendarNoneExample({super.key});
@override
State<CalendarNoneExample> createState() => _CalendarNoneExampleState();
}
class _CalendarNoneExampleState extends State<CalendarNoneExample> {
@override
Component build(BuildContext context) {
return Calendar(
selectionMode: CoreCalendarSelectionMode.none,
);
}
}
class CalendarNoneExample extends StatefulWidget {
const CalendarNoneExample({super.key});
@override
State<CalendarNoneExample> createState() => _CalendarNoneExampleState();
}
class _CalendarNoneExampleState extends State<CalendarNoneExample> {
@override
Widget build(BuildContext context) {
return Calendar(
selectionMode: CoreCalendarSelectionMode.none,
);
}
}
class CalendarDefaultExample extends StatefulComponent {
const CalendarDefaultExample({super.key});
@override
State<CalendarDefaultExample> createState() => _CalendarDefaultExampleState();
}
class _CalendarDefaultExampleState extends State<CalendarDefaultExample> {
@override
Component build(BuildContext context) {
return Calendar.single();
}
}
class CalendarDefaultExample extends StatefulWidget {
const CalendarDefaultExample({super.key});
@override
State<CalendarDefaultExample> createState() => _CalendarDefaultExampleState();
}
class _CalendarDefaultExampleState extends State<CalendarDefaultExample> {
@override
Widget build(BuildContext context) {
return Calendar.single();
}
}
class CalendarMultiExample extends StatefulComponent {
const CalendarMultiExample({super.key});
@override
State<CalendarMultiExample> createState() => _CalendarMultiExampleState();
}
class _CalendarMultiExampleState extends State<CalendarMultiExample> {
@override
Component build(BuildContext context) {
return Calendar.multi();
}
}
class CalendarMultiExample extends StatefulWidget {
const CalendarMultiExample({super.key});
@override
State<CalendarMultiExample> createState() => _CalendarMultiExampleState();
}
class _CalendarMultiExampleState extends State<CalendarMultiExample> {
@override
Widget build(BuildContext context) {
return Calendar.multi();
}
}
class CalendarRangeExample extends StatefulComponent {
const CalendarRangeExample({super.key});
@override
State<CalendarRangeExample> createState() => _CalendarRangeExampleState();
}
class _CalendarRangeExampleState extends State<CalendarRangeExample> {
@override
Component build(BuildContext context) {
return Calendar.range();
}
}
class CalendarRangeExample extends StatefulWidget {
const CalendarRangeExample({super.key});
@override
State<CalendarRangeExample> createState() => _CalendarRangeExampleState();
}
class _CalendarRangeExampleState extends State<CalendarRangeExample> {
@override
Widget build(BuildContext context) {
return Calendar.range();
}
}
class CalendarChainExample extends StatelessComponent {
const CalendarChainExample({super.key});
@override
Component build(BuildContext context) {
return Calendar.single().withStyle(
const CoreCalendarStyle(
dayCellSize: CoreSpace.space40,
cellSpacing: CoreSpace.space12,
endpointPillBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
headerTextStyle: CoreTextStyle.token(
CoreTextStyles.titleSmall,
color: CoreColor.token(CoreColors.primary),
),
),
);
}
}
class CalendarChainExample extends StatelessWidget {
const CalendarChainExample({super.key});
@override
Widget build(BuildContext context) {
return Calendar.single().withStyle(
const CoreCalendarStyle(
dayCellSize: CoreSpace.space40,
cellSpacing: CoreSpace.space12,
endpointPillBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
headerTextStyle: CoreTextStyle.token(
CoreTextStyles.titleSmall,
color: CoreColor.token(CoreColors.primary),
),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 날짜를 시각적으로 탐색하고 선택해야 할 때
- 범위(체크인/체크아웃) 또는 다중 날짜 선택이 필요할 때
대신 다른 컴포넌트를 사용하세요:
DatePicker: 입력 필드와 결합된 날짜 선택 폼 요소일 때TextField: 단순 날짜 텍스트 입력만 필요할 때
기본 사용법 (Basic Usage)#
// 단일 선택
Calendar.single(
onChanged: (value) => print('Selected: $value'),
)
// 범위 선택
Calendar.range(
onChanged: (value) => print('Range: $value'),
)
// 다중 선택
Calendar.multi(
onChanged: (value) => print('Multi: $value'),
)
// 단일 선택
Calendar.single(
onChanged: (value) => print('Selected: $value'),
)
// 범위 선택
Calendar.range(
onChanged: (value) => print('Range: $value'),
)
// 다중 선택
Calendar.multi(
onChanged: (value) => print('Multi: $value'),
)
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
selectionMode |
CoreCalendarSelectionMode |
single |
선택 모드 |
value |
CoreCalendarValue? |
null |
현재 선택 값 |
onChanged |
Function(CoreCalendarValue?)? |
null |
선택 변경 콜백 |
initialView |
CoreCalendarView? |
null |
초기 표시 월/년 |
stateBuilder |
CoreCalendarDateStateBuilder? |
null |
날짜별 활성/비활성 결정 |
minDate |
DateTime? |
null |
최소 선택 가능 날짜 |
maxDate |
DateTime? |
null |
최대 선택 가능 날짜 |
calendarStyle |
CoreCalendarStyle? |
null |
인스턴스별 스타일 (chrome / 슬롯 / 상태 토큰) |
스타일 시스템 (Style System)#
Calendar 의 모든 chrome / dimensional / 슬롯 / 상태 토큰 override 는
단일 calendarStyle: CoreCalendarStyle 슬롯으로 흐릅니다.
selectionMode / initialView 같은 시맨틱 enum 과 동작 필드는 위젯에 그대로
남고, 스타일 슬롯에는 들어가지 않습니다 (원칙 6 / 7).
CoreCalendarStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
dayCellSize |
double? |
Day / month / year cell size (logical px). null defers to [defaultDayCellSize]. |
monthCellWidth |
double? |
Month / year grid cell width (logical px). null defers to [defaultMonthCellWidth]. |
cellSpacing |
double? |
Gap between calendar grid cells (logical px).
null
defers to [defaultCellSpacing]. Consumed natively as
Column.spacing
on Flutter and
gap-${cellSpacing.tailwindSpace}
on Web.
|
headerGridGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the header-row → grid gap (also reused as the inter-button gap inside the header row). Forwarded straight to
Gap(gapStyle: …)
by the Flutter resolver. Per- instance override is merged on top of [defaultHeaderGridGapStyle].
|
yearRowSpacing |
double? |
Row gap between year-grid rows (logical px).
null
defers to [defaultYearRowSpacing]. Consumed natively as
Column.spacing
on Flutter and
gap-${yearRowSpacing.tailwindSpace}
on Web.
|
endpointPillBorderRadius |
CoreBorderRadius? |
Border radius for the range endpoint pill.
null
defers to [defaultEndpointPillBorderRadius].
|
fadedOpacity |
double? |
Opacity applied to dates from the previous / next month.
null
defers to [defaultFadedOpacity].
|
yearGridSize |
int? |
Total items in the year grid. null defers to [defaultYearGridSize]. |
monthGridColumns |
int? |
Month grid columns. null defers to [defaultMonthGridColumns]. |
yearGridColumns |
int? |
Year grid columns. null defers to [defaultYearGridColumns]. |
navButtonStyle |
CoreButtonStyle? |
Style applied to navigation buttons (prev / next month / year arrows + the header button that opens the year/month grid). |
dateCellStyle |
CoreButtonStyle? |
Style applied to each date / month / year cell button. |
arrowIconStyle |
CoreIconStyle? |
Icon style used by the prev / next arrow icons inside the nav buttons. |
headerTextStyle |
CoreTextStyle? |
Header label text style (the "May 2026" / "2026" / decade label).
null
defers to [defaultHeaderTextStyle].
|
weekdayTextStyle |
CoreTextStyle? |
Weekday header label text style (Sun / Mon / …).
null
defers to [defaultWeekdayTextStyle].
|
cellTextStyle |
CoreTextStyle? |
Date / month / year cell text style — applied to the cell
Button.labelStyle
.
null
defers to [defaultCellTextStyle].
|
selectedColor |
CoreColor? |
Background colour for the selected (start / end pill) date cell.
null
defers to [defaultSelectedColor].
|
selectedTextStyle |
CoreTextStyle? |
Text style for the selected (start / end pill) date cell. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
selectedTextColor
field removed).
null
defers to [defaultSelectedTextStyle].
|
todayColor |
CoreColor? |
Background colour for today's date cell.
null
defers to Button's secondary variant default. Carries no
defaultTodayColor
. The today cell is a composed
Button(variant:.secondary)
, so its fill is
CoreButtonStyle.defaultsByVariant[.secondary].backgroundColor
and a constant here would be a second copy of it (
style-contract.md
: the parent must not reach into the child's defaults). Both platforms key the
override
on absence, so a default would double the fill rather than replace it: Flutter passes it as
dateCellDefault.copyWith(backgroundColor: merged.todayColor)
, where null leaves the Button's own variant fill in place, and Web skips the override wrapper entirely (
todayColor == null
emits neither class nor inline), so a constant would paint a fill on top of the Button's on every calendar.
|
todayTextStyle |
CoreTextStyle? |
Text style for today's date cell. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
todayTextColor
field removed).
null
defers to [defaultTodayTextStyle].
|
rangeSelectionColor |
CoreColor? |
Background colour for cells inside a selected range (between start and end, exclusive).
null
defers to [defaultRangeSelectionColor].
|
rangeSelectionTextStyle |
CoreTextStyle? |
Text style for cells inside a selected range. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
rangeSelectionTextColor
field removed).
null
defers to [defaultRangeSelectionTextStyle].
|
weekdayLabelTextStyle |
CoreTextStyle? |
Weekday header label text style (Sun / Mon / …). Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
weekdayLabelColor
field removed).
null
defers to [defaultWeekdayLabelTextStyle].
|
Resolve chain#
design system default
→ CoreCalendarTheme.style // 프로젝트 공통
→ parent component slot override
(예: CoreDatePickerStyle.calendarStyle)
→ widget.calendarStyle // 인스턴스별
CoreCalendarStyle.merge 는 nested slot Style (navButtonStyle 등) 을
재귀적으로 머지하므로 부모 / theme / 인스턴스 단계에서 부분 override 가
가능합니다.
사용 예#
Calendar(
selectionMode: CoreCalendarSelectionMode.single,
calendarStyle: CoreCalendarStyle(
selectedColor: CoreColor.token(CoreColors.success),
dateCellStyle: CoreButtonStyle(
labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
),
),
onChanged: (value) => print(value),
)
Calendar(
selectionMode: CoreCalendarSelectionMode.single,
calendarStyle: CoreCalendarStyle(
selectedColor: CoreColor.token(CoreColors.primary),
rangeSelectionColor: CoreColor.token(CoreColors.secondary),
),
onChanged: (value) => print(value),
)
빠른 오버라이드 (Chain)#
이미 만든 Calendar 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class CalendarChainExample extends StatelessWidget {
const CalendarChainExample({super.key});
@override
Widget build(BuildContext context) {
return Calendar.single().withStyle(
const CoreCalendarStyle(
dayCellSize: CoreSpace.space40,
cellSpacing: CoreSpace.space12,
endpointPillBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
headerTextStyle: CoreTextStyle.token(
CoreTextStyles.titleSmall,
color: CoreColor.token(CoreColors.primary),
),
),
);
}
}
class CalendarChainExample extends StatelessComponent {
const CalendarChainExample({super.key});
@override
Component build(BuildContext context) {
return Calendar.single().withStyle(
const CoreCalendarStyle(
dayCellSize: CoreSpace.space40,
cellSpacing: CoreSpace.space12,
endpointPillBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
headerTextStyle: CoreTextStyle.token(
CoreTextStyles.titleSmall,
color: CoreColor.token(CoreColors.primary),
),
),
);
}
}
변형 (Variants)#
단일 선택 (Single)#
Calendar.single(
onChanged: (value) => print('Selected: $value'),
)
범위 선택 (Range)#
Calendar.range(
onChanged: (value) => print('Range: $value'),
)
범위 모드에서는 듀얼 캘린더 레이아웃이 표시됩니다.
다중 선택 (Multi)#
Calendar.multi(
onChanged: (value) => print('Multi: $value'),
)
표시 전용 (None)#
Calendar(
selectionMode: CoreCalendarSelectionMode.none,
)
동작 스펙 (Behavior)#
선택 모드#
CoreCalendarSelectionMode.none // 표시 전용
CoreCalendarSelectionMode.single // 단일 날짜 선택
CoreCalendarSelectionMode.range // 시작~끝 범위 선택
CoreCalendarSelectionMode.multi // 다중 날짜 선택
- Single: 클릭으로 선택, 다시 클릭하면 해제
- Range: 첫 번째 클릭=시작, 두 번째 클릭=끝. 자동 정렬(start < end)
- Multi: 클릭할 때마다 선택/해제 토글
CoreCalendarValue#
// 단일 선택
CoreCalendarValue.single(DateTime(2024, 3, 15))
// 범위 선택
CoreCalendarValue.range(DateTime(2024, 3, 10), DateTime(2024, 3, 20))
// 다중 선택
CoreCalendarValue.multi([DateTime(2024, 3, 5), DateTime(2024, 3, 15)])
뷰 전환#
CoreCalendarViewType.date: 월별 일 그리드 (6행 × 7열)CoreCalendarViewType.month: 월 선택 (4×3 그리드)CoreCalendarViewType.year: 연도 선택 (4×4 그리드)- 헤더의 월/연도를 클릭하면 상위 뷰로 전환
날짜 상태 검증#
Calendar.single(
stateBuilder: (date) {
if (date.isBefore(DateTime.now())) return CoreCalendarDateState.disabled;
return CoreCalendarDateState.enabled;
},
onChanged: (value) => print(value),
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
minDate/maxDate 범위 제한과 커스텀 로직을 함께 쓰려면 stateBuilder 안에서 직접 검사
Calendar.single(
stateBuilder: (date) {
final min = DateTime(2024, 1, 1);
final max = DateTime(2024, 12, 31);
if (date.isBefore(min) || date.isAfter(max)) {
return CoreCalendarDateState.disabled;
}
if (date.weekday == DateTime.saturday || date.weekday == DateTime.sunday) {
return CoreCalendarDateState.disabled;
}
return CoreCalendarDateState.enabled;
},
onChanged: (value) => print(value),
)
stateBuilder가 있으면 범위 검사를 그 안에 직접 포함시켜야 합니다 — 아래 Don't 예제처럼 minDate/maxDate와 나란히 넘겨도 minDate/maxDate 쪽은 실행되지 않습니다.
❌ Don't#
stateBuilder와 minDate/maxDate가 함께 적용될 거라 기대하지 않기
// ❌ 주말만 비활성화하려는 의도인데 minDate/maxDate 도 같이 넘김
Calendar.single(
minDate: DateTime(2024, 1, 1),
maxDate: DateTime(2024, 12, 31),
stateBuilder: (date) =>
(date.weekday == DateTime.saturday || date.weekday == DateTime.sunday)
? CoreCalendarDateState.disabled
: CoreCalendarDateState.enabled,
onChanged: (value) => print(value),
)
내부 날짜 상태 판정은 stateBuilder가 non-null이면 그 결과를 즉시 반환하고 minDate/maxDate 검사로는 아예 넘어가지 않습니다 — 위 코드는 주말만 비활성화될 뿐 minDate/maxDate 범위는 조용히 무시됩니다.
접근성 (Accessibility)#
역할 / Semantics#
| 루트 | 그리드 | 행 | 요일 헤더 셀 | 날짜 / 월 / 연도 셀 | |
|---|---|---|---|---|---|
| Flutter | Semantics(container: true, label:) — single · range 레이아웃 공통. 역할은 없습니다. |
Semantics(container: true, role: .table) |
Semantics(container: true, role: .row) |
Semantics(container: true, role: .columnHeader) (날짜 그리드만 — 월/연도 그리드엔 헤더 행이 없음) |
Semantics(container: true, role: .cell)
로 감싼
Button
이 그 안에서
Clickable
을 통해
Semantics(button: true, enabled:)
를 겹침
|
| Web | <div> + aria-label (역할 없음, single · range 공통) |
role="grid" |
role="row" |
role="columnheader" (날짜 그리드만) |
role="gridcell"
(
display: contents
) 로 감싼 네이티브
<button type="button">
. 비활성 시
disabled
+
tabindex="-1"
+
aria-disabled="true"
|
라벨 문자열은 양쪽 모두 로케일(calendarLabel)에서 옵니다.
table/grid 역할은 루트가 아니라 그리드 자체(날짜·월·연도 세 뷰 모두)에 있습니다 — 루트는 헤더 내비게이션 버튼도 함께 담고 있어 그 전체에 grid 역할을 씌우면 내비게이션 버튼까지 그리드 셀처럼 읽히기 때문입니다.
role="gridcell"/Semantics(role: .cell) 은 셀을 감싸는 별도 wrapper 에 있고 Button
자신에는 없습니다 — 한 요소는 ARIA 역할을 하나만 가질 수 있어, 버튼이 이미 갖는 role="button"/button: true
시맨틱과 겹칠 수 없기 때문입니다(Web 의 wrapper 는 display: contents 로 레이아웃엔 관여하지 않습니다). aria-selected
/ aria-current 는 여전히 어느 플랫폼에도 없습니다 — 선택 상태는 여전히 Button variant 색으로만 표현됩니다.
range 모드의 범위 내부 셀과 시작/끝 셀만 Button 이 아니라 직접 그린 요소입니다 — 범위 띠가 끝점 알약 뒤로 이어져야 해서 컴포넌트를 합성하지 않습니다. 대신 버튼이라는 사실을 스스로 진술합니다: Flutter 는
Clickable(Semantics(button: true) + Enter/Space + 포커스 링), Web 은 role="button"
+ tabindex="0" + Enter/Space keydown 핸들러. 포인터로 고를 수 있는 날짜는 키보드로도 고를 수 있습니다.
stateBuilder 가 비활성으로 판정한 셀도 버튼으로 남습니다 — Web 은 role="button" 을 유지한 채 tabindex
대신 aria-disabled="true" 를, Flutter 는 Clickable(enabled: false) 를 씁니다. 역할을 빼버리면 고를 수 없는 날짜가 달 주변의 빈 칸과 구분되지 않는데, "지금은 못 고름"과 "날짜가 아님"은 같은 질문에 대한 다른 답입니다.
키보드#
| 키 | 동작 |
|---|---|
Enter | 포커스된 날짜 / 월 / 연도 / 네비게이션 버튼 활성화 |
Space | 위와 동일 |
이것이 전부입니다. 화살표 키 날짜 이동, Home / End, PageUp / PageDown, Escape
는 어느 플랫폼에도 없습니다. 캘린더 자체는 그리드 차원의 키 핸들러를 설치하지 않으며, 위 두 키는 셀이 각자 제공합니다(Button
은 Flutter Clickable 의 activation shortcut / Web 네이티브 <button> 의 기본 동작, range 의 직접 그린 셀은 위 절의
Clickable 과 keydown 핸들러).
포커스#
셀 단위 포커스만 있고 그리드 차원의 조율은 없습니다.
-
Flutter: 각 셀이 자기
FocusNode를 갖고FocusOutline링을 그립니다(range 의 직접 그린 셀도Clickable기본값으로 동일). - Web: 네이티브 버튼 포커스 +
focus-visible링 클래스, range 의 직접 그린 셀은tabindex="0". -
roving tabindex 가 없어 날짜 하나하나가 독립된 탭 스톱입니다 — 한 달 그리드는 주 단위로 채워져 대개 35–42칸이고, 헤더·네비게이션 버튼까지 더한 만큼을
Tab으로 지나야 그리드를 벗어납니다. - 초기 포커스/autofocus 대상이 없고, 뷰 전환(date → month → year)이나 월 이동 후 포커스를 복원하지 않으며, 포커스 트랩도 없습니다.
스크린 리더#
-
루트: 양 플랫폼 모두 로컬라이즈된 라벨을 가진, 역할 없는 컨테이너로 읽힙니다 — single · range 레이아웃에 동일하게 적용됩니다. 예전엔 Web 루트가
role="application"이라 리더의 브라우즈 모드(가상 커서) 탐색 전체를 껐지만, 이 요소는Enter/Space외의 어떤 키도 가로챈 적이 없어 얻는 것 없이 탐색만 막고 있었습니다 — 지금은 제거되었습니다. -
그리드: 날짜/월/연도 그리드 자체는 이제 "표" (Web
role="grid", Flutterrole: .table)로 읽히고, 행은 "행"으로, 요일 헤더 셀은 "열 머리글"로 읽힙니다. 격자 구조는 전달되지만, 화살표 키로 그 구조를 순회하는 것(roving tabindex)은 아직 없습니다 — 위 "알려진 제약" 참조. -
날짜 셀: "15, 버튼" 처럼 날짜 숫자와 버튼 역할만 읽힙니다. 월·연도·요일은 물론 "오늘" · "선택됨" · "범위 안" 도 전달되지 않습니다 — 선택과 오늘은
Buttonvariant 색으로만 표현됩니다. - range 의 범위 내부·시작/끝 날짜도 버튼으로 읽히지만, 마찬가지로 숫자만 말할 뿐 범위 안이라는 사실은 전달되지 않습니다.
-
비활성 날짜(
stateBuilder)는 모든 셀에서 전달됩니다 —Button셀과 직접 그린 range 셀 모두 Web 은aria-disabled, Flutter 는Semantics(enabled: false)로 알립니다.
알려진 제약#
-
grid 구조는 있지만 roving tabindex 는 없습니다.
role="grid"/Semantics(role: .table)이 셀 사이의 2D 관계를 스크린 리더에 알리긴 하지만, 화살표 키로 그 관계를 따라 이동하는 것은 별도 기능입니다 — 지금은 화살표 키도 roving tabindex 도 없어 키보드 사용자는 원하는 날짜까지Tab으로 셀 하나하나를 거쳐야 합니다. grid 역할을 부여했다는 것은 "이 구조를 화살표 키로 순회할 수 있다"는 기대를 스크린 리더 사용자에게 주므로, 그 기대에 실제로 부응하는 roving tabindex 는 이 구조 위에 올라갈 후속 작업입니다. -
선택 상태가 비시각적으로 전달되지 않습니다.
aria-selected/aria-current가 없어 선택된 날짜와 오늘은 색으로만 구분되며, 스크린 리더 사용자와 색 구분이 어려운 사용자 모두에게 도달하지 않습니다. 범위 안에 든 날짜도 마찬가지로 색으로만 표시됩니다. - 날짜 셀이 날짜를 말하지 않습니다. "15" 만 읽히므로 어느 달·어느 요일인지 알 수 없습니다.
-
Web 은
aria-label을 호출자attributes뒤에 병합하므로 소비자가 그 값을 덮어쓸 수 없습니다.
위 항목들은 현재 코드 상태이며, 캘린더를 접근 가능한 형태로 출하해야 한다면 소비자가 직접 보완해야 합니다. 모든 컴포넌트에 공통으로 적용되는 축은 전역 접근성 축에 있습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Calendar | Calendar |
| 값 모델 | CoreCalendarValue | CoreCalendarValue |
| 선택 모드 | none/single/range/multi | none/single/range/multi |
| 뷰 전환 | date → month → year | date → month → year |
| 범위 레이아웃 | 듀얼 캘린더 | 듀얼 캘린더 |
| 네비게이션 | Button(variant: .outline) |
Button(variant: .outline) |
| 날짜 셀 | Button(variant: .ghost/.primary/.secondary) |
Button(variant: .ghost/.primary/.secondary) |
관련 컴포넌트 (Related Components)#
- DatePicker: 입력 필드 + 캘린더 팝업