Files
andrew 987ff560f5 Add initial Nebula Core packages and docs
Import initial monorepo structure for Nebula Core: add packages (@nebula/core, core-glyphs, core-input, core-navigation, core-theme, core-ui, core-utils) with source, dist, tests and assets. Expand README with overview, quick start and API snippets, and add package-level documentation files. Add jsconfig.json for path mapping, package.json and lockfiles to bootstrap the repo. This commit sets up the project layout, docs, and local package links for further development.
2026-01-31 22:57:16 +13:00

8.2 KiB

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