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

@mounaji_npm/notifications

v0.1.0

Published

Notification system — multi-channel (web, email, push), provider, hooks, and UI components.

Readme

@mounaji_npm/notifications

Multi-channel notification system — in-app web notifications, email via Resend, and an extensible channel architecture ready for push, WhatsApp, and any custom delivery channel.

Includes: React provider, hooks, bell widget, real-time toasts, history page, and preferences panel.


Install

npm install @mounaji_npm/notifications

Peer dependencies:

npm install react  # >=17.0.0

@mounaji_npm/event-core is a direct dependency — installed automatically.


Quick start

// 1. Wrap your app with NotificationProvider
import { NotificationProvider, WebChannel, EmailChannel } from '@mounaji_npm/notifications';

export default function RootLayout({ children }) {
  const webChannel = new WebChannel({
    onFetch:       () => fetch('/api/events').then(r => r.json()),
    onMarkRead:    (ids) => fetch('/api/events/mark-read', { method: 'POST', body: JSON.stringify({ ids }) }),
    onMarkAllRead: () => fetch('/api/events/mark-all-read', { method: 'POST' }),
  });

  const emailChannel = new EmailChannel({
    apiEndpoint: '/api/notifications/send-email',
  });

  return (
    <NotificationProvider channels={[webChannel, emailChannel]}>
      {children}
      <NotificationToast isDark={true} />
    </NotificationProvider>
  );
}

// 2. Add the bell to your TopNav
import { NotificationBell } from '@mounaji_npm/notifications';
<NotificationBell isDark={isDark} historyPath="/notifications" onNavigate={router.push} />

// 3. Add the history page
import { NotificationHistoryPage } from '@mounaji_npm/notifications';
export default function NotificationsPage() {
  return <NotificationHistoryPage isDark={isDark} />;
}

Exports

import {
  // Channels
  BaseChannel, WebChannel, EmailChannel, PushChannel, ChannelRegistry,

  // Service
  NotificationService,

  // Provider & context
  NotificationProvider, useNotificationContext,

  // Hooks
  useNotifications, useUnreadCount,

  // UI Components
  NotificationBell, NotificationToast,
  NotificationHistoryPage, NotificationPreferences,
} from '@mounaji_npm/notifications';

// Server-side only (Node.js / Next.js Route Handlers)
import { createResendSender } from '@mounaji_npm/notifications/server';

Channels

The channel system is the delivery layer. Each channel implements a standard interface, and the NotificationService orchestrates across all registered channels.

WebChannel — In-app notifications

Fetches and updates stored notifications via adapter functions you provide. Does not hardcode any API path or database client — stays portable across backends.

import { WebChannel } from '@mounaji_npm/notifications';

const webChannel = new WebChannel({
  onFetch:       async () => fetch('/api/events').then(r => r.json()),
  onMarkRead:    async (ids) => fetch('/api/events/mark-read', {
                   method: 'POST',
                   headers: { 'Content-Type': 'application/json' },
                   body: JSON.stringify({ ids }),
                 }),
  onMarkAllRead: async () => fetch('/api/events/mark-all-read', { method: 'POST' }),
});

| Constructor option | Type | Description | |---|---|---| | onFetch | async () => Notification[] | Fetches the notification list | | onMarkRead | async (ids: string[]) => void | Marks specific notifications as read | | onMarkAllRead | async () => void | Marks all notifications as read |

EmailChannel — Email via API endpoint

Calls your own API route, which uses createResendSender server-side. The Resend API key never enters the client bundle.

import { EmailChannel } from '@mounaji_npm/notifications';

// Option A — POST to your API route
const emailChannel = new EmailChannel({
  apiEndpoint: '/api/notifications/send-email',
});

// Option B — custom async function (e.g. a Next.js Server Action)
const emailChannel = new EmailChannel({
  onSend: async (payload) => sendEmailServerAction(payload),
});

| Constructor option | Type | Description | |---|---|---| | apiEndpoint | string | Your POST endpoint that proxies to Resend | | onSend | async (payload) => void | Alternative to apiEndpoint | | enabled | boolean | Defaults to true |

PushChannel — Web Push / FCM (Phase 2)

Architecture stub — registered in the system but disabled until Phase 2.

import { PushChannel } from '@mounaji_npm/notifications';

const pushChannel = new PushChannel({
  // Phase 2 options: vapidPublicKey, subscriptionEndpoint, etc.
});
// isEnabled() returns false until implemented

Custom channels

Extend BaseChannel to add any delivery channel — WhatsApp, Slack, SMS, webhooks, etc.

import { BaseChannel } from '@mounaji_npm/notifications';

class WhatsAppChannel extends BaseChannel {
  name = 'whatsapp';

  get canSend() { return true; }

  constructor({ token, phoneNumberId }) {
    super();
    this._token = token;
    this._phoneNumberId = phoneNumberId;
  }

  async send({ to, title, message }) {
    await fetch(`https://graph.facebook.com/v18.0/${this._phoneNumberId}/messages`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${this._token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        messaging_product: 'whatsapp',
        to,
        type: 'text',
        text: { body: `${title}\n${message ?? ''}` },
      }),
    });
  }
}

BaseChannel interface

| Property / Method | Description | |---|---| | name | string — unique channel identifier | | canFetch | boolean (getter) — can retrieve stored notifications | | canSend | boolean (getter) — can deliver outbound notifications | | isEnabled() | Returns true if channel is active | | fetch() | Returns Notification[] | | markRead(ids) | Marks specific notifications as read | | markAllRead() | Marks all as read | | send(notification) | Sends a notification through this channel |


NotificationService

Orchestrates all registered channels. Typically created internally by NotificationProvider, but can be instantiated directly for non-React usage or advanced control.

import { NotificationService, WebChannel, EmailChannel } from '@mounaji_npm/notifications';

const service = new NotificationService({
  channels: [webChannel, emailChannel],
});

// Fetch in-app notifications (from the first canFetch channel)
const notifications = await service.fetch();

// Mark specific notifications as read (across all fetch channels)
await service.markRead(['id1', 'id2']);

// Mark all as read
await service.markAllRead();

// Send via specific channels
await service.send({
  title:    'Stock crítico: Harina 000',
  to:       '[email protected]',   // used by email channel
  channels: ['web', 'email'],        // omit to use all enabled send channels
  severity: 'danger',
  metadata: { ingredient: 'Harina 000', available: '2kg', required: '15kg' },
});

// Add a channel dynamically
service.addChannel(new WhatsAppChannel({ token, phoneNumberId }));

API reference

| Method | Description | |---|---| | fetch() | Fetches from the primary canFetch channel | | markRead(ids) | Marks read on all canFetch channels | | markAllRead() | Marks all read on all canFetch channels | | send(notification) | Sends via channels listed in notification.channels, or all enabled canSend channels | | addChannel(channel) | Register a channel at runtime | | registry | Direct access to the ChannelRegistry instance |


NotificationProvider

React context root. Wraps your app (or a subtree) to enable all hooks and connected components.

Handles:

  • Initial fetch on mount
  • Background poll every pollInterval milliseconds
  • Real-time bridge — listens to mn:events-updated DOM event dispatched by your API client
import { NotificationProvider, WebChannel, EmailChannel } from '@mounaji_npm/notifications';

<NotificationProvider
  channels={[webChannel, emailChannel]}
  config={{
    pollInterval: 60_000,       // background poll interval in ms (0 = disabled)
    watchEvent: 'mn:events-updated', // DOM event to listen for (default value shown)
  }}
>
  {children}
</NotificationProvider>

Advanced: Pass a pre-built NotificationService instance via service instead of channels:

<NotificationProvider service={myService}>...</NotificationProvider>

Props

| Prop | Type | Default | Description | |---|---|---|---| | channels | BaseChannel[] | [] | Channel instances to register | | service | NotificationService | — | Pre-built service; overrides channels if provided | | config.pollInterval | number | 60_000 | Background poll in ms; 0 to disable | | config.watchEvent | string | 'mn:events-updated' | DOM event that triggers a silent re-fetch |


Hooks

useNotifications()

Primary hook. Must be used inside <NotificationProvider> — throws otherwise.

import { useNotifications } from '@mounaji_npm/notifications';

const {
  notifications,  // Notification[] — full list (read + unread)
  unreadCount,    // number
  loading,        // boolean — true during the initial fetch or manual refresh
  refresh,        // async () => void — manual re-fetch (shows loading spinner)
  markRead,       // (id: string) => void — optimistic update + persists via channel
  markAllRead,    // () => void — optimistic update + persists via channel
  send,           // (notification) => Promise<void> — send via service
  service,        // NotificationService — direct access
} = useNotifications();

useUnreadCount()

Lightweight hook — returns only the unread count. Returns 0 safely if there is no provider in the tree (no throw), making it safe for standalone components.

import { useUnreadCount } from '@mounaji_npm/notifications';

const unreadCount = useUnreadCount(); // 0 if no provider in tree

UI Components

All components read Mounaji design tokens (--mn-* CSS variables) and fall back gracefully to built-in defaults.

NotificationBell

Context-aware bell widget for the TopNav. Requires <NotificationProvider> in the tree.

import { NotificationBell } from '@mounaji_npm/notifications';

<NotificationBell
  isDark={isDark}
  historyPath="/notifications"
  onNavigate={router.push}
/>

| Prop | Type | Default | Description | |---|---|---|---| | isDark | boolean | true | Dark/light mode | | historyPath | string | '/notifications' | Path for the "Ver historial completo" link | | onNavigate | (path) => void | — | Navigation callback (e.g. router.push) |

Features:

  • Unread count badge (red dot on the bell icon)
  • Dropdown panel with the 8 most recent notifications
  • Click to expand metadata details per row
  • "Marcar leídas" button (marks all read)
  • "+N más" when over 8 notifications
  • Clicking "Ver historial completo" calls onNavigate(historyPath)

NotificationToast

Real-time toast stack that appears in a corner when new unread notifications arrive. Does not toast notifications that exist on initial load — only new arrivals after mount.

Place it as a sibling inside <NotificationProvider>.

import { NotificationToast } from '@mounaji_npm/notifications';

<NotificationProvider channels={[...]}>
  <App />
  <NotificationToast isDark={isDark} position="bottom-right" duration={5000} />
</NotificationProvider>

| Prop | Type | Default | Description | |---|---|---|---| | isDark | boolean | true | Dark/light mode | | position | string | 'bottom-right' | 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | | duration | number | 5000 | Auto-dismiss in ms | | maxVisible | number | 5 | Max toasts shown simultaneously |

NotificationHistoryPage

Full-page notification history. Designed to fill a page layout — set max-width, centered.

import { NotificationHistoryPage } from '@mounaji_npm/notifications';

// app/notifications/page.jsx (Next.js App Router)
export default function NotificationsPage() {
  return <NotificationHistoryPage isDark={isDark} />;
}

| Prop | Type | Default | Description | |---|---|---|---| | isDark | boolean | true | Dark/light mode | | onNavigate | (path) => void | — | Navigation callback |

Features:

  • Filter tabs: Todas / Sin leer (with unread count badge)
  • Category filter pills: all categories + custom registered ones
  • Notifications grouped by date: Hoy / Ayer / Esta semana / Anteriores
  • Expandable rows with full metadata
  • "Cargar más" pagination (25 per page)
  • "Marcar todas como leídas" button

NotificationPreferences

User-facing panel to enable/disable notification channels.

import { NotificationPreferences } from '@mounaji_npm/notifications';

<NotificationPreferences
  isDark={isDark}
  onSave={(prefs) => saveUserPreferences(prefs)}
/>

| Prop | Type | Default | Description | |---|---|---|---| | isDark | boolean | true | Dark/light mode | | onSave | (prefs: Record<string, boolean>) => void | — | Called with channel enable/disable map |

Channel rows are derived from the NotificationService.registry — only registered channels appear. Channels not yet implemented (push, whatsapp) appear with a "Pronto" badge and are disabled.


Server-side: createResendSender

Import from @mounaji_npm/notifications/server — never import in client code. This export path is not bundled by Vite and runs in Node.js only.

import { createResendSender } from '@mounaji_npm/notifications/server';

const sender = createResendSender({
  apiKey: process.env.RESEND_API_KEY,   // required
  from:   'Mounaji <[email protected]>', // optional, defaults to placeholder
});

// Send a raw email
await sender.sendEmail({
  to:      '[email protected]',
  subject: 'Bienvenido a Mounaji',
  html:    '<h1>Hola!</h1>',
  text:    'Hola!',
});

// Send a notification object — HTML template is auto-generated
await sender.sendNotification({
  to:           '[email protected]',
  notification: {
    title:    'Stock crítico: Harina 000',
    severity: 'danger',
    metadata: { available: '2kg', required: '15kg' },
  },
});

Wire-up in a Next.js Route Handler

// app/api/notifications/send-email/route.js
import { createResendSender } from '@mounaji_npm/notifications/server';

const sender = createResendSender({
  apiKey: process.env.RESEND_API_KEY,
  from:   'Mounaji <[email protected]>',
});

export async function POST(req) {
  const body = await req.json();
  // body is the payload sent by EmailChannel.send()
  await sender.sendNotification({ to: body.to, notification: body });
  return Response.json({ ok: true });
}

Wire-up in a Next.js Server Action

// lib/actions/notifications.js
'use server';
import { createResendSender } from '@mounaji_npm/notifications/server';

const sender = createResendSender({ apiKey: process.env.RESEND_API_KEY });

export async function sendEmailAction(payload) {
  return sender.sendNotification({ to: payload.to, notification: payload });
}

createResendSender options

| Option | Required | Description | |---|---|---| | apiKey | ✅ | Resend API key — from process.env.RESEND_API_KEY | | from | — | Sender address shown in the email; defaults to placeholder |

sender.sendEmail(params)

| Param | Required | Description | |---|---|---| | to | ✅ | Recipient email or array of emails | | subject | ✅ | Email subject line | | html | — | HTML body | | text | — | Plain text body | | fromOverride | — | Override the from address for this send |

sender.sendNotification(params)

| Param | Required | Description | |---|---|---| | to | ✅ | Recipient email or array of emails | | notification | ✅ | Notification object (title, severity, metadata, ...) | | subject | — | Override subject; defaults to notification.title | | html | — | Override HTML; auto-generated from notification if omitted | | text | — | Override plain text; auto-generated if omitted |


Architecture overview

<NotificationProvider>          ← React context root
  channels: [WebChannel, EmailChannel]
  config: { pollInterval, watchEvent }
  │
  ├── NotificationService       ← orchestrator
  │     └── ChannelRegistry     ← Map<name, BaseChannel>
  │           ├── WebChannel    ← canFetch, in-app storage
  │           ├── EmailChannel  ← canSend, Resend via API
  │           └── PushChannel   ← stub (Phase 2)
  │
  ├── State: notifications[], loading, unreadCount
  │
  ├── Real-time: window 'mn:events-updated' → silentRefresh()
  ├── Polling:   setInterval(silentRefresh, pollInterval)
  │
  └── Context value exposed to:
        useNotifications()        ← full context
        useUnreadCount()          ← unreadCount only (safe w/o provider)
        <NotificationBell />      ← dropdown widget
        <NotificationToast />     ← corner toast stack
        <NotificationHistoryPage />← full history page
        <NotificationPreferences />← channel toggles

Adding a custom channel (WhatsApp example — Phase 2)

// 1. Extend BaseChannel
import { BaseChannel } from '@mounaji_npm/notifications';

class WhatsAppChannel extends BaseChannel {
  name = 'whatsapp';
  get canSend() { return true; }

  constructor({ token, phoneNumberId }) {
    super();
    this._token         = token;
    this._phoneNumberId = phoneNumberId;
  }

  async send({ to, title, message }) {
    const text = message ? `*${title}*\n${message}` : title;
    await fetch(`https://graph.facebook.com/v18.0/${this._phoneNumberId}/messages`, {
      method:  'POST',
      headers: { Authorization: `Bearer ${this._token}`, 'Content-Type': 'application/json' },
      body:    JSON.stringify({ messaging_product: 'whatsapp', to, type: 'text', text: { body: text } }),
    });
  }
}

// 2. Register it in the provider
<NotificationProvider
  channels={[
    webChannel,
    emailChannel,
    new WhatsAppChannel({
      token:         process.env.WHATSAPP_TOKEN,
      phoneNumberId: process.env.WHATSAPP_PHONE_ID,
    }),
  ]}
>

// 3. Send via WhatsApp
const { send } = useNotifications();
await send({
  title:    'Stock crítico: Harina 000',
  to:       '+5491112345678',
  channels: ['whatsapp'],
  severity: 'danger',
});

Notification shape

The object shape expected by all components and produced by @mounaji_npm/event-core's createEvent:

{
  id:         string;                         // UUID
  type:       string;                         // EVENT_TYPES constant
  category:   string;                         // EVENT_CATEGORIES constant
  severity:   'info' | 'success' | 'warning' | 'danger';
  priority:   'critical' | 'high' | 'normal' | 'low';
  source:     string | null;
  target:     string | null;
  title:      string;
  message?:   string;                         // optional longer description
  metadata?:  Record<string, unknown>;        // shown in expandable panel
  read:       boolean;
  created_at: string;                         // ISO 8601
  color?:     string;                         // CSS color override for the icon bubble
}

Related packages

| Package | Role | |---|---| | @mounaji_npm/event-core | Event types, bus, registry, DOM bridge — the foundation | | @mounaji_npm/topnav-widgets | Standalone NotificationBell (no provider required) | | @mounaji_npm/api-client | HTTP client that dispatches signalEventsUpdate() |