global.css defined its own fixed palette -- bg 0b1020, text e7ecff -- while Wire UI surfaces follow the theme tokens. Switching to light turned the cards light and left this text light with them, so the sign-in form rendered at a contrast of about 1.1 and could not be read. The palette now derives from the wire tokens, and the body wash is tinted from the primary token rather than a fixed blue. Measured on the login form: light goes from 1.1 to 17.7, dark stays at 18.2. Anything an application hardcodes has to be themed as well, or it only ever looks right in one mode. The navigation tour is also reachable now: a Navigation entry in the site nav, translated in both locales, and the page adopts the public layout so there is a way back out of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
439 lines
13 KiB
Plaintext
439 lines
13 KiB
Plaintext
// 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 {
|
|
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(--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;
|
|
}
|
|
}
|
|
}
|
|
}
|