@apliteni/apliteni-ui 0.129.0 → 0.130.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/package.json +1 -1
- package/react/README.md +7 -0
- package/react/dist/index.d.ts +3 -0
- package/react/dist/index.js +10 -8
- package/src/components/toasts.js +38 -14
- package/src/styles/callout.css +13 -3
package/package.json
CHANGED
package/react/README.md
CHANGED
|
@@ -585,6 +585,13 @@ or neutral. Notices disappear after five seconds, pausing while hovered or focus
|
|
|
585
585
|
Danger notices are announced assertively; other tones are polite. An `action` with a `label` and
|
|
586
586
|
`onClick` stays until selected or dismissed. New notices appear at the bottom.
|
|
587
587
|
|
|
588
|
+
Pass `compact: true` to show only the title, or `dismissible: false` to hide the
|
|
589
|
+
close button. Hiding the close button keeps the five-second timer for notices
|
|
590
|
+
without actions; notices with actions remain until their action is selected, so a
|
|
591
|
+
notice with an action and no close button has no other dismissal affordance.
|
|
592
|
+
Pass `timer: 12` to dismiss a notice after that many seconds. Pass `timer: false` or `timer: 0`
|
|
593
|
+
to keep a notice until it is dismissed.
|
|
594
|
+
|
|
588
595
|
```tsx
|
|
589
596
|
function SaveButton() {
|
|
590
597
|
const push = useToast();
|
package/react/dist/index.d.ts
CHANGED
|
@@ -778,6 +778,9 @@ declare const SearchField: react.ForwardRefExoticComponent<Omit<InputHTMLAttribu
|
|
|
778
778
|
|
|
779
779
|
type ToastNotice = {
|
|
780
780
|
tone?: 'success' | 'danger' | 'warn' | 'info' | 'neutral';
|
|
781
|
+
compact?: boolean;
|
|
782
|
+
dismissible?: boolean;
|
|
783
|
+
timer?: boolean | number;
|
|
781
784
|
title: string;
|
|
782
785
|
text?: string;
|
|
783
786
|
action?: {
|
package/react/dist/index.js
CHANGED
|
@@ -3432,11 +3432,13 @@ function Notice({ notice, remove }) {
|
|
|
3432
3432
|
const root = useRef18(null);
|
|
3433
3433
|
const [leaving2, setLeaving] = useState12(false);
|
|
3434
3434
|
const dismissed = useRef18(false);
|
|
3435
|
-
const remaining = useRef18(5e3);
|
|
3436
3435
|
const [hovered, setHovered] = useState12(false);
|
|
3437
3436
|
const [focused, setFocused] = useState12(false);
|
|
3438
3437
|
const paused = hovered || focused;
|
|
3439
3438
|
const tone = notice.tone ?? "success";
|
|
3439
|
+
const timed = !notice.action && notice.timer !== false && notice.timer !== 0;
|
|
3440
|
+
const duration = typeof notice.timer === "number" ? notice.timer * 1e3 : 5e3;
|
|
3441
|
+
const remaining = useRef18(duration);
|
|
3440
3442
|
const dismiss = useCallback5(() => {
|
|
3441
3443
|
if (dismissed.current) return;
|
|
3442
3444
|
dismissed.current = true;
|
|
@@ -3458,19 +3460,19 @@ function Notice({ notice, remove }) {
|
|
|
3458
3460
|
}
|
|
3459
3461
|
}, [leaving2, remove]);
|
|
3460
3462
|
useEffect12(() => {
|
|
3461
|
-
if (leaving2 ||
|
|
3463
|
+
if (leaving2 || !timed || paused) return;
|
|
3462
3464
|
const started = Date.now();
|
|
3463
3465
|
const timer = setTimeout(dismiss, remaining.current);
|
|
3464
3466
|
return () => {
|
|
3465
3467
|
clearTimeout(timer);
|
|
3466
3468
|
remaining.current = Math.max(0, remaining.current - (Date.now() - started));
|
|
3467
3469
|
};
|
|
3468
|
-
}, [dismiss, leaving2,
|
|
3470
|
+
}, [dismiss, leaving2, paused, timed]);
|
|
3469
3471
|
return /* @__PURE__ */ jsxs27(
|
|
3470
3472
|
"div",
|
|
3471
3473
|
{
|
|
3472
3474
|
ref: root,
|
|
3473
|
-
className: `ui-toast ui-toast--${tone} ui-toast--soft${leaving2 ? " is-leaving" : ""}`,
|
|
3475
|
+
className: `ui-toast ui-toast--${tone} ui-toast--soft${notice.compact ? " ui-toast--compact" : ""}${leaving2 ? " is-leaving" : ""}`,
|
|
3474
3476
|
role: tone === "danger" ? "alert" : "status",
|
|
3475
3477
|
"aria-live": tone === "danger" ? "assertive" : "polite",
|
|
3476
3478
|
onMouseEnter: () => setHovered(true),
|
|
@@ -3483,7 +3485,7 @@ function Notice({ notice, remove }) {
|
|
|
3483
3485
|
/* @__PURE__ */ jsx32("span", { className: "ui-toast__icon", children: /* @__PURE__ */ jsx32(Icon, { name: glyphs[tone] }) }),
|
|
3484
3486
|
/* @__PURE__ */ jsxs27("div", { className: "ui-toast__body", children: [
|
|
3485
3487
|
/* @__PURE__ */ jsx32("div", { className: "ui-toast__title", children: notice.title }),
|
|
3486
|
-
notice.text && /* @__PURE__ */ jsx32("div", { className: "ui-toast__text", children: notice.text })
|
|
3488
|
+
!notice.compact && notice.text && /* @__PURE__ */ jsx32("div", { className: "ui-toast__text", children: notice.text })
|
|
3487
3489
|
] }),
|
|
3488
3490
|
notice.action && /* @__PURE__ */ jsx32(
|
|
3489
3491
|
"button",
|
|
@@ -3500,13 +3502,13 @@ function Notice({ notice, remove }) {
|
|
|
3500
3502
|
children: notice.action.label
|
|
3501
3503
|
}
|
|
3502
3504
|
),
|
|
3503
|
-
/* @__PURE__ */ jsx32("button", { type: "button", className: "ui-toast__close", "aria-label": "Dismiss", disabled: leaving2, onClick: dismiss, children: /* @__PURE__ */ jsx32(Icon, { name: "x" }) }),
|
|
3504
|
-
|
|
3505
|
+
notice.dismissible !== false && /* @__PURE__ */ jsx32("button", { type: "button", className: "ui-toast__close", "aria-label": "Dismiss", disabled: leaving2, onClick: dismiss, children: /* @__PURE__ */ jsx32(Icon, { name: "x" }) }),
|
|
3506
|
+
timed && /* @__PURE__ */ jsx32(
|
|
3505
3507
|
"span",
|
|
3506
3508
|
{
|
|
3507
3509
|
className: "ui-toast__timer is-running",
|
|
3508
3510
|
"aria-hidden": "true",
|
|
3509
|
-
style: { animationPlayState: paused ? "paused" : "running" }
|
|
3511
|
+
style: { animationDuration: `${duration}ms`, animationPlayState: paused ? "paused" : "running" }
|
|
3510
3512
|
}
|
|
3511
3513
|
)
|
|
3512
3514
|
]
|
package/src/components/toasts.js
CHANGED
|
@@ -16,6 +16,9 @@ import { toast } from './index.js';
|
|
|
16
16
|
const reduceMotion = () =>
|
|
17
17
|
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
18
18
|
|
|
19
|
+
const resolve = (container) =>
|
|
20
|
+
(typeof container === 'string' ? document.querySelector(container) : container) || null;
|
|
21
|
+
|
|
19
22
|
// Slide-and-fade a toast out, then remove it. Idempotent.
|
|
20
23
|
export function dismissToast(el) {
|
|
21
24
|
if (!el || el.dataset.leaving) return;
|
|
@@ -36,20 +39,41 @@ function wireToast(el) {
|
|
|
36
39
|
el.querySelector('.ui-toast__close')?.addEventListener('click', () => dismissToast(el));
|
|
37
40
|
|
|
38
41
|
const bar = el.querySelector('[data-toast-timer]');
|
|
39
|
-
if (bar
|
|
42
|
+
if (bar) {
|
|
40
43
|
bar.classList.add('is-running');
|
|
41
44
|
const dur = parseFloat(getComputedStyle(el).getPropertyValue('--toast-dur')) || 5;
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
let remaining = dur * 1000;
|
|
46
|
+
let started = Date.now();
|
|
47
|
+
let timer = setTimeout(() => dismissToast(el), remaining);
|
|
48
|
+
// The line's own pause lives in the sheet, on :hover and :focus-within, so
|
|
49
|
+
// it covers a keyboard reader too. What is left here is the countdown that
|
|
50
|
+
// has to stop with it. This runs even under reduced motion, where the line
|
|
51
|
+
// is hidden but the reader still gets the same hold.
|
|
52
|
+
let held = false;
|
|
53
|
+
const hold = () => {
|
|
54
|
+
if (held) return;
|
|
55
|
+
held = true;
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
const visualLeft = !reduceMotion()
|
|
58
|
+
? parseFloat(getComputedStyle(bar).transform.split(',')[0].slice(7))
|
|
59
|
+
: NaN;
|
|
60
|
+
remaining = Number.isFinite(visualLeft)
|
|
61
|
+
? dur * 1000 * visualLeft
|
|
62
|
+
: Math.max(0, remaining - (Date.now() - started));
|
|
63
|
+
};
|
|
64
|
+
const release = () => {
|
|
65
|
+
if (!held || el.matches(':hover, :focus-within')) return;
|
|
66
|
+
held = false;
|
|
67
|
+
started = Date.now();
|
|
68
|
+
timer = setTimeout(() => dismissToast(el), remaining);
|
|
69
|
+
};
|
|
70
|
+
el.addEventListener('mouseenter', hold);
|
|
71
|
+
el.addEventListener('mouseleave', release);
|
|
72
|
+
el.addEventListener('focusin', hold);
|
|
73
|
+
// At focusout the old target has already blurred and the new one has not
|
|
74
|
+
// focused yet, so :focus-within reads false even for a move from the close
|
|
75
|
+
// button to the action. relatedTarget is the only thing that knows.
|
|
76
|
+
el.addEventListener('focusout', (e) => { if (!el.contains(e.relatedTarget)) release(); });
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
// Swipe-to-dismiss (ignore drags that start on a button).
|
|
@@ -77,7 +101,7 @@ function wireToast(el) {
|
|
|
77
101
|
|
|
78
102
|
// Wire every toast currently inside a container (string selector or element).
|
|
79
103
|
export function wireToastStack(container) {
|
|
80
|
-
const root =
|
|
104
|
+
const root = resolve(container);
|
|
81
105
|
if (!root) return null;
|
|
82
106
|
root.querySelectorAll('.ui-toast').forEach(wireToast);
|
|
83
107
|
return root;
|
|
@@ -85,7 +109,7 @@ export function wireToastStack(container) {
|
|
|
85
109
|
|
|
86
110
|
// Build a toast from opts, prepend it to a live stack (newest on top), wire it.
|
|
87
111
|
export function pushToast(container, opts = {}) {
|
|
88
|
-
const root =
|
|
112
|
+
const root = resolve(container);
|
|
89
113
|
if (!root) return null;
|
|
90
114
|
root.insertAdjacentHTML('afterbegin', toast(opts));
|
|
91
115
|
const el = root.firstElementChild;
|
package/src/styles/callout.css
CHANGED
|
@@ -93,9 +93,16 @@
|
|
|
93
93
|
.ui-toast__close:hover { color: var(--text); }
|
|
94
94
|
.ui-toast__close:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); border-radius: var(--radius-xs); }
|
|
95
95
|
|
|
96
|
-
/* auto-dismiss progress
|
|
96
|
+
/* auto-dismiss progress line — .is-running is added by wireToastStack() */
|
|
97
97
|
.ui-toast__timer { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: var(--toast-accent); opacity: 0.85; transform-origin: left; }
|
|
98
|
-
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; /* motion: ambient — the
|
|
98
|
+
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; /* motion: ambient — the line spends the toast's dismiss timer, which the caller sets */ }
|
|
99
|
+
/* The line spends the notice's own timer, so it stops exactly when the timer
|
|
100
|
+
does: under the pointer, and while focus is anywhere inside. Holding both
|
|
101
|
+
pauses here rather than in each face's event wiring keeps the line and the
|
|
102
|
+
countdown from drifting apart, and gives a keyboard reader the same reprieve
|
|
103
|
+
a pointer reader gets. */
|
|
104
|
+
.ui-toast:hover > .ui-toast__timer.is-running,
|
|
105
|
+
.ui-toast:focus-within > .ui-toast__timer.is-running { animation-play-state: paused; }
|
|
99
106
|
|
|
100
107
|
/* Legacy style classes share the neutral card. */
|
|
101
108
|
.ui-toast--soft, .ui-toast--solid, .ui-toast--outline { background: var(--surface); color: var(--text); }
|
|
@@ -110,7 +117,10 @@
|
|
|
110
117
|
|
|
111
118
|
@media (prefers-reduced-motion: reduce) {
|
|
112
119
|
.ui-toast, .ui-toast.is-leaving { animation: none; }
|
|
113
|
-
|
|
120
|
+
/* A line that cannot move sits full for the whole five seconds and reads as
|
|
121
|
+
"no time spent yet" — the opposite of what it is there to say. The notice
|
|
122
|
+
still expires on time; only the false readout goes. */
|
|
123
|
+
.ui-toast__timer { display: none; }
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
/* stack container for queued toasts */
|