├── .gitignore ├── README.md ├── components ├── anatomy.tsx ├── color-mode.tsx ├── components-map.ts └── visualizer │ ├── background.tsx │ ├── canvas.tsx │ ├── controls │ ├── icons.tsx │ └── index.tsx │ ├── dom-sandbox.tsx │ ├── graph │ ├── action.tsx │ ├── bounds.tsx │ ├── edges │ │ ├── arrow-marker.tsx │ │ ├── edge.tsx │ │ ├── index.tsx │ │ └── initial-edge.tsx │ ├── index.tsx │ ├── nav.tsx │ ├── skeleton.tsx │ ├── state-node │ │ ├── actions.tsx │ │ ├── header.tsx │ │ ├── index.tsx │ │ └── layout.ts │ ├── transition.tsx │ └── wrapper.tsx │ ├── index.tsx │ ├── simulation │ ├── context.tsx │ └── machine.ts │ ├── types.ts │ └── utils │ ├── graph │ ├── create-graph.ts │ ├── create-root-node.ts │ ├── elk.machine.ts │ ├── elk.types.ts │ ├── elk.utils.ts │ └── types.ts │ ├── hooks │ └── use-effect-once.ts │ ├── machine │ ├── misc.ts │ ├── parse-guard.ts │ ├── serialize.ts │ └── state-node.ts │ └── path.ts ├── eslint.config.mjs ├── next.config.ts ├── package.json ├── pages ├── [component].tsx ├── _app.tsx ├── _document.tsx └── index.tsx ├── pnpm-lock.yaml ├── public └── favicon.ico ├── styles └── globals.css └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Zag Visualizer 2 | 3 | Explore Zag's state machines through interactive graphs 4 | 5 | ## Sponsors ✨ 6 | 7 | Thanks goes to these wonderful people 8 | 9 |

10 | 11 | 12 | 13 |

14 | -------------------------------------------------------------------------------- /components/anatomy.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentAnatomyName, getComponent } from "@zag-js/anatomy-icons"; 2 | 3 | export function Anatomy(props: { component: string }) { 4 | const Component = getComponent(props.component as ComponentAnatomyName); 5 | 6 | if (!Component) return null; 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /components/color-mode.tsx: -------------------------------------------------------------------------------- 1 | import { SunIcon } from "@/components/visualizer/controls/icons" 2 | 3 | import { MoonIcon } from "@/components/visualizer/controls/icons" 4 | import { useTheme } from "next-themes" 5 | 6 | export function ColorMode() { 7 | const { theme, setTheme } = useTheme() 8 | 9 | return ( 10 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /components/components-map.ts: -------------------------------------------------------------------------------- 1 | import * as accordion from "@zag-js/accordion" 2 | import * as angleSlider from "@zag-js/angle-slider" 3 | import * as avatar from "@zag-js/avatar" 4 | import * as carousel from "@zag-js/carousel" 5 | import * as checkbox from "@zag-js/checkbox" 6 | import * as clipboard from "@zag-js/clipboard" 7 | import * as collapsible from "@zag-js/collapsible" 8 | import * as colorPicker from "@zag-js/color-picker" 9 | import * as combobox from "@zag-js/combobox" 10 | import * as datePicker from "@zag-js/date-picker" 11 | import * as dialog from "@zag-js/dialog" 12 | import * as editable from "@zag-js/editable" 13 | import * as fileUpload from "@zag-js/file-upload" 14 | import * as floatingPanel from "@zag-js/floating-panel" 15 | import * as hoverCard from "@zag-js/hover-card" 16 | import * as menu from "@zag-js/menu" 17 | // import * as navigationMenu from "@zag-js/navigation-menu" 18 | import * as numberInput from "@zag-js/number-input" 19 | import * as pagination from "@zag-js/pagination" 20 | import * as pinInput from "@zag-js/pin-input" 21 | import * as popover from "@zag-js/popover" 22 | import * as presence from "@zag-js/presence" 23 | import * as progress from "@zag-js/progress" 24 | import * as qrCode from "@zag-js/qr-code" 25 | import * as radioGroup from "@zag-js/radio-group" 26 | import * as ratingGroup from "@zag-js/rating-group" 27 | import * as select from "@zag-js/select" 28 | import * as signaturePad from "@zag-js/signature-pad" 29 | import * as slider from "@zag-js/slider" 30 | import * as splitter from "@zag-js/splitter" 31 | import * as steps from "@zag-js/steps" 32 | import * as zagSwitch from "@zag-js/switch" 33 | import * as tabs from "@zag-js/tabs" 34 | import * as tagsInput from "@zag-js/tags-input" 35 | import * as timePicker from "@zag-js/time-picker" 36 | import * as timer from "@zag-js/timer" 37 | import * as toast from "@zag-js/toast" 38 | import * as toggleGroup from "@zag-js/toggle-group" 39 | import * as tooltip from "@zag-js/tooltip" 40 | import * as tour from "@zag-js/tour" 41 | import * as treeView from "@zag-js/tree-view" 42 | import type { Machine as ZagMachine } from "@zag-js/core" 43 | 44 | type ComponentInfo = { 45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 46 | component: ZagMachine 47 | title: string 48 | } 49 | 50 | export type Component = keyof typeof componentsMap 51 | 52 | export const componentsMap = { 53 | accordion: { component: accordion, title: "Accordion" }, 54 | "angle-slider": { component: angleSlider, title: "Angle Slider" }, 55 | avatar: { component: avatar, title: "Avatar" }, 56 | carousel: { component: carousel, title: "Carousel" }, 57 | checkbox: { component: checkbox, title: "Checkbox" }, 58 | clipboard: { component: clipboard, title: "Clipboard" }, 59 | collapsible: { component: collapsible, title: "Collapsible" }, 60 | "color-picker": { component: colorPicker, title: "Color Picker" }, 61 | combobox: { component: combobox, title: "Combobox" }, 62 | "date-picker": { component: datePicker, title: "Date Picker" }, 63 | dialog: { component: dialog, title: "Dialog" }, 64 | editable: { component: editable, title: "Editable" }, 65 | "file-upload": { component: fileUpload, title: "File Upload" }, 66 | "floating-panel": { component: floatingPanel, title: "Floating Panel" }, 67 | "hover-card": { component: hoverCard, title: "Hover Card" }, 68 | menu: { component: menu, title: "Menu" }, 69 | // "navigation-menu": { component: navigationMenu, title: "Navigation Menu" }, 70 | "number-input": { component: numberInput, title: "Number Input" }, 71 | pagination: { component: pagination, title: "Pagination" }, 72 | "pin-input": { component: pinInput, title: "Pin Input" }, 73 | popover: { component: popover, title: "Popover" }, 74 | presence: { component: presence, title: "Presence" }, 75 | progress: { component: progress, title: "Progress" }, 76 | "qr-code": { component: qrCode, title: "QR Code" }, 77 | "radio-group": { component: radioGroup, title: "Radio Group" }, 78 | "rating-group": { component: ratingGroup, title: "Rating Group" }, 79 | select: { component: select, title: "Select" }, 80 | "signature-pad": { component: signaturePad, title: "Signature Pad" }, 81 | slider: { component: slider, title: "Slider" }, 82 | splitter: { component: splitter, title: "Splitter" }, 83 | steps: { component: steps, title: "Steps" }, 84 | switch: { component: zagSwitch, title: "Switch" }, 85 | tabs: { component: tabs, title: "Tabs" }, 86 | "tags-input": { component: tagsInput, title: "Tags Input" }, 87 | "time-picker": { component: timePicker, title: "Time Picker" }, 88 | timer: { component: timer, title: "Timer" }, 89 | toast: { component: toast, title: "Toast" }, 90 | "toggle-group": { component: toggleGroup, title: "Toggle Group" }, 91 | tooltip: { component: tooltip, title: "Tooltip" }, 92 | tour: { component: tour, title: "Tour" }, 93 | "tree-view": { component: treeView, title: "Tree View" }, 94 | } as const 95 | 96 | export type ComponentsMap = { 97 | [K in Component]: ComponentInfo 98 | } 99 | 100 | export const settingsMap: Partial>> = { 101 | carousel: { 102 | // others 103 | // ... 104 | 105 | // required 106 | slideCount: 2, 107 | }, 108 | toast: { 109 | // others 110 | // ... 111 | 112 | // required 113 | id: "toast", 114 | type: "success", 115 | removeDelay: 1000, 116 | parent: { 117 | // To prevent errors when toast tries to communicate with the non existent service 118 | send() {}, 119 | }, 120 | }, 121 | "floating-panel": { 122 | // others 123 | // ... 124 | 125 | // required 126 | id: "floating-panel", 127 | }, 128 | } 129 | -------------------------------------------------------------------------------- /components/visualizer/background.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from "react" 2 | import { useTransformComponent } from "react-zoom-pan-pinch" 3 | 4 | export const Background = () => { 5 | const _patternId = useId() 6 | 7 | const background = useTransformComponent(({ state }) => { 8 | const { scale, positionX, positionY } = state 9 | 10 | const patternSize = 1 11 | const gapXY: [number, number] = [20, 20] 12 | const offsetXY: [number, number] = [0, 0] 13 | 14 | const scaledSize = patternSize * scale 15 | const scaledGap: [number, number] = [gapXY[0] * scale || 1, gapXY[1] * scale || 1] 16 | const patternDimensions: [number, number] = scaledGap 17 | const scaledOffset: [number, number] = [ 18 | offsetXY[0] * scale || 1 + patternDimensions[0] / 2, 19 | offsetXY[1] * scale || 1 + patternDimensions[1] / 2, 20 | ] 21 | 22 | const radius = scaledSize / 2 23 | 24 | return ( 25 | 26 | 35 | 36 | 37 | 38 | 39 | ) 40 | }) 41 | 42 | return background 43 | } 44 | -------------------------------------------------------------------------------- /components/visualizer/canvas.tsx: -------------------------------------------------------------------------------- 1 | import { TransformComponent } from "react-zoom-pan-pinch"; 2 | 3 | type CanvasProps = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | export default function Canvas(props: CanvasProps) { 8 | return ( 9 | <> 10 | 11 | {props.children} 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/visualizer/controls/icons.tsx: -------------------------------------------------------------------------------- 1 | export function FitIcon() { 2 | return ( 3 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export function PlusIcon() { 14 | return ( 15 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export function MinusIcon() { 26 | return ( 27 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export function ResetIcon() { 38 | return ( 39 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | 58 | export function MoonIcon(props: { className?: string }) { 59 | return ( 60 | 72 | 73 | 74 | ); 75 | } 76 | 77 | export const SunIcon = (props: { className?: string }) => { 78 | return ( 79 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ); 102 | }; 103 | 104 | export function GithubIcon() { 105 | return ( 106 | 117 | 118 | 119 | 120 | ); 121 | } 122 | 123 | export function HomeIcon() { 124 | return ( 125 | 136 | 137 | 138 | 139 | ); 140 | } 141 | 142 | export function ZagIcon() { 143 | return ( 144 | 151 | 157 | 158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /components/visualizer/controls/index.tsx: -------------------------------------------------------------------------------- 1 | import { FitIcon, MinusIcon, PlusIcon, ResetIcon } from "./icons" 2 | import { useControls, useTransformComponent, useTransformContext } from "react-zoom-pan-pinch" 3 | import { useSimulation } from "@/components/visualizer/simulation/context" 4 | import { ColorMode } from "@/components/color-mode" 5 | 6 | export const Controls = () => { 7 | const { child, service } = useSimulation() 8 | 9 | const { 10 | setup: { minScale, maxScale }, 11 | } = useTransformContext() 12 | 13 | const { zoomIn, zoomOut, zoomToElement } = useControls() 14 | const transformControls = useTransformComponent(({ state }) => { 15 | const { scale } = state 16 | 17 | return ( 18 | <> 19 | 22 | 25 | 28 | 29 | ) 30 | }) 31 | 32 | const reset = () => { 33 | child?.state.set(child.state.initial) 34 | const settings = service.refs.get("settings") ?? {} 35 | Object.entries(settings).forEach(([key, value]) => { 36 | child?.context.set(key, value) 37 | }) 38 | } 39 | 40 | return ( 41 |
42 | {transformControls} 43 | 46 | 47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /components/visualizer/dom-sandbox.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react" 2 | 3 | /** Creates an isolated DOM environment for state machine services 4 | * 5 | * Why is this needed? 6 | * ------------------ 7 | * When visualizing state machines, we want to see the state transitions and structure, 8 | * but we don't want the actual DOM operations (like scroll locks, focus management, 9 | * or drag and drop) to affect the visualization UI. 10 | * 11 | */ 12 | export function useDOMSandbox() { 13 | const sandboxRef = useRef(null) 14 | 15 | const DomSandbox = () => ( 16 |