docs(example): one page wiring the whole navigation group together
examples/basic-app/app/pages/navigation.wrn puts all nine navigation
components in a single console shell instead of showing each alone: Navbar
with a nested dropdown, MegaMenu beside it, Breadcrumb, Sidebar as a rail that
becomes a Drawer, Tabs backed by a query parameter, a Stepper wizard,
Pagination, Scrollspy following the article, and Nav in the footer.
Three framework limits shaped the layout and are written into the page rather
than hidden:
- object props are held in state and bound, because a brace at the start of
an attribute is read as an interpolation
- a shared function on a page is compiled standalone and cannot see page
state by name, so state is passed as arguments
- component props and slot content render once and do not track page state,
so anything that has to react lives in page scope; Tabs reports the
selection and the page owns what is shown
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
<div class="tour">
|
||||
<Navbar
|
||||
brand={brand}
|
||||
items={topLinks}
|
||||
actions={topActions}
|
||||
active="overview"
|
||||
label="Primary navigation"
|
||||
mobileLabel="Toggle navigation"
|
||||
/>
|
||||
|
||||
<div class="tour__toolbar">
|
||||
<MegaMenu
|
||||
label="Products"
|
||||
icon="icon-[lucide--box]"
|
||||
columns={megaColumns}
|
||||
footer="Not sure where to start? Talk to us."
|
||||
@select="onMegaSelect(payload)"
|
||||
/>
|
||||
<Breadcrumb items={crumbs} />
|
||||
</div>
|
||||
|
||||
<div class="tour__shell">
|
||||
<aside class="tour__rail">
|
||||
<Sidebar
|
||||
items={sideItems}
|
||||
active={sideActive}
|
||||
label="Console"
|
||||
mobileLabel="Open navigation"
|
||||
drawerTitle="Console"
|
||||
@select="onSideSelect(payload)"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<main class="tour__main">
|
||||
<p class="tour__status" role="status">{lastEvent}</p>
|
||||
|
||||
<Tabs
|
||||
items={contentTabs}
|
||||
active="article"
|
||||
mode="url"
|
||||
param="tab"
|
||||
label="Console sections"
|
||||
@change="onTabChange(payload)"
|
||||
/>
|
||||
|
||||
<section class="tour__panel" data-show="section === 'article'">
|
||||
<section id="overview" class="tour__section">
|
||||
<h3>Overview</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="projects" class="tour__section">
|
||||
<h3>Projects</h3>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="settings" class="tour__section">
|
||||
<h3>Settings</h3>
|
||||
<p>
|
||||
The tab you are reading is mirrored into the URL. Switch tabs and the
|
||||
address bar follows, then use the browser back button.
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="tour__panel" data-show="section === 'wizard'">
|
||||
<Stepper
|
||||
steps={wizardSteps}
|
||||
active={wizardStep}
|
||||
showPanel={true}
|
||||
controls={true}
|
||||
allowSkip={true}
|
||||
nextDisabled={false}
|
||||
backLabel="Back"
|
||||
nextLabel="Next"
|
||||
skipLabel="Skip"
|
||||
finishLabel="Create project"
|
||||
label="Create a project"
|
||||
@change="onWizardChange(payload)"
|
||||
@skip="onWizardSkip(payload)"
|
||||
@finish="onWizardFinish()"
|
||||
/>
|
||||
|
||||
<p class="tour__note">
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="tour__panel" data-show="section === 'members'">
|
||||
<p class="tour__note">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<ul class="tour__members">
|
||||
{#each members as person}
|
||||
<li>{person}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
pageSize={pageSize}
|
||||
total={members.length}
|
||||
variant="numbered"
|
||||
@change="onPageChange(payload)"
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside class="tour__toc">
|
||||
<Scrollspy
|
||||
items={spyLinks}
|
||||
heading="On this page"
|
||||
active="#overview"
|
||||
@change="onSpyChange(payload)"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<footer class="tour__footer">
|
||||
<Nav
|
||||
items={footerLinks}
|
||||
active="status"
|
||||
label="Secondary"
|
||||
collapsible={false}
|
||||
@select="onNavSelect(payload)"
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export interface Routes {
|
||||
"/language-tools": Record<string, never>;
|
||||
"/login": Record<string, never>;
|
||||
"/modal": Record<string, never>;
|
||||
"/navigation": Record<string, never>;
|
||||
"/partial-static": Record<string, never>;
|
||||
"/platform-showcase": Record<string, never>;
|
||||
"/reactive": Record<string, never>;
|
||||
@@ -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<N extends RouteName>(
|
||||
"language.tools": "/language-tools",
|
||||
"login": "/login",
|
||||
"modal": "/modal",
|
||||
"navigation": "/navigation",
|
||||
"partial.static": "/partial-static",
|
||||
"platform.showcase": "/platform-showcase",
|
||||
"reactive": "/reactive",
|
||||
|
||||
Reference in New Issue
Block a user