Hierarchical Traffic Light
import { defineStates, matchina } from "matchina";import { submachine, nestedHsmRoot } from "matchina/hsm";
// 1. Define the Child Machine (Light Cycle)// We need a factory for the child machine so it can be instantiated freshlyconst lightCycleStates = defineStates({ Red: undefined, Green: undefined, Yellow: undefined,});
const createLightCycle = () => matchina( lightCycleStates, { Red: { tick: "Green" }, Green: { tick: "Yellow" }, Yellow: { tick: "Red" }, }, lightCycleStates.Red() );
// 2. Define the Parent Machine (Controller)// We use `submachine` to embed the child machine factoryconst controllerStates = defineStates({ Broken: undefined, Working: submachine(createLightCycle), Maintenance: undefined,});
const createController = () => matchina( controllerStates, { Broken: { repair: "Working", maintenance: "Maintenance" }, Working: { break: "Broken", maintenance: "Maintenance" }, Maintenance: { complete: "Working" }, }, controllerStates.Working() );
// 3. Create the Hierarchical Machine// This wraps the controller with propagation logicexport function createPropagatingTrafficLight() { const root = createController(); return nestedHsmRoot(root);}import { createHSM } from "matchina/hsm";
export function createFlatTrafficLight() { return createHSM({ initial: "Working", states: { Broken: { on: { repair: "Working", maintenance: "Maintenance", }, },
// Working is a hierarchical state with light cycle substates Working: { initial: "Red", states: { Red: { on: { tick: "Green", }, }, Green: { on: { tick: "Yellow", }, }, Yellow: { on: { tick: "Red", }, }, }, // Parent-level transitions apply to all child states on: { break: "^Broken", maintenance: "^Maintenance", }, },
Maintenance: { on: { complete: "Working", }, }, }, });}import { useMachine } from "matchina/react";
interface TrafficLightViewNestedProps { machine: any;}
const glowColor: Record<string, string> = { red: "shadow-red-500/60", yellow: "shadow-yellow-400/60", green: "shadow-green-500/60",};
function Light({ color, active }: { color: "red" | "yellow" | "green"; active: boolean }) { const offColor = { red: "bg-[oklch(0.22_0.04_15)]", yellow: "bg-[oklch(0.22_0.04_85)]", green: "bg-[oklch(0.22_0.04_142)]" }[color]; const onColor = { red: "bg-red-500", yellow: "bg-yellow-400", green: "bg-green-500" }[color]; return ( <div className={`w-12 h-12 rounded-full transition-all duration-200 ${active ? `${onColor} shadow-lg ${glowColor[color]}` : offColor}`} /> );}
export function TrafficLightViewNested({ machine }: TrafficLightViewNestedProps) { const change = useMachine(machine) as { to: { key: string; data?: any } }; const state = change.to; const send = (event: string) => machine.send(event);
const parent = state.key; let child: string | null = null; if (state.data?.machine) { const childState = state.data.machine.getState(); if (childState) child = childState.key; }
const isWorking = parent === "Working"; const lightColor = child ?? "off";
return ( <div className="flex flex-col items-center gap-5"> {/* Housing */} <div className="flex flex-col items-center gap-3 bg-[oklch(0.18_0.01_240)] rounded-2xl px-5 py-6 border border-[oklch(0.25_0.01_240)]"> <Light color="red" active={isWorking && lightColor === "Red"} /> <Light color="yellow" active={isWorking && lightColor === "Yellow"} /> <Light color="green" active={isWorking && lightColor === "Green"} /> </div>
{/* State badges */} <div className="flex gap-2 flex-wrap justify-center"> <span className="badge badge-outline text-[10px] font-mono">{parent}</span> {child && <span className="badge badge-outline text-[10px] font-mono">{child}</span>} </div>
{/* Controls */} <div className="flex flex-wrap gap-2 justify-center"> {isWorking && ( <button onClick={() => send("tick")} className="btn btn-primary btn-sm">Tick</button> )} {parent === "Broken" && ( <button onClick={() => send("repair")} className="btn btn-outline btn-sm">Repair</button> )} {parent === "Maintenance" && ( <button onClick={() => send("complete")} className="btn btn-outline btn-sm">Complete</button> )} {(isWorking || parent === "Broken") && ( <button onClick={() => send("maintenance")} className="btn btn-outline btn-sm">Maintenance</button> )} {isWorking && ( <button onClick={() => send("break")} className="btn btn-destructive btn-sm">Break</button> )} </div> </div> );}import { useMachine } from "matchina/react";
interface TrafficLightViewFlatProps { machine: any;}
const glowColor: Record<string, string> = { red: "shadow-red-500/60", yellow: "shadow-yellow-400/60", green: "shadow-green-500/60",};
function Light({ color, active }: { color: "red" | "yellow" | "green"; active: boolean }) { const offColor = { red: "bg-[oklch(0.22_0.04_15)]", yellow: "bg-[oklch(0.22_0.04_85)]", green: "bg-[oklch(0.22_0.04_142)]" }[color]; const onColor = { red: "bg-red-500", yellow: "bg-yellow-400", green: "bg-green-500" }[color]; return ( <div className={`w-12 h-12 rounded-full transition-all duration-200 ${active ? `${onColor} shadow-lg ${glowColor[color]}` : offColor}`} /> );}
export function TrafficLightViewFlat({ machine }: TrafficLightViewFlatProps) { const change = useMachine(machine) as { to: { key: string; data?: any } }; const state = change.to; const send = (event: string) => machine.send(event);
const parts = state.key.split("."); const parent = parts[0]; const child = parts[1] as string | undefined; const isWorking = parent === "Working"; const lightColor = child ?? "off";
return ( <div className="flex flex-col items-center gap-5"> {/* Housing */} <div className="flex flex-col items-center gap-3 bg-[oklch(0.18_0.01_240)] rounded-2xl px-5 py-6 border border-[oklch(0.25_0.01_240)]"> <Light color="red" active={isWorking && lightColor === "Red"} /> <Light color="yellow" active={isWorking && lightColor === "Yellow"} /> <Light color="green" active={isWorking && lightColor === "Green"} /> </div>
{/* State badges */} <div className="flex gap-2 flex-wrap justify-center"> <span className="badge badge-outline text-[10px] font-mono">{parent}</span> {child && <span className="badge badge-outline text-[10px] font-mono">{child}</span>} </div>
{/* Controls */} <div className="flex flex-wrap gap-2 justify-center"> {isWorking && ( <button onClick={() => send("tick")} className="btn btn-primary btn-sm">Tick</button> )} {parent === "Broken" && ( <button onClick={() => send("repair")} className="btn btn-outline btn-sm">Repair</button> )} {parent === "Maintenance" && ( <button onClick={() => send("complete")} className="btn btn-outline btn-sm">Complete</button> )} {(isWorking || parent === "Broken") && ( <button onClick={() => send("maintenance")} className="btn btn-outline btn-sm">Maintenance</button> )} {isWorking && ( <button onClick={() => send("break")} className="btn btn-destructive btn-sm">Break</button> )} </div> </div> );}import { useMemo } from "react";import { TrafficLightViewNested } from "./TrafficLightViewNested";import { createPropagatingTrafficLight } from "./machine";
export function HsmTrafficLightDemo() { const machine = useMemo(createPropagatingTrafficLight, []); return <TrafficLightViewNested machine={machine} />;}