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

308 lines
8.2 KiB
Markdown

# 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<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
```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