| name | webkit-navigation |
| description | Two navigation shells for UI on @aziontech/webkit — the persistent console shell (AppLayout + AppSidebar: full sidebar + in-content GlobalHeader, always shown) for hub/browse pages, and the focused creation shell (CreationHeader: no sidebar, GlobalHeader with back + brand + breadcrumb) for create/edit/deploy flows. A page picks exactly ONE shell, renders exactly ONE GlobalHeader, and the signed-in user stays visible in both. Also fixes the content layout inside the persistent shell: a --spacing-md inset (matching the header), the PageHeading out of the card, list tables in a flush borderless CardBox, and resource-detail tab pages sharing the same rhythm. Built on @aziontech/webkit/global-header, /sidebar, /breadcrumb. |
Skill: webkit-navigation
Purpose
Every screen in a console lives in one of two navigation shells, and picking the wrong one — or
worse, blending them — is where a prototype stops feeling like one product. A browse page with no way
back to the rest of the console strands the user; a focused create flow that keeps the full sidebar
lets the user wander off mid-task and lose their work. This skill fixes which shell a page uses, how
each is composed on @aziontech/webkit, and the one invariant they share: the signed-in user is
always visible. It is the layout-shell companion to webkit-form (what goes inside a focused
flow) and webkit-ux-heuristics (states + feedback).
How to use
/webkit-navigation
Apply the shell rules below to any page or flow you build in this conversation.
/webkit-navigation <file>
Review the file against the rules and output, per gap:
- the exact line / element (quoted),
- which rule it breaks (1 short sentence),
- the concrete fix, naming the shell or webkit primitive to use.
When to invoke
- Starting a new page and deciding how it sits in the app (a hub page vs a create/edit/deploy flow).
- The user asks "which header goes here", "should this have a sidebar", "how do I get back", "the user
disappeared on the create screen".
- Reviewing a screen for whether it uses the right shell and shows exactly one header.
How to find the components
Never guess an import path, and never copy one from another repo or sample app — files move. Resolve
every webkit primitive the same way:
- Ask the webkit MCP —
suggest_component in plain words ("sidebar", "app header", "breadcrumb").
- Or read
node_modules/@aziontech/webkit/catalog.json — every key under imports is a real
published subpath. If a subpath is not a key there, it does not exist.
The primitives you compose the shells from — sidebar (+ sidebar-header, sidebar-group,
sidebar-footer), global-header, breadcrumb — all resolve this way. There is no
@aziontech/webkit/app-layout and no @aziontech/webkit/creation-header: the two shells are
components you compose in your own app (shown below), not webkit imports — never copy a dead import
for them.
For spacing / color / typography use the @aziontech/theme tokens (the webkit MCP lists them) —
--spacing-md, --spacing-lg, var(--bg-canvas), text-heading-xxs — never raw px.
The two shells
A page picks exactly one. The choice is not stylistic — it follows from what the page is for.
| Persistent console shell | Focused creation shell |
|---|
| What it is | A shell you compose (e.g. components/AppShell.vue) around a full Sidebar + one in-content GlobalHeader | A shell you compose (e.g. components/CreationShell.vue): one GlobalHeader (back + brand + breadcrumb + avatar), no sidebar |
| Use for | Hubs, lists, dashboards, settings — anywhere the user browses and moves laterally | Create / edit / deploy / wizard — a single task with a start and an end |
| Sidebar | Yes — full console nav, always present | No — removed so the task is the only focus |
| Header | GlobalHeader inside the content zone: breadcrumb + Create/Docs actions + account avatar | GlobalHeader: back button + Azion brand + breadcrumb + account avatar |
| Way out | Lateral — pick any other module from the sidebar | Linear — the back button (and breadcrumb) returns to where the flow started |
| Signed-in user | Sidebar footer (avatar + account menu) and header avatar | Header avatar (right) |
| Typical pages | dashboards, list / index pages, settings hubs, resource-detail pages | create / edit forms, multi-step wizards, deploy / publish flows |
Shell 1 — persistent console
For pages the user browses. A full-height Sidebar on the left carries the whole console
navigation (grouped by product area) plus the team switcher and the user footer; one GlobalHeader
lives inside the content zone and carries the module breadcrumb and the primary actions. The page
renders only its own content through the shell's default slot.
The shell is yours to compose — there is no @aziontech/webkit/app-layout. Build it once from
the webkit primitives and reuse it on every browse page:
<!-- components/AppShell.vue — a shell YOU compose in your app. NOT importable
from webkit; it wraps the webkit primitives below (each resolved via the
MCP / catalog, see "How to find the components"). -->
<script setup lang="ts">
import Sidebar from '@aziontech/webkit/sidebar'
import SidebarHeader from '@aziontech/webkit/sidebar-header'
import SidebarGroup from '@aziontech/webkit/sidebar-group'
import SidebarFooter from '@aziontech/webkit/sidebar-footer'
import GlobalHeader from '@aziontech/webkit/global-header'
import Breadcrumb from '@aziontech/webkit/breadcrumb'
defineProps<{
active: string
breadcrumb: { label: string; href?: string }[]
collapsible?: boolean // default true — the sidebar toggle in GlobalHeader.Left
}>()
</script>
<template>
<div class="flex h-dvh">
<!-- The way out: full console nav + team switcher + the signed-in user -->
<Sidebar :active="active">
<SidebarHeader><!-- team / account switcher --></SidebarHeader>
<SidebarGroup label="Build"><!-- grouped module links --></SidebarGroup>
<SidebarFooter><!-- avatar + name + account menu (theme / logout) --></SidebarFooter>
</Sidebar>
<div class="flex min-w-0 flex-1 flex-col">
<!-- EXACTLY ONE GlobalHeader, inside the content zone -->
<GlobalHeader>
<GlobalHeader.Left>
<!-- sidebar toggle (pi pi-bars) + the location breadcrumb -->
<Breadcrumb :items="breadcrumb" />
</GlobalHeader.Left>
<GlobalHeader.Right>
<!-- Create / Docs actions + the account avatar (always visible) -->
</GlobalHeader.Right>
</GlobalHeader>
<!-- The shell owns the --spacing-md inset ONCE; pages never re-pad -->
<main class="min-h-0 flex-1 overflow-auto p-[var(--spacing-md)]">
<slot />
</main>
</div>
</div>
</template>
A browse page then imports your shell — not a webkit path — and renders only its content:
<script setup lang="ts">
import AppShell from '@/components/AppShell.vue' // your shell — NOT a webkit import
</script>
<template>
<AppShell
active="applications"
:breadcrumb="[{ label: 'Applications' }]"
>
<!-- page content only -->
</AppShell>
</template>
- The sidebar is the way out — the user moves laterally to any other module. Never remove it on a
browse page.
- The breadcrumb names where the user is on every level: a single crumb at the first level
(
[{ label: 'Applications' }]), a full trail (Applications › New Application) once nested.
The header always names the location (breadcrumb) + the sidebar toggle (collapsible)
- The breadcrumb renders on every level. The header always names where the user is: a page that
passes a single first-level crumb (
[{ label: 'Home' }], [{ label: 'Applications' }]) shows that
one crumb, and a nested page shows the full trail (Applications › New Application). This is
consistent across the console — first-level pages are not special-cased to hide the crumb. Crumb
links (href) navigate; the last crumb is the current page.
- Sidebar toggle (
collapsible, default true). A toggle (pi pi-bars) sits at the far left of
GlobalHeader.Left on every console page. Clicking it collapses the rail totally (not to a
mini-rail — it is removed, and the content zone spans the full width); clicking again restores it.
The user stays visible via the header avatar while collapsed, so the shared invariant holds even
with no sidebar footer. It is standard chrome — you don't opt in; pass :collapsible="false" only
to suppress it on a page that must not collapse its rail.
<template>
<!-- Home: the header names the module via its single crumb; the sidebar
toggle is present by default. Home leads with content (no page heading). -->
<AppShell
active="home"
:breadcrumb="[{ label: 'Home' }]"
>
<!-- content only -->
</AppShell>
</template>
- The breadcrumb naming the location and an in-page
PageHeading are different roles: the crumb is
top chrome that says where you are; the PageHeading is the page's own title + description +
actions. A second-level page carries both (the crumb and its PageHeading); Home carries only the
crumb and lets its content lead.
Shell 2 — focused creation
For a single task — create, edit, deploy, a wizard step. The sidebar is gone so nothing competes
with the form. The only chrome is one GlobalHeader: a back button, the Azion brand, the flow
breadcrumb, and the account avatar. The page owns its own full-bleed body below the header.
Again, you compose this shell — there is no @aziontech/webkit/creation-header. It is one
GlobalHeader with no sidebar:
<!-- components/CreationShell.vue — a focused shell YOU compose. NOT importable
from webkit; it is one GlobalHeader (back + brand + breadcrumb + avatar)
with NO sidebar, wrapping webkit primitives. -->
<script setup lang="ts">
import GlobalHeader from '@aziontech/webkit/global-header'
import Breadcrumb from '@aziontech/webkit/breadcrumb'
import IconButton from '@aziontech/webkit/icon-button'
defineProps<{
breadcrumb: { label: string; href?: string }[]
backLabel?: string
showBack?: boolean // default true — hide only on a terminal success screen
}>()
const emit = defineEmits<{ back: [event: MouseEvent]; navigate: [event: MouseEvent] }>()
</script>
<template>
<div class="flex h-dvh flex-col bg-[var(--bg-canvas)]">
<!-- EXACTLY ONE GlobalHeader; it IS the whole chrome for a focused flow -->
<GlobalHeader>
<GlobalHeader.Brand><!-- Azion brand --></GlobalHeader.Brand>
<GlobalHeader.Left>
<!-- the ONLY way out: back button + the flow breadcrumb -->
<IconButton
v-if="showBack !== false"
icon="pi pi-arrow-left"
:aria-label="backLabel"
@click="emit('back', $event)"
/>
<Breadcrumb :items="breadcrumb" />
</GlobalHeader.Left>
<GlobalHeader.Right>
<!-- the account avatar — the persistent identity in a focused flow -->
</GlobalHeader.Right>
</GlobalHeader>
<main class="min-h-0 flex-1 overflow-auto">
<slot />
</main>
</div>
</template>
A create page uses your shell and wires the exit:
<script setup lang="ts">
import CreationShell from '@/components/CreationShell.vue' // your shell — NOT a webkit import
const cancel = () => {
/* route back to where the flow started */
}
</script>
<template>
<CreationShell
:breadcrumb="[{ label: 'Applications', href: '/applications' }, { label: 'Create' }]"
back-label="Back to Applications"
@back="cancel"
@navigate="cancel"
>
<!-- the focused task -->
</CreationShell>
</template>
- The back button (and the breadcrumb links) are the only way out — the exit is linear and
explicit, so a half-finished task is never abandoned by an accidental sidebar click.
- Hide the back button (
:show-back="false") only on a terminal state (a success screen with nowhere
left to go back to).
The shared invariant — the user is never signed out
Both shells keep the signed-in user visible. A focused flow drops the sidebar, but it does not
drop the account avatar — it stays anchored to GlobalHeader.Right, mirroring the console header. A
user who is mid-create must still see they're signed in and reach their account; a header with no
identity reads as "logged out" and breaks trust.
- Persistent shell: the user shows twice — the sidebar footer (avatar + name + account menu with
theme + logout) and the header avatar.
- Focused shell: the header avatar (
GlobalHeader.Right) is the persistent identity. It routes to
the same account destination as the console header, so identity is continuous across shells.
Wire this into the focused shell itself (render the avatar in GlobalHeader.Right there), so every
creation page gets it for free — a page must not have to remember to add it.
Content layout (inside the persistent shell)
The shell frames the page; this is how the page's own content sits inside it. Every browse/list page
and every resource-detail page follows the same layout and spacing, so moving between modules
never feels like a different app.
The inset matches the header (--spacing-md)
The content zone's padding is --spacing-md — the same edge padding as GlobalHeader — so page
content lines up with the breadcrumb and the header actions. Your persistent shell applies this inset
once (e.g. on its <main>, as in AppShell above); don't re-pad the page with a bespoke
inset (p-[var(--spacing-lg)]). A resource-detail page that opts out of the shell inset (to run a
full-bleed tab bar) re-applies the same p-[var(--spacing-md)] on its own scrolling content region —
never lg.
List / module pages — heading out, table in a flush card
<template>
<AppShell
active="applications"
:breadcrumb="[{ label: 'Applications' }]"
>
<main class="flex h-full flex-col gap-[var(--spacing-lg)]">
<!-- PageHeading sits OUT of the card and carries the primary actions. -->
<PageHeading
title="Applications"
description="…"
>
<template #actions>
<Button
label="New Resource"
kind="primary"
size="medium"
icon="pi pi-plus"
@click="…"
/>
</template>
</PageHeading>
<!-- The Table lives in a flush, borderless CardBox: the card supplies the
frame, so the table is edge-to-edge (padded=false) and border-free. -->
<section class="flex min-h-0 flex-col">
<CardBox :padded="false">
<template #content>
<Table
:data="…"
:columns="…"
row-key="id"
:border="false"
>
<template #toolbar>…<Table.Search … /></template>
</Table>
</template>
</CardBox>
</section>
</main>
</AppShell>
</template>
- The
PageHeading is out of the card, never in its #header — one heading role per page region.
--spacing-lg between the heading and the card (gap on main); the shell supplies the
--spacing-md inset.
- The table is framed by the card, not itself:
CardBox :padded="false" + Table :border="false"
— the card is the only border, so the table sits edge-to-edge.
- Search / filter live in the table's own
#toolbar (context-aware Table.Search, Table.Filter
…), not above the card.
Resource-detail pages — a fluid tab bar over the same content
A resource you navigate INTO, or a settings hub, opts out of the shell inset to run a full-bleed tab
bar as the bottom of the header, then scrolls its content in a p-[var(--spacing-md)] region. Each
tab renders one of two bodies — and both lead with the heading OUT, --spacing-lg down to the content:
- A list tab → the exact list pattern above (
PageHeading out + flush borderless Table), with
the tab's create action in the heading's #actions.
- A settings tab → the
ItemGroup form: --spacing-sm-titled sections (text-heading-xxs) each
over a flush CardBox + Item.List, committed as one block by a single Save bar pinned below
the scroll region (see webkit-form and webkit-ui-states).
The rhythm is invariant across both: the heading is always out of the card, the inset is always
--spacing-md, and the heading→content gap is always --spacing-lg.
Hard rules
- One shell per page. A page is either a persistent-console page or a focused-creation page —
never both, never neither.
- Exactly one
GlobalHeader per page. The persistent shell renders it inside the content zone;
the focused shell is it. Never render a second header inside the content.
- The user is always visible. Every shell shows the account avatar; a focused flow keeps it in
GlobalHeader.Right. Never ship a screen where the user looks signed out.
- Focused flows have no sidebar. If a create/edit/deploy page renders a sidebar, it's the wrong
shell. Remove the sidebar or switch to a browse page.
- Focused flows have a back way out. A focused shell without a working
@back (or a terminal state
that justifies :show-back="false") strands the user.
- The header always names the location. The breadcrumb renders on every level — a single
first-level crumb (Home, Applications) or a nested trail (Applications › New Application). Crumb
links (
href) navigate; the last crumb is the current page. The sidebar toggle (collapsible) is
separate chrome and on by default.
- Compose the shells, don't hand-roll. Build each shell from
@aziontech/webkit/global-header,
/sidebar, /breadcrumb (resolved via the MCP / catalog). Never build a bare <header>/<nav>
when the primitives exist, and never import a nonexistent @aziontech/webkit/app-layout or
/creation-header.
- The content inset is
--spacing-md — the header's edge padding. Take it from your shell (applied
once); a page that runs a full-bleed tab bar re-applies p-[var(--spacing-md)] on its own content
region. Never a bespoke p-[var(--spacing-lg)] page inset.
- A list table lives in a flush, borderless
CardBox with the PageHeading out of the card.
CardBox :padded="false" + Table :border="false"; the heading (and its actions) sits above the
card, --spacing-lg away. Never bury the heading in the card #header, never ship a bare bordered
table, never wrap the table in a padded card.
Review output
For /webkit-navigation <file>, list gaps. Each:
✗ CreateResource.vue:12 renders a sidebar inside a create flow
rule: One shell per page — a create flow is the focused shell, which has no sidebar.
fix: drop the sidebar; wrap the page in your focused shell (back + brand + breadcrumb + avatar).
✗ NewResourceForm.vue:40 GlobalHeader.Right is empty on a focused flow
rule: The user is always visible — a header with no identity reads as signed out.
fix: render the account avatar in GlobalHeader.Right (your focused shell should anchor it for you).
End with: navigation sound or N gaps — fix before polish.
References
- Persistent console shell — a shell you compose that renders exactly one
GlobalHeader inside the
content zone and a full-height Sidebar (with its header / grouped links / user footer) beside the
page content. Not importable from webkit; you build it once and every browse page reuses it.
- Focused creation shell — a focused shell = one
GlobalHeader with a back button + Azion brand +
flow breadcrumb + account avatar, and no sidebar. Also consumer-composed.
- The webkit primitives both shells wrap (resolve each via the MCP / catalog — see "How to find the
components"):
@aziontech/webkit/global-header (regions GlobalHeader.Left / .Middle / .Right /
.Brand), @aziontech/webkit/sidebar (+ /sidebar-header, /sidebar-group, /sidebar-footer),
@aziontech/webkit/breadcrumb. There is no @aziontech/webkit/app-layout or /creation-header — do
not import one.
- Companion skills:
webkit-form (what fills a focused flow), webkit-ux-heuristics (states +
feedback), webkit-ui-states (locking + toast), webkit-baseline-ui (components + tokens).
Definition of Done