npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@subrotosaha/notification

v1.2.0

Published

A lightweight toast notification and confirmation dialog library for any JavaScript framework

Readme

@subrotosaha/notification

A lightweight, zero-dependency toast notification & confirm dialog library for vanilla JS, React, Vue, Angular, and Svelte. Features multiple visual variants, six positions, inline action buttons, promise tracking, 15 input types, async hooks, multi-step dialogs, 32 built-in icons, custom icon registration, icon library integration (Lucide, Iconify, etc.), and full TypeScript support.


Table of Contents


Installation

npm install @subrotosaha/notification

Setup

Vanilla JS

import notification from "@subrotosaha/notification";
import "@subrotosaha/notification/style.css";

React

// main.jsx or App.jsx — import styles once at the root
import "@subrotosaha/notification/style.css";

// In any component
import notification from "@subrotosaha/notification";

Note: This library manipulates document.body directly — it is client-side only. In Next.js or other SSR environments, call it inside useEffect or a browser-only code path.

Next.js (App Router)

"use client";
import notification from "@subrotosaha/notification";
import "@subrotosaha/notification/style.css";

export default function Page() {
  const handleClick = () => {
    notification.notify.success("Saved successfully!", {
      position: "top-right",
    });
  };

  return <button onClick={handleClick}>Save</button>;
}

Global Config

Set default options applied to all subsequent toasts and/or confirm dialogs. Call once during app bootstrap — options are merged, not replaced.

notification.config({
  toast: {
    position: "bottom-right",
    duration: 4000,
    variant: "filled",
    pauseOnHover: true,
    progressBar: true,
  },
  confirm: {
    confirmButtonText: "Yes",
    cancelButtonText: "No",
  },
});

| Field | Type | Description | | --------- | ---------------------------- | ----------------------------------------------- | | toast | Partial<GlobalToastConfig> | Default position, duration, variant, etc. | | confirm | Partial<FireOptions> | Default confirm dialog options |


Toast Notifications

Basic Usage

notification.notify.success("Profile saved!");
notification.notify.warning("Low disk space.");
notification.notify.error("Upload failed.");
notification.notify.info("Your session expires soon.");

Each method returns a numeric toast ID (number | null).

notification.notify.success("File uploaded", {
  title: "Upload Complete",
  variant: "filled",
  position: "bottom-right",
  duration: 3000,
  progressBar: true,
});

Loading Toast

Displays a persistent spinner toast that never auto-dismisses. Call dismiss(id) when done.

const id = notification.notify.loading("Uploading…");
await upload();
notification.notify.dismiss(id);

Promise Toast

Tracks an async operation and transitions through loading → success | error automatically.

notification.notify.promise(
  fetch("/api/save").then((r) => r.json()),
  {
    loading: "Saving…",
    success: (data) => `Saved ${data.count} records.`,
    error: (err) => `Failed: ${err.message}`,
  }
);

The original promise is returned so it can still be awaited:

const data = await notification.notify.promise(fetchUser(id), {
  loading: "Loading user…",
  success: "User loaded!",
  error: "Could not load user.",
});

Dismiss

// Dismiss a single toast by ID
notification.notify.dismiss(id);

// Dismiss all visible toasts
notification.notify.dismissAll();

NotifyOptions

All fields are optional and merge with global defaults from config().

| Option | Type | Default | Description | | -------------- | ---------------------------------- | ------------- | --------------------------------------------------------------------------- | | title | string \| null | null | Optional bold title above the message | | variant | 'filled' \| 'outlined' | '' | Visual style | | position | NotifyPosition | 'top-right' | Screen position | | duration | number | 5000 | Auto-dismiss delay in ms. 0 = persistent | | pauseOnHover | boolean | true | Pause the timer while the cursor is over the toast | | progressBar | boolean | false | Show an animated draining progress bar | | action | NotifyActionConfig \| null | null | Inline action button (e.g. "Undo") | | onClick | () => void \| null | null | Called when the toast body is clicked (then dismisses) | | onClose | (reason: string) => void \| null | null | Called after the toast is removed from the DOM | | iconHtml | string \| Element \| null | null | Custom HTML string, SVG string, or DOM Element replacing the default icon | | closable | boolean | true | Show or hide the × close button |

Action Button

Render an inline action button on the right side of the toast.

notification.notify.warning("Item moved to trash.", {
  action: {
    text: "Undo",
    onClick: () => restoreItem(),
  },
});

Positions

| Value | Description | | ----------------- | ----------------------------------- | | 'top-right' | Top-right corner (default) | | 'top-left' | Top-left corner | | 'top-center' | Top-center, slides down from above | | 'bottom-right' | Bottom-right corner | | 'bottom-left' | Bottom-left corner | | 'bottom-center' | Bottom-center, slides up from below |

Toasts at the same position stack and reposition automatically when one is dismissed.

Toast Variants

| Variant | Description | | ------------ | ---------------------------------------------------- | | (default) | Tinted glassmorphism background with coloured border | | 'filled' | Solid saturated background with white text | | 'outlined' | Transparent background with a 1.5 px coloured border |


Confirm Dialogs

fire()

Full options API — returns Promise<FireResult<T>>.

const result = await notification.confirm.fire({
  icon: "warning",
  title: "Delete account?",
  text: "All your data will be permanently removed.",
  confirmButtonText: "Yes, delete it",
  cancelButtonText: "Cancel",
  showDenyButton: true,
  denyButtonText: "Archive instead",
});

if (result.isConfirmed) {
  // confirm button clicked
} else if (result.isDenied) {
  // deny button clicked
} else if (result.isDismissed) {
  console.log("Dismissed by:", result.dismiss); // 'cancel' | 'backdrop' | 'esc' | 'close' | 'timer'
}

Shorthand Methods

Pre-configured fire() calls with a specific icon. Accept a FireOptions object (minus icon).

notification.confirm.success(options?)
notification.confirm.warning(options?)
notification.confirm.error(options?)
notification.confirm.info(options?)
notification.confirm.question(options?)

Example:

const result = await notification.confirm.warning({
  title: "Delete item?",
  text: "This cannot be undone.",
  confirmButtonText: "Delete",
});

if (result.isConfirmed) {
  await deleteItem();
  notification.notify.success("Deleted.", { variant: "filled" });
}
// No cancel button
await notification.confirm.info({
  title: "Heads up",
  text: "Your free trial ends in 3 days.",
  showCancelButton: false,
  confirmButtonText: "Got it",
});

mixin()

Create a reusable factory with shared defaults. Per-call options override the mixin defaults.

const DeleteAlert = notification.confirm.mixin({
  icon: "warning",
  confirmButtonText: "Delete",
  cancelButtonText: "Cancel",
  showCancelButton: true,
});

const result = await DeleteAlert.fire({ title: "Delete this record?" });
const result2 = await DeleteAlert.fire({ title: "Delete all records?" });

queue()

Show multiple dialogs in sequence. Stops early if any step is dismissed.

const steps = ["1", "2", "3"];

const { results, dismiss, isConfirmed } = await notification.confirm.queue([
  {
    title: "Step 1 — Enter your name",
    input: "text",
    inputLabel: "Name",
    progressSteps: steps,
    currentProgressStep: 0,
  },
  {
    title: "Step 2 — Enter your email",
    input: "email",
    inputLabel: "Email",
    progressSteps: steps,
    currentProgressStep: 1,
  },
  {
    title: "Step 3 — Confirm",
    text: "Review your details and submit.",
    progressSteps: steps,
    currentProgressStep: 2,
  },
]);

if (isConfirmed) {
  console.log(
    "Values:",
    results.map((r) => r.value)
  );
}

Note: queue() auto-injects currentProgressStep and progressSteps when not provided, numbering steps "1", "2", "3", etc.

Runtime Controls

// Check if a dialog is currently visible
notification.confirm.isVisible(); // boolean

// Check if preConfirm is running
notification.confirm.isLoading(); // boolean

// Get the popup DOM element
const popup = notification.confirm.getPopup(); // HTMLElement | null

// Programmatically close with 'close' dismiss reason
notification.confirm.close();

// Update live dialog options while it is open
notification.confirm.update({
  title: "Still processing…",
  confirmButtonText: "Please wait",
  showCancelButton: false,
});

FireOptions Reference

| Option | Type | Default | Description | | --------------------- | -------------------------------------------------------- | ------------ | ------------------------------------------------------------------- | | title | string | '' | Dialog title (HTML allowed) | | text | string | '' | Plain-text body content | | html | string | '' | HTML body content (overrides text) | | icon | IconType (built-in name or any registered custom name) | 'question' | Icon to display — see Icons for all 32 built-in names | | iconHtml | string \| Element \| null | null | Custom HTML string or DOM Element that replaces the icon entirely | | variant | 'filled' \| 'outlined' | '' | Visual style | | showConfirmButton | boolean | true | Show the Confirm button | | showDenyButton | boolean | false | Show an optional Deny button | | showCancelButton | boolean | true | Show the Cancel button | | confirmButtonText | string | 'OK' | Confirm button label | | denyButtonText | string | 'No' | Deny button label | | cancelButtonText | string | 'Cancel' | Cancel button label | | reverseButtons | boolean | false | Swap Confirm and Cancel button order | | showCloseButton | boolean | false | Show an × close button in the corner | | allowOutsideClick | boolean \| () => boolean | true | Close on backdrop click | | allowEscapeKey | boolean \| () => boolean | true | Close on Escape key | | footer | string | '' | HTML/text rendered below the action buttons | | imageUrl | string | '' | URL for a custom image inside the dialog | | imageWidth | number \| string \| null | null | CSS width of the custom image | | imageHeight | number \| string \| null | null | CSS height of the custom image | | imageAlt | string | '' | Alt text for the custom image | | input | InputType \| null | null | Render an input field inside the dialog | | inputLabel | string | '' | Label text above the input | | inputPlaceholder | string | '' | Input placeholder | | inputValue | string \| number \| boolean | '' | Pre-filled input value | | inputOptions | Record<string, string> \| Map<string, string> | {} | Options for select or radio inputs | | inputAttributes | Record<string, string> | {} | Extra HTML attributes on the input element | | inputValidator | (value) => string \| null \| Promise<...> | null | Return an error string to block confirm, falsy to allow | | timer | number \| null | null | Auto-close after N milliseconds | | timerProgressBar | boolean | false | Show a draining progress bar when timer is set | | preConfirm | (inputValue) => Promise<T \| false> | null | Async hook on Confirm — return false to keep the dialog open | | showLoaderOnConfirm | boolean | false | Show a spinner on the Confirm button while preConfirm runs | | preDeny | () => Promise<false \| void> | null | Async hook on Deny — return false to abort the denial | | progressSteps | Array<string \| number> | [] | Multi-step progress indicator labels | | currentProgressStep | number \| null | null | Zero-based index of the active step | | willOpen | (popup: HTMLElement) => void | null | Called before the dialog animates in | | didOpen | (popup: HTMLElement) => void | null | Called after the dialog is fully visible | | willClose | (popup: HTMLElement) => void | null | Called before the dialog begins closing | | didClose | () => void | null | Called after the dialog is removed from the DOM |


FireResult Object

| Property | Type | Description | | ------------- | ------------------- | --------------------------------------------------------------------- | | isConfirmed | boolean | true when the Confirm button was clicked | | isDenied | boolean | true when the Deny button was clicked | | isDismissed | boolean | true when the dialog was closed without confirming | | dismiss | DismissReasonType | Reason string when isDismissed is true | | value | T | Input value, preConfirm return value, or true for simple confirms |


ConfirmUpdateOptions

Fields that can be changed on a currently open dialog via notification.confirm.update().

| Field | Type | Description | | ------------------- | --------- | ------------------------------------- | | title | string | New title HTML | | text | string | New plain-text body | | html | string | New rich HTML body (overrides text) | | confirmButtonText | string | New Confirm button label | | cancelButtonText | string | New Cancel button label | | showConfirmButton | boolean | Show or hide the Confirm button | | showCancelButton | boolean | Show or hide the Cancel button |


Input Types

Pass any of these to the input option:

'text' · 'email' · 'password' · 'number' · 'tel' · 'url' · 'textarea' · 'select' · 'checkbox' · 'radio' · 'range' · 'date' · 'datetime-local' · 'time' · 'search'

Text input with validation:

const result = await notification.confirm.fire({
  title: "Enter your email",
  input: "email",
  inputPlaceholder: "[email protected]",
  inputValidator: (value) => {
    if (!value) return "Email is required.";
  },
  confirmButtonText: "Submit",
});

if (result.isConfirmed) console.log("Email:", result.value);

Select input:

await notification.confirm.fire({
  title: "Choose a country",
  input: "select",
  inputOptions: { us: "United States", ca: "Canada", gb: "United Kingdom" },
  inputPlaceholder: "Select a country",
});

Range input:

await notification.confirm.fire({
  title: "Select volume",
  input: "range",
  inputAttributes: { min: "0", max: "100", step: "5" },
  inputValue: 50,
});

Dismiss Reasons

When isDismissed is true, result.dismiss will be one of:

| Value | Trigger | | ------------ | --------------------------------- | | 'backdrop' | Clicked outside the dialog | | 'cancel' | Clicked the Cancel button | | 'close' | Clicked the × close button | | 'esc' | Pressed the Escape key | | 'timer' | Auto-closed by the timer option |

Use the DismissReason constant for type-safe comparisons:

import notification, { DismissReason } from "@subrotosaha/notification";

const result = await notification.confirm.fire({ title: "Continue?" });

if (result.dismiss === DismissReason.timer) console.log("Timed out");
if (result.dismiss === DismissReason.cancel) console.log("User cancelled");

// Also accessible on the notification object:
notification.DismissReason.backdrop;
notification.DismissReason.esc;

Confirm Variants

| Variant | Description | | ------------ | ------------------------------------------------------------- | | (default) | Tinted glassmorphism with accent border per icon type | | 'filled' | Deep solid saturated background matching the icon colour | | 'outlined' | Glass background with a full-perimeter 1.5 px coloured border |

// Filled
await notification.confirm.fire({
  icon: "error",
  title: "Delete permanently?",
  variant: "filled",
  confirmButtonText: "Delete",
});

// Outlined
await notification.confirm.fire({
  icon: "info",
  title: "Heads up",
  text: "Your free trial ends in 3 days.",
  variant: "outlined",
  showCancelButton: false,
  confirmButtonText: "Got it",
});

Advanced Examples

Timer with progress bar

await notification.confirm.fire({
  icon: "info",
  title: "Session expiring",
  text: "You will be logged out automatically.",
  timer: 5000,
  timerProgressBar: true,
  showCancelButton: false,
  confirmButtonText: "Stay logged in",
});

preConfirm — async validation / server action

const result = await notification.confirm.fire({
  title: "Submit form?",
  input: "textarea",
  inputLabel: "Your message",
  inputPlaceholder: "Write something…",
  confirmButtonText: "Send",
  showLoaderOnConfirm: true,
  preConfirm: async (message) => {
    const response = await fetch("/api/messages", {
      method: "POST",
      body: JSON.stringify({ message }),
    });
    if (!response.ok) return false; // keep dialog open
    return await response.json();
  },
});

if (result.isConfirmed) {
  notification.notify.success("Message sent!", { variant: "filled" });
}

preDeny — confirm before discarding

await notification.confirm.fire({
  title: "Save changes?",
  showDenyButton: true,
  denyButtonText: "Discard",
  preDeny: async () => {
    const check = await notification.confirm.warning({
      title: "Really discard?",
      confirmButtonText: "Yes, discard",
    });
    if (!check.isConfirmed) return false; // keep original dialog open
  },
});

Lifecycle hooks

await notification.confirm.fire({
  title: "Loading…",
  willOpen: (popup) => console.log("opening", popup),
  didOpen: (popup) => {
    // start animations, fetch data, focus an element…
  },
  willClose: (popup) => console.log("closing"),
  didClose: () => console.log("removed from DOM"),
});

Live update() inside preConfirm

await notification.confirm.fire({
  title: "Uploading file",
  confirmButtonText: "Upload",
  preConfirm: async () => {
    notification.confirm.update({ confirmButtonText: "Uploading…" });
    await uploadFile();
    notification.confirm.update({ confirmButtonText: "Done!" });
  },
});

Icons

Built-in Icons

32 icons are bundled and ready to use by name. Pass them to the icon option in confirm dialogs, or pull their SVG strings from notification.icons.

| Category | Names | | ----------- | ----------------------------------------------------- | | Status | success · warning · error · info · question | | UI Controls | close · spinner · loading | | Actions | trash · edit · copy · download · upload | | Navigation | refresh · search · link · mail · bell | | Feedback | star · heart · thumbup · thumbdown | | Identity | user · lock · unlock · settings · pin | | Misc | fire · shield · calendar · clock · home |

await notification.confirm.fire({ icon: "trash", title: "Delete this?" });
await notification.confirm.fire({ icon: "shield", title: "Access restricted" });
await notification.confirm.fire({
  icon: "calendar",
  title: "Schedule a meeting",
});

Custom Icons

Register your own icons with registerIcon(). They work anywhere the icon option is accepted and can override built-ins.

import notification, { registerIcon } from "@subrotosaha/notification";

// Plain SVG string
registerIcon("bitcoin", '<svg viewBox="0 0 320 512">…</svg>');

// Also available on the notification object
notification.registerIcon("bitcoin", '<svg viewBox="0 0 320 512">…</svg>');

// Use by name in confirm dialogs
await notification.confirm.fire({
  icon: "bitcoin",
  title: "Pay with Bitcoin?",
});

// Or grab the SVG string directly
notification.notify.success("Payment received", {
  iconHtml: notification.icons["bitcoin"],
});

Access the full live registry at any time:

console.log(notification.icons); // Record<string, string> — built-ins + your custom icons

Icon Library Integration

iconHtml accepts a raw HTML/SVG string or a DOM Element — so icon libraries that return nodes work directly with no extra conversion step.

Lucide (vanilla JS)

import { createElement, Zap } from "lucide";

// Pass an Element directly
notification.notify.success("Charged!", {
  iconHtml: createElement(Zap),
});

// Or register once and use by name
notification.registerIcon("zap", createElement(Zap));
await notification.confirm.fire({ icon: "zap", title: "Quick action?" });

Lucide React

import { renderToStaticMarkup } from "react-dom/server";
import { Zap } from "lucide-react";

notification.notify.success("Charged!", {
  iconHtml: renderToStaticMarkup(<Zap size={20} />),
});

Iconify

import { loadIcon, iconToSVG, iconToHTML } from "@iconify/utils";

const data = await loadIcon("mdi:flash");
notification.registerIcon("flash", iconToHTML(iconToSVG(data)));

await notification.confirm.fire({ icon: "flash", title: "Go!" });

Iconify web component (DOM element)

const el = document.createElement("iconify-icon");
el.setAttribute("icon", "mdi:flash");
notification.notify.info("Tip", { iconHtml: el });

Emoji or <img>

notification.notify.success("Deployed!", { iconHtml: "🚀" });
notification.notify.warning("Check this", {
  iconHtml: '<img src="/logo.svg" width="20" alt="logo">',
});

resolveIconHtml()

A utility that normalises any accepted icon value into a plain HTML string suitable for innerHTML. Useful when consuming the library alongside your own rendering logic.

import { resolveIconHtml } from "@subrotosaha/notification";

resolveIconHtml(null); // → ''
resolveIconHtml("…svg string…"); // → '…svg string…'
resolveIconHtml(domElement); // → element.outerHTML

TypeScript

Full type definitions are included. No @types package needed.

import notification, {
  FireOptions,
  FireResult,
  NotifyOptions,
  NotifyPosition,
  NotifyVariant,
  PromiseMessages,
  ConfirmUpdateOptions,
  DismissReason,
  DismissReasonType,
  MixinInstance,
  QueueResult,
  InputType,
  IconType,
  GlobalConfig,
  registerIcon,
  resolveIconHtml,
} from "@subrotosaha/notification";

// Typed confirm with preConfirm return value
const options: FireOptions<{ id: number; name: string }> = {
  title: "Confirm action",
  input: "text",
  inputLabel: "Username",
  confirmButtonText: "Submit",
  showLoaderOnConfirm: true,
  preConfirm: async (username) => {
    const res = await fetch(`/api/users/${username}`);
    if (!res.ok) throw new Error("User not found");
    return res.json();
  },
};

const result: FireResult<{ id: number; name: string }> =
  await notification.confirm.fire(options);

if (result.isConfirmed) {
  console.log(result.value?.name); // fully typed
}

// Typed toast
const notifyOptions: NotifyOptions = {
  variant: "filled",
  position: "bottom-right",
  title: "Done",
  duration: 3000,
  progressBar: true,
  action: { text: "Undo", onClick: () => undo() },
};
notification.notify.success("Saved!", notifyOptions);

// Typed promise toast
notification.notify.promise<{ count: number }>(
  fetch("/api/save").then((r) => r.json()),
  {
    loading: "Saving…",
    success: (data) => `Saved ${data.count} items`,
    error: (err) => `Error: ${(err as Error).message}`,
  } satisfies PromiseMessages<{ count: number }>
);

// Global config with type checking
notification.config({
  toast: { position: "top-right", variant: "filled", duration: 5000 },
  confirm: { confirmButtonText: "Yes", cancelButtonText: "No" },
} satisfies GlobalConfig);

// Dismiss reasons
if (result.dismiss === DismissReason.timer) {
  console.log("Timed out");
}

Changelog

Toast

  • notify.success(), notify.warning(), notify.error(), notify.info()
  • notify.loading() — persistent spinner toast, manual dismiss required
  • notify.dismiss(id) / notify.dismissAll()
  • notify.promise(promise, messages, options?) — auto loading → success/error tracking
  • NotifyOptions: title, variant, position, duration, pauseOnHover, progressBar, action, onClick, onClose, iconHtml, closable
  • Inline action button support (action.text + action.onClick)
  • Variants: default, filled, outlined
  • Positions: top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
  • Toasts stack and reposition automatically per position bucket

Confirm

  • confirm.fire(options) — full options API
  • confirm.success/warning/error/info/question(options) — icon-preset shorthand methods
  • confirm.mixin(defaults) — reusable pre-configured dialog factory
  • confirm.queue(steps) — sequential multi-step dialogs with auto progress steps
  • confirm.isVisible(), confirm.isLoading(), confirm.getPopup(), confirm.close()
  • confirm.update(options) — live-update title, text, and button labels on an open dialog
  • 15 input types: inputValidator, inputOptions, inputAttributes
  • preConfirm + showLoaderOnConfirm — async confirm with loading state
  • preDeny — async deny hook, abort with return false
  • timer + timerProgressBar — auto-close with draining progress bar
  • Multi-step dialogs: progressSteps + currentProgressStep
  • Lifecycle hooks: willOpen, didOpen, willClose, didClose
  • DismissReason constant for type-safe dismiss comparisons
  • Variants: default, filled, outlined

Icons

  • 32 built-in SVG icons across 7 categories: status, UI controls, actions, navigation, feedback, identity, misc
  • registerIcon(name, svg: string | Element) — register custom icons by name; accepts a raw SVG string or any DOM Element (e.g. from Lucide, Iconify)
  • resolveIconHtml(icon) — normalises string | Element | null to an HTML string for innerHTML
  • notification.icons — live Record<string, string> registry of all built-ins + custom icons
  • iconHtml option accepts string | Element | null in both NotifyOptions and FireOptions
  • IconType includes all built-in names with full autocomplete plus (string & {}) for custom names

Build & TypeScript

  • Full TypeScript source — zero any types, strict mode
  • ESM-only output (dist/) — no CommonJS bundle
  • Individual compiled files, no bundle merging: confirm.js, toast.js, icons.js, types.js, index.js
  • Individual .d.ts declarations per module
  • No @types package required