feat(ui): add typed navbar menu items
Quality / quality (ubuntu-latest) (push) Failing after 11m12s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-24 21:36:46 +05:30
parent fc1eea3939
commit 9d64ec60c4
9 changed files with 396 additions and 27 deletions
+126 -11
View File
@@ -44,6 +44,60 @@ size: string = "default"
const slug = String(source).replace(/^\/+|\/+$/g, "").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase()
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() {
mobileOpen = !mobileOpen
output.toggle({ open: mobileOpen })
@@ -61,10 +115,31 @@ size: string = "default"
}
shared function isItemActive(item) {
if (!item) return false
if (item.value && item.value === active) return true
if (!item.children || !item.children.length) return false
return item.children.some(function (child) {
if (!item) {
return false
}
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)
})
}
@@ -104,7 +179,27 @@ size: string = "default"
<div class="wrn-navbar__collapse {mobileOpen ? 'is-open' : ''}">
<nav class="wrn-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
{#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)">
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
@@ -113,12 +208,12 @@ size: string = "default"
</summary>
<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}
{#each item.children as child}
{#each dropdownItems(item) as child}
<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.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)">
{#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>
@@ -309,6 +404,18 @@ size: string = "default"
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__dropdown > summary,
.wrn-navbar__action {
@@ -325,13 +432,15 @@ size: string = "default"
}
.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);
background: var(--wrn-color-surface-2);
}
.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));
font-weight: 600;
background: color-mix(
@@ -343,7 +452,8 @@ size: string = "default"
}
.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));
background: color-mix(
in srgb,
@@ -587,6 +697,10 @@ size: string = "default"
align-items: stretch;
flex-direction: column;
}
.wrn-navbar__mega,
.wrn-navbar__mega .wrn-mega__trigger {
width: 100%;
}
.wrn-navbar__menu-link,
.wrn-navbar__dropdown > summary,
.wrn-navbar__action {
@@ -641,6 +755,7 @@ size: string = "default"
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__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; }
}