npm package: @notilane/notif-sdk
Description: NotiLane Notification Widget for external applications
Latest version: 1.2.0

# NotiLane Notification Widget

A production-ready notification widget for displaying NotiLane posts in your web applications. Features a notification bell with badge, dropdown panel, "Load more" button for pagination, real-time polling, and theme support.

**Note**: The widget automatically filters to display only **Notification** type posts (not Article posts). This ensures the bell icon dropdown shows notifications intended for end-users, while Article posts are used separately for contextual help features.

## Features

- 🔐 OAuth 2.0-style authentication with client credentials
- 🔔 Notification bell with unread badge count
- 📋 Dropdown panel with post list
- 📜 "Load more" button for pagination (loads posts on demand)
- 🔄 Real-time polling for new posts
- ✅ Read/unread status tracking (persisted in IndexedDB with localStorage fallback)
- 🌓 Light and dark themes
- 🌍 Internationalization (15 languages supported)
- ♿ Keyboard accessibility
- 📨 **Send Smart Message** - Programmatic dialog to send messages across multiple channels (Email, SMS, Teams, Push, WebPush)
- 📱 Responsive design
- 🎨 Contextual article help icons
- 🚀 Zero external dependencies

## Installation

### Via NPM

```bash
npm install @notilane/notif-sdk
```

Then import in your JavaScript/TypeScript:

```javascript
import '@notilane/widget';
```

### Via Script Tag (CDN)

```html
<!-- Production CDN -->
<script src="https://cdn.notilane.com/widget/v1/notilane-widget.umd.js"></script>

<!-- Or specific version -->
<script src="https://cdn.notilane.com/widget/v1.0.0/notilane-widget.umd.js"></script>
```

### Self-Hosted

Download the latest release and include it in your project:

```html
<script src="/path/to/notilane-widget.umd.js"></script>
```

## Quick Start

```html
<!DOCTYPE html>
<html>
<head>
  <title>My Application</title>
</head>
<body>
  <!-- Notification Bell Icon -->
  <div id="notification-bell" style="position: relative; cursor: pointer;">
    <span style="font-size: 24px;">🔔</span>
  </div>

  <!-- NotiLane Widget Script -->
  <script src="https://cdn.notilane.com/widget/v1/notilane-widget.umd.js"></script>
  <script>
    window.NotiLaneWidget = window.NotiLaneWidget || {};
    window.NotiLaneWidget.config = {
      // Required configuration
      apiBaseUrl: "https://api.notilane.com/api/v1",
      clientId: "your-client-id",
      clientSecret: "your-client-secret",
      laneId: "your-lane-id",
      bellElementId: "notification-bell",
      
      // Optional configuration
      theme: "light",                    // "light" or "dark"
      maxNotifications: 200,             // Max notifications in memory (default: 200)
      postsPerPage: 10,                  // Posts per API request
      autoFetch: true,                   // Fetch posts on init (default: true)
      pollingIntervalInSeconds: 60,      // Poll interval in seconds (min: 60, 0 to disable)
      maxPostsAgeDays: 30,               // Max age of posts to fetch (1-30 days)
      dropdownPosition: "bottom-right",  // Dropdown position
      showTimestamp: true,               // Show relative time
      showReadStatus: true,              // Track read/unread
      locale: "en"                       // Auto-detected from browser, or set explicitly
    };
  </script>
</body>
</html>
```

## Configuration Options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `apiBaseUrl` | string | ✅ | - | NotiLane API base URL |
| `clientId` | string | ✅ | - | External app client ID |
| `clientSecret` | string | ✅ | - | External app client secret |
| `laneId` | string | ✅ | - | Lane ID (MongoDB ObjectId) |
| `bellElementId` | string | ✅ | - | HTML element ID for bell icon |
| `theme` | "light" \| "dark" | ❌ | "light" | Visual theme |
| `maxNotifications` | number | ❌ | 200 | Max notifications in memory |
| `postsPerPage` | number | ❌ | 10 | Posts per API request |
| `autoFetch` | boolean | ❌ | true | Fetch posts on init (false = lazy load on dropdown open) |
| `pollingIntervalInSeconds` | number | ❌ | 60 | Poll interval in seconds (min: 60, 0 to disable) |
| `maxPostsAgeDays` | number | ❌ | 30 | Max age of posts to fetch in days (1-30) |
| `dropdownPosition` | string | ❌ | "bottom-right" | Dropdown position |
| `showTimestamp` | boolean | ❌ | true | Show relative timestamps |
| `showReadStatus` | boolean | ❌ | true | Track read/unread status |
| `locale` | string | ❌ | auto | Interface language (auto-detected). Supported: en, fr, cs, de, es, es-PE, es-CL, fr-BE, hr, hu, nl, nl-BE, pl, sk, sl |
| `customClass` | string | ❌ | - | Custom CSS class |
| `cssClasses` | object | ❌ | - | Override CSS classes (supports light/dark themes) |
| `enableArticleHelp` | boolean | ❌ | false | Enable article help feature |
| `helpIconClass` | string | ❌ | - | Custom CSS class for article help icons |
| `onPostClick` | function | ❌ | - | Callback when post is clicked |
| `onAuthError` | function | ❌ | - | Callback on auth error |

## Pagination

The widget uses a **"Load more" button** for pagination instead of infinite scroll.

### How It Works

1. Initial load fetches the first page of posts (default: 10 posts)
2. A "Load more" button appears at the bottom if more posts are available
3. Clicking the button loads the next page and appends posts to the list
4. The button automatically hides when all posts are loaded

### Configuration

```javascript
window.NotiLaneWidget.config = {
  // ...
  postsPerPage: 10,        // Number of posts per page (default: 10)
  maxNotifications: 200    // Maximum posts to load in memory (default: 200)
};
```

## Mandatory Posts Popup

Posts with `isMandatory: true` are automatically displayed in a modal popup that requires user acknowledgment.

### Behavior

- **Auto-display on load**: Unread mandatory posts are shown immediately when the widget initializes
- **Modal popup**: The popup blocks interaction with the page until dismissed
- **Queue navigation**: If multiple mandatory posts exist, users can navigate using Previous/Next buttons
- **Acknowledgment required**: Posts must be explicitly marked as read
- **Persistence**: Read status is stored in IndexedDB

### Events

```javascript
const widget = window.NotiLaneWidget.instance;

// Popup opened
widget.on('mandatory-popup-open', () => {
  console.log('Mandatory popup opened');
});

// Popup closed (all mandatory posts read)
widget.on('mandatory-popup-close', () => {
  console.log('Mandatory popup closed');
});

// Individual post marked as read
widget.on('mandatory-post-read', (event) => {
  console.log('Mandatory post read:', event.data.postId);
});
```

## CSS Class Customization

All CSS classes can be overridden via the `cssClasses` configuration option.

### Theme-Aware CSS Classes

```javascript
window.NotiLaneWidget.config = {
  theme: "dark",
  
  cssClasses: {
    // Light theme classes
    light: {
      dropdown: 'bg-white border shadow-lg',
      postItem: 'bg-white hover:bg-gray-50',
      postTitle: 'text-gray-900',
      postBody: 'text-gray-600'
    },
    
    // Dark theme classes (override light)
    dark: {
      dropdown: 'bg-gray-800 border-gray-700 shadow-lg',
      postItem: 'bg-gray-800 hover:bg-gray-700',
      postTitle: 'text-white',
      postBody: 'text-gray-300'
    }
  }
};
```

### Available Class Overrides

| Class Key | Default Class | Description |
|-----------|---------------|-------------|
| `widget` | `notilane-widget` | Main widget container |
| `badge` | `notilane-badge` | Unread count badge |
| `dropdown` | `notilane-dropdown` | Dropdown container |
| `header` | `notilane-header` | Header container |
| `headerTitle` | `notilane-header-title` | Header title |
| `postsList` | `notilane-posts-list` | Posts list container |
| `postItem` | `notilane-post-item` | Post item container |
| `postItemUnread` | `unread` | Unread post modifier |
| `postTitle` | `notilane-post-title` | Post title |
| `postBody` | `notilane-post-body` | Post body text |
| `postTags` | `notilane-post-tags` | Tags container |
| `tag` | `notilane-tag` | Individual tag |
| `postTime` | `notilane-post-time` | Timestamp |
| `loading` | `notilane-loading` | Loading container |
| `spinner` | `notilane-spinner` | Loading spinner |
| `error` | `notilane-error` | Error container |
| `empty` | `notilane-empty` | Empty state container |
| `loadMoreButton` | `notilane-load-more` | Load more button |

### Example: Tailwind CSS Integration

```javascript
window.NotiLaneWidget.config = {
  cssClasses: {
    light: {
      dropdown: 'absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-xl border',
      header: 'flex justify-between items-center px-4 py-3 border-b',
      headerTitle: 'text-lg font-semibold text-gray-900',
      postItem: 'px-4 py-3 border-b hover:bg-gray-50 cursor-pointer',
      postItemUnread: 'bg-blue-50',
      postTitle: 'font-medium text-gray-900',
      postBody: 'text-sm text-gray-600 mt-1',
      tag: 'inline-block px-2 py-1 text-xs bg-gray-100 rounded mr-1'
    }
  }
};
```

## Article Help Icons

Display contextual help icons next to any element on your page that fetch and display article content from NotiLane.

### Enabling the Feature

```javascript
window.NotiLaneWidget.config = {
  // ... required config ...
  
  enableArticleHelp: true,  // Enable article help feature
  helpIconClass: 'fa fa-question-circle'  // Optional: Custom icon class
};
```

### HTML Markup

Add the `nl-article-aid` attribute to any element. The value should be the **External ID** of the article in NotiLane:

```html
<!-- The External ID should match the externalId in NotiLane -->
<label nl-article-aid="help-password-requirements">Password</label>
<input type="password" />

<div nl-article-aid="feature-explanation" class="my-feature">
  Feature Title
</div>

<span nl-article-aid="tooltip-123">Click for help</span>
```

### How It Works

1. **Initialization**: Widget scans DOM for `[nl-article-aid]` elements
2. **Icon Display**: A help icon appears next to each element
3. **Click Handler**: Clicking fetches the article from the API
4. **Popover Display**: Article content is displayed in a styled popover
5. **Error Handling**: Errors are shown in a toast notification

### Custom Icon Styling

```javascript
// FontAwesome
window.NotiLaneWidget.config = {
  enableArticleHelp: true,
  helpIconClass: 'fa-solid fa-circle-question'
};

// Material Icons
window.NotiLaneWidget.config = {
  enableArticleHelp: true,
  helpIconClass: 'material-icons'
};
```

### Showing/Hiding Help Icons

Control help icon visibility programmatically:

#### Via Instance Methods

```javascript
const widget = window.NotiLaneWidget.instance;

widget.hideArticleHelp();     // Hide all help icons
widget.showArticleHelp();     // Show all help icons
widget.toggleArticleHelp();   // Toggle visibility

// Check current state
const isVisible = widget.isArticleHelpVisible;
```

#### Via DOM Events

```javascript
// Hide all help icons
document.dispatchEvent(new CustomEvent('notilane:help:hide'));

// Show all help icons
document.dispatchEvent(new CustomEvent('notilane:help:show'));

// Toggle visibility
document.dispatchEvent(new CustomEvent('notilane:help:toggle'));
```

#### Listening for Visibility Changes

```javascript
document.addEventListener('notilane:help:visibility-changed', (event) => {
  console.log('Help icons visible:', event.detail.visible);
});
```

### Complete Example

```html
<button id="helpToggle">👁️ Toggle Help Icons</button>

<script>
  document.getElementById('helpToggle').addEventListener('click', () => {
    window.NotiLaneWidget.instance.toggleArticleHelp();
  });

  document.addEventListener('notilane:help:visibility-changed', (e) => {
    const btn = document.getElementById('helpToggle');
    btn.textContent = e.detail.visible ? '👁️ Hide Help' : '👁️ Show Help';
  });
</script>
```

## Programmatic API

### Widget Methods

```javascript
const widget = window.NotiLaneWidget.instance;

// Open/close dropdown
widget.open();
widget.close();
widget.toggle();

// Refresh posts
await widget.refresh();

// Get data
const posts = widget.getPosts();
const unreadCount = widget.getUnreadCount();

// Check status
const isReady = widget.isReady();

// Event subscription
const unsubscribe = widget.on('new-posts', (event) => {
  console.log('New posts:', event.data.posts);
});

// Clean up
widget.destroy();
```

### Dynamic Theme Switching

Switch between light and dark themes at runtime when your application's theme changes:

```javascript
const widget = window.NotiLaneWidget.instance;

// Get current theme
console.log(widget.theme); // 'light' or 'dark'

// Set theme dynamically
widget.setTheme('dark');
widget.setTheme('light');

// Example: Sync with your app's theme toggle
document.getElementById('theme-toggle').addEventListener('click', () => {
  const isDark = document.body.classList.toggle('dark-mode');
  widget.setTheme(isDark ? 'dark' : 'light');
});

// Example: Watch for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  widget.setTheme(e.matches ? 'dark' : 'light');
});

// Listen for theme changes
widget.on('theme-changed', (event) => {
  console.log('Theme changed to:', event.data.theme);
});
```

### User Context (Read Tracking)

The widget **always** calls the read tracking API (`POST /lanes/{laneId}/posts/{postId}/read`) when a user clicks "Read more" or "Mark as read". This allows counting total read clicks regardless of user identification.

Optionally, set user context to associate reads with specific users for analytics:

```javascript
const widget = window.NotiLaneWidget.instance;

// Set user context (userId is mandatory)
widget.setUserContext({
  userId: 'user_12345',       // Required: Unique ID in your system
  name: 'John Doe',           // Optional: Display name
  email: 'john@example.com',  // Optional: Email
  plan: 'premium',            // Optional: Custom properties allowed
  department: 'Engineering'   // Optional: Any flat key-value pairs
});

// Clear context on logout
widget.clearUserContext();

// Get current context
const ctx = widget.getUserContext();
```

**Behavior:**
- **Always**: API call `POST /lanes/{laneId}/posts/{postId}/read` is sent on every read action
- **With userContext**: Request includes `X-NotiLane-User-Context` header for user-level tracking
- **Without userContext**: Request is sent without user header (anonymous read count)

**Constraints (when setting userContext):**
- `userId` is **mandatory** - context is rejected without it
- Maximum context size: 2KB
- Values truncated to 256 characters
- Only flat properties (no nested objects/arrays)

### Events

| Event | Data | Description |
|-------|------|-------------|
| `initialized` | `{ success: boolean }` | Widget initialized |
| `authenticated` | `{ success: boolean }` | Authentication completed |
| `posts-loaded` | `{ posts: PostDto[], count: number }` | Posts loaded |
| `new-posts` | `{ posts: PostDto[], count: number }` | New posts from polling |
| `error` | `{ message: string, error: any }` | Error occurred |
| `dropdown-open` | `{}` | Dropdown opened |
| `dropdown-close` | `{}` | Dropdown closed |
| `mandatory-popup-open` | `{}` | Mandatory popup opened |
| `mandatory-popup-close` | `{}` | Mandatory popup closed |
| `mandatory-post-read` | `{ postId: string }` | Mandatory post marked as read |
| `article-help-open` | `{ externalId: string }` | Article help popover opened |
| `article-help-close` | `{}` | Article help popover closed |
| `article-help-error` | `{ error: string, externalId: string }` | Error loading article |
| `theme-changed` | `{ theme: 'light' \| 'dark' }` | Theme changed via setTheme() |
| `user-context-changed` | `{ userId: string \| null }` | User context set or cleared |
| `smart-message-open` | `{}` | Send smart message dialog opened |
| `smart-message-close` | `{}` | Send smart message dialog closed |
| `smart-message-success` | `{ result: SendSmartMessageResult }` | Message sent successfully |
| `smart-message-error` | `{ error: SendSmartMessageError }` | Send message failed |

### DOM Events (Article Help Visibility)

| Event (dispatch) | Description |
|------------------|-------------|
| `notilane:help:show` | Show all help icons |
| `notilane:help:hide` | Hide all help icons |
| `notilane:help:toggle` | Toggle help icon visibility |

| Event (listen) | Detail | Description |
|----------------|--------|-------------|
| `notilane:help:visibility-changed` | `{ visible: boolean }` | Emitted when visibility changes |

## Storage

The widget uses **IndexedDB** for storing read post IDs (with localStorage fallback):

- Read post IDs are stored locally per lane
- Automatic cleanup removes oldest entries when exceeding 5,000 posts per lane
- Data persists across browser sessions

### Clearing Read Posts

```javascript
const widget = window.NotiLaneWidget.instance;
const storage = widget.postsManager.getReadPostsStorage();
await storage.clear();
```

## Send Smart Message

The Send Smart Message feature allows host applications to open a dialog for sending messages to selected recipients across multiple notification channels (Email, SMS, Teams, Push, WebPush).

### Opening the Dialog

```javascript
const widget = window.NotiLaneWidget.instance;

widget.openSendSmartMessageDialog({
  // Optional: Recipients provided by your application
  recipients: [
    {
      id: 'user-123',
      firstName: 'John',
      lastName: 'Doe',
      email: 'john.doe@example.com',
      phoneNumber: '+15551234567',
      // Optional: Custom data sent in the API payload for this recipient
      customData: {
        department: 'Engineering',
        employeeId: 'EMP-001'
      }
    },
    {
      id: 'user-456',
      fullName: 'Jane Smith',
      email: 'jane.smith@example.com'
    }
  ],
  
  // Optional: Restrict which channels are shown (default: all)
  // Use this to hide channels not relevant to your application
  allowedChannels: ['Email', 'Sms', 'Teams', 'Push', 'WebPush'],
  
  // Optional: Pre-select channels
  defaultChannels: ['Email'],
  
  // Optional: Pre-select recipients by ID
  preselectedRecipientIds: ['user-123'],
  
  // Optional: Default selection mode
  defaultSelectionMode: 'TryAllChannels', // or 'StopOnFirstSuccess'
  
  // Optional: Pre-fill message content
  defaultTitle: 'Important Update',
  defaultBody: '',
  
  // Optional: Teams card customization
  teamsOptions: {
    format: 'Hero',        // 'Simple' | 'Hero' | 'Fact' | 'Alert' | 'Announcement'
    themeColor: '0078D4',  // Hex color without #
    headerImageUrl: 'https://example.com/image.png'
  },
  
  // Optional: Advanced options (debugging)
  showAdvancedOptions: false,
  defaultAdvancedOptions: {
    actionUrl: 'https://example.com/action',
    externalId: 'tracking-123'
  },
  
  // Optional: External data for tracking
  externalData: {
    externalId: 'msg-123',
    payload: { customField: 'value' }
  },
  
  // Callbacks
  onSuccess: (result) => {
    console.log('Message sent successfully:', result);
    // result.success - true if at least one message sent
    // result.deliveredVia - channels used for single recipient
    // result.successCount / result.failureCount - for bulk
  },
  onError: (error) => {
    console.error('Failed to send message:', error);
  },
  onCancel: () => {
    console.log('User cancelled the dialog');
  }
});
```

### Closing the Dialog

```javascript
widget.closeSendSmartMessageDialog();
```

### Recipient Interface

Each recipient object should conform to the `WidgetRecipient` interface:

```typescript
interface WidgetRecipient {
  id: string;                   // Unique identifier (used as externalId in API)
  firstName?: string;           // First name
  lastName?: string;            // Last name
  fullName?: string;            // Full name (used if firstName/lastName not set)
  email?: string;               // Required for Email channel
  phoneNumber?: string;         // Required for SMS channel (E.164 format)
  customData?: Record<string, string>; // Per-recipient custom data (see below)
}

type NotificationChannelName = 'Email' | 'Sms' | 'Teams' | 'Push' | 'WebPush' | 'WhatsApp' | 'Telegram';
```

#### Per-Recipient Custom Data

The `customData` field allows you to attach recipient-specific metadata that will be included in the API payload:

```javascript
recipients: [
  {
    id: 'user-123',
    firstName: 'John',
    lastName: 'Doe',
    email: 'john@example.com',
    customData: {
      department: 'Engineering',
      employeeId: 'EMP-001',
      region: 'EU'
    }
  }
]
```

**How it works:**
- Each recipient's `customData` is sent as `externalData.payload` in the API request
- For single recipient: merged with dialog-level `externalData.payload` (recipient data overrides)
- For bulk recipients: each recipient DTO includes their own `externalData` with `payload`
- The recipient's `id` is automatically used as `externalData.externalId` for tracking

### Dialog Options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `recipients` | `WidgetRecipient[]` | ❌ | `[]` | Initial recipients to display (more can be added dynamically) |
| `allowedChannels` | `NotificationChannelName[]` | ❌ | All channels | Restricts which channels are shown in the dialog. Hidden channels cannot be selected. |
| `defaultChannels` | `NotificationChannelName[]` | ❌ | `[]` | Pre-selected channels |
| `preselectedRecipientIds` | `string[]` | ❌ | `[]` | Pre-selected recipient IDs |
| `defaultSelectionMode` | `'TryAllChannels' \| 'StopOnFirstSuccess'` | ❌ | `'TryAllChannels'` | Default channel selection mode |
| `defaultTitle` | `string` | ❌ | `''` | Pre-filled message title |
| `defaultBody` | `string` | ❌ | `''` | Pre-filled message body |
| `teamsOptions` | `TeamsCardOptions` | ❌ | - | Teams card customization (format, theme color, header image, facts, actions) |
| `showAdvancedOptions` | `boolean` | ❌ | `false` | Show advanced options panel (Action URL, External ID) |
| `defaultAdvancedOptions` | `object` | ❌ | - | Pre-fill advanced options `{ actionUrl, externalId }` |
| `externalData` | `object` | ❌ | - | External data to include with messages `{ externalId, payload }` |
| `laneId` | `string` | ❌ | widget's laneId | Override lane ID for lane-specific configuration |
| `onSuccess` | `function` | ❌ | - | Called on successful send |
| `onError` | `function` | ❌ | - | Called on send error |
| `onCancel` | `function` | ❌ | - | Called when user cancels |

### TeamsCardOptions Interface

```typescript
interface TeamsCardOptions {
  format?: 'Simple' | 'Hero' | 'Fact' | 'Alert' | 'Announcement';
  headerImageUrl?: string;   // URL for header image (Hero/Announcement formats)
  themeColor?: string;       // Hex color without # (e.g., "0078D4")
  facts?: Array<{ name: string; value: string }>;  // Key-value pairs (Fact format)
  additionalActions?: Array<{ type: 'OpenUrl'; name: string; url: string }>;
  footerText?: string;       // Footer text
}
```

### Channel Selection Modes

| Mode | Description |
|------|-------------|
| `TryAllChannels` | Attempt delivery on all selected channels simultaneously |
| `StopOnFirstSuccess` | Stop after the first successful delivery (ordered by channel priority) |

### Features

- **Multi-recipient selection**: Select all or individual recipients with checkboxes
- **Dynamic recipients**: Add new recipients directly in the dialog
- **Channel filtering**: Use `allowedChannels` to show only relevant channels for your app
- **Channel selection**: Choose delivery channels with sortable order (drag & drop)
- **Selection mode**: "Try all channels" or "Stop on first success"
- **Teams customization**: Card format, theme color, header image, facts, action buttons
- **Advanced options**: Action URL, External ID for tracking (optional panel)
- **Validation**: Real-time form validation with clear error messages
- **Localization**: Supports all 15 widget locales
- **Theming**: Inherits light/dark theme from widget configuration
- **Accessibility**: Full keyboard navigation and ARIA support
- **Result dialog**: Shows detailed success/failure results

### Events

```javascript
// Listen for send smart message events
widget.on('smart-message-open', () => {
  console.log('Send smart message dialog opened');
});

widget.on('smart-message-close', () => {
  console.log('Send smart message dialog closed');
});

widget.on('smart-message-success', (event) => {
  console.log('Message sent:', event.data);
});

widget.on('smart-message-error', (event) => {
  console.error('Send failed:', event.data.error);
});
```

### CSS Customization

Override send message dialog styles via `cssClasses`:

```javascript
window.NotiLaneWidget.config = {
  cssClasses: {
    light: {
      sendDialogOverlay: 'my-overlay',
      sendDialog: 'my-dialog',
      sendDialogHeader: 'my-header',
      sendDialogContent: 'my-content',
      sendDialogFooter: 'my-footer',
      sendRecipientList: 'my-recipients',
      sendChannelChip: 'my-channel-chip',
      sendSubmitButton: 'my-submit-btn'
    }
  }
};
```

## Security Considerations

⚠️ **Important**: Exposing client secrets in browser JavaScript is not recommended for production.

### Recommended: Backend Proxy

For production applications, use a backend proxy to keep credentials secure:

```javascript
// Frontend
window.NotiLaneWidget.config = {
  apiBaseUrl: "https://yourapp.com/api/notilane-proxy",
  laneId: "your-lane-id",
  bellElementId: "notification-bell"
  // No clientId or clientSecret needed
};
```

```javascript
// Backend (Node.js/Express example)
app.post('/api/notilane-proxy/authentications/external-app-token', async (req, res) => {
  const response = await fetch('https://api.notilane.com/api/v1/authentications/external-app-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      clientId: process.env.NOTILANE_CLIENT_ID,
      clientSecret: process.env.NOTILANE_CLIENT_SECRET
    })
  });
  res.json(await response.json());
});
```

## Custom Styling

All CSS classes are prefixed with `notilane-` for easy targeting:

```css
/* Custom badge color */
.notilane-badge {
  background-color: #ff6b6b;
}

/* Custom post item style */
.notilane-post-item.unread {
  background-color: #e8f4fd;
}

/* Custom header */
.notilane-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
```

### Adding a Custom Class

```javascript
window.NotiLaneWidget.config = {
  customClass: 'my-notifications'
};
```

## Self-Hosted Deployment (Docker)

For organizations that need to host the widget CDN themselves, we provide Docker support for easy deployment.

### Prerequisites

- Docker 20.10+ and Docker Compose v2+
- Node.js 20+ (for local development only)

### Quick Start

```bash
# Clone and build
git clone https://github.com/your-org/notilane-widget.git
cd notilane-widget

# Build and run with Docker Compose
docker-compose up --build

# Widget is now available at:
# http://localhost:8080/widget/latest/notilane-widget.es.js
# http://localhost:8080/widget/latest/notilane-widget.umd.js
```

### Configuration

#### Environment Variables

Create a `.env` file (copy from `.env.example`):

```bash
# Widget version (auto-detected from package.json if not set)
WIDGET_VERSION=1.1.2

# Default API URL baked into the widget build
DEFAULT_API_URL=https://api.notilane.app

# Local CDN port
CDN_PORT=8080
```

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `WIDGET_VERSION` | No | From package.json | Version tag for the build |
| `DEFAULT_API_URL` | No | `https://api.notilane.app` | API URL embedded in widget |
| `CDN_PORT` | No | `8080` | Port for local CDN server |

### Building the Docker Image

#### Using Docker Compose (Recommended)

```bash
# Build with default settings
docker-compose up --build

# Build with custom version
WIDGET_VERSION=2.0.0 docker-compose up --build

# Build with custom API URL
DEFAULT_API_URL=https://api.mycompany.com docker-compose up --build
```

#### Using the Build Script (Windows)

```powershell
# Build with version from package.json
.\scripts\docker-build.ps1

# Build specific version
.\scripts\docker-build.ps1 -Version 1.2.0

# Build and push to registry
.\scripts\docker-build.ps1 -Version 1.2.0 -Registry ghcr.io -Push

# Full options
.\scripts\docker-build.ps1 `
  -Version 1.2.0 `
  -Registry docker.io `
  -ImageName myorg/widget-cdn `
  -ApiUrl https://api.mycompany.com `
  -Push
```

#### Using the Build Script (Linux/macOS)

```bash
# Make executable
chmod +x scripts/docker-build.sh

# Build with version from package.json
./scripts/docker-build.sh

# Build specific version
./scripts/docker-build.sh -v 1.2.0

# Build and push to registry
./scripts/docker-build.sh -v 1.2.0 -r ghcr.io -p
```

### Widget URL Structure

Once deployed, the widget is available at these URLs:

| URL Pattern | Cache Duration | Use Case |
|-------------|----------------|----------|
| `/widget/latest/notilane-widget.es.js` | 5 minutes | Always get latest version |
| `/widget/latest/notilane-widget.umd.js` | 5 minutes | Always get latest version |
| `/widget/v1.2.0/notilane-widget.es.js` | 1 year (immutable) | Lock to specific version |
| `/widget/v1.2.0/notilane-widget.umd.js` | 1 year (immutable) | Lock to specific version |
| `/health` | No cache | Health check endpoint |
| `/widget/version.json` | No cache | Current version info |

### Using Your Self-Hosted Widget

Update your HTML to point to your CDN:

```html
<!-- ES Module (recommended for modern browsers) -->
<script type="module">
  import 'https://cdn.yourcompany.com/widget/latest/notilane-widget.es.js';
</script>

<!-- UMD (for broader compatibility) -->
<script src="https://cdn.yourcompany.com/widget/v1.2.0/notilane-widget.umd.js"></script>

<script>
  window.NotiLaneWidget.config = {
    apiBaseUrl: "https://api.yourcompany.com/api/v1",
    clientId: "your-client-id",
    clientSecret: "your-client-secret",
    laneId: "your-lane-id",
    bellElementId: "notification-bell"
  };
</script>
```

### Health Check

The container exposes a health endpoint for orchestration:

```bash
curl http://localhost:8080/health
# Response: {"status":"healthy","service":"notilane-widget-cdn"}
```

### Production Deployment

#### Docker Run

```bash
docker run -d \
  --name notilane-widget-cdn \
  -p 80:80 \
  --restart unless-stopped \
  notilane/widget-cdn:1.2.0
```

#### Kubernetes

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: notilane-widget-cdn
spec:
  replicas: 2
  selector:
    matchLabels:
      app: notilane-widget-cdn
  template:
    metadata:
      labels:
        app: notilane-widget-cdn
    spec:
      containers:
      - name: widget-cdn
        image: notilane/widget-cdn:1.2.0
        ports:
        - containerPort: 80
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "100m"
```

### CORS Configuration

The NGINX configuration includes permissive CORS headers for widget embedding:

- `Access-Control-Allow-Origin: *` - Allows embedding from any domain
- `Access-Control-Allow-Methods: GET, HEAD, OPTIONS`
- Preflight requests are handled automatically

If you need to restrict origins, edit the `nginx.conf` file:

```nginx
# Replace this:
add_header Access-Control-Allow-Origin "*" always;

# With specific origins:
add_header Access-Control-Allow-Origin "https://app.yourcompany.com" always;
```

### Serving Multiple Versions

To serve multiple widget versions simultaneously, rebuild the image for each version and update the volume mappings:

```bash
# Build version 1.0.0
WIDGET_VERSION=1.0.0 docker-compose up --build
docker cp notilane-widget-cdn:/usr/share/nginx/html/widget/v1.0.0 ./versions/

# Build version 1.1.0
WIDGET_VERSION=1.1.0 docker-compose up --build
docker cp notilane-widget-cdn:/usr/share/nginx/html/widget/v1.1.0 ./versions/

# Mount all versions
docker run -d \
  -v ./versions:/usr/share/nginx/html/widget:ro \
  -p 80:80 \
  notilane/widget-cdn:latest
```

## Browser Support

- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+

## Getting Your Credentials

To use the NotiLane widget, you need:

1. **Client ID** and **Client Secret**: Obtained from your NotiLane dashboard under **Settings > External Apps**
2. **Lane ID**: Found in your NotiLane dashboard under **Lanes > [Your Lane] > Settings**
3. **API Base URL**: `https://api.notilane.com/api/v1`

## Support

- 📧 Email: support@notilane.app

## License

MIT