diff --git a/examples/component-showcase/scripts/showcase-profiles.mjs b/examples/component-showcase/scripts/showcase-profiles.mjs
index 0b6923ce..8987b29a 100644
--- a/examples/component-showcase/scripts/showcase-profiles.mjs
+++ b/examples/component-showcase/scripts/showcase-profiles.mjs
@@ -253,6 +253,63 @@ const DATATABLE_ROWS =
'[{"id": 1, "name": "Northwind", "plan": "Scale", "owner": "A. Okafor", "seats": 6, "status": "Active", "statusHtml": "Active"}, {"id": 2, "name": "Acme Industrial", "plan": "Team", "owner": "R. Silva", "seats": 13, "status": "Trial", "statusHtml": "Trial"}, {"id": 3, "name": "Globex", "plan": "Enterprise", "owner": "M. Chen", "seats": 20, "status": "Past due", "statusHtml": "Past due"}, {"id": 4, "name": "Initech", "plan": "Starter", "owner": "J. Dubois", "seats": 27, "status": "Active", "statusHtml": "Active"}, {"id": 5, "name": "Umbrella", "plan": "Scale", "owner": "P. Novak", "seats": 34, "status": "Trial", "statusHtml": "Trial"}, {"id": 6, "name": "Stark Labs", "plan": "Team", "owner": "A. Okafor", "seats": 41, "status": "Past due", "statusHtml": "Past due"}, {"id": 7, "name": "Wayne Foods", "plan": "Enterprise", "owner": "R. Silva", "seats": 48, "status": "Active", "statusHtml": "Active"}, {"id": 8, "name": "Soylent", "plan": "Starter", "owner": "M. Chen", "seats": 55, "status": "Trial", "statusHtml": "Trial"}, {"id": 9, "name": "Hooli", "plan": "Scale", "owner": "J. Dubois", "seats": 62, "status": "Past due", "statusHtml": "Past due"}, {"id": 10, "name": "Vehement", "plan": "Team", "owner": "P. Novak", "seats": 69, "status": "Active", "statusHtml": "Active"}, {"id": 11, "name": "Massive Dynamic", "plan": "Enterprise", "owner": "A. Okafor", "seats": 76, "status": "Trial", "statusHtml": "Trial"}, {"id": 12, "name": "Cyberdyne", "plan": "Starter", "owner": "R. Silva", "seats": 83, "status": "Past due", "statusHtml": "Past due"}, {"id": 13, "name": "Tyrell", "plan": "Scale", "owner": "M. Chen", "seats": 90, "status": "Active", "statusHtml": "Active"}, {"id": 14, "name": "Aperture", "plan": "Team", "owner": "J. Dubois", "seats": 97, "status": "Trial", "statusHtml": "Trial"}, {"id": 15, "name": "Black Mesa", "plan": "Enterprise", "owner": "P. Novak", "seats": 104, "status": "Past due", "statusHtml": "Past due"}]';
export const componentProfiles = {
+ Nav: {
+ demos: [
+ standard(
+ "Horizontal links",
+ "A flat link bar. The active item is marked with aria-current, and the arrow keys move between items.",
+ {
+ items: [
+ { label: "Home", href: "/", value: "home", icon: "icon-[lucide--house]" },
+ { label: "Inbox", href: "/inbox", value: "inbox", badge: "9" },
+ { label: "Reports", href: "/reports", value: "reports" },
+ { label: "Archive", href: "/archive", value: "archive", disabled: true },
+ ],
+ active: "inbox",
+ },
+ ),
+ advanced(
+ "Nested submenus",
+ "An item carrying its own items array opens a submenu on hover or focus, to a maximum of three levels. On a phone the submenus stack inline instead of floating, because a hover-opened overlay is unreachable on touch.",
+ {
+ items: [
+ { label: "Home", href: "/", value: "home" },
+ {
+ label: "Products",
+ value: "products",
+ items: [
+ { label: "Overview", href: "/p", value: "p-overview" },
+ {
+ label: "Platform",
+ value: "p-platform",
+ items: [
+ { label: "Runtime", href: "/p/runtime", value: "runtime" },
+ { label: "Compiler", href: "/p/compiler", value: "compiler" },
+ ],
+ },
+ ],
+ },
+ ],
+ active: "p-overview",
+ },
+ ),
+ standard("Vertical rail", "Vertical orientation switches the arrow keys to up and down.", {
+ items: [
+ { label: "Dashboard", href: "/", value: "dash", icon: "icon-[lucide--gauge]" },
+ { label: "Team", href: "/team", value: "team", icon: "icon-[lucide--users]" },
+ {
+ label: "Settings",
+ href: "/settings",
+ value: "settings",
+ icon: "icon-[lucide--settings]",
+ },
+ ],
+ active: "team",
+ orientation: "vertical",
+ collapsible: false,
+ }),
+ ],
+ },
Stepper: {
demos: [
standard(
diff --git a/packages/ui/components/Nav.wrn b/packages/ui/components/Nav.wrn
index 935c0f45..52d9a481 100644
--- a/packages/ui/components/Nav.wrn
+++ b/packages/ui/components/Nav.wrn
@@ -1,22 +1,320 @@
+// Nav -- a navigation link list, flat or with submenus.
+//
+//
+//
+// An item carrying its own items array becomes a submenu. Depth is capped at
+// three levels: this template language has no component recursion, so each
+// level is written out, and three covers any realistic navigation.
+//
+// NOTE: the style block uses /* */ comments only -- // is not a CSS comment
+// and silently swallows the rule that follows it.
component Nav {
outputs {
- select(payload: { value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
- change(payload: { value?: string | number | boolean | null; values?: Array; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
+ select(payload: { item: object; value: string })
}
props {
- size: string = "default"
- color: string = "primary"
- label: string = "Nav"
items: unknown[] = []
active: string = ""
orientation: string = "horizontal"
+ label: string = "Main"
+ collapsible: boolean = true
+ toggleLabel: string = "Menu"
class: string = ""
}
+
+ state expanded = false
+
+ functions {
+ shared function itemList() {
+ return Array.isArray(items) ? items : []
+ }
+
+ shared function childrenOf(item) {
+ return item && Array.isArray(item.items) ? item.items : []
+ }
+
+ shared function isActive(item) {
+ return Boolean(item.value) && item.value === active
+ }
+
+ shared function rovingAxis() {
+ return orientation === "vertical" ? "vertical" : "horizontal"
+ }
+
+ client function choose(item) {
+ if (item.disabled) {
+ return
+ }
+ output.select({ item: item, value: item.value || "" })
+ }
+
+ client function toggleMenu() {
+ expanded = !expanded
+ }
+ }
+
view {
-