andrew b2e42264ae Add .gitignore and remove checked-in nebula modules
Add a .gitignore and remove numerous @nebulaproject packages and their asset files from node_modules to stop committing dependency artifacts (glyphs, binaries, src/dist files). Also update packages/core/package.json metadata and refresh lockfiles.
2026-01-31 23:19:19 +13:00
2026-01-31 22:14:29 +13:00

Nebula Core

Nebula Core is a controller-first, SteamOS-friendly foundation for building living-room and handheld applications. It focuses on predictable navigation, input abstraction, and UI patterns that work without a mouse or keyboard.

This repository is a monorepo of small, independent packages. Each package solves one problem well and stays UI-agnostic where possible.

Goals

  • Controller and gamepad first
  • Steam Deck and SteamOS as a primary target
  • Large, readable UI defaults
  • Minimal dependencies and no framework lock-in
  • ES Modules, Node 18+

Packages

  • @nebula/core-utils — shared helpers and small utilities
  • @nebula/core-input — unified, action-based input abstraction
  • @nebula/core-navigation — focus management and spatial navigation primitives
  • @nebula/core-theme — tokens for color, spacing, typography, and motion
  • @nebula/core-glyphs — controller glyph mappings and lookup helpers
  • @nebula/core-ui — optional, minimal UI primitives for controller-first apps

Installation

Install only the packages you need:

npm install @nebula/core-input @nebula/core-navigation @nebula/core-ui @nebula/core-theme @nebula/core-glyphs @nebula/core-utils

Quick start

import { createActionMapper } from "@nebula/core-input";
import { pickBestCandidate } from "@nebula/core-navigation";
import { getFocusableAttributes, focusRing, hitTarget } from "@nebula/core-ui";
import { createTheme } from "@nebula/core-theme";
import { getGlyph } from "@nebula/core-glyphs";

const theme = createTheme({ colors: { accent: "#44d3ff" } });
const glyph = getGlyph("steam-deck", "confirm");

const mapper = createActionMapper({
	bindings: {
		confirm: [{ source: "gamepad", control: "a" }],
		back: [{ source: "gamepad", control: "b" }]
	}
});

mapper.onAction((update) => {
	if (update.action === "confirm" && update.active) {
		console.log("Confirm pressed", glyph, theme.colors.accent);
	}
});

const current = { id: "settings", x: 100, y: 100, width: 180, height: 80 };
const candidates = [
	{ id: "play", x: 100, y: 10, width: 180, height: 80 },
	{ id: "help", x: 320, y: 100, width: 180, height: 80 }
];

const next = pickBestCandidate(current, candidates, "up");

const focusableProps = getFocusableAttributes({ role: "button", focusKey: "play" });
const styles = {
	minHeight: hitTarget.minHeight,
	minWidth: hitTarget.minWidth,
	outline: `${focusRing.outlineWidth}px solid ${focusRing.outlineColor}`,
	outlineOffset: focusRing.outlineOffset
};

Architecture overview

Nebula Core is designed as small, composable packages:

  1. Input: Normalize device events into actions.
  2. Navigation: Choose the next focus target with spatial heuristics.
  3. UI: Provide hit-target and focus-ring defaults for controller use.
  4. Theme: Provide tokens tuned for couch and handheld readability.
  5. Glyphs: Map logical actions to controller glyphs and assets.
  6. Utils: Small reusable helpers used by other packages.

Package documentation

@nebula/core-utils

Small, dependency-free helpers. No DOM or platform assumptions.

API

clamp(value, min, max)

Clamp a number between a minimum and maximum.

lerp(from, to, t)

Linear interpolation between two numbers.

roundTo(value, decimals)

Round a number to a specific decimal precision.

createEmitter()

Create a tiny event emitter with on, emit, and clear.

Example

import { clamp, createEmitter } from "@nebula/core-utils";

const value = clamp(12, 0, 10);
const events = createEmitter();
const off = events.on((payload) => console.log(payload));
events.emit({ action: "confirm" });
off();

@nebula/core-input

Action-based input abstraction for controller-first apps. Convert raw device events into app-level actions.

Key concepts

  • InputEvent: Normalized input from any device (gamepad, keyboard, mouse, touch).
  • InputBinding: Maps a physical control to an action name.
  • ActionUpdate: Emitted update with action, value, and active state.

API

createActionMapper(options)

Creates an action mapper instance.

Options

  • bindings: Record<string, InputBinding[]>
  • deadzone?: number (default: 0.2)

Returns

  • mapEvent(event): map a single InputEvent to action updates.
  • getActionState(action): get current value and active.
  • onAction(listener): subscribe to action updates.
  • reset(): clear all action states.

Example

import { createActionMapper } from "@nebula/core-input";

const mapper = createActionMapper({
	bindings: {
		confirm: [{ source: "gamepad", control: "a" }],
		back: [{ source: "gamepad", control: "b" }]
	}
});

mapper.onAction((update) => {
	if (update.action === "confirm" && update.active) {
		console.log("Confirm action");
	}
});

mapper.mapEvent({
	source: "gamepad",
	control: "a",
	type: "pressed",
	value: 1
});

InputEvent shape

type InputEvent = {
	source: "gamepad" | "keyboard" | "mouse" | "touch" | "unknown";
	control: string;
	type: "pressed" | "released" | "moved" | "axis";
	value: number; // 0..1 for digital, -1..1 for analog
	timestamp?: number;
};

@nebula/core-navigation

Focus management and spatial navigation primitives for controller-first UIs.

API

getRectCenter(rect)

Return the center point of a rectangle.

getDirectionalCandidates(current, candidates, direction)

Filter candidates in the requested direction (up, down, left, right).

pickBestCandidate(current, candidates, direction)

Pick the nearest candidate using a weighted distance score.

Example

import { pickBestCandidate } from "@nebula/core-navigation";

const current = { id: "settings", x: 100, y: 100, width: 180, height: 80 };
const candidates = [
	{ id: "play", x: 100, y: 10, width: 180, height: 80 },
	{ id: "help", x: 320, y: 100, width: 180, height: 80 }
];

const next = pickBestCandidate(current, candidates, "up");

@nebula/core-theme

Theme tokens tuned for readable, couch-friendly UI. Exports plain objects and helpers only.

API

baseTheme

Default theme tokens: colors, spacing, radius, typography, motion.

createTheme(overrides)

Merge overrides with the base theme. Returns a full theme object.

Example

import { baseTheme, createTheme } from "@nebula/core-theme";

const theme = createTheme({ colors: { accent: "#44d3ff" } });
console.log(baseTheme.typography.display, theme.colors.accent);

@nebula/core-glyphs

Controller glyph mappings and lookup helpers. Data-only, no rendering logic.

API

glyphMap

Mapping of logical actions to glyph labels per controller.

glyphAssetMap

Mapping of logical actions to asset paths per controller.

getGlyph(controller, action)

Return the glyph label for a controller and action.

getGlyphAssetPath(controller, action)

Return the asset path for a controller and action.

Example

import { getGlyph, getGlyphAssetPath } from "@nebula/core-glyphs";

const glyph = getGlyph("steam-deck", "confirm");
const asset = getGlyphAssetPath("steam-deck", "confirm");

@nebula/core-ui

Minimal UI helpers for controller-first applications. No framework lock-in.

API

hitTarget

Recommended hit target sizes (pixels at 100% scale).

focusRing

Focus ring style tokens for UI libraries or CSS-in-JS.

getFocusableAttributes(options)

Build a minimal set of focusable attributes for controller navigation.

Example

import { focusRing, hitTarget, getFocusableAttributes } from "@nebula/core-ui";

const props = getFocusableAttributes({ role: "button", focusKey: "play" });
const styles = {
	minHeight: hitTarget.minHeight,
	minWidth: hitTarget.minWidth,
	outline: `${focusRing.outlineWidth}px solid ${focusRing.outlineColor}`,
	outlineOffset: focusRing.outlineOffset
};

SteamOS / Steam Deck notes

  • Favor large hit targets (48px+ at 100% scale) and strong focus rings.
  • Keep text contrast high and use title/display sizes for primary UI.
  • Map actions to glyphs rather than hard-coded button labels.
  • Provide a thin input adapter that converts raw device events into InputEvent objects.

Development

  • Node 18+
  • JavaScript only (ES Modules)
  • JSDoc for all public APIs

Run tests from the repo root:

npm test

License

MIT

S
Description
No description provided
Readme MIT 635 KiB
Languages
JavaScript 100%