sh-ui-cli 0.52.3 → 0.55.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/data/changelog/versions.json +41 -0
- package/data/registry/react/components/calendar/index.module.tsx +124 -25
- package/data/registry/react/components/calendar/index.tailwind.tsx +94 -20
- package/data/registry/react/components/calendar/index.tsx +124 -25
- package/data/registry/react/components/date-picker/index.module.tsx +41 -10
- package/data/registry/react/components/date-picker/index.tailwind.tsx +28 -8
- package/data/registry/react/components/date-picker/index.tsx +50 -10
- package/package.json +1 -1
- package/src/create/theme/decode.js +20 -1
- package/src/create/theme/encode.js +24 -0
- package/src/create/theme/inject.js +19 -3
- package/src/mcp.mjs +93 -1
|
@@ -12,8 +12,8 @@ import "./styles.css";
|
|
|
12
12
|
|
|
13
13
|
/* ───────── Helpers ───────── */
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const
|
|
15
|
+
/** 미지정 시의 기본 로케일. 기존 동작(한국어) 보존. */
|
|
16
|
+
export const DEFAULT_LOCALE = "ko-KR";
|
|
17
17
|
|
|
18
18
|
const isSameDay = (a: Date, b: Date) =>
|
|
19
19
|
a.getFullYear() === b.getFullYear() &&
|
|
@@ -32,8 +32,68 @@ const addMonths = (d: Date, n: number) =>
|
|
|
32
32
|
const formatIsoDate = (d: Date) =>
|
|
33
33
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/** Intl 기반 short weekday 라벨, Sunday-first 7개. */
|
|
36
|
+
function deriveWeekdayLabels(locale: string): string[] {
|
|
37
|
+
const fmt = new Intl.DateTimeFormat(locale, { weekday: "short" });
|
|
38
|
+
// 2017-01-01 은 일요일 — 안전한 기준점.
|
|
39
|
+
return Array.from({ length: 7 }, (_, i) =>
|
|
40
|
+
fmt.format(new Date(2017, 0, 1 + i)),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deriveMonthLabel(locale: string) {
|
|
45
|
+
const fmt = new Intl.DateTimeFormat(locale, { year: "numeric", month: "long" });
|
|
46
|
+
return (year: number, month: number) => fmt.format(new Date(year, month, 1));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function deriveYearLabel(locale: string) {
|
|
50
|
+
const fmt = new Intl.DateTimeFormat(locale, { year: "numeric" });
|
|
51
|
+
return (y: number) => fmt.format(new Date(y, 0, 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deriveMonthSelectLabel(locale: string) {
|
|
55
|
+
const fmt = new Intl.DateTimeFormat(locale, { month: "long" });
|
|
56
|
+
return (m: number) => fmt.format(new Date(2017, m, 1));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Calendar 의 비-날짜 텍스트(aria-label 등). 일부만 override 가능. */
|
|
60
|
+
export interface CalendarMessages {
|
|
61
|
+
/** 1년 이전 버튼 aria-label. */
|
|
62
|
+
prevYear?: string;
|
|
63
|
+
/** 1년 다음 버튼 aria-label. */
|
|
64
|
+
nextYear?: string;
|
|
65
|
+
/** 1개월 이전 버튼 aria-label. */
|
|
66
|
+
prevMonth?: string;
|
|
67
|
+
/** 1개월 다음 버튼 aria-label. */
|
|
68
|
+
nextMonth?: string;
|
|
69
|
+
/** 연도 select aria-label. */
|
|
70
|
+
yearSelectLabel?: string;
|
|
71
|
+
/** 월 select aria-label. */
|
|
72
|
+
monthSelectLabel?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MESSAGES_KO: Required<CalendarMessages> = {
|
|
76
|
+
prevYear: "이전 해",
|
|
77
|
+
nextYear: "다음 해",
|
|
78
|
+
prevMonth: "이전 달",
|
|
79
|
+
nextMonth: "다음 달",
|
|
80
|
+
yearSelectLabel: "연도",
|
|
81
|
+
monthSelectLabel: "월",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const MESSAGES_EN: Required<CalendarMessages> = {
|
|
85
|
+
prevYear: "Previous year",
|
|
86
|
+
nextYear: "Next year",
|
|
87
|
+
prevMonth: "Previous month",
|
|
88
|
+
nextMonth: "Next month",
|
|
89
|
+
yearSelectLabel: "Year",
|
|
90
|
+
monthSelectLabel: "Month",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function defaultMessagesFor(locale: string): Required<CalendarMessages> {
|
|
94
|
+
const lang = locale.toLowerCase().split(/[-_]/)[0];
|
|
95
|
+
return lang === "ko" ? MESSAGES_KO : MESSAGES_EN;
|
|
96
|
+
}
|
|
37
97
|
|
|
38
98
|
function getDaysGrid(year: number, month: number, weekStartsOn: 0 | 1) {
|
|
39
99
|
const first = new Date(year, month, 1);
|
|
@@ -94,10 +154,22 @@ interface CalendarCommonProps {
|
|
|
94
154
|
showOutsideDays?: boolean;
|
|
95
155
|
/** 주의 시작 요일. 0=일, 1=월. @default 0 */
|
|
96
156
|
weekStartsOn?: 0 | 1;
|
|
97
|
-
/** 요일 라벨 (Sunday-first 7개). */
|
|
157
|
+
/** 요일 라벨 (Sunday-first 7개). 미지정 시 `locale` 기반 자동 생성. */
|
|
98
158
|
weekdayLabels?: readonly string[];
|
|
99
|
-
/** 월 헤더 그룹의 aria-label 포맷.
|
|
159
|
+
/** 월 헤더 그룹의 aria-label 포맷. 미지정 시 `locale` 기반 자동 생성. */
|
|
100
160
|
formatMonthLabel?: (year: number, month: number) => string;
|
|
161
|
+
/**
|
|
162
|
+
* BCP47 로케일 (예: `"en-US"`, `"ja-JP"`, `"ko-KR"`). 요일·월·연도 라벨이 `Intl.DateTimeFormat` 으로
|
|
163
|
+
* 자동 생성된다. 개별 포맷터(`weekdayLabels`/`formatMonthLabel`/`CalendarYearSelect.formatYear` 등) 가
|
|
164
|
+
* 우선되고, 미지정 항목만 이 로케일에서 파생.
|
|
165
|
+
* @default "ko-KR"
|
|
166
|
+
*/
|
|
167
|
+
locale?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Nav 버튼/select 의 aria-label 등 비-날짜 텍스트 override. 미지정 시 `locale` 의 언어 코드(`ko`/`en`)
|
|
170
|
+
* 기반 기본값. 다른 언어를 쓸 땐 해당 언어 문자열을 직접 넘긴다.
|
|
171
|
+
*/
|
|
172
|
+
messages?: CalendarMessages;
|
|
101
173
|
/** 연도 dropdown 시작 연도. */
|
|
102
174
|
fromYear?: number;
|
|
103
175
|
/** 연도 dropdown 끝 연도. */
|
|
@@ -195,6 +267,11 @@ interface CalendarContextValue {
|
|
|
195
267
|
weekdayLabels: string[];
|
|
196
268
|
showOutsideDays: boolean;
|
|
197
269
|
formatMonthLabel: (year: number, month: number) => string;
|
|
270
|
+
/** YearSelect 가 자체 `formatYear` 를 받지 않을 때 사용할 로케일 기본값. */
|
|
271
|
+
defaultFormatYear: (y: number) => string;
|
|
272
|
+
/** MonthSelect 가 자체 `formatMonth` 를 받지 않을 때 사용할 로케일 기본값. */
|
|
273
|
+
defaultFormatMonth: (m: number) => string;
|
|
274
|
+
messages: Required<CalendarMessages>;
|
|
198
275
|
ariaLabel?: string;
|
|
199
276
|
|
|
200
277
|
isSelected: (date: Date) => boolean;
|
|
@@ -240,7 +317,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
240
317
|
showOutsideDays = true,
|
|
241
318
|
weekStartsOn = 0,
|
|
242
319
|
weekdayLabels: weekdayLabelsProp,
|
|
243
|
-
formatMonthLabel
|
|
320
|
+
formatMonthLabel: formatMonthLabelProp,
|
|
321
|
+
locale = DEFAULT_LOCALE,
|
|
322
|
+
messages: messagesProp,
|
|
244
323
|
fromYear,
|
|
245
324
|
toYear,
|
|
246
325
|
className,
|
|
@@ -304,11 +383,26 @@ export function Calendar(props: CalendarProps) {
|
|
|
304
383
|
return new Date();
|
|
305
384
|
});
|
|
306
385
|
|
|
386
|
+
/* locale-derived defaults */
|
|
387
|
+
const localeWeekdays = React.useMemo(() => deriveWeekdayLabels(locale), [locale]);
|
|
388
|
+
const localeMonthLabel = React.useMemo(() => deriveMonthLabel(locale), [locale]);
|
|
389
|
+
const localeYearLabel = React.useMemo(() => deriveYearLabel(locale), [locale]);
|
|
390
|
+
const localeMonthSelectLabel = React.useMemo(
|
|
391
|
+
() => deriveMonthSelectLabel(locale),
|
|
392
|
+
[locale],
|
|
393
|
+
);
|
|
394
|
+
const resolvedMessages = React.useMemo<Required<CalendarMessages>>(
|
|
395
|
+
() => ({ ...defaultMessagesFor(locale), ...messagesProp }),
|
|
396
|
+
[locale, messagesProp],
|
|
397
|
+
);
|
|
398
|
+
|
|
307
399
|
/* weekday labels */
|
|
308
400
|
const weekdayLabels = React.useMemo(() => {
|
|
309
|
-
const base = weekdayLabelsProp ??
|
|
401
|
+
const base = weekdayLabelsProp ?? localeWeekdays;
|
|
310
402
|
return rotateWeekdays(base, weekStartsOn);
|
|
311
|
-
}, [weekdayLabelsProp, weekStartsOn]);
|
|
403
|
+
}, [weekdayLabelsProp, localeWeekdays, weekStartsOn]);
|
|
404
|
+
|
|
405
|
+
const formatMonthLabel = formatMonthLabelProp ?? localeMonthLabel;
|
|
312
406
|
|
|
313
407
|
/* year options */
|
|
314
408
|
const nowYear = new Date().getFullYear();
|
|
@@ -490,6 +584,9 @@ export function Calendar(props: CalendarProps) {
|
|
|
490
584
|
weekdayLabels,
|
|
491
585
|
showOutsideDays,
|
|
492
586
|
formatMonthLabel,
|
|
587
|
+
defaultFormatYear: localeYearLabel,
|
|
588
|
+
defaultFormatMonth: localeMonthSelectLabel,
|
|
589
|
+
messages: resolvedMessages,
|
|
493
590
|
ariaLabel,
|
|
494
591
|
isSelected: isDateSelected,
|
|
495
592
|
isInRange: getRangeState,
|
|
@@ -562,7 +659,7 @@ export interface CalendarNavButtonProps
|
|
|
562
659
|
function makeNavButton(
|
|
563
660
|
displayName: string,
|
|
564
661
|
defaultIcon: React.ReactNode,
|
|
565
|
-
|
|
662
|
+
resolveDefaultLabel: (ctx: CalendarContextValue) => string,
|
|
566
663
|
resolveHandler: (ctx: CalendarContextValue) => () => void,
|
|
567
664
|
) {
|
|
568
665
|
const Component = React.forwardRef<HTMLButtonElement, CalendarNavButtonProps>(
|
|
@@ -573,7 +670,7 @@ function makeNavButton(
|
|
|
573
670
|
ref={ref}
|
|
574
671
|
type="button"
|
|
575
672
|
className={cn("sh-ui-calendar__nav", className)}
|
|
576
|
-
aria-label={ariaLabel ??
|
|
673
|
+
aria-label={ariaLabel ?? resolveDefaultLabel(ctx)}
|
|
577
674
|
onClick={(e) => {
|
|
578
675
|
resolveHandler(ctx)();
|
|
579
676
|
onClick?.(e);
|
|
@@ -593,7 +690,7 @@ function makeNavButton(
|
|
|
593
690
|
export const CalendarPrevYearButton = makeNavButton(
|
|
594
691
|
"CalendarPrevYearButton",
|
|
595
692
|
<ChevronDoubleLeftIcon />,
|
|
596
|
-
|
|
693
|
+
(ctx) => ctx.messages.prevYear,
|
|
597
694
|
(ctx) => ctx.prevYear,
|
|
598
695
|
);
|
|
599
696
|
|
|
@@ -601,7 +698,7 @@ export const CalendarPrevYearButton = makeNavButton(
|
|
|
601
698
|
export const CalendarNextYearButton = makeNavButton(
|
|
602
699
|
"CalendarNextYearButton",
|
|
603
700
|
<ChevronDoubleRightIcon />,
|
|
604
|
-
|
|
701
|
+
(ctx) => ctx.messages.nextYear,
|
|
605
702
|
(ctx) => ctx.nextYear,
|
|
606
703
|
);
|
|
607
704
|
|
|
@@ -609,7 +706,7 @@ export const CalendarNextYearButton = makeNavButton(
|
|
|
609
706
|
export const CalendarPrevMonthButton = makeNavButton(
|
|
610
707
|
"CalendarPrevMonthButton",
|
|
611
708
|
<ChevronLeftIcon />,
|
|
612
|
-
|
|
709
|
+
(ctx) => ctx.messages.prevMonth,
|
|
613
710
|
(ctx) => ctx.prevMonth,
|
|
614
711
|
);
|
|
615
712
|
|
|
@@ -617,7 +714,7 @@ export const CalendarPrevMonthButton = makeNavButton(
|
|
|
617
714
|
export const CalendarNextMonthButton = makeNavButton(
|
|
618
715
|
"CalendarNextMonthButton",
|
|
619
716
|
<ChevronRightIcon />,
|
|
620
|
-
|
|
717
|
+
(ctx) => ctx.messages.nextMonth,
|
|
621
718
|
(ctx) => ctx.nextMonth,
|
|
622
719
|
);
|
|
623
720
|
|
|
@@ -626,16 +723,17 @@ export const CalendarNextMonthButton = makeNavButton(
|
|
|
626
723
|
export interface CalendarYearSelectProps {
|
|
627
724
|
/** select trigger 의 className. */
|
|
628
725
|
className?: string;
|
|
629
|
-
/** label 표시 포맷.
|
|
726
|
+
/** label 표시 포맷. 미지정 시 Calendar `locale` 기반 자동 생성. */
|
|
630
727
|
formatYear?: (year: number) => string;
|
|
631
728
|
}
|
|
632
729
|
|
|
633
730
|
/** 연도 dropdown. sh-ui Select 로 구현. */
|
|
634
731
|
export function CalendarYearSelect({
|
|
635
732
|
className,
|
|
636
|
-
formatYear
|
|
733
|
+
formatYear,
|
|
637
734
|
}: CalendarYearSelectProps) {
|
|
638
735
|
const ctx = useCalendarContext("CalendarYearSelect");
|
|
736
|
+
const resolvedFormat = formatYear ?? ctx.defaultFormatYear;
|
|
639
737
|
const year = ctx.visibleMonth.getFullYear();
|
|
640
738
|
const items = ctx.yearOptions.includes(year)
|
|
641
739
|
? ctx.yearOptions
|
|
@@ -648,14 +746,14 @@ export function CalendarYearSelect({
|
|
|
648
746
|
>
|
|
649
747
|
<SelectTrigger
|
|
650
748
|
className={cn("sh-ui-calendar__select-trigger", className)}
|
|
651
|
-
aria-label=
|
|
749
|
+
aria-label={ctx.messages.yearSelectLabel}
|
|
652
750
|
>
|
|
653
|
-
<span className="sh-ui-calendar__select-value">{
|
|
751
|
+
<span className="sh-ui-calendar__select-value">{resolvedFormat(year)}</span>
|
|
654
752
|
</SelectTrigger>
|
|
655
753
|
<SelectContent className="sh-ui-calendar__select-popup">
|
|
656
754
|
{items.map((y) => (
|
|
657
755
|
<SelectItem key={y} value={String(y)}>
|
|
658
|
-
{
|
|
756
|
+
{resolvedFormat(y)}
|
|
659
757
|
</SelectItem>
|
|
660
758
|
))}
|
|
661
759
|
</SelectContent>
|
|
@@ -665,16 +763,17 @@ export function CalendarYearSelect({
|
|
|
665
763
|
|
|
666
764
|
export interface CalendarMonthSelectProps {
|
|
667
765
|
className?: string;
|
|
668
|
-
/** label 표시 포맷.
|
|
766
|
+
/** label 표시 포맷. 미지정 시 Calendar `locale` 기반 자동 생성. */
|
|
669
767
|
formatMonth?: (month: number) => string;
|
|
670
768
|
}
|
|
671
769
|
|
|
672
770
|
/** 월 dropdown. */
|
|
673
771
|
export function CalendarMonthSelect({
|
|
674
772
|
className,
|
|
675
|
-
formatMonth
|
|
773
|
+
formatMonth,
|
|
676
774
|
}: CalendarMonthSelectProps) {
|
|
677
775
|
const ctx = useCalendarContext("CalendarMonthSelect");
|
|
776
|
+
const resolvedFormat = formatMonth ?? ctx.defaultFormatMonth;
|
|
678
777
|
const month = ctx.visibleMonth.getMonth();
|
|
679
778
|
return (
|
|
680
779
|
<Select
|
|
@@ -683,14 +782,14 @@ export function CalendarMonthSelect({
|
|
|
683
782
|
>
|
|
684
783
|
<SelectTrigger
|
|
685
784
|
className={cn("sh-ui-calendar__select-trigger", className)}
|
|
686
|
-
aria-label=
|
|
785
|
+
aria-label={ctx.messages.monthSelectLabel}
|
|
687
786
|
>
|
|
688
|
-
<span className="sh-ui-calendar__select-value">{
|
|
787
|
+
<span className="sh-ui-calendar__select-value">{resolvedFormat(month)}</span>
|
|
689
788
|
</SelectTrigger>
|
|
690
789
|
<SelectContent className="sh-ui-calendar__select-popup">
|
|
691
790
|
{Array.from({ length: 12 }, (_, m) => (
|
|
692
791
|
<SelectItem key={m} value={String(m)}>
|
|
693
|
-
{
|
|
792
|
+
{resolvedFormat(m)}
|
|
694
793
|
</SelectItem>
|
|
695
794
|
))}
|
|
696
795
|
</SelectContent>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
5
|
-
import { Calendar, type DateRange } from "../calendar";
|
|
5
|
+
import { Calendar, DEFAULT_LOCALE, type CalendarMessages, type DateRange } from "../calendar";
|
|
6
6
|
import styles from "./styles.module.css";
|
|
7
7
|
|
|
8
8
|
import { cn } from "@SH_UI_UTILS@";
|
|
@@ -17,6 +17,15 @@ const formatDefault = (d: Date) =>
|
|
|
17
17
|
const startOfMonth = (d: Date) =>
|
|
18
18
|
new Date(d.getFullYear(), d.getMonth(), 1);
|
|
19
19
|
|
|
20
|
+
function defaultDatePlaceholder(locale: string): string {
|
|
21
|
+
const lang = locale.toLowerCase().split(/[-_]/)[0];
|
|
22
|
+
return lang === "ko" ? "날짜 선택" : "Select date";
|
|
23
|
+
}
|
|
24
|
+
function defaultRangePlaceholder(locale: string): string {
|
|
25
|
+
const lang = locale.toLowerCase().split(/[-_]/)[0];
|
|
26
|
+
return lang === "ko" ? "시작일 ~ 종료일" : "Start date – end date";
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/* ───────── Icons ───────── */
|
|
21
30
|
|
|
22
31
|
function CalendarIcon() {
|
|
@@ -39,6 +48,8 @@ interface DatePickerContextValue {
|
|
|
39
48
|
setFocusedDate: (date: Date) => void;
|
|
40
49
|
formatDate: (date: Date) => string;
|
|
41
50
|
placeholder: string;
|
|
51
|
+
locale: string;
|
|
52
|
+
messages?: CalendarMessages;
|
|
42
53
|
min?: Date;
|
|
43
54
|
max?: Date;
|
|
44
55
|
disabled?: boolean;
|
|
@@ -76,10 +87,13 @@ export interface DatePickerProps {
|
|
|
76
87
|
/** 선택 가능 최대 날짜 (포함). 이후 날짜는 비활성. */
|
|
77
88
|
max?: Date;
|
|
78
89
|
/**
|
|
79
|
-
* 미선택 상태의 트리거 텍스트.
|
|
80
|
-
* @default "날짜 선택"
|
|
90
|
+
* 미선택 상태의 트리거 텍스트. 미지정 시 `locale` 기반 기본값.
|
|
81
91
|
*/
|
|
82
92
|
placeholder?: string;
|
|
93
|
+
/** BCP47 로케일. 내부 Calendar 와 placeholder 에 모두 적용. @default "ko-KR" */
|
|
94
|
+
locale?: string;
|
|
95
|
+
/** 내부 Calendar 의 nav/select aria-label override. */
|
|
96
|
+
messages?: CalendarMessages;
|
|
83
97
|
/** 비활성. 트리거 클릭·키보드 모두 차단. */
|
|
84
98
|
disabled?: boolean;
|
|
85
99
|
/** 읽기 전용. 트리거 표시는 유지하되 popover가 열리지 않는다. */
|
|
@@ -119,7 +133,9 @@ export function DatePicker({
|
|
|
119
133
|
formatDate = formatDefault,
|
|
120
134
|
min,
|
|
121
135
|
max,
|
|
122
|
-
placeholder
|
|
136
|
+
placeholder,
|
|
137
|
+
locale = DEFAULT_LOCALE,
|
|
138
|
+
messages,
|
|
123
139
|
disabled,
|
|
124
140
|
readOnly,
|
|
125
141
|
"aria-invalid": ariaInvalid,
|
|
@@ -128,6 +144,7 @@ export function DatePicker({
|
|
|
128
144
|
container,
|
|
129
145
|
children,
|
|
130
146
|
}: DatePickerProps) {
|
|
147
|
+
const resolvedPlaceholder = placeholder ?? defaultDatePlaceholder(locale);
|
|
131
148
|
const isControlled = value !== undefined;
|
|
132
149
|
const [internal, setInternal] = React.useState<Date | undefined>(defaultValue);
|
|
133
150
|
const selected = isControlled ? value : internal;
|
|
@@ -160,7 +177,9 @@ export function DatePicker({
|
|
|
160
177
|
focusedDate,
|
|
161
178
|
setFocusedDate,
|
|
162
179
|
formatDate,
|
|
163
|
-
placeholder,
|
|
180
|
+
placeholder: resolvedPlaceholder,
|
|
181
|
+
locale,
|
|
182
|
+
messages,
|
|
164
183
|
min,
|
|
165
184
|
max,
|
|
166
185
|
disabled,
|
|
@@ -174,7 +193,9 @@ export function DatePicker({
|
|
|
174
193
|
open,
|
|
175
194
|
focusedDate,
|
|
176
195
|
formatDate,
|
|
177
|
-
|
|
196
|
+
resolvedPlaceholder,
|
|
197
|
+
locale,
|
|
198
|
+
messages,
|
|
178
199
|
min,
|
|
179
200
|
max,
|
|
180
201
|
disabled,
|
|
@@ -355,6 +376,8 @@ export function DatePickerCalendar() {
|
|
|
355
376
|
onMonthChange={ctx.setFocusedDate}
|
|
356
377
|
min={ctx.min}
|
|
357
378
|
max={ctx.max}
|
|
379
|
+
locale={ctx.locale}
|
|
380
|
+
messages={ctx.messages}
|
|
358
381
|
/>
|
|
359
382
|
);
|
|
360
383
|
}
|
|
@@ -407,10 +430,13 @@ export interface DateRangePickerProps {
|
|
|
407
430
|
/** 선택 가능 최대 날짜. */
|
|
408
431
|
max?: Date;
|
|
409
432
|
/**
|
|
410
|
-
* 미선택 상태의 트리거 텍스트.
|
|
411
|
-
* @default "시작일 ~ 종료일"
|
|
433
|
+
* 미선택 상태의 트리거 텍스트. 미지정 시 `locale` 기반 기본값.
|
|
412
434
|
*/
|
|
413
435
|
placeholder?: string;
|
|
436
|
+
/** BCP47 로케일. @default "ko-KR" */
|
|
437
|
+
locale?: string;
|
|
438
|
+
/** 내부 Calendar 의 nav/select aria-label override. */
|
|
439
|
+
messages?: CalendarMessages;
|
|
414
440
|
/** 비활성. */
|
|
415
441
|
disabled?: boolean;
|
|
416
442
|
/** 읽기 전용. popover가 열리지 않는다. */
|
|
@@ -438,7 +464,9 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
438
464
|
formatDate = formatDefault,
|
|
439
465
|
min,
|
|
440
466
|
max,
|
|
441
|
-
placeholder
|
|
467
|
+
placeholder,
|
|
468
|
+
locale = DEFAULT_LOCALE,
|
|
469
|
+
messages,
|
|
442
470
|
disabled,
|
|
443
471
|
readOnly,
|
|
444
472
|
"aria-invalid": ariaInvalid,
|
|
@@ -447,6 +475,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
447
475
|
},
|
|
448
476
|
ref,
|
|
449
477
|
) {
|
|
478
|
+
const resolvedPlaceholder = placeholder ?? defaultRangePlaceholder(locale);
|
|
450
479
|
const isControlled = value !== undefined;
|
|
451
480
|
const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue);
|
|
452
481
|
const selected = isControlled ? value : internal;
|
|
@@ -485,7 +514,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
485
514
|
}}
|
|
486
515
|
>
|
|
487
516
|
<span className={cn(styles["date-picker__value"], !displayText && styles["date-picker__placeholder"])}>
|
|
488
|
-
{displayText ??
|
|
517
|
+
{displayText ?? resolvedPlaceholder}
|
|
489
518
|
</span>
|
|
490
519
|
<span className={styles["date-picker__icon"]} aria-hidden>
|
|
491
520
|
<CalendarIcon />
|
|
@@ -509,6 +538,8 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
509
538
|
onMonthChange={setCalendarMonth}
|
|
510
539
|
min={min}
|
|
511
540
|
max={max}
|
|
541
|
+
locale={locale}
|
|
542
|
+
messages={messages}
|
|
512
543
|
/>
|
|
513
544
|
</BasePopover.Popup>
|
|
514
545
|
</BasePopover.Positioner>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
5
|
-
import { Calendar, type DateRange } from "../calendar";
|
|
5
|
+
import { Calendar, DEFAULT_LOCALE, type CalendarMessages, type DateRange } from "../calendar";
|
|
6
6
|
|
|
7
7
|
import { cn } from "@SH_UI_UTILS@";
|
|
8
8
|
export type { DateRange };
|
|
@@ -12,6 +12,15 @@ const formatDefault = (d: Date) =>
|
|
|
12
12
|
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
13
13
|
const startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);
|
|
14
14
|
|
|
15
|
+
function defaultDatePlaceholder(locale: string): string {
|
|
16
|
+
const lang = locale.toLowerCase().split(/[-_]/)[0];
|
|
17
|
+
return lang === "ko" ? "날짜 선택" : "Select date";
|
|
18
|
+
}
|
|
19
|
+
function defaultRangePlaceholder(locale: string): string {
|
|
20
|
+
const lang = locale.toLowerCase().split(/[-_]/)[0];
|
|
21
|
+
return lang === "ko" ? "시작일 ~ 종료일" : "Start date – end date";
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
const triggerClasses =
|
|
16
25
|
"inline-flex items-center justify-between w-full h-[var(--control-md)] px-[var(--space-3)] bg-background text-foreground border border-border rounded-[var(--radius)] font-[inherit] text-[length:var(--text-sm)] leading-none cursor-pointer transition-[border-color,box-shadow] duration-[var(--duration-fast)] hover:not-disabled:border-border-strong focus-visible:outline-none focus-visible:border-foreground focus-visible:shadow-[0_0_0_1px_var(--foreground)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed disabled:bg-background-subtle aria-[invalid=true]:border-danger aria-[invalid=true]:focus-visible:shadow-[0_0_0_1px_var(--danger)] [@media(hover:none)_and_(pointer:coarse)]:h-11 [@media(hover:none)_and_(pointer:coarse)]:text-[length:var(--text-base)]";
|
|
17
26
|
|
|
@@ -36,6 +45,8 @@ interface DatePickerContextValue {
|
|
|
36
45
|
setFocusedDate: (date: Date) => void;
|
|
37
46
|
formatDate: (date: Date) => string;
|
|
38
47
|
placeholder: string;
|
|
48
|
+
locale: string;
|
|
49
|
+
messages?: CalendarMessages;
|
|
39
50
|
min?: Date; max?: Date; disabled?: boolean; readOnly?: boolean;
|
|
40
51
|
ariaInvalid?: boolean | "true";
|
|
41
52
|
closeOnSelect: boolean;
|
|
@@ -54,7 +65,8 @@ export interface DatePickerProps {
|
|
|
54
65
|
onValueChange?: (date: Date | undefined) => void;
|
|
55
66
|
formatDate?: (date: Date) => string;
|
|
56
67
|
min?: Date; max?: Date;
|
|
57
|
-
placeholder?: string;
|
|
68
|
+
placeholder?: string; locale?: string; messages?: CalendarMessages;
|
|
69
|
+
disabled?: boolean; readOnly?: boolean;
|
|
58
70
|
"aria-invalid"?: boolean | "true";
|
|
59
71
|
className?: string; closeOnSelect?: boolean;
|
|
60
72
|
container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
|
|
@@ -63,9 +75,10 @@ export interface DatePickerProps {
|
|
|
63
75
|
|
|
64
76
|
export function DatePicker({
|
|
65
77
|
value, defaultValue, onValueChange, formatDate = formatDefault,
|
|
66
|
-
min, max, placeholder =
|
|
78
|
+
min, max, placeholder, locale = DEFAULT_LOCALE, messages, disabled, readOnly,
|
|
67
79
|
"aria-invalid": ariaInvalid, className, closeOnSelect = true, container, children,
|
|
68
80
|
}: DatePickerProps) {
|
|
81
|
+
const resolvedPlaceholder = placeholder ?? defaultDatePlaceholder(locale);
|
|
69
82
|
const isControlled = value !== undefined;
|
|
70
83
|
const [internal, setInternal] = React.useState<Date | undefined>(defaultValue);
|
|
71
84
|
const selected = isControlled ? value : internal;
|
|
@@ -84,9 +97,10 @@ export function DatePicker({
|
|
|
84
97
|
const ctx = React.useMemo<DatePickerContextValue>(
|
|
85
98
|
() => ({
|
|
86
99
|
selected, setSelected, open, setOpen, focusedDate, setFocusedDate,
|
|
87
|
-
formatDate, placeholder
|
|
100
|
+
formatDate, placeholder: resolvedPlaceholder, locale, messages,
|
|
101
|
+
min, max, disabled, readOnly, ariaInvalid, closeOnSelect,
|
|
88
102
|
}),
|
|
89
|
-
[selected, setSelected, open, focusedDate, formatDate,
|
|
103
|
+
[selected, setSelected, open, focusedDate, formatDate, resolvedPlaceholder, locale, messages, min, max, disabled, readOnly, ariaInvalid, closeOnSelect],
|
|
90
104
|
);
|
|
91
105
|
|
|
92
106
|
return (
|
|
@@ -187,6 +201,8 @@ export function DatePickerCalendar() {
|
|
|
187
201
|
onMonthChange={ctx.setFocusedDate}
|
|
188
202
|
min={ctx.min}
|
|
189
203
|
max={ctx.max}
|
|
204
|
+
locale={ctx.locale}
|
|
205
|
+
messages={ctx.messages}
|
|
190
206
|
/>
|
|
191
207
|
);
|
|
192
208
|
}
|
|
@@ -221,7 +237,8 @@ export interface DateRangePickerProps {
|
|
|
221
237
|
onValueChange?: (range: DateRange | undefined) => void;
|
|
222
238
|
formatDate?: (date: Date) => string;
|
|
223
239
|
min?: Date; max?: Date;
|
|
224
|
-
placeholder?: string;
|
|
240
|
+
placeholder?: string; locale?: string; messages?: CalendarMessages;
|
|
241
|
+
disabled?: boolean; readOnly?: boolean;
|
|
225
242
|
"aria-invalid"?: boolean | "true";
|
|
226
243
|
className?: string;
|
|
227
244
|
container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
|
|
@@ -230,9 +247,10 @@ export interface DateRangePickerProps {
|
|
|
230
247
|
export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(
|
|
231
248
|
function DateRangePicker(
|
|
232
249
|
{ value, defaultValue, onValueChange, formatDate = formatDefault, min, max,
|
|
233
|
-
placeholder =
|
|
250
|
+
placeholder, locale = DEFAULT_LOCALE, messages, disabled, readOnly, "aria-invalid": ariaInvalid, className, container },
|
|
234
251
|
ref,
|
|
235
252
|
) {
|
|
253
|
+
const resolvedPlaceholder = placeholder ?? defaultRangePlaceholder(locale);
|
|
236
254
|
const isControlled = value !== undefined;
|
|
237
255
|
const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue);
|
|
238
256
|
const selected = isControlled ? value : internal;
|
|
@@ -262,7 +280,7 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
262
280
|
onClick={(e) => { if (readOnly) e.preventDefault(); }}
|
|
263
281
|
>
|
|
264
282
|
<span className={cn("overflow-hidden text-ellipsis whitespace-nowrap", !displayText && "text-foreground-subtle")}>
|
|
265
|
-
{displayText ??
|
|
283
|
+
{displayText ?? resolvedPlaceholder}
|
|
266
284
|
</span>
|
|
267
285
|
<span className="shrink-0 inline-flex text-foreground-muted ml-[var(--space-2)]" aria-hidden>
|
|
268
286
|
<CalendarIcon />
|
|
@@ -281,6 +299,8 @@ export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePick
|
|
|
281
299
|
onMonthChange={setCalendarMonth}
|
|
282
300
|
min={min}
|
|
283
301
|
max={max}
|
|
302
|
+
locale={locale}
|
|
303
|
+
messages={messages}
|
|
284
304
|
/>
|
|
285
305
|
</BasePopover.Popup>
|
|
286
306
|
</BasePopover.Positioner>
|