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

@statisfy/digital-workers-react

v0.1.9

Published

Embeddable Digital Worker chat SDK for React.

Downloads

463

Readme

@statisfy/digital-workers-react

Embeddable Digital Worker chat and project modules for React.

Install

npm install @statisfy/digital-workers-react react react-dom

react and react-dom (18 or 19) are peer dependencies you provide. Everything else the SDK needs is bundled.

Usage

Wrap your app once in <StatisfyProvider>, then render components anywhere beneath it. Connection and auth live on the provider; components take only their own display props.

import {
  DigitalWorkerChat,
  StatisfyProvider
} from '@statisfy/digital-workers-react'
import '@statisfy/digital-workers-react/styles.css' // required — import once

export function Support() {
  return (
    <StatisfyProvider
      publishableKey="pk_live_…"
      getToken={async () => '<bearer>'}
    >
      <DigitalWorkerChat workerId="<uuid>" workerName="Assistant" />
    </StatisfyProvider>
  )
}

You do not configure a gateway URL — the SDK targets the Statisfy production gateway. Mint the bearer on your own backend and return it from getToken; never ship a secret to the browser.

A component rendered outside <StatisfyProvider> throws, rather than silently sending unauthenticated requests. The stylesheet import is required; without it the widget renders unstyled.

Theming

Everything visual in the SDK is reachable from host CSS through two seams — a variable table and a set of anatomy attributes. Both work with plain, normal declarations.

!important is not needed and must not be used. All SDK CSS ships inside @layer (theme, base, components, utilities), and for normal declarations an unlayered author rule outranks every layer regardless of specificity or import order. !important inverts layer order, so it makes the SDK's own rules win again — the opposite of what you want.

One exception, and it is not part of the theming contract. Under prefers-reduced-motion: reduce the SDK sets transition-duration, animation-duration, animation-iteration-count and scroll-behavior with !important on [data-dw-part] and its descendants. That is an accessibility floor, not a style: the person asking for less motion is the end user, not the integrator, so it is the one axis a host does not get to overrule. Everything you actually theme — color, radius, elevation, spacing, layout, and the motion tokens themselves — stays plain-declaration territory where you always win. The floor is scoped to [data-dw-part], so it can never reach your own DOM.

1. Override a variable

Declare it in an unlayered rule — :root, or any scope you want it confined to. No !important, and it holds no matter which stylesheet loads first.

:root { --dw-primary: 13 148 136; }

/* or scope it to one mount point */
.support-drawer { --dw-primary: 13 148 136; }

Colors are space-separated RGB triplets, not rgb() or hex — that is what lets opacity modifiers such as dw:bg-destructive/10 work internally. Write 13 148 136, never #0d9488.

Colors

| variable | default | used for | |---|---|---| | --dw-background | 255 255 255 | primary surface | | --dw-background-secondary | 248 248 249 | headers, inset surfaces | | --dw-foreground | 26 26 26 | body text | | --dw-muted | 241 241 242 | hover fills, agent bubble, badges | | --dw-muted-foreground | 107 114 128 | secondary text, timestamps | | --dw-primary | 83 35 181 | brand fill: send button, progress, current step | | --dw-primary-foreground | 255 255 255 | text on --dw-primary | | --dw-border | 229 231 235 | hairlines, dividers, table cells | | --dw-input | 209 213 219 | form control borders | | --dw-destructive | 220 38 38 | errors, failed tool calls | | --dw-success | 22 128 90 | completed tool call, completed journey step | | --dw-warning | 180 83 9 | "Awaiting reply", pending states |

Tinted grounds pair with text-foreground, never with their own hue. A semantic hue is used one of three ways: as a solid fill with --dw-primary-foreground-style inverse text on it, as an icon or dot at full saturation, or as a low-alpha tint (bg-success/12) behind ordinary body text. What it is never used for is small text on a tint of itself. At any mid-luminance hue — and --dw-success, --dw-warning and --dw-destructive are all mid-luminance by construction, because they have to read on white — the tint lands only a few percent away from the type, so text-success on bg-success/12 cannot reach 4.5:1 no matter which green you pick. Darkening the token to fix the text then breaks the icon and the solid fill. So the hue carries the meaning and the foreground token carries the legibility; if you retheme a semantic color, you only have to keep it distinguishable, not readable-on-itself.

Dark palette (0.1.5) — opt-in

The SDK now ships a measured dark palette, and you turn it on. Before 0.1.5 there was none, so dark mode was entirely your problem. It is opt-in rather than automatic because an embedded widget should match the page it is mounted in, not the visitor's OS — most host pages are light-only whatever the OS says, so keying dark off prefers-color-scheme alone flips the widget dark inside a light page. CSS cannot ask "is my host page dark?", so you tell it:

<html data-dw-theme="dark">   <!-- always dark -->
<html data-dw-theme="auto">   <!-- follow the visitor's OS -->
<!-- attribute absent      -->  <!-- light: the 0.1.x behaviour, unchanged -->

Nothing changes appearance when you upgrade. If you already declare a full palette you need none of this — your unlayered :root outranks both blocks.

| variable | light | dark | |---|---|---| | --dw-background | 255 255 255 | 42 43 53 | | --dw-background-secondary | 248 248 249 | 52 53 65 | | --dw-foreground | 26 26 26 | 237 238 243 | | --dw-muted | 241 241 242 | 60 61 74 | | --dw-muted-foreground | 107 114 128 | 172 175 188 | | --dw-primary | 83 35 181 | 183 163 255 | | --dw-primary-foreground | 255 255 255 | 26 26 33 | | --dw-border | 229 231 235 | 80 82 97 | | --dw-input | 209 213 219 | 116 119 136 | | --dw-destructive | 220 38 38 | 254 139 139 | | --dw-success | 22 128 90 | 70 205 138 | | --dw-warning | 180 83 9 | 251 191 60 | | --dw-shadow-color | 15 23 42 | 0 0 0 | | --dw-overlay | rgb(15 23 42 / 0.55) | rgb(0 0 0 / 0.72) | | --dw-color-scheme | light | dark |

Three of those are worth calling out. --dw-background steps up from the near-black most dark apps use, because a widget cannot build an edge against its own value — if your page and the widget share a background, no border and no shadow will separate them. --dw-shadow-color goes to black, because the light default (slate) is lighter than a dark page and would make every --dw-elev-* step inert. And --dw-primary-foreground flips to dark ink; see the invariant below for why it has to.

Your :root still wins. Both dark blocks live inside @layer theme, exactly like the light defaults, so an unlayered host declaration beats them at any OS setting, in any import order. Verified in Chromium:

| your setup | OS light | OS dark | |---|---|---| | no data-dw-theme | light | light | | data-dw-theme="dark" | dark | dark | | data-dw-theme="auto" | light | dark | | full unlayered :root | yours | yours |

If you opt in with a partial palette, re-point the whole thing. Declaring --dw-primary and letting the SDK supply the grounds is fine in light and breaks in dark: a light-tuned mid-luminance violet as md-link ink on the dark --dw-muted measures 1.16:1, and status icons land at ~2.0:1 on their own 12% tints. CSS gives no way to ask "did the author declare this property", so the SDK cannot detect a half-set palette and compensate. If you set data-dw-theme, own every token in the table above or none of them.

One invariant you own the moment you re-point the brand: --dw-primary must clear 4.5:1 against --dw-primary-foreground. This is the pair that breaks first, because --dw-primary has two jobs: a solid fill under --dw-primary-foreground (chat-send, form-submit, the customer's own chat bubbles, the current journey marker) and ink on the page (md-link, history-new). On a light ground one dark brand value serves both. On a dark ground it cannot: a violet dark enough to carry white text is too dark to read as a link on a dark surface, and lightening it for the link drops the filled controls below 4.5:1. Tune the hue for ink and flip the foreground to dark — that is what the shipped dark values do, and they measure 4.95:1 as md-link ink on --dw-muted and 7.99:1 as a fill under dark ink. If you re-point --dw-primary for dark, re-point --dw-primary-foreground with it.

--dw-color-scheme covers the native controls. Checkbox ticks, the date input's calendar glyph, <select> arrows and the scroll regions are painted by the browser and read none of the other --dw-* variables; color-scheme is the only lever. The SDK declares it on [data-dw-part] from this token, so it reaches every control inside the widget and nothing outside it — deliberately not on :root, where it would repaint your page's canvas and document scrollbars. Measured on an unchecked checkbox: rgb(59 59 59) under --dw-color-scheme: dark, rgb(255 255 255) under light.

Focus ring

Drawn once for every interactive part by a @layer components rule, so there is no per-component ring to chase.

| variable | default | |---|---| | --dw-ring | var(--dw-primary) (triplet — inherits your brand for free) | | --dw-focus-width | 2px | | --dw-focus-offset | 2px |

Elevation

--dw-shadow-color is the ink; the four steps are complete box-shadow strings. Set a step to none to flatten that tier everywhere.

| variable | default | used for | |---|---|---| | --dw-shadow-color | 15 23 42 | shadow ink (triplet) | | --dw-elev-1 | 0 1px 2px …/.06 | chips, badges, widget root, project card | | --dw-elev-2 | 0 1px 2px …/.05, 0 2px 6px -2px …/.08 | cards, task groups, form card | | --dw-elev-3 | 0 2px 4px …/.05, 0 10px 24px -8px …/.16 | toast | | --dw-elev-4 | 0 2px 4px …/.06, 0 24px 48px -16px …/.28 | drawer, lightbox |

Overlay

The modal scrim. Full color values, not triplets — the scrim carries its own alpha.

| variable | default | |---|---| | --dw-overlay | rgb(15 23 42 / 0.55) | | --dw-overlay-blur | 2px |

Radius

| variable | default | used for | |---|---|---| | --dw-radius-sm | 0.375rem | chips, inline code, thumbnails, bubble tail | | --dw-radius-md | 0.5rem | inputs, buttons, rows, lightbox panel | | --dw-radius-lg | 0.75rem | cards, bubbles, toast, task groups | | --dw-radius-xl | 1rem | widget root, task drawer |

There is no --dw-radius-full; pills, dots and avatars are fully round by design and are not on the radius scale.

Motion

| variable | default | |---|---| | --dw-default-transition-duration | 160ms | | --dw-default-transition-timing-function | cubic-bezier(0.2, 0.7, 0.2, 1) | | --dw-animate-spin / --dw-animate-pulse / --dw-animate-bounce | Tailwind defaults |

Under prefers-reduced-motion: reduce the SDK already sets the three --dw-animate-* tokens to none and the transition duration to 0ms, for the whole widget at once. You do not need to add your own guard.

Zeroing those tokens is the graceful half. It cannot be the whole guard, because --dw-default-transition-duration is a dial you are invited to set: a host that declares :root { --dw-default-transition-duration: 150ms } in its own unlayered sheet wins that variable in every media context, reduced-motion included, and the widget animates anyway. So a second, token-independent floor (see the !important note above) backs it up. Net effect: set the duration token to whatever you like — reduced-motion users still get a still interface, and you do not have to remember to write the media query yourself.

2. Target one element

Every styled element carries a data-dw-part attribute naming its role in the anatomy. Select it directly — an attribute selector has the same specificity as a class, and the same unlayered-wins rule applies.

[data-dw-part='task-row'] { border-radius: 0; }

Some parts also carry state attributes you can key off: data-dw-role (user|agent), data-dw-state (loading|error|empty|ready), data-dw-status (slugified status value — see below), data-dw-step (done|current|upcoming), data-dw-selected, data-dw-unread, data-dw-busy, data-dw-level (16), data-dw-tool-state, data-dw-held, data-dw-field, data-dw-type, data-dw-empty.

[data-dw-part='chat-bubble'][data-dw-role='user'] { background: #0d9488; }
[data-dw-part='journey-marker'][data-dw-step='done'] { background: #ecfdf5; }

data-dw-status carries your own status value, lowercased with every run of separators collapsed to a single -. Letters and digits survive in any script, so a non-Latin status keeps its identity and you quote it like any other value — In Progressin-progress, 進行中進行中, Готовоготово:

[data-dw-part='task-row'][data-dw-status='進行中'] { background: #fffbeb; }

Documented-stable part names

The 74 names below are the public contract and will not be renamed without a major version. (That count is just the length of this list — the SDK ships more data-dw-part values than these; the rest exist in the DOM but are present, not yet guaranteed, so style them at your own risk.)

attachment-chip · attachment-list · auth-card · auth-input · auth-submit · chat-bubble · chat-composer · chat-error · chat-header · chat-input · chat-name · chat-pane · chat-root · chat-row · chat-send · chat-toast · chat-tool-chip · comment · comment-body · comment-composer · comment-input · comment-submit · comments · comments-list · field · field-input · field-label · field-value · form-card · form-submit · history-list · history-root · history-row · journey · journey-marker · journey-step · journey-title · lightbox-overlay · lightbox-panel · md-blockquote · md-code-inline · md-heading · md-link · md-list · md-p · md-pre · md-root · md-table · messages · picker-button · picker-chip · progress · progress-fill · progress-track · progress-value · project-card · project-error · project-header · project-title · status-badge · status-dot · status-notice · status-select · status-select-control · task-detail-backdrop · task-detail-body · task-detail-panel · task-detail-save · task-detail-title · task-group · task-group-header · task-list · task-row · task-row-title

One caveat on that list: chat-tool-chip is unreachable in the current build. MessageBubble renders it (and its chat-tool-chips wrapper) from ChatMessage.toolCalls, but nothing in the SDK ever populates that field, so no render path produces the element. The name and its data-dw-tool-state values are held for when the tool-call surface is wired back up; a selector you write against it today is valid CSS that simply never matches. Every other name above is live in the DOM and covered by a test.

fill, sidekick, sidekick-launcher, sidekick-badge and sidekick-panel are reserved for host portal shells — the SDK will never mint them.

Examples

Recolor to your brand. Two variables move the majority of the surface; --dw-ring follows --dw-primary automatically.

:root {
  --dw-primary: 13 148 136;
  --dw-primary-foreground: 255 255 255;
}

Re-radius everything. Four variables, one step each — every card, bubble, input and chip follows.

:root {
  --dw-radius-sm: 2px;
  --dw-radius-md: 3px;
  --dw-radius-lg: 4px;
  --dw-radius-xl: 6px;
}

Go square and flat. The radius and elevation axes both run to zero, and between them they carry the whole "soft product card" vs. "technical blueprint" distinction — no part selectors needed. Radius 0 squares every corner that is on the scale (pills, dots and avatars stay round by design — see above); each elevation step set to none removes that tier's shadow entirely, leaving the 1px hairlines as the only separation.

:root {
  --dw-radius-sm: 0;
  --dw-radius-md: 0;
  --dw-radius-lg: 0;
  --dw-radius-xl: 0;

  --dw-elev-1: none;
  --dw-elev-2: none;
  --dw-elev-3: none;
  --dw-elev-4: none;
}

Flatten only the upper tiers (--dw-elev-3/-4: none) if you want cards to keep their lift but modals to sit flush.

Restyle one element. No variable covers "make task rows a card"; reach for the part instead.

[data-dw-part='task-row'] {
  margin-block: 4px;
  border-top: 0;
  border-radius: var(--dw-radius-md);
  box-shadow: var(--dw-elev-1);
}

Two caveats

  1. Overriding background or color on a part also kills that element's :hover and :focus-visible fill. Your unlayered rule beats the SDK's layered utility in every state, not just the resting one, so re-declare the states you still want:

    [data-dw-part='task-row'] { background: #fafafa; }
    [data-dw-part='task-row']:hover { background: #f0f0f0; }

    Overriding border-radius or box-shadow has no such side effect — nothing changes those on hover.

  2. Two radii are not tokenised. A handful of literals sit outside the scale: bare rounded compiles to a hardcoded 0.25rem, and rounded-full compiles to calc(infinity * 1px) with no --dw-radius-full behind it. If you need to change a pill or a dot, target its part.

Documentation

Full guide in the Statisfy help center:

License

MIT