npm package: @madcoders/wishlist
Description: Vanilla JS wishlist widget library with localStorage, backend sync, and cross-tab support
Latest version: 0.1.3

# @madcoders/wishlist

Vanilla JS wishlist widget library with localStorage-first architecture, backend sync, and cross-tab/cross-browser synchronization.

## Features

- **Zero dependencies** — pure vanilla JS, no frameworks
- **localStorage-first** — UI always renders from localStorage, instant and offline-ready
- **Widget-based** — drop `data-wishlist-*` attributes in your HTML, the library does the rest
- **Cross-tab sync** — BroadcastChannel with `storage` event fallback
- **Backend sync** — optimistic UI with SharedWorker coordination for consistent state
- **Deferred loading** — load with `<script defer>`, never blocks rendering
- **Multi-wishlist ready** — data structure supports multiple wishlists (default list implemented)

## Quick Start

### 1. Install

```bash
npm install @madcoders/wishlist
```

Or use the built files directly:

```html
<link rel="stylesheet" href="path/to/wishlist.css">
<script defer src="path/to/wishlist.js"></script>
```

### 2. Configure

```html
<script defer>
  document.addEventListener('DOMContentLoaded', () => {
    Wishlist.configure({
      apiBaseUrl: 'https://your-api.com/api',   // optional, for backend sync
      workerUrl: '/path/to/wishlist-sync-worker.js', // optional, for SharedWorker sync
      debug: false,
    });

    // For logged-in users:
    Wishlist.setUser('user-123');
  });
</script>
```

### 3. Add Widgets to Your HTML

#### Toggle Button (Product Listing / Product Detail Page)

```html
<button class="wishlist-toggle"
        data-wishlist-toggle="product-123"
        data-wishlist-meta='{"name":"Product Name","image":"/img/product.jpg","price":29.99}'>
</button>
```

The library auto-injects the heart SVG icon and manages the `wishlist-toggle--active` class.

#### Counter Badge (Header)

```html
<span data-wishlist-counter></span>
```

Displays the current item count. Updates automatically on add/remove.

#### Wishlist Overview (Wishlist Page)

```html
<div data-wishlist-overview data-wishlist-list="default"></div>
```

Renders the full item list with quantity editing, remove buttons, and loading state during sync.

## JavaScript API

```js
// Configuration
Wishlist.configure({ apiBaseUrl, workerUrl, endpoints, transformRequest, transformResponse, debug })
Wishlist.setUser(userId)       // Enable backend sync for logged-in user
Wishlist.clearUser()           // Switch to anonymous mode (localStorage only)

// CRUD operations
Wishlist.add(productId, { listId?, qty?, meta? })
Wishlist.remove(productId, { listId? })
Wishlist.toggle(productId, { listId?, meta? })   // Add if absent, remove if present
Wishlist.updateQty(productId, qty, { listId? })
Wishlist.has(productId, { listId? })              // Returns boolean
Wishlist.getItems({ listId? })                    // Returns array of items
Wishlist.getCount({ listId? })                    // Returns number
Wishlist.clear({ listId? })

// Utilities
Wishlist.refresh()                          // Re-scan DOM for new widgets
Wishlist.sync()                             // Force backend sync
Wishlist.registerWidget(selector, Class)    // Register a custom widget

// Events
Wishlist.on(eventName, callback)
Wishlist.off(eventName, callback)
```

## Events

All events are `CustomEvent` instances dispatched on `document`. Listen with `document.addEventListener()` or `Wishlist.on()`.

| Event | When | `detail` |
|---|---|---|
| `wishlist:added` | Item added | `{ productId, listId, item }` |
| `wishlist:removed` | Item removed | `{ productId, listId }` |
| `wishlist:changed` | Any mutation | `{ listId, source }` — source: `"local"`, `"tab"`, or `"sync"` |
| `wishlist:loading` | Backend sync starts | `{ listId }` |
| `wishlist:synced` | Backend sync done | `{ listId }` |
| `wishlist:sync-error` | Backend sync failed | `{ listId, error }` |

### Example: Custom Event Listener

```js
document.addEventListener('wishlist:added', (e) => {
  console.log(`Added ${e.detail.productId} to wishlist`);
  showToast(`${e.detail.item.meta.name} added to wishlist!`);
});
```

## Widget HTML Attributes

| Attribute | Used On | Description |
|---|---|---|
| `data-wishlist-toggle="<productId>"` | `<button>` | Makes element a wishlist toggle for the given product |
| `data-wishlist-meta='<json>'` | Toggle button | Product metadata to store (name, image, price) |
| `data-wishlist-list="<listId>"` | Any widget | Which wishlist to use (default: `"default"`) |
| `data-wishlist-counter` | `<span>` | Displays item count |
| `data-wishlist-hide-empty="true"` | Counter | Hide badge when count is 0 |
| `data-wishlist-overview` | `<div>` | Renders full wishlist with edit/remove controls |

## CSS Classes

| Class | Description |
|---|---|
| `.wishlist-toggle` | Base class for toggle buttons |
| `.wishlist-toggle--active` | Applied when item is in wishlist |
| `.wishlist-toggle--syncing` | Applied during backend sync (heart blink animation) |
| `.wishlist-toggle__icon` | The heart SVG icon |

All styles use BEM naming and are easily overridable.

## CSS Themes

Three CSS themes are provided. Pick the one that matches your project:

| Theme | File | Description |
|---|---|---|
| **Default** | `wishlist.css` | Standalone, no dependencies |
| **Bootstrap** | `wishlist-bootstrap.css` | Uses Bootstrap 5 CSS variables (`--bs-*`) |
| **Semantic UI** | `wishlist-semanticui.css` | Matches Semantic UI / Fomantic UI conventions |

### Usage

**Via `<link>` tag** — pick one:

```html
<!-- Default (standalone) -->
<link rel="stylesheet" href="path/to/wishlist.css">

<!-- Bootstrap 5 (load after bootstrap.css) -->
<link rel="stylesheet" href="path/to/wishlist-bootstrap.css">

<!-- Semantic UI (load after semantic.css) -->
<link rel="stylesheet" href="path/to/wishlist-semanticui.css">
```

**Via npm import** — pick one:

```js
import '@madcoders/wishlist/styles';              // default
import '@madcoders/wishlist/styles/bootstrap';    // Bootstrap 5
import '@madcoders/wishlist/styles/semanticui';   // Semantic UI
```

The Bootstrap theme uses `var(--bs-*)` custom properties so it automatically follows your Bootstrap theming (including dark mode). The Semantic UI theme uses Semantic's standard colors, border-radius, and shadow conventions.

### SCSS Source Files

Raw SCSS files are included in the package for full customization. The structure is modular — import only the components you need.

```
scss/
├── _variables.scss                # All customizable variables with !default
├── _mixins.scss                   # Shared mixins
├── components/
│   ├── _index.scss                # All components (convenience import)
│   ├── _toggle.scss               # Heart toggle button
│   ├── _counter.scss              # Counter badge
│   ├── _overview.scss             # Wishlist overview container
│   ├── _item.scss                 # Wishlist item row (image, name, qty, remove)
│   └── _loading.scss              # Loading spinner
├── wishlist-default.scss          # Default theme entry
├── wishlist-bootstrap.scss        # Bootstrap 5 theme entry
└── wishlist-semanticui.scss       # Semantic UI theme entry
```

### Branding / Custom Theme

Override variables **before** importing to customize:

```scss
// your-project.scss
$wishlist-color-active: #ff6b6b;     // your brand color for hearts
$wishlist-border-radius: 12px;       // rounder cards
$wishlist-font-weight-bold: 700;     // bolder names

@import '@madcoders/wishlist/scss/wishlist-default';
```

### Key Branding Variables

| Variable | Default | Description |
|---|---|---|
| `$wishlist-color-active` | `#e53e3e` | Active heart color, badge background |
| `$wishlist-color-inactive` | `#ccc` | Inactive heart fill |
| `$wishlist-focus-color` | `#4299e1` | Input focus ring color |
| `$wishlist-spinner-color` | `#4299e1` | Loading spinner color |
| `$wishlist-border-radius` | `8px` | Item card border radius |
| `$wishlist-border-color` | `#e2e8f0` | Border color for cards, inputs |
| `$wishlist-item-bg` | `#fff` | Item card background |
| `$wishlist-text-primary` | `#1a202c` | Product name text color |
| `$wishlist-text-secondary` | `#718096` | Price / label text color |
| `$wishlist-badge-border-radius` | `10px` | Badge pill shape |
| `$wishlist-item-image-size` | `64px` | Thumbnail size in overview |

See `scss/_variables.scss` for the full list of 40+ customizable variables.

### Importing Individual Components

If you only need specific widgets, import them directly:

```scss
// Only the heart toggle and counter badge — no overview/item styles
@import '@madcoders/wishlist/scss/variables';
@import '@madcoders/wishlist/scss/mixins';
@import '@madcoders/wishlist/scss/components/toggle';
@import '@madcoders/wishlist/scss/components/counter';
```

## Configurable Backend

Override API endpoint paths and data transforms to integrate with any backend:

```js
Wishlist.configure({
  apiBaseUrl: 'https://my-api.com/v2',
  endpoints: {
    getList:     (listId) => ({ method: 'GET',    path: `/favorites/${listId}` }),
    replaceList: (listId) => ({ method: 'PUT',    path: `/favorites/${listId}` }),
    addItem:     (listId) => ({ method: 'POST',   path: `/favorites/${listId}/products` }),
    removeItem:  (listId, pid) => ({ method: 'DELETE', path: `/favorites/${listId}/products/${pid}` }),
    updateItem:  (listId, pid) => ({ method: 'PATCH',  path: `/favorites/${listId}/products/${pid}` }),
  },
  // Optional: transform data before sending / after receiving
  transformRequest: (action, data) => {
    if (action === 'addItem') return { sku: data.productId, quantity: data.qty };
    return data;
  },
  transformResponse: (action, data) => data,
});
```

Default endpoints match the PoC backend (`/wishlists/:listId/items`). Override only the ones you need.

## Custom Widgets

### Register a widget class

```js
class RecentlyAddedWidget {
  constructor(el) {
    this.el = el;
    document.addEventListener('wishlist:changed', () => this.render());
    this.render();
  }
  render() {
    const items = Wishlist.getItems().slice(-3);
    this.el.innerHTML = items.map(i => `<span>${i.meta.name}</span>`).join(', ');
  }
  destroy() {}
}

Wishlist.registerWidget('[data-wishlist-recent]', RecentlyAddedWidget);
```

```html
<div data-wishlist-recent></div>
```

### Data-only integration (no widget)

Use the JS API and events directly without registering widgets:

```js
document.addEventListener('wishlist:changed', () => {
  const count = Wishlist.getCount();
  myCustomHeader.updateBadge(count);
});
```

## Architecture

### Modes

**Anonymous mode** (default): All data stored in localStorage only. No network requests.

**Logged-in mode** (`Wishlist.setUser(id)`): localStorage remains the source of truth for UI. Backend sync runs in the background via SharedWorker. Optimistic updates — UI updates immediately, sync happens asynchronously.

### Cross-Tab Synchronization

Same-origin tabs sync via `BroadcastChannel` (97% browser support). Falls back to the `storage` event for older browsers. Changes in one tab appear instantly in all other tabs.

### Backend Synchronization

When `workerUrl` is provided and `SharedWorker` is supported, a single SharedWorker coordinates all backend communication:

- **Periodic polling** (every 30s) ensures eventual consistency
- **Debounced mutations** (500ms) batch rapid changes
- **Single coordinator** — only the worker talks to the backend, preventing race conditions
- **Event-based updates** — worker pushes updates to all connected tabs

Falls back to direct `fetch()` calls when SharedWorker is unavailable.

### localStorage Schema

Key: `wishlist_data`

```json
{
  "_version": 1,
  "_lastSyncedAt": "2026-03-22T10:00:00Z",
  "_userId": "user-123",
  "lists": {
    "default": {
      "id": "default",
      "name": "My Wishlist",
      "items": {
        "product-123": {
          "productId": "product-123",
          "qty": 1,
          "addedAt": "2026-03-22T09:30:00Z",
          "meta": { "name": "Product", "image": "/img/p.jpg", "price": 29.99 }
        }
      }
    }
  }
}
```

## Backend API Contract

If you implement your own backend, it should expose these endpoints. Auth is via `X-User-Id` header (replace with your auth mechanism).

| Method | Path | Body | Response |
|---|---|---|---|
| `GET` | `/api/wishlists/:listId` | — | `{ id, name, items: { [productId]: item } }` |
| `PUT` | `/api/wishlists/:listId` | `{ items }` | `{ id, name, items }` |
| `POST` | `/api/wishlists/:listId/items` | `{ productId, qty, meta }` | `{ item }` |
| `DELETE` | `/api/wishlists/:listId/items/:productId` | — | `204` |
| `PATCH` | `/api/wishlists/:listId/items/:productId` | `{ qty }` | `{ item }` |

## Dynamic Content

For SPAs or pages with infinite scroll, call `Wishlist.refresh()` after injecting new product elements into the DOM. The library also uses `MutationObserver` to auto-detect new widgets, but `refresh()` is more reliable for batch insertions.

```js
// After loading more products via AJAX:
container.innerHTML += newProductsHtml;
Wishlist.refresh();
```

## Build

```bash
nvm use          # Uses Node 22 from .nvmrc
npm install
npm run build    # Production build
npm run dev      # Watch mode
npm run server   # Start PoC backend on :3000
npm run demo     # Watch + server concurrently
```

## Output Files

| File | Format | Usage |
|---|---|---|
| `dist/wishlist.js` | IIFE | `<script defer src="...">` |
| `dist/wishlist.esm.mjs` | ESM | `import { Wishlist } from '...'` |
| `dist/wishlist-sync-worker.js` | IIFE | SharedWorker for backend sync |
| `dist/wishlist.css` | CSS | Default theme (standalone) |
| `dist/wishlist-bootstrap.css` | CSS | Bootstrap 5 theme |
| `dist/wishlist-semanticui.css` | CSS | Semantic UI theme |

## Browser Support

- All modern browsers (Chrome, Firefox, Safari, Edge)
- BroadcastChannel: 97% (fallback to storage event)
- SharedWorker: ~51% (fallback to direct fetch)
- localStorage: universal