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/topnav-widgets

v0.1.2

Published

Modular top-nav widgets: NotificationBell, TodayMenuWidget, QuickSearch — theme-aware, token-driven

Readme

@mounaji_npm/topnav-widgets

Theme-aware, token-driven top-nav widgets for Mounaji SaaS applications. Designed to slot directly into the topNavLeft / topNavRight props of @mounaji_npm/saas-template's AppShell.

Widgets:

  • NotificationBell — reactive notification badge + expandable list with metadata detail
  • TodayMenuWidget — daily menu quick-access dropdown
  • QuickSearch — global search trigger

Install

npm install @mounaji_npm/topnav-widgets

Peer dependency: React ≥ 17


Exports

import { NotificationBell, TodayMenuWidget, QuickSearch } from '@mounaji_npm/topnav-widgets';

NotificationBell

Bell icon widget with unread count badge. Opens a dropdown listing the latest events with support for expandable metadata detail panels. Refreshes reactively — the badge updates automatically when new events are emitted by the server, without requiring a page reload or any app-level polling code.

Basic usage

import { NotificationBell } from '@mounaji_npm/topnav-widgets';

<NotificationBell
  isDark={isDark}
  onFetch={() => fetch('/api/events').then(r => r.json())}
  onMarkAllRead={() => fetch('/api/events/mark-all-read', { method: 'PATCH' })}
  onNotificationClick={(n) => fetch(`/api/events/${n.id}/read`, { method: 'PATCH' })}
  historyPath="/history"
  onNavigate={(path) => router.push(path)}
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | notifications | Notification[] | [] | Optional static initial list | | onFetch | async () => Notification[] | — | Lazy-fetch called every time the dropdown opens | | onMarkAllRead | () => void | — | Called when "Marcar leídas" is clicked | | onNotificationClick | (n: Notification) => void | — | Called when a row is clicked | | historyPath | string | '/history' | Path for the "Ver historial completo →" footer link | | onNavigate | (path: string) => void | — | Router push used by the history footer link | | watchEvent | string | 'mn:events-updated' | DOM CustomEvent name to listen for background refresh | | pollInterval | number (ms) | 60000 | Background poll interval in ms. Set 0 to disable polling. | | isDark | boolean | true | Theme mode |

Notification shape

onFetch must return an array of objects with this shape (matches the events Supabase table):

{
  id:         string | number,
  title:      string,          // Primary notification text
  message?:   string,          // Fallback if title is absent
  created_at?: string,         // ISO timestamp → "hace 2 min" (preferred)
  time?:       string,         // Static override if created_at is absent
  read:        boolean,
  severity?:   'info' | 'warning' | 'danger' | 'success',
  category?:   'user' | 'menu' | 'stock' | 'production' | 'system',
  color?:      string,         // CSS color override
  metadata?:   object,         // Key-value pairs shown in expandable detail panel
}

Category icons:

| Category | Icon shape | Color | |---|---|---| | user | Person silhouette | Navy | | menu | Arrow/send | Teal | | stock | Warning triangle | Amber | | production | Pulse/waveform | Green | | system | Info circle / danger circle | Muted / Red |

Metadata detail panel:

When a notification has a non-empty metadata object, clicking the row expands a detail panel below it showing key-value pairs. Built-in label translations (Spanish):

nombre → Nombre     tipo → Tipo        fecha → Fecha
semana → Semana     escuela → Escuela  rol → Rol
ingrediente → Ingrediente              faltante → Faltante
raciones → Raciones estado → Estado    cantidad → Cantidad

Reactive refresh system

NotificationBell manages its own reactivity internally. The app only needs to wire onFetch — no polling state, no useEffect, no setInterval in the app code.

How it works:

Server emits event → API route returns x-events-updated: 1
       ↓
App's API client detects header → calls signalEventsUpdate()
       ↓ (from @mounaji_npm/event-core)
window.dispatchEvent(new CustomEvent('mn:events-updated'))
       ↓
NotificationBell.useEffect([watchEvent]) fires
       ↓
Silent onFetch() → setItems(data) → badge updated ✅

Background poll (every pollInterval ms) catches events not triggered by this client session — e.g. stock alerts computed server-side, or actions by other users.

To disable polling:

<NotificationBell pollInterval={0} onFetch={...} />

To use a custom DOM event name:

<NotificationBell watchEvent="my-app:events-updated" onFetch={...} />

Full wiring example (Next.js App Router)

// app/ClientShell.js
import { useCallback } from 'react';
import { NotificationBell } from '@mounaji_npm/topnav-widgets';
import { apiGet, apiPatch, apiClear } from '../lib/services/_apiClient.js';

function TopNavRight({ isDark, router }) {
  const handleFetch = useCallback(() => {
    apiClear('/api/events');
    return apiGet('/api/events');
  }, []);

  const handleMarkAllRead = useCallback(async () => {
    await apiPatch('/api/events/mark-all-read', {});
    apiClear('/api/events');
  }, []);

  const handleClick = useCallback((n) => {
    apiPatch(`/api/events/${n.id}/read`, {}).then(() => apiClear('/api/events'));
  }, []);

  return (
    <NotificationBell
      isDark={isDark}
      onFetch={handleFetch}
      onMarkAllRead={handleMarkAllRead}
      onNotificationClick={handleClick}
      historyPath="/history"
      onNavigate={(path) => router.push(path)}
    />
  );
}

Server-side: emitting the refresh signal

For NotificationBell to refresh automatically after a mutation, the API route must include the x-events-updated: 1 header:

// app/api/menus/route.js
export const POST = async (req) => {
  const { data } = await createMenu(await req.json());
  return Response.json(data, {
    status: 201,
    headers: { 'x-events-updated': '1' }, // ← triggers NotificationBell refresh
  });
};

And the API client must detect it and dispatch the DOM event:

// lib/services/_apiClient.js
import { signalEventsUpdate } from '@mounaji_npm/event-core';

async function _request(path, options = {}) {
  const res = await fetch(path, options);
  if (res.headers.get('x-events-updated') === '1') {
    apiClear('/api/events');
    signalEventsUpdate();
  }
  // ...
}

TodayMenuWidget

Quick-access dropdown showing today's menu plan grouped by meal type (Desayuno / Almuerzo / Merienda) and course.

Usage

import { TodayMenuWidget } from '@mounaji_npm/topnav-widgets';

<TodayMenuWidget
  date="2026-04-26"
  isDark={isDark}
  onFetch={(date) => fetch(`/api/planning/${date}`).then(r => r.json())}
  onNavigate={(path) => router.push(path)}
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | date | string | today (ISO) | Date to display — 'YYYY-MM-DD' | | onFetch | async (date) => { plans: Plan[] } | — | Fetches planning data for the given date | | onNavigate | (path: string) => void | — | Router push for "Ver planificación completa" | | isDark | boolean | true | Theme mode | | label | string | 'Menú Hoy' | Button label | | planningPath | string | /planning/{date} | Override for the footer navigation link |

Expected data shape

onFetch should return an array of plan records (or { plans: PlanRecord[] }):

{
  menu: {
    id:        string,
    name:      string,
    meal_type: 'desayuno' | 'almuerzo' | 'merienda',
    course?:   'entrada' | 'principal' | 'guarnicion' | 'postre' | 'solido' | 'bebida',
  },
  school?: { name: string },
  student_count?: number,
}

Meal type colors: Desayuno → amber, Almuerzo → green, Merienda → teal
Course colors: Entrada → teal, Principal → green, Postre → amber, Guarnición → purple


QuickSearch

Minimal search trigger for the TopNav. Currently a UI placeholder — wire onSearch to your search provider.

import { QuickSearch } from '@mounaji_npm/topnav-widgets';

<QuickSearch
  isDark={isDark}
  onSearch={(query) => router.push(`/search?q=${query}`)}
  placeholder="Buscar..."
/>

Theme system

All widgets read from CSS variables injected by @mounaji_npm/tokens. No hardcoded colors.

Variables used:

| Variable | Usage | |---|---| | --mn-color-nav-dark/light | Bell button background | | --mn-color-card-dark/light | Dropdown panel background | | --mn-border-dark/light | Panel and row borders | | --mn-topnav-text-primary-dark/light | Primary text | | --mn-topnav-text-secondary-dark/light | Secondary text | | --mn-topnav-text-muted-dark/light | Muted/timestamp text | | --mn-color-primary | Unread dot, "ver más" links, primary accents | | --mn-color-danger | Unread badge on bell icon | | --mn-color-warning | Warning severity notifications | | --mn-color-success | Success severity notifications | | --mn-shadow-lg | Dropdown shadow | | --mn-radius-lg | Dropdown border radius |

TopNav-specific text tokens (--mn-topnav-text-*) fall back to --mn-nav-text-* if not set, so existing themes require no changes.


Integration with AppShell

Pass widgets into the topNavLeft / topNavRight slots of AppShell:

import { AppShell } from '@mounaji_npm/saas-template';
import { NotificationBell, TodayMenuWidget } from '@mounaji_npm/topnav-widgets';

<AppShell
  modules={modules}
  isDark={isDark}
  topNavLeft={
    <TodayMenuWidget
      date={today}
      isDark={isDark}
      onFetch={(d) => apiGet(`/api/planning/${d}`)}
      onNavigate={(p) => router.push(p)}
    />
  }
  topNavRight={
    <NotificationBell
      isDark={isDark}
      onFetch={() => apiGet('/api/events')}
      onMarkAllRead={() => apiPatch('/api/events/mark-all-read', {})}
      onNotificationClick={(n) => apiPatch(`/api/events/${n.id}/read`, {})}
      historyPath="/history"
      onNavigate={(p) => router.push(p)}
    />
  }
>
  {children}
</AppShell>