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

@pmcollab/coworkstream

v1.5.0

Published

Headless, props-driven Work Stream list component for React. Designed for embedding in operator/agent dashboards.

Readme

@pmcollab/coworkstream

A headless, props-driven Work Stream list component for React. Drop it into an operator dashboard, agent console, or inbox-style UI and wire it to your own data and events.

Coding agents: if you are a coding agent integrating this package on behalf of a developer, read AGENTS.md. It contains a single-page integration recipe with the full prop schema, event contract, and a paste-ready example.

Migrating from an existing in-house implementation? Read MIGRATION.md for the adapter pattern (extracted from a real ~600-LoC migration of the CoWorkStream host app onto this package).


Highlights

  • Headless and uncontrolled-by-default. Items in, events out. No global stores, no data fetching, no opinions about your backend.
  • Slot-based. renderDetails, renderActions, renderHeader, renderEmptyState, renderLoading let you replace any chunk of the UI.
  • Controllable. Both activeFilter and expandedId support controlled and uncontrolled patterns.
  • Mobile-aware. Built-in responsive layout, with optional "defer to desktop" workflow.
  • TypeScript-first. Hand-authored .d.ts declarations are shipped alongside the source. Type-tested with tsd.
  • Pre-built ESM + CJS + standalone CSS. Use it without Tailwind. Use it without a JSX-aware build.
  • Production-grade safety. Runtime prop validation in dev, error boundaries around render slots, axe-core accessibility tests in CI, SSR smoke-tested for Next.js / Remix.
  • Tiny. ~12 kB ESM (brotli), ~3.6 kB CSS (gzip). Tree-shakable.

Install

npm install @pmcollab/coworkstream
# peer deps
npm install react react-dom

You have a few ways to load the styles. Pick one:

  1. No Tailwind in your app, Tailwind v3-shaped host CSS (most apps today):

    import '@pmcollab/coworkstream/styles.css'
  2. No Tailwind in your app, but you want full isolation from any host styling:

    import '@pmcollab/coworkstream/styles-scoped.css'

    Every selector in this bundle is prefixed with .coworkstream-root, so the rules only match inside the component's subtree. <WorkStream> automatically adds the coworkstream-root class to its <section> root, so this just works without extra wrappers.

  3. Host is on Tailwind v4 and you don't want to wire up the preset:

    import '@pmcollab/coworkstream/styles-v4.css'

    Same utilities, compiled with @tailwindcss/cli v4 so the variable schema (--tw-gradient-position, color-mix(in oklab …), registered @property declarations) matches a v4 host.

  4. Already using Tailwind? Wire up the Tailwind preset so unused classes aren't purged and skip every styles*.css import — you only need one styling path.

Cascade safety. All three prebuilt bundles wrap their rules in @layer coworkstream { … }. Per CSS Cascade Level 5, unlayered rules beat layered rules, so if you import a bundle in an app that already emits Tailwind utilities, your app's .mb-4 / .bg-amber-100 / etc. always take precedence and the bundle only fills in classes you haven't emitted. The scoped bundle adds belt-and-suspenders isolation for hosts whose own CSS uses cascade layers in a way that could otherwise change the order.


Quickstart

import { WorkStream } from '@pmcollab/coworkstream'

const items = [
  {
    id: 'roadmap-q2',
    category: 'decision',
    priority: 'high',
    title: 'Roadmap conflict: Slot Q2 capacity',
    subtitle: '3 agents • conflict',
    summary: 'Engineering is blocked until Friday.',
    icon: '✋',
    iconBg: '#ef4444',
    deadline: 'Friday',
  },
  {
    id: 'beta-feedback',
    category: 'signal',
    title: 'Beta feedback spike: Workflow Builder permissions',
    subtitle: 'Customer Insights Agent • 9 of 23 beta users flagged',
    icon: '📡',
  },
]

export function Inbox() {
  return (
    <WorkStream
      items={items}
      onAction={(action, item) => {
        console.log('action', action, 'on', item.id)
      }}
      onExpandChange={(id) => console.log('expanded', id)}
    />
  )
}

That's the minimum. The component renders its own header, filter chips, and quick action buttons.


The data model

interface WorkStreamItem {
  id: string                  // required, stable
  category: string            // 'decision' | 'signal' | 'draft' | ... or any custom string
  title: string               // required
  subtitle?: string
  summary?: string            // shown in the expanded body
  priority?: 'low' | 'medium' | 'high'
  icon?: ReactNode            // emoji string or any node
  iconBg?: string             // CSS color (20% alpha applied)
  deadline?: string           // free-form, e.g. 'Friday', 'EOD', '24 hours'
  badge?: { label: string; tone?: BadgeTone; tooltip?: string }   // override default badge
  tooltips?: Partial<Record<string, string>>   // per-action hover text (see below)
  help?: string | { title?: ReactNode; html?: string; content?: ReactNode }  // ? help icon + modal
  deviceTarget?: 'mobile' | 'desktop' | 'any'
  unbundledFrom?: string
  consolidatedItems?: WorkStreamItem[]
  isMultiAgent?: boolean
  multiAgent?: {
    alignmentStatus: 'aligned' | 'partial' | 'conflict'
    tooltip?: string   // hover text for the whole multi-agent badge
    positions: Array<{ avatar: string; agentId?: string; agentName?: string; tooltip?: string }>
  }
  meta?: Record<string, unknown>   // pass-through; available in render slots
}

Built-in category keys (with default badge colors): decision, prepared, signal, draft, consolidated, audit, incident, hiring. Custom strings are allowed — if no built-in tone matches, the badge falls back to default styling. Use item.badge.tone to opt into a specific color.


Custom filters

const filters = [
  { key: 'all', label: 'All' },
  { key: 'urgent', label: 'Urgent', match: (item) => item.priority === 'high' },
  { key: 'mine',   label: 'Mine',   match: (item) => item.meta?.assignee === currentUser.id },
  { key: 'signal', label: 'Signal' },   // implicit category match
]

<WorkStream items={items} filters={filters} defaultActiveFilter="urgent" />

If match is omitted, items are matched by item.category === filter.key. The chip with key: 'all' is special-cased and always shows everything.


Custom expanded content

<WorkStream
  items={items}
  renderDetails={(item) => (
    <div className="space-y-3">
      <h3 className="text-sm font-semibold">Risks</h3>
      <ul>
        {(item.meta?.risks ?? []).map((r) => <li key={r.id}>{r.label}</li>)}
      </ul>
    </div>
  )}
  renderActions={(item, ctx) => (
    <div className="flex gap-2">
      <button onClick={() => ctx.onAction('approve')}>Approve</button>
      <button onClick={() => ctx.onAction('reject')}>Reject</button>
    </div>
  )}
  onAction={(action, item) => api.itemAction(item.id, action)}
/>

renderActions receives a ctx.onAction(action) helper — call it from inside your custom buttons and the parent onAction will fire with the (action, item) pair, just like the built-in quick actions.


Controlled mode

const [filter, setFilter] = useState('all')
const [openId, setOpenId] = useState(null)

<WorkStream
  items={items}
  activeFilter={filter}
  onFilterChange={setFilter}
  expandedId={openId}
  onExpandChange={setOpenId}
/>

Defer-to-desktop

Opt-in workflow for "I'll handle this when I'm at a real keyboard." On mobile, items render a 🖥 button; on desktop, deferred items float to the top with a banner.

const [deferredIds, setDeferredIds] = useState([])

<WorkStream
  items={items}
  deferredIds={deferredIds}
  onDeferToDesktop={(item) => setDeferredIds((ids) => [...ids, item.id])}
  onClearDeferred={(item) => setDeferredIds((ids) => ids.filter((x) => x !== item.id))}
/>

If you don't pass onDeferToDesktop, the feature is disabled and the 🖥 button is not rendered.


Hover text (tooltips)

Every action button on a card renders a native hover tooltip (the HTML title attribute). Each one has a sensible default, and you can override any of them per item through a single tooltips map — the consistent way to set custom hover text across all cards:

const items = [
  {
    id: 'roadmap-q2',
    category: 'decision',
    title: 'Roadmap conflict: Slot Q2 capacity',
    tooltips: {
      delegate: 'Hand this to the staffing agent',
      defer: 'Snooze until the next planning cycle',
      ignore: 'Dismiss — this no longer needs a decision',
    },
  },
]

Recognized keys map to the built-in buttons: delegate, defer, ignore, deferToDesktop, viewFullDetails, and help. Any key you omit falls back to the shared default in labels[${key}Tooltip] (overridable globally via the labels prop for i18n). Custom keys are allowed too, so the same map can carry hover text for buttons you render yourself in renderActions:

<WorkStream
  items={items}
  renderActions={(item, ctx) => (
    <button title={item.tooltips?.approve} onClick={() => ctx.onAction('approve')}>
      Approve
    </button>
  )}
/>

Badge and agent-avatar tooltips

Badges accept hover text the same way:

{
  id: 'x',
  category: 'decision',
  title: 'Approve Q2 plan',
  badge: { label: 'Decision', tone: 'decision', tooltip: 'Needs a human sign-off' },
  isMultiAgent: true,
  multiAgent: {
    alignmentStatus: 'conflict',
    tooltip: '2 agents disagree on this item',   // whole badge
    positions: [
      { avatar: '🛰', agentName: 'Atlas' },                       // hover → "Atlas"
      { avatar: '📡', agentName: 'Beacon', tooltip: 'Beacon (lead)' }, // hover → "Beacon (lead)"
    ],
  },
}

Each stacked agent avatar shows its own hover text. Set position.tooltip to customize it; otherwise it defaults to the agent's agentName.


Help content (the ? icon)

Attach help to an item to surface a ? icon on the card. The icon only renders when help is present. Clicking it floats a modal dialog (focus- trapped, Esc-to-close) containing the help content:

const items = [
  {
    id: 'doc-review',
    category: 'prepared',
    title: 'Review onboarding doc',
    // String shorthand = raw HTML rendered inside the modal.
    help: '<p>This item needs a <strong>legal</strong> sign-off before publishing. See the <a href="/runbook">runbook</a>.</p>',
  },
]

For a custom title or rich React content, pass an object instead of a string:

{
  id: 'approvals',
  category: 'decision',
  title: 'Approve budget',
  help: {
    title: 'How approvals work',
    content: <ApprovalsHelpPanel />,   // any ReactNode; takes precedence over `html`
  },
}

The modal title defaults to labels.helpTitle ("Help") and the icon's hover text to labels.helpTooltip — both overridable globally via labels, or per-item via tooltips.help.

Security: help (or help.html) is rendered with dangerouslySetInnerHTML. It is author-controlled content from your item model. Sanitize it if any part originates from untrusted input.


Theming

The package uses Tailwind CSS classes and supports class-based dark mode (<html class="dark">).

1. Add the package paths to your content glob

// tailwind.config.{js,ts}
import workstreamPreset from '@pmcollab/coworkstream/tailwind-preset'

export default {
  presets: [workstreamPreset],
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './node_modules/@pmcollab/coworkstream/src/**/*.{js,jsx}',
  ],
  // ...
}

This ensures Tailwind doesn't purge classes used inside the package, and adds the scrollbar-hide utility used by the filter chip row.

2. Customize via className / itemClassName

The outer <section> accepts className. Each item card accepts itemClassName (applied to every <WorkStreamItemCard>). For deeper customization, use the render slots.

3. Internationalization

Pass labels to override any user-facing string. Defaults are in English.

<WorkStream
  items={items}
  labels={{
    title: 'Bandeja',
    delegate: 'Delegar',
    defer: 'Aplazar',
    ignore: 'Ignorar',
    allClear: 'Todo en orden',
  }}
/>

See the WorkStreamLabels type for the full list.


Full prop reference

See types/index.d.ts — the declarations are the source of truth for the public API. Every prop, event, and helper is documented there with JSDoc.

| Prop | Type | Notes | |---|---|---| | items | WorkStreamItem[] | Required. | | filters | FilterDef[] | Default: 8 built-in chips. | | activeFilter / defaultActiveFilter / onFilterChange | controlled trio | | | expandedId / defaultExpandedId / onExpandChange | controlled trio | | | isLoading | boolean | Renders skeleton via renderLoading. | | isMobile | boolean | Override viewport detection. | | deferredIds | string[] | Opt-in defer-to-desktop. | | onDeferToDesktop / onClearDeferred | (item) => void | | | showDesktopItemsOnMobile (+ default + onToggle) | controlled trio | | | onAction | (action, item) => void | Built-ins: 'delegate' \| 'defer' \| 'ignore'. | | onItemClick | (item) => void | Fires before expand toggles. | | renderDetails | (item) => ReactNode | Replace expanded body. | | renderActions | (item, ctx) => ReactNode | Replace action row. | | renderHeader | (args) => ReactNode | Replace title + chips. | | renderLoading / renderEmptyState | () => ReactNode | | | labels | Partial<WorkStreamLabels> | i18n. | | className / itemClassName | string | |


Examples

The examples/ folder contains paste-ready integration files:

  • basic.jsx — minimal mount with mock data.
  • custom-actions.jsx — replacing the action row with category-specific buttons.
  • custom-details.jsx — using renderDetails to render rich expanded content.
  • with-backend.jsx — wiring onAction to a REST API and an optimistic store.

Versioning and stability

Pre-1.0 the API may evolve based on licensee feedback. Breaking changes will be documented in CHANGELOG.md.


License

Commercial. See LICENSE.