@zvk/ui 0.1.8 → 0.1.11

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 +23 -0
  2. package/README.md +5 -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 +1730 -222
  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 +137 -61
@@ -8,6 +8,7 @@ import { Portal } from "../../internal/portal/index.js";
8
8
  import { DismissableLayer } from "../../internal/dismissable-layer/dismissable-layer.js";
9
9
  import { useFloatingPosition } from "../../internal/floating/index.js";
10
10
  import { placementFromSideAlign, placementParts } from "../../internal/floating/placement-aliases.js";
11
+ import { usePresence } from "../../internal/presence/index.js";
11
12
  import { Slot } from "../../internal/slot/index.js";
12
13
  const defaultContentPositioning = {
13
14
  sideOffset: 8,
@@ -15,6 +16,7 @@ const defaultContentPositioning = {
15
16
  collisionPadding: 0,
16
17
  matchTriggerWidth: false
17
18
  };
19
+ const dropdownMenuExitDurationMs = 120;
18
20
  const DropdownMenuContext = React.createContext(null);
19
21
  function useDropdownMenuContext(calledBy) {
20
22
  const context = React.useContext(DropdownMenuContext);
@@ -23,6 +25,9 @@ function useDropdownMenuContext(calledBy) {
23
25
  }
24
26
  return context;
25
27
  }
28
+ function focusWithoutScroll(element) {
29
+ element?.focus({ preventScroll: true });
30
+ }
26
31
  function composeRefs(...refs) {
27
32
  return (node) => {
28
33
  for (const ref of refs) {
@@ -111,7 +116,7 @@ function DropdownMenuRoot({ children, className, container, defaultOpen = false,
111
116
  }, []);
112
117
  React.useEffect(() => {
113
118
  if (wasOpenRef.current && !open) {
114
- triggerRef.current?.focus();
119
+ focusWithoutScroll(triggerRef.current);
115
120
  }
116
121
  wasOpenRef.current = open;
117
122
  }, [open]);
@@ -119,7 +124,7 @@ function DropdownMenuRoot({ children, className, container, defaultOpen = false,
119
124
  if (open && triggerRef.current !== null) {
120
125
  const focusFirstItem = () => {
121
126
  const first = getEnabledItems()[0];
122
- first?.ref?.focus();
127
+ focusWithoutScroll(first?.ref);
123
128
  };
124
129
  queueMicrotask(focusFirstItem);
125
130
  }
@@ -141,35 +146,31 @@ function DropdownMenuRoot({ children, className, container, defaultOpen = false,
141
146
  ...(container === undefined ? {} : { container })
142
147
  }, children: _jsx("div", { ...props, className: cn("zvk-ui-dropdown-menu", className), "data-state": open ? "open" : "closed", children: children }) }));
143
148
  }
144
- function DropdownMenuTrigger({ asChild = false, className, disabled, onClick, ref, type = "button", ...props }) {
145
- const { contentId, open, setOpen, triggerRef, triggerId, referenceRef, getEnabledItems } = useDropdownMenuContext("DropdownMenu.Trigger");
149
+ function DropdownMenuTrigger({ asChild = false, children, className, disabled, onClick, ref, type = "button", ...props }) {
150
+ const { contentId, open, setOpen, triggerRef, triggerId, referenceRef } = useDropdownMenuContext("DropdownMenu.Trigger");
146
151
  const handleClick = () => {
147
152
  if (disabled) {
148
153
  return;
149
154
  }
150
155
  const nextOpen = !open;
151
156
  setOpen(nextOpen);
152
- if (!nextOpen) {
153
- return;
154
- }
155
- queueMicrotask(() => {
156
- const first = getEnabledItems()[0];
157
- first?.ref?.focus();
158
- });
159
157
  };
160
158
  if (asChild) {
161
- return (_jsx(Slot, { ...props, ref: composeRefs(ref, triggerRef, referenceRef), id: triggerId, "aria-controls": contentId, "aria-disabled": disabled ? true : undefined, "aria-expanded": open ? "true" : "false", "aria-haspopup": "menu", className: cn("zvk-ui-dropdown-menu__trigger", className), "data-disabled": disabled ? "true" : undefined, "data-state": open ? "open" : "closed", onClick: composeEventHandlers(onClick, handleClick), children: props.children }));
159
+ return (_jsx(Slot, { ...props, ref: composeRefs(ref, triggerRef, referenceRef), id: triggerId, "aria-controls": contentId, "aria-disabled": disabled ? true : undefined, "aria-expanded": open ? "true" : "false", "aria-haspopup": "menu", className: cn("zvk-ui-dropdown-menu__trigger", className), "data-disabled": disabled ? "true" : undefined, "data-state": open ? "open" : "closed", onClick: composeEventHandlers(onClick, handleClick), children: children }));
162
160
  }
163
- return (_jsx("button", { ...props, ref: composeRefs(ref, triggerRef, referenceRef), type: type, disabled: disabled, id: triggerId, role: "button", "aria-haspopup": "menu", "aria-expanded": open ? "true" : "false", "aria-controls": contentId, className: cn("zvk-ui-dropdown-menu__trigger", className), "data-state": open ? "open" : "closed", onClick: composeEventHandlers(onClick, handleClick) }));
161
+ return (_jsxs("button", { ...props, ref: composeRefs(ref, triggerRef, referenceRef), type: type, disabled: disabled, id: triggerId, role: "button", "aria-haspopup": "menu", "aria-expanded": open ? "true" : "false", "aria-controls": contentId, className: cn("zvk-ui-dropdown-menu__trigger", className), "data-state": open ? "open" : "closed", onClick: composeEventHandlers(onClick, handleClick), children: [_jsx("span", { className: "zvk-ui-dropdown-menu__trigger-label", children: children }), _jsx("span", { className: "zvk-ui-dropdown-menu__trigger-indicator", "aria-hidden": "true" })] }));
164
162
  }
165
- function DropdownMenuContent({ align, alignOffset = defaultContentPositioning.alignOffset, children, className, forceMount = false, side, sideOffset = defaultContentPositioning.sideOffset, collisionPadding = defaultContentPositioning.collisionPadding, matchTriggerWidth = defaultContentPositioning.matchTriggerWidth, ref, onKeyDown, ...props }) {
163
+ function DropdownMenuContent({ align, alignOffset = defaultContentPositioning.alignOffset, children, className, forceMount = false, side, sideOffset = defaultContentPositioning.sideOffset, collisionPadding = defaultContentPositioning.collisionPadding, matchTriggerWidth = defaultContentPositioning.matchTriggerWidth, ref, onAnimationEnd, onKeyDown, ...props }) {
166
164
  const { container, contentId, open, setOpen, updateContentPositioning, triggerId, floatingRef, floatingPlacement, floatingStyle, getEnabledItems } = useDropdownMenuContext("DropdownMenu.Content");
165
+ const presence = usePresence({ open, forceMount, exitDurationMs: dropdownMenuExitDurationMs });
166
+ const wasOpenRef = React.useRef(open);
167
+ const isClosing = wasOpenRef.current && !open;
167
168
  const focusItem = React.useCallback((index, items) => {
168
169
  if (items.length === 0) {
169
170
  return;
170
171
  }
171
172
  const clampedIndex = ((index % items.length) + items.length) % items.length;
172
- items[clampedIndex]?.ref?.focus();
173
+ focusWithoutScroll(items[clampedIndex]?.ref);
173
174
  }, []);
174
175
  const findCurrentIndex = React.useCallback((items) => {
175
176
  const activeElement = document.activeElement;
@@ -196,10 +197,10 @@ function DropdownMenuContent({ align, alignOffset = defaultContentPositioning.al
196
197
  return;
197
198
  }
198
199
  if (isEnd) {
199
- enabledItems.at(-1)?.ref?.focus();
200
+ focusWithoutScroll(enabledItems.at(-1)?.ref);
200
201
  return;
201
202
  }
202
- enabledItems[0]?.ref?.focus();
203
+ focusWithoutScroll(enabledItems[0]?.ref);
203
204
  }, [getEnabledItems]);
204
205
  React.useEffect(() => {
205
206
  updateContentPositioning({
@@ -213,9 +214,13 @@ function DropdownMenuContent({ align, alignOffset = defaultContentPositioning.al
213
214
  updateContentPositioning(defaultContentPositioning);
214
215
  };
215
216
  }, [align, alignOffset, collisionPadding, matchTriggerWidth, side, sideOffset, updateContentPositioning]);
216
- if (!open && !forceMount) {
217
+ React.useEffect(() => {
218
+ wasOpenRef.current = open;
219
+ }, [open]);
220
+ if (!presence.present) {
217
221
  return null;
218
222
  }
223
+ const hidden = presence.inert && !isClosing && presence.motionState !== "exiting" ? true : undefined;
219
224
  const content = (_jsx("div", { ...props, ref: (node) => {
220
225
  floatingRef(node);
221
226
  if (typeof ref === "function") {
@@ -224,7 +229,7 @@ function DropdownMenuContent({ align, alignOffset = defaultContentPositioning.al
224
229
  else if (ref) {
225
230
  ref.current = node;
226
231
  }
227
- }, id: contentId, role: "menu", "aria-labelledby": triggerId, className: cn("zvk-ui-dropdown-menu__content", className), style: floatingStyle, "data-align": placementParts(floatingPlacement).align, "data-side": placementParts(floatingPlacement).side, "data-state": open ? "open" : "closed", hidden: open ? undefined : true, onKeyDown: composeEventHandlers(onKeyDown, (event) => {
232
+ }, "aria-hidden": presence.inert ? true : undefined, id: contentId, inert: presence.inert ? true : undefined, role: "menu", "aria-labelledby": triggerId, className: cn("zvk-ui-dropdown-menu__content", className), style: floatingStyle, "data-align": placementParts(floatingPlacement).align, "data-side": placementParts(floatingPlacement).side, "data-state": presence.state, hidden: hidden, onAnimationEnd: composeEventHandlers(onAnimationEnd, presence.onExitComplete), onKeyDown: composeEventHandlers(onKeyDown, (event) => {
228
233
  if (event.key === "ArrowDown") {
229
234
  event.preventDefault();
230
235
  moveFocus("next");
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ export interface FileDropzoneProps extends React.HTMLAttributes<HTMLDivElement> {
3
+ label?: React.ReactNode;
4
+ description?: React.ReactNode;
5
+ error?: React.ReactNode;
6
+ accept?: string;
7
+ multiple?: boolean;
8
+ disabled?: boolean;
9
+ required?: boolean;
10
+ maxFiles?: number;
11
+ maxSizeBytes?: number;
12
+ value?: readonly File[];
13
+ defaultFiles?: readonly File[];
14
+ onFilesChange?: (files: readonly File[]) => void;
15
+ ref?: React.Ref<HTMLDivElement>;
16
+ }
17
+ export interface FileDropzoneListProps extends React.HTMLAttributes<HTMLUListElement> {
18
+ ref?: React.Ref<HTMLUListElement>;
19
+ }
20
+ declare function FileDropzoneRoot({ accept, children, className, defaultFiles, description, disabled, error, id, label, maxFiles, maxSizeBytes, multiple, onFilesChange, ref, required, value, ...props }: FileDropzoneProps): React.JSX.Element;
21
+ declare function FileDropzoneList({ className, ref, ...props }: FileDropzoneListProps): React.JSX.Element | null;
22
+ export declare const FileDropzone: typeof FileDropzoneRoot & {
23
+ List: typeof FileDropzoneList;
24
+ };
25
+ export {};
@@ -0,0 +1,171 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Field } from "../field/field.js";
5
+ import { cn } from "../../utils/cn.js";
6
+ import { useControllableState } from "../../hooks/use-controllable-state.js";
7
+ const FileDropzoneContext = React.createContext(null);
8
+ function useFileDropzoneContext(calledBy) {
9
+ const context = React.useContext(FileDropzoneContext);
10
+ if (context === null) {
11
+ throw new Error(`"${calledBy}" must be used within <FileDropzone />`);
12
+ }
13
+ return context;
14
+ }
15
+ function hasRenderableNode(value) {
16
+ return value !== undefined && value !== null && value !== false;
17
+ }
18
+ function joinIds(...ids) {
19
+ const value = ids.filter(Boolean).join(" ");
20
+ return value.length > 0 ? value : undefined;
21
+ }
22
+ function normalizeFiles(files) {
23
+ return Array.from(files ?? []);
24
+ }
25
+ function parseAccept(accept) {
26
+ return accept
27
+ ?.split(",")
28
+ .map((value) => value.trim())
29
+ .filter(Boolean) ?? [];
30
+ }
31
+ function fileMatchesAccept(file, accept) {
32
+ const tokens = parseAccept(accept);
33
+ if (tokens.length === 0) {
34
+ return true;
35
+ }
36
+ const fileName = file.name.toLowerCase();
37
+ const mimeType = file.type.toLowerCase();
38
+ return tokens.some((token) => {
39
+ const normalized = token.toLowerCase();
40
+ if (normalized.startsWith(".")) {
41
+ return fileName.endsWith(normalized);
42
+ }
43
+ if (normalized.endsWith("/*")) {
44
+ return mimeType.startsWith(normalized.slice(0, -1));
45
+ }
46
+ return mimeType === normalized;
47
+ });
48
+ }
49
+ function formatFileSize(size) {
50
+ if (size < 1024) {
51
+ return `${size} B`;
52
+ }
53
+ const kib = size / 1024;
54
+ if (kib < 1024) {
55
+ return `${kib.toFixed(kib >= 10 ? 0 : 1)} KiB`;
56
+ }
57
+ const mib = kib / 1024;
58
+ return `${mib.toFixed(mib >= 10 ? 0 : 1)} MiB`;
59
+ }
60
+ function getValidationError(files, { accept, maxFiles, maxSizeBytes }) {
61
+ const tooManyFiles = maxFiles !== undefined && files.length > maxFiles;
62
+ if (tooManyFiles) {
63
+ return `You can add up to ${maxFiles} file${maxFiles === 1 ? "" : "s"}.`;
64
+ }
65
+ const tooLargeFile = maxSizeBytes !== undefined ? files.find((file) => file.size > maxSizeBytes) : undefined;
66
+ if (tooLargeFile) {
67
+ return `${tooLargeFile.name} is too large.`;
68
+ }
69
+ const rejectedFile = files.find((file) => !fileMatchesAccept(file, accept));
70
+ if (rejectedFile) {
71
+ return `${rejectedFile.name} does not match the accepted file types.`;
72
+ }
73
+ return null;
74
+ }
75
+ function getNextFiles(currentFiles, incomingFiles, multiple) {
76
+ if (!multiple) {
77
+ return incomingFiles.slice(0, 1);
78
+ }
79
+ return [...currentFiles, ...incomingFiles];
80
+ }
81
+ function FileDropzoneRoot({ accept, children, className, defaultFiles = [], description, disabled = false, error, id, label, maxFiles, maxSizeBytes, multiple = false, onFilesChange, ref, required, value, ...props }) {
82
+ const generatedId = React.useId();
83
+ const inputId = id ? `${id}-input` : generatedId;
84
+ const labelId = label !== undefined ? `${inputId}-label` : undefined;
85
+ const descriptionId = hasRenderableNode(description) ? `${inputId}-description` : undefined;
86
+ const errorId = `${inputId}-error`;
87
+ const [validationError, setValidationError] = React.useState(null);
88
+ const [files, setFiles] = useControllableState({
89
+ ...(value !== undefined ? { value } : {}),
90
+ defaultValue: defaultFiles,
91
+ ...(onFilesChange ? { onChange: onFilesChange } : {})
92
+ });
93
+ const effectiveMaxFiles = multiple ? maxFiles : 1;
94
+ const invalidState = Boolean(error) || validationError !== null;
95
+ const inputRequired = Boolean(required && files.length === 0);
96
+ const describedBy = joinIds(descriptionId, invalidState ? errorId : undefined);
97
+ const [isDragging, setIsDragging] = React.useState(false);
98
+ const dragCounterRef = React.useRef(0);
99
+ const removeFile = React.useCallback((file) => {
100
+ setValidationError(null);
101
+ setFiles((currentFiles) => currentFiles.filter((currentFile) => currentFile !== file));
102
+ }, [setFiles]);
103
+ const acceptFiles = React.useCallback((incomingFiles) => {
104
+ if (disabled) {
105
+ return;
106
+ }
107
+ const nextFiles = getNextFiles(files, incomingFiles, multiple);
108
+ const validationErrorMessage = getValidationError(nextFiles, {
109
+ ...(accept !== undefined ? { accept } : {}),
110
+ ...(effectiveMaxFiles !== undefined ? { maxFiles: effectiveMaxFiles } : {}),
111
+ ...(maxSizeBytes !== undefined ? { maxSizeBytes } : {})
112
+ });
113
+ if (validationErrorMessage !== null) {
114
+ setValidationError(validationErrorMessage);
115
+ return;
116
+ }
117
+ setValidationError(null);
118
+ setFiles(nextFiles);
119
+ }, [accept, disabled, effectiveMaxFiles, files, maxSizeBytes, multiple, setFiles]);
120
+ const handleInputChange = React.useCallback((event) => {
121
+ acceptFiles(normalizeFiles(event.currentTarget.files));
122
+ event.currentTarget.value = "";
123
+ }, [acceptFiles]);
124
+ const handleDragEnter = React.useCallback((event) => {
125
+ if (disabled) {
126
+ return;
127
+ }
128
+ event.preventDefault();
129
+ dragCounterRef.current += 1;
130
+ setIsDragging(true);
131
+ }, [disabled]);
132
+ const handleDragOver = React.useCallback((event) => {
133
+ event.preventDefault();
134
+ }, []);
135
+ const handleDragLeave = React.useCallback((event) => {
136
+ if (disabled) {
137
+ return;
138
+ }
139
+ event.preventDefault();
140
+ dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
141
+ if (dragCounterRef.current === 0) {
142
+ setIsDragging(false);
143
+ }
144
+ }, [disabled]);
145
+ const handleDrop = React.useCallback((event) => {
146
+ if (disabled) {
147
+ return;
148
+ }
149
+ event.preventDefault();
150
+ dragCounterRef.current = 0;
151
+ setIsDragging(false);
152
+ acceptFiles(normalizeFiles(event.dataTransfer.files));
153
+ }, [acceptFiles, disabled]);
154
+ const input = (_jsx("div", { className: "zvk-ui-file-dropzone__panel", "aria-hidden": "true", children: _jsx("span", { "aria-hidden": "true", className: "zvk-ui-file-dropzone__prompt", children: isDragging ? "Drop files to add them" : "Drop files here or browse" }) }));
155
+ const content = (_jsx(FileDropzoneContext.Provider, { value: {
156
+ disabled,
157
+ files,
158
+ removeFile
159
+ }, children: _jsxs(Field, { ...props, id: id, ...(label !== undefined || description !== undefined || error !== undefined ? { role: "group" } : {}), "aria-describedby": describedBy, "aria-labelledby": labelId, className: cn("zvk-ui-file-dropzone", className), "data-dragging": isDragging ? "true" : undefined, "data-disabled": disabled ? "true" : undefined, "data-invalid": invalidState ? "true" : undefined, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, disabled: disabled, invalid: invalidState, ...(ref !== undefined ? { ref } : {}), required: Boolean(required), children: [hasRenderableNode(label) ? _jsx(Field.Label, { id: labelId, htmlFor: inputId, children: label }) : null, _jsxs("div", { className: "zvk-ui-file-dropzone__surface", children: [input, _jsx("input", { accept: accept, "aria-describedby": describedBy, "aria-invalid": invalidState ? true : undefined, "aria-labelledby": labelId, "aria-required": required ? true : undefined, className: "zvk-ui-file-dropzone__input", "data-disabled": disabled ? "true" : undefined, "data-invalid": invalidState ? "true" : undefined, disabled: disabled, id: inputId, multiple: multiple, onChange: handleInputChange, required: inputRequired, type: "file" })] }), hasRenderableNode(description) ? _jsx(Field.Description, { id: descriptionId, children: description }) : null, invalidState ? (_jsxs(Field.Error, { id: errorId, children: [validationError !== null ? _jsx("span", { children: validationError }) : null, validationError !== null && hasRenderableNode(error) ? _jsx("span", { children: " " }) : null, hasRenderableNode(error) ? error : null] })) : null, children] }) }));
160
+ return content;
161
+ }
162
+ function FileDropzoneList({ className, ref, ...props }) {
163
+ const { disabled, files, removeFile } = useFileDropzoneContext("FileDropzone.List");
164
+ if (files.length === 0) {
165
+ return null;
166
+ }
167
+ return (_jsx("ul", { ...props, ref: ref, "aria-label": props["aria-label"] ?? "Selected files", className: cn("zvk-ui-file-dropzone__list", className), children: files.map((file) => (_jsxs("li", { className: "zvk-ui-file-dropzone__item", children: [_jsx("span", { className: "zvk-ui-file-dropzone__item-name", children: file.name }), _jsx("span", { className: "zvk-ui-file-dropzone__item-meta", children: formatFileSize(file.size) }), _jsx("button", { "aria-label": `Remove ${file.name}`, className: "zvk-ui-file-dropzone__remove", disabled: disabled, type: "button", onClick: () => removeFile(file), children: "Remove" })] }, `${file.name}-${file.size}-${file.lastModified}`))) }));
168
+ }
169
+ export const FileDropzone = Object.assign(FileDropzoneRoot, {
170
+ List: FileDropzoneList
171
+ });
@@ -0,0 +1,2 @@
1
+ export { FileDropzone } from "./file-dropzone.js";
2
+ export type { FileDropzoneListProps, FileDropzoneProps } from "./file-dropzone.js";
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ export { FileDropzone } from "./file-dropzone.js";
@@ -21,10 +21,25 @@ export interface FormControlProps {
21
21
  export interface FormTextProps extends React.HTMLAttributes<HTMLParagraphElement> {
22
22
  ref?: React.Ref<HTMLParagraphElement>;
23
23
  }
24
- export declare function Form({ className, ref, ...props }: FormProps): React.JSX.Element;
24
+ export interface FormValidationSummaryError {
25
+ fieldId?: string;
26
+ href?: string;
27
+ message: string;
28
+ }
29
+ export interface FormValidationSummaryProps extends React.HTMLAttributes<HTMLDivElement> {
30
+ errors: readonly FormValidationSummaryError[];
31
+ ref?: React.Ref<HTMLDivElement>;
32
+ title: string;
33
+ }
34
+ declare function FormRoot({ className, ref, ...props }: FormProps): React.JSX.Element;
25
35
  export declare function FormField({ children, className, disabled, id, invalid, required, ...props }: FormFieldProps): React.JSX.Element;
26
36
  export declare function FormItem({ className, ref, ...props }: FormItemProps): React.JSX.Element;
27
37
  export declare function FormLabel({ className, htmlFor, ref, ...props }: FormLabelProps): React.JSX.Element;
28
38
  export declare function FormControl({ children }: FormControlProps): React.ReactElement<Record<string, unknown>, string | React.JSXElementConstructor<any>>;
29
39
  export declare function FormDescription({ className, ...props }: FormTextProps): React.JSX.Element;
30
40
  export declare function FormMessage({ className, role, ...props }: FormTextProps): React.JSX.Element;
41
+ declare function FormValidationSummary({ className, errors, ref, title, ...props }: FormValidationSummaryProps): React.JSX.Element | null;
42
+ export declare const Form: typeof FormRoot & {
43
+ ValidationSummary: typeof FormValidationSummary;
44
+ };
45
+ export {};
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { cn } from "../../utils/cn.js";
5
5
  const FormFieldContext = React.createContext(null);
@@ -14,7 +14,7 @@ function joinIds(ids) {
14
14
  const value = ids.filter(Boolean).join(" ");
15
15
  return value.length > 0 ? value : undefined;
16
16
  }
17
- export function Form({ className, ref, ...props }) {
17
+ function FormRoot({ className, ref, ...props }) {
18
18
  return _jsx("form", { ...props, ref: ref, className: cn("zvk-ui-form", className) });
19
19
  }
20
20
  export function FormField({ children, className, disabled, id, invalid, required, ...props }) {
@@ -86,3 +86,14 @@ export function FormDescription({ className, ...props }) {
86
86
  export function FormMessage({ className, role = "alert", ...props }) {
87
87
  return _jsx(FormText, { ...props, className: cn("zvk-ui-form-message", className), role: role, textRole: "FormMessage" });
88
88
  }
89
+ function FormValidationSummary({ className, errors, ref, title, ...props }) {
90
+ const titleId = React.useId();
91
+ if (errors.length === 0) {
92
+ return null;
93
+ }
94
+ return (_jsxs("div", { ...props, "aria-atomic": "true", "aria-live": "polite", "aria-labelledby": titleId, className: cn("zvk-ui-form-validation-summary", className), ref: ref, role: "status", children: [_jsx("p", { id: titleId, className: "zvk-ui-form-validation-summary__title", children: title }), _jsx("ul", { className: "zvk-ui-form-validation-summary__list", children: errors.map((error, index) => {
95
+ const href = error.href ?? (error.fieldId !== undefined ? `#${error.fieldId}` : undefined);
96
+ return (_jsx("li", { className: "zvk-ui-form-validation-summary__item", children: href !== undefined ? (_jsx("a", { className: "zvk-ui-form-validation-summary__link", href: href, children: error.message })) : (error.message) }, `${error.message}-${index}`));
97
+ }) })] }));
98
+ }
99
+ export const Form = Object.assign(FormRoot, { ValidationSummary: FormValidationSummary });
@@ -1,2 +1,2 @@
1
1
  export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./form.js";
2
- export type { FormControlProps, FormFieldProps, FormItemProps, FormLabelProps, FormProps, FormTextProps } from "./form.js";
2
+ export type { FormControlProps, FormFieldProps, FormItemProps, FormLabelProps, FormProps, FormTextProps, FormValidationSummaryError, FormValidationSummaryProps } from "./form.js";
@@ -29,7 +29,7 @@ export interface HoverCardContentProps extends React.HTMLAttributes<HTMLDivEleme
29
29
  }
30
30
  declare function HoverCardRoot({ children, closeDelay, defaultOpen, disabled, onOpenChange, open: openProp, openDelay, placement }: HoverCardProps): React.JSX.Element;
31
31
  declare function HoverCardTrigger({ children, asChild: _asChild }: HoverCardTriggerProps): React.ReactElement<unknown, string | React.JSXElementConstructor<any>>;
32
- declare function HoverCardContent({ align, alignOffset, children, className, collisionPadding, container, forceMount, id, onBlur, onFocus, onMouseEnter, onMouseLeave, onPointerEnter, onPointerLeave, placement, ref, side, sideOffset, style, ...props }: HoverCardContentProps): React.JSX.Element | null;
32
+ declare function HoverCardContent({ align, alignOffset, children, className, collisionPadding, container, forceMount, id, onAnimationEnd, onBlur, onFocus, onMouseEnter, onMouseLeave, onPointerEnter, onPointerLeave, placement, ref, side, sideOffset, style, ...props }: HoverCardContentProps): React.JSX.Element | null;
33
33
  export declare const HoverCard: typeof HoverCardRoot & {
34
34
  Trigger: typeof HoverCardTrigger;
35
35
  Content: typeof HoverCardContent;
@@ -5,6 +5,7 @@ import { DismissableLayer } from "../../internal/dismissable-layer/index.js";
5
5
  import { useFloatingPosition } from "../../internal/floating/index.js";
6
6
  import { placementFromSideAlign } from "../../internal/floating/placement-aliases.js";
7
7
  import { Portal } from "../../internal/portal/index.js";
8
+ import { usePresence } from "../../internal/presence/index.js";
8
9
  import { useControllableState } from "../../hooks/use-controllable-state.js";
9
10
  import { cn } from "../../utils/cn.js";
10
11
  import { composeEventHandlers } from "../../utils/compose-event-handlers.js";
@@ -13,6 +14,7 @@ const defaultContentPositioning = {
13
14
  alignOffset: 0,
14
15
  collisionPadding: 0
15
16
  };
17
+ const hoverCardExitDurationMs = 120;
16
18
  const HoverCardContext = React.createContext(null);
17
19
  function useHoverCardContext(calledBy) {
18
20
  const context = React.useContext(HoverCardContext);
@@ -236,8 +238,11 @@ function HoverCardTrigger({ children, asChild: _asChild = false }) {
236
238
  onPointerLeave: composeEventHandlers(childProps.onPointerLeave, context.closeWithDelay)
237
239
  });
238
240
  }
239
- function HoverCardContent({ align, alignOffset = defaultContentPositioning.alignOffset, children, className, collisionPadding = defaultContentPositioning.collisionPadding, container, forceMount = false, id, onBlur, onFocus, onMouseEnter, onMouseLeave, onPointerEnter, onPointerLeave, placement, ref, side, sideOffset = defaultContentPositioning.sideOffset, style, ...props }) {
241
+ function HoverCardContent({ align, alignOffset = defaultContentPositioning.alignOffset, children, className, collisionPadding = defaultContentPositioning.collisionPadding, container, forceMount = false, id, onAnimationEnd, onBlur, onFocus, onMouseEnter, onMouseLeave, onPointerEnter, onPointerLeave, placement, ref, side, sideOffset = defaultContentPositioning.sideOffset, style, ...props }) {
240
242
  const context = useHoverCardContext("HoverCard.Content");
243
+ const presence = usePresence({ open: context.open, forceMount, exitDurationMs: hoverCardExitDurationMs });
244
+ const wasOpenRef = React.useRef(context.open);
245
+ const isClosing = wasOpenRef.current && !context.open;
241
246
  const contentId = id ?? context.contentId;
242
247
  const placementParts = getPlacementParts(context.resolvedPlacement);
243
248
  const { setContentPositioning } = context;
@@ -251,10 +256,14 @@ function HoverCardContent({ align, alignOffset = defaultContentPositioning.align
251
256
  });
252
257
  return () => setContentPositioning(defaultContentPositioning);
253
258
  }, [align, alignOffset, collisionPadding, placement, setContentPositioning, side, sideOffset]);
254
- if (!context.open && !forceMount) {
259
+ React.useEffect(() => {
260
+ wasOpenRef.current = context.open;
261
+ }, [context.open]);
262
+ if (!presence.present) {
255
263
  return null;
256
264
  }
257
- const content = (_jsx("div", { ...props, ref: composeRefs(ref, context.floatingRef), id: contentId, role: "dialog", className: cn("zvk-ui-hover-card__content", className), "data-align": placementParts.align, "data-side": placementParts.side, "data-state": context.open ? "open" : "closed", hidden: context.open ? undefined : true, onBlur: composeEventHandlers(onBlur, (event) => {
265
+ const hidden = presence.inert && !isClosing && presence.motionState !== "exiting" ? true : undefined;
266
+ const content = (_jsx("div", { ...props, ref: composeRefs(ref, context.floatingRef), "aria-hidden": presence.inert ? true : undefined, id: contentId, inert: presence.inert ? true : undefined, role: "dialog", className: cn("zvk-ui-hover-card__content", className), "data-align": placementParts.align, "data-side": placementParts.side, "data-state": presence.state, hidden: hidden, onAnimationEnd: composeEventHandlers(onAnimationEnd, presence.onExitComplete), onBlur: composeEventHandlers(onBlur, (event) => {
258
267
  context.closeAfterFocusLeaves(event.relatedTarget);
259
268
  }), onFocus: composeEventHandlers(onFocus, context.openImmediately), onMouseEnter: composeEventHandlers(onMouseEnter, context.cancelClose), onMouseLeave: composeEventHandlers(onMouseLeave, context.closeWithDelay), onPointerEnter: composeEventHandlers(onPointerEnter, context.cancelClose), onPointerLeave: composeEventHandlers(onPointerLeave, context.closeWithDelay), style: { ...context.floatingStyle, ...style }, children: children }));
260
269
  if (!context.open) {
@@ -26,6 +26,8 @@ export { CopyButton, CopyableText } from "./copy-button/index.js";
26
26
  export type { CopyButtonProps, CopyButtonSize, CopyStatus, CopyableTextProps } from "./copy-button/index.js";
27
27
  export { DatePicker } from "./date-picker/index.js";
28
28
  export type { DatePickerProps } from "./date-picker/index.js";
29
+ export { DateRangePicker } from "./date-range-picker/index.js";
30
+ export type { DateRange, DateRangePickerProps } from "./date-range-picker/index.js";
29
31
  export { Dialog } from "./dialog/index.js";
30
32
  export type { DialogCloseProps, DialogContentProps, DialogDescriptionProps, DialogFooterProps, DialogHeaderProps, DialogOverlayProps, DialogProps, DialogTitleProps, DialogTriggerProps } from "./dialog/index.js";
31
33
  export { DropdownMenu } from "./dropdown-menu/index.js";
@@ -54,12 +56,16 @@ export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, For
54
56
  export type { FormControlProps, FormFieldProps, FormItemProps, FormLabelProps, FormProps, FormTextProps } from "./form/index.js";
55
57
  export { FileUploadInput } from "./file-upload-input/index.js";
56
58
  export type { FileUploadInputProps, FileUploadInputSize } from "./file-upload-input/index.js";
59
+ export { FileDropzone } from "./file-dropzone/index.js";
60
+ export type { FileDropzoneListProps, FileDropzoneProps } from "./file-dropzone/index.js";
57
61
  export { HoverCard } from "./hover-card/index.js";
58
62
  export type { HoverCardContentProps, HoverCardProps, HoverCardTriggerProps } from "./hover-card/index.js";
59
63
  export { IconButton } from "./icon-button/index.js";
60
64
  export type { IconButtonProps } from "./icon-button/index.js";
61
65
  export { Input } from "./input/index.js";
62
66
  export type { InputProps, InputSize } from "./input/index.js";
67
+ export { Kbd } from "./kbd/index.js";
68
+ export type { KbdProps, KbdSize } from "./kbd/index.js";
63
69
  export { Label } from "./label/index.js";
64
70
  export type { LabelProps, LabelSize } from "./label/index.js";
65
71
  export { Pagination } from "./pagination/index.js";
@@ -12,6 +12,7 @@ export { CodeBlock } from "./code-block/index.js";
12
12
  export { Collapsible } from "./collapsible/index.js";
13
13
  export { CopyButton, CopyableText } from "./copy-button/index.js";
14
14
  export { DatePicker } from "./date-picker/index.js";
15
+ export { DateRangePicker } from "./date-range-picker/index.js";
15
16
  export { Dialog } from "./dialog/index.js";
16
17
  export { DropdownMenu } from "./dropdown-menu/index.js";
17
18
  export { AlertDialog } from "./alert-dialog/index.js";
@@ -26,9 +27,11 @@ export { ErrorBoundary, ErrorFallback } from "./error-boundary/index.js";
26
27
  export { Field } from "./field/index.js";
27
28
  export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./form/index.js";
28
29
  export { FileUploadInput } from "./file-upload-input/index.js";
30
+ export { FileDropzone } from "./file-dropzone/index.js";
29
31
  export { HoverCard } from "./hover-card/index.js";
30
32
  export { IconButton } from "./icon-button/index.js";
31
33
  export { Input } from "./input/index.js";
34
+ export { Kbd } from "./kbd/index.js";
32
35
  export { Label } from "./label/index.js";
33
36
  export { Pagination } from "./pagination/index.js";
34
37
  export { Popover } from "./popover/index.js";
@@ -0,0 +1,2 @@
1
+ export { Kbd } from "./kbd.js";
2
+ export type { KbdProps, KbdSize } from "./kbd.js";
@@ -0,0 +1 @@
1
+ export { Kbd } from "./kbd.js";
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ export type KbdSize = "sm" | "md";
3
+ type KbdSharedProps = Omit<React.HTMLAttributes<HTMLElement>, "children"> & {
4
+ size?: KbdSize;
5
+ ref?: React.Ref<HTMLElement>;
6
+ };
7
+ export type KbdProps = (KbdSharedProps & {
8
+ children: React.ReactNode;
9
+ keys?: never;
10
+ }) | (KbdSharedProps & {
11
+ children?: never;
12
+ keys: readonly string[];
13
+ });
14
+ export declare function Kbd({ children, className, keys, ref, size, ...props }: KbdProps): React.JSX.Element;
15
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../../utils/cn.js";
4
+ export function Kbd({ children, className, keys, ref, size = "md", ...props }) {
5
+ if (keys !== undefined) {
6
+ const label = props["aria-label"] ?? keys.join(" + ");
7
+ return (_jsx("span", { ...props, ref: ref, "aria-label": label, className: cn("zvk-ui-kbd-group", className), "data-size": size, children: keys.map((key, index) => (_jsxs(React.Fragment, { children: [index > 0 ? (_jsx("span", { "aria-hidden": "true", className: "zvk-ui-kbd__separator", children: " + " })) : null, _jsx("kbd", { className: "zvk-ui-kbd", "data-size": size, children: key })] }, `${index}-${key}`))) }));
8
+ }
9
+ return (_jsx("kbd", { ...props, ref: ref, className: cn("zvk-ui-kbd", className), "data-size": size, children: children }));
10
+ }
@@ -208,7 +208,7 @@ function MenubarContent({ align, alignOffset = 0, children, className, onKeyDown
208
208
  return (_jsx(Portal, { children: _jsx(DismissableLayer, { open: menu.open, onDismiss: () => menubar.setOpenValue(undefined), children: _jsx("div", { ...props, ref: (node) => {
209
209
  floatingRef(node);
210
210
  setForwardedRef(ref, node);
211
- }, id: menu.contentId, role: "menu", "aria-label": menu.label, "aria-labelledby": menu.triggerId, className: cn("zvk-ui-menubar__content", className), style: { ...style, ...floatingStyle }, "data-align": placementParts(resolvedPlacement).align, "data-side": placementParts(resolvedPlacement).side, onKeyDown: composeEventHandlers(onKeyDown, (event) => {
211
+ }, id: menu.contentId, role: "menu", "aria-label": menu.label, "aria-labelledby": menu.triggerId, className: cn("zvk-ui-menubar__content", className), style: { ...style, ...floatingStyle }, "data-align": placementParts(resolvedPlacement).align, "data-side": placementParts(resolvedPlacement).side, "data-state": "open", onKeyDown: composeEventHandlers(onKeyDown, (event) => {
212
212
  const items = menu.getItems();
213
213
  const index = activeIndex(items);
214
214
  if (event.key === "ArrowDown") {
@@ -30,7 +30,7 @@ export interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement
30
30
  }
31
31
  declare function PopoverRoot({ children, className, defaultOpen, modal, onOpenChange, open, placement, ref, ...props }: PopoverProps): React.JSX.Element;
32
32
  declare function PopoverTrigger({ asChild, className, disabled, onClick, ref, type, ...props }: PopoverTriggerProps): React.JSX.Element;
33
- declare function PopoverContent({ align, alignOffset, className, container, forceMount, id, disableEscapeKeyDown, disableOutsidePointerDown, ref, placement, side, sideOffset, collisionPadding, matchTriggerWidth, style, ...props }: PopoverContentProps): React.JSX.Element | null;
33
+ declare function PopoverContent({ align, alignOffset, className, container, forceMount, id, disableEscapeKeyDown, disableOutsidePointerDown, onAnimationEnd, ref, placement, side, sideOffset, collisionPadding, matchTriggerWidth, style, ...props }: PopoverContentProps): React.JSX.Element | null;
34
34
  export declare const Popover: typeof PopoverRoot & {
35
35
  Trigger: typeof PopoverTrigger;
36
36
  Content: typeof PopoverContent;