import React, { useState, useEffect, useMemo } from "react"; import { useInput } from "ink"; export type Route< T extends Record, K extends keyof T > = (router: ToRoute) => React.FC; export function router>() { return { build: (_: K, route: Route) => { return route; }, withRoutes(..._: K[]) { type Filtered = { [K2 in K]: T[K2] }; return { build: (_: K, route: Route) => { return route; }, }; }, route: (componentBuilders: RouterComponents) => { return new RouteBuilder(componentBuilders); }, }; } type RouterComponents> = { [K in keyof T]: Route }; export function Back({ go, children }: { go: () => any, children: React.ReactNode }) { useInput((_, key) => { if(key.escape) go(); }); return children; } export class RouteBuilder> { Root: (initial: { route: Initial, props: T[Initial] }) => React.ReactNode; constructor( componentBuilders: RouterComponents ) { this.Root = (initial: { route: Initial, props: T[Initial] }) => { const router = useMemo(() => { return new Router(initial); }, []); const [ current, setCurrent ] = useState(router.current()); const Current = useMemo(() => { const minirouter: Partial> = {}; for(const key of Object.keys(componentBuilders)) { // @ts-ignore minirouter[key] = props => { router.route({ from: current.route, to: key, props, }); }; } const builder = componentBuilders[current.route]; return builder(minirouter as ToRoute); }, [ current ]); useEffect(() => { const listener = router.addRouteListener((route, props) => { setCurrent({ route, props }); }); setCurrent(router.current()); return () => router.removeRouteListener(listener); }, [ router ]); return }; } } export type ToRoute> = { [K in keyof T]: (props: T[K]) => void }; class Router, Initial extends keyof T> { private _current: { route: keyof T, props: T[keyof T], }; private _routeChangeCallbacks: Array< (route: K, data: T[K]) => any > = []; constructor( initial: { route: Initial, props: T[Initial], }, ) { this._current = initial; } current() { return { ...this._current }; } route({ from, to, props }: { from: keyof T, to: K, props: T[K], }) { if(this._current.route === from) { this._current = { route: to, props }; this.onRouteChange(to, props); } } addRouteListener(listener: (route: K, props: T[K]) => any) { this._routeChangeCallbacks.push(listener); return listener; } removeRouteListener(listener: (route: K, props: T[K]) => any) { const index = this._routeChangeCallbacks.indexOf(listener); if(index >= 0) this._routeChangeCallbacks.splice(index, 1); } private onRouteChange(route: K, props: T[K]) { for(const cb of this._routeChangeCallbacks) { cb(route, props); } } }