@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.
- package/accessibility.json +1 -1
- package/dist/components/Avatar/Avatar.d.ts +3 -0
- package/dist/components/Avatar/Avatar.js +28 -5
- package/dist/components/Avatar/index.d.ts +1 -1
- package/dist/components/Button/Button.d.ts +2 -1
- package/dist/components/Button/Button.js +37 -7
- package/dist/components/DataTable/DataTable.d.ts +5 -1
- package/dist/components/DataTable/DataTable.js +6 -2
- package/dist/components/Progress/Progress.d.ts +4 -0
- package/dist/components/Progress/Progress.js +73 -20
- package/dist/components/Skeleton/Skeleton.d.ts +9 -0
- package/dist/components/Skeleton/Skeleton.js +18 -1
- package/dist/components/Skeleton/index.d.ts +2 -2
- package/dist/components/Skeleton/index.js +2 -1
- package/dist/components/Toast/Toaster.js +68 -23
- package/dist/components/Toast/index.d.ts +1 -1
- package/dist/components/Toast/useToast.d.ts +18 -1
- package/dist/components/Toast/useToast.js +14 -0
- package/dist/hooks/useAsyncAction.d.ts +27 -0
- package/dist/hooks/useAsyncAction.js +36 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
package/accessibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "7.
|
|
2
|
+
"version": "7.4.0",
|
|
3
3
|
"standard": "WCAG 2.2 Level AA",
|
|
4
4
|
"method": {
|
|
5
5
|
"automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark, and on open popups (menus, listboxes, dialogs), on every pull request; a new violation fails the build.",
|
|
@@ -3,11 +3,14 @@ import { type VariantProps } from 'class-variance-authority';
|
|
|
3
3
|
export declare const avatarVariants: (props?: ({
|
|
4
4
|
size?: "xs" | "sm" | "md" | "lg" | "xl" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline';
|
|
6
7
|
export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof avatarVariants> {
|
|
7
8
|
src?: string;
|
|
8
9
|
name?: string;
|
|
9
10
|
fallback?: React.ReactNode;
|
|
10
11
|
badge?: boolean;
|
|
11
12
|
badgeColor?: string;
|
|
13
|
+
/** Presence dot, added to the accessible name ("Ada Lovelace, Away"). Replaces `badge` when both are set. */
|
|
14
|
+
status?: AvatarStatus;
|
|
12
15
|
}
|
|
13
16
|
export declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
import { cn } from "../../utils/cn.js";
|
|
6
6
|
import { safeSrc } from "../../utils/safeHref.js";
|
|
7
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
7
8
|
const avatarVariants = cva(
|
|
8
9
|
"relative inline-flex items-center justify-center shrink-0 rounded-full overflow-hidden bg-[var(--color-surface-container-high)] text-[var(--color-on-surface-muted)] font-medium select-none",
|
|
9
10
|
{
|
|
@@ -28,6 +29,12 @@ const badgeSizeMap = {
|
|
|
28
29
|
lg: "h-3 w-3",
|
|
29
30
|
xl: "h-3.5 w-3.5"
|
|
30
31
|
};
|
|
32
|
+
const statusColor = {
|
|
33
|
+
online: "bg-[var(--color-success)]",
|
|
34
|
+
away: "bg-[var(--color-warning)]",
|
|
35
|
+
busy: "bg-[var(--color-error)]",
|
|
36
|
+
offline: "bg-[var(--color-outline)]"
|
|
37
|
+
};
|
|
31
38
|
function getInitials(name) {
|
|
32
39
|
const parts = name.trim().split(/\s+/);
|
|
33
40
|
const first = parts[0] ?? "";
|
|
@@ -44,19 +51,30 @@ const Avatar = React.forwardRef(
|
|
|
44
51
|
fallback,
|
|
45
52
|
badge,
|
|
46
53
|
badgeColor,
|
|
54
|
+
status,
|
|
47
55
|
...props
|
|
48
56
|
}, ref) => {
|
|
49
57
|
const src = safeSrc(srcProp);
|
|
50
58
|
const [failedSrc, setFailedSrc] = React.useState();
|
|
59
|
+
const [pendingSrc, setPendingSrc] = React.useState();
|
|
60
|
+
const hideUntilLoaded = React.useCallback(
|
|
61
|
+
(img) => {
|
|
62
|
+
if (img && !img.complete) setPendingSrc(img.getAttribute("src") ?? void 0);
|
|
63
|
+
},
|
|
64
|
+
[]
|
|
65
|
+
);
|
|
66
|
+
const messages = useNexubMessages();
|
|
67
|
+
const statusLabel = status ? messages.presence[status] : void 0;
|
|
68
|
+
const label = [name, statusLabel].filter(Boolean).join(", ") || void 0;
|
|
51
69
|
const showImage = Boolean(src) && failedSrc !== src;
|
|
52
70
|
const initials = name ? getInitials(name) : null;
|
|
53
|
-
const labelled = Boolean(
|
|
71
|
+
const labelled = Boolean(label || props["aria-label"] || props["aria-labelledby"]);
|
|
54
72
|
return /* @__PURE__ */ jsxs(
|
|
55
73
|
"span",
|
|
56
74
|
{
|
|
57
75
|
ref,
|
|
58
76
|
className: cn(avatarVariants({ size }), className),
|
|
59
|
-
"aria-label":
|
|
77
|
+
"aria-label": label,
|
|
60
78
|
role: labelled ? "img" : void 0,
|
|
61
79
|
"aria-hidden": labelled ? void 0 : true,
|
|
62
80
|
...props,
|
|
@@ -66,7 +84,12 @@ const Avatar = React.forwardRef(
|
|
|
66
84
|
{
|
|
67
85
|
src,
|
|
68
86
|
alt: name ?? "",
|
|
69
|
-
|
|
87
|
+
ref: hideUntilLoaded,
|
|
88
|
+
className: cn(
|
|
89
|
+
"h-full w-full object-cover transition-opacity duration-effects-default ease-effects-default",
|
|
90
|
+
pendingSrc === src && "opacity-0"
|
|
91
|
+
),
|
|
92
|
+
onLoad: () => setPendingSrc(void 0),
|
|
70
93
|
onError: () => setFailedSrc(src)
|
|
71
94
|
}
|
|
72
95
|
) : initials ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: initials }) : fallback ? fallback : (
|
|
@@ -82,13 +105,13 @@ const Avatar = React.forwardRef(
|
|
|
82
105
|
}
|
|
83
106
|
)
|
|
84
107
|
),
|
|
85
|
-
badge && /* @__PURE__ */ jsx(
|
|
108
|
+
(badge || status) && /* @__PURE__ */ jsx(
|
|
86
109
|
"span",
|
|
87
110
|
{
|
|
88
111
|
className: cn(
|
|
89
112
|
"absolute bottom-0 end-0 rounded-full ring-2 ring-[var(--color-surface)]",
|
|
90
113
|
badgeSizeMap[size ?? "md"],
|
|
91
|
-
badgeColor ?? "bg-[var(--color-success)]"
|
|
114
|
+
status ? statusColor[status] : badgeColor ?? "bg-[var(--color-success)]"
|
|
92
115
|
),
|
|
93
116
|
"aria-hidden": "true"
|
|
94
117
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { Avatar, avatarVariants } from './Avatar.js';
|
|
2
|
-
export type { AvatarProps } from './Avatar.js';
|
|
2
|
+
export type { AvatarProps, AvatarStatus } from './Avatar.js';
|
|
@@ -14,7 +14,8 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
14
14
|
/** Async feedback. `loading` blocks clicks (aria-disabled, focus kept),
|
|
15
15
|
* `success` draws a check, `error` shakes once. Not applied with `asChild`. */
|
|
16
16
|
status?: ButtonStatus;
|
|
17
|
-
/** Alias for `status="loading"`.
|
|
17
|
+
/** Alias for `status="loading"`. A `type="submit"` button also shows loading
|
|
18
|
+
* while its parent `<form action>` is pending (React 19 `useFormStatus`). */
|
|
18
19
|
loading?: boolean;
|
|
19
20
|
/** Announced while loading. @default 'Loading' */
|
|
20
21
|
loadingLabel?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
|
+
import { useFormStatus } from "react-dom";
|
|
4
5
|
import { Slot } from "@radix-ui/react-slot";
|
|
5
6
|
import { Loader2 } from "@nexub/icons";
|
|
6
7
|
import { cva } from "class-variance-authority";
|
|
@@ -67,6 +68,22 @@ const buttonVariants = cva(
|
|
|
67
68
|
}
|
|
68
69
|
);
|
|
69
70
|
const CheckIcon = () => /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { className: "nx-check-draw", pathLength: 1, d: "M20 6 9 17l-5-5" }) });
|
|
71
|
+
const SPINNER_DELAY = 150;
|
|
72
|
+
const SPINNER_MIN = 400;
|
|
73
|
+
function useSpinner(busy) {
|
|
74
|
+
const [shown, setShown] = React.useState(busy);
|
|
75
|
+
const shownAt = React.useRef(0);
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (shown) shownAt.current = Date.now();
|
|
78
|
+
}, [shown]);
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (busy === shown) return;
|
|
81
|
+
const wait = busy ? SPINNER_DELAY : Math.max(0, SPINNER_MIN - (Date.now() - shownAt.current));
|
|
82
|
+
const t = setTimeout(() => setShown(busy), wait);
|
|
83
|
+
return () => clearTimeout(t);
|
|
84
|
+
}, [busy, shown]);
|
|
85
|
+
return shown;
|
|
86
|
+
}
|
|
70
87
|
const Button = React.forwardRef(
|
|
71
88
|
({
|
|
72
89
|
className,
|
|
@@ -89,7 +106,9 @@ const Button = React.forwardRef(
|
|
|
89
106
|
children,
|
|
90
107
|
...props
|
|
91
108
|
}, ref) => {
|
|
92
|
-
const
|
|
109
|
+
const formPending = useFormStatus().pending && props.type === "submit";
|
|
110
|
+
const status = statusProp ?? (loading || formPending ? "loading" : "idle");
|
|
111
|
+
const spinner = useSpinner(status === "loading");
|
|
93
112
|
const handlePointerMove = usePointerSheen(magnetic, onPointerMove);
|
|
94
113
|
const sharedProps = {
|
|
95
114
|
className: cn(
|
|
@@ -111,7 +130,7 @@ const Button = React.forwardRef(
|
|
|
111
130
|
}
|
|
112
131
|
const busy = status === "loading";
|
|
113
132
|
const swapLabel = status === "success" && successLabel !== void 0;
|
|
114
|
-
const overlay =
|
|
133
|
+
const overlay = spinner ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin", "aria-hidden": "true" }) : status === "success" && !swapLabel ? /* @__PURE__ */ jsx(CheckIcon, {}) : null;
|
|
115
134
|
const announcement = status === "loading" ? loadingLabel : status === "success" ? successLabel ?? "Done" : status === "error" ? errorLabel : "";
|
|
116
135
|
const button = /* @__PURE__ */ jsxs(
|
|
117
136
|
"button",
|
|
@@ -130,14 +149,25 @@ const Button = React.forwardRef(
|
|
|
130
149
|
"data-status": status === "idle" ? void 0 : status,
|
|
131
150
|
...sharedProps,
|
|
132
151
|
children: [
|
|
133
|
-
/* @__PURE__ */
|
|
134
|
-
/* @__PURE__ */ jsx(CheckIcon, {}),
|
|
135
|
-
successLabel
|
|
136
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
152
|
+
successLabel === void 0 ? /* @__PURE__ */ jsxs("span", { className: cn("contents", overlay && "text-transparent [&_img]:invisible [&_svg]:invisible"), children: [
|
|
137
153
|
leadingIcon,
|
|
138
154
|
children,
|
|
139
155
|
trailingIcon
|
|
140
|
-
] })
|
|
156
|
+
] }) : (
|
|
157
|
+
// Both labels share one grid cell, so the button is as wide as the wider one
|
|
158
|
+
// whichever shows; the hidden one keeps its space.
|
|
159
|
+
/* @__PURE__ */ jsxs("span", { className: cn("inline-grid", overlay && "text-transparent [&_img]:invisible [&_svg]:invisible"), children: [
|
|
160
|
+
/* @__PURE__ */ jsxs("span", { className: cn("col-start-1 row-start-1 inline-flex items-center justify-center gap-2", swapLabel && "invisible"), "aria-hidden": swapLabel || void 0, children: [
|
|
161
|
+
leadingIcon,
|
|
162
|
+
children,
|
|
163
|
+
trailingIcon
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsxs("span", { className: cn("col-start-1 row-start-1 inline-flex items-center justify-center gap-2", !swapLabel && "invisible"), "aria-hidden": !swapLabel || void 0, children: [
|
|
166
|
+
swapLabel ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx("span", { className: "h-4 w-4" }),
|
|
167
|
+
successLabel
|
|
168
|
+
] })
|
|
169
|
+
] })
|
|
170
|
+
),
|
|
141
171
|
overlay && /* @__PURE__ */ jsx("span", { className: "absolute inset-0 flex items-center justify-center", "aria-hidden": "true", children: overlay })
|
|
142
172
|
]
|
|
143
173
|
}
|
|
@@ -91,5 +91,9 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
|
|
|
91
91
|
onRowOrderChange?: (data: TData[]) => void;
|
|
92
92
|
/** Names a row in "Move ‹label›" and the move announcements. @default its position */
|
|
93
93
|
getRowLabel?: (row: TData) => string;
|
|
94
|
+
/** Shows skeleton rows (one page's worth) and sets aria-busy on the table. */
|
|
95
|
+
loading?: boolean;
|
|
96
|
+
/** Shown in a full-width row when there are no rows. @default messages.noResults */
|
|
97
|
+
emptyState?: React.ReactNode;
|
|
94
98
|
}
|
|
95
|
-
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, getSubRows, expanded: expandedProp, defaultExpanded, onExpandedChange, onRowOrderChange, getRowLabel, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
99
|
+
export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, grid, rowWindow, pagination, getSubRows, expanded: expandedProp, defaultExpanded, onExpandedChange, onRowOrderChange, getRowLabel, loading, emptyState, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
|
|
@@ -23,6 +23,7 @@ import { Button } from "../Button/Button.js";
|
|
|
23
23
|
import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
|
|
24
24
|
import { createGridNavigation } from "./gridNavigation.js";
|
|
25
25
|
import { useReorder } from "../Sortable/reorder.js";
|
|
26
|
+
import { Skeleton } from "../Skeleton/Skeleton.js";
|
|
26
27
|
const alignText = {
|
|
27
28
|
start: "text-start",
|
|
28
29
|
end: "text-end",
|
|
@@ -170,6 +171,8 @@ function DataTable({
|
|
|
170
171
|
onExpandedChange,
|
|
171
172
|
onRowOrderChange,
|
|
172
173
|
getRowLabel,
|
|
174
|
+
loading = false,
|
|
175
|
+
emptyState,
|
|
173
176
|
caption,
|
|
174
177
|
captionHidden = false,
|
|
175
178
|
className,
|
|
@@ -429,6 +432,7 @@ function DataTable({
|
|
|
429
432
|
"aria-label": ariaLabel,
|
|
430
433
|
"aria-labelledby": ariaLabelledBy,
|
|
431
434
|
"aria-rowcount": rowWin.virtual ? rows.length + 1 : void 0,
|
|
435
|
+
"aria-busy": loading || void 0,
|
|
432
436
|
role: grid ? "grid" : void 0,
|
|
433
437
|
onKeyDown: grid ? gridNav.onKeyDown : void 0,
|
|
434
438
|
onFocus: grid ? gridNav.onFocus : void 0,
|
|
@@ -528,7 +532,7 @@ function DataTable({
|
|
|
528
532
|
header.id
|
|
529
533
|
);
|
|
530
534
|
}) }, headerGroup.id)) }),
|
|
531
|
-
/* @__PURE__ */ jsx(TableBody, { children: rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
535
|
+
/* @__PURE__ */ jsx(TableBody, { children: loading ? Array.from({ length: pagination?.pageSize ?? 5 }, (_, i) => /* @__PURE__ */ jsx(TableRow, { "data-loading": "", className: "hover:bg-transparent", children: table.getVisibleLeafColumns().map((c) => /* @__PURE__ */ jsx(TableCell, { className: cn(compact && "py-1.5"), children: /* @__PURE__ */ jsx(Skeleton, {}) }, c.id)) }, i)) : rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
532
536
|
rowWin.before > 0 && /* @__PURE__ */ jsx(Spacer, { height: rowWin.before }),
|
|
533
537
|
rowWin.indexes.map((i) => {
|
|
534
538
|
const row = rows[i];
|
|
@@ -564,7 +568,7 @@ function DataTable({
|
|
|
564
568
|
{
|
|
565
569
|
colSpan: table.getVisibleLeafColumns().length,
|
|
566
570
|
className: "h-24 text-center text-[var(--color-on-surface-muted)]",
|
|
567
|
-
children: messages.noResults
|
|
571
|
+
children: emptyState ?? messages.noResults
|
|
568
572
|
}
|
|
569
573
|
) }) })
|
|
570
574
|
]
|
|
@@ -13,5 +13,9 @@ export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>
|
|
|
13
13
|
max?: number;
|
|
14
14
|
tone?: ToneValue;
|
|
15
15
|
animated?: boolean;
|
|
16
|
+
/** `circular` draws a ring; same value/max/indeterminate semantics. @default 'linear' */
|
|
17
|
+
variant?: 'linear' | 'circular';
|
|
18
|
+
/** Linear only: splits the bar into N equal steps, filled up to the value. */
|
|
19
|
+
segments?: number;
|
|
16
20
|
}
|
|
17
21
|
export declare const Progress: React.ForwardRefExoticComponent<ProgressProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cva } from "class-variance-authority";
|
|
5
5
|
import { cn } from "../../utils/cn.js";
|
|
@@ -43,6 +43,18 @@ const progressFillVariants = cva(
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
);
|
|
46
|
+
const ringTone = {
|
|
47
|
+
neutral: "stroke-[var(--color-outline)]",
|
|
48
|
+
info: "stroke-[var(--color-info)]",
|
|
49
|
+
success: "stroke-[var(--color-success)]",
|
|
50
|
+
warning: "stroke-[var(--color-warning)]",
|
|
51
|
+
danger: "stroke-[var(--color-error)]",
|
|
52
|
+
leap: "stroke-[var(--color-leap)]",
|
|
53
|
+
bookub: "stroke-[var(--color-bookub)]",
|
|
54
|
+
payub: "stroke-[var(--color-payub)]",
|
|
55
|
+
founderub: "stroke-[var(--color-founderub)]"
|
|
56
|
+
};
|
|
57
|
+
const ringSize = { sm: "size-6", md: "size-10", lg: "size-16" };
|
|
46
58
|
const Progress = React.forwardRef(
|
|
47
59
|
({
|
|
48
60
|
className,
|
|
@@ -51,6 +63,8 @@ const Progress = React.forwardRef(
|
|
|
51
63
|
value,
|
|
52
64
|
max = 100,
|
|
53
65
|
animated = false,
|
|
66
|
+
variant = "linear",
|
|
67
|
+
segments,
|
|
54
68
|
...props
|
|
55
69
|
}, ref) => {
|
|
56
70
|
const isIndeterminate = value === void 0 || value === null;
|
|
@@ -60,31 +74,70 @@ const Progress = React.forwardRef(
|
|
|
60
74
|
useIsomorphicLayoutEffect(() => {
|
|
61
75
|
fillRef.current?.style.setProperty("width", `${percentage}%`);
|
|
62
76
|
}, [percentage]);
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"div",
|
|
77
|
+
const aria = {
|
|
78
|
+
role: "progressbar",
|
|
79
|
+
"aria-valuenow": isIndeterminate ? void 0 : clampedValue,
|
|
80
|
+
"aria-valuemin": 0,
|
|
81
|
+
"aria-valuemax": max
|
|
82
|
+
};
|
|
83
|
+
if (variant === "circular") {
|
|
84
|
+
return /* @__PURE__ */ jsx("div", { ref, ...aria, className: cn("inline-flex", ringSize[size ?? "md"], className), ...props, children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 36 36", className: cn("size-full -rotate-90", isIndeterminate && "animate-spin motion-reduce:animate-none"), "aria-hidden": "true", children: [
|
|
85
|
+
/* @__PURE__ */ jsx("circle", { cx: "18", cy: "18", r: "15", fill: "none", strokeWidth: "4", className: "stroke-[var(--color-surface-container-high)]" }),
|
|
86
|
+
/* @__PURE__ */ jsx(
|
|
87
|
+
"circle",
|
|
75
88
|
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
cx: "18",
|
|
90
|
+
cy: "18",
|
|
91
|
+
r: "15",
|
|
92
|
+
fill: "none",
|
|
93
|
+
strokeWidth: "4",
|
|
94
|
+
strokeLinecap: "round",
|
|
95
|
+
pathLength: 100,
|
|
96
|
+
strokeDasharray: `${isIndeterminate ? 25 : percentage} 100`,
|
|
97
|
+
className: cn("transition-all duration-spatial-default ease-spatial-default", ringTone[tone]),
|
|
82
98
|
"data-part": "fill",
|
|
83
99
|
"data-indeterminate": isIndeterminate || void 0
|
|
84
100
|
}
|
|
85
101
|
)
|
|
102
|
+
] }) });
|
|
103
|
+
}
|
|
104
|
+
if (segments && segments > 0 && !isIndeterminate) {
|
|
105
|
+
const filled = Math.round(percentage / 100 * segments);
|
|
106
|
+
return /* @__PURE__ */ jsx(
|
|
107
|
+
"div",
|
|
108
|
+
{
|
|
109
|
+
ref,
|
|
110
|
+
...aria,
|
|
111
|
+
className: cn(progressTrackVariants({ size }), "flex gap-1 bg-transparent", className),
|
|
112
|
+
...props,
|
|
113
|
+
children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx(
|
|
114
|
+
"div",
|
|
115
|
+
{
|
|
116
|
+
"data-part": "segment",
|
|
117
|
+
"data-filled": i < filled || void 0,
|
|
118
|
+
className: cn(
|
|
119
|
+
"flex-1",
|
|
120
|
+
i < filled ? progressFillVariants({ tone }) : "rounded-full bg-[var(--color-surface-container-high)]"
|
|
121
|
+
)
|
|
122
|
+
},
|
|
123
|
+
i
|
|
124
|
+
))
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return /* @__PURE__ */ jsx("div", { ref, ...aria, className: cn(progressTrackVariants({ size }), className), ...props, children: /* @__PURE__ */ jsx(
|
|
129
|
+
"div",
|
|
130
|
+
{
|
|
131
|
+
className: cn(
|
|
132
|
+
progressFillVariants({ tone }),
|
|
133
|
+
animated && "animate-pulse",
|
|
134
|
+
isIndeterminate && "animate-[nx-progress-indeterminate_1.4s_ease-in-out_infinite]"
|
|
135
|
+
),
|
|
136
|
+
ref: fillRef,
|
|
137
|
+
"data-part": "fill",
|
|
138
|
+
"data-indeterminate": isIndeterminate || void 0
|
|
86
139
|
}
|
|
87
|
-
);
|
|
140
|
+
) });
|
|
88
141
|
}
|
|
89
142
|
);
|
|
90
143
|
Progress.displayName = "Progress";
|
|
@@ -10,5 +10,14 @@ export interface SkeletonProps extends React.HTMLAttributes<HTMLSpanElement>, Va
|
|
|
10
10
|
width?: string | number;
|
|
11
11
|
height?: string | number;
|
|
12
12
|
animated?: boolean;
|
|
13
|
+
/** Effect when `animated`: `pulse` fades, `shimmer` sweeps a highlight across. Both stop under reduced motion. @default 'pulse' */
|
|
14
|
+
animation?: 'pulse' | 'shimmer';
|
|
13
15
|
}
|
|
14
16
|
export declare const Skeleton: React.ForwardRefExoticComponent<SkeletonProps & React.RefAttributes<HTMLSpanElement>>;
|
|
17
|
+
export interface SkeletonTextProps extends React.HTMLAttributes<HTMLSpanElement>, Pick<SkeletonProps, 'animated' | 'animation'> {
|
|
18
|
+
/** Number of lines; the last is shorter. @default 3 */
|
|
19
|
+
lines?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Placeholder paragraph. Each line is one line-height tall (inherited from the
|
|
22
|
+
* surrounding text style), so swapping in the real text doesn't shift layout. */
|
|
23
|
+
export declare const SkeletonText: React.ForwardRefExoticComponent<SkeletonTextProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -29,6 +29,7 @@ const Skeleton = React.forwardRef(
|
|
|
29
29
|
width,
|
|
30
30
|
height,
|
|
31
31
|
animated = true,
|
|
32
|
+
animation = "pulse",
|
|
32
33
|
style,
|
|
33
34
|
...props
|
|
34
35
|
}, ref) => {
|
|
@@ -40,7 +41,11 @@ const Skeleton = React.forwardRef(
|
|
|
40
41
|
{
|
|
41
42
|
ref,
|
|
42
43
|
"aria-hidden": "true",
|
|
43
|
-
className: cn(
|
|
44
|
+
className: cn(
|
|
45
|
+
skeletonVariants({ shape, animated: animated && animation === "pulse" }),
|
|
46
|
+
animated && animation === "shimmer" && "nx-skeleton-shimmer",
|
|
47
|
+
className
|
|
48
|
+
),
|
|
44
49
|
style: sizeStyle || style ? { ...sizeStyle, ...style } : void 0,
|
|
45
50
|
...props
|
|
46
51
|
}
|
|
@@ -48,7 +53,19 @@ const Skeleton = React.forwardRef(
|
|
|
48
53
|
}
|
|
49
54
|
);
|
|
50
55
|
Skeleton.displayName = "Skeleton";
|
|
56
|
+
const SkeletonText = React.forwardRef(
|
|
57
|
+
({ className, lines = 3, animated, animation, ...props }, ref) => /* @__PURE__ */ jsx("span", { ref, "aria-hidden": "true", className: cn("block", className), ...props, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx("span", { className: "flex h-[1lh] items-center", children: /* @__PURE__ */ jsx(
|
|
58
|
+
Skeleton,
|
|
59
|
+
{
|
|
60
|
+
animated,
|
|
61
|
+
animation,
|
|
62
|
+
className: i === lines - 1 && lines > 1 ? "w-3/5" : void 0
|
|
63
|
+
}
|
|
64
|
+
) }, i)) })
|
|
65
|
+
);
|
|
66
|
+
SkeletonText.displayName = "SkeletonText";
|
|
51
67
|
export {
|
|
52
68
|
Skeleton,
|
|
69
|
+
SkeletonText,
|
|
53
70
|
skeletonVariants
|
|
54
71
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Skeleton, skeletonVariants } from './Skeleton.js';
|
|
2
|
-
export type { SkeletonProps } from './Skeleton.js';
|
|
1
|
+
export { Skeleton, SkeletonText, skeletonVariants } from './Skeleton.js';
|
|
2
|
+
export type { SkeletonProps, SkeletonTextProps } from './Skeleton.js';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as RadixToast from "@radix-ui/react-toast";
|
|
5
|
-
import { X } from "@nexub/icons";
|
|
5
|
+
import { AlertTriangle, CheckCircle2, Info, Loader2, X, XCircle } from "@nexub/icons";
|
|
6
6
|
import { cn } from "../../utils/cn.js";
|
|
7
7
|
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
|
|
8
8
|
import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
@@ -12,8 +12,17 @@ const toneMap = {
|
|
|
12
12
|
error: "border-s-4 border-[var(--color-error)]",
|
|
13
13
|
warning: "border-s-4 border-[var(--color-warning)]",
|
|
14
14
|
info: "border-s-4 border-[var(--color-info)]",
|
|
15
|
-
neutral: ""
|
|
15
|
+
neutral: "",
|
|
16
|
+
loading: ""
|
|
16
17
|
};
|
|
18
|
+
const toneIcon = {
|
|
19
|
+
success: /* @__PURE__ */ jsx(CheckCircle2, { size: "sm", "aria-hidden": "true", className: "shrink-0 text-[var(--color-success-container)]" }),
|
|
20
|
+
error: /* @__PURE__ */ jsx(XCircle, { size: "sm", "aria-hidden": "true", className: "shrink-0 text-[var(--color-error-container)]" }),
|
|
21
|
+
warning: /* @__PURE__ */ jsx(AlertTriangle, { size: "sm", "aria-hidden": "true", className: "shrink-0 text-[var(--color-warning-container)]" }),
|
|
22
|
+
info: /* @__PURE__ */ jsx(Info, { size: "sm", "aria-hidden": "true", className: "shrink-0 text-[var(--color-info-container)]" }),
|
|
23
|
+
loading: /* @__PURE__ */ jsx(Loader2, { size: "sm", "aria-hidden": "true", className: "shrink-0 animate-spin motion-reduce:animate-none" })
|
|
24
|
+
};
|
|
25
|
+
const FLICK_VELOCITY = 0.11;
|
|
17
26
|
const GAP = 8;
|
|
18
27
|
const vertical = {
|
|
19
28
|
top: { viewport: "top-0", toast: "top-4 origin-bottom", enter: "[--nx-y:calc(-100%-1rem)]" },
|
|
@@ -41,6 +50,8 @@ function Toaster({ expand = false, visibleToasts = 3, position = "top-end" }) {
|
|
|
41
50
|
const state = React.useRef({ order, expanded, visibleToasts });
|
|
42
51
|
state.current = { order, expanded, visibleToasts };
|
|
43
52
|
const timers = React.useRef(/* @__PURE__ */ new Set());
|
|
53
|
+
const firstSeen = React.useRef(/* @__PURE__ */ new Map());
|
|
54
|
+
const swipeStart = React.useRef(0);
|
|
44
55
|
React.useEffect(() => {
|
|
45
56
|
const pending = timers.current;
|
|
46
57
|
return () => pending.forEach(clearTimeout);
|
|
@@ -53,12 +64,12 @@ function Toaster({ expand = false, visibleToasts = 3, position = "top-end" }) {
|
|
|
53
64
|
let front = 0;
|
|
54
65
|
let stack = 0;
|
|
55
66
|
order2.forEach((id, i) => {
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
58
|
-
if (i === 0 || expanded2 || !heights.current.has(id)) heights.current.set(id,
|
|
67
|
+
const el2 = ol.querySelector(`[data-toast-id="${id}"]`);
|
|
68
|
+
if (!el2) return;
|
|
69
|
+
if (i === 0 || expanded2 || !heights.current.has(id)) heights.current.set(id, el2.offsetHeight);
|
|
59
70
|
const h2 = heights.current.get(id);
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
el2.style.setProperty("--nx-toast-index", String(i));
|
|
72
|
+
el2.style.setProperty("--nx-toast-offset", `${offset}px`);
|
|
62
73
|
if (i === 0) front = h2;
|
|
63
74
|
if (i < visibleToasts2) stack = offset + h2;
|
|
64
75
|
offset += h2 + GAP;
|
|
@@ -104,30 +115,53 @@ function Toaster({ expand = false, visibleToasts = 3, position = "top-end" }) {
|
|
|
104
115
|
setFocused(false);
|
|
105
116
|
}
|
|
106
117
|
}, [order.length]);
|
|
118
|
+
const el = (id) => viewportRef.current?.querySelector(`[data-toast-id="${id}"]`);
|
|
119
|
+
const close = (t) => {
|
|
120
|
+
if (closed.has(t.id)) return;
|
|
121
|
+
fireToastClose(t);
|
|
122
|
+
setClosed((s) => new Set(s).add(t.id));
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
timers.current.delete(timer);
|
|
125
|
+
firstSeen.current.delete(t.id);
|
|
126
|
+
toast.dismiss(t.id);
|
|
127
|
+
setClosed((s) => {
|
|
128
|
+
const next = new Set(s);
|
|
129
|
+
next.delete(t.id);
|
|
130
|
+
return next;
|
|
131
|
+
});
|
|
132
|
+
}, 1e3);
|
|
133
|
+
timers.current.add(timer);
|
|
134
|
+
};
|
|
107
135
|
return /* @__PURE__ */ jsxs(RadixToast.Provider, { swipeDirection: align === "start" === (dir === "rtl") ? "right" : "left", children: [
|
|
108
136
|
toasts.map((t) => {
|
|
109
137
|
const i = order.indexOf(t.id);
|
|
110
138
|
const action = t.action ?? (t.undo && { label: t.undo.label ?? messages.undo, onClick: t.undo.onUndo });
|
|
139
|
+
if (!firstSeen.current.has(t.id)) firstSeen.current.set(t.id, t);
|
|
140
|
+
const announce = firstSeen.current.get(t.id) !== t && t.tone !== "loading" ? [t.title, t.description].filter(Boolean).join(". ") : "";
|
|
141
|
+
const icon = t.icon === false ? null : t.icon ?? toneIcon[t.tone ?? "neutral"];
|
|
142
|
+
const duration = t.tone === "loading" ? Infinity : t.duration ?? 5e3;
|
|
111
143
|
return /* @__PURE__ */ jsxs(
|
|
112
144
|
RadixToast.Root,
|
|
113
145
|
{
|
|
114
146
|
"data-toast-id": t.id,
|
|
115
147
|
"data-front": i === -1 ? void 0 : i === 0,
|
|
116
148
|
"data-hidden": i === -1 ? void 0 : i >= visibleToasts,
|
|
149
|
+
open: !closed.has(t.id),
|
|
117
150
|
onOpenChange: (open) => {
|
|
118
|
-
if (open)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
if (!open) close(t);
|
|
152
|
+
},
|
|
153
|
+
onPause: () => el(t.id)?.setAttribute("data-paused", ""),
|
|
154
|
+
onResume: () => el(t.id)?.removeAttribute("data-paused"),
|
|
155
|
+
onSwipeStart: () => {
|
|
156
|
+
swipeStart.current = performance.now();
|
|
157
|
+
},
|
|
158
|
+
onSwipeCancel: (e) => {
|
|
159
|
+
const { x, y } = e.detail.delta;
|
|
160
|
+
if (Math.hypot(x, y) / Math.max(1, performance.now() - swipeStart.current) < FLICK_VELOCITY) return;
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
e.currentTarget.setAttribute("data-swipe", "end");
|
|
163
|
+
e.currentTarget.style.setProperty("--radix-toast-swipe-end-x", `${x}px`);
|
|
164
|
+
close(t);
|
|
131
165
|
},
|
|
132
166
|
className: cn(
|
|
133
167
|
"group pointer-events-auto absolute inset-x-4 flex items-center justify-between gap-3 overflow-hidden rounded-shape-container bg-[var(--color-inverse-surface)] py-3 ps-4 pe-2 text-[var(--color-inverse-on-surface)] shadow-elevation-2",
|
|
@@ -152,9 +186,10 @@ function Toaster({ expand = false, visibleToasts = 3, position = "top-end" }) {
|
|
|
152
186
|
align === "center" ? v.enter : h.enter,
|
|
153
187
|
toneMap[t.tone ?? "neutral"]
|
|
154
188
|
),
|
|
155
|
-
duration
|
|
189
|
+
duration,
|
|
156
190
|
children: [
|
|
157
|
-
|
|
191
|
+
icon,
|
|
192
|
+
/* @__PURE__ */ jsxs("div", { className: "grid flex-1 gap-1", children: [
|
|
158
193
|
/* @__PURE__ */ jsx(RadixToast.Title, { className: "text-title-sm", children: t.title }),
|
|
159
194
|
t.description && /* @__PURE__ */ jsx(RadixToast.Description, { className: "text-body-sm text-[color-mix(in_srgb,var(--color-inverse-on-surface)_80%,transparent)]", children: t.description })
|
|
160
195
|
] }),
|
|
@@ -170,7 +205,17 @@ function Toaster({ expand = false, visibleToasts = 3, position = "top-end" }) {
|
|
|
170
205
|
/* @__PURE__ */ jsxs(RadixToast.Close, { className: "nx-state-layer relative grid h-8 w-8 place-items-center rounded-full transition-opacity [@media(hover:hover)]:opacity-0 group-hover:opacity-100 focus-visible:opacity-100", children: [
|
|
171
206
|
/* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }),
|
|
172
207
|
/* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.close })
|
|
173
|
-
] })
|
|
208
|
+
] }),
|
|
209
|
+
/* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: announce }),
|
|
210
|
+
t.showProgress && duration !== Infinity && /* @__PURE__ */ jsx(
|
|
211
|
+
"span",
|
|
212
|
+
{
|
|
213
|
+
"aria-hidden": "true",
|
|
214
|
+
ref: (bar) => bar?.style.setProperty("animation-duration", `${duration}ms`),
|
|
215
|
+
className: "absolute inset-x-0 bottom-0 h-0.5 origin-left bg-current opacity-40 rtl:origin-right [animation:nx-scroll-progress_1s_linear_reverse_forwards] group-data-[paused]:[animation-play-state:paused] motion-reduce:hidden"
|
|
216
|
+
},
|
|
217
|
+
duration
|
|
218
|
+
)
|
|
174
219
|
]
|
|
175
220
|
},
|
|
176
221
|
t.id
|
|
@@ -2,4 +2,4 @@ export { Toast } from './Toast.js';
|
|
|
2
2
|
export { Toaster } from './Toaster.js';
|
|
3
3
|
export type { ToasterProps, ToasterPosition } from './Toaster.js';
|
|
4
4
|
export { toast, useToastStore } from './useToast.js';
|
|
5
|
-
export type { ToastItem, ToastTone, UndoableToastOptions } from './useToast.js';
|
|
5
|
+
export type { ToastItem, ToastTone, UndoableToastOptions, PromiseToastOptions } from './useToast.js';
|