diff --git a/libs/shell/layout/src/lib/shell-layout.component.html b/libs/shell/layout/src/lib/shell-layout.component.html
index 0538bb0f3..f74c28ab1 100644
--- a/libs/shell/layout/src/lib/shell-layout.component.html
+++ b/libs/shell/layout/src/lib/shell-layout.component.html
@@ -1,5 +1,10 @@
+
+@if (renderNavigation()) {
+
+}
+
diff --git a/libs/shell/layout/src/lib/shell-layout.component.ts b/libs/shell/layout/src/lib/shell-layout.component.ts
index e0c8ac674..b4b90b72a 100644
--- a/libs/shell/layout/src/lib/shell-layout.component.ts
+++ b/libs/shell/layout/src/lib/shell-layout.component.ts
@@ -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();
+ });
+}
diff --git a/libs/shell/navigation/src/index.ts b/libs/shell/navigation/src/index.ts
index 4a52e42c5..8870efac0 100644
--- a/libs/shell/navigation/src/index.ts
+++ b/libs/shell/navigation/src/index.ts
@@ -1 +1 @@
-export * from './lib/shell-navigation/shell-navigation.component';
+export * from './lib/shell-navigation.component';
diff --git a/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.css b/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.css
new file mode 100644
index 000000000..224543935
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.css
@@ -0,0 +1,3 @@
+:host {
+ @apply block self-stretch;
+}
diff --git a/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.html b/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.html
new file mode 100644
index 000000000..49dda3704
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.html
@@ -0,0 +1,10 @@
+
+
+ {{ group().label }}
+
+
+@for (item of group().items; track item.label) {
+
+}
diff --git a/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.ts b/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.ts
new file mode 100644
index 000000000..2af69be37
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-group/navigation-group.component.ts
@@ -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();
+}
diff --git a/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.css b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.css
new file mode 100644
index 000000000..e1574133c
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.css
@@ -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;
+ }
+}
diff --git a/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.html b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.html
new file mode 100644
index 000000000..81b87beae
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.html
@@ -0,0 +1,67 @@
+@if (route(); as r) {
+
+
+
+} @else {
+
+}
+
+@if (hasChildren()) {
+
+
+ @for (child of item().childRoutes; track $index) {
+
+ }
+
+
+}
+
+
+
+
+
+ {{ item().label }}
+
+ @if (hasChildren()) {
+
+ }
+
diff --git a/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.ts b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.ts
new file mode 100644
index 000000000..9a9d93566
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-item/navigation-item.component.ts
@@ -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();
+
+ 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);
+ }
+ });
+ }
+}
diff --git a/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.css b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.css
new file mode 100644
index 000000000..4351f725b
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.css
@@ -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;
+ }
+}
diff --git a/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.html b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.html
new file mode 100644
index 000000000..54f2e603e
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.html
@@ -0,0 +1,21 @@
+@if (route(); as r) {
+
+
+ {{ label() }}
+
+
+
+}
diff --git a/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.ts b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.ts
new file mode 100644
index 000000000..e5c4a90eb
--- /dev/null
+++ b/libs/shell/navigation/src/lib/components/navigation-sub-item/navigation-sub-item.component.ts
@@ -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 ?? '');
+}
diff --git a/libs/shell/navigation/src/lib/navigations.ts b/libs/shell/navigation/src/lib/navigations.ts
new file mode 100644
index 000000000..6064f729e
--- /dev/null
+++ b/libs/shell/navigation/src/lib/navigations.ts
@@ -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 = [
+ // 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',
+ },
+ },
+ },
+ ],
+ },
+];
diff --git a/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.css b/libs/shell/navigation/src/lib/shell-navigation.component.css
similarity index 100%
rename from libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.css
rename to libs/shell/navigation/src/lib/shell-navigation.component.css
diff --git a/libs/shell/navigation/src/lib/shell-navigation.component.html b/libs/shell/navigation/src/lib/shell-navigation.component.html
new file mode 100644
index 000000000..5c5a4f3aa
--- /dev/null
+++ b/libs/shell/navigation/src/lib/shell-navigation.component.html
@@ -0,0 +1,16 @@
+
diff --git a/libs/shell/navigation/src/lib/shell-navigation.component.ts b/libs/shell/navigation/src/lib/shell-navigation.component.ts
new file mode 100644
index 000000000..d77a3bdb2
--- /dev/null
+++ b/libs/shell/navigation/src/lib/shell-navigation.component.ts
@@ -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;
+}
diff --git a/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.html b/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.html
deleted file mode 100644
index 32dca0864..000000000
--- a/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.html
+++ /dev/null
@@ -1 +0,0 @@
-ShellNavigation works!
diff --git a/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.spec.ts b/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.spec.ts
deleted file mode 100644
index dc4824af6..000000000
--- a/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.spec.ts
+++ /dev/null
@@ -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;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [ShellNavigationComponent],
- }).compileComponents();
-
- fixture = TestBed.createComponent(ShellNavigationComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.ts b/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.ts
deleted file mode 100644
index f17b10e38..000000000
--- a/libs/shell/navigation/src/lib/shell-navigation/shell-navigation.component.ts
+++ /dev/null
@@ -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 {}
diff --git a/libs/shell/navigation/src/lib/types.ts b/libs/shell/navigation/src/lib/types.ts
new file mode 100644
index 000000000..bcca4a0e4
--- /dev/null
+++ b/libs/shell/navigation/src/lib/types.ts
@@ -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;
+
+export type NavigationRouteWithLabelFn = () =>
+ | NavigationRouteWithLabel
+ | Signal;
+
+export type NavigationItem = {
+ type: 'item';
+ icon?: string;
+ label: string;
+
+ route?: NavigationRoute | NavigationRouteFn;
+ childRoutes?: Array;
+};
+
+export type NavigationGroup = {
+ type: 'group';
+ label: string;
+ items: Array;
+};