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

@wildride/pad-kit

v0.4.0

Published

Dependency-free gamepad kernel: profile schema, capture, decode, resolution gates, glyph sets, controller diagram geometry — plus optional Vue presentation components.

Downloads

22

Readme

@wildride/pad-kit

The gamepad kernel, extracted from game-kube.

Everything a browser app needs to turn navigator.getGamepads() into a mapping it can trust: the stored profile schema, the capture engine that builds one, the decode that applies it, the resolution gates that refuse to apply a wrong one, the glyph sets that label a button the way the pad in the user's hands does, and the diagram geometry that draws it.

The kernel is dependency-free — no framework, no HTTP client, no DOM. A synthetic { id, mapping, buttons: [{pressed, value}], axes: [] } object literal is a valid "Gamepad" to every function in it. Three optional Vue 3 components ship alongside for the presentation layer; vue is an optional peer dependency, so a pure node consumer (a server-side profile validator, the geometry checker) never resolves it.


Install

npm install @wildride/pad-kit
import { archetypeSteps, SCHEMA_VERSION } from '@wildride/pad-kit/schema';
import { applyProfile, profileToRemap }   from '@wildride/pad-kit/apply';
import { restSnapshot, detectInput }      from '@wildride/pad-kit/capture';
import { createResolver }                 from '@wildride/pad-kit/resolver';
import { resolveGlyph }                   from '@wildride/pad-kit/glyphs';
import { CONSOLE_SHAPES }                 from '@wildride/pad-kit/padShapes';
import { hitTest, moveControl }           from '@wildride/pad-kit/padGeometry';
import { pickCandidate }                  from '@wildride/pad-kit/lrud';

import { PadDiagram, PadButtonGlyph, GlyphChip, providePadGlyphContext }
  from '@wildride/pad-kit/components';
import '@wildride/pad-kit/tokens.css';

The root entry (import * as padKit from '@wildride/pad-kit') hands back one namespace per module rather than a flat barrel, because capture.js and resolver.js deliberately each carry their own parseVendorProduct / isXInputWrapper and a flat re-export would make those ambiguous.


What's in it

| Subpath | What | |---|---| | ./schema | SCHEMA_VERSION, the semantic BUTTONS/STICK_AXES/TRIGGERS vocabulary, CONTROLLER_TYPE, the ARCHETYPES step lists, platformBucket(), profileId() | | ./apply | applyProfile() (live pad → semantic names), profileToRemap() (semantic → wire flags), hatDirections, triggerValue, stickValue, BUTTON_FLAGS | | ./capture | restSnapshot, detectInput, selectForStep, resolveCapture, buildProfile, verifyProfile, parseVendorProduct, isXInputWrapper — the registration wizard's engine, DOM-free | | ./resolver | createResolver({ fetchProfile, saveProfile }) plus the stateless gates (gateProfile, REJECT, mergeResolvedLayers, resolveProfileFrom, buildRemapTable) | | ./glyphs | GLYPH_SETS, resolveGlyph(), diagramLabels(), sniffGlyphSet(), NAV_TO_SEMANTIC, PS_CLASS, ARROW_POINTS | | ./padShapes | PHYSICAL_SHAPES (per archetype), CONSOLE_SHAPES (per console family), PAD_SCALE, physicalShape(), consoleShape(), normalizePadLayout() | | ./padGeometry | Pure editor math over a shape: hitTest, moveControl, setControlField, snap, coverageDiff | | ./lrud | pickCandidate() / isDegenerate() — geometric spatial navigation (BBC lrud-spatial / W3C spatial-navigation model). DOM-free: callers pass DOMRect-likes | | ./components | PadDiagram, GlyphChip, PadButtonGlyph, providePadGlyphContext | | ./tokens.css | Default values for the CSS custom properties those components read | | ./tools/pad-geometry-check.mjs | The mechanical geometry checker, as a CLI and as importable pure rules | | ./testkit/* | padModel.mjs / padShim.mjs — a fake gamepad and the Playwright init-script builder that installs it in a page |

What's deliberately NOT in it

| Stayed in game-kube | Why | |---|---| | consoleFit.js + consoleFit.json | The libretro/RetroPad fit model: which console family a core presents, how a physical pad trades onto it, which control is unreachable. That is emulator-frontend domain knowledge, not gamepad knowledge — a non-emulator consumer of this kit would carry a 30KB table it can never use. It reads padShapes and glyphs from here; the dependency points the right way. | | consoleRemap.js | Same reason — it is the remap layer over the fit model. | | internal/gamepaddb (Go) | Server-side profile storage. The kit is the browser half. | | The wizard/panel app surfaces (GamepadCaptureStep.vue, ConsoleFitPanel.vue, PadLayoutPanel.vue, …) | Product UI: routing, stores, notifications, the app's own layout. They compose the kit; they are not part of it. |

PadDiagram / GlyphChip / PadButtonGlyph came across because they are pure presentation over kit data — they read no store, fetch nothing, and route nowhere.


The resolver's injection API

resolver.js is the one module that used to reach for the host app. In game-kube it imported apiRequest/getAuthToken from the app's api client and the URL from the app's endpoint table — two touchpoints, both about where the profile table comes from. The kit takes them as injected functions, so the gates ship with no transport opinion:

import { createResolver } from '@wildride/pad-kit/resolver';

export const resolver = createResolver({
  // () => Promise<body|null>. Resolve the raw GET /gamepads/resolved body:
  //   { schemaVersion, profiles: [...], library: [...] }
  // …or NULL for "there is no table for this caller".
  fetchProfile: () => {
    if (!getAuthToken()) return null;        // guest / logged out — see below
    return apiRequest('/gamepads/resolved', { method: 'GET' });
  },
  // Optional. Never called on the resolve path.
  saveProfile: (profile) => apiRequest('/gamepads', { method: 'POST', body: profile }),
});

null is a real answer, not an error. A caller with no credential gets "no table", and the pad still works through the browser's own standard mapping. This is load-bearing: an api client that treats a 401 as "your session ended" will clear the token and navigate away, and the pad database must never be able to take down the page that asked for it. A null answer is not cached — it is a statement about the caller's credential, not about the table.

createResolver returns the full surface: resolveProfile, remapTable, loadProfiles, loadRemapTable, setProfiles, clearProfiles, getProfiles, profilesLoaded, saveProfile, plus the stateless REJECT, gateProfile, parseVendorProduct, liveMappingState, isXInputWrapper, mergeResolvedLayers. Create one instance per app, at module scope — the profile cache is shared state on purpose (the wizard seeds it with setProfiles straight after a save; the stream path and the nav path both read it back).

The gates are the point

A stored profile is a set of raw hardware indices. Applied to the wrong pad state or the wrong report shape it does not degrade gracefully — it silently means something else, and the user cannot tell it is wrong. So gateProfile refuses rather than adapts, and a refusal drops the pad to the next tier:

user override > global verified > global unverified > your seed > browser standard

| REJECT | Refused because | |---|---| | SCHEMA | The profile's schemaVersion is newer than this build's | | IDENTITY | No vendor/product in the pad id, or it doesn't match | | MAPPING_STATE | The browser normalized this pad on one platform and not the other. Never adapt across that line | | REPORT_SHAPE | Button/axis counts differ. Equality, not tolerance — one extra button is not the pad that was captured |


The token layer

The Vue components style themselves entirely from CSS custom properties. Import the kit's defaults before your own token sheet so the cascade lets yours win:

import '@wildride/pad-kit/tokens.css';   // neutral-dark defaults
import './styles/tokens.css';            // yours — same :root specificity, later wins

Values are space-separated R G B integers, not rgb(...) strings, because the components read them as rgb(var(--token) / 0.3). A hex value breaks every rule that uses an alpha modifier.

The list is measured from the components' style blocks, not aspirational:

  • PadDiagram--surface-sunken --surface-overlay --edge --edge-strong --ink-secondary --ink-muted --color-brand --color-danger --color-warn --color-action
  • GlyphChip / PadButtonGlyph--surface-overlay --edge-strong --ink --color-brand --color-live --color-danger --color-warn --radius-lg

A host that already defines these names does not need to import tokens.css at all.

Glyph context

PadButtonGlyph learns which pad is in the user's hands through provide/inject, not through a store:

providePadGlyphContext({
  glyphSet: computed(() => ui.state.glyphSet),           // 'xbox' | 'playstation' | …
  glyphOverrides: computed(() => ui.state.glyphOverrides), // the profile's display.labels
});

Both fields take a ref/computed or a plain value. Nothing is required: with no provider at all every chip resolves through the generic set. A chip must degrade, never throw.


Versioning

SCHEMA_VERSION is the contract. Stored profiles are indices into real hardware; a consumer that reads a profile written by a newer schema than it understands is the exact failure the gates exist to prevent.

  • A SCHEMA_VERSION bump is a MAJOR release. No exceptions, however additive the change looks.
  • Adding a glyph set, a pad shape, an archetype, or a component prop: MINOR.
  • Changing a PAD_SCALE radius, a shape's geometry, or a BUTTON_FLAGS value: MAJOR (the wire flags are pinned in lockstep with a stream core; the radii are pinned by the geometry checker).
  • Consumers pin exactly. game-kube's web-ui/package.json carries an exact version, not a range.

Consumers

  1. game-kube (web-ui/) — the origin. Registration wizard, console-fit / remap panels, in-stream pad mapping, FR-41 controller navigation.
  2. (second consumer — this repo exists because the kernel outgrew one app.)

Development

npm install
npm test                 # vitest — the ported specs plus the kit's component spec
npm run check:geometry   # node tools/pad-geometry-check.mjs — every built-in shape

tools/pad-geometry-check.mjs is both a CLI and a set of importable pure rules. test/padGeometryCheck.spec.js hands it the same tables the bundler resolves, so npm test runs the geometry rules over the real built-ins — the checker cannot rot by nobody remembering to type the command.

node tools/pad-geometry-check.mjs                       # every built-in
node tools/pad-geometry-check.mjs layout.json           # a spec.padLayout document
node tools/pad-geometry-check.mjs layout.json --family ps2

Authoring or re-measuring a shape is .claude/skills/pad-geometry (/pad-geometry): it grids a reference photograph, reads control centres off it mechanically, rasterizes the result the way PadDiagram.vue draws it, and runs the checker before showing anyone a shape. The photographs live in a gitignored gamepad_refrence_images/ and are a reference, never a source — no outline is traced, no image or crop is embedded, and what lands in a diff is integers. Note that a CONSOLE.* control-id set is a cross-repo contract: consoleFit.js stayed in game-kube, so that is where the coverage-both-ways pin lives, and a renamed or added id passes here and fails there.

Phase 0 note

During the extraction, the specs under test/ also still live in game-kube (web-ui/src/gamepad/__tests__/, web-ui/src/composables/__tests__/lrud.spec.js), running against re-export shims. That duplication is deliberate and temporary: it is the equivalence proof that the extraction changed nothing. Two of them differ on purpose — game-kube's apply.spec.js and resolver.spec.js additionally pin against its vendored stream core (StreamControllerButton, the core's own parseVendorProduct), which the kit has no copy of; the kit pins the same values as literals and cross-checks resolver's parser against capture's.