# @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

```bash
npm install @mounaji_npm/topnav-widgets
```

**Peer dependency:** React ≥ 17

---

## Exports

```js
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

```jsx
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):

```ts
{
  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:**
```jsx
<NotificationBell pollInterval={0} onFetch={...} />
```

**To use a custom DOM event name:**
```jsx
<NotificationBell watchEvent="my-app:events-updated" onFetch={...} />
```

### Full wiring example (Next.js App Router)

```jsx
// 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:

```js
// 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:

```js
// 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

```jsx
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[] }`):

```ts
{
  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.

```jsx
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`:

```jsx
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>
```
