@behivetech/atoms.toast 0.0.0-stage → 0.2.3
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 +10 -2
- package/dist/index.cjs +223 -0
- package/dist/index.css +159 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +208 -0
- package/docs.json +47 -0
- package/index.ts +7 -0
- package/package.json +59 -3
- package/toast.example.tsx +41 -0
- package/toast.module.scss +205 -0
- package/toast.tsx +114 -0
package/README.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Toast
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TODO: Add component description.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```jsx
|
|
8
|
+
import { Toast } from "@behivetech/atoms.toast";
|
|
9
|
+
|
|
10
|
+
<Toast>Content</Toast>;
|
|
11
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
|
|
34
|
+
var require_classnames = __commonJS({
|
|
35
|
+
"../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports2, module2) {
|
|
36
|
+
"use strict";
|
|
37
|
+
(function() {
|
|
38
|
+
"use strict";
|
|
39
|
+
var hasOwn = {}.hasOwnProperty;
|
|
40
|
+
function classNames() {
|
|
41
|
+
var classes = "";
|
|
42
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
43
|
+
var arg = arguments[i];
|
|
44
|
+
if (arg) {
|
|
45
|
+
classes = appendClass(classes, parseValue(arg));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return classes;
|
|
49
|
+
}
|
|
50
|
+
function parseValue(arg) {
|
|
51
|
+
if (typeof arg === "string" || typeof arg === "number") {
|
|
52
|
+
return arg;
|
|
53
|
+
}
|
|
54
|
+
if (typeof arg !== "object") {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(arg)) {
|
|
58
|
+
return classNames.apply(null, arg);
|
|
59
|
+
}
|
|
60
|
+
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
|
|
61
|
+
return arg.toString();
|
|
62
|
+
}
|
|
63
|
+
var classes = "";
|
|
64
|
+
for (var key in arg) {
|
|
65
|
+
if (hasOwn.call(arg, key) && arg[key]) {
|
|
66
|
+
classes = appendClass(classes, key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return classes;
|
|
70
|
+
}
|
|
71
|
+
function appendClass(value, newClass) {
|
|
72
|
+
if (!newClass) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
if (value) {
|
|
76
|
+
return value + " " + newClass;
|
|
77
|
+
}
|
|
78
|
+
return value + newClass;
|
|
79
|
+
}
|
|
80
|
+
if (typeof module2 !== "undefined" && module2.exports) {
|
|
81
|
+
classNames.default = classNames;
|
|
82
|
+
module2.exports = classNames;
|
|
83
|
+
} else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
|
|
84
|
+
define("classnames", [], function() {
|
|
85
|
+
return classNames;
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
window.classNames = classNames;
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// index.ts
|
|
95
|
+
var index_exports = {};
|
|
96
|
+
__export(index_exports, {
|
|
97
|
+
Toast: () => Toast,
|
|
98
|
+
showToast: () => showToast
|
|
99
|
+
});
|
|
100
|
+
module.exports = __toCommonJS(index_exports);
|
|
101
|
+
|
|
102
|
+
// toast.tsx
|
|
103
|
+
var import_react = require("react");
|
|
104
|
+
var RadixToast = __toESM(require("@radix-ui/react-toast"), 1);
|
|
105
|
+
|
|
106
|
+
// ../../../../packages/registry/class-names/dist/index.js
|
|
107
|
+
var import_classnames = __toESM(require_classnames(), 1);
|
|
108
|
+
var getClassName = ({
|
|
109
|
+
className,
|
|
110
|
+
modifiers = {},
|
|
111
|
+
rootClass,
|
|
112
|
+
styles = {}
|
|
113
|
+
}) => {
|
|
114
|
+
function getModifiers() {
|
|
115
|
+
const updatedModifiers = {};
|
|
116
|
+
const modifierKeys = Object.keys(modifiers);
|
|
117
|
+
if (modifierKeys.length) {
|
|
118
|
+
modifierKeys.forEach((modKey) => {
|
|
119
|
+
const classKey = `${rootClass}--${modKey}`;
|
|
120
|
+
const newClassName = styles[classKey] || classKey;
|
|
121
|
+
updatedModifiers[newClassName] = modifiers[modKey] ?? false;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return updatedModifiers;
|
|
125
|
+
}
|
|
126
|
+
function getChildClass(childClassName) {
|
|
127
|
+
const classKey = `${rootClass}__${childClassName}`;
|
|
128
|
+
return styles[classKey] || classKey;
|
|
129
|
+
}
|
|
130
|
+
function getRootStyles() {
|
|
131
|
+
return (0, import_classnames.default)(
|
|
132
|
+
className,
|
|
133
|
+
{
|
|
134
|
+
...getModifiers()
|
|
135
|
+
},
|
|
136
|
+
styles[rootClass] || rootClass
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return [getRootStyles(), getChildClass];
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// toast.module.scss
|
|
143
|
+
var toast_module_default = {
|
|
144
|
+
"BHT__Toast__viewport": "BHT__Toast__viewport",
|
|
145
|
+
"BHT__Toast__root": "BHT__Toast__root",
|
|
146
|
+
"toast-in": "toast-in",
|
|
147
|
+
"toast-out": "toast-out",
|
|
148
|
+
"toast-swipe-out": "toast-swipe-out",
|
|
149
|
+
"BHT__Toast__title": "BHT__Toast__title",
|
|
150
|
+
"BHT__Toast__description": "BHT__Toast__description",
|
|
151
|
+
"BHT__Toast__action": "BHT__Toast__action",
|
|
152
|
+
"BHT__Toast__close": "BHT__Toast__close"
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// toast.tsx
|
|
156
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
157
|
+
var TOAST_EVENT = "bht:toast";
|
|
158
|
+
function showToast(message) {
|
|
159
|
+
const detail = { id: crypto.randomUUID(), ...message };
|
|
160
|
+
window.dispatchEvent(new CustomEvent(TOAST_EVENT, { detail }));
|
|
161
|
+
}
|
|
162
|
+
var Toast = ({ duration = 5e3 }) => {
|
|
163
|
+
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
164
|
+
const [, getChildClass] = getClassName({ rootClass: "BHT__Toast", styles: toast_module_default });
|
|
165
|
+
const dismiss = (0, import_react.useCallback)((id) => {
|
|
166
|
+
setMessages((current) => current.filter((message) => message.id !== id));
|
|
167
|
+
}, []);
|
|
168
|
+
(0, import_react.useEffect)(() => {
|
|
169
|
+
const handleToastEvent = (event) => {
|
|
170
|
+
const { detail } = event;
|
|
171
|
+
setMessages((current) => [...current, detail]);
|
|
172
|
+
};
|
|
173
|
+
window.addEventListener(TOAST_EVENT, handleToastEvent);
|
|
174
|
+
return () => window.removeEventListener(TOAST_EVENT, handleToastEvent);
|
|
175
|
+
}, []);
|
|
176
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RadixToast.Provider, { duration, children: [
|
|
177
|
+
messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
178
|
+
RadixToast.Root,
|
|
179
|
+
{
|
|
180
|
+
className: getChildClass("root"),
|
|
181
|
+
"data-variant": message.variant ?? "info",
|
|
182
|
+
onOpenChange: (open) => !open && dismiss(message.id),
|
|
183
|
+
children: [
|
|
184
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixToast.Title, { className: getChildClass("title"), children: message.title }),
|
|
185
|
+
message.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixToast.Description, { className: getChildClass("description"), children: message.description }),
|
|
186
|
+
message.action && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
187
|
+
RadixToast.Action,
|
|
188
|
+
{
|
|
189
|
+
className: getChildClass("action"),
|
|
190
|
+
altText: message.action.altText ?? message.action.label,
|
|
191
|
+
onClick: message.action.onClick,
|
|
192
|
+
children: message.action.label
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
196
|
+
RadixToast.Close,
|
|
197
|
+
{
|
|
198
|
+
className: getChildClass("close"),
|
|
199
|
+
"aria-label": "Dismiss",
|
|
200
|
+
children: "\xD7"
|
|
201
|
+
}
|
|
202
|
+
)
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
message.id
|
|
206
|
+
)),
|
|
207
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixToast.Viewport, { className: getChildClass("viewport") })
|
|
208
|
+
] });
|
|
209
|
+
};
|
|
210
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
211
|
+
0 && (module.exports = {
|
|
212
|
+
Toast,
|
|
213
|
+
showToast
|
|
214
|
+
});
|
|
215
|
+
/*! Bundled license information:
|
|
216
|
+
|
|
217
|
+
classnames/index.js:
|
|
218
|
+
(*!
|
|
219
|
+
Copyright (c) 2018 Jed Watson.
|
|
220
|
+
Licensed under the MIT License (MIT), see
|
|
221
|
+
http://jedwatson.github.io/classnames
|
|
222
|
+
*)
|
|
223
|
+
*/
|
package/dist/index.css
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* esbuild-sass-plugin:css-chunktoast.module.scss */
|
|
2
|
+
.BHT__Toast__viewport {
|
|
3
|
+
position: fixed;
|
|
4
|
+
bottom: var(--spacing-4, 1rem);
|
|
5
|
+
right: var(--spacing-4, 1rem);
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--spacing-2, 0.5rem);
|
|
9
|
+
width: min(22.5rem, 100vw - 2rem);
|
|
10
|
+
z-index: var(--z-toast, 500);
|
|
11
|
+
list-style: none;
|
|
12
|
+
margin: 0;
|
|
13
|
+
padding: 0;
|
|
14
|
+
outline: none;
|
|
15
|
+
}
|
|
16
|
+
.BHT__Toast__root {
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-template-columns: 1fr auto;
|
|
19
|
+
align-items: start;
|
|
20
|
+
gap: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);
|
|
21
|
+
padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);
|
|
22
|
+
background: var(--md-sys-color-inverse-surface, var(--color-on-surface));
|
|
23
|
+
color: var(--md-sys-color-inverse-on-surface, var(--color-surface));
|
|
24
|
+
border: 1px solid transparent;
|
|
25
|
+
border-left: 3px solid var(--md-sys-color-inverse-primary, var(--color-primary));
|
|
26
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
27
|
+
box-shadow: var(--md-sys-elevation-3, var(--shadow-lg));
|
|
28
|
+
}
|
|
29
|
+
.BHT__Toast__root[data-variant=success] {
|
|
30
|
+
border-left-color: var(--color-success);
|
|
31
|
+
}
|
|
32
|
+
.BHT__Toast__root[data-variant=error] {
|
|
33
|
+
border-left-color: var(--md-sys-color-error-container, var(--color-error));
|
|
34
|
+
}
|
|
35
|
+
.BHT__Toast__root[data-state=open] {
|
|
36
|
+
animation: toast-in var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard-decelerate, ease-out);
|
|
37
|
+
}
|
|
38
|
+
.BHT__Toast__root[data-state=closed] {
|
|
39
|
+
animation: toast-out var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard-accelerate, ease-in);
|
|
40
|
+
}
|
|
41
|
+
.BHT__Toast__root[data-swipe=move] {
|
|
42
|
+
transform: translateX(var(--radix-toast-swipe-move-x));
|
|
43
|
+
}
|
|
44
|
+
.BHT__Toast__root[data-swipe=end] {
|
|
45
|
+
animation: toast-swipe-out var(--md-sys-motion-duration-short-3, 150ms) var(--md-sys-motion-easing-standard-accelerate, ease-out);
|
|
46
|
+
}
|
|
47
|
+
.BHT__Toast__title {
|
|
48
|
+
grid-column: 1;
|
|
49
|
+
font-family: var(--md-sys-typescale-label-large-font, var(--font-body));
|
|
50
|
+
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
51
|
+
font-weight: var(--md-sys-typescale-label-large-weight, 500);
|
|
52
|
+
line-height: var(--md-sys-typescale-label-large-line-height, 1.25rem);
|
|
53
|
+
letter-spacing: var(--md-sys-typescale-label-large-tracking, 0.0071em);
|
|
54
|
+
}
|
|
55
|
+
.BHT__Toast__description {
|
|
56
|
+
grid-column: 1;
|
|
57
|
+
font-family: var(--md-sys-typescale-body-medium-font, var(--font-body));
|
|
58
|
+
font-size: var(--md-sys-typescale-body-medium-size, 0.875rem);
|
|
59
|
+
font-weight: var(--md-sys-typescale-body-medium-weight, 400);
|
|
60
|
+
line-height: var(--md-sys-typescale-body-medium-line-height, 1.25rem);
|
|
61
|
+
letter-spacing: var(--md-sys-typescale-body-medium-tracking, 0.0179em);
|
|
62
|
+
}
|
|
63
|
+
.BHT__Toast__action,
|
|
64
|
+
.BHT__Toast__close {
|
|
65
|
+
position: relative;
|
|
66
|
+
isolation: isolate;
|
|
67
|
+
background: transparent;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
.BHT__Toast__action::before,
|
|
71
|
+
.BHT__Toast__close::before {
|
|
72
|
+
content: "";
|
|
73
|
+
position: absolute;
|
|
74
|
+
top: 50%;
|
|
75
|
+
left: 50%;
|
|
76
|
+
translate: -50% -50%;
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: 100%;
|
|
79
|
+
min-width: 48px;
|
|
80
|
+
min-height: 48px;
|
|
81
|
+
}
|
|
82
|
+
.BHT__Toast__action::after,
|
|
83
|
+
.BHT__Toast__close::after {
|
|
84
|
+
content: "";
|
|
85
|
+
position: absolute;
|
|
86
|
+
inset: 0;
|
|
87
|
+
border-radius: inherit;
|
|
88
|
+
background: currentColor;
|
|
89
|
+
opacity: 0;
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
transition: opacity var(--md-sys-motion-duration-short-2, 100ms) var(--md-sys-motion-easing-standard, ease);
|
|
92
|
+
}
|
|
93
|
+
.BHT__Toast__action:hover::after,
|
|
94
|
+
.BHT__Toast__close:hover::after {
|
|
95
|
+
opacity: var(--md-sys-state-hover-opacity, 0.08);
|
|
96
|
+
}
|
|
97
|
+
.BHT__Toast__action:focus-visible::after,
|
|
98
|
+
.BHT__Toast__close:focus-visible::after {
|
|
99
|
+
opacity: var(--md-sys-state-focus-opacity, 0.1);
|
|
100
|
+
}
|
|
101
|
+
.BHT__Toast__action:active::after,
|
|
102
|
+
.BHT__Toast__close:active::after {
|
|
103
|
+
opacity: var(--md-sys-state-pressed-opacity, 0.1);
|
|
104
|
+
}
|
|
105
|
+
.BHT__Toast__action:focus-visible,
|
|
106
|
+
.BHT__Toast__close:focus-visible {
|
|
107
|
+
outline: 3px solid var(--md-sys-color-secondary, var(--color-border-focus));
|
|
108
|
+
outline-offset: 2px;
|
|
109
|
+
}
|
|
110
|
+
.BHT__Toast__action {
|
|
111
|
+
grid-column: 1;
|
|
112
|
+
justify-self: start;
|
|
113
|
+
margin-top: var(--spacing-1, 0.25rem);
|
|
114
|
+
padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);
|
|
115
|
+
border: 1px solid transparent;
|
|
116
|
+
border-radius: var(--md-sys-shape-corner-full, 9999px);
|
|
117
|
+
color: var(--md-sys-color-inverse-primary, var(--color-primary));
|
|
118
|
+
font-family: var(--md-sys-typescale-label-large-font, var(--font-body));
|
|
119
|
+
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
120
|
+
font-weight: var(--md-sys-typescale-label-large-weight, 500);
|
|
121
|
+
letter-spacing: var(--md-sys-typescale-label-large-tracking, 0.0071em);
|
|
122
|
+
line-height: 1;
|
|
123
|
+
}
|
|
124
|
+
.BHT__Toast__close {
|
|
125
|
+
grid-column: 2;
|
|
126
|
+
grid-row: 1/3;
|
|
127
|
+
border: none;
|
|
128
|
+
border-radius: var(--md-sys-shape-corner-full, 9999px);
|
|
129
|
+
color: var(--md-sys-color-inverse-on-surface, var(--color-surface));
|
|
130
|
+
font-size: 1.125rem;
|
|
131
|
+
line-height: 1;
|
|
132
|
+
padding: 0;
|
|
133
|
+
}
|
|
134
|
+
@keyframes toast-in {
|
|
135
|
+
from {
|
|
136
|
+
opacity: 0;
|
|
137
|
+
transform: translateY(0.5rem);
|
|
138
|
+
}
|
|
139
|
+
to {
|
|
140
|
+
opacity: 1;
|
|
141
|
+
transform: translateY(0);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
@keyframes toast-out {
|
|
145
|
+
from {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
}
|
|
148
|
+
to {
|
|
149
|
+
opacity: 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
@keyframes toast-swipe-out {
|
|
153
|
+
from {
|
|
154
|
+
transform: translateX(var(--radix-toast-swipe-end-x));
|
|
155
|
+
}
|
|
156
|
+
to {
|
|
157
|
+
transform: translateX(100%);
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
type ToastVariant = "info" | "success" | "error";
|
|
4
|
+
interface ToastAction {
|
|
5
|
+
/** Button label, e.g. "Undo" */
|
|
6
|
+
label: string;
|
|
7
|
+
/** Fired on click; the toast dismisses itself afterwards */
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
/**
|
|
10
|
+
* How to achieve the same thing without the toast, for assistive tech.
|
|
11
|
+
* Required by Radix; defaults to `label`.
|
|
12
|
+
*/
|
|
13
|
+
altText?: string;
|
|
14
|
+
}
|
|
15
|
+
interface ToastMessage {
|
|
16
|
+
/** Unique identifier, auto-generated by `showToast` */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Bold headline text */
|
|
19
|
+
title: string;
|
|
20
|
+
/** Optional supporting text shown below the title */
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Controls the accent color; defaults to "info" */
|
|
23
|
+
variant?: ToastVariant;
|
|
24
|
+
/** Optional button rendered alongside the dismiss control, e.g. an undo */
|
|
25
|
+
action?: ToastAction;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Queues a toast on any mounted `Toast` component. Dispatches a plain `window`
|
|
29
|
+
* event under the hood, so it's callable from anywhere with no React context
|
|
30
|
+
* required — including directly from the browser console, e.g.
|
|
31
|
+
* `showToast({ title: "Saved", variant: "success" })`.
|
|
32
|
+
*/
|
|
33
|
+
declare function showToast(message: Omit<ToastMessage, "id">): void;
|
|
34
|
+
interface ToastProps {
|
|
35
|
+
/** How long a toast stays visible before auto-dismissing, in ms. Defaults to 5000. */
|
|
36
|
+
duration?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mount once, anywhere in the app (e.g. a root layout). Listens for `showToast()`
|
|
40
|
+
* calls and renders each as a dismissible, auto-expiring Radix Toast in a fixed
|
|
41
|
+
* viewport.
|
|
42
|
+
*/
|
|
43
|
+
declare const Toast: ({ duration }: ToastProps) => react.JSX.Element;
|
|
44
|
+
|
|
45
|
+
export { Toast, type ToastAction, type ToastMessage, type ToastProps, type ToastVariant, showToast };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
|
|
28
|
+
var require_classnames = __commonJS({
|
|
29
|
+
"../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports, module) {
|
|
30
|
+
"use strict";
|
|
31
|
+
(function() {
|
|
32
|
+
"use strict";
|
|
33
|
+
var hasOwn = {}.hasOwnProperty;
|
|
34
|
+
function classNames() {
|
|
35
|
+
var classes = "";
|
|
36
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
37
|
+
var arg = arguments[i];
|
|
38
|
+
if (arg) {
|
|
39
|
+
classes = appendClass(classes, parseValue(arg));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return classes;
|
|
43
|
+
}
|
|
44
|
+
function parseValue(arg) {
|
|
45
|
+
if (typeof arg === "string" || typeof arg === "number") {
|
|
46
|
+
return arg;
|
|
47
|
+
}
|
|
48
|
+
if (typeof arg !== "object") {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(arg)) {
|
|
52
|
+
return classNames.apply(null, arg);
|
|
53
|
+
}
|
|
54
|
+
if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
|
|
55
|
+
return arg.toString();
|
|
56
|
+
}
|
|
57
|
+
var classes = "";
|
|
58
|
+
for (var key in arg) {
|
|
59
|
+
if (hasOwn.call(arg, key) && arg[key]) {
|
|
60
|
+
classes = appendClass(classes, key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return classes;
|
|
64
|
+
}
|
|
65
|
+
function appendClass(value, newClass) {
|
|
66
|
+
if (!newClass) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
if (value) {
|
|
70
|
+
return value + " " + newClass;
|
|
71
|
+
}
|
|
72
|
+
return value + newClass;
|
|
73
|
+
}
|
|
74
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
75
|
+
classNames.default = classNames;
|
|
76
|
+
module.exports = classNames;
|
|
77
|
+
} else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
|
|
78
|
+
define("classnames", [], function() {
|
|
79
|
+
return classNames;
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
window.classNames = classNames;
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// toast.tsx
|
|
89
|
+
import { useCallback, useEffect, useState } from "react";
|
|
90
|
+
import * as RadixToast from "@radix-ui/react-toast";
|
|
91
|
+
|
|
92
|
+
// ../../../../packages/registry/class-names/dist/index.js
|
|
93
|
+
var import_classnames = __toESM(require_classnames(), 1);
|
|
94
|
+
var getClassName = ({
|
|
95
|
+
className,
|
|
96
|
+
modifiers = {},
|
|
97
|
+
rootClass,
|
|
98
|
+
styles = {}
|
|
99
|
+
}) => {
|
|
100
|
+
function getModifiers() {
|
|
101
|
+
const updatedModifiers = {};
|
|
102
|
+
const modifierKeys = Object.keys(modifiers);
|
|
103
|
+
if (modifierKeys.length) {
|
|
104
|
+
modifierKeys.forEach((modKey) => {
|
|
105
|
+
const classKey = `${rootClass}--${modKey}`;
|
|
106
|
+
const newClassName = styles[classKey] || classKey;
|
|
107
|
+
updatedModifiers[newClassName] = modifiers[modKey] ?? false;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return updatedModifiers;
|
|
111
|
+
}
|
|
112
|
+
function getChildClass(childClassName) {
|
|
113
|
+
const classKey = `${rootClass}__${childClassName}`;
|
|
114
|
+
return styles[classKey] || classKey;
|
|
115
|
+
}
|
|
116
|
+
function getRootStyles() {
|
|
117
|
+
return (0, import_classnames.default)(
|
|
118
|
+
className,
|
|
119
|
+
{
|
|
120
|
+
...getModifiers()
|
|
121
|
+
},
|
|
122
|
+
styles[rootClass] || rootClass
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return [getRootStyles(), getChildClass];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// toast.module.scss
|
|
129
|
+
var toast_module_default = {
|
|
130
|
+
"BHT__Toast__viewport": "BHT__Toast__viewport",
|
|
131
|
+
"BHT__Toast__root": "BHT__Toast__root",
|
|
132
|
+
"toast-in": "toast-in",
|
|
133
|
+
"toast-out": "toast-out",
|
|
134
|
+
"toast-swipe-out": "toast-swipe-out",
|
|
135
|
+
"BHT__Toast__title": "BHT__Toast__title",
|
|
136
|
+
"BHT__Toast__description": "BHT__Toast__description",
|
|
137
|
+
"BHT__Toast__action": "BHT__Toast__action",
|
|
138
|
+
"BHT__Toast__close": "BHT__Toast__close"
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// toast.tsx
|
|
142
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
143
|
+
var TOAST_EVENT = "bht:toast";
|
|
144
|
+
function showToast(message) {
|
|
145
|
+
const detail = { id: crypto.randomUUID(), ...message };
|
|
146
|
+
window.dispatchEvent(new CustomEvent(TOAST_EVENT, { detail }));
|
|
147
|
+
}
|
|
148
|
+
var Toast = ({ duration = 5e3 }) => {
|
|
149
|
+
const [messages, setMessages] = useState([]);
|
|
150
|
+
const [, getChildClass] = getClassName({ rootClass: "BHT__Toast", styles: toast_module_default });
|
|
151
|
+
const dismiss = useCallback((id) => {
|
|
152
|
+
setMessages((current) => current.filter((message) => message.id !== id));
|
|
153
|
+
}, []);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const handleToastEvent = (event) => {
|
|
156
|
+
const { detail } = event;
|
|
157
|
+
setMessages((current) => [...current, detail]);
|
|
158
|
+
};
|
|
159
|
+
window.addEventListener(TOAST_EVENT, handleToastEvent);
|
|
160
|
+
return () => window.removeEventListener(TOAST_EVENT, handleToastEvent);
|
|
161
|
+
}, []);
|
|
162
|
+
return /* @__PURE__ */ jsxs(RadixToast.Provider, { duration, children: [
|
|
163
|
+
messages.map((message) => /* @__PURE__ */ jsxs(
|
|
164
|
+
RadixToast.Root,
|
|
165
|
+
{
|
|
166
|
+
className: getChildClass("root"),
|
|
167
|
+
"data-variant": message.variant ?? "info",
|
|
168
|
+
onOpenChange: (open) => !open && dismiss(message.id),
|
|
169
|
+
children: [
|
|
170
|
+
/* @__PURE__ */ jsx(RadixToast.Title, { className: getChildClass("title"), children: message.title }),
|
|
171
|
+
message.description && /* @__PURE__ */ jsx(RadixToast.Description, { className: getChildClass("description"), children: message.description }),
|
|
172
|
+
message.action && /* @__PURE__ */ jsx(
|
|
173
|
+
RadixToast.Action,
|
|
174
|
+
{
|
|
175
|
+
className: getChildClass("action"),
|
|
176
|
+
altText: message.action.altText ?? message.action.label,
|
|
177
|
+
onClick: message.action.onClick,
|
|
178
|
+
children: message.action.label
|
|
179
|
+
}
|
|
180
|
+
),
|
|
181
|
+
/* @__PURE__ */ jsx(
|
|
182
|
+
RadixToast.Close,
|
|
183
|
+
{
|
|
184
|
+
className: getChildClass("close"),
|
|
185
|
+
"aria-label": "Dismiss",
|
|
186
|
+
children: "\xD7"
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
message.id
|
|
192
|
+
)),
|
|
193
|
+
/* @__PURE__ */ jsx(RadixToast.Viewport, { className: getChildClass("viewport") })
|
|
194
|
+
] });
|
|
195
|
+
};
|
|
196
|
+
export {
|
|
197
|
+
Toast,
|
|
198
|
+
showToast
|
|
199
|
+
};
|
|
200
|
+
/*! Bundled license information:
|
|
201
|
+
|
|
202
|
+
classnames/index.js:
|
|
203
|
+
(*!
|
|
204
|
+
Copyright (c) 2018 Jed Watson.
|
|
205
|
+
Licensed under the MIT License (MIT), see
|
|
206
|
+
http://jedwatson.github.io/classnames
|
|
207
|
+
*)
|
|
208
|
+
*/
|
package/docs.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://github.com/behivetech/bht-component-registry/docs.schema.json",
|
|
3
|
+
"version": 2,
|
|
4
|
+
"generatedAt": "2026-10-23T16:57:42.311Z",
|
|
5
|
+
"docs": [
|
|
6
|
+
{
|
|
7
|
+
"displayName": "Toast",
|
|
8
|
+
"description": "Mount once, anywhere in the app (e.g. a root layout). Listens for `showToast()`\ncalls and renders each as a dismissible, auto-expiring Radix Toast in a fixed\nviewport.",
|
|
9
|
+
"extendsNative": false,
|
|
10
|
+
"props": [
|
|
11
|
+
{
|
|
12
|
+
"name": "duration",
|
|
13
|
+
"type": "number",
|
|
14
|
+
"required": false,
|
|
15
|
+
"defaultValue": "5000",
|
|
16
|
+
"description": "How long a toast stays visible before auto-dismissing, in ms. Defaults to 5000."
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"functions": [
|
|
22
|
+
{
|
|
23
|
+
"name": "showToast",
|
|
24
|
+
"kind": "function",
|
|
25
|
+
"description": "Queues a toast on any mounted `Toast` component. Dispatches a plain `window`\nevent under the hood, so it's callable from anywhere with no React context\nrequired — including directly from the browser console, e.g.\n`showToast({ title: \"Saved\", variant: \"success\" })`.",
|
|
26
|
+
"signatures": [
|
|
27
|
+
{
|
|
28
|
+
"text": "showToast(message: Omit<ToastMessage, \"id\">): void",
|
|
29
|
+
"params": [
|
|
30
|
+
{
|
|
31
|
+
"name": "message",
|
|
32
|
+
"type": "Omit<ToastMessage, \"id\">",
|
|
33
|
+
"optional": false,
|
|
34
|
+
"defaultValue": null,
|
|
35
|
+
"description": ""
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"returns": {
|
|
39
|
+
"type": "void",
|
|
40
|
+
"description": ""
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"examples": []
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
package/index.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@behivetech/atoms.toast",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./styles.css": "./dist/index.css"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"index.ts",
|
|
19
|
+
"toast.tsx",
|
|
20
|
+
"toast.module.scss",
|
|
21
|
+
"*.example.tsx",
|
|
22
|
+
"docs.json"
|
|
23
|
+
],
|
|
24
|
+
"registry": {
|
|
25
|
+
"status": "stable",
|
|
26
|
+
"tags": []
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@radix-ui/react-toast": "^1.2.23"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^19.2.7",
|
|
33
|
+
"react-dom": "^19.2.7"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/react": "19.2.17",
|
|
37
|
+
"@types/react-dom": "19.2.3",
|
|
38
|
+
"eslint": "^9.39.5",
|
|
39
|
+
"tsup": "^8.5.1",
|
|
40
|
+
"typescript": "^6.0.3",
|
|
41
|
+
"vitest": "^4.1.10",
|
|
42
|
+
"@bht-component-registry/class-names": "0.0.6",
|
|
43
|
+
"@bht-component-registry/env": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/behivetech/bht-galaxy.git",
|
|
48
|
+
"directory": "registry/behivetech/atoms/toast"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"registry": "https://registry.npmjs.org",
|
|
52
|
+
"access": "public",
|
|
53
|
+
"provenance": true
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup && bht-registry-docgen",
|
|
57
|
+
"dev": "tsup --watch",
|
|
58
|
+
"lint": "eslint . --max-warnings 0",
|
|
59
|
+
"check-types": "tsc --noEmit",
|
|
60
|
+
"test": "vitest run"
|
|
61
|
+
}
|
|
6
62
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Toast, showToast } from "./toast.js";
|
|
2
|
+
|
|
3
|
+
export const BasicToast = () => (
|
|
4
|
+
<>
|
|
5
|
+
<button
|
|
6
|
+
type="button"
|
|
7
|
+
onClick={() =>
|
|
8
|
+
showToast({ title: "Saved", description: "Your changes were saved." })
|
|
9
|
+
}
|
|
10
|
+
>
|
|
11
|
+
Show info toast
|
|
12
|
+
</button>
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
onClick={() => showToast({ title: "Published", variant: "success" })}
|
|
16
|
+
>
|
|
17
|
+
Show success toast
|
|
18
|
+
</button>
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
onClick={() => showToast({ title: "Failed to save", variant: "error" })}
|
|
22
|
+
>
|
|
23
|
+
Show error toast
|
|
24
|
+
</button>
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() =>
|
|
28
|
+
showToast({
|
|
29
|
+
title: "Structure moved",
|
|
30
|
+
action: {
|
|
31
|
+
label: "Undo",
|
|
32
|
+
onClick: () => showToast({ title: "Move undone" }),
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
>
|
|
37
|
+
Show toast with an action
|
|
38
|
+
</button>
|
|
39
|
+
<Toast />
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Material Design 3 snackbar. `inverse-surface` and `inverse-primary` have no
|
|
2
|
+
// pre-MD3 names, so their fallbacks are the old surface/on-surface pair
|
|
3
|
+
// swapped, which is exactly what "inverse" means.
|
|
4
|
+
//
|
|
5
|
+
// The `data-variant` accent stripe is kept (it is part of the public API) even
|
|
6
|
+
// though MD3 snackbars have no variants.
|
|
7
|
+
|
|
8
|
+
.BHT__Toast {
|
|
9
|
+
&__viewport {
|
|
10
|
+
position: fixed;
|
|
11
|
+
bottom: var(--spacing-4, 1rem);
|
|
12
|
+
right: var(--spacing-4, 1rem);
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: var(--spacing-2, 0.5rem);
|
|
16
|
+
width: min(22.5rem, calc(100vw - 2rem));
|
|
17
|
+
z-index: var(--z-toast, 500);
|
|
18
|
+
list-style: none;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
outline: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&__root {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: 1fr auto;
|
|
27
|
+
align-items: start;
|
|
28
|
+
gap: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);
|
|
29
|
+
padding: var(--spacing-3, 0.75rem) var(--spacing-4, 1rem);
|
|
30
|
+
background: var(--md-sys-color-inverse-surface, var(--color-on-surface));
|
|
31
|
+
color: var(--md-sys-color-inverse-on-surface, var(--color-surface));
|
|
32
|
+
// Transparent rather than removed so the content box doesn't shift.
|
|
33
|
+
border: 1px solid transparent;
|
|
34
|
+
border-left: 3px solid
|
|
35
|
+
var(--md-sys-color-inverse-primary, var(--color-primary));
|
|
36
|
+
border-radius: var(--md-sys-shape-corner-extra-small, 4px);
|
|
37
|
+
box-shadow: var(--md-sys-elevation-3, var(--shadow-lg));
|
|
38
|
+
|
|
39
|
+
&[data-variant="success"] {
|
|
40
|
+
border-left-color: var(--color-success);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `error-container`, not `error`: the stripe sits on the inverse surface,
|
|
44
|
+
// where `error` (tuned for the normal surface) has almost no contrast.
|
|
45
|
+
&[data-variant="error"] {
|
|
46
|
+
border-left-color: var(
|
|
47
|
+
--md-sys-color-error-container,
|
|
48
|
+
var(--color-error)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&[data-state="open"] {
|
|
53
|
+
animation: toast-in var(--md-sys-motion-duration-short-3, 150ms)
|
|
54
|
+
var(--md-sys-motion-easing-standard-decelerate, ease-out);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&[data-state="closed"] {
|
|
58
|
+
animation: toast-out var(--md-sys-motion-duration-short-3, 150ms)
|
|
59
|
+
var(--md-sys-motion-easing-standard-accelerate, ease-in);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&[data-swipe="move"] {
|
|
63
|
+
transform: translateX(var(--radix-toast-swipe-move-x));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&[data-swipe="end"] {
|
|
67
|
+
animation: toast-swipe-out var(--md-sys-motion-duration-short-3, 150ms)
|
|
68
|
+
var(--md-sys-motion-easing-standard-accelerate, ease-out);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&__title {
|
|
73
|
+
grid-column: 1;
|
|
74
|
+
font-family: var(--md-sys-typescale-label-large-font, var(--font-body));
|
|
75
|
+
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
76
|
+
font-weight: var(--md-sys-typescale-label-large-weight, 500);
|
|
77
|
+
line-height: var(--md-sys-typescale-label-large-line-height, 1.25rem);
|
|
78
|
+
letter-spacing: var(--md-sys-typescale-label-large-tracking, 0.0071em);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
&__description {
|
|
82
|
+
grid-column: 1;
|
|
83
|
+
font-family: var(--md-sys-typescale-body-medium-font, var(--font-body));
|
|
84
|
+
font-size: var(--md-sys-typescale-body-medium-size, 0.875rem);
|
|
85
|
+
font-weight: var(--md-sys-typescale-body-medium-weight, 400);
|
|
86
|
+
line-height: var(--md-sys-typescale-body-medium-line-height, 1.25rem);
|
|
87
|
+
letter-spacing: var(--md-sys-typescale-body-medium-tracking, 0.0179em);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Action and close share the interaction treatment: a currentColor state
|
|
91
|
+
// layer on ::after and an invisible 48px touch target on ::before (px, as on
|
|
92
|
+
// Button — a touch target is a physical size, not a typographic one). The
|
|
93
|
+
// target is centered on both axes because the close glyph is narrow as well
|
|
94
|
+
// as short.
|
|
95
|
+
&__action,
|
|
96
|
+
&__close {
|
|
97
|
+
position: relative;
|
|
98
|
+
isolation: isolate;
|
|
99
|
+
background: transparent;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
|
|
102
|
+
&::before {
|
|
103
|
+
content: "";
|
|
104
|
+
position: absolute;
|
|
105
|
+
top: 50%;
|
|
106
|
+
left: 50%;
|
|
107
|
+
translate: -50% -50%;
|
|
108
|
+
width: 100%;
|
|
109
|
+
height: 100%;
|
|
110
|
+
min-width: 48px;
|
|
111
|
+
min-height: 48px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&::after {
|
|
115
|
+
content: "";
|
|
116
|
+
position: absolute;
|
|
117
|
+
inset: 0;
|
|
118
|
+
border-radius: inherit;
|
|
119
|
+
background: currentColor;
|
|
120
|
+
opacity: 0;
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
transition: opacity var(--md-sys-motion-duration-short-2, 100ms)
|
|
123
|
+
var(--md-sys-motion-easing-standard, ease);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&:hover::after {
|
|
127
|
+
opacity: var(--md-sys-state-hover-opacity, 0.08);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&:focus-visible::after {
|
|
131
|
+
opacity: var(--md-sys-state-focus-opacity, 0.1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&:active::after {
|
|
135
|
+
opacity: var(--md-sys-state-pressed-opacity, 0.1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
&:focus-visible {
|
|
139
|
+
outline: 3px solid
|
|
140
|
+
var(--md-sys-color-secondary, var(--color-border-focus));
|
|
141
|
+
outline-offset: 2px;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
&__action {
|
|
146
|
+
grid-column: 1;
|
|
147
|
+
justify-self: start;
|
|
148
|
+
margin-top: var(--spacing-1, 0.25rem);
|
|
149
|
+
padding: var(--spacing-1, 0.25rem) var(--spacing-3, 0.75rem);
|
|
150
|
+
// MD3's snackbar action is a text button; the border stays, transparent,
|
|
151
|
+
// only so the button keeps its size.
|
|
152
|
+
border: 1px solid transparent;
|
|
153
|
+
border-radius: var(--md-sys-shape-corner-full, 9999px);
|
|
154
|
+
color: var(--md-sys-color-inverse-primary, var(--color-primary));
|
|
155
|
+
font-family: var(--md-sys-typescale-label-large-font, var(--font-body));
|
|
156
|
+
font-size: var(--md-sys-typescale-label-large-size, 0.875rem);
|
|
157
|
+
font-weight: var(--md-sys-typescale-label-large-weight, 500);
|
|
158
|
+
letter-spacing: var(--md-sys-typescale-label-large-tracking, 0.0071em);
|
|
159
|
+
line-height: 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&__close {
|
|
163
|
+
grid-column: 2;
|
|
164
|
+
grid-row: 1 / 3;
|
|
165
|
+
border: none;
|
|
166
|
+
border-radius: var(--md-sys-shape-corner-full, 9999px);
|
|
167
|
+
color: var(--md-sys-color-inverse-on-surface, var(--color-surface));
|
|
168
|
+
// A glyph size, not a type role — the × is sized as an icon.
|
|
169
|
+
font-size: 1.125rem;
|
|
170
|
+
line-height: 1;
|
|
171
|
+
padding: 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@keyframes toast-in {
|
|
176
|
+
from {
|
|
177
|
+
opacity: 0;
|
|
178
|
+
transform: translateY(0.5rem);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
to {
|
|
182
|
+
opacity: 1;
|
|
183
|
+
transform: translateY(0);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@keyframes toast-out {
|
|
188
|
+
from {
|
|
189
|
+
opacity: 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
to {
|
|
193
|
+
opacity: 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@keyframes toast-swipe-out {
|
|
198
|
+
from {
|
|
199
|
+
transform: translateX(var(--radix-toast-swipe-end-x));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
to {
|
|
203
|
+
transform: translateX(100%);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/toast.tsx
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import * as RadixToast from "@radix-ui/react-toast";
|
|
3
|
+
import { getClassName } from "@bht-component-registry/class-names";
|
|
4
|
+
import styles from "./toast.module.scss";
|
|
5
|
+
|
|
6
|
+
export type ToastVariant = "info" | "success" | "error";
|
|
7
|
+
|
|
8
|
+
export interface ToastAction {
|
|
9
|
+
/** Button label, e.g. "Undo" */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Fired on click; the toast dismisses itself afterwards */
|
|
12
|
+
onClick: () => void;
|
|
13
|
+
/**
|
|
14
|
+
* How to achieve the same thing without the toast, for assistive tech.
|
|
15
|
+
* Required by Radix; defaults to `label`.
|
|
16
|
+
*/
|
|
17
|
+
altText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ToastMessage {
|
|
21
|
+
/** Unique identifier, auto-generated by `showToast` */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Bold headline text */
|
|
24
|
+
title: string;
|
|
25
|
+
/** Optional supporting text shown below the title */
|
|
26
|
+
description?: string;
|
|
27
|
+
/** Controls the accent color; defaults to "info" */
|
|
28
|
+
variant?: ToastVariant;
|
|
29
|
+
/** Optional button rendered alongside the dismiss control, e.g. an undo */
|
|
30
|
+
action?: ToastAction;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TOAST_EVENT = "bht:toast";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Queues a toast on any mounted `Toast` component. Dispatches a plain `window`
|
|
37
|
+
* event under the hood, so it's callable from anywhere with no React context
|
|
38
|
+
* required — including directly from the browser console, e.g.
|
|
39
|
+
* `showToast({ title: "Saved", variant: "success" })`.
|
|
40
|
+
*/
|
|
41
|
+
export function showToast(message: Omit<ToastMessage, "id">): void {
|
|
42
|
+
const detail: ToastMessage = { id: crypto.randomUUID(), ...message };
|
|
43
|
+
// `action.onClick` survives this trip intact: `CustomEvent.detail` is handed
|
|
44
|
+
// to same-realm listeners by reference, not structured-cloned, so functions
|
|
45
|
+
// are fine here. This is not a bug in need of a serializable payload.
|
|
46
|
+
window.dispatchEvent(new CustomEvent<ToastMessage>(TOAST_EVENT, { detail }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ToastProps {
|
|
50
|
+
/** How long a toast stays visible before auto-dismissing, in ms. Defaults to 5000. */
|
|
51
|
+
duration?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Mount once, anywhere in the app (e.g. a root layout). Listens for `showToast()`
|
|
56
|
+
* calls and renders each as a dismissible, auto-expiring Radix Toast in a fixed
|
|
57
|
+
* viewport.
|
|
58
|
+
*/
|
|
59
|
+
export const Toast = ({ duration = 5000 }: ToastProps) => {
|
|
60
|
+
const [messages, setMessages] = useState<ToastMessage[]>([]);
|
|
61
|
+
const [, getChildClass] = getClassName({ rootClass: "BHT__Toast", styles });
|
|
62
|
+
|
|
63
|
+
const dismiss = useCallback((id: string) => {
|
|
64
|
+
setMessages((current) => current.filter((message) => message.id !== id));
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const handleToastEvent = (event: Event) => {
|
|
69
|
+
const { detail } = event as CustomEvent<ToastMessage>;
|
|
70
|
+
setMessages((current) => [...current, detail]);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
window.addEventListener(TOAST_EVENT, handleToastEvent);
|
|
74
|
+
return () => window.removeEventListener(TOAST_EVENT, handleToastEvent);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<RadixToast.Provider duration={duration}>
|
|
79
|
+
{messages.map((message) => (
|
|
80
|
+
<RadixToast.Root
|
|
81
|
+
key={message.id}
|
|
82
|
+
className={getChildClass("root")}
|
|
83
|
+
data-variant={message.variant ?? "info"}
|
|
84
|
+
onOpenChange={(open) => !open && dismiss(message.id)}
|
|
85
|
+
>
|
|
86
|
+
<RadixToast.Title className={getChildClass("title")}>
|
|
87
|
+
{message.title}
|
|
88
|
+
</RadixToast.Title>
|
|
89
|
+
{message.description && (
|
|
90
|
+
<RadixToast.Description className={getChildClass("description")}>
|
|
91
|
+
{message.description}
|
|
92
|
+
</RadixToast.Description>
|
|
93
|
+
)}
|
|
94
|
+
{message.action && (
|
|
95
|
+
<RadixToast.Action
|
|
96
|
+
className={getChildClass("action")}
|
|
97
|
+
altText={message.action.altText ?? message.action.label}
|
|
98
|
+
onClick={message.action.onClick}
|
|
99
|
+
>
|
|
100
|
+
{message.action.label}
|
|
101
|
+
</RadixToast.Action>
|
|
102
|
+
)}
|
|
103
|
+
<RadixToast.Close
|
|
104
|
+
className={getChildClass("close")}
|
|
105
|
+
aria-label="Dismiss"
|
|
106
|
+
>
|
|
107
|
+
×
|
|
108
|
+
</RadixToast.Close>
|
|
109
|
+
</RadixToast.Root>
|
|
110
|
+
))}
|
|
111
|
+
<RadixToast.Viewport className={getChildClass("viewport")} />
|
|
112
|
+
</RadixToast.Provider>
|
|
113
|
+
);
|
|
114
|
+
};
|