A directory of React mounted full-bleed under the page slug.

Single-page app pages

When a view cannot express it and a component inside a block page is still too constrained — the app needs the viewport, its own URLs, a multi-screen flow — declare the page as a single-page app: a directory of React beside the app definition, built at install into the app's extension and mounted full-bleed in the workspace under the page's slug.

app dispatch "Dispatch" {}

page board "Runs" list Run {
  board Run summary [driver, stops]
}

/// The driver console: map, stop list, proof-of-delivery capture.
page console "Driver Console" spa from "console/" using sdk write, fsm, files {}

nav [board, console]

home board

console/index.tsx default-exports the page. Every .ts and .tsx file under the directory is part of it, and files import each other with relative paths; react, react-router and @fastyoke/sdk come from the platform. Stylesheets and images are not bundled: use the host's utility classes or style props, and reference assets by URL.

The page owns its URL space. It is mounted at /workspace/app/<app>/console and everything after that is the page's: /console/run/abc/stop/2 reaches it as subpath = "run/abc/stop/2", deep links survive a reload, and the sidebar keeps "Driver Console" active. The host does not impose a router. The entry receives base, subpath and navigate, and any depth of the tree reads them with useSpaRoute(); react-router users add one line:

import { MemoryRouter, Route, Routes } from 'react-router';
import { SpaRouterSync, useSpaRoute } from '@fastyoke/sdk';

export default function Console() {
  const route = useSpaRoute();
  return (
    <MemoryRouter initialEntries={[`/${route.subpath}`]}>
      <SpaRouterSync route={route} />
      <Routes>
        <Route path="/" element={<RunList />} />
        <Route path="/run/:id/stop/:n" element={<Stop />} />
      </Routes>
    </MemoryRouter>
  );
}

The grant is a component's: using sdk write, fsm gives the page a client that reads, writes and fires transitions; no clause gives it props and routing only. It runs on the same short-lived, scoped token inside the tenant's session, so it cannot reach a route the grant does not cover. A page with unsaved state calls useLeaveGuard(dirty) so the shell asks before leaving.

The directory travels with the source like a component's file: the command line, the Studio and the MCP server inline it before install, and yokec check reads it next to the .yoke file, refusing a missing index.tsx, a file it cannot bundle or an import that climbs out of the directory. The page is a page: it appears in nav, may be home, and comes back as spa from "console/" when the app is exported.