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

@signalsafe/simulator-device

v0.3.3

Published

Reusable full phone/device simulator UI composition on top of @signalsafe/simulator-react.

Readme

@signalsafe/simulator-device

Reusable full phone/device simulator UI for hosts building on @signalsafe/simulator-react and @signalsafe/simulator-core.

This package provides the complete reusable device UI composition:

  • SimulatorDevice — JSON-driven entry point (value={simulatorJson})
  • SimulatorPhoneDevice — shell + bottom nav + runtime + default incoming-call history (state/dispatch API)
  • SimulatorPhoneShell / SimulatorPhoneNav — lower-level primitives for custom layouts
  • Nav, screen-class, host-mode, and incoming-call history helpers

It does not include routing, API calls, template management, auth, or CSS. Hosts style the semantic classes from this package and @signalsafe/simulator-react in their own stylesheets.

Install

yarn add @signalsafe/simulator-device @signalsafe/simulator-react react react-dom

Peer dependencies: react, react-dom.

Usage

JSON-driven rendering (recommended)

Pass stored simulator JSON directly — the same full-device shape used in database/API simulator_json (entry_point, device, contacts, phone, email, messages, internet, home).

import { SimulatorDevice } from '@signalsafe/simulator-device';
import type { SimulatorDevicePayload } from '@signalsafe/simulator-device';

function Preview({ simulatorJson }: { simulatorJson: SimulatorDevicePayload }) {
  return <SimulatorDevice value={simulatorJson} />;
}

DeliveryPlus-style flow:

  1. Load simulator_json from the database/API.
  2. Render <SimulatorDevice value={json} />.
  3. Wire onChange to persist contact edits back to the database (see editable contact detail below).

Future desktop simulator support can use the same entry point with a discriminated JSON shape; unsupported values render renderUnsupported or a safe built-in fallback.

Runtime passthrough (preview/run without manual session)

SimulatorDevice owns session state from value. Pass runtime props at the top level — they forward to SimulatorPhoneDeviceSimulatorWithSession. You do not need state / dispatch.

| Prop | Purpose | | --- | --- | | onSimulatorEvent | Structured interaction events (clicks, screen views, etc.) | | developerTools | Developer/QA panel configuration | | developerToolsTimelineEntries | Timeline entries for the developer panel | | developerToolsRuntimeIssues | Runtime lint/warning issues for the developer panel | | initialContactsSearch | Seed contacts search from deep-link query params | | exitLink, exitTo, exitLabel | Exit chrome | | compact | Embedded preview layout | | renderChoice, renderFeedback, renderContactsOverlay | Custom render slots |

Phone shell options (phone.className, phone.contactDetail, etc.) remain under phone.

import { SimulatorDevice } from '@signalsafe/simulator-device';
import type { SimulatorDevicePayload } from '@signalsafe/simulator-device';

function RunPreview({ simulatorJson }: { simulatorJson: SimulatorDevicePayload }) {
  return (
    <SimulatorDevice
      value={simulatorJson}
      onSimulatorEvent={(event) => console.log(event)}
      developerTools={{ enabled: true, sections: { timeline: true } }}
      exitTo="/courses"
      exitLabel="Exit"
      initialContactsSearch="helpdesk"
      phone={{ contactDetail: { mode: 'editable' } }}
    />
  );
}

Editable contact detail (package-owned form)

Use phone.contactDetail for the generic package form. The host owns persistence via onChange and optional onSave / onDelete callbacks — the package does not call APIs or databases.

When onChange is provided, contact save/delete updates value.contacts immutably and calls onChange(nextValue) before host onSave / onDelete callbacks. If onChange is omitted, callbacks still fire but no JSON persistence occurs inside the package.

Omit contactDetail to keep the built-in @signalsafe/simulator-react contact detail view. Use renderContactDetail as a full escape hatch when you need completely custom UI (it takes precedence over contactDetail when both are set).

import { SimulatorDevice } from '@signalsafe/simulator-device';
import type { SimulatorDevicePayload } from '@signalsafe/simulator-device';

function Preview({
  simulatorJson,
  setSimulatorJson,
}: {
  simulatorJson: SimulatorDevicePayload;
  setSimulatorJson: (next: SimulatorDevicePayload) => void;
}) {
  return (
    <SimulatorDevice
      value={simulatorJson}
      onChange={setSimulatorJson}
      phone={{
        contactDetail: {
          mode: 'editable',
          onSave: (contact) => console.log('saved', contact),
          onDelete: (contact) => console.log('deleted', contact),
        },
      }}
    />
  );
}

Custom fields via renderExtraFields

import { SimulatorDevice } from '@signalsafe/simulator-device';
import type {
  SimulatorDevicePayload,
  SimulatorPhoneContactDetailContext,
  SimulatorPhoneContactDetailValues,
} from '@signalsafe/simulator-device';

function ExtraFields({
  contact,
  updateContact,
}: {
  contact: SimulatorPhoneContactDetailValues;
  updateContact: (patch: Partial<SimulatorPhoneContactDetailValues>) => void;
  context: SimulatorPhoneContactDetailContext;
}) {
  return (
    <label>
      Notes
      <input
        value={contact.displayName}
        onChange={(event) => updateContact({ displayName: event.target.value })}
      />
    </label>
  );
}

function Preview({ value, onChange }: { value: SimulatorDevicePayload; onChange: (v: SimulatorDevicePayload) => void }) {
  return (
    <SimulatorDevice
      value={value}
      onChange={onChange}
      phone={{
        contactDetail: {
          mode: 'editable',
          renderExtraFields: (props) => <ExtraFields {...props} />,
        },
      }}
    />
  );
}

Fully custom contact detail (renderContactDetail)

import { SimulatorDevice } from '@signalsafe/simulator-device';
import type { SimulatorDevicePayload } from '@signalsafe/simulator-device';

function Preview({
  simulatorJson,
  onSave,
}: {
  simulatorJson: SimulatorDevicePayload;
  onSave: (next: SimulatorDevicePayload) => void;
}) {
  return (
    <SimulatorDevice
      value={simulatorJson}
      onChange={onSave}
      phone={{
        renderContactDetail: ({ contact, onBack }) => (
          <div>
            <button type="button" onClick={onBack}>Back</button>
            <h1>{contact.displayName}</h1>
          </div>
        ),
      }}
    />
  );
}

Advanced phone-only state/dispatch usage

When you already manage session state (preview hosts, timeline tooling, deep links), use SimulatorPhoneDevice directly with getInitialSessionState and simulatorSessionReducerWithLogging from @signalsafe/simulator-react.

Basic full phone UI

SimulatorPhoneDevice is the default entry point for a complete phone simulator preview or embed. No CSS framework is included — target semantic classes in your host CSS.

import {
  SimulatorPhoneDevice,
  SIMULATOR_DEVICE_CLASS_NAMES,
  SIMULATOR_DEVICE_SCREEN_CLASS_NAMES,
} from '@signalsafe/simulator-device';
import {
  getInitialSessionState,
  type SimulatorDispatchAction,
  type SimulatorSessionState,
} from '@signalsafe/simulator-react';

/** Host CSS targets `.simulator-device-shell`, `.simulator-device-nav`, etc. */
void SIMULATOR_DEVICE_CLASS_NAMES;
void SIMULATOR_DEVICE_SCREEN_CLASS_NAMES;

function PhonePreview({
  state,
  dispatch,
}: {
  state: SimulatorSessionState;
  dispatch: (action: SimulatorDispatchAction) => void;
}) {
  return <SimulatorPhoneDevice state={state} dispatch={dispatch} />;
}

const state = getInitialSessionState(templatePayload);

Custom host-owned contact detail

Option A — package generic form (contactDetail):

import { SimulatorPhoneDevice } from '@signalsafe/simulator-device';
import type { SimulatorDispatchAction, SimulatorSessionState } from '@signalsafe/simulator-react';

function PhonePreview({
  state,
  dispatch,
}: {
  state: SimulatorSessionState;
  dispatch: (action: SimulatorDispatchAction) => void;
}) {
  return (
    <SimulatorPhoneDevice
      state={state}
      dispatch={dispatch}
      contactDetail={{
        mode: 'editable',
        onSave: (contact) => console.log('save', contact),
        onDelete: (contact) => console.log('delete', contact),
      }}
    />
  );
}

Option B — full escape hatch (renderContactDetail):

Pass renderContactDetail to replace the contact detail view with your own UI. The device shell applies simulator-phone-shell--screen-phone-contact-detail while the overlay is active. When both renderContactDetail and contactDetail are set, renderContactDetail wins.

import { SimulatorPhoneDevice } from '@signalsafe/simulator-device';
import type { SimulatorDispatchAction, SimulatorSessionState } from '@signalsafe/simulator-react';

function PhonePreview({
  state,
  dispatch,
}: {
  state: SimulatorSessionState;
  dispatch: (action: SimulatorDispatchAction) => void;
}) {
  return (
    <SimulatorPhoneDevice
      state={state}
      dispatch={dispatch}
      className="my-simulator-root"
      screenClassNames={['my-simulator-root--embedded']}
      renderContactDetail={({ contact, onBack }) => (
        <div className="my-contact-detail">
          <button type="button" onClick={onBack}>
            Back
          </button>
          <h1>{contact.displayName}</h1>
          <p>{contact.number}</p>
        </div>
      )}
    />
  );
}

onBack clears the host selection and returns to the simulator runtime.

Manual shell composition

For lower-level control, compose primitives directly:

import {
  SimulatorPhoneShell,
  SimulatorPhoneNav,
  shouldHideHostPhoneNav,
  SIMULATOR_DEVICE_CLASS_NAMES,
} from '@signalsafe/simulator-device';
import { SimulatorWithSession, type SimulatorSessionState } from '@signalsafe/simulator-react';

void SIMULATOR_DEVICE_CLASS_NAMES;

function DevicePreview({ state, dispatch }: { state: SimulatorSessionState; dispatch: DispatchFn }) {
  return (
    <SimulatorPhoneShell
      useHostNav={!shouldHideHostPhoneNav(state)}
      nav={shouldHideHostPhoneNav(state) ? null : <SimulatorPhoneNav state={state} dispatch={dispatch} />}
    >
      <SimulatorWithSession state={state} dispatch={dispatch} />
    </SimulatorPhoneShell>
  );
}

Public API

| Export | Description | | --- | --- | | SimulatorDevice | JSON-driven entry point; owns session from SimulatorDevicePayload | | SimulatorDeviceProps | Props for JSON-driven device rendering | | SimulatorDeviceRuntimePassthroughProps | Runtime props forwarded to SimulatorWithSession (events, dev tools, deep-link search) | | SimulatorDevicePhoneOptions | Optional phone overrides passed through to SimulatorPhoneDevice | | resolveSimulatorDeviceKind | Classify supported vs future/unsupported JSON shapes | | SimulatorPhoneDevice | Composed phone shell + nav + runtime + optional host contact detail | | SimulatorPhoneDeviceProps | Props for the composed phone device | | SimulatorPhoneDeviceContactDetailRenderProps | renderContactDetail callback context | | SimulatorPhoneContactDetailForm | Generic contact detail form (displayName, number, email) | | SimulatorPhoneContactDetailFormProps | Props for the contact detail form | | SimulatorPhoneContactDetailValues | Contact value shape for save/delete callbacks | | SimulatorPhoneDeviceContactDetailOptions | contactDetail options for SimulatorPhoneDevice / SimulatorDevice | | SimulatorPhoneContactDetailContext | Save/delete callback context (state, dispatch, originalContact) | | contactSnapshotFromSessionContact | Map session contact → contact detail values | | splitContactDisplayName | Split display name into first/last parts | | patchContactInDevicePayload | Immutably update one contact in SimulatorDevicePayload | | removeContactFromDevicePayload | Immutably remove one contact from SimulatorDevicePayload | | SimulatorPhoneShell | Presentational device shell | | SimulatorPhoneNav | Session-driven bottom navigation | | SimulatorPhoneNavItem | Single nav button (for custom layouts) | | resolveSimulatorPhoneNav | Derive primary/secondary/hidden nav model from session state | | dispatchSimulatorPhoneNavItem | Map nav item clicks to simulator dispatch actions | | shouldHideHostPhoneNav | Hide nav on thread/email detail screens | | SIMULATOR_PRIMARY_NAV_ITEMS | Primary tab metadata | | SIMULATOR_DEVICE_CLASS_NAMES | BEM class constants for host styling | | SIMULATOR_DEVICE_SCREEN_CLASS_NAMES | Shell screen modifier class strings for host CSS | | resolveSimulatorPhoneShellScreenClasses | Derive screenClassNames for SimulatorPhoneShell from session view | | resolveSimulatorPhoneShellHostMode | Host overlay mode (e.g. phone contact edit) from session + selected contact id | | SimulatorPhoneShellHostMode | Host overlay mode type | | SimulatorPhoneNavModel, SimulatorPhoneNavItemModel | Nav model types | | SimulatorPhoneIncomingCallHistory | Previous-calls table for incoming-call screen | | renderPhoneIncomingCallHistoryExtra | Drop-in renderIncomingCallExtra slot for @signalsafe/simulator-react | | normalizePhoneNumber, resolveIncomingCallCaller, getRecentCallsForCaller | Incoming-call history helpers | | PhoneIncomingCallHistoryRow, PhoneIncomingCallCaller | History row and caller types |

Types for session state and dispatch actions come from @signalsafe/simulator-react.

Incoming-call previous calls

SimulatorPhoneDevice passes renderPhoneIncomingCallHistoryExtra to SimulatorWithSession by default. Override with renderIncomingCallExtra when needed.

The history table uses the semantic class simulator-phone__incoming-call-history from @signalsafe/simulator-react. Host apps style that class in their own CSS — this package does not ship stylesheets.

When no matching recent calls exist for the active caller, the slot returns null (no empty wrappers).

Development

yarn install
yarn typecheck
yarn test
yarn build
yarn smoke:package

License

MIT