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

@wcstack/idle

v1.21.0

Published

Declarative Idle Detection component for Web Components. Framework-agnostic IdleDetector wrapper via wc-bindable-protocol.

Readme

@wcstack/idle

@wcstack/idle is a headless Idle Detection component for the wcstack ecosystem.

It is not a visual UI widget. It is an async primitive node that turns IdleDetector's live user/screen state into reactive state, gated behind an explicit, gesture-driven permission command.

With @wcstack/state, <wcs-idle> can be bound directly through path contracts:

  • input surface: threshold (ms, minimum 60000)
  • output state surface: userState, screenState, active, error, errorInfo

Why this exists — the reference implementation for gesture-gated permission

IdleDetector.requestPermission() is a static method that must be called from within a real user gesture. connectedCallback runs outside that context, so this node never auto-starts on connect — the caller drives requestPermission()start() explicitly, typically from a click handler.

Compose with @wcstack/permission. navigator.permissions.query({name:"idle-detection"}) already exists — pair <wcs-idle> with <wcs-permission name="idle-detection"> for granted/denied/prompt status. <wcs-idle> itself only exposes the actual idle state plus the one-time requestPermission() action; it does not duplicate the 4-value permission state.

Chromium-only. Firefox and Safari do not implement IdleDetector.

Install

npm install @wcstack/idle

Quick Start

<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/permission/auto"></script>
<script type="module" src="https://esm.run/@wcstack/idle/auto"></script>

<wcs-state>
  <script type="module">
    export default {
      // Assume present until told otherwise: `wcs-idle:change` never fires
      // before start(), so a `false` default here would show "Away" on
      // every initial load even though presence is simply unknown yet.
      presenceActive: true,
      idleGranted: false,
      async enableIdleDetection() {
        const el = document.querySelector("wcs-idle");
        const result = await el.requestPermission();
        if (result === "granted") await el.start();
      },
    };
  </script>
</wcs-state>

<wcs-permission name="idle-detection" data-wcs="granted: idleGranted"></wcs-permission>
<wcs-idle threshold="60000" data-wcs="active: presenceActive"></wcs-idle>

<!-- Note: don't bind `disabled: idleGranted` here — the grant persists across
     page loads, so on a revisit the button would start out disabled and start()
     (only reachable through this click) could never run. Re-clicking when
     already granted is harmless: requestPermission() resolves "granted"
     immediately and start() proceeds. -->
<button data-wcs="onclick: enableIdleDetection">Enable presence detection</button>
<p>Permission granted: <span data-wcs="textContent: idleGranted"></span></p>
<template data-wcs="if: presenceActive|not">
  <span class="badge">Away</span>
</template>

Observable Properties (outputs)

| Property | Event | Description | | ------------- | ----------------- | ------------ | | userState | wcs-idle:change | "active" | "idle", or null before start(). | | screenState | wcs-idle:change | "locked" | "unlocked", or null before start(). | | active | wcs-idle:change | true when userState === "active". | | error | wcs-idle:error | The last requestPermission()/start() failure, or null. | | errorInfo | wcs-idle:error-info-changed | Serializable failure taxonomy WcsIoErrorInfo \| null (stable code / phase / recoverable) for that same failure, or null when clear. Additive — the error shape is unchanged. |

Commands

| Command | Async | Description | | ------------------- | ----- | ------------ | | requestPermission | yes | Wraps the static, gesture-gated IdleDetector.requestPermission(). Must be called from within a real user gesture handler. Never-throw: a rejection resolves to "denied". | | start | yes | Begin an idle-detection session (threshold in ms, minimum 60000). Superseded by a later start()/stop(). | | stop | no | Stop the current session. Safe to call when not started. |

Attributes / Inputs

| Attribute | Type | Default | Description | | ----------- | ------ | ------- | ------------ | | threshold | number | 60000 | Minimum idle time (ms) before userState becomes "idle". Not validated — an out-of-range value is left to the browser's own rejection. |

CSS styling with :state()

<wcs-idle> reflects two boolean output states onto its ElementInternals CustomStateSet, so you can style it directly from CSS with the :state() pseudo-class — no data-wcs binding or extra class toggling required.

| State | On when | |-------|---------| | active | wcs-idle:change fires with detail.userState === "active" (cleared when it becomes "idle") | | error | wcs-idle:error fires with a non-null detail (cleared on null) |

screenState has no derived boolean getter (§4.2), so it is not reflected in v1.

wcs-idle:state(active) ~ .presence-dot { background: green; }
wcs-idle:not(:state(active)) ~ .presence-dot { background: gray; }

form:has(wcs-idle:state(error)) .banner { display: block; }

Unlike attributes or classes, :state() cannot be written from outside the element, so there is no risk of confusing this output state with an input.

Browser support (:state(x) syntax): Chrome/Edge 125+, Safari 17.4+, Firefox 126+. In older browsers the states are simply never set — :state() selectors never match, but <wcs-idle> itself keeps working normally (graceful degradation, never-throw).

SSR: :state() cannot be serialized into HTML, so server-rendered markup never carries these states on first paint (@wcstack/server is unaffected). If you need to style the pre-hydration gap, pair your rule with wcs-idle:not(:defined) instead.

Debugging

Custom states are invisible in DevTools' Elements panel and attachInternals() cannot be called twice, so there is no console way to inspect them directly. Two debug-only aids are provided for that:

  • el.debugStates — a snapshot array of the currently-on state names (e.g. ["active"]). It is not part of wc-bindable (not a bind target) and its shape is not a guaranteed contract — use it for debugging only.

  • The debug-states attribute (opt-in, default off) mirrors state changes onto data-wcs-state-active / data-wcs-state-error attributes on the element, so the Elements panel highlights them as they toggle:

    <wcs-idle debug-states></wcs-idle>

Write your CSS against :state(), not data-wcs-state-*. The mirrored attributes exist purely to make state changes visible while debugging with DevTools open; they are not a supported styling hook.

Notes & limitations

  • Does not auto-start on connect. See "Why this exists" above.
  • Does not duplicate permission state. Compose with <wcs-permission name="idle-detection">.
  • Chromium-only, and secure-context-only. Firefox and Safari do not implement IdleDetector at all. Even in Chromium, IdleDetector is a [SecureContext]-only interface, so over plain http:// (other than localhost) window.IdleDetector itself is undefined — same as an unsupported browser, this falls into the unsupported (via error) path.
  • Permissions-Policy gate. Idle detection is governed by the idle-detection Permissions-Policy directive (default allowlist: self). Using <wcs-idle> inside a cross-origin <iframe> requires allow="idle-detection" on that <iframe> element — otherwise requestPermission()/start() fail the same way as an unsupported browser.
  • stop()/disconnect does not reset userState/screenState/active. They keep their last observed value until the next successful start() — the same "retain the last reading" behavior as the Generic Sensor family (<wcs-gyroscope> et al.).
  • errorInfo taxonomy. An additive bindable output (wcs-idle:error-info-changed) that classifies the same requestPermission()/start() failure surfaced on error into a serializable WcsIoErrorInfo with a stable code / phase / recoverable, without changing the error shape. A missing IdleDetector (unsupported browser, or a non-secure context where window.IdleDetector is undefined) is capability-missing (phase probe); a NotAllowedError (permission denied or called outside a user gesture — the two are deliberately not distinguished) is not-allowed (phase start); any other failure (a raw throw, a TypeError from a bad threshold, a nullish rejection) is idle-error (phase execute). All are recoverable: false. errorInfo transitions exactly when error does (cleared to null alongside it). The shared WcsIoErrorInfo type and the WCS_IDLE_ERROR_CODE constants are exported.

Headless usage (IdleCore)

import { IdleCore } from "@wcstack/idle";

const core = new IdleCore();
core.addEventListener("wcs-idle:change", (e) => {
  console.log((e as CustomEvent).detail); // { userState, screenState }
});

// from within a real user gesture handler:
const result = await core.requestPermission();
if (result === "granted") await core.start(60000);

// later:
core.dispose();

License

MIT