@nexub/react 7.3.0 → 7.4.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.
@@ -1,10 +1,16 @@
1
- export type ToastTone = 'success' | 'error' | 'warning' | 'info' | 'neutral';
1
+ import { type ReactNode } from 'react';
2
+ /** `loading` shows a spinner and never auto-dismisses; update it (or use `toast.promise`) to settle it. */
3
+ export type ToastTone = 'success' | 'error' | 'warning' | 'info' | 'neutral' | 'loading';
2
4
  export interface ToastItem {
3
5
  id: string;
4
6
  title: string;
5
7
  description?: string;
6
8
  tone?: ToastTone;
7
9
  duration?: number;
10
+ /** Leading icon. Defaults to the tone's icon (none for neutral); `false` hides it. */
11
+ icon?: ReactNode | false;
12
+ /** A thin bar counting down the time left before auto-dismiss; pauses with the timer. */
13
+ showProgress?: boolean;
8
14
  action?: {
9
15
  label: string;
10
16
  onClick: () => void;
@@ -31,6 +37,15 @@ export declare function useToastStore(): ToastItem[];
31
37
  /** Runs a toast's onClose at most once. Internal: the Toaster calls it when Radix closes a toast. */
32
38
  export declare function fireToastClose(item: ToastItem): void;
33
39
  declare function dismiss(id?: string): void;
40
+ /** Changes an open toast in place (same element; a settled tone is announced once). No-op if it's gone. */
41
+ declare function update(id: string, patch: Partial<Omit<ToastItem, 'id'>>): void;
42
+ export interface PromiseToastOptions<T> {
43
+ loading: string;
44
+ success: string | ((value: T) => string);
45
+ error: string | ((error: unknown) => string);
46
+ }
47
+ /** Shows a loading toast, then turns that same toast into success or error. Returns `promise` as is. */
48
+ declare function promise<T>(promise: Promise<T>, { loading, success, error }: PromiseToastOptions<T>): Promise<T>;
34
49
  declare function undoable(title: string, { onUndo, onCommit, undoLabel, description, duration }: UndoableToastOptions): string;
35
50
  export declare const toast: {
36
51
  success: (title: string, opts?: Partial<Omit<ToastItem, "id" | "title" | "tone">>) => string;
@@ -40,6 +55,8 @@ export declare const toast: {
40
55
  custom: (title: string, opts?: Partial<Omit<ToastItem, "id" | "title" | "tone">>) => string;
41
56
  /** A toast with an Undo action: Undo calls `onUndo`; any other close calls `onCommit`, once. */
42
57
  undoable: typeof undoable;
58
+ update: typeof update;
59
+ promise: typeof promise;
43
60
  dismiss: typeof dismiss;
44
61
  };
45
62
  export {};
@@ -41,6 +41,18 @@ function dismiss(id) {
41
41
  closeFired.delete(item.id);
42
42
  }
43
43
  }
44
+ function update(id, patch) {
45
+ g.__nexub_toast_items = g.__nexub_toast_items.map((i) => i.id === id ? { ...i, ...patch } : i);
46
+ notify();
47
+ }
48
+ function promise(promise2, { loading, success, error }) {
49
+ const id = add({ title: loading, tone: "loading" });
50
+ promise2.then(
51
+ (v) => update(id, { title: typeof success === "function" ? success(v) : success, tone: "success" }),
52
+ (e) => update(id, { title: typeof error === "function" ? error(e) : error, tone: "error" })
53
+ );
54
+ return promise2;
55
+ }
44
56
  function undoable(title, { onUndo, onCommit, undoLabel, description, duration }) {
45
57
  let undone = false;
46
58
  return add({
@@ -65,6 +77,8 @@ const toast = {
65
77
  custom: (title, opts) => add({ title, tone: "neutral", ...opts }),
66
78
  /** A toast with an Undo action: Undo calls `onUndo`; any other close calls `onCommit`, once. */
67
79
  undoable,
80
+ update,
81
+ promise,
68
82
  dismiss
69
83
  };
70
84
  export {
@@ -0,0 +1,27 @@
1
+ export interface AsyncAction<A extends unknown[], R> {
2
+ /** Calls `fn`. Resolves to its result, or `undefined` if it threw or a newer call (or `reset`) superseded it. Never rejects. */
3
+ run: (...args: A) => Promise<R | undefined>;
4
+ /** True while the latest call is in flight. */
5
+ pending: boolean;
6
+ /** What the latest call threw; cleared by the next `run` or `reset`. */
7
+ error: unknown;
8
+ /** Clears `pending` and `error` and ignores any call still in flight. */
9
+ reset: () => void;
10
+ }
11
+ /**
12
+ * Wraps an async function with pending/error state. The latest call wins:
13
+ * results and errors from older calls are dropped, and so is anything that
14
+ * settles after unmount.
15
+ *
16
+ * ```tsx
17
+ * const save = useAsyncAction(async (draft: Draft) => api.save(draft));
18
+ * <Button loading={save.pending} onClick={() => save.run(draft)}>Save</Button>
19
+ * {save.error ? <Alert tone="danger">Could not save</Alert> : null}
20
+ * ```
21
+ *
22
+ * For optimistic UI, pair it with React 19 `useOptimistic`: apply the
23
+ * optimistic value inside `startTransition`, then `await run(...)`; React
24
+ * drops the optimistic value when the transition ends. Inside a
25
+ * `<form action>`, a `type="submit"` Button already tracks pending on its own.
26
+ */
27
+ export declare function useAsyncAction<A extends unknown[], R>(fn: (...args: A) => Promise<R> | R): AsyncAction<A, R>;
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
4
+ function useAsyncAction(fn) {
5
+ const [pending, setPending] = useState(false);
6
+ const [error, setError] = useState(void 0);
7
+ const latest = useRef(0);
8
+ const fnRef = useRef(fn);
9
+ useIsomorphicLayoutEffect(() => {
10
+ fnRef.current = fn;
11
+ });
12
+ useEffect(() => () => void latest.current++, []);
13
+ const run = useCallback(async (...args) => {
14
+ const id = ++latest.current;
15
+ setPending(true);
16
+ setError(void 0);
17
+ try {
18
+ const result = await fnRef.current(...args);
19
+ return id === latest.current ? result : void 0;
20
+ } catch (e) {
21
+ if (id === latest.current) setError(e);
22
+ return void 0;
23
+ } finally {
24
+ if (id === latest.current) setPending(false);
25
+ }
26
+ }, []);
27
+ const reset = useCallback(() => {
28
+ latest.current++;
29
+ setPending(false);
30
+ setError(void 0);
31
+ }, []);
32
+ return { run, pending, error, reset };
33
+ }
34
+ export {
35
+ useAsyncAction
36
+ };
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export { useMediaQuery } from './hooks/useMediaQuery.js';
11
11
  export { useMergedRefs } from './hooks/useMergedRefs.js';
12
12
  export { useUnsavedChanges } from './hooks/useUnsavedChanges.js';
13
13
  export { usePointerSheen } from './hooks/usePointerSheen.js';
14
+ export { useAsyncAction } from './hooks/useAsyncAction.js';
15
+ export type { AsyncAction } from './hooks/useAsyncAction.js';
14
16
  export { PromptInput } from './components/PromptInput/index.js';
15
17
  export type { PromptInputProps, PromptInputMessages, PromptInputStatus } from './components/PromptInput/index.js';
16
18
  export { ThinkingIndicator } from './components/ThinkingIndicator/index.js';
@@ -84,7 +86,7 @@ export type { ChipProps } from './components/Chip/index.js';
84
86
  export { EyebrowChip } from './components/EyebrowChip/index.js';
85
87
  export type { EyebrowChipProps } from './components/EyebrowChip/index.js';
86
88
  export { Avatar, avatarVariants } from './components/Avatar/index.js';
87
- export type { AvatarProps } from './components/Avatar/index.js';
89
+ export type { AvatarProps, AvatarStatus } from './components/Avatar/index.js';
88
90
  export { AvatarGroup, avatarGroupVariants } from './components/AvatarGroup/index.js';
89
91
  export type { AvatarGroupProps } from './components/AvatarGroup/index.js';
90
92
  export { Card, CardLink, cardVariants } from './components/Card/index.js';
@@ -97,8 +99,8 @@ export { MediaCard } from './components/MediaCard/index.js';
97
99
  export type { MediaCardProps, MediaCardRatio } from './components/MediaCard/index.js';
98
100
  export { Alert, alertVariants } from './components/Alert/index.js';
99
101
  export type { AlertProps } from './components/Alert/index.js';
100
- export { Skeleton, skeletonVariants } from './components/Skeleton/index.js';
101
- export type { SkeletonProps } from './components/Skeleton/index.js';
102
+ export { Skeleton, SkeletonText, skeletonVariants } from './components/Skeleton/index.js';
103
+ export type { SkeletonProps, SkeletonTextProps } from './components/Skeleton/index.js';
102
104
  export { Spinner, spinnerVariants } from './components/Spinner/index.js';
103
105
  export type { SpinnerProps } from './components/Spinner/index.js';
104
106
  export { Progress, progressTrackVariants, progressFillVariants } from './components/Progress/index.js';
@@ -256,3 +258,4 @@ export { Fade, Slide, Scale, Collapse, ListTransition, SkeletonSwap, usePrefersR
256
258
  export type { FadeProps, SlideProps, SlideDirection, ScaleProps, CollapseProps, ListTransitionProps, SkeletonSwapProps, MotionDuration, MotionEasing, MotionBaseProps, RevealProps, FloatProps, MarqueeProps } from './components/Motion/index.js';
257
259
  export type { DataTableColumn } from './components/DataTable/index.js';
258
260
  export type { TableRootProps } from './components/Table/index.js';
261
+ export type { PromiseToastOptions } from './components/Toast/index.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { useMediaQuery } from "./hooks/useMediaQuery.js";
9
9
  import { useMergedRefs } from "./hooks/useMergedRefs.js";
10
10
  import { useUnsavedChanges } from "./hooks/useUnsavedChanges.js";
11
11
  import { usePointerSheen } from "./hooks/usePointerSheen.js";
12
+ import { useAsyncAction } from "./hooks/useAsyncAction.js";
12
13
  import { PromptInput } from "./components/PromptInput/index.js";
13
14
  import { ThinkingIndicator } from "./components/ThinkingIndicator/index.js";
14
15
  import { StreamingText } from "./components/StreamingText/index.js";
@@ -52,7 +53,7 @@ import { RadioCardGroup, RadioCard } from "./components/RadioCard/index.js";
52
53
  import { CheckboxCard } from "./components/CheckboxCard/index.js";
53
54
  import { MediaCard } from "./components/MediaCard/index.js";
54
55
  import { Alert, alertVariants } from "./components/Alert/index.js";
55
- import { Skeleton, skeletonVariants } from "./components/Skeleton/index.js";
56
+ import { Skeleton, SkeletonText, skeletonVariants } from "./components/Skeleton/index.js";
56
57
  import { Spinner, spinnerVariants } from "./components/Spinner/index.js";
57
58
  import { Progress, progressTrackVariants, progressFillVariants } from "./components/Progress/index.js";
58
59
  import { Separator, separatorVariants } from "./components/Separator/index.js";
@@ -266,6 +267,7 @@ export {
266
267
  Sidebar,
267
268
  Skeleton,
268
269
  SkeletonSwap,
270
+ SkeletonText,
269
271
  Slide,
270
272
  Slider,
271
273
  Sortable,
@@ -342,6 +344,7 @@ export {
342
344
  todayIn,
343
345
  toggleVariants,
344
346
  transitionName,
347
+ useAsyncAction,
345
348
  useControllableState,
346
349
  useDisclosure,
347
350
  useForm,