@kennycorrea/ui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/atoms/Icon/Icon.d.ts +4 -0
- package/dist/atoms/Icon/Icon.js +5 -0
- package/dist/atoms/Toast/Toast.d.ts +59 -0
- package/dist/atoms/Toast/Toast.js +96 -0
- package/dist/atoms/Toast/index.d.ts +1 -0
- package/dist/atoms/Toast/index.js +1 -0
- package/dist/atoms/index.d.ts +1 -0
- package/dist/atoms/index.js +1 -0
- package/dist/strings.d.ts +2 -0
- package/dist/strings.js +1 -0
- package/package.json +2 -1
- package/tokens.css +23 -0
package/README.md
CHANGED
|
@@ -237,3 +237,23 @@ worth doing as its own commit rather than inside this one.
|
|
|
237
237
|
⚠ **A `file:` install was a symlink; a version install is a copy.** Edits here no longer appear in
|
|
238
238
|
a consumer. An app that needs an unreleased change wants `npm link` or a temporary `file:`
|
|
239
239
|
override — never a copied file, which is the drift this package exists to end.
|
|
240
|
+
|
|
241
|
+
## Toasts — 0.3.0
|
|
242
|
+
|
|
243
|
+
Mount `<Toaster />` once, inside `<Theme>`, and call `toast()` from anywhere — it is a factory
|
|
244
|
+
over a module-level store, so each call stacks another toast and returns its id.
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<Toaster duration={5000} position="bottom-right" />
|
|
248
|
+
|
|
249
|
+
toast({ title: 'Saved' }); // info by default
|
|
250
|
+
toast.success({ title: 'Booking moved', description: 'Tue 14:30' });
|
|
251
|
+
toast.warning({ title: 'Close to the IVA ceiling', duration: 10_000 });
|
|
252
|
+
const id = toast.error({ title: 'Payment failed', duration: Infinity });
|
|
253
|
+
toast.info({ title: 'Cancelled', action: { label: 'Undo', onClick: restore } });
|
|
254
|
+
toast.dismiss(id); // or toast.dismiss() for all
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`duration` overrides the `Toaster` default per toast. An action closes the toast after its
|
|
258
|
+
`onClick` unless `keepOpen` is set. Passing an existing `id` replaces that toast in place.
|
|
259
|
+
The close button's name comes from `UiStrings.dismiss`, so feed it through `UiStringsProvider`.
|
|
@@ -31,6 +31,10 @@ declare const PATHS: {
|
|
|
31
31
|
readonly 'close-circle': "M12 21a9 9 0 100-18 9 9 0 000 18zM9.5 9.5l5 5M14.5 9.5l-5 5";
|
|
32
32
|
readonly 'chevron-up': "M6 15l6-6 6 6";
|
|
33
33
|
readonly 'chevron-down': "M6 9l6 6 6-6";
|
|
34
|
+
readonly 'check-circle': "M12 21a9 9 0 100-18 9 9 0 000 18zM8.5 12.5l2.5 2.5 4.5-5";
|
|
35
|
+
readonly warning: "M12 3l10 18H2zM12 10v4M12 17.5v.01";
|
|
36
|
+
readonly error: "M12 21a9 9 0 100-18 9 9 0 000 18zM12 7.5v5M12 16v.01";
|
|
37
|
+
readonly info: "M12 21a9 9 0 100-18 9 9 0 000 18zM12 11v5M12 8v.01";
|
|
34
38
|
};
|
|
35
39
|
export type IconName = keyof typeof PATHS;
|
|
36
40
|
export interface IconProps {
|
package/dist/atoms/Icon/Icon.js
CHANGED
|
@@ -37,6 +37,11 @@ const PATHS = {
|
|
|
37
37
|
// section both point along the axis the content moves in.
|
|
38
38
|
'chevron-up': 'M6 15l6-6 6 6',
|
|
39
39
|
'chevron-down': 'M6 9l6 6 6-6',
|
|
40
|
+
// The four toast types: each a shape that reads without its colour.
|
|
41
|
+
'check-circle': 'M12 21a9 9 0 100-18 9 9 0 000 18zM8.5 12.5l2.5 2.5 4.5-5',
|
|
42
|
+
warning: 'M12 3l10 18H2zM12 10v4M12 17.5v.01',
|
|
43
|
+
error: 'M12 21a9 9 0 100-18 9 9 0 000 18zM12 7.5v5M12 16v.01',
|
|
44
|
+
info: 'M12 21a9 9 0 100-18 9 9 0 000 18zM12 11v5M12 8v.01',
|
|
40
45
|
};
|
|
41
46
|
export function Icon({ name, className, label }) {
|
|
42
47
|
return (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", className: cn('size-5 shrink-0', className), "aria-hidden": label ? undefined : true, role: label ? 'img' : undefined, "aria-label": label, children: _jsx("path", { d: PATHS[name] }) }));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type ToastType = 'success' | 'warning' | 'error' | 'info';
|
|
3
|
+
export interface ToastAction {
|
|
4
|
+
label: string;
|
|
5
|
+
onClick: () => void;
|
|
6
|
+
/**
|
|
7
|
+
* Leaves the toast open after the click. Closing is the default because nearly every
|
|
8
|
+
* toast action ("Undo", "View") is finished the moment it is pressed.
|
|
9
|
+
*/
|
|
10
|
+
keepOpen?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ToastOptions {
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
description?: ReactNode;
|
|
15
|
+
/** Defaults to `info`. */
|
|
16
|
+
type?: ToastType;
|
|
17
|
+
/**
|
|
18
|
+
* Milliseconds before it closes on its own, overriding the `<Toaster>` default.
|
|
19
|
+
* `Infinity` keeps it until dismissed — right for an error the user has to read.
|
|
20
|
+
*/
|
|
21
|
+
duration?: number;
|
|
22
|
+
action?: ToastAction;
|
|
23
|
+
/** Reusing an id replaces that toast in place rather than stacking a second one. */
|
|
24
|
+
id?: string;
|
|
25
|
+
}
|
|
26
|
+
declare function show(options: ToastOptions): string;
|
|
27
|
+
type Shorthand = (options: Omit<ToastOptions, 'type'>) => string;
|
|
28
|
+
/**
|
|
29
|
+
* The factory. Each call stacks a new toast and returns its id.
|
|
30
|
+
*
|
|
31
|
+
* toast({ title: 'Saved' });
|
|
32
|
+
* toast.error({ title: 'Payment failed', duration: Infinity });
|
|
33
|
+
* toast.success({ title: 'Booking cancelled', action: { label: 'Undo', onClick: restore } });
|
|
34
|
+
* toast.dismiss(id);
|
|
35
|
+
*/
|
|
36
|
+
export declare const toast: typeof show & {
|
|
37
|
+
success: Shorthand;
|
|
38
|
+
warning: Shorthand;
|
|
39
|
+
error: Shorthand;
|
|
40
|
+
info: Shorthand;
|
|
41
|
+
/** Closes one toast, or every toast when called with no id. */
|
|
42
|
+
dismiss(id?: string): void;
|
|
43
|
+
};
|
|
44
|
+
export interface ToasterProps {
|
|
45
|
+
/** Milliseconds a toast stays when it was not given its own `duration`. */
|
|
46
|
+
duration?: number;
|
|
47
|
+
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
|
|
48
|
+
/** The landmark name screen readers announce, e.g. "Notificações ({hotkey})". */
|
|
49
|
+
label?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Renders the stack. Mount once, inside `<Theme>` so the toasts take the app's palette.
|
|
53
|
+
*
|
|
54
|
+
* Built on Radix's toast primitive rather than Themes, which has none: that is what gives the
|
|
55
|
+
* stack its live-region announcements, the F8 hotkey to reach it, pause-on-hover and swipe to
|
|
56
|
+
* dismiss.
|
|
57
|
+
*/
|
|
58
|
+
export declare function Toaster({ duration, position, label }: ToasterProps): import("react").JSX.Element;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as RadixToast from '@radix-ui/react-toast';
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
import { useUiStrings } from '../../strings.js';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
import { Icon } from '../Icon/Icon.js';
|
|
7
|
+
/**
|
|
8
|
+
* A module-level store, so `toast()` can be called from anywhere — an event handler, a
|
|
9
|
+
* mutation's `onError`, a store action — without threading a hook through to it.
|
|
10
|
+
*
|
|
11
|
+
* One store per bundle is the intent: an app renders exactly one `<Toaster>`. Two would show
|
|
12
|
+
* every toast twice, which is a mistake to fix in the app rather than to guard against here.
|
|
13
|
+
*/
|
|
14
|
+
let records = [];
|
|
15
|
+
const listeners = new Set();
|
|
16
|
+
let counter = 0;
|
|
17
|
+
function emit(next) {
|
|
18
|
+
records = next;
|
|
19
|
+
for (const listener of listeners)
|
|
20
|
+
listener();
|
|
21
|
+
}
|
|
22
|
+
function subscribe(listener) {
|
|
23
|
+
listeners.add(listener);
|
|
24
|
+
return () => listeners.delete(listener);
|
|
25
|
+
}
|
|
26
|
+
function show(options) {
|
|
27
|
+
const id = options.id ?? `toast-${++counter}`;
|
|
28
|
+
const record = { ...options, id, type: options.type ?? 'info', open: true };
|
|
29
|
+
const exists = records.some((r) => r.id === id);
|
|
30
|
+
emit(exists ? records.map((r) => (r.id === id ? record : r)) : [...records, record]);
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The factory. Each call stacks a new toast and returns its id.
|
|
35
|
+
*
|
|
36
|
+
* toast({ title: 'Saved' });
|
|
37
|
+
* toast.error({ title: 'Payment failed', duration: Infinity });
|
|
38
|
+
* toast.success({ title: 'Booking cancelled', action: { label: 'Undo', onClick: restore } });
|
|
39
|
+
* toast.dismiss(id);
|
|
40
|
+
*/
|
|
41
|
+
export const toast = Object.assign(show, {
|
|
42
|
+
success: ((o) => show({ ...o, type: 'success' })),
|
|
43
|
+
warning: ((o) => show({ ...o, type: 'warning' })),
|
|
44
|
+
error: ((o) => show({ ...o, type: 'error' })),
|
|
45
|
+
info: ((o) => show({ ...o, type: 'info' })),
|
|
46
|
+
/** Closes one toast, or every toast when called with no id. */
|
|
47
|
+
dismiss(id) {
|
|
48
|
+
emit(records.map((r) => (id === undefined || r.id === id ? { ...r, open: false } : r)));
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
/** Removed after the close animation, not at close, or the exit would be cut off. */
|
|
52
|
+
function remove(id) {
|
|
53
|
+
emit(records.filter((r) => r.id !== id));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Themes' colour vocabulary, matching `Badge`: `data-accent-color` re-points `--accent-*` at
|
|
57
|
+
* that scale for the subtree, so the stripe and icon need no colour of their own.
|
|
58
|
+
*/
|
|
59
|
+
const TYPES = {
|
|
60
|
+
success: { color: 'jade', icon: 'check-circle' },
|
|
61
|
+
warning: { color: 'amber', icon: 'warning' },
|
|
62
|
+
error: { color: 'red', icon: 'error' },
|
|
63
|
+
info: { color: 'blue', icon: 'info' },
|
|
64
|
+
};
|
|
65
|
+
const POSITIONS = {
|
|
66
|
+
'top-right': 'top-0 right-0',
|
|
67
|
+
'top-left': 'top-0 left-0',
|
|
68
|
+
'bottom-right': 'bottom-0 right-0 flex-col-reverse',
|
|
69
|
+
'bottom-left': 'bottom-0 left-0 flex-col-reverse',
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Renders the stack. Mount once, inside `<Theme>` so the toasts take the app's palette.
|
|
73
|
+
*
|
|
74
|
+
* Built on Radix's toast primitive rather than Themes, which has none: that is what gives the
|
|
75
|
+
* stack its live-region announcements, the F8 hotkey to reach it, pause-on-hover and swipe to
|
|
76
|
+
* dismiss.
|
|
77
|
+
*/
|
|
78
|
+
export function Toaster({ duration = 5000, position = 'bottom-right', label }) {
|
|
79
|
+
const items = useSyncExternalStore(subscribe, () => records, () => records);
|
|
80
|
+
const strings = useUiStrings();
|
|
81
|
+
const swipeDirection = position.endsWith('left') ? 'left' : 'right';
|
|
82
|
+
return (_jsxs(RadixToast.Provider, { duration: duration, swipeDirection: swipeDirection, label: label, children: [items.map((item) => {
|
|
83
|
+
const { color, icon } = TYPES[item.type];
|
|
84
|
+
return (_jsxs(RadixToast.Root, { open: item.open, duration: item.duration, type: item.type === 'error' ? 'foreground' : 'background', onOpenChange: (open) => {
|
|
85
|
+
if (!open)
|
|
86
|
+
toast.dismiss(item.id);
|
|
87
|
+
}, onAnimationEnd: () => {
|
|
88
|
+
if (!item.open)
|
|
89
|
+
remove(item.id);
|
|
90
|
+
}, "data-accent-color": color, className: "ui-toast panel pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden py-3 pr-3 pl-4", children: [_jsx("span", { "aria-hidden": true, className: "absolute inset-y-0 left-0 w-1 bg-[var(--accent-9)]" }), _jsx(Icon, { name: icon, className: "mt-0.5 text-[var(--accent-11)]" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(RadixToast.Title, { className: "text-sm font-medium text-ink", children: item.title }), item.description ? (_jsx(RadixToast.Description, { className: "mt-0.5 text-sm text-ink-muted", children: item.description })) : null] }), item.action ? (_jsx(RadixToast.Action, { altText: item.action.label, onClick: (event) => {
|
|
91
|
+
item.action?.onClick();
|
|
92
|
+
if (item.action?.keepOpen)
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
}, className: cn('shrink-0 rounded-md px-2 py-1 text-sm font-medium', 'text-[var(--accent-11)] hover:bg-[var(--accent-a3)]'), children: item.action.label })) : null, _jsx(RadixToast.Close, { "aria-label": strings.dismiss, className: "shrink-0 rounded-md p-1 text-ink-muted hover:bg-[var(--gray-a3)]", children: _jsx(Icon, { name: "close", className: "size-4" }) })] }, item.id));
|
|
95
|
+
}), _jsx(RadixToast.Viewport, { className: cn('fixed z-50 m-0 flex w-full max-w-sm list-none flex-col gap-2 p-4 outline-none', POSITIONS[position]) })] }));
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Toast.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Toast.js';
|
package/dist/atoms/index.d.ts
CHANGED
package/dist/atoms/index.js
CHANGED
package/dist/strings.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface UiStrings {
|
|
|
18
18
|
loading: string;
|
|
19
19
|
/** The placeholder in a Select that was given none. */
|
|
20
20
|
select: string;
|
|
21
|
+
/** The accessible name of a toast's close button. */
|
|
22
|
+
dismiss: string;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Feeds the atoms their strings, once, near the root.
|
package/dist/strings.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kennycorrea/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Upfront's house component layer: atoms over Radix Themes, the token bridge, and the accent logic.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"react-dom": "^19.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@radix-ui/react-toast": "^1.2.23",
|
|
36
37
|
"clsx": "^2.1.1",
|
|
37
38
|
"tailwind-merge": "^3.7.0"
|
|
38
39
|
},
|
package/tokens.css
CHANGED
|
@@ -238,3 +238,26 @@
|
|
|
238
238
|
was a blend with nothing to reveal. */
|
|
239
239
|
@apply rounded-2xl bg-sheet shadow-panel ring-1 ring-hairline;
|
|
240
240
|
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Toast motion. Radix exposes state and swipe through data attributes and CSS variables, and
|
|
244
|
+
* `Toaster` removes a record on `animationend` — so the closed state must animate, or a
|
|
245
|
+
* dismissed toast would never leave the store.
|
|
246
|
+
*/
|
|
247
|
+
@keyframes ui-toast-in {
|
|
248
|
+
from { opacity: 0; transform: translateY(0.5rem); }
|
|
249
|
+
}
|
|
250
|
+
@keyframes ui-toast-out {
|
|
251
|
+
to { opacity: 0; }
|
|
252
|
+
}
|
|
253
|
+
@keyframes ui-toast-swipe-out {
|
|
254
|
+
to { transform: translateX(calc(var(--radix-toast-swipe-end-x) + 100%)); opacity: 0; }
|
|
255
|
+
}
|
|
256
|
+
.ui-toast[data-state='open'] { animation: ui-toast-in 180ms ease-out; }
|
|
257
|
+
.ui-toast[data-state='closed'] { animation: ui-toast-out 140ms ease-in forwards; }
|
|
258
|
+
.ui-toast[data-swipe='move'] { transform: translateX(var(--radix-toast-swipe-move-x)); }
|
|
259
|
+
.ui-toast[data-swipe='cancel'] { transform: translateX(0); transition: transform 200ms ease-out; }
|
|
260
|
+
.ui-toast[data-swipe='end'] { animation: ui-toast-swipe-out 120ms ease-out forwards; }
|
|
261
|
+
@media (prefers-reduced-motion: reduce) {
|
|
262
|
+
.ui-toast[data-state] { animation-duration: 1ms; }
|
|
263
|
+
}
|