{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "name": "CrossFly examples",
  "description": "Four complete example apps and thirty-four recipes for the CrossFly UI framework. Each runs unchanged on Android, iOS, desktop (Windows, macOS and Linux) and the web (Chrome, Firefox, Safari and other browsers) from one Java source.",
  "site": "https://crossfly.dev",
  "count": 38,
  "kinds": {
    "app": "A complete example app: several screens showing how the pieces fit. Complete in shape, not in feature.",
    "recipe": "One screen answering one question, short enough to read whole and copy without editing."
  },
  "howToRead": "Fetch `json` for the full record (documentation + every source file), or `md` for the same thing as one Markdown document. `run` opens it compiled and running in a browser.",
  "examples": [
    {
      "id": "layouts",
      "kind": "recipe",
      "title": "Layouts",
      "note": "Any Component becomes a container with setLayout. The same three labels, three ways.",
      "tags": [
        "layout"
      ],
      "className": "R01Layouts",
      "lines": 93,
      "files": 1,
      "page": "https://crossfly.dev/examples/layouts",
      "json": "https://crossfly.dev/examples/layouts.json",
      "md": "https://crossfly.dev/examples/layouts.md",
      "java": "https://crossfly.dev/examples/layouts.java",
      "run": "https://crossfly.dev/playground/index.html?ex=layouts"
    },
    {
      "id": "theme-tokens",
      "kind": "recipe",
      "title": "Theme tokens",
      "note": "Flip the switch. The token line follows the theme; the hardcoded one does not, and turns unreadable — no rebuild involved.",
      "tags": [
        "theming"
      ],
      "className": "R02Theme",
      "lines": 65,
      "files": 1,
      "page": "https://crossfly.dev/examples/theme-tokens",
      "json": "https://crossfly.dev/examples/theme-tokens.json",
      "md": "https://crossfly.dev/examples/theme-tokens.md",
      "java": "https://crossfly.dev/examples/theme-tokens.java",
      "run": "https://crossfly.dev/playground/index.html?ex=theme-tokens"
    },
    {
      "id": "skins",
      "kind": "recipe",
      "title": "Switching skins",
      "note": "Shape, radius, padding and typeface all come from the skin. Nothing below knows which one is in force.",
      "tags": [
        "theming"
      ],
      "className": "R03Skins",
      "lines": 76,
      "files": 1,
      "page": "https://crossfly.dev/examples/skins",
      "json": "https://crossfly.dev/examples/skins.json",
      "md": "https://crossfly.dev/examples/skins.md",
      "java": "https://crossfly.dev/examples/skins.java",
      "run": "https://crossfly.dev/playground/index.html?ex=skins"
    },
    {
      "id": "bottom-sheet",
      "kind": "recipe",
      "title": "Bottom sheet with a fixed footer",
      "note": "BottomSheet already gives you the header, the grabber, the close button and a footer that floats over the scroll. You only fill body().",
      "tags": [
        "navigation",
        "overlay"
      ],
      "className": "R04Sheet",
      "lines": 71,
      "files": 1,
      "page": "https://crossfly.dev/examples/bottom-sheet",
      "json": "https://crossfly.dev/examples/bottom-sheet.json",
      "md": "https://crossfly.dev/examples/bottom-sheet.md",
      "java": "https://crossfly.dev/examples/bottom-sheet.java",
      "run": "https://crossfly.dev/playground/index.html?ex=bottom-sheet"
    },
    {
      "id": "listing",
      "kind": "recipe",
      "title": "Listing: cards or table",
      "note": "A card answers \"what is this one?\"; a row answers \"how do these compare?\". Which one the reader needs changes during the day, so Listing lets them switch.",
      "tags": [
        "lists"
      ],
      "className": "R05Listing",
      "lines": 113,
      "files": 1,
      "page": "https://crossfly.dev/examples/listing",
      "json": "https://crossfly.dev/examples/listing.json",
      "md": "https://crossfly.dev/examples/listing.md",
      "java": "https://crossfly.dev/examples/listing.java",
      "run": "https://crossfly.dev/playground/index.html?ex=listing"
    },
    {
      "id": "select",
      "kind": "recipe",
      "title": "Choosing from a list",
      "note": "ListViewWindow does single and multi selection, with the title, the confirm and the tap-to-choose already wired.",
      "tags": [
        "lists",
        "input"
      ],
      "className": "R06Select",
      "lines": 81,
      "files": 1,
      "page": "https://crossfly.dev/examples/select",
      "json": "https://crossfly.dev/examples/select.json",
      "md": "https://crossfly.dev/examples/select.md",
      "java": "https://crossfly.dev/examples/select.java",
      "run": "https://crossfly.dev/playground/index.html?ex=select"
    },
    {
      "id": "date-range",
      "kind": "recipe",
      "title": "Picking a date range",
      "note": "DatePicker answers \"which day\". This answers \"between which two\", which has its own rules — and its own ways of being wrong.",
      "tags": [
        "input"
      ],
      "className": "R07DateRange",
      "lines": 123,
      "files": 1,
      "page": "https://crossfly.dev/examples/date-range",
      "json": "https://crossfly.dev/examples/date-range.json",
      "md": "https://crossfly.dev/examples/date-range.md",
      "java": "https://crossfly.dev/examples/date-range.java",
      "run": "https://crossfly.dev/playground/index.html?ex=date-range"
    },
    {
      "id": "reorder",
      "kind": "recipe",
      "title": "Reordering a list",
      "note": "The first row has no up arrow and the last has no down arrow — hidden, not removed, so the column does not jitter as rows move.",
      "tags": [
        "lists"
      ],
      "className": "R08Reorder",
      "lines": 65,
      "files": 1,
      "page": "https://crossfly.dev/examples/reorder",
      "json": "https://crossfly.dev/examples/reorder.json",
      "md": "https://crossfly.dev/examples/reorder.md",
      "java": "https://crossfly.dev/examples/reorder.java",
      "run": "https://crossfly.dev/playground/index.html?ex=reorder"
    },
    {
      "id": "testable",
      "kind": "recipe",
      "title": "Making a screen testable",
      "note": "Tag anything a test must reach. Text breaks on rewording, class breaks on a second instance, index breaks on insertion — a tag breaks only when someone removes it.",
      "tags": [
        "testing"
      ],
      "className": "R09Testable",
      "lines": 77,
      "files": 1,
      "page": "https://crossfly.dev/examples/testable",
      "json": "https://crossfly.dev/examples/testable.json",
      "md": "https://crossfly.dev/examples/testable.md",
      "java": "https://crossfly.dev/examples/testable.java",
      "run": "https://crossfly.dev/playground/index.html?ex=testable"
    },
    {
      "id": "form",
      "kind": "recipe",
      "title": "A form that fights back",
      "note": "setMode does most of the work: it picks the keyboard, the mask and the validation together. A regex on the way out only tells the user they already got it wrong.",
      "tags": [
        "forms",
        "input"
      ],
      "className": "R10Form",
      "lines": 84,
      "files": 1,
      "page": "https://crossfly.dev/examples/form",
      "json": "https://crossfly.dev/examples/form.json",
      "md": "https://crossfly.dev/examples/form.md",
      "java": "https://crossfly.dev/examples/form.java",
      "run": "https://crossfly.dev/playground/index.html?ex=form"
    },
    {
      "id": "telling",
      "kind": "recipe",
      "title": "Telling the user something",
      "note": "Interrupt in proportion to what you need back. A confirmation on something reversible teaches people to dismiss confirmations — including the one that mattered.",
      "tags": [
        "feedback"
      ],
      "className": "R11Telling",
      "lines": 97,
      "files": 1,
      "page": "https://crossfly.dev/examples/telling",
      "json": "https://crossfly.dev/examples/telling.json",
      "md": "https://crossfly.dev/examples/telling.md",
      "java": "https://crossfly.dev/examples/telling.java",
      "run": "https://crossfly.dev/playground/index.html?ex=telling"
    },
    {
      "id": "nav",
      "kind": "recipe",
      "title": "Bottom nav that becomes a rail",
      "note": "One flag — tabsAtBottom — and the strip is a bottom bar on a phone and a rail on a tablet. Resize the window to see it happen.",
      "tags": [
        "navigation",
        "layout"
      ],
      "className": "R12Nav",
      "lines": 65,
      "files": 1,
      "page": "https://crossfly.dev/examples/nav",
      "json": "https://crossfly.dev/examples/nav.json",
      "md": "https://crossfly.dev/examples/nav.md",
      "java": "https://crossfly.dev/examples/nav.java",
      "run": "https://crossfly.dev/playground/index.html?ex=nav"
    },
    {
      "id": "waiting",
      "kind": "recipe",
      "title": "Waiting",
      "note": "Known length gets a bar. Unknown gets a spinner. A finished measurement — a share, a score — gets a ring, and that one is not waiting at all.",
      "tags": [
        "feedback"
      ],
      "className": "R13Waiting",
      "lines": 131,
      "files": 1,
      "page": "https://crossfly.dev/examples/waiting",
      "json": "https://crossfly.dev/examples/waiting.json",
      "md": "https://crossfly.dev/examples/waiting.md",
      "java": "https://crossfly.dev/examples/waiting.java",
      "run": "https://crossfly.dev/playground/index.html?ex=waiting"
    },
    {
      "id": "reflow",
      "kind": "recipe",
      "title": "Cards that reflow",
      "note": "Tell CardGrid the width a card wants, not how many columns to use. Resize the window — the reflow is the framework's, not yours.",
      "tags": [
        "layout"
      ],
      "className": "R14Reflow",
      "lines": 80,
      "files": 1,
      "page": "https://crossfly.dev/examples/reflow",
      "json": "https://crossfly.dev/examples/reflow.json",
      "md": "https://crossfly.dev/examples/reflow.md",
      "java": "https://crossfly.dev/examples/reflow.java",
      "run": "https://crossfly.dev/playground/index.html?ex=reflow"
    },
    {
      "id": "header",
      "kind": "recipe",
      "title": "A header that collapses",
      "note": "header() scrolls away, pinned() never does. Put the identity in the first and the control the reader needs at row 200 in the second.",
      "tags": [
        "layout",
        "lists"
      ],
      "className": "R15Header",
      "lines": 97,
      "files": 1,
      "page": "https://crossfly.dev/examples/header",
      "json": "https://crossfly.dev/examples/header.json",
      "md": "https://crossfly.dev/examples/header.md",
      "java": "https://crossfly.dev/examples/header.java",
      "run": "https://crossfly.dev/playground/index.html?ex=header"
    },
    {
      "id": "charts",
      "kind": "recipe",
      "title": "Charts",
      "note": "autoThemeColors themes the axes, grid and background — but NOT the series. A series left at colour 0 falls back to a library grey, and becomes the one thing on screen ignoring your brand.",
      "tags": [
        "charts"
      ],
      "className": "R16Charts",
      "lines": 102,
      "files": 1,
      "page": "https://crossfly.dev/examples/charts",
      "json": "https://crossfly.dev/examples/charts.json",
      "md": "https://crossfly.dev/examples/charts.md",
      "java": "https://crossfly.dev/examples/charts.java",
      "run": "https://crossfly.dev/playground/index.html?ex=charts"
    },
    {
      "id": "big-list",
      "kind": "recipe",
      "title": "A list too big to build",
      "note": "createView is rare, bindView is constant. Anything slow in bindView — a query, a request, an allocation — is felt directly as stutter while scrolling.",
      "tags": [
        "lists",
        "performance"
      ],
      "className": "R17BigList",
      "lines": 110,
      "files": 1,
      "page": "https://crossfly.dev/examples/big-list",
      "json": "https://crossfly.dev/examples/big-list.json",
      "md": "https://crossfly.dev/examples/big-list.md",
      "java": "https://crossfly.dev/examples/big-list.java",
      "run": "https://crossfly.dev/playground/index.html?ex=big-list"
    },
    {
      "id": "storage",
      "kind": "recipe",
      "title": "Storing something",
      "note": "Add a few notes and reopen the recipe — they are still there. The same code runs on desktop, Android, iOS and the web.",
      "tags": [
        "data"
      ],
      "className": "R18Storage",
      "lines": 154,
      "files": 1,
      "page": "https://crossfly.dev/examples/storage",
      "json": "https://crossfly.dev/examples/storage.json",
      "md": "https://crossfly.dev/examples/storage.md",
      "java": "https://crossfly.dev/examples/storage.java",
      "run": "https://crossfly.dev/playground/index.html?ex=storage"
    },
    {
      "id": "signature",
      "kind": "recipe",
      "title": "Capturing a signature",
      "note": "White pad, not the theme surface: the image is captured to be printed. Under dark mode a themed pad captures white ink on navy, which is unusable downstream.",
      "tags": [
        "input",
        "graphics"
      ],
      "className": "R19Signature",
      "lines": 188,
      "files": 1,
      "page": "https://crossfly.dev/examples/signature",
      "json": "https://crossfly.dev/examples/signature.json",
      "md": "https://crossfly.dev/examples/signature.md",
      "java": "https://crossfly.dev/examples/signature.java",
      "run": "https://crossfly.dev/playground/index.html?ex=signature"
    },
    {
      "id": "pickers",
      "kind": "recipe",
      "title": "Pickers",
      "note": "show(answer -> …) on all of them, and answer is null when the user backed out. The ones needing hardware live in R21, and the system file picker in R31 — those can be refused.",
      "tags": [
        "input"
      ],
      "className": "R20Pickers",
      "lines": 84,
      "files": 1,
      "page": "https://crossfly.dev/examples/pickers",
      "json": "https://crossfly.dev/examples/pickers.json",
      "md": "https://crossfly.dev/examples/pickers.md",
      "java": "https://crossfly.dev/examples/pickers.java",
      "run": "https://crossfly.dev/playground/index.html?ex=pickers"
    },
    {
      "id": "device",
      "kind": "recipe",
      "title": "Camera, barcode and GPS",
      "note": "Ask whether the device HAS it before offering it. PlatformUtils.isSupported answers per capability, at runtime — never a check on which platform this is.",
      "tags": [
        "device"
      ],
      "className": "R21Device",
      "lines": 244,
      "files": 1,
      "page": "https://crossfly.dev/examples/device",
      "json": "https://crossfly.dev/examples/device.json",
      "md": "https://crossfly.dev/examples/device.md",
      "java": "https://crossfly.dev/examples/device.java",
      "run": "https://crossfly.dev/playground/index.html?ex=device"
    },
    {
      "id": "animation",
      "kind": "recipe",
      "title": "Animation",
      "note": "Styles.animationDuration is the house value — match it, so the app moves at one speed. Anything the user waits through twice was too long the first time.",
      "tags": [
        "animation"
      ],
      "className": "R22Animation",
      "lines": 162,
      "files": 1,
      "page": "https://crossfly.dev/examples/animation",
      "json": "https://crossfly.dev/examples/animation.json",
      "md": "https://crossfly.dev/examples/animation.md",
      "java": "https://crossfly.dev/examples/animation.java",
      "run": "https://crossfly.dev/playground/index.html?ex=animation"
    },
    {
      "id": "painting",
      "kind": "recipe",
      "title": "Painting your own",
      "note": "A component is a rectangle that can draw. width/height are yours, getAutoHeight says how much room you want, and onPaint puts something in it.",
      "tags": [
        "graphics"
      ],
      "className": "R23Painting",
      "lines": 119,
      "files": 1,
      "page": "https://crossfly.dev/examples/painting",
      "json": "https://crossfly.dev/examples/painting.json",
      "md": "https://crossfly.dev/examples/painting.md",
      "java": "https://crossfly.dev/examples/painting.java",
      "run": "https://crossfly.dev/playground/index.html?ex=painting"
    },
    {
      "id": "fab",
      "kind": "recipe",
      "title": "Floating action button",
      "note": "It floats over the content, so leave room at the bottom of the list — bottomInset is what stops it covering the last row nobody can then scroll past.",
      "tags": [
        "navigation"
      ],
      "className": "R24Fab",
      "lines": 90,
      "files": 1,
      "page": "https://crossfly.dev/examples/fab",
      "json": "https://crossfly.dev/examples/fab.json",
      "md": "https://crossfly.dev/examples/fab.md",
      "java": "https://crossfly.dev/examples/fab.java",
      "run": "https://crossfly.dev/playground/index.html?ex=fab"
    },
    {
      "id": "map",
      "kind": "recipe",
      "title": "A map with pins",
      "note": "OSM needs no API key, so a map works the moment you write it. Opens in a window of its own — a native view inside a scrolling page floats over everything around it.",
      "tags": [
        "device"
      ],
      "className": "R25Map",
      "lines": 124,
      "files": 1,
      "page": "https://crossfly.dev/examples/map",
      "json": "https://crossfly.dev/examples/map.json",
      "md": "https://crossfly.dev/examples/map.md",
      "java": "https://crossfly.dev/examples/map.java",
      "run": "https://crossfly.dev/playground/index.html?ex=map"
    },
    {
      "id": "images",
      "kind": "recipe",
      "title": "Images",
      "note": "Decode once, keep the Image, reuse it. Loading inside onPaint or inside a list's bind step is what makes scrolling stutter where the pictures are.",
      "tags": [
        "images"
      ],
      "className": "R26Images",
      "lines": 97,
      "files": 1,
      "page": "https://crossfly.dev/examples/images",
      "json": "https://crossfly.dev/examples/images.json",
      "md": "https://crossfly.dev/examples/images.md",
      "java": "https://crossfly.dev/examples/images.java",
      "run": "https://crossfly.dev/playground/index.html?ex=images"
    },
    {
      "id": "focus",
      "kind": "recipe",
      "title": "Keyboard and focus",
      "note": "Tab follows the order you ADDED the fields, not where they ended up on screen. Build in reading order and the tab order is correct without being designed.",
      "tags": [
        "input"
      ],
      "className": "R27Focus",
      "lines": 110,
      "files": 1,
      "page": "https://crossfly.dev/examples/focus",
      "json": "https://crossfly.dev/examples/focus.json",
      "md": "https://crossfly.dev/examples/focus.md",
      "java": "https://crossfly.dev/examples/focus.java",
      "run": "https://crossfly.dev/playground/index.html?ex=focus"
    },
    {
      "id": "own-component",
      "kind": "recipe",
      "title": "Writing your own component",
      "note": "Search dev/crossfly/ui first — most of what you are about to write is already there. When it genuinely is not, this is the shape.",
      "tags": [
        "components"
      ],
      "className": "R28Component",
      "lines": 161,
      "files": 1,
      "page": "https://crossfly.dev/examples/own-component",
      "json": "https://crossfly.dev/examples/own-component.json",
      "md": "https://crossfly.dev/examples/own-component.md",
      "java": "https://crossfly.dev/examples/own-component.java",
      "run": "https://crossfly.dev/playground/index.html?ex=own-component"
    },
    {
      "id": "brand",
      "kind": "recipe",
      "title": "Brand and dark mode",
      "note": "One seed generates the whole palette, for both themes. Nothing below names a colour, which is why the switch needs no rebuild.",
      "tags": [
        "theming"
      ],
      "className": "R29Brand",
      "lines": 129,
      "files": 1,
      "page": "https://crossfly.dev/examples/brand",
      "json": "https://crossfly.dev/examples/brand.json",
      "md": "https://crossfly.dev/examples/brand.md",
      "java": "https://crossfly.dev/examples/brand.java",
      "run": "https://crossfly.dev/playground/index.html?ex=brand"
    },
    {
      "id": "empty-states",
      "kind": "recipe",
      "title": "Empty, loading and broken",
      "note": "Three of these are usually left to render as blank space. Blank space reads as a bug, never as an answer.",
      "tags": [
        "feedback",
        "lists"
      ],
      "className": "R30Empty",
      "lines": 160,
      "files": 1,
      "page": "https://crossfly.dev/examples/empty-states",
      "json": "https://crossfly.dev/examples/empty-states.json",
      "md": "https://crossfly.dev/examples/empty-states.md",
      "java": "https://crossfly.dev/examples/empty-states.java",
      "run": "https://crossfly.dev/playground/index.html?ex=empty-states"
    },
    {
      "id": "files",
      "kind": "recipe",
      "title": "Files, sharing and printing",
      "note": "Ask the capability, not the platform. Phones, Windows 10+ and the Mac have the picker and the share sheet; Linux has no share sheet; a browser has the picker always and the sheet only sometimes. Printing takes a PDF, and every platform's own dialog does the rest.",
      "tags": [
        "device"
      ],
      "className": "R31Files",
      "lines": 249,
      "files": 1,
      "page": "https://crossfly.dev/examples/files",
      "json": "https://crossfly.dev/examples/files.json",
      "md": "https://crossfly.dev/examples/files.md",
      "java": "https://crossfly.dev/examples/files.java",
      "run": "https://crossfly.dev/playground/index.html?ex=files"
    },
    {
      "id": "video",
      "kind": "recipe",
      "title": "Video playback",
      "note": "A native surface over the canvas, like the map. Ask the capability first: all five platforms have it now, and one that does not says so instead of showing black.",
      "tags": [
        "device",
        "media"
      ],
      "className": "R32Video",
      "lines": 284,
      "files": 1,
      "page": "https://crossfly.dev/examples/video",
      "json": "https://crossfly.dev/examples/video.json",
      "md": "https://crossfly.dev/examples/video.md",
      "java": "https://crossfly.dev/examples/video.java",
      "run": "https://crossfly.dev/playground/index.html?ex=video"
    },
    {
      "id": "webview",
      "kind": "recipe",
      "title": "Embedded web page",
      "note": "A native browser over the canvas, at this component's rect. On the web it is an <iframe>, and script only reaches a page from your own origin.",
      "tags": [
        "device",
        "media"
      ],
      "className": "R33WebView",
      "lines": 120,
      "files": 1,
      "page": "https://crossfly.dev/examples/webview",
      "json": "https://crossfly.dev/examples/webview.json",
      "md": "https://crossfly.dev/examples/webview.md",
      "java": "https://crossfly.dev/examples/webview.java",
      "run": "https://crossfly.dev/playground/index.html?ex=webview"
    },
    {
      "id": "odometer",
      "kind": "recipe",
      "title": "Reading an odometer (OCR)",
      "note": "Show a strip, read the strip. The viewfinder is only as tall as the digits, and the recognizer gets that band and nothing else — so the speedometer and the trip meter around it never compete with the number.",
      "tags": [
        "device"
      ],
      "className": "R34Odometer",
      "lines": 402,
      "files": 1,
      "page": "https://crossfly.dev/examples/odometer",
      "json": "https://crossfly.dev/examples/odometer.json",
      "md": "https://crossfly.dev/examples/odometer.md",
      "java": "https://crossfly.dev/examples/odometer.java",
      "run": "https://crossfly.dev/playground/index.html?ex=odometer"
    },
    {
      "id": "app-crud",
      "kind": "app",
      "title": "Depot — offline-first CRUD",
      "note": "One shell, three listings over a local database, a detail and a long form. A thing you navigate away from is a Window; a thing you switch between is not.",
      "tags": [
        "data",
        "lists",
        "forms"
      ],
      "className": "CrudApp",
      "lines": 2025,
      "files": 11,
      "page": "https://crossfly.dev/examples/app-crud",
      "json": "https://crossfly.dev/examples/app-crud.json",
      "md": "https://crossfly.dev/examples/app-crud.md",
      "java": "https://crossfly.dev/examples/app-crud.java",
      "run": "https://crossfly.dev/playground/index.html?ex=app-crud"
    },
    {
      "id": "app-dashboard",
      "kind": "app",
      "title": "Pulse — a dashboard",
      "note": "One screen, read rather than operated: a headline number, a KPI grid and charts, all re-fed by the single control that changes the period.",
      "tags": [
        "charts"
      ],
      "className": "DashboardApp",
      "lines": 495,
      "files": 2,
      "page": "https://crossfly.dev/examples/app-dashboard",
      "json": "https://crossfly.dev/examples/app-dashboard.json",
      "md": "https://crossfly.dev/examples/app-dashboard.md",
      "java": "https://crossfly.dev/examples/app-dashboard.java",
      "run": "https://crossfly.dev/playground/index.html?ex=app-dashboard"
    },
    {
      "id": "app-capture",
      "kind": "app",
      "title": "Fieldwork — capturing from the device",
      "note": "Every capture has three states, not two: empty, captured, and unavailable on this device — and the third is the one a phone-only developer never sees.",
      "tags": [
        "device"
      ],
      "className": "CaptureApp",
      "lines": 445,
      "files": 2,
      "page": "https://crossfly.dev/examples/app-capture",
      "json": "https://crossfly.dev/examples/app-capture.json",
      "md": "https://crossfly.dev/examples/app-capture.md",
      "java": "https://crossfly.dev/examples/app-capture.java",
      "run": "https://crossfly.dev/playground/index.html?ex=app-capture"
    },
    {
      "id": "app-wizard",
      "kind": "app",
      "title": "Order entry — a wizard",
      "note": "A linear flow that validates per step, lets the reader walk back freely, and ends on a review built from the same data the steps collected.",
      "tags": [
        "forms",
        "navigation"
      ],
      "className": "WizardApp",
      "lines": 1017,
      "files": 7,
      "page": "https://crossfly.dev/examples/app-wizard",
      "json": "https://crossfly.dev/examples/app-wizard.json",
      "md": "https://crossfly.dev/examples/app-wizard.md",
      "java": "https://crossfly.dev/examples/app-wizard.java",
      "run": "https://crossfly.dev/playground/index.html?ex=app-wizard"
    }
  ]
}