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

@reqdesk/widget

v1.6.0

Published

Embeddable support widget SDK for Reqdesk — ticket submission, tracking, and support portal.

Readme

@reqdesk/widget

Embeddable support widget SDK for Reqdesk. Add a floating support button to any website — ticket submission with file attachments, ticket tracking, My Tickets, and optional Keycloak SSO authentication.

What's new in v1.2.0

Five interlocking capabilities plus a visible FAB-centering fix. All additions are opt-in; existing integrations render and behave identically.

  • Custom menu actions — register imperative (onClick) or declarative (custom-event / call-global / url) menu entries, anchored before or after any built-in item, with per-action badges, visibility predicates, and optional per-action display overrides. See Custom menu actions.
  • Standalone trigger button — three integration layers for hosts that need a button outside the FAB: styled <ReqdeskTrigger>, headless useReqdeskTrigger(), and vanilla mountTrigger(). Combine with hideFab: true for trigger-only layouts. See Standalone trigger button.
  • Full programmatic controlopenMenu, openAction(id, input?), setDisplayMode, setPreferences, addAction, removeAction. on(event, cb) now returns an unsubscribe function. See Programmatic control.
  • Four display modes — popover (default), side-sheet (start / end with logical mirror in RTL), bottom-sheet. Backdrop on viewports ≥ 640px, Escape closes, and prefers-reduced-motion disables slide-ins. See Display modes.
  • Host-owned preferences exchange — supply defaults and a stored user blob at init; receive the full blob on every change via onPreferencesChange. localStorage still acts as a device cache. See Preferences exchange.

Glossary — two meanings of "trigger." Inside the actions array, an ActionTrigger is the declarative descriptor that routes a menu item's click to a custom event, global function, or URL. Outside the menu, <ReqdeskTrigger> / mountTrigger() / useReqdeskTrigger() are the standalone button surfaces that open the widget from anywhere on the page. The README uses "trigger" only in these two senses — the context makes clear which one.

Entry-point scope for v1.2.0. The custom-action menu rendering, runtime display-mode swap (side-sheet / bottom-sheet shells), and reactive useReqdesk() slices land in @reqdesk/widget/react. The vanilla IIFE (<script> tag) retains the existing ticket-form + tracker experience unchanged; the registry core, mountTrigger(), preferences exchange, events, hideFab, and FAB centering fix are all available on the vanilla entry. See Vanilla IIFE scope (v1.2.0) for the exact split.

Features

  • Floating Action Button — circular FAB that opens the support surface
  • Submit Tickets — form with title, description, email, priority, file attachments (drag-drop), and project tags (tags: v1.2.17)
  • My Tickets — view ticket history by email or automatically when authenticated
  • Ticket Detail — full ticket view with reply thread, attachments, and reply composer
  • Track by Token — anonymous ticket tracking via tracking tokens
  • Dual Auth — API key mode (anonymous/email) + optional Keycloak OIDC login
  • Preferences — end-user language (EN/AR with RTL), theme (light/dark/system), accent color
  • Branding — custom logo, brand name, and "Powered by Reqdesk" footer (hideable)
  • Shadow DOM — fully isolated styles, no CSS conflicts with host page
  • i18n — English and Arabic built-in, custom translations supported
  • Custom menu actions — register your own menu items with imperative handlers or declarative triggers (v1.2.0)
  • Standalone trigger button — three integration paths for non-FAB invocation (v1.2.0)
  • Display modes — popover, side-sheet (start/end), bottom-sheet (v1.2.0)

Choosing an API Key

Reqdesk API keys come in two scopes — pick the one that matches your widget's use case:

| Prefix | Scope | Use case | Anonymous tickets? | |---|---|---|---| | rqd_pk_… | Single project | Public website "Report a bug" widgets | ✅ Yes — users submit with just email | | rqd_ws_… | Entire workspace | Internal/customer portals where users log in and pick a project | 🔒 No — login required |

Rule of thumb:

  • If your widget appears on a public page where anonymous visitors file tickets, use a project-scoped key (rqd_pk_).
  • If the widget appears inside a logged-in experience spanning multiple projects, use a workspace-scoped key (rqd_ws_). Users must log in via Keycloak before submitting.

Create project keys in Project Settings → API Keys. Create workspace keys from the workspace switcher → Manage API Keys.

Installation

npm install @reqdesk/widget
# or
bun add @reqdesk/widget
# or
yarn add @reqdesk/widget

Quick Start

Script Tag (any website)

<script src="https://unpkg.com/@reqdesk/widget/dist/index.iife.js"></script>
<script>
  ReqdeskWidget.init({
    apiKey: 'rqd_live_your_api_key_here',
    position: 'bottom-right',
    language: 'en',
    theme: {
      primaryColor: '#42b983',
      mode: 'light',
      brandName: 'Acme Support',
      logo: 'https://example.com/logo.png',
    },
  });
</script>

React Component

import { ReqdeskProvider, FloatingWidget } from '@reqdesk/widget/react';

function App() {
  return (
    <ReqdeskProvider
      apiKey="rqd_live_your_api_key_here"
      language="en"
      theme={{ primaryColor: '#42b983', mode: 'light' }}
    >
      <FloatingWidget position="bottom-right" />
    </ReqdeskProvider>
  );
}

Configuration

ReqdeskWidgetConfig

| Property | Type | Required | Default | Description | |----------|------|----------|---------|-------------| | apiKey | string | Yes | — | Project API key from Reqdesk dashboard | | auth | OidcAuthConfig | No | — | Keycloak OIDC config for authenticated mode | | position | 'bottom-right' \| 'bottom-left' | No | 'bottom-right' | FAB button position | | language | string | No | 'en' | Language code ('en' or 'ar') | | theme | ThemeConfig | No | — | Visual customization | | customer | CustomerConfig | No | — | Pre-fill customer info | | translations | Record<string, string> | No | — | Override built-in translations | | inline | boolean | No | false | Render inline instead of floating | | container | string \| HTMLElement | No | — | Target element for inline mode |

ThemeConfig

| Property | Type | Default | Description | |----------|------|---------|-------------| | primaryColor | string | '#42b983' | Accent color (hex) | | mode | 'light' \| 'dark' \| 'auto' | 'light' | Color scheme | | borderRadius | string | '8px' | Border radius for panels and inputs | | fontFamily | string | 'inherit' | Font family | | zIndex | number | 9999 | Z-index for FAB and panel | | logo | string | — | URL to brand logo (shown in header) | | brandName | string | — | Brand name (shown in header instead of "Support") | | hideBranding | boolean | false | Hide "Powered by Reqdesk" footer |

OidcAuthConfig

| Property | Type | Description | |----------|------|-------------| | issuerUri | string | Keycloak realm URL (e.g., https://auth.example.com/realms/reqdesk) | | clientId | string | Keycloak client ID (e.g., reqdesk-widget) |

Authentication

The widget supports three authentication modes. The project admin picks which ones are enabled; the widget adapts its UI accordingly. Defaults for existing projects are sso and email — no action needed to keep today's behaviour.

| Mode | Who identifies the user | When to pick it | |---|---|---| | sso | Keycloak OIDC login (popup) | Your customer support site is standalone or users already have a Reqdesk account | | email | Manual email entry in the widget form | Simple anonymous/contact-form submissions — trust the email at face value | | signed | Your host app signs the email server-side with a shared secret | Your customers are already logged in to the embedding app and you want zero extra prompts |

You can combine any two or all three; the widget prefers the mode that produces the least friction.

Signed host-app identity (recommended when your users are already logged in)

The host app computes an HMAC-SHA256 signature over {unixSeconds}.{lowercasedTrimmedEmail} using a per-project secret issued by Reqdesk. The widget forwards the signature and timestamp as headers; the backend verifies before trusting the email. Timestamps expire after a configurable window (default 10 minutes) to prevent replay. Secret rotation is zero-downtime via a current + previous overlap.

Admin setup:

  1. In the Reqdesk admin, go to your project's Settings → Widget tab.
  2. Enable the Signed host-app identity checkbox.
  3. Click Generate signing secret. Copy the plaintext — it is shown exactly once and stored encrypted afterwards.
  4. Put the plaintext in your host app's secret manager (e.g. REQDESK_SIGNING_SECRET).

Host-app code (Node.js):

import crypto from 'node:crypto';

const secret = process.env.REQDESK_SIGNING_SECRET;
const email = user.email.trim().toLowerCase();
const ts = Math.floor(Date.now() / 1000);
const userHash = 'sha256=' + crypto
  .createHmac('sha256', secret)
  .update(`${ts}.${email}`)
  .digest('hex');

// Server-render or pass to the widget on page load:
ReqdeskWidget.init({ apiKey: 'rqd_live_...' });
ReqdeskWidget.identify({
  email: user.email,
  name: user.displayName,
  userHash,
  userHashTimestamp: ts,
});

Host-app code (C# / .NET):

var secret = Environment.GetEnvironmentVariable("REQDESK_SIGNING_SECRET")!;
var email = user.Email.Trim().ToLowerInvariant();
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var userHash = "sha256=" + Convert.ToHexStringLower(
    hmac.ComputeHash(Encoding.UTF8.GetBytes($"{ts}.{email}")));

See examples/identify-node.js, examples/identify-dotnet.cs, and examples/identify-php.php for complete server snippets.

Auto-refresh for long-lived sessions (v1.2.13+)

The backend rejects signatures older than the project's SignedIdentityTtlMinutes (default 10 minutes). If the host app computes userHash + userHashTimestamp once at page load and the widget stays open longer than the TTL, every subsequent call 401s with INVALID_SIGNATURE — The signed identity timestamp is outside the configured freshness window. To eliminate this, supply a refreshIdentity callback on customer:

<ReqdeskProvider
  apiKey="rqd_live_..."
  authMode={['signed']}
  customer={{
    email: user.email,
    name: user.displayName,
    userHash: initialUserHash,
    userHashTimestamp: initialTs,
    // Host-owned endpoint that re-computes the HMAC server-side and returns the fresh pair.
    // The widget calls this automatically when the current timestamp is older than 5 minutes
    // (half the default 10-minute backend TTL) — dedupe + fall-back are handled for you.
    async refreshIdentity() {
      const r = await fetch('/widget/sign-identity', { credentials: 'include' })
      if (!r.ok) throw new Error(`sign-identity ${r.status}`)
      return (await r.json()) as { userHash: string; userHashTimestamp: number }
    },
  }}
>
  <FloatingWidget />
</ReqdeskProvider>

A Laravel host-side endpoint for that callback:

// routes/web.php
Route::get('/widget/sign-identity', function () {
    abort_unless(auth()->check(), 401);
    $secret = config('services.reqdesk.signing_secret');
    $email  = strtolower(trim(auth()->user()->email));
    $ts     = time();
    $hash   = 'sha256=' . hash_hmac('sha256', "{$ts}.{$email}", $secret);
    return response()->json(['userHash' => $hash, 'userHashTimestamp' => $ts]);
})->middleware('auth');

What the widget does under the hood:

  • Checks Math.floor(Date.now() / 1000) - userHashTimestamp > 5 * 60 before every request. If stale, awaits refreshIdentity(), mutates the in-memory customer with the fresh pair, and sends the fresh X-Widget-User-Signature / X-Widget-User-Timestamp headers.
  • Dedupes in-flight refreshes — 20 parallel widget calls during a refresh trigger exactly one host-endpoint hit.
  • If refreshIdentity throws or returns an invalid shape, logs one console.warn per minute and falls through to the stale signature. The server will then reject with a clean 401 INVALID_SIGNATURE that the global notification stack surfaces as a toast — no hang, no unhandled rejection.
  • File uploads (uploadAttachment via XHR) re-sign the same way.

Backwards-compat: hosts on 1.2.12 or earlier (or any 1.2.13+ host that omits refreshIdentity) see no change — the widget uses whatever { userHash, userHashTimestamp } was last set via <ReqdeskProvider customer> or identify(), and you own rotation.

Security model:

  • The signing secret lives only on your host app's server. It must never reach the browser.
  • A tampered, missing, or expired signature is hard-rejected with 401 INVALID_SIGNATURE — the widget does not silently fall back to unsigned submission.
  • When the signed email matches an existing SSO user, the ticket is owned directly by that user with no synthetic widget-user row.

Per-project allowed modes

The widget fetches the project's allowed auth modes from /widget-config on every init. Its UI adapts accordingly:

  • If signed is in scope and you called identify({ email, userHash, userHashTimestamp }), the form opens directly — no login, no email field.
  • If email is in scope and you called identify({ email }) without a hash, the form opens with the email pre-filled.
  • If only sso is in scope, the widget shows a Sign-in button.
  • If /widget-config cannot be reached, the widget falls back to the safe default (['sso', 'email']).

The host app can also narrow — but never widen — the allowed set via authMode:

ReqdeskWidget.init({
  apiKey: 'rqd_live_...',
  authMode: ['signed'], // host chooses to surface only the signed path, if the admin allows it
});

API Key Mode (default)

Every widget requires an API key. In this mode:

  • Visitors submit tickets anonymously or with an email address
  • Email identifies the visitor for "My Tickets" (trust-based, like a contact form)
  • Tracking tokens allow anonymous ticket access from any browser
  • A "Remember me" checkbox saves the email to localStorage
ReqdeskWidget.init({
  apiKey: 'rqd_live_your_api_key_here',
});

Keycloak OIDC Mode (optional)

When auth config is provided, the widget shows a Login button. After authenticating via Keycloak:

  • User is automatically identified (no email entry needed)
  • "My Tickets" shows all tickets instantly
  • Tickets are attributed to the real Keycloak user account
  • Session persists via silent refresh (one-time login)
ReqdeskWidget.init({
  apiKey: 'rqd_live_your_api_key_here',
  auth: {
    issuerUri: 'https://auth.example.com/realms/reqdesk',
    clientId: 'reqdesk-widget',
  },
});

The widget's OIDC session is completely independent from your host app's auth. It uses its own Keycloak client (reqdesk-widget) with separate tokens.

Keycloak Client Setup

Add a reqdesk-widget client to your Keycloak realm:

  • Client Type: Public (publicClient: true)
  • Flow: Authorization Code with PKCE (standardFlowEnabled: true)
  • Redirect URIs: * (or restrict to your domains)
  • Web Origins: + (CORS for any origin)
  • Client Scopes: basic, openid, profile, email

Custom menu actions

Register your own entries in the support menu. Actions can run imperative code (onClick) or route to a declarative destination (custom-event, call-global, url). Both React and vanilla are supported.

import { ReqdeskProvider, FloatingWidget, type ReactMenuActionInput } from '@reqdesk/widget/react';

const actions: ReactMenuActionInput[] = [
  {
    id: 'ai-chat',
    label: { en: 'Ask AI', ar: 'اسأل الذكاء الاصطناعي' },
    description: 'Instant answers from our assistant',
    icon: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10…',
    anchor: { before: 'preferences' },
    closeOnClick: false,
    badge: 'New',
    onClick: (ctx) => {
      ctx.setBadge(null);
      ctx.keepOpen();
      // …launch your host-owned AI experience
    },
  },
  {
    id: 'docs',
    label: 'Documentation',
    icon: 'M14 2H6c-1.1 0-1.99.9-1.99 2…',
    trigger: { kind: 'url', href: 'https://reqdesk.com/docs', target: '_blank' },
  },
];

<ReqdeskProvider apiKey="…" actions={actions}>
  <FloatingWidget />
</ReqdeskProvider>
  • Slotting. anchor.before / anchor.after reference a built-in id (new-ticket, my-tickets, track, preferences, select-project) or another custom id. anchor.before wins when both are set.
  • Handler-wins precedence. If an action declares both onClick and trigger, the handler runs and the trigger is ignored (with a single console.debug).
  • Badges. badge is mutable via ctx.setBadge(value) inside the handler and re-renders the menu.
  • Visibility predicates. visible may be a boolean or a function — predicate errors hide the item and emit action-visibility-failed.
  • Per-action display override. Set display: { mode: 'side-sheet', side: 'end' } on the action to temporarily switch the surface for that flow. Closing the widget restores the prior mode.
  • openAction(id, input?). The host can invoke the same flow programmatically. input folds into the handler's ctx.input and, for declarative triggers, merges into the event detail / replaces args[0] / becomes a URL query string.

Walkthrough: examples/react-appCustom actions (US1) variant.

Standalone trigger button (three layers)

A non-FAB button for hosts that want the widget to open from their own navigation, toolbar, or inline CTA. Pair with hideFab: true to suppress the FAB entirely (all menu / action / display / preference paths remain operational).

Layer A — styled component. Zero configuration, uses the widget's theme tokens.

import { ReqdeskProvider, FloatingWidget, ReqdeskTrigger } from '@reqdesk/widget/react';

<ReqdeskProvider apiKey="…" hideFab>
  <ReqdeskTrigger variant="pill">Open support</ReqdeskTrigger>
  {/* or variant="ghost" | "icon" */}
  <FloatingWidget />
</ReqdeskProvider>

Layer B — headless hook. Wrap your own button, keep all your design-system styling.

import { useReqdeskTrigger } from '@reqdesk/widget/react';

function HostCTA() {
  const { onClick, isOpen, ariaProps } = useReqdeskTrigger();
  return <MyButton onClick={onClick} {...ariaProps}>{isOpen ? 'Close' : 'Need help?'}</MyButton>;
}

Layer C — vanilla mountTrigger. Drop a button into any DOM element without React.

<div id="support-slot"></div>
<script>
  ReqdeskWidget.init({ apiKey: 'rqd_pk_…', hideFab: true });
  ReqdeskWidget.mountTrigger('#support-slot', { variant: 'pill', label: 'Support' });
</script>

All three paths record the activator element and restore focus to it when the widget closes.

Display modes

The widget renders as one of four surfaces, switchable at runtime via setDisplayMode(mode, side?).

| Mode | side | Shape | |------|--------|-------| | popover (default) | — | Anchored near the FAB | | side-sheet | start | end | Full-height side drawer (logical — mirrors in RTL) | | bottom-sheet | — | Full-width drawer from the bottom edge |

const { setDisplayMode } = useReqdesk();
setDisplayMode('side-sheet', 'end'); // right in LTR, left in RTL
setDisplayMode('bottom-sheet');
setDisplayMode('popover');
  • Sheet modes render a backdrop on viewports ≥ 640px; backdrop click closes unless dismissOnBackdrop: false.
  • Escape always closes in sheet modes (popover mode relies on the header close button).
  • @media (prefers-reduced-motion: reduce) disables slide-ins and the backdrop fade.
  • Set defaults via ReqdeskProvider prop display={{ mode: 'side-sheet', side: 'end', width: '480px' }}.

Walkthrough: examples/react-appDisplay modes (US4) variant.

Preferences exchange

End-user preferences (language, theme mode, accent color, widget position, display mode) can be stored on your own servers and round-tripped through the widget.

<ReqdeskProvider
  apiKey="…"
  initialPreferences={{ language: 'en', themeMode: 'auto' }}   // defaults + reset target
  userPreferences={hostStoredBlob}                             // from your API
  onPreferencesChange={(next) => saveToMyApi(next)}            // full blob on every change
>
  <FloatingWidget />
</ReqdeskProvider>

Resolution order (highest wins): runtime setPreferencesuserPreferenceslocalStorageinitialPreferences → built-in defaults. localStorage still writes even when onPreferencesChange is set, so offline and slow-host scenarios don't regress. Unknown keys are dropped with a single warning per key.

A Reset to defaults button appears in the Preferences view whenever initialPreferences is non-empty.

Programmatic control

import { useReqdesk } from '@reqdesk/widget/react';

const {
  isOpen, currentView, currentDisplayMode, preferences,  // reactive
  open, close, toggle, openMenu, openAction,             // imperative (stable refs)
  setDisplayMode, setPreferences, addAction, removeAction,
  on,                                                    // returns unsubscribe
} = useReqdesk();

// Jump straight to a specific action, passing input:
openAction('ai-chat', { prompt: 'How do I reset my password?' });

// Subscribe; store the unsubscribe and call it on unmount:
const off = on('action:triggered', (data) => console.log(data));
useEffect(() => off, []);

The same surface is available on the vanilla import (import * as ReqdeskWidget from '@reqdesk/widget').

hideFab config

Pass hideFab: true on ReqdeskWidgetConfig (vanilla) or <ReqdeskProvider hideFab> (React) to suppress the floating action button. The widget still initializes, still owns its shadow DOM, and every menu / action / display / preference path keeps working — hosts just drive the opens from their own UI.

Integration patterns

| Scenario | Recommended path | |----------|------------------| | Public marketing site, anonymous "Report a bug" | Default config — FAB + popover | | App with its own nav bar | hideFab: true + <ReqdeskTrigger variant="pill"> in the header | | Hosts with strict design-system buttons | hideFab: true + useReqdeskTrigger() hook wrapping their <Button> | | Long-form conversational flow | display: { mode: 'side-sheet', side: 'end' } | | Mobile-first layout | display: { mode: 'bottom-sheet', height: '70vh' } | | Multi-device preference portability | initialPreferences + userPreferences + onPreferencesChange | | Host-owned AI / chat launchpad | Custom action with onClick that calls ctx.keepOpen() | | Route click to host event bus | Custom action with trigger: { kind: 'custom-event', name: '…' } |

Events

// Vanilla JS
ReqdeskWidget.on('ticket:created', (ticket) => {
  console.log('Ticket created:', ticket.ticketNumber);
});

ReqdeskWidget.on('open', () => console.log('Widget opened'));
ReqdeskWidget.on('close', () => console.log('Widget closed'));
ReqdeskWidget.on('error', (err) => console.error(err));

| Event | Payload | Description | |-------|---------|-------------| | open | — | Widget panel opened | | close | — | Widget panel closed | | menu:opened | — | Menu view shown (v1.2) | | action:triggered | { actionId, viaTrigger, input? } | Action activated (v1.2) | | display-mode:changed | { mode, side?, reason } | Display mode switched (v1.2) | | prefs:changed | WidgetPreferences | Preferences mutated (v1.2) | | ticket:created | TicketResult | Ticket submitted successfully | | ticket:tracked | TrackedTicketResult | Ticket tracked by token | | reply:sent | — | Reply submitted | | error | WidgetError | Error occurred |

Since v1.2, on(event, cb) returns an unsubscribe function. Callers that ignored the return value continue to work; callers who capture it gain cleanup.

Vanilla JS API

ReqdeskWidget.init(config)             // Initialize the widget
ReqdeskWidget.open()                   // Open the panel
ReqdeskWidget.close()                  // Close the panel
ReqdeskWidget.toggle()                 // Toggle open/close

// v1.2 — programmatic control
ReqdeskWidget.openMenu()               // Open the menu view
ReqdeskWidget.openAction(id, input?)   // Activate a registered action
ReqdeskWidget.setDisplayMode(mode, side?)
ReqdeskWidget.setPreferences(partial)
ReqdeskWidget.getPreferences()
ReqdeskWidget.addAction(action)        // Returns unsubscribe
ReqdeskWidget.removeAction(id)
ReqdeskWidget.mountTrigger(target, opts)  // Returns { unmount() }

ReqdeskWidget.setLanguage('ar')        // Change language (supports RTL)
ReqdeskWidget.setTheme({ mode: 'dark' })
ReqdeskWidget.identify({ email: '[email protected]', name: 'Alice' })
ReqdeskWidget.on(event, callback)      // Returns unsubscribe (v1.2)
ReqdeskWidget.destroy()                // Remove widget from DOM

Vanilla IIFE scope (v1.2.0)

The v1.2.0 additions split cleanly between what the vanilla IIFE (<script>-tag) entry renders today and what renders only through the React entry (@reqdesk/widget/react).

Available on the vanilla entry — ticket-form + tracker tabs (unchanged v1.1.0 behaviour); init() accepts every new config key (actions, display, initialPreferences, userPreferences, onPreferencesChange, menuCloseOnAction, hideFab); mountTrigger() standalone button with ARIA + stylesheet injection; setPreferences / getPreferences / addAction / removeAction / setDisplayMode / openMenu / openAction on the registry (state + events); on(event, cb) returning an unsubscribe function; the full event stream (menu:opened, action:triggered, display-mode:changed, prefs:changed, plus the new error codes); FAB centering fix; hideFab respected.

React-entry-only for v1.2.0 — the rendered menu view that walks registered actions alongside the built-ins (<FloatingWidget> via useRegistrySnapshot); the animated side-sheet / bottom-sheet shells with backdrop + Escape + dismissOnBackdrop; the reactive useReqdesk() slices (isOpen, currentView, currentDisplayMode, preferences); the "Reset to host defaults" preferences control.

If your host needs the custom menu or sheet modes outside React, the recommended path is to ship a small React bundle for the widget surface — the package isolates inside shadow DOM and does not leak into the host render tree. Bridging a registry subscribe loop into the vanilla renderPanel() is tracked as a follow-up.

Error handling — 422 validation, global toasts, inline field errors (v1.2.7+)

The widget normalizes every failed request once at the ofetch layer, funnels it through TanStack Query v5's QueryCache + MutationCache error callbacks, and surfaces the outcome in two places:

  1. A global notification stack at the top of the widget panel — shows the backend's own responseMessage plus any unmapped JSON:API errors[].detail. Errors stay until dismissed; info/success auto-dismiss. Accessible (role="alert" + aria-live="polite").
  2. Inline per-field errors next to the offending input — routed from the JSON:API source.pointer (e.g. /data/attributes/priority) to the matching field. aria-invalid + aria-describedby + a red border applied automatically.

Both layers are opt-in / opt-out per call site using TanStack Query's meta field (fully typed — autocomplete included):

// Form-bound mutation: suppress the 422 toast (inline field errors take over). Non-422
// failures (network, 5xx) still toast so the user isn't left wondering.
useMutation({ meta: { form: 'ticket-submit' }, mutationFn: … })

// Probe-style query that legitimately 404s on a cold cache — silence that one status.
useQuery({ meta: { allow404: true, silent: true }, ... })

Host-form example

Host apps that render their own forms via useReqdesk() can plug into the same 422 → inline-error pipeline in a few lines:

import {
  ReqdeskProvider,
  useReqdesk,
  FormErrorProvider,
  useFieldError,
  FieldError,
} from '@reqdesk/widget/react'
import { useMutation } from '@tanstack/react-query'

function HostTicketForm() {
  const { submitTicket } = useReqdesk()
  const submit = useMutation({
    meta: { form: 'ticket-submit' }, // opt out of global 422 toasts
    mutationFn: (data) => submitTicket(data),
  })

  return (
    <FormErrorProvider mutation={submit}>
      <PriorityField />
      <button onClick={() => submit.mutate({ … })}>Submit</button>
    </FormErrorProvider>
  )
}

// Custom styled field — the hook gives you everything a11y-wise; you own the layout.
function PriorityField() {
  const { inputProps } = useFieldError('priority')
  return (
    <div className="my-form-row">
      <label htmlFor="priority">Priority</label>
      <select id="priority" name="priority" className="my-input" {...inputProps}>
        <option value="low">Low</option>
        <option value="medium">Medium</option>
        <option value="high">High</option>
        <option value="critical">Critical</option>
      </select>
      <FieldError name="priority" /> {/* role="alert" div, hidden when no error */}
    </div>
  )
}

The same useFieldError hook is drop-in for <input> / <textarea> / any native form control. Use <FormFieldGroup name="…" label="…"> for the common label + input + error layout when you don't need custom markup.

Host apps that want to push their own toasts through the widget's stack — e.g. "Session expired, please refresh" — call useNotify().push({ kind: 'error', title, detail }) from anywhere under <ReqdeskProvider>.

React API

Components

import {
  ReqdeskProvider,  // Context provider (required wrapper)
  FloatingWidget,   // FAB + slide-up panel
  TicketForm,       // Standalone ticket form (inline mode)
  SupportPortal,    // Standalone support portal
  ShadowRoot,       // Shadow DOM wrapper for custom components
} from '@reqdesk/widget/react';

ReqdeskProvider Props

| Prop | Type | Required | Description | |------|------|----------|-------------| | apiKey | string | Yes | Project API key | | auth | OidcAuthConfig | No | Keycloak OIDC config | | theme | ThemeConfig | No | Visual customization | | language | string | No | Language code | | customer | CustomerConfig | No | Pre-fill customer info | | translations | Record<string, string> | No | Custom translations |

FloatingWidget Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | FAB position | | contained | boolean | false | Use absolute positioning (for previews) | | onTicketCreated | (ticket: TicketResult) => void | — | Callback on ticket creation | | onError | (error: WidgetError) => void | — | Callback on error |

Hooks

import { useReqdesk } from '@reqdesk/widget/react';

function CustomForm() {
  const { submitTicket, trackTicket, isLoading, error } = useReqdesk();

  const handleSubmit = async () => {
    const result = await submitTicket({
      title: 'Bug report',
      email: '[email protected]',
      priority: 'high',
    });
    console.log('Created:', result.ticketNumber);
  };
}

File Attachments

The widget supports file attachments on ticket submission and replies:

  • Drag-and-drop or click to browse
  • Max 5 files per submission, 5 per reply
  • Max 10MB per file (configurable per project)
  • Allowed types: Images (JPEG, PNG, GIF, WebP), PDF, Office documents, text, CSV, ZIP
  • Blocked: Executables (.exe, .bat, .cmd, .sh, .ps1, .msi, .dll, .scr)
  • Upload progress shown per-file after submission

Files are uploaded sequentially after the ticket/reply is created.

Customization

Custom Translations

Override any built-in string:

ReqdeskWidget.init({
  apiKey: '...',
  translations: {
    'widget.title': 'Help Center',
    'form.submit': 'Send Request',
    'menu.newTicket': 'New Request',
  },
});

See src/i18n/en.ts for all available translation keys.

RTL Support

Set language: 'ar' for full RTL layout. The widget automatically mirrors all UI elements.

End-User Preferences

Visitors can customize their experience from the Preferences menu:

  • Language: English / Arabic toggle
  • Theme: Light / Dark / System
  • Accent Color: 5 preset colors (green, blue, purple, orange, red)

Preferences are persisted to localStorage per API key.

Architecture

  • Shadow DOM: Complete CSS isolation — host page styles cannot affect the widget
  • Zero global state: Widget context is self-contained, no window globals
  • ofetch: HTTP client with automatic retry, timeout, and auth interceptors
  • TanStack Query: Data fetching with caching, optimistic updates, and loading states (React only)
  • Optional dependencies: React, oidc-spa, and TanStack Query are all optional peer deps

Bundle Sizes

| Entry | Size (gzip) | |-------|-------------| | index.iife.js (vanilla) | ~12 KB | | react.js (ESM) | ~6 KB |

React, oidc-spa, and TanStack Query are not bundled — they're peer dependencies.

Environment Variables

See .env.example for all available environment variables. These are for use in your application's build process — the widget SDK receives configuration via props/init, not directly from env vars.

TypeScript

Full TypeScript support with exported types:

import type {
  ReqdeskWidgetConfig,
  ThemeConfig,
  OidcAuthConfig,
  CustomerConfig,
  TicketResult,
  WidgetError,
  TrackedTicketResult,
  PublicReply,
  SubmitTicketData,
  WidgetConfigPersist,
} from '@reqdesk/widget/react';

Browser Support

Chrome, Firefox, Safari, and Edge (latest versions). Requires Shadow DOM support.

License

Proprietary. See LICENSE file for details.