@zvk/ui 0.1.8 → 0.1.9

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 (59) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/dist/components/alert-dialog/alert-dialog.d.ts +1 -1
  4. package/dist/components/alert-dialog/alert-dialog.js +18 -10
  5. package/dist/components/combobox/combobox.js +3 -1
  6. package/dist/components/command/command-filter.js +1 -1
  7. package/dist/components/command/command.d.ts +5 -0
  8. package/dist/components/command/command.js +9 -2
  9. package/dist/components/command/index.d.ts +1 -1
  10. package/dist/components/context-menu/context-menu.js +1 -1
  11. package/dist/components/date-range-picker/date-range-picker.d.ts +27 -0
  12. package/dist/components/date-range-picker/date-range-picker.js +193 -0
  13. package/dist/components/date-range-picker/index.d.ts +2 -0
  14. package/dist/components/date-range-picker/index.js +2 -0
  15. package/dist/components/dialog/dialog.d.ts +1 -1
  16. package/dist/components/dialog/dialog.js +19 -14
  17. package/dist/components/dropdown-menu/dropdown-menu.d.ts +2 -2
  18. package/dist/components/dropdown-menu/dropdown-menu.js +24 -19
  19. package/dist/components/file-dropzone/file-dropzone.d.ts +25 -0
  20. package/dist/components/file-dropzone/file-dropzone.js +171 -0
  21. package/dist/components/file-dropzone/index.d.ts +2 -0
  22. package/dist/components/file-dropzone/index.js +2 -0
  23. package/dist/components/form/form.d.ts +16 -1
  24. package/dist/components/form/form.js +13 -2
  25. package/dist/components/form/index.d.ts +1 -1
  26. package/dist/components/hover-card/hover-card.d.ts +1 -1
  27. package/dist/components/hover-card/hover-card.js +12 -3
  28. package/dist/components/index.d.ts +6 -0
  29. package/dist/components/index.js +3 -0
  30. package/dist/components/kbd/index.d.ts +2 -0
  31. package/dist/components/kbd/index.js +1 -0
  32. package/dist/components/kbd/kbd.d.ts +15 -0
  33. package/dist/components/kbd/kbd.js +10 -0
  34. package/dist/components/menubar/menubar.js +1 -1
  35. package/dist/components/popover/popover.d.ts +1 -1
  36. package/dist/components/popover/popover.js +14 -24
  37. package/dist/components/select/select.d.ts +1 -1
  38. package/dist/components/select/select.js +88 -8
  39. package/dist/components/sheet/sheet.d.ts +1 -1
  40. package/dist/components/sheet/sheet.js +17 -12
  41. package/dist/components/table/index.d.ts +1 -1
  42. package/dist/components/table/table.d.ts +37 -0
  43. package/dist/components/table/table.js +30 -2
  44. package/dist/components/toast/index.d.ts +1 -1
  45. package/dist/components/toast/toast.d.ts +18 -0
  46. package/dist/components/toast/toast.js +60 -0
  47. package/dist/components/tooltip/tooltip.d.ts +1 -1
  48. package/dist/components/tooltip/tooltip.js +12 -3
  49. package/dist/internal/floating/transform-origin.d.ts +2 -0
  50. package/dist/internal/floating/transform-origin.js +22 -0
  51. package/dist/internal/presence/index.d.ts +2 -0
  52. package/dist/internal/presence/index.js +2 -0
  53. package/dist/internal/presence/use-presence.d.ts +18 -0
  54. package/dist/internal/presence/use-presence.js +119 -0
  55. package/dist/styles.css +1710 -224
  56. package/dist/tokens/token-types.d.ts +4 -0
  57. package/dist/tokens/tokens.d.ts +41 -5
  58. package/dist/tokens/tokens.js +45 -9
  59. package/package.json +135 -61
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.9](https://github.com/brandon-schabel/zvk/compare/v0.1.8...v0.1.9) (2026-06-19)
4
+
5
+
6
+ ### Features
7
+
8
+ * add theme presets and package improvements ([#19](https://github.com/brandon-schabel/zvk/issues/19)) ([867f955](https://github.com/brandon-schabel/zvk/commit/867f9556f7a60144cad4e747f2c032f2d5ede353))
9
+
3
10
  ## [0.1.8](https://github.com/brandon-schabel/zvk/compare/v0.1.7...v0.1.8) (2026-06-16)
4
11
 
5
12
 
package/README.md CHANGED
@@ -20,7 +20,7 @@ import "@zvk/ui/styles.css";
20
20
 
21
21
  ## Release Status
22
22
 
23
- GitHub Actions runs `bun run preflight` before release publishing. Release Please opens
23
+ GitHub Actions runs `bun run preflight:ci` before release publishing. Release Please opens
24
24
  reviewable version-bump PRs from conventional commits; merging a release PR creates the
25
25
  GitHub release and publishes any package with a created release, including `@zvk/ui`, to npm
26
26
  through trusted publishing in the protected `npm-publish` environment.
@@ -31,7 +31,7 @@ export interface AlertDialogCancelProps extends React.ButtonHTMLAttributes<HTMLB
31
31
  }
32
32
  declare function AlertDialogRoot({ children, className, defaultOpen, onOpenChange, open: openProp, ref, ...props }: AlertDialogProps): React.JSX.Element;
33
33
  declare function AlertDialogTrigger({ asChild, className, disabled, onClick, ref, type, ...props }: AlertDialogTriggerProps): React.JSX.Element;
34
- declare function AlertDialogContent({ children, className, ref, ...props }: AlertDialogContentProps): React.JSX.Element | null;
34
+ declare function AlertDialogContent({ children, className, onAnimationEnd, onTransitionEnd, ref, ...props }: AlertDialogContentProps): React.JSX.Element | null;
35
35
  declare function AlertDialogTitle({ className, ref, ...props }: AlertDialogTitleProps): React.JSX.Element;
36
36
  declare function AlertDialogDescription({ className, ref, ...props }: AlertDialogDescriptionProps): React.JSX.Element;
37
37
  declare function AlertDialogFooter({ className, ref, ...props }: AlertDialogFooterProps): React.JSX.Element;
@@ -6,6 +6,7 @@ import { cn } from "../../utils/cn.js";
6
6
  import { useControllableState } from "../../hooks/use-controllable-state.js";
7
7
  import { DismissableLayer } from "../../internal/dismissable-layer/index.js";
8
8
  import { FocusScope } from "../../internal/focus/index.js";
9
+ import { usePresence } from "../../internal/presence/index.js";
9
10
  import { Portal } from "../../internal/portal/index.js";
10
11
  import { lockScroll, unlockScroll } from "../../internal/scroll-lock/index.js";
11
12
  import { Slot } from "../../internal/slot/index.js";
@@ -29,6 +30,7 @@ function composeRefs(...refs) {
29
30
  }
30
31
  };
31
32
  }
33
+ const alertDialogExitDurationMs = 180;
32
34
  function AlertDialogRoot({ children, className, defaultOpen = false, onOpenChange, open: openProp, ref, ...props }) {
33
35
  const [open, setOpen] = useControllableState({
34
36
  ...(openProp !== undefined ? { value: openProp } : {}),
@@ -47,13 +49,6 @@ function AlertDialogRoot({ children, className, defaultOpen = false, onOpenChang
47
49
  setDescribedBy(id);
48
50
  return () => setDescribedBy((current) => (current === id ? undefined : current));
49
51
  }, []);
50
- React.useLayoutEffect(() => {
51
- if (!open) {
52
- return;
53
- }
54
- lockScroll();
55
- return () => unlockScroll();
56
- }, [open]);
57
52
  return (_jsx(AlertDialogContext.Provider, { value: {
58
53
  close: () => setOpen(false),
59
54
  contentId: `${instanceId}-content`,
@@ -80,12 +75,25 @@ function AlertDialogTrigger({ asChild = false, className, disabled, onClick, ref
80
75
  }
81
76
  return (_jsx("button", { ...props, ref: composeRefs(ref, triggerRef), type: type, disabled: disabled, "aria-controls": contentId, "aria-expanded": open ? "true" : "false", className: cn("zvk-ui-alert-dialog__trigger", className), "data-state": open ? "open" : "closed", onClick: composeEventHandlers(onClick, handleClick) }));
82
77
  }
83
- function AlertDialogContent({ children, className, ref, ...props }) {
78
+ function AlertDialogContent({ children, className, onAnimationEnd, onTransitionEnd, ref, ...props }) {
84
79
  const { close, contentId, describedBy, labelledBy, open } = useAlertDialogContext("AlertDialog.Content");
85
- if (!open) {
80
+ const presence = usePresence({ open, exitDurationMs: alertDialogExitDurationMs });
81
+ const shouldLockScroll = presence.present && (!presence.inert || presence.motionState === "exiting");
82
+ React.useLayoutEffect(() => {
83
+ if (!shouldLockScroll) {
84
+ return;
85
+ }
86
+ lockScroll();
87
+ return () => unlockScroll();
88
+ }, [shouldLockScroll]);
89
+ if (!presence.present) {
86
90
  return null;
87
91
  }
88
- return (_jsxs(Portal, { children: [_jsx("div", { "aria-hidden": "true", className: "zvk-ui-alert-dialog__overlay" }), _jsx(DismissableLayer, { open: open, onDismiss: close, disableEscapeKeyDown: true, disableOutsidePointerDown: true, children: _jsx(FocusScope, { active: open, trapped: true, children: _jsx("div", { ...props, ref: ref, id: contentId, role: "alertdialog", "aria-modal": "true", "aria-describedby": describedBy, "aria-labelledby": labelledBy, className: cn("zvk-ui-alert-dialog__content", className), "data-state": open ? "open" : "closed", children: children }) }) })] }));
92
+ const content = (_jsx("div", { ...props, ref: ref, id: contentId, role: "alertdialog", "aria-hidden": presence.inert ? true : undefined, "aria-modal": open ? true : undefined, "aria-describedby": describedBy, "aria-labelledby": labelledBy, className: cn("zvk-ui-alert-dialog__content", className), "data-motion-state": presence.motionState, "data-state": presence.state, inert: presence.inert ? true : undefined, onAnimationEnd: composeEventHandlers(onAnimationEnd, presence.onExitComplete, { checkDefaultPrevented: false }), onTransitionEnd: composeEventHandlers(onTransitionEnd, presence.onExitComplete, { checkDefaultPrevented: false }), children: children }));
93
+ if (!open) {
94
+ return (_jsxs(Portal, { children: [_jsx("div", { "aria-hidden": "true", className: "zvk-ui-alert-dialog__overlay", "data-state": presence.state }), content] }));
95
+ }
96
+ return (_jsxs(Portal, { children: [_jsx("div", { "aria-hidden": "true", className: "zvk-ui-alert-dialog__overlay", "data-state": presence.state }), _jsx(DismissableLayer, { open: open, onDismiss: close, disableEscapeKeyDown: true, disableOutsidePointerDown: true, children: _jsx(FocusScope, { active: open, trapped: true, children: content }) })] }));
89
97
  }
90
98
  function AlertDialogTitle({ className, ref, ...props }) {
91
99
  const { registerTitle, titleId } = useAlertDialogContext("AlertDialog.Title");
@@ -7,6 +7,7 @@ import { cn } from "../../utils/cn.js";
7
7
  import { useControllableState } from "../../hooks/use-controllable-state.js";
8
8
  import { DismissableLayer } from "../../internal/dismissable-layer/index.js";
9
9
  import { useFloatingPosition } from "../../internal/floating/index.js";
10
+ import { placementParts } from "../../internal/floating/placement-aliases.js";
10
11
  import { Portal } from "../../internal/portal/index.js";
11
12
  function hasRenderableNode(value) {
12
13
  return value !== undefined && value !== null && value !== false;
@@ -76,6 +77,7 @@ export function Combobox({ "aria-describedby": ariaDescribedBy, className, clear
76
77
  const activeOption = filteredOptions.find((option) => option.value === activeValue && option.disabled !== true) ??
77
78
  firstEnabled(filteredOptions);
78
79
  const activeOptionIndex = activeOption === undefined ? -1 : filteredOptions.findIndex((option) => option.value === activeOption.value);
80
+ const resolvedPlacement = placementParts(floating.placement);
79
81
  React.useEffect(() => {
80
82
  if (!open) {
81
83
  setQuery(selectedOption?.label ?? "");
@@ -145,7 +147,7 @@ export function Combobox({ "aria-describedby": ariaDescribedBy, className, clear
145
147
  setOpen(false);
146
148
  }
147
149
  }), placeholder: placeholder, required: required, value: open ? query : selectedOption?.label ?? query }));
148
- return (_jsxs("div", { className: "zvk-ui-combobox", "data-disabled": disabled ? "true" : undefined, "data-invalid": invalid ? "true" : undefined, children: [hasLabel ? _jsx("label", { className: "zvk-ui-combobox__label", htmlFor: inputId, children: label }) : null, _jsxs("div", { className: "zvk-ui-combobox__control", children: [input, clearable && selectedValue !== null ? (_jsx("button", { type: "button", className: "zvk-ui-combobox__clear", "aria-label": "Clear selection", disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: clearSelection, children: "Clear" })) : null] }), hasDescription ? _jsx("div", { className: "zvk-ui-combobox__description", id: descriptionId, children: description }) : null, hasError ? _jsx("div", { className: "zvk-ui-combobox__error", id: errorId, children: error }) : null, open ? (_jsx(Portal, { ...(container === undefined ? {} : { container }), children: _jsx(DismissableLayer, { open: open, onDismiss: () => setOpen(false), children: _jsx("div", { id: listboxId, role: "listbox", className: "zvk-ui-combobox__popup", ref: floating.floatingRef, style: floating.floatingStyle, children: filteredOptions.map((option, index) => {
150
+ return (_jsxs("div", { className: "zvk-ui-combobox", "data-disabled": disabled ? "true" : undefined, "data-invalid": invalid ? "true" : undefined, children: [hasLabel ? _jsx("label", { className: "zvk-ui-combobox__label", htmlFor: inputId, children: label }) : null, _jsxs("div", { className: "zvk-ui-combobox__control", children: [input, clearable && selectedValue !== null ? (_jsx("button", { type: "button", className: "zvk-ui-combobox__clear", "aria-label": "Clear selection", disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: clearSelection, children: "Clear" })) : null] }), hasDescription ? _jsx("div", { className: "zvk-ui-combobox__description", id: descriptionId, children: description }) : null, hasError ? _jsx("div", { className: "zvk-ui-combobox__error", id: errorId, children: error }) : null, open ? (_jsx(Portal, { ...(container === undefined ? {} : { container }), children: _jsx(DismissableLayer, { open: open, onDismiss: () => setOpen(false), children: _jsx("div", { id: listboxId, role: "listbox", className: "zvk-ui-combobox__popup", "data-align": resolvedPlacement.align, "data-side": resolvedPlacement.side, "data-state": "open", ref: floating.floatingRef, style: floating.floatingStyle, children: filteredOptions.map((option, index) => {
149
151
  const isActive = activeOption?.value === option.value;
150
152
  const isSelected = selectedValue === option.value;
151
153
  return (_jsx("div", { id: optionId(inputId, index), role: "option", "aria-disabled": option.disabled ? "true" : undefined, "aria-selected": isSelected ? "true" : "false", className: "zvk-ui-combobox__option", "data-disabled": option.disabled ? "true" : undefined, "data-highlighted": isActive ? "true" : undefined, "data-selected": isSelected ? "true" : undefined, onMouseDown: (event) => event.preventDefault(), onClick: () => selectOption(option), children: option.label }, option.value));
@@ -1,5 +1,5 @@
1
1
  function normalized(value) {
2
- return value.trim().toLocaleLowerCase();
2
+ return value.trim().toLowerCase();
3
3
  }
4
4
  export function commandItemMatches(item, query) {
5
5
  const needle = normalized(query);
@@ -18,6 +18,9 @@ export interface CommandListProps extends React.HTMLAttributes<HTMLDivElement> {
18
18
  export interface CommandEmptyProps extends React.HTMLAttributes<HTMLDivElement> {
19
19
  ref?: React.Ref<HTMLDivElement>;
20
20
  }
21
+ export interface CommandLoadingProps extends React.HTMLAttributes<HTMLDivElement> {
22
+ ref?: React.Ref<HTMLDivElement>;
23
+ }
21
24
  export interface CommandGroupProps extends React.HTMLAttributes<HTMLDivElement> {
22
25
  heading?: React.ReactNode;
23
26
  ref?: React.Ref<HTMLDivElement>;
@@ -43,6 +46,7 @@ declare function CommandRoot({ children, className, defaultValue, onItemSelect,
43
46
  declare function CommandInput({ className, onKeyDown, onValueChange, placeholder, ref, value, ...props }: CommandInputProps): React.JSX.Element;
44
47
  declare function CommandList({ className, ref, ...props }: CommandListProps): React.JSX.Element;
45
48
  declare function CommandEmpty({ className, ref, ...props }: CommandEmptyProps): React.JSX.Element | null;
49
+ declare function CommandLoading({ className, ref, ...props }: CommandLoadingProps): React.JSX.Element;
46
50
  declare function CommandGroup({ children, className, heading, ref, ...props }: CommandGroupProps): React.JSX.Element;
47
51
  declare function CommandItem({ children, className, disabled, keywords, onClick, onSelect, ref, value, ...props }: CommandItemProps): React.JSX.Element | null;
48
52
  declare function CommandSeparator({ className, ref, ...props }: CommandSeparatorProps): React.JSX.Element;
@@ -55,6 +59,7 @@ type CommandComponent = typeof CommandRoot & {
55
59
  Input: typeof CommandInput;
56
60
  Item: typeof CommandItem;
57
61
  List: typeof CommandList;
62
+ Loading: typeof CommandLoading;
58
63
  Separator: typeof CommandSeparator;
59
64
  Shortcut: typeof CommandShortcut;
60
65
  };
@@ -54,6 +54,10 @@ function CommandRoot({ children, className, defaultValue = "", onItemSelect, onV
54
54
  return;
55
55
  }
56
56
  const currentIndex = activeId === undefined ? -1 : visible.findIndex((item) => item.id === activeId);
57
+ if (currentIndex === -1) {
58
+ setActiveId(direction === "next" ? visible[0]?.id : visible.at(-1)?.id);
59
+ return;
60
+ }
57
61
  const nextIndex = direction === "next" ? currentIndex + 1 : currentIndex - 1;
58
62
  const wrapped = ((nextIndex % visible.length) + visible.length) % visible.length;
59
63
  setActiveId(visible[wrapped]?.id);
@@ -89,8 +93,7 @@ function CommandRoot({ children, className, defaultValue = "", onItemSelect, onV
89
93
  setActiveId(visible[0]?.id);
90
94
  }
91
95
  }, [activeId, query, version, visibleEnabledItems]);
92
- const empty = collectionRef.current.items().some((item) => !item.data.visible) &&
93
- !collectionRef.current.items().some((item) => item.data.visible);
96
+ const empty = collectionRef.current.items().every((item) => !item.data.visible);
94
97
  return (_jsx(CommandContext.Provider, { value: {
95
98
  activeId,
96
99
  empty,
@@ -152,6 +155,9 @@ function CommandEmpty({ className, ref, ...props }) {
152
155
  }
153
156
  return _jsx("div", { ...props, ref: ref, className: cn("zvk-ui-command__empty", className), "cmdk-empty": "" });
154
157
  }
158
+ function CommandLoading({ className, ref, ...props }) {
159
+ return (_jsx("div", { ...props, ref: ref, role: "status", "aria-atomic": "true", "aria-live": "polite", className: cn("zvk-ui-command__loading", className), "cmdk-loading": "" }));
160
+ }
155
161
  function CommandGroup({ children, className, heading, ref, ...props }) {
156
162
  const headingId = React.useId();
157
163
  return (_jsxs("div", { ...props, ref: ref, role: "group", "aria-labelledby": heading ? headingId : undefined, className: cn("zvk-ui-command__group", className), "cmdk-group": "", children: [heading ? _jsx("div", { id: headingId, className: "zvk-ui-command__group-heading", "cmdk-group-heading": "", children: heading }) : null, children] }));
@@ -206,6 +212,7 @@ export const Command = Object.assign(CommandRoot, {
206
212
  Input: CommandInput,
207
213
  Item: CommandItem,
208
214
  List: CommandList,
215
+ Loading: CommandLoading,
209
216
  Separator: CommandSeparator,
210
217
  Shortcut: CommandShortcut
211
218
  });
@@ -1,2 +1,2 @@
1
1
  export { Command, CommandDialog } from "./command.js";
2
- export type { CommandDialogProps, CommandEmptyProps, CommandGroupProps, CommandInputProps, CommandItemProps, CommandListProps, CommandProps, CommandSeparatorProps, CommandShortcutProps } from "./command.js";
2
+ export type { CommandDialogProps, CommandEmptyProps, CommandGroupProps, CommandInputProps, CommandItemProps, CommandListProps, CommandLoadingProps, CommandProps, CommandSeparatorProps, CommandShortcutProps } from "./command.js";
@@ -181,7 +181,7 @@ function ContextMenuContent({ align, alignOffset = 0, children, className, colli
181
181
  if (node !== null) {
182
182
  schedulePositionUpdate();
183
183
  }
184
- }, id: contentId, role: "menu", className: cn("zvk-ui-context-menu__content", className), "data-align": placementParts(resolvedPlacement).align, "data-side": placementParts(resolvedPlacement).side, style: { ...style, ...contentStyle }, onKeyDown: composeEventHandlers(onKeyDown, (event) => {
184
+ }, id: contentId, role: "menu", className: cn("zvk-ui-context-menu__content", className), "data-align": placementParts(resolvedPlacement).align, "data-side": placementParts(resolvedPlacement).side, "data-state": "open", style: { ...style, ...contentStyle }, onKeyDown: composeEventHandlers(onKeyDown, (event) => {
185
185
  const items = getItems();
186
186
  const index = currentIndex(items);
187
187
  if (event.key === "ArrowDown") {
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ export interface DateRange {
3
+ start: Date | null;
4
+ end: Date | null;
5
+ }
6
+ export interface DateRangePickerProps {
7
+ label?: React.ReactNode;
8
+ description?: React.ReactNode;
9
+ error?: React.ReactNode;
10
+ value?: DateRange;
11
+ defaultValue?: DateRange;
12
+ onValueChange?: (range: DateRange) => void;
13
+ startLabel?: string;
14
+ endLabel?: string;
15
+ startPlaceholder?: string;
16
+ endPlaceholder?: string;
17
+ formatDate?: (date: Date) => string;
18
+ parseDate?: (value: string) => Date | null;
19
+ parseErrorMessage?: (part: DateRangePart, value: string) => string;
20
+ minDate?: Date;
21
+ maxDate?: Date;
22
+ disabledDate?: (date: Date) => boolean;
23
+ ref?: React.Ref<HTMLDivElement>;
24
+ }
25
+ type DateRangePart = "start" | "end";
26
+ export declare function DateRangePicker({ defaultValue, description, disabledDate, endLabel, endPlaceholder, error, formatDate, label, maxDate, minDate, onValueChange, parseDate, parseErrorMessage, ref, startLabel, startPlaceholder, value }: DateRangePickerProps): React.JSX.Element;
27
+ export {};
@@ -0,0 +1,193 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Calendar } from "../calendar/calendar.js";
5
+ import { Field } from "../field/field.js";
6
+ import { Input } from "../input/input.js";
7
+ import { Popover } from "../popover/popover.js";
8
+ const emptyRange = { start: null, end: null };
9
+ function hasRenderableNode(value) {
10
+ return value !== undefined && value !== null && value !== false;
11
+ }
12
+ function joinIds(...ids) {
13
+ const value = ids.filter(Boolean).join(" ");
14
+ return value.length > 0 ? value : undefined;
15
+ }
16
+ function defaultFormatDate(date) {
17
+ return new Intl.DateTimeFormat(undefined, {
18
+ day: "numeric",
19
+ month: "short",
20
+ year: "numeric"
21
+ }).format(date);
22
+ }
23
+ function normalizeRange(range) {
24
+ if (!range) {
25
+ return emptyRange;
26
+ }
27
+ return {
28
+ start: range.start ?? null,
29
+ end: range.end ?? null
30
+ };
31
+ }
32
+ function startOfDay(date) {
33
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
34
+ }
35
+ function isBeforeDay(left, right) {
36
+ return startOfDay(left).getTime() < startOfDay(right).getTime();
37
+ }
38
+ function isAfterDay(left, right) {
39
+ return startOfDay(left).getTime() > startOfDay(right).getTime();
40
+ }
41
+ function isUnavailableDate(date, { disabledDate, maxDate, minDate }) {
42
+ if (minDate && isBeforeDay(date, minDate)) {
43
+ return true;
44
+ }
45
+ if (maxDate && isAfterDay(date, maxDate)) {
46
+ return true;
47
+ }
48
+ return disabledDate?.(startOfDay(date)) === true;
49
+ }
50
+ function isValidDate(value) {
51
+ return value instanceof Date && !Number.isNaN(value.getTime());
52
+ }
53
+ function getNextCalendarRange(range, date) {
54
+ const selectedDate = startOfDay(date);
55
+ if (!range.start || range.end) {
56
+ return { start: selectedDate, end: null };
57
+ }
58
+ if (isBeforeDay(selectedDate, range.start)) {
59
+ return { start: selectedDate, end: startOfDay(range.start) };
60
+ }
61
+ return { start: startOfDay(range.start), end: selectedDate };
62
+ }
63
+ function getNextInputRange(range, part, date) {
64
+ const selectedDate = startOfDay(date);
65
+ if (part === "start") {
66
+ if (range.end && isAfterDay(selectedDate, range.end)) {
67
+ return { start: startOfDay(range.end), end: selectedDate };
68
+ }
69
+ return { start: selectedDate, end: range.end ? startOfDay(range.end) : null };
70
+ }
71
+ if (range.start && isBeforeDay(selectedDate, range.start)) {
72
+ return { start: selectedDate, end: startOfDay(range.start) };
73
+ }
74
+ return { start: range.start ? startOfDay(range.start) : null, end: selectedDate };
75
+ }
76
+ function clearRangePart(range, part) {
77
+ if (part === "start") {
78
+ return emptyRange;
79
+ }
80
+ return { start: range.start ? startOfDay(range.start) : null, end: null };
81
+ }
82
+ function getDefaultParseErrorMessage(part) {
83
+ return `Enter a valid ${part} date.`;
84
+ }
85
+ function getUnavailableDateMessage(part) {
86
+ return `Choose an available ${part} date.`;
87
+ }
88
+ function getInputValues(range, formatDate) {
89
+ return {
90
+ start: range.start ? formatDate(range.start) : "",
91
+ end: range.end ? formatDate(range.end) : ""
92
+ };
93
+ }
94
+ export function DateRangePicker({ defaultValue, description, disabledDate, endLabel = "End date", endPlaceholder = "End date", error, formatDate = defaultFormatDate, label, maxDate, minDate, onValueChange, parseDate, parseErrorMessage, ref, startLabel = "Start date", startPlaceholder = "Start date", value }) {
95
+ const generatedId = React.useId();
96
+ const labelId = hasRenderableNode(label) ? `${generatedId}-label` : undefined;
97
+ const descriptionId = hasRenderableNode(description) ? `${generatedId}-description` : undefined;
98
+ const externalErrorId = hasRenderableNode(error) ? `${generatedId}-error` : undefined;
99
+ const parseErrorId = `${generatedId}-parse-error`;
100
+ const startInputId = `${generatedId}-start`;
101
+ const endInputId = `${generatedId}-end`;
102
+ const hasLabel = hasRenderableNode(label);
103
+ const isControlled = value !== undefined;
104
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(() => normalizeRange(defaultValue));
105
+ const [openPart, setOpenPart] = React.useState(null);
106
+ const [parseError, setParseError] = React.useState(null);
107
+ const [draftValues, setDraftValues] = React.useState(() => getInputValues(normalizeRange(value ?? defaultValue), formatDate));
108
+ const currentRange = isControlled ? normalizeRange(value) : uncontrolledValue;
109
+ const displayedError = hasRenderableNode(error) ? error : parseError;
110
+ const hasError = hasRenderableNode(displayedError);
111
+ const errorId = hasRenderableNode(error) ? externalErrorId : parseError ? parseErrorId : undefined;
112
+ const describedBy = joinIds(descriptionId, errorId);
113
+ const fieldRefProps = ref !== undefined ? { ref } : {};
114
+ const calendarMonth = currentRange.start ?? currentRange.end ?? minDate ?? undefined;
115
+ const calendarProps = {
116
+ ...(calendarMonth !== undefined ? { defaultMonth: calendarMonth } : {}),
117
+ ...(disabledDate !== undefined ? { disabledDate } : {}),
118
+ ...(maxDate !== undefined ? { maxDate } : {}),
119
+ ...(minDate !== undefined ? { minDate } : {})
120
+ };
121
+ React.useEffect(() => {
122
+ setDraftValues(getInputValues(currentRange, formatDate));
123
+ }, [currentRange.start, currentRange.end, formatDate]);
124
+ const commitRange = (nextRange) => {
125
+ const normalizedRange = normalizeRange(nextRange);
126
+ setParseError(null);
127
+ if (!isControlled) {
128
+ setUncontrolledValue(normalizedRange);
129
+ }
130
+ onValueChange?.(normalizedRange);
131
+ };
132
+ const handleOpenChange = (part) => (nextOpen) => {
133
+ setOpenPart((currentPart) => {
134
+ if (nextOpen) {
135
+ return part;
136
+ }
137
+ return currentPart === part ? null : currentPart;
138
+ });
139
+ };
140
+ const handleCalendarSelect = (date) => {
141
+ if (!date) {
142
+ commitRange(emptyRange);
143
+ setOpenPart(null);
144
+ return;
145
+ }
146
+ const nextRange = currentRange.start && currentRange.end && openPart === "end"
147
+ ? getNextInputRange(currentRange, openPart, date)
148
+ : getNextCalendarRange(currentRange, date);
149
+ commitRange(nextRange);
150
+ if (nextRange.start && nextRange.end) {
151
+ setOpenPart(null);
152
+ }
153
+ };
154
+ const setDraftValue = (part, value) => {
155
+ setDraftValues((range) => ({ ...range, [part]: value }));
156
+ };
157
+ const commitInputValue = (part, value) => {
158
+ const trimmedValue = value.trim();
159
+ if (trimmedValue.length === 0) {
160
+ commitRange(clearRangePart(currentRange, part));
161
+ return;
162
+ }
163
+ if (!parseDate) {
164
+ return;
165
+ }
166
+ const parsedDate = parseDate?.(trimmedValue) ?? null;
167
+ if (!isValidDate(parsedDate)) {
168
+ setParseError(parseErrorMessage?.(part, trimmedValue) ?? getDefaultParseErrorMessage(part));
169
+ return;
170
+ }
171
+ const normalizedDate = startOfDay(parsedDate);
172
+ if (isUnavailableDate(normalizedDate, { disabledDate, maxDate, minDate })) {
173
+ setParseError(getUnavailableDateMessage(part));
174
+ return;
175
+ }
176
+ commitRange(getNextInputRange(currentRange, part, normalizedDate));
177
+ };
178
+ const renderDateInput = (part) => {
179
+ const inputId = part === "start" ? startInputId : endInputId;
180
+ const inputLabel = part === "start" ? startLabel : endLabel;
181
+ const placeholder = part === "start" ? startPlaceholder : endPlaceholder;
182
+ const calendarLabel = part === "start" ? "Open start date calendar" : "Open end date calendar";
183
+ const calendarDialogLabel = part === "start" ? "Start date calendar" : "End date calendar";
184
+ const inputValue = draftValues[part];
185
+ return (_jsxs("div", { className: "zvk-ui-date-range-picker__field", children: [_jsx("label", { className: "zvk-ui-date-range-picker__field-label", htmlFor: inputId, children: inputLabel }), _jsxs("div", { className: "zvk-ui-date-range-picker__input-row", children: [_jsx(Input, { "aria-describedby": describedBy, className: "zvk-ui-date-range-picker__input", id: inputId, invalid: hasError, placeholder: placeholder, readOnly: !parseDate, value: inputValue, onBlur: (event) => commitInputValue(part, event.currentTarget.value), onChange: (event) => setDraftValue(part, event.currentTarget.value), onKeyDown: (event) => {
186
+ if (event.key === "Enter") {
187
+ event.preventDefault();
188
+ commitInputValue(part, event.currentTarget.value);
189
+ }
190
+ } }), _jsxs(Popover, { open: openPart === part, onOpenChange: handleOpenChange(part), placement: "bottom-start", children: [_jsx(Popover.Trigger, { "aria-label": calendarLabel, className: "zvk-ui-date-range-picker__calendar-trigger", children: "Open" }), _jsx(Popover.Content, { "aria-label": calendarDialogLabel, className: "zvk-ui-date-range-picker__content", matchTriggerWidth: false, sideOffset: 4, children: _jsx(Calendar, { ...calendarProps, selected: currentRange.end ?? currentRange.start, onSelect: handleCalendarSelect }) })] })] })] }));
191
+ };
192
+ return (_jsxs(Field, { className: "zvk-ui-date-range-picker", invalid: hasError, ...fieldRefProps, children: [hasLabel ? _jsx(Field.Label, { id: labelId, children: label }) : null, _jsxs("div", { "aria-describedby": describedBy, "aria-labelledby": labelId, className: "zvk-ui-date-range-picker__group", role: hasLabel ? "group" : undefined, children: [_jsxs("div", { className: "zvk-ui-date-range-picker__controls", children: [renderDateInput("start"), renderDateInput("end")] }), currentRange.start || currentRange.end ? (_jsx("button", { className: "zvk-ui-date-range-picker__clear", type: "button", onClick: () => commitRange(emptyRange), children: "Clear date range" })) : null] }), hasRenderableNode(description) ? _jsx(Field.Description, { id: descriptionId, children: description }) : null, hasError ? _jsx(Field.Error, { id: errorId, children: displayedError }) : null] }));
193
+ }
@@ -0,0 +1,2 @@
1
+ export { DateRangePicker } from "./date-range-picker.js";
2
+ export type { DateRange, DateRangePickerProps } from "./date-range-picker.js";
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ export { DateRangePicker } from "./date-range-picker.js";
@@ -43,7 +43,7 @@ interface DialogPortalProps {
43
43
  declare function DialogRoot({ children, className, container, defaultOpen, disableEscapeKeyDown, disableOutsidePointerDown, onOpenChange, open: openProp, ref, ...props }: DialogProps): React.JSX.Element;
44
44
  declare function DialogPortal({ container, children }: DialogPortalProps): React.JSX.Element;
45
45
  declare function DialogOverlay({ className, ref, ...props }: DialogOverlayProps): React.JSX.Element;
46
- declare function DialogContent({ className, children, forceMount, ref, ...props }: DialogContentProps): React.JSX.Element | null;
46
+ declare function DialogContent({ className, children, forceMount, onAnimationEnd, onTransitionEnd, ref, ...props }: DialogContentProps): React.JSX.Element | null;
47
47
  declare function DialogHeader({ className, ref, ...props }: DialogHeaderProps): React.JSX.Element;
48
48
  declare function DialogTitle({ className, ref, ...props }: DialogTitleProps): React.JSX.Element;
49
49
  declare function DialogDescription({ className, ref, ...props }: DialogDescriptionProps): React.JSX.Element;
@@ -6,6 +6,7 @@ import { cn } from "../../utils/cn.js";
6
6
  import { useControllableState } from "../../hooks/use-controllable-state.js";
7
7
  import { DismissableLayer } from "../../internal/dismissable-layer/dismissable-layer.js";
8
8
  import { FocusScope } from "../../internal/focus/focus-scope.js";
9
+ import { usePresence } from "../../internal/presence/index.js";
9
10
  import { lockScroll, unlockScroll } from "../../internal/scroll-lock/scroll-lock.js";
10
11
  import { Portal } from "../../internal/portal/index.js";
11
12
  import { Slot } from "../../internal/slot/index.js";
@@ -29,6 +30,7 @@ function composeRefs(...refs) {
29
30
  }
30
31
  };
31
32
  }
33
+ const dialogExitDurationMs = 180;
32
34
  function DialogRoot({ children, className, container, defaultOpen = false, disableEscapeKeyDown = false, disableOutsidePointerDown = false, onOpenChange, open: openProp, ref, ...props }) {
33
35
  const [open, setOpen] = useControllableState({
34
36
  ...(openProp !== undefined ? { value: openProp } : {}),
@@ -51,15 +53,6 @@ function DialogRoot({ children, className, container, defaultOpen = false, disab
51
53
  setDescribedBy((current) => (current === id ? undefined : current));
52
54
  };
53
55
  }, []);
54
- React.useLayoutEffect(() => {
55
- if (!open) {
56
- return;
57
- }
58
- lockScroll();
59
- return () => {
60
- unlockScroll();
61
- };
62
- }, [open]);
63
56
  return (_jsx(DialogContext.Provider, { value: {
64
57
  close: () => {
65
58
  setOpen(false);
@@ -88,16 +81,28 @@ function DialogPortal({ container, children }) {
88
81
  function DialogOverlay({ className, ref, ...props }) {
89
82
  return _jsx("div", { ...props, ref: ref, className: cn("zvk-ui-dialog__overlay", className) });
90
83
  }
91
- function DialogContent({ className, children, forceMount = false, ref, ...props }) {
84
+ function DialogContent({ className, children, forceMount = false, onAnimationEnd, onTransitionEnd, ref, ...props }) {
92
85
  const { close, container, contentId, describedBy, disableEscapeKeyDown, disableOutsidePointerDown, labelledBy, open } = useDialogContext("Dialog.Content");
93
- if (!open && !forceMount) {
86
+ const presence = usePresence({ open, forceMount, exitDurationMs: dialogExitDurationMs });
87
+ const hidden = !open && forceMount && presence.motionState === "idle";
88
+ const shouldLockScroll = presence.present && (!presence.inert || presence.motionState === "exiting");
89
+ React.useLayoutEffect(() => {
90
+ if (!shouldLockScroll) {
91
+ return;
92
+ }
93
+ lockScroll();
94
+ return () => {
95
+ unlockScroll();
96
+ };
97
+ }, [shouldLockScroll]);
98
+ if (!presence.present) {
94
99
  return null;
95
100
  }
96
- const content = (_jsx("div", { ...props, ref: ref, id: contentId, role: "dialog", "aria-modal": true, "aria-labelledby": labelledBy, "aria-describedby": describedBy, className: cn("zvk-ui-dialog__content", className), "data-state": open ? "open" : "closed", hidden: open ? undefined : true, children: children }));
101
+ const content = (_jsx("div", { ...props, ref: ref, id: contentId, role: "dialog", "aria-hidden": presence.inert ? true : undefined, "aria-modal": open ? true : undefined, "aria-labelledby": labelledBy, "aria-describedby": describedBy, className: cn("zvk-ui-dialog__content", className), "data-motion-state": presence.motionState, "data-state": presence.state, hidden: hidden ? true : undefined, inert: presence.inert ? true : undefined, onAnimationEnd: composeEventHandlers(onAnimationEnd, presence.onExitComplete, { checkDefaultPrevented: false }), onTransitionEnd: composeEventHandlers(onTransitionEnd, presence.onExitComplete, { checkDefaultPrevented: false }), children: children }));
97
102
  if (!open) {
98
- return (_jsxs(DialogPortal, { container: container, children: [_jsx(DialogOverlay, { "aria-hidden": true, hidden: true }), content] }));
103
+ return (_jsxs(DialogPortal, { container: container, children: [_jsx(DialogOverlay, { "aria-hidden": true, "data-state": presence.state, hidden: hidden ? true : undefined }), content] }));
99
104
  }
100
- return (_jsxs(DialogPortal, { container: container, children: [_jsx(DialogOverlay, { "aria-hidden": true }), _jsx(DismissableLayer, { open: open, disableEscapeKeyDown: disableEscapeKeyDown, disableOutsidePointerDown: disableOutsidePointerDown, onDismiss: close, children: _jsx(FocusScope, { trapped: true, active: open, children: content }) })] }));
105
+ return (_jsxs(DialogPortal, { container: container, children: [_jsx(DialogOverlay, { "aria-hidden": true, "data-state": presence.state }), _jsx(DismissableLayer, { open: open, disableEscapeKeyDown: disableEscapeKeyDown, disableOutsidePointerDown: disableOutsidePointerDown, onDismiss: close, children: _jsx(FocusScope, { trapped: true, active: open, children: content }) })] }));
101
106
  }
102
107
  function DialogHeader({ className, ref, ...props }) {
103
108
  return _jsx("div", { ...props, ref: ref, className: cn("zvk-ui-dialog__header", className) });
@@ -57,8 +57,8 @@ export interface DropdownMenuSubProps extends React.HTMLAttributes<HTMLDivElemen
57
57
  ref?: React.Ref<HTMLDivElement>;
58
58
  }
59
59
  declare function DropdownMenuRoot({ children, className, container, defaultOpen, onOpenChange, open: openProp, placement, sideOffset, collisionPadding, matchTriggerWidth, ...props }: DropdownMenuProps): React.JSX.Element;
60
- declare function DropdownMenuTrigger({ asChild, className, disabled, onClick, ref, type, ...props }: DropdownMenuTriggerProps): React.JSX.Element;
61
- declare function DropdownMenuContent({ align, alignOffset, children, className, forceMount, side, sideOffset, collisionPadding, matchTriggerWidth, ref, onKeyDown, ...props }: DropdownMenuContentProps): React.JSX.Element | null;
60
+ declare function DropdownMenuTrigger({ asChild, children, className, disabled, onClick, ref, type, ...props }: DropdownMenuTriggerProps): React.JSX.Element;
61
+ declare function DropdownMenuContent({ align, alignOffset, children, className, forceMount, side, sideOffset, collisionPadding, matchTriggerWidth, ref, onAnimationEnd, onKeyDown, ...props }: DropdownMenuContentProps): React.JSX.Element | null;
62
62
  declare function DropdownMenuItem({ asChild, children, className, disabled, onClick, onSelect, onKeyDown, ref, ...props }: DropdownMenuItemProps): React.JSX.Element;
63
63
  declare function DropdownMenuSeparator({ className, ref, ...props }: DropdownMenuSeparatorProps): React.JSX.Element;
64
64
  declare function DropdownMenuLabel({ className, ref, ...props }: DropdownMenuLabelProps): React.JSX.Element;