npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@trilok-zs/chat-component

v1.0.1

Published

Framework-independent chat Web Component (Custom Element + Shadow DOM) implemented internally with React + TypeScript. Completely API-agnostic: every endpoint, id, token and language is supplied by the consuming application.

Readme

@trilok-zs/chat-component

A production-ready chat UI, built internally with React + TypeScript, published as a framework-independent Web Component (Custom Element + Shadow DOM).

Consume it from Vue 3, Angular, Svelte, or plain HTML. React is bundled inside the package — the host application never installs or imports React.

The package is completely API-agnostic. It contains no endpoint, customer id, conversation id, auth token, or language. Every one of those is supplied by the consuming application through attributes, JS properties, or configure().

<chat-component
  endpoint="https://XXXXX/api/chat"
  details-endpoint="https://XXXXX/api/details"
  customer-id="XXXXX"
  language="en"
></chat-component>

Table of contents

  1. Install
  2. Quick start
  3. Attributes
  4. JavaScript properties
  5. configure()
  6. Public methods
  7. Events
  8. API contract
  9. Authentication
  10. Error handling
  11. Streaming / SSE
  12. Vue 3 integration
  13. Styling and Shadow DOM
  14. Project structure
  15. Build and publish
  16. Local development

1. Install

npm install @trilok-zs/chat-component

No peer dependencies. No React in the host app.

2. Quick start

import '@trilok-zs/chat-component'; // registers <chat-component>
<chat-component
  endpoint="https://XXXXX/api/chat"
  details-endpoint="https://XXXXX/api/details"
  customer-id="XXXXX"
  language="en"
></chat-component>

Registration happens as an import side effect. To register under a different tag name:

globalThis.CHAT_COMPONENT_DISABLE_AUTO_DEFINE = true;
import { defineChatComponent } from '@trilok-zs/chat-component';
defineChatComponent('acme-chat');

3. Attributes

| Attribute | Required | Description | | ---------------------- | ------------------- | ----------- | | endpoint | for sending | POST URL for sending a message. | | details-endpoint | for rendering | Base URL of the details API. Source of truth for the transcript. | | customer-id | optional | Sent as customer_id. Missing → chat-error warning, request still sent with null. | | language | optional | Sent as language and appended as ?language=. No default locale. | | conversation-id | optional | Resumes an existing conversation; loads it automatically. | | auth-token | optional | Sent as Authorization: Bearer <token>. | | with-credentials | optional | Send cookies with both calls (credentials: 'include'). See Authentication. | | credentials | optional | include | same-origin | omit. Long form of with-credentials. | | theme | optional | light | dark | auto (default auto). | | disabled | optional | Read-only transcript; composer disabled. | | auto-load | optional | Default true. false disables automatic details loading. | | refresh-from-details | optional | Default true. false skips the details call after a send (streaming). | | title | optional | Header title text. | | placeholder | optional | Composer placeholder text. |

All attributes are optional; endpoint and details-endpoint are required for API communication and produce typed errors when missing.

4. JavaScript properties

Every attribute has a camelCase property, plus headers and read-only messages.

const chat = document.querySelector('chat-component');

chat.endpoint = 'https://XXXXX/api/chat';
chat.detailsEndpoint = 'https://XXXXX/api/details';
chat.customerId = 'XXXXX';
chat.language = 'en';
chat.conversationId = 'a3f1…';
chat.authToken = 'TOKEN';
chat.headers = { 'X-Tenant': 'acme' };

Properties do not reflect back to attributes, so they never fight a host framework binding the same attribute. Properties set before the element is connected are applied on connect, and take precedence over markup.

5. configure()

For anything not expressible as a string attribute:

chat.configure({
  endpoint: '…',
  detailsEndpoint: '…',
  customerId: '…',
  language: 'en',
  conversationId: '…',
  headers: { Authorization: 'Bearer …' },
  extraPayload: { channel: 'web' },   // merged into the POST body
  credentials: 'include',             // cookie-based auth
  timeoutMs: 30000,                   // 0/null disables
  refreshFromDetails: true,
  autoLoad: true,
  theme: 'light',
  labels: { title: 'Support', placeholder: 'Ask anything…' },
  fetchImpl: myInstrumentedFetch,
  transport: myTransport,
  onEvent: (name, detail) => console.log(name, detail),
});

Merge semantics: undefined leaves a field untouched, null clears it, objects (headers, labels, extraPayload) merge. Consumer values always override defaults.

getConfig() returns the resolved config; getState() returns { messages, status, error, conversationId, sfPanel, busy, lastFailedInput }.

6. Public methods

chat.sendMessage(message: string): Promise<void>   // POST → details → render
chat.clearChat(): void                             // clears transcript + conversation id
chat.setConversationId(id: string): void           // sets id, auto-loads it
chat.loadConversation(id?: string): Promise<void>  // loads id, or the current one
chat.abort(): void                                 // cancels the in-flight request
chat.getMessages(): ChatMessage[]
chat.getState()  /  chat.getConfig()

sendMessage and loadConversation never reject — failures surface via chat-error and getState().error, so a fire-and-forget call from a template can't cause an unhandled rejection.

7. Events

All events are CustomEvents, bubbles: true and composed: true, so they cross the shadow boundary and can be caught on an ancestor.

| Event | event.detail | | ------------------------ | -------------- | | chat-message-sent | { conversationId, message, messages } | | chat-response-received | { conversationId, message, messages, raw } | | conversation-loaded | { conversationId, messages, sfPanel, raw } | | conversation-changed | { conversationId, previousConversationId } | | chat-error | { code, message, severity, status?, phase, retryable, details?, input? } |

import type { ChatResponseReceivedEvent } from '@trilok-zs/chat-component';

chat.addEventListener('chat-response-received', (event) => {
  const { conversationId, messages } = (event as ChatResponseReceivedEvent).detail;
});

8. API contract

POST {endpoint}

{
  "id": "<conversationId or null>",
  "content": "<user message>",
  "customer_id": "<customerId>",
  "attachment": null,
  "language": "<language>",
  "message": "<user message>"
}

Only id is read from the response and becomes the current conversation id:

{ "id": "XXXXX", "content": "hello", "…": "…" }

GET {detailsEndpoint}/{conversationId}?language={language}

{
  "status": "success",
  "message": "success",
  "data": {
    "chats": [
      { "role": "user", "content": "hello" },
      { "role": "assistant", "content": "Hello there!" }
    ],
    "sf_panel": false
  }
}

Flow on send: POST → read id → save id → GET details → read data.chats → update UI. The POST response is never assumed to contain the assistant answer; the details API is the source of truth.

Responses are normalized into:

interface ChatMessage {
  id?: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp?: string;
  feedback?: unknown;
  suggestion?: unknown;
}

Only user and assistant turns render — system, tool, and unknown roles are dropped. content may be a string or a content-part array ([{ type, text }]).

9. Authentication

<chat-component auth-token="TOKEN" …></chat-component>

sends Authorization: Bearer TOKEN on both calls. For other header schemes:

chat.configure({ headers: { Authorization: 'Basic …', 'X-Api-Key': '…' } });

Cookie / session authentication

Browsers forbid setting a Cookie header from JavaScript — fetch silently drops it. Cookies are sent by opting into a credentials mode instead:

<!-- shorthand: credentials: 'include' -->
<chat-component with-credentials endpoint="…" details-endpoint="…"></chat-component>

<!-- or explicit -->
<chat-component credentials="include" endpoint="…" details-endpoint="…"></chat-component>
chat.credentials = 'include';              // property
chat.configure({ credentials: 'include' }); // or configure()

| Value | Behaviour | | ----- | --------- | | include | Send cookies on cross-origin requests too. Needed when the API is on a different origin than the app. | | same-origin | Send cookies only for same-origin requests (browser default). | | omit | Never send cookies. | | unset / invalid | Browser default. |

Applied to both the POST and the details GET. If you pass a Cookie header anyway, the component logs a one-time warning explaining why it won't work rather than letting it look like a server-side auth failure.

The session itself belongs to the host app — the component never logs in. Establish the session (SSO redirect, login call with credentials: 'include'), then let the component ride the cookie.

For credentials: 'include' to work cross-origin, the API must:

  1. Return Access-Control-Allow-Credentials: true.
  2. Return Access-Control-Allow-Origin: <your exact origin> — the wildcard * is rejected by browsers on credentialed requests.
  3. Set the cookie with SameSite=None; Secure (so HTTPS in any real deployment; http://localhost counts as a secure context in dev).
  4. Answer the OPTIONS preflight with those same headers, plus Vary: Origin if responses are cached.

Missing any of these shows up as a chat-error with NETWORK_ERROR (the browser blocks the response before the component sees a status) or HTTP_CLIENT_ERROR with status: 401. Try the mock server with REQUIRE_AUTH=1 npm run mock to see both paths.

No credential is ever stored or defaulted inside the package. Prefer short-lived tokens or HttpOnly cookies; refresh on a chat-error with status: 401.

10. Error handling

Every failure is a typed chat-error with a stable code:

| Code | Cause | | ---- | ----- | | MISSING_ENDPOINT / MISSING_DETAILS_ENDPOINT | Endpoint not supplied | | MISSING_CUSTOMER_ID | No customer id (warning, request proceeds) | | MISSING_CONVERSATION_ID | loadConversation() with no id | | EMPTY_MESSAGE / BUSY | Invalid call (warnings) | | NETWORK_ERROR / TIMEOUT / ABORTED | Transport failures | | HTTP_CLIENT_ERROR / HTTP_SERVER_ERROR | 4xx / 5xx (with status and parsed body) | | INVALID_POST_RESPONSE | Response not JSON, empty, or missing id | | INVALID_DETAILS_RESPONSE | Missing data.chats, or status: "error" |

The UI shows an inline banner with Retry (which re-sends the failed text) and Dismiss. retryable tells the host whether retrying can help.

11. Streaming / SSE

The UI consumes transport events, never a raw HTTP response, so streaming is additive:

type ChatTransportEvent =
  | { type: 'conversation'; conversationId: string }
  | { type: 'delta'; content: string; messageId?: string }
  | { type: 'message'; message: ChatMessage }
  | { type: 'messages'; messages: ChatMessage[] }
  | { type: 'done'; conversationId?: string | null };

The default JSON transport emits conversation + done. A ready-made SSE transport ships with the package:

import { createSseTransport } from '@trilok-zs/chat-component';

chat.configure({
  transport: createSseTransport(),
  refreshFromDetails: false, // render from the stream instead of re-reading details
});

createSseTransport({ mapEvent }) lets you map any server frame shape onto those events. Any object implementing ChatTransport works — the UI, state machine, and events stay unchanged.

12. Vue 3 integration

npm install @trilok-zs/chat-component

vite.config.ts — tell the Vue compiler this tag is a custom element:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag === 'chat-component',
        },
      },
    }),
  ],
});

Vue CLI / webpack equivalent:

// vue.config.js
module.exports = {
  chainWebpack: (config) => {
    config.module.rule('vue').use('vue-loader').tap((options) => {
      options.compilerOptions = {
        ...options.compilerOptions,
        isCustomElement: (tag) => tag === 'chat-component',
      };
      return options;
    });
  },
};

App.vue

<script setup lang="ts">
import '@trilok-zs/chat-component';
</script>

<template>
  <chat-component
    endpoint="https://XXXXX/api/chat"
    details-endpoint="https://XXXXX/api/details"
    customer-id="XXXXX"
    language="en"
  />
</template>

Events + TypeScript

<script setup lang="ts">
import '@trilok-zs/chat-component';
import type {
  ChatElement,
  ChatErrorEvent,
  ChatResponseReceivedEvent,
} from '@trilok-zs/chat-component';
import { useTemplateRef } from 'vue';

const chat = useTemplateRef<ChatElement>('chat');

function handleResponse(event: Event) {
  const { conversationId, messages } = (event as ChatResponseReceivedEvent).detail;
  console.log(conversationId, messages);
}

function handleError(event: Event) {
  const detail = (event as ChatErrorEvent).detail;
  if (detail.status === 401) chat.value?.configure({ authToken: 'fresh-token' });
}
</script>

<template>
  <chat-component
    ref="chat"
    :endpoint="endpoint"
    :details-endpoint="detailsEndpoint"
    @chat-response-received="handleResponse"
    @chat-error="handleError"
  />
</template>

Notes for Vue hosts:

  • Vue binds string values as attributes on unknown elements, so :endpoint="url" works; use .prop or configure() for objects.
  • Vue's @kebab-case listeners map directly onto the CustomEvent names — no .native and no camelCase conversion.
  • v-if toggling is safe: the element unmounts React on disconnect and remounts with its configuration intact.
  • Optional template typing for the tag lives in examples/vue-app/src/chat-component.d.ts.

A complete runnable host app is in examples/vue-app/.

13. Styling and Shadow DOM

The UI renders inside an open Shadow DOM with its CSS injected via a constructable stylesheet (with a <style> fallback). Host CSS cannot reach in; component CSS cannot leak out. Theme through custom properties, which do pierce the boundary:

chat-component {
  --cc-height: 600px;
  --cc-accent: #6d28d9;
  --cc-user-bubble: #6d28d9;
  --cc-radius: 16px;
  --cc-font: 'Inter', system-ui, sans-serif;
}

Full list: see :host in src/styles/chat.css (also shipped as dist/style.css).

UI features: header, message list, user/assistant bubbles, auto-growing textarea, send button, loading/error/empty states, auto-scroll (pauses while reading history), Enter to send, Shift+Enter for newline, send disabled during requests, stop button to cancel, typing indicator, prefers-color-scheme and prefers-reduced-motion support, ARIA live region.

14. Project structure

src/
  components/          React — rendering and UI state only
    Chat.tsx
    ChatMessage.tsx
    ChatInput.tsx
    useChatController.ts
  core/                framework-agnostic logic
    ChatController.ts  state machine: network, state, events
    config.ts          defaults + consumer merge semantics
    errors.ts          ChatError with stable codes
  services/            generic API layer — receives URLs, never owns them
    chatApi.ts         postChatMessage / getConversationDetails
    transport.ts       default JSON transport
    sseTransport.ts    opt-in SSE/streaming transport
  types/chat.ts        public type contract
  styles/chat.css      Shadow DOM styles
  web-component/
    ChatElement.ts     attributes, lifecycle, Shadow DOM, events, methods
  index.ts             public entry (registers <chat-component>)

React renders; ChatController decides; ChatElement is the framework boundary. Because the controller is framework-agnostic, the element's public methods work even before React mounts.

15. Build and publish

npm install
npm run build
npm publish

npm run build type-checks, bundles to dist/index.js (React included), rolls all types into a single dist/index.d.ts, and emits dist/style.css.

dist/
  index.js      ES module, self-contained
  index.js.map
  index.d.ts    single rolled-up declaration file, zero React types
  style.css     optional standalone stylesheet

Publishing checklist:

  1. Own the scope. @trilok-zs must be either your npm username or an org you belong to (create one at https://www.npmjs.com/org/create). Otherwise publishing fails with E402/E403.
  2. Log in: npm login, then confirm with npm whoami.
  3. Dry run: npm publish --dry-run — validates the tarball without uploading.
  4. Publish: npm publish. prepublishOnly re-runs the build so dist/ is never stale, and publishConfig.access: public covers the first scoped publish. With 2FA enabled: npm publish --otp=123456.
  5. Verify: npm view @trilok-zs/chat-component, then install it in a host app.

Releasing again: npm version patch|minor|major && npm publish. A version can never be republished, and unpublishing is only possible within 72 hours.

Publishing from CI instead of a laptop — create an Automation token on npm (bypasses 2FA) and expose it as NODE_AUTH_TOKEN:

# .github/workflows/publish.yml
name: publish
on:
  push:
    tags: ['v*']
permissions:
  contents: read
  id-token: write            # enables --provenance
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
      - run: npm ci
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

To publish privately instead, set "access": "restricted" in publishConfig (requires a paid npm plan or an org with private packages).

Then in the Vue application:

npm install @trilok-zs/chat-component

16. Local development

npm install
npm run mock     # terminal 1 — mock API on http://localhost:8787
npm run dev      # terminal 2 — dev harness on http://localhost:3000

The harness (demo/) plays the role of the consuming app: it configures endpoints at runtime and logs every event. It deliberately applies hostile global CSS to demonstrate Shadow DOM isolation.

npm run typecheck   # strict TS, sources + build config
npm run build       # typecheck + bundle + types + css

License

MIT