@osiris-smarttv/screen-navigation
v0.1.2
Published
TV screen stack coordinator — lifecycle, keep-alive ports, automatic focus restore
Maintainers
Readme
@osiris-smarttv/screen-navigation
Screen stack coordinator for React TV apps — intents, lifecycle, keep-alive, and focus restore in one place.
On lean-back devices, navigation is more than URL changes. Screens need a stack, cached views, remote Back, and spatial focus that survives tab switches. @osiris-smarttv/screen-navigation gives you a single coordinator for all of that. React Router syncs to the stack; it does not own it.
Remote / UI ScreenCoordinator (SSOT) Adapters
─────────── ───────────────────────── ────────
push / pop / back ──► stack + lifecycle + focus ──────► React Router (URL)
detail deeplink ──► intent queue + registry ──────► @osiris-smarttv/keep-alive
memory warning ──► eviction policy ──────► Norigin focus restoreWhy use it
| Problem on TV | What this package does |
|---|---|
| Back button handled in five places | back() / pop() unified with stack policy |
| Route change ≠ screen lifecycle | Explicit enter · resume · pause · exit · refresh phases |
| Heavy screens remount on every tab | Registry cache: true + keep-alive port |
| Focus lost after navigation | ScreenFocusScope + deferred focus policy |
| Memory pressure on console | Optional bridge + planMemoryEviction |
| Ad-hoc navigate(-1) everywhere | Typed push · replace · reset · reload · prefetch |
Install
npm i @osiris-smarttv/screen-navigation @osiris-smarttv/keep-alivePeer dependencies
| Package | Purpose |
|---|---|
| react react-dom | Provider & hooks |
| react-router-dom | URL sync adapter |
| @osiris-smarttv/keep-alive | Page cache port (optional but recommended) |
| @noriginmedia/norigin-spatial-navigation-core | Focus key model |
| @noriginmedia/norigin-spatial-navigation-react | Focus scope integration |
Package exports
| Entry | Contents |
|---|---|
| @osiris-smarttv/screen-navigation | React provider, hooks, route helper, adapters |
| @osiris-smarttv/screen-navigation/core | Pure TypeScript — no React imports |
Quick start
1. Declare screens
import { defineScreenRegistry } from '@osiris-smarttv/screen-navigation'
export const screenRegistry = defineScreenRegistry({
home: {
id: 'home',
buildKey: () => '/main',
buildPath: () => '/main',
cache: true,
stackPolicy: 'replace-top',
defaultFocusKey: () => 'HOME_HERO',
matchCacheKey: (key) => key === '/main',
},
detail: {
id: 'detail',
buildKey: (p) => `/detail/${p.id}`,
buildPath: (p) => `/detail/${p.id}`,
cache: true,
stackPolicy: 'push',
parseParamsFromCacheKey: (key) => {
const id = key.split('/').pop() ?? ''
return { id }
},
matchCacheKey: (key) => key.startsWith('/detail/'),
},
})2. Mount the provider
Wrap your router shell (inside BrowserRouter / RouterProvider):
import { ScreenNavigationProvider } from '@osiris-smarttv/screen-navigation'
import { screenRegistry } from './screenRegistry'
export function AppNavigation({ children }: { children: React.ReactNode }) {
return (
<ScreenNavigationProvider
registry={screenRegistry}
homeScreenId="home"
popAtRoot="home"
memoryPressureBridge
>
{children}
</ScreenNavigationProvider>
)
}3. Wire routes
Use createScreenRouteElement so each route gets a focus scope:
import type { RouteObject } from 'react-router-dom'
import { createScreenRouteElement } from '@osiris-smarttv/screen-navigation'
import { HomeScreen } from './HomeScreen'
import { DetailScreen } from './DetailScreen'
export const routes: RouteObject[] = [
{ path: '/main', element: createScreenRouteElement(HomeScreen) },
{ path: '/detail/:id', element: createScreenRouteElement(DetailScreen) },
]4. Navigate from UI
import { useScreenNavigation } from '@osiris-smarttv/screen-navigation'
function OpenDetail({ id }: { id: string }) {
const nav = useScreenNavigation()
return (
<button onClick={() => nav.push('detail', { id })}>
Open detail
</button>
)
}useScreenNavigation() returns:
push(screenId, params?, options?)pop(options?)·back()(remote Back)replace·reset·reload·prefetchsnapshot— current stack framescoordinator— escape hatch to core API
Screen lifecycle
Subscribe per screen with useScreenLifecycle:
import { useScreenLifecycle } from '@osiris-smarttv/screen-navigation'
export function PlayerScreen() {
useScreenLifecycle({
onEnter: () => console.log('first mount'),
onResume: () => player.resume(),
onPause: () => player.pause(),
onExit: () => player.teardown(),
onRefresh: () => player.reload(),
})
return <PlayerChrome />
}Phases map to TV navigation reality: a cached screen resumes instead of re-entering when you return to it.
Remote Back
ScreenNavigationProvider already wraps children with ScreenBackListener — remote Back keys route through coordinator.handleRemoteBack() and respect stack policy.
For overlays that capture back (player, modal), stop propagation or render a nested listener with enabled={false} on the default handler while your layer is active.
isRemoteBackKey is exported if you need custom key routing.
Stack graph & prefetch
Declare allowed transitions and optional prefetch targets:
import type { ScreenStackGraph } from '@osiris-smarttv/screen-navigation/core'
export const stackGraph: ScreenStackGraph = {
home: [{ to: 'detail', prefetch: true }],
detail: [{ to: 'home' }],
}Pass stackGraph + stackGraphPolicy to the provider. Use onPrefetch to warm routes or data before the user lands.
Core-only usage
Use @osiris-smarttv/screen-navigation/core when you need the coordinator without React — tests, native shells, or custom bindings:
import {
ScreenCoordinator,
defineScreenRegistry,
NavigationIntentKind,
ScreenCacheMode,
} from '@osiris-smarttv/screen-navigation/core'
const coordinator = new ScreenCoordinator({ registry })
coordinator.attachRouterPort(myRouterPort)
coordinator.attachCachePort(myCachePort)
await coordinator.push('home')Highlights: ScreenLifecycleBus, NavigationIntentQueue, scheduleDeferredFocusApply, planMemoryEviction, ScreenTelemetryBus.
Works with @osiris-smarttv/keep-alive
The provider wires createKeepAliveCachePort automatically when AliveScope is mounted. Registry cache: true keeps DOM snapshots; reload / pop call clearEntry / refreshEntry through the port.
Recommended tree:
AliveScope
└─ ScreenNavigationProvider
└─ KeepAliveOutlet (from keep-alive/router)
└─ your routesSource privacy on npm
Published tarballs are public but ship only compiled output:
dist/**README.mdpackage.json,LICENSE
src/** is excluded via the files allowlist — git repo can stay private while npm consumers get minified dist only.
Local development
yarn install
yarn build
yarn typecheck
yarn testMonorepo consumers can alias src/ for hot reload; npm installs resolve dist/.
Publish
yarn publish:check
yarn publish:dry-run
yarn publish:manualSee PUBLISHING.md and ARCHITECTURE.md.
License
MIT © OsirisTech SmartTV
