sh-ui-cli 0.46.0 → 0.47.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 (85) hide show
  1. package/data/changelog/versions.json +13 -0
  2. package/data/registry/react/components/accordion/index.module.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.module.css +111 -0
  4. package/data/registry/react/components/avatar/index.module.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.module.css +36 -0
  6. package/data/registry/react/components/badge/index.module.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.module.css +57 -0
  8. package/data/registry/react/components/breadcrumb/index.module.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.module.css +82 -0
  10. package/data/registry/react/components/calendar/index.module.tsx +806 -0
  11. package/data/registry/react/components/calendar/styles.module.css +213 -0
  12. package/data/registry/react/components/carousel/index.module.tsx +430 -0
  13. package/data/registry/react/components/carousel/styles.module.css +155 -0
  14. package/data/registry/react/components/checkbox/index.module.tsx +96 -0
  15. package/data/registry/react/components/checkbox/styles.module.css +75 -0
  16. package/data/registry/react/components/code-editor/index.module.tsx +230 -0
  17. package/data/registry/react/components/code-editor/styles.module.css +76 -0
  18. package/data/registry/react/components/code-panel/index.module.tsx +191 -0
  19. package/data/registry/react/components/code-panel/styles.module.css +124 -0
  20. package/data/registry/react/components/color-picker/index.module.tsx +467 -0
  21. package/data/registry/react/components/color-picker/styles.module.css +166 -0
  22. package/data/registry/react/components/combobox/index.module.tsx +165 -0
  23. package/data/registry/react/components/combobox/styles.module.css +151 -0
  24. package/data/registry/react/components/context-menu/index.module.tsx +251 -0
  25. package/data/registry/react/components/context-menu/styles.module.css +140 -0
  26. package/data/registry/react/components/date-picker/index.module.tsx +520 -0
  27. package/data/registry/react/components/date-picker/styles.module.css +103 -0
  28. package/data/registry/react/components/dialog/index.module.tsx +95 -0
  29. package/data/registry/react/components/dialog/styles.module.css +127 -0
  30. package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
  31. package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
  32. package/data/registry/react/components/file-upload/index.module.tsx +487 -0
  33. package/data/registry/react/components/file-upload/styles.module.css +170 -0
  34. package/data/registry/react/components/form/index.module.tsx +61 -0
  35. package/data/registry/react/components/form/styles.module.css +47 -0
  36. package/data/registry/react/components/header/index.module.tsx +805 -0
  37. package/data/registry/react/components/header/styles.module.css +350 -0
  38. package/data/registry/react/components/label/index.module.tsx +52 -0
  39. package/data/registry/react/components/label/styles.module.css +90 -0
  40. package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
  41. package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
  42. package/data/registry/react/components/menubar/index.module.tsx +32 -0
  43. package/data/registry/react/components/menubar/styles.module.css +45 -0
  44. package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
  45. package/data/registry/react/components/numeric-input/styles.module.css +56 -0
  46. package/data/registry/react/components/page-toc/index.module.tsx +174 -0
  47. package/data/registry/react/components/page-toc/styles.module.css +82 -0
  48. package/data/registry/react/components/pagination/index.module.tsx +269 -0
  49. package/data/registry/react/components/pagination/styles.module.css +105 -0
  50. package/data/registry/react/components/popover/index.module.tsx +113 -0
  51. package/data/registry/react/components/popover/styles.module.css +65 -0
  52. package/data/registry/react/components/progress/index.module.tsx +54 -0
  53. package/data/registry/react/components/progress/styles.module.css +41 -0
  54. package/data/registry/react/components/radio/index.module.tsx +65 -0
  55. package/data/registry/react/components/radio/styles.module.css +80 -0
  56. package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
  57. package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
  58. package/data/registry/react/components/select/index.module.tsx +234 -0
  59. package/data/registry/react/components/select/styles.module.css +193 -0
  60. package/data/registry/react/components/separator/index.module.tsx +46 -0
  61. package/data/registry/react/components/separator/styles.module.css +15 -0
  62. package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
  63. package/data/registry/react/components/sidebar/styles.module.css +502 -0
  64. package/data/registry/react/components/skeleton/index.module.tsx +22 -0
  65. package/data/registry/react/components/skeleton/styles.module.css +24 -0
  66. package/data/registry/react/components/slider/index.module.tsx +298 -0
  67. package/data/registry/react/components/slider/styles.module.css +64 -0
  68. package/data/registry/react/components/spinner/index.module.tsx +38 -0
  69. package/data/registry/react/components/spinner/styles.module.css +37 -0
  70. package/data/registry/react/components/switch/index.module.tsx +39 -0
  71. package/data/registry/react/components/switch/styles.module.css +83 -0
  72. package/data/registry/react/components/tabs/index.module.tsx +91 -0
  73. package/data/registry/react/components/tabs/styles.module.css +148 -0
  74. package/data/registry/react/components/textarea/index.module.tsx +23 -0
  75. package/data/registry/react/components/textarea/styles.module.css +54 -0
  76. package/data/registry/react/components/toast/index.module.tsx +258 -0
  77. package/data/registry/react/components/toast/styles.module.css +290 -0
  78. package/data/registry/react/components/toggle/index.module.tsx +131 -0
  79. package/data/registry/react/components/toggle/styles.module.css +85 -0
  80. package/data/registry/react/components/tooltip/index.module.tsx +83 -0
  81. package/data/registry/react/components/tooltip/styles.module.css +44 -0
  82. package/data/registry/react/registry.json +560 -0
  83. package/package.json +1 -1
  84. package/src/api.d.ts +4 -3
  85. package/src/constants.js +4 -3
@@ -0,0 +1,520 @@
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 styles from "./styles.module.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
+ styles["date-picker__value"],
250
+ !displayText && styles["date-picker__placeholder"],
251
+ )}
252
+ >
253
+ {displayText ?? ctx.placeholder}
254
+ </span>
255
+ <span className={styles["date-picker__icon"]} aria-hidden>
256
+ <CalendarIcon />
257
+ </span>
258
+ </>
259
+ );
260
+ };
261
+
262
+ return (
263
+ <BasePopover.Trigger
264
+ ref={ref}
265
+ className={cn(styles["date-picker__trigger"], 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={styles["date-picker__positioner"]}
321
+ sideOffset={sideOffset}
322
+ side={side}
323
+ align={align}
324
+ >
325
+ <BasePopover.Popup
326
+ ref={ref}
327
+ className={cn(styles["date-picker__popup"], 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(styles["date-picker__footer"], 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(styles["date-picker__trigger"], 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(styles["date-picker__value"], !displayText && styles["date-picker__placeholder"])}>
488
+ {displayText ?? placeholder}
489
+ </span>
490
+ <span className={styles["date-picker__icon"]} aria-hidden>
491
+ <CalendarIcon />
492
+ </span>
493
+ </BasePopover.Trigger>
494
+
495
+ {!disabled && !readOnly && (
496
+ <BasePopover.Portal container={container}>
497
+ <BasePopover.Positioner
498
+ className={styles["date-picker__positioner"]}
499
+ sideOffset={4}
500
+ side="bottom"
501
+ align="start"
502
+ >
503
+ <BasePopover.Popup className={styles["date-picker__popup"]}>
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
+ );
@@ -0,0 +1,103 @@
1
+ /* ── Trigger (input-like) ── */
2
+
3
+ .date-picker__trigger {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ width: 100%;
8
+ height: var(--control-md);
9
+ padding: 0 var(--space-3);
10
+ background: var(--background);
11
+ color: var(--foreground);
12
+ border: 1px solid var(--border);
13
+ border-radius: var(--radius);
14
+ font-family: inherit;
15
+ font-size: var(--text-sm);
16
+ line-height: 1;
17
+ cursor: pointer;
18
+ transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
19
+ -webkit-tap-highlight-color: transparent;
20
+ }
21
+
22
+ @media (hover: none) and (pointer: coarse) {
23
+ .date-picker__trigger {
24
+ height: 2.75rem;
25
+ font-size: var(--text-base);
26
+ }
27
+ }
28
+
29
+ .date-picker__trigger:hover:not(:disabled) {
30
+ border-color: var(--border-strong);
31
+ }
32
+
33
+ .date-picker__trigger:focus-visible {
34
+ outline: none;
35
+ border-color: var(--foreground);
36
+ box-shadow: 0 0 0 1px var(--foreground);
37
+ }
38
+
39
+ .date-picker__trigger:disabled {
40
+ opacity: var(--opacity-disabled);
41
+ cursor: not-allowed;
42
+ background: var(--background-subtle);
43
+ }
44
+
45
+ .date-picker__trigger[aria-invalid="true"] {
46
+ border-color: var(--danger);
47
+ }
48
+ .date-picker__trigger[aria-invalid="true"]:focus-visible {
49
+ box-shadow: 0 0 0 1px var(--danger);
50
+ }
51
+
52
+ .date-picker__value {
53
+ overflow: hidden;
54
+ text-overflow: ellipsis;
55
+ white-space: nowrap;
56
+ }
57
+
58
+ .date-picker__placeholder {
59
+ color: var(--foreground-subtle);
60
+ }
61
+
62
+ .date-picker__icon {
63
+ flex-shrink: 0;
64
+ display: inline-flex;
65
+ color: var(--foreground-muted);
66
+ margin-left: var(--space-2);
67
+ }
68
+
69
+ /* ── Positioner / Popup ── */
70
+
71
+ .date-picker__positioner {
72
+ z-index: var(--z-popover);
73
+ outline: none;
74
+ }
75
+
76
+ .date-picker__popup {
77
+ background: var(--background);
78
+ border: 1px solid var(--border);
79
+ border-radius: var(--radius);
80
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
81
+ outline: none;
82
+ padding: var(--space-3);
83
+ transform-origin: var(--transform-origin);
84
+ transition: opacity 140ms ease, transform 140ms ease;
85
+ }
86
+
87
+ .date-picker__popup[data-starting-style],
88
+ .date-picker__popup[data-ending-style] {
89
+ opacity: 0;
90
+ transform: scale(0.96);
91
+ }
92
+
93
+ /* ── Footer ── */
94
+
95
+ .date-picker__footer {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: flex-end;
99
+ gap: var(--space-2);
100
+ margin-top: var(--space-2);
101
+ padding-top: var(--space-2);
102
+ border-top: 1px solid var(--border);
103
+ }