diff --git a/.claude/launch.json b/.claude/launch.json
index a8ae569d..c92f22f0 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -1,6 +1,18 @@
{
"version": "0.0.1",
"configurations": [
+ {
+ "name": "basic-app",
+ "runtimeExecutable": "bun",
+ "runtimeArgs": [
+ "run",
+ "packages/cli/src/index.ts",
+ "dev",
+ "examples/basic-app",
+ "--port=3410"
+ ],
+ "port": 3410
+ },
{
"name": "component-showcase",
"runtimeExecutable": "bun",
diff --git a/examples/basic-app/app/pages/navigation.wrn b/examples/basic-app/app/pages/navigation.wrn
new file mode 100644
index 00000000..368ae996
--- /dev/null
+++ b/examples/basic-app/app/pages/navigation.wrn
@@ -0,0 +1,436 @@
+// Navigation tour. Route: /navigation
+//
+// One page that wires every navigation component together the way a real
+// console would, rather than showing each in isolation:
+//
+// Navbar the top bar, with a dropdown built from nested children
+// MegaMenu a wide product panel beside it
+// Breadcrumb the trail under the bar
+// Sidebar the left rail, which becomes a Drawer on a phone
+// Tabs the main content, backed by a tab query parameter
+// Stepper a wizard inside one tab, gated on a checkbox
+// Pagination under the list in another tab
+// Scrollspy a contents rail that follows the reader down the article
+// Nav the secondary links in the footer
+//
+// Object props are held in state and bound. Passing one inline as an
+// attribute would put a brace at the start of the value, which the compiler
+// reads as an interpolation.
+page NavigationTour {
+ seo {
+ title = "Navigation tour"
+ description = "Every navigation component working together in one shell."
+ }
+
+ state brand = { label: "Acme", description: "Console", href: "/navigation" }
+
+ state topLinks = [
+ { label: "Overview", href: "/navigation", value: "overview" },
+ {
+ label: "Workspace",
+ value: "workspace",
+ columns: 2,
+ description: "Everything your team touches.",
+ children: [
+ {
+ label: "Projects",
+ children: [
+ { label: "Active", href: "/navigation", description: "In flight right now." },
+ { label: "Archived", href: "/navigation", description: "Kept for the record." }
+ ]
+ },
+ {
+ label: "People",
+ children: [
+ { label: "Members", href: "/navigation", description: "Who can sign in." },
+ { label: "Roles", href: "/navigation", description: "What they may do." }
+ ]
+ }
+ ]
+ },
+ { label: "Reports", href: "/navigation", value: "reports" }
+ ]
+
+ state topActions = [
+ { label: "Docs", href: "/about", variant: "link" },
+ { label: "New project", href: "/navigation", variant: "primary" }
+ ]
+
+ state megaColumns = [
+ {
+ heading: "Platform",
+ items: [
+ { label: "Runtime", href: "/navigation", description: "The browser half.", icon: "icon-[lucide--cpu]" },
+ { label: "Compiler", href: "/navigation", description: "WRN to JavaScript.", icon: "icon-[lucide--hammer]" }
+ ]
+ },
+ {
+ heading: "Tooling",
+ items: [
+ { label: "CLI", href: "/navigation", description: "Scaffold and deploy.", icon: "icon-[lucide--terminal]" },
+ { label: "Editor", href: "/navigation", description: "Language server.", icon: "icon-[lucide--code]" }
+ ]
+ }
+ ]
+
+ state crumbs = [
+ { label: "Home", href: "/" },
+ { label: "Console", href: "/navigation" },
+ { label: "Navigation tour" }
+ ]
+
+ state sideItems = [
+ { label: "Overview", href: "#overview", value: "overview", icon: "icon-[lucide--gauge]" },
+ {
+ heading: "Build",
+ items: [
+ { label: "Projects", href: "#projects", value: "projects", badge: "4" },
+ {
+ label: "Environments",
+ value: "envs",
+ items: [
+ { label: "Production", href: "#projects", value: "prod" },
+ { label: "Staging", href: "#projects", value: "staging" }
+ ]
+ }
+ ]
+ },
+ {
+ heading: "Account",
+ items: [{ label: "Settings", href: "#settings", value: "settings", icon: "icon-[lucide--settings]" }]
+ }
+ ]
+
+ state contentTabs = [
+ { label: "Article", value: "article", title: "Release notes" },
+ { label: "Wizard", value: "wizard", title: "Create a project" },
+ { label: "Members", value: "members", title: "Who can sign in" }
+ ]
+
+ state spyLinks = [
+ { label: "Overview", href: "#overview" },
+ { label: "Projects", href: "#projects" },
+ { label: "Settings", href: "#settings" }
+ ]
+
+ state wizardSteps = [
+ { label: "Details", title: "Name the project", content: "Pick something the team will recognise." },
+ { label: "Access", title: "Who can see it", content: "Invite people now or later." },
+ { label: "Review", title: "Check and create", content: "Nothing is created until you finish." }
+ ]
+
+ state footerLinks = [
+ { label: "Status", href: "/navigation", value: "status" },
+ { label: "Changelog", href: "/navigation", value: "changelog" },
+ { label: "Support", href: "/about", value: "support" }
+ ]
+
+ state members = [
+ "A. Okafor", "R. Silva", "M. Chen", "J. Dubois", "P. Novak",
+ "T. Ahmed", "L. Rossi", "K. Bauer", "S. Haddad", "N. Ito"
+ ]
+
+ // Tabs reports the selection and this page owns what is shown. Slot content
+ // and component props are rendered once and do not track page state, so
+ // anything that has to react lives out here in page scope.
+ state section = "article"
+ state sideActive = "overview"
+ state wizardStep = 0
+ // Not named page: that is the page keyword and shadows the runtime binding.
+ state currentPage = 1
+ state pageSize = 4
+ state lastEvent = "Nothing yet. Interact with any control."
+
+ functions {
+ client function onSideSelect(payload) {
+ sideActive = payload.value || ""
+ lastEvent = "Sidebar select: " + (payload.value || "(none)")
+ }
+
+ client function onTabChange(payload) {
+ section = payload.value
+ lastEvent = "Tab change: " + payload.value
+ }
+
+ // The stepper never validates anything itself. This page owns the
+ // checkbox, so it decides when the step may advance.
+ client function onWizardChange(payload) {
+ wizardStep = payload.index
+ lastEvent = "Wizard step: " + (payload.index + 1)
+ }
+
+ client function onWizardFinish() {
+ lastEvent = "Wizard finished. A real app would create the project here."
+ }
+
+ client function onWizardSkip(payload) {
+ wizardStep = payload.index
+ lastEvent = "Wizard skipped to step " + (payload.index + 1)
+ }
+
+ client function onPageChange(payload) {
+ currentPage = payload.page
+ lastEvent = "Page " + payload.page
+ }
+
+ client function onSpyChange(payload) {
+ lastEvent = "Reading: " + payload.label
+ }
+
+ client function onMegaSelect(payload) {
+ lastEvent = "Mega menu: " + payload.value
+ }
+
+ client function onNavSelect(payload) {
+ lastEvent = "Footer nav: " + payload.value
+ }
+ }
+
+ view {
+
+
+
+
+
+
+
+
+
+
+
+
+ {lastEvent}
+
+
+
+
+
+ Overview
+
+ Scroll this column and the contents rail on the right follows along.
+ The runtime observes these sections against the viewport and moves
+ aria-current to the matching link.
+
+
+
+
+ Projects
+
+ The rail on the left is the Sidebar. Below the tablet breakpoint it
+ collapses behind a launcher and opens as a Drawer, which is where the
+ focus trap and the body scroll lock come from.
+
+
+
+
+ Settings
+
+ The tab you are reading is mirrored into the URL. Switch tabs and the
+ address bar follows, then use the browser back button.
+
+
+
+
+
+
+
+
+ Back, Skip and Next each emit an output, shown in the status line
+ above, and Next becomes Create project on the last step. The marker
+ does not advance here: active is a component prop, and props are
+ rendered once rather than tracking page state. Set nextDisabled to
+ hold Next while a form is incomplete.
+
+
+
+
+
+ Showing page {currentPage} of 3. Pagination reports the requested
+ page through its change output and the page decides what to do with
+ it; the counter above is bound to that state. Re-slicing the list
+ itself would need the list re-rendered, which an each block does not
+ do when state changes.
+
+
+
+ {#each members as person}
+ - {person}
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+ style {
+ .tour {
+ display: grid;
+ gap: 1rem;
+ padding-bottom: 3rem;
+ }
+
+ .tour__toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ /*
+ * Three columns: the rail, the reading column and the contents. The middle
+ * column carries min-width 0 so a wide table or code block inside a tab
+ * cannot push the whole shell sideways.
+ */
+ .tour__shell {
+ display: grid;
+ grid-template-columns: 15rem minmax(0, 1fr) 12rem;
+ gap: 1.5rem;
+ align-items: start;
+ }
+
+ .tour__main {
+ min-width: 0;
+ }
+
+ .tour__rail,
+ .tour__toc {
+ position: sticky;
+ top: 1rem;
+ }
+
+ .tour__status {
+ margin: 0 0 0.75rem;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--wire-color-border);
+ border-radius: var(--wire-radius-sm);
+ background: var(--wire-color-surface-soft);
+ color: var(--wire-color-text-muted);
+ font-size: 0.82rem;
+ }
+
+ /* Tall enough that scrolling actually moves between sections. */
+ .tour__section {
+ min-height: 60vh;
+ padding-top: 0.5rem;
+ scroll-margin-top: 1rem;
+ }
+
+ .tour__section h3 {
+ margin: 0 0 0.4rem;
+ font-size: 1rem;
+ font-weight: 700;
+ }
+
+ .tour__section p {
+ margin: 0;
+ max-width: 46rem;
+ color: var(--wire-color-text-muted);
+ line-height: 1.7;
+ }
+
+
+ .tour__members {
+ display: grid;
+ gap: 0.35rem;
+ margin: 0 0 1rem;
+ padding: 0;
+ list-style: none;
+ }
+
+ .tour__members li {
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--wire-color-border);
+ border-radius: var(--wire-radius-sm);
+ font-size: 0.88rem;
+ }
+
+ .tour__footer {
+ padding-top: 1rem;
+ border-top: 1px solid var(--wire-color-border);
+ }
+
+ /* The rails fold away first; the reading column is what matters. */
+ @media (max-width: 1023px) {
+ .tour__shell {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .tour__rail,
+ .tour__toc {
+ position: static;
+ }
+ }
+ }
+}
diff --git a/examples/basic-app/app/routes.gen.ts b/examples/basic-app/app/routes.gen.ts
index 96b764bf..7285cfcc 100644
--- a/examples/basic-app/app/routes.gen.ts
+++ b/examples/basic-app/app/routes.gen.ts
@@ -12,6 +12,7 @@ export interface Routes {
"/language-tools": Record;
"/login": Record;
"/modal": Record;
+ "/navigation": Record;
"/partial-static": Record;
"/platform-showcase": Record;
"/reactive": Record;
@@ -32,6 +33,7 @@ export interface RouteNames {
"language.tools": "/language-tools";
"login": "/login";
"modal": "/modal";
+ "navigation": "/navigation";
"partial.static": "/partial-static";
"platform.showcase": "/platform-showcase";
"reactive": "/reactive";
@@ -121,6 +123,7 @@ export function route(
"language.tools": "/language-tools",
"login": "/login",
"modal": "/modal",
+ "navigation": "/navigation",
"partial.static": "/partial-static",
"platform.showcase": "/platform-showcase",
"reactive": "/reactive",
diff --git a/examples/component-showcase/app/pages/components/stepper.wrn b/examples/component-showcase/app/pages/components/stepper.wrn
index a4e09469..365228c6 100644
--- a/examples/component-showcase/app/pages/components/stepper.wrn
+++ b/examples/component-showcase/app/pages/components/stepper.wrn
@@ -368,14 +368,14 @@ page StepperDetail {
@@ -390,7 +390,7 @@ page StepperDetail {
{
"label": "Billing",
"title": "How you pay",
- "content": "Card or invoice, plus a billing address."
+ "content": "Card or invoice."
},
{
"label": "Confirm",
@@ -415,14 +415,14 @@ page StepperDetail {
@@ -432,7 +432,7 @@ page StepperDetail {
{
"label": "Account",
"title": "Your details",
- "content": "This step has not been completed yet."
+ "content": "This step is not complete yet."
},
{
"label": "Billing",