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 {
-
Advanced

Wizard with content and controls

showPanel renders each step body and shows only the active one, the same contract Tabs uses. controls adds Back, Skip and Next, which becomes Finish on the last step. nextDisabled is how a form holds the step: the component never validates anything itself, so the page sets it while the step is incomplete and clears it once the step passes.

+
Advanced

Wizard with content and controls

showPanel renders each step body and shows only the active one, the same contract Tabs uses. controls adds Back, Skip and Next, which becomes Finish on the last step.

03
Live preview
-
+
Component usage.wrn
@@ -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 {
-
Advanced

Held until the step is valid

The same wizard with nextDisabled set. Next stays held, so a form can keep the reader on the step until it validates.

+
Advanced

Held until the step is valid

The same wizard with nextDisabled set. The component never validates anything itself: the page owns the form, sets this while the step is incomplete, and clears it once the step passes.

04
Live preview
-
+
Component usage.wrn
@@ -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",