Sign In

@wildride/pad-kit

Package Overview
Dependencies
Maintainers
2
Versions
1
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@wildride/pad-kit

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

latest
Source
npmnpm
Version
0.4.0
Version published
Weekly downloads
16
-78.95%
Maintainers
2
Weekly downloads
 
Created
Source

@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

SubpathWhat
./schemaSCHEMA_VERSION, the semantic BUTTONS/STICK_AXES/TRIGGERS vocabulary, CONTROLLER_TYPE, the ARCHETYPES step lists, platformBucket(), profileId()
./applyapplyProfile() (live pad → semantic names), profileToRemap() (semantic → wire flags), hatDirections, triggerValue, stickValue, BUTTON_FLAGS
./capturerestSnapshot, detectInput, selectForStep, resolveCapture, buildProfile, verifyProfile, parseVendorProduct, isXInputWrapper — the registration wizard's engine, DOM-free
./resolvercreateResolver({ fetchProfile, saveProfile }) plus the stateless gates (gateProfile, REJECT, mergeResolvedLayers, resolveProfileFrom, buildRemapTable)
./glyphsGLYPH_SETS, resolveGlyph(), diagramLabels(), sniffGlyphSet(), NAV_TO_SEMANTIC, PS_CLASS, ARROW_POINTS
./padShapesPHYSICAL_SHAPES (per archetype), CONSOLE_SHAPES (per console family), PAD_SCALE, physicalShape(), consoleShape(), normalizePadLayout()
./padGeometryPure editor math over a shape: hitTest, moveControl, setControlField, snap, coverageDiff
./lrudpickCandidate() / isDegenerate() — geometric spatial navigation (BBC lrud-spatial / W3C spatial-navigation model). DOM-free: callers pass DOMRect-likes
./componentsPadDiagram, GlyphChip, PadButtonGlyph, providePadGlyphContext
./tokens.cssDefault values for the CSS custom properties those components read
./tools/pad-geometry-check.mjsThe 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-kubeWhy
consoleFit.js + consoleFit.jsonThe 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.jsSame 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
REJECTRefused because
SCHEMAThe profile's schemaVersion is newer than this build's
IDENTITYNo vendor/product in the pad id, or it doesn't match
MAPPING_STATEThe browser normalized this pad on one platform and not the other. Never adapt across that line
REPORT_SHAPEButton/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

  • game-kube (web-ui/) — the origin. Registration wizard, console-fit / remap panels, in-stream pad mapping, FR-41 controller navigation.
  • (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.

FAQs

Package last updated on 09 Aug 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts