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.
This commit is contained in:
2026-01-31 22:57:16 +13:00
parent 40dcfc1853
commit 987ff560f5
309 changed files with 2611 additions and 1 deletions
+308 -1
View File
@@ -1 +1,308 @@
# Nebula-Core
# 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
+22
View File
@@ -0,0 +1,22 @@
# @nebula/core-glyphs
Controller glyph mappings and lookup helpers. Data-only, no rendering logic.
## What it does
- Maps logical actions to glyph labels per controller type.
- Provides asset path lookups for common glyphs.
- Keeps glyph decisions centralized and consistent.
## Included data and helpers
- `glyphMap` — Action-to-label mappings per controller type.
- `glyphAssetMap` — Action-to-asset-path mappings per controller type.
- `getGlyph(controller, action)` — Resolve a label.
- `getGlyphAssetPath(controller, action)` — Resolve an asset path.
## Typical use cases
- Displaying correct prompts for Steam Deck, Xbox, PlayStation, or Switch.
- Using consistent glyph labels in UI and localization.
- Swapping controller visuals based on detected device.
+21
View File
@@ -0,0 +1,21 @@
# @nebula/core-input
Action-based input abstraction for controller-first apps. Converts raw device events into app-level actions.
## What it does
- Normalizes device input into a consistent `InputEvent` shape.
- Maps physical controls to logical actions (e.g. confirm, back).
- Emits action updates with `value` and `active` state.
## Key concepts
- **InputEvent**: Normalized event with `source`, `control`, `type`, and `value`.
- **InputBinding**: Maps a physical control to an action name.
- **ActionUpdate**: The emitted result when an input matches a binding.
## Typical use cases
- Gamepad-first UIs that should work across Deck, Xbox, and PlayStation layouts.
- Unified handling of keyboard and controller inputs without coupling to hardware.
- Action-driven UI logic (confirm/back/menu) instead of button-driven logic.
+20
View File
@@ -0,0 +1,20 @@
# @nebula/core-navigation
Focus management and spatial navigation primitives for controller-first UIs.
## What it does
- Calculates spatial relationships between focusable rectangles.
- Filters candidates by direction (up/down/left/right).
- Picks the best next focus target using a weighted distance heuristic.
## Key concepts
- **Rect**: A focusable element described by `x`, `y`, `width`, `height`, and `id`.
- **Direction**: The requested movement direction (up/down/left/right).
## Typical use cases
- D-pad or stick navigation across grids and lists.
- Consistent focus movement in TV or handheld UIs.
- Simple focus logic without a framework dependency.
+23
View File
@@ -0,0 +1,23 @@
# @nebula/core-theme
Theme tokens tuned for readable, couch-friendly UI. Exports plain objects and helpers only.
## What it does
- Provides a default token set for colors, spacing, typography, radius, and motion.
- Lets you merge overrides with `createTheme()` for custom styling.
- Keeps everything as plain objects for easy use in any UI layer.
## Token groups
- **colors**: Background, surface, text, accent, focus, danger.
- **spacing**: XS through XXL spacing scale.
- **radius**: Corner radius presets.
- **typography**: Caption, body, title, display sizes.
- **motion**: Fast/base/slow durations.
## Typical use cases
- Consistent visual system across packages and apps.
- Theme customization without framework coupling.
- Readable defaults for TV and handheld screens.
+21
View File
@@ -0,0 +1,21 @@
# @nebula/core-ui
Minimal UI helpers for controller-first applications. No framework lock-in.
## What it does
- Defines recommended hit target sizes for controller navigation.
- Provides focus ring tokens for visible focus styling.
- Builds minimal focusable attributes for DOM-based UIs.
## Included helpers
- `hitTarget` — Minimum dimensions for interactive elements.
- `focusRing` — Outline width/color/offset for focus indication.
- `getFocusableAttributes(options)` — Returns `role`, `tabIndex`, and `data-` attributes.
## Typical use cases
- Ensuring touch and controller targets are large enough.
- Consistent focus styling in custom UI frameworks.
- Simple DOM attribute helpers for focus management.
+22
View File
@@ -0,0 +1,22 @@
# @nebula/core-utils
Small, dependency-free utilities shared across Nebula Core packages. No DOM or platform assumptions.
## What it does
- Provides tiny, reusable math helpers.
- Exposes a minimal event emitter for small, decoupled messaging.
- Stays platform-agnostic so it works in browser, Electron, or Node.
## Included utilities
- `clamp(value, min, max)` — Clamp a number between bounds.
- `lerp(from, to, t)` — Linear interpolation between two numbers.
- `roundTo(value, decimals)` — Round to a fixed decimal precision.
- `createEmitter()` — Lightweight emitter with `on`, `emit`, and `clear`.
## Typical use cases
- Input or navigation modules needing small math helpers.
- App-level messaging without pulling in larger event libraries.
- Shared helpers across packages in the monorepo.
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"checkJs": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022",
"allowJs": true,
"baseUrl": ".",
"paths": {
"@nebula/*": ["packages/*/src/index.js"]
},
"resolveJsonModule": true,
"strict": true,
"noEmit": true
},
"exclude": [
"node_modules",
"**/node_modules"
]
}
+85
View File
@@ -0,0 +1,85 @@
{
"name": "nebula-core",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@nebulaproject/core": {
"resolved": "packages/core",
"link": true
},
"node_modules/@nebulaproject/core-glyphs": {
"resolved": "packages/core-glyphs",
"link": true
},
"node_modules/@nebulaproject/core-input": {
"resolved": "packages/core-input",
"link": true
},
"node_modules/@nebulaproject/core-navigation": {
"resolved": "packages/core-navigation",
"link": true
},
"node_modules/@nebulaproject/core-theme": {
"resolved": "packages/core-theme",
"link": true
},
"node_modules/@nebulaproject/core-ui": {
"resolved": "packages/core-ui",
"link": true
},
"node_modules/@nebulaproject/core-utils": {
"resolved": "packages/core-utils",
"link": true
},
"packages/core": {
"name": "@nebulaproject/core",
"version": "0.1.3",
"license": "MIT"
},
"packages/core-glyphs": {
"name": "@nebulaproject/core-glyphs",
"version": "0.1.0",
"engines": {
"node": ">=18"
}
},
"packages/core-input": {
"name": "@nebulaproject/core-input",
"version": "0.1.0",
"dependencies": {
"@nebulaproject/core-utils": "0.1.0"
},
"engines": {
"node": ">=18"
}
},
"packages/core-navigation": {
"name": "@nebulaproject/core-navigation",
"version": "0.1.0",
"engines": {
"node": ">=18"
}
},
"packages/core-theme": {
"name": "@nebulaproject/core-theme",
"version": "0.1.0",
"engines": {
"node": ">=18"
}
},
"packages/core-ui": {
"name": "@nebulaproject/core-ui",
"version": "0.1.0",
"engines": {
"node": ">=18"
}
},
"packages/core-utils": {
"name": "@nebulaproject/core-utils",
"version": "0.1.0",
"engines": {
"node": ">=18"
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
# @nebula/core-glyphs
Controller glyph mappings and lookup helpers. Data-only, no rendering logic.
## Why it exists
Controller prompts should be consistent across Steam Deck, Xbox, and PlayStation. This package keeps glyph mapping in one place.
## Usage
```js
import { getGlyph, getGlyphAssetPath } from "@nebula/core-glyphs";
const glyph = getGlyph("steam-deck", "confirm");
console.log(glyph); // "A"
const asset = getGlyphAssetPath("steam-deck", "confirm");
console.log(asset); // "assets/Steam Deck/SteamDeck_A.png"
```
## SteamOS / Steam Deck notes
Provide Deck-specific prompts by selecting `steam-deck` when the device is detected. Use the asset path helper or bind your own icon assets keyed by the glyph labels.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More