439 lines
13 KiB
Plaintext
439 lines
13 KiB
Plaintext
// Navigation tour. Route: /navigation
|
|
//
|
|
// One page that connects 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 {
|
|
layout = "public"
|
|
|
|
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(--wrn-color-border);
|
|
border-radius: var(--wrn-radius-sm);
|
|
background: var(--wrn-color-surface-soft);
|
|
color: var(--wrn-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(--wrn-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(--wrn-color-border);
|
|
border-radius: var(--wrn-radius-sm);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.tour__footer {
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--wrn-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;
|
|
}
|
|
}
|
|
}
|
|
}
|