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.
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:
- Input: Normalize device events into actions.
- Navigation: Choose the next focus target with spatial heuristics.
- UI: Provide hit-target and focus-ring defaults for controller use.
- Theme: Provide tokens tuned for couch and handheld readability.
- Glyphs: Map logical actions to controller glyphs and assets.
- 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, andactivestate.
API
createActionMapper(options)
Creates an action mapper instance.
Options
bindings:Record<string, InputBinding[]>deadzone?:number(default:0.2)
Returns
mapEvent(event): map a singleInputEventto action updates.getActionState(action): get currentvalueandactive.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/displaysizes for primary UI. - Map actions to glyphs rather than hard-coded button labels.
- Provide a thin input adapter that converts raw device events into
InputEventobjects.
Development
- Node 18+
- JavaScript only (ES Modules)
- JSDoc for all public APIs
Run tests from the repo root:
npm test
License
MIT