@yusufceng1z/ding 0.0.1 → 0.0.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 ADDED
@@ -0,0 +1,39 @@
1
+ # @yusufceng1z/ding
2
+
3
+ Toasts with real spring physics. One toast that morphs in place. No dependencies, no stylesheet.
4
+
5
+ This is the core: springs, timers, store, sound and a plain DOM renderer. For React or Svelte, use [`@yusufceng1z/ding-react`](https://www.npmjs.com/package/@yusufceng1z/ding-react) or [`@yusufceng1z/ding-svelte`](https://www.npmjs.com/package/@yusufceng1z/ding-svelte).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i @yusufceng1z/ding
11
+ ```
12
+
13
+ ## Use
14
+
15
+ ```ts
16
+ import { mountToaster, toast } from '@yusufceng1z/ding';
17
+
18
+ mountToaster({ position: 'top-center' });
19
+
20
+ toast.success('Saved');
21
+ toast.error('Payment failed', { description: 'Card declined' });
22
+
23
+ toast.promise(save(), {
24
+ loading: 'Saving…',
25
+ success: 'Saved',
26
+ error: (err) => `Could not save: ${err.message}`,
27
+ });
28
+ ```
29
+
30
+ Only one toast shows at a time. A new call morphs the toast on screen instead of stacking a second one. Options, actions, progress, sound and accessibility are covered in the [docs](https://ding.yusufcengiz.dev/docs.html).
31
+
32
+ ## Links
33
+
34
+ - [Live demo and docs](https://ding.yusufcengiz.dev)
35
+ - [Source on GitHub](https://github.com/YusufCeng1z/ding)
36
+
37
+ ## License
38
+
39
+ [MIT](LICENSE)
package/dist/index.cjs CHANGED
@@ -94,8 +94,10 @@ var TIMING = {
94
94
  duration: 5e3
95
95
  };
96
96
  var autoDuration = (type, hasAction) => type === "loading" || type === "error" || hasAction ? 0 : TIMING.duration;
97
+ var DEFAULT_LABELS = { region: "Notifications", dismiss: "Dismiss notification" };
98
+ var definedOnly = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
97
99
  function createToaster() {
98
- let state = { toasts: [], position: "top-center", theme: "system", status: "", alert: "" };
100
+ let state = { toasts: [], position: "top-center", theme: "system", labels: DEFAULT_LABELS, status: "", alert: "" };
99
101
  let flip = false;
100
102
  const subs = /* @__PURE__ */ new Set();
101
103
  const entries = /* @__PURE__ */ new Map();
@@ -278,7 +280,13 @@ function createToaster() {
278
280
  configure(c) {
279
281
  if (c.sound !== void 0) sound = c.sound;
280
282
  if (c.volume !== void 0) volume = c.volume;
281
- state = { ...state, ...c.position && { position: c.position }, ...c.theme && { theme: c.theme } };
283
+ state = {
284
+ ...state,
285
+ ...c.position && { position: c.position },
286
+ ...c.theme && { theme: c.theme },
287
+ // undefined keys are skipped so `{ region: undefined }` keeps the current label
288
+ ...c.labels && { labels: { ...state.labels, ...definedOnly(c.labels) } }
289
+ };
282
290
  emit();
283
291
  },
284
292
  subscribe(fn) {
@@ -363,7 +371,7 @@ position:fixed;inset:0;z-index:2147483647;pointer-events:none;font:500 14px/1.3
363
371
  .tst-viewport[data-position$=left]{--tst-ox:0%;--tx:0px}
364
372
  .tst-viewport[data-position$=right]{--tst-ox:100%;--tx:0px}
365
373
  .tst-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
366
- .tst-slot{position:absolute;pointer-events:auto;touch-action:none;user-select:none;cursor:grab;--tst-i:0;
374
+ .tst-slot{position:absolute;pointer-events:auto;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;cursor:grab;--tst-i:0;
367
375
  --y:calc(var(--tst-dir)*-1*var(--tst-i)*var(--tst-stack-offset));--s:calc(1 - var(--tst-i)*var(--tst-stack-scale));
368
376
  transform:translate(calc(var(--tx) + var(--dx,0px)),calc(var(--y) + var(--dy,0px))) scale(var(--s));
369
377
  transform-origin:var(--tst-ox) var(--tst-oy);z-index:calc(100 - var(--tst-i));
@@ -388,7 +396,7 @@ translate:0 var(--tst-y0);scale:var(--tst-enter-scale,.5);opacity:0;will-change:
388
396
  .tst[data-type=warning]{--c:var(--tst-warning)}
389
397
  .tst[data-type=info]{--c:var(--tst-info)}
390
398
 
391
- .tst-icon{position:absolute;inset-inline-start:0;top:0;width:var(--tst-size);height:var(--tst-size);display:grid;place-items:center}
399
+ .tst-icon{position:absolute;inset-inline-start:-1px;top:-1px;width:var(--tst-size);height:var(--tst-size);display:grid;place-items:center}
392
400
  .tst-media{width:28px;height:28px;border-radius:50%;object-fit:cover;outline:1px solid light-dark(oklch(0 0 0/.1),oklch(1 0 0/.1));outline-offset:-1px;animation:tst-swap var(--tst-stack-dur) var(--tst-stack) .1s both}
393
401
  .tst-badge{position:relative;width:24px;height:24px;display:grid;place-items:center}
394
402
  .tst-svg{width:22px;height:22px;fill:none;stroke:var(--c);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;transition:stroke .3s cubic-bezier(.2,0,0,1);animation:tst-swap var(--tst-stack-dur) var(--tst-stack) .1s both}
@@ -409,7 +417,7 @@ translate:0 var(--tst-y0);scale:var(--tst-enter-scale,.5);opacity:0;will-change:
409
417
  .tst[data-type=info] .tst-svg path:nth-child(3){stroke-dasharray:none;stroke-dashoffset:0;animation:tst-dot .4s var(--tst-pop) var(--md) both}
410
418
  .tst[data-type=info] .tst-svg path:nth-child(2){animation-delay:calc(var(--md) + .16s)}
411
419
 
412
- .tst-body{position:absolute;inset-inline-start:calc(var(--tst-size) - 1px);top:-1px;display:flex;flex-wrap:wrap;align-items:center;gap:0 8px;width:max-content;max-width:calc(100vw - 2*var(--tst-pad) - var(--tst-size) + 1px);min-height:var(--tst-size);padding-inline-end:18px;
420
+ .tst-body{position:absolute;inset-inline-start:calc(var(--tst-size) - 1px);top:-1px;display:flex;flex-wrap:wrap;align-items:center;gap:0 8px;width:max-content;max-width:calc(100vw - 2*var(--tst-pad) - var(--tst-size) + 1px);min-height:var(--tst-size);padding-inline-end:8px;
413
421
  opacity:calc(var(--tp)*1.5 - .25);filter:blur(calc((1 - var(--tp))*10px));transform:translateX(calc((1 - var(--tp))*-12px*var(--tst-flip,1)))}
414
422
  .tst-body:has(.tst-action,.tst-close){padding-inline-end:8px;padding-bottom:0}
415
423
  .tst-body[data-wrapped]{padding-bottom:10px}
@@ -429,12 +437,16 @@ opacity:calc(var(--tp)*1.5 - .25);filter:blur(calc((1 - var(--tp))*10px));transf
429
437
  .tst[data-phase=open] .tst-desc{animation:tst-in-up var(--tst-pop-dur) var(--tst-pop) .1s both}
430
438
  .tst[data-phase=open] .tst-action,.tst[data-phase=open] .tst-close{animation:tst-in-btn var(--tst-pop-dur) var(--tst-pop) .16s backwards}
431
439
  .tst[data-phase=open] .tst-secondary{animation-delay:.1s}
432
- .tst-action,.tst-close{flex:none;border:0;color:inherit;font:inherit;cursor:pointer;background:transparent;
440
+ .tst-action,.tst-close{position:relative;flex:none;margin:0;border:0;color:inherit;font:inherit;cursor:pointer;background:transparent;
433
441
  opacity:calc(var(--ap)*1.3 - .15);transform:scale(calc(.85 + var(--ap)*.15));
434
442
  transition-property:background-color,scale;transition-duration:.15s;transition-timing-function:ease-out}
435
443
  .tst-action{height:30px;padding:0 14px;border-radius:999px;font-size:13px;font-weight:600;background:var(--tst-fg);color:var(--tst-bg)}
436
444
  .tst-secondary{background:transparent;color:color-mix(in srgb,var(--tst-fg) 65%,transparent);padding:0 10px}
437
445
  .tst-close{width:30px;height:30px;padding:0;border-radius:50%;display:grid;place-items:center}
446
+ /* 30px look, 44px target: taller hit area, narrow enough sideways that neighbours (8px apart) never overlap */
447
+ .tst-action::before,.tst-close::before{content:'';position:absolute;inset:-7px -3px}
448
+ .tst-slot:focus-visible{outline:none}
449
+ .tst-slot:focus-visible .tst{outline:2px solid var(--tst-fg);outline-offset:-5px}
438
450
  .tst-close svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round}
439
451
  @media (hover:hover){.tst-action:hover{background:color-mix(in srgb,var(--tst-fg) 82%,var(--tst-bg))}.tst-secondary:hover,.tst-close:hover{background:color-mix(in srgb,var(--tst-fg) 10%,transparent);color:var(--tst-fg)}}
440
452
  .tst-action:not(:disabled):active,.tst-close:not(:disabled):active{scale:.96}
@@ -777,10 +789,15 @@ var ICONS = {
777
789
  };
778
790
 
779
791
  // src/vanilla.ts
792
+ var BUSY = "dialog,[role=dialog],[aria-modal=true],[role=menu],[role=listbox],input,textarea,select,[contenteditable]";
780
793
  function onToastKey(e) {
781
794
  if (e.defaultPrevented || !toast.getState().toasts.length) return;
782
- if (e.key === "Escape") toast.dismiss();
783
- else if (e.altKey && e.code === "KeyT") document.querySelector('.tst-viewport button:not([tabindex="-1"])')?.focus();
795
+ if (e.key === "Escape") {
796
+ if (!document.activeElement?.closest(BUSY)) toast.dismiss();
797
+ } else if (e.altKey && e.code === "KeyT") {
798
+ const slots = document.querySelectorAll(".tst-viewport .tst-slot");
799
+ (document.querySelector('.tst-viewport button:not([tabindex="-1"])') ?? slots[slots.length - 1])?.focus();
800
+ }
784
801
  }
785
802
  var el = (tag, cls, parent) => {
786
803
  const n = document.createElement(tag);
@@ -808,7 +825,6 @@ function mountToaster(opts = {}) {
808
825
  toast.configure(opts);
809
826
  const vp = el("div", "tst-viewport", document.body);
810
827
  vp.setAttribute("role", "region");
811
- vp.setAttribute("aria-label", "Notifications");
812
828
  const status = el("div", "tst-sr", vp);
813
829
  status.setAttribute("role", "status");
814
830
  const alert = el("div", "tst-sr", vp);
@@ -821,6 +837,7 @@ function mountToaster(opts = {}) {
821
837
  const nodes = /* @__PURE__ */ new Map();
822
838
  function build(t) {
823
839
  const slot = el("div", "tst-slot", vp);
840
+ slot.tabIndex = -1;
824
841
  el("div", "tst", slot);
825
842
  return { slot };
826
843
  }
@@ -828,6 +845,7 @@ function mountToaster(opts = {}) {
828
845
  const s = toast.getState();
829
846
  vp.dataset.position = s.position;
830
847
  vp.dataset.theme = s.theme;
848
+ vp.setAttribute("aria-label", s.labels.region);
831
849
  status.textContent = s.status;
832
850
  alert.textContent = s.alert;
833
851
  const live = new Set(s.toasts.map((t) => t.id));
@@ -907,10 +925,10 @@ function mountToaster(opts = {}) {
907
925
  if (!close) {
908
926
  close = el("button", "tst-close", body);
909
927
  close.type = "button";
910
- close.setAttribute("aria-label", "Dismiss notification");
911
928
  close.append(svg("", ["M6 6l12 12M18 6L6 18"]));
912
929
  close.onclick = () => toast.dismiss(t.id);
913
930
  }
931
+ close.setAttribute("aria-label", s.labels.dismiss);
914
932
  close.tabIndex = t.phase === "open" ? 0 : -1;
915
933
  }
916
934
  n.off ??= bindToast(n.slot, t.id);
package/dist/index.d.cts CHANGED
@@ -3,6 +3,13 @@ type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading';
3
3
  type Phase = 'enter' | 'open' | 'closing' | 'exit';
4
4
  type Position = 'top-center' | 'top-left' | 'top-right' | 'bottom-center' | 'bottom-left' | 'bottom-right';
5
5
  type Theme = 'system' | 'light' | 'dark';
6
+ /** Accessible names read by screen readers. Pass your own strings to localize them. */
7
+ interface ToastLabels {
8
+ /** Name of the notifications region. Default `Notifications` */
9
+ region: string;
10
+ /** Name of the close button on sticky toasts. Default `Dismiss notification` */
11
+ dismiss: string;
12
+ }
6
13
  interface ToastAction {
7
14
  label: string;
8
15
  /** Return a patch to morph this toast in place (e.g. Undo → "Restored"). Return nothing to dismiss it. */
@@ -58,6 +65,7 @@ interface ToasterState {
58
65
  toasts: ToastItem[];
59
66
  position: Position;
60
67
  theme: Theme;
68
+ labels: ToastLabels;
61
69
  /** text for the persistent polite live region (role=status) */
62
70
  status: string;
63
71
  /** text for the persistent assertive live region (role=alert) */
@@ -96,6 +104,8 @@ declare function createToaster(): {
96
104
  configure(c: {
97
105
  position?: Position;
98
106
  theme?: Theme;
107
+ /** Partial: keys you leave out keep their current value. */
108
+ labels?: Partial<ToastLabels>;
99
109
  sound?: boolean | ((type: ToastType) => void);
100
110
  volume?: number;
101
111
  }): void;
@@ -147,12 +157,13 @@ declare function injectStyles(doc?: Document): void;
147
157
  /** Adapter hook-up: call with the `.tst-slot` element (its first child is the pill). Returns cleanup. */
148
158
  declare function bindToast(slot: HTMLElement, id: string, t?: Toaster): () => void;
149
159
 
150
- /** Shared keyboard handling for every adapter: Escape dismisses, Alt+T moves focus into the toast. */
160
+ /** Shared keyboard handling for every adapter: Escape dismisses, Alt+T moves focus into the toast (which also pauses its timer). */
151
161
  declare function onToastKey(e: KeyboardEvent): void;
152
162
  /** Framework-free renderer. Returns an unmount function. */
153
163
  declare function mountToaster(opts?: {
154
164
  position?: Position;
155
165
  theme?: Theme;
166
+ labels?: Partial<ToastLabels>;
156
167
  }): () => void;
157
168
 
158
169
  /** Play the cue for a toast type. Silent for types without one, and wherever Web Audio is missing or still locked. */
@@ -161,4 +172,4 @@ declare function chime(type: ToastType, volume?: number): void;
161
172
  declare const ICONS: Record<ToastType, string[]>;
162
173
 
163
174
  export { ICONS, TIMING, bindToast, buildCss, chime, createToaster, getSprings, injectStyles, mountToaster, onToastKey, setSprings, springEasing, toast };
164
- export type { Phase, Position, PromiseMessages, SpringConfig, Theme, ToastAction, ToastItem, ToastOptions, ToastPatch, ToastType, Toaster, ToasterState };
175
+ export type { Phase, Position, PromiseMessages, SpringConfig, Theme, ToastAction, ToastItem, ToastLabels, ToastOptions, ToastPatch, ToastType, Toaster, ToasterState };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,13 @@ type ToastType = 'success' | 'error' | 'warning' | 'info' | 'loading';
3
3
  type Phase = 'enter' | 'open' | 'closing' | 'exit';
4
4
  type Position = 'top-center' | 'top-left' | 'top-right' | 'bottom-center' | 'bottom-left' | 'bottom-right';
5
5
  type Theme = 'system' | 'light' | 'dark';
6
+ /** Accessible names read by screen readers. Pass your own strings to localize them. */
7
+ interface ToastLabels {
8
+ /** Name of the notifications region. Default `Notifications` */
9
+ region: string;
10
+ /** Name of the close button on sticky toasts. Default `Dismiss notification` */
11
+ dismiss: string;
12
+ }
6
13
  interface ToastAction {
7
14
  label: string;
8
15
  /** Return a patch to morph this toast in place (e.g. Undo → "Restored"). Return nothing to dismiss it. */
@@ -58,6 +65,7 @@ interface ToasterState {
58
65
  toasts: ToastItem[];
59
66
  position: Position;
60
67
  theme: Theme;
68
+ labels: ToastLabels;
61
69
  /** text for the persistent polite live region (role=status) */
62
70
  status: string;
63
71
  /** text for the persistent assertive live region (role=alert) */
@@ -96,6 +104,8 @@ declare function createToaster(): {
96
104
  configure(c: {
97
105
  position?: Position;
98
106
  theme?: Theme;
107
+ /** Partial: keys you leave out keep their current value. */
108
+ labels?: Partial<ToastLabels>;
99
109
  sound?: boolean | ((type: ToastType) => void);
100
110
  volume?: number;
101
111
  }): void;
@@ -147,12 +157,13 @@ declare function injectStyles(doc?: Document): void;
147
157
  /** Adapter hook-up: call with the `.tst-slot` element (its first child is the pill). Returns cleanup. */
148
158
  declare function bindToast(slot: HTMLElement, id: string, t?: Toaster): () => void;
149
159
 
150
- /** Shared keyboard handling for every adapter: Escape dismisses, Alt+T moves focus into the toast. */
160
+ /** Shared keyboard handling for every adapter: Escape dismisses, Alt+T moves focus into the toast (which also pauses its timer). */
151
161
  declare function onToastKey(e: KeyboardEvent): void;
152
162
  /** Framework-free renderer. Returns an unmount function. */
153
163
  declare function mountToaster(opts?: {
154
164
  position?: Position;
155
165
  theme?: Theme;
166
+ labels?: Partial<ToastLabels>;
156
167
  }): () => void;
157
168
 
158
169
  /** Play the cue for a toast type. Silent for types without one, and wherever Web Audio is missing or still locked. */
@@ -161,4 +172,4 @@ declare function chime(type: ToastType, volume?: number): void;
161
172
  declare const ICONS: Record<ToastType, string[]>;
162
173
 
163
174
  export { ICONS, TIMING, bindToast, buildCss, chime, createToaster, getSprings, injectStyles, mountToaster, onToastKey, setSprings, springEasing, toast };
164
- export type { Phase, Position, PromiseMessages, SpringConfig, Theme, ToastAction, ToastItem, ToastOptions, ToastPatch, ToastType, Toaster, ToasterState };
175
+ export type { Phase, Position, PromiseMessages, SpringConfig, Theme, ToastAction, ToastItem, ToastLabels, ToastOptions, ToastPatch, ToastType, Toaster, ToasterState };
package/dist/index.js CHANGED
@@ -56,8 +56,10 @@ var TIMING = {
56
56
  duration: 5e3
57
57
  };
58
58
  var autoDuration = (type, hasAction) => type === "loading" || type === "error" || hasAction ? 0 : TIMING.duration;
59
+ var DEFAULT_LABELS = { region: "Notifications", dismiss: "Dismiss notification" };
60
+ var definedOnly = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
59
61
  function createToaster() {
60
- let state = { toasts: [], position: "top-center", theme: "system", status: "", alert: "" };
62
+ let state = { toasts: [], position: "top-center", theme: "system", labels: DEFAULT_LABELS, status: "", alert: "" };
61
63
  let flip = false;
62
64
  const subs = /* @__PURE__ */ new Set();
63
65
  const entries = /* @__PURE__ */ new Map();
@@ -240,7 +242,13 @@ function createToaster() {
240
242
  configure(c) {
241
243
  if (c.sound !== void 0) sound = c.sound;
242
244
  if (c.volume !== void 0) volume = c.volume;
243
- state = { ...state, ...c.position && { position: c.position }, ...c.theme && { theme: c.theme } };
245
+ state = {
246
+ ...state,
247
+ ...c.position && { position: c.position },
248
+ ...c.theme && { theme: c.theme },
249
+ // undefined keys are skipped so `{ region: undefined }` keeps the current label
250
+ ...c.labels && { labels: { ...state.labels, ...definedOnly(c.labels) } }
251
+ };
244
252
  emit();
245
253
  },
246
254
  subscribe(fn) {
@@ -325,7 +333,7 @@ position:fixed;inset:0;z-index:2147483647;pointer-events:none;font:500 14px/1.3
325
333
  .tst-viewport[data-position$=left]{--tst-ox:0%;--tx:0px}
326
334
  .tst-viewport[data-position$=right]{--tst-ox:100%;--tx:0px}
327
335
  .tst-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}
328
- .tst-slot{position:absolute;pointer-events:auto;touch-action:none;user-select:none;cursor:grab;--tst-i:0;
336
+ .tst-slot{position:absolute;pointer-events:auto;touch-action:none;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;cursor:grab;--tst-i:0;
329
337
  --y:calc(var(--tst-dir)*-1*var(--tst-i)*var(--tst-stack-offset));--s:calc(1 - var(--tst-i)*var(--tst-stack-scale));
330
338
  transform:translate(calc(var(--tx) + var(--dx,0px)),calc(var(--y) + var(--dy,0px))) scale(var(--s));
331
339
  transform-origin:var(--tst-ox) var(--tst-oy);z-index:calc(100 - var(--tst-i));
@@ -350,7 +358,7 @@ translate:0 var(--tst-y0);scale:var(--tst-enter-scale,.5);opacity:0;will-change:
350
358
  .tst[data-type=warning]{--c:var(--tst-warning)}
351
359
  .tst[data-type=info]{--c:var(--tst-info)}
352
360
 
353
- .tst-icon{position:absolute;inset-inline-start:0;top:0;width:var(--tst-size);height:var(--tst-size);display:grid;place-items:center}
361
+ .tst-icon{position:absolute;inset-inline-start:-1px;top:-1px;width:var(--tst-size);height:var(--tst-size);display:grid;place-items:center}
354
362
  .tst-media{width:28px;height:28px;border-radius:50%;object-fit:cover;outline:1px solid light-dark(oklch(0 0 0/.1),oklch(1 0 0/.1));outline-offset:-1px;animation:tst-swap var(--tst-stack-dur) var(--tst-stack) .1s both}
355
363
  .tst-badge{position:relative;width:24px;height:24px;display:grid;place-items:center}
356
364
  .tst-svg{width:22px;height:22px;fill:none;stroke:var(--c);stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;transition:stroke .3s cubic-bezier(.2,0,0,1);animation:tst-swap var(--tst-stack-dur) var(--tst-stack) .1s both}
@@ -371,7 +379,7 @@ translate:0 var(--tst-y0);scale:var(--tst-enter-scale,.5);opacity:0;will-change:
371
379
  .tst[data-type=info] .tst-svg path:nth-child(3){stroke-dasharray:none;stroke-dashoffset:0;animation:tst-dot .4s var(--tst-pop) var(--md) both}
372
380
  .tst[data-type=info] .tst-svg path:nth-child(2){animation-delay:calc(var(--md) + .16s)}
373
381
 
374
- .tst-body{position:absolute;inset-inline-start:calc(var(--tst-size) - 1px);top:-1px;display:flex;flex-wrap:wrap;align-items:center;gap:0 8px;width:max-content;max-width:calc(100vw - 2*var(--tst-pad) - var(--tst-size) + 1px);min-height:var(--tst-size);padding-inline-end:18px;
382
+ .tst-body{position:absolute;inset-inline-start:calc(var(--tst-size) - 1px);top:-1px;display:flex;flex-wrap:wrap;align-items:center;gap:0 8px;width:max-content;max-width:calc(100vw - 2*var(--tst-pad) - var(--tst-size) + 1px);min-height:var(--tst-size);padding-inline-end:8px;
375
383
  opacity:calc(var(--tp)*1.5 - .25);filter:blur(calc((1 - var(--tp))*10px));transform:translateX(calc((1 - var(--tp))*-12px*var(--tst-flip,1)))}
376
384
  .tst-body:has(.tst-action,.tst-close){padding-inline-end:8px;padding-bottom:0}
377
385
  .tst-body[data-wrapped]{padding-bottom:10px}
@@ -391,12 +399,16 @@ opacity:calc(var(--tp)*1.5 - .25);filter:blur(calc((1 - var(--tp))*10px));transf
391
399
  .tst[data-phase=open] .tst-desc{animation:tst-in-up var(--tst-pop-dur) var(--tst-pop) .1s both}
392
400
  .tst[data-phase=open] .tst-action,.tst[data-phase=open] .tst-close{animation:tst-in-btn var(--tst-pop-dur) var(--tst-pop) .16s backwards}
393
401
  .tst[data-phase=open] .tst-secondary{animation-delay:.1s}
394
- .tst-action,.tst-close{flex:none;border:0;color:inherit;font:inherit;cursor:pointer;background:transparent;
402
+ .tst-action,.tst-close{position:relative;flex:none;margin:0;border:0;color:inherit;font:inherit;cursor:pointer;background:transparent;
395
403
  opacity:calc(var(--ap)*1.3 - .15);transform:scale(calc(.85 + var(--ap)*.15));
396
404
  transition-property:background-color,scale;transition-duration:.15s;transition-timing-function:ease-out}
397
405
  .tst-action{height:30px;padding:0 14px;border-radius:999px;font-size:13px;font-weight:600;background:var(--tst-fg);color:var(--tst-bg)}
398
406
  .tst-secondary{background:transparent;color:color-mix(in srgb,var(--tst-fg) 65%,transparent);padding:0 10px}
399
407
  .tst-close{width:30px;height:30px;padding:0;border-radius:50%;display:grid;place-items:center}
408
+ /* 30px look, 44px target: taller hit area, narrow enough sideways that neighbours (8px apart) never overlap */
409
+ .tst-action::before,.tst-close::before{content:'';position:absolute;inset:-7px -3px}
410
+ .tst-slot:focus-visible{outline:none}
411
+ .tst-slot:focus-visible .tst{outline:2px solid var(--tst-fg);outline-offset:-5px}
400
412
  .tst-close svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round}
401
413
  @media (hover:hover){.tst-action:hover{background:color-mix(in srgb,var(--tst-fg) 82%,var(--tst-bg))}.tst-secondary:hover,.tst-close:hover{background:color-mix(in srgb,var(--tst-fg) 10%,transparent);color:var(--tst-fg)}}
402
414
  .tst-action:not(:disabled):active,.tst-close:not(:disabled):active{scale:.96}
@@ -739,10 +751,15 @@ var ICONS = {
739
751
  };
740
752
 
741
753
  // src/vanilla.ts
754
+ var BUSY = "dialog,[role=dialog],[aria-modal=true],[role=menu],[role=listbox],input,textarea,select,[contenteditable]";
742
755
  function onToastKey(e) {
743
756
  if (e.defaultPrevented || !toast.getState().toasts.length) return;
744
- if (e.key === "Escape") toast.dismiss();
745
- else if (e.altKey && e.code === "KeyT") document.querySelector('.tst-viewport button:not([tabindex="-1"])')?.focus();
757
+ if (e.key === "Escape") {
758
+ if (!document.activeElement?.closest(BUSY)) toast.dismiss();
759
+ } else if (e.altKey && e.code === "KeyT") {
760
+ const slots = document.querySelectorAll(".tst-viewport .tst-slot");
761
+ (document.querySelector('.tst-viewport button:not([tabindex="-1"])') ?? slots[slots.length - 1])?.focus();
762
+ }
746
763
  }
747
764
  var el = (tag, cls, parent) => {
748
765
  const n = document.createElement(tag);
@@ -770,7 +787,6 @@ function mountToaster(opts = {}) {
770
787
  toast.configure(opts);
771
788
  const vp = el("div", "tst-viewport", document.body);
772
789
  vp.setAttribute("role", "region");
773
- vp.setAttribute("aria-label", "Notifications");
774
790
  const status = el("div", "tst-sr", vp);
775
791
  status.setAttribute("role", "status");
776
792
  const alert = el("div", "tst-sr", vp);
@@ -783,6 +799,7 @@ function mountToaster(opts = {}) {
783
799
  const nodes = /* @__PURE__ */ new Map();
784
800
  function build(t) {
785
801
  const slot = el("div", "tst-slot", vp);
802
+ slot.tabIndex = -1;
786
803
  el("div", "tst", slot);
787
804
  return { slot };
788
805
  }
@@ -790,6 +807,7 @@ function mountToaster(opts = {}) {
790
807
  const s = toast.getState();
791
808
  vp.dataset.position = s.position;
792
809
  vp.dataset.theme = s.theme;
810
+ vp.setAttribute("aria-label", s.labels.region);
793
811
  status.textContent = s.status;
794
812
  alert.textContent = s.alert;
795
813
  const live = new Set(s.toasts.map((t) => t.id));
@@ -869,10 +887,10 @@ function mountToaster(opts = {}) {
869
887
  if (!close) {
870
888
  close = el("button", "tst-close", body);
871
889
  close.type = "button";
872
- close.setAttribute("aria-label", "Dismiss notification");
873
890
  close.append(svg("", ["M6 6l12 12M18 6L6 18"]));
874
891
  close.onclick = () => toast.dismiss(t.id);
875
892
  }
893
+ close.setAttribute("aria-label", s.labels.dismiss);
876
894
  close.tabIndex = t.phase === "open" ? 0 : -1;
877
895
  }
878
896
  n.off ??= bindToast(n.slot, t.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yusufceng1z/ding",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Framework-agnostic toast engine: spring solver, timers, state machine.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -22,6 +22,10 @@
22
22
  "files": [
23
23
  "dist"
24
24
  ],
25
+ "homepage": "https://ding.yusufcengiz.dev",
26
+ "bugs": {
27
+ "url": "https://github.com/YusufCeng1z/ding/issues"
28
+ },
25
29
  "repository": {
26
30
  "type": "git",
27
31
  "url": "git+https://github.com/YusufCeng1z/ding.git",
@@ -40,7 +44,7 @@
40
44
  "author": "Yusuf Cengiz",
41
45
  "scripts": {
42
46
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
43
- "test": "node --test src/",
47
+ "test": "node --test \"src/**/*.test.ts\"",
44
48
  "typecheck": "tsc -p ."
45
49
  }
46
50
  }