ng-sidebar-pro
Version:
A complete and professional sidebar pro library for modern Angular applications
1,236 lines (1,205 loc) • 84.1 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, signal, computed, Injectable, DOCUMENT, input, Component, output, ViewEncapsulation, Directive, HostListener } from '@angular/core';
import * as i1 from '@angular/router';
import { Router, NavigationEnd, RouterModule } from '@angular/router';
import { filter, map } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { CommonModule } from '@angular/common';
import * as i2 from '@angular/material/list';
import { MatListModule } from '@angular/material/list';
import * as i3 from '@angular/material/icon';
import { MatIconModule } from '@angular/material/icon';
import * as i2$1 from '@angular/material/button';
import { MatButtonModule } from '@angular/material/button';
import * as i4$1 from '@angular/material/input';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import * as i4 from '@angular/material/tooltip';
import { MatTooltipModule, MatTooltip } from '@angular/material/tooltip';
import * as i5 from '@angular/material/badge';
import { MatBadgeModule } from '@angular/material/badge';
import { trigger, transition, style, animate } from '@angular/animations';
import * as i3$1 from '@angular/material/menu';
import { MatMenuModule } from '@angular/material/menu';
import { MatDividerModule } from '@angular/material/divider';
class NavigationService {
router = inject(Router);
configSubject = new BehaviorSubject(null);
collapsedSubject = new BehaviorSubject(false);
searchTermSubject = new BehaviorSubject('');
// Signals
config = signal(null);
collapsed = signal(false);
searchTerm = signal('');
// Computed values
filteredItems = computed(() => {
const config = this.config();
const search = this.searchTerm();
if (!config || !search) {
return config?.items || [];
}
return this.filterItemsBySearch(config.items, search);
});
currentRoute = signal('');
breadcrumbs = computed(() => {
const config = this.config();
const route = this.currentRoute();
if (!config || !route)
return [];
return this.generateBreadcrumbs(config.items, route);
});
constructor() {
// Listen to route changes
this.router.events
.pipe(filter(event => event instanceof NavigationEnd), map(event => event.url))
.subscribe(url => {
this.currentRoute.set(url);
});
}
// Configuration methods
setConfig(config) {
this.config.set(config);
this.configSubject.next(config);
}
getConfig() {
return this.configSubject.asObservable();
}
// Collapse methods
setCollapsed(collapsed) {
this.collapsed.set(collapsed);
this.collapsedSubject.next(collapsed);
}
toggleCollapsed() {
const current = this.collapsed();
this.setCollapsed(!current);
}
getCollapsed() {
return this.collapsedSubject.asObservable();
}
// Search methods
setSearchTerm(term) {
this.searchTerm.set(term);
this.searchTermSubject.next(term);
}
getSearchTerm() {
return this.searchTermSubject.asObservable();
}
// Navigation methods
navigateToItem(item) {
if (item.external) {
window.open(item.route, item.target || '_blank');
return;
}
if (item.redirectTo) {
this.router.navigate([item.redirectTo]);
return;
}
this.router.navigate([item.route]);
}
// Utility methods
findItemById(id) {
const config = this.config();
if (!config)
return null;
return this.findItemInList(config.items, id);
}
findItemByRoute(route) {
const config = this.config();
if (!config)
return null;
return this.findItemByRouteInList(config.items, route);
}
isItemActive(item) {
const currentRoute = this.currentRoute();
return currentRoute.startsWith(item.route);
}
canAccessItem(item) {
// Implement your access control logic here
// This is a simplified version
if (item.disabled || item.hidden)
return false;
// Check permissions, roles, etc.
// You can inject your auth service here
return true;
}
filterItemsBySearch(items, searchTerm) {
const filtered = [];
for (const item of items) {
if (this.itemMatchesSearch(item, searchTerm)) {
filtered.push(item);
}
else if (item.subItems) {
const filteredSubItems = this.filterItemsBySearch(item.subItems, searchTerm);
if (filteredSubItems.length > 0) {
filtered.push({
...item,
subItems: filteredSubItems
});
}
}
}
return filtered;
}
itemMatchesSearch(item, searchTerm) {
const term = searchTerm.toLowerCase();
return item.label.toLowerCase().includes(term) ||
(item.tooltip && item.tooltip.toLowerCase().includes(term)) ||
(item.data?.description && item.data.description.toLowerCase().includes(term)) || false;
}
findItemInList(items, id) {
for (const item of items) {
if (item.id === id)
return item;
if (item.subItems) {
const found = this.findItemInList(item.subItems, id);
if (found)
return found;
}
}
return null;
}
findItemByRouteInList(items, route) {
for (const item of items) {
if (item.route === route)
return item;
if (item.subItems) {
const found = this.findItemByRouteInList(item.subItems, route);
if (found)
return found;
}
}
return null;
}
generateBreadcrumbs(items, route) {
const breadcrumbs = [];
const findPath = (itemList, targetRoute, path = []) => {
for (const item of itemList) {
const currentPath = [...path, item];
if (item.route === targetRoute) {
breadcrumbs.push(...currentPath);
return true;
}
if (item.subItems && findPath(item.subItems, targetRoute, currentPath)) {
return true;
}
}
return false;
};
findPath(items, route);
return breadcrumbs;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class NavigationThemeService {
document = inject(DOCUMENT);
currentTheme = signal('modern-light');
customStyles = signal(null);
// Built-in themes
themes = {
'modern-light': {
colors: {
primary: '#2563eb',
accent: '#3b82f6',
background: '#ffffff',
surface: '#f8fafc',
text: '#1e293b',
textSecondary: '#64748b',
border: '#e2e8f0',
hover: '#f1f5f9',
active: '#e0e7ff',
disabled: '#94a3b8'
},
typography: {
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif',
fontSize: '14px',
fontWeight: '500',
lineHeight: '1.5'
},
spacing: {
padding: '12px 16px',
margin: '4px 0',
itemSpacing: '2px',
levelIndent: '20px'
},
borders: {
radius: '8px',
width: '1px',
style: 'solid'
},
shadows: {
small: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
medium: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
large: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
},
animations: {
duration: '200ms',
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
expandCollapse: '300ms ease-in-out',
hover: '150ms ease-out'
}
},
'modern-dark': {
colors: {
primary: '#3b82f6',
accent: '#60a5fa',
background: '#0f172a',
surface: '#1e293b',
text: '#f1f5f9',
textSecondary: '#94a3b8',
border: '#334155',
hover: '#475569',
active: '#1e40af',
disabled: '#64748b'
},
typography: {
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif',
fontSize: '14px',
fontWeight: '500',
lineHeight: '1.5'
},
spacing: {
padding: '12px 16px',
margin: '4px 0',
itemSpacing: '2px',
levelIndent: '20px'
},
borders: {
radius: '8px',
width: '1px',
style: 'solid'
},
shadows: {
small: '0 1px 2px 0 rgb(0 0 0 / 0.3)',
medium: '0 4px 6px -1px rgb(0 0 0 / 0.4)',
large: '0 10px 15px -3px rgb(0 0 0 / 0.5)'
},
animations: {
duration: '200ms',
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
expandCollapse: '300ms ease-in-out',
hover: '150ms ease-out'
}
},
'glassmorphism': {
colors: {
primary: '#3b82f6',
accent: '#60a5fa',
background: 'rgba(255, 255, 255, 0.1)',
surface: 'rgba(255, 255, 255, 0.2)',
text: '#ffffff',
textSecondary: 'rgba(255, 255, 255, 0.7)',
border: 'rgba(255, 255, 255, 0.2)',
hover: 'rgba(255, 255, 255, 0.1)',
active: 'rgba(59, 130, 246, 0.3)',
disabled: 'rgba(255, 255, 255, 0.3)'
},
typography: {
fontFamily: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif',
fontSize: '14px',
fontWeight: '500',
lineHeight: '1.5'
},
spacing: {
padding: '12px 16px',
margin: '4px 0',
itemSpacing: '2px',
levelIndent: '20px'
},
borders: {
radius: '12px',
width: '1px',
style: 'solid'
},
shadows: {
small: '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
medium: '0 8px 32px 0 rgba(31, 38, 135, 0.37)',
large: '0 8px 32px 0 rgba(31, 38, 135, 0.37)'
},
animations: {
duration: '300ms',
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
expandCollapse: '400ms ease-in-out',
hover: '200ms ease-out'
}
},
'classic-light': {
// Classic theme styles...
colors: {
primary: '#1976d2',
accent: '#2196f3',
background: '#fafafa',
surface: '#ffffff',
text: '#212121',
textSecondary: '#757575',
border: '#e0e0e0',
hover: '#f5f5f5',
active: '#e3f2fd',
disabled: '#bdbdbd'
}
// ... other properties
},
'classic-dark': {
// Classic dark theme styles...
colors: {
primary: '#2196f3',
accent: '#64b5f6',
background: '#303030',
surface: '#424242',
text: '#ffffff',
textSecondary: '#bdbdbd',
border: '#616161',
hover: '#525252',
active: '#1976d2',
disabled: '#757575'
}
// ... other properties
},
'minimal': {
// Minimal theme styles...
colors: {
primary: '#000000',
accent: '#333333',
background: '#ffffff',
surface: '#ffffff',
text: '#000000',
textSecondary: '#666666',
border: '#e5e5e5',
hover: '#f8f8f8',
active: '#f0f0f0',
disabled: '#cccccc'
}
// ... other properties
},
'custom': {
// Will be overridden by customStyles
colors: {},
typography: {},
spacing: {},
borders: {},
shadows: {},
animations: {}
}
};
// Computed styles based on current theme
computedStyles = computed(() => {
const theme = this.currentTheme();
const custom = this.customStyles();
if (theme === 'custom' && custom) {
return custom;
}
const baseStyles = this.themes[theme];
// Merge with custom styles if provided
if (custom) {
return this.mergeStyles(baseStyles, custom);
}
return baseStyles;
});
setTheme(theme) {
this.currentTheme.set(theme);
this.applyThemeToDocument();
}
setCustomStyles(styles) {
this.customStyles.set(styles);
if (this.currentTheme() === 'custom') {
this.applyThemeToDocument();
}
}
getThemeStyles(theme) {
return this.themes[theme] || this.themes['modern-light'];
}
applyThemeToDocument() {
const styles = this.computedStyles();
const root = this.document.documentElement;
// Apply CSS custom properties
if (styles.colors) {
Object.entries(styles.colors).forEach(([key, value]) => {
if (value) {
root.style.setProperty(`--nav-color-${this.kebabCase(key)}`, value);
}
});
}
if (styles.typography) {
Object.entries(styles.typography).forEach(([key, value]) => {
if (value) {
root.style.setProperty(`--nav-typography-${this.kebabCase(key)}`, value);
}
});
}
if (styles.spacing) {
Object.entries(styles.spacing).forEach(([key, value]) => {
if (value) {
root.style.setProperty(`--nav-spacing-${this.kebabCase(key)}`, value);
}
});
}
// Apply other style categories...
}
mergeStyles(base, custom) {
return {
colors: { ...base.colors, ...custom.colors },
typography: { ...base.typography, ...custom.typography },
spacing: { ...base.spacing, ...custom.spacing },
borders: { ...base.borders, ...custom.borders },
shadows: { ...base.shadows, ...custom.shadows },
animations: { ...base.animations, ...custom.animations }
};
}
kebabCase(str) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationThemeService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationThemeService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class NavigationRouteGeneratorService {
/**
* Generates Angular routes from navigation configuration
*/
generateRoutes(config) {
const routes = [];
if (config.rootComponent) {
routes.push({
path: '',
component: config.rootComponent,
data: config.baseData,
children: [
...config.items.map(item => this.itemToRoute(item)),
...(config.defaultRoute ? [{
path: '',
redirectTo: config.defaultRoute,
pathMatch: 'full'
}] : [])
]
});
}
else {
routes.push(...config.items.map(item => this.itemToRoute(item)));
if (config.defaultRoute) {
routes.push({
path: '',
redirectTo: config.defaultRoute,
pathMatch: 'full'
});
}
}
return routes;
}
/**
* Converts a navigation item to an Angular route
*/
itemToRoute(item) {
const route = {
path: item.route,
data: {
...item.data,
navigationItem: item
}
};
// Handle different route types
if (item.loadChildren) {
route.loadChildren = item.loadChildren;
}
else if (item.component) {
route.component = item.component;
}
// Handle redirects
if (item.redirectTo) {
route.redirectTo = item.redirectTo;
if (item.routeMatch) {
route.pathMatch = item.routeMatch;
}
}
// Handle guards
if (item.canActivate) {
route.canActivate = item.canActivate;
}
if (item.guards?.canActivate) {
route.canActivate = item.guards.canActivate;
}
if (item.guards?.canDeactivate) {
route.canDeactivate = item.guards.canDeactivate;
}
// Handle resolvers
if (item.resolve) {
route.resolve = item.resolve;
}
// Handle children
if (item.subItems && item.subItems.length > 0) {
route.children = item.subItems.map(subItem => this.itemToRoute(subItem));
// Add default redirect for container routes
if (item.routeType === 'container' || item.hasRootComponent) {
route.children.unshift({
path: '',
redirectTo: item.subItems[0].route,
pathMatch: 'full'
});
}
}
return route;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationRouteGeneratorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationRouteGeneratorService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationRouteGeneratorService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class RouteBuilderUtils {
/**
* Construye la ruta completa para navegación basada en la configuración
*/
static buildNavigationRoute(item, parentPath = '') {
const currentPath = parentPath ? `${parentPath}/${item.route}` : item.route;
// Si es ruta raíz sin parent
if (!parentPath) {
return item.route;
}
return currentPath;
}
/**
* Determina si un item es navegable (puede ser clickeado para navegar)
*/
static isNavigableItem(item) {
// Si tiene component o loadChildren, es navegable
if (item.component || item.loadChildren) {
return true;
}
// Si tiene redirectTo, es navegable
if (item.redirectTo) {
return true;
}
// Si no tiene subitems, asumimos que es navegable
if (!item.subItems || item.subItems.length === 0) {
return true;
}
// Si tiene container type, también es navegable
// if (item.routeType === 'container' || item.hasRootComponent || item.rootComponent) {
// return true;
// }
return false;
}
/**
* Construye el routerLink apropiado para el template
*/
static buildRouterLink(item, parentRoute) {
if (item.redirectTo) {
return item.redirectTo;
}
// Para items de primer nivel
if (!parentRoute) {
return `/${item.route}`;
}
// Para subitems
return `/${parentRoute}/${item.route}`;
}
/**
* Obtiene la ruta completa desde la raíz
*/
static getFullRoute(item, menuItems) {
const pathParts = [];
// Buscar el parent de este item
const parent = this.findParentItem(item, menuItems);
if (parent) {
// Si el parent es container type, incluir su ruta
if (parent.routeType === 'container' || parent.hasRootComponent || parent.rootComponent) {
pathParts.push(parent.route);
}
}
pathParts.push(item.route);
return '/' + pathParts.join('/');
}
/**
* Encuentra el item padre de un subitem
*/
static findParentItem(targetItem, menuItems) {
for (const item of menuItems) {
if (item.subItems) {
for (const subItem of item.subItems) {
if (subItem === targetItem || subItem.id === targetItem.id) {
return item;
}
// Búsqueda recursiva para subitems anidados
const found = this.findParentItem(targetItem, item.subItems);
if (found)
return item;
}
}
}
return null;
}
}
class NavigationItemComponent {
item = input.required();
collapsed = input(false);
level = input(0);
parentRoute = input();
allMenuItems = input([]);
navigationService = inject(NavigationService);
expanded = signal(false);
// Computed properties
isNavigable = computed(() => {
return RouteBuilderUtils.isNavigableItem(this.item());
});
routerLink = computed(() => {
if (!this.isNavigable())
return null;
if (this.isContainerType()) {
return `/${this.item().route}`;
}
if (this.parentRoute()) {
return `/${this.parentRoute()}/${this.item().route}`;
}
if (this.allMenuItems().length > 0) {
return RouteBuilderUtils.getFullRoute(this.item(), this.allMenuItems());
}
return `/${this.item().route}`;
});
routerLinkOptions = computed(() => {
return this.isContainerType() ? { exact: false } : { exact: true };
});
hasSubItems = computed(() => {
const subItems = this.item().subItems;
return subItems && subItems.length > 0;
});
isContainerType = computed(() => {
return this.item().routeType === 'container' ||
this.item().hasRootComponent ||
!!this.item().rootComponent;
});
hasExpandableSubItems = computed(() => {
return this.hasSubItems() && !this.isContainerType();
});
shouldShowSubItems = computed(() => {
if (!this.hasSubItems() || this.collapsed())
return false;
return this.expanded();
});
getParentRouteForSubItem = computed(() => {
if (this.isContainerType()) {
return this.item().route;
}
if (this.parentRoute()) {
return `${this.parentRoute()}/${this.item().route}`;
}
return this.item().route;
});
// Badge computed properties
hasBadge = computed(() => {
const badge = this.item().badge;
return badge && (badge.text || badge.count !== undefined);
});
badgeText = computed(() => {
const badge = this.item().badge;
if (!badge)
return '';
return badge.text || (badge.count !== undefined ? badge.count.toString() : '');
});
badgeColor = computed(() => {
return this.item().badge?.color || 'primary';
});
badgePosition = computed(() => {
return this.item().badge?.position || 'top-right';
});
badgeSize = computed(() => {
const level = this.level();
return level > 1 ? 'small' : 'medium';
});
// Style computed properties
itemClasses = computed(() => {
const classes = [];
if (this.item().cssClass) {
classes.push(this.item().cssClass);
}
if (this.item().disabled) {
classes.push('disabled');
}
if (this.level() > 0) {
classes.push(`level-${this.level()}`);
}
if (this.expanded()) {
classes.push('expanded');
}
return classes.join(' ');
});
itemStyles = computed(() => {
const styles = { ...this.item().style };
// Dynamic padding based on level
styles.paddingLeft = `${16 + (this.level() * 20)}px`;
// Dynamic font size
const baseSize = 14;
const sizeDecrement = 1;
const minSize = 12;
const fontSize = Math.max(baseSize - (this.level() * sizeDecrement), minSize);
styles.fontSize = `${fontSize}px`;
return styles;
});
subItemsStyles = computed(() => {
return {
borderLeft: `${Math.max(1, 3 - this.level())}px solid var(--nav-color-border)`,
marginLeft: `${12 + (this.level() * 8)}px`
};
});
iconOpacity = computed(() => {
const baseOpacity = 1;
const opacityDecrement = 0.1;
const minOpacity = 0.6;
return Math.max(baseOpacity - (this.level() * opacityDecrement), minOpacity);
});
// Tooltip
getTooltip = () => {
return this.item().tooltip || (this.collapsed() ? this.item().label : '');
};
shouldShowTooltip = computed(() => {
return this.collapsed() || !!this.item().tooltip;
});
// Event handlers
toggleExpanded(event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
if (this.hasSubItems()) {
this.expanded.update(current => !current);
}
}
onItemClick() {
if (this.item().disabled)
return;
// Handle external links
if (this.item().external && this.item().route) {
window.open(this.item().route, this.item().target || '_blank');
return;
}
// Let router handle the navigation
this.navigationService.navigateToItem(this.item());
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: NavigationItemComponent, isStandalone: true, selector: "es-navigation-item", inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, parentRoute: { classPropertyName: "parentRoute", publicName: "parentRoute", isSignal: true, isRequired: false, transformFunction: null }, allMenuItems: { classPropertyName: "allMenuItems", publicName: "allMenuItems", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
<!-- Navigable Item -->
@if (isNavigable()) {
<mat-list-item
[routerLink]="routerLink()"
routerLinkActive="active"
[routerLinkActiveOptions]="routerLinkOptions()"
[matTooltip]="getTooltip()"
[matTooltipDisabled]="!shouldShowTooltip()"
matTooltipPosition="right"
class="nav-item navigable"
[class]="itemClasses()"
[style]="itemStyles()"
(click)="onItemClick()">
<!-- Icon -->
@if (item().icon) {
<mat-icon
matListItemIcon
[style.opacity]="iconOpacity()"
[matBadge]="badgeText()"
[matBadgeColor]="badgeColor()"
[matBadgeHidden]="!hasBadge() || badgePosition() !== 'icon'"
[matBadgeSize]="badgeSize()">
{{ item().icon }}
</mat-icon>
}
<!-- Label -->
@if (!collapsed()) {
<span matListItemTitle class="item-label">
{{ item().label }}
<!-- Inline Badge -->
@if (hasBadge() && badgePosition() === 'inline') {
<span class="inline-badge" [class]="'badge-' + badgeColor()">
{{ badgeText() }}
</span>
}
</span>
}
<!-- Expand Icon for Container Types -->
@if (hasExpandableSubItems() && !collapsed()) {
<mat-icon
matListItemMeta
class="expand-icon"
(click)="toggleExpanded($event)">
{{ expanded() ? 'expand_less' : 'expand_more' }}
</mat-icon>
}
<!-- Badge (Top-right position) -->
@if (hasBadge() && badgePosition() === 'top-right' && !collapsed()) {
<span class="top-right-badge" [class]="'badge-' + badgeColor()">
{{ badgeText() }}
</span>
}
</mat-list-item>
}
@else {
<!-- Non-navigable Item (Expandable) -->
<mat-list-item
(click)="toggleExpanded()"
[matTooltip]="getTooltip()"
[matTooltipDisabled]="!shouldShowTooltip()"
matTooltipPosition="right"
class="nav-item expandable"
[class]="itemClasses()"
[style]="itemStyles()">
<!-- Icon -->
@if (item().icon) {
<mat-icon matListItemIcon [style.opacity]="iconOpacity()">
{{ item().icon }}
</mat-icon>
}
<!-- Label -->
@if (!collapsed()) {
<span matListItemTitle class="item-label">{{ item().label }}</span>
<mat-icon matListItemMeta class="expand-icon">
{{ expanded() ? 'expand_less' : 'expand_more' }}
</mat-icon>
}
</mat-list-item>
}
<!-- Sub-items -->
@if (hasSubItems() && shouldShowSubItems()) {
<div class="sub-items"
[class.container-subitems]="isContainerType()"
[style]="subItemsStyles()"
@expandContractMenu>
@for (subItem of item().subItems; track subItem.id || $index) {
<es-navigation-item
[item]="subItem"
[collapsed]="collapsed()"
[level]="level() + 1"
[parentRoute]="getParentRouteForSubItem()"
[allMenuItems]="allMenuItems()"
/>
}
</div>
}
`, isInline: true, styles: [""], dependencies: [{ kind: "component", type: NavigationItemComponent, selector: "es-navigation-item", inputs: ["item", "collapsed", "level", "parentRoute", "allMenuItems"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: i1.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i2.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "directive", type: i2.MatListItemIcon, selector: "[matListItemIcon]" }, { kind: "directive", type: i2.MatListItemTitle, selector: "[matListItemTitle]" }, { kind: "directive", type: i2.MatListItemMeta, selector: "[matListItemMeta]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatBadgeModule }, { kind: "directive", type: i5.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }], animations: [
trigger('expandContractMenu', [
transition(':enter', [
style({ opacity: 0, height: '0px', overflow: 'hidden' }),
animate('300ms ease-in-out', style({ opacity: 1, height: '*' }))
]),
transition(':leave', [
style({ opacity: 1, height: '*', overflow: 'hidden' }),
animate('300ms ease-in-out', style({ opacity: 0, height: '0px' }))
])
])
] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationItemComponent, decorators: [{
type: Component,
args: [{ selector: 'es-navigation-item', standalone: true, imports: [
CommonModule,
RouterModule,
MatListModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
MatBadgeModule
], template: `
<!-- Navigable Item -->
@if (isNavigable()) {
<mat-list-item
[routerLink]="routerLink()"
routerLinkActive="active"
[routerLinkActiveOptions]="routerLinkOptions()"
[matTooltip]="getTooltip()"
[matTooltipDisabled]="!shouldShowTooltip()"
matTooltipPosition="right"
class="nav-item navigable"
[class]="itemClasses()"
[style]="itemStyles()"
(click)="onItemClick()">
<!-- Icon -->
@if (item().icon) {
<mat-icon
matListItemIcon
[style.opacity]="iconOpacity()"
[matBadge]="badgeText()"
[matBadgeColor]="badgeColor()"
[matBadgeHidden]="!hasBadge() || badgePosition() !== 'icon'"
[matBadgeSize]="badgeSize()">
{{ item().icon }}
</mat-icon>
}
<!-- Label -->
@if (!collapsed()) {
<span matListItemTitle class="item-label">
{{ item().label }}
<!-- Inline Badge -->
@if (hasBadge() && badgePosition() === 'inline') {
<span class="inline-badge" [class]="'badge-' + badgeColor()">
{{ badgeText() }}
</span>
}
</span>
}
<!-- Expand Icon for Container Types -->
@if (hasExpandableSubItems() && !collapsed()) {
<mat-icon
matListItemMeta
class="expand-icon"
(click)="toggleExpanded($event)">
{{ expanded() ? 'expand_less' : 'expand_more' }}
</mat-icon>
}
<!-- Badge (Top-right position) -->
@if (hasBadge() && badgePosition() === 'top-right' && !collapsed()) {
<span class="top-right-badge" [class]="'badge-' + badgeColor()">
{{ badgeText() }}
</span>
}
</mat-list-item>
}
@else {
<!-- Non-navigable Item (Expandable) -->
<mat-list-item
(click)="toggleExpanded()"
[matTooltip]="getTooltip()"
[matTooltipDisabled]="!shouldShowTooltip()"
matTooltipPosition="right"
class="nav-item expandable"
[class]="itemClasses()"
[style]="itemStyles()">
<!-- Icon -->
@if (item().icon) {
<mat-icon matListItemIcon [style.opacity]="iconOpacity()">
{{ item().icon }}
</mat-icon>
}
<!-- Label -->
@if (!collapsed()) {
<span matListItemTitle class="item-label">{{ item().label }}</span>
<mat-icon matListItemMeta class="expand-icon">
{{ expanded() ? 'expand_less' : 'expand_more' }}
</mat-icon>
}
</mat-list-item>
}
<!-- Sub-items -->
@if (hasSubItems() && shouldShowSubItems()) {
<div class="sub-items"
[class.container-subitems]="isContainerType()"
[style]="subItemsStyles()"
@expandContractMenu>
@for (subItem of item().subItems; track subItem.id || $index) {
<es-navigation-item
[item]="subItem"
[collapsed]="collapsed()"
[level]="level() + 1"
[parentRoute]="getParentRouteForSubItem()"
[allMenuItems]="allMenuItems()"
/>
}
</div>
}
`, animations: [
trigger('expandContractMenu', [
transition(':enter', [
style({ opacity: 0, height: '0px', overflow: 'hidden' }),
animate('300ms ease-in-out', style({ opacity: 1, height: '*' }))
]),
transition(':leave', [
style({ opacity: 1, height: '*', overflow: 'hidden' }),
animate('300ms ease-in-out', style({ opacity: 0, height: '0px' }))
])
])
] }]
}] });
class NavigationHeaderComponent {
config = input.required();
collapsed = input(false);
toggleCollapse = output();
onUserMenuClick(menuItem) {
if (menuItem.external && menuItem.route) {
window.open(menuItem.route, menuItem.target || '_blank');
}
else if (menuItem.action) {
menuItem.action();
}
// Add more navigation logic as needed
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: NavigationHeaderComponent, isStandalone: true, selector: "es-navigation-header", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggleCollapse: "toggleCollapse" }, ngImport: i0, template: `
<div class="nav-header" [class.collapsed]="collapsed()">
<!-- Logo and Title -->
@if (!collapsed()) {
<div class="header-brand">
@if (config().logo) {
<img [src]="config().logo" alt="Logo" class="logo">
}
@if (config().title) {
<h1 class="title">{{ config().title }}</h1>
}
</div>
} @else {
@if (config().logo) {
<img [src]="config().logo" alt="Logo" class="logo-mini">
}
}
<!-- Header Actions -->
@if (config().actions && (config().actions || []).length > 0) {
<div class="header-actions" [class.collapsed]="collapsed()">
@for (action of config().actions; track $index) {
<button mat-icon-button
[matTooltip]="action.label"
(click)="action.action()"
class="header-action">
<mat-icon [matBadge]="action.badge?.text || ''"
[matBadgeHidden]="!action.badge"
[matBadgeColor]="'primary'">
{{ action.icon }}
</mat-icon>
</button>
}
</div>
}
<!-- User Profile -->
@if (config().showUserProfile && config().userProfile && !collapsed()) {
<div class="user-profile">
<button mat-button [matMenuTriggerFor]="userMenu" class="user-button">
@if (config().userProfile?.avatar) {
<img [src]="config().userProfile!.avatar" alt="Avatar" class="avatar">
} @else {
<mat-icon>account_circle</mat-icon>
}
<div class="user-info">
<span class="user-name">{{ config().userProfile?.name }}</span>
@if (config().userProfile?.role) {
<span class="user-role">{{ config().userProfile?.role }}</span>
}
</div>
<mat-icon>expand_more</mat-icon>
</button>
<mat-menu #userMenu="matMenu">
@if (config().userProfile?.menu) {
@for (menuItem of config().userProfile!.menu; track menuItem.id) {
<button mat-menu-item (click)="onUserMenuClick(menuItem)">
@if (menuItem.icon) {
<mat-icon>{{ menuItem.icon }}</mat-icon>
}
<span>{{ menuItem.label }}</span>
</button>
@if ($index < config().userProfile!.menu!.length - 1) {
<mat-divider></mat-divider>
}
}
}
</mat-menu>
</div>
}
<!-- Collapse Toggle -->
<button mat-icon-button
class="collapse-toggle-header"
(click)="toggleCollapse.emit()"
matTooltip="Toggle Navigation">
<mat-icon>menu</mat-icon>
</button>
</div>
`, isInline: true, styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2$1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2$1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i2.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatBadgeModule }, { kind: "directive", type: i5.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationHeaderComponent, decorators: [{
type: Component,
args: [{ selector: 'es-navigation-header', standalone: true, imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatMenuModule,
MatDividerModule,
MatTooltip,
MatBadgeModule
], template: `
<div class="nav-header" [class.collapsed]="collapsed()">
<!-- Logo and Title -->
@if (!collapsed()) {
<div class="header-brand">
@if (config().logo) {
<img [src]="config().logo" alt="Logo" class="logo">
}
@if (config().title) {
<h1 class="title">{{ config().title }}</h1>
}
</div>
} @else {
@if (config().logo) {
<img [src]="config().logo" alt="Logo" class="logo-mini">
}
}
<!-- Header Actions -->
@if (config().actions && (config().actions || []).length > 0) {
<div class="header-actions" [class.collapsed]="collapsed()">
@for (action of config().actions; track $index) {
<button mat-icon-button
[matTooltip]="action.label"
(click)="action.action()"
class="header-action">
<mat-icon [matBadge]="action.badge?.text || ''"
[matBadgeHidden]="!action.badge"
[matBadgeColor]="'primary'">
{{ action.icon }}
</mat-icon>
</button>
}
</div>
}
<!-- User Profile -->
@if (config().showUserProfile && config().userProfile && !collapsed()) {
<div class="user-profile">
<button mat-button [matMenuTriggerFor]="userMenu" class="user-button">
@if (config().userProfile?.avatar) {
<img [src]="config().userProfile!.avatar" alt="Avatar" class="avatar">
} @else {
<mat-icon>account_circle</mat-icon>
}
<div class="user-info">
<span class="user-name">{{ config().userProfile?.name }}</span>
@if (config().userProfile?.role) {
<span class="user-role">{{ config().userProfile?.role }}</span>
}
</div>
<mat-icon>expand_more</mat-icon>
</button>
<mat-menu #userMenu="matMenu">
@if (config().userProfile?.menu) {
@for (menuItem of config().userProfile!.menu; track menuItem.id) {
<button mat-menu-item (click)="onUserMenuClick(menuItem)">
@if (menuItem.icon) {
<mat-icon>{{ menuItem.icon }}</mat-icon>
}
<span>{{ menuItem.label }}</span>
</button>
@if ($index < config().userProfile!.menu!.length - 1) {
<mat-divider></mat-divider>
}
}
}
</mat-menu>
</div>
}
<!-- Collapse Toggle -->
<button mat-icon-button
class="collapse-toggle-header"
(click)="toggleCollapse.emit()"
matTooltip="Toggle Navigation">
<mat-icon>menu</mat-icon>
</button>
</div>
` }]
}] });
class NavigationFooterComponent {
config = input.required();
collapsed = input(false);
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: NavigationFooterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: NavigationFooterComponent, isStandalone: true, selector: "es-navigation-footer", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, tran