---
name: widget-component
description: Use when building any UI piece inside packages/widget — the embeddable vanilla-JS chat widget. Establishes the Shadow-DOM + tokens.css contract and bundle-size guardrails that keep the embed safe to drop on any host page.
---

# widget-component

The widget is loaded via `<script src="…widget.iife.js">` on host pages we don't control (indianic.com, careers, etc.). Anything that leaks from the widget — global names, styles, focus traps, console noise — is a bug. Defensive defaults beat clever ones here.

## When to use

- Touching any file in `packages/widget/src/`
- Adding a new visitor-facing UI piece (bubble, panel, pre-chat form, chat scroller, video tile, file-share UI)
- Wiring widget state, host-page integration, or any DOM observation

Skip for code that is purely shared (event types, design tokens) — that lives in `packages/shared`.

## Do

- Mount everything inside a **single Shadow DOM root** attached to a host `<div>` the widget injects into `document.body`. The Shadow host is the widget's only contact with the host page.
- Import design tokens from `@indianic/shared/design/tokens.css` and inject the stylesheet into the Shadow root with `<link>` or constructable stylesheets. Reference tokens as CSS custom properties (`var(--inic-paper)` etc.).
- Keep all state in a **single store object** (plain TS object behind a small `getState`/`setState`/`subscribe` API). No external state libs — they cost bytes and the widget should ship under 100KB gzipped.
- Use event delegation on the Shadow root for clicks; do not attach a listener per button.
- Persist only the visitor UUID to `localStorage` (key: `indianic.visitor.id`). Everything else is in-memory.
- Build DOM with `document.createElement` + `textContent` + `append`. The host page is untrusted; user-supplied strings (visitor name, geo, messages) must never reach `innerHTML`.
- Bundle to a single IIFE (`vite build`) that self-mounts on load. The IIFE entry creates `window.IndianicWidget` (read-only handle for the host page).
- Honor `prefers-reduced-motion` — already routed through the token CSS layer; don't override.

## Don't

- Don't render outside Shadow DOM. Light-DOM injection will collide with host CSS.
- Don't use `innerHTML` / `insertAdjacentHTML` with anything that touched a network payload, the host page, or visitor input. XSS vector.
- Don't import React, lit-html, or any UI framework. Vanilla JS + small helpers only. If a pattern feels like it needs React, write it as a Web Component.
- Don't read or write `localStorage` for anything other than the visitor UUID. Use the in-memory store.
- Don't bind global event listeners on `window`/`document` outside the widget's mount lifecycle — and always remove them in `unmount()`.
- Don't `console.log` past Phase 0. Use a `widgetLog()` helper that is silenced in production builds.
- Don't load fonts from a third-party CDN at runtime. Host fonts ourselves once Phase 5 ships; until then accept the system fallback.

## One short example

```ts
// packages/widget/src/mount.ts
import tokensCss from '@indianic/shared/design/tokens.css?inline';

export function mount(host: HTMLElement = document.body) {
  const shadowHost = document.createElement('div');
  shadowHost.setAttribute('data-indianic-widget', '');
  host.appendChild(shadowHost);
  const root = shadowHost.attachShadow({ mode: 'open' });

  const style = new CSSStyleSheet();
  style.replaceSync(tokensCss);
  root.adoptedStyleSheets = [style];

  const bubble = document.createElement('button');
  bubble.type = 'button';
  bubble.className = 'bubble';
  bubble.setAttribute('aria-label', 'Open chat');
  bubble.textContent = '●';
  bubble.addEventListener('click', () => openPanel(root));
  root.append(bubble);
}
```

## Reference

- Aesthetic + tokens: `packages/shared/src/design/README.md`
- Event schema (what to emit): `packages/shared/src/events/schema.ts`
- Phase doc: `dev-docs/phase-1-widget-mockup.md`
