@olundot/ui 0.23.0 → 0.24.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @olundot/ui
2
2
 
3
+ ## 0.24.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 052cb2d: DatePicker 기간 고르기를 다듬고 기간 이동을 더했습니다.
8
+
9
+ - 1024px 이상 `day` 기간 팝오버는 두 달을 나란히 보입니다(폭 558px). 이전 달, 다음 달 버튼은 양 끝에 하나씩이고 한 달씩 넘깁니다. 키보드는 두 달 사이를 그대로 넘습니다.
10
+ - `day` 기간에도 끝 버튼 「시작일」「종료일」을 둡니다. 누른 끝만 고칩니다. 두 끝이 찬 뒤 시작을 고르면 종료는 그대로 두고 반영합니다. 1024px 미만 시트에도 보입니다.
11
+ - 시작을 고른 뒤 가리킨 날(마우스, 키보드 포커스)까지 범위를 옅게 미리 보입니다.
12
+ - 새 prop `lockStart`(시작 끝 잠금)와 `endMin`(종료 전용 하한)을 더했습니다.
13
+ - 새 prop `onStep`, `prevStepDisabled`, `nextStepDisabled`: 칸 양옆에 「이전 기간」「다음 기간」 버튼을 두고, 옮길 폭은 사용하는 쪽이 정합니다.
14
+
15
+ ## 0.23.1
16
+
17
+ ### Patch Changes
18
+
19
+ - e2448e1: `BulkActionBar`의 작업 줄이 맨 앞 작업만 남겨도 카드에 들어가지 않을 때, 이동 버튼이 보이면 키 표시를 먼저 숨긴다. 그래도 넘치면 맨 앞 작업도 더보기 메뉴로 옮긴다. 375px 폭에서 이동 버튼이 카드 왼쪽 밖으로 나가던 문제를 고친다. 키 표시를 숨겨도 라벨, 접근 이름, `aria-keyshortcuts`는 그대로다. 이동 버튼이 보이지 않을 때는 기존처럼 맨 앞 작업 하나를 남긴다.
20
+
3
21
  ## 0.23.0
4
22
 
5
23
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -66,7 +66,7 @@ type GhostDefaultActionStyle = {
66
66
  declare const buttonCva: (props?: ({
67
67
  appearance?: "filled" | "outline" | "ghost" | "link" | null | undefined;
68
68
  tone?: "primary" | "neutral" | "danger" | null | undefined;
69
- size?: "default" | "md" | "xs" | "sm" | "lg" | "xl" | null | undefined;
69
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | "default" | null | undefined;
70
70
  } & class_variance_authority_types.ClassProp) | undefined) => string;
71
71
  type ButtonVariantsOptions = Parameters<typeof buttonCva>[0];
72
72
  declare function buttonVariants(options?: ButtonVariantsOptions): string;
@@ -101,13 +101,13 @@ type CopyButtonProps = Omit<ButtonProps, 'appearance' | 'children' | 'loading' |
101
101
  declare const CopyButton: React.ForwardRefExoticComponent<CopyButtonProps & React.RefAttributes<HTMLButtonElement>>;
102
102
 
103
103
  declare const inputVariants: (props?: ({
104
- size?: "md" | "sm" | "lg" | null | undefined;
104
+ size?: "sm" | "md" | "lg" | null | undefined;
105
105
  fullWidth?: boolean | null | undefined;
106
106
  surface?: SurfaceLevel | null | undefined;
107
107
  } & class_variance_authority_types.ClassProp) | undefined) => string;
108
108
  declare const inputMessageVariants: (props?: ({
109
- state?: "default" | "error" | "success" | null | undefined;
110
- size?: "md" | "sm" | "lg" | null | undefined;
109
+ state?: "error" | "success" | "default" | null | undefined;
110
+ size?: "sm" | "md" | "lg" | null | undefined;
111
111
  } & class_variance_authority_types.ClassProp) | undefined) => string;
112
112
  type InputState = NonNullable<VariantProps<typeof inputMessageVariants>["state"]>;
113
113
  /** Canonical size type (0.2.0+). Use `size` prop with these values. */
@@ -155,11 +155,11 @@ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "
155
155
  declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
156
156
 
157
157
  declare const fileUploadZoneVariants: (props?: ({
158
- size?: "default" | "compact" | null | undefined;
158
+ size?: "compact" | "default" | null | undefined;
159
159
  surface?: SurfaceLevel | null | undefined;
160
160
  } & class_variance_authority_types.ClassProp) | undefined) => string;
161
161
  declare const fileUploadIconVariants: (props?: ({
162
- size?: "default" | "compact" | null | undefined;
162
+ size?: "compact" | "default" | null | undefined;
163
163
  } & class_variance_authority_types.ClassProp) | undefined) => string;
164
164
  interface FileUploadZoneProps extends Omit<React.LabelHTMLAttributes<HTMLLabelElement>, "onChange">, VariantProps<typeof fileUploadZoneVariants> {
165
165
  accept?: string;
@@ -203,12 +203,12 @@ interface InlineAlertProps extends Omit<HTMLAttributes<HTMLDivElement>, 'childre
203
203
  declare const InlineAlert: React.ForwardRefExoticComponent<InlineAlertProps & React.RefAttributes<HTMLDivElement>>;
204
204
 
205
205
  declare const textareaVariants: (props?: ({
206
- size?: "md" | "sm" | "lg" | null | undefined;
206
+ size?: "sm" | "md" | "lg" | null | undefined;
207
207
  fullWidth?: boolean | null | undefined;
208
208
  surface?: SurfaceLevel | null | undefined;
209
209
  } & class_variance_authority_types.ClassProp) | undefined) => string;
210
210
  declare const textareaMessageVariants: (props?: ({
211
- state?: "default" | "error" | "success" | null | undefined;
211
+ state?: "error" | "success" | "default" | null | undefined;
212
212
  } & class_variance_authority_types.ClassProp) | undefined) => string;
213
213
  type TextareaState = NonNullable<VariantProps<typeof textareaMessageVariants>["state"]>;
214
214
  /** Canonical size type (0.2.0+). Use `size` prop with these values. */
@@ -257,14 +257,14 @@ interface TextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaEl
257
257
  declare const Textarea: React.ForwardRefExoticComponent<TextareaProps & React.RefAttributes<HTMLTextAreaElement>>;
258
258
 
259
259
  declare const selectVariants: (props?: ({
260
- size?: "md" | "sm" | "lg" | null | undefined;
260
+ size?: "sm" | "md" | "lg" | null | undefined;
261
261
  fullWidth?: boolean | null | undefined;
262
262
  appearance?: "inline" | "field" | null | undefined;
263
263
  surface?: SurfaceLevel | null | undefined;
264
264
  } & class_variance_authority_types.ClassProp) | undefined) => string;
265
265
  declare const selectMessageVariants: (props?: ({
266
- state?: "default" | "error" | "success" | null | undefined;
267
- size?: "md" | "sm" | "lg" | null | undefined;
266
+ state?: "error" | "success" | "default" | null | undefined;
267
+ size?: "sm" | "md" | "lg" | null | undefined;
268
268
  } & class_variance_authority_types.ClassProp) | undefined) => string;
269
269
  type SelectState = NonNullable<VariantProps<typeof selectMessageVariants>["state"]>;
270
270
  /** Canonical size type (0.2.0+). Use `size` prop with these values. */
@@ -396,6 +396,15 @@ interface DatePickerBaseProps {
396
396
  className?: string;
397
397
  "aria-label"?: string;
398
398
  "aria-describedby"?: string;
399
+ /**
400
+ * Shows "이전 기간" and "다음 기간" buttons beside the field, for views and filters that page
401
+ * through periods. Called with -1 or 1; the caller moves the value by its own step.
402
+ */
403
+ onStep?: (direction: -1 | 1) => void;
404
+ /** Disables "이전 기간". */
405
+ prevStepDisabled?: boolean;
406
+ /** Disables "다음 기간" (the next period would pass `max`, for example). */
407
+ nextStepDisabled?: boolean;
399
408
  }
400
409
  interface DatePickerSingleProps extends DatePickerBaseProps {
401
410
  selectionMode?: "single";
@@ -403,6 +412,8 @@ interface DatePickerSingleProps extends DatePickerBaseProps {
403
412
  value: string;
404
413
  onChange: (value: string) => void;
405
414
  maxDays?: never;
415
+ lockStart?: never;
416
+ endMin?: never;
406
417
  }
407
418
  interface DatePickerRangeProps extends DatePickerBaseProps {
408
419
  selectionMode: "range";
@@ -412,6 +423,10 @@ interface DatePickerRangeProps extends DatePickerBaseProps {
412
423
  onChange: (value: DateRangeValue) => void;
413
424
  /** Most days the range spans, the start day included. */
414
425
  maxDays?: number;
426
+ /** Keeps the start as it is: its end button is disabled and only the end is picked. Give the reason in `description`. */
427
+ lockStart?: boolean;
428
+ /** Earliest end, in the value format. Unlike `min`, it leaves the start alone. With it, an earlier end day no longer swaps the ends. */
429
+ endMin?: string;
415
430
  }
416
431
  type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
417
432
  declare function DatePicker(props: DatePickerProps): react_jsx_runtime.JSX.Element;
@@ -1396,7 +1411,8 @@ interface BulkActionBarProps extends BulkActionBarBaseProps {
1396
1411
  * with the clear (✕) button on its right, `summary` under it at full width, then
1397
1412
  * one line of outline sm Buttons: the move button back to the list at the left (shown
1398
1413
  * only while keyboard focus is in the card), the actions at the right. A line that does
1399
- * not fit folds actions into a ⋯ menu from the end. `variant: "destructive"` is outline danger. The card is
1414
+ * not fit folds actions into a ⋯ menu from the end, then hides the move button's key caps, then
1415
+ * folds the first action too. `variant: "destructive"` is outline danger. The card is
1400
1416
  * min(360px, its slot − 32px) wide; while it is near the viewport bottom the document root
1401
1417
  * carries `--olun-bulk-bar-offset` (viewport bottom → card top) for toasts to clear it.
1402
1418
  * The card is a named region around a toolbar; DOM order is count → move → actions → ⋯ → ✕.
@@ -1773,7 +1789,7 @@ interface StatusDotProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'ch
1773
1789
  declare const StatusDot: React.ForwardRefExoticComponent<StatusDotProps & React.RefAttributes<HTMLSpanElement>>;
1774
1790
 
1775
1791
  declare const notificationBadgeVariants: (props?: ({
1776
- size?: "md" | "sm" | null | undefined;
1792
+ size?: "sm" | "md" | null | undefined;
1777
1793
  placement?: "none" | "inline" | "corner" | null | undefined;
1778
1794
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1779
1795
  type NotificationBadgeSize = NonNullable<VariantProps<typeof notificationBadgeVariants>['size']>;
@@ -1786,13 +1802,13 @@ declare const NotificationBadge: React.ForwardRefExoticComponent<NotificationBad
1786
1802
 
1787
1803
  declare const chipVariants: (props?: ({
1788
1804
  variant?: "filter" | "choice" | "removable" | "status-filter" | null | undefined;
1789
- tone?: "accent" | "neutral" | "danger" | "success" | "warning" | "muted" | null | undefined;
1790
- size?: "md" | "sm" | "lg" | null | undefined;
1805
+ tone?: "muted" | "accent" | "warning" | "success" | "neutral" | "danger" | null | undefined;
1806
+ size?: "sm" | "md" | "lg" | null | undefined;
1791
1807
  pressed?: boolean | null | undefined;
1792
1808
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1793
1809
  declare const chipCountVariants: (props?: ({
1794
- tone?: "accent" | "neutral" | "danger" | "success" | "warning" | "muted" | null | undefined;
1795
- size?: "md" | "sm" | "lg" | null | undefined;
1810
+ tone?: "muted" | "accent" | "warning" | "success" | "neutral" | "danger" | null | undefined;
1811
+ size?: "sm" | "md" | "lg" | null | undefined;
1796
1812
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1797
1813
  type ChipVariant = NonNullable<VariantProps<typeof chipVariants>["variant"]>;
1798
1814
  type ChipTone = NonNullable<VariantProps<typeof chipVariants>["tone"]>;
@@ -1806,7 +1822,7 @@ interface ChipProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,
1806
1822
  declare const Chip: React.ForwardRefExoticComponent<ChipProps & React.RefAttributes<HTMLButtonElement>>;
1807
1823
 
1808
1824
  declare const emptyStateBaseVariants: (props?: ({
1809
- size?: "md" | "sm" | "lg" | null | undefined;
1825
+ size?: "sm" | "md" | "lg" | null | undefined;
1810
1826
  bordered?: boolean | null | undefined;
1811
1827
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1812
1828
  type EmptyStateTone = "neutral" | "muted" | "raised";
@@ -1818,7 +1834,7 @@ interface EmptyStateVariantOptions extends VariantProps<typeof emptyStateBaseVar
1818
1834
  }
1819
1835
  declare function emptyStateVariants({ size, tone, surface, bordered, className, }?: EmptyStateVariantOptions): string;
1820
1836
  declare const emptyStateIconVariants: (props?: ({
1821
- size?: "md" | "sm" | "lg" | null | undefined;
1837
+ size?: "sm" | "md" | "lg" | null | undefined;
1822
1838
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1823
1839
  type EmptyStateSize = NonNullable<VariantProps<typeof emptyStateBaseVariants>["size"]>;
1824
1840
  interface EmptyStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">, VariantProps<typeof emptyStateBaseVariants> {
@@ -1844,7 +1860,7 @@ interface EmptyStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "ti
1844
1860
  declare const EmptyState: React.ForwardRefExoticComponent<EmptyStateProps & React.RefAttributes<HTMLDivElement>>;
1845
1861
 
1846
1862
  declare const errorStateVariants: (props?: ({
1847
- size?: "md" | "sm" | "lg" | null | undefined;
1863
+ size?: "sm" | "md" | "lg" | null | undefined;
1848
1864
  surface?: SurfaceLevel | null | undefined;
1849
1865
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1850
1866
  interface ErrorStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">, VariantProps<typeof errorStateVariants> {
@@ -1864,7 +1880,7 @@ declare const skeletonVariants: (props?: ({
1864
1880
  shape?: "text" | "block" | "circle" | null | undefined;
1865
1881
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1866
1882
  declare const spinnerVariants: (props?: ({
1867
- size?: "md" | "sm" | "lg" | 16 | 24 | 32 | null | undefined;
1883
+ size?: "sm" | "md" | "lg" | 16 | 24 | 32 | null | undefined;
1868
1884
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1869
1885
  interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof skeletonVariants> {
1870
1886
  }
@@ -1897,7 +1913,7 @@ interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement> {
1897
1913
  declare const Spinner: React.ForwardRefExoticComponent<SpinnerProps & React.RefAttributes<HTMLSpanElement>>;
1898
1914
 
1899
1915
  declare const iconButtonCva: (props?: ({
1900
- size?: "md" | "xs" | "sm" | "lg" | "xl" | null | undefined;
1916
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
1901
1917
  pressed?: boolean | null | undefined;
1902
1918
  appearance?: "filled" | "outline" | "ghost" | null | undefined;
1903
1919
  tone?: "primary" | "neutral" | "danger" | null | undefined;
@@ -1906,7 +1922,7 @@ type IconButtonSize = NonNullable<VariantProps<typeof iconButtonCva>["size"]>;
1906
1922
  type IconButtonVariantsOptions = NonNullable<Parameters<typeof iconButtonCva>[0]>;
1907
1923
  declare function iconButtonVariants(options?: IconButtonVariantsOptions): string;
1908
1924
  declare const iconButtonBadgeVariants: (props?: ({
1909
- tone?: "error" | "success" | "info" | null | undefined;
1925
+ tone?: "error" | "info" | "success" | null | undefined;
1910
1926
  } & class_variance_authority_types.ClassProp) | undefined) => string;
1911
1927
  type IconButtonBadgeTone = NonNullable<VariantProps<typeof iconButtonBadgeVariants>["tone"]>;
1912
1928
  type IconButtonIcon = React.ComponentType<React.SVGProps<SVGSVGElement> & {
@@ -2169,7 +2185,7 @@ interface ActionFooterProps extends Omit<React.HTMLAttributes<HTMLDivElement>, '
2169
2185
  declare const ActionFooter: React.ForwardRefExoticComponent<ActionFooterProps & React.RefAttributes<HTMLDivElement>>;
2170
2186
 
2171
2187
  declare const toastVariants: (props?: ({
2172
- variant?: "default" | "error" | "success" | "info" | "warning" | null | undefined;
2188
+ variant?: "error" | "warning" | "info" | "success" | "default" | null | undefined;
2173
2189
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2174
2190
  declare const toastViewportVariants: (props?: class_variance_authority_types.ClassProp | undefined) => string;
2175
2191
  type ToastVariant = NonNullable<VariantProps<typeof toastVariants>['variant']>;
@@ -2223,19 +2239,19 @@ interface ToastProviderProps extends React.HTMLAttributes<HTMLDivElement> {
2223
2239
  declare function ToastProvider({ children, maxToasts, className, ...props }: ToastProviderProps): react_jsx_runtime.JSX.Element;
2224
2240
 
2225
2241
  declare const otpFieldVariants: (props?: ({
2226
- size?: "default" | "compact" | null | undefined;
2242
+ size?: "compact" | "default" | null | undefined;
2227
2243
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2228
2244
  declare const otpFieldGroupVariants: (props?: ({
2229
- size?: "default" | "compact" | null | undefined;
2245
+ size?: "compact" | "default" | null | undefined;
2230
2246
  fluid?: boolean | null | undefined;
2231
2247
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2232
2248
  declare const otpCellVariants: (props?: ({
2233
- size?: "default" | "compact" | null | undefined;
2249
+ size?: "compact" | "default" | null | undefined;
2234
2250
  fluid?: boolean | null | undefined;
2235
2251
  surface?: SurfaceLevel | null | undefined;
2236
2252
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2237
2253
  declare const otpFieldMessageVariants: (props?: ({
2238
- state?: "default" | "error" | null | undefined;
2254
+ state?: "error" | "default" | null | undefined;
2239
2255
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2240
2256
  type OTPFieldSize = NonNullable<VariantProps<typeof otpCellVariants>["size"]>;
2241
2257
  interface OTPFieldProps extends Omit<React.FieldsetHTMLAttributes<HTMLFieldSetElement>, "onChange" | "children" | "defaultValue" | "value">, VariantProps<typeof otpFieldVariants> {
@@ -2262,10 +2278,10 @@ interface OTPFieldProps extends Omit<React.FieldsetHTMLAttributes<HTMLFieldSetEl
2262
2278
  declare const OTPField: React.ForwardRefExoticComponent<OTPFieldProps & React.RefAttributes<HTMLFieldSetElement>>;
2263
2279
 
2264
2280
  declare const searchFieldVariants: (props?: ({
2265
- size?: "md" | "sm" | "lg" | null | undefined;
2281
+ size?: "sm" | "md" | "lg" | null | undefined;
2266
2282
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2267
2283
  declare const searchFieldInputVariants: (props?: ({
2268
- size?: "md" | "sm" | "lg" | null | undefined;
2284
+ size?: "sm" | "md" | "lg" | null | undefined;
2269
2285
  hasClear?: boolean | null | undefined;
2270
2286
  surface?: SurfaceLevel | null | undefined;
2271
2287
  } & class_variance_authority_types.ClassProp) | undefined) => string;
@@ -2289,7 +2305,7 @@ declare const checkboxVariants: (props?: ({
2289
2305
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2290
2306
  declare const checkboxRootVariants: (props?: class_variance_authority_types.ClassProp | undefined) => string;
2291
2307
  declare const checkboxBoxVariants: (props?: ({
2292
- size?: "md" | "sm" | null | undefined;
2308
+ size?: "sm" | "md" | null | undefined;
2293
2309
  } & class_variance_authority_types.ClassProp) | undefined) => string;
2294
2310
  declare const checkboxGroupItemVariants: (props?: ({
2295
2311
  appearance?: "card" | "plain" | null | undefined;
package/dist/index.js CHANGED
@@ -1887,7 +1887,7 @@ Select.displayName = "Select";
1887
1887
 
1888
1888
  // src/components/ui/date-picker.tsx
1889
1889
  import * as React19 from "react";
1890
- import { AlertCircle as AlertCircle4, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react";
1890
+ import { AlertCircle as AlertCircle4, Calendar as CalendarIcon, ChevronLeft, ChevronRight, Lock } from "lucide-react";
1891
1891
 
1892
1892
  // src/components/ui/dialog.tsx
1893
1893
  import * as React17 from "react";
@@ -3704,7 +3704,7 @@ function formatRangeParts(value, granularity, now) {
3704
3704
  import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
3705
3705
  var WEEKDAYS = ["\uC77C", "\uC6D4", "\uD654", "\uC218", "\uBAA9", "\uAE08", "\uD1A0"];
3706
3706
  var HOURS = Array.from({ length: 24 }, (_, hour) => hour);
3707
- var POPOVER_WIDTH = { day: 280, minute: 424 };
3707
+ var POPOVER_WIDTH = { day: 280, dayRange: 558, minute: 424 };
3708
3708
  var COLLISION_PADDING = 8;
3709
3709
  var pad22 = (n) => String(n).padStart(2, "0");
3710
3710
  var isoDay = (parts) => `${parts.year}-${pad22(parts.month)}-${pad22(parts.day)}`;
@@ -3742,11 +3742,17 @@ function DatePicker(props) {
3742
3742
  id,
3743
3743
  className,
3744
3744
  "aria-label": ariaLabel,
3745
- "aria-describedby": ariaDescribedBy
3745
+ "aria-describedby": ariaDescribedBy,
3746
+ onStep,
3747
+ prevStepDisabled = false,
3748
+ nextStepDisabled = false
3746
3749
  } = props;
3747
3750
  const range = props.selectionMode === "range";
3748
3751
  const minute = granularity === "minute";
3749
3752
  const maxDays = range ? props.maxDays : void 0;
3753
+ const lockStart = range && Boolean(props.lockStart);
3754
+ const endMinValue = range ? props.endMin : void 0;
3755
+ const popoverKind = minute ? "minute" : range ? "dayRange" : "day";
3750
3756
  const field = useFormFieldControl();
3751
3757
  const generatedId = React19.useId();
3752
3758
  const baseId = `date-picker-${generatedId.replace(/:/g, "")}`;
@@ -3760,14 +3766,18 @@ function DatePicker(props) {
3760
3766
  const parts = parseValue(max, granularity);
3761
3767
  return parts && !minute ? { ...parts, hour: 23, minute: 59 } : parts;
3762
3768
  }, [max, granularity, minute]);
3769
+ const endMinParts = React19.useMemo(() => parseValue(endMinValue, granularity), [endMinValue, granularity]);
3770
+ const swapEnds = !minute && !lockStart && !endMinParts;
3763
3771
  const [open, setOpen] = React19.useState(false);
3764
3772
  const [layer, setLayer] = React19.useState("popover");
3765
3773
  const [align, setAlign] = React19.useState("start");
3766
3774
  const [draft, setDraft] = React19.useState({ from: "", to: "" });
3767
- const [activeEnd, setActiveEnd] = React19.useState("from");
3775
+ const [pickedEnd, setActiveEnd] = React19.useState("from");
3776
+ const activeEnd = lockStart ? "to" : pickedEnd;
3768
3777
  const [timeOpen, setTimeOpen] = React19.useState(false);
3769
3778
  const [view, setView] = React19.useState(() => ({ year: 2e3, month: 1 }));
3770
3779
  const [focusDay, setFocusDay] = React19.useState(() => todayParts(/* @__PURE__ */ new Date()));
3780
+ const [pointDay, setPointDay] = React19.useState(null);
3771
3781
  const triggerRef = React19.useRef(null);
3772
3782
  const [outerLabel, setOuterLabel] = React19.useState(void 0);
3773
3783
  const [windowName, setWindowName] = React19.useState(void 0);
@@ -3807,7 +3817,7 @@ function DatePicker(props) {
3807
3817
  setOuterLabel(labelTextOf(triggerRef.current));
3808
3818
  }, [ariaLabel, label, field, triggerId]);
3809
3819
  function startView() {
3810
- const anchor = range ? parseValue(rangeValue.from, granularity) ?? parseValue(rangeValue.to, granularity) : parseValue(singleValue, granularity);
3820
+ const anchor = range ? (lockStart ? parseValue(rangeValue.to, granularity) : null) ?? parseValue(rangeValue.from, granularity) ?? parseValue(rangeValue.to, granularity) : parseValue(singleValue, granularity);
3811
3821
  return anchor ?? parseValue(initialMonth, granularity) ?? todayParts(/* @__PURE__ */ new Date());
3812
3822
  }
3813
3823
  function handleOpenChange(next) {
@@ -3819,13 +3829,14 @@ function DatePicker(props) {
3819
3829
  setDraft(rangeValue);
3820
3830
  setActiveEnd(range && rangeValue.from && !rangeValue.to ? "to" : "from");
3821
3831
  setTimeOpen(false);
3832
+ setPointDay(null);
3822
3833
  followed.current = rangeValue;
3823
3834
  lastEmitted.current = null;
3824
3835
  setWindowName(currentWindowName());
3825
3836
  const wide = isWideViewport();
3826
3837
  setLayer(wide ? "popover" : "sheet");
3827
3838
  const box = triggerRef.current?.getBoundingClientRect();
3828
- const width = POPOVER_WIDTH[granularity];
3839
+ const width = POPOVER_WIDTH[popoverKind];
3829
3840
  setAlign(box && typeof window !== "undefined" && box.left + width > window.innerWidth - COLLISION_PADDING ? "end" : "start");
3830
3841
  }
3831
3842
  setOpen(next);
@@ -3834,13 +3845,25 @@ function DatePicker(props) {
3834
3845
  const toParts = parseValue(shownRange.to, granularity);
3835
3846
  const singleParts = parseValue(singleValue, granularity);
3836
3847
  const editing = range ? activeEnd === "from" ? fromParts : toParts : singleParts;
3837
- const editMin = range && activeEnd === "to" && fromParts && minute ? minParts && minuteKey(minParts) > minuteKey(fromParts) ? minParts : fromParts : minParts;
3848
+ const editMin = range && activeEnd === "to" ? [minParts, minute ? fromParts : null, endMinParts].reduce(
3849
+ (latest, parts) => parts && (!latest || minuteKey(parts) > minuteKey(latest)) ? parts : latest,
3850
+ null
3851
+ ) : minParts;
3852
+ const editMax = (() => {
3853
+ if (!range || activeEnd !== "to" || !fromParts || maxDays === void 0) return maxParts;
3854
+ const last = { ...addDays(fromParts, maxDays - 1), hour: 23, minute: 59 };
3855
+ return maxParts && minuteKey(maxParts) < minuteKey(last) ? maxParts : last;
3856
+ })();
3838
3857
  function commitRange(next, edited) {
3839
3858
  const from = parseValue(next.from, granularity);
3840
3859
  const to = parseValue(next.to, granularity);
3860
+ if (edited === "to" && to && (editMin && minuteKey(to) < minuteKey(editMin) || editMax && minuteKey(to) > minuteKey(editMax))) {
3861
+ return draft;
3862
+ }
3841
3863
  let value = next;
3842
3864
  if (from && to && (minuteKey(from) > minuteKey(to) || maxDays !== void 0 && daysBetween(from, to) > maxDays - 1)) {
3843
3865
  const cleared = edited === "from" ? "to" : "from";
3866
+ if (lockStart && cleared === "from") return draft;
3844
3867
  value = cleared === "to" ? { from: next.from, to: "" } : { from: "", to: next.to };
3845
3868
  setActiveEnd(cleared);
3846
3869
  }
@@ -3859,16 +3882,11 @@ function DatePicker(props) {
3859
3882
  function dayDisabled(cell) {
3860
3883
  if (minParts && dayKey(cell) < dayKey(minParts)) return true;
3861
3884
  if (maxParts && dayKey(cell) > dayKey(maxParts)) return true;
3862
- if (!range) return false;
3863
- if (minute) {
3864
- if (activeEnd === "to" && fromParts) {
3865
- if (dayKey(cell) < dayKey(fromParts)) return true;
3866
- if (maxDays !== void 0 && daysBetween(fromParts, cell) > maxDays - 1) return true;
3867
- }
3868
- return false;
3869
- }
3870
- if (fromParts && !toParts && maxDays !== void 0) return daysBetween(fromParts, cell) > maxDays - 1;
3871
- return false;
3885
+ if (!range || activeEnd !== "to") return false;
3886
+ if (endMinParts && dayKey(cell) < dayKey(endMinParts)) return true;
3887
+ if (!fromParts) return false;
3888
+ if (!swapEnds && dayKey(cell) < dayKey(fromParts)) return true;
3889
+ return maxDays !== void 0 && daysBetween(fromParts, cell) > maxDays - 1;
3872
3890
  }
3873
3891
  function selectDay(cell) {
3874
3892
  if (dayDisabled(cell)) return;
@@ -3879,13 +3897,16 @@ function DatePicker(props) {
3879
3897
  return;
3880
3898
  }
3881
3899
  if (!minute) {
3882
- if (!fromParts || toParts) {
3883
- setDraft({ from: formatValue(cell, granularity), to: "" });
3900
+ const picked = formatValue(cell, granularity);
3901
+ const filled = Boolean(fromParts && toParts);
3902
+ const next = activeEnd === "from" ? { from: picked, to: draft.to } : swapEnds && fromParts && dayKey(cell) < dayKey(fromParts) ? { from: picked, to: draft.from } : { from: draft.from, to: picked };
3903
+ const value = commitRange(next, activeEnd);
3904
+ if (!value.from || !value.to) {
3905
+ setActiveEnd(value.from ? "to" : "from");
3884
3906
  return;
3885
3907
  }
3886
- const picked = formatValue(cell, granularity);
3887
- commitRange(dayKey(cell) < dayKey(fromParts) ? { from: picked, to: draft.from } : { from: draft.from, to: picked }, "to");
3888
- setOpen(false);
3908
+ if (activeEnd === "to" || !filled) setOpen(false);
3909
+ else setActiveEnd("to");
3889
3910
  return;
3890
3911
  }
3891
3912
  if (activeEnd === "from") {
@@ -3893,7 +3914,7 @@ function DatePicker(props) {
3893
3914
  if (!commitRange({ from: formatValue(nextFrom, granularity), to: draft.to }, "from").to) setActiveEnd("to");
3894
3915
  return;
3895
3916
  }
3896
- const nextTo = clampParts({ ...baseFor("to"), year: cell.year, month: cell.month, day: cell.day }, editMin, maxParts);
3917
+ const nextTo = clampParts({ ...baseFor("to"), year: cell.year, month: cell.month, day: cell.day }, editMin, editMax);
3897
3918
  commitRange({ from: draft.from, to: formatValue(nextTo, granularity) }, "to");
3898
3919
  }
3899
3920
  function changeTime(patch) {
@@ -3901,9 +3922,18 @@ function DatePicker(props) {
3901
3922
  props.onChange(formatValue(clampParts({ ...baseFor(null), ...patch }, minParts, maxParts), granularity));
3902
3923
  return;
3903
3924
  }
3904
- const next = formatValue(clampParts({ ...baseFor(activeEnd), ...patch }, editMin, maxParts), granularity);
3925
+ const next = formatValue(clampParts({ ...baseFor(activeEnd), ...patch }, editMin, editMax), granularity);
3905
3926
  commitRange(activeEnd === "from" ? { from: next, to: draft.to } : { from: draft.from, to: next }, activeEnd);
3906
3927
  }
3928
+ function activateEnd(end, kind) {
3929
+ setActiveEnd(end);
3930
+ const parts = end === "from" ? fromParts : toParts;
3931
+ if (!parts) return;
3932
+ const shown = kind === "popover" && popoverKind === "dayRange" ? 2 : 1;
3933
+ const offset = (parts.year - view.year) * 12 + parts.month - view.month;
3934
+ if (offset < 0 || offset >= shown) setView({ year: parts.year, month: parts.month });
3935
+ setFocusDay({ ...parts, hour: 0, minute: 0 });
3936
+ }
3907
3937
  const endPlaceholder = minute ? "\uC885\uB8CC \uC77C\uC2DC" : "\uC885\uB8CC\uC77C";
3908
3938
  let summary;
3909
3939
  let summaryText = "";
@@ -3995,11 +4025,25 @@ function DatePicker(props) {
3995
4025
  }
3996
4026
  );
3997
4027
  const today = todayParts(now);
4028
+ const preview = (() => {
4029
+ if (!range || !pointDay || dayDisabled(pointDay)) return null;
4030
+ const edited = activeEnd === "from" ? fromParts : toParts;
4031
+ if (edited && sameDay(pointDay, edited)) return null;
4032
+ if (activeEnd === "to" && fromParts) {
4033
+ return dayKey(pointDay) < dayKey(fromParts) ? { from: pointDay, to: fromParts, kept: fromParts } : { from: fromParts, to: pointDay, kept: fromParts };
4034
+ }
4035
+ if (activeEnd === "from" && toParts && dayKey(pointDay) <= dayKey(toParts) && (maxDays === void 0 || daysBetween(pointDay, toParts) <= maxDays - 1)) {
4036
+ return { from: pointDay, to: toParts, kept: toParts };
4037
+ }
4038
+ return null;
4039
+ })();
3998
4040
  const selectedFor = (cell) => {
3999
4041
  if (!range) return Boolean(singleParts && sameDay(cell, singleParts));
4042
+ if (preview) return sameDay(cell, preview.kept);
4000
4043
  return Boolean(fromParts && sameDay(cell, fromParts) || toParts && sameDay(cell, toParts));
4001
4044
  };
4002
- const inRange = (cell) => Boolean(range && fromParts && toParts && dayKey(cell) > dayKey(fromParts) && dayKey(cell) < dayKey(toParts));
4045
+ const inRange = (cell) => Boolean(range && !preview && fromParts && toParts && dayKey(cell) > dayKey(fromParts) && dayKey(cell) < dayKey(toParts));
4046
+ const inPreview = (cell) => Boolean(preview && !sameDay(cell, preview.kept) && dayKey(cell) >= dayKey(preview.from) && dayKey(cell) <= dayKey(preview.to));
4003
4047
  const focusCalendar = React19.useRef(false);
4004
4048
  React19.useEffect(() => {
4005
4049
  const active = typeof document !== "undefined" ? document.activeElement : null;
@@ -4032,6 +4076,7 @@ function DatePicker(props) {
4032
4076
  {
4033
4077
  kind,
4034
4078
  baseId,
4079
+ months: kind === "popover" && popoverKind === "dayRange" ? 2 : 1,
4035
4080
  view,
4036
4081
  onView: setView,
4037
4082
  focusDay,
@@ -4040,6 +4085,8 @@ function DatePicker(props) {
4040
4085
  isDisabled: dayDisabled,
4041
4086
  isSelected: selectedFor,
4042
4087
  isInRange: inRange,
4088
+ isPreview: inPreview,
4089
+ onPoint: setPointDay,
4043
4090
  onSelect: selectDay
4044
4091
  }
4045
4092
  );
@@ -4049,12 +4096,14 @@ function DatePicker(props) {
4049
4096
  const timeRowValue = editing ? `${pad22(editing.hour)}:${pad22(editing.minute)}` : "-";
4050
4097
  const panel = (kind) => /* @__PURE__ */ jsxs14("div", { "data-slot": "date-picker-panel", "data-layer": kind, "data-picker": baseId, className: "flex flex-col", children: [
4051
4098
  popoverHeader != null ? /* @__PURE__ */ jsx16("div", { className: "mb-[var(--space-3)]", children: popoverHeader }) : null,
4052
- range && minute ? /* @__PURE__ */ jsx16(
4099
+ range ? /* @__PURE__ */ jsx16(
4053
4100
  EndTabs,
4054
4101
  {
4055
4102
  kind,
4103
+ names: minute ? ["\uC2DC\uC791 \uC77C\uC2DC", "\uC885\uB8CC \uC77C\uC2DC"] : ["\uC2DC\uC791\uC77C", "\uC885\uB8CC\uC77C"],
4104
+ locked: lockStart ? "from" : void 0,
4056
4105
  active: activeEnd,
4057
- onActivate: setActiveEnd,
4106
+ onActivate: (end) => activateEnd(end, kind),
4058
4107
  from: fromParts ? formatSingle(shownRange.from, granularity, now) : "",
4059
4108
  to: toParts ? formatSingle(shownRange.to, granularity, now) : ""
4060
4109
  }
@@ -4093,7 +4142,7 @@ function DatePicker(props) {
4093
4142
  ) : null
4094
4143
  ] })
4095
4144
  ] });
4096
- const control = /* @__PURE__ */ jsxs14(Fragment3, { children: [
4145
+ const picker = /* @__PURE__ */ jsxs14(Fragment3, { children: [
4097
4146
  /* @__PURE__ */ jsxs14(Popover, { open: open && layer === "popover", onOpenChange: handleOpenChange, children: [
4098
4147
  /* @__PURE__ */ jsx16(PopoverAnchor, { asChild: true, children: trigger }),
4099
4148
  /* @__PURE__ */ jsx16(
@@ -4102,7 +4151,7 @@ function DatePicker(props) {
4102
4151
  align,
4103
4152
  sideOffset: 4,
4104
4153
  collisionPadding: COLLISION_PADDING,
4105
- className: cn("p-[var(--space-3)]", minute ? "w-[424px]" : "w-[280px]"),
4154
+ className: cn("p-[var(--space-3)]", { day: "w-[280px]", dayRange: "w-[558px]", minute: "w-[424px]" }[popoverKind]),
4106
4155
  "aria-label": titleText,
4107
4156
  onOpenAutoFocus: (event) => {
4108
4157
  event.preventDefault();
@@ -4125,6 +4174,34 @@ function DatePicker(props) {
4125
4174
  ] }),
4126
4175
  /* @__PURE__ */ jsx16(Dialog, { open: open && layer === "sheet", onOpenChange: handleOpenChange, title: titleText, size: "sm", children: /* @__PURE__ */ jsx16("div", { className: "mx-auto w-full max-w-[360px]", children: panel("sheet") }) })
4127
4176
  ] });
4177
+ const stepSize = { sm: "sm", md: "md", lg: "lg" };
4178
+ const control = onStep ? /* @__PURE__ */ jsxs14("div", { "data-slot": "date-picker-stepper", className: cn("flex items-center gap-[var(--space-1)]", fullWidth ? "w-full" : "w-fit"), children: [
4179
+ /* @__PURE__ */ jsx16(
4180
+ IconButton,
4181
+ {
4182
+ icon: ChevronLeft,
4183
+ appearance: "outline",
4184
+ tone: "neutral",
4185
+ size: stepSize[size],
4186
+ "aria-label": "\uC774\uC804 \uAE30\uAC04",
4187
+ disabled: disabled || prevStepDisabled,
4188
+ onClick: () => onStep(-1)
4189
+ }
4190
+ ),
4191
+ /* @__PURE__ */ jsx16("div", { className: cn("min-w-0", fullWidth && "flex-1"), children: picker }),
4192
+ /* @__PURE__ */ jsx16(
4193
+ IconButton,
4194
+ {
4195
+ icon: ChevronRight,
4196
+ appearance: "outline",
4197
+ tone: "neutral",
4198
+ size: stepSize[size],
4199
+ "aria-label": "\uB2E4\uC74C \uAE30\uAC04",
4200
+ disabled: disabled || nextStepDisabled,
4201
+ onClick: () => onStep(1)
4202
+ }
4203
+ )
4204
+ ] }) : picker;
4128
4205
  if (!topLabel && !showDescription && !showMessage) return control;
4129
4206
  return /* @__PURE__ */ jsxs14("div", { "data-slot": "date-picker-field", className: cn("flex flex-col gap-[var(--space-1)]", fullWidth ? "w-full" : "w-fit"), children: [
4130
4207
  topLabel ? /* @__PURE__ */ jsx16(
@@ -4148,51 +4225,69 @@ function DatePicker(props) {
4148
4225
  DatePicker.displayName = "DatePicker";
4149
4226
  function EndTabs({
4150
4227
  kind,
4228
+ names,
4229
+ locked,
4151
4230
  active,
4152
4231
  onActivate,
4153
4232
  from,
4154
4233
  to
4155
4234
  }) {
4156
4235
  const ends = [
4157
- { end: "from", name: "\uC2DC\uC791 \uC77C\uC2DC", value: from },
4158
- { end: "to", name: "\uC885\uB8CC \uC77C\uC2DC", value: to }
4236
+ { end: "from", name: names[0], value: from },
4237
+ { end: "to", name: names[1], value: to }
4159
4238
  ];
4160
- return /* @__PURE__ */ jsx16("div", { "data-slot": "date-picker-ends", className: cn("mb-[var(--space-3)] grid grid-cols-2 gap-[var(--space-2)]", kind === "sheet" && "mb-[var(--space-4)]"), children: ends.map(({ end, name, value }) => /* @__PURE__ */ jsxs14(
4161
- "button",
4162
- {
4163
- type: "button",
4164
- "data-slot": "date-picker-end",
4165
- "aria-pressed": active === end,
4166
- onClick: () => onActivate(end),
4167
- className: cn(
4168
- "flex min-w-0 flex-col items-start rounded-[var(--radius-md)] border px-[var(--space-3)] py-[var(--space-2)] text-left",
4169
- "outline-none focus-visible:shadow-[0_0_0_2px_var(--focus-ring)]",
4170
- active === end ? "border-[var(--border-strong)] bg-[var(--action-selected)]" : "border-[var(--border)] hover:bg-[var(--action-hover)]"
4171
- ),
4172
- children: [
4173
- /* @__PURE__ */ jsx16("span", { className: "text-[11px] leading-snug text-text-tertiary", children: name }),
4174
- /* @__PURE__ */ jsx16("span", { className: cn("w-full truncate text-[13px] font-semibold leading-snug font-numeric tabular-nums", value ? "text-foreground" : "text-text-tertiary"), children: value || "-" })
4175
- ]
4176
- },
4177
- end
4178
- )) });
4239
+ return /* @__PURE__ */ jsx16("div", { "data-slot": "date-picker-ends", className: cn("mb-[var(--space-3)] grid grid-cols-2 gap-[var(--space-2)]", kind === "sheet" && "mb-[var(--space-4)]"), children: ends.map(({ end, name, value }) => {
4240
+ const isLocked = locked === end;
4241
+ return /* @__PURE__ */ jsxs14(
4242
+ "button",
4243
+ {
4244
+ type: "button",
4245
+ "data-slot": "date-picker-end",
4246
+ "aria-pressed": active === end,
4247
+ disabled: isLocked,
4248
+ onClick: () => onActivate(end),
4249
+ className: cn(
4250
+ "flex min-w-0 flex-col items-start rounded-[var(--radius-md)] border px-[var(--space-3)] py-[var(--space-2)] text-left",
4251
+ "outline-none focus-visible:shadow-[0_0_0_2px_var(--focus-ring)]",
4252
+ isLocked ? "cursor-not-allowed border-[var(--border)] bg-[var(--action-disabled-bg)]" : active === end ? "border-[var(--border-strong)] bg-[var(--action-selected)]" : "border-[var(--border)] hover:bg-[var(--action-hover)]"
4253
+ ),
4254
+ children: [
4255
+ /* @__PURE__ */ jsxs14("span", { className: "flex w-full items-center justify-between gap-[var(--space-1)] text-[11px] leading-snug text-text-tertiary", children: [
4256
+ /* @__PURE__ */ jsx16("span", { children: name }),
4257
+ isLocked ? /* @__PURE__ */ jsx16(Lock, { "aria-hidden": "true", size: 12, strokeWidth: 2, className: "shrink-0" }) : null
4258
+ ] }),
4259
+ /* @__PURE__ */ jsx16(
4260
+ "span",
4261
+ {
4262
+ className: cn(
4263
+ "w-full truncate text-[13px] font-semibold leading-snug font-numeric tabular-nums",
4264
+ isLocked ? "text-text-disabled" : value ? "text-foreground" : "text-text-tertiary"
4265
+ ),
4266
+ children: value || "-"
4267
+ }
4268
+ )
4269
+ ]
4270
+ },
4271
+ end
4272
+ );
4273
+ }) });
4179
4274
  }
4180
- function Calendar({ kind, baseId, view, onView, focusDay, onFocusDay, today, isDisabled, isSelected, isInRange, onSelect }) {
4181
- const gridRef = React19.useRef(null);
4275
+ function Calendar({ kind, baseId, months, view, onView, focusDay, onFocusDay, today, isDisabled, isSelected, isInRange, isPreview, onPoint, onSelect }) {
4276
+ const rootRef = React19.useRef(null);
4182
4277
  const focusPending = React19.useRef(false);
4183
- const headingId = `${baseId}-month`;
4184
- const cells = buildCalendarCells(view.year, view.month).map((cell) => ({ ...cell, hour: 0, minute: 0 }));
4185
- const weeks = Array.from({ length: 6 }, (_, week) => cells.slice(week * 7, week * 7 + 7));
4186
- const tabbable = focusDay.year === view.year && focusDay.month === view.month ? focusDay : { ...focusDay, year: view.year, month: view.month, day: 1 };
4278
+ const shown = Array.from({ length: months }, (_, index) => addMonths(view.year, view.month, index));
4279
+ const inView = (day) => shown.some((month) => month.year === day.year && month.month === day.month);
4280
+ const tabbable = inView(focusDay) ? focusDay : { ...focusDay, year: view.year, month: view.month, day: 1 };
4187
4281
  React19.useEffect(() => {
4188
4282
  if (!focusPending.current) return;
4189
4283
  focusPending.current = false;
4190
- gridRef.current?.querySelector(`[data-date="${isoDay(focusDay)}"]`)?.focus();
4284
+ rootRef.current?.querySelector(`[data-date="${isoDay(focusDay)}"]`)?.focus();
4191
4285
  });
4192
4286
  function move(next) {
4193
4287
  focusPending.current = true;
4194
4288
  onFocusDay(next);
4195
- if (next.year !== view.year || next.month !== view.month) onView({ year: next.year, month: next.month });
4289
+ if (inView(next)) return;
4290
+ onView(dayKey(next) < dayKey({ ...view, day: 1 }) ? { year: next.year, month: next.month } : addMonths(next.year, next.month, 1 - months));
4196
4291
  }
4197
4292
  function moveMonth(delta) {
4198
4293
  onView(addMonths(view.year, view.month, delta));
@@ -4216,61 +4311,100 @@ function Calendar({ kind, baseId, view, onView, focusDay, onFocusDay, today, isD
4216
4311
  move(target());
4217
4312
  }
4218
4313
  const sheet = kind === "sheet";
4219
- return /* @__PURE__ */ jsxs14("div", { "data-slot": "date-picker-calendar", className: cn("flex flex-col", sheet ? "w-full" : "w-[256px] shrink-0"), children: [
4220
- /* @__PURE__ */ jsxs14("div", { className: "flex items-center justify-between", children: [
4221
- /* @__PURE__ */ jsx16(IconButton, { icon: ChevronLeft, appearance: "ghost", tone: "neutral", size: sheet ? "md" : "xs", "aria-label": "\uC774\uC804 \uB2EC", onClick: () => moveMonth(-1) }),
4222
- /* @__PURE__ */ jsxs14("span", { id: headingId, "aria-live": "polite", className: "text-[13px] font-semibold text-text-primary font-numeric tabular-nums", children: [
4223
- view.year,
4224
- "\uB144 ",
4225
- view.month,
4226
- "\uC6D4"
4227
- ] }),
4228
- /* @__PURE__ */ jsx16(IconButton, { icon: ChevronRight, appearance: "ghost", tone: "neutral", size: sheet ? "md" : "xs", "aria-label": "\uB2E4\uC74C \uB2EC", onClick: () => moveMonth(1) })
4229
- ] }),
4230
- /* @__PURE__ */ jsxs14("div", { ref: gridRef, role: "grid", "aria-labelledby": headingId, "data-slot": "date-picker-grid", className: "mt-[var(--space-1)] flex flex-col gap-[2px]", children: [
4231
- /* @__PURE__ */ jsx16("div", { role: "row", className: "grid grid-cols-7 gap-[2px]", children: WEEKDAYS.map((weekday) => /* @__PURE__ */ jsx16("span", { role: "columnheader", className: "py-[var(--space-1)] text-center text-[10px] font-semibold text-text-tertiary", children: weekday }, weekday)) }),
4232
- weeks.map((week, index) => /* @__PURE__ */ jsx16("div", { role: "row", className: "grid grid-cols-7 gap-[2px]", children: week.map((cell) => {
4233
- const outside = !cell.inMonth;
4234
- const disabled = isDisabled(cell);
4235
- const selected = !outside && isSelected(cell);
4236
- const between = !outside && !selected && isInRange(cell);
4237
- const isToday = !outside && sameDay(cell, today);
4238
- return /* @__PURE__ */ jsx16(
4239
- "button",
4240
- {
4241
- type: "button",
4242
- role: "gridcell",
4243
- "data-date": outside ? void 0 : isoDay(cell),
4244
- "data-selected": selected ? "" : void 0,
4245
- "data-in-range": between ? "" : void 0,
4246
- disabled: outside,
4247
- tabIndex: !outside && sameDay(cell, tabbable) ? 0 : -1,
4248
- "aria-label": `${cell.year}\uB144 ${cell.month}\uC6D4 ${cell.day}\uC77C`,
4249
- "aria-selected": outside ? void 0 : selected,
4250
- "aria-disabled": !outside && disabled ? true : void 0,
4251
- "aria-current": isToday ? "date" : void 0,
4252
- onClick: () => {
4253
- if (outside || disabled) return;
4254
- focusPending.current = false;
4255
- onFocusDay(cell);
4256
- onSelect(cell);
4314
+ const navSize = sheet ? "md" : "xs";
4315
+ const headingId = (index) => index === 0 ? `${baseId}-month` : `${baseId}-month-${index + 1}`;
4316
+ const width = sheet ? "w-full" : "w-[256px]";
4317
+ const blankOutside = months > 1;
4318
+ return /* @__PURE__ */ jsxs14("div", { ref: rootRef, "data-slot": "date-picker-calendar", className: cn("flex flex-col", sheet ? "w-full" : "shrink-0"), children: [
4319
+ /* @__PURE__ */ jsx16("div", { className: "flex gap-[var(--space-5)]", children: shown.map((month, index) => (
4320
+ // Keyed by position, not month: the ‹ › pressed stays the same node and keeps focus.
4321
+ /* @__PURE__ */ jsxs14("div", { className: cn("flex items-center justify-between", width), children: [
4322
+ index === 0 ? /* @__PURE__ */ jsx16(IconButton, { icon: ChevronLeft, appearance: "ghost", tone: "neutral", size: navSize, "aria-label": "\uC774\uC804 \uB2EC", onClick: () => moveMonth(-1) }) : /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "size-[28px]" }),
4323
+ /* @__PURE__ */ jsxs14("span", { id: headingId(index), "aria-live": "polite", className: "text-[13px] font-semibold text-text-primary font-numeric tabular-nums", children: [
4324
+ month.year,
4325
+ "\uB144 ",
4326
+ month.month,
4327
+ "\uC6D4"
4328
+ ] }),
4329
+ index === months - 1 ? /* @__PURE__ */ jsx16(IconButton, { icon: ChevronRight, appearance: "ghost", tone: "neutral", size: navSize, "aria-label": "\uB2E4\uC74C \uB2EC", onClick: () => moveMonth(1) }) : /* @__PURE__ */ jsx16("span", { "aria-hidden": "true", className: "size-[28px]" })
4330
+ ] }, index)
4331
+ )) }),
4332
+ /* @__PURE__ */ jsx16(
4333
+ "div",
4334
+ {
4335
+ className: "mt-[var(--space-1)] flex gap-[var(--space-5)]",
4336
+ onMouseLeave: () => onPoint(null),
4337
+ onBlur: (event) => {
4338
+ if (!(event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget))) onPoint(null);
4339
+ },
4340
+ children: shown.map((month, index) => {
4341
+ const cells = buildCalendarCells(month.year, month.month).map((cell) => ({ ...cell, hour: 0, minute: 0 }));
4342
+ const weeks = Array.from({ length: 6 }, (_, week) => cells.slice(week * 7, week * 7 + 7));
4343
+ return /* @__PURE__ */ jsxs14(
4344
+ "div",
4345
+ {
4346
+ role: "grid",
4347
+ "aria-labelledby": headingId(index),
4348
+ "data-slot": "date-picker-grid",
4349
+ className: cn("flex flex-col gap-[2px]", width),
4350
+ children: [
4351
+ /* @__PURE__ */ jsx16("div", { role: "row", className: "grid grid-cols-7 gap-[2px]", children: WEEKDAYS.map((weekday) => /* @__PURE__ */ jsx16("span", { role: "columnheader", className: "py-[var(--space-1)] text-center text-[10px] font-semibold text-text-tertiary", children: weekday }, weekday)) }),
4352
+ weeks.map((week, row) => /* @__PURE__ */ jsx16("div", { role: "row", className: "grid grid-cols-7 gap-[2px]", children: week.map((cell) => {
4353
+ const outside = !cell.inMonth;
4354
+ if (outside && blankOutside) {
4355
+ return /* @__PURE__ */ jsx16("span", { role: "gridcell", "data-outside": "", className: "aspect-square" }, isoDay(cell));
4356
+ }
4357
+ const disabled = isDisabled(cell);
4358
+ const selected = !outside && isSelected(cell);
4359
+ const between = !outside && !selected && isInRange(cell);
4360
+ const previewed = !outside && !selected && isPreview(cell);
4361
+ const isToday = !outside && sameDay(cell, today);
4362
+ return /* @__PURE__ */ jsx16(
4363
+ "button",
4364
+ {
4365
+ type: "button",
4366
+ role: "gridcell",
4367
+ "data-date": outside ? void 0 : isoDay(cell),
4368
+ "data-selected": selected ? "" : void 0,
4369
+ "data-in-range": between ? "" : void 0,
4370
+ "data-preview": previewed ? "" : void 0,
4371
+ disabled: outside,
4372
+ tabIndex: !outside && sameDay(cell, tabbable) ? 0 : -1,
4373
+ "aria-label": `${cell.year}\uB144 ${cell.month}\uC6D4 ${cell.day}\uC77C`,
4374
+ "aria-selected": outside ? void 0 : selected,
4375
+ "aria-disabled": !outside && disabled ? true : void 0,
4376
+ "aria-current": isToday ? "date" : void 0,
4377
+ onClick: () => {
4378
+ if (outside || disabled) return;
4379
+ focusPending.current = false;
4380
+ onFocusDay(cell);
4381
+ onSelect(cell);
4382
+ },
4383
+ onKeyDown: (event) => onKeyDown(event, cell),
4384
+ onMouseEnter: () => onPoint(cell),
4385
+ onFocus: () => onPoint(cell),
4386
+ className: cn(
4387
+ "flex items-center justify-center rounded-[var(--radius-sm)] text-[12px] font-numeric tabular-nums outline-none",
4388
+ sheet ? "h-[40px] text-[14px]" : "aspect-square",
4389
+ "focus-visible:shadow-[0_0_0_2px_var(--focus-ring)]",
4390
+ outside || disabled ? "cursor-default text-text-disabled" : "cursor-pointer text-text-secondary hover:bg-[var(--action-hover)]",
4391
+ between && "bg-[var(--action-selected)] text-foreground",
4392
+ previewed && "bg-[var(--action-hover)] text-foreground shadow-[inset_0_1px_0_0_var(--border-strong),inset_0_-1px_0_0_var(--border-strong)]",
4393
+ isToday && !selected && "border border-[var(--border-strong)]",
4394
+ selected && "bg-[var(--primary)] font-bold text-[var(--primary-fg)] hover:bg-[var(--primary)]"
4395
+ ),
4396
+ children: cell.day
4397
+ },
4398
+ isoDay(cell)
4399
+ );
4400
+ }) }, row))
4401
+ ]
4257
4402
  },
4258
- onKeyDown: (event) => onKeyDown(event, cell),
4259
- className: cn(
4260
- "flex items-center justify-center rounded-[var(--radius-sm)] text-[12px] font-numeric tabular-nums outline-none",
4261
- sheet ? "h-[40px] text-[14px]" : "aspect-square",
4262
- "focus-visible:shadow-[0_0_0_2px_var(--focus-ring)]",
4263
- outside || disabled ? "cursor-default text-text-disabled" : "cursor-pointer text-text-secondary hover:bg-[var(--action-hover)]",
4264
- between && "bg-[var(--action-selected)] text-foreground",
4265
- isToday && !selected && "border border-[var(--border-strong)]",
4266
- selected && "bg-[var(--primary)] font-bold text-[var(--primary-fg)] hover:bg-[var(--primary)]"
4267
- ),
4268
- children: cell.day
4269
- },
4270
- isoDay(cell)
4271
- );
4272
- }) }, index))
4273
- ] })
4403
+ `${month.year}-${month.month}`
4404
+ );
4405
+ })
4406
+ }
4407
+ )
4274
4408
  ] });
4275
4409
  }
4276
4410
  function TimeColumns({ kind, baseId, editing, hasValue, centerKey, minuteStep, min, max, onChange, onEnter, autoFocus = false }) {
@@ -4328,15 +4462,10 @@ function TimeColumn({ kind, id, name, options, selected, centerKey, onSelect, on
4328
4462
  if (!list || !item) return;
4329
4463
  list.scrollTop = item.offsetTop - list.offsetTop - (list.clientHeight - item.offsetHeight) / 2;
4330
4464
  }, [centerKey]);
4331
- const pendingFocus = React19.useRef(null);
4465
+ const [focusAsk, setFocusAsk] = React19.useState(0);
4332
4466
  React19.useEffect(() => {
4333
- if (pendingFocus.current === null) return;
4334
- listRef.current?.querySelector(`[data-value="${pendingFocus.current}"]`)?.focus();
4335
- pendingFocus.current = null;
4336
- });
4337
- function focusValue(value) {
4338
- pendingFocus.current = value;
4339
- }
4467
+ if (focusAsk) listRef.current?.querySelector('[aria-selected="true"]')?.focus();
4468
+ }, [focusAsk]);
4340
4469
  function onKeyDown(event, value) {
4341
4470
  if (event.key === "Enter") {
4342
4471
  event.preventDefault();
@@ -4368,7 +4497,7 @@ function TimeColumn({ kind, id, name, options, selected, centerKey, onSelect, on
4368
4497
  const next = target();
4369
4498
  if (!next || next.value === value) return;
4370
4499
  onSelect(next.value);
4371
- focusValue(next.value);
4500
+ setFocusAsk((count) => count + 1);
4372
4501
  }
4373
4502
  const sheet = kind === "sheet";
4374
4503
  return /* @__PURE__ */ jsxs14("div", { className: cn("flex flex-col", sheet ? "w-[88px]" : "h-full w-[56px]"), children: [
@@ -5621,7 +5750,7 @@ import * as React30 from "react";
5621
5750
  import * as React29 from "react";
5622
5751
  import { Tabs as TabsPrimitive } from "radix-ui";
5623
5752
  import { cva as cva10 } from "class-variance-authority";
5624
- import { Lock } from "lucide-react";
5753
+ import { Lock as Lock2 } from "lucide-react";
5625
5754
  import { Fragment as Fragment10, jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
5626
5755
  var tabsRootVariants = cva10("w-full", {
5627
5756
  variants: {
@@ -5894,7 +6023,7 @@ var Tabs = React29.forwardRef(
5894
6023
  /* @__PURE__ */ jsx26("span", { className: "sr-only", children: ` ${tab.badge}` })
5895
6024
  ] }) : null,
5896
6025
  tab.disabled ? /* @__PURE__ */ jsx26(
5897
- Lock,
6026
+ Lock2,
5898
6027
  {
5899
6028
  "aria-hidden": "true",
5900
6029
  "data-slot": "tabs-disabled-icon",
@@ -9798,6 +9927,7 @@ var visuallyHiddenStyle = {
9798
9927
  function MoveButton({
9799
9928
  mac,
9800
9929
  shown,
9930
+ keys,
9801
9931
  onMove
9802
9932
  }) {
9803
9933
  return /* @__PURE__ */ jsxs36(
@@ -9805,7 +9935,11 @@ function MoveButton({
9805
9935
  {
9806
9936
  appearance: "outline",
9807
9937
  size: "sm",
9808
- className: "mr-auto shrink-0 pr-[var(--space-2)] before:-inset-y-[5px] [@media(hover:none)_and_(pointer:coarse)]:hidden",
9938
+ className: cn(
9939
+ "mr-auto shrink-0",
9940
+ keys && "pr-[var(--space-2)]",
9941
+ "before:-inset-y-[5px] [@media(hover:none)_and_(pointer:coarse)]:hidden"
9942
+ ),
9809
9943
  style: shown ? void 0 : visuallyHiddenStyle,
9810
9944
  "data-slot": "bulk-action-bar-move",
9811
9945
  "data-hidden": shown ? void 0 : "",
@@ -9814,23 +9948,32 @@ function MoveButton({
9814
9948
  onClick: onMove,
9815
9949
  children: [
9816
9950
  /* @__PURE__ */ jsx40("span", { "data-slot": "bulk-action-bar-move-label", children: TO_LIST_LABEL }),
9817
- /* @__PURE__ */ jsxs36("span", { "aria-hidden": "true", "data-slot": "bulk-action-bar-keys", style: { display: "inline-flex", gap: "var(--space-1)" }, children: [
9818
- /* @__PURE__ */ jsx40("kbd", { style: keyCapStyle, children: mac ? "\u2303" : "Ctrl" }),
9819
- /* @__PURE__ */ jsx40("kbd", { style: keyCapStyle, children: "." })
9820
- ] })
9951
+ /* @__PURE__ */ jsxs36(
9952
+ "span",
9953
+ {
9954
+ "aria-hidden": "true",
9955
+ "data-slot": "bulk-action-bar-keys",
9956
+ style: { display: "inline-flex", gap: "var(--space-1)", ...keys ? void 0 : foldedStyle },
9957
+ children: [
9958
+ /* @__PURE__ */ jsx40("kbd", { style: keyCapStyle, children: mac ? "\u2303" : "Ctrl" }),
9959
+ /* @__PURE__ */ jsx40("kbd", { style: keyCapStyle, children: "." })
9960
+ ]
9961
+ }
9962
+ )
9821
9963
  ]
9822
9964
  }
9823
9965
  );
9824
9966
  }
9825
- var sameFold = (a, b) => a !== null && a.folded === b.folded && a.texts.join("\n") === b.texts.join("\n");
9967
+ var sameFold = (a, b) => a !== null && a.folded === b.folded && a.keys === b.keys && a.texts.join("\n") === b.texts.join("\n");
9826
9968
  var ROW_GAP = 8;
9827
9969
  var MORE_WIDTH = 30;
9828
9970
  var MOVE_EDGES = 2 + 12 + 6 + 8;
9971
+ var MOVE_EDGES_NO_KEYS = 2 + 12 + 12;
9829
9972
  function measureFold(row, actionCount) {
9830
9973
  const buttons = Array.from(row.querySelectorAll('[data-slot="bulk-action-bar-action"]'));
9831
9974
  const texts = buttons.map((node) => node.textContent ?? "");
9832
9975
  const available = row.clientWidth;
9833
- if (available === 0) return { folded: 0, texts };
9976
+ if (available === 0) return { folded: 0, keys: true, texts };
9834
9977
  const scale = row.getBoundingClientRect().width / available || 1;
9835
9978
  const width = (node) => (node?.getBoundingClientRect().width ?? 0) / scale;
9836
9979
  const move = row.querySelector('[data-slot="bulk-action-bar-move"]');
@@ -9838,15 +9981,17 @@ function measureFold(row, actionCount) {
9838
9981
  const label = hasMove ? width(row.querySelector('[data-slot="bulk-action-bar-move-label"]')) : 0;
9839
9982
  const keys = hasMove ? width(row.querySelector('[data-slot="bulk-action-bar-keys"]')) : 0;
9840
9983
  const actions = buttons.map(width);
9841
- const need = (folded2) => {
9984
+ const fits = (folded2, withKeys) => {
9842
9985
  const items = actions.slice(0, actionCount - folded2);
9843
- if (hasMove) items.unshift(keys + label + MOVE_EDGES);
9986
+ if (hasMove) items.unshift(withKeys ? keys + label + MOVE_EDGES : label + MOVE_EDGES_NO_KEYS);
9844
9987
  if (folded2 > 0) items.push(MORE_WIDTH);
9845
- return items.reduce((sum, item) => sum + item, 0) + ROW_GAP * Math.max(0, items.length - 1);
9988
+ return items.reduce((sum, item) => sum + item, 0) + ROW_GAP * Math.max(0, items.length - 1) <= available + 0.5;
9846
9989
  };
9847
9990
  let folded = 0;
9848
- while (folded < actionCount - 1 && need(folded) > available + 0.5) folded += 1;
9849
- return { folded, texts };
9991
+ while (folded < actionCount - 1 && !fits(folded, true)) folded += 1;
9992
+ if (!hasMove || fits(folded, true)) return { folded, keys: true, texts };
9993
+ while (folded < actionCount && !fits(folded, false)) folded += 1;
9994
+ return { folded, keys: false, texts };
9850
9995
  }
9851
9996
  var CLEAR_CLEARANCE = 32;
9852
9997
  var IDLE_HEIGHT = 40;
@@ -10037,7 +10182,7 @@ function BulkActionBar({
10037
10182
  activeRef.current = active;
10038
10183
  scopePropRef.current = shortcutScope;
10039
10184
  menuOpenRef.current = menuOpen;
10040
- menuKeysRef.current = actions.slice(actions.length - Math.min(fold?.folded ?? 0, Math.max(0, actions.length - 1))).map((action) => action.key);
10185
+ menuKeysRef.current = actions.slice(actions.length - Math.min(fold?.folded ?? 0, actions.length)).map((action) => action.key);
10041
10186
  });
10042
10187
  const toBar = React38.useCallback(() => {
10043
10188
  const node = cardRef.current;
@@ -10210,7 +10355,8 @@ function BulkActionBar({
10210
10355
  moreLostFocusRef.current = false;
10211
10356
  if (!focused || focused === document.body) {
10212
10357
  const shown = (button) => !button.disabled && !button.hasAttribute("data-folded");
10213
- const back = buttons.find((button, i) => actions[i]?.key === foldedFocusRef.current && shown(button)) ?? buttons.find(shown);
10358
+ const move = row.querySelector('[data-slot="bulk-action-bar-move"]');
10359
+ const back = buttons.find((button, i) => actions[i]?.key === foldedFocusRef.current && shown(button)) ?? buttons.find(shown) ?? (move && !move.hasAttribute("data-hidden") && isShown(move) ? move : null) ?? clearRef.current;
10214
10360
  back?.focus();
10215
10361
  }
10216
10362
  foldedFocusRef.current = null;
@@ -10300,7 +10446,7 @@ function BulkActionBar({
10300
10446
  }
10301
10447
  ) : null;
10302
10448
  const showMove = active;
10303
- const folded = Math.min(fold?.folded ?? 0, Math.max(0, actions.length - 1));
10449
+ const folded = Math.min(fold?.folded ?? 0, actions.length);
10304
10450
  const shownCount = actions.length - folded;
10305
10451
  const menuItems = actions.slice(shownCount).map((action, i) => ({
10306
10452
  id: action.key,
@@ -10373,7 +10519,7 @@ function BulkActionBar({
10373
10519
  marginTop: "var(--space-3)"
10374
10520
  },
10375
10521
  children: [
10376
- showMove ? /* @__PURE__ */ jsx40(MoveButton, { mac, shown: keyboardIn, onMove: toList }) : null,
10522
+ showMove ? /* @__PURE__ */ jsx40(MoveButton, { mac, shown: keyboardIn, keys: fold?.keys ?? true, onMove: toList }) : null,
10377
10523
  actions.map((action, index) => /* @__PURE__ */ jsx40(CardActionButton, { action, folded: index >= shownCount }, action.key)),
10378
10524
  folded > 0 ? /* @__PURE__ */ jsx40(
10379
10525
  ActionMenu,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olundot/ui",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "OLUN. shared React UI components built on OLUN design tokens",
5
5
  "license": "MIT",
6
6
  "keywords": [