@mihcm/ui 0.15.1 → 0.16.1
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/README.md +8 -2
- package/dist/Toast.d.ts +95 -35
- package/dist/Toast.d.ts.map +1 -1
- package/dist/Toast.js +148 -138
- package/dist/Toast.js.map +1 -1
- package/package.json +5 -4
- package/src/Toast.tsx +301 -325
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ This is the primary component package. It ships typed, tokenized primitives with
|
|
|
6
6
|
|
|
7
7
|
## Current release
|
|
8
8
|
|
|
9
|
-
Current package version: **0.
|
|
9
|
+
Current package version: **0.16.0**.
|
|
10
10
|
|
|
11
|
-
This release
|
|
11
|
+
This release rebuilds `Toast` on top of [sonner](https://github.com/emilkowalski/sonner) (themed end-to-end with MiHCM tokens) and lands the `MainSidebar` polish + whitelabel layer: five interaction variants (drilldown / floating / columns / command / hover), shadcn-style `collapsible="icon"`, mobile hamburger + Sheet pattern, vertical guide lines for arbitrary nesting, and a 14-token `colorScheme` prop for full whitelabeling. See `CHANGELOG.md` and the public changelog at `https://designsystem.mihcm.com/help/changelog`.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -58,6 +58,12 @@ Variant styling uses proven helpers rather than handwritten class maps: `class-v
|
|
|
58
58
|
|
|
59
59
|
## Recent additions and fixes
|
|
60
60
|
|
|
61
|
+
### 0.16.0
|
|
62
|
+
- **Toast** rebuilt on top of [sonner](https://github.com/emilkowalski/sonner). Exposes the full upstream feature set (`toast.success/error/warning/info/message/loading`, `toast.promise`, `toast.custom`, action+cancel buttons, swipe-to-dismiss, expand-on-hover, theme switching) with MiHCM tokens mapped through sonner's CSS variables so surface, type tints, and buttons follow the design-system palette in light + dark mode. Legacy `<ToastProvider>` + `<Toast>` sub-components remain as deprecated shims.
|
|
63
|
+
- **MainSidebar** ships shadcn-style collapsible toggle (`collapsible="icon"`), five interaction variants, deep-nesting breadcrumbs (`PathBreadcrumb`), mobile hamburger pattern via existing `Sheet`, and a 14-token `colorScheme` prop (`bg`, `fg`, `railBg`, `railFg`, `panelBg`, `panelFg`, `accentBg`, `accentFg`, `border`, `mutedFg`, `hoverBg`, `ring`, `tooltipBg`, `tooltipFg`) that cascades through every nested affordance — including tooltips, badges, breadcrumbs, panel surfaces, and dividers. Outside-click + Esc dismissal unified, columns variant `flushSync`-resets deep stack on close, focus uses `preventScroll`.
|
|
64
|
+
- **Icons + badges in active rows** inherit `currentColor` from the parent button so themed accent surfaces render the right contrast.
|
|
65
|
+
|
|
66
|
+
### 0.15.0
|
|
61
67
|
- Added a semantic `Link` primitive for real navigation, with inline, nav, standalone, button-like, muted, foreground, and accent variants.
|
|
62
68
|
- Added semantic `Text as="h1" | "h2" | "p" | ...` rendering so docs, apps, and AI-generated text preserve the actual document outline instead of styling generic spans.
|
|
63
69
|
- Made `Button` default to `type="button"` while still supporting explicit `type="submit"` and `type="reset"` for form actions.
|
package/dist/Toast.d.ts
CHANGED
|
@@ -1,29 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Toast (web variant — React DOM).
|
|
3
3
|
*
|
|
4
|
+
* Built on top of [sonner](https://sonner.emilkowal.ski/) so MiHCM apps get
|
|
5
|
+
* every feature the upstream library ships — promise toasts, loading state,
|
|
6
|
+
* action + cancel buttons, swipe-to-dismiss, hover-to-expand stacks, rich
|
|
7
|
+
* colours per type, theme switching, full a11y wiring — without forking.
|
|
8
|
+
*
|
|
9
|
+
* Surface is themed with MiHCM tokens (`bg-card`, `border-border`,
|
|
10
|
+
* `text-foreground`, `text-success-foreground`, etc.) via sonner's
|
|
11
|
+
* `toastOptions.classNames` so the toasts read as part of the rest of the
|
|
12
|
+
* design system regardless of light/dark mode or custom `colorScheme`s.
|
|
13
|
+
*
|
|
4
14
|
* Two-part API:
|
|
5
|
-
* 1. `<
|
|
6
|
-
* in a
|
|
7
|
-
*
|
|
15
|
+
* 1. `<Toaster />` — mount once near the app root. It renders the queue
|
|
16
|
+
* in a portal at the chosen `position`. Pass `richColors`, `expand`,
|
|
17
|
+
* `closeButton`, `visibleToasts`, `theme`, etc. to tune behaviour.
|
|
18
|
+
* 2. `toast()` — call from anywhere to enqueue. Sub-APIs:
|
|
8
19
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
20
|
+
* toast('Title')
|
|
21
|
+
* toast.success('Saved')
|
|
22
|
+
* toast.error('Failed', { description: '...' })
|
|
23
|
+
* toast.warning('Heads up')
|
|
24
|
+
* toast.info('Just so you know')
|
|
25
|
+
* toast.message('Plain')
|
|
26
|
+
* toast.loading('Working…')
|
|
27
|
+
* toast.promise(myPromise, {
|
|
28
|
+
* loading: 'Saving…',
|
|
29
|
+
* success: (data) => `Saved ${data.name}`,
|
|
30
|
+
* error: (err) => `Failed: ${err.message}`,
|
|
31
|
+
* })
|
|
32
|
+
* toast.custom((id) => <MyJSX onDismiss={() => toast.dismiss(id)} />)
|
|
33
|
+
* toast.dismiss() // dismiss all
|
|
34
|
+
* toast.dismiss(id) // dismiss one
|
|
12
35
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
36
|
+
* Backward-compat shims for the previous imperative MiHCM API are below —
|
|
37
|
+
* `ToastProvider` is a deprecated alias for `Toaster`; the legacy
|
|
38
|
+
* `Toast`/`ToastTitle`/`ToastDescription`/`ToastAction`/`ToastClose`
|
|
39
|
+
* sub-components remain so any pre-sonner consumer keeps rendering.
|
|
16
40
|
*
|
|
17
41
|
* Wiki: docs/components/Toast.md
|
|
18
42
|
*/
|
|
19
|
-
import { type ButtonHTMLAttributes, type
|
|
43
|
+
import { type ButtonHTMLAttributes, type ComponentProps, type HTMLAttributes } from 'react';
|
|
44
|
+
import { Toaster as SonnerToaster, toast as sonnerToast } from 'sonner';
|
|
20
45
|
import { type VariantProps } from 'class-variance-authority';
|
|
46
|
+
/**
|
|
47
|
+
* Imperative toast API. Backed by sonner.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* toast('Saved');
|
|
51
|
+
* toast.success('Profile updated');
|
|
52
|
+
* toast.error('Network error', { description: 'Retrying in 5s…' });
|
|
53
|
+
* toast.loading('Compiling…');
|
|
54
|
+
* toast.promise(savePost(), {
|
|
55
|
+
* loading: 'Saving…',
|
|
56
|
+
* success: 'Saved!',
|
|
57
|
+
* error: 'Save failed',
|
|
58
|
+
* });
|
|
59
|
+
* toast.dismiss(id);
|
|
60
|
+
*/
|
|
61
|
+
export declare const toast: typeof sonnerToast;
|
|
62
|
+
export type { ExternalToast, ToastT, ToasterProps } from 'sonner';
|
|
63
|
+
export interface MihcmToasterProps extends ComponentProps<typeof SonnerToaster> {
|
|
64
|
+
/** Default visual treatment for toasts without an explicit appearance. */
|
|
65
|
+
appearance?: 'soft' | 'solid' | 'outline' | 'minimal';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Mount `<Toaster />` once near the root of your app (e.g. in `layout.tsx`).
|
|
69
|
+
* Forwards every prop to sonner's `<Toaster />`; the only addition is the
|
|
70
|
+
* MiHCM token styling applied via `toastOptions.classNames`.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // app/layout.tsx
|
|
74
|
+
* import { Toaster } from '@mihcm/ui/Toast';
|
|
75
|
+
* <Toaster richColors closeButton position="bottom-right" />
|
|
76
|
+
*/
|
|
77
|
+
export declare function Toaster({ position, expand, richColors, closeButton, visibleToasts, duration, theme, toastOptions, className, appearance, ...rest }: MihcmToasterProps): import("react/jsx-runtime").JSX.Element;
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated Use `<Toaster />` instead. Kept as a wrapping component for
|
|
80
|
+
* pre-sonner consumers — renders children, then mounts the Toaster portal.
|
|
81
|
+
*/
|
|
82
|
+
export declare function ToastProvider({ children, ...rest }: MihcmToasterProps & {
|
|
83
|
+
children?: React.ReactNode;
|
|
84
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
85
|
+
export type ToastProviderProps = MihcmToasterProps & {
|
|
86
|
+
children?: React.ReactNode;
|
|
87
|
+
};
|
|
21
88
|
export declare const toastVariants: (props?: ({
|
|
22
89
|
variant?: "accent" | "default" | "success" | "warning" | "error" | null | undefined;
|
|
23
90
|
appearance?: "outline" | "solid" | "soft" | "minimal" | null | undefined;
|
|
24
91
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
25
92
|
export type ToastVariant = NonNullable<VariantProps<typeof toastVariants>['variant']>;
|
|
26
93
|
export type ToastAppearance = NonNullable<VariantProps<typeof toastVariants>['appearance']>;
|
|
94
|
+
export interface ToastProps extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof toastVariants> {
|
|
95
|
+
className?: string;
|
|
96
|
+
}
|
|
97
|
+
/** @deprecated Sonner renders rows internally. Kept for stand-alone use. */
|
|
98
|
+
export declare const Toast: import("react").ForwardRefExoticComponent<ToastProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
99
|
+
/** @deprecated Use sonner's title slot in `toast()` instead. */
|
|
100
|
+
export declare const ToastTitle: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
|
|
101
|
+
/** @deprecated Use sonner's `description` option in `toast()` instead. */
|
|
102
|
+
export declare const ToastDescription: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & import("react").RefAttributes<HTMLDivElement>>;
|
|
103
|
+
/** @deprecated Use sonner's `action` option in `toast()` instead. */
|
|
104
|
+
export declare const ToastAction: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & import("react").RefAttributes<HTMLButtonElement>>;
|
|
105
|
+
/** @deprecated `closeButton` prop on `<Toaster />` renders this automatically. */
|
|
106
|
+
export declare const ToastClose: import("react").ForwardRefExoticComponent<ButtonHTMLAttributes<HTMLButtonElement> & import("react").RefAttributes<HTMLButtonElement>>;
|
|
107
|
+
/**
|
|
108
|
+
* Legacy `ToastData` shape kept for typing in any pre-sonner consumer.
|
|
109
|
+
* @deprecated Sonner uses its own `ExternalToast` shape; prefer that.
|
|
110
|
+
*/
|
|
27
111
|
export interface ToastData {
|
|
28
112
|
id: string;
|
|
29
113
|
title: string;
|
|
@@ -35,38 +119,14 @@ export interface ToastData {
|
|
|
35
119
|
label: string;
|
|
36
120
|
onClick: () => void;
|
|
37
121
|
};
|
|
38
|
-
icon?: ReactNode;
|
|
39
|
-
}
|
|
40
|
-
export interface ToastProps extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof toastVariants> {
|
|
41
|
-
className?: string;
|
|
122
|
+
icon?: React.ReactNode;
|
|
42
123
|
}
|
|
43
124
|
export interface ToastTitleProps extends HTMLAttributes<HTMLDivElement> {
|
|
44
|
-
className?: string;
|
|
45
125
|
}
|
|
46
126
|
export interface ToastDescriptionProps extends HTMLAttributes<HTMLDivElement> {
|
|
47
|
-
className?: string;
|
|
48
127
|
}
|
|
49
128
|
export interface ToastActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
50
|
-
className?: string;
|
|
51
129
|
}
|
|
52
130
|
export interface ToastCloseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
53
|
-
className?: string;
|
|
54
131
|
}
|
|
55
|
-
export interface ToastProviderProps {
|
|
56
|
-
children: ReactNode;
|
|
57
|
-
/** Maximum number of visible toasts. @default 5 */
|
|
58
|
-
max?: number;
|
|
59
|
-
/** Screen corner for the toast stack. */
|
|
60
|
-
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
61
|
-
/** Default visual treatment for toasts without an explicit appearance. */
|
|
62
|
-
appearance?: ToastAppearance;
|
|
63
|
-
}
|
|
64
|
-
/** Imperative API — call from anywhere to show a toast. */
|
|
65
|
-
export declare function toast(data: Omit<ToastData, 'id'>): string;
|
|
66
|
-
export declare const Toast: import("react").ForwardRefExoticComponent<ToastProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
67
|
-
export declare const ToastTitle: import("react").ForwardRefExoticComponent<ToastTitleProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
68
|
-
export declare const ToastDescription: import("react").ForwardRefExoticComponent<ToastDescriptionProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
69
|
-
export declare const ToastAction: import("react").ForwardRefExoticComponent<ToastActionProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
70
|
-
export declare const ToastClose: import("react").ForwardRefExoticComponent<ToastCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
71
|
-
export declare function ToastProvider({ children, max, position, appearance }: ToastProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
72
132
|
//# sourceMappingURL=Toast.d.ts.map
|
package/dist/Toast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../src/Toast.tsx"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../src/Toast.tsx"],"names":[],"mappings":"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"}
|
package/dist/Toast.js
CHANGED
|
@@ -1,31 +1,157 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
4
|
* Toast (web variant — React DOM).
|
|
5
5
|
*
|
|
6
|
+
* Built on top of [sonner](https://sonner.emilkowal.ski/) so MiHCM apps get
|
|
7
|
+
* every feature the upstream library ships — promise toasts, loading state,
|
|
8
|
+
* action + cancel buttons, swipe-to-dismiss, hover-to-expand stacks, rich
|
|
9
|
+
* colours per type, theme switching, full a11y wiring — without forking.
|
|
10
|
+
*
|
|
11
|
+
* Surface is themed with MiHCM tokens (`bg-card`, `border-border`,
|
|
12
|
+
* `text-foreground`, `text-success-foreground`, etc.) via sonner's
|
|
13
|
+
* `toastOptions.classNames` so the toasts read as part of the rest of the
|
|
14
|
+
* design system regardless of light/dark mode or custom `colorScheme`s.
|
|
15
|
+
*
|
|
6
16
|
* Two-part API:
|
|
7
|
-
* 1. `<
|
|
8
|
-
* in a
|
|
9
|
-
*
|
|
17
|
+
* 1. `<Toaster />` — mount once near the app root. It renders the queue
|
|
18
|
+
* in a portal at the chosen `position`. Pass `richColors`, `expand`,
|
|
19
|
+
* `closeButton`, `visibleToasts`, `theme`, etc. to tune behaviour.
|
|
20
|
+
* 2. `toast()` — call from anywhere to enqueue. Sub-APIs:
|
|
10
21
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
22
|
+
* toast('Title')
|
|
23
|
+
* toast.success('Saved')
|
|
24
|
+
* toast.error('Failed', { description: '...' })
|
|
25
|
+
* toast.warning('Heads up')
|
|
26
|
+
* toast.info('Just so you know')
|
|
27
|
+
* toast.message('Plain')
|
|
28
|
+
* toast.loading('Working…')
|
|
29
|
+
* toast.promise(myPromise, {
|
|
30
|
+
* loading: 'Saving…',
|
|
31
|
+
* success: (data) => `Saved ${data.name}`,
|
|
32
|
+
* error: (err) => `Failed: ${err.message}`,
|
|
33
|
+
* })
|
|
34
|
+
* toast.custom((id) => <MyJSX onDismiss={() => toast.dismiss(id)} />)
|
|
35
|
+
* toast.dismiss() // dismiss all
|
|
36
|
+
* toast.dismiss(id) // dismiss one
|
|
14
37
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
38
|
+
* Backward-compat shims for the previous imperative MiHCM API are below —
|
|
39
|
+
* `ToastProvider` is a deprecated alias for `Toaster`; the legacy
|
|
40
|
+
* `Toast`/`ToastTitle`/`ToastDescription`/`ToastAction`/`ToastClose`
|
|
41
|
+
* sub-components remain so any pre-sonner consumer keeps rendering.
|
|
18
42
|
*
|
|
19
43
|
* Wiki: docs/components/Toast.md
|
|
20
44
|
*/
|
|
21
|
-
import { forwardRef,
|
|
45
|
+
import { forwardRef, } from 'react';
|
|
46
|
+
import { Toaster as SonnerToaster, toast as sonnerToast } from 'sonner';
|
|
22
47
|
import { cva } from 'class-variance-authority';
|
|
23
48
|
import { cn } from './internal/cn.js';
|
|
24
49
|
/* -------------------------------------------------------------------------- */
|
|
25
|
-
/*
|
|
50
|
+
/* Re-exports — the canonical sonner API */
|
|
51
|
+
/* -------------------------------------------------------------------------- */
|
|
52
|
+
/**
|
|
53
|
+
* Imperative toast API. Backed by sonner.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* toast('Saved');
|
|
57
|
+
* toast.success('Profile updated');
|
|
58
|
+
* toast.error('Network error', { description: 'Retrying in 5s…' });
|
|
59
|
+
* toast.loading('Compiling…');
|
|
60
|
+
* toast.promise(savePost(), {
|
|
61
|
+
* loading: 'Saving…',
|
|
62
|
+
* success: 'Saved!',
|
|
63
|
+
* error: 'Save failed',
|
|
64
|
+
* });
|
|
65
|
+
* toast.dismiss(id);
|
|
66
|
+
*/
|
|
67
|
+
export const toast = sonnerToast;
|
|
68
|
+
/**
|
|
69
|
+
* Mount `<Toaster />` once near the root of your app (e.g. in `layout.tsx`).
|
|
70
|
+
* Forwards every prop to sonner's `<Toaster />`; the only addition is the
|
|
71
|
+
* MiHCM token styling applied via `toastOptions.classNames`.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // app/layout.tsx
|
|
75
|
+
* import { Toaster } from '@mihcm/ui/Toast';
|
|
76
|
+
* <Toaster richColors closeButton position="bottom-right" />
|
|
77
|
+
*/
|
|
78
|
+
export function Toaster({ position = 'bottom-right', expand = false, richColors = false, closeButton = false, visibleToasts = 3, duration = 5000, theme = 'system', toastOptions, className, appearance, ...rest }) {
|
|
79
|
+
void appearance; /* reserved — sonner already handles visual variants via richColors */
|
|
80
|
+
/*
|
|
81
|
+
* Sonner injects its own CSS that uses these variables to colour each
|
|
82
|
+
* row. By mapping them to MiHCM design-system tokens here, the surface,
|
|
83
|
+
* border, and type tints inherit the brand palette AND light/dark mode
|
|
84
|
+
* flips automatically — no need to fight specificity with Tailwind.
|
|
85
|
+
*/
|
|
86
|
+
const sonnerCssVars = {
|
|
87
|
+
'--normal-bg': 'var(--color-card)',
|
|
88
|
+
'--normal-text': 'var(--color-card-foreground)',
|
|
89
|
+
'--normal-border': 'var(--color-border)',
|
|
90
|
+
'--success-bg': 'var(--color-success-50, color-mix(in oklab, var(--color-success) 12%, var(--color-card)))',
|
|
91
|
+
'--success-text': 'var(--color-success-700, var(--color-success))',
|
|
92
|
+
'--success-border': 'var(--color-success)',
|
|
93
|
+
'--error-bg': 'var(--color-destructive-50, color-mix(in oklab, var(--color-destructive) 12%, var(--color-card)))',
|
|
94
|
+
'--error-text': 'var(--color-destructive-700, var(--color-destructive))',
|
|
95
|
+
'--error-border': 'var(--color-destructive)',
|
|
96
|
+
'--warning-bg': 'var(--color-warning-50, color-mix(in oklab, var(--color-warning) 12%, var(--color-card)))',
|
|
97
|
+
'--warning-text': 'var(--color-warning-700, var(--color-warning))',
|
|
98
|
+
'--warning-border': 'var(--color-warning)',
|
|
99
|
+
'--info-bg': 'var(--color-primary-50, color-mix(in oklab, var(--color-primary) 12%, var(--color-card)))',
|
|
100
|
+
'--info-text': 'var(--color-primary-700, var(--color-primary))',
|
|
101
|
+
'--info-border': 'var(--color-primary)',
|
|
102
|
+
};
|
|
103
|
+
return (_jsx(SonnerToaster, { position: position, expand: expand, richColors: richColors, closeButton: closeButton, visibleToasts: visibleToasts, duration: duration, theme: theme, className: cn('mihcm-toaster', className), style: { ...sonnerCssVars, ...(rest.style ?? {}) }, toastOptions: {
|
|
104
|
+
...toastOptions,
|
|
105
|
+
classNames: {
|
|
106
|
+
/*
|
|
107
|
+
* Surface — the toast row itself. Polished default look:
|
|
108
|
+
* - bg-card / text-card-foreground (theme-aware)
|
|
109
|
+
* - shadow-mi-modal for a soft floating elevation
|
|
110
|
+
* - rounded-xl + slightly bigger padding for a modern feel
|
|
111
|
+
* - ring-1 ring-border for crisp edge separation in light mode
|
|
112
|
+
* (the border-border on its own can look washed out)
|
|
113
|
+
* - max-width capped so long messages don't sprawl
|
|
114
|
+
*/
|
|
115
|
+
toast: cn('group toast pointer-events-auto flex w-full items-start gap-3 rounded-xl border border-border bg-card p-4 pr-12', 'text-card-foreground shadow-mi-modal ring-1 ring-border/40', toastOptions?.classNames?.toast),
|
|
116
|
+
title: cn('text-sm font-semibold leading-snug text-foreground', toastOptions?.classNames?.title),
|
|
117
|
+
description: cn('text-sm text-muted-foreground leading-snug', toastOptions?.classNames?.description),
|
|
118
|
+
actionButton: cn('inline-flex shrink-0 items-center rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', toastOptions?.classNames?.actionButton),
|
|
119
|
+
cancelButton: cn('inline-flex shrink-0 items-center rounded-md border border-border bg-transparent px-3 py-1 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', toastOptions?.classNames?.cancelButton),
|
|
120
|
+
closeButton: cn('absolute right-2 top-2 grid size-6 place-items-center rounded-md border border-transparent bg-transparent text-foreground/60 transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', toastOptions?.classNames?.closeButton),
|
|
121
|
+
icon: cn('mt-0.5 shrink-0 [&_svg]:size-5', toastOptions?.classNames?.icon),
|
|
122
|
+
/*
|
|
123
|
+
* Per-type styling — sonner adds `data-type="success|error|…"`
|
|
124
|
+
* to the toast root. Use richColors-like surface treatment so
|
|
125
|
+
* tone is unmistakeable even without sonner's own richColors
|
|
126
|
+
* (which forces hard-coded colours).
|
|
127
|
+
*/
|
|
128
|
+
default: cn('border-border bg-card text-card-foreground', toastOptions?.classNames?.default),
|
|
129
|
+
success: cn('border-success/30 bg-success/10 text-foreground [&_[data-icon]]:text-success', toastOptions?.classNames?.success),
|
|
130
|
+
error: cn('border-destructive/30 bg-destructive/10 text-foreground [&_[data-icon]]:text-destructive', toastOptions?.classNames?.error),
|
|
131
|
+
warning: cn('border-warning/30 bg-warning/10 text-foreground [&_[data-icon]]:text-warning', toastOptions?.classNames?.warning),
|
|
132
|
+
info: cn('border-primary/30 bg-primary/10 text-foreground [&_[data-icon]]:text-primary', toastOptions?.classNames?.info),
|
|
133
|
+
loading: cn('border-border bg-card text-foreground', toastOptions?.classNames?.loading),
|
|
134
|
+
},
|
|
135
|
+
}, ...rest }));
|
|
136
|
+
}
|
|
137
|
+
/* -------------------------------------------------------------------------- */
|
|
138
|
+
/* Backward-compat layer */
|
|
139
|
+
/* */
|
|
140
|
+
/* The previous MiHCM Toast API exported `ToastProvider`, `Toast`, */
|
|
141
|
+
/* `ToastTitle`, `ToastDescription`, `ToastAction`, `ToastClose`. Keep them */
|
|
142
|
+
/* as thin shims so existing call sites stay green. New code should import */
|
|
143
|
+
/* `Toaster` + `toast` only. */
|
|
26
144
|
/* -------------------------------------------------------------------------- */
|
|
27
|
-
|
|
28
|
-
|
|
145
|
+
/**
|
|
146
|
+
* @deprecated Use `<Toaster />` instead. Kept as a wrapping component for
|
|
147
|
+
* pre-sonner consumers — renders children, then mounts the Toaster portal.
|
|
148
|
+
*/
|
|
149
|
+
export function ToastProvider({ children, ...rest }) {
|
|
150
|
+
return (_jsxs(_Fragment, { children: [children, _jsx(Toaster, { ...rest })] }));
|
|
151
|
+
}
|
|
152
|
+
/* The original `toastVariants` CVA — kept for any consumer that referenced
|
|
153
|
+
* its variant union types or used it to style a stand-alone Toast row. */
|
|
154
|
+
export const toastVariants = cva('pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-lg border p-4 shadow-lg transition-all duration-300 ease-out', {
|
|
29
155
|
variants: {
|
|
30
156
|
variant: {
|
|
31
157
|
default: 'border-border bg-card text-foreground',
|
|
@@ -55,140 +181,24 @@ export const toastVariants = cva('pointer-events-auto relative flex w-full items
|
|
|
55
181
|
],
|
|
56
182
|
defaultVariants: { variant: 'default', appearance: 'soft' },
|
|
57
183
|
});
|
|
58
|
-
|
|
59
|
-
function getStore() {
|
|
60
|
-
const g = globalThis;
|
|
61
|
-
if (!g[STORE_KEY]) {
|
|
62
|
-
g[STORE_KEY] = { toasts: [], listeners: new Set(), viewports: new Set(), counter: 0, viewportCounter: 0 };
|
|
63
|
-
}
|
|
64
|
-
return g[STORE_KEY];
|
|
65
|
-
}
|
|
66
|
-
function notify() {
|
|
67
|
-
for (const l of getStore().listeners)
|
|
68
|
-
l();
|
|
69
|
-
}
|
|
70
|
-
function getSnapshot() {
|
|
71
|
-
return getStore().toasts;
|
|
72
|
-
}
|
|
73
|
-
function getViewportSnapshot() {
|
|
74
|
-
return Array.from(getStore().viewports).at(-1);
|
|
75
|
-
}
|
|
76
|
-
function subscribe(listener) {
|
|
77
|
-
const store = getStore();
|
|
78
|
-
store.listeners.add(listener);
|
|
79
|
-
return () => store.listeners.delete(listener);
|
|
80
|
-
}
|
|
81
|
-
/** Imperative API — call from anywhere to show a toast. */
|
|
82
|
-
export function toast(data) {
|
|
83
|
-
const store = getStore();
|
|
84
|
-
const id = `toast-${++store.counter}-${Date.now()}`;
|
|
85
|
-
const entry = { id, ...data };
|
|
86
|
-
store.toasts = [...store.toasts, entry];
|
|
87
|
-
notify();
|
|
88
|
-
return id;
|
|
89
|
-
}
|
|
90
|
-
function dismiss(id) {
|
|
91
|
-
const store = getStore();
|
|
92
|
-
store.toasts = store.toasts.filter((t) => t.id !== id);
|
|
93
|
-
notify();
|
|
94
|
-
}
|
|
95
|
-
/* -------------------------------------------------------------------------- */
|
|
96
|
-
/* Sub-components */
|
|
97
|
-
/* -------------------------------------------------------------------------- */
|
|
98
|
-
const ACCENT = {
|
|
99
|
-
default: 'border-l-border',
|
|
100
|
-
success: 'border-l-success',
|
|
101
|
-
error: 'border-l-destructive',
|
|
102
|
-
warning: 'border-l-warning',
|
|
103
|
-
accent: 'border-l-accent',
|
|
104
|
-
};
|
|
184
|
+
/** @deprecated Sonner renders rows internally. Kept for stand-alone use. */
|
|
105
185
|
export const Toast = forwardRef(function Toast({ className, variant = 'default', appearance = 'soft', children, ...props }, ref) {
|
|
106
|
-
return (_jsx("div", { ref: ref, className: cn(toastVariants({ variant, appearance }),
|
|
186
|
+
return (_jsx("div", { ref: ref, className: cn(toastVariants({ variant, appearance }), className), ...props, children: children }));
|
|
107
187
|
});
|
|
188
|
+
/** @deprecated Use sonner's title slot in `toast()` instead. */
|
|
108
189
|
export const ToastTitle = forwardRef(function ToastTitle({ className, ...props }, ref) {
|
|
109
190
|
return _jsx("div", { ref: ref, className: cn('text-sm font-semibold', className), ...props });
|
|
110
191
|
});
|
|
192
|
+
/** @deprecated Use sonner's `description` option in `toast()` instead. */
|
|
111
193
|
export const ToastDescription = forwardRef(function ToastDescription({ className, ...props }, ref) {
|
|
112
194
|
return _jsx("div", { ref: ref, className: cn('text-sm text-muted-foreground', className), ...props });
|
|
113
195
|
});
|
|
196
|
+
/** @deprecated Use sonner's `action` option in `toast()` instead. */
|
|
114
197
|
export const ToastAction = forwardRef(function ToastAction({ className, ...props }, ref) {
|
|
115
|
-
return (_jsx("button", { ref: ref, type: "button", className: cn('inline-flex shrink-0 items-center rounded-md border border-border bg-transparent px-3 py-1 '
|
|
116
|
-
'text-sm font-medium transition-colors duration-150 hover:bg-muted focus-visible:outline-none ' +
|
|
117
|
-
'focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', className), ...props }));
|
|
198
|
+
return (_jsx("button", { ref: ref, type: "button", className: cn('inline-flex shrink-0 items-center rounded-md border border-border bg-transparent px-3 py-1 text-sm font-medium transition-colors duration-150 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', className), ...props }));
|
|
118
199
|
});
|
|
200
|
+
/** @deprecated `closeButton` prop on `<Toaster />` renders this automatically. */
|
|
119
201
|
export const ToastClose = forwardRef(function ToastClose({ className, ...props }, ref) {
|
|
120
|
-
return (_jsx("button", { ref: ref, type: "button", "aria-label": "Close", className: cn('absolute right-2 top-2 rounded-md p-1 text-foreground/50 transition-opacity hover:text-foreground '
|
|
121
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', className), ...props, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }) }));
|
|
202
|
+
return (_jsx("button", { ref: ref, type: "button", "aria-label": "Close", className: cn('absolute right-2 top-2 rounded-md p-1 text-foreground/50 transition-opacity hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', className), ...props, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }) }));
|
|
122
203
|
});
|
|
123
|
-
/* -------------------------------------------------------------------------- */
|
|
124
|
-
/* Individual toast item (manages timer + animation) */
|
|
125
|
-
/* -------------------------------------------------------------------------- */
|
|
126
|
-
const DEFAULT_DURATION = 5000;
|
|
127
|
-
function ToastItem({ data, onDismiss, appearance, }) {
|
|
128
|
-
const [visible, setVisible] = useState(false);
|
|
129
|
-
const [exiting, setExiting] = useState(false);
|
|
130
|
-
const timerRef = useRef(undefined);
|
|
131
|
-
const remainingRef = useRef(data.duration ?? DEFAULT_DURATION);
|
|
132
|
-
// eslint-disable-next-line react-hooks/purity -- initial value only, never re-read during render
|
|
133
|
-
const startRef = useRef(Date.now());
|
|
134
|
-
const startTimer = useCallback(() => {
|
|
135
|
-
startRef.current = Date.now();
|
|
136
|
-
timerRef.current = setTimeout(() => {
|
|
137
|
-
setExiting(true);
|
|
138
|
-
setTimeout(() => onDismiss(data.id), 300);
|
|
139
|
-
}, remainingRef.current);
|
|
140
|
-
}, [data.id, onDismiss]);
|
|
141
|
-
const pauseTimer = useCallback(() => {
|
|
142
|
-
if (timerRef.current) {
|
|
143
|
-
clearTimeout(timerRef.current);
|
|
144
|
-
remainingRef.current -= Date.now() - startRef.current;
|
|
145
|
-
if (remainingRef.current < 0)
|
|
146
|
-
remainingRef.current = 0;
|
|
147
|
-
}
|
|
148
|
-
}, []);
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
// Trigger entry animation on next frame
|
|
151
|
-
const raf = requestAnimationFrame(() => setVisible(true));
|
|
152
|
-
startTimer();
|
|
153
|
-
return () => {
|
|
154
|
-
cancelAnimationFrame(raf);
|
|
155
|
-
if (timerRef.current)
|
|
156
|
-
clearTimeout(timerRef.current);
|
|
157
|
-
};
|
|
158
|
-
}, [startTimer]);
|
|
159
|
-
const handleClose = () => {
|
|
160
|
-
if (timerRef.current)
|
|
161
|
-
clearTimeout(timerRef.current);
|
|
162
|
-
setExiting(true);
|
|
163
|
-
setTimeout(() => onDismiss(data.id), 300);
|
|
164
|
-
};
|
|
165
|
-
const isError = data.variant === 'error';
|
|
166
|
-
return (_jsx("div", { role: "status", "aria-live": isError ? 'assertive' : 'polite', className: cn('transform transition-all duration-300', visible && !exiting ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'), onMouseEnter: pauseTimer, onMouseLeave: startTimer, children: _jsxs(Toast, { variant: data.variant, appearance: data.appearance ?? appearance, children: [data.icon ? _jsx("div", { className: "mt-0.5 shrink-0 text-muted-foreground", children: data.icon }) : null, _jsxs("div", { className: "flex-1 space-y-1 pr-6", children: [_jsx(ToastTitle, { children: data.title }), data.description !== undefined && (_jsx(ToastDescription, { children: data.description })), data.action !== undefined && (_jsx("div", { className: "mt-2", children: _jsx(ToastAction, { onClick: data.action.onClick, children: data.action.label }) }))] }), _jsx(ToastClose, { onClick: handleClose })] }) }));
|
|
167
|
-
}
|
|
168
|
-
/* -------------------------------------------------------------------------- */
|
|
169
|
-
/* ToastProvider */
|
|
170
|
-
/* -------------------------------------------------------------------------- */
|
|
171
|
-
const POSITION_CLASS = {
|
|
172
|
-
'bottom-right': 'bottom-4 right-4',
|
|
173
|
-
'bottom-left': 'bottom-4 left-4',
|
|
174
|
-
'top-right': 'right-4 top-4',
|
|
175
|
-
'top-left': 'left-4 top-4',
|
|
176
|
-
};
|
|
177
|
-
export function ToastProvider({ children, max = 5, position = 'bottom-right', appearance = 'soft' }) {
|
|
178
|
-
const [viewportId] = useState(() => ++getStore().viewportCounter);
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
const store = getStore();
|
|
181
|
-
store.viewports.add(viewportId);
|
|
182
|
-
notify();
|
|
183
|
-
return () => {
|
|
184
|
-
store.viewports.delete(viewportId);
|
|
185
|
-
notify();
|
|
186
|
-
};
|
|
187
|
-
}, [viewportId]);
|
|
188
|
-
const items = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
189
|
-
const activeViewportId = useSyncExternalStore(subscribe, getViewportSnapshot, () => undefined);
|
|
190
|
-
const visible = items.slice(-max);
|
|
191
|
-
const isActiveViewport = viewportId === activeViewportId;
|
|
192
|
-
return (_jsxs(_Fragment, { children: [children, isActiveViewport ? (_jsx("div", { "aria-label": "Notifications", className: cn('pointer-events-none fixed z-[100] flex max-w-[420px] flex-col gap-2', POSITION_CLASS[position]), children: visible.map((t) => (_jsx(ToastItem, { data: t, onDismiss: dismiss, appearance: appearance }, t.id))) })) : null] }));
|
|
193
|
-
}
|
|
194
204
|
//# sourceMappingURL=Toast.js.map
|
package/dist/Toast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toast.js","sourceRoot":"","sources":["../src/Toast.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb
|
|
1
|
+
{"version":3,"file":"Toast.js","sourceRoot":"","sources":["../src/Toast.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mihcm/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Universal primitives (Button, Input, ...) for React, Next.js, and React Native. Tailwind 4 + NativeWind v5.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -454,11 +454,12 @@
|
|
|
454
454
|
"react-resizable-panels": "4.11.1",
|
|
455
455
|
"react-select": "^5.10.2",
|
|
456
456
|
"recharts": "^3.8.1",
|
|
457
|
+
"sonner": "2.0.7",
|
|
457
458
|
"tailwind-merge": "^3.6.0",
|
|
458
459
|
"tailwind-variants": "3.2.2",
|
|
459
460
|
"vaul": "1.1.2",
|
|
460
|
-
"@mihcm/theme": "0.2.
|
|
461
|
-
"@mihcm/tokens": "0.6.
|
|
461
|
+
"@mihcm/theme": "0.2.11",
|
|
462
|
+
"@mihcm/tokens": "0.6.4"
|
|
462
463
|
},
|
|
463
464
|
"peerDependencies": {
|
|
464
465
|
"nativewind": ">=4.1.0",
|
|
@@ -487,7 +488,7 @@
|
|
|
487
488
|
"react-dom": "19.2.6",
|
|
488
489
|
"react-native": "0.85.3",
|
|
489
490
|
"typescript": "^6.0.3",
|
|
490
|
-
"@mihcm/eslint-config": "0.1.
|
|
491
|
+
"@mihcm/eslint-config": "0.1.4"
|
|
491
492
|
},
|
|
492
493
|
"scripts": {
|
|
493
494
|
"build": "tsc -p tsconfig.json",
|
package/src/Toast.tsx
CHANGED
|
@@ -3,42 +3,250 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Toast (web variant — React DOM).
|
|
5
5
|
*
|
|
6
|
+
* Built on top of [sonner](https://sonner.emilkowal.ski/) so MiHCM apps get
|
|
7
|
+
* every feature the upstream library ships — promise toasts, loading state,
|
|
8
|
+
* action + cancel buttons, swipe-to-dismiss, hover-to-expand stacks, rich
|
|
9
|
+
* colours per type, theme switching, full a11y wiring — without forking.
|
|
10
|
+
*
|
|
11
|
+
* Surface is themed with MiHCM tokens (`bg-card`, `border-border`,
|
|
12
|
+
* `text-foreground`, `text-success-foreground`, etc.) via sonner's
|
|
13
|
+
* `toastOptions.classNames` so the toasts read as part of the rest of the
|
|
14
|
+
* design system regardless of light/dark mode or custom `colorScheme`s.
|
|
15
|
+
*
|
|
6
16
|
* Two-part API:
|
|
7
|
-
* 1. `<
|
|
8
|
-
* in a
|
|
9
|
-
*
|
|
17
|
+
* 1. `<Toaster />` — mount once near the app root. It renders the queue
|
|
18
|
+
* in a portal at the chosen `position`. Pass `richColors`, `expand`,
|
|
19
|
+
* `closeButton`, `visibleToasts`, `theme`, etc. to tune behaviour.
|
|
20
|
+
* 2. `toast()` — call from anywhere to enqueue. Sub-APIs:
|
|
10
21
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
22
|
+
* toast('Title')
|
|
23
|
+
* toast.success('Saved')
|
|
24
|
+
* toast.error('Failed', { description: '...' })
|
|
25
|
+
* toast.warning('Heads up')
|
|
26
|
+
* toast.info('Just so you know')
|
|
27
|
+
* toast.message('Plain')
|
|
28
|
+
* toast.loading('Working…')
|
|
29
|
+
* toast.promise(myPromise, {
|
|
30
|
+
* loading: 'Saving…',
|
|
31
|
+
* success: (data) => `Saved ${data.name}`,
|
|
32
|
+
* error: (err) => `Failed: ${err.message}`,
|
|
33
|
+
* })
|
|
34
|
+
* toast.custom((id) => <MyJSX onDismiss={() => toast.dismiss(id)} />)
|
|
35
|
+
* toast.dismiss() // dismiss all
|
|
36
|
+
* toast.dismiss(id) // dismiss one
|
|
14
37
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
38
|
+
* Backward-compat shims for the previous imperative MiHCM API are below —
|
|
39
|
+
* `ToastProvider` is a deprecated alias for `Toaster`; the legacy
|
|
40
|
+
* `Toast`/`ToastTitle`/`ToastDescription`/`ToastAction`/`ToastClose`
|
|
41
|
+
* sub-components remain so any pre-sonner consumer keeps rendering.
|
|
18
42
|
*
|
|
19
43
|
* Wiki: docs/components/Toast.md
|
|
20
44
|
*/
|
|
21
45
|
import {
|
|
22
46
|
forwardRef,
|
|
23
|
-
useCallback,
|
|
24
|
-
useEffect,
|
|
25
|
-
useRef,
|
|
26
|
-
useState,
|
|
27
|
-
useSyncExternalStore,
|
|
28
47
|
type ButtonHTMLAttributes,
|
|
48
|
+
type ComponentProps,
|
|
29
49
|
type HTMLAttributes,
|
|
30
|
-
type ReactNode,
|
|
31
50
|
} from 'react';
|
|
51
|
+
import { Toaster as SonnerToaster, toast as sonnerToast } from 'sonner';
|
|
32
52
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
33
53
|
import { cn } from './internal/cn.js';
|
|
34
54
|
|
|
35
55
|
/* -------------------------------------------------------------------------- */
|
|
36
|
-
/*
|
|
56
|
+
/* Re-exports — the canonical sonner API */
|
|
57
|
+
/* -------------------------------------------------------------------------- */
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Imperative toast API. Backed by sonner.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* toast('Saved');
|
|
64
|
+
* toast.success('Profile updated');
|
|
65
|
+
* toast.error('Network error', { description: 'Retrying in 5s…' });
|
|
66
|
+
* toast.loading('Compiling…');
|
|
67
|
+
* toast.promise(savePost(), {
|
|
68
|
+
* loading: 'Saving…',
|
|
69
|
+
* success: 'Saved!',
|
|
70
|
+
* error: 'Save failed',
|
|
71
|
+
* });
|
|
72
|
+
* toast.dismiss(id);
|
|
73
|
+
*/
|
|
74
|
+
export const toast: typeof sonnerToast = sonnerToast;
|
|
75
|
+
|
|
76
|
+
export type { ExternalToast, ToastT, ToasterProps } from 'sonner';
|
|
77
|
+
|
|
78
|
+
/* -------------------------------------------------------------------------- */
|
|
79
|
+
/* Toaster — themed wrapper around sonner's <Toaster /> */
|
|
80
|
+
/* -------------------------------------------------------------------------- */
|
|
81
|
+
|
|
82
|
+
export interface MihcmToasterProps extends ComponentProps<typeof SonnerToaster> {
|
|
83
|
+
/** Default visual treatment for toasts without an explicit appearance. */
|
|
84
|
+
appearance?: 'soft' | 'solid' | 'outline' | 'minimal';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Mount `<Toaster />` once near the root of your app (e.g. in `layout.tsx`).
|
|
89
|
+
* Forwards every prop to sonner's `<Toaster />`; the only addition is the
|
|
90
|
+
* MiHCM token styling applied via `toastOptions.classNames`.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // app/layout.tsx
|
|
94
|
+
* import { Toaster } from '@mihcm/ui/Toast';
|
|
95
|
+
* <Toaster richColors closeButton position="bottom-right" />
|
|
96
|
+
*/
|
|
97
|
+
export function Toaster({
|
|
98
|
+
position = 'bottom-right',
|
|
99
|
+
expand = false,
|
|
100
|
+
richColors = false,
|
|
101
|
+
closeButton = false,
|
|
102
|
+
visibleToasts = 3,
|
|
103
|
+
duration = 5000,
|
|
104
|
+
theme = 'system',
|
|
105
|
+
toastOptions,
|
|
106
|
+
className,
|
|
107
|
+
appearance,
|
|
108
|
+
...rest
|
|
109
|
+
}: MihcmToasterProps) {
|
|
110
|
+
void appearance; /* reserved — sonner already handles visual variants via richColors */
|
|
111
|
+
|
|
112
|
+
/*
|
|
113
|
+
* Sonner injects its own CSS that uses these variables to colour each
|
|
114
|
+
* row. By mapping them to MiHCM design-system tokens here, the surface,
|
|
115
|
+
* border, and type tints inherit the brand palette AND light/dark mode
|
|
116
|
+
* flips automatically — no need to fight specificity with Tailwind.
|
|
117
|
+
*/
|
|
118
|
+
const sonnerCssVars = {
|
|
119
|
+
'--normal-bg': 'var(--color-card)',
|
|
120
|
+
'--normal-text': 'var(--color-card-foreground)',
|
|
121
|
+
'--normal-border': 'var(--color-border)',
|
|
122
|
+
'--success-bg': 'var(--color-success-50, color-mix(in oklab, var(--color-success) 12%, var(--color-card)))',
|
|
123
|
+
'--success-text': 'var(--color-success-700, var(--color-success))',
|
|
124
|
+
'--success-border': 'var(--color-success)',
|
|
125
|
+
'--error-bg': 'var(--color-destructive-50, color-mix(in oklab, var(--color-destructive) 12%, var(--color-card)))',
|
|
126
|
+
'--error-text': 'var(--color-destructive-700, var(--color-destructive))',
|
|
127
|
+
'--error-border': 'var(--color-destructive)',
|
|
128
|
+
'--warning-bg': 'var(--color-warning-50, color-mix(in oklab, var(--color-warning) 12%, var(--color-card)))',
|
|
129
|
+
'--warning-text': 'var(--color-warning-700, var(--color-warning))',
|
|
130
|
+
'--warning-border': 'var(--color-warning)',
|
|
131
|
+
'--info-bg': 'var(--color-primary-50, color-mix(in oklab, var(--color-primary) 12%, var(--color-card)))',
|
|
132
|
+
'--info-text': 'var(--color-primary-700, var(--color-primary))',
|
|
133
|
+
'--info-border': 'var(--color-primary)',
|
|
134
|
+
} as React.CSSProperties;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<SonnerToaster
|
|
138
|
+
position={position}
|
|
139
|
+
expand={expand}
|
|
140
|
+
richColors={richColors}
|
|
141
|
+
closeButton={closeButton}
|
|
142
|
+
visibleToasts={visibleToasts}
|
|
143
|
+
duration={duration}
|
|
144
|
+
theme={theme}
|
|
145
|
+
className={cn('mihcm-toaster', className)}
|
|
146
|
+
style={{ ...sonnerCssVars, ...(rest.style ?? {}) }}
|
|
147
|
+
toastOptions={{
|
|
148
|
+
...toastOptions,
|
|
149
|
+
classNames: {
|
|
150
|
+
/*
|
|
151
|
+
* Surface — the toast row itself. Polished default look:
|
|
152
|
+
* - bg-card / text-card-foreground (theme-aware)
|
|
153
|
+
* - shadow-mi-modal for a soft floating elevation
|
|
154
|
+
* - rounded-xl + slightly bigger padding for a modern feel
|
|
155
|
+
* - ring-1 ring-border for crisp edge separation in light mode
|
|
156
|
+
* (the border-border on its own can look washed out)
|
|
157
|
+
* - max-width capped so long messages don't sprawl
|
|
158
|
+
*/
|
|
159
|
+
toast: cn(
|
|
160
|
+
'group toast pointer-events-auto flex w-full items-start gap-3 rounded-xl border border-border bg-card p-4 pr-12',
|
|
161
|
+
'text-card-foreground shadow-mi-modal ring-1 ring-border/40',
|
|
162
|
+
toastOptions?.classNames?.toast,
|
|
163
|
+
),
|
|
164
|
+
title: cn('text-sm font-semibold leading-snug text-foreground', toastOptions?.classNames?.title),
|
|
165
|
+
description: cn(
|
|
166
|
+
'text-sm text-muted-foreground leading-snug',
|
|
167
|
+
toastOptions?.classNames?.description,
|
|
168
|
+
),
|
|
169
|
+
actionButton: cn(
|
|
170
|
+
'inline-flex shrink-0 items-center rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
171
|
+
toastOptions?.classNames?.actionButton,
|
|
172
|
+
),
|
|
173
|
+
cancelButton: cn(
|
|
174
|
+
'inline-flex shrink-0 items-center rounded-md border border-border bg-transparent px-3 py-1 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
175
|
+
toastOptions?.classNames?.cancelButton,
|
|
176
|
+
),
|
|
177
|
+
closeButton: cn(
|
|
178
|
+
'absolute right-2 top-2 grid size-6 place-items-center rounded-md border border-transparent bg-transparent text-foreground/60 transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
179
|
+
toastOptions?.classNames?.closeButton,
|
|
180
|
+
),
|
|
181
|
+
icon: cn(
|
|
182
|
+
'mt-0.5 shrink-0 [&_svg]:size-5',
|
|
183
|
+
toastOptions?.classNames?.icon,
|
|
184
|
+
),
|
|
185
|
+
/*
|
|
186
|
+
* Per-type styling — sonner adds `data-type="success|error|…"`
|
|
187
|
+
* to the toast root. Use richColors-like surface treatment so
|
|
188
|
+
* tone is unmistakeable even without sonner's own richColors
|
|
189
|
+
* (which forces hard-coded colours).
|
|
190
|
+
*/
|
|
191
|
+
default: cn('border-border bg-card text-card-foreground', toastOptions?.classNames?.default),
|
|
192
|
+
success: cn(
|
|
193
|
+
'border-success/30 bg-success/10 text-foreground [&_[data-icon]]:text-success',
|
|
194
|
+
toastOptions?.classNames?.success,
|
|
195
|
+
),
|
|
196
|
+
error: cn(
|
|
197
|
+
'border-destructive/30 bg-destructive/10 text-foreground [&_[data-icon]]:text-destructive',
|
|
198
|
+
toastOptions?.classNames?.error,
|
|
199
|
+
),
|
|
200
|
+
warning: cn(
|
|
201
|
+
'border-warning/30 bg-warning/10 text-foreground [&_[data-icon]]:text-warning',
|
|
202
|
+
toastOptions?.classNames?.warning,
|
|
203
|
+
),
|
|
204
|
+
info: cn(
|
|
205
|
+
'border-primary/30 bg-primary/10 text-foreground [&_[data-icon]]:text-primary',
|
|
206
|
+
toastOptions?.classNames?.info,
|
|
207
|
+
),
|
|
208
|
+
loading: cn(
|
|
209
|
+
'border-border bg-card text-foreground',
|
|
210
|
+
toastOptions?.classNames?.loading,
|
|
211
|
+
),
|
|
212
|
+
},
|
|
213
|
+
}}
|
|
214
|
+
{...rest}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* -------------------------------------------------------------------------- */
|
|
220
|
+
/* Backward-compat layer */
|
|
221
|
+
/* */
|
|
222
|
+
/* The previous MiHCM Toast API exported `ToastProvider`, `Toast`, */
|
|
223
|
+
/* `ToastTitle`, `ToastDescription`, `ToastAction`, `ToastClose`. Keep them */
|
|
224
|
+
/* as thin shims so existing call sites stay green. New code should import */
|
|
225
|
+
/* `Toaster` + `toast` only. */
|
|
37
226
|
/* -------------------------------------------------------------------------- */
|
|
38
227
|
|
|
228
|
+
/**
|
|
229
|
+
* @deprecated Use `<Toaster />` instead. Kept as a wrapping component for
|
|
230
|
+
* pre-sonner consumers — renders children, then mounts the Toaster portal.
|
|
231
|
+
*/
|
|
232
|
+
export function ToastProvider({
|
|
233
|
+
children,
|
|
234
|
+
...rest
|
|
235
|
+
}: MihcmToasterProps & { children?: React.ReactNode }) {
|
|
236
|
+
return (
|
|
237
|
+
<>
|
|
238
|
+
{children}
|
|
239
|
+
<Toaster {...rest} />
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export type ToastProviderProps = MihcmToasterProps & { children?: React.ReactNode };
|
|
245
|
+
|
|
246
|
+
/* The original `toastVariants` CVA — kept for any consumer that referenced
|
|
247
|
+
* its variant union types or used it to style a stand-alone Toast row. */
|
|
39
248
|
export const toastVariants = cva(
|
|
40
|
-
'pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-lg border p-4 shadow-lg '
|
|
41
|
-
'transition-all duration-300 ease-out',
|
|
249
|
+
'pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-lg border p-4 shadow-lg transition-all duration-300 ease-out',
|
|
42
250
|
{
|
|
43
251
|
variants: {
|
|
44
252
|
variant: {
|
|
@@ -71,338 +279,106 @@ export const toastVariants = cva(
|
|
|
71
279
|
},
|
|
72
280
|
);
|
|
73
281
|
|
|
74
|
-
/* -------------------------------------------------------------------------- */
|
|
75
|
-
/* Types */
|
|
76
|
-
/* -------------------------------------------------------------------------- */
|
|
77
|
-
|
|
78
282
|
export type ToastVariant = NonNullable<VariantProps<typeof toastVariants>['variant']>;
|
|
79
283
|
export type ToastAppearance = NonNullable<VariantProps<typeof toastVariants>['appearance']>;
|
|
80
284
|
|
|
81
|
-
export interface ToastData {
|
|
82
|
-
id: string;
|
|
83
|
-
title: string;
|
|
84
|
-
description?: string;
|
|
85
|
-
variant?: ToastVariant;
|
|
86
|
-
appearance?: ToastAppearance;
|
|
87
|
-
duration?: number;
|
|
88
|
-
action?: { label: string; onClick: () => void };
|
|
89
|
-
icon?: ReactNode;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
285
|
export interface ToastProps extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof toastVariants> {
|
|
93
286
|
className?: string;
|
|
94
287
|
}
|
|
95
288
|
|
|
96
|
-
|
|
97
|
-
className?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface ToastDescriptionProps extends HTMLAttributes<HTMLDivElement> {
|
|
101
|
-
className?: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface ToastActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
105
|
-
className?: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface ToastCloseProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
109
|
-
className?: string;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface ToastProviderProps {
|
|
113
|
-
children: ReactNode;
|
|
114
|
-
/** Maximum number of visible toasts. @default 5 */
|
|
115
|
-
max?: number;
|
|
116
|
-
/** Screen corner for the toast stack. */
|
|
117
|
-
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
118
|
-
/** Default visual treatment for toasts without an explicit appearance. */
|
|
119
|
-
appearance?: ToastAppearance;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/* -------------------------------------------------------------------------- */
|
|
123
|
-
/* Global singleton store */
|
|
124
|
-
/* */
|
|
125
|
-
/* Bundlers that transpile this package (e.g. Next.js `transpilePackages`) */
|
|
126
|
-
/* may create multiple module instances. Using `globalThis` ensures toast() */
|
|
127
|
-
/* and ToastProvider always share the same queue, even across chunks. */
|
|
128
|
-
/* -------------------------------------------------------------------------- */
|
|
129
|
-
|
|
130
|
-
type Listener = () => void;
|
|
131
|
-
|
|
132
|
-
interface ToastStore {
|
|
133
|
-
toasts: ToastData[];
|
|
134
|
-
listeners: Set<Listener>;
|
|
135
|
-
viewports: Set<number>;
|
|
136
|
-
counter: number;
|
|
137
|
-
viewportCounter: number;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const STORE_KEY = '__mihcm_toast_store__';
|
|
141
|
-
|
|
142
|
-
function getStore(): ToastStore {
|
|
143
|
-
const g = globalThis as unknown as Record<string, ToastStore>;
|
|
144
|
-
if (!g[STORE_KEY]) {
|
|
145
|
-
g[STORE_KEY] = { toasts: [], listeners: new Set(), viewports: new Set(), counter: 0, viewportCounter: 0 };
|
|
146
|
-
}
|
|
147
|
-
return g[STORE_KEY];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function notify() {
|
|
151
|
-
for (const l of getStore().listeners) l();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getSnapshot(): ToastData[] {
|
|
155
|
-
return getStore().toasts;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function getViewportSnapshot(): number | undefined {
|
|
159
|
-
return Array.from(getStore().viewports).at(-1);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function subscribe(listener: Listener): () => void {
|
|
163
|
-
const store = getStore();
|
|
164
|
-
store.listeners.add(listener);
|
|
165
|
-
return () => store.listeners.delete(listener);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Imperative API — call from anywhere to show a toast. */
|
|
169
|
-
export function toast(data: Omit<ToastData, 'id'>): string {
|
|
170
|
-
const store = getStore();
|
|
171
|
-
const id = `toast-${++store.counter}-${Date.now()}`;
|
|
172
|
-
const entry: ToastData = { id, ...data };
|
|
173
|
-
store.toasts = [...store.toasts, entry];
|
|
174
|
-
notify();
|
|
175
|
-
return id;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function dismiss(id: string) {
|
|
179
|
-
const store = getStore();
|
|
180
|
-
store.toasts = store.toasts.filter((t) => t.id !== id);
|
|
181
|
-
notify();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/* -------------------------------------------------------------------------- */
|
|
185
|
-
/* Sub-components */
|
|
186
|
-
/* -------------------------------------------------------------------------- */
|
|
187
|
-
|
|
188
|
-
const ACCENT: Record<ToastVariant, string> = {
|
|
189
|
-
default: 'border-l-border',
|
|
190
|
-
success: 'border-l-success',
|
|
191
|
-
error: 'border-l-destructive',
|
|
192
|
-
warning: 'border-l-warning',
|
|
193
|
-
accent: 'border-l-accent',
|
|
194
|
-
};
|
|
195
|
-
|
|
289
|
+
/** @deprecated Sonner renders rows internally. Kept for stand-alone use. */
|
|
196
290
|
export const Toast = forwardRef<HTMLDivElement, ToastProps>(function Toast(
|
|
197
291
|
{ className, variant = 'default', appearance = 'soft', children, ...props },
|
|
198
292
|
ref,
|
|
199
293
|
) {
|
|
200
294
|
return (
|
|
201
|
-
<div
|
|
202
|
-
ref={ref}
|
|
203
|
-
className={cn(toastVariants({ variant, appearance }), 'border-l-4', ACCENT[variant ?? 'default'], className)}
|
|
204
|
-
{...props}
|
|
205
|
-
>
|
|
295
|
+
<div ref={ref} className={cn(toastVariants({ variant, appearance }), className)} {...props}>
|
|
206
296
|
{children}
|
|
207
297
|
</div>
|
|
208
298
|
);
|
|
209
299
|
});
|
|
210
300
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
ref
|
|
214
|
-
) {
|
|
215
|
-
|
|
216
|
-
|
|
301
|
+
/** @deprecated Use sonner's title slot in `toast()` instead. */
|
|
302
|
+
export const ToastTitle = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
303
|
+
function ToastTitle({ className, ...props }, ref) {
|
|
304
|
+
return <div ref={ref} className={cn('text-sm font-semibold', className)} {...props} />;
|
|
305
|
+
},
|
|
306
|
+
);
|
|
217
307
|
|
|
218
|
-
|
|
308
|
+
/** @deprecated Use sonner's `description` option in `toast()` instead. */
|
|
309
|
+
export const ToastDescription = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
219
310
|
function ToastDescription({ className, ...props }, ref) {
|
|
220
311
|
return <div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />;
|
|
221
312
|
},
|
|
222
313
|
);
|
|
223
314
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
ref
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
);
|
|
241
|
-
});
|
|
315
|
+
/** @deprecated Use sonner's `action` option in `toast()` instead. */
|
|
316
|
+
export const ToastAction = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
317
|
+
function ToastAction({ className, ...props }, ref) {
|
|
318
|
+
return (
|
|
319
|
+
<button
|
|
320
|
+
ref={ref}
|
|
321
|
+
type="button"
|
|
322
|
+
className={cn(
|
|
323
|
+
'inline-flex shrink-0 items-center rounded-md border border-border bg-transparent px-3 py-1 text-sm font-medium transition-colors duration-150 hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
324
|
+
className,
|
|
325
|
+
)}
|
|
326
|
+
{...props}
|
|
327
|
+
/>
|
|
328
|
+
);
|
|
329
|
+
},
|
|
330
|
+
);
|
|
242
331
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
ref
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)}
|
|
257
|
-
{...props}
|
|
258
|
-
>
|
|
259
|
-
<svg
|
|
260
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
261
|
-
width="16"
|
|
262
|
-
height="16"
|
|
263
|
-
viewBox="0 0 24 24"
|
|
264
|
-
fill="none"
|
|
265
|
-
stroke="currentColor"
|
|
266
|
-
strokeWidth={2}
|
|
267
|
-
strokeLinecap="round"
|
|
268
|
-
strokeLinejoin="round"
|
|
269
|
-
aria-hidden
|
|
332
|
+
/** @deprecated `closeButton` prop on `<Toaster />` renders this automatically. */
|
|
333
|
+
export const ToastClose = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
334
|
+
function ToastClose({ className, ...props }, ref) {
|
|
335
|
+
return (
|
|
336
|
+
<button
|
|
337
|
+
ref={ref}
|
|
338
|
+
type="button"
|
|
339
|
+
aria-label="Close"
|
|
340
|
+
className={cn(
|
|
341
|
+
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 transition-opacity hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
342
|
+
className,
|
|
343
|
+
)}
|
|
344
|
+
{...props}
|
|
270
345
|
>
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
onDismiss: (id: string) => void;
|
|
291
|
-
appearance: ToastAppearance;
|
|
292
|
-
}) {
|
|
293
|
-
const [visible, setVisible] = useState(false);
|
|
294
|
-
const [exiting, setExiting] = useState(false);
|
|
295
|
-
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
296
|
-
const remainingRef = useRef(data.duration ?? DEFAULT_DURATION);
|
|
297
|
-
// eslint-disable-next-line react-hooks/purity -- initial value only, never re-read during render
|
|
298
|
-
const startRef = useRef(Date.now());
|
|
299
|
-
|
|
300
|
-
const startTimer = useCallback(() => {
|
|
301
|
-
startRef.current = Date.now();
|
|
302
|
-
timerRef.current = setTimeout(() => {
|
|
303
|
-
setExiting(true);
|
|
304
|
-
setTimeout(() => onDismiss(data.id), 300);
|
|
305
|
-
}, remainingRef.current);
|
|
306
|
-
}, [data.id, onDismiss]);
|
|
307
|
-
|
|
308
|
-
const pauseTimer = useCallback(() => {
|
|
309
|
-
if (timerRef.current) {
|
|
310
|
-
clearTimeout(timerRef.current);
|
|
311
|
-
remainingRef.current -= Date.now() - startRef.current;
|
|
312
|
-
if (remainingRef.current < 0) remainingRef.current = 0;
|
|
313
|
-
}
|
|
314
|
-
}, []);
|
|
315
|
-
|
|
316
|
-
useEffect(() => {
|
|
317
|
-
// Trigger entry animation on next frame
|
|
318
|
-
const raf = requestAnimationFrame(() => setVisible(true));
|
|
319
|
-
startTimer();
|
|
320
|
-
return () => {
|
|
321
|
-
cancelAnimationFrame(raf);
|
|
322
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
323
|
-
};
|
|
324
|
-
}, [startTimer]);
|
|
325
|
-
|
|
326
|
-
const handleClose = () => {
|
|
327
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
328
|
-
setExiting(true);
|
|
329
|
-
setTimeout(() => onDismiss(data.id), 300);
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const isError = data.variant === 'error';
|
|
346
|
+
<svg
|
|
347
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
348
|
+
width="16"
|
|
349
|
+
height="16"
|
|
350
|
+
viewBox="0 0 24 24"
|
|
351
|
+
fill="none"
|
|
352
|
+
stroke="currentColor"
|
|
353
|
+
strokeWidth={2}
|
|
354
|
+
strokeLinecap="round"
|
|
355
|
+
strokeLinejoin="round"
|
|
356
|
+
aria-hidden
|
|
357
|
+
>
|
|
358
|
+
<path d="M18 6 6 18" />
|
|
359
|
+
<path d="m6 6 12 12" />
|
|
360
|
+
</svg>
|
|
361
|
+
</button>
|
|
362
|
+
);
|
|
363
|
+
},
|
|
364
|
+
);
|
|
333
365
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
<div className="flex-1 space-y-1 pr-6">
|
|
348
|
-
<ToastTitle>{data.title}</ToastTitle>
|
|
349
|
-
{data.description !== undefined && (
|
|
350
|
-
<ToastDescription>{data.description}</ToastDescription>
|
|
351
|
-
)}
|
|
352
|
-
{data.action !== undefined && (
|
|
353
|
-
<div className="mt-2">
|
|
354
|
-
<ToastAction onClick={data.action.onClick}>{data.action.label}</ToastAction>
|
|
355
|
-
</div>
|
|
356
|
-
)}
|
|
357
|
-
</div>
|
|
358
|
-
<ToastClose onClick={handleClose} />
|
|
359
|
-
</Toast>
|
|
360
|
-
</div>
|
|
361
|
-
);
|
|
366
|
+
/**
|
|
367
|
+
* Legacy `ToastData` shape kept for typing in any pre-sonner consumer.
|
|
368
|
+
* @deprecated Sonner uses its own `ExternalToast` shape; prefer that.
|
|
369
|
+
*/
|
|
370
|
+
export interface ToastData {
|
|
371
|
+
id: string;
|
|
372
|
+
title: string;
|
|
373
|
+
description?: string;
|
|
374
|
+
variant?: ToastVariant;
|
|
375
|
+
appearance?: ToastAppearance;
|
|
376
|
+
duration?: number;
|
|
377
|
+
action?: { label: string; onClick: () => void };
|
|
378
|
+
icon?: React.ReactNode;
|
|
362
379
|
}
|
|
363
380
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const POSITION_CLASS: Record<NonNullable<ToastProviderProps['position']>, string> = {
|
|
369
|
-
'bottom-right': 'bottom-4 right-4',
|
|
370
|
-
'bottom-left': 'bottom-4 left-4',
|
|
371
|
-
'top-right': 'right-4 top-4',
|
|
372
|
-
'top-left': 'left-4 top-4',
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
export function ToastProvider({ children, max = 5, position = 'bottom-right', appearance = 'soft' }: ToastProviderProps) {
|
|
376
|
-
const [viewportId] = useState(() => ++getStore().viewportCounter);
|
|
377
|
-
|
|
378
|
-
useEffect(() => {
|
|
379
|
-
const store = getStore();
|
|
380
|
-
store.viewports.add(viewportId);
|
|
381
|
-
notify();
|
|
382
|
-
return () => {
|
|
383
|
-
store.viewports.delete(viewportId);
|
|
384
|
-
notify();
|
|
385
|
-
};
|
|
386
|
-
}, [viewportId]);
|
|
387
|
-
|
|
388
|
-
const items = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
389
|
-
const activeViewportId = useSyncExternalStore(subscribe, getViewportSnapshot, () => undefined);
|
|
390
|
-
const visible = items.slice(-max);
|
|
391
|
-
const isActiveViewport = viewportId === activeViewportId;
|
|
392
|
-
|
|
393
|
-
return (
|
|
394
|
-
<>
|
|
395
|
-
{children}
|
|
396
|
-
{isActiveViewport ? (
|
|
397
|
-
<div
|
|
398
|
-
aria-label="Notifications"
|
|
399
|
-
className={cn('pointer-events-none fixed z-[100] flex max-w-[420px] flex-col gap-2', POSITION_CLASS[position])}
|
|
400
|
-
>
|
|
401
|
-
{visible.map((t) => (
|
|
402
|
-
<ToastItem key={t.id} data={t} onDismiss={dismiss} appearance={appearance} />
|
|
403
|
-
))}
|
|
404
|
-
</div>
|
|
405
|
-
) : null}
|
|
406
|
-
</>
|
|
407
|
-
);
|
|
408
|
-
}
|
|
381
|
+
export interface ToastTitleProps extends HTMLAttributes<HTMLDivElement> {}
|
|
382
|
+
export interface ToastDescriptionProps extends HTMLAttributes<HTMLDivElement> {}
|
|
383
|
+
export interface ToastActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
|
|
384
|
+
export interface ToastCloseProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
|