@x-plat/design-system 0.4.2 → 0.4.4

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.
@@ -1091,6 +1091,10 @@ var Calendar = (props) => {
1091
1091
  setYear,
1092
1092
  setMonth
1093
1093
  } = useCalendar(yearProp, monthProp);
1094
+ const [pickerMode, setPickerMode] = import_react2.default.useState("days");
1095
+ const [yearRangeStart, setYearRangeStart] = import_react2.default.useState(
1096
+ Math.floor(year / 12) * 12
1097
+ );
1094
1098
  import_react2.default.useEffect(() => {
1095
1099
  if (yearProp !== void 0) setYear(yearProp);
1096
1100
  }, [yearProp, setYear]);
@@ -1098,22 +1102,54 @@ var Calendar = (props) => {
1098
1102
  if (monthProp !== void 0) setMonth(monthProp);
1099
1103
  }, [monthProp, setMonth]);
1100
1104
  const handlePrev = () => {
1101
- goToPrevMonth();
1102
- const prevMonth = month === 0 ? 11 : month - 1;
1103
- const prevYear = month === 0 ? year - 1 : year;
1104
- onMonthChange?.(prevYear, prevMonth);
1105
+ if (pickerMode === "days") {
1106
+ goToPrevMonth();
1107
+ const prevMonth = month === 0 ? 11 : month - 1;
1108
+ const prevYear = month === 0 ? year - 1 : year;
1109
+ onMonthChange?.(prevYear, prevMonth);
1110
+ } else if (pickerMode === "months") {
1111
+ setYear(year - 1);
1112
+ onMonthChange?.(year - 1, month);
1113
+ } else {
1114
+ setYearRangeStart((s) => s - 12);
1115
+ }
1105
1116
  };
1106
1117
  const handleNext = () => {
1107
- goToNextMonth();
1108
- const nextMonth = month === 11 ? 0 : month + 1;
1109
- const nextYear = month === 11 ? year + 1 : year;
1110
- onMonthChange?.(nextYear, nextMonth);
1118
+ if (pickerMode === "days") {
1119
+ goToNextMonth();
1120
+ const nextMonth = month === 11 ? 0 : month + 1;
1121
+ const nextYear = month === 11 ? year + 1 : year;
1122
+ onMonthChange?.(nextYear, nextMonth);
1123
+ } else if (pickerMode === "months") {
1124
+ setYear(year + 1);
1125
+ onMonthChange?.(year + 1, month);
1126
+ } else {
1127
+ setYearRangeStart((s) => s + 12);
1128
+ }
1111
1129
  };
1112
1130
  const handleToday = () => {
1113
1131
  goToToday();
1132
+ setPickerMode("days");
1114
1133
  const today = /* @__PURE__ */ new Date();
1115
1134
  onMonthChange?.(today.getFullYear(), today.getMonth());
1116
1135
  };
1136
+ const handleTitleClick = () => {
1137
+ if (pickerMode === "days") setPickerMode("months");
1138
+ else if (pickerMode === "months") {
1139
+ setYearRangeStart(Math.floor(year / 12) * 12);
1140
+ setPickerMode("years");
1141
+ }
1142
+ };
1143
+ const handleMonthSelect = (m) => {
1144
+ setMonth(m);
1145
+ setPickerMode("days");
1146
+ onMonthChange?.(year, m);
1147
+ };
1148
+ const handleYearSelect = (y) => {
1149
+ setYear(y);
1150
+ setPickerMode("months");
1151
+ onMonthChange?.(y, month);
1152
+ };
1117
1153
  const isDisabled = (date) => {
1118
1154
  if (minDate && date < new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate())) return true;
1119
1155
  if (maxDate && date > new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate())) return true;
@@ -1121,6 +1157,8 @@ var Calendar = (props) => {
1121
1157
  };
1122
1158
  const getEventsForDay = (date) => events.filter((e) => isSameDay(e.date, date));
1123
1159
  const weekdays = WEEKDAY_LABELS[locale];
1160
+ const monthLabels = MONTH_LABELS[locale];
1161
+ const titleText = pickerMode === "days" ? locale === "ko" ? `${year}\uB144 ${monthLabels[month]}` : `${monthLabels[month]} ${year}` : pickerMode === "months" ? `${year}` : `${yearRangeStart} - ${yearRangeStart + 11}`;
1124
1162
  return /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)(
1125
1163
  "div",
1126
1164
  {
@@ -1128,29 +1166,71 @@ var Calendar = (props) => {
1128
1166
  style: selectedColor ? { "--calendar-selected-color": `var(--${selectedColor})` } : void 0,
1129
1167
  children: [
1130
1168
  /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)("div", { className: "calendar-header", children: [
1131
- /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("button", { className: "calendar-nav", onClick: handlePrev, "aria-label": "\uC774\uC804 \uB2EC", children: /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(ChevronLeftIcon_default, {}) }),
1132
- /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("span", { className: "calendar-title", children: locale === "ko" ? `${year}\uB144 ${MONTH_LABELS.ko[month]}` : `${MONTH_LABELS.en[month]} ${year}` }),
1133
- /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("button", { className: "calendar-nav", onClick: handleNext, "aria-label": "\uB2E4\uC74C \uB2EC", children: /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(ChevronRightIcon_default, {}) }),
1169
+ /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("button", { className: "calendar-nav", onClick: handlePrev, "aria-label": "\uC774\uC804", children: /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(ChevronLeftIcon_default, {}) }),
1170
+ /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("button", { className: "calendar-title", onClick: handleTitleClick, type: "button", children: titleText }),
1171
+ /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("button", { className: "calendar-nav", onClick: handleNext, "aria-label": "\uB2E4\uC74C", children: /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(ChevronRightIcon_default, {}) }),
1134
1172
  showToday && /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("button", { className: "calendar-today-btn", onClick: handleToday, children: locale === "ko" ? "\uC624\uB298" : "Today" })
1135
1173
  ] }),
1136
- /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("div", { className: "calendar-weekdays", children: weekdays.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1137
- "div",
1174
+ pickerMode === "years" && /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("div", { className: "calendar-picker-grid", children: Array.from({ length: 12 }, (_, i) => {
1175
+ const y = yearRangeStart + i;
1176
+ return /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1177
+ "button",
1178
+ {
1179
+ type: "button",
1180
+ className: clsx_default("calendar-picker-cell", y === year && "active"),
1181
+ onClick: () => handleYearSelect(y),
1182
+ children: y
1183
+ },
1184
+ y
1185
+ );
1186
+ }) }),
1187
+ pickerMode === "months" && /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("div", { className: "calendar-picker-grid", children: monthLabels.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1188
+ "button",
1138
1189
  {
1139
- className: clsx_default(
1140
- "calendar-weekday",
1141
- i === 0 && "sunday",
1142
- i === 6 && "saturday"
1143
- ),
1190
+ type: "button",
1191
+ className: clsx_default("calendar-picker-cell", i === month && "active"),
1192
+ onClick: () => handleMonthSelect(i),
1144
1193
  children: label
1145
1194
  },
1146
- label
1195
+ i
1147
1196
  )) }),
1148
- /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("div", { className: "calendar-grid", children: days.map((day, idx) => {
1149
- const dayEvents = getEventsForDay(day.date);
1150
- const disabled = isDisabled(day.date);
1151
- const isSelected = selectedDate ? isSameDay(day.date, selectedDate) : false;
1152
- if (renderDay) {
1153
- return /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1197
+ pickerMode === "days" && /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)(import_jsx_runtime295.Fragment, { children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("div", { className: "calendar-weekdays", children: weekdays.map((label, i) => /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1199
+ "div",
1200
+ {
1201
+ className: clsx_default(
1202
+ "calendar-weekday",
1203
+ i === 0 && "sunday",
1204
+ i === 6 && "saturday"
1205
+ ),
1206
+ children: label
1207
+ },
1208
+ label
1209
+ )) }),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("div", { className: "calendar-grid", children: days.map((day, idx) => {
1211
+ const dayEvents = getEventsForDay(day.date);
1212
+ const disabled = isDisabled(day.date);
1213
+ const isSelected = selectedDate ? isSameDay(day.date, selectedDate) : false;
1214
+ if (renderDay) {
1215
+ return /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1216
+ "div",
1217
+ {
1218
+ className: clsx_default(
1219
+ "calendar-day",
1220
+ !day.isCurrentMonth && "outside",
1221
+ disabled && "disabled",
1222
+ isSelected && "selected",
1223
+ day.isToday && "today"
1224
+ ),
1225
+ onClick: () => {
1226
+ if (!disabled && day.isCurrentMonth) onSelect?.(day.date);
1227
+ },
1228
+ children: renderDay(day, dayEvents)
1229
+ },
1230
+ idx
1231
+ );
1232
+ }
1233
+ return /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)(
1154
1234
  "div",
1155
1235
  {
1156
1236
  className: clsx_default(
@@ -1158,57 +1238,40 @@ var Calendar = (props) => {
1158
1238
  !day.isCurrentMonth && "outside",
1159
1239
  disabled && "disabled",
1160
1240
  isSelected && "selected",
1161
- day.isToday && "today"
1241
+ day.isToday && "today",
1242
+ day.isSunday && "sunday",
1243
+ day.isSaturday && "saturday"
1162
1244
  ),
1163
1245
  onClick: () => {
1164
1246
  if (!disabled && day.isCurrentMonth) onSelect?.(day.date);
1165
1247
  },
1166
- children: renderDay(day, dayEvents)
1248
+ children: [
1249
+ /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("span", { className: "calendar-day-number", children: day.day }),
1250
+ dayEvents.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)("div", { className: "calendar-day-events", children: [
1251
+ dayEvents.slice(0, 3).map((event, ei) => /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1252
+ "span",
1253
+ {
1254
+ className: "calendar-event-dot",
1255
+ style: { backgroundColor: event.color ?? "var(--xplat-blue-500)" },
1256
+ title: event.label,
1257
+ onClick: (e) => {
1258
+ e.stopPropagation();
1259
+ onEventClick?.(event);
1260
+ }
1261
+ },
1262
+ ei
1263
+ )),
1264
+ dayEvents.length > 3 && /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)("span", { className: "calendar-event-more", children: [
1265
+ "+",
1266
+ dayEvents.length - 3
1267
+ ] })
1268
+ ] })
1269
+ ]
1167
1270
  },
1168
1271
  idx
1169
1272
  );
1170
- }
1171
- return /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)(
1172
- "div",
1173
- {
1174
- className: clsx_default(
1175
- "calendar-day",
1176
- !day.isCurrentMonth && "outside",
1177
- disabled && "disabled",
1178
- isSelected && "selected",
1179
- day.isToday && "today",
1180
- day.isSunday && "sunday",
1181
- day.isSaturday && "saturday"
1182
- ),
1183
- onClick: () => {
1184
- if (!disabled && day.isCurrentMonth) onSelect?.(day.date);
1185
- },
1186
- children: [
1187
- /* @__PURE__ */ (0, import_jsx_runtime295.jsx)("span", { className: "calendar-day-number", children: day.day }),
1188
- dayEvents.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)("div", { className: "calendar-day-events", children: [
1189
- dayEvents.slice(0, 3).map((event, ei) => /* @__PURE__ */ (0, import_jsx_runtime295.jsx)(
1190
- "span",
1191
- {
1192
- className: "calendar-event-dot",
1193
- style: { backgroundColor: event.color ?? "var(--xplat-blue-500)" },
1194
- title: event.label,
1195
- onClick: (e) => {
1196
- e.stopPropagation();
1197
- onEventClick?.(event);
1198
- }
1199
- },
1200
- ei
1201
- )),
1202
- dayEvents.length > 3 && /* @__PURE__ */ (0, import_jsx_runtime295.jsxs)("span", { className: "calendar-event-more", children: [
1203
- "+",
1204
- dayEvents.length - 3
1205
- ] })
1206
- ] })
1207
- ]
1208
- },
1209
- idx
1210
- );
1211
- }) })
1273
+ }) })
1274
+ ] })
1212
1275
  ]
1213
1276
  }
1214
1277
  );
@@ -1,7 +1,7 @@
1
1
  /* src/components/Calendar/calendar.scss */
2
2
  .lib-xplat-calendar {
3
3
  width: 100%;
4
- min-width: 280px;
4
+ min-width: 200px;
5
5
  user-select: none;
6
6
  container-type: inline-size;
7
7
  }
@@ -17,6 +17,15 @@
17
17
  color: var(--xplat-neutral-900);
18
18
  min-width: 120px;
19
19
  text-align: center;
20
+ background: none;
21
+ border: none;
22
+ cursor: pointer;
23
+ padding: 0.25rem 0.5rem;
24
+ border-radius: 0.375rem;
25
+ transition: background-color 0.15s;
26
+ }
27
+ .lib-xplat-calendar .calendar-title:hover {
28
+ background-color: var(--xplat-neutral-100);
20
29
  }
21
30
  .lib-xplat-calendar .calendar-nav {
22
31
  display: flex;
@@ -106,33 +115,17 @@
106
115
  color: var(--xplat-neutral-300);
107
116
  }
108
117
  .lib-xplat-calendar .calendar-day.today .calendar-day-number {
109
- position: relative;
110
118
  font-weight: 700;
111
119
  color: var(--xplat-blue-600);
112
120
  }
113
- .lib-xplat-calendar .calendar-day.today .calendar-day-number::before {
114
- content: "";
115
- position: absolute;
116
- top: 50%;
117
- left: -6px;
118
- transform: translateY(-50%);
119
- width: 4px;
120
- height: 4px;
121
- border-radius: 50%;
122
- background-color: var(--xplat-blue-600);
123
- }
124
121
  .lib-xplat-calendar .calendar-day.selected {
125
- background-color: var(--calendar-selected-color, var(--xplat-neutral-900));
122
+ box-shadow: inset 0 0 0 1.5px var(--calendar-selected-color, var(--xplat-neutral-500));
126
123
  }
127
124
  .lib-xplat-calendar .calendar-day.selected .calendar-day-number {
128
- color: var(--xplat-white);
129
- font-weight: 600;
125
+ font-weight: 700;
130
126
  }
131
127
  .lib-xplat-calendar .calendar-day.selected:hover {
132
- background-color: color-mix(in srgb, var(--calendar-selected-color, var(--xplat-neutral-900)), black 12%);
133
- }
134
- .lib-xplat-calendar .calendar-day.selected.today .calendar-day-number::before {
135
- background-color: var(--xplat-white);
128
+ background-color: color-mix(in srgb, var(--calendar-selected-color, var(--xplat-neutral-500)) 10%, transparent);
136
129
  }
137
130
  .lib-xplat-calendar .calendar-day.sunday:not(.outside) .calendar-day-number {
138
131
  color: var(--xplat-red-500);
@@ -140,9 +133,6 @@
140
133
  .lib-xplat-calendar .calendar-day.saturday:not(.outside) .calendar-day-number {
141
134
  color: var(--xplat-blue-500);
142
135
  }
143
- .lib-xplat-calendar .calendar-day.selected .calendar-day-number {
144
- color: var(--xplat-white);
145
- }
146
136
  .lib-xplat-calendar .calendar-day-number {
147
137
  font-size: clamp(0.75rem, 2.5cqi, 1rem);
148
138
  line-height: 1;
@@ -166,3 +156,30 @@
166
156
  color: var(--xplat-neutral-400);
167
157
  line-height: 1;
168
158
  }
159
+ .lib-xplat-calendar .calendar-picker-grid {
160
+ display: grid;
161
+ grid-template-columns: repeat(3, 1fr);
162
+ gap: 0.5rem;
163
+ padding: 0.5rem 0;
164
+ }
165
+ .lib-xplat-calendar .calendar-picker-cell {
166
+ display: flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ padding: 0.5rem;
170
+ border: none;
171
+ border-radius: 0.375rem;
172
+ background: none;
173
+ font-size: clamp(0.75rem, 2.5cqi, 0.9375rem);
174
+ color: var(--xplat-neutral-700);
175
+ cursor: pointer;
176
+ transition: background-color 0.15s;
177
+ }
178
+ .lib-xplat-calendar .calendar-picker-cell:hover {
179
+ background-color: var(--xplat-neutral-100);
180
+ }
181
+ .lib-xplat-calendar .calendar-picker-cell.active {
182
+ background-color: var(--xplat-neutral-900);
183
+ color: var(--xplat-white);
184
+ font-weight: 600;
185
+ }
@@ -29,31 +29,18 @@ interface CalendarEvent {
29
29
  data?: unknown;
30
30
  }
31
31
  interface CalendarProps {
32
- /** 초기 연도 */
33
32
  year?: number;
34
- /** 초기 월 (0-11) */
35
33
  month?: number;
36
- /** 선택된 날짜 */
37
34
  selectedDate?: Date | null;
38
- /** 날짜 선택 콜백 */
39
35
  onSelect?: (date: Date) => void;
40
- /** 월 변경 콜백 */
41
36
  onMonthChange?: (year: number, month: number) => void;
42
- /** 이벤트 목록 */
43
37
  events?: CalendarEvent[];
44
- /** 날짜별 커스텀 렌더 */
45
38
  renderDay?: (day: CalendarDay, events: CalendarEvent[]) => React.ReactNode;
46
- /** 이벤트 클릭 콜백 */
47
39
  onEventClick?: (event: CalendarEvent) => void;
48
- /** 언어 */
49
40
  locale?: "ko" | "en";
50
- /** 최소 선택 가능 날짜 */
51
41
  minDate?: Date;
52
- /** 최대 선택 가능 날짜 */
53
42
  maxDate?: Date;
54
- /** 선택 날짜 색상 (CSS 변수명, 예: "xplat-blue-500") */
55
43
  selectedColor?: string;
56
- /** 오늘 버튼 표시 */
57
44
  showToday?: boolean;
58
45
  className?: string;
59
46
  }
@@ -29,31 +29,18 @@ interface CalendarEvent {
29
29
  data?: unknown;
30
30
  }
31
31
  interface CalendarProps {
32
- /** 초기 연도 */
33
32
  year?: number;
34
- /** 초기 월 (0-11) */
35
33
  month?: number;
36
- /** 선택된 날짜 */
37
34
  selectedDate?: Date | null;
38
- /** 날짜 선택 콜백 */
39
35
  onSelect?: (date: Date) => void;
40
- /** 월 변경 콜백 */
41
36
  onMonthChange?: (year: number, month: number) => void;
42
- /** 이벤트 목록 */
43
37
  events?: CalendarEvent[];
44
- /** 날짜별 커스텀 렌더 */
45
38
  renderDay?: (day: CalendarDay, events: CalendarEvent[]) => React.ReactNode;
46
- /** 이벤트 클릭 콜백 */
47
39
  onEventClick?: (event: CalendarEvent) => void;
48
- /** 언어 */
49
40
  locale?: "ko" | "en";
50
- /** 최소 선택 가능 날짜 */
51
41
  minDate?: Date;
52
- /** 최대 선택 가능 날짜 */
53
42
  maxDate?: Date;
54
- /** 선택 날짜 색상 (CSS 변수명, 예: "xplat-blue-500") */
55
43
  selectedColor?: string;
56
- /** 오늘 버튼 표시 */
57
44
  showToday?: boolean;
58
45
  className?: string;
59
46
  }