@k8o/arte-odyssey 6.0.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/components/buttons/button/button.d.mts +1 -0
  2. package/dist/components/buttons/button/button.mjs +31 -11
  3. package/dist/components/buttons/icon-button/icon-button.d.mts +1 -0
  4. package/dist/components/buttons/icon-button/icon-button.mjs +19 -2
  5. package/dist/components/data-display/accordion/accordion-item.mjs +1 -1
  6. package/dist/components/data-display/accordion/context.mjs +1 -1
  7. package/dist/components/data-display/avatar/avatar.mjs +1 -1
  8. package/dist/components/data-display/baseline-status/baseline-status.mjs +13 -15
  9. package/dist/components/data-display/code/code.mjs +2 -2
  10. package/dist/components/feedback/toast/provider.mjs +10 -7
  11. package/dist/components/feedback/toast/toast.mjs +1 -1
  12. package/dist/components/form/autocomplete/autocomplete.mjs +14 -4
  13. package/dist/components/form/checkbox/checkbox.mjs +4 -2
  14. package/dist/components/form/checkbox-card/checkbox-card.mjs +1 -1
  15. package/dist/components/form/checkbox-group/checkbox-group.mjs +1 -1
  16. package/dist/components/form/file-field/file-field.d.mts +1 -1
  17. package/dist/components/form/file-field/file-field.mjs +7 -4
  18. package/dist/components/form/form/form.d.mts +10 -0
  19. package/dist/components/form/form/form.mjs +14 -0
  20. package/dist/components/form/form/index.d.mts +2 -0
  21. package/dist/components/form/form/index.mjs +2 -0
  22. package/dist/components/form/form-control/form-control.mjs +1 -1
  23. package/dist/components/form/number-field/number-field.mjs +7 -4
  24. package/dist/components/form/password-input/password-input.mjs +6 -3
  25. package/dist/components/form/radio/radio.mjs +8 -5
  26. package/dist/components/form/radio-card/radio-card.mjs +1 -1
  27. package/dist/components/form/select/select.mjs +4 -1
  28. package/dist/components/form/slider/slider.mjs +5 -2
  29. package/dist/components/form/switch/switch.mjs +8 -5
  30. package/dist/components/form/text-field/text-field.mjs +5 -1
  31. package/dist/components/form/textarea/textarea.mjs +14 -7
  32. package/dist/components/index.d.mts +2 -1
  33. package/dist/components/index.mjs +3 -2
  34. package/dist/components/navigation/breadcrumb/breadcrumb.d.mts +1 -1
  35. package/dist/components/navigation/pagination/pagination.mjs +2 -4
  36. package/dist/components/navigation/tabs/tabs.mjs +1 -1
  37. package/dist/components/overlays/dialog/dialog.mjs +1 -1
  38. package/dist/components/overlays/drawer/drawer.mjs +1 -1
  39. package/dist/components/overlays/dropdown-menu/dropdown-menu.mjs +1 -1
  40. package/dist/components/overlays/list-box/list-box.mjs +1 -1
  41. package/dist/components/overlays/modal/modal.mjs +20 -11
  42. package/dist/components/overlays/popover/popover.mjs +1 -1
  43. package/dist/components/providers/portal-root.mjs +1 -1
  44. package/dist/hooks/debounced-transition/index.d.mts +5 -0
  45. package/dist/hooks/debounced-transition/index.mjs +50 -0
  46. package/dist/hooks/deferred-debounce/index.d.mts +4 -0
  47. package/dist/hooks/deferred-debounce/index.mjs +9 -0
  48. package/dist/hooks/index.d.mts +3 -3
  49. package/dist/hooks/index.mjs +3 -3
  50. package/dist/hooks/interval/index.mjs +9 -6
  51. package/dist/hooks/resize/index.d.mts +0 -1
  52. package/dist/hooks/resize/index.mjs +4 -12
  53. package/dist/hooks/timeout/index.mjs +9 -6
  54. package/dist/hooks/window-resize/index.d.mts +0 -1
  55. package/dist/hooks/window-resize/index.mjs +5 -17
  56. package/dist/index.d.mts +4 -3
  57. package/dist/index.mjs +5 -4
  58. package/dist/styles/index.css +9 -14
  59. package/dist/styles/tokens.d.mts +84 -0
  60. package/dist/styles/tokens.mjs +752 -0
  61. package/docs/references/hooks.md +29 -27
  62. package/package.json +27 -20
  63. package/dist/hooks/debounce/index.d.mts +0 -6
  64. package/dist/hooks/debounce/index.mjs +0 -35
  65. package/dist/hooks/throttle/index.d.mts +0 -6
  66. package/dist/hooks/throttle/index.mjs +0 -53
@@ -9,6 +9,7 @@ declare const Button: FC<{
9
9
  fullWidth?: boolean;
10
10
  startIcon?: ReactNode;
11
11
  endIcon?: ReactNode;
12
+ onAction?: () => void | Promise<void>;
12
13
  } & Omit<HTMLProps<HTMLButtonElement>, 'size' | 'type'>>;
13
14
  //#endregion
14
15
  export { Button };
@@ -1,29 +1,49 @@
1
+ "use client";
1
2
  import { cn } from "../../../helpers/cn.mjs";
2
- import { jsxs } from "react/jsx-runtime";
3
+ import { Spinner } from "../../feedback/spinner/spinner.mjs";
4
+ import { useTransition } from "react";
5
+ import { useFormStatus } from "react-dom";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
3
7
  //#region src/components/buttons/button/button.tsx
4
- const Button = ({ ref, children, type = "button", size = "md", color = "primary", variant = "contained", disabled = false, fullWidth = false, onClick, startIcon, endIcon, ...rest }) => {
8
+ const Button = ({ ref, children, type = "button", size = "md", color = "primary", variant = "contained", disabled = false, fullWidth = false, onAction, onClick, startIcon, endIcon, ...rest }) => {
9
+ const [transitionPending, startTransition] = useTransition();
10
+ const { pending: formPending } = useFormStatus();
11
+ const isPending = transitionPending || type === "submit" && formPending;
12
+ const isDisabled = disabled || isPending;
13
+ const handleClick = onClick || onAction ? (event) => {
14
+ onClick?.(event);
15
+ if (event.defaultPrevented) return;
16
+ if (onAction) startTransition(async () => {
17
+ await onAction();
18
+ });
19
+ } : void 0;
20
+ const resolvedStartIcon = isPending ? /* @__PURE__ */ jsx(Spinner, {
21
+ label: "Loading",
22
+ size: size === "lg" ? "md" : "sm"
23
+ }) : startIcon;
5
24
  return /* @__PURE__ */ jsxs("button", {
25
+ "aria-busy": isPending || void 0,
6
26
  className: cn("cursor-pointer rounded-full border-2 text-center font-bold transition-colors", {
7
27
  "border-transparent bg-primary-bg text-fg hover:bg-primary-bg-emphasize/80 active:bg-primary-bg-emphasize": variant === "contained" && color === "primary",
8
28
  "border-transparent bg-secondary-bg text-fg hover:bg-secondary-bg-emphasize/80 active:bg-secondary-bg-emphasize": variant === "contained" && color === "secondary",
9
29
  "border-transparent bg-bg-subtle text-fg-base hover:bg-bg-mute/80 active:bg-bg-mute": variant === "contained" && color === "gray",
10
- "cursor-not-allowed opacity-35 hover:bg-primary-bg active:bg-primary-bg": disabled && variant === "contained" && color === "primary",
11
- "cursor-not-allowed opacity-35 hover:bg-secondary-bg active:bg-secondary-bg": disabled && variant === "contained" && color === "secondary",
12
- "cursor-not-allowed opacity-35 hover:bg-bg-subtle active:bg-bg-subtle": disabled && variant === "contained" && color === "gray",
30
+ "cursor-not-allowed opacity-35 hover:bg-primary-bg active:bg-primary-bg": isDisabled && variant === "contained" && color === "primary",
31
+ "cursor-not-allowed opacity-35 hover:bg-secondary-bg active:bg-secondary-bg": isDisabled && variant === "contained" && color === "secondary",
32
+ "cursor-not-allowed opacity-35 hover:bg-bg-subtle active:bg-bg-subtle": isDisabled && variant === "contained" && color === "gray",
13
33
  "border-primary-border bg-bg-base text-primary-fg hover:bg-bg-subtle active:bg-bg-mute": variant === "outlined" && color === "primary",
14
34
  "border-secondary-border bg-bg-base text-secondary-fg hover:bg-bg-subtle active:bg-bg-mute": variant === "outlined" && color === "secondary",
15
35
  "border-border-base bg-bg-base text-fg-base hover:bg-bg-subtle active:bg-bg-mute": variant === "outlined" && color === "gray",
16
- "cursor-not-allowed bg-bg-base opacity-35 hover:bg-bg-base active:bg-bg-base": disabled && variant === "outlined",
36
+ "cursor-not-allowed bg-bg-base opacity-35 hover:bg-bg-base active:bg-bg-base": isDisabled && variant === "outlined",
17
37
  "border-transparent bg-transparent text-fg-mute hover:bg-bg-subtle hover:text-fg-base active:bg-bg-mute active:text-fg-base": variant === "skeleton",
18
- "cursor-not-allowed bg-transparent text-fg-mute opacity-35 hover:bg-transparent hover:text-fg-mute active:bg-transparent active:text-fg-mute": disabled && variant === "skeleton"
19
- }, "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info", size === "sm" && "px-3 py-1 text-sm", size === "md" && "px-4 py-2 text-md", size === "lg" && "px-6 py-3 text-lg", fullWidth && "w-full", Boolean(startIcon ?? endIcon) && "flex items-center gap-2", startIcon && endIcon ? "justify-between" : startIcon && variant !== "skeleton" ? "justify-center" : endIcon && "justify-between"),
20
- disabled,
21
- onClick,
38
+ "cursor-not-allowed bg-transparent text-fg-mute opacity-35 hover:bg-transparent hover:text-fg-mute active:bg-transparent active:text-fg-mute": isDisabled && variant === "skeleton"
39
+ }, "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info", size === "sm" && "px-3 py-1 text-sm", size === "md" && "px-4 py-2 text-md", size === "lg" && "px-6 py-3 text-lg", fullWidth && "w-full", Boolean(resolvedStartIcon ?? endIcon) && "flex items-center gap-2", resolvedStartIcon && endIcon ? "justify-between" : resolvedStartIcon && variant !== "skeleton" ? "justify-center" : endIcon && "justify-between"),
40
+ disabled: isDisabled,
41
+ onClick: handleClick,
22
42
  ref,
23
43
  type,
24
44
  ...rest,
25
45
  children: [
26
- startIcon,
46
+ resolvedStartIcon,
27
47
  children,
28
48
  endIcon
29
49
  ]
@@ -5,6 +5,7 @@ type Props = {
5
5
  size?: 'sm' | 'md' | 'lg';
6
6
  bg?: 'transparent' | 'base' | 'primary' | 'secondary';
7
7
  label: string;
8
+ onAction?: () => void | Promise<void>;
8
9
  } & Omit<HTMLProps<HTMLButtonElement>, 'size' | 'type'>;
9
10
  declare const IconButton: FC<Props>;
10
11
  //#endregion
@@ -1,10 +1,27 @@
1
+ "use client";
1
2
  import { cn } from "../../../helpers/cn.mjs";
3
+ import { useTransition } from "react";
4
+ import { useFormStatus } from "react-dom";
2
5
  import { jsx, jsxs } from "react/jsx-runtime";
3
6
  //#region src/components/buttons/icon-button/icon-button.tsx
4
- const IconButton = ({ ref, size = "md", bg = "transparent", label, children, ...props }) => {
7
+ const IconButton = ({ ref, size = "md", bg = "transparent", label, children, onAction, onClick, disabled, ...props }) => {
8
+ const [transitionPending, startTransition] = useTransition();
9
+ const { pending: formPending } = useFormStatus();
10
+ const isPending = transitionPending || formPending;
11
+ const isDisabled = Boolean(disabled) || isPending;
12
+ const handleClick = onClick || onAction ? (event) => {
13
+ onClick?.(event);
14
+ if (event.defaultPrevented) return;
15
+ if (onAction) startTransition(async () => {
16
+ await onAction();
17
+ });
18
+ } : void 0;
5
19
  return /* @__PURE__ */ jsxs("button", {
20
+ "aria-busy": isPending || void 0,
6
21
  "aria-label": props.role ? label : void 0,
7
- className: cn("inline-flex cursor-pointer rounded-full transition-colors", "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info", (bg === "transparent" || bg === "base") && "hover:bg-bg-subtle active:bg-bg-mute", bg === "base" && "bg-bg-base", bg === "transparent" && "bg-transparent", bg === "primary" && "bg-primary-bg hover:bg-primary-bg-emphasize/80 active:bg-primary-bg-emphasize", bg === "secondary" && "bg-secondary-bg hover:bg-secondary-bg-emphasize/80 active:bg-secondary-bg-emphasize", size === "sm" && "p-1", size === "md" && "p-2", size === "lg" && "p-3", props.disabled && "cursor-not-allowed opacity-50 hover:bg-transparent active:bg-transparent"),
22
+ className: cn("inline-flex cursor-pointer rounded-full transition-colors", "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info", (bg === "transparent" || bg === "base") && "hover:bg-bg-subtle active:bg-bg-mute", bg === "base" && "bg-bg-base", bg === "transparent" && "bg-transparent", bg === "primary" && "bg-primary-bg hover:bg-primary-bg-emphasize/80 active:bg-primary-bg-emphasize", bg === "secondary" && "bg-secondary-bg hover:bg-secondary-bg-emphasize/80 active:bg-secondary-bg-emphasize", size === "sm" && "p-1", size === "md" && "p-2", size === "lg" && "p-3", isDisabled && "cursor-not-allowed opacity-50 hover:bg-transparent active:bg-transparent"),
23
+ disabled: isDisabled,
24
+ onClick: handleClick,
8
25
  ref,
9
26
  ...props,
10
27
  children: [!props.role && /* @__PURE__ */ jsx("span", {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { AccordionItemProvider } from "./context.mjs";
3
- import { jsx } from "react/jsx-runtime";
4
3
  import { useId } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
5
  //#region src/components/data-display/accordion/accordion-item.tsx
6
6
  const AccordionItem = ({ children, defaultOpen = false }) => {
7
7
  return /* @__PURE__ */ jsx(AccordionItemProvider, {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { useDisclosure } from "../../../hooks/disclosure/index.mjs";
3
- import { jsx } from "react/jsx-runtime";
4
3
  import { createContext, use } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
5
  //#region src/components/data-display/accordion/context.tsx
6
6
  const OpenContext = createContext(false);
7
7
  const ToggleOpenContext = createContext(void 0);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
- import { jsx } from "react/jsx-runtime";
4
3
  import { useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
5
  //#region src/components/data-display/avatar/avatar.tsx
6
6
  const getInitials = (name) => {
7
7
  if (!name) return "?";
@@ -1,25 +1,23 @@
1
1
  "use client";
2
+ import { Suspense, use } from "react";
2
3
  import { jsx } from "react/jsx-runtime";
3
- import { useSyncExternalStore } from "react";
4
4
  //#region src/components/data-display/baseline-status/baseline-status.tsx
5
- let didInit = false;
6
- const BaselineStatus = ({ featureId }) => {
7
- if (!useSyncExternalStore((onStoreChange) => {
8
- if (!didInit) {
9
- didInit = true;
10
- import("baseline-status").then(() => {
11
- onStoreChange();
12
- });
13
- }
14
- return () => {};
15
- }, () => didInit, () => false)) return /* @__PURE__ */ jsx("div", {
16
- className: "max-w-full animate-pulse rounded-lg border border-border-base bg-bg-base p-4",
17
- style: { height: "120px" }
18
- });
5
+ let loadPromise = null;
6
+ const loadBaselineStatus = () => {
7
+ loadPromise ??= import("baseline-status");
8
+ return loadPromise;
9
+ };
10
+ const BaselineStatusResolved = ({ featureId }) => {
11
+ use(loadBaselineStatus());
19
12
  return /* @__PURE__ */ jsx("baseline-status", {
20
13
  className: "wrap-normal max-w-full rounded-lg border border-border-base bg-bg-base p-4",
21
14
  featureId
22
15
  });
23
16
  };
17
+ const BaselineStatusSkeleton = () => /* @__PURE__ */ jsx("div", { className: "h-58 max-w-full animate-pulse rounded-lg border border-border-base bg-bg-base p-4 sm:h-40 md:h-30" });
18
+ const BaselineStatus = ({ featureId }) => /* @__PURE__ */ jsx(Suspense, {
19
+ fallback: /* @__PURE__ */ jsx(BaselineStatusSkeleton, {}),
20
+ children: /* @__PURE__ */ jsx(BaselineStatusResolved, { featureId })
21
+ });
24
22
  //#endregion
25
23
  export { BaselineStatus };
@@ -1,6 +1,6 @@
1
1
  import { findAllColors } from "../../../helpers/color/find-all-colors.mjs";
2
+ import { Fragment } from "react";
2
3
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { Fragment as Fragment$1 } from "react";
4
4
  //#region src/components/data-display/code/code.tsx
5
5
  const Code = ({ children }) => {
6
6
  const colors = findAllColors(children);
@@ -12,7 +12,7 @@ const Code = ({ children }) => {
12
12
  let lastIndex = 0;
13
13
  colors.forEach((colorInfo, index) => {
14
14
  if (colorInfo.start > lastIndex) parts.push(children.slice(lastIndex, colorInfo.start));
15
- parts.push(/* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
15
+ parts.push(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
16
16
  "aria-label": `Color: ${colorInfo.color}`,
17
17
  className: "inline-block h-3 w-3 shrink-0 rounded-sm border border-border-base",
18
18
  role: "img",
@@ -2,23 +2,26 @@
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
3
  import { uuidV4 } from "../../../helpers/uuid-v4.mjs";
4
4
  import { Toast } from "./toast.mjs";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
5
  import { createContext, use, useCallback, useEffect, useRef, useState } from "react";
6
+ import { createPortal } from "react-dom";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
7
8
  import { AnimatePresence } from "motion/react";
8
9
  import * as motion$1 from "motion/react-client";
9
- import { createPortal } from "react-dom";
10
10
  //#region src/components/feedback/toast/provider.tsx
11
+ const MAX_TOAST_COUNT = 5;
11
12
  const SetToastContext = createContext(void 0);
12
13
  const useToast = () => {
13
14
  const setToasts = use(SetToastContext);
14
15
  if (!setToasts) throw new Error("useToast must be used within a ToastProvider");
15
16
  return {
16
17
  onOpen: useCallback((status, message) => {
17
- setToasts((prev) => [...prev, {
18
- id: uuidV4(),
19
- status,
20
- message
21
- }]);
18
+ setToasts((prev) => {
19
+ return [...prev, {
20
+ id: uuidV4(),
21
+ status,
22
+ message
23
+ }].slice(-MAX_TOAST_COUNT);
24
+ });
22
25
  }, [setToasts]),
23
26
  onClose: useCallback((id) => {
24
27
  setToasts((prev) => prev.filter((toast) => toast.id !== id));
@@ -2,8 +2,8 @@
2
2
  import { Alert } from "../alert/alert.mjs";
3
3
  import { useTimeout } from "../../../hooks/timeout/index.mjs";
4
4
  import { useToast } from "./provider.mjs";
5
- import { jsx } from "react/jsx-runtime";
6
5
  import { useCallback } from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
7
  //#region src/components/feedback/toast/toast.tsx
8
8
  const DELAY_MS = 5e3;
9
9
  const Toast = ({ id, status, message }) => {
@@ -3,8 +3,10 @@ import { cn } from "../../../helpers/cn.mjs";
3
3
  import { IconButton } from "../../buttons/icon-button/icon-button.mjs";
4
4
  import { CloseIcon } from "../../icons/lucide.mjs";
5
5
  import { useControllableState } from "../../../hooks/controllable-state/index.mjs";
6
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useDeferredDebounce } from "../../../hooks/deferred-debounce/index.mjs";
7
7
  import { useCallback, useEffect, useRef, useState } from "react";
8
+ import { useFormStatus } from "react-dom";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
8
10
  //#region src/components/form/autocomplete/autocomplete.tsx
9
11
  const Autocomplete = ({ id, name, describedbyId, isInvalid, isDisabled, isRequired, options, value, defaultValue, onChange }) => {
10
12
  const [currentValue, handleChange] = useControllableState({
@@ -16,7 +18,10 @@ const Autocomplete = ({ id, name, describedbyId, isInvalid, isDisabled, isRequir
16
18
  const [open, setOpen] = useState(false);
17
19
  const [text, setText] = useState("");
18
20
  const [selectIndex, setSelectIndex] = useState();
19
- const filteredOptions = options.filter((option) => option.label.includes(text));
21
+ const [deferredText, isPending] = useDeferredDebounce(text);
22
+ const filteredOptions = options.filter((option) => option.label.includes(deferredText));
23
+ const { pending: formPending } = useFormStatus();
24
+ const isDisabledResolved = isDisabled || formPending;
20
25
  const reset = useCallback(() => {
21
26
  setText("");
22
27
  setOpen(false);
@@ -70,7 +75,7 @@ const Autocomplete = ({ id, name, describedbyId, isInvalid, isDisabled, isRequir
70
75
  "aria-required": isRequired,
71
76
  autoComplete: "off",
72
77
  className: cn("grow bg-transparent focus-visible:outline-hidden", "disabled:cursor-not-allowed"),
73
- disabled: isDisabled,
78
+ disabled: isDisabledResolved,
74
79
  id,
75
80
  onBlur: (e) => {
76
81
  if (e.relatedTarget?.id.startsWith(`${id}_option_`)) return;
@@ -112,6 +117,10 @@ const Autocomplete = ({ id, name, describedbyId, isInvalid, isDisabled, isRequir
112
117
  return;
113
118
  }
114
119
  if (e.key === "Enter" && selectIndex !== void 0 && selectIndex >= 0) {
120
+ if (isPending) {
121
+ e.preventDefault();
122
+ return;
123
+ }
115
124
  const selected = filteredOptions[selectIndex];
116
125
  if (!selected) return;
117
126
  if (currentValue.includes(selected.value)) {
@@ -145,7 +154,8 @@ const Autocomplete = ({ id, name, describedbyId, isInvalid, isDisabled, isRequir
145
154
  className: "absolute top-1 z-10 w-full rounded-xl bg-bg-raised shadow-md",
146
155
  role: "presentation",
147
156
  children: /* @__PURE__ */ jsxs("ul", {
148
- className: "max-h-96 py-2",
157
+ "aria-busy": isPending || void 0,
158
+ className: cn("max-h-96 py-2 transition-opacity", isPending && "opacity-60"),
149
159
  id: `${id}_listbox`,
150
160
  children: [filteredOptions.length === 0 && /* @__PURE__ */ jsx("li", {
151
161
  className: "px-3 py-2 text-fg-mute",
@@ -2,16 +2,18 @@
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
3
  import { CheckIcon } from "../../icons/lucide.mjs";
4
4
  import { useCheckboxGroupContext } from "../checkbox-group/checkbox-group.mjs";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
5
  import { useState } from "react";
6
+ import { useFormStatus } from "react-dom";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
7
8
  //#region src/components/form/checkbox/checkbox.tsx
8
9
  const Checkbox = ({ name, itemValue, isDisabled = false, label, value, defaultChecked, onChange }) => {
9
10
  const groupContext = useCheckboxGroupContext();
11
+ const { pending } = useFormStatus();
10
12
  const [internalChecked, setInternalChecked] = useState(defaultChecked ?? false);
11
13
  const groupItemValue = itemValue ?? "";
12
14
  if (groupContext && !itemValue) throw new Error("Checkbox inside CheckboxGroup requires itemValue");
13
15
  const isControlled = value !== void 0;
14
- const isDisabledResolved = isDisabled || groupContext?.isDisabled;
16
+ const isDisabledResolved = isDisabled || groupContext?.isDisabled || pending;
15
17
  const checked = groupContext ? groupContext.currentValue.includes(groupItemValue) : isControlled ? value : internalChecked;
16
18
  const setChecked = (nextChecked) => {
17
19
  if (!isControlled) setInternalChecked(nextChecked);
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
3
  import { CheckIcon } from "../../icons/lucide.mjs";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
4
  import { useId, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/components/form/checkbox-card/checkbox-card.tsx
7
7
  const CheckboxCard = ({ labelId, name, isDisabled, isInvalid = false, options, value, defaultValue, onChange }) => {
8
8
  const groupId = useId();
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
3
  import { useControllableState } from "../../../hooks/controllable-state/index.mjs";
4
- import { jsx } from "react/jsx-runtime";
5
4
  import { createContext, use } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
6
  //#region src/components/form/checkbox-group/checkbox-group.tsx
7
7
  const CheckboxGroupContext = createContext(void 0);
8
8
  const useCheckboxGroupContext = () => use(CheckboxGroupContext);
@@ -1,5 +1,5 @@
1
- import * as _$react_jsx_runtime0 from "react/jsx-runtime";
2
1
  import { ChangeEventHandler, FC, PropsWithChildren, ReactElement } from "react";
2
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
3
3
 
4
4
  //#region src/components/form/file-field/file-field.d.ts
5
5
  declare const FileField: {
@@ -2,8 +2,9 @@
2
2
  import { IconButton } from "../../buttons/icon-button/icon-button.mjs";
3
3
  import { CloseIcon } from "../../icons/lucide.mjs";
4
4
  import { uuidV4 } from "../../../helpers/uuid-v4.mjs";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
5
  import { createContext, use, useCallback, useId, useMemo, useRef, useState } from "react";
6
+ import { useFormStatus } from "react-dom";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
7
8
  //#region src/components/form/file-field/file-field.tsx
8
9
  const FileFieldContext = createContext(null);
9
10
  const FileFieldProvider = FileFieldContext;
@@ -15,6 +16,8 @@ const useFileFieldContext = () => {
15
16
  const Root = ({ children, id, name, describedbyId, isDisabled = false, isInvalid = false, isRequired = false, accept, multiple = false, maxFiles, onChange, webkitDirectory = false }) => {
16
17
  const generatedId = useId();
17
18
  const inputRef = useRef(null);
19
+ const { pending } = useFormStatus();
20
+ const isDisabledResolved = isDisabled || pending;
18
21
  const [acceptedFiles, setAcceptedFiles] = useState([]);
19
22
  const onFilesChange = useCallback((event) => {
20
23
  onChange?.(event);
@@ -50,13 +53,13 @@ const Root = ({ children, id, name, describedbyId, isDisabled = false, isInvalid
50
53
  }, []);
51
54
  return /* @__PURE__ */ jsx(FileFieldProvider, {
52
55
  value: useMemo(() => ({
53
- isDisabled,
56
+ isDisabled: isDisabledResolved,
54
57
  isInvalid,
55
58
  acceptedFiles,
56
59
  onFileDelete,
57
60
  openFilePicker
58
61
  }), [
59
- isDisabled,
62
+ isDisabledResolved,
60
63
  isInvalid,
61
64
  acceptedFiles,
62
65
  onFileDelete,
@@ -69,7 +72,7 @@ const Root = ({ children, id, name, describedbyId, isDisabled = false, isInvalid
69
72
  "aria-describedby": describedbyId,
70
73
  "aria-invalid": isInvalid,
71
74
  className: "sr-only",
72
- disabled: isDisabled,
75
+ disabled: isDisabledResolved,
73
76
  id: id ?? generatedId,
74
77
  multiple,
75
78
  name,
@@ -0,0 +1,10 @@
1
+ import { FC, FormHTMLAttributes, ReactNode } from "react";
2
+
3
+ //#region src/components/form/form/form.d.ts
4
+ type Props = {
5
+ action?: ((formData: FormData) => void | Promise<void>) | string;
6
+ children: ReactNode;
7
+ } & Omit<FormHTMLAttributes<HTMLFormElement>, 'action' | 'children'>;
8
+ declare const Form: FC<Props>;
9
+ //#endregion
10
+ export { Form };
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { cn } from "../../../helpers/cn.mjs";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region src/components/form/form/form.tsx
5
+ const Form = ({ action, className, children, ...rest }) => {
6
+ return /* @__PURE__ */ jsx("form", {
7
+ action,
8
+ className: cn("flex flex-col gap-6", className),
9
+ ...rest,
10
+ children
11
+ });
12
+ };
13
+ //#endregion
14
+ export { Form };
@@ -0,0 +1,2 @@
1
+ import { Form } from "./form.mjs";
2
+ export { Form };
@@ -0,0 +1,2 @@
1
+ import { Form } from "./form.mjs";
2
+ export { Form };
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
2
  import { useId } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
4
  //#region src/components/form/form-control/form-control.tsx
5
5
  const FormControl = ({ isDisabled = false, isInvalid = false, isRequired = false, label, labelAs = "label", helpText, errorText, renderInput }) => {
6
6
  const id = useId();
@@ -4,8 +4,9 @@ import { ChevronIcon } from "../../icons/lucide.mjs";
4
4
  import { between } from "../../../helpers/number/between.mjs";
5
5
  import { toPrecision } from "../../../helpers/number/to-precision.mjs";
6
6
  import { cast } from "../../../helpers/number/cast.mjs";
7
- import { jsx, jsxs } from "react/jsx-runtime";
8
7
  import { useState } from "react";
8
+ import { useFormStatus } from "react-dom";
9
+ import { jsx, jsxs } from "react/jsx-runtime";
9
10
  //#region src/components/form/number-field/number-field.tsx
10
11
  const NumberField = ({ id, name, describedbyId, isInvalid, isDisabled, isRequired, value, defaultValue, onChange, step = 1, precision = 0, max = 9007199254740991, min = -9007199254740991, placeholder }) => {
11
12
  const isControlled = value !== void 0;
@@ -13,6 +14,7 @@ const NumberField = ({ id, name, describedbyId, isInvalid, isDisabled, isRequire
13
14
  const [internalValue, setInternalValue] = useState(initialValue);
14
15
  const [displayValue, setDisplayValue] = useState(initialValue.toFixed(precision));
15
16
  const [prevValue, setPrevValue] = useState(initialValue);
17
+ const { pending } = useFormStatus();
16
18
  const currentValue = isControlled ? value : internalValue;
17
19
  if (isControlled && value !== prevValue) {
18
20
  setDisplayValue(value.toFixed(precision));
@@ -33,11 +35,12 @@ const NumberField = ({ id, name, describedbyId, isInvalid, isDisabled, isRequire
33
35
  "aria-valuenow": currentValue,
34
36
  autoComplete: "off",
35
37
  autoCorrect: "off",
36
- className: cn("h-full w-full grow bg-transparent pr-8 pl-3 focus-visible:outline-hidden", "disabled:cursor-not-allowed"),
38
+ className: cn("h-full w-full grow bg-transparent pr-8 pl-3 focus-visible:outline-hidden", "disabled:cursor-not-allowed", "read-only:cursor-not-allowed"),
37
39
  disabled: isDisabled,
38
40
  id,
39
41
  inputMode: "decimal",
40
42
  name,
43
+ readOnly: pending || void 0,
41
44
  onBlur: () => {
42
45
  const newValue = between(cast(displayValue, precision), min, max);
43
46
  handleChange(newValue);
@@ -69,7 +72,7 @@ const NumberField = ({ id, name, describedbyId, isInvalid, isDisabled, isRequire
69
72
  className: "absolute right-1 flex h-full flex-col py-1",
70
73
  children: [/* @__PURE__ */ jsxs("button", {
71
74
  className: cn("flex w-6 grow items-center justify-center rounded-md text-fg-mute transition-colors", "hover:bg-bg-mute hover:text-fg-base", "disabled:cursor-not-allowed disabled:text-fg-mute disabled:hover:bg-transparent"),
72
- disabled: isDisabled,
75
+ disabled: isDisabled || pending,
73
76
  onClick: () => {
74
77
  const newValue = between(toPrecision(cast(displayValue, precision) + step, precision), min, max);
75
78
  handleChange(newValue);
@@ -86,7 +89,7 @@ const NumberField = ({ id, name, describedbyId, isInvalid, isDisabled, isRequire
86
89
  })]
87
90
  }), /* @__PURE__ */ jsxs("button", {
88
91
  className: cn("flex w-6 grow items-center justify-center rounded-md text-fg-mute transition-colors", "hover:bg-bg-mute hover:text-fg-base", "disabled:cursor-not-allowed disabled:text-fg-mute disabled:hover:bg-transparent"),
89
- disabled: isDisabled,
92
+ disabled: isDisabled || pending,
90
93
  onClick: () => {
91
94
  const newValue = between(toPrecision(cast(displayValue, precision) - step, precision), min, max);
92
95
  handleChange(newValue);
@@ -2,10 +2,12 @@
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
3
  import { ViewIcon, ViewOffIcon } from "../../icons/lucide.mjs";
4
4
  import { useDisclosure } from "../../../hooks/disclosure/index.mjs";
5
+ import { useFormStatus } from "react-dom";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/components/form/password-input/password-input.tsx
7
8
  const PasswordInput = ({ id, name, describedbyId, isInvalid, isDisabled, isRequired, placeholder, autoComplete = "current-password", showLabel = "Show password", hideLabel = "Hide password", defaultValue, value, onChange }) => {
8
9
  const { isOpen: isVisible, toggle: toggleVisible } = useDisclosure();
10
+ const { pending } = useFormStatus();
9
11
  return /* @__PURE__ */ jsxs("div", {
10
12
  className: "relative w-full",
11
13
  children: [/* @__PURE__ */ jsx("input", {
@@ -13,20 +15,21 @@ const PasswordInput = ({ id, name, describedbyId, isInvalid, isDisabled, isRequi
13
15
  "aria-invalid": isInvalid,
14
16
  "aria-required": isRequired,
15
17
  autoComplete,
16
- className: cn("w-full rounded-xl border border-border-base bg-bg-base px-3 py-2 pr-12", "aria-invalid:border-border-error", "disabled:cursor-not-allowed disabled:border-border-mute disabled:bg-bg-mute disabled:hover:bg-bg-mute", "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info"),
18
+ className: cn("w-full rounded-xl border border-border-base bg-bg-base px-3 py-2 pr-12", "aria-invalid:border-border-error", "disabled:cursor-not-allowed disabled:border-border-mute disabled:bg-bg-mute disabled:hover:bg-bg-mute", "read-only:cursor-not-allowed read-only:bg-bg-subtle", "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info"),
17
19
  defaultValue,
18
20
  disabled: isDisabled,
19
21
  id,
20
22
  name,
21
23
  onChange,
22
24
  placeholder,
25
+ readOnly: pending || void 0,
23
26
  required: isRequired,
24
27
  type: isVisible ? "text" : "password",
25
28
  value
26
29
  }), /* @__PURE__ */ jsx("button", {
27
30
  "aria-label": isVisible ? hideLabel : showLabel,
28
- className: cn("absolute top-1/2 right-2 inline-flex -translate-y-1/2 items-center justify-center rounded-md p-1 text-fg-mute transition-colors", "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info", !isDisabled && "hover:bg-bg-mute hover:text-fg-base", isDisabled && "cursor-not-allowed text-fg-mute/70"),
29
- disabled: isDisabled,
31
+ className: cn("absolute top-1/2 right-2 inline-flex -translate-y-1/2 items-center justify-center rounded-md p-1 text-fg-mute transition-colors", "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info", !isDisabled && !pending && "hover:bg-bg-mute hover:text-fg-base", (isDisabled || pending) && "cursor-not-allowed text-fg-mute/70"),
32
+ disabled: isDisabled || pending,
30
33
  onClick: toggleVisible,
31
34
  type: "button",
32
35
  children: isVisible ? /* @__PURE__ */ jsx(ViewOffIcon, { size: "sm" }) : /* @__PURE__ */ jsx(ViewIcon, { size: "sm" })
@@ -1,28 +1,31 @@
1
1
  "use client";
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { useState } from "react";
4
+ import { useFormStatus } from "react-dom";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
5
6
  //#region src/components/form/radio/radio.tsx
6
7
  const Radio = ({ labelId, name, isDisabled, value, defaultValue, onChange, options }) => {
7
8
  const [internalValue, setInternalValue] = useState(defaultValue);
9
+ const { pending } = useFormStatus();
8
10
  const isControlled = value !== void 0;
9
11
  const selectedValue = isControlled ? value : internalValue;
12
+ const isDisabledResolved = isDisabled || pending;
10
13
  const selectValue = (nextValue) => {
11
14
  if (!isControlled) setInternalValue(nextValue);
12
15
  onChange?.({ target: { value: nextValue } });
13
16
  };
14
17
  return /* @__PURE__ */ jsx("div", {
15
18
  "aria-labelledby": labelId,
16
- className: cn("flex cursor-pointer flex-col gap-2", isDisabled && "cursor-not-allowed"),
19
+ className: cn("flex cursor-pointer flex-col gap-2", isDisabledResolved && "cursor-not-allowed"),
17
20
  role: "radiogroup",
18
21
  children: options.map((option) => /* @__PURE__ */ jsxs("label", {
19
- className: cn("flex items-center gap-2 text-left", isDisabled ? "cursor-not-allowed" : "cursor-pointer"),
22
+ className: cn("flex items-center gap-2 text-left", isDisabledResolved ? "cursor-not-allowed" : "cursor-pointer"),
20
23
  children: [
21
24
  /* @__PURE__ */ jsx("input", {
22
25
  checked: isControlled ? value === option.value : void 0,
23
26
  className: "peer sr-only",
24
27
  defaultChecked: isControlled ? void 0 : defaultValue === option.value,
25
- disabled: isDisabled,
28
+ disabled: isDisabledResolved,
26
29
  name: name ?? labelId,
27
30
  onChange: () => {
28
31
  selectValue(option.value);
@@ -32,7 +35,7 @@ const Radio = ({ labelId, name, isDisabled, value, defaultValue, onChange, optio
32
35
  }),
33
36
  /* @__PURE__ */ jsx("span", {
34
37
  "aria-hidden": true,
35
- className: cn("inline-flex size-5 items-center justify-center rounded-full border-2 transition-colors", "peer-focus-visible:border-transparent peer-focus-visible:outline-hidden peer-focus-visible:ring-2 peer-focus-visible:ring-border-info", selectedValue === option.value ? "border-border-base bg-primary-bg" : "border-border-mute bg-bg-base", isDisabled && "border-border-mute bg-bg-mute"),
38
+ className: cn("inline-flex size-5 items-center justify-center rounded-full border-2 transition-colors", "peer-focus-visible:border-transparent peer-focus-visible:outline-hidden peer-focus-visible:ring-2 peer-focus-visible:ring-border-info", selectedValue === option.value ? "border-border-base bg-primary-bg" : "border-border-mute bg-bg-base", isDisabledResolved && "border-border-mute bg-bg-mute"),
36
39
  children: /* @__PURE__ */ jsx("span", { className: cn("size-2 rounded-full transition-opacity", selectedValue === option.value ? "bg-primary-border opacity-100" : "bg-transparent opacity-0") })
37
40
  }),
38
41
  /* @__PURE__ */ jsx("span", { children: option.label })
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../../../helpers/cn.mjs";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
3
  import { useId, useRef, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/components/form/radio-card/radio-card.tsx
6
6
  const RadioCard = ({ labelId, name, isDisabled, isInvalid = false, options, value, defaultValue, onChange }) => {
7
7
  const groupId = useId();
@@ -1,8 +1,11 @@
1
+ "use client";
1
2
  import { cn } from "../../../helpers/cn.mjs";
2
3
  import { ChevronIcon } from "../../icons/lucide.mjs";
4
+ import { useFormStatus } from "react-dom";
3
5
  import { jsx, jsxs } from "react/jsx-runtime";
4
6
  //#region src/components/form/select/select.tsx
5
7
  const Select = ({ id, name, describedbyId, isInvalid, isDisabled, isRequired, options, value, defaultValue, onChange }) => {
8
+ const { pending } = useFormStatus();
6
9
  return /* @__PURE__ */ jsxs("div", {
7
10
  className: "relative h-fit w-full",
8
11
  children: [/* @__PURE__ */ jsx("select", {
@@ -11,7 +14,7 @@ const Select = ({ id, name, describedbyId, isInvalid, isDisabled, isRequired, op
11
14
  "aria-required": isRequired,
12
15
  className: cn("w-full appearance-none rounded-xl border border-border-base bg-bg-base px-3 py-2 text-fg-base", "aria-invalid:border-border-error", "disabled:cursor-not-allowed disabled:border-border-mute disabled:bg-bg-mute disabled:hover:bg-bg-mute", "focus-visible:border-transparent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info"),
13
16
  defaultValue,
14
- disabled: isDisabled,
17
+ disabled: isDisabled || pending,
15
18
  id,
16
19
  name,
17
20
  onChange,