sh-ui-cli 0.43.0 → 0.45.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.
Files changed (46) hide show
  1. package/data/changelog/versions.json +24 -0
  2. package/data/registry/react/components/accordion/index.tailwind.tsx +88 -0
  3. package/data/registry/react/components/avatar/index.tailwind.tsx +74 -0
  4. package/data/registry/react/components/badge/index.tailwind.tsx +47 -0
  5. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +138 -0
  6. package/data/registry/react/components/calendar/index.tailwind.tsx +498 -0
  7. package/data/registry/react/components/carousel/index.tailwind.tsx +309 -0
  8. package/data/registry/react/components/checkbox/index.tailwind.tsx +72 -0
  9. package/data/registry/react/components/code-editor/index.tailwind.tsx +168 -0
  10. package/data/registry/react/components/code-panel/index.tailwind.tsx +107 -0
  11. package/data/registry/react/components/color-picker/index.tailwind.tsx +309 -0
  12. package/data/registry/react/components/combobox/index.tailwind.tsx +160 -0
  13. package/data/registry/react/components/context-menu/index.tailwind.tsx +170 -0
  14. package/data/registry/react/components/date-picker/index.tailwind.tsx +294 -0
  15. package/data/registry/react/components/dialog/index.tailwind.tsx +96 -0
  16. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +205 -0
  17. package/data/registry/react/components/file-upload/index.tailwind.tsx +290 -0
  18. package/data/registry/react/components/form/field.tailwind.tsx +165 -0
  19. package/data/registry/react/components/form/form.tailwind.tsx +129 -0
  20. package/data/registry/react/components/form/index.tailwind.tsx +49 -0
  21. package/data/registry/react/components/header/index.tailwind.tsx +550 -0
  22. package/data/registry/react/components/label/index.tailwind.tsx +78 -0
  23. package/data/registry/react/components/markdown-editor/index.tailwind.tsx +118 -0
  24. package/data/registry/react/components/menubar/index.tailwind.tsx +32 -0
  25. package/data/registry/react/components/numeric-input/index.tailwind.tsx +113 -0
  26. package/data/registry/react/components/page-toc/index.tailwind.tsx +149 -0
  27. package/data/registry/react/components/pagination/index.tailwind.tsx +148 -0
  28. package/data/registry/react/components/popover/index.tailwind.tsx +77 -0
  29. package/data/registry/react/components/progress/index.tailwind.tsx +60 -0
  30. package/data/registry/react/components/radio/index.tailwind.tsx +54 -0
  31. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +211 -0
  32. package/data/registry/react/components/select/index.tailwind.tsx +199 -0
  33. package/data/registry/react/components/separator/index.tailwind.tsx +42 -0
  34. package/data/registry/react/components/sidebar/index.tailwind.tsx +635 -0
  35. package/data/registry/react/components/skeleton/index.tailwind.tsx +39 -0
  36. package/data/registry/react/components/slider/index.tailwind.tsx +255 -0
  37. package/data/registry/react/components/spinner/index.tailwind.tsx +63 -0
  38. package/data/registry/react/components/switch/index.tailwind.tsx +62 -0
  39. package/data/registry/react/components/tabs/index.tailwind.tsx +113 -0
  40. package/data/registry/react/components/textarea/index.tailwind.tsx +21 -0
  41. package/data/registry/react/components/toast/index.tailwind.tsx +215 -0
  42. package/data/registry/react/components/toggle/index.tailwind.tsx +111 -0
  43. package/data/registry/react/components/tooltip/index.tailwind.tsx +55 -0
  44. package/data/registry/react/registry.json +696 -98
  45. package/package.json +1 -1
  46. package/src/mcp.mjs +1 -1
@@ -0,0 +1,498 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Select, SelectContent, SelectItem, SelectTrigger } from "../select";
5
+
6
+ function cx(...args: (string | undefined | false)[]) {
7
+ return args.filter(Boolean).join(" ");
8
+ }
9
+
10
+ const DEFAULT_WEEKDAYS_KO = ["일", "월", "화", "수", "목", "금", "토"] as const;
11
+
12
+ const isSameDay = (a: Date, b: Date) =>
13
+ a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
14
+ const toDateOnly = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
15
+ const startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);
16
+ const addMonths = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth() + n, 1);
17
+ const formatIsoDate = (d: Date) =>
18
+ `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
19
+ const defaultMonthLabel = (year: number, month: number) => `${year}년 ${month + 1}월`;
20
+
21
+ function getDaysGrid(year: number, month: number, weekStartsOn: 0 | 1) {
22
+ const first = new Date(year, month, 1);
23
+ const startDay = (first.getDay() - weekStartsOn + 7) % 7;
24
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
25
+ const prevDays = new Date(year, month, 0).getDate();
26
+ const cells: { date: Date; current: boolean }[] = [];
27
+ for (let i = startDay - 1; i >= 0; i--) cells.push({ date: new Date(year, month - 1, prevDays - i), current: false });
28
+ for (let d = 1; d <= daysInMonth; d++) cells.push({ date: new Date(year, month, d), current: true });
29
+ const remaining = 7 - (cells.length % 7);
30
+ if (remaining < 7) {
31
+ for (let d = 1; d <= remaining; d++) cells.push({ date: new Date(year, month + 1, d), current: false });
32
+ }
33
+ return cells;
34
+ }
35
+
36
+ function rotateWeekdays(labels: readonly string[], weekStartsOn: 0 | 1): string[] {
37
+ if (weekStartsOn === 0) return [...labels];
38
+ return [...labels.slice(weekStartsOn), ...labels.slice(0, weekStartsOn)];
39
+ }
40
+
41
+ export interface DateRange { from: Date; to: Date; }
42
+ export type CalendarMode = "single" | "multiple" | "range";
43
+
44
+ interface CalendarCommonProps {
45
+ month?: Date; defaultMonth?: Date;
46
+ onMonthChange?: (month: Date) => void;
47
+ numberOfMonths?: number;
48
+ min?: Date; max?: Date;
49
+ disabled?: (date: Date) => boolean;
50
+ showOutsideDays?: boolean;
51
+ weekStartsOn?: 0 | 1;
52
+ weekdayLabels?: readonly string[];
53
+ formatMonthLabel?: (year: number, month: number) => string;
54
+ fromYear?: number; toYear?: number;
55
+ className?: string;
56
+ "aria-label"?: string;
57
+ children?: React.ReactNode;
58
+ }
59
+
60
+ export type CalendarSingleProps = CalendarCommonProps & {
61
+ mode?: "single"; value?: Date; defaultValue?: Date; onValueChange?: (date: Date | undefined) => void;
62
+ };
63
+ export type CalendarMultipleProps = CalendarCommonProps & {
64
+ mode: "multiple"; value?: Date[]; defaultValue?: Date[]; onValueChange?: (dates: Date[]) => void;
65
+ };
66
+ export type CalendarRangeProps = CalendarCommonProps & {
67
+ mode: "range"; value?: DateRange; defaultValue?: DateRange; onValueChange?: (range: DateRange | undefined) => void;
68
+ };
69
+ export type CalendarProps = CalendarSingleProps | CalendarMultipleProps | CalendarRangeProps;
70
+
71
+ function ChevronLeftIcon() {
72
+ return <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden><path d="M10 3 5.5 8 10 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>;
73
+ }
74
+ function ChevronRightIcon() {
75
+ return <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden><path d="M6 3 10.5 8 6 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>;
76
+ }
77
+ function ChevronDoubleLeftIcon() {
78
+ return <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden><path d="M8 3 3.5 8 8 13M13 3 8.5 8 13 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>;
79
+ }
80
+ function ChevronDoubleRightIcon() {
81
+ return <svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden><path d="M3 3 7.5 8 3 13M8 3 12.5 8 8 13" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>;
82
+ }
83
+
84
+ interface CalendarContextValue {
85
+ visibleMonth: Date;
86
+ monthIndex: number;
87
+ monthsLength: number;
88
+ yearOptions: number[];
89
+ setYearForVisible: (y: number) => void;
90
+ setMonthForVisible: (m: number) => void;
91
+ prevMonth: () => void; nextMonth: () => void;
92
+ prevYear: () => void; nextYear: () => void;
93
+ weekStartsOn: 0 | 1;
94
+ weekdayLabels: string[];
95
+ showOutsideDays: boolean;
96
+ formatMonthLabel: (year: number, month: number) => string;
97
+ ariaLabel?: string;
98
+ isSelected: (date: Date) => boolean;
99
+ isInRange: (date: Date) => { inRange: boolean; isStart: boolean; isEnd: boolean };
100
+ isDisabled: (date: Date) => boolean;
101
+ handleSelect: (date: Date) => void;
102
+ setHoverDate: (date: Date | undefined) => void;
103
+ onKeyDown: (e: React.KeyboardEvent) => void;
104
+ isFirstMonth: boolean;
105
+ isLastMonth: boolean;
106
+ }
107
+
108
+ const CalendarContext = React.createContext<CalendarContextValue | null>(null);
109
+
110
+ function useCalendarContext(component: string) {
111
+ const ctx = React.useContext(CalendarContext);
112
+ if (!ctx) throw new Error(`${component}는 <Calendar> 내부에서 사용해야 합니다.`);
113
+ return ctx;
114
+ }
115
+
116
+ export function Calendar(props: CalendarProps) {
117
+ const {
118
+ mode = "single", month: monthProp, defaultMonth, onMonthChange,
119
+ numberOfMonths: numberOfMonthsProp = 1,
120
+ min, max, disabled, showOutsideDays = true, weekStartsOn = 0,
121
+ weekdayLabels: weekdayLabelsProp,
122
+ formatMonthLabel = defaultMonthLabel,
123
+ fromYear, toYear, className, "aria-label": ariaLabel, children,
124
+ } = props as CalendarCommonProps & { mode?: CalendarMode };
125
+
126
+ const numberOfMonths = children ? 1 : Math.max(1, numberOfMonthsProp);
127
+
128
+ const isControlled = "value" in props && props.value !== undefined;
129
+ const [internalSingle, setInternalSingle] = React.useState<Date | undefined>(
130
+ mode === "single" ? (props as CalendarSingleProps).defaultValue : undefined,
131
+ );
132
+ const [internalMultiple, setInternalMultiple] = React.useState<Date[]>(
133
+ mode === "multiple" ? (props as CalendarMultipleProps).defaultValue ?? [] : [],
134
+ );
135
+ const [internalRange, setInternalRange] = React.useState<DateRange | undefined>(
136
+ mode === "range" ? (props as CalendarRangeProps).defaultValue : undefined,
137
+ );
138
+
139
+ const singleValue = isControlled ? (props as CalendarSingleProps).value : internalSingle;
140
+ const multipleValue = isControlled ? (props as CalendarMultipleProps).value ?? [] : internalMultiple;
141
+ const rangeValue = isControlled ? (props as CalendarRangeProps).value : internalRange;
142
+
143
+ const [picking, setPicking] = React.useState<Date | undefined>(undefined);
144
+ const [hoverDate, setHoverDate] = React.useState<Date | undefined>(undefined);
145
+
146
+ const monthControlled = monthProp !== undefined;
147
+ const [internalMonth, setInternalMonth] = React.useState<Date>(() => {
148
+ if (defaultMonth) return startOfMonth(defaultMonth);
149
+ if (mode === "single" && singleValue) return startOfMonth(singleValue);
150
+ if (mode === "multiple" && multipleValue.length > 0) return startOfMonth(multipleValue[0]);
151
+ if (mode === "range" && rangeValue?.from) return startOfMonth(rangeValue.from);
152
+ return startOfMonth(new Date());
153
+ });
154
+ const currentMonth = monthControlled ? startOfMonth(monthProp!) : internalMonth;
155
+
156
+ const setMonth = React.useCallback((next: Date) => {
157
+ const normalized = startOfMonth(next);
158
+ if (!monthControlled) setInternalMonth(normalized);
159
+ onMonthChange?.(normalized);
160
+ }, [monthControlled, onMonthChange]);
161
+
162
+ const [focusedDate, setFocusedDate] = React.useState<Date>(() => {
163
+ if (mode === "single" && singleValue) return singleValue;
164
+ if (mode === "range" && rangeValue?.from) return rangeValue.from;
165
+ return new Date();
166
+ });
167
+
168
+ const weekdayLabels = React.useMemo(() => {
169
+ const base = weekdayLabelsProp ?? DEFAULT_WEEKDAYS_KO;
170
+ return rotateWeekdays(base, weekStartsOn);
171
+ }, [weekdayLabelsProp, weekStartsOn]);
172
+
173
+ const nowYear = new Date().getFullYear();
174
+ const resolvedFromYear = fromYear ?? min?.getFullYear() ?? nowYear - 10;
175
+ const resolvedToYear = toYear ?? max?.getFullYear() ?? nowYear + 10;
176
+ const yearOptions = React.useMemo(() => {
177
+ const out: number[] = [];
178
+ const start = Math.min(resolvedFromYear, resolvedToYear);
179
+ const end = Math.max(resolvedFromYear, resolvedToYear);
180
+ for (let y = start; y <= end; y++) out.push(y);
181
+ return out;
182
+ }, [resolvedFromYear, resolvedToYear]);
183
+
184
+ const isDateDisabled = React.useCallback((date: Date) => {
185
+ const d = toDateOnly(date);
186
+ if (min && d < toDateOnly(min)) return true;
187
+ if (max && d > toDateOnly(max)) return true;
188
+ if (disabled?.(date)) return true;
189
+ return false;
190
+ }, [min, max, disabled]);
191
+
192
+ const isDateSelected = React.useCallback((date: Date) => {
193
+ if (mode === "single") return !!singleValue && isSameDay(date, singleValue);
194
+ if (mode === "multiple") return multipleValue.some((d) => isSameDay(d, date));
195
+ if (mode === "range") {
196
+ if (picking) return isSameDay(date, picking);
197
+ if (rangeValue?.from && isSameDay(date, rangeValue.from)) return true;
198
+ if (rangeValue?.to && isSameDay(date, rangeValue.to)) return true;
199
+ return false;
200
+ }
201
+ return false;
202
+ }, [mode, singleValue, multipleValue, rangeValue, picking]);
203
+
204
+ const getRangeState = React.useCallback((date: Date) => {
205
+ if (mode !== "range") return { inRange: false, isStart: false, isEnd: false };
206
+ const from = picking ?? rangeValue?.from;
207
+ if (!from) return { inRange: false, isStart: false, isEnd: false };
208
+ const to = picking ? hoverDate : rangeValue?.to;
209
+ if (!to) return { inRange: false, isStart: isSameDay(date, from), isEnd: false };
210
+ const [rStart, rEnd] = from <= to ? [from, to] : [to, from];
211
+ const s = toDateOnly(rStart);
212
+ const e = toDateOnly(rEnd);
213
+ const d = toDateOnly(date);
214
+ return { inRange: d >= s && d <= e, isStart: isSameDay(d, s), isEnd: isSameDay(d, e) };
215
+ }, [mode, picking, hoverDate, rangeValue]);
216
+
217
+ const handleSelect = React.useCallback((date: Date) => {
218
+ if (mode === "single") {
219
+ if (!isControlled) setInternalSingle(date);
220
+ (props as CalendarSingleProps).onValueChange?.(date);
221
+ return;
222
+ }
223
+ if (mode === "multiple") {
224
+ const exists = multipleValue.some((d) => isSameDay(d, date));
225
+ const next = exists ? multipleValue.filter((d) => !isSameDay(d, date)) : [...multipleValue, date];
226
+ if (!isControlled) setInternalMultiple(next);
227
+ (props as CalendarMultipleProps).onValueChange?.(next);
228
+ return;
229
+ }
230
+ if (mode === "range") {
231
+ if (!picking) { setPicking(date); return; }
232
+ const [from, to] = picking <= date ? [picking, date] : [date, picking];
233
+ const range: DateRange = { from, to };
234
+ setPicking(undefined); setHoverDate(undefined);
235
+ if (!isControlled) setInternalRange(range);
236
+ (props as CalendarRangeProps).onValueChange?.(range);
237
+ }
238
+ }, [mode, isControlled, multipleValue, picking, props]);
239
+
240
+ const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => {
241
+ let next: Date | null = null;
242
+ const cursor = focusedDate;
243
+ switch (e.key) {
244
+ case "ArrowLeft": next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - 1); break;
245
+ case "ArrowRight": next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 1); break;
246
+ case "ArrowUp": next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() - 7); break;
247
+ case "ArrowDown": next = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate() + 7); break;
248
+ case "PageUp": next = new Date(cursor.getFullYear(), cursor.getMonth() - 1, cursor.getDate()); break;
249
+ case "PageDown": next = new Date(cursor.getFullYear(), cursor.getMonth() + 1, cursor.getDate()); break;
250
+ case "Home": next = new Date(cursor.getFullYear(), cursor.getMonth(), 1); break;
251
+ case "End": next = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0); break;
252
+ case "Enter":
253
+ case " ":
254
+ e.preventDefault();
255
+ if (!isDateDisabled(cursor)) handleSelect(cursor);
256
+ return;
257
+ default: return;
258
+ }
259
+ e.preventDefault();
260
+ if (!next || isDateDisabled(next)) return;
261
+ setFocusedDate(next);
262
+ const visibleEnd = addMonths(currentMonth, numberOfMonths - 1);
263
+ if (next < currentMonth) setMonth(addMonths(currentMonth, -1));
264
+ else if (next > new Date(visibleEnd.getFullYear(), visibleEnd.getMonth() + 1, 0)) setMonth(addMonths(currentMonth, 1));
265
+ }, [focusedDate, isDateDisabled, handleSelect, currentMonth, numberOfMonths, setMonth]);
266
+
267
+ const months = Array.from({ length: numberOfMonths }, (_, i) => addMonths(currentMonth, i));
268
+
269
+ const buildMonthContext = (visibleMonth: Date, idx: number): CalendarContextValue => ({
270
+ visibleMonth, monthIndex: idx, monthsLength: numberOfMonths, yearOptions,
271
+ setYearForVisible: (y) => {
272
+ const yearDiff = y - visibleMonth.getFullYear();
273
+ setMonth(new Date(currentMonth.getFullYear() + yearDiff, currentMonth.getMonth(), 1));
274
+ },
275
+ setMonthForVisible: (m) => setMonth(addMonths(currentMonth, m - visibleMonth.getMonth())),
276
+ prevMonth: () => setMonth(addMonths(currentMonth, -1)),
277
+ nextMonth: () => setMonth(addMonths(currentMonth, 1)),
278
+ prevYear: () => setMonth(addMonths(currentMonth, -12)),
279
+ nextYear: () => setMonth(addMonths(currentMonth, 12)),
280
+ weekStartsOn, weekdayLabels, showOutsideDays, formatMonthLabel, ariaLabel,
281
+ isSelected: isDateSelected, isInRange: getRangeState, isDisabled: isDateDisabled,
282
+ handleSelect,
283
+ setHoverDate: mode === "range" ? setHoverDate : () => {},
284
+ onKeyDown: handleKeyDown,
285
+ isFirstMonth: idx === 0, isLastMonth: idx === numberOfMonths - 1,
286
+ });
287
+
288
+ return (
289
+ <div
290
+ className={cx("inline-flex gap-[var(--space-4)] select-none", numberOfMonths > 1 && "flex-wrap", className)}
291
+ aria-label={ariaLabel}
292
+ >
293
+ {children
294
+ ? <CalendarContext.Provider value={buildMonthContext(months[0], 0)}>{children}</CalendarContext.Provider>
295
+ : months.map((m, idx) => (
296
+ <CalendarContext.Provider key={`${m.getFullYear()}-${m.getMonth()}`} value={buildMonthContext(m, idx)}>
297
+ <div className="w-[17.5rem]">
298
+ <CalendarHeader>
299
+ {idx === 0 ? <CalendarPrevYearButton /> : <CalendarNavPlaceholder />}
300
+ {idx === 0 ? <CalendarPrevMonthButton /> : <CalendarNavPlaceholder />}
301
+ <div className="inline-flex items-center gap-[var(--space-1)] flex-1 justify-center">
302
+ <CalendarYearSelect />
303
+ <CalendarMonthSelect />
304
+ </div>
305
+ {idx === months.length - 1 ? <CalendarNextMonthButton /> : <CalendarNavPlaceholder />}
306
+ {idx === months.length - 1 ? <CalendarNextYearButton /> : <CalendarNavPlaceholder />}
307
+ </CalendarHeader>
308
+ <CalendarGrid />
309
+ </div>
310
+ </CalendarContext.Provider>
311
+ ))}
312
+ </div>
313
+ );
314
+ }
315
+
316
+ export interface CalendarHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
317
+ export const CalendarHeader = React.forwardRef<HTMLDivElement, CalendarHeaderProps>(
318
+ function CalendarHeader({ className, ...props }, ref) {
319
+ return <div ref={ref} className={cx("flex items-center justify-between gap-[var(--space-1)] mb-[var(--space-2)]", className)} {...props} />;
320
+ },
321
+ );
322
+
323
+ const navButtonClasses =
324
+ "inline-flex items-center justify-center w-7 h-7 p-0 border-none rounded-[calc(var(--radius)-2px)] bg-transparent text-foreground-muted cursor-pointer shrink-0 transition-[background-color,color] duration-[var(--duration-fast)] hover:not-disabled:bg-background-muted hover:not-disabled:text-foreground focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 motion-reduce:transition-none";
325
+
326
+ function CalendarNavPlaceholder() {
327
+ return <span className={cx(navButtonClasses, "invisible pointer-events-none")} aria-hidden />;
328
+ }
329
+
330
+ export interface CalendarNavButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
331
+ children?: React.ReactNode;
332
+ }
333
+
334
+ function makeNavButton(
335
+ displayName: string,
336
+ defaultIcon: React.ReactNode,
337
+ defaultLabel: string,
338
+ resolveHandler: (ctx: CalendarContextValue) => () => void,
339
+ ) {
340
+ const Component = React.forwardRef<HTMLButtonElement, CalendarNavButtonProps>(
341
+ function NavButton({ className, children, "aria-label": ariaLabel, onClick, ...props }, ref) {
342
+ const ctx = useCalendarContext(displayName);
343
+ return (
344
+ <button
345
+ ref={ref}
346
+ type="button"
347
+ className={cx(navButtonClasses, className)}
348
+ aria-label={ariaLabel ?? defaultLabel}
349
+ onClick={(e) => { resolveHandler(ctx)(); onClick?.(e); }}
350
+ {...props}
351
+ >
352
+ {children ?? defaultIcon}
353
+ </button>
354
+ );
355
+ },
356
+ );
357
+ Component.displayName = displayName;
358
+ return Component;
359
+ }
360
+
361
+ export const CalendarPrevYearButton = makeNavButton("CalendarPrevYearButton", <ChevronDoubleLeftIcon />, "이전 해", (ctx) => ctx.prevYear);
362
+ export const CalendarNextYearButton = makeNavButton("CalendarNextYearButton", <ChevronDoubleRightIcon />, "다음 해", (ctx) => ctx.nextYear);
363
+ export const CalendarPrevMonthButton = makeNavButton("CalendarPrevMonthButton", <ChevronLeftIcon />, "이전 달", (ctx) => ctx.prevMonth);
364
+ export const CalendarNextMonthButton = makeNavButton("CalendarNextMonthButton", <ChevronRightIcon />, "다음 달", (ctx) => ctx.nextMonth);
365
+
366
+ const calendarSelectTriggerClasses =
367
+ "min-w-0 h-7 gap-[var(--space-1)] px-[var(--space-2)] bg-transparent border-transparent font-semibold text-[length:var(--text-sm)] text-foreground hover:not-disabled:bg-background-muted hover:not-disabled:border-transparent data-[popup-open]:bg-background-muted data-[popup-open]:border-transparent";
368
+
369
+ export interface CalendarYearSelectProps {
370
+ className?: string;
371
+ formatYear?: (year: number) => string;
372
+ }
373
+
374
+ export function CalendarYearSelect({ className, formatYear = (y) => `${y}년` }: CalendarYearSelectProps) {
375
+ const ctx = useCalendarContext("CalendarYearSelect");
376
+ const year = ctx.visibleMonth.getFullYear();
377
+ const items = ctx.yearOptions.includes(year) ? ctx.yearOptions : [...ctx.yearOptions, year].sort((a, b) => a - b);
378
+ return (
379
+ <Select value={String(year)} onValueChange={(v) => ctx.setYearForVisible(Number(v))}>
380
+ <SelectTrigger className={cx(calendarSelectTriggerClasses, className)} aria-label="연도">
381
+ <span>{formatYear(year)}</span>
382
+ </SelectTrigger>
383
+ <SelectContent>
384
+ {items.map((y) => <SelectItem key={y} value={String(y)}>{formatYear(y)}</SelectItem>)}
385
+ </SelectContent>
386
+ </Select>
387
+ );
388
+ }
389
+
390
+ export interface CalendarMonthSelectProps {
391
+ className?: string;
392
+ formatMonth?: (month: number) => string;
393
+ }
394
+
395
+ export function CalendarMonthSelect({ className, formatMonth = (m) => `${m + 1}월` }: CalendarMonthSelectProps) {
396
+ const ctx = useCalendarContext("CalendarMonthSelect");
397
+ const month = ctx.visibleMonth.getMonth();
398
+ return (
399
+ <Select value={String(month)} onValueChange={(v) => ctx.setMonthForVisible(Number(v))}>
400
+ <SelectTrigger className={cx(calendarSelectTriggerClasses, className)} aria-label="월">
401
+ <span>{formatMonth(month)}</span>
402
+ </SelectTrigger>
403
+ <SelectContent>
404
+ {Array.from({ length: 12 }, (_, m) => <SelectItem key={m} value={String(m)}>{formatMonth(m)}</SelectItem>)}
405
+ </SelectContent>
406
+ </Select>
407
+ );
408
+ }
409
+
410
+ export interface CalendarGridProps extends React.HTMLAttributes<HTMLDivElement> {}
411
+
412
+ export const CalendarGrid = React.forwardRef<HTMLDivElement, CalendarGridProps>(
413
+ function CalendarGrid({ className, ...rest }, ref) {
414
+ const ctx = useCalendarContext("CalendarGrid");
415
+ const year = ctx.visibleMonth.getFullYear();
416
+ const month = ctx.visibleMonth.getMonth();
417
+ const cells = getDaysGrid(year, month, ctx.weekStartsOn);
418
+ const today = new Date();
419
+ const monthLabel = ctx.formatMonthLabel(year, month);
420
+ const ariaLabel = ctx.ariaLabel ?? monthLabel;
421
+
422
+ return (
423
+ <div ref={ref} className={cx("", className)} {...rest}>
424
+ <div className="grid grid-cols-7 mb-[var(--space-1)]" role="row">
425
+ {ctx.weekdayLabels.map((label) => (
426
+ <span key={label} className="flex items-center justify-center h-8 text-[length:var(--text-xs)] font-medium text-foreground-muted" role="columnheader" aria-label={label}>
427
+ {label}
428
+ </span>
429
+ ))}
430
+ </div>
431
+ <div
432
+ className="grid grid-cols-7 outline-none focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 focus-visible:rounded-[calc(var(--radius)-2px)]"
433
+ role="grid"
434
+ tabIndex={0}
435
+ onKeyDown={ctx.onKeyDown}
436
+ aria-label={ariaLabel}
437
+ >
438
+ {cells.map(({ date, current }, i) => {
439
+ const dDisabled = ctx.isDisabled(date);
440
+ const selected = ctx.isSelected(date);
441
+ const isToday = isSameDay(date, today);
442
+ const { inRange, isStart, isEnd } = ctx.isInRange(date);
443
+ const hidden = !current && !ctx.showOutsideDays;
444
+ if (hidden) return <span key={i} className="flex items-center justify-center w-full h-[2.375rem] min-w-0" aria-hidden />;
445
+
446
+ const cellRangeBg = (inRange || isStart || isEnd) ? "bg-[color-mix(in_srgb,var(--primary)_12%,transparent)]" : "";
447
+ const cellRangeRadius =
448
+ isStart && !isEnd ? "rounded-l-[calc(var(--radius)-2px)]" :
449
+ isEnd && !isStart ? "rounded-r-[calc(var(--radius)-2px)]" :
450
+ isStart && isEnd ? "rounded-[calc(var(--radius)-2px)]" : "";
451
+
452
+ return (
453
+ <div key={i} className={cx("flex items-center justify-center w-full h-[2.375rem] min-w-0", cellRangeBg, cellRangeRadius)}>
454
+ <button
455
+ type="button"
456
+ className={cx(
457
+ "flex items-center justify-center w-9 h-9 p-0 border-none rounded-[calc(var(--radius)-2px)] bg-transparent text-foreground text-[0.8125rem] font-[inherit] cursor-pointer transition-[background-color,color] duration-[var(--duration-fast)] hover:not-disabled:bg-background-muted focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-foreground focus-visible:outline-offset-2 disabled:opacity-30 disabled:cursor-not-allowed motion-reduce:transition-none",
458
+ !current && "text-[var(--foreground-subtle,var(--foreground-muted))] opacity-40",
459
+ isToday && "font-bold underline underline-offset-[0.125rem]",
460
+ selected && "bg-primary text-primary-foreground font-semibold hover:not-disabled:bg-primary-hover hover:not-disabled:text-primary-foreground",
461
+ )}
462
+ disabled={dDisabled}
463
+ tabIndex={-1}
464
+ onClick={() => { if (!dDisabled) ctx.handleSelect(date); }}
465
+ onMouseEnter={() => ctx.setHoverDate(date)}
466
+ onMouseLeave={() => ctx.setHoverDate(undefined)}
467
+ aria-label={formatIsoDate(date)}
468
+ aria-selected={selected || inRange || undefined}
469
+ data-today={isToday || undefined}
470
+ >
471
+ {date.getDate()}
472
+ </button>
473
+ </div>
474
+ );
475
+ })}
476
+ </div>
477
+ </div>
478
+ );
479
+ },
480
+ );
481
+
482
+ export function useCalendar() {
483
+ const ctx = useCalendarContext("useCalendar");
484
+ return {
485
+ visibleMonth: ctx.visibleMonth,
486
+ monthIndex: ctx.monthIndex,
487
+ monthsLength: ctx.monthsLength,
488
+ setYear: ctx.setYearForVisible,
489
+ setMonth: ctx.setMonthForVisible,
490
+ prevMonth: ctx.prevMonth,
491
+ nextMonth: ctx.nextMonth,
492
+ prevYear: ctx.prevYear,
493
+ nextYear: ctx.nextYear,
494
+ yearOptions: ctx.yearOptions,
495
+ isFirstMonth: ctx.isFirstMonth,
496
+ isLastMonth: ctx.isLastMonth,
497
+ };
498
+ }