- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Label
- Menubar
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner (Toast)
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
Navigation Menu
A collection of links for navigating websites
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideCheck, lucideCircle, lucideInfo } from '@ng-icons/lucide';
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';
@Component({
selector: 'spartan-navigation-menu-preview',
imports: [HlmNavigationMenuImports, NgIcon, RouterLink],
providers: [provideIcons({ lucideCircle, lucideCheck, lucideInfo })],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nav hlmNavigationMenu>
<ul hlmNavigationMenuList class="flex-wrap">
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Getting started</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="w-96">
<li>
<a hlmNavigationMenuLink routerLink="/documentation/introduction">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">Introduction</div>
<div class="text-muted-foreground line-clamp-2">Re-usable components built with Tailwind CSS.</div>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink routerLink="/documentation/installation">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">Installation</div>
<div class="text-muted-foreground line-clamp-2">
How to install dependencies and structure your app.
</div>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink routerLink="/documentation/typography">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">Typography</div>
<div class="text-muted-foreground line-clamp-2">Styles for headings, paragraphs, lists...etc</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<!-- Components Menu -->
<li hlmNavigationMenuItem class="hidden md:flex">
<button hlmNavigationMenuTrigger align="center">Components</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid w-[400px] gap-2 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
@for (component of _components; track $index) {
<li>
<a hlmNavigationMenuLink [href]="component.href">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">{{ component.title }}</div>
<div class="text-muted-foreground line-clamp-2">{{ component.description }}</div>
</div>
</a>
</li>
}
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger align="end">With Icon</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideInfo" />
Backlog
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCircle" />
To Do
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCheck" />
Done
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<a hlmNavigationMenuLink href="/documentation/introduction">Docs</a>
</li>
</ul>
</nav>
`,
})
export class NavigationMenuPreview {
protected readonly _components = [
{
title: 'Alert Dialog',
description: 'A modal dialog that interrupts the user with important content and expects a response.',
href: '/components/alert-dialog',
},
{
title: 'Hover Card',
description: 'For sighted users to preview content available behind a link.',
href: '/components/hover-card',
},
{
title: 'Progress',
description: 'Displays an indicator showing the completion progress of a task.',
href: '/components/progress',
},
{
title: 'Scroll Area',
description: 'Visually or semantically separates content.',
href: '/components/scroll-area',
},
{
title: 'Tabs',
description: 'A set of layered content panels displayed one at a time.',
href: '/components/tabs',
},
{
title: 'Tooltip',
description: 'A popup that displays information on hover or focus.',
href: '/components/tooltip',
},
];
}Installation
ng g @spartan-ng/cli:ui navigation-menunx g @spartan-ng/cli:ui navigation-menuimport { DestroyRef, ElementRef, HostAttributeToken, Injector, PLATFORM_ID, effect, inject, makeEnvironmentProviders, runInInjectionContext, type EnvironmentProviders } from '@angular/core';
import { OVERLAY_DEFAULT_CONFIG } from '@angular/cdk/overlay';
import { clsx, type ClassValue } from 'clsx';
import { isPlatformBrowser } from '@angular/common';
import { provideSpartanHlm } from '@spartan-ng/helm/utils';
import { twMerge } from 'tailwind-merge';
export function hlm(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Global map to track class managers per element
const elementClassManagers = new WeakMap<HTMLElement, ElementClassManager>();
// Global mutation observer for all elements
let globalObserver: MutationObserver | null = null;
const observedElements = new Set<HTMLElement>();
interface ElementClassManager {
element: HTMLElement;
sources: Map<number, { classes: Set<string>; order: number }>;
baseClasses: Set<string>;
isUpdating: boolean;
nextOrder: number;
hasInitialized: boolean;
restoreRafId: number | null;
/** Transitions are suppressed until the first effect writes correct classes */
transitionsSuppressed: boolean;
/** Original inline transition value to restore after suppression (empty string = none was set) */
previousTransition: string;
/** Original inline transition priority to preserve !important when restoring */
previousTransitionPriority: string;
}
let sourceCounter = 0;
/**
* This function dynamically adds and removes classes for a given element without requiring
* the a class binding (e.g. `[class]="..."`) which may interfere with other class bindings.
*
* 1. This will merge the existing classes on the element with the new classes.
* 2. It will also remove any classes that were previously added by this function but are no longer present in the new classes.
* 3. Multiple calls to this function on the same element will be merged efficiently.
*/
export function classes(computed: () => ClassValue[] | string, options: ClassesOptions = {}) {
runInInjectionContext(options.injector ?? inject(Injector), () => {
const elementRef = options.elementRef ?? inject(ElementRef);
const platformId = inject(PLATFORM_ID);
const destroyRef = inject(DestroyRef);
const baseClasses = inject(new HostAttributeToken('class'), { optional: true });
const element = elementRef.nativeElement;
// Create unique identifier for this source
const sourceId = sourceCounter++;
// Get or create the class manager for this element
let manager = elementClassManagers.get(element);
if (!manager) {
// Initialize base classes from variation (host attribute 'class')
const initialBaseClasses = new Set<string>();
if (baseClasses) {
toClassList(baseClasses).forEach((cls) => initialBaseClasses.add(cls));
}
manager = {
element,
sources: new Map(),
baseClasses: initialBaseClasses,
isUpdating: false,
nextOrder: 0,
hasInitialized: false,
restoreRafId: null,
transitionsSuppressed: false,
previousTransition: '',
previousTransitionPriority: '',
};
elementClassManagers.set(element, manager);
// Setup global observer if needed and register this element
setupGlobalObserver(platformId);
observedElements.add(element);
// Suppress transitions until the first effect writes correct classes and
// the browser has painted them. This prevents CSS transition animations
// during hydration when classes change from SSR state to client state.
if (isPlatformBrowser(platformId)) {
manager.previousTransition = element.style.getPropertyValue('transition');
manager.previousTransitionPriority = element.style.getPropertyPriority('transition');
element.style.setProperty('transition', 'none', 'important');
manager.transitionsSuppressed = true;
}
}
// Assign order once at registration time
const sourceOrder = manager.nextOrder++;
function updateClasses(): void {
// Get the new classes from the computed function
const newClasses = toClassList(computed());
// Update this source's classes, keeping the original order
manager!.sources.set(sourceId, {
classes: new Set(newClasses),
order: sourceOrder,
});
// Update the element
updateElement(manager!);
// Re-enable transitions after the first effect writes correct classes.
// Deferred to next animation frame so the browser paints the class change
// with transitions disabled first, then re-enables them.
if (manager!.transitionsSuppressed) {
manager!.transitionsSuppressed = false;
manager!.restoreRafId = requestAnimationFrame(() => {
manager!.restoreRafId = null;
restoreTransitionSuppression(manager!);
});
}
}
// Register cleanup with DestroyRef
destroyRef.onDestroy(() => {
if (manager!.restoreRafId !== null) {
cancelAnimationFrame(manager!.restoreRafId);
manager!.restoreRafId = null;
}
if (manager!.transitionsSuppressed) {
manager!.transitionsSuppressed = false;
restoreTransitionSuppression(manager!);
}
// Remove this source from the manager
manager!.sources.delete(sourceId);
// If no more sources, clean up the manager
if (manager!.sources.size === 0) {
cleanupManager(element);
} else {
// Update element without this source's classes
updateElement(manager!);
}
});
/**
* We need this effect to track changes to the computed classes. Ideally, we would use
* afterRenderEffect here, but that doesn't run in SSR contexts, so we use a standard
* effect which works in both browser and SSR.
*/
effect(updateClasses);
});
}
function restoreTransitionSuppression(manager: ElementClassManager): void {
const prev = manager.previousTransition;
if (prev) {
manager.element.style.setProperty('transition', prev, manager.previousTransitionPriority || undefined);
} else {
manager.element.style.removeProperty('transition');
}
}
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
function setupGlobalObserver(platformId: Object): void {
if (isPlatformBrowser(platformId) && !globalObserver) {
// Create single global observer that watches the entire document
globalObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const element = mutation.target as HTMLElement;
const manager = elementClassManagers.get(element);
// Only process elements we're managing
if (manager && observedElements.has(element)) {
if (manager.isUpdating) continue; // Ignore changes we're making
// Update base classes to include any externally added classes
const currentClasses = toClassList(element.className);
const allSourceClasses = new Set<string>();
// Collect all classes from all sources
for (const source of manager.sources.values()) {
for (const className of source.classes) {
allSourceClasses.add(className);
}
}
// Any classes not from sources become new base classes
manager.baseClasses.clear();
for (const className of currentClasses) {
if (!allSourceClasses.has(className)) {
manager.baseClasses.add(className);
}
}
updateElement(manager);
}
}
}
});
// Start observing the entire document for class attribute changes
globalObserver.observe(document, {
attributes: true,
attributeFilter: ['class'],
subtree: true, // Watch all descendants
});
}
}
function updateElement(manager: ElementClassManager): void {
if (manager.isUpdating) return; // Prevent recursive updates
manager.isUpdating = true;
// Handle initialization: capture base classes after first source registration
if (!manager.hasInitialized && manager.sources.size > 0) {
// Get current classes on element (may include SSR classes)
const currentClasses = toClassList(manager.element.className);
// Get all classes that will be applied by sources
const allSourceClasses = new Set<string>();
for (const source of manager.sources.values()) {
source.classes.forEach((className) => allSourceClasses.add(className));
}
// Only consider classes as "base" if they're not produced by any source
// This prevents SSR-rendered classes from being preserved as base classes
currentClasses.forEach((className) => {
if (!allSourceClasses.has(className)) {
manager.baseClasses.add(className);
}
});
manager.hasInitialized = true;
}
// Get classes from all sources, sorted by registration order (later takes precedence)
const sortedSources = Array.from(manager.sources.entries()).sort(([, a], [, b]) => a.order - b.order);
const allSourceClasses: string[] = [];
for (const [, source] of sortedSources) {
allSourceClasses.push(...source.classes);
}
// Combine base classes with all source classes, ensuring base classes take precedence
const classesToApply =
allSourceClasses.length > 0 || manager.baseClasses.size > 0
? hlm([...allSourceClasses, ...manager.baseClasses])
: '';
// Apply the classes to the element
if (manager.element.className !== classesToApply) {
manager.element.className = classesToApply;
}
manager.isUpdating = false;
}
function cleanupManager(element: HTMLElement): void {
// Remove from global tracking
observedElements.delete(element);
elementClassManagers.delete(element);
// If no more elements being tracked, cleanup global observer
if (observedElements.size === 0 && globalObserver) {
globalObserver.disconnect();
globalObserver = null;
}
}
interface ClassesOptions {
elementRef?: ElementRef<HTMLElement>;
injector?: Injector;
}
// Cache for parsed class lists to avoid repeated string operations
const classListCache = new Map<string, string[]>();
function toClassList(className: string | ClassValue[]): string[] {
// For simple string inputs, use cache to avoid repeated parsing
if (typeof className === 'string' && classListCache.has(className)) {
return classListCache.get(className)!;
}
const result = clsx(className)
.split(' ')
.filter((c) => c.length > 0);
// Cache string results, but limit cache size to prevent memory growth
if (typeof className === 'string' && classListCache.size < 1000) {
classListCache.set(className, result);
}
return result;
}
/**
* Provides default configuration for Spartan Helm components.
*
* This utility configures the Angular CDK overlay to disable the `usePopover`
* behavior introduced in Angular 21, which causes CDK overlay-based components
* (sheets, dialogs, tooltips, etc.) to render above `position: fixed` elements
* like `<hlm-toaster>`.
*
* @returns {EnvironmentProviders} Environment providers to be added to the application config.
*
* @example
* ```ts
* // app.config.ts
*
*
* export const appConfig: ApplicationConfig = {
* providers: [
* provideSpartanHlm(),
* // ... other providers
* ],
* };
* ```
*/
export function provideSpartanHlm(): EnvironmentProviders {
return makeEnvironmentProviders([
{
provide: OVERLAY_DEFAULT_CONFIG,
useValue: { usePopover: false },
},
]);
}import { BrnNavigationMenu, BrnNavigationMenuContent, BrnNavigationMenuItem, BrnNavigationMenuLink, BrnNavigationMenuList, BrnNavigationMenuTrigger } from '@spartan-ng/brain/navigation-menu';
import { ChangeDetectionStrategy, Component, Directive, input, numberAttribute } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { classes } from '@spartan-ng/helm/utils';
import { lucideChevronDown } from '@ng-icons/lucide';
import { type NumberInput } from '@angular/cdk/coercion';
@Directive({
selector: '[hlmNavigationMenuContent],hlm-navigation-menu-content',
host: {
'data-slot': 'navigation-menu-content',
'[style.--nav-offset]': 'navOffset()',
},
})
export class HlmNavigationMenuContent {
public readonly navOffset = input<number, NumberInput>(1.5, { transform: numberAttribute });
constructor() {
classes(() => [
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:w-auto',
'data-[orientation=horizontal]:data-[motion=from-end]:slide-in-from-right-52 data-[orientation=horizontal]:data-[motion=from-start]:slide-in-from-left-52 data-[orientation=horizontal]:data-[motion=to-end]:slide-out-to-right-52 data-[orientation=horizontal]:data-[motion=to-start]:slide-out-to-left-52',
'data-[orientation=vertical]:data-[motion=from-end]:slide-in-from-bottom-52 data-[orientation=vertical]:data-[motion=from-start]:slide-in-from-top-52 data-[orientation=vertical]:data-[motion=to-end]:slide-out-to-bottom-52 data-[orientation=vertical]:data-[motion=to-start]:slide-out-to-top-52',
'data-[orientation=horizontal]:mt-[--spacing(var(--nav-offset))] data-[orientation=vertical]:mx-[--spacing(var(--nav-offset))]',
'bg-popover text-popover-foreground ring-foreground/10 rounded-lg shadow ring-1 transition-all ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 block',
]);
}
}
@Directive({
selector: 'li[hlmNavigationMenuItem]',
hostDirectives: [{ directive: BrnNavigationMenuItem, inputs: ['id'] }],
host: {
'data-slot': 'navigation-menu-item',
},
})
export class HlmNavigationMenuItem {
constructor() {
classes(() => 'relative has-[:focus]:z-10 data-active:z-10');
}
}
@Directive({
selector: 'a[hlmNavigationMenuLink]',
hostDirectives: [{ directive: BrnNavigationMenuLink, inputs: ['active'] }],
host: {
'data-slot': 'navigation-menu-link',
},
})
export class HlmNavigationMenuLink {
constructor() {
classes(() => 'data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-lg p-2 text-sm transition-all outline-none focus-visible:ring-3 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md [&_ng-icon:not([class*=\'text-\'])]:text-[length:--spacing(4)]');
}
}
@Directive({
selector: 'ul[hlmNavigationMenuList]',
hostDirectives: [
{
directive: BrnNavigationMenuList,
},
],
host: {
'data-slot': 'navigation-menu-list',
},
})
export class HlmNavigationMenuList {
constructor() {
classes(() => [
'gap-0 group flex flex-1 list-none items-center justify-center',
'data-[orientation=vertical]:flex-col',
]);
}
}
@Directive({
selector: '[hlmNavigationMenuPortal]',
hostDirectives: [BrnNavigationMenuContent],
})
export class HlmNavigationMenuPortal {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'button[hlmNavigationMenuTrigger]',
imports: [NgIcon],
providers: [provideIcons({ lucideChevronDown })],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [{ directive: BrnNavigationMenuTrigger, inputs: ['align'] }],
host: { 'data-slot': 'navigation-menu-trigger' },
template: `
<ng-content />
<ng-icon name="lucideChevronDown" class="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180" />
`,
})
export class HlmNavigationMenuTrigger {
constructor() {
classes(
() =>
'bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 rounded-lg px-2.5 py-1.5 text-sm font-medium transition-all focus-visible:ring-3 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none',
);
}
}
@Directive({
selector: 'nav[hlmNavigationMenu]',
hostDirectives: [
{
directive: BrnNavigationMenu,
inputs: ['value', 'delayDuration', 'skipDelayDuration', 'orientation', 'openOn'],
outputs: ['valueChange'],
},
],
host: {
'data-slot': 'navigation-menu',
},
})
export class HlmNavigationMenu {
constructor() {
classes(
() => 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
);
}
}
export const HlmNavigationMenuImports = [
HlmNavigationMenu,
HlmNavigationMenuContent,
HlmNavigationMenuItem,
HlmNavigationMenuLink,
HlmNavigationMenuList,
HlmNavigationMenuPortal,
HlmNavigationMenuTrigger,
] as const;import { BrnNavigationMenu, BrnNavigationMenuContent, BrnNavigationMenuItem, BrnNavigationMenuLink, BrnNavigationMenuList, BrnNavigationMenuTrigger } from '@spartan-ng/brain/navigation-menu';
import { ChangeDetectionStrategy, Component, Directive, input, numberAttribute } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { classes } from '@spartan-ng/helm/utils';
import { lucideChevronDown } from '@ng-icons/lucide';
import { type NumberInput } from '@angular/cdk/coercion';
@Directive({
selector: '[hlmNavigationMenuContent],hlm-navigation-menu-content',
host: {
'data-slot': 'navigation-menu-content',
'[style.--nav-offset]': 'navOffset()',
},
})
export class HlmNavigationMenuContent {
public readonly navOffset = input<number, NumberInput>(1.5, { transform: numberAttribute });
constructor() {
classes(() => [
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:w-auto',
'data-[orientation=horizontal]:data-[motion=from-end]:slide-in-from-right-52 data-[orientation=horizontal]:data-[motion=from-start]:slide-in-from-left-52 data-[orientation=horizontal]:data-[motion=to-end]:slide-out-to-right-52 data-[orientation=horizontal]:data-[motion=to-start]:slide-out-to-left-52',
'data-[orientation=vertical]:data-[motion=from-end]:slide-in-from-bottom-52 data-[orientation=vertical]:data-[motion=from-start]:slide-in-from-top-52 data-[orientation=vertical]:data-[motion=to-end]:slide-out-to-bottom-52 data-[orientation=vertical]:data-[motion=to-start]:slide-out-to-top-52',
'data-[orientation=horizontal]:mt-[--spacing(var(--nav-offset))] data-[orientation=vertical]:mx-[--spacing(var(--nav-offset))]',
'bg-popover text-popover-foreground ring-foreground/10 rounded-lg shadow ring-1 transition-all ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 block',
]);
}
}
@Directive({
selector: 'li[hlmNavigationMenuItem]',
hostDirectives: [{ directive: BrnNavigationMenuItem, inputs: ['id'] }],
host: {
'data-slot': 'navigation-menu-item',
},
})
export class HlmNavigationMenuItem {
constructor() {
classes(() => 'relative has-[:focus]:z-10 data-active:z-10');
}
}
@Directive({
selector: 'a[hlmNavigationMenuLink]',
hostDirectives: [{ directive: BrnNavigationMenuLink, inputs: ['active'] }],
host: {
'data-slot': 'navigation-menu-link',
},
})
export class HlmNavigationMenuLink {
constructor() {
classes(() => 'data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-1.5 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-3 focus-visible:outline-1 [&_ng-icon:not([class*=\'text-\'])]:text-[length:--spacing(4)]');
}
}
@Directive({
selector: 'ul[hlmNavigationMenuList]',
hostDirectives: [
{
directive: BrnNavigationMenuList,
},
],
host: {
'data-slot': 'navigation-menu-list',
},
})
export class HlmNavigationMenuList {
constructor() {
classes(() => [
'gap-0 group flex flex-1 list-none items-center justify-center',
'data-[orientation=vertical]:flex-col',
]);
}
}
@Directive({
selector: '[hlmNavigationMenuPortal]',
hostDirectives: [BrnNavigationMenuContent],
})
export class HlmNavigationMenuPortal {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'button[hlmNavigationMenuTrigger]',
imports: [NgIcon],
providers: [provideIcons({ lucideChevronDown })],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [{ directive: BrnNavigationMenuTrigger, inputs: ['align'] }],
host: { 'data-slot': 'navigation-menu-trigger' },
template: `
<ng-content />
<ng-icon name="lucideChevronDown" class="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180 group-data-popup-open/navigation-menu-trigger:rotate-180" />
`,
})
export class HlmNavigationMenuTrigger {
constructor() {
classes(
() =>
'bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 rounded-md px-4 py-2 text-sm font-medium transition-all focus-visible:ring-3 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none',
);
}
}
@Directive({
selector: 'nav[hlmNavigationMenu]',
hostDirectives: [
{
directive: BrnNavigationMenu,
inputs: ['value', 'delayDuration', 'skipDelayDuration', 'orientation', 'openOn'],
outputs: ['valueChange'],
},
],
host: {
'data-slot': 'navigation-menu',
},
})
export class HlmNavigationMenu {
constructor() {
classes(
() => 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
);
}
}
export const HlmNavigationMenuImports = [
HlmNavigationMenu,
HlmNavigationMenuContent,
HlmNavigationMenuItem,
HlmNavigationMenuLink,
HlmNavigationMenuList,
HlmNavigationMenuPortal,
HlmNavigationMenuTrigger,
] as const;import { BrnNavigationMenu, BrnNavigationMenuContent, BrnNavigationMenuItem, BrnNavigationMenuLink, BrnNavigationMenuList, BrnNavigationMenuTrigger } from '@spartan-ng/brain/navigation-menu';
import { ChangeDetectionStrategy, Component, Directive, input, numberAttribute } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { classes } from '@spartan-ng/helm/utils';
import { lucideChevronDown } from '@ng-icons/lucide';
import { type NumberInput } from '@angular/cdk/coercion';
@Directive({
selector: '[hlmNavigationMenuContent],hlm-navigation-menu-content',
host: {
'data-slot': 'navigation-menu-content',
'[style.--nav-offset]': 'navOffset()',
},
})
export class HlmNavigationMenuContent {
public readonly navOffset = input<number, NumberInput>(1.5, { transform: numberAttribute });
constructor() {
classes(() => [
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:w-auto',
'data-[orientation=horizontal]:data-[motion=from-end]:slide-in-from-right-52 data-[orientation=horizontal]:data-[motion=from-start]:slide-in-from-left-52 data-[orientation=horizontal]:data-[motion=to-end]:slide-out-to-right-52 data-[orientation=horizontal]:data-[motion=to-start]:slide-out-to-left-52',
'data-[orientation=vertical]:data-[motion=from-end]:slide-in-from-bottom-52 data-[orientation=vertical]:data-[motion=from-start]:slide-in-from-top-52 data-[orientation=vertical]:data-[motion=to-end]:slide-out-to-bottom-52 data-[orientation=vertical]:data-[motion=to-start]:slide-out-to-top-52',
'data-[orientation=horizontal]:mt-[--spacing(var(--nav-offset))] data-[orientation=vertical]:mx-[--spacing(var(--nav-offset))]',
'bg-popover text-popover-foreground ring-foreground/10 rounded-none shadow ring-1 transition-all ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 block',
]);
}
}
@Directive({
selector: 'li[hlmNavigationMenuItem]',
hostDirectives: [{ directive: BrnNavigationMenuItem, inputs: ['id'] }],
host: {
'data-slot': 'navigation-menu-item',
},
})
export class HlmNavigationMenuItem {
constructor() {
classes(() => 'relative has-[:focus]:z-10 data-active:z-10');
}
}
@Directive({
selector: 'a[hlmNavigationMenuLink]',
hostDirectives: [{ directive: BrnNavigationMenuLink, inputs: ['active'] }],
host: {
'data-slot': 'navigation-menu-link',
},
})
export class HlmNavigationMenuLink {
constructor() {
classes(() => 'data-active:focus:bg-muted data-active:hover:bg-muted data-active:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-2 rounded-none p-2 text-xs transition-all outline-none focus-visible:ring-1 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-none [&_ng-icon:not([class*=\'text-\'])]:text-[length:--spacing(4)]');
}
}
@Directive({
selector: 'ul[hlmNavigationMenuList]',
hostDirectives: [
{
directive: BrnNavigationMenuList,
},
],
host: {
'data-slot': 'navigation-menu-list',
},
})
export class HlmNavigationMenuList {
constructor() {
classes(() => [
'gap-0 group flex flex-1 list-none items-center justify-center',
'data-[orientation=vertical]:flex-col',
]);
}
}
@Directive({
selector: '[hlmNavigationMenuPortal]',
hostDirectives: [BrnNavigationMenuContent],
})
export class HlmNavigationMenuPortal {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'button[hlmNavigationMenuTrigger]',
imports: [NgIcon],
providers: [provideIcons({ lucideChevronDown })],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [{ directive: BrnNavigationMenuTrigger, inputs: ['align'] }],
host: { 'data-slot': 'navigation-menu-trigger' },
template: `
<ng-content />
<ng-icon name="lucideChevronDown" class="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180" />
`,
})
export class HlmNavigationMenuTrigger {
constructor() {
classes(
() =>
'bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 rounded-none px-2.5 py-1.5 text-xs font-medium transition-all focus-visible:ring-1 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none',
);
}
}
@Directive({
selector: 'nav[hlmNavigationMenu]',
hostDirectives: [
{
directive: BrnNavigationMenu,
inputs: ['value', 'delayDuration', 'skipDelayDuration', 'orientation', 'openOn'],
outputs: ['valueChange'],
},
],
host: {
'data-slot': 'navigation-menu',
},
})
export class HlmNavigationMenu {
constructor() {
classes(
() => 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
);
}
}
export const HlmNavigationMenuImports = [
HlmNavigationMenu,
HlmNavigationMenuContent,
HlmNavigationMenuItem,
HlmNavigationMenuLink,
HlmNavigationMenuList,
HlmNavigationMenuPortal,
HlmNavigationMenuTrigger,
] as const;import { BrnNavigationMenu, BrnNavigationMenuContent, BrnNavigationMenuItem, BrnNavigationMenuLink, BrnNavigationMenuList, BrnNavigationMenuTrigger } from '@spartan-ng/brain/navigation-menu';
import { ChangeDetectionStrategy, Component, Directive, input, numberAttribute } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { classes } from '@spartan-ng/helm/utils';
import { lucideChevronDown } from '@ng-icons/lucide';
import { type NumberInput } from '@angular/cdk/coercion';
@Directive({
selector: '[hlmNavigationMenuContent],hlm-navigation-menu-content',
host: {
'data-slot': 'navigation-menu-content',
'[style.--nav-offset]': 'navOffset()',
},
})
export class HlmNavigationMenuContent {
public readonly navOffset = input<number, NumberInput>(1.5, { transform: numberAttribute });
constructor() {
classes(() => [
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:w-auto',
'data-[orientation=horizontal]:data-[motion=from-end]:slide-in-from-right-52 data-[orientation=horizontal]:data-[motion=from-start]:slide-in-from-left-52 data-[orientation=horizontal]:data-[motion=to-end]:slide-out-to-right-52 data-[orientation=horizontal]:data-[motion=to-start]:slide-out-to-left-52',
'data-[orientation=vertical]:data-[motion=from-end]:slide-in-from-bottom-52 data-[orientation=vertical]:data-[motion=from-start]:slide-in-from-top-52 data-[orientation=vertical]:data-[motion=to-end]:slide-out-to-bottom-52 data-[orientation=vertical]:data-[motion=to-start]:slide-out-to-top-52',
'data-[orientation=horizontal]:mt-[--spacing(var(--nav-offset))] data-[orientation=vertical]:mx-[--spacing(var(--nav-offset))]',
'bg-popover text-popover-foreground ring-foreground/5 rounded-2xl shadow ring-1 transition-all ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 block',
]);
}
}
@Directive({
selector: 'li[hlmNavigationMenuItem]',
hostDirectives: [{ directive: BrnNavigationMenuItem, inputs: ['id'] }],
host: {
'data-slot': 'navigation-menu-item',
},
})
export class HlmNavigationMenuItem {
constructor() {
classes(() => 'relative has-[:focus]:z-10 data-active:z-10');
}
}
@Directive({
selector: 'a[hlmNavigationMenuLink]',
hostDirectives: [{ directive: BrnNavigationMenuLink, inputs: ['active'] }],
host: {
'data-slot': 'navigation-menu-link',
},
})
export class HlmNavigationMenuLink {
constructor() {
classes(() => 'data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 focus-visible:ring-ring/50 hover:bg-muted focus:bg-muted flex items-center gap-1.5 rounded-xl p-3 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_ng-icon:not([class*=\'text-\'])]:text-[length:--spacing(4)]');
}
}
@Directive({
selector: 'ul[hlmNavigationMenuList]',
hostDirectives: [
{
directive: BrnNavigationMenuList,
},
],
host: {
'data-slot': 'navigation-menu-list',
},
})
export class HlmNavigationMenuList {
constructor() {
classes(() => [
'gap-0 group flex flex-1 list-none items-center justify-center',
'data-[orientation=vertical]:flex-col',
]);
}
}
@Directive({
selector: '[hlmNavigationMenuPortal]',
hostDirectives: [BrnNavigationMenuContent],
})
export class HlmNavigationMenuPortal {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'button[hlmNavigationMenuTrigger]',
imports: [NgIcon],
providers: [provideIcons({ lucideChevronDown })],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [{ directive: BrnNavigationMenuTrigger, inputs: ['align'] }],
host: { 'data-slot': 'navigation-menu-trigger' },
template: `
<ng-content />
<ng-icon name="lucideChevronDown" class="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180" />
`,
})
export class HlmNavigationMenuTrigger {
constructor() {
classes(
() =>
'bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/50 rounded-2xl px-4.5 py-2.5 text-sm font-medium transition-all focus-visible:ring-[3px] focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none',
);
}
}
@Directive({
selector: 'nav[hlmNavigationMenu]',
hostDirectives: [
{
directive: BrnNavigationMenu,
inputs: ['value', 'delayDuration', 'skipDelayDuration', 'orientation', 'openOn'],
outputs: ['valueChange'],
},
],
host: {
'data-slot': 'navigation-menu',
},
})
export class HlmNavigationMenu {
constructor() {
classes(
() => 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
);
}
}
export const HlmNavigationMenuImports = [
HlmNavigationMenu,
HlmNavigationMenuContent,
HlmNavigationMenuItem,
HlmNavigationMenuLink,
HlmNavigationMenuList,
HlmNavigationMenuPortal,
HlmNavigationMenuTrigger,
] as const;import { BrnNavigationMenu, BrnNavigationMenuContent, BrnNavigationMenuItem, BrnNavigationMenuLink, BrnNavigationMenuList, BrnNavigationMenuTrigger } from '@spartan-ng/brain/navigation-menu';
import { ChangeDetectionStrategy, Component, Directive, input, numberAttribute } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { classes } from '@spartan-ng/helm/utils';
import { lucideChevronDown } from '@ng-icons/lucide';
import { type NumberInput } from '@angular/cdk/coercion';
@Directive({
selector: '[hlmNavigationMenuContent],hlm-navigation-menu-content',
host: {
'data-slot': 'navigation-menu-content',
'[style.--nav-offset]': 'navOffset()',
},
})
export class HlmNavigationMenuContent {
public readonly navOffset = input<number, NumberInput>(1.5, { transform: numberAttribute });
constructor() {
classes(() => [
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:w-auto',
'data-[orientation=horizontal]:data-[motion=from-end]:slide-in-from-right-52 data-[orientation=horizontal]:data-[motion=from-start]:slide-in-from-left-52 data-[orientation=horizontal]:data-[motion=to-end]:slide-out-to-right-52 data-[orientation=horizontal]:data-[motion=to-start]:slide-out-to-left-52',
'data-[orientation=vertical]:data-[motion=from-end]:slide-in-from-bottom-52 data-[orientation=vertical]:data-[motion=from-start]:slide-in-from-top-52 data-[orientation=vertical]:data-[motion=to-end]:slide-out-to-bottom-52 data-[orientation=vertical]:data-[motion=to-start]:slide-out-to-top-52',
'data-[orientation=horizontal]:mt-[--spacing(var(--nav-offset))] data-[orientation=vertical]:mx-[--spacing(var(--nav-offset))]',
'bg-popover text-popover-foreground ring-foreground/10 rounded-xl shadow ring-1 transition-all ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 block',
]);
}
}
@Directive({
selector: 'li[hlmNavigationMenuItem]',
hostDirectives: [{ directive: BrnNavigationMenuItem, inputs: ['id'] }],
host: {
'data-slot': 'navigation-menu-item',
},
})
export class HlmNavigationMenuItem {
constructor() {
classes(() => 'relative has-[:focus]:z-10 data-active:z-10');
}
}
@Directive({
selector: 'a[hlmNavigationMenuLink]',
hostDirectives: [{ directive: BrnNavigationMenuLink, inputs: ['active'] }],
host: {
'data-slot': 'navigation-menu-link',
},
})
export class HlmNavigationMenuLink {
constructor() {
classes(() => 'data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 focus-visible:ring-ring/30 hover:bg-muted focus:bg-muted flex items-center gap-1.5 rounded-lg p-2 text-xs/relaxed transition-all outline-none focus-visible:ring-2 focus-visible:outline-1 [&_ng-icon:not([class*=\'text-\'])]:text-[length:--spacing(4)]');
}
}
@Directive({
selector: 'ul[hlmNavigationMenuList]',
hostDirectives: [
{
directive: BrnNavigationMenuList,
},
],
host: {
'data-slot': 'navigation-menu-list',
},
})
export class HlmNavigationMenuList {
constructor() {
classes(() => [
'gap-0 group flex flex-1 list-none items-center justify-center',
'data-[orientation=vertical]:flex-col',
]);
}
}
@Directive({
selector: '[hlmNavigationMenuPortal]',
hostDirectives: [BrnNavigationMenuContent],
})
export class HlmNavigationMenuPortal {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'button[hlmNavigationMenuTrigger]',
imports: [NgIcon],
providers: [provideIcons({ lucideChevronDown })],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [{ directive: BrnNavigationMenuTrigger, inputs: ['align'] }],
host: { 'data-slot': 'navigation-menu-trigger' },
template: `
<ng-content />
<ng-icon name="lucideChevronDown" class="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180" />
`,
})
export class HlmNavigationMenuTrigger {
constructor() {
classes(
() =>
'bg-background hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/30 rounded-md px-2.5 py-1.5 text-xs/relaxed font-medium transition-all focus-visible:ring-2 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none',
);
}
}
@Directive({
selector: 'nav[hlmNavigationMenu]',
hostDirectives: [
{
directive: BrnNavigationMenu,
inputs: ['value', 'delayDuration', 'skipDelayDuration', 'orientation', 'openOn'],
outputs: ['valueChange'],
},
],
host: {
'data-slot': 'navigation-menu',
},
})
export class HlmNavigationMenu {
constructor() {
classes(
() => 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
);
}
}
export const HlmNavigationMenuImports = [
HlmNavigationMenu,
HlmNavigationMenuContent,
HlmNavigationMenuItem,
HlmNavigationMenuLink,
HlmNavigationMenuList,
HlmNavigationMenuPortal,
HlmNavigationMenuTrigger,
] as const;import { BrnNavigationMenu, BrnNavigationMenuContent, BrnNavigationMenuItem, BrnNavigationMenuLink, BrnNavigationMenuList, BrnNavigationMenuTrigger } from '@spartan-ng/brain/navigation-menu';
import { ChangeDetectionStrategy, Component, Directive, input, numberAttribute } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { classes } from '@spartan-ng/helm/utils';
import { lucideChevronDown } from '@ng-icons/lucide';
import { type NumberInput } from '@angular/cdk/coercion';
@Directive({
selector: '[hlmNavigationMenuContent],hlm-navigation-menu-content',
host: {
'data-slot': 'navigation-menu-content',
'[style.--nav-offset]': 'navOffset()',
},
})
export class HlmNavigationMenuContent {
public readonly navOffset = input<number, NumberInput>(1.5, { transform: numberAttribute });
constructor() {
classes(() => [
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out top-0 left-0 w-full p-2 pr-2.5 md:w-auto',
'data-[orientation=horizontal]:data-[motion=from-end]:slide-in-from-right-52 data-[orientation=horizontal]:data-[motion=from-start]:slide-in-from-left-52 data-[orientation=horizontal]:data-[motion=to-end]:slide-out-to-right-52 data-[orientation=horizontal]:data-[motion=to-start]:slide-out-to-left-52',
'data-[orientation=vertical]:data-[motion=from-end]:slide-in-from-bottom-52 data-[orientation=vertical]:data-[motion=from-start]:slide-in-from-top-52 data-[orientation=vertical]:data-[motion=to-end]:slide-out-to-bottom-52 data-[orientation=vertical]:data-[motion=to-start]:slide-out-to-top-52',
'data-[orientation=horizontal]:mt-[--spacing(var(--nav-offset))] data-[orientation=vertical]:mx-[--spacing(var(--nav-offset))]',
'bg-popover text-popover-foreground ring-foreground/5 dark:ring-foreground/10 rounded-3xl shadow-lg ring-1 transition-all ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 block',
]);
}
}
@Directive({
selector: 'li[hlmNavigationMenuItem]',
hostDirectives: [{ directive: BrnNavigationMenuItem, inputs: ['id'] }],
host: {
'data-slot': 'navigation-menu-item',
},
})
export class HlmNavigationMenuItem {
constructor() {
classes(() => 'relative has-[:focus]:z-10 data-active:z-10');
}
}
@Directive({
selector: 'a[hlmNavigationMenuLink]',
hostDirectives: [{ directive: BrnNavigationMenuLink, inputs: ['active'] }],
host: {
'data-slot': 'navigation-menu-link',
},
})
export class HlmNavigationMenuLink {
constructor() {
classes(() => 'data-[active=true]:focus:bg-muted data-[active=true]:hover:bg-muted data-[active=true]:bg-muted/50 focus-visible:ring-ring/30 hover:bg-muted focus:bg-muted flex items-center gap-1.5 rounded-3xl p-3 text-sm transition-all outline-none focus-visible:ring-3 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-2xl [&_ng-icon:not([class*=\'text-\'])]:text-[length:--spacing(4)]');
}
}
@Directive({
selector: 'ul[hlmNavigationMenuList]',
hostDirectives: [
{
directive: BrnNavigationMenuList,
},
],
host: {
'data-slot': 'navigation-menu-list',
},
})
export class HlmNavigationMenuList {
constructor() {
classes(() => [
'gap-0 group flex flex-1 list-none items-center justify-center',
'data-[orientation=vertical]:flex-col',
]);
}
}
@Directive({
selector: '[hlmNavigationMenuPortal]',
hostDirectives: [BrnNavigationMenuContent],
})
export class HlmNavigationMenuPortal {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'button[hlmNavigationMenuTrigger]',
imports: [NgIcon],
providers: [provideIcons({ lucideChevronDown })],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [{ directive: BrnNavigationMenuTrigger, inputs: ['align'] }],
host: { 'data-slot': 'navigation-menu-trigger' },
template: `
<ng-content />
<ng-icon name="lucideChevronDown" class="relative top-px ml-1 size-3 transition duration-300 group-data-open/navigation-menu-trigger:rotate-180" />
`,
})
export class HlmNavigationMenuTrigger {
constructor() {
classes(
() =>
'hover:bg-muted focus:bg-muted data-open:hover:bg-muted data-open:focus:bg-muted data-open:bg-muted/50 focus-visible:ring-ring/30 rounded-3xl px-4.5 py-2.5 text-sm font-medium transition-all focus-visible:ring-3 focus-visible:outline-1 disabled:opacity-50 group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center outline-none disabled:pointer-events-none',
);
}
}
@Directive({
selector: 'nav[hlmNavigationMenu]',
hostDirectives: [
{
directive: BrnNavigationMenu,
inputs: ['value', 'delayDuration', 'skipDelayDuration', 'orientation', 'openOn'],
outputs: ['valueChange'],
},
],
host: {
'data-slot': 'navigation-menu',
},
})
export class HlmNavigationMenu {
constructor() {
classes(
() => 'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
);
}
}
export const HlmNavigationMenuImports = [
HlmNavigationMenu,
HlmNavigationMenuContent,
HlmNavigationMenuItem,
HlmNavigationMenuLink,
HlmNavigationMenuList,
HlmNavigationMenuPortal,
HlmNavigationMenuTrigger,
] as const;Usage
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';<nav hlmNavigationMenu>
<ul hlmNavigationMenuList>
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Home</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<div>Content</div>
</hlm-navigation-menu-content>
<li>
</ul>
</nav>Examples
Vertical
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideCheck, lucideCircle, lucideInfo } from '@ng-icons/lucide';
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';
@Component({
selector: 'spartan-navigation-menu-vertical',
imports: [HlmNavigationMenuImports, NgIcon],
providers: [provideIcons({ lucideCircle, lucideCheck, lucideInfo })],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nav hlmNavigationMenu orientation="vertical">
<ul hlmNavigationMenuList class="w-35 flex-wrap">
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Home</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li class="row-span-3">
<a
hlmNavigationMenuLink
class="from-muted/50 to-muted flex h-full w-full flex-col justify-end rounded-md bg-linear-to-b p-4 no-underline outline-hidden select-none focus:shadow-md md:p-6"
href="/"
>
<div class="mb-2 text-lg font-medium sm:mt-4">spartan.ng</div>
<p class="text-muted-foreground text-sm leading-tight">
Beautifully designed components built with Tailwind CSS.
</p>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/introduction">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Introduction</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Re-usable components built using Radix UI and Tailwind CSS.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/installation">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Installation</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
How to install dependencies and structure your app.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/typography">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Typography</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Styles for headings, paragraphs, lists...etc
</p>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<!-- Components Menu -->
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Components</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 sm:w-[400px] md:w-[500px] md:grid-cols-2 lg:w-[600px]">
@for (component of _components; track $index) {
<li>
<a hlmNavigationMenuLink [href]="component.href">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">{{ component.title }}</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
{{ component.description }}
</p>
</div>
</a>
</li>
}
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<a hlmNavigationMenuLink class="mx-auto" href="/documentation/introduction">Docs</a>
</li>
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>List</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="/components">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Components</div>
<div class="text-muted-foreground">Browse all components in the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="/documentation">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Documentation</div>
<div class="text-muted-foreground">Learn how to use the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="#">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Blog</div>
<div class="text-muted-foreground">Read our latest blog posts.</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Simple</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="/components">Components</a>
<a hlmNavigationMenuLink href="/documentation">Documentation</a>
<a hlmNavigationMenuLink href="/blocks">Blocks</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>With Icon</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideInfo" />
Backlog
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCircle" />
To Do
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCheck" />
Done
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
</ul>
</nav>
`,
})
export class NavigationMenuVertical {
protected readonly _components = [
{
title: 'Alert Dialog',
description: 'A modal dialog that interrupts the user with important content and expects a response.',
href: '/components/alert-dialog',
},
{
title: 'Hover Card',
description: 'For sighted users to preview content available behind a link.',
href: '/components/hover-card',
},
{
title: 'Progress',
description: 'Displays an indicator showing the completion progress of a task.',
href: '/components/progress',
},
{
title: 'Scroll Area',
description: 'Visually or semantically separates content.',
href: '/components/scroll-area',
},
{
title: 'Tabs',
description: 'A set of layered content panels displayed one at a time.',
href: '/components/tabs',
},
{
title: 'Tooltip',
description: 'A popup that displays information on hover or focus.',
href: '/components/tooltip',
},
];
}Controlled
The navigation menu's value input can be set to the menu item's id to activate it. Declaratively set navigation menu item stays activated until it is hovered or focused out.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideCheck, lucideCircle, lucideInfo } from '@ng-icons/lucide';
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';
@Component({
selector: 'spartan-navigation-menu-controlled',
imports: [HlmNavigationMenuImports, NgIcon],
providers: [provideIcons({ lucideCircle, lucideInfo, lucideCheck })],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nav hlmNavigationMenu [(value)]="_value">
<ul hlmNavigationMenuList class="flex-wrap">
<li hlmNavigationMenuItem id="home">
<button hlmNavigationMenuTrigger>Home</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li class="row-span-3">
<a
hlmNavigationMenuLink
class="from-muted/50 to-muted flex h-full w-full flex-col justify-end rounded-md bg-linear-to-b p-4 no-underline outline-hidden select-none focus:shadow-md md:p-6"
href="/"
>
<div class="mb-2 text-lg font-medium sm:mt-4">spartan.ng</div>
<p class="text-muted-foreground text-sm leading-tight">
Beautifully designed components built with Tailwind CSS.
</p>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/introduction">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Introduction</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Re-usable components built using Radix UI and Tailwind CSS.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/installation">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Installation</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
How to install dependencies and structure your app.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/typography">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Typography</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Styles for headings, paragraphs, lists...etc
</p>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<!-- Components Menu -->
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Components</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 sm:w-[400px] md:w-[500px] md:grid-cols-2 lg:w-[600px]">
@for (component of _components; track $index) {
<li>
<a hlmNavigationMenuLink [href]="component.href">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">{{ component.title }}</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
{{ component.description }}
</p>
</div>
</a>
</li>
}
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<a hlmNavigationMenuLink href="/documentation/introduction">Docs</a>
</li>
<li hlmNavigationMenuItem class="hidden md:block">
<button hlmNavigationMenuTrigger>List</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid w-[300px] gap-4">
<li>
<a hlmNavigationMenuLink href="/components">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Components</div>
<div class="text-muted-foreground">Browse all components in the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="/documentation">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Documentation</div>
<div class="text-muted-foreground">Learn how to use the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="#">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Blog</div>
<div class="text-muted-foreground">Read our latest blog posts.</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem class="hidden md:block">
<button hlmNavigationMenuTrigger>Simple</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="/components">Components</a>
<a hlmNavigationMenuLink href="/documentation">Documentation</a>
<a hlmNavigationMenuLink href="/blocks">Blocks</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem class="hidden md:block">
<button hlmNavigationMenuTrigger>With Icon</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideInfo" />
Backlog
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCircle" />
To Do
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCheck" />
Done
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
</ul>
</nav>
`,
})
export class NavigationMenuControlled {
protected readonly _value = signal('home');
protected readonly _components = [
{
title: 'Alert Dialog',
description: 'A modal dialog that interrupts the user with important content and expects a response.',
href: '/components/alert-dialog',
},
{
title: 'Hover Card',
description: 'For sighted users to preview content available behind a link.',
href: '/components/hover-card',
},
{
title: 'Progress',
description: 'Displays an indicator showing the completion progress of a task.',
href: '/components/progress',
},
{
title: 'Scroll Area',
description: 'Visually or semantically separates content.',
href: '/components/scroll-area',
},
{
title: 'Tabs',
description: 'A set of layered content panels displayed one at a time.',
href: '/components/tabs',
},
{
title: 'Tooltip',
description: 'A popup that displays information on hover or focus.',
href: '/components/tooltip',
},
];
}Nested
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideCheck, lucideCircle, lucideInfo } from '@ng-icons/lucide';
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';
@Component({
selector: 'spartan-navigation-menu-nested',
imports: [HlmNavigationMenuImports, NgIcon],
providers: [provideIcons({ lucideCircle, lucideInfo, lucideCheck })],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nav hlmNavigationMenu>
<ul hlmNavigationMenuList class="flex-wrap">
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Root</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<nav hlmNavigationMenu orientation="vertical">
<ul hlmNavigationMenuList class="w-35 flex-wrap">
<li hlmNavigationMenuItem class="w-full">
<button hlmNavigationMenuTrigger class="w-full">Home</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li class="row-span-3">
<a
hlmNavigationMenuLink
class="from-muted/50 to-muted flex h-full w-full flex-col justify-end rounded-md bg-linear-to-b p-4 no-underline outline-hidden select-none focus:shadow-md md:p-6"
href="/"
>
<div class="mb-2 text-lg font-medium sm:mt-4">spartan.ng</div>
<p class="text-muted-foreground text-sm leading-tight">
Beautifully designed components built with Tailwind CSS.
</p>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/introduction">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Introduction</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Re-usable components built using Radix UI and Tailwind CSS.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/installation">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Installation</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
How to install dependencies and structure your app.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/typography">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Typography</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Styles for headings, paragraphs, lists...etc
</p>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<!-- Components Menu -->
<li hlmNavigationMenuItem class="w-full">
<button hlmNavigationMenuTrigger class="w-full">Components</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 sm:w-[400px] md:w-[500px] md:grid-cols-2 lg:w-[600px]">
@for (component of _components; track $index) {
<li>
<a hlmNavigationMenuLink [href]="component.href">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">{{ component.title }}</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
{{ component.description }}
</p>
</div>
</a>
</li>
}
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem class="w-full">
<button hlmNavigationMenuTrigger class="w-full">List</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid w-[300px] gap-4">
<li>
<a hlmNavigationMenuLink href="/components">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Components</div>
<div class="text-muted-foreground">Browse all components in the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="/documentation">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Documentation</div>
<div class="text-muted-foreground">Learn how to use the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="#">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Blog</div>
<div class="text-muted-foreground">Read our latest blog posts.</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem class="w-full">
<button hlmNavigationMenuTrigger class="w-full">Simple</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="/components">Components</a>
<a hlmNavigationMenuLink href="/documentation">Documentation</a>
<a hlmNavigationMenuLink href="/blocks">Blocks</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem class="w-full">
<button hlmNavigationMenuTrigger class="w-full">With Icon</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideInfo" />
Backlog
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCircle" />
To Do
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCheck" />
Done
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
</ul>
</nav>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<a hlmNavigationMenuLink href="/documentation/introduction">Docs</a>
</li>
</ul>
</nav>
`,
})
export class NavigationMenuNested {
protected readonly _components = [
{
title: 'Alert Dialog',
description: 'A modal dialog that interrupts the user with important content and expects a response.',
href: '/components/alert-dialog',
},
{
title: 'Hover Card',
description: 'For sighted users to preview content available behind a link.',
href: '/components/hover-card',
},
{
title: 'Progress',
description: 'Displays an indicator showing the completion progress of a task.',
href: '/components/progress',
},
{
title: 'Scroll Area',
description: 'Visually or semantically separates content.',
href: '/components/scroll-area',
},
{
title: 'Tabs',
description: 'A set of layered content panels displayed one at a time.',
href: '/components/tabs',
},
{
title: 'Tooltip',
description: 'A popup that displays information on hover or focus.',
href: '/components/tooltip',
},
];
}Open on Click
Set openOn="click" to require a click to open the menu initially. Once a menu is open, hovering still switches between menu items.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideCheck, lucideCircle, lucideInfo } from '@ng-icons/lucide';
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';
@Component({
selector: 'spartan-navigation-menu-open-on-click',
imports: [HlmNavigationMenuImports, NgIcon],
providers: [provideIcons({ lucideCircle, lucideCheck, lucideInfo })],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nav hlmNavigationMenu openOn="click">
<ul hlmNavigationMenuList class="flex-wrap">
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Home</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<li class="row-span-3">
<a
hlmNavigationMenuLink
class="from-muted/50 to-muted flex h-full w-full flex-col justify-end rounded-md bg-linear-to-b p-4 no-underline outline-hidden select-none focus:shadow-md md:p-6"
href="/"
>
<div class="mb-2 text-lg font-medium sm:mt-4">spartan.ng</div>
<p class="text-muted-foreground text-sm leading-tight">
Beautifully designed components built with Tailwind CSS.
</p>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/introduction">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Introduction</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Re-usable components built using Radix UI and Tailwind CSS.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/installation">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Installation</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
How to install dependencies and structure your app.
</p>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink href="/documentation/typography">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">Typography</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
Styles for headings, paragraphs, lists...etc
</p>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<!-- Components Menu -->
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>Components</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid gap-2 sm:w-[400px] md:w-[500px] md:grid-cols-2 lg:w-[600px]">
@for (component of _components; track $index) {
<li>
<a hlmNavigationMenuLink [href]="component.href">
<div class="flex flex-col gap-1 text-sm">
<div class="text-sm leading-none font-medium">{{ component.title }}</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
{{ component.description }}
</p>
</div>
</a>
</li>
}
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<a hlmNavigationMenuLink href="/documentation/introduction">Docs</a>
</li>
<li hlmNavigationMenuItem class="hidden md:block">
<button hlmNavigationMenuTrigger>List</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid w-[300px] gap-4">
<li>
<a hlmNavigationMenuLink href="/components">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Components</div>
<div class="text-muted-foreground">Browse all components in the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="/documentation">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Documentation</div>
<div class="text-muted-foreground">Learn how to use the library.</div>
</div>
</a>
<a hlmNavigationMenuLink href="#">
<div class="flex flex-col gap-1 text-sm">
<div class="font-medium">Blog</div>
<div class="text-muted-foreground">Read our latest blog posts.</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem class="hidden md:block">
<button hlmNavigationMenuTrigger>Simple</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="/components">Components</a>
<a hlmNavigationMenuLink href="/documentation">Documentation</a>
<a hlmNavigationMenuLink href="/blocks">Blocks</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem class="hidden md:block">
<button hlmNavigationMenuTrigger>With Icon</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul>
<li>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideInfo" />
Backlog
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCircle" />
To Do
</a>
<a hlmNavigationMenuLink href="#">
<ng-icon name="lucideCheck" />
Done
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
</ul>
</nav>
`,
})
export class NavigationMenuOpenOnClick {
protected readonly _components = [
{
title: 'Alert Dialog',
description: 'A modal dialog that interrupts the user with important content and expects a response.',
href: '/components/alert-dialog',
},
{
title: 'Hover Card',
description: 'For sighted users to preview content available behind a link.',
href: '/components/hover-card',
},
{
title: 'Progress',
description: 'Displays an indicator showing the completion progress of a task.',
href: '/components/progress',
},
{
title: 'Scroll Area',
description: 'Visually or semantically separates content.',
href: '/components/scroll-area',
},
{
title: 'Tabs',
description: 'A set of layered content panels displayed one at a time.',
href: '/components/tabs',
},
{
title: 'Tooltip',
description: 'A popup that displays information on hover or focus.',
href: '/components/tooltip',
},
];
}Align
Use the align input to control how the content aligns relative to the trigger.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';
@Component({
selector: 'spartan-navigation-menu-align',
imports: [HlmNavigationMenuImports],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<nav hlmNavigationMenu>
<ul hlmNavigationMenuList class="flex-wrap">
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger align="start">Start</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="w-48">
<li>
<a hlmNavigationMenuLink href="/">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">Aligned Start</div>
<div class="text-muted-foreground line-clamp-2">Content aligns to the start of the trigger.</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger align="center">Center</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="w-48">
<li>
<a hlmNavigationMenuLink href="/">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">Aligned Center</div>
<div class="text-muted-foreground line-clamp-2">Content aligns to the center of the trigger.</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger align="end">End</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="w-48">
<li>
<a hlmNavigationMenuLink href="/">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">Aligned End</div>
<div class="text-muted-foreground line-clamp-2">Content aligns to the end of the trigger.</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
</ul>
</nav>
`,
})
export class NavigationMenuAlign {}RTL
To enable RTL support in spartan-ng, see the RTL configuration guide.
import { Directionality } from '@angular/cdk/bidi';
import { Component, computed, effect, inject, untracked } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslateService, Translations } from '@spartan-ng/app/app/shared/translate.service';
import { HlmNavigationMenuImports } from '@spartan-ng/helm/navigation-menu';
@Component({
selector: 'spartan-navigation-menu-rtl',
imports: [HlmNavigationMenuImports, RouterLink],
providers: [Directionality],
host: {
'[dir]': '_dir()',
},
template: `
<nav hlmNavigationMenu>
<ul hlmNavigationMenuList class="flex-wrap">
<li hlmNavigationMenuItem>
<button hlmNavigationMenuTrigger>{{ _t()['gettingStarted'] }}</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="w-96">
<li>
<a hlmNavigationMenuLink routerLink="/documentation/introduction">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">{{ _t()['introduction'] }}</div>
<div class="text-muted-foreground line-clamp-2">{{ _t()['introductionDesc'] }}</div>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink routerLink="/documentation/installation">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">{{ _t()['installation'] }}</div>
<div class="text-muted-foreground line-clamp-2">{{ _t()['installationDesc'] }}</div>
</div>
</a>
</li>
<li>
<a hlmNavigationMenuLink routerLink="/documentation/typography">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">{{ _t()['typography'] }}</div>
<div class="text-muted-foreground line-clamp-2">{{ _t()['typographyDesc'] }}</div>
</div>
</a>
</li>
</ul>
</hlm-navigation-menu-content>
</li>
<!-- Components Menu -->
<li hlmNavigationMenuItem class="hidden md:flex">
<button hlmNavigationMenuTrigger>{{ _t()['components'] }}</button>
<hlm-navigation-menu-content *hlmNavigationMenuPortal>
<ul class="grid w-[400px] gap-2 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
@for (component of _components; track $index) {
<li>
<a hlmNavigationMenuLink [href]="component.href">
<div class="flex flex-col gap-1 text-sm">
<div class="leading-none font-medium">{{ _t()[component.titleKey] }}</div>
<div class="text-muted-foreground line-clamp-2">{{ _t()[component.descriptionKey] }}</div>
</div>
</a>
</li>
}
</ul>
</hlm-navigation-menu-content>
</li>
<li hlmNavigationMenuItem>
<a hlmNavigationMenuLink href="/documentation/introduction">{{ _t()['docs'] }}</a>
</li>
</ul>
</nav>
`,
})
export class NavigationMenuRtl {
private readonly _language = inject(TranslateService).language;
private readonly _translations: Translations = {
en: {
dir: 'ltr',
values: {
gettingStarted: 'Getting started',
introduction: 'Introduction',
introductionDesc: 'Re-usable components built with Tailwind CSS.',
installation: 'Installation',
installationDesc: 'How to install dependencies and structure your app.',
typography: 'Typography',
typographyDesc: 'Styles for headings, paragraphs, lists...etc',
components: 'Components',
alertDialog: 'Alert Dialog',
alertDialogDesc: 'A modal dialog that interrupts the user with important content and expects a response.',
hoverCard: 'Hover Card',
hoverCardDesc: 'For sighted users to preview content available behind a link.',
progress: 'Progress',
progressDesc:
'Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.',
scrollArea: 'Scroll-area',
scrollAreaDesc: 'Visually or semantically separates content.',
tabs: 'Tabs',
tabsDesc: 'A set of layered sections of content—known as tab panels—that are displayed one at a time.',
tooltip: 'Tooltip',
tooltipDesc:
'A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.',
withIcon: 'With Icon',
backlog: 'Backlog',
toDo: 'To Do',
done: 'Done',
docs: 'Docs',
},
},
ar: {
dir: 'rtl',
values: {
gettingStarted: 'البدء',
introduction: 'مقدمة',
introductionDesc: 'مكونات قابلة لإعادة الاستخدام مبنية باستخدام Tailwind CSS.',
installation: 'التثبيت',
installationDesc: 'كيفية تثبيت التبعيات وتنظيم تطبيقك.',
typography: 'الطباعة',
typographyDesc: 'أنماط للعناوين والفقرات والقوائم...إلخ',
components: 'المكونات',
alertDialog: 'حوار التنبيه',
alertDialogDesc: 'حوار نافذة يقطع المستخدم بمحتوى مهم ويتوقع استجابة.',
hoverCard: 'بطاقة التحويم',
hoverCardDesc: 'للمستخدمين المبصرين لمعاينة المحتوى المتاح خلف الرابط.',
progress: 'التقدم',
progressDesc: 'يعرض مؤشرًا يوضح تقدم إتمام المهمة، عادةً يتم عرضه كشريط تقدم.',
scrollArea: 'منطقة التمرير',
scrollAreaDesc: 'يفصل المحتوى بصريًا أو دلاليًا.',
tabs: 'التبويبات',
tabsDesc: 'مجموعة من أقسام المحتوى المتعددة الطبقات—المعروفة بألواح التبويب—التي يتم عرضها واحدة في كل مرة.',
tooltip: 'تلميح',
tooltipDesc:
'نافذة منبثقة تعرض معلومات متعلقة بعنصر عندما يتلقى العنصر التركيز على لوحة المفاتيح أو عند تحويم الماوس فوقه.',
withIcon: 'مع أيقونة',
backlog: 'قائمة الانتظار',
toDo: 'المهام',
done: 'منجز',
docs: 'الوثائق',
},
},
he: {
dir: 'rtl',
values: {
gettingStarted: 'התחלה',
introduction: 'הקדמה',
introductionDesc: 'רכיבים לשימוש חוזר שנבנו עם Tailwind CSS.',
installation: 'התקנה',
installationDesc: 'כיצד להתקין תלויות ולבנות את האפליקציה שלך.',
typography: 'טיפוגרפיה',
typographyDesc: "סגנונות לכותרות, פסקאות, רשימות...וכו'",
components: 'רכיבים',
alertDialog: 'דיאלוג התראה',
alertDialogDesc: 'דיאלוג מודאלי שמפריע למשתמש עם תוכן חשוב ומצפה לתגובה.',
hoverCard: 'כרטיס ריחוף',
hoverCardDesc: 'למשתמשים רואים כדי להציג תצוגה מקדימה של תוכן זמין מאחורי קישור.',
progress: 'התקדמות',
progressDesc: 'מציג אינדיקטור המציג את התקדמות ההשלמה של משימה, בדרך כלל מוצג כסרגל התקדמות.',
scrollArea: 'אזור גלילה',
scrollAreaDesc: 'מפריד תוכן חזותית או סמנטית.',
tabs: 'כרטיסיות',
tabsDesc: 'קבוצה של חלקי תוכן מרובדים—המכונים לוחות כרטיסיות—המוצגים אחד בכל פעם.',
tooltip: 'טולטיפ',
tooltipDesc: 'חלון קופץ המציג מידע הקשור לאלמנט כאשר האלמנט מקבל מיקוד מקלדת או כאשר העכבר מרחף מעליו.',
withIcon: 'עם אייקון',
backlog: 'רשימת המתנה',
toDo: 'לעשות',
done: 'הושלם',
docs: 'תיעוד',
},
},
};
protected readonly _components = [
{
titleKey: 'alertDialog' as const,
descriptionKey: 'alertDialogDesc' as const,
href: '/components/alert-dialog',
},
{
titleKey: 'hoverCard' as const,
descriptionKey: 'hoverCardDesc' as const,
href: '/components/hover-card',
},
{
titleKey: 'progress' as const,
descriptionKey: 'progressDesc' as const,
href: '/components/progress',
},
{
titleKey: 'scrollArea' as const,
descriptionKey: 'scrollAreaDesc' as const,
href: '/components/scroll-area',
},
{
titleKey: 'tabs' as const,
descriptionKey: 'tabsDesc' as const,
href: '/components/tabs',
},
{
titleKey: 'tooltip' as const,
descriptionKey: 'tooltipDesc' as const,
href: '/components/tooltip',
},
];
private readonly _translation = computed(() => this._translations[this._language()]);
protected readonly _t = computed(() => this._translation().values);
protected readonly _dir = computed(() => this._translation().dir);
private readonly _directionality = inject(Directionality);
constructor() {
effect(() => {
const dir = this._dir();
untracked(() => this._directionality.valueSignal.set(dir));
});
}
}Brain API
BrnNavigationMenuContent
Selector: [brnNavigationMenuContent]
BrnNavigationMenuItem
Selector: li[brnNavigationMenuItem]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | `brn-navigation-menu-item-${++BrnNavigationMenuItem._id}` | The id of the navigation menu item |
BrnNavigationMenuLink
Selector: a[brnNavigationMenuLink]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| active | boolean | undefined | undefined | Used to identify the link as the currently active page. |
BrnNavigationMenuList
Selector: ul[brnNavigationMenuList]
BrnNavigationMenuTrigger
Selector: button[brnNavigationMenuTrigger]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| align | MenuAlign | start | - |
BrnNavigationMenu
Selector: nav[brnNavigationMenu]
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| delayDuration | number | 200 | The duration from when the mouse enters a trigger until the content opens. |
| skipDelayDuration | number | 300 | How much time a user has to enter another trigger without incurring a delay again. |
| openOn | 'hover' | 'click' | hover | Controls whether the menu opens on hover or click. When 'click', initial open requires a click, but hover still switches between items once open. |
| orientation | 'horizontal' | 'vertical' | horizontal | The orientation of the menu. |
| value | string | - | The controlled value of the menu item to activate. |
Outputs
| Prop | Type | Default | Description |
|---|---|---|---|
| valueChange | string | - | The controlled value of the menu item to activate. |
Helm API
HlmNavigationMenuContent
Selector: [hlmNavigationMenuContent],hlm-navigation-menu-content
Inputs
| Prop | Type | Default | Description |
|---|---|---|---|
| navOffset | number | 1.5 | - |
HlmNavigationMenuItem
Selector: li[hlmNavigationMenuItem]
HlmNavigationMenuLink
Selector: a[hlmNavigationMenuLink]
HlmNavigationMenuList
Selector: ul[hlmNavigationMenuList]
HlmNavigationMenuPortal
Selector: [hlmNavigationMenuPortal]
HlmNavigationMenuTrigger
Selector: button[hlmNavigationMenuTrigger]
HlmNavigationMenu
Selector: nav[hlmNavigationMenu]
On This Page