feat(ui): add typed navbar menu items
This commit is contained in:
@@ -688,7 +688,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.28",
|
"version": "0.8.29",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
|
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
|
||||||
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
||||||
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
|
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
|
||||||
"packages/ui/components/Navbar.wrn": "ce33334b98a363ea2c3e4ba111077ff2f3dc875f87e839729cce365789de6f65",
|
"packages/ui/components/Navbar.wrn": "e8f6a6367d2fe4086dc1f0fbbadc49828470a3c78ad27e30220368a5682392e5",
|
||||||
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
|
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
|
||||||
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
|
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
|
||||||
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
|
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
|
||||||
|
|||||||
@@ -90,6 +90,62 @@ the component directory at runtime.
|
|||||||
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
|
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
|
||||||
equivalent is exported as `@wrnexus/ui/component-reference.json`.
|
equivalent is exported as `@wrnexus/ui/component-reference.json`.
|
||||||
|
|
||||||
|
### Typed Navbar items
|
||||||
|
|
||||||
|
Keep navigation data in a shared TypeScript module and validate it with the
|
||||||
|
package contract. `NavbarItem` is a discriminated union of `link`, `dropdown`,
|
||||||
|
and `mega`; a mega item accepts every `MegaMenu` layout option.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { NavbarItem } from "@wrnexus/ui";
|
||||||
|
|
||||||
|
export const navigation = [
|
||||||
|
{ type: "link", label: "Pricing", href: "/pricing" },
|
||||||
|
{
|
||||||
|
type: "dropdown",
|
||||||
|
label: "Company",
|
||||||
|
items: [
|
||||||
|
{ label: "About", href: "/about" },
|
||||||
|
{ label: "Contact", href: "/contact" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mega",
|
||||||
|
label: "Products",
|
||||||
|
mega: {
|
||||||
|
variant: "icon-grid",
|
||||||
|
panelWidth: "2xl",
|
||||||
|
columnCount: 3,
|
||||||
|
rail: [{ label: "All products", href: "/products" }],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
heading: "Work",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Projects",
|
||||||
|
href: "/products/projects",
|
||||||
|
icon: "icon-[lucide--folders]",
|
||||||
|
description: "Plan and deliver work",
|
||||||
|
badge: "New",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featured: {
|
||||||
|
title: "What's new",
|
||||||
|
actionLabel: "Explore",
|
||||||
|
actionHref: "/new",
|
||||||
|
},
|
||||||
|
actionLabel: "View all products",
|
||||||
|
actionHref: "/products",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] satisfies NavbarItem[];
|
||||||
|
```
|
||||||
|
|
||||||
|
Importing from `@wrnexus/ui/navigation` is also supported. Existing untyped
|
||||||
|
Navbar data using `children` remains accepted at runtime for migration.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
|
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
|
||||||
|
|||||||
@@ -44,6 +44,60 @@ size: string = "default"
|
|||||||
const slug = String(source).replace(/^\/+|\/+$/g, "").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase()
|
const slug = String(source).replace(/^\/+|\/+$/g, "").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase()
|
||||||
return slug ? translationPrefix + "." + slug + (field === "description" ? ".description" : "") : ""
|
return slug ? translationPrefix + "." + slug + (field === "description" ? ".description" : "") : ""
|
||||||
}
|
}
|
||||||
|
shared function itemType(item) {
|
||||||
|
if (item && item.type) {
|
||||||
|
return item.type
|
||||||
|
}
|
||||||
|
return item && ((item.items && item.items.length) || (item.children && item.children.length)) ? "dropdown" : "link"
|
||||||
|
}
|
||||||
|
|
||||||
|
shared function dropdownItems(item) {
|
||||||
|
if (!item) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.items)) {
|
||||||
|
return item.items
|
||||||
|
}
|
||||||
|
return Array.isArray(item.children) ? item.children : []
|
||||||
|
}
|
||||||
|
|
||||||
|
shared function dropdownGroupItems(item) {
|
||||||
|
if (!item) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.items)) {
|
||||||
|
return item.items
|
||||||
|
}
|
||||||
|
return Array.isArray(item.children) ? item.children : []
|
||||||
|
}
|
||||||
|
|
||||||
|
shared function megaData(item) {
|
||||||
|
return item && item.mega && typeof item.mega === "object" ? item.mega : item || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared function megaColumns(item) {
|
||||||
|
const data = megaData(item)
|
||||||
|
if (Array.isArray(data.columns)) {
|
||||||
|
return data.columns
|
||||||
|
}
|
||||||
|
const legacy = dropdownItems(item)
|
||||||
|
if (!legacy.length) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const grouped = legacy.some(function (entry) {
|
||||||
|
return dropdownGroupItems(entry).length > 0
|
||||||
|
})
|
||||||
|
if (grouped) {
|
||||||
|
return legacy.map(function (entry) {
|
||||||
|
return {
|
||||||
|
heading: entry.label || "",
|
||||||
|
description: entry.description || "",
|
||||||
|
items: dropdownGroupItems(entry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return [{ heading: item.description || "", items: legacy }]
|
||||||
|
}
|
||||||
client function toggleNavigation() {
|
client function toggleNavigation() {
|
||||||
mobileOpen = !mobileOpen
|
mobileOpen = !mobileOpen
|
||||||
output.toggle({ open: mobileOpen })
|
output.toggle({ open: mobileOpen })
|
||||||
@@ -61,10 +115,31 @@ size: string = "default"
|
|||||||
}
|
}
|
||||||
|
|
||||||
shared function isItemActive(item) {
|
shared function isItemActive(item) {
|
||||||
if (!item) return false
|
if (!item) {
|
||||||
if (item.value && item.value === active) return true
|
return false
|
||||||
if (!item.children || !item.children.length) return false
|
}
|
||||||
return item.children.some(function (child) {
|
if (item.value && item.value === active) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (itemType(item) === "mega") {
|
||||||
|
const data = megaData(item)
|
||||||
|
const links = Array.isArray(data.rail) ? data.rail.slice() : []
|
||||||
|
megaColumns(item).forEach(function (column) {
|
||||||
|
if (column && Array.isArray(column.items)) {
|
||||||
|
column.items.forEach(function (child) {
|
||||||
|
links.push(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return links.some(function (child) {
|
||||||
|
return child.value && child.value === active
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const entries = dropdownItems(item)
|
||||||
|
if (!entries.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return entries.some(function (child) {
|
||||||
return isItemActive(child)
|
return isItemActive(child)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -104,7 +179,27 @@ size: string = "default"
|
|||||||
<div class="wrn-navbar__collapse {mobileOpen ? 'is-open' : ''}">
|
<div class="wrn-navbar__collapse {mobileOpen ? 'is-open' : ''}">
|
||||||
<nav class="wrn-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
|
<nav class="wrn-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
|
||||||
{#each items as item}
|
{#each items as item}
|
||||||
{#if item.children && item.children.length}
|
{#if itemType(item) === "mega"}
|
||||||
|
<MegaMenu
|
||||||
|
class="wrn-navbar__mega"
|
||||||
|
data-active="{isItemActive(item) ? 'true' : 'false'}"
|
||||||
|
label="{item.label}"
|
||||||
|
icon="{item.icon || ''}"
|
||||||
|
columns="{megaColumns(item)}"
|
||||||
|
rail="{megaData(item).rail || []}"
|
||||||
|
featured="{megaData(item).featured || {}}"
|
||||||
|
variant="{megaData(item).variant || 'default'}"
|
||||||
|
density="{megaData(item).density || 'default'}"
|
||||||
|
panelWidth="{megaData(item).panelWidth || 'xl'}"
|
||||||
|
panelAlign="{megaData(item).panelAlign || 'start'}"
|
||||||
|
columnCount="{megaData(item).columnCount || 0}"
|
||||||
|
fullWidth="{megaData(item).fullWidth || false}"
|
||||||
|
actionLabel="{megaData(item).actionLabel || ''}"
|
||||||
|
actionHref="{megaData(item).actionHref || ''}"
|
||||||
|
actionIcon="{megaData(item).actionIcon || 'icon-[lucide--arrow-right]'}"
|
||||||
|
footer="{megaData(item).footer || ''}"
|
||||||
|
/>
|
||||||
|
{:else if itemType(item) === "dropdown"}
|
||||||
<details class="wrn-navbar__dropdown wrn-navbar__dropdown--{item.type || 'dropdown'}" name="wrn-navbar-menu" @toggle="toggleDropdown(event, item)">
|
<details class="wrn-navbar__dropdown wrn-navbar__dropdown--{item.type || 'dropdown'}" name="wrn-navbar-menu" @toggle="toggleDropdown(event, item)">
|
||||||
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
|
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
|
||||||
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
|
||||||
@@ -113,12 +208,12 @@ size: string = "default"
|
|||||||
</summary>
|
</summary>
|
||||||
<div class="wrn-navbar__panel wrn-navbar__panel--columns-{item.columns || 1}">
|
<div class="wrn-navbar__panel wrn-navbar__panel--columns-{item.columns || 1}">
|
||||||
{#if item.description}<p class="wrn-navbar__panel-intro" data-t="{translationKey(item, 'description')}">{item.description}</p>{/if}
|
{#if item.description}<p class="wrn-navbar__panel-intro" data-t="{translationKey(item, 'description')}">{item.description}</p>{/if}
|
||||||
{#each item.children as child}
|
{#each dropdownItems(item) as child}
|
||||||
<div class="wrn-navbar__group">
|
<div class="wrn-navbar__group">
|
||||||
{#if child.children && child.children.length}
|
{#if dropdownGroupItems(child).length}
|
||||||
{#if child.label}<strong class="wrn-navbar__group-title" data-t="{translationKey(child, 'label')}">{child.label}</strong>{/if}
|
{#if child.label}<strong class="wrn-navbar__group-title" data-t="{translationKey(child, 'label')}">{child.label}</strong>{/if}
|
||||||
{#if child.description}<small data-t="{translationKey(child, 'description')}">{child.description}</small>{/if}
|
{#if child.description}<small data-t="{translationKey(child, 'description')}">{child.description}</small>{/if}
|
||||||
{#each child.children as nested}
|
{#each dropdownGroupItems(child) as nested}
|
||||||
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
|
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
|
||||||
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
|
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
|
||||||
<span><strong data-t="{translationKey(nested, 'label')}">{nested.label}</strong>{#if nested.description}<small data-t="{translationKey(nested, 'description')}">{nested.description}</small>{/if}</span>
|
<span><strong data-t="{translationKey(nested, 'label')}">{nested.label}</strong>{#if nested.description}<small data-t="{translationKey(nested, 'description')}">{nested.description}</small>{/if}</span>
|
||||||
@@ -309,6 +404,18 @@ size: string = "default"
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-navbar__mega {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrn-navbar__mega .wrn-mega__trigger {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding: 0.65rem 0.8rem;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.wrn-navbar__menu-link,
|
.wrn-navbar__menu-link,
|
||||||
.wrn-navbar__dropdown > summary,
|
.wrn-navbar__dropdown > summary,
|
||||||
.wrn-navbar__action {
|
.wrn-navbar__action {
|
||||||
@@ -325,13 +432,15 @@ size: string = "default"
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrn-navbar__menu-link:hover,
|
.wrn-navbar__menu-link:hover,
|
||||||
.wrn-navbar__dropdown > summary:hover {
|
.wrn-navbar__dropdown > summary:hover,
|
||||||
|
.wrn-navbar__mega .wrn-mega__trigger:hover {
|
||||||
color: var(--wrn-color-text);
|
color: var(--wrn-color-text);
|
||||||
background: var(--wrn-color-surface-2);
|
background: var(--wrn-color-surface-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrn-navbar__menu-link[aria-current="page"],
|
.wrn-navbar__menu-link[aria-current="page"],
|
||||||
.wrn-navbar__dropdown > summary[aria-current="page"] {
|
.wrn-navbar__dropdown > summary[aria-current="page"],
|
||||||
|
.wrn-navbar__mega[data-active="true"] .wrn-mega__trigger {
|
||||||
color: var(--wrn-component-color, var(--wrn-color-primary));
|
color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: color-mix(
|
background: color-mix(
|
||||||
@@ -343,7 +452,8 @@ size: string = "default"
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrn-navbar__menu-link[aria-current="page"]:hover,
|
.wrn-navbar__menu-link[aria-current="page"]:hover,
|
||||||
.wrn-navbar__dropdown > summary[aria-current="page"]:hover {
|
.wrn-navbar__dropdown > summary[aria-current="page"]:hover,
|
||||||
|
.wrn-navbar__mega[data-active="true"] .wrn-mega__trigger:hover {
|
||||||
color: var(--wrn-component-color, var(--wrn-color-primary));
|
color: var(--wrn-component-color, var(--wrn-color-primary));
|
||||||
background: color-mix(
|
background: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
@@ -587,6 +697,10 @@ size: string = "default"
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.wrn-navbar__mega,
|
||||||
|
.wrn-navbar__mega .wrn-mega__trigger {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.wrn-navbar__menu-link,
|
.wrn-navbar__menu-link,
|
||||||
.wrn-navbar__dropdown > summary,
|
.wrn-navbar__dropdown > summary,
|
||||||
.wrn-navbar__action {
|
.wrn-navbar__action {
|
||||||
@@ -641,6 +755,7 @@ size: string = "default"
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__menu-link, .wrn-navbar__dropdown > summary, .wrn-navbar__action) { width: 100%; }
|
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__menu-link, .wrn-navbar__dropdown > summary, .wrn-navbar__action) { width: 100%; }
|
||||||
|
.wrn-navbar[data-collapse-at="1100px"] :is(.wrn-navbar__mega, .wrn-navbar__mega .wrn-mega__trigger) { width: 100%; }
|
||||||
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__panel { position: static; width: 100%; max-width: none; transform: none; }
|
.wrn-navbar[data-collapse-at="1100px"] .wrn-navbar__panel { position: static; width: 100%; max-width: none; transform: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.28",
|
"version": "0.8.29",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
|
"./navigation": "./src/navigation.ts",
|
||||||
"./registry": "./src/registry.ts",
|
"./registry": "./src/registry.ts",
|
||||||
"./components/*": "./components/*",
|
"./components/*": "./components/*",
|
||||||
"./component-catalog.json": "./component-catalog.json",
|
"./component-catalog.json": "./component-catalog.json",
|
||||||
|
|||||||
@@ -24,4 +24,22 @@
|
|||||||
* server-only subpath. Import from there instead if you need them.
|
* server-only subpath. Import from there instead if you need them.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {};
|
export type {
|
||||||
|
MegaMenuColumn,
|
||||||
|
MegaMenuDensity,
|
||||||
|
MegaMenuFeatured,
|
||||||
|
MegaMenuPanelAlign,
|
||||||
|
MegaMenuPanelWidth,
|
||||||
|
MegaMenuRailItem,
|
||||||
|
MegaMenuVariant,
|
||||||
|
NavbarAction,
|
||||||
|
NavbarBrand,
|
||||||
|
NavbarDropdownGroup,
|
||||||
|
NavbarDropdownItem,
|
||||||
|
NavbarItem,
|
||||||
|
NavbarItemType,
|
||||||
|
NavbarLinkItem,
|
||||||
|
NavbarMegaMenuData,
|
||||||
|
NavbarMegaMenuItem,
|
||||||
|
NavigationLink,
|
||||||
|
} from "./navigation";
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/** Shared, serializable navigation contracts for Navbar and application configs. */
|
||||||
|
|
||||||
|
export type NavbarItemType = "link" | "dropdown" | "mega";
|
||||||
|
export type MegaMenuVariant = "default" | "icon-grid" | "catalog" | "dark";
|
||||||
|
export type MegaMenuDensity = "compact" | "default" | "comfortable";
|
||||||
|
export type MegaMenuPanelWidth = "sm" | "md" | "lg" | "xl" | "2xl" | "full";
|
||||||
|
export type MegaMenuPanelAlign = "start" | "center" | "end";
|
||||||
|
|
||||||
|
export interface NavigationLink {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
value?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
description?: string;
|
||||||
|
descriptionKey?: string;
|
||||||
|
icon?: string;
|
||||||
|
image?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
eyebrow?: string;
|
||||||
|
badge?: string;
|
||||||
|
target?: string;
|
||||||
|
rel?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarLinkItem extends NavigationLink {
|
||||||
|
type?: "link";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarDropdownGroup {
|
||||||
|
label?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
description?: string;
|
||||||
|
descriptionKey?: string;
|
||||||
|
items: NavigationLink[];
|
||||||
|
/** Compatibility alias accepted by Navbar. Prefer `items`. */
|
||||||
|
children?: NavigationLink[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarDropdownItem {
|
||||||
|
type: "dropdown";
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
description?: string;
|
||||||
|
descriptionKey?: string;
|
||||||
|
icon?: string;
|
||||||
|
columns?: number;
|
||||||
|
items: Array<NavigationLink | NavbarDropdownGroup>;
|
||||||
|
/** Compatibility alias accepted by Navbar. Prefer `items`. */
|
||||||
|
children?: Array<NavigationLink | NavbarDropdownGroup>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MegaMenuColumn {
|
||||||
|
heading?: string;
|
||||||
|
eyebrow?: string;
|
||||||
|
description?: string;
|
||||||
|
image?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
items: NavigationLink[];
|
||||||
|
actionLabel?: string;
|
||||||
|
actionHref?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MegaMenuRailItem extends NavigationLink {}
|
||||||
|
|
||||||
|
export interface MegaMenuFeatured {
|
||||||
|
eyebrow?: string;
|
||||||
|
image?: string;
|
||||||
|
imageAlt?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
actionLabel?: string;
|
||||||
|
actionHref?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarMegaMenuData {
|
||||||
|
variant?: MegaMenuVariant;
|
||||||
|
density?: MegaMenuDensity;
|
||||||
|
panelWidth?: MegaMenuPanelWidth;
|
||||||
|
panelAlign?: MegaMenuPanelAlign;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
columnCount?: number;
|
||||||
|
columns: MegaMenuColumn[];
|
||||||
|
rail?: MegaMenuRailItem[];
|
||||||
|
featured?: MegaMenuFeatured;
|
||||||
|
actionLabel?: string;
|
||||||
|
actionHref?: string;
|
||||||
|
actionIcon?: string;
|
||||||
|
footer?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarMegaMenuItem {
|
||||||
|
type: "mega";
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
icon?: string;
|
||||||
|
mega: NavbarMegaMenuData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NavbarItem = NavbarLinkItem | NavbarDropdownItem | NavbarMegaMenuItem;
|
||||||
|
|
||||||
|
export interface NavbarAction extends NavigationLink {
|
||||||
|
variant?: "link" | "outline" | "primary";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavbarBrand {
|
||||||
|
label?: string;
|
||||||
|
labelKey?: string;
|
||||||
|
description?: string;
|
||||||
|
descriptionKey?: string;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
icon?: string;
|
||||||
|
logo?: string;
|
||||||
|
logoAlt?: string;
|
||||||
|
logoWidth?: number | string;
|
||||||
|
logoHeight?: number | string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import type { NavbarItem } from "../src/navigation";
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ type: "link", label: "Pricing", href: "/pricing" },
|
||||||
|
{
|
||||||
|
type: "dropdown",
|
||||||
|
label: "Company",
|
||||||
|
items: [{ label: "About", href: "/about" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "mega",
|
||||||
|
label: "Products",
|
||||||
|
mega: {
|
||||||
|
variant: "icon-grid",
|
||||||
|
density: "compact",
|
||||||
|
panelWidth: "2xl",
|
||||||
|
panelAlign: "center",
|
||||||
|
columnCount: 3,
|
||||||
|
fullWidth: true,
|
||||||
|
rail: [{ label: "All products", href: "/products", icon: "icon-products" }],
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
heading: "Work",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: "Projects",
|
||||||
|
href: "/projects",
|
||||||
|
icon: "icon-projects",
|
||||||
|
description: "Plan and deliver work",
|
||||||
|
badge: "New",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featured: {
|
||||||
|
title: "What's new",
|
||||||
|
image: "/new.png",
|
||||||
|
actionLabel: "Explore",
|
||||||
|
actionHref: "/new",
|
||||||
|
},
|
||||||
|
footer: "One connected platform",
|
||||||
|
actionLabel: "View all products",
|
||||||
|
actionHref: "/products",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] satisfies NavbarItem[];
|
||||||
|
|
||||||
|
test("navbar item contract covers links, dropdowns, and complete mega menus", () => {
|
||||||
|
expect(items.map((item) => item.type)).toEqual(["link", "dropdown", "mega"]);
|
||||||
|
});
|
||||||
@@ -239,7 +239,9 @@ test("navbar supports brands, nested menus, actions, utility slots, and public e
|
|||||||
expect(defaults.get("openOnHover")).toBe("false");
|
expect(defaults.get("openOnHover")).toBe("false");
|
||||||
expect(defaults.get("maxWidth")).toBe('"full"');
|
expect(defaults.get("maxWidth")).toBe('"full"');
|
||||||
expect(source).toContain("item.children");
|
expect(source).toContain("item.children");
|
||||||
expect(source).toContain("child.children");
|
expect(source).toContain("dropdownGroupItems(child)");
|
||||||
|
expect(source).toContain('itemType(item) === "mega"');
|
||||||
|
expect(source).toContain("megaData(item).featured");
|
||||||
expect(source).toContain("wrn-navbar__dropdown--{item.type || 'dropdown'}");
|
expect(source).toContain("wrn-navbar__dropdown--{item.type || 'dropdown'}");
|
||||||
const outputs = new Set(contract.outputs.map((output) => output.name));
|
const outputs = new Set(contract.outputs.map((output) => output.name));
|
||||||
for (const event of ["toggle", "open", "close", "select", "action"]) {
|
for (const event of ["toggle", "open", "close", "select", "action"]) {
|
||||||
@@ -304,13 +306,19 @@ test("navbar renders a brand, nested dropdowns, mega groups, and actions", async
|
|||||||
{
|
{
|
||||||
label: "Safety",
|
label: "Safety",
|
||||||
type: "mega",
|
type: "mega",
|
||||||
columns: 2,
|
mega: {
|
||||||
children: [
|
variant: "icon-grid",
|
||||||
|
columnCount: 2,
|
||||||
|
rail: [{ label: "Overview", href: "/safety" }],
|
||||||
|
columns: [
|
||||||
{
|
{
|
||||||
label: "Resources",
|
heading: "Resources",
|
||||||
children: [{ label: "Guidance", href: "/guidance" }],
|
items: [{ label: "Guidance", href: "/guidance", badge: "New" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
featured: { title: "Safety report", actionLabel: "Read report" },
|
||||||
|
actionLabel: "All safety resources",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [{ label: "Sign in", href: "/login", variant: "primary" }],
|
actions: [{ label: "Sign in", href: "/login", variant: "primary" }],
|
||||||
@@ -320,7 +328,9 @@ test("navbar renders a brand, nested dropdowns, mega groups, and actions", async
|
|||||||
expect(html).toContain("Public portal");
|
expect(html).toContain("Public portal");
|
||||||
expect(html).toContain("Reports");
|
expect(html).toContain("Reports");
|
||||||
expect(html).toContain("Guidance");
|
expect(html).toContain("Guidance");
|
||||||
expect(html).toContain("wrn-navbar__dropdown--mega");
|
expect(html).toContain("wrn-navbar__mega");
|
||||||
|
expect(html).toContain("Safety report");
|
||||||
|
expect(html).toContain("All safety resources");
|
||||||
expect(html).toContain("data-wrn-navbar");
|
expect(html).toContain("data-wrn-navbar");
|
||||||
expect(html).toContain('name="wrn-navbar-menu"');
|
expect(html).toContain('name="wrn-navbar-menu"');
|
||||||
expect(html).toContain("Sign in");
|
expect(html).toContain("Sign in");
|
||||||
@@ -2951,9 +2961,7 @@ test("mega menu supports rails, icons, badges, media, featured content and foote
|
|||||||
heading: "Products",
|
heading: "Products",
|
||||||
description: "Choose a workspace",
|
description: "Choose a workspace",
|
||||||
image: "/products.png",
|
image: "/products.png",
|
||||||
items: [
|
items: [{ label: "CRM", description: "Customer records", icon: "icon-crm", badge: "New" }],
|
||||||
{ label: "CRM", description: "Customer records", icon: "icon-crm", badge: "New" },
|
|
||||||
],
|
|
||||||
actionLabel: "All products",
|
actionLabel: "All products",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user