# chatgram-widget

Production-grade embeddable contact form widget for [Chatgram](https://brandid.app/chatgram).  
Works with React, Vue, vanilla JS — or any frontend via `<script>` tag.

## Features

- **Zero dependencies** — pure vanilla JS, no React/Vue/jQuery required
- **Shadow DOM isolation** — widget CSS never leaks into your site (and vice versa)
- **Full accessibility** — ARIA roles, focus trapping, keyboard navigation, `prefers-reduced-motion`, high contrast mode
- **Anti-spam** — honeypot field, timing-based bot detection, client-side rate limiting
- **Resilient API client** — timeout handling, exponential backoff retry, structured error codes
- **Runtime theming** — switch colors on the fly without re-init
- **Event system** — subscribe to `open`, `close`, `submit`, `success`, `error`, `state-change`
- **ESM + CJS + UMD** — tree-shakeable, CDN-ready, works everywhere
- **TypeScript-first** — complete type definitions and IntelliSense
- **SSR-safe** — no `window`/`document` access at import time

---

## Install

```bash
npm install chatgram-widget
```

### CDN

```html
<script src="https://cdn.jsdelivr.net/npm/chatgram-widget"></script>
```

or

```html
<script src="https://unpkg.com/chatgram-widget"></script>
```

---

## Quick Start

### Vanilla JS (CDN)

```html
<script src="https://cdn.jsdelivr.net/npm/chatgram-widget"></script>
<script>
  window.addEventListener('load', function () {
    ChatgramWidget.init({
      showTriggerButton: false,
      triggerPosition: 'bottom-right',
      theme: {
        primaryColor: '#EE6055',
        secondaryColor: '#D94F44',
      },
    });

    document.getElementById('openBtn').addEventListener('click', function () {
      ChatgramWidget.open();
    });
    document.getElementById('closeBtn').addEventListener('click', function () {
      ChatgramWidget.close();
    });
    document.getElementById('destroyBtn').addEventListener('click', function () {
      ChatgramWidget.destroy();
    });
  });
</script>
```

### ES Module

```js
import { init, open, close, destroy } from 'chatgram-widget';

init({
  showTriggerButton: false,
  theme: {
    primaryColor: '#EE6055',
    secondaryColor: '#D94F44',
  },
});

document.querySelector('#open-btn').addEventListener('click', open);
document.querySelector('#close-btn').addEventListener('click', close);
document.querySelector('#destroy-btn').addEventListener('click', destroy);
```

### React

```jsx
import { useEffect } from 'react';
import { init, open, destroy } from 'chatgram-widget';

function App() {
  useEffect(() => {
    init({
      theme: {
        primaryColor: '#2563EB',
        secondaryColor: '#1D4ED8',
        backgroundColor: '#FFFFFF',
        headerBackgroundColor: '#F8FAFC',
        headerTextColor: '#0F172A',
        textColor: '#475569',
        inputBackgroundColor: '#F1F5F9',
        inputTextColor: '#0F172A',
        inputPlaceholderColor: '#94A3B8',
        borderRadius: '10px',
        modalBorderRadius: '18px',
        fontFamily: 'Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif',
        boxShadow: '0 20px 40px rgba(0,0,0,0.08)',
      },
    });
    return () => destroy();
  }, []);

  return <button onClick={open}>Contact Us</button>;
}
```

### Vue 3

```vue
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { init, open, destroy } from 'chatgram-widget';

onMounted(() => {
  init({
    theme: {
      primaryColor: '#EE6055',
      secondaryColor: '#D94F44',
    },
  });
});
onUnmounted(() => destroy());
</script>

<template>
  <button @click="open">Contact Us</button>
</template>
```

---

## API Reference

| Method | Description |
|--------|-------------|
| `init(config)` | Initialize the widget. |
| `open()` | Open the modal |
| `close()` | Close the modal |
| `destroy()` | Remove widget from DOM entirely |
| `updateTheme(theme)` | Change colors at runtime |
| `on(event, handler)` | Subscribe to events |
| `off(event, handler)` | Unsubscribe |
| `once(event, handler)` | Subscribe once |
| `isOpen()` | Returns `boolean` |
| `getState()` | Returns `'idle' \| 'loading' \| 'success' \| 'error' \| 'uninitialized'` |
| `getVersion()` | Returns version string |

---

## Configuration

```ts
init({
  publicKey: 'pk_abc123',               // Optional — when provided, domain auto-detection is skipped
  domain: 'mysite.com',                 // Default: auto-detected (skipped when publicKey is set)
  apiBaseUrl: 'https://custom-api.com', // Default: chatgram-server.brandid.app

  // Theme
  theme: {
    primaryColor: '#EE6055',
    secondaryColor: '#D94F44',
    backgroundColor: '#FFFFFF',
    headerBackgroundColor: '#2C3345',
    headerTextColor: '#FFFFFF',
    textColor: '#6B7280',
    inputBackgroundColor: '#1E2535',
    inputTextColor: '#FFFFFF',
    inputPlaceholderColor: '#9CA3AF',
    borderRadius: '8px',
    modalBorderRadius: '16px',
    fontFamily: 'system-ui, sans-serif',
    boxShadow: '0 25px 60px rgba(0,0,0,0.3)',
  },

  // Texts (i18n)
  texts: {
    title: 'Contact Support',
    subtitle: "We're here to help you succeed",
    submitButtonText: 'Submit',
    successTitle: 'Message Sent!',
    successMessage: "We've received your message and will get back to you via Email.",
    errorMessage: 'Something went wrong.',
    categories: { feedback: 'Feedback', bug: 'Bug', feature: 'Feature', support: 'Support' },
    // ... see types.ts for all options
  },

  // Form behavior
  maxMessageLength: 5000,
  showTriggerButton: true,
  triggerPosition: 'bottom-right',
  triggerIconUrl: 'https://example.com/icon.png', // Custom trigger button icon
  triggerBackgroundColor: '#EE6055',               // Trigger button bg color (default: theme.primaryColor)
  triggerTextColor: '#FFFFFF',                     // Trigger button icon color
  triggerTooltip: 'Need Help?',                    // Trigger button tooltip text
  showNameField: true,
  zIndex: 999999,
  closeOnOverlayClick: true,
  closeOnEscape: true,
  defaultEmail: 'user@example.com', // Pre-fill
  defaultName: 'John Doe',          // Pre-fill

  // Security
  enableHoneypot: true,
  minSubmitDelay: 2,      // seconds — blocks bots that submit instantly
  rateLimitMax: 3,        // max submissions per window
  rateLimitWindow: 300000, // 5 minutes

  // Network
  requestTimeout: 15000,
  retryAttempts: 2,

  // Auto-close
  autoResetDelay: 0, // ms after success (0 = manual close)

  // Callbacks
  onSuccess: (data) => console.log('Ticket:', data.ticketId),
  onError: (err) => console.error(err.code, err.message),
  onOpen: () => {},
  onClose: () => {},
  onSubmit: (payload) => {
    // Return false to cancel submission
    return true;
  },
});
```

---

## Events

```js
import { on, off, once } from 'chatgram-widget';

on('open', () => analytics.track('widget_opened'));
on('close', () => {});
on('submit', (payload) => console.log('Submitting:', payload));
on('success', (data) => console.log('Ticket ID:', data.ticketId));
on('error', (err) => console.error(err.code)); // NETWORK_ERROR | TIMEOUT | SERVER_ERROR | etc.
on('state-change', (state) => {}); // idle | loading | success | error
on('theme-change', (theme) => {});

once('success', () => showConfetti()); // Fires once then auto-removes
```

### Error Codes

| Code | Meaning |
|------|---------|
| `NETWORK_ERROR` | No internet or DNS failure |
| `TIMEOUT` | Request exceeded `requestTimeout` |
| `SERVER_ERROR` | 5xx from API |
| `VALIDATION_ERROR` | 4xx from API |
| `RATE_LIMITED` | 429 from API or client-side limit |
| `HONEYPOT_TRIGGERED` | Bot detected (silently faked success) |
| `NOT_INITIALIZED` | `open()` called before `init()` |

---

## Security

| Protection | How |
|------------|-----|
| XSS | All user input rendered via `textContent`, never `innerHTML`. HTML entities escaped. |
| CSS Injection | Theme values sanitized — blocks `expression()`, `url()`, `javascript:`, `data:`. |
| CSS Isolation | Shadow DOM prevents leakage in both directions. |
| Spam (bots) | Honeypot hidden field — bots fill it, humans don't. Silently fakes success. |
| Spam (timing) | `minSubmitDelay` — rejects submissions faster than N seconds. |
| Spam (rate limit) | Sliding window rate limiter — max N submissions per window. |
| API safety | 15s timeout, AbortController, exponential backoff retry (only on 5xx/network). |
| Input validation | Email: RFC 5322. Subject/message: non-empty + length check. Newline injection blocked. |

---

## Architecture

```
chatgram-widget/
├── src/
│   ├── core/
│   │   ├── types.ts         # TypeScript interfaces, ChatgramError class
│   │   ├── config.ts        # Defaults, deep merge, resolver, validation
│   │   ├── api.ts           # HTTP client — timeout, retry, structured errors
│   │   ├── events.ts        # Typed event emitter (on/off/once/emit)
│   │   └── rate-limiter.ts  # Sliding window rate limiter
│   ├── ui/
│   │   ├── modal.ts         # Modal renderer — Shadow DOM, focus trap, ARIA, states
│   │   ├── trigger.ts       # Floating trigger button (Shadow DOM)
│   │   └── icons.ts         # SVG icon set (trusted static markup)
│   ├── utils/
│   │   ├── sanitize.ts      # XSS prevention, validation, CSS injection protection
│   │   └── dom.ts           # Safe element creation, focus trap, scroll lock
│   ├── styles/
│   │   ├── widget-css.ts    # CSS-in-JS for Shadow DOM injection
│   │   └── widget.css       # External CSS entry point
│   └── index.ts             # Public API (init/open/close/destroy/on/off)
├── dist/                     # Build output
│   ├── chatgram-widget.esm.js
│   ├── chatgram-widget.cjs.js
│   ├── chatgram-widget.umd.js
│   ├── chatgram-widget.css
│   └── types/
├── package.json
├── tsconfig.json
└── rollup.config.mjs
```

---

## Browser Support

Chrome 80+, Firefox 78+, Safari 14+, Edge 80+

---

## License

MIT — Built by [brandID](https://brandid.app)
