mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
✨ feat(shell-navigation): implement navigation components with collapsible menu
Add modular navigation system with three reusable components: - NavigationGroupComponent for grouped navigation sections - NavigationItemComponent for main navigation entries with expand/collapse - NavigationSubItemComponent for nested child routes Include navigation types, route configuration with dynamic tabId support, and integrate navigation into shell-layout sidebar.
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
<shell-network-status-banner />
|
||||
<shell-header />
|
||||
|
||||
@if (renderNavigation()) {
|
||||
<shell-navigation />
|
||||
}
|
||||
|
||||
<main>
|
||||
<ng-content></ng-content>
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { NetworkStatusBannerComponent } from './components/network-status-banner.component';
|
||||
|
||||
import { NavigationService } from '@isa/shell/common';
|
||||
import { ShellHeaderComponent } from '@isa/shell/header';
|
||||
import { ShellNavigationComponent } from '@isa/shell/navigation';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-layout',
|
||||
@@ -9,9 +16,27 @@ import { ShellHeaderComponent } from '@isa/shell/header';
|
||||
templateUrl: './shell-layout.component.html',
|
||||
styleUrls: ['./shell-layout.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NetworkStatusBannerComponent, ShellHeaderComponent],
|
||||
imports: [
|
||||
NetworkStatusBannerComponent,
|
||||
ShellHeaderComponent,
|
||||
ShellNavigationComponent,
|
||||
],
|
||||
host: {
|
||||
class: 'text-isa-neutral-900',
|
||||
},
|
||||
})
|
||||
export class ShellLayoutComponent {}
|
||||
export class ShellLayoutComponent {
|
||||
#navigationService = inject(NavigationService);
|
||||
|
||||
readonly tablet = breakpoint(Breakpoint.Tablet);
|
||||
|
||||
readonly renderNavigation = computed(() => {
|
||||
const tablet = this.tablet();
|
||||
|
||||
if (!tablet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.#navigationService.get();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './lib/shell-navigation/shell-navigation.component';
|
||||
export * from './lib/shell-navigation.component';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block self-stretch;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="flex flex-col h-14 items-start self-stretch px-4">
|
||||
<div
|
||||
class="isa-text-caption-caps flex h-6 flex-col justify-center flex-[1_0_0] self-stretch"
|
||||
>
|
||||
{{ group().label }}
|
||||
</div>
|
||||
</div>
|
||||
@for (item of group().items; track item.label) {
|
||||
<shell-navigation-item [item]="item" />
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
import { NavigationGroup } from '../../types';
|
||||
import { ShellNavigationItemComponent } from '../navigation-item/navigation-item.component';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-navigation-group',
|
||||
templateUrl: './navigation-group.component.html',
|
||||
styleUrls: ['./navigation-group.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [ShellNavigationItemComponent],
|
||||
})
|
||||
export class ShellNavigationGroupComponent {
|
||||
readonly group = input.required<NavigationGroup>();
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
:host {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
@apply isa-text-body-1-regular;
|
||||
transition:
|
||||
background-color 0.15s ease-out,
|
||||
font-weight 0.15s ease-out;
|
||||
}
|
||||
|
||||
a.active,
|
||||
a:hover,
|
||||
a:focus,
|
||||
button:hover,
|
||||
button:focus {
|
||||
@apply bg-isa-neutral-200;
|
||||
}
|
||||
|
||||
a.active .label {
|
||||
@apply isa-text-body-1-semibold;
|
||||
}
|
||||
|
||||
.active-bar {
|
||||
transform: scaleX(0);
|
||||
transform-origin: right;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
a.active .active-bar {
|
||||
animation: barReveal 0.25s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes barReveal {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
.chevron.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.sub-items {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
transition:
|
||||
grid-template-rows 0.25s ease-out,
|
||||
opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.sub-items.expanded {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sub-items.collapsed {
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chevron {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
a.active .active-bar {
|
||||
animation: none;
|
||||
transform: scaleX(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sub-items {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
@if (route(); as r) {
|
||||
<a
|
||||
class="relative flex flex-row h-14 px-6 items-center justify-start gap-2 self-stretch"
|
||||
routerLinkActive="active"
|
||||
[routerLink]="r.route"
|
||||
[queryParams]="r.queryParams"
|
||||
[queryParamsHandling]="r.queryParamsHandling"
|
||||
data-what="navigation-item"
|
||||
[attr.data-which]="item().label"
|
||||
[attr.aria-label]="'Navigate to ' + item().label"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
} @else {
|
||||
<button
|
||||
type="button"
|
||||
class="relative flex flex-row h-14 px-6 items-center justify-start gap-2 self-stretch w-full text-left"
|
||||
(click)="toggleExpand()"
|
||||
data-what="navigation-toggle"
|
||||
[attr.data-which]="item().label"
|
||||
[attr.aria-expanded]="expanded()"
|
||||
[attr.aria-label]="'Toggle ' + item().label + ' submenu'"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (hasChildren()) {
|
||||
<div
|
||||
class="sub-items bg-isa-neutral-100 mx-6 desktop:mx-[0.62rem] rounded-[.25rem] overflow-hidden"
|
||||
[class.expanded]="expanded()"
|
||||
[class.collapsed]="!expanded()"
|
||||
>
|
||||
<div class="min-h-0">
|
||||
@for (child of item().childRoutes; track $index) {
|
||||
<shell-navigation-sub-item [item]="child" [style.--i]="$index" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-template #content>
|
||||
<div
|
||||
class="active-bar absolute right-0 top-0 bottom-0 w-[0.375rem] bg-isa-neutral-700 rounded-full"
|
||||
></div>
|
||||
<div class="size-6 grow-0" [innerHTML]="sanitizedHtml()"></div>
|
||||
<div class="label grow">
|
||||
{{ item().label }}
|
||||
</div>
|
||||
@if (hasChildren()) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="$event.stopPropagation(); toggleExpand()"
|
||||
data-what="navigation-chevron"
|
||||
[attr.data-which]="item().label"
|
||||
[attr.aria-expanded]="expanded()"
|
||||
[attr.aria-label]="'Toggle ' + item().label + ' submenu'"
|
||||
>
|
||||
<ng-icon
|
||||
name="isaActionChevronRight"
|
||||
size="1.5rem"
|
||||
class="chevron"
|
||||
[class.expanded]="expanded()"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
Injector,
|
||||
input,
|
||||
isSignal,
|
||||
runInInjectionContext,
|
||||
signal,
|
||||
viewChildren,
|
||||
} from '@angular/core';
|
||||
import { NavigationItem } from '../../types';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { isaActionChevronRight } from '@isa/icons';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { ShellNavigationSubItemComponent } from '../navigation-sub-item/navigation-sub-item.component';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-navigation-item',
|
||||
templateUrl: './navigation-item.component.html',
|
||||
styleUrls: ['./navigation-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
NgIcon,
|
||||
NgTemplateOutlet,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
ShellNavigationSubItemComponent,
|
||||
],
|
||||
providers: [
|
||||
provideIcons({
|
||||
isaActionChevronRight,
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ShellNavigationItemComponent {
|
||||
#injector = inject(Injector);
|
||||
#sanitizer = inject(DomSanitizer);
|
||||
|
||||
readonly subItems = viewChildren(ShellNavigationSubItemComponent);
|
||||
|
||||
expanded = signal(false);
|
||||
|
||||
readonly item = input.required<NavigationItem>();
|
||||
|
||||
sanitizedHtml = computed(() => {
|
||||
const icon = this.item().icon;
|
||||
|
||||
if (!icon) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.#sanitizer.bypassSecurityTrustHtml(icon);
|
||||
});
|
||||
|
||||
hasChildren = computed(() => {
|
||||
return (this.item().childRoutes?.length ?? 0) > 0;
|
||||
});
|
||||
|
||||
toggleExpand(): void {
|
||||
this.expanded.update((value) => !value);
|
||||
}
|
||||
|
||||
readonly route = computed(() => {
|
||||
const route = this.item().route;
|
||||
|
||||
if (typeof route === 'function') {
|
||||
const result = runInInjectionContext(this.#injector, () => route());
|
||||
return isSignal(result) ? result() : result;
|
||||
}
|
||||
|
||||
return route;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Auto-expand when a child route becomes active.
|
||||
// This is a valid effect use case: one-way sync of UI state based on router state.
|
||||
// It only expands (never auto-collapses), preserving user's manual toggle preference.
|
||||
effect(() => {
|
||||
const subItems = this.subItems();
|
||||
if (!subItems.length) return;
|
||||
|
||||
const hasActiveChild = subItems.some((subItem) => subItem.isActive());
|
||||
|
||||
if (hasActiveChild) {
|
||||
this.expanded.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply isa-text-body-2-regular;
|
||||
transition:
|
||||
background-color 0.15s ease-out,
|
||||
font-weight 0.15s ease-out;
|
||||
}
|
||||
|
||||
a.slide-in {
|
||||
animation: slideIn 0.2s ease-out backwards;
|
||||
animation-delay: calc(var(--i, 0) * 50ms);
|
||||
}
|
||||
|
||||
a.active,
|
||||
a:hover,
|
||||
a:focus,
|
||||
a:active {
|
||||
@apply bg-isa-neutral-200 isa-text-body-2-semibold;
|
||||
}
|
||||
|
||||
a .active-bar {
|
||||
@apply hidden;
|
||||
transform: scaleX(0);
|
||||
transform-origin: right;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
a.active .active-bar {
|
||||
@apply block;
|
||||
animation: barReveal 0.25s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes barReveal {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
a.slide-in {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
a.active .active-bar {
|
||||
animation: none;
|
||||
transform: scaleX(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
@if (route(); as r) {
|
||||
<a
|
||||
class="flex flex-row h-14 px-4 items-center self-stretch rounded-[.25rem]"
|
||||
animate.enter="slide-in"
|
||||
routerLinkActive="active"
|
||||
(isActiveChange)="isActive.set($event)"
|
||||
[routerLink]="r.route"
|
||||
[queryParams]="r.queryParams"
|
||||
[queryParamsHandling]="r.queryParamsHandling"
|
||||
data-what="navigation-sub-item"
|
||||
[attr.data-which]="label()"
|
||||
[attr.aria-label]="'Navigate to ' + label()"
|
||||
>
|
||||
<div class="grow">
|
||||
{{ label() }}
|
||||
</div>
|
||||
<div
|
||||
class="active-bar grow-0 w-[0.375rem] bg-isa-neutral-700 rounded-full self-stretch -mr-4"
|
||||
></div>
|
||||
</a>
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
afterNextRender,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
Injector,
|
||||
input,
|
||||
isSignal,
|
||||
runInInjectionContext,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import {
|
||||
NavigationRouteWithLabel,
|
||||
NavigationRouteWithLabelFn,
|
||||
} from '../../types';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-navigation-sub-item',
|
||||
templateUrl: './navigation-sub-item.component.html',
|
||||
styleUrls: ['./navigation-sub-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [RouterLink, RouterLinkActive],
|
||||
})
|
||||
export class ShellNavigationSubItemComponent {
|
||||
#injector = inject(Injector);
|
||||
|
||||
readonly routerLinkActive = viewChild(RouterLinkActive);
|
||||
readonly isActive = signal(false);
|
||||
|
||||
constructor() {
|
||||
afterNextRender(() => {
|
||||
const rla = this.routerLinkActive();
|
||||
if (rla?.isActive) {
|
||||
this.isActive.set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
readonly item = input.required<
|
||||
NavigationRouteWithLabel | NavigationRouteWithLabelFn
|
||||
>();
|
||||
|
||||
readonly route = computed(() => {
|
||||
const item = this.item();
|
||||
|
||||
if (typeof item === 'function') {
|
||||
const result = runInInjectionContext(this.#injector, () => item());
|
||||
return isSignal(result) ? result() : result;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
readonly label = computed(() => this.route()?.label ?? '');
|
||||
}
|
||||
187
libs/shell/navigation/src/lib/navigations.ts
Normal file
187
libs/shell/navigation/src/lib/navigations.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import {
|
||||
isaNavigationAbholfach,
|
||||
isaNavigationArtikelsuche,
|
||||
isaNavigationCalender,
|
||||
isaNavigationDashboard,
|
||||
isaNavigationKunden,
|
||||
isaNavigationRemission1,
|
||||
isaNavigationReturn,
|
||||
isaNavigationSortiment,
|
||||
isaNavigationWarenausgabe,
|
||||
isaNavigationWareneingang1,
|
||||
} from '@isa/icons';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
|
||||
import { NavigationGroup, NavigationItem } from './types';
|
||||
import { computed } from '@angular/core';
|
||||
|
||||
export const navigations: Array<NavigationGroup | NavigationItem> = [
|
||||
// Standalone item with dynamic tabId route
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationDashboard,
|
||||
label: 'Dashboard',
|
||||
route: {
|
||||
route: '/dashboard',
|
||||
},
|
||||
},
|
||||
|
||||
// KUNDEN group
|
||||
{
|
||||
type: 'group',
|
||||
label: 'Kunden',
|
||||
items: [
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationArtikelsuche,
|
||||
label: 'Artikelsuche',
|
||||
route: () => {
|
||||
const activeTabId = injectTabId();
|
||||
return computed(() => {
|
||||
const tabId = activeTabId() ?? Date.now();
|
||||
return {
|
||||
route: ['/kunde', tabId, 'product'],
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationKunden,
|
||||
label: 'Kunden',
|
||||
route: () => {
|
||||
const activeTabId = injectTabId();
|
||||
return computed(() => {
|
||||
const tabId = activeTabId() ?? Date.now();
|
||||
return {
|
||||
route: ['/kunde', tabId, 'customer'],
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationWarenausgabe,
|
||||
label: 'Warenausgabe',
|
||||
route: () => {
|
||||
const activeTabId = injectTabId();
|
||||
return computed(() => {
|
||||
const tabId = activeTabId() ?? Date.now();
|
||||
return {
|
||||
route: ['/kunde', tabId, 'pickup-shelf'],
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationReturn,
|
||||
label: 'Rückgabe',
|
||||
route: () => {
|
||||
const activeTabId = injectTabId();
|
||||
return computed(() => {
|
||||
const tabId = activeTabId() ?? Date.now();
|
||||
return {
|
||||
route: ['/', tabId, 'return'],
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// FILIALE group
|
||||
{
|
||||
type: 'group',
|
||||
label: 'Filiale',
|
||||
items: [
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationCalender,
|
||||
label: 'Kalender',
|
||||
route: {
|
||||
route: '/filiale/task-calendar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationSortiment,
|
||||
label: 'Sortiment',
|
||||
route: {
|
||||
route: '/filiale/assortment',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationAbholfach,
|
||||
label: 'Abholfach',
|
||||
childRoutes: [
|
||||
{
|
||||
label: 'Einbuchen',
|
||||
route: '/filiale/pickup-shelf',
|
||||
},
|
||||
{
|
||||
label: 'Reservierung',
|
||||
route: '/filiale/goods/in/reservation',
|
||||
queryParams: { view: 'reservation' },
|
||||
},
|
||||
{
|
||||
label: 'Ausräumen',
|
||||
route: '/filiale/goods/in/cleanup',
|
||||
queryParams: { view: 'cleanup' },
|
||||
},
|
||||
{
|
||||
label: 'Remi Vorschau',
|
||||
route: '/filiale/goods/in/preview',
|
||||
queryParams: { view: 'remission' },
|
||||
},
|
||||
{
|
||||
label: 'Fehlende',
|
||||
route: '/filiale/goods/in/list',
|
||||
queryParams: { view: 'wareneingangsliste' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationRemission1,
|
||||
label: 'Remission',
|
||||
childRoutes: [
|
||||
() => {
|
||||
const activeTabId = injectTabId();
|
||||
return computed(() => {
|
||||
const tabId = activeTabId() ?? Date.now();
|
||||
return {
|
||||
label: 'Remission',
|
||||
route: ['/', tabId, 'remission'],
|
||||
};
|
||||
});
|
||||
},
|
||||
() => {
|
||||
const activeTabId = injectTabId();
|
||||
return computed(() => {
|
||||
const tabId = activeTabId() ?? Date.now();
|
||||
return {
|
||||
label: 'Warenbegleitscheine',
|
||||
route: ['/', tabId, 'remission', 'return-receipt'],
|
||||
};
|
||||
});
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: isaNavigationWareneingang1,
|
||||
label: 'Wareneingang',
|
||||
route: {
|
||||
route: '/filiale/package-inspection/packages',
|
||||
queryParams: {
|
||||
filter_status: '0;8',
|
||||
filter_zeitraum: '"2025-11-26T23:00:00Z"-"2025-12-04T23:00:00Z"',
|
||||
filter_lieferant: 'libri;knv%7Cknvbs',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
<nav
|
||||
class="bg-isa-white rounded-r-2xl flex w-[13.75rem] py-8 gap-8 flex-col"
|
||||
aria-label="Main navigation"
|
||||
data-what="main-navigation"
|
||||
>
|
||||
@for (navigation of navigations; track navigation.label) {
|
||||
@switch (navigation.type) {
|
||||
@case ('group') {
|
||||
<shell-navigation-group [group]="navigation" />
|
||||
}
|
||||
@case ('item') {
|
||||
<shell-navigation-item [item]="navigation" />
|
||||
}
|
||||
}
|
||||
}
|
||||
</nav>
|
||||
14
libs/shell/navigation/src/lib/shell-navigation.component.ts
Normal file
14
libs/shell/navigation/src/lib/shell-navigation.component.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { navigations } from './navigations';
|
||||
import { ShellNavigationGroupComponent } from './components/navigation-group/navigation-group.component';
|
||||
import { ShellNavigationItemComponent } from './components/navigation-item/navigation-item.component';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-navigation',
|
||||
imports: [ShellNavigationGroupComponent, ShellNavigationItemComponent],
|
||||
templateUrl: './shell-navigation.component.html',
|
||||
styleUrl: './shell-navigation.component.css',
|
||||
})
|
||||
export class ShellNavigationComponent {
|
||||
readonly navigations = navigations;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<p>ShellNavigation works!</p>
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ShellNavigationComponent } from './shell-navigation.component';
|
||||
|
||||
describe('ShellNavigationComponent', () => {
|
||||
let component: ShellNavigationComponent;
|
||||
let fixture: ComponentFixture<ShellNavigationComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ShellNavigationComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ShellNavigationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-shell-navigation',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './shell-navigation.component.html',
|
||||
styleUrl: './shell-navigation.component.css',
|
||||
})
|
||||
export class ShellNavigationComponent {}
|
||||
33
libs/shell/navigation/src/lib/types.ts
Normal file
33
libs/shell/navigation/src/lib/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Signal } from '@angular/core';
|
||||
import { Params, QueryParamsHandling, UrlTree } from '@angular/router';
|
||||
|
||||
export type NavigationRoute = {
|
||||
route: string | unknown[] | UrlTree;
|
||||
queryParams?: Params;
|
||||
queryParamsHandling?: QueryParamsHandling;
|
||||
};
|
||||
|
||||
export type NavigationRouteWithLabel = NavigationRoute & {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type NavigationRouteFn = () => NavigationRoute | Signal<NavigationRoute>;
|
||||
|
||||
export type NavigationRouteWithLabelFn = () =>
|
||||
| NavigationRouteWithLabel
|
||||
| Signal<NavigationRouteWithLabel>;
|
||||
|
||||
export type NavigationItem = {
|
||||
type: 'item';
|
||||
icon?: string;
|
||||
label: string;
|
||||
|
||||
route?: NavigationRoute | NavigationRouteFn;
|
||||
childRoutes?: Array<NavigationRouteWithLabel | NavigationRouteWithLabelFn>;
|
||||
};
|
||||
|
||||
export type NavigationGroup = {
|
||||
type: 'group';
|
||||
label: string;
|
||||
items: Array<NavigationItem>;
|
||||
};
|
||||
Reference in New Issue
Block a user