# 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: ```bash npm install @nebula/core-input @nebula/core-navigation @nebula/core-ui @nebula/core-theme @nebula/core-glyphs @nebula/core-utils ``` ## Quick start ```js 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 ```js 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` - `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 ```js 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 ```ts 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 ```js 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 ```js 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 ```js 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 ```js 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