sh-ui-cli 0.52.0 → 0.52.2
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 +25 -0
- package/data/registry/react/components/_smoke/vanilla-extract.test.ts +33 -0
- package/data/registry/react/components/input/styles.css.ts +6 -6
- package/data/registry/react/registry.json +35 -852
- package/package.json +2 -2
- package/src/api.d.ts +3 -4
- package/src/constants.js +9 -5
- package/src/create/plugins/pluginSchema.js +5 -3
- package/src/mcp.mjs +4 -3
- package/data/registry/react/components/accordion/index.vanilla-extract.tsx +0 -97
- package/data/registry/react/components/accordion/styles.css.ts +0 -131
- package/data/registry/react/components/avatar/index.vanilla-extract.tsx +0 -73
- package/data/registry/react/components/avatar/styles.css.ts +0 -68
- package/data/registry/react/components/badge/index.vanilla-extract.tsx +0 -40
- package/data/registry/react/components/badge/styles.css.ts +0 -71
- package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +0 -152
- package/data/registry/react/components/breadcrumb/styles.css.ts +0 -95
- package/data/registry/react/components/calendar/index.vanilla-extract.tsx +0 -806
- package/data/registry/react/components/calendar/styles.css.ts +0 -250
- package/data/registry/react/components/carousel/index.vanilla-extract.tsx +0 -430
- package/data/registry/react/components/carousel/styles.css.ts +0 -169
- package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +0 -96
- package/data/registry/react/components/checkbox/styles.css.ts +0 -74
- package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +0 -230
- package/data/registry/react/components/code-editor/styles.css.ts +0 -97
- package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +0 -191
- package/data/registry/react/components/code-panel/styles.css.ts +0 -151
- package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +0 -467
- package/data/registry/react/components/color-picker/styles.css.ts +0 -169
- package/data/registry/react/components/combobox/index.vanilla-extract.tsx +0 -165
- package/data/registry/react/components/combobox/styles.css.ts +0 -174
- package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +0 -251
- package/data/registry/react/components/context-menu/styles.css.ts +0 -167
- package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +0 -520
- package/data/registry/react/components/date-picker/styles.css.ts +0 -111
- package/data/registry/react/components/dialog/index.vanilla-extract.tsx +0 -95
- package/data/registry/react/components/dialog/styles.css.ts +0 -140
- package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +0 -255
- package/data/registry/react/components/dropdown-menu/styles.css.ts +0 -175
- package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +0 -487
- package/data/registry/react/components/file-upload/styles.css.ts +0 -193
- package/data/registry/react/components/form/index.vanilla-extract.tsx +0 -61
- package/data/registry/react/components/form/styles.css.ts +0 -56
- package/data/registry/react/components/header/index.vanilla-extract.tsx +0 -805
- package/data/registry/react/components/header/styles.css.ts +0 -413
- package/data/registry/react/components/label/index.vanilla-extract.tsx +0 -52
- package/data/registry/react/components/label/styles.css.ts +0 -141
- package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +0 -119
- package/data/registry/react/components/markdown-editor/styles.css.ts +0 -231
- package/data/registry/react/components/menubar/index.vanilla-extract.tsx +0 -32
- package/data/registry/react/components/menubar/styles.css.ts +0 -53
- package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +0 -148
- package/data/registry/react/components/numeric-input/styles.css.ts +0 -65
- package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +0 -174
- package/data/registry/react/components/page-toc/styles.css.ts +0 -97
- package/data/registry/react/components/pagination/index.vanilla-extract.tsx +0 -269
- package/data/registry/react/components/pagination/styles.css.ts +0 -113
- package/data/registry/react/components/popover/index.vanilla-extract.tsx +0 -113
- package/data/registry/react/components/popover/styles.css.ts +0 -78
- package/data/registry/react/components/progress/index.vanilla-extract.tsx +0 -54
- package/data/registry/react/components/progress/styles.css.ts +0 -53
- package/data/registry/react/components/radio/index.vanilla-extract.tsx +0 -65
- package/data/registry/react/components/radio/styles.css.ts +0 -79
- package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +0 -348
- package/data/registry/react/components/rich-text-editor/styles.css.ts +0 -243
- package/data/registry/react/components/select/index.vanilla-extract.tsx +0 -234
- package/data/registry/react/components/select/styles.css.ts +0 -225
- package/data/registry/react/components/separator/index.vanilla-extract.tsx +0 -46
- package/data/registry/react/components/separator/styles.css.ts +0 -24
- package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +0 -1067
- package/data/registry/react/components/sidebar/styles.css.ts +0 -578
- package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +0 -22
- package/data/registry/react/components/skeleton/styles.css.ts +0 -30
- package/data/registry/react/components/slider/index.vanilla-extract.tsx +0 -298
- package/data/registry/react/components/slider/styles.css.ts +0 -75
- package/data/registry/react/components/spinner/index.vanilla-extract.tsx +0 -38
- package/data/registry/react/components/spinner/styles.css.ts +0 -60
- package/data/registry/react/components/switch/index.vanilla-extract.tsx +0 -39
- package/data/registry/react/components/switch/styles.css.ts +0 -87
- package/data/registry/react/components/tabs/index.vanilla-extract.tsx +0 -91
- package/data/registry/react/components/tabs/styles.css.ts +0 -145
- package/data/registry/react/components/textarea/index.vanilla-extract.tsx +0 -23
- package/data/registry/react/components/textarea/styles.css.ts +0 -55
- package/data/registry/react/components/toast/index.vanilla-extract.tsx +0 -258
- package/data/registry/react/components/toast/styles.css.ts +0 -307
- package/data/registry/react/components/toggle/index.vanilla-extract.tsx +0 -131
- package/data/registry/react/components/toggle/styles.css.ts +0 -109
- package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +0 -83
- package/data/registry/react/components/tooltip/styles.css.ts +0 -59
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { style, keyframes } from "@vanilla-extract/css";
|
|
2
|
-
|
|
3
|
-
export const shUiCmIn = keyframes({
|
|
4
|
-
"from": {
|
|
5
|
-
opacity: 0,
|
|
6
|
-
transform: "scale(0.96)",
|
|
7
|
-
},
|
|
8
|
-
"to": {
|
|
9
|
-
opacity: 1,
|
|
10
|
-
transform: "scale(1)",
|
|
11
|
-
},
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export const shUiCmOut = keyframes({
|
|
15
|
-
"from": {
|
|
16
|
-
opacity: 1,
|
|
17
|
-
transform: "scale(1)",
|
|
18
|
-
},
|
|
19
|
-
"to": {
|
|
20
|
-
opacity: 0,
|
|
21
|
-
transform: "scale(0.96)",
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export const cm__trigger = style({
|
|
26
|
-
display: "contents",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
export const cm__positioner = style({
|
|
30
|
-
outline: "none",
|
|
31
|
-
zIndex: "var(--z-dropdown)",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export const cm__content = style({
|
|
35
|
-
minWidth: "10rem",
|
|
36
|
-
maxHeight: "min(24rem, var(--available-height, 24rem))",
|
|
37
|
-
overflowY: "auto",
|
|
38
|
-
padding: "var(--space-1)",
|
|
39
|
-
background: "var(--background)",
|
|
40
|
-
color: "var(--foreground)",
|
|
41
|
-
border: "1px solid var(--border)",
|
|
42
|
-
borderRadius: "var(--radius)",
|
|
43
|
-
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.08),\n 0 2px 4px -2px rgba(0, 0, 0, 0.05)",
|
|
44
|
-
fontSize: "var(--text-sm)",
|
|
45
|
-
transformOrigin: "var(--transform-origin)",
|
|
46
|
-
animation: "sh-ui-cm-in 140ms ease-out",
|
|
47
|
-
outline: "none",
|
|
48
|
-
selectors: {
|
|
49
|
-
"&[data-ending-style]": {
|
|
50
|
-
animation: "sh-ui-cm-out 100ms ease-in forwards",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
"@media": {
|
|
54
|
-
"(prefers-reduced-motion: reduce)": {
|
|
55
|
-
animation: "none",
|
|
56
|
-
selectors: {
|
|
57
|
-
"&[data-ending-style]": {
|
|
58
|
-
animation: "none",
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
export const cm__item = style({
|
|
66
|
-
position: "relative",
|
|
67
|
-
display: "flex",
|
|
68
|
-
alignItems: "center",
|
|
69
|
-
gap: "var(--space-2)",
|
|
70
|
-
padding: "0.5rem 0.75rem",
|
|
71
|
-
borderRadius: "calc(var(--radius) - 2px)",
|
|
72
|
-
cursor: "pointer",
|
|
73
|
-
outline: "none",
|
|
74
|
-
userSelect: "none",
|
|
75
|
-
transition: "background-color 80ms",
|
|
76
|
-
selectors: {
|
|
77
|
-
"&[data-highlighted]": {
|
|
78
|
-
background: "var(--background-muted)",
|
|
79
|
-
},
|
|
80
|
-
"&:hover": {
|
|
81
|
-
background: "var(--background-muted)",
|
|
82
|
-
},
|
|
83
|
-
"&[data-disabled]": {
|
|
84
|
-
opacity: "var(--opacity-disabled)",
|
|
85
|
-
pointerEvents: "none",
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
"@media": {
|
|
89
|
-
"(prefers-reduced-motion: reduce)": {
|
|
90
|
-
transition: "none",
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
export const cmItemText = style({
|
|
96
|
-
flex: 1,
|
|
97
|
-
minWidth: 0,
|
|
98
|
-
overflow: "hidden",
|
|
99
|
-
textOverflow: "ellipsis",
|
|
100
|
-
whiteSpace: "nowrap",
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
export const cmItemCheck = style({
|
|
104
|
-
paddingLeft: "1.75rem",
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
export const cmItemIndicator = style({
|
|
108
|
-
position: "absolute",
|
|
109
|
-
left: "0.5rem",
|
|
110
|
-
display: "inline-flex",
|
|
111
|
-
alignItems: "center",
|
|
112
|
-
justifyContent: "center",
|
|
113
|
-
width: "1rem",
|
|
114
|
-
height: "1rem",
|
|
115
|
-
color: "var(--foreground)",
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
export const cm__group = style({
|
|
119
|
-
padding: 0,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
export const cm__label = style({
|
|
123
|
-
padding: "var(--space-2) var(--space-2) var(--space-1)",
|
|
124
|
-
fontSize: "var(--text-xs)",
|
|
125
|
-
fontWeight: "var(--weight-semibold)",
|
|
126
|
-
color: "var(--foreground-muted)",
|
|
127
|
-
textTransform: "uppercase",
|
|
128
|
-
letterSpacing: "0.04em",
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
export const cm__separator = style({
|
|
132
|
-
height: "1px",
|
|
133
|
-
background: "var(--border)",
|
|
134
|
-
margin: "var(--space-1) 0",
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
export const cmSubArrow = style({
|
|
138
|
-
display: "inline-flex",
|
|
139
|
-
alignItems: "center",
|
|
140
|
-
justifyContent: "center",
|
|
141
|
-
marginLeft: "auto",
|
|
142
|
-
color: "var(--foreground-muted)",
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
export const cmSubTrigger = style({
|
|
146
|
-
selectors: {
|
|
147
|
-
"&[data-popup-open]": {
|
|
148
|
-
background: "var(--background-muted)",
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
/** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
|
|
154
|
-
export const byKey: Record<string, string> = {
|
|
155
|
-
"cm__trigger": cm__trigger,
|
|
156
|
-
"cm__positioner": cm__positioner,
|
|
157
|
-
"cm__content": cm__content,
|
|
158
|
-
"cm__item": cm__item,
|
|
159
|
-
"cm__item-text": cmItemText,
|
|
160
|
-
"cm__item--check": cmItemCheck,
|
|
161
|
-
"cm__item-indicator": cmItemIndicator,
|
|
162
|
-
"cm__group": cm__group,
|
|
163
|
-
"cm__label": cm__label,
|
|
164
|
-
"cm__separator": cm__separator,
|
|
165
|
-
"cm__sub-arrow": cmSubArrow,
|
|
166
|
-
"cm__sub-trigger": cmSubTrigger,
|
|
167
|
-
};
|
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
5
|
-
import { Calendar, type DateRange } from "../calendar";
|
|
6
|
-
import { byKey, datePickerTrigger, datePickerValue, datePickerPlaceholder, datePickerIcon, datePickerPositioner, datePickerPopup, datePickerFooter } from "./styles.css";
|
|
7
|
-
|
|
8
|
-
import { cn } from "@SH_UI_UTILS@";
|
|
9
|
-
export type { DateRange };
|
|
10
|
-
|
|
11
|
-
/* ───────── Helpers ───────── */
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const formatDefault = (d: Date) =>
|
|
15
|
-
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
16
|
-
|
|
17
|
-
const startOfMonth = (d: Date) =>
|
|
18
|
-
new Date(d.getFullYear(), d.getMonth(), 1);
|
|
19
|
-
|
|
20
|
-
/* ───────── Icons ───────── */
|
|
21
|
-
|
|
22
|
-
function CalendarIcon() {
|
|
23
|
-
return (
|
|
24
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" aria-hidden>
|
|
25
|
-
<rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
|
|
26
|
-
<path d="M2 6.5h12M5.5 2v2M10.5 2v2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
27
|
-
</svg>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* ───────── Context ───────── */
|
|
32
|
-
|
|
33
|
-
interface DatePickerContextValue {
|
|
34
|
-
selected: Date | undefined;
|
|
35
|
-
setSelected: (date: Date | undefined) => void;
|
|
36
|
-
open: boolean;
|
|
37
|
-
setOpen: (open: boolean) => void;
|
|
38
|
-
focusedDate: Date;
|
|
39
|
-
setFocusedDate: (date: Date) => void;
|
|
40
|
-
formatDate: (date: Date) => string;
|
|
41
|
-
placeholder: string;
|
|
42
|
-
min?: Date;
|
|
43
|
-
max?: Date;
|
|
44
|
-
disabled?: boolean;
|
|
45
|
-
readOnly?: boolean;
|
|
46
|
-
ariaInvalid?: boolean | "true";
|
|
47
|
-
closeOnSelect: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const DatePickerContext = React.createContext<DatePickerContextValue | null>(null);
|
|
51
|
-
|
|
52
|
-
function useDatePickerContext(component: string) {
|
|
53
|
-
const ctx = React.useContext(DatePickerContext);
|
|
54
|
-
if (!ctx) {
|
|
55
|
-
throw new Error(`${component}는 <DatePicker> 내부에서 사용해야 합니다.`);
|
|
56
|
-
}
|
|
57
|
-
return ctx;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/* ───────── DatePicker Root ───────── */
|
|
61
|
-
|
|
62
|
-
export interface DatePickerProps {
|
|
63
|
-
/** 제어 모드 선택값. `undefined`는 미선택. */
|
|
64
|
-
value?: Date;
|
|
65
|
-
/** 비제어 모드 초기값. */
|
|
66
|
-
defaultValue?: Date;
|
|
67
|
-
/** 값 변경 콜백. 미선택 상태로 전환되면 `undefined`. */
|
|
68
|
-
onValueChange?: (date: Date | undefined) => void;
|
|
69
|
-
/**
|
|
70
|
-
* 트리거에 표시할 문자열 포맷터.
|
|
71
|
-
* @default (d) => "YYYY-MM-DD"
|
|
72
|
-
*/
|
|
73
|
-
formatDate?: (date: Date) => string;
|
|
74
|
-
/** 선택 가능 최소 날짜 (포함). 이전 날짜는 비활성. */
|
|
75
|
-
min?: Date;
|
|
76
|
-
/** 선택 가능 최대 날짜 (포함). 이후 날짜는 비활성. */
|
|
77
|
-
max?: Date;
|
|
78
|
-
/**
|
|
79
|
-
* 미선택 상태의 트리거 텍스트.
|
|
80
|
-
* @default "날짜 선택"
|
|
81
|
-
*/
|
|
82
|
-
placeholder?: string;
|
|
83
|
-
/** 비활성. 트리거 클릭·키보드 모두 차단. */
|
|
84
|
-
disabled?: boolean;
|
|
85
|
-
/** 읽기 전용. 트리거 표시는 유지하되 popover가 열리지 않는다. */
|
|
86
|
-
readOnly?: boolean;
|
|
87
|
-
/** invalid 상태. 트리거 보더가 위험색으로 바뀌고 스크린리더에 오류로 노출. */
|
|
88
|
-
"aria-invalid"?: boolean | "true";
|
|
89
|
-
/**
|
|
90
|
-
* children 없을 때(기본 레이아웃) Trigger로 전달된다.
|
|
91
|
-
* children 조립 모드에서는 `DatePickerTrigger`에 직접 className을 넘긴다.
|
|
92
|
-
*/
|
|
93
|
-
className?: string;
|
|
94
|
-
/**
|
|
95
|
-
* 날짜 선택 시 popover 자동 닫힘.
|
|
96
|
-
* @default true
|
|
97
|
-
*/
|
|
98
|
-
closeOnSelect?: boolean;
|
|
99
|
-
/**
|
|
100
|
-
* Portal이 마운트될 DOM 노드. 토큰 스코프(다크 모드 등) 안에 popover를 띄우려면 해당 컨테이너 ref 전달.
|
|
101
|
-
* @default document.body
|
|
102
|
-
*/
|
|
103
|
-
container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
|
|
104
|
-
/**
|
|
105
|
-
* compound 모드. 미지정 시 기본 레이아웃(Trigger + Content + Calendar)이 자동 렌더된다.
|
|
106
|
-
* 직접 조립하려면 `DatePickerTrigger`/`DatePickerContent`/`DatePickerCalendar`/`DatePickerFooter`를 자식으로 넘긴다.
|
|
107
|
-
*/
|
|
108
|
-
children?: React.ReactNode;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 단일 날짜 선택. 트리거 클릭 시 popover 캘린더가 열리고 키보드 화살표로 이동한다.
|
|
113
|
-
* children을 생략하면 기본 레이아웃이 자동 렌더되며, 직접 조립하려면 DatePickerTrigger/Content/Calendar/Footer를 사용한다.
|
|
114
|
-
*/
|
|
115
|
-
export function DatePicker({
|
|
116
|
-
value,
|
|
117
|
-
defaultValue,
|
|
118
|
-
onValueChange,
|
|
119
|
-
formatDate = formatDefault,
|
|
120
|
-
min,
|
|
121
|
-
max,
|
|
122
|
-
placeholder = "날짜 선택",
|
|
123
|
-
disabled,
|
|
124
|
-
readOnly,
|
|
125
|
-
"aria-invalid": ariaInvalid,
|
|
126
|
-
className,
|
|
127
|
-
closeOnSelect = true,
|
|
128
|
-
container,
|
|
129
|
-
children,
|
|
130
|
-
}: DatePickerProps) {
|
|
131
|
-
const isControlled = value !== undefined;
|
|
132
|
-
const [internal, setInternal] = React.useState<Date | undefined>(defaultValue);
|
|
133
|
-
const selected = isControlled ? value : internal;
|
|
134
|
-
|
|
135
|
-
const [open, setOpen] = React.useState(false);
|
|
136
|
-
const [focusedDate, setFocusedDate] = React.useState<Date>(
|
|
137
|
-
() => selected ?? new Date(),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
React.useEffect(() => {
|
|
141
|
-
if (open && selected) {
|
|
142
|
-
setFocusedDate(startOfMonth(selected));
|
|
143
|
-
}
|
|
144
|
-
}, [open, selected]);
|
|
145
|
-
|
|
146
|
-
const setSelected = React.useCallback(
|
|
147
|
-
(date: Date | undefined) => {
|
|
148
|
-
if (!isControlled) setInternal(date);
|
|
149
|
-
onValueChange?.(date);
|
|
150
|
-
},
|
|
151
|
-
[isControlled, onValueChange],
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const ctx = React.useMemo<DatePickerContextValue>(
|
|
155
|
-
() => ({
|
|
156
|
-
selected,
|
|
157
|
-
setSelected,
|
|
158
|
-
open,
|
|
159
|
-
setOpen,
|
|
160
|
-
focusedDate,
|
|
161
|
-
setFocusedDate,
|
|
162
|
-
formatDate,
|
|
163
|
-
placeholder,
|
|
164
|
-
min,
|
|
165
|
-
max,
|
|
166
|
-
disabled,
|
|
167
|
-
readOnly,
|
|
168
|
-
ariaInvalid,
|
|
169
|
-
closeOnSelect,
|
|
170
|
-
}),
|
|
171
|
-
[
|
|
172
|
-
selected,
|
|
173
|
-
setSelected,
|
|
174
|
-
open,
|
|
175
|
-
focusedDate,
|
|
176
|
-
formatDate,
|
|
177
|
-
placeholder,
|
|
178
|
-
min,
|
|
179
|
-
max,
|
|
180
|
-
disabled,
|
|
181
|
-
readOnly,
|
|
182
|
-
ariaInvalid,
|
|
183
|
-
closeOnSelect,
|
|
184
|
-
],
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<DatePickerContext.Provider value={ctx}>
|
|
189
|
-
<BasePopover.Root open={open} onOpenChange={setOpen}>
|
|
190
|
-
{children ?? (
|
|
191
|
-
<>
|
|
192
|
-
<DatePickerTrigger className={className} />
|
|
193
|
-
<DatePickerContent container={container}>
|
|
194
|
-
<DatePickerCalendar />
|
|
195
|
-
</DatePickerContent>
|
|
196
|
-
</>
|
|
197
|
-
)}
|
|
198
|
-
</BasePopover.Root>
|
|
199
|
-
</DatePickerContext.Provider>
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/* ───────── DatePickerTrigger ───────── */
|
|
204
|
-
|
|
205
|
-
export interface DatePickerTriggerProps
|
|
206
|
-
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
207
|
-
/**
|
|
208
|
-
* 트리거 본문. 직접 노드를 넘기거나, 함수를 넘기면 현재 상태를 받아 직접 렌더할 수 있다.
|
|
209
|
-
*
|
|
210
|
-
* @example
|
|
211
|
-
* <DatePickerTrigger>
|
|
212
|
-
* {({ formatted, placeholder }) => <span>{formatted ?? placeholder}</span>}
|
|
213
|
-
* </DatePickerTrigger>
|
|
214
|
-
*/
|
|
215
|
-
children?:
|
|
216
|
-
| React.ReactNode
|
|
217
|
-
| ((state: {
|
|
218
|
-
/** 현재 선택된 Date. 미선택 시 `undefined`. */
|
|
219
|
-
value: Date | undefined;
|
|
220
|
-
/** `formatDate`로 포맷된 문자열. 미선택 시 `undefined`. */
|
|
221
|
-
formatted: string | undefined;
|
|
222
|
-
/** DatePicker `placeholder` prop. */
|
|
223
|
-
placeholder: string;
|
|
224
|
-
}) => React.ReactNode);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 캘린더 popover를 여는 트리거 버튼. children에 함수를 넘기면 현재 값/포맷 문자열/placeholder를
|
|
229
|
-
* 받아 직접 렌더할 수 있다.
|
|
230
|
-
*/
|
|
231
|
-
export const DatePickerTrigger = React.forwardRef<HTMLButtonElement, DatePickerTriggerProps>(
|
|
232
|
-
function DatePickerTrigger({ className, children, onClick, ...props }, ref) {
|
|
233
|
-
const ctx = useDatePickerContext("DatePickerTrigger");
|
|
234
|
-
const displayText = ctx.selected ? ctx.formatDate(ctx.selected) : undefined;
|
|
235
|
-
|
|
236
|
-
const renderContent = () => {
|
|
237
|
-
if (typeof children === "function") {
|
|
238
|
-
return children({
|
|
239
|
-
value: ctx.selected,
|
|
240
|
-
formatted: displayText,
|
|
241
|
-
placeholder: ctx.placeholder,
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
if (children !== undefined) return children;
|
|
245
|
-
return (
|
|
246
|
-
<>
|
|
247
|
-
<span
|
|
248
|
-
className={cn(
|
|
249
|
-
datePickerValue,
|
|
250
|
-
!displayText && datePickerPlaceholder,
|
|
251
|
-
)}
|
|
252
|
-
>
|
|
253
|
-
{displayText ?? ctx.placeholder}
|
|
254
|
-
</span>
|
|
255
|
-
<span className={datePickerIcon} aria-hidden>
|
|
256
|
-
<CalendarIcon />
|
|
257
|
-
</span>
|
|
258
|
-
</>
|
|
259
|
-
);
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<BasePopover.Trigger
|
|
264
|
-
ref={ref}
|
|
265
|
-
className={cn(datePickerTrigger, className)}
|
|
266
|
-
disabled={ctx.disabled}
|
|
267
|
-
aria-invalid={ctx.ariaInvalid}
|
|
268
|
-
aria-haspopup="dialog"
|
|
269
|
-
onClick={(e) => {
|
|
270
|
-
if (ctx.readOnly) e.preventDefault();
|
|
271
|
-
onClick?.(e);
|
|
272
|
-
}}
|
|
273
|
-
{...props}
|
|
274
|
-
>
|
|
275
|
-
{renderContent()}
|
|
276
|
-
</BasePopover.Trigger>
|
|
277
|
-
);
|
|
278
|
-
},
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
/* ───────── DatePickerContent ───────── */
|
|
282
|
-
|
|
283
|
-
export interface DatePickerContentProps
|
|
284
|
-
extends Omit<React.ComponentPropsWithoutRef<typeof BasePopover.Popup>, "className"> {
|
|
285
|
-
className?: string;
|
|
286
|
-
/**
|
|
287
|
-
* Trigger와 popover 간격(px).
|
|
288
|
-
* @default 4
|
|
289
|
-
*/
|
|
290
|
-
sideOffset?: React.ComponentPropsWithoutRef<typeof BasePopover.Positioner>["sideOffset"];
|
|
291
|
-
/**
|
|
292
|
-
* Trigger 기준 popover 방향. 공간 부족 시 자동 반대편으로 뒤집힘.
|
|
293
|
-
* @default "bottom"
|
|
294
|
-
*/
|
|
295
|
-
side?: React.ComponentPropsWithoutRef<typeof BasePopover.Positioner>["side"];
|
|
296
|
-
/**
|
|
297
|
-
* Trigger 축에서의 정렬.
|
|
298
|
-
* @default "start"
|
|
299
|
-
*/
|
|
300
|
-
align?: React.ComponentPropsWithoutRef<typeof BasePopover.Positioner>["align"];
|
|
301
|
-
/**
|
|
302
|
-
* Portal이 마운트될 DOM 노드. 토큰 스코프(다크 모드 등) 안에 popover를 띄우려면 해당 컨테이너 ref 전달.
|
|
303
|
-
* @default document.body
|
|
304
|
-
*/
|
|
305
|
-
container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** 캘린더 popover 본문. portal로 마운트되며 `disabled`/`readOnly`이면 렌더되지 않는다. */
|
|
309
|
-
export const DatePickerContent = React.forwardRef<HTMLDivElement, DatePickerContentProps>(
|
|
310
|
-
function DatePickerContent(
|
|
311
|
-
{ className, children, sideOffset = 4, side = "bottom", align = "start", container, ...props },
|
|
312
|
-
ref,
|
|
313
|
-
) {
|
|
314
|
-
const ctx = useDatePickerContext("DatePickerContent");
|
|
315
|
-
if (ctx.disabled || ctx.readOnly) return null;
|
|
316
|
-
|
|
317
|
-
return (
|
|
318
|
-
<BasePopover.Portal container={container}>
|
|
319
|
-
<BasePopover.Positioner
|
|
320
|
-
className={datePickerPositioner}
|
|
321
|
-
sideOffset={sideOffset}
|
|
322
|
-
side={side}
|
|
323
|
-
align={align}
|
|
324
|
-
>
|
|
325
|
-
<BasePopover.Popup
|
|
326
|
-
ref={ref}
|
|
327
|
-
className={cn(datePickerPopup, className)}
|
|
328
|
-
{...props}
|
|
329
|
-
>
|
|
330
|
-
{children}
|
|
331
|
-
</BasePopover.Popup>
|
|
332
|
-
</BasePopover.Positioner>
|
|
333
|
-
</BasePopover.Portal>
|
|
334
|
-
);
|
|
335
|
-
},
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
/* ───────── DatePickerCalendar ───────── */
|
|
339
|
-
|
|
340
|
-
/** 월 단위 날짜 그리드. 화살표 키 이동, Home/End, Enter/Space 선택을 지원한다. */
|
|
341
|
-
export function DatePickerCalendar() {
|
|
342
|
-
const ctx = useDatePickerContext("DatePickerCalendar");
|
|
343
|
-
|
|
344
|
-
const handleSelect = (date: Date | undefined) => {
|
|
345
|
-
ctx.setSelected(date);
|
|
346
|
-
if (date && ctx.closeOnSelect) ctx.setOpen(false);
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
return (
|
|
350
|
-
<Calendar
|
|
351
|
-
mode="single"
|
|
352
|
-
value={ctx.selected}
|
|
353
|
-
onValueChange={handleSelect}
|
|
354
|
-
month={ctx.focusedDate}
|
|
355
|
-
onMonthChange={ctx.setFocusedDate}
|
|
356
|
-
min={ctx.min}
|
|
357
|
-
max={ctx.max}
|
|
358
|
-
/>
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/* ───────── DatePickerFooter ───────── */
|
|
363
|
-
|
|
364
|
-
export interface DatePickerFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
365
|
-
|
|
366
|
-
/** popover 하단 액션 영역. "오늘", "지우기" 같은 커스텀 버튼을 두는 슬롯. */
|
|
367
|
-
export const DatePickerFooter = React.forwardRef<HTMLDivElement, DatePickerFooterProps>(
|
|
368
|
-
function DatePickerFooter({ className, ...props }, ref) {
|
|
369
|
-
return (
|
|
370
|
-
<div
|
|
371
|
-
ref={ref}
|
|
372
|
-
className={cn(datePickerFooter, className)}
|
|
373
|
-
{...props}
|
|
374
|
-
/>
|
|
375
|
-
);
|
|
376
|
-
},
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
/* ───────── useDatePicker (for custom footer actions) ───────── */
|
|
380
|
-
|
|
381
|
-
/** 커스텀 footer 액션에서 값/open 상태를 직접 다룰 때 사용. DatePicker 내부에서만 호출 가능. */
|
|
382
|
-
export function useDatePicker() {
|
|
383
|
-
const ctx = useDatePickerContext("useDatePicker");
|
|
384
|
-
return {
|
|
385
|
-
value: ctx.selected,
|
|
386
|
-
setValue: ctx.setSelected,
|
|
387
|
-
open: ctx.open,
|
|
388
|
-
setOpen: ctx.setOpen,
|
|
389
|
-
focusedDate: ctx.focusedDate,
|
|
390
|
-
setFocusedDate: ctx.setFocusedDate,
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/* ───────── DateRangePicker (단일 컴포넌트, 스코프 외) ───────── */
|
|
395
|
-
|
|
396
|
-
export interface DateRangePickerProps {
|
|
397
|
-
/** 선택된 범위 (controlled). */
|
|
398
|
-
value?: DateRange;
|
|
399
|
-
/** 초기 범위 (uncontrolled). */
|
|
400
|
-
defaultValue?: DateRange;
|
|
401
|
-
/** 범위 변경 콜백. */
|
|
402
|
-
onValueChange?: (range: DateRange | undefined) => void;
|
|
403
|
-
/** 표시 포맷 함수. 기본 YYYY-MM-DD. */
|
|
404
|
-
formatDate?: (date: Date) => string;
|
|
405
|
-
/** 선택 가능 최소 날짜. */
|
|
406
|
-
min?: Date;
|
|
407
|
-
/** 선택 가능 최대 날짜. */
|
|
408
|
-
max?: Date;
|
|
409
|
-
/**
|
|
410
|
-
* 미선택 상태의 트리거 텍스트.
|
|
411
|
-
* @default "시작일 ~ 종료일"
|
|
412
|
-
*/
|
|
413
|
-
placeholder?: string;
|
|
414
|
-
/** 비활성. */
|
|
415
|
-
disabled?: boolean;
|
|
416
|
-
/** 읽기 전용. popover가 열리지 않는다. */
|
|
417
|
-
readOnly?: boolean;
|
|
418
|
-
/** invalid 상태. */
|
|
419
|
-
"aria-invalid"?: boolean | "true";
|
|
420
|
-
className?: string;
|
|
421
|
-
/**
|
|
422
|
-
* Portal이 마운트될 DOM 노드. 토큰 스코프(다크 모드 등) 안에 popover를 띄우려면 해당 컨테이너 ref 전달.
|
|
423
|
-
* @default document.body
|
|
424
|
-
*/
|
|
425
|
-
container?: React.ComponentPropsWithoutRef<typeof BasePopover.Portal>["container"];
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* 시작·종료일을 선택하는 범위 picker. 첫 클릭으로 시작일, 두 번째 클릭으로 종료일이 결정된다.
|
|
430
|
-
* 호버 시 미리보기 범위가 시각화되고 두 번째 선택과 동시에 popover가 닫힌다.
|
|
431
|
-
*/
|
|
432
|
-
export const DateRangePicker = React.forwardRef<HTMLButtonElement, DateRangePickerProps>(
|
|
433
|
-
function DateRangePicker(
|
|
434
|
-
{
|
|
435
|
-
value,
|
|
436
|
-
defaultValue,
|
|
437
|
-
onValueChange,
|
|
438
|
-
formatDate = formatDefault,
|
|
439
|
-
min,
|
|
440
|
-
max,
|
|
441
|
-
placeholder = "시작일 ~ 종료일",
|
|
442
|
-
disabled,
|
|
443
|
-
readOnly,
|
|
444
|
-
"aria-invalid": ariaInvalid,
|
|
445
|
-
className,
|
|
446
|
-
container,
|
|
447
|
-
},
|
|
448
|
-
ref,
|
|
449
|
-
) {
|
|
450
|
-
const isControlled = value !== undefined;
|
|
451
|
-
const [internal, setInternal] = React.useState<DateRange | undefined>(defaultValue);
|
|
452
|
-
const selected = isControlled ? value : internal;
|
|
453
|
-
|
|
454
|
-
const [open, setOpen] = React.useState(false);
|
|
455
|
-
const [calendarMonth, setCalendarMonth] = React.useState<Date>(
|
|
456
|
-
() => selected?.from ?? new Date(),
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
React.useEffect(() => {
|
|
460
|
-
if (open && selected?.from) {
|
|
461
|
-
setCalendarMonth(startOfMonth(selected.from));
|
|
462
|
-
}
|
|
463
|
-
}, [open, selected?.from]);
|
|
464
|
-
|
|
465
|
-
const handleRangeChange = (range: DateRange | undefined) => {
|
|
466
|
-
if (!isControlled) setInternal(range);
|
|
467
|
-
onValueChange?.(range);
|
|
468
|
-
if (range) setOpen(false);
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
const displayText = selected
|
|
472
|
-
? `${formatDate(selected.from)} ~ ${formatDate(selected.to)}`
|
|
473
|
-
: undefined;
|
|
474
|
-
|
|
475
|
-
return (
|
|
476
|
-
<BasePopover.Root open={open} onOpenChange={setOpen}>
|
|
477
|
-
<BasePopover.Trigger
|
|
478
|
-
ref={ref}
|
|
479
|
-
className={cn(datePickerTrigger, className)}
|
|
480
|
-
disabled={disabled}
|
|
481
|
-
aria-invalid={ariaInvalid}
|
|
482
|
-
aria-haspopup="dialog"
|
|
483
|
-
onClick={(e) => {
|
|
484
|
-
if (readOnly) e.preventDefault();
|
|
485
|
-
}}
|
|
486
|
-
>
|
|
487
|
-
<span className={cn(datePickerValue, !displayText && datePickerPlaceholder)}>
|
|
488
|
-
{displayText ?? placeholder}
|
|
489
|
-
</span>
|
|
490
|
-
<span className={datePickerIcon} aria-hidden>
|
|
491
|
-
<CalendarIcon />
|
|
492
|
-
</span>
|
|
493
|
-
</BasePopover.Trigger>
|
|
494
|
-
|
|
495
|
-
{!disabled && !readOnly && (
|
|
496
|
-
<BasePopover.Portal container={container}>
|
|
497
|
-
<BasePopover.Positioner
|
|
498
|
-
className={datePickerPositioner}
|
|
499
|
-
sideOffset={4}
|
|
500
|
-
side="bottom"
|
|
501
|
-
align="start"
|
|
502
|
-
>
|
|
503
|
-
<BasePopover.Popup className={datePickerPopup}>
|
|
504
|
-
<Calendar
|
|
505
|
-
mode="range"
|
|
506
|
-
value={selected}
|
|
507
|
-
onValueChange={handleRangeChange}
|
|
508
|
-
month={calendarMonth}
|
|
509
|
-
onMonthChange={setCalendarMonth}
|
|
510
|
-
min={min}
|
|
511
|
-
max={max}
|
|
512
|
-
/>
|
|
513
|
-
</BasePopover.Popup>
|
|
514
|
-
</BasePopover.Positioner>
|
|
515
|
-
</BasePopover.Portal>
|
|
516
|
-
)}
|
|
517
|
-
</BasePopover.Root>
|
|
518
|
-
);
|
|
519
|
-
},
|
|
520
|
-
);
|