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.
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